{"owner":"kestra-io","repo":"kestra","hasSkills":true,"totalSkillsCount":21,"totalTokensCount":26233,"categories":["root-instruction","anthropic-skill","plugin-manifest"],"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md","ui/packages/slot-contracts/package.json","ui/packages/topology/package.json","ui/AGENTS.md","ui/packages/topology/tsconfig.json","ui/packages/kestra-sdk/tsconfig.json","ui/packages/kestra-sdk/package.json","ui/packages/kestra-sdk/README.md","ui/packages/kestra-sdk/tsconfig.node.json","ui/packages/slot-contracts/tsconfig.json","ui/packages/hey-api-plugin/package.json","ui/packages/hey-api-plugin/README.md","ui/packages/hey-api-plugin/tsconfig.json","ui/packages/design-system/package.json","ui/packages/design-system/tsconfig.app.json","ui/packages/design-system/tsconfig.json","ui/packages/design-system/tsconfig.node.json","ui/packages/design-system/tsconfig.storybook.json","ui/packages/design-system/web-types.json","ui/packages/design-system/scripts/Figma.json","ui/packages/design-system/tests/storybook/Basic/Color-variables.json"],"skills":{"AGENTS.md":"# Coding Agent Guidelines for Kestra Open Source Edition\n\nThis document provides essential information for AI coding agents working on the Kestra codebase.\n\n**IMPORTANT — READ FIRST**\n\n- **Act as a Senior Software Engineer and Software Architect.** Approach software development with:\n    - **Pragmatism**: Favor simple solutions over clever ones\n    - **Skepticism**: Question decisions that could cause technical debt or scalability issues\n    - **Efficiency**: Only challenge when it genuinely matters\n- **Think before coding**: explicitly state assumptions, compare alternatives, and justify choices.\n- **Simplicity first (KISS)**: overengineering and \"gas factories\" are strictly forbidden.\n- **Surgical changes only**: touch **only** what is strictly necessary to achieve the goal.\n- **Goal-driven execution**: define what success looks like *before* writing the first line of code.\n- **Reuse before writing**: the pattern you need almost always exists already. Find the closest equivalent in the codebase and follow it instead of inventing a second way to do the same thing.\n- **No comments by default**: see [Comments](#comments). Over-commenting is one of the most common reasons a PR written by an agent has to be revised.\n- **How Kestra looks is decided by the design system**: see [UI Design System](#ui-design-system). No color, spacing, radius or component pattern may be invented in feature code.\n- **A test has to be able to fail for a reason a reviewer would care about**: see [When a test earns its place](#when-a-test-earns-its-place). More tests do not make a change safer.\n- **Build and test only what you touched** rather than the whole repo: see [What to run after a change](#what-to-run-after-a-change).\n\n## Comments\n\n**The default is no comment.** Small methods with accurate names need no explanation, and every comment is a second thing that has to be kept in sync with the code. Over-commenting is easy to slip into: the next line gets narrated, the method name gets restated above the method, and `// Step 1` / `// Step 2` scaffolding is left behind. None of that will be accepted in review.\n\nThe reader to write for is the developer opening the file in a month. They do not have time to read a paragraph, and most of what a paragraph would tell them is already in the code.\n\nWrite a comment only when a reviewer would otherwise have to ask **why** the code exists:\n\n- a non-obvious constraint, or an upstream bug being worked around (link the issue)\n- an ordering, locking or concurrency requirement that the code itself cannot express\n- a deliberate omission, so that the next person does not \"fix\" it back\n\nKeep it to **one sentence**. When a paragraph seems necessary, make the code clearer instead.\n\nNever write:\n\n- a restatement of the line below it (`// increment the counter`, `// return the result`)\n- a narration of a self-explanatory getter, loop, or well-named call\n- section banners (`// ---- helpers ----`), changelog notes, or a `// TODO` with no issue link\n- a comment on every entry of a list when the rule is already stated once above the list\n\n**Existing comments** should be left alone unless the code they describe changed. When that code is deleted, delete the comment with it rather than rewording it around the gap. When the code changed, either correct the comment or remove it.\n\n**Javadoc** describes the contract rather than the body. Add it on public API called from other modules or from plugins, and on anything whose behavior cannot be seen from the signature (nullability, thread-safety, side effects, units, ownership of a returned collection). Skip it on getters, setters, builders, an override that adds nothing (a bare `{@inheritDoc}` is noise), and any method whose name already says everything.\n\n## Project\n\nMonorepo built with Java (backend) and Vue (frontend), using Gradle as the build system.\n\n## Tech Stack\n- **Backend:** Java 25, Micronaut Framework, Lombok\n- **Frontend:** Vue 3, TypeScript, Vite, Element Plus, Pinia\n- **Build:** Gradle, multi-project. The version is pinned in `gradle/wrapper/gradle-wrapper.properties`, and `settings.gradle` is the list of modules\n- **Testing:** JUnit 5, Mockito, AssertJ, Vitest, Playwright\n\n## Critical Code Patterns\n\n### Dependency Injection\n\n**DO**: Use constructor injection with final fields.\n\n```java\n@Singleton\npublic class MyService {\n    private final SomeDependency dependency;\n\n    @Inject\n    public MyService(SomeDependency dependency) {\n        this.dependency = Objects.requireNonNull(dependency);\n    }\n}\n```\n\n**DON'T**: Use field injection (`@Inject` on fields directly). Always prefer constructor injection.\n\n### Class Structure\n\n```java\n// 1. Package declaration and imports\n// 2. Class-level annotations (@Slf4j, @Singleton, etc.)\n// 3. Class declaration with Javadoc\n// 4. Static constants (UPPER_SNAKE_CASE)\n// 5. Injected fields (@Inject)\n// 6. Constructors\n// 7. Public methods\n// 8. Protected methods\n// 9. Private methods\n// 10. Inner classes/records\n```\n\n### Annotations\n- **Micronaut:** `@Singleton`, `@Inject`, `@Controller`, `@Replaces`, `@Requires`\n- **Validation:** `@Valid`, `@NotNull`, `@Nullable`\n- **Lombok:** `@Slf4j`, `@Getter`, `@NoArgsConstructor`, `@AllArgsConstructor`\n- Use `@Builder` for complex object creation\n\n### Error Handling\n\n**DO**:\n- Use specific exception types — extend `KestraException` or `KestraRuntimeException`\n- Use `Optional<T>` for potentially absent returned values\n- Return empty collections (e.g., `List.of()`, `Collections.emptyList()`) for absent values\n- Use try-with-resources for resource management\n- Log errors before re-throwing: `log.error(\"message\", exception)`\n- 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)`\n\n**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.\n\n### Java Language Features\n- Use java records for simple data carriers\n\n### Naming Conventions\n- Follow Java naming-convention best practices for Classes, Methods, Variables, Constants.\n- Boolean methods: Start with `is`, `has`, `should`, `can` (e.g., `isReadOnly()`).\n\n### File Organization\n- Use 4-space indentation (configured in .editorconfig)\n- UTF-8 encoding with LF line endings\n- No trailing whitespace\n\n### Utility Classes\n* Mark utility classes as `final` with a private constructor\n* Use static methods only\n* Use existing utility classes (e.g., `ListUtils`, `MapUtils`) instead of creating new ones (`io.kestra.core.utils.*`)\n\n**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.\n\n### Enums\n- 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\n- Use `@JsonValue` for custom serialization if needed\n- Use `UNKNOWN` enum value for unknown cases in deserialization\n- Compare Constants From The Left (a.k.a., Yoda conditions)\n- Use a static `fromString` method for case-insensitive lookups using `Enums` class.\n\ne.g.:\n```java\npublic enum MyEnum {\n    VALUE_ONE,\n    VALUE_TWO,\n    UNKNOWN;\n\n    @JsonCreator\n    public static ResourceType fromString(final String value) {\n        return Enums.getForNameIgnoreCase(value, MyEnum.class, UNKNOWN);\n    }\n}\n```\n\n### Documentation\n\nSee [Comments](#comments) for when a Javadoc block is worth writing at all. Once you have decided that it is:\n\n- Keep it to the contract: what the caller has to know that the signature does not already say\n- Use `@param`, `@return` and `@throws` only where each adds something. `@param name the name` is noise\n- Skip a bare `{@inheritDoc}` on an override that changes nothing\n- Add a usage example only when the call site would otherwise be guessed wrong\n\n## Webserver Constraints\n- Put classes used by only controllers in the webserver module (not core)\n- No business code/rule inside controllers - instead use a Service class\n- All APIs must return a valid JSON object\n- APIs should not return a response being a JSON array which cannot be evolved in a backwards-compatible way\n- Any endpoint returning a collection should be paged through `PagedResults` with `page` and `size` parameters, like the rest of the API\n- A DTO used by a single controller belongs as an inner record of that controller. Promote it to `io.kestra.webserver.models.api` only once a second caller exists, and name it with the `Api` prefix there (`ApiExecution`, `ApiTriggerState`, `ApiPluginArtifact`)\n- Import a class rather than writing its fully-qualified name, as soon as it is used more than once in the same file\n- Unit tests must assert that a user can only access a given API if authorized to do so, and that access is denied otherwise\n- APIs must be documented with OpenAPI annotations\n- Use DTOs for requests/responses\n- A dashboard chart export must use the chart's declared columns for its header, in declaration order, whether or not there are rows\n- Always validate input parameters with `@Valid`\n- Use `@ExecuteOn(TaskExecutors.IO)` for blocking operations\n- Return meaningful error responses in controllers\n\n## Worker Constraints\n- Never depend on repositories for code called by the workers - instead use MetaStore/StateStore facades\n\n## Executor Constraints\n- Run the `H2RunnerTest` whenever you update part of the executor\n\n## Testing Guidelines\n\n### When a test earns its place\n\nMore tests do not make a change safer. Every test is code that has to be read, kept passing, and understood by whoever changes that behavior next, so a test is only worth its cost when it can fail for a reason a reviewer would care about.\n\n**Add a test when:**\n\n- new behavior is introduced, or a bug is fixed. The test should fail on the commit before yours; if it passes there, it is testing something else\n- an edge case would be easy to get wrong later: an empty collection, a null, a boundary, a race, a permission check\n- the change is in the executor, the scheduler, the queue, or an ACL path, where a regression is expensive and hard to notice\n\n**Don't add a test when:**\n\n- it only exercises getters, setters, builders, or a Lombok-generated method\n- it asserts behavior that the framework already documents (Jackson serializing a field, Micronaut injecting a bean)\n- it repeats an existing test with one property changed. One test per behavior, rather than one per property, since otherwise there would be hundreds of them\n- the mocks would have to restate the implementation for the assertions to pass. Such a test is pinned to the current code rather than to the behavior, so it is broken by every legitimate refactor\n- it exists only to move a coverage number\n\n**Delete tests too.** Removing a feature should reduce the test count rather than raise it. When a test no longer describes behavior anyone relies on, or when it duplicates another one, delete it in the same PR and say so in the description. A redundant test that is always green is still maintenance cost.\n\n### Java Tests\n\n**DO**:\n- Place tests in same package structure as source code\n- Simple unit test with mocks over complex integration tests when possible\n- Structure the body as given / when / then. The three marker comments are optional and should be dropped when the test is only a few obvious lines\n- Test method naming: `should<ExpectedBehavior>When<ConditionOrAction>` (also `...Given<Input>`, `...For<Condition>`, `...If<Condition>`), e.g. `shouldThrowExceptionWhenDividingByZero()`\n- Use `@MicronautTest` for tests that require Micronaut beans\n- Use `@KestraTest` for tests that require running Kestra services (e.g., Executor, Scheduler)\n-\n```java\n@KestraTest\nclass ServiceTest {\n    @Inject\n    private ServiceClass service;\n\n    @Test\n    void shouldPerformActionWhenCondition() {\n        // Given (setup)\n\n        // When (action)\n\n        // Then (assertions)\n        assertThat(result).isNotNull();\n    }\n}\n```\n\n**DON'T**: Use Nested classes for test organization. Avoid complex test hierarchies.\n\n**Assertions:**\n- Use AssertJ: `assertThat().isEqualTo()`, `assertThat().isNotNull()`, `assertThatThrownBy()`, `assertThatObject()`\n- Prefer descriptive assertion methods\n- Use `@MockBean` for mocking dependencies\n\n**Test Categories:**\n- Unit tests: Fast, isolated, no external dependencies\n- Integration tests: Test component interaction, use `@Tag(\"integration\")`\n- Flaky tests: Use `@Tag(\"flaky\")` for unreliable tests\n\n### Frontend Tests\n- Unit tests with Vitest and `@vue/test-utils`, next to the file they cover (`Foo.vue` beside `Foo.spec.ts` and `Foo.stories.ts`), see [ui/AGENTS.md](ui/AGENTS.md)\n- E2E tests with Playwright\n- Storybook component tests\n- Use JSdom environment for DOM testing\n- **Prefer Storybook component tests over Vitest unit tests whenever possible** — components render through their real story setup (props, slots, design-system deps) instead of being stubbed out, catching regressions unit mocks miss. Fall back to a Vitest unit test only when the logic under test isn't component-rendering behavior (e.g. a pure helper/composable) or no story exists and adding one isn't practical.\n- Don't keep a Vitest test that only re-asserts what a story already covers. Delete it when you add the story.\n- Assert on `data-test` attributes and rendered text. A test written against `.el-*` or `.ks-*` class names is broken by the next Element Plus or design-system upgrade, without any behavior having changed.\n\n## UI Design System\n\nThe design system at [ui/packages/design-system/](ui/packages/design-system/) is the **single source of truth for every design decision** in Kestra: colors, typography, spacing, radii, shadows, and the component vocabulary. Nothing rendered to a user may be styled outside it.\n\nIn practice that means no hex codes, no `rgb()`, no `--el-*` or `--bs-*` variables, no hardcoded pixel spacing, no `:deep()` into another component, and no locally built equivalent of a component that already exists. When the design you need cannot be expressed with the existing `Ks*` components and `--ks-*` tokens, that is a gap in the design system: raise it with design and add the token, the prop or the component upstream. Approximating it with custom CSS in a feature component is not an option, since the result will only be right in the one theme you checked it in.\n\nThe full rules, the component catalogue, the token reference and the frontend best practices live in [ui/AGENTS.md](ui/AGENTS.md), which is auto-loaded for work under `ui/` in OSS and `ui-ee/` in Enterprise Edition. Read it before any frontend change, and update it in the same PR whenever a component, a prop or a token is added.\n\n@ui/AGENTS.md\n\n## Frontend Code Style (Vue 3)\n\n**File Organization:**\n- Use 2-space indentation for Vue, JSON, YAML, CSS\n- Use 4-space indentation for JavaScript/TypeScript\n- Follow Vue 3 Composition API patterns\n- Organize imports: Vue/framework → third-party → local modules\n\n**Naming Conventions:**\n- Components: `PascalCase` files (e.g., `MyComponent.vue`)\n- Variables/functions: `camelCase`\n- Constants: `UPPER_SNAKE_CASE`\n- CSS classes: Follow Element Plus conventions\n\n**TypeScript:**\n- Use strict TypeScript configuration\n- Prefer type definitions over `any`\n- Use interfaces for object shapes\n- Use enums for fixed sets of values\n\n## Build Commands\n\n### What to run after a change\n\nA whole-repo `./gradlew build` can take tens of minutes, and almost nothing about a three-line change can be learned from it. Start with the narrowest command that covers what you touched, and widen it only once that one passes.\n\n| What changed | Run |\n|---|---|\n| One backend module | `./gradlew :<module>:test --tests \"TheClassName\"`, then `./gradlew :<module>:test` |\n| Anything in the executor | `./gradlew :jdbc-h2:test --tests \"H2RunnerTest\"` (mandatory) |\n| Something crossing module boundaries | `./gradlew build -x integrationTest` |\n| A Vue component or composable | `cd ui && npm run check:types && npm run test:unit && npm run lint` |\n| A design-system component | the above, plus `npm run test:storybook` |\n| An e2e spec or fixture | `cd e2e && npm run check:types && npm run test:lint` |\n| `en.json`, whether a key was added or a value edited | `cd ui && npm run translations:generate && npm run translations:check` |\n| A controller or its DTOs | that module's tests, including the authorization tests for the route |\n\n### Java Backend\n\n```bash\n# Clean build\n./gradlew clean\n\n# Full build (includes tests)\n./gradlew build\n\n# Build without tests (faster)\n./gradlew build -x test -x integrationTest -x testCodeCoverageReport --refresh-dependencies --no-daemon --parallel\n```\n\n### Test Commands\n\n```bash\n# Run all tests (excludes flaky tests)\n./gradlew test\n\n# Run only unit tests (fastest)\n./gradlew unitTest\n\n# Run integration tests\n./gradlew integrationTest\n\n# Run flaky tests (separate from build)\n./gradlew flakyTest\n\n# Run tests for specific module\n./gradlew :core:test\n\n# Run single test class\n./gradlew :module-name:test --tests \"ClassName\"\n\n# Run single test method\n./gradlew :module-name:test --tests \"ClassName.methodName\"\n\n# After running tests: generate a markdown summary of failures only\nnpx --yes @kestra-io/kestra-devtools generateTestReportSummary --only-errors $(pwd)\n```\n\n### Frontend (UI)\n\n```bash\ncd ui\n\n# Install dependencies\nnpm install\n\n# Development server\nnpm run dev\n\n# Type checking\nnpm run check:types\n\n# Build for production\nnpm run build\n\n# Run tests\nnpm run test:all        # All tests with coverage\nnpm run test:unit       # Unit tests only\nnpm run test:storybook  # Storybook tests\n\n# Linting\nnpm run lint            # Fix linting issues\nnpm run test:lint       # Check linting only\n\n# Storybook\nnpm run storybook       # Development\nnpm run build-storybook # Build\n```\n\n### End-to-end tests\n\nThe Playwright suite is its own npm package at [e2e/](e2e/), installed separately from `ui/` so that\na spec change pulls in neither the frontend dependency tree nor the frontend CI jobs.\n\n```bash\ncd e2e\nnpm ci\nnpx playwright install chromium\n\nnpm run test:e2e                          # starts a Kestra docker image, runs the suite, stops it\nnpm run test:e2e-without-starting-backend # against an instance you already have running\nnpm run check:types && npm run test:lint\n```\n\n## Development Workflow\n\n### Running Locally\n\n1. **Start/stop backends:**\n```bash\n# Start databases with Docker Compose\ndocker compose -f docker-compose-ci.yml up\n\n# Stop databases with Docker Compose\ndocker compose -f docker-compose-ci.yml down\n```\n\n2. **Access application:** http://localhost:8080\n\n### Worktree setup\n\nWhen working in an OSS worktree (the working directory is under a `worktrees/` directory):\n```bash\ndev-tools/setup-worktree.sh ../worktrees/foo\n```\nThis 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.\n\n### Security Considerations\n- Use tenant isolation for multi-tenant features\n- Implement proper authorization with `@HasAnyPermission`\n- Handle secrets securely (never log sensitive data)\n\n### Performance Best Practices\n- Implement pagination for large datasets\n- Use streaming for large file operations\n- Cache frequently accessed data appropriately\n- Initialize collections with the expected size to avoid resizing overhead\n\n## Troubleshooting\n\n- **A Gradle build fails with no visible cause:** run `./gradlew clean` and retry once. When it repeats, read the actual error instead of retrying again.\n- **Tests fail on connection errors:** the databases are not running. `docker compose -f docker-compose-ci.yml up -d`.\n- **The frontend build fails on dependency resolution:** check the Node and npm versions against `package.json` engines before touching anything else.\n- **`npm install` fails with `EBADENGINE`:** the UI workspaces require npm >= 11.7 and set `engine-strict=true`. Older npm mislabels the `sass`/`sass-embedded` platform binaries as peer dependencies and rewrites `package-lock.json` on every install. Install the pinned npm (`npm i -g npm@11.16.0`) or use the Node release named in `ui/.nvmrc`.\n- **The UI builds but the app never mounts:** module federation, so the cause is usually workspace resolution rather than the change itself. Verify against a running instance rather than against the dev server.\n- **Debug logging:** `--logging.level.io.kestra=DEBUG`.\n\n## Module Structure\n\n**Core Modules:**\n- `cli` - Command Line Interface\n- `core` - Core functionality\n- `webserver` - Web server\n- `ui` - Vue 3 frontend application\n- `executor` - The component responsible for managing execution state\n- `scheduler` - The component responsible for scheduling polling and schedule triggers\n- `worker` - The component that executes tasks and manages worker instances\n- `worker-controller` - The component that manages worker instances and job distribution\n- `indexer` - The component responsible for indexing executions\n- `platform` - provides the Platform Bill of Materials (BOM) for dependency management\n\n**Queuing Layer:**\n- `queue` - Core API for queue implementations\n- `queue-jdbc` - JDBC-based queue implementation\n\n**Data Layer:**\n- `jdbc-*` - Database implementations (H2, Postgres, MySQL)\n\n**Testing Modules:**\n- `e2e` - Playwright end-to-end suite, run against a Kestra docker image\n- `tests` - Common test utilities and base classes\n- `jmh-benchmark` - JMH benchmarks for performance testing\n\n**Key Patterns:**\n- Repository pattern for data access\n- Service layer for business logic\n- Controller layer for HTTP endpoints\n- Builder pattern for object construction (often with Lombok `@Builder`)\n\n## Pull request guidelines\n\n- **One PR, one scope.** When you cannot pick a single conventional-commit scope for the title, the PR is doing too much and should be split.\n- Title format `type(scope): lowercase description`, following https://www.conventionalcommits.org/en/v1.0.0. Name the changed things rather than the actions taken on them: `fix(cases): assignees header, missing Acknowledged status, auto-link toggle`.\n- Use types: chore, feat, fix, refactor, test, docs, build\n- 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\n- Put the closing keyword on the first line of the description, as a full URL so that the link is not lost in a squash merge: `Closes https://github.com/kestra-io/kestra/issues/<id>.`\n- Cover new behavior with tests that could fail, following [When a test earns its place](#when-a-test-earns-its-place). \"Add tests\" never means one test per changed line.\n- Run `npm run lint` in `ui/` before pushing any frontend change. Otherwise one comment per violation is posted by reviewdog, and the human review is buried under trailing-comma suggestions.\n- Attach a screenshot or a short recording for every user-visible change, taken against a running instance.\n- Keep the branch rebased on `develop` rather than merged.\n- Fill in `.github/pull_request_template.md`, and delete the checklist section that does not apply instead of leaving it unticked.\n- Write the description as problem, fix and evidence, once each. No table restating the diff, and no checklist of tests that all passed.\n- Never commit generated output by hand: the twelve non-English translation files, `fingerprints.json`, and the generated SDK are produced by their scripts or by CI.\n\n## Issue guidelines\n- **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`).\n- **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.\n- Leave triage labels such as `kind/cooldown` to `kestrabot`; it applies them automatically on new issues.\n\n## UI Translations\n\n**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. When the string embeds markup or a component (`<code>`, `router-link`, `KsId`), render it with `<i18n-t>` (vue-i18n's component interpolation) and one named slot per placeholder, keeping the markup out of the translation instead of rendering it with `v-html`. 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.\n\nTranslation 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`.\n\n### Checking for missing translations\n\nRun the check script from the `ui/` directory:\n\n```bash\ncd ui && npm run translations:check\n```\n\nA clean run reports `No missing keys.`, `No extra keys.` and `No stale keys.` for every language. Anything listed must be fixed before merging — the same check runs as a PR gate.\n\n> **Enterprise Edition:** EE-only keys live in `ui-ee/src/translations/ee_translations/en.json`, and a tenant type's own keys in `ui-ee/src/tenantTypes/<type>/translations/`, unprefixed and rooted under `tenantTypes.<type>` at runtime. Both are checked separately — run `npm run translations:check` in `ui-ee` as well (see `kestra-ee/AGENTS.md` → \"Frontend i18n\").\n\n### Editing English strings\n\n**Changing an existing English value is a translation change.** Every key carries a fingerprint of the English text its translations were generated from, so editing `en.json` — even just the capitalisation — marks that key stale in all twelve languages and fails `translations:check` until it is regenerated. Run `npm run translations:generate` and commit the result alongside your change.\n\nThis is deliberate: before it existed, edited values were never propagated, and a rename of \"SuperAdmin\" to \"Superadmin\" sat un-translated in eleven locales for a year (kestra-io/kestra#10656).\n\n### Adding or regenerating translations\n\nPrefer `npm run translations:generate` (needs `GOOGLE_CLOUD_PROJECT` and Application Default Credentials, since Gemini is reached through Vertex AI: run `gcloud auth application-default login` once); it fills missing keys and re-translates stale ones on its own, with no flag to remember. Pass `true` to force a full re-translation of everything.\n\nIf you must write a translation by hand:\n\n1. Identify gaps by running `npm run translations:check`.\n2. Follow these translation rules (mirroring `ui/scripts/translations/generateTranslations.ts`, the generator shared by OSS and EE):\n   - **Reserved English terms — never translate:** `kv store`, `namespace`, `tenant`, `flow`, `subflow`, `task`, `log`, `blueprint`, `id`, `trigger`, `label`, `key`, `value`, `input`, `output`, `port`, `worker`, `backfill`, `healthcheck`, `artifact`, `min`, `max`.\n   - **ALL-CAPS status labels stay in English:** `WARNING`, `FAILED`, `SUCCESS`, `PAUSED`, `RUNNING`, etc.\n   - **Preserve `{placeholder}` variables** exactly — vue-i18n uses a **single** pair of braces. Do not translate the name inside the braces, do not rename it, and never write `{{placeholder}}`: double braces are a compile error (`Not allowed nest placeholder`) and make `t()` throw at render time. Each translation must carry exactly the same placeholders as the English source — no invented ones, none dropped.\n   - **Use natural UI terminology** — avoid false friends or overly literal translations (e.g. German: Execution → Ausführung, Theme → Modus, State → Zustand).\n3. Insert the translated keys into the correct position in the target language JSON, mirroring the key order of `en.json`.\n4. Re-run `npm run translations:check` to confirm everything is clean before committing.\n\nThe tooling itself lives in `ui/scripts/translations/` and is shared with EE, which keeps only thin entry points. Rules live in `.mjs` so the dependency-free PR gate can apply them; file IO and orchestration stay in `.ts`.\n\n### Conflicts in `fingerprints.json`\n\nTwo branches that both touch `en.json` will both regenerate `ui/scripts/translations/fingerprints.json`, so it conflicts often. **Never hand-merge the hashes and never pick a side** — a hash says \"this English text is what the twelve translations were generated from\", so choosing the wrong one silently marks a drifted key as current and the drift becomes invisible again.\n\nResolve it the same way as a `kestra-sdk` conflict — regenerate:\n\n```bash\ngit checkout --ours ui/src/translations/*.json ui/scripts/translations/fingerprints*.json\ncd ui && npm run translations:generate   # fills whatever the other branch added\nnpm run translations:check               # must report no missing / extra / stale keys\n```\n\n`en.json` itself normally merges cleanly, since branches usually add different keys; it is the generated files that collide.\n\n## Keeping this file up to date\n\nWhen a review comment corrects something that is not written here, and the correction is a rule rather than a one-off, add it in the same PR: one or two lines, with the example next to the rule. Delete a rule once it no longer matches the codebase. A guideline that no longer matches the code is worse than no guideline, since it will be followed confidently.\n\nWhen in doubt, follow the existing patterns in the codebase and stay consistent with them.\n","ui/packages/slot-contracts/package.json":"{\n    \"name\": \"@kestra-io/slot-contracts\",\n    \"version\": \"0.0.0-dev\",\n    \"description\": \"Kestra slot prop contracts – shared type definitions for federated plugin UI slots\",\n    \"type\": \"module\",\n    \"private\": true,\n    \"homepage\": \"https://github.com/kestra-io/kestra#readme\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"git+https://github.com/kestra-io/kestra.git\"\n    },\n    \"bugs\": {\n        \"url\": \"https://github.com/kestra-io/kestra/issues\"\n    },\n    \"exports\": {\n        \".\": \"./src/index.ts\"\n    },\n    \"main\": \"./dist/index.js\",\n    \"types\": \"./dist/index.d.ts\",\n    \"files\": [\n        \"dist\"\n    ],\n    \"scripts\": {\n        \"build\": \"tsdown\",\n        \"dev\": \"tsdown --watch\",\n        \"check:types\": \"vue-tsc --noEmit\",\n        \"prepublishOnly\": \"npm run build\"\n    },\n    \"peerDependencies\": {\n        \"@kestra-io/kestra-sdk\": \"*\"\n    },\n    \"devDependencies\": {\n        \"tsdown\": \"^0.23.1\",\n        \"ts-morph\": \"^28.0.0\",\n        \"unrun\": \"^0.3.1\"\n    },\n    \"dependencies\": {\n        \"zod\": \"^4.6.7\"\n    }\n}\n","ui/packages/topology/package.json":"{\n    \"name\": \"@kestra-io/topology\",\n    \"version\": \"0.0.0-dev\",\n    \"description\": \"Kestra flow topology graph component – powered by Vue Flow\",\n    \"type\": \"module\",\n    \"homepage\": \"https://github.com/kestra-io/kestra#readme\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"git+https://github.com/kestra-io/kestra.git\"\n    },\n    \"bugs\": {\n        \"url\": \"https://github.com/kestra-io/kestra/issues\"\n    },\n    \"exports\": {\n        \".\": \"./src/index.ts\",\n        \"./vue-flow-utils\": \"./src/vue-flow-utils.ts\",\n        \"./flow-yaml-utils\": \"./src/utils/flowYamlUtils.ts\"\n    },\n    \"main\": \"./dist/index.js\",\n    \"types\": \"./dist/index.d.ts\",\n    \"files\": [\n        \"dist\"\n    ],\n    \"sideEffects\": [\n        \"**/*.css\",\n        \"./dist/style.css\",\n        \"./src/index.ts\"\n    ],\n    \"publishConfig\": {\n        \"access\": \"public\"\n    },\n    \"scripts\": {\n        \"build\": \"tsdown\",\n        \"dev\": \"tsdown --watch\",\n        \"check:types\": \"vue-tsc --noEmit\",\n        \"test\": \"npm run test:unit\",\n        \"test:unit\": \"vitest run --config vitest.unit.config.ts\",\n        \"test:unit:watch\": \"vitest --config vitest.unit.config.ts\",\n        \"lint\": \"eslint --fix\",\n        \"test:lint\": \"eslint\",\n        \"prepublishOnly\": \"npm run build\"\n    },\n    \"dependencies\": {\n        \"dagre\": \"^0.8.5\",\n        \"html-to-image\": \"^1.11.13\",\n        \"js-yaml\": \"^5.5.0\",\n        \"vue-material-design-icons\": \"^5.4.0\",\n        \"yaml\": \"^2\"\n    },\n    \"peerDependencies\": {\n        \"@kestra-io/design-system\": \"*\",\n        \"@vue-flow/background\": \"^1.3.2\",\n        \"@vue-flow/controls\": \"^1.1.3\",\n        \"@vue-flow/core\": \"^1.48.2\",\n        \"vue\": \"^3.5.0\",\n        \"vue-i18n\": \"^11.4.4\"\n    },\n    \"peerDependenciesMeta\": {\n        \"vue-i18n\": {\n            \"optional\": true\n        }\n    },\n    \"devDependencies\": {\n        \"@tsdown/css\": \"^0.23.1\",\n        \"@types/dagre\": \"^0.7.54\",\n        \"@types/js-yaml\": \"^4.0.9\",\n        \"@types/node\": \"^26.6.5\",\n        \"@vitejs/plugin-vue\": \"^6.0.10\",\n        \"sass-embedded\": \"^1.105.2\",\n        \"tsdown\": \"^0.23.1\",\n        \"typescript\": \"^6\",\n        \"unrun\": \"^0.3.1\",\n        \"vite\": \"^8.3.4\",\n        \"vitest\": \"^5.0.5\",\n        \"vue\": \"^3.5.44\",\n        \"vue-i18n\": \"^11.4.16\",\n        \"vue-tsc\": \"^3.3.13\"\n    }\n}\n","ui/AGENTS.md":"# UI Design System Guidelines\n\nScope: 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.\n\nThe 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.\n\n## What this is, in plain terms\n\nThink of the design system as the product's **visual vocabulary**:\n\n- A short list of agreed-upon **colors**, **fonts**, and **spacings** (called *design tokens*).\n- A library of pre-built **components** (`KsButton`, `KsTable`, `KsDialog`, …) that already use those tokens.\n- 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.\n\nIf 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.\n\nUnder 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/`.\n\n> **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.\n\n## Golden rules (non-negotiable)\n\nThese rules are what keep the UI maintainable as it grows. Treat any deviation as a bug.\n\n**How Kestra looks is decided by the design system; what it does is decided by feature code.** A new visual pattern (a new card treatment, a different spacing rhythm, another status color, a bespoke empty state, a one-off modal layout) counts as a design-system change and belongs in `ui/packages/design-system/`, after a conversation with design. Approximating one inside a feature component is the fastest route to a screen that is fine in the one theme you checked it in and wrong everywhere else. When nothing in the catalogue below covers what you need, raise it as a gap in the design system instead of improvising around it.\n\n1. **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.\n2. **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.css` / `ks-theme-dark.css` / `ks-theme-dark-2.css` — do not pick a one-off color.\n3. **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 `--ks-font-family-*` and `--ks-font-size-*` tokens; feature code should not redefine them.\n4. **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.\n5. **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.\n6. **No magic numbers for theme values.** Spacing, radii, font sizes, and shadows must reference tokens — 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.css`](packages/design-system/src/assets/styles/ks-tokens.css)). 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)`).\n7. **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.\n8. **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.\n9. **Every new `Ks*` component needs a Storybook story and a unit test.** Stories double as living documentation for design and product reviewers.\n10. **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`.\n11. **Check that a token exists before using it.** With an invalid `var(--ks-…)` and no fallback, the property is silently inherited instead, so the mistake remains invisible until the computed style is measured. The cases feature was released using `--ks-font-size-medium`, `--ks-font-size-small`, `--ks-radius-2` and `--ks-border-active`, none of which are declared anywhere, and the interval filter's Apply-to row shipped with no visible selection for the same reason (kestra-io/kestra#18777).\n\n    Your editor underlines it: `kestra-tokens/no-undeclared-css-token` (eslint, `@eslint/css`) covers `.css` and plain `<style>` blocks, `kestra-tokens/no-undeclared-ks-token` (eslint) covers `cssVar(\"--ks-…\")` and tokens written inside strings, and `ks/no-undeclared-custom-property` (stylelint) covers the design system's remaining SCSS. All three propose a replacement, and `npm run lint` runs them all. See [scripts/tokens/README.md](scripts/tokens/README.md).\n12. **Copying an existing rule is not proof that it is correct.** Several hundred `:deep()` selectors, some hex codes and some raw pixel values are older than these rules and are being removed over time. Treat them as debt rather than as precedent: don't add more, and clean up the ones in the component you are already editing.\n13. **Bootstrap is gone; don't use its utility classes.** The `d-flex`, `mb-3` and similar classes still in the templates are kept alive by `packages/design-system/src/assets/styles/_utilities.css` until their migration issues land, and that file only shrinks: replace a utility with scoped CSS on `--ks-*` tokens and delete its rule once the last use is gone. `tests/unit/designSystem/bootstrapGuard.spec.ts` fails on a utility the file does not define and on any `--kbs-*` variable.\n\n## Best practices for keeping the design system healthy\n\nA 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.\n\n### Before you write code\n\n- Search the component tables and Storybook first. The most common waste in this codebase is rebuilding something that already exists.\n- 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.\"\n- For anything visible to a user, check both light and dark mode in Storybook before merging.\n\n### While you write code\n\n- 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.\n- Keep the style **inside** the SFC. `<style scoped src=\"./x.css\">` is valid and `scoped` still applies, but an external file separates the CSS from the markup it describes for no gain, and it is one more file to open. Block order is `template`, `script`, `style`, enforced by `vue/block-order` in `ui/eslint.config.js`.\n- Write styles as plain CSS: no `lang=\"scss\"` and no new `.scss` file outside `packages/design-system/` (kestra-io/kestra#21346). Native nesting works with `scoped`, but start a nested selector with `&` (`& :deep(.x)`, `& > *`), or Vue puts `[data-v-*]` on the nested part instead of the parent.\n- 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.\n- 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.\n- 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 }`).\n- Use semantic tokens, not raw colors. `var(--ks-text-link)` communicates intent; `var(--ks-text-blue-500)` does not exist for a reason.\n- Co-locate component-specific tokens (e.g. `--ks-card-shadow`) in the component's SCSS, but always derive them from semantic tokens.\n\n### When extending the design system\n\n- Component styles are plain CSS. A component that needs Element Plus's base styles keeps them in a `<style lang=\"scss\">` block holding only the `el-ns` and `theme-chalk` `@use` lines, placed before the plain `<style>` with its own rules, so each published component entry still ships its chalk CSS. Palette colours are `var(--ks-<family>-<step>)`, not `palette.$base-*`.\n- Don't write one rule per variant. When the tokens follow the variant's name (`--ks-status-<status>`), bind them with `v-bind()` and add a test that every name has its token, since the token lint cannot see a built name (`KsExecutionStatus`, `KsAlert`). When they don't, keep a typed table of `var(--ks-*)` strings in the component's `<script>`, where `kestra-tokens/no-undeclared-ks-token` still checks them, and bind that (`KsTag`, `KsButton`).\n- Only expose props that are actually used somewhere in the codebase. Speculative props rot.\n- Mirror Element Plus prop names where possible — predictability is a feature.\n- Pass `v-bind=\"$attrs\"` and forward slots so wrappers don't trap consumer extension points.\n- 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.\n- Document tokens in code comments next to where they're declared in `ks-theme-*.css`. The `scripts/generate-palette.mjs` file is auto-generated — don't hand-edit it.\n\n### When reviewing a UI PR\n\nReject (or ask to fix) anything that:\n\n- Imports from `element-plus` directly into `ui/src/`.\n- Uses a hex code, `rgb(...)`, `--el-*`, or `--bs-*` for color.\n- Uses `:deep()` to reach into a `Ks*` or `el-*` component.\n- Hardcodes pixel values for padding, margin, radii, font sizes, or shadows.\n- Adds a CSS class that overrides `.el-...` selectors.\n- Duplicates a component that already exists in the design system.\n- Adds a `Ks*` component without a Storybook story or test.\n- Mounts `KsDataTable` without binding `:currentPage` / `:pageSize` (or `v-model:currentPage` / `v-model:pageSize`) — pagination is controlled; see \"Data tables & pagination state\".\n- 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\".\n- Adds a modal/drawer where the user enters data without guarding accidental dismissal — see \"Unsaved input in modals (discard guard)\".\n\n### Accessibility\n\n- Every icon-only `KsIconButton` must have an accessible label (`aria-label` or `title`). Screen readers do not see the icon glyph.\n- Never convey state with color alone — pair status colors with an icon (`KsExecutionStatus` already does this) or a text label.\n- Use semantic HTML inside slots: real `<button>`, `<a>`, `<label>`, headings in document order. Don't fake interactivity with `<div @click>`.\n- `KsDialog`, `KsDrawer`, `KsPopover` already manage focus trap and `Escape`-to-close — don't reimplement these in feature code.\n- Keep tab order logical; rely on the DOM order rather than `tabindex` hacks.\n- 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.\n\n### Internationalization\n\n- No hardcoded user-facing strings. Always go through i18n.\n- **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.\n- Pass plurals and interpolation as named arguments to `$t(...)`, never string-concatenate.\n- A string that embeds markup or a component (`<code>`, `router-link`, `KsId`) renders through `<i18n-t keypath=\"...\" tag=\"span\" scope=\"global\">` (vue-i18n's component interpolation, registered globally, so there is nothing to import; `scope=\"global\"` names the scope every message lives in, or the component looks for a local one up the tree and warns; `kestra-i18n/i18n-t-scope` fails a tag without it and `npm run lint` adds it), with one named slot per placeholder: `Mark as {status}` becomes `<template #status><code>{{ item.code }}</code></template>`. Keep the markup out of the translation, so no `v-html` and no `escapeHtml` are needed, and translators can reorder the placeholders.\n- `splitTranslation(t, key, slot)` and markup passed as a named argument to `v-html` are older patterns for the same job. Don't add new uses; move a string to `<i18n-t>` when you touch it.\n- 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. `tests/unit/dateFormatGuard.spec.ts` (and its `ui-ee` twin) fails on `dayjs(...).format(`, `toLocaleString()` on a `Date`, and `Intl.DateTimeFormat` in `src/`; a file that serializes a date rather than displaying it goes in its `ALLOWED` list with the reason.\n- Date arithmetic goes through the `dayjs` re-exported by the design system. `dayjs` objects are **immutable**, so `d.add(1, \"day\")` returns a new instance and mutating idioms (`cursor.add(...)` in a loop) silently no-op — reassign instead. Parse untrusted input with `dateUtils.parseIso()`: bare `dayjs()` happily reads an epoch number or a bare year as a date.\n- 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.\n\n### Loading, empty, and error states\n\nEvery async surface must render all four states. \"Happy path only\" is a bug.\n\n- **Loading:** `KsSkeleton` for content placeholders; `vKsLoading` directive for sections that already have layout; `KsLoading` component for full-page or container-level spinners.\n- **Empty:** `KsEmpty` with an action where possible — never a blank screen.\n- **Error:** `KsAlert type=\"error\"` with retry affordance, or `KsMessage` for transient errors.\n- **Success / data:** the actual content.\n\n### Data tables & pagination state\n\n`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.\n\n**The contract:**\n\n- Bind `:currentPage` / `:pageSize` (one-way) OR use `v-model:currentPage` / `v-model:pageSize` (two-way).\n- 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)})`.\n- 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.\n- `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.\n- When a column's `prop` is not the backend sort key (e.g. `auditLog.detail.resourceType` vs `detail.resourceType`), pass `:sortKeyMapper` (`(key: string) => string`); a sort click reloads immediately with the raw column prop otherwise, and the backend rejects it with a 422.\n\n**URL-driven pattern** — the default for top-level list pages (Logs, Flows, Executions, KV, Secrets, Triggers, FlowsSearch, Blueprints):\n\n```vue\n<KsDataTable\n    :loadData=\"loadData\"\n    :currentPage=\"urlPage\"\n    :pageSize=\"urlSize\"\n    :total=\"store.total\"\n    @page-changed=\"({page, size}) => router.push({query: {...route.query, page: String(page), size: String(size)}})\"\n/>\n\n<script setup>\nconst urlPage = computed(() => Number(route.query.page) || 1)\nconst urlSize = computed(() => Number(route.query.size) || 25)\n</script>\n```\n\n**Local-state pattern** — for embedded tables that should not appear in the URL (MetricsTable, side-panel views):\n\n```vue\n<KsDataTable\n    v-model:currentPage=\"currentPage\"\n    v-model:pageSize=\"pageSize\"\n    :loadData=\"loadData\"\n    :total=\"...\"\n/>\n\n<script setup>\nconst currentPage = ref(1)\nconst pageSize = ref(25)\n</script>\n```\n\n**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.\n\n**Defaults that land in the URL after mount** — a page whose filter defaults are written by a `router.replace` (e.g. the Logs page's default level, via `useRouteFilterPolicy`) must gate its first `loadData` on `isRouteSettled`. `KsDataTable` loads on mount, so without the gate the page fires a request for the bare query, throws that result away when the defaults arrive, and leaves the two responses racing each other. Gating means the query change is what triggers the first load, so also handle the navigation never landing: `isRouteSettled` gives up after a timeout, and the page must reload when it does — otherwise the list stays empty for good, with no request in flight and nothing to retry it.\n\n**Stores that back a filterable list** must ignore superseded responses: keep a sequence number per search and drop a response whose sequence is no longer the newest (see `stores/logs.ts`). Otherwise the response that happens to land last wins and the list can show results for filters the user already left — or nothing at all, when the stale search matched nothing.\n\n### The deep-watch / computed-spread trap\n\nA `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.\n\nThis 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.\n\n**Don't:**\n\n```ts\nconst filterQuery = computed(() => {\n    const {page: _p, size: _s, sort: _so, ...filters} = route.query\n    return filters  // new object reference on every route.query change\n})\nwatch(filterQuery, () => dataTable.value?.resetAndReload(), {deep: true})\n// Fires on every route.query change — page clicks, sort clicks, anything —\n// and bounces the user back to page 1.\n```\n\n**Do:**\n\n```ts\nconst filterQueryKey = computed(() => {\n    const {page: _p, size: _s, sort: _so, ...filters} = route.query\n    return JSON.stringify(filters)  // stable string — same content, same value\n})\nwatch(filterQueryKey, () => dataTable.value?.resetAndReload())\n// Fires only when filter content actually changes.\n```\n\nThe 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.\n\n### Router guards and `initApp`\n\nGuards are wired through `initApp`, and three things about that are easy to get wrong.\n\n**Register before the router installs.** `app.use(router)` starts the first navigation, and `vue-router` reads `beforeGuards.list()` once that navigation reaches its guard phase. A guard registered synchronously after `app.use(router)` is still picked up; one registered after an `await` is not, and `initApp` awaits i18n and the moment locale between installing the router and returning. A guard added in `initApp(...).then(...)` therefore misses the first navigation, which on a cold load is the only navigation there is. Pass it through the `guards` argument instead.\n\n`afterEach` is in the same window, for a less obvious reason: `afterGuards.list()` is read when a navigation *finishes*, and the first navigation can finish while `initApp` is still awaiting. An `afterEach` registered after those awaits therefore never sees it either, which silently costs anything keyed on the first navigation (`eventsRouter` attributes the landing page to `document.referrer` on exactly that hop).\n\n**A guard's arity decides how `vue-router` reads it.** `initApp` registers guards as `guards.beforeEach.bind(null, router)`, so a `(router, to, from)` guard arrives bound with `length` 2 and stays in return-value mode, where returning a route object redirects. Add a fourth parameter and the bound length becomes 3, which flips `vue-router` into `next()`-callback mode: the returned redirect is ignored and the navigation hangs with no error. Keep guards at `(router, to, from)`.\n\n**There is one `beforeEach` slot.** OSS spends it on `tenantGuard` and can only do so because its auth guard is a `beforeResolve`; EE spends it on `authGuard`. A second `beforeEach` has to be composed into the existing one, or `initApp` has to grow to accept a list, rather than being registered afterwards, since \"afterwards\" is exactly the case that misses the first navigation.\n\n### Session loss and an unreachable server\n\nA 401 on a logged-out session opens `ReauthDialog` through `requestReauth(signIn)` and replays the failed request once the user signs in, so the page and its unsaved state survive; the edition supplies its own `signIn` from `onUnauthorized`. A request that fails without a response (or behind a 502/503/504) raises `ServerUnreachableBanner`, which clears on the next response. Don't add per-page handling for either, and don't clear `unsavedChange` before the user has chosen to leave. The native basic-auth dialog is suppressed for fetch and XHR only: `EventSource` cannot set headers and relies on `Sec-Fetch-Dest`, which browsers send only over HTTPS or localhost, so on plain HTTP its streams can still open the dialog.\n\n### Unsaved input in modals (discard guard)\n\nAny modal/drawer where the user **enters data** must not silently lose it on an accidental dismissal. `KsDialog` and `KsDrawer` take a `dirty` prop and ask for confirmation themselves; never reimplement the confirm-before-discard logic per modal.\n\n```vue\n<KsDialog v-model=\"visible\" :dirty=\"isDirty\" ... />\n<KsDrawer  v-model=\"visible\" :dirty=\"isDirty\" ... />\n```\n\n```ts\n// isDirty: true when there is unsaved input worth a prompt, usually a comparison against a snapshot taken on open\nconst baseline = ref(\"\")\nconst isDirty = computed(() => JSON.stringify(form.value) !== baseline.value)\n```\n\n`dirtyMessage` replaces the default confirmation text when the discarded thing is not a form (`TriggerFlow.vue` uses it for an unsubmitted execution). When the close also has to run cleanup, keep `:beforeClose=\"(done) => { reset(); done() }\"` next to `:dirty`: the confirmation runs first, `beforeClose` only once the user agreed. The composable behind the prop, `useDiscardGuard(isDirty, {message?})` from `@kestra-io/design-system`, stays available for closes that do not go through a `Ks*` container.\n\nRules:\n- **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.\n- **`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.\n- **`KsDialog` and `KsDrawer` both expose `dirty` and 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.)\n- **Don't guard** read-only viewers, action/confirmation dialogs, or ephemeral forms that reset on every open.\n\n### Icons\n\n- All icons come from [`vue-material-design-icons`](https://github.com/robcresswell/vue-material-design-icons) via `<KsIcon>` (or `<KsIconButton>` for clickable icons).\n- 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.\n- Pass `name` (the kebab-case Material name); size and color come from props or the surrounding token context — don't override with inline `style`.\n- **Two file-type icon sets coexist on purpose, so don't merge them.** `fileUtils.fileIcon()` (behind `KsFileTag`) maps an extension to a monochrome `vue-material-design-icons` component, which inherits `--ks-icon-*` and so recolors per theme and per tag variant. `ui/src/components/utils/icons/Type.vue` renders the colored `material-file-icons` SVGs for the namespace file explorer, where the brand colors are the point. That package is a `ui/` dependency the design system does not have, and it bakes its colors into a base64 `<img>` that no token can reach, so it cannot be used from a `Ks*` component.\n\n### Performance\n\n- Lazy-load heavy surfaces: `KsEchart`, `KsLine`, `KsBar`, `KsPie`, `KsGraph`, `KsMarkdown`, code-editor surfaces. Use `defineAsyncComponent` or route-level code splitting.\n- Prefer `v-show` for frequent toggles (tabs, filters), `v-if` for rare/heavy mounts (modals, big tables).\n- Pass stable `key` props in lists. Avoid index-based keys when items have IDs.\n- Don't render giant tables without `KsDataTable`'s pagination/virtualization — server-side paging is the default for anything that can grow.\n- Watch out for `watch(..., { deep: true })` and `computed` with object identity — they often re-run more than you expect.\n\n### Types\n\n**Never write `any`.** TypeScript stops checking a value the moment it is typed `any`, so a typo or a renamed field is found by whoever opens the page instead of by the compiler. The rule covers every spelling: `(row: any)`, `x as any`, `any[]`, `Record<string, any>`, in the `<script>` block and in template expressions alike.\n\nWhere the type comes from, in this order:\n\n1. **Data from the backend: `kestra-sdk`.** It is generated from our OpenAPI spec, so the type already exists and stays in step with the API. `import type {Execution} from \"@kestra-io/kestra-sdk\"`.\n2. **Data from a library** (vue-router, element-plus, monaco, echarts, the DOM): the library's own types. When a package ships none, add a `declare module` file next to `src/material-icons.d.ts` rather than reaching for `any`.\n3. **An interface the app already has.** Search for one before writing another.\n4. **A new interface**, only when none of the above fit.\n\nFor a value whose shape really is not known yet, `unknown` with a narrowing check is the honest escape hatch. `any` is not.\n\n**A legitimate `any`, or a package with no types?** Give the package a small `.d.ts` shim next to `src/material-icons.d.ts`. An `any` that truly cannot be avoided is a conversation with a maintainer, not a number you change on your own.\n\n**MANDATORY — never raise the baseline to make the check pass.** A bigger number hides the new `any` from every later run, which is the one thing the baseline exists to prevent, and it will be treated as a bug in review. Specifically, and this applies to coding agents as much as to people:\n\n- Do not hand-edit `scripts/explicit-any/baseline.json`. The only writes to it come from the check itself.\n- Do not run `--accept-new-any`. A maintainer who has already agreed to a raise records it with `npm run check:ts-any -- --write --accept-new-any`; it is never a way to get a green check.\n- A red `check:ts-any` is fixed by typing the value, not by making the check agree with the code.\n- When you cannot type it, stop. Leave the check red, say in the PR which value defeated you and why, and let a maintainer decide. An unfinished PR is fine; a silently raised baseline is not.\n\n**The check.** `npm run check:ts-any` counts the explicit `any` per file, oxlint for the `<script>` block and the Vue compiler for template expressions, and compares the counts with `scripts/explicit-any/baseline.json`, which records what was already in the tree when the rule came in. The same check runs on every PR, as `Npm - check ts-any`. It fails in two directions and the message says which:\n\n- ``New `any` in 1 file(s)`` with a line like `src/utils/filters.ts: 1 -> 2`. Your change added one. Type it with the order above.\n- `The baseline is out of date` with `src/utils/filters.ts: 3 -> 2`, or `moved from …` when you renamed a file. Nothing got worse, the baseline just has to follow the code. Run `npm run check:ts-any -- --write` and commit it alongside your change. A rename is followed whether it is staged or already committed on your branch, so the count travels with the file either way.\n\nInstall the repo hooks once with `.github/.hooks/setup_hooks.sh` and the second case stops happening: the pre-commit hook lowers the baseline and stages it with the rest of your commit.\n\n### Testing UI\n\n- Unit tests with **Vitest** + `@vue/test-utils`, and Storybook stories, live next to the file they cover: `src/components/logs/Foo.vue` has `Foo.spec.ts` and `Foo.stories.ts` beside it. A spec or story about one aspect of a file is named `<file>.<focus>` (`Foo.download.spec.ts`, `flow.draftSave.spec.ts`) so it sorts next to its subject; one that exercises several modules together keeps a descriptive name in their folder (`entityLoadRace.spec.ts`). `tests/unit/` keeps only the setup, the shared helpers (`i18nMount.ts`) and the repo-wide guards; `tests/storybook/` keeps only shared fixtures and the stories with no single source file. The `design-system` and `topology` packages follow the same rule with `*.test.ts`, their `tests/` keeping only setup and helpers. `tests/unit/specLocationGuard.spec.ts` fails on a spec or story left in any of these `tests/` folders for a file that exists in `src/`. Playwright specs stay in the `e2e/` package: they exercise the running app, not one source file.\n- Mount through `i18nMount` or `i18nShallowMount` rather than calling `mount` with your own `createI18n`. Pass `messages` for the keys the spec asserts on, or `locales: en` when it needs the real `en.json`; with neither, `t(\"key\")` renders the key. Missing-key warnings are off in both helpers, so a spec asserting on raw keys stays quiet. Two copies exist and behave the same: specs under `src/`, `tests/unit/` and `packages/topology/src/` import `tests/unit/i18nMount.ts`, and design-system specs import `packages/design-system/tests/units/i18nMount.ts`. Both install the design system, as the app does at bootstrap, so a mounted `Ks*` component resolves instead of warning; a spec that mocks `@kestra-io/design-system` away has to keep a `default` export for that install, such as `default: {install: () => {}}`. Only a spec that never mounts anything, such as one testing pluralisation on the instance itself, builds its own i18n.\n- `i18nMount` also installs a fresh `memoryRouter()` (`tests/unit/memoryRouter.ts`), so `useRoute()` / `useRouter()` resolve instead of warning; a spec that asserts on navigation passes its own router in `global.plugins`, which replaces the default, or mocks `vue-router`. A router already in test-utils' global `config.global.plugins` (ui-ee's setup installs one) also replaces it. A spec mounting with plain `mount` passes `memoryRouter()` itself. A spec that creates a store outside a component activates `routedPinia()` from the same file instead of a bare `createPinia()`, so a store calling `useRoute()` at setup resolves it there too.\n- A spec that mounts a no-code field (`TaskObject`, `TaskObjectField`, `TaskArray`, `Add`, `TaskEdit`) ends with `afterAll(() => taskComponentsReady())` from `useBlockComponent.ts`: the block components are imported on mount, and with `isolate: false` an import still in flight when the file ends is reported as an `EnvironmentTeardownError` for the whole run.\n- A component passed inside `props` (a tab's `component`, an `icon`) is wrapped in `markRaw`: test-utils makes the props object reactive, and Vue warns about a component that was made reactive on every render.\n- Check a spec's console with `npx vitest run --project=unit --silent=false <file>`. Vitest picks a minimal reporter in agent and non-interactive shells that hides the output of passing tests, so a warning CI prints can be invisible locally.\n- A test that exercises a failure path the code reports with `console.error` stubs it (`vi.spyOn(console, \"error\").mockImplementation(() => {})`) and restores it, and ideally asserts on the call; output from a passing test is noise that hides the warning worth reading.\n- 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.\n- 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.\n- Visual regressions caught in Storybook are cheaper to fix than caught in production.\n- A test only earns its place when it can fail for a reason a reviewer would care about. Don't test that a prop is passed through, that a computed returns its own input, or that a `Ks*` component renders: the template is only restated by those assertions, and they are broken by every refactor. Delete a Vitest test once a story covers the same behavior, and say so in the PR description.\n\n### Checking a frontend change before pushing\n\n```bash\nnpm run check:types && npm run test:unit && npm run lint\n```\n\n`npm run lint` is not optional. Without it, one PR comment per eslint violation is posted by reviewdog (missing trailing commas, mostly) and the human review is buried underneath them.\n\n`npm run check:ts-any` compares the explicit `any` per file against `scripts/explicit-any/baseline.json`. It fails when a file gains one, so type it instead of raising the number. It also fails when a file loses one, because the baseline has to come down with the code: run `npm run check:ts-any -- --write` and commit the smaller numbers, or install the repo's git hooks (`.github/.hooks/setup_hooks.sh`) and the pre-commit hook does it for you. `--write` only ever lowers; it refuses to raise a count.\n\nColour is gated automatically. `tests/unit/designSystem/colorGuard.spec.ts` fails on any hex, `rgb()`, `hsl()`, `oklch()`, `--el-*`, `--bs-*`, SCSS colour variable or bare keyword colour reaching a colour property anywhere under `ui/src` and `ui/packages/topology/src`, skipping `*.spec.ts`, `*.test.ts` and `*.stories.ts`, and prints `path:line (colours)` for every offending line. It runs with `npm run test:unit` and on every PR. Replace what it reports with a `--ks-*` token, a `Ks*` prop, or a change in the design system.\n\nTwo escape hatches exist, each taking a reason, for the case the guard cannot judge: artwork whose colours **are** the asset (a brand mark, a third-party logo) rather than a themed surface. Nothing else qualifies, and a hardcoded colour that a token could carry is a bug whether or not the guard is silenced. There is deliberately no whole-file opt-out, and a `-start` with no `-end` is reported instead of muting the rest of the file.\n\n```scss\n/* design-system-disable-next-line: the Kestra mark is fixed artwork */\n/* design-system-disable-start: … */  /* … */  /* design-system-disable-end */\n```\n\nThe guard reads `.vue`, `.scss`, `.css`, `.ts` and `.js`; in a `.vue` file it reads `<style>` blocks plus `fill=` / `stroke=` attributes, and in a script it reads a hex under a colour-named key (`colorHex: \"#…\"`), so a `//`, `/* */` or `<!-- -->` comment all work.\n\nSpacing, radii and `:deep()` are not gated, so still read your own diff for those:\n\n```bash\ngit diff -U0 develop... -- '*.vue' '*.scss' \\\n  | grep -nE '^\\+.*(:deep\\(|(padding|margin|gap|border-radius|font-size)[[:space:]]*:[[:space:]]*[0-9]+px)'\n```\n\nFor a user-visible change, also check it in light **and** dark mode, and attach a screenshot to the PR.\n\n### Deprecation contract\n\nWhen retiring a `Ks*` component, prop, or token:\n\n1. Mark with a `@deprecated` JSDoc tag *and* a one-line replacement path: `@deprecated since 0.x — use <KsNewThing> instead`.\n2. Keep it working for at least one minor release; add a `console.warn` in dev mode if the cost is reasonable.\n3. Migrate all callers in the same release where feasible — don't leave half-migrations.\n4. Only delete after the deprecation window. A silent removal breaks downstream EE / plugin code.\n\n## Anti-patterns (do not write these)\n\n```vue\n<!-- Wrong: raw element-plus, hex color, :deep, SCSS variable in feature code -->\n<template>\n    <el-button class=\"my-btn\">Save</el-button>\n</template>\n<style lang=\"scss\" scoped>\n    .my-btn {\n        background: #8405ff;\n        font-size: $font-size-md;\n    }\n    :deep(.el-button__text) { color: white; }\n</style>\n```\n\n```vue\n<!-- Right: Ks component, semantic tokens, no deep selector, i18n -->\n<template>\n    <KsButton type=\"primary\">{{ t(\"save\") }}</KsButton>\n</template>\n<style scoped>\n    /* Almost always: no custom CSS is needed at all. */\n</style>\n```\n\nIf your `<style>` block needs to exist:\n\n```css\n/* Right: --ks-* tokens, no SCSS vars in feature code, no :deep */\n.my-feature {\n    background: var(--ks-bg-surface);\n    color: var(--ks-text-primary);\n    border: 1px solid var(--ks-border-default);\n}\n```\n\n## Components\n\n### Basic / Layout\n\n| Component | Purpose |\n|-----------|---------|\n| `KsButton` / `KsButtonGroup` | Primary action button and grouped buttons; `--ks-button-content-width` stretches the content (default `auto`); `block` stretches the button to its container's width |\n| `KsIcon` / `KsIconButton` | Material Design icon display; icon-only button (always with `aria-label`) |\n| `KsLink` | Styled hyperlink |\n| `KsText` | Typography wrapper — preferred over raw `<span>` / `<p>` for theme-aware text |\n| `KsScrollbar` | Custom-styled scrollbar wrapper |\n| `KsContainer` / `KsHeader` / `KsMain` | Page layout shell |\n| `KsRow` / `KsCol` | Responsive grid |\n| `KsSplitter` / `KsSplitterPanel` | Resizable split-pane layout |\n\n### Feedback\n\n| Component | Purpose |\n|-----------|---------|\n| `KsAlert` | Alert banner for messages and status feedback. Pass `banner` for a full-width system bar (licence, impersonation): square, bottom rule only, body text in `--ks-text-primary` so it reads at AA on the tint |\n| `KsDialog` | Modal dialog (handles focus trap + Escape); `dirty` asks before an accidental close; content that cannot fit the default width widens it to 1024px with a `data-dialog-wide` attribute on an element inside it, so a step that renders it is wide and the others are not |\n| `KsDrawer` | Side drawer / panel; `dirty` asks before an accidental close |\n| `KsTooltip` | Hover tooltip |\n| `KsPopover` | Popover for contextual content |\n| `KsLoading` (`vKsLoading`) | Loading spinner directive |\n| `KsMessage` | Toast notification service |\n| `KsNotification` | Notification service |\n| `KsMessageBox` | Confirmation dialog service |\n\n### Form\n\n| Component | Purpose |\n|-----------|---------|\n| `KsInput` / `KsPassword` | Text and password inputs |\n| `KsInputNumber` | Numeric input with increment / decrement |\n| `KsSelect` / `KsOption` / `KsOptionGroup` | Dropdown select |\n| `KsAutocomplete` | Autocomplete input with suggestions |\n| `KsCheckbox` / `KsCheckboxGroup` / `KsCheckboxButton` | Checkbox variants |\n| `KsRadio` / `KsRadioGroup` / `KsRadioButton` | Radio button variants |\n| `KsRadioCardGroup` | Single-select radio group rendered as option cards (title + optional hint/icon/disabled); options-driven via `:options` + `v-model` |\n| `KsSwitch` | Toggle switch |\n| `KsThemePicker` | Theme chooser, one miniature of the app per theme painted in that theme's own colours; options-driven via `:options` + `v-model`. A `#preview=\"{option}\"` slot replaces the built-in miniature, `columns` lays the options out as a grid, `ariaLabel` names the radiogroup |\n| `KsDatePicker` / `KsTimePicker` | Date and time pickers |\n| `KsColorPicker` | Color picker; `alphaOnSurface` shows translucent colours over the page surface instead of a checkerboard |\n| `KsDurationPicker` | ISO 8601 duration picker |\n| `KsCascaderPanel` | Cascading hierarchical selector |\n| `KsUpload` | File upload |\n| `KsForm` / `KsFormItem` | Form container with validation |\n\n### Data Display\n\n| Component | Purpose |\n|-----------|---------|\n| `KsCard` | Card container |\n| `KsTable` / `KsTableColumn` | Basic table |\n| `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\". Pass `mobileColumns` (`{leading?, title, titleAside?, meta?, metaAside?, actions?}`, each naming a column `prop`, or the `columnKey` of a prop-less actions column) to render a card per row below 768px: `leading` is a status dot, `title` is bold, `titleAside` sits on its right, `meta` columns share the second line joined by a dot and `metaAside` closes it; `actions` is a corner cell on the first line; unlisted columns are hidden, bulk selection is desktop-only (the selection column is not shown on cards), and the regular table returns from 768px. |\n| `KsEntityLink` | Clickable cross-entity reference (namespace / flow) for table cells — neutral tag with leading icon, violet on hover. Pass `noIcon` in dense embedded tables (e.g. dashboard chart tables, ~90px columns) where the icon's 20px costs more than it tells |\n| `KsBadge` | Small indicator badge; `size=\"small\"` shrinks the pill so it sits on an icon button instead of covering it |\n| `KsNewBadge` | Compact uppercase \"NEW\" pill flagging a newly shipped feature — caller supplies the label via the default slot |\n| `KsTag` / `KsCheckTag` | Tag / label; clickable checkbox-style tag. Pass `truncate` to clip a long label with an ellipsis instead of letting the tag outgrow its container |\n| `KsAvatar` | Avatar with fallback |\n| `KsProgress` | Progress bar |\n| `KsPagination` | Pagination controls |\n| `KsEmpty` | Empty state placeholder |\n| `KsSkeleton` | Skeleton loader |\n| `KsId` | Copyable ID display |\n| `KsDateAgo` | Relative time display (\"2 hours ago\") |\n| `KsSegmented` | Segmented control; object options may carry an `icon` component, rendered before the label. `variant=\"neutral\"` swaps the primary-coloured selection for a grey one with wider segments |\n| `KsCollapse` / `KsCollapseItem` | Collapsible sections |\n| `KsFileTag` | Storage URI rendered as a file reference: a `KsTag` whose symbol comes from the extension, with a readable name (`name`, defaulting to the URI's last segment); the full URI stays in the tooltip |\n| `KsTree` | Hierarchical tree view |\n| `KsJsonTree` | Read-only JSON tree viewer; leaves holding a storage URI render through `KsFileTag` |\n| `KsTimeline` / `KsTimelineItem` | Timeline visualization |\n| `KsExecutionStatus` | Execution / task status badge with icon and color; `dot` collapses it to a coloured dot (label kept for screen readers) for dense layouts such as mobile cards |\n| `KsCodeStatus` | Compact validity badge with icon (`valid` / `error`) — caller supplies the label |\n| `KsMarkdown` | Markdown renderer (lazy-load on heavy surfaces) |\n\n### Charts\n\n| Component | Purpose |\n|-----------|---------|\n| `KsEchart` | ECharts base wrapper (lazy-load) |\n| `KsLine` / `KsBar` / `KsPie` | Line, bar, and pie charts (lazy-load) |\n| `KsGraph` | Graph / network visualization (lazy-load) |\n\n### Navigation\n\n| Component | Purpose |\n|-----------|---------|\n| `KsTabs` / `KsTabPane` | Tabbed interface |\n| `KsMenu` / `KsMenuItem` | Hierarchical menu |\n| `KsDropdown` / `KsDropdownMenu` / `KsDropdownItem` | Dropdown menu; pass `danger` on an item to give a destructive or exit action (delete, log out) the error-coloured hover |\n| `KsTopNavBar` | Top navigation bar; `titleSiblings` reaches the breadcrumb's current page |\n| `KsSideBar` / `KsSideBarSection` / `KsSideBarItem` | Left sidebar shell (header / scrollable body / footer slots), section with title, and styled link primitive with icon, active and locked states |\n| `KsBreadcrumb` / `KsBreadcrumbItem` | Breadcrumb navigation. An item's `siblings` loader (or `children` on the first item, and the `titleSiblings` prop for the current page) adds a chevron whose hover menu lists that level, headed by the level above (its `scope` when the label is not the right name for its content); an entry with a `children` loader flies out its own content to the right, as deep as the tree goes. Level loaders run as soon as the bar renders and an empty result shows no chevron; fly-outs load on hover. Namespace and flow pages build theirs with `useNamespaceBreadcrumb` |\n| `KsSteps` / `KsStep` | Step / wizard progress indicator |\n\n## Utilities (import from the design system)\n\n- `State`, `STATES`, `LOG_LEVELS` — execution state constants and icons. A state's colour is not a property of the state: read it with `State.getStateColor(name)`, which resolves `--ks-status-*`\n- `cssVar(name, opacity?)` — read a `--ks-*` CSS custom property at runtime (use this in JS / chart configs instead of hardcoding hex)\n- `dayjs` — the one configured dayjs instance (utc, timezone, duration, advancedFormat, calendar, isoWeek, localizedFormat, minMax, relativeTime, weekOfYear, isSameOrBefore). Never `import dayjs from \"dayjs\"` in feature code: plugins are registered on this instance, so a bare import silently lacks them\n- `dateUtils` — `dateFilter()`, `parseIso()`, `toIsoKeepOffset()`, `currentTimezone()`, `timezonesWithOffset()`, `currentLocale()`, `setLocale()`, `DATE_FORMAT_STORAGE_KEY`, `TIMEZONE_STORAGE_KEY`\n- `durationUtils` — `duration()`, `isValidDuration()`, `humanDuration()` — ISO 8601 ↔ ms and human-readable\n- `stringUtils` — `afterLastDot()`\n- `escapeHtml()`, `cloneDeep()`, `deepMerge()`, `isDeepEqual()`, `isPlainObject()`, `getPath()`, `setPath()`, `groupBy()`, `mapValues()`, `debounce()`, `throttle()` — the general-purpose helpers that replaced lodash. `debounce`/`throttle` return a function carrying `cancel()` and `flush()`. Do not add `lodash` back for any of these\n- `fileUtils` — `isFileUri()`, `fileName()`, `fileExtension()`, `fileIcon()` — storage-URI detection and the file symbol used by `KsFileTag`\n- `flowYamlUtils` — YAML parsing / manipulation for flow definitions\n- `Comparators` — enum of filter comparison operators\n- Filter helpers — `decodeSearchParams()`, `encodeFiltersToQuery()`, `getUniqueFilters()`, etc.\n- `applyDefaultFilters()`, `useRouteFilterPolicy()` — filter composables\n- `designSystemLocale`, `setDesignSystemLocale`, `registerDesignSystemI18n` — i18n\n- `designSystemI18nReady()` — the locale registration the plugin's `install` started, to await instead of leaving it in flight (the unit setup awaits it after each test)\n\n## Composables\n\n- `useTheme()` — detects and tracks dark / light mode via MutationObserver. Use this instead of reading `document.documentElement` yourself.\n- `useFilters`, `useSavedFilters`, `useDefaultFilter`, `usePreAppliedFilters`, `useRouteFilterPolicy`, `useTableColumns`, `useDataOptions`, `useDragAndDrop`, `usePeriodicRefresh` — data-table filter composables\n- `useDiscardGuard(isDirty, {message?})` — confirm-before-discard behind the `dirty` prop of `KsDialog` / `KsDrawer`; see \"Unsaved input in modals (discard guard)\"\n- `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.\n\n## Design tokens\n\nTokens are CSS custom properties declared in [`ks-theme-light.css`](packages/design-system/src/assets/styles/ks-the","ui/packages/topology/tsconfig.json":"{\n    \"compilerOptions\": {\n        \"target\": \"esnext\",\n        \"lib\": [\"es2023\", \"DOM\", \"DOM.Iterable\"],\n        \"moduleDetection\": \"force\",\n        \"module\": \"preserve\",\n        \"moduleResolution\": \"bundler\",\n        \"resolveJsonModule\": true,\n        \"types\": [\"node\"],\n        \"strict\": true,\n        \"noUnusedLocals\": true,\n        \"declaration\": true,\n        \"emitDeclarationOnly\": true,\n        \"esModuleInterop\": true,\n        \"isolatedModules\": true,\n        \"verbatimModuleSyntax\": true,\n        \"skipLibCheck\": true,\n        \"allowImportingTsExtensions\": true\n    },\n    \"include\": [\"src\", \"tsdown.config.ts\"],\n    \"exclude\": [\"src/**/*.test.ts\"]\n}\n","ui/packages/kestra-sdk/tsconfig.json":"{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"lib\": [\"es2023\", \"dom\"],\n    \"moduleDetection\": \"force\",\n    \"module\": \"preserve\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"esModuleInterop\": true,\n    \"isolatedModules\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src\"],\n  \"references\": [\n    { \"path\": \"./tsconfig.node.json\" }\n  ]\n}\n","ui/packages/kestra-sdk/package.json":"{\n  \"name\": \"@kestra-io/kestra-sdk\",\n  \"version\": \"2.0.0\",\n  \"private\": true,\n  \"description\": \"JS/TS client generated from the OSS OpenAPI spec (io.kestra:webserver), in-repo, on the same commit as the backend it describes. The generated code under src/openapi is committed to git. Keeps the client's name so plugins import the same specifier the app does (auth + routing). See README.md.\",\n  \"license\": \"Apache-2.0\",\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./dist/index.js\",\n    \"./ai\": \"./dist/ai.js\",\n    \"./all\": \"./dist/all.js\",\n    \"./blueprint-tags\": \"./dist/blueprint-tags.js\",\n    \"./blueprints\": \"./dist/blueprints.js\",\n    \"./client\": \"./dist/client.js\",\n    \"./dashboards\": \"./dist/dashboards.js\",\n    \"./default\": \"./dist/default.js\",\n    \"./default-admin\": \"./dist/default-admin.js\",\n    \"./executions\": \"./dist/executions.js\",\n    \"./expressions\": \"./dist/expressions.js\",\n    \"./files\": \"./dist/files.js\",\n    \"./flows\": \"./dist/flows.js\",\n    \"./kv\": \"./dist/kv.js\",\n    \"./logs\": \"./dist/logs.js\",\n    \"./mcp\": \"./dist/mcp.js\",\n    \"./metrics\": \"./dist/metrics.js\",\n    \"./misc\": \"./dist/misc.js\",\n    \"./namespaces\": \"./dist/namespaces.js\",\n    \"./notifications\": \"./dist/notifications.js\",\n    \"./outputs\": \"./dist/outputs.js\",\n    \"./plugins\": \"./dist/plugins.js\",\n    \"./secrets\": \"./dist/secrets.js\",\n    \"./services\": \"./dist/services.js\",\n    \"./shared\": \"./dist/shared.js\",\n    \"./triggers\": \"./dist/triggers.js\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"generate:openapi\": \"openapi-ts\",\n    \"build\": \"tsdown\",\n    \"check:types\": \"tsc --noEmit -p tsconfig.json\"\n  },\n  \"devDependencies\": {\n    \"@hey-api/openapi-ts\": \"^0.99.0\",\n    \"@kestra-io/hey-api-plugin\": \"*\",\n    \"@types/node\": \"^26.6.5\",\n    \"tsdown\": \"^0.23.1\",\n    \"typescript\": \"^6\",\n    \"unrun\": \"^0.3.1\"\n  }\n}\n","ui/packages/kestra-sdk/README.md":"# @kestra-io/kestra-sdk (OSS)\n\nThe JS/TS client for the Kestra **OSS** API, generated from the backend's own OpenAPI spec and\nliving **in the same repo, on the same commit** as the backend it describes.\n\n- **Generated from:** `io.kestra:webserver` → `./gradlew :webserver:generateOpenapiSpec` → `openapi.yml`\n- **Generator:** [`@hey-api/openapi-ts`](https://heyapi.dev/) with `@hey-api/client-fetch` + the shared\n  [`@kestra-io/hey-api-plugin`](../hey-api-plugin) (tenant-aware, human-friendly wrappers).\n- **Committed:** the generated code under [`src/openapi`](src/openapi) **is checked into git**. This\n  decouples the fast (npm) build from the Gradle/backend build — `npm run dev`, `build`, and\n  `check:types` never need a Java toolchain.\n\nThe package keeps the name `@kestra-io/kestra-sdk` because that is the specifier plugins import to\nreach the app's client (auth + routing).\n\n## Entry points\n\n| Import | Contents |\n| --- | --- |\n| `@kestra-io/kestra-sdk` | `useClient` / `configureClient` / `setMockClient`, and every generated **type** |\n| `@kestra-io/kestra-sdk/<tag>` | the operations of one tag, e.g. `/flows`, `/executions` |\n| `@kestra-io/kestra-sdk/all` | every operation at once — named exports, plus the namespace as `default` |\n\nThe root entry exports **no operations**: re-exporting the generated operations there put all of them\nin the app's initial graph. Reach an operation through its tag, or through `/all` if one import is\npreferable to several.\n\n## Regenerating the SDK\n\nOnly needed when the OSS API changes. From `ui/`:\n\n```bash\nnpm run generate:sdk\n```\n\nThat is the **only** path that invokes Gradle. It: generates `openapi.yml` (Gradle) → hashes it and\ncompares against the `OPENAPI_SPEC_HASH` already committed in `src/openapi/sdk/shared.gen.ts` → if\nthey match, the committed SDK is already correct for the current spec and the rest is skipped; if\nthey differ, builds the shared plugin → runs `openapi-ts` (generate + convert + hash-stamp) →\nbundles `dist/`. This makes `generate:sdk` cheap to run speculatively (e.g. after any backend change)\nsince a no-op spec diff short-circuits before the expensive steps. Commit the resulting\n`src/openapi/` changes (and the regenerated `package.json` `exports` map) when it does regenerate.\n\nEveryday commands never regenerate: `ui/scripts/ensure-sdk.mjs` (the `predev` / `prebuild` /\n`precheck:types` hook) only bundles the already-committed `src/openapi` into `dist/` when `dist/` is\nmissing.\n\n## Drift detection\n\nThe generated SDK exports `OPENAPI_SPEC_HASH` (`sha256(openapi.yml)[:16]`, stamped by the shared\nplugin at generation time — no external bin). Drift is caught **at dev time, not in CI**: on the\nfirst `configureClient` call in a dev build, `dev-freshness.ts` fetches the backend's live spec,\nhashes it the same way, and warns if the committed SDK is behind. The check is guarded by\n`import.meta.env.DEV` + a dynamic import, so it is tree-shaken out of production builds entirely.\n\n## Runtime\n\n`src/index.ts` binds the shared `createConfigureClient(client, formDataBodySerializer)` from\n`@kestra-io/hey-api-plugin/runtime` (bundled into `dist/`, not a runtime dependency) and keeps the\napp-only `useClient()` / `setMockClient()` — an axios-like facade over fetch that shares the same\ninterceptors, so existing `useClient().get/post(...)` call sites are unchanged. The EE SDK reuses\nthese by relative import.\n","ui/packages/kestra-sdk/tsconfig.node.json":"{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"lib\": [\"es2023\"],\n    \"moduleDetection\": \"force\",\n    \"module\": \"preserve\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"types\": [\"node\"],\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"esModuleInterop\": true,\n    \"isolatedModules\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"composite\": true\n  },\n  \"include\": [\"tsdown.config.ts\", \"openapi-ts.config.ts\"]\n}\n","ui/packages/slot-contracts/tsconfig.json":"{\n    \"compilerOptions\": {\n        \"target\": \"esnext\",\n        \"lib\": [\"es2023\", \"DOM\"],\n        \"moduleDetection\": \"force\",\n        \"module\": \"preserve\",\n        \"moduleResolution\": \"bundler\",\n        \"resolveJsonModule\": true,\n        \"types\": [\"node\"],\n        \"strict\": true,\n        \"noUnusedLocals\": true,\n        \"declaration\": true,\n        \"emitDeclarationOnly\": true,\n        \"esModuleInterop\": true,\n        \"isolatedModules\": true,\n        \"verbatimModuleSyntax\": true,\n        \"skipLibCheck\": true,\n        \"allowImportingTsExtensions\": true\n    },\n    \"include\": [\"src\", \"tsdown.config.ts\"]\n}\n","ui/packages/hey-api-plugin/package.json":"{\n  \"name\": \"@kestra-io/hey-api-plugin\",\n  \"version\": \"0.4.0\",\n  \"description\": \"Kestra's shared SDK tooling. Two entry points: the '.' entry is the @hey-api/openapi-ts generator plugin (generation-time) that turns a Kestra OpenAPI spec into tenant-aware, human-friendly SDK wrappers; the './runtime' entry is createConfigureClient — the universal fetch-based client setup every Kestra SDK ships. Single source of truth used by the OSS UI SDK, the EE UI SDK, and the client-sdk repo.\",\n  \"license\": \"Apache-2.0\",\n  \"type\": \"module\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.mts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.mts\",\n      \"default\": \"./dist/index.mjs\"\n    },\n    \"./runtime\": {\n      \"types\": \"./dist/runtime.d.ts\",\n      \"default\": \"./dist/runtime.js\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"tsdown\",\n    \"prepack\": \"tsdown\",\n    \"check:types\": \"tsc --noEmit -p tsconfig.json\"\n  },\n  \"peerDependencies\": {\n    \"@hey-api/openapi-ts\": \">=0.97.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"@hey-api/openapi-ts\": {\n      \"optional\": true\n    }\n  },\n  \"devDependencies\": {\n    \"@hey-api/openapi-ts\": \"^0.99.0\",\n    \"@types/node\": \"^26.6.5\",\n    \"tsdown\": \"^0.23.1\",\n    \"typescript\": \"^6\",\n    \"unrun\": \"^0.3.1\"\n  }\n}\n","ui/packages/hey-api-plugin/README.md":"# hey-api-plugin Documentation\n\nPath: `ui/packages/hey-api-plugin/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/hey-api-plugin/README.md)","ui/packages/hey-api-plugin/tsconfig.json":"# Hey-api-plugin Skill\n\nPath: `ui/packages/hey-api-plugin/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/hey-api-plugin/tsconfig.json)","ui/packages/design-system/package.json":"# Design-system Skill\n\nPath: `ui/packages/design-system/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/package.json)","ui/packages/design-system/tsconfig.app.json":"# Design-system Skill\n\nPath: `ui/packages/design-system/tsconfig.app.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.app.json)","ui/packages/design-system/tsconfig.json":"# Design-system Skill\n\nPath: `ui/packages/design-system/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.json)","ui/packages/design-system/tsconfig.node.json":"# Design-system Skill\n\nPath: `ui/packages/design-system/tsconfig.node.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.node.json)","ui/packages/design-system/tsconfig.storybook.json":"# Design-system Skill\n\nPath: `ui/packages/design-system/tsconfig.storybook.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.storybook.json)","ui/packages/design-system/web-types.json":"# Design-system Skill\n\nPath: `ui/packages/design-system/web-types.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/web-types.json)","ui/packages/design-system/scripts/Figma.json":"# Scripts Skill\n\nPath: `ui/packages/design-system/scripts/Figma.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/scripts/Figma.json)","ui/packages/design-system/tests/storybook/Basic/Color-variables.json":"# Basic Skill\n\nPath: `ui/packages/design-system/tests/storybook/Basic/Color-variables.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tests/storybook/Basic/Color-variables.json)"},"files":{"AGENTS.md":"# Coding Agent Guidelines for Kestra Open Source Edition\n\nThis document provides essential information for AI coding agents working on the Kestra codebase.\n\n**IMPORTANT — READ FIRST**\n\n- **Act as a Senior Software Engineer and Software Architect.** Approach software development with:\n    - **Pragmatism**: Favor simple solutions over clever ones\n    - **Skepticism**: Question decisions that could cause technical debt or scalability issues\n    - **Efficiency**: Only challenge when it genuinely matters\n- **Think before coding**: explicitly state assumptions, compare alternatives, and justify choices.\n- **Simplicity first (KISS)**: overengineering and \"gas factories\" are strictly forbidden.\n- **Surgical changes only**: touch **only** what is strictly necessary to achieve the goal.\n- **Goal-driven execution**: define what success looks like *before* writing the first line of code.\n- **Reuse before writing**: the pattern you need almost always exists already. Find the closest equivalent in the codebase and follow it instead of inventing a second way to do the same thing.\n- **No comments by default**: see [Comments](#comments). Over-commenting is one of the most common reasons a PR written by an agent has to be revised.\n- **How Kestra looks is decided by the design system**: see [UI Design System](#ui-design-system). No color, spacing, radius or component pattern may be invented in feature code.\n- **A test has to be able to fail for a reason a reviewer would care about**: see [When a test earns its place](#when-a-test-earns-its-place). More tests do not make a change safer.\n- **Build and test only what you touched** rather than the whole repo: see [What to run after a change](#what-to-run-after-a-change).\n\n## Comments\n\n**The default is no comment.** Small methods with accurate names need no explanation, and every comment is a second thing that has to be kept in sync with the code. Over-commenting is easy to slip into: the next line gets narrated, the method name gets restated above the method, and `// Step 1` / `// Step 2` scaffolding is left behind. None of that will be accepted in review.\n\nThe reader to write for is the developer opening the file in a month. They do not have time to read a paragraph, and most of what a paragraph would tell them is already in the code.\n\nWrite a comment only when a reviewer would otherwise have to ask **why** the code exists:\n\n- a non-obvious constraint, or an upstream bug being worked around (link the issue)\n- an ordering, locking or concurrency requirement that the code itself cannot express\n- a deliberate omission, so that the next person does not \"fix\" it back\n\nKeep it to **one sentence**. When a paragraph seems necessary, make the code clearer instead.\n\nNever write:\n\n- a restatement of the line below it (`// increment the counter`, `// return the result`)\n- a narration of a self-explanatory getter, loop, or well-named call\n- section banners (`// ---- helpers ----`), changelog notes, or a `// TODO` with no issue link\n- a comment on every entry of a list when the rule is already stated once above the list\n\n**Existing comments** should be left alone unless the code they describe changed. When that code is deleted, delete the comment with it rather than rewording it around the gap. When the code changed, either correct the comment or remove it.\n\n**Javadoc** describes the contract rather than the body. Add it on public API called from other modules or from plugins, and on anything whose behavior cannot be seen from the signature (nullability, thread-safety, side effects, units, ownership of a returned collection). Skip it on getters, setters, builders, an override that adds nothing (a bare `{@inheritDoc}` is noise), and any method whose name already says everything.\n\n## Project\n\nMonorepo built with Java (backend) and Vue (frontend), using Gradle as the build system.\n\n## Tech Stack\n- **Backend:** Java 25, Micronaut Framework, Lombok\n- **Frontend:** Vue 3, TypeScript, Vite, Element Plus, Pinia\n- **Build:** Gradle, multi-project. The version is pinned in `gradle/wrapper/gradle-wrapper.properties`, and `settings.gradle` is the list of modules\n- **Testing:** JUnit 5, Mockito, AssertJ, Vitest, Playwright\n\n## Critical Code Patterns\n\n### Dependency Injection\n\n**DO**: Use constructor injection with final fields.\n\n```java\n@Singleton\npublic class MyService {\n    private final SomeDependency dependency;\n\n    @Inject\n    public MyService(SomeDependency dependency) {\n        this.dependency = Objects.requireNonNull(dependency);\n    }\n}\n```\n\n**DON'T**: Use field injection (`@Inject` on fields directly). Always prefer constructor injection.\n\n### Class Structure\n\n```java\n// 1. Package declaration and imports\n// 2. Class-level annotations (@Slf4j, @Singleton, etc.)\n// 3. Class declaration with Javadoc\n// 4. Static constants (UPPER_SNAKE_CASE)\n// 5. Injected fields (@Inject)\n// 6. Constructors\n// 7. Public methods\n// 8. Protected methods\n// 9. Private methods\n// 10. Inner classes/records\n```\n\n### Annotations\n- **Micronaut:** `@Singleton`, `@Inject`, `@Controller`, `@Replaces`, `@Requires`\n- **Validation:** `@Valid`, `@NotNull`, `@Nullable`\n- **Lombok:** `@Slf4j`, `@Getter`, `@NoArgsConstructor`, `@AllArgsConstructor`\n- Use `@Builder` for complex object creation\n\n### Error Handling\n\n**DO**:\n- Use specific exception types — extend `KestraException` or `KestraRuntimeException`\n- Use `Optional<T>` for potentially absent returned values\n- Return empty collections (e.g., `List.of()`, `Collections.emptyList()`) for absent values\n- Use try-with-resources for resource management\n- Log errors before re-throwing: `log.error(\"message\", exception)`\n- 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)`\n\n**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.\n\n### Java Language Features\n- Use java records for simple data carriers\n\n### Naming Conventions\n- Follow Java naming-convention best practices for Classes, Methods, Variables, Constants.\n- Boolean methods: Start with `is`, `has`, `should`, `can` (e.g., `isReadOnly()`).\n\n### File Organization\n- Use 4-space indentation (configured in .editorconfig)\n- UTF-8 encoding with LF line endings\n- No trailing whitespace\n\n### Utility Classes\n* Mark utility classes as `final` with a private constructor\n* Use static methods only\n* Use existing utility classes (e.g., `ListUtils`, `MapUtils`) instead of creating new ones (`io.kestra.core.utils.*`)\n\n**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.\n\n### Enums\n- 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\n- Use `@JsonValue` for custom serialization if needed\n- Use `UNKNOWN` enum value for unknown cases in deserialization\n- Compare Constants From The Left (a.k.a., Yoda conditions)\n- Use a static `fromString` method for case-insensitive lookups using `Enums` class.\n\ne.g.:\n```java\npublic enum MyEnum {\n    VALUE_ONE,\n    VALUE_TWO,\n    UNKNOWN;\n\n    @JsonCreator\n    public static ResourceType fromString(final String value) {\n        return Enums.getForNameIgnoreCase(value, MyEnum.class, UNKNOWN);\n    }\n}\n```\n\n### Documentation\n\nSee [Comments](#comments) for when a Javadoc block is worth writing at all. Once you have decided that it is:\n\n- Keep it to the contract: what the caller has to know that the signature does not already say\n- Use `@param`, `@return` and `@throws` only where each adds something. `@param name the name` is noise\n- Skip a bare `{@inheritDoc}` on an override that changes nothing\n- Add a usage example only when the call site would otherwise be guessed wrong\n\n## Webserver Constraints\n- Put classes used by only controllers in the webserver module (not core)\n- No business code/rule inside controllers - instead use a Service class\n- All APIs must return a valid JSON object\n- APIs should not return a response being a JSON array which cannot be evolved in a backwards-compatible way\n- Any endpoint returning a collection should be paged through `PagedResults` with `page` and `size` parameters, like the rest of the API\n- A DTO used by a single controller belongs as an inner record of that controller. Promote it to `io.kestra.webserver.models.api` only once a second caller exists, and name it with the `Api` prefix there (`ApiExecution`, `ApiTriggerState`, `ApiPluginArtifact`)\n- Import a class rather than writing its fully-qualified name, as soon as it is used more than once in the same file\n- Unit tests must assert that a user can only access a given API if authorized to do so, and that access is denied otherwise\n- APIs must be documented with OpenAPI annotations\n- Use DTOs for requests/responses\n- A dashboard chart export must use the chart's declared columns for its header, in declaration order, whether or not there are rows\n- Always validate input parameters with `@Valid`\n- Use `@ExecuteOn(TaskExecutors.IO)` for blocking operations\n- Return meaningful error responses in controllers\n\n## Worker Constraints\n- Never depend on repositories for code called by the workers - instead use MetaStore/StateStore facades\n\n## Executor Constraints\n- Run the `H2RunnerTest` whenever you update part of the executor\n\n## Testing Guidelines\n\n### When a test earns its place\n\nMore tests do not make a change safer. Every test is code that has to be read, kept passing, and understood by whoever changes that behavior next, so a test is only worth its cost when it can fail for a reason a reviewer would care about.\n\n**Add a test when:**\n\n- new behavior is introduced, or a bug is fixed. The test should fail on the commit before yours; if it passes there, it is testing something else\n- an edge case would be easy to get wrong later: an empty collection, a null, a boundary, a race, a permission check\n- the change is in the executor, the scheduler, the queue, or an ACL path, where a regression is expensive and hard to notice\n\n**Don't add a test when:**\n\n- it only exercises getters, setters, builders, or a Lombok-generated method\n- it asserts behavior that the framework already documents (Jackson serializing a field, Micronaut injecting a bean)\n- it repeats an existing test with one property changed. One test per behavior, rather than one per property, since otherwise there would be hundreds of them\n- the mocks would have to restate the implementation for the assertions to pass. Such a test is pinned to the current code rather than to the behavior, so it is broken by every legitimate refactor\n- it exists only to move a coverage number\n\n**Delete tests too.** Removing a feature should reduce the test count rather than raise it. When a test no longer describes behavior anyone relies on, or when it duplicates another one, delete it in the same PR and say so in the description. A redundant test that is always green is still maintenance cost.\n\n### Java Tests\n\n**DO**:\n- Place tests in same package structure as source code\n- Simple unit test with mocks over complex integration tests when possible\n- Structure the body as given / when / then. The three marker comments are optional and should be dropped when the test is only a few obvious lines\n- Test method naming: `should<ExpectedBehavior>When<ConditionOrAction>` (also `...Given<Input>`, `...For<Condition>`, `...If<Condition>`), e.g. `shouldThrowExceptionWhenDividingByZero()`\n- Use `@MicronautTest` for tests that require Micronaut beans\n- Use `@KestraTest` for tests that require running Kestra services (e.g., Executor, Scheduler)\n-\n```java\n@KestraTest\nclass ServiceTest {\n    @Inject\n    private ServiceClass service;\n\n    @Test\n    void shouldPerformActionWhenCondition() {\n        // Given (setup)\n\n        // When (action)\n\n        // Then (assertions)\n        assertThat(result).isNotNull();\n    }\n}\n```\n\n**DON'T**: Use Nested classes for test organization. Avoid complex test hierarchies.\n\n**Assertions:**\n- Use AssertJ: `assertThat().isEqualTo()`, `assertThat().isNotNull()`, `assertThatThrownBy()`, `assertThatObject()`\n- Prefer descriptive assertion methods\n- Use `@MockBean` for mocking dependencies\n\n**Test Categories:**\n- Unit tests: Fast, isolated, no external dependencies\n- Integration tests: Test component interaction, use `@Tag(\"integration\")`\n- Flaky tests: Use `@Tag(\"flaky\")` for unreliable tests\n\n### Frontend Tests\n- Unit tests with Vitest and `@vue/test-utils`, next to the file they cover (`Foo.vue` beside `Foo.spec.ts` and `Foo.stories.ts`), see [ui/AGENTS.md](ui/AGENTS.md)\n- E2E tests with Playwright\n- Storybook component tests\n- Use JSdom environment for DOM testing\n- **Prefer Storybook component tests over Vitest unit tests whenever possible** — components render through their real story setup (props, slots, design-system deps) instead of being stubbed out, catching regressions unit mocks miss. Fall back to a Vitest unit test only when the logic under test isn't component-rendering behavior (e.g. a pure helper/composable) or no story exists and adding one isn't practical.\n- Don't keep a Vitest test that only re-asserts what a story already covers. Delete it when you add the story.\n- Assert on `data-test` attributes and rendered text. A test written against `.el-*` or `.ks-*` class names is broken by the next Element Plus or design-system upgrade, without any behavior having changed.\n\n## UI Design System\n\nThe design system at [ui/packages/design-system/](ui/packages/design-system/) is the **single source of truth for every design decision** in Kestra: colors, typography, spacing, radii, shadows, and the component vocabulary. Nothing rendered to a user may be styled outside it.\n\nIn practice that means no hex codes, no `rgb()`, no `--el-*` or `--bs-*` variables, no hardcoded pixel spacing, no `:deep()` into another component, and no locally built equivalent of a component that already exists. When the design you need cannot be expressed with the existing `Ks*` components and `--ks-*` tokens, that is a gap in the design system: raise it with design and add the token, the prop or the component upstream. Approximating it with custom CSS in a feature component is not an option, since the result will only be right in the one theme you checked it in.\n\nThe full rules, the component catalogue, the token reference and the frontend best practices live in [ui/AGENTS.md](ui/AGENTS.md), which is auto-loaded for work under `ui/` in OSS and `ui-ee/` in Enterprise Edition. Read it before any frontend change, and update it in the same PR whenever a component, a prop or a token is added.\n\n@ui/AGENTS.md\n\n## Frontend Code Style (Vue 3)\n\n**File Organization:**\n- Use 2-space indentation for Vue, JSON, YAML, CSS\n- Use 4-space indentation for JavaScript/TypeScript\n- Follow Vue 3 Composition API patterns\n- Organize imports: Vue/framework → third-party → local modules\n\n**Naming Conventions:**\n- Components: `PascalCase` files (e.g., `MyComponent.vue`)\n- Variables/functions: `camelCase`\n- Constants: `UPPER_SNAKE_CASE`\n- CSS classes: Follow Element Plus conventions\n\n**TypeScript:**\n- Use strict TypeScript configuration\n- Prefer type definitions over `any`\n- Use interfaces for object shapes\n- Use enums for fixed sets of values\n\n## Build Commands\n\n### What to run after a change\n\nA whole-repo `./gradlew build` can take tens of minutes, and almost nothing about a three-line change can be learned from it. Start with the narrowest command that covers what you touched, and widen it only once that one passes.\n\n| What changed | Run |\n|---|---|\n| One backend module | `./gradlew :<module>:test --tests \"TheClassName\"`, then `./gradlew :<module>:test` |\n| Anything in the executor | `./gradlew :jdbc-h2:test --tests \"H2RunnerTest\"` (mandatory) |\n| Something crossing module boundaries | `./gradlew build -x integrationTest` |\n| A Vue component or composable | `cd ui && npm run check:types && npm run test:unit && npm run lint` |\n| A design-system component | the above, plus `npm run test:storybook` |\n| An e2e spec or fixture | `cd e2e && npm run check:types && npm run test:lint` |\n| `en.json`, whether a key was added or a value edited | `cd ui && npm run translations:generate && npm run translations:check` |\n| A controller or its DTOs | that module's tests, including the authorization tests for the route |\n\n### Java Backend\n\n```bash\n# Clean build\n./gradlew clean\n\n# Full build (includes tests)\n./gradlew build\n\n# Build without tests (faster)\n./gradlew build -x test -x integrationTest -x testCodeCoverageReport --refresh-dependencies --no-daemon --parallel\n```\n\n### Test Commands\n\n```bash\n# Run all tests (excludes flaky tests)\n./gradlew test\n\n# Run only unit tests (fastest)\n./gradlew unitTest\n\n# Run integration tests\n./gradlew integrationTest\n\n# Run flaky tests (separate from build)\n./gradlew flakyTest\n\n# Run tests for specific module\n./gradlew :core:test\n\n# Run single test class\n./gradlew :module-name:test --tests \"ClassName\"\n\n# Run single test method\n./gradlew :module-name:test --tests \"ClassName.methodName\"\n\n# After running tests: generate a markdown summary of failures only\nnpx --yes @kestra-io/kestra-devtools generateTestReportSummary --only-errors $(pwd)\n```\n\n### Frontend (UI)\n\n```bash\ncd ui\n\n# Install dependencies\nnpm install\n\n# Development server\nnpm run dev\n\n# Type checking\nnpm run check:types\n\n# Build for production\nnpm run build\n\n# Run tests\nnpm run test:all        # All tests with coverage\nnpm run test:unit       # Unit tests only\nnpm run test:storybook  # Storybook tests\n\n# Linting\nnpm run lint            # Fix linting issues\nnpm run test:lint       # Check linting only\n\n# Storybook\nnpm run storybook       # Development\nnpm run build-storybook # Build\n```\n\n### End-to-end tests\n\nThe Playwright suite is its own npm package at [e2e/](e2e/), installed separately from `ui/` so that\na spec change pulls in neither the frontend dependency tree nor the frontend CI jobs.\n\n```bash\ncd e2e\nnpm ci\nnpx playwright install chromium\n\nnpm run test:e2e                          # starts a Kestra docker image, runs the suite, stops it\nnpm run test:e2e-without-starting-backend # against an instance you already have running\nnpm run check:types && npm run test:lint\n```\n\n## Development Workflow\n\n### Running Locally\n\n1. **Start/stop backends:**\n```bash\n# Start databases with Docker Compose\ndocker compose -f docker-compose-ci.yml up\n\n# Stop databases with Docker Compose\ndocker compose -f docker-compose-ci.yml down\n```\n\n2. **Access application:** http://localhost:8080\n\n### Worktree setup\n\nWhen working in an OSS worktree (the working directory is under a `worktrees/` directory):\n```bash\ndev-tools/setup-worktree.sh ../worktrees/foo\n```\nThis 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.\n\n### Security Considerations\n- Use tenant isolation for multi-tenant features\n- Implement proper authorization with `@HasAnyPermission`\n- Handle secrets securely (never log sensitive data)\n\n### Performance Best Practices\n- Implement pagination for large datasets\n- Use streaming for large file operations\n- Cache frequently accessed data appropriately\n- Initialize collections with the expected size to avoid resizing overhead\n\n## Troubleshooting\n\n- **A Gradle build fails with no visible cause:** run `./gradlew clean` and retry once. When it repeats, read the actual error instead of retrying again.\n- **Tests fail on connection errors:** the databases are not running. `docker compose -f docker-compose-ci.yml up -d`.\n- **The frontend build fails on dependency resolution:** check the Node and npm versions against `package.json` engines before touching anything else.\n- **`npm install` fails with `EBADENGINE`:** the UI workspaces require npm >= 11.7 and set `engine-strict=true`. Older npm mislabels the `sass`/`sass-embedded` platform binaries as peer dependencies and rewrites `package-lock.json` on every install. Install the pinned npm (`npm i -g npm@11.16.0`) or use the Node release named in `ui/.nvmrc`.\n- **The UI builds but the app never mounts:** module federation, so the cause is usually workspace resolution rather than the change itself. Verify against a running instance rather than against the dev server.\n- **Debug logging:** `--logging.level.io.kestra=DEBUG`.\n\n## Module Structure\n\n**Core Modules:**\n- `cli` - Command Line Interface\n- `core` - Core functionality\n- `webserver` - Web server\n- `ui` - Vue 3 frontend application\n- `executor` - The component responsible for managing execution state\n- `scheduler` - The component responsible for scheduling polling and schedule triggers\n- `worker` - The component that executes tasks and manages worker instances\n- `worker-controller` - The component that manages worker instances and job distribution\n- `indexer` - The component responsible for indexing executions\n- `platform` - provides the Platform Bill of Materials (BOM) for dependency management\n\n**Queuing Layer:**\n- `queue` - Core API for queue implementations\n- `queue-jdbc` - JDBC-based queue implementation\n\n**Data Layer:**\n- `jdbc-*` - Database implementations (H2, Postgres, MySQL)\n\n**Testing Modules:**\n- `e2e` - Playwright end-to-end suite, run against a Kestra docker image\n- `tests` - Common test utilities and base classes\n- `jmh-benchmark` - JMH benchmarks for performance testing\n\n**Key Patterns:**\n- Repository pattern for data access\n- Service layer for business logic\n- Controller layer for HTTP endpoints\n- Builder pattern for object construction (often with Lombok `@Builder`)\n\n## Pull request guidelines\n\n- **One PR, one scope.** When you cannot pick a single conventional-commit scope for the title, the PR is doing too much and should be split.\n- Title format `type(scope): lowercase description`, following https://www.conventionalcommits.org/en/v1.0.0. Name the changed things rather than the actions taken on them: `fix(cases): assignees header, missing Acknowledged status, auto-link toggle`.\n- Use types: chore, feat, fix, refactor, test, docs, build\n- 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\n- Put the closing keyword on the first line of the description, as a full URL so that the link is not lost in a squash merge: `Closes https://github.com/kestra-io/kestra/issues/<id>.`\n- Cover new behavior with tests that could fail, following [When a test earns its place](#when-a-test-earns-its-place). \"Add tests\" never means one test per changed line.\n- Run `npm run lint` in `ui/` before pushing any frontend change. Otherwise one comment per violation is posted by reviewdog, and the human review is buried under trailing-comma suggestions.\n- Attach a screenshot or a short recording for every user-visible change, taken against a running instance.\n- Keep the branch rebased on `develop` rather than merged.\n- Fill in `.github/pull_request_template.md`, and delete the checklist section that does not apply instead of leaving it unticked.\n- Write the description as problem, fix and evidence, once each. No table restating the diff, and no checklist of tests that all passed.\n- Never commit generated output by hand: the twelve non-English translation files, `fingerprints.json`, and the generated SDK are produced by their scripts or by CI.\n\n## Issue guidelines\n- **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`).\n- **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.\n- Leave triage labels such as `kind/cooldown` to `kestrabot`; it applies them automatically on new issues.\n\n## UI Translations\n\n**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. When the string embeds markup or a component (`<code>`, `router-link`, `KsId`), render it with `<i18n-t>` (vue-i18n's component interpolation) and one named slot per placeholder, keeping the markup out of the translation instead of rendering it with `v-html`. 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.\n\nTranslation 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`.\n\n### Checking for missing translations\n\nRun the check script from the `ui/` directory:\n\n```bash\ncd ui && npm run translations:check\n```\n\nA clean run reports `No missing keys.`, `No extra keys.` and `No stale keys.` for every language. Anything listed must be fixed before merging — the same check runs as a PR gate.\n\n> **Enterprise Edition:** EE-only keys live in `ui-ee/src/translations/ee_translations/en.json`, and a tenant type's own keys in `ui-ee/src/tenantTypes/<type>/translations/`, unprefixed and rooted under `tenantTypes.<type>` at runtime. Both are checked separately — run `npm run translations:check` in `ui-ee` as well (see `kestra-ee/AGENTS.md` → \"Frontend i18n\").\n\n### Editing English strings\n\n**Changing an existing English value is a translation change.** Every key carries a fingerprint of the English text its translations were generated from, so editing `en.json` — even just the capitalisation — marks that key stale in all twelve languages and fails `translations:check` until it is regenerated. Run `npm run translations:generate` and commit the result alongside your change.\n\nThis is deliberate: before it existed, edited values were never propagated, and a rename of \"SuperAdmin\" to \"Superadmin\" sat un-translated in eleven locales for a year (kestra-io/kestra#10656).\n\n### Adding or regenerating translations\n\nPrefer `npm run translations:generate` (needs `GOOGLE_CLOUD_PROJECT` and Application Default Credentials, since Gemini is reached through Vertex AI: run `gcloud auth application-default login` once); it fills missing keys and re-translates stale ones on its own, with no flag to remember. Pass `true` to force a full re-translation of everything.\n\nIf you must write a translation by hand:\n\n1. Identify gaps by running `npm run translations:check`.\n2. Follow these translation rules (mirroring `ui/scripts/translations/generateTranslations.ts`, the generator shared by OSS and EE):\n   - **Reserved English terms — never translate:** `kv store`, `namespace`, `tenant`, `flow`, `subflow`, `task`, `log`, `blueprint`, `id`, `trigger`, `label`, `key`, `value`, `input`, `output`, `port`, `worker`, `backfill`, `healthcheck`, `artifact`, `min`, `max`.\n   - **ALL-CAPS status labels stay in English:** `WARNING`, `FAILED`, `SUCCESS`, `PAUSED`, `RUNNING`, etc.\n   - **Preserve `{placeholder}` variables** exactly — vue-i18n uses a **single** pair of braces. Do not translate the name inside the braces, do not rename it, and never write `{{placeholder}}`: double braces are a compile error (`Not allowed nest placeholder`) and make `t()` throw at render time. Each translation must carry exactly the same placeholders as the English source — no invented ones, none dropped.\n   - **Use natural UI terminology** — avoid false friends or overly literal translations (e.g. German: Execution → Ausführung, Theme → Modus, State → Zustand).\n3. Insert the translated keys into the correct position in the target language JSON, mirroring the key order of `en.json`.\n4. Re-run `npm run translations:check` to confirm everything is clean before committing.\n\nThe tooling itself lives in `ui/scripts/translations/` and is shared with EE, which keeps only thin entry points. Rules live in `.mjs` so the dependency-free PR gate can apply them; file IO and orchestration stay in `.ts`.\n\n### Conflicts in `fingerprints.json`\n\nTwo branches that both touch `en.json` will both regenerate `ui/scripts/translations/fingerprints.json`, so it conflicts often. **Never hand-merge the hashes and never pick a side** — a hash says \"this English text is what the twelve translations were generated from\", so choosing the wrong one silently marks a drifted key as current and the drift becomes invisible again.\n\nResolve it the same way as a `kestra-sdk` conflict — regenerate:\n\n```bash\ngit checkout --ours ui/src/translations/*.json ui/scripts/translations/fingerprints*.json\ncd ui && npm run translations:generate   # fills whatever the other branch added\nnpm run translations:check               # must report no missing / extra / stale keys\n```\n\n`en.json` itself normally merges cleanly, since branches usually add different keys; it is the generated files that collide.\n\n## Keeping this file up to date\n\nWhen a review comment corrects something that is not written here, and the correction is a rule rather than a one-off, add it in the same PR: one or two lines, with the example next to the rule. Delete a rule once it no longer matches the codebase. A guideline that no longer matches the code is worse than no guideline, since it will be followed confidently.\n\nWhen in doubt, follow the existing patterns in the codebase and stay consistent with them.\n","ui/packages/slot-contracts/package.json":"{\n    \"name\": \"@kestra-io/slot-contracts\",\n    \"version\": \"0.0.0-dev\",\n    \"description\": \"Kestra slot prop contracts – shared type definitions for federated plugin UI slots\",\n    \"type\": \"module\",\n    \"private\": true,\n    \"homepage\": \"https://github.com/kestra-io/kestra#readme\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"git+https://github.com/kestra-io/kestra.git\"\n    },\n    \"bugs\": {\n        \"url\": \"https://github.com/kestra-io/kestra/issues\"\n    },\n    \"exports\": {\n        \".\": \"./src/index.ts\"\n    },\n    \"main\": \"./dist/index.js\",\n    \"types\": \"./dist/index.d.ts\",\n    \"files\": [\n        \"dist\"\n    ],\n    \"scripts\": {\n        \"build\": \"tsdown\",\n        \"dev\": \"tsdown --watch\",\n        \"check:types\": \"vue-tsc --noEmit\",\n        \"prepublishOnly\": \"npm run build\"\n    },\n    \"peerDependencies\": {\n        \"@kestra-io/kestra-sdk\": \"*\"\n    },\n    \"devDependencies\": {\n        \"tsdown\": \"^0.23.1\",\n        \"ts-morph\": \"^28.0.0\",\n        \"unrun\": \"^0.3.1\"\n    },\n    \"dependencies\": {\n        \"zod\": \"^4.6.7\"\n    }\n}\n","ui/packages/topology/package.json":"{\n    \"name\": \"@kestra-io/topology\",\n    \"version\": \"0.0.0-dev\",\n    \"description\": \"Kestra flow topology graph component – powered by Vue Flow\",\n    \"type\": \"module\",\n    \"homepage\": \"https://github.com/kestra-io/kestra#readme\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"git+https://github.com/kestra-io/kestra.git\"\n    },\n    \"bugs\": {\n        \"url\": \"https://github.com/kestra-io/kestra/issues\"\n    },\n    \"exports\": {\n        \".\": \"./src/index.ts\",\n        \"./vue-flow-utils\": \"./src/vue-flow-utils.ts\",\n        \"./flow-yaml-utils\": \"./src/utils/flowYamlUtils.ts\"\n    },\n    \"main\": \"./dist/index.js\",\n    \"types\": \"./dist/index.d.ts\",\n    \"files\": [\n        \"dist\"\n    ],\n    \"sideEffects\": [\n        \"**/*.css\",\n        \"./dist/style.css\",\n        \"./src/index.ts\"\n    ],\n    \"publishConfig\": {\n        \"access\": \"public\"\n    },\n    \"scripts\": {\n        \"build\": \"tsdown\",\n        \"dev\": \"tsdown --watch\",\n        \"check:types\": \"vue-tsc --noEmit\",\n        \"test\": \"npm run test:unit\",\n        \"test:unit\": \"vitest run --config vitest.unit.config.ts\",\n        \"test:unit:watch\": \"vitest --config vitest.unit.config.ts\",\n        \"lint\": \"eslint --fix\",\n        \"test:lint\": \"eslint\",\n        \"prepublishOnly\": \"npm run build\"\n    },\n    \"dependencies\": {\n        \"dagre\": \"^0.8.5\",\n        \"html-to-image\": \"^1.11.13\",\n        \"js-yaml\": \"^5.5.0\",\n        \"vue-material-design-icons\": \"^5.4.0\",\n        \"yaml\": \"^2\"\n    },\n    \"peerDependencies\": {\n        \"@kestra-io/design-system\": \"*\",\n        \"@vue-flow/background\": \"^1.3.2\",\n        \"@vue-flow/controls\": \"^1.1.3\",\n        \"@vue-flow/core\": \"^1.48.2\",\n        \"vue\": \"^3.5.0\",\n        \"vue-i18n\": \"^11.4.4\"\n    },\n    \"peerDependenciesMeta\": {\n        \"vue-i18n\": {\n            \"optional\": true\n        }\n    },\n    \"devDependencies\": {\n        \"@tsdown/css\": \"^0.23.1\",\n        \"@types/dagre\": \"^0.7.54\",\n        \"@types/js-yaml\": \"^4.0.9\",\n        \"@types/node\": \"^26.6.5\",\n        \"@vitejs/plugin-vue\": \"^6.0.10\",\n        \"sass-embedded\": \"^1.105.2\",\n        \"tsdown\": \"^0.23.1\",\n        \"typescript\": \"^6\",\n        \"unrun\": \"^0.3.1\",\n        \"vite\": \"^8.3.4\",\n        \"vitest\": \"^5.0.5\",\n        \"vue\": \"^3.5.44\",\n        \"vue-i18n\": \"^11.4.16\",\n        \"vue-tsc\": \"^3.3.13\"\n    }\n}\n","ui/AGENTS.md":"# UI Design System Guidelines\n\nScope: 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.\n\nThe 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.\n\n## What this is, in plain terms\n\nThink of the design system as the product's **visual vocabulary**:\n\n- A short list of agreed-upon **colors**, **fonts**, and **spacings** (called *design tokens*).\n- A library of pre-built **components** (`KsButton`, `KsTable`, `KsDialog`, …) that already use those tokens.\n- 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.\n\nIf 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.\n\nUnder 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/`.\n\n> **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.\n\n## Golden rules (non-negotiable)\n\nThese rules are what keep the UI maintainable as it grows. Treat any deviation as a bug.\n\n**How Kestra looks is decided by the design system; what it does is decided by feature code.** A new visual pattern (a new card treatment, a different spacing rhythm, another status color, a bespoke empty state, a one-off modal layout) counts as a design-system change and belongs in `ui/packages/design-system/`, after a conversation with design. Approximating one inside a feature component is the fastest route to a screen that is fine in the one theme you checked it in and wrong everywhere else. When nothing in the catalogue below covers what you need, raise it as a gap in the design system instead of improvising around it.\n\n1. **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.\n2. **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.css` / `ks-theme-dark.css` / `ks-theme-dark-2.css` — do not pick a one-off color.\n3. **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 `--ks-font-family-*` and `--ks-font-size-*` tokens; feature code should not redefine them.\n4. **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.\n5. **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.\n6. **No magic numbers for theme values.** Spacing, radii, font sizes, and shadows must reference tokens — 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.css`](packages/design-system/src/assets/styles/ks-tokens.css)). 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)`).\n7. **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.\n8. **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.\n9. **Every new `Ks*` component needs a Storybook story and a unit test.** Stories double as living documentation for design and product reviewers.\n10. **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`.\n11. **Check that a token exists before using it.** With an invalid `var(--ks-…)` and no fallback, the property is silently inherited instead, so the mistake remains invisible until the computed style is measured. The cases feature was released using `--ks-font-size-medium`, `--ks-font-size-small`, `--ks-radius-2` and `--ks-border-active`, none of which are declared anywhere, and the interval filter's Apply-to row shipped with no visible selection for the same reason (kestra-io/kestra#18777).\n\n    Your editor underlines it: `kestra-tokens/no-undeclared-css-token` (eslint, `@eslint/css`) covers `.css` and plain `<style>` blocks, `kestra-tokens/no-undeclared-ks-token` (eslint) covers `cssVar(\"--ks-…\")` and tokens written inside strings, and `ks/no-undeclared-custom-property` (stylelint) covers the design system's remaining SCSS. All three propose a replacement, and `npm run lint` runs them all. See [scripts/tokens/README.md](scripts/tokens/README.md).\n12. **Copying an existing rule is not proof that it is correct.** Several hundred `:deep()` selectors, some hex codes and some raw pixel values are older than these rules and are being removed over time. Treat them as debt rather than as precedent: don't add more, and clean up the ones in the component you are already editing.\n13. **Bootstrap is gone; don't use its utility classes.** The `d-flex`, `mb-3` and similar classes still in the templates are kept alive by `packages/design-system/src/assets/styles/_utilities.css` until their migration issues land, and that file only shrinks: replace a utility with scoped CSS on `--ks-*` tokens and delete its rule once the last use is gone. `tests/unit/designSystem/bootstrapGuard.spec.ts` fails on a utility the file does not define and on any `--kbs-*` variable.\n\n## Best practices for keeping the design system healthy\n\nA 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.\n\n### Before you write code\n\n- Search the component tables and Storybook first. The most common waste in this codebase is rebuilding something that already exists.\n- 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.\"\n- For anything visible to a user, check both light and dark mode in Storybook before merging.\n\n### While you write code\n\n- 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.\n- Keep the style **inside** the SFC. `<style scoped src=\"./x.css\">` is valid and `scoped` still applies, but an external file separates the CSS from the markup it describes for no gain, and it is one more file to open. Block order is `template`, `script`, `style`, enforced by `vue/block-order` in `ui/eslint.config.js`.\n- Write styles as plain CSS: no `lang=\"scss\"` and no new `.scss` file outside `packages/design-system/` (kestra-io/kestra#21346). Native nesting works with `scoped`, but start a nested selector with `&` (`& :deep(.x)`, `& > *`), or Vue puts `[data-v-*]` on the nested part instead of the parent.\n- 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.\n- 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.\n- 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 }`).\n- Use semantic tokens, not raw colors. `var(--ks-text-link)` communicates intent; `var(--ks-text-blue-500)` does not exist for a reason.\n- Co-locate component-specific tokens (e.g. `--ks-card-shadow`) in the component's SCSS, but always derive them from semantic tokens.\n\n### When extending the design system\n\n- Component styles are plain CSS. A component that needs Element Plus's base styles keeps them in a `<style lang=\"scss\">` block holding only the `el-ns` and `theme-chalk` `@use` lines, placed before the plain `<style>` with its own rules, so each published component entry still ships its chalk CSS. Palette colours are `var(--ks-<family>-<step>)`, not `palette.$base-*`.\n- Don't write one rule per variant. When the tokens follow the variant's name (`--ks-status-<status>`), bind them with `v-bind()` and add a test that every name has its token, since the token lint cannot see a built name (`KsExecutionStatus`, `KsAlert`). When they don't, keep a typed table of `var(--ks-*)` strings in the component's `<script>`, where `kestra-tokens/no-undeclared-ks-token` still checks them, and bind that (`KsTag`, `KsButton`).\n- Only expose props that are actually used somewhere in the codebase. Speculative props rot.\n- Mirror Element Plus prop names where possible — predictability is a feature.\n- Pass `v-bind=\"$attrs\"` and forward slots so wrappers don't trap consumer extension points.\n- 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.\n- Document tokens in code comments next to where they're declared in `ks-theme-*.css`. The `scripts/generate-palette.mjs` file is auto-generated — don't hand-edit it.\n\n### When reviewing a UI PR\n\nReject (or ask to fix) anything that:\n\n- Imports from `element-plus` directly into `ui/src/`.\n- Uses a hex code, `rgb(...)`, `--el-*`, or `--bs-*` for color.\n- Uses `:deep()` to reach into a `Ks*` or `el-*` component.\n- Hardcodes pixel values for padding, margin, radii, font sizes, or shadows.\n- Adds a CSS class that overrides `.el-...` selectors.\n- Duplicates a component that already exists in the design system.\n- Adds a `Ks*` component without a Storybook story or test.\n- Mounts `KsDataTable` without binding `:currentPage` / `:pageSize` (or `v-model:currentPage` / `v-model:pageSize`) — pagination is controlled; see \"Data tables & pagination state\".\n- 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\".\n- Adds a modal/drawer where the user enters data without guarding accidental dismissal — see \"Unsaved input in modals (discard guard)\".\n\n### Accessibility\n\n- Every icon-only `KsIconButton` must have an accessible label (`aria-label` or `title`). Screen readers do not see the icon glyph.\n- Never convey state with color alone — pair status colors with an icon (`KsExecutionStatus` already does this) or a text label.\n- Use semantic HTML inside slots: real `<button>`, `<a>`, `<label>`, headings in document order. Don't fake interactivity with `<div @click>`.\n- `KsDialog`, `KsDrawer`, `KsPopover` already manage focus trap and `Escape`-to-close — don't reimplement these in feature code.\n- Keep tab order logical; rely on the DOM order rather than `tabindex` hacks.\n- 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.\n\n### Internationalization\n\n- No hardcoded user-facing strings. Always go through i18n.\n- **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.\n- Pass plurals and interpolation as named arguments to `$t(...)`, never string-concatenate.\n- A string that embeds markup or a component (`<code>`, `router-link`, `KsId`) renders through `<i18n-t keypath=\"...\" tag=\"span\" scope=\"global\">` (vue-i18n's component interpolation, registered globally, so there is nothing to import; `scope=\"global\"` names the scope every message lives in, or the component looks for a local one up the tree and warns; `kestra-i18n/i18n-t-scope` fails a tag without it and `npm run lint` adds it), with one named slot per placeholder: `Mark as {status}` becomes `<template #status><code>{{ item.code }}</code></template>`. Keep the markup out of the translation, so no `v-html` and no `escapeHtml` are needed, and translators can reorder the placeholders.\n- `splitTranslation(t, key, slot)` and markup passed as a named argument to `v-html` are older patterns for the same job. Don't add new uses; move a string to `<i18n-t>` when you touch it.\n- 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. `tests/unit/dateFormatGuard.spec.ts` (and its `ui-ee` twin) fails on `dayjs(...).format(`, `toLocaleString()` on a `Date`, and `Intl.DateTimeFormat` in `src/`; a file that serializes a date rather than displaying it goes in its `ALLOWED` list with the reason.\n- Date arithmetic goes through the `dayjs` re-exported by the design system. `dayjs` objects are **immutable**, so `d.add(1, \"day\")` returns a new instance and mutating idioms (`cursor.add(...)` in a loop) silently no-op — reassign instead. Parse untrusted input with `dateUtils.parseIso()`: bare `dayjs()` happily reads an epoch number or a bare year as a date.\n- 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.\n\n### Loading, empty, and error states\n\nEvery async surface must render all four states. \"Happy path only\" is a bug.\n\n- **Loading:** `KsSkeleton` for content placeholders; `vKsLoading` directive for sections that already have layout; `KsLoading` component for full-page or container-level spinners.\n- **Empty:** `KsEmpty` with an action where possible — never a blank screen.\n- **Error:** `KsAlert type=\"error\"` with retry affordance, or `KsMessage` for transient errors.\n- **Success / data:** the actual content.\n\n### Data tables & pagination state\n\n`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.\n\n**The contract:**\n\n- Bind `:currentPage` / `:pageSize` (one-way) OR use `v-model:currentPage` / `v-model:pageSize` (two-way).\n- 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)})`.\n- 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.\n- `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.\n- When a column's `prop` is not the backend sort key (e.g. `auditLog.detail.resourceType` vs `detail.resourceType`), pass `:sortKeyMapper` (`(key: string) => string`); a sort click reloads immediately with the raw column prop otherwise, and the backend rejects it with a 422.\n\n**URL-driven pattern** — the default for top-level list pages (Logs, Flows, Executions, KV, Secrets, Triggers, FlowsSearch, Blueprints):\n\n```vue\n<KsDataTable\n    :loadData=\"loadData\"\n    :currentPage=\"urlPage\"\n    :pageSize=\"urlSize\"\n    :total=\"store.total\"\n    @page-changed=\"({page, size}) => router.push({query: {...route.query, page: String(page), size: String(size)}})\"\n/>\n\n<script setup>\nconst urlPage = computed(() => Number(route.query.page) || 1)\nconst urlSize = computed(() => Number(route.query.size) || 25)\n</script>\n```\n\n**Local-state pattern** — for embedded tables that should not appear in the URL (MetricsTable, side-panel views):\n\n```vue\n<KsDataTable\n    v-model:currentPage=\"currentPage\"\n    v-model:pageSize=\"pageSize\"\n    :loadData=\"loadData\"\n    :total=\"...\"\n/>\n\n<script setup>\nconst currentPage = ref(1)\nconst pageSize = ref(25)\n</script>\n```\n\n**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.\n\n**Defaults that land in the URL after mount** — a page whose filter defaults are written by a `router.replace` (e.g. the Logs page's default level, via `useRouteFilterPolicy`) must gate its first `loadData` on `isRouteSettled`. `KsDataTable` loads on mount, so without the gate the page fires a request for the bare query, throws that result away when the defaults arrive, and leaves the two responses racing each other. Gating means the query change is what triggers the first load, so also handle the navigation never landing: `isRouteSettled` gives up after a timeout, and the page must reload when it does — otherwise the list stays empty for good, with no request in flight and nothing to retry it.\n\n**Stores that back a filterable list** must ignore superseded responses: keep a sequence number per search and drop a response whose sequence is no longer the newest (see `stores/logs.ts`). Otherwise the response that happens to land last wins and the list can show results for filters the user already left — or nothing at all, when the stale search matched nothing.\n\n### The deep-watch / computed-spread trap\n\nA `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.\n\nThis 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.\n\n**Don't:**\n\n```ts\nconst filterQuery = computed(() => {\n    const {page: _p, size: _s, sort: _so, ...filters} = route.query\n    return filters  // new object reference on every route.query change\n})\nwatch(filterQuery, () => dataTable.value?.resetAndReload(), {deep: true})\n// Fires on every route.query change — page clicks, sort clicks, anything —\n// and bounces the user back to page 1.\n```\n\n**Do:**\n\n```ts\nconst filterQueryKey = computed(() => {\n    const {page: _p, size: _s, sort: _so, ...filters} = route.query\n    return JSON.stringify(filters)  // stable string — same content, same value\n})\nwatch(filterQueryKey, () => dataTable.value?.resetAndReload())\n// Fires only when filter content actually changes.\n```\n\nThe 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.\n\n### Router guards and `initApp`\n\nGuards are wired through `initApp`, and three things about that are easy to get wrong.\n\n**Register before the router installs.** `app.use(router)` starts the first navigation, and `vue-router` reads `beforeGuards.list()` once that navigation reaches its guard phase. A guard registered synchronously after `app.use(router)` is still picked up; one registered after an `await` is not, and `initApp` awaits i18n and the moment locale between installing the router and returning. A guard added in `initApp(...).then(...)` therefore misses the first navigation, which on a cold load is the only navigation there is. Pass it through the `guards` argument instead.\n\n`afterEach` is in the same window, for a less obvious reason: `afterGuards.list()` is read when a navigation *finishes*, and the first navigation can finish while `initApp` is still awaiting. An `afterEach` registered after those awaits therefore never sees it either, which silently costs anything keyed on the first navigation (`eventsRouter` attributes the landing page to `document.referrer` on exactly that hop).\n\n**A guard's arity decides how `vue-router` reads it.** `initApp` registers guards as `guards.beforeEach.bind(null, router)`, so a `(router, to, from)` guard arrives bound with `length` 2 and stays in return-value mode, where returning a route object redirects. Add a fourth parameter and the bound length becomes 3, which flips `vue-router` into `next()`-callback mode: the returned redirect is ignored and the navigation hangs with no error. Keep guards at `(router, to, from)`.\n\n**There is one `beforeEach` slot.** OSS spends it on `tenantGuard` and can only do so because its auth guard is a `beforeResolve`; EE spends it on `authGuard`. A second `beforeEach` has to be composed into the existing one, or `initApp` has to grow to accept a list, rather than being registered afterwards, since \"afterwards\" is exactly the case that misses the first navigation.\n\n### Session loss and an unreachable server\n\nA 401 on a logged-out session opens `ReauthDialog` through `requestReauth(signIn)` and replays the failed request once the user signs in, so the page and its unsaved state survive; the edition supplies its own `signIn` from `onUnauthorized`. A request that fails without a response (or behind a 502/503/504) raises `ServerUnreachableBanner`, which clears on the next response. Don't add per-page handling for either, and don't clear `unsavedChange` before the user has chosen to leave. The native basic-auth dialog is suppressed for fetch and XHR only: `EventSource` cannot set headers and relies on `Sec-Fetch-Dest`, which browsers send only over HTTPS or localhost, so on plain HTTP its streams can still open the dialog.\n\n### Unsaved input in modals (discard guard)\n\nAny modal/drawer where the user **enters data** must not silently lose it on an accidental dismissal. `KsDialog` and `KsDrawer` take a `dirty` prop and ask for confirmation themselves; never reimplement the confirm-before-discard logic per modal.\n\n```vue\n<KsDialog v-model=\"visible\" :dirty=\"isDirty\" ... />\n<KsDrawer  v-model=\"visible\" :dirty=\"isDirty\" ... />\n```\n\n```ts\n// isDirty: true when there is unsaved input worth a prompt, usually a comparison against a snapshot taken on open\nconst baseline = ref(\"\")\nconst isDirty = computed(() => JSON.stringify(form.value) !== baseline.value)\n```\n\n`dirtyMessage` replaces the default confirmation text when the discarded thing is not a form (`TriggerFlow.vue` uses it for an unsubmitted execution). When the close also has to run cleanup, keep `:beforeClose=\"(done) => { reset(); done() }\"` next to `:dirty`: the confirmation runs first, `beforeClose` only once the user agreed. The composable behind the prop, `useDiscardGuard(isDirty, {message?})` from `@kestra-io/design-system`, stays available for closes that do not go through a `Ks*` container.\n\nRules:\n- **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.\n- **`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.\n- **`KsDialog` and `KsDrawer` both expose `dirty` and 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.)\n- **Don't guard** read-only viewers, action/confirmation dialogs, or ephemeral forms that reset on every open.\n\n### Icons\n\n- All icons come from [`vue-material-design-icons`](https://github.com/robcresswell/vue-material-design-icons) via `<KsIcon>` (or `<KsIconButton>` for clickable icons).\n- 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.\n- Pass `name` (the kebab-case Material name); size and color come from props or the surrounding token context — don't override with inline `style`.\n- **Two file-type icon sets coexist on purpose, so don't merge them.** `fileUtils.fileIcon()` (behind `KsFileTag`) maps an extension to a monochrome `vue-material-design-icons` component, which inherits `--ks-icon-*` and so recolors per theme and per tag variant. `ui/src/components/utils/icons/Type.vue` renders the colored `material-file-icons` SVGs for the namespace file explorer, where the brand colors are the point. That package is a `ui/` dependency the design system does not have, and it bakes its colors into a base64 `<img>` that no token can reach, so it cannot be used from a `Ks*` component.\n\n### Performance\n\n- Lazy-load heavy surfaces: `KsEchart`, `KsLine`, `KsBar`, `KsPie`, `KsGraph`, `KsMarkdown`, code-editor surfaces. Use `defineAsyncComponent` or route-level code splitting.\n- Prefer `v-show` for frequent toggles (tabs, filters), `v-if` for rare/heavy mounts (modals, big tables).\n- Pass stable `key` props in lists. Avoid index-based keys when items have IDs.\n- Don't render giant tables without `KsDataTable`'s pagination/virtualization — server-side paging is the default for anything that can grow.\n- Watch out for `watch(..., { deep: true })` and `computed` with object identity — they often re-run more than you expect.\n\n### Types\n\n**Never write `any`.** TypeScript stops checking a value the moment it is typed `any`, so a typo or a renamed field is found by whoever opens the page instead of by the compiler. The rule covers every spelling: `(row: any)`, `x as any`, `any[]`, `Record<string, any>`, in the `<script>` block and in template expressions alike.\n\nWhere the type comes from, in this order:\n\n1. **Data from the backend: `kestra-sdk`.** It is generated from our OpenAPI spec, so the type already exists and stays in step with the API. `import type {Execution} from \"@kestra-io/kestra-sdk\"`.\n2. **Data from a library** (vue-router, element-plus, monaco, echarts, the DOM): the library's own types. When a package ships none, add a `declare module` file next to `src/material-icons.d.ts` rather than reaching for `any`.\n3. **An interface the app already has.** Search for one before writing another.\n4. **A new interface**, only when none of the above fit.\n\nFor a value whose shape really is not known yet, `unknown` with a narrowing check is the honest escape hatch. `any` is not.\n\n**A legitimate `any`, or a package with no types?** Give the package a small `.d.ts` shim next to `src/material-icons.d.ts`. An `any` that truly cannot be avoided is a conversation with a maintainer, not a number you change on your own.\n\n**MANDATORY — never raise the baseline to make the check pass.** A bigger number hides the new `any` from every later run, which is the one thing the baseline exists to prevent, and it will be treated as a bug in review. Specifically, and this applies to coding agents as much as to people:\n\n- Do not hand-edit `scripts/explicit-any/baseline.json`. The only writes to it come from the check itself.\n- Do not run `--accept-new-any`. A maintainer who has already agreed to a raise records it with `npm run check:ts-any -- --write --accept-new-any`; it is never a way to get a green check.\n- A red `check:ts-any` is fixed by typing the value, not by making the check agree with the code.\n- When you cannot type it, stop. Leave the check red, say in the PR which value defeated you and why, and let a maintainer decide. An unfinished PR is fine; a silently raised baseline is not.\n\n**The check.** `npm run check:ts-any` counts the explicit `any` per file, oxlint for the `<script>` block and the Vue compiler for template expressions, and compares the counts with `scripts/explicit-any/baseline.json`, which records what was already in the tree when the rule came in. The same check runs on every PR, as `Npm - check ts-any`. It fails in two directions and the message says which:\n\n- ``New `any` in 1 file(s)`` with a line like `src/utils/filters.ts: 1 -> 2`. Your change added one. Type it with the order above.\n- `The baseline is out of date` with `src/utils/filters.ts: 3 -> 2`, or `moved from …` when you renamed a file. Nothing got worse, the baseline just has to follow the code. Run `npm run check:ts-any -- --write` and commit it alongside your change. A rename is followed whether it is staged or already committed on your branch, so the count travels with the file either way.\n\nInstall the repo hooks once with `.github/.hooks/setup_hooks.sh` and the second case stops happening: the pre-commit hook lowers the baseline and stages it with the rest of your commit.\n\n### Testing UI\n\n- Unit tests with **Vitest** + `@vue/test-utils`, and Storybook stories, live next to the file they cover: `src/components/logs/Foo.vue` has `Foo.spec.ts` and `Foo.stories.ts` beside it. A spec or story about one aspect of a file is named `<file>.<focus>` (`Foo.download.spec.ts`, `flow.draftSave.spec.ts`) so it sorts next to its subject; one that exercises several modules together keeps a descriptive name in their folder (`entityLoadRace.spec.ts`). `tests/unit/` keeps only the setup, the shared helpers (`i18nMount.ts`) and the repo-wide guards; `tests/storybook/` keeps only shared fixtures and the stories with no single source file. The `design-system` and `topology` packages follow the same rule with `*.test.ts`, their `tests/` keeping only setup and helpers. `tests/unit/specLocationGuard.spec.ts` fails on a spec or story left in any of these `tests/` folders for a file that exists in `src/`. Playwright specs stay in the `e2e/` package: they exercise the running app, not one source file.\n- Mount through `i18nMount` or `i18nShallowMount` rather than calling `mount` with your own `createI18n`. Pass `messages` for the keys the spec asserts on, or `locales: en` when it needs the real `en.json`; with neither, `t(\"key\")` renders the key. Missing-key warnings are off in both helpers, so a spec asserting on raw keys stays quiet. Two copies exist and behave the same: specs under `src/`, `tests/unit/` and `packages/topology/src/` import `tests/unit/i18nMount.ts`, and design-system specs import `packages/design-system/tests/units/i18nMount.ts`. Both install the design system, as the app does at bootstrap, so a mounted `Ks*` component resolves instead of warning; a spec that mocks `@kestra-io/design-system` away has to keep a `default` export for that install, such as `default: {install: () => {}}`. Only a spec that never mounts anything, such as one testing pluralisation on the instance itself, builds its own i18n.\n- `i18nMount` also installs a fresh `memoryRouter()` (`tests/unit/memoryRouter.ts`), so `useRoute()` / `useRouter()` resolve instead of warning; a spec that asserts on navigation passes its own router in `global.plugins`, which replaces the default, or mocks `vue-router`. A router already in test-utils' global `config.global.plugins` (ui-ee's setup installs one) also replaces it. A spec mounting with plain `mount` passes `memoryRouter()` itself. A spec that creates a store outside a component activates `routedPinia()` from the same file instead of a bare `createPinia()`, so a store calling `useRoute()` at setup resolves it there too.\n- A spec that mounts a no-code field (`TaskObject`, `TaskObjectField`, `TaskArray`, `Add`, `TaskEdit`) ends with `afterAll(() => taskComponentsReady())` from `useBlockComponent.ts`: the block components are imported on mount, and with `isolate: false` an import still in flight when the file ends is reported as an `EnvironmentTeardownError` for the whole run.\n- A component passed inside `props` (a tab's `component`, an `icon`) is wrapped in `markRaw`: test-utils makes the props object reactive, and Vue warns about a component that was made reactive on every render.\n- Check a spec's console with `npx vitest run --project=unit --silent=false <file>`. Vitest picks a minimal reporter in agent and non-interactive shells that hides the output of passing tests, so a warning CI prints can be invisible locally.\n- A test that exercises a failure path the code reports with `console.error` stubs it (`vi.spyOn(console, \"error\").mockImplementation(() => {})`) and restores it, and ideally asserts on the call; output from a passing test is noise that hides the warning worth reading.\n- 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.\n- 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.\n- Visual regressions caught in Storybook are cheaper to fix than caught in production.\n- A test only earns its place when it can fail for a reason a reviewer would care about. Don't test that a prop is passed through, that a computed returns its own input, or that a `Ks*` component renders: the template is only restated by those assertions, and they are broken by every refactor. Delete a Vitest test once a story covers the same behavior, and say so in the PR description.\n\n### Checking a frontend change before pushing\n\n```bash\nnpm run check:types && npm run test:unit && npm run lint\n```\n\n`npm run lint` is not optional. Without it, one PR comment per eslint violation is posted by reviewdog (missing trailing commas, mostly) and the human review is buried underneath them.\n\n`npm run check:ts-any` compares the explicit `any` per file against `scripts/explicit-any/baseline.json`. It fails when a file gains one, so type it instead of raising the number. It also fails when a file loses one, because the baseline has to come down with the code: run `npm run check:ts-any -- --write` and commit the smaller numbers, or install the repo's git hooks (`.github/.hooks/setup_hooks.sh`) and the pre-commit hook does it for you. `--write` only ever lowers; it refuses to raise a count.\n\nColour is gated automatically. `tests/unit/designSystem/colorGuard.spec.ts` fails on any hex, `rgb()`, `hsl()`, `oklch()`, `--el-*`, `--bs-*`, SCSS colour variable or bare keyword colour reaching a colour property anywhere under `ui/src` and `ui/packages/topology/src`, skipping `*.spec.ts`, `*.test.ts` and `*.stories.ts`, and prints `path:line (colours)` for every offending line. It runs with `npm run test:unit` and on every PR. Replace what it reports with a `--ks-*` token, a `Ks*` prop, or a change in the design system.\n\nTwo escape hatches exist, each taking a reason, for the case the guard cannot judge: artwork whose colours **are** the asset (a brand mark, a third-party logo) rather than a themed surface. Nothing else qualifies, and a hardcoded colour that a token could carry is a bug whether or not the guard is silenced. There is deliberately no whole-file opt-out, and a `-start` with no `-end` is reported instead of muting the rest of the file.\n\n```scss\n/* design-system-disable-next-line: the Kestra mark is fixed artwork */\n/* design-system-disable-start: … */  /* … */  /* design-system-disable-end */\n```\n\nThe guard reads `.vue`, `.scss`, `.css`, `.ts` and `.js`; in a `.vue` file it reads `<style>` blocks plus `fill=` / `stroke=` attributes, and in a script it reads a hex under a colour-named key (`colorHex: \"#…\"`), so a `//`, `/* */` or `<!-- -->` comment all work.\n\nSpacing, radii and `:deep()` are not gated, so still read your own diff for those:\n\n```bash\ngit diff -U0 develop... -- '*.vue' '*.scss' \\\n  | grep -nE '^\\+.*(:deep\\(|(padding|margin|gap|border-radius|font-size)[[:space:]]*:[[:space:]]*[0-9]+px)'\n```\n\nFor a user-visible change, also check it in light **and** dark mode, and attach a screenshot to the PR.\n\n### Deprecation contract\n\nWhen retiring a `Ks*` component, prop, or token:\n\n1. Mark with a `@deprecated` JSDoc tag *and* a one-line replacement path: `@deprecated since 0.x — use <KsNewThing> instead`.\n2. Keep it working for at least one minor release; add a `console.warn` in dev mode if the cost is reasonable.\n3. Migrate all callers in the same release where feasible — don't leave half-migrations.\n4. Only delete after the deprecation window. A silent removal breaks downstream EE / plugin code.\n\n## Anti-patterns (do not write these)\n\n```vue\n<!-- Wrong: raw element-plus, hex color, :deep, SCSS variable in feature code -->\n<template>\n    <el-button class=\"my-btn\">Save</el-button>\n</template>\n<style lang=\"scss\" scoped>\n    .my-btn {\n        background: #8405ff;\n        font-size: $font-size-md;\n    }\n    :deep(.el-button__text) { color: white; }\n</style>\n```\n\n```vue\n<!-- Right: Ks component, semantic tokens, no deep selector, i18n -->\n<template>\n    <KsButton type=\"primary\">{{ t(\"save\") }}</KsButton>\n</template>\n<style scoped>\n    /* Almost always: no custom CSS is needed at all. */\n</style>\n```\n\nIf your `<style>` block needs to exist:\n\n```css\n/* Right: --ks-* tokens, no SCSS vars in feature code, no :deep */\n.my-feature {\n    background: var(--ks-bg-surface);\n    color: var(--ks-text-primary);\n    border: 1px solid var(--ks-border-default);\n}\n```\n\n## Components\n\n### Basic / Layout\n\n| Component | Purpose |\n|-----------|---------|\n| `KsButton` / `KsButtonGroup` | Primary action button and grouped buttons; `--ks-button-content-width` stretches the content (default `auto`); `block` stretches the button to its container's width |\n| `KsIcon` / `KsIconButton` | Material Design icon display; icon-only button (always with `aria-label`) |\n| `KsLink` | Styled hyperlink |\n| `KsText` | Typography wrapper — preferred over raw `<span>` / `<p>` for theme-aware text |\n| `KsScrollbar` | Custom-styled scrollbar wrapper |\n| `KsContainer` / `KsHeader` / `KsMain` | Page layout shell |\n| `KsRow` / `KsCol` | Responsive grid |\n| `KsSplitter` / `KsSplitterPanel` | Resizable split-pane layout |\n\n### Feedback\n\n| Component | Purpose |\n|-----------|---------|\n| `KsAlert` | Alert banner for messages and status feedback. Pass `banner` for a full-width system bar (licence, impersonation): square, bottom rule only, body text in `--ks-text-primary` so it reads at AA on the tint |\n| `KsDialog` | Modal dialog (handles focus trap + Escape); `dirty` asks before an accidental close; content that cannot fit the default width widens it to 1024px with a `data-dialog-wide` attribute on an element inside it, so a step that renders it is wide and the others are not |\n| `KsDrawer` | Side drawer / panel; `dirty` asks before an accidental close |\n| `KsTooltip` | Hover tooltip |\n| `KsPopover` | Popover for contextual content |\n| `KsLoading` (`vKsLoading`) | Loading spinner directive |\n| `KsMessage` | Toast notification service |\n| `KsNotification` | Notification service |\n| `KsMessageBox` | Confirmation dialog service |\n\n### Form\n\n| Component | Purpose |\n|-----------|---------|\n| `KsInput` / `KsPassword` | Text and password inputs |\n| `KsInputNumber` | Numeric input with increment / decrement |\n| `KsSelect` / `KsOption` / `KsOptionGroup` | Dropdown select |\n| `KsAutocomplete` | Autocomplete input with suggestions |\n| `KsCheckbox` / `KsCheckboxGroup` / `KsCheckboxButton` | Checkbox variants |\n| `KsRadio` / `KsRadioGroup` / `KsRadioButton` | Radio button variants |\n| `KsRadioCardGroup` | Single-select radio group rendered as option cards (title + optional hint/icon/disabled); options-driven via `:options` + `v-model` |\n| `KsSwitch` | Toggle switch |\n| `KsThemePicker` | Theme chooser, one miniature of the app per theme painted in that theme's own colours; options-driven via `:options` + `v-model`. A `#preview=\"{option}\"` slot replaces the built-in miniature, `columns` lays the options out as a grid, `ariaLabel` names the radiogroup |\n| `KsDatePicker` / `KsTimePicker` | Date and time pickers |\n| `KsColorPicker` | Color picker; `alphaOnSurface` shows translucent colours over the page surface instead of a checkerboard |\n| `KsDurationPicker` | ISO 8601 duration picker |\n| `KsCascaderPanel` | Cascading hierarchical selector |\n| `KsUpload` | File upload |\n| `KsForm` / `KsFormItem` | Form container with validation |\n\n### Data Display\n\n| Component | Purpose |\n|-----------|---------|\n| `KsCard` | Card container |\n| `KsTable` / `KsTableColumn` | Basic table |\n| `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\". Pass `mobileColumns` (`{leading?, title, titleAside?, meta?, metaAside?, actions?}`, each naming a column `prop`, or the `columnKey` of a prop-less actions column) to render a card per row below 768px: `leading` is a status dot, `title` is bold, `titleAside` sits on its right, `meta` columns share the second line joined by a dot and `metaAside` closes it; `actions` is a corner cell on the first line; unlisted columns are hidden, bulk selection is desktop-only (the selection column is not shown on cards), and the regular table returns from 768px. |\n| `KsEntityLink` | Clickable cross-entity reference (namespace / flow) for table cells — neutral tag with leading icon, violet on hover. Pass `noIcon` in dense embedded tables (e.g. dashboard chart tables, ~90px columns) where the icon's 20px costs more than it tells |\n| `KsBadge` | Small indicator badge; `size=\"small\"` shrinks the pill so it sits on an icon button instead of covering it |\n| `KsNewBadge` | Compact uppercase \"NEW\" pill flagging a newly shipped feature — caller supplies the label via the default slot |\n| `KsTag` / `KsCheckTag` | Tag / label; clickable checkbox-style tag. Pass `truncate` to clip a long label with an ellipsis instead of letting the tag outgrow its container |\n| `KsAvatar` | Avatar with fallback |\n| `KsProgress` | Progress bar |\n| `KsPagination` | Pagination controls |\n| `KsEmpty` | Empty state placeholder |\n| `KsSkeleton` | Skeleton loader |\n| `KsId` | Copyable ID display |\n| `KsDateAgo` | Relative time display (\"2 hours ago\") |\n| `KsSegmented` | Segmented control; object options may carry an `icon` component, rendered before the label. `variant=\"neutral\"` swaps the primary-coloured selection for a grey one with wider segments |\n| `KsCollapse` / `KsCollapseItem` | Collapsible sections |\n| `KsFileTag` | Storage URI rendered as a file reference: a `KsTag` whose symbol comes from the extension, with a readable name (`name`, defaulting to the URI's last segment); the full URI stays in the tooltip |\n| `KsTree` | Hierarchical tree view |\n| `KsJsonTree` | Read-only JSON tree viewer; leaves holding a storage URI render through `KsFileTag` |\n| `KsTimeline` / `KsTimelineItem` | Timeline visualization |\n| `KsExecutionStatus` | Execution / task status badge with icon and color; `dot` collapses it to a coloured dot (label kept for screen readers) for dense layouts such as mobile cards |\n| `KsCodeStatus` | Compact validity badge with icon (`valid` / `error`) — caller supplies the label |\n| `KsMarkdown` | Markdown renderer (lazy-load on heavy surfaces) |\n\n### Charts\n\n| Component | Purpose |\n|-----------|---------|\n| `KsEchart` | ECharts base wrapper (lazy-load) |\n| `KsLine` / `KsBar` / `KsPie` | Line, bar, and pie charts (lazy-load) |\n| `KsGraph` | Graph / network visualization (lazy-load) |\n\n### Navigation\n\n| Component | Purpose |\n|-----------|---------|\n| `KsTabs` / `KsTabPane` | Tabbed interface |\n| `KsMenu` / `KsMenuItem` | Hierarchical menu |\n| `KsDropdown` / `KsDropdownMenu` / `KsDropdownItem` | Dropdown menu; pass `danger` on an item to give a destructive or exit action (delete, log out) the error-coloured hover |\n| `KsTopNavBar` | Top navigation bar; `titleSiblings` reaches the breadcrumb's current page |\n| `KsSideBar` / `KsSideBarSection` / `KsSideBarItem` | Left sidebar shell (header / scrollable body / footer slots), section with title, and styled link primitive with icon, active and locked states |\n| `KsBreadcrumb` / `KsBreadcrumbItem` | Breadcrumb navigation. An item's `siblings` loader (or `children` on the first item, and the `titleSiblings` prop for the current page) adds a chevron whose hover menu lists that level, headed by the level above (its `scope` when the label is not the right name for its content); an entry with a `children` loader flies out its own content to the right, as deep as the tree goes. Level loaders run as soon as the bar renders and an empty result shows no chevron; fly-outs load on hover. Namespace and flow pages build theirs with `useNamespaceBreadcrumb` |\n| `KsSteps` / `KsStep` | Step / wizard progress indicator |\n\n## Utilities (import from the design system)\n\n- `State`, `STATES`, `LOG_LEVELS` — execution state constants and icons. A state's colour is not a property of the state: read it with `State.getStateColor(name)`, which resolves `--ks-status-*`\n- `cssVar(name, opacity?)` — read a `--ks-*` CSS custom property at runtime (use this in JS / chart configs instead of hardcoding hex)\n- `dayjs` — the one configured dayjs instance (utc, timezone, duration, advancedFormat, calendar, isoWeek, localizedFormat, minMax, relativeTime, weekOfYear, isSameOrBefore). Never `import dayjs from \"dayjs\"` in feature code: plugins are registered on this instance, so a bare import silently lacks them\n- `dateUtils` — `dateFilter()`, `parseIso()`, `toIsoKeepOffset()`, `currentTimezone()`, `timezonesWithOffset()`, `currentLocale()`, `setLocale()`, `DATE_FORMAT_STORAGE_KEY`, `TIMEZONE_STORAGE_KEY`\n- `durationUtils` — `duration()`, `isValidDuration()`, `humanDuration()` — ISO 8601 ↔ ms and human-readable\n- `stringUtils` — `afterLastDot()`\n- `escapeHtml()`, `cloneDeep()`, `deepMerge()`, `isDeepEqual()`, `isPlainObject()`, `getPath()`, `setPath()`, `groupBy()`, `mapValues()`, `debounce()`, `throttle()` — the general-purpose helpers that replaced lodash. `debounce`/`throttle` return a function carrying `cancel()` and `flush()`. Do not add `lodash` back for any of these\n- `fileUtils` — `isFileUri()`, `fileName()`, `fileExtension()`, `fileIcon()` — storage-URI detection and the file symbol used by `KsFileTag`\n- `flowYamlUtils` — YAML parsing / manipulation for flow definitions\n- `Comparators` — enum of filter comparison operators\n- Filter helpers — `decodeSearchParams()`, `encodeFiltersToQuery()`, `getUniqueFilters()`, etc.\n- `applyDefaultFilters()`, `useRouteFilterPolicy()` — filter composables\n- `designSystemLocale`, `setDesignSystemLocale`, `registerDesignSystemI18n` — i18n\n- `designSystemI18nReady()` — the locale registration the plugin's `install` started, to await instead of leaving it in flight (the unit setup awaits it after each test)\n\n## Composables\n\n- `useTheme()` — detects and tracks dark / light mode via MutationObserver. Use this instead of reading `document.documentElement` yourself.\n- `useFilters`, `useSavedFilters`, `useDefaultFilter`, `usePreAppliedFilters`, `useRouteFilterPolicy`, `useTableColumns`, `useDataOptions`, `useDragAndDrop`, `usePeriodicRefresh` — data-table filter composables\n- `useDiscardGuard(isDirty, {message?})` — confirm-before-discard behind the `dirty` prop of `KsDialog` / `KsDrawer`; see \"Unsaved input in modals (discard guard)\"\n- `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.\n\n## Design tokens\n\nTokens are CSS custom properties declared in [`ks-theme-light.css`](packages/design-system/src/assets/styles/ks-the","ui/packages/topology/tsconfig.json":"{\n    \"compilerOptions\": {\n        \"target\": \"esnext\",\n        \"lib\": [\"es2023\", \"DOM\", \"DOM.Iterable\"],\n        \"moduleDetection\": \"force\",\n        \"module\": \"preserve\",\n        \"moduleResolution\": \"bundler\",\n        \"resolveJsonModule\": true,\n        \"types\": [\"node\"],\n        \"strict\": true,\n        \"noUnusedLocals\": true,\n        \"declaration\": true,\n        \"emitDeclarationOnly\": true,\n        \"esModuleInterop\": true,\n        \"isolatedModules\": true,\n        \"verbatimModuleSyntax\": true,\n        \"skipLibCheck\": true,\n        \"allowImportingTsExtensions\": true\n    },\n    \"include\": [\"src\", \"tsdown.config.ts\"],\n    \"exclude\": [\"src/**/*.test.ts\"]\n}\n","ui/packages/kestra-sdk/tsconfig.json":"{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"lib\": [\"es2023\", \"dom\"],\n    \"moduleDetection\": \"force\",\n    \"module\": \"preserve\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"esModuleInterop\": true,\n    \"isolatedModules\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src\"],\n  \"references\": [\n    { \"path\": \"./tsconfig.node.json\" }\n  ]\n}\n","ui/packages/kestra-sdk/package.json":"{\n  \"name\": \"@kestra-io/kestra-sdk\",\n  \"version\": \"2.0.0\",\n  \"private\": true,\n  \"description\": \"JS/TS client generated from the OSS OpenAPI spec (io.kestra:webserver), in-repo, on the same commit as the backend it describes. The generated code under src/openapi is committed to git. Keeps the client's name so plugins import the same specifier the app does (auth + routing). See README.md.\",\n  \"license\": \"Apache-2.0\",\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./dist/index.js\",\n    \"./ai\": \"./dist/ai.js\",\n    \"./all\": \"./dist/all.js\",\n    \"./blueprint-tags\": \"./dist/blueprint-tags.js\",\n    \"./blueprints\": \"./dist/blueprints.js\",\n    \"./client\": \"./dist/client.js\",\n    \"./dashboards\": \"./dist/dashboards.js\",\n    \"./default\": \"./dist/default.js\",\n    \"./default-admin\": \"./dist/default-admin.js\",\n    \"./executions\": \"./dist/executions.js\",\n    \"./expressions\": \"./dist/expressions.js\",\n    \"./files\": \"./dist/files.js\",\n    \"./flows\": \"./dist/flows.js\",\n    \"./kv\": \"./dist/kv.js\",\n    \"./logs\": \"./dist/logs.js\",\n    \"./mcp\": \"./dist/mcp.js\",\n    \"./metrics\": \"./dist/metrics.js\",\n    \"./misc\": \"./dist/misc.js\",\n    \"./namespaces\": \"./dist/namespaces.js\",\n    \"./notifications\": \"./dist/notifications.js\",\n    \"./outputs\": \"./dist/outputs.js\",\n    \"./plugins\": \"./dist/plugins.js\",\n    \"./secrets\": \"./dist/secrets.js\",\n    \"./services\": \"./dist/services.js\",\n    \"./shared\": \"./dist/shared.js\",\n    \"./triggers\": \"./dist/triggers.js\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"generate:openapi\": \"openapi-ts\",\n    \"build\": \"tsdown\",\n    \"check:types\": \"tsc --noEmit -p tsconfig.json\"\n  },\n  \"devDependencies\": {\n    \"@hey-api/openapi-ts\": \"^0.99.0\",\n    \"@kestra-io/hey-api-plugin\": \"*\",\n    \"@types/node\": \"^26.6.5\",\n    \"tsdown\": \"^0.23.1\",\n    \"typescript\": \"^6\",\n    \"unrun\": \"^0.3.1\"\n  }\n}\n","ui/packages/kestra-sdk/README.md":"# @kestra-io/kestra-sdk (OSS)\n\nThe JS/TS client for the Kestra **OSS** API, generated from the backend's own OpenAPI spec and\nliving **in the same repo, on the same commit** as the backend it describes.\n\n- **Generated from:** `io.kestra:webserver` → `./gradlew :webserver:generateOpenapiSpec` → `openapi.yml`\n- **Generator:** [`@hey-api/openapi-ts`](https://heyapi.dev/) with `@hey-api/client-fetch` + the shared\n  [`@kestra-io/hey-api-plugin`](../hey-api-plugin) (tenant-aware, human-friendly wrappers).\n- **Committed:** the generated code under [`src/openapi`](src/openapi) **is checked into git**. This\n  decouples the fast (npm) build from the Gradle/backend build — `npm run dev`, `build`, and\n  `check:types` never need a Java toolchain.\n\nThe package keeps the name `@kestra-io/kestra-sdk` because that is the specifier plugins import to\nreach the app's client (auth + routing).\n\n## Entry points\n\n| Import | Contents |\n| --- | --- |\n| `@kestra-io/kestra-sdk` | `useClient` / `configureClient` / `setMockClient`, and every generated **type** |\n| `@kestra-io/kestra-sdk/<tag>` | the operations of one tag, e.g. `/flows`, `/executions` |\n| `@kestra-io/kestra-sdk/all` | every operation at once — named exports, plus the namespace as `default` |\n\nThe root entry exports **no operations**: re-exporting the generated operations there put all of them\nin the app's initial graph. Reach an operation through its tag, or through `/all` if one import is\npreferable to several.\n\n## Regenerating the SDK\n\nOnly needed when the OSS API changes. From `ui/`:\n\n```bash\nnpm run generate:sdk\n```\n\nThat is the **only** path that invokes Gradle. It: generates `openapi.yml` (Gradle) → hashes it and\ncompares against the `OPENAPI_SPEC_HASH` already committed in `src/openapi/sdk/shared.gen.ts` → if\nthey match, the committed SDK is already correct for the current spec and the rest is skipped; if\nthey differ, builds the shared plugin → runs `openapi-ts` (generate + convert + hash-stamp) →\nbundles `dist/`. This makes `generate:sdk` cheap to run speculatively (e.g. after any backend change)\nsince a no-op spec diff short-circuits before the expensive steps. Commit the resulting\n`src/openapi/` changes (and the regenerated `package.json` `exports` map) when it does regenerate.\n\nEveryday commands never regenerate: `ui/scripts/ensure-sdk.mjs` (the `predev` / `prebuild` /\n`precheck:types` hook) only bundles the already-committed `src/openapi` into `dist/` when `dist/` is\nmissing.\n\n## Drift detection\n\nThe generated SDK exports `OPENAPI_SPEC_HASH` (`sha256(openapi.yml)[:16]`, stamped by the shared\nplugin at generation time — no external bin). Drift is caught **at dev time, not in CI**: on the\nfirst `configureClient` call in a dev build, `dev-freshness.ts` fetches the backend's live spec,\nhashes it the same way, and warns if the committed SDK is behind. The check is guarded by\n`import.meta.env.DEV` + a dynamic import, so it is tree-shaken out of production builds entirely.\n\n## Runtime\n\n`src/index.ts` binds the shared `createConfigureClient(client, formDataBodySerializer)` from\n`@kestra-io/hey-api-plugin/runtime` (bundled into `dist/`, not a runtime dependency) and keeps the\napp-only `useClient()` / `setMockClient()` — an axios-like facade over fetch that shares the same\ninterceptors, so existing `useClient().get/post(...)` call sites are unchanged. The EE SDK reuses\nthese by relative import.\n","ui/packages/kestra-sdk/tsconfig.node.json":"{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"lib\": [\"es2023\"],\n    \"moduleDetection\": \"force\",\n    \"module\": \"preserve\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"types\": [\"node\"],\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"esModuleInterop\": true,\n    \"isolatedModules\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"composite\": true\n  },\n  \"include\": [\"tsdown.config.ts\", \"openapi-ts.config.ts\"]\n}\n","ui/packages/slot-contracts/tsconfig.json":"{\n    \"compilerOptions\": {\n        \"target\": \"esnext\",\n        \"lib\": [\"es2023\", \"DOM\"],\n        \"moduleDetection\": \"force\",\n        \"module\": \"preserve\",\n        \"moduleResolution\": \"bundler\",\n        \"resolveJsonModule\": true,\n        \"types\": [\"node\"],\n        \"strict\": true,\n        \"noUnusedLocals\": true,\n        \"declaration\": true,\n        \"emitDeclarationOnly\": true,\n        \"esModuleInterop\": true,\n        \"isolatedModules\": true,\n        \"verbatimModuleSyntax\": true,\n        \"skipLibCheck\": true,\n        \"allowImportingTsExtensions\": true\n    },\n    \"include\": [\"src\", \"tsdown.config.ts\"]\n}\n","ui/packages/hey-api-plugin/package.json":"{\n  \"name\": \"@kestra-io/hey-api-plugin\",\n  \"version\": \"0.4.0\",\n  \"description\": \"Kestra's shared SDK tooling. Two entry points: the '.' entry is the @hey-api/openapi-ts generator plugin (generation-time) that turns a Kestra OpenAPI spec into tenant-aware, human-friendly SDK wrappers; the './runtime' entry is createConfigureClient — the universal fetch-based client setup every Kestra SDK ships. Single source of truth used by the OSS UI SDK, the EE UI SDK, and the client-sdk repo.\",\n  \"license\": \"Apache-2.0\",\n  \"type\": \"module\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.mts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.mts\",\n      \"default\": \"./dist/index.mjs\"\n    },\n    \"./runtime\": {\n      \"types\": \"./dist/runtime.d.ts\",\n      \"default\": \"./dist/runtime.js\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"tsdown\",\n    \"prepack\": \"tsdown\",\n    \"check:types\": \"tsc --noEmit -p tsconfig.json\"\n  },\n  \"peerDependencies\": {\n    \"@hey-api/openapi-ts\": \">=0.97.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"@hey-api/openapi-ts\": {\n      \"optional\": true\n    }\n  },\n  \"devDependencies\": {\n    \"@hey-api/openapi-ts\": \"^0.99.0\",\n    \"@types/node\": \"^26.6.5\",\n    \"tsdown\": \"^0.23.1\",\n    \"typescript\": \"^6\",\n    \"unrun\": \"^0.3.1\"\n  }\n}\n","ui/packages/hey-api-plugin/README.md":"# hey-api-plugin Documentation\n\nPath: `ui/packages/hey-api-plugin/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/hey-api-plugin/README.md)","ui/packages/hey-api-plugin/tsconfig.json":"# Hey-api-plugin Skill\n\nPath: `ui/packages/hey-api-plugin/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/hey-api-plugin/tsconfig.json)","ui/packages/design-system/package.json":"# Design-system Skill\n\nPath: `ui/packages/design-system/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/package.json)","ui/packages/design-system/tsconfig.app.json":"# Design-system Skill\n\nPath: `ui/packages/design-system/tsconfig.app.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.app.json)","ui/packages/design-system/tsconfig.json":"# Design-system Skill\n\nPath: `ui/packages/design-system/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.json)","ui/packages/design-system/tsconfig.node.json":"# Design-system Skill\n\nPath: `ui/packages/design-system/tsconfig.node.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.node.json)","ui/packages/design-system/tsconfig.storybook.json":"# Design-system Skill\n\nPath: `ui/packages/design-system/tsconfig.storybook.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.storybook.json)","ui/packages/design-system/web-types.json":"# Design-system Skill\n\nPath: `ui/packages/design-system/web-types.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/web-types.json)","ui/packages/design-system/scripts/Figma.json":"# Scripts Skill\n\nPath: `ui/packages/design-system/scripts/Figma.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/scripts/Figma.json)","ui/packages/design-system/tests/storybook/Basic/Color-variables.json":"# Basic Skill\n\nPath: `ui/packages/design-system/tests/storybook/Basic/Color-variables.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tests/storybook/Basic/Color-variables.json)"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Coding Agent Guidelines for Kestra Open Source Edition\n\nThis document provides essential information for AI coding agents working on the Kestra codebase.\n\n**IMPORTANT — READ FIRST**\n\n- **Act as a Senior Software Engineer and Software Architect.** Approach software development with:\n    - **Pragmatism**: Favor simple solutions over clever ones\n    - **Skepticism**: Question decisions that could cause technical debt or scalability issues\n    - **Efficiency**: Only challenge when it genuinely matters\n- **Think before coding**: explicitly state assumptions, compare alternatives, and justify choices.\n- **Simplicity first (KISS)**: overengineering and \"gas factories\" are strictly forbidden.\n- **Surgical changes only**: touch **only** what is strictly necessary to achieve the goal.\n- **Goal-driven execution**: define what success looks like *before* writing the first line of code.\n- **Reuse before writing**: the pattern you need almost always exists already. Find the closest equivalent in the codebase and follow it instead of inventing a second way to do the same thing.\n- **No comments by default**: see [Comments](#comments). Over-commenting is one of the most common reasons a PR written by an agent has to be revised.\n- **How Kestra looks is decided by the design system**: see [UI Design System](#ui-design-system). No color, spacing, radius or component pattern may be invented in feature code.\n- **A test has to be able to fail for a reason a reviewer would care about**: see [When a test earns its place](#when-a-test-earns-its-place). More tests do not make a change safer.\n- **Build and test only what you touched** rather than the whole repo: see [What to run after a change](#what-to-run-after-a-change).\n\n## Comments\n\n**The default is no comment.** Small methods with accurate names need no explanation, and every comment is a second thing that has to be kept in sync with the code. Over-commenting is easy to slip into: the next line gets narrated, the method name gets restated above the method, and `// Step 1` / `// Step 2` scaffolding is left behind. None of that will be accepted in review.\n\nThe reader to write for is the developer opening the file in a month. They do not have time to read a paragraph, and most of what a paragraph would tell them is already in the code.\n\nWrite a comment only when a reviewer would otherwise have to ask **why** the code exists:\n\n- a non-obvious constraint, or an upstream bug being worked around (link the issue)\n- an ordering, locking or concurrency requirement that the code itself cannot express\n- a deliberate omission, so that the next person does not \"fix\" it back\n\nKeep it to **one sentence**. When a paragraph seems necessary, make the code clearer instead.\n\nNever write:\n\n- a restatement of the line below it (`// increment the counter`, `// return the result`)\n- a narration of a self-explanatory getter, loop, or well-named call\n- section banners (`// ---- helpers ----`), changelog notes, or a `// TODO` with no issue link\n- a comment on every entry of a list when the rule is already stated once above the list\n\n**Existing comments** should be left alone unless the code they describe changed. When that code is deleted, delete the comment with it rather than rewording it around the gap. When the code changed, either correct the comment or remove it.\n\n**Javadoc** describes the contract rather than the body. Add it on public API called from other modules or from plugins, and on anything whose behavior cannot be seen from the signature (nullability, thread-safety, side effects, units, ownership of a returned collection). Skip it on getters, setters, builders, an override that adds nothing (a bare `{@inheritDoc}` is noise), and any method whose name already says everything.\n\n## Project\n\nMonorepo built with Java (backend) and Vue (frontend), using Gradle as the build system.\n\n## Tech Stack\n- **Backend:** Java 25, Micronaut Framework, Lombok\n- **Frontend:** Vue 3, TypeScript, Vite, Element Plus, Pinia\n- **Build:** Gradle, multi-project. The version is pinned in `gradle/wrapper/gradle-wrapper.properties`, and `settings.gradle` is the list of modules\n- **Testing:** JUnit 5, Mockito, AssertJ, Vitest, Playwright\n\n## Critical Code Patterns\n\n### Dependency Injection\n\n**DO**: Use constructor injection with final fields.\n\n```java\n@Singleton\npublic class MyService {\n    private final SomeDependency dependency;\n\n    @Inject\n    public MyService(SomeDependency dependency) {\n        this.dependency = Objects.requireNonNull(dependency);\n    }\n}\n```\n\n**DON'T**: Use field injection (`@Inject` on fields directly). Always prefer constructor injection.\n\n### Class Structure\n\n```java\n// 1. Package declaration and imports\n// 2. Class-level annotations (@Slf4j, @Singleton, etc.)\n// 3. Class declaration with Javadoc\n// 4. Static constants (UPPER_SNAKE_CASE)\n// 5. Injected fields (@Inject)\n// 6. Constructors\n// 7. Public methods\n// 8. Protected methods\n// 9. Private methods\n// 10. Inner classes/records\n```\n\n### Annotations\n- **Micronaut:** `@Singleton`, `@Inject`, `@Controller`, `@Replaces`, `@Requires`\n- **Validation:** `@Valid`, `@NotNull`, `@Nullable`\n- **Lombok:** `@Slf4j`, `@Getter`, `@NoArgsConstructor`, `@AllArgsConstructor`\n- Use `@Builder` for complex object creation\n\n### Error Handling\n\n**DO**:\n- Use specific exception types — extend `KestraException` or `KestraRuntimeException`\n- Use `Optional<T>` for potentially absent returned values\n- Return empty collections (e.g., `List.of()`, `Collections.emptyList()`) for absent values\n- Use try-with-resources for resource management\n- Log errors before re-throwing: `log.error(\"message\", exception)`\n- 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)`\n\n**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.\n\n### Java Language Features\n- Use java records for simple data carriers\n\n### Naming Conventions\n- Follow Java naming-convention best practices for Classes, Methods, Variables, Constants.\n- Boolean methods: Start with `is`, `has`, `should`, `can` (e.g., `isReadOnly()`).\n\n### File Organization\n- Use 4-space indentation (configured in .editorconfig)\n- UTF-8 encoding with LF line endings\n- No trailing whitespace\n\n### Utility Classes\n* Mark utility classes as `final` with a private constructor\n* Use static methods only\n* Use existing utility classes (e.g., `ListUtils`, `MapUtils`) instead of creating new ones (`io.kestra.core.utils.*`)\n\n**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.\n\n### Enums\n- 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\n- Use `@JsonValue` for custom serialization if needed\n- Use `UNKNOWN` enum value for unknown cases in deserialization\n- Compare Constants From The Left (a.k.a., Yoda conditions)\n- Use a static `fromString` method for case-insensitive lookups using `Enums` class.\n\ne.g.:\n```java\npublic enum MyEnum {\n    VALUE_ONE,\n    VALUE_TWO,\n    UNKNOWN;\n\n    @JsonCreator\n    public static ResourceType fromString(final String value) {\n        return Enums.getForNameIgnoreCase(value, MyEnum.class, UNKNOWN);\n    }\n}\n```\n\n### Documentation\n\nSee [Comments](#comments) for when a Javadoc block is worth writing at all. Once you have decided that it is:\n\n- Keep it to the contract: what the caller has to know that the signature does not already say\n- Use `@param`, `@return` and `@throws` only where each adds something. `@param name the name` is noise\n- Skip a bare `{@inheritDoc}` on an override that changes nothing\n- Add a usage example only when the call site would otherwise be guessed wrong\n\n## Webserver Constraints\n- Put classes used by only controllers in the webserver module (not core)\n- No business code/rule inside controllers - instead use a Service class\n- All APIs must return a valid JSON object\n- APIs should not return a response being a JSON array which cannot be evolved in a backwards-compatible way\n- Any endpoint returning a collection should be paged through `PagedResults` with `page` and `size` parameters, like the rest of the API\n- A DTO used by a single controller belongs as an inner record of that controller. Promote it to `io.kestra.webserver.models.api` only once a second caller exists, and name it with the `Api` prefix there (`ApiExecution`, `ApiTriggerState`, `ApiPluginArtifact`)\n- Import a class rather than writing its fully-qualified name, as soon as it is used more than once in the same file\n- Unit tests must assert that a user can only access a given API if authorized to do so, and that access is denied otherwise\n- APIs must be documented with OpenAPI annotations\n- Use DTOs for requests/responses\n- A dashboard chart export must use the chart's declared columns for its header, in declaration order, whether or not there are rows\n- Always validate input parameters with `@Valid`\n- Use `@ExecuteOn(TaskExecutors.IO)` for blocking operations\n- Return meaningful error responses in controllers\n\n## Worker Constraints\n- Never depend on repositories for code called by the workers - instead use MetaStore/StateStore facades\n\n## Executor Constraints\n- Run the `H2RunnerTest` whenever you update part of the executor\n\n## Testing Guidelines\n\n### When a test earns its place\n\nMore tests do not make a change safer. Every test is code that has to be read, kept passing, and understood by whoever changes that behavior next, so a test is only worth its cost when it can fail for a reason a reviewer would care about.\n\n**Add a test when:**\n\n- new behavior is introduced, or a bug is fixed. The test should fail on the commit before yours; if it passes there, it is testing something else\n- an edge case would be easy to get wrong later: an empty collection, a null, a boundary, a race, a permission check\n- the change is in the executor, the scheduler, the queue, or an ACL path, where a regression is expensive and hard to notice\n\n**Don't add a test when:**\n\n- it only exercises getters, setters, builders, or a Lombok-generated method\n- it asserts behavior that the framework already documents (Jackson serializing a field, Micronaut injecting a bean)\n- it repeats an existing test with one property changed. One test per behavior, rather than one per property, since otherwise there would be hundreds of them\n- the mocks would have to restate the implementation for the assertions to pass. Such a test is pinned to the current code rather than to the behavior, so it is broken by every legitimate refactor\n- it exists only to move a coverage number\n\n**Delete tests too.** Removing a feature should reduce the test count rather than raise it. When a test no longer describes behavior anyone relies on, or when it duplicates another one, delete it in the same PR and say so in the description. A redundant test that is always green is still maintenance cost.\n\n### Java Tests\n\n**DO**:\n- Place tests in same package structure as source code\n- Simple unit test with mocks over complex integration tests when possible\n- Structure the body as given / when / then. The three marker comments are optional and should be dropped when the test is only a few obvious lines\n- Test method naming: `should<ExpectedBehavior>When<ConditionOrAction>` (also `...Given<Input>`, `...For<Condition>`, `...If<Condition>`), e.g. `shouldThrowExceptionWhenDividingByZero()`\n- Use `@MicronautTest` for tests that require Micronaut beans\n- Use `@KestraTest` for tests that require running Kestra services (e.g., Executor, Scheduler)\n-\n```java\n@KestraTest\nclass ServiceTest {\n    @Inject\n    private ServiceClass service;\n\n    @Test\n    void shouldPerformActionWhenCondition() {\n        // Given (setup)\n\n        // When (action)\n\n        // Then (assertions)\n        assertThat(result).isNotNull();\n    }\n}\n```\n\n**DON'T**: Use Nested classes for test organization. Avoid complex test hierarchies.\n\n**Assertions:**\n- Use AssertJ: `assertThat().isEqualTo()`, `assertThat().isNotNull()`, `assertThatThrownBy()`, `assertThatObject()`\n- Prefer descriptive assertion methods\n- Use `@MockBean` for mocking dependencies\n\n**Test Categories:**\n- Unit tests: Fast, isolated, no external dependencies\n- Integration tests: Test component interaction, use `@Tag(\"integration\")`\n- Flaky tests: Use `@Tag(\"flaky\")` for unreliable tests\n\n### Frontend Tests\n- Unit tests with Vitest and `@vue/test-utils`, next to the file they cover (`Foo.vue` beside `Foo.spec.ts` and `Foo.stories.ts`), see [ui/AGENTS.md](ui/AGENTS.md)\n- E2E tests with Playwright\n- Storybook component tests\n- Use JSdom environment for DOM testing\n- **Prefer Storybook component tests over Vitest unit tests whenever possible** — components render through their real story setup (props, slots, design-system deps) instead of being stubbed out, catching regressions unit mocks miss. Fall back to a Vitest unit test only when the logic under test isn't component-rendering behavior (e.g. a pure helper/composable) or no story exists and adding one isn't practical.\n- Don't keep a Vitest test that only re-asserts what a story already covers. Delete it when you add the story.\n- Assert on `data-test` attributes and rendered text. A test written against `.el-*` or `.ks-*` class names is broken by the next Element Plus or design-system upgrade, without any behavior having changed.\n\n## UI Design System\n\nThe design system at [ui/packages/design-system/](ui/packages/design-system/) is the **single source of truth for every design decision** in Kestra: colors, typography, spacing, radii, shadows, and the component vocabulary. Nothing rendered to a user may be styled outside it.\n\nIn practice that means no hex codes, no `rgb()`, no `--el-*` or `--bs-*` variables, no hardcoded pixel spacing, no `:deep()` into another component, and no locally built equivalent of a component that already exists. When the design you need cannot be expressed with the existing `Ks*` components and `--ks-*` tokens, that is a gap in the design system: raise it with design and add the token, the prop or the component upstream. Approximating it with custom CSS in a feature component is not an option, since the result will only be right in the one theme you checked it in.\n\nThe full rules, the component catalogue, the token reference and the frontend best practices live in [ui/AGENTS.md](ui/AGENTS.md), which is auto-loaded for work under `ui/` in OSS and `ui-ee/` in Enterprise Edition. Read it before any frontend change, and update it in the same PR whenever a component, a prop or a token is added.\n\n@ui/AGENTS.md\n\n## Frontend Code Style (Vue 3)\n\n**File Organization:**\n- Use 2-space indentation for Vue, JSON, YAML, CSS\n- Use 4-space indentation for JavaScript/TypeScript\n- Follow Vue 3 Composition API patterns\n- Organize imports: Vue/framework → third-party → local modules\n\n**Naming Conventions:**\n- Components: `PascalCase` files (e.g., `MyComponent.vue`)\n- Variables/functions: `camelCase`\n- Constants: `UPPER_SNAKE_CASE`\n- CSS classes: Follow Element Plus conventions\n\n**TypeScript:**\n- Use strict TypeScript configuration\n- Prefer type definitions over `any`\n- Use interfaces for object shapes\n- Use enums for fixed sets of values\n\n## Build Commands\n\n### What to run after a change\n\nA whole-repo `./gradlew build` can take tens of minutes, and almost nothing about a three-line change can be learned from it. Start with the narrowest command that covers what you touched, and widen it only once that one passes.\n\n| What changed | Run |\n|---|---|\n| One backend module | `./gradlew :<module>:test --tests \"TheClassName\"`, then `./gradlew :<module>:test` |\n| Anything in the executor | `./gradlew :jdbc-h2:test --tests \"H2RunnerTest\"` (mandatory) |\n| Something crossing module boundaries | `./gradlew build -x integrationTest` |\n| A Vue component or composable | `cd ui && npm run check:types && npm run test:unit && npm run lint` |\n| A design-system component | the above, plus `npm run test:storybook` |\n| An e2e spec or fixture | `cd e2e && npm run check:types && npm run test:lint` |\n| `en.json`, whether a key was added or a value edited | `cd ui && npm run translations:generate && npm run translations:check` |\n| A controller or its DTOs | that module's tests, including the authorization tests for the route |\n\n### Java Backend\n\n```bash\n# Clean build\n./gradlew clean\n\n# Full build (includes tests)\n./gradlew build\n\n# Build without tests (faster)\n./gradlew build -x test -x integrationTest -x testCodeCoverageReport --refresh-dependencies --no-daemon --parallel\n```\n\n### Test Commands\n\n```bash\n# Run all tests (excludes flaky tests)\n./gradlew test\n\n# Run only unit tests (fastest)\n./gradlew unitTest\n\n# Run integration tests\n./gradlew integrationTest\n\n# Run flaky tests (separate from build)\n./gradlew flakyTest\n\n# Run tests for specific module\n./gradlew :core:test\n\n# Run single test class\n./gradlew :module-name:test --tests \"ClassName\"\n\n# Run single test method\n./gradlew :module-name:test --tests \"ClassName.methodName\"\n\n# After running tests: generate a markdown summary of failures only\nnpx --yes @kestra-io/kestra-devtools generateTestReportSummary --only-errors $(pwd)\n```\n\n### Frontend (UI)\n\n```bash\ncd ui\n\n# Install dependencies\nnpm install\n\n# Development server\nnpm run dev\n\n# Type checking\nnpm run check:types\n\n# Build for production\nnpm run build\n\n# Run tests\nnpm run test:all        # All tests with coverage\nnpm run test:unit       # Unit tests only\nnpm run test:storybook  # Storybook tests\n\n# Linting\nnpm run lint            # Fix linting issues\nnpm run test:lint       # Check linting only\n\n# Storybook\nnpm run storybook       # Development\nnpm run build-storybook # Build\n```\n\n### End-to-end tests\n\nThe Playwright suite is its own npm package at [e2e/](e2e/), installed separately from `ui/` so that\na spec change pulls in neither the frontend dependency tree nor the frontend CI jobs.\n\n```bash\ncd e2e\nnpm ci\nnpx playwright install chromium\n\nnpm run test:e2e                          # starts a Kestra docker image, runs the suite, stops it\nnpm run test:e2e-without-starting-backend # against an instance you already have running\nnpm run check:types && npm run test:lint\n```\n\n## Development Workflow\n\n### Running Locally\n\n1. **Start/stop backends:**\n```bash\n# Start databases with Docker Compose\ndocker compose -f docker-compose-ci.yml up\n\n# Stop databases with Docker Compose\ndocker compose -f docker-compose-ci.yml down\n```\n\n2. **Access application:** http://localhost:8080\n\n### Worktree setup\n\nWhen working in an OSS worktree (the working directory is under a `worktrees/` directory):\n```bash\ndev-tools/setup-worktree.sh ../worktrees/foo\n```\nThis 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.\n\n### Security Considerations\n- Use tenant isolation for multi-tenant features\n- Implement proper authorization with `@HasAnyPermission`\n- Handle secrets securely (never log sensitive data)\n\n### Performance Best Practices\n- Implement pagination for large datasets\n- Use streaming for large file operations\n- Cache frequently accessed data appropriately\n- Initialize collections with the expected size to avoid resizing overhead\n\n## Troubleshooting\n\n- **A Gradle build fails with no visible cause:** run `./gradlew clean` and retry once. When it repeats, read the actual error instead of retrying again.\n- **Tests fail on connection errors:** the databases are not running. `docker compose -f docker-compose-ci.yml up -d`.\n- **The frontend build fails on dependency resolution:** check the Node and npm versions against `package.json` engines before touching anything else.\n- **`npm install` fails with `EBADENGINE`:** the UI workspaces require npm >= 11.7 and set `engine-strict=true`. Older npm mislabels the `sass`/`sass-embedded` platform binaries as peer dependencies and rewrites `package-lock.json` on every install. Install the pinned npm (`npm i -g npm@11.16.0`) or use the Node release named in `ui/.nvmrc`.\n- **The UI builds but the app never mounts:** module federation, so the cause is usually workspace resolution rather than the change itself. Verify against a running instance rather than against the dev server.\n- **Debug logging:** `--logging.level.io.kestra=DEBUG`.\n\n## Module Structure\n\n**Core Modules:**\n- `cli` - Command Line Interface\n- `core` - Core functionality\n- `webserver` - Web server\n- `ui` - Vue 3 frontend application\n- `executor` - The component responsible for managing execution state\n- `scheduler` - The component responsible for scheduling polling and schedule triggers\n- `worker` - The component that executes tasks and manages worker instances\n- `worker-controller` - The component that manages worker instances and job distribution\n- `indexer` - The component responsible for indexing executions\n- `platform` - provides the Platform Bill of Materials (BOM) for dependency management\n\n**Queuing Layer:**\n- `queue` - Core API for queue implementations\n- `queue-jdbc` - JDBC-based queue implementation\n\n**Data Layer:**\n- `jdbc-*` - Database implementations (H2, Postgres, MySQL)\n\n**Testing Modules:**\n- `e2e` - Playwright end-to-end suite, run against a Kestra docker image\n- `tests` - Common test utilities and base classes\n- `jmh-benchmark` - JMH benchmarks for performance testing\n\n**Key Patterns:**\n- Repository pattern for data access\n- Service layer for business logic\n- Controller layer for HTTP endpoints\n- Builder pattern for object construction (often with Lombok `@Builder`)\n\n## Pull request guidelines\n\n- **One PR, one scope.** When you cannot pick a single conventional-commit scope for the title, the PR is doing too much and should be split.\n- Title format `type(scope): lowercase description`, following https://www.conventionalcommits.org/en/v1.0.0. Name the changed things rather than the actions taken on them: `fix(cases): assignees header, missing Acknowledged status, auto-link toggle`.\n- Use types: chore, feat, fix, refactor, test, docs, build\n- 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\n- Put the closing keyword on the first line of the description, as a full URL so that the link is not lost in a squash merge: `Closes https://github.com/kestra-io/kestra/issues/<id>.`\n- Cover new behavior with tests that could fail, following [When a test earns its place](#when-a-test-earns-its-place). \"Add tests\" never means one test per changed line.\n- Run `npm run lint` in `ui/` before pushing any frontend change. Otherwise one comment per violation is posted by reviewdog, and the human review is buried under trailing-comma suggestions.\n- Attach a screenshot or a short recording for every user-visible change, taken against a running instance.\n- Keep the branch rebased on `develop` rather than merged.\n- Fill in `.github/pull_request_template.md`, and delete the checklist section that does not apply instead of leaving it unticked.\n- Write the description as problem, fix and evidence, once each. No table restating the diff, and no checklist of tests that all passed.\n- Never commit generated output by hand: the twelve non-English translation files, `fingerprints.json`, and the generated SDK are produced by their scripts or by CI.\n\n## Issue guidelines\n- **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`).\n- **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.\n- Leave triage labels such as `kind/cooldown` to `kestrabot`; it applies them automatically on new issues.\n\n## UI Translations\n\n**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. When the string embeds markup or a component (`<code>`, `router-link`, `KsId`), render it with `<i18n-t>` (vue-i18n's component interpolation) and one named slot per placeholder, keeping the markup out of the translation instead of rendering it with `v-html`. 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.\n\nTranslation 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`.\n\n### Checking for missing translations\n\nRun the check script from the `ui/` directory:\n\n```bash\ncd ui && npm run translations:check\n```\n\nA clean run reports `No missing keys.`, `No extra keys.` and `No stale keys.` for every language. Anything listed must be fixed before merging — the same check runs as a PR gate.\n\n> **Enterprise Edition:** EE-only keys live in `ui-ee/src/translations/ee_translations/en.json`, and a tenant type's own keys in `ui-ee/src/tenantTypes/<type>/translations/`, unprefixed and rooted under `tenantTypes.<type>` at runtime. Both are checked separately — run `npm run translations:check` in `ui-ee` as well (see `kestra-ee/AGENTS.md` → \"Frontend i18n\").\n\n### Editing English strings\n\n**Changing an existing English value is a translation change.** Every key carries a fingerprint of the English text its translations were generated from, so editing `en.json` — even just the capitalisation — marks that key stale in all twelve languages and fails `translations:check` until it is regenerated. Run `npm run translations:generate` and commit the result alongside your change.\n\nThis is deliberate: before it existed, edited values were never propagated, and a rename of \"SuperAdmin\" to \"Superadmin\" sat un-translated in eleven locales for a year (kestra-io/kestra#10656).\n\n### Adding or regenerating translations\n\nPrefer `npm run translations:generate` (needs `GOOGLE_CLOUD_PROJECT` and Application Default Credentials, since Gemini is reached through Vertex AI: run `gcloud auth application-default login` once); it fills missing keys and re-translates stale ones on its own, with no flag to remember. Pass `true` to force a full re-translation of everything.\n\nIf you must write a translation by hand:\n\n1. Identify gaps by running `npm run translations:check`.\n2. Follow these translation rules (mirroring `ui/scripts/translations/generateTranslations.ts`, the generator shared by OSS and EE):\n   - **Reserved English terms — never translate:** `kv store`, `namespace`, `tenant`, `flow`, `subflow`, `task`, `log`, `blueprint`, `id`, `trigger`, `label`, `key`, `value`, `input`, `output`, `port`, `worker`, `backfill`, `healthcheck`, `artifact`, `min`, `max`.\n   - **ALL-CAPS status labels stay in English:** `WARNING`, `FAILED`, `SUCCESS`, `PAUSED`, `RUNNING`, etc.\n   - **Preserve `{placeholder}` variables** exactly — vue-i18n uses a **single** pair of braces. Do not translate the name inside the braces, do not rename it, and never write `{{placeholder}}`: double braces are a compile error (`Not allowed nest placeholder`) and make `t()` throw at render time. Each translation must carry exactly the same placeholders as the English source — no invented ones, none dropped.\n   - **Use natural UI terminology** — avoid false friends or overly literal translations (e.g. German: Execution → Ausführung, Theme → Modus, State → Zustand).\n3. Insert the translated keys into the correct position in the target language JSON, mirroring the key order of `en.json`.\n4. Re-run `npm run translations:check` to confirm everything is clean before committing.\n\nThe tooling itself lives in `ui/scripts/translations/` and is shared with EE, which keeps only thin entry points. Rules live in `.mjs` so the dependency-free PR gate can apply them; file IO and orchestration stay in `.ts`.\n\n### Conflicts in `fingerprints.json`\n\nTwo branches that both touch `en.json` will both regenerate `ui/scripts/translations/fingerprints.json`, so it conflicts often. **Never hand-merge the hashes and never pick a side** — a hash says \"this English text is what the twelve translations were generated from\", so choosing the wrong one silently marks a drifted key as current and the drift becomes invisible again.\n\nResolve it the same way as a `kestra-sdk` conflict — regenerate:\n\n```bash\ngit checkout --ours ui/src/translations/*.json ui/scripts/translations/fingerprints*.json\ncd ui && npm run translations:generate   # fills whatever the other branch added\nnpm run translations:check               # must report no missing / extra / stale keys\n```\n\n`en.json` itself normally merges cleanly, since branches usually add different keys; it is the generated files that collide.\n\n## Keeping this file up to date\n\nWhen a review comment corrects something that is not written here, and the correction is a rule rather than a one-off, add it in the same PR: one or two lines, with the example next to the rule. Delete a rule once it no longer matches the codebase. A guideline that no longer matches the code is worse than no guideline, since it will be followed confidently.\n\nWhen in doubt, follow the existing patterns in the codebase and stay consistent with them.\n","isInternal":false,"tokens":7258,"sizeBytes":31051},{"name":"AGENTS.md","path":"ui/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/AGENTS.md","title":"ui - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# UI Design System Guidelines\n\nScope: 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.\n\nThe 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.\n\n## What this is, in plain terms\n\nThink of the design system as the product's **visual vocabulary**:\n\n- A short list of agreed-upon **colors**, **fonts**, and **spacings** (called *design tokens*).\n- A library of pre-built **components** (`KsButton`, `KsTable`, `KsDialog`, …) that already use those tokens.\n- 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.\n\nIf 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.\n\nUnder 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/`.\n\n> **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.\n\n## Golden rules (non-negotiable)\n\nThese rules are what keep the UI maintainable as it grows. Treat any deviation as a bug.\n\n**How Kestra looks is decided by the design system; what it does is decided by feature code.** A new visual pattern (a new card treatment, a different spacing rhythm, another status color, a bespoke empty state, a one-off modal layout) counts as a design-system change and belongs in `ui/packages/design-system/`, after a conversation with design. Approximating one inside a feature component is the fastest route to a screen that is fine in the one theme you checked it in and wrong everywhere else. When nothing in the catalogue below covers what you need, raise it as a gap in the design system instead of improvising around it.\n\n1. **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.\n2. **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.css` / `ks-theme-dark.css` / `ks-theme-dark-2.css` — do not pick a one-off color.\n3. **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 `--ks-font-family-*` and `--ks-font-size-*` tokens; feature code should not redefine them.\n4. **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.\n5. **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.\n6. **No magic numbers for theme values.** Spacing, radii, font sizes, and shadows must reference tokens — 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.css`](packages/design-system/src/assets/styles/ks-tokens.css)). 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)`).\n7. **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.\n8. **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.\n9. **Every new `Ks*` component needs a Storybook story and a unit test.** Stories double as living documentation for design and product reviewers.\n10. **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`.\n11. **Check that a token exists before using it.** With an invalid `var(--ks-…)` and no fallback, the property is silently inherited instead, so the mistake remains invisible until the computed style is measured. The cases feature was released using `--ks-font-size-medium`, `--ks-font-size-small`, `--ks-radius-2` and `--ks-border-active`, none of which are declared anywhere, and the interval filter's Apply-to row shipped with no visible selection for the same reason (kestra-io/kestra#18777).\n\n    Your editor underlines it: `kestra-tokens/no-undeclared-css-token` (eslint, `@eslint/css`) covers `.css` and plain `<style>` blocks, `kestra-tokens/no-undeclared-ks-token` (eslint) covers `cssVar(\"--ks-…\")` and tokens written inside strings, and `ks/no-undeclared-custom-property` (stylelint) covers the design system's remaining SCSS. All three propose a replacement, and `npm run lint` runs them all. See [scripts/tokens/README.md](scripts/tokens/README.md).\n12. **Copying an existing rule is not proof that it is correct.** Several hundred `:deep()` selectors, some hex codes and some raw pixel values are older than these rules and are being removed over time. Treat them as debt rather than as precedent: don't add more, and clean up the ones in the component you are already editing.\n13. **Bootstrap is gone; don't use its utility classes.** The `d-flex`, `mb-3` and similar classes still in the templates are kept alive by `packages/design-system/src/assets/styles/_utilities.css` until their migration issues land, and that file only shrinks: replace a utility with scoped CSS on `--ks-*` tokens and delete its rule once the last use is gone. `tests/unit/designSystem/bootstrapGuard.spec.ts` fails on a utility the file does not define and on any `--kbs-*` variable.\n\n## Best practices for keeping the design system healthy\n\nA 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.\n\n### Before you write code\n\n- Search the component tables and Storybook first. The most common waste in this codebase is rebuilding something that already exists.\n- 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.\"\n- For anything visible to a user, check both light and dark mode in Storybook before merging.\n\n### While you write code\n\n- 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.\n- Keep the style **inside** the SFC. `<style scoped src=\"./x.css\">` is valid and `scoped` still applies, but an external file separates the CSS from the markup it describes for no gain, and it is one more file to open. Block order is `template`, `script`, `style`, enforced by `vue/block-order` in `ui/eslint.config.js`.\n- Write styles as plain CSS: no `lang=\"scss\"` and no new `.scss` file outside `packages/design-system/` (kestra-io/kestra#21346). Native nesting works with `scoped`, but start a nested selector with `&` (`& :deep(.x)`, `& > *`), or Vue puts `[data-v-*]` on the nested part instead of the parent.\n- 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.\n- 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.\n- 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 }`).\n- Use semantic tokens, not raw colors. `var(--ks-text-link)` communicates intent; `var(--ks-text-blue-500)` does not exist for a reason.\n- Co-locate component-specific tokens (e.g. `--ks-card-shadow`) in the component's SCSS, but always derive them from semantic tokens.\n\n### When extending the design system\n\n- Component styles are plain CSS. A component that needs Element Plus's base styles keeps them in a `<style lang=\"scss\">` block holding only the `el-ns` and `theme-chalk` `@use` lines, placed before the plain `<style>` with its own rules, so each published component entry still ships its chalk CSS. Palette colours are `var(--ks-<family>-<step>)`, not `palette.$base-*`.\n- Don't write one rule per variant. When the tokens follow the variant's name (`--ks-status-<status>`), bind them with `v-bind()` and add a test that every name has its token, since the token lint cannot see a built name (`KsExecutionStatus`, `KsAlert`). When they don't, keep a typed table of `var(--ks-*)` strings in the component's `<script>`, where `kestra-tokens/no-undeclared-ks-token` still checks them, and bind that (`KsTag`, `KsButton`).\n- Only expose props that are actually used somewhere in the codebase. Speculative props rot.\n- Mirror Element Plus prop names where possible — predictability is a feature.\n- Pass `v-bind=\"$attrs\"` and forward slots so wrappers don't trap consumer extension points.\n- 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.\n- Document tokens in code comments next to where they're declared in `ks-theme-*.css`. The `scripts/generate-palette.mjs` file is auto-generated — don't hand-edit it.\n\n### When reviewing a UI PR\n\nReject (or ask to fix) anything that:\n\n- Imports from `element-plus` directly into `ui/src/`.\n- Uses a hex code, `rgb(...)`, `--el-*`, or `--bs-*` for color.\n- Uses `:deep()` to reach into a `Ks*` or `el-*` component.\n- Hardcodes pixel values for padding, margin, radii, font sizes, or shadows.\n- Adds a CSS class that overrides `.el-...` selectors.\n- Duplicates a component that already exists in the design system.\n- Adds a `Ks*` component without a Storybook story or test.\n- Mounts `KsDataTable` without binding `:currentPage` / `:pageSize` (or `v-model:currentPage` / `v-model:pageSize`) — pagination is controlled; see \"Data tables & pagination state\".\n- 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\".\n- Adds a modal/drawer where the user enters data without guarding accidental dismissal — see \"Unsaved input in modals (discard guard)\".\n\n### Accessibility\n\n- Every icon-only `KsIconButton` must have an accessible label (`aria-label` or `title`). Screen readers do not see the icon glyph.\n- Never convey state with color alone — pair status colors with an icon (`KsExecutionStatus` already does this) or a text label.\n- Use semantic HTML inside slots: real `<button>`, `<a>`, `<label>`, headings in document order. Don't fake interactivity with `<div @click>`.\n- `KsDialog`, `KsDrawer`, `KsPopover` already manage focus trap and `Escape`-to-close — don't reimplement these in feature code.\n- Keep tab order logical; rely on the DOM order rather than `tabindex` hacks.\n- 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.\n\n### Internationalization\n\n- No hardcoded user-facing strings. Always go through i18n.\n- **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.\n- Pass plurals and interpolation as named arguments to `$t(...)`, never string-concatenate.\n- A string that embeds markup or a component (`<code>`, `router-link`, `KsId`) renders through `<i18n-t keypath=\"...\" tag=\"span\" scope=\"global\">` (vue-i18n's component interpolation, registered globally, so there is nothing to import; `scope=\"global\"` names the scope every message lives in, or the component looks for a local one up the tree and warns; `kestra-i18n/i18n-t-scope` fails a tag without it and `npm run lint` adds it), with one named slot per placeholder: `Mark as {status}` becomes `<template #status><code>{{ item.code }}</code></template>`. Keep the markup out of the translation, so no `v-html` and no `escapeHtml` are needed, and translators can reorder the placeholders.\n- `splitTranslation(t, key, slot)` and markup passed as a named argument to `v-html` are older patterns for the same job. Don't add new uses; move a string to `<i18n-t>` when you touch it.\n- 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. `tests/unit/dateFormatGuard.spec.ts` (and its `ui-ee` twin) fails on `dayjs(...).format(`, `toLocaleString()` on a `Date`, and `Intl.DateTimeFormat` in `src/`; a file that serializes a date rather than displaying it goes in its `ALLOWED` list with the reason.\n- Date arithmetic goes through the `dayjs` re-exported by the design system. `dayjs` objects are **immutable**, so `d.add(1, \"day\")` returns a new instance and mutating idioms (`cursor.add(...)` in a loop) silently no-op — reassign instead. Parse untrusted input with `dateUtils.parseIso()`: bare `dayjs()` happily reads an epoch number or a bare year as a date.\n- 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.\n\n### Loading, empty, and error states\n\nEvery async surface must render all four states. \"Happy path only\" is a bug.\n\n- **Loading:** `KsSkeleton` for content placeholders; `vKsLoading` directive for sections that already have layout; `KsLoading` component for full-page or container-level spinners.\n- **Empty:** `KsEmpty` with an action where possible — never a blank screen.\n- **Error:** `KsAlert type=\"error\"` with retry affordance, or `KsMessage` for transient errors.\n- **Success / data:** the actual content.\n\n### Data tables & pagination state\n\n`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.\n\n**The contract:**\n\n- Bind `:currentPage` / `:pageSize` (one-way) OR use `v-model:currentPage` / `v-model:pageSize` (two-way).\n- 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)})`.\n- 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.\n- `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.\n- When a column's `prop` is not the backend sort key (e.g. `auditLog.detail.resourceType` vs `detail.resourceType`), pass `:sortKeyMapper` (`(key: string) => string`); a sort click reloads immediately with the raw column prop otherwise, and the backend rejects it with a 422.\n\n**URL-driven pattern** — the default for top-level list pages (Logs, Flows, Executions, KV, Secrets, Triggers, FlowsSearch, Blueprints):\n\n```vue\n<KsDataTable\n    :loadData=\"loadData\"\n    :currentPage=\"urlPage\"\n    :pageSize=\"urlSize\"\n    :total=\"store.total\"\n    @page-changed=\"({page, size}) => router.push({query: {...route.query, page: String(page), size: String(size)}})\"\n/>\n\n<script setup>\nconst urlPage = computed(() => Number(route.query.page) || 1)\nconst urlSize = computed(() => Number(route.query.size) || 25)\n</script>\n```\n\n**Local-state pattern** — for embedded tables that should not appear in the URL (MetricsTable, side-panel views):\n\n```vue\n<KsDataTable\n    v-model:currentPage=\"currentPage\"\n    v-model:pageSize=\"pageSize\"\n    :loadData=\"loadData\"\n    :total=\"...\"\n/>\n\n<script setup>\nconst currentPage = ref(1)\nconst pageSize = ref(25)\n</script>\n```\n\n**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.\n\n**Defaults that land in the URL after mount** — a page whose filter defaults are written by a `router.replace` (e.g. the Logs page's default level, via `useRouteFilterPolicy`) must gate its first `loadData` on `isRouteSettled`. `KsDataTable` loads on mount, so without the gate the page fires a request for the bare query, throws that result away when the defaults arrive, and leaves the two responses racing each other. Gating means the query change is what triggers the first load, so also handle the navigation never landing: `isRouteSettled` gives up after a timeout, and the page must reload when it does — otherwise the list stays empty for good, with no request in flight and nothing to retry it.\n\n**Stores that back a filterable list** must ignore superseded responses: keep a sequence number per search and drop a response whose sequence is no longer the newest (see `stores/logs.ts`). Otherwise the response that happens to land last wins and the list can show results for filters the user already left — or nothing at all, when the stale search matched nothing.\n\n### The deep-watch / computed-spread trap\n\nA `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.\n\nThis 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.\n\n**Don't:**\n\n```ts\nconst filterQuery = computed(() => {\n    const {page: _p, size: _s, sort: _so, ...filters} = route.query\n    return filters  // new object reference on every route.query change\n})\nwatch(filterQuery, () => dataTable.value?.resetAndReload(), {deep: true})\n// Fires on every route.query change — page clicks, sort clicks, anything —\n// and bounces the user back to page 1.\n```\n\n**Do:**\n\n```ts\nconst filterQueryKey = computed(() => {\n    const {page: _p, size: _s, sort: _so, ...filters} = route.query\n    return JSON.stringify(filters)  // stable string — same content, same value\n})\nwatch(filterQueryKey, () => dataTable.value?.resetAndReload())\n// Fires only when filter content actually changes.\n```\n\nThe 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.\n\n### Router guards and `initApp`\n\nGuards are wired through `initApp`, and three things about that are easy to get wrong.\n\n**Register before the router installs.** `app.use(router)` starts the first navigation, and `vue-router` reads `beforeGuards.list()` once that navigation reaches its guard phase. A guard registered synchronously after `app.use(router)` is still picked up; one registered after an `await` is not, and `initApp` awaits i18n and the moment locale between installing the router and returning. A guard added in `initApp(...).then(...)` therefore misses the first navigation, which on a cold load is the only navigation there is. Pass it through the `guards` argument instead.\n\n`afterEach` is in the same window, for a less obvious reason: `afterGuards.list()` is read when a navigation *finishes*, and the first navigation can finish while `initApp` is still awaiting. An `afterEach` registered after those awaits therefore never sees it either, which silently costs anything keyed on the first navigation (`eventsRouter` attributes the landing page to `document.referrer` on exactly that hop).\n\n**A guard's arity decides how `vue-router` reads it.** `initApp` registers guards as `guards.beforeEach.bind(null, router)`, so a `(router, to, from)` guard arrives bound with `length` 2 and stays in return-value mode, where returning a route object redirects. Add a fourth parameter and the bound length becomes 3, which flips `vue-router` into `next()`-callback mode: the returned redirect is ignored and the navigation hangs with no error. Keep guards at `(router, to, from)`.\n\n**There is one `beforeEach` slot.** OSS spends it on `tenantGuard` and can only do so because its auth guard is a `beforeResolve`; EE spends it on `authGuard`. A second `beforeEach` has to be composed into the existing one, or `initApp` has to grow to accept a list, rather than being registered afterwards, since \"afterwards\" is exactly the case that misses the first navigation.\n\n### Session loss and an unreachable server\n\nA 401 on a logged-out session opens `ReauthDialog` through `requestReauth(signIn)` and replays the failed request once the user signs in, so the page and its unsaved state survive; the edition supplies its own `signIn` from `onUnauthorized`. A request that fails without a response (or behind a 502/503/504) raises `ServerUnreachableBanner`, which clears on the next response. Don't add per-page handling for either, and don't clear `unsavedChange` before the user has chosen to leave. The native basic-auth dialog is suppressed for fetch and XHR only: `EventSource` cannot set headers and relies on `Sec-Fetch-Dest`, which browsers send only over HTTPS or localhost, so on plain HTTP its streams can still open the dialog.\n\n### Unsaved input in modals (discard guard)\n\nAny modal/drawer where the user **enters data** must not silently lose it on an accidental dismissal. `KsDialog` and `KsDrawer` take a `dirty` prop and ask for confirmation themselves; never reimplement the confirm-before-discard logic per modal.\n\n```vue\n<KsDialog v-model=\"visible\" :dirty=\"isDirty\" ... />\n<KsDrawer  v-model=\"visible\" :dirty=\"isDirty\" ... />\n```\n\n```ts\n// isDirty: true when there is unsaved input worth a prompt, usually a comparison against a snapshot taken on open\nconst baseline = ref(\"\")\nconst isDirty = computed(() => JSON.stringify(form.value) !== baseline.value)\n```\n\n`dirtyMessage` replaces the default confirmation text when the discarded thing is not a form (`TriggerFlow.vue` uses it for an unsubmitted execution). When the close also has to run cleanup, keep `:beforeClose=\"(done) => { reset(); done() }\"` next to `:dirty`: the confirmation runs first, `beforeClose` only once the user agreed. The composable behind the prop, `useDiscardGuard(isDirty, {message?})` from `@kestra-io/design-system`, stays available for closes that do not go through a `Ks*` container.\n\nRules:\n- **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.\n- **`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.\n- **`KsDialog` and `KsDrawer` both expose `dirty` and 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.)\n- **Don't guard** read-only viewers, action/confirmation dialogs, or ephemeral forms that reset on every open.\n\n### Icons\n\n- All icons come from [`vue-material-design-icons`](https://github.com/robcresswell/vue-material-design-icons) via `<KsIcon>` (or `<KsIconButton>` for clickable icons).\n- 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.\n- Pass `name` (the kebab-case Material name); size and color come from props or the surrounding token context — don't override with inline `style`.\n- **Two file-type icon sets coexist on purpose, so don't merge them.** `fileUtils.fileIcon()` (behind `KsFileTag`) maps an extension to a monochrome `vue-material-design-icons` component, which inherits `--ks-icon-*` and so recolors per theme and per tag variant. `ui/src/components/utils/icons/Type.vue` renders the colored `material-file-icons` SVGs for the namespace file explorer, where the brand colors are the point. That package is a `ui/` dependency the design system does not have, and it bakes its colors into a base64 `<img>` that no token can reach, so it cannot be used from a `Ks*` component.\n\n### Performance\n\n- Lazy-load heavy surfaces: `KsEchart`, `KsLine`, `KsBar`, `KsPie`, `KsGraph`, `KsMarkdown`, code-editor surfaces. Use `defineAsyncComponent` or route-level code splitting.\n- Prefer `v-show` for frequent toggles (tabs, filters), `v-if` for rare/heavy mounts (modals, big tables).\n- Pass stable `key` props in lists. Avoid index-based keys when items have IDs.\n- Don't render giant tables without `KsDataTable`'s pagination/virtualization — server-side paging is the default for anything that can grow.\n- Watch out for `watch(..., { deep: true })` and `computed` with object identity — they often re-run more than you expect.\n\n### Types\n\n**Never write `any`.** TypeScript stops checking a value the moment it is typed `any`, so a typo or a renamed field is found by whoever opens the page instead of by the compiler. The rule covers every spelling: `(row: any)`, `x as any`, `any[]`, `Record<string, any>`, in the `<script>` block and in template expressions alike.\n\nWhere the type comes from, in this order:\n\n1. **Data from the backend: `kestra-sdk`.** It is generated from our OpenAPI spec, so the type already exists and stays in step with the API. `import type {Execution} from \"@kestra-io/kestra-sdk\"`.\n2. **Data from a library** (vue-router, element-plus, monaco, echarts, the DOM): the library's own types. When a package ships none, add a `declare module` file next to `src/material-icons.d.ts` rather than reaching for `any`.\n3. **An interface the app already has.** Search for one before writing another.\n4. **A new interface**, only when none of the above fit.\n\nFor a value whose shape really is not known yet, `unknown` with a narrowing check is the honest escape hatch. `any` is not.\n\n**A legitimate `any`, or a package with no types?** Give the package a small `.d.ts` shim next to `src/material-icons.d.ts`. An `any` that truly cannot be avoided is a conversation with a maintainer, not a number you change on your own.\n\n**MANDATORY — never raise the baseline to make the check pass.** A bigger number hides the new `any` from every later run, which is the one thing the baseline exists to prevent, and it will be treated as a bug in review. Specifically, and this applies to coding agents as much as to people:\n\n- Do not hand-edit `scripts/explicit-any/baseline.json`. The only writes to it come from the check itself.\n- Do not run `--accept-new-any`. A maintainer who has already agreed to a raise records it with `npm run check:ts-any -- --write --accept-new-any`; it is never a way to get a green check.\n- A red `check:ts-any` is fixed by typing the value, not by making the check agree with the code.\n- When you cannot type it, stop. Leave the check red, say in the PR which value defeated you and why, and let a maintainer decide. An unfinished PR is fine; a silently raised baseline is not.\n\n**The check.** `npm run check:ts-any` counts the explicit `any` per file, oxlint for the `<script>` block and the Vue compiler for template expressions, and compares the counts with `scripts/explicit-any/baseline.json`, which records what was already in the tree when the rule came in. The same check runs on every PR, as `Npm - check ts-any`. It fails in two directions and the message says which:\n\n- ``New `any` in 1 file(s)`` with a line like `src/utils/filters.ts: 1 -> 2`. Your change added one. Type it with the order above.\n- `The baseline is out of date` with `src/utils/filters.ts: 3 -> 2`, or `moved from …` when you renamed a file. Nothing got worse, the baseline just has to follow the code. Run `npm run check:ts-any -- --write` and commit it alongside your change. A rename is followed whether it is staged or already committed on your branch, so the count travels with the file either way.\n\nInstall the repo hooks once with `.github/.hooks/setup_hooks.sh` and the second case stops happening: the pre-commit hook lowers the baseline and stages it with the rest of your commit.\n\n### Testing UI\n\n- Unit tests with **Vitest** + `@vue/test-utils`, and Storybook stories, live next to the file they cover: `src/components/logs/Foo.vue` has `Foo.spec.ts` and `Foo.stories.ts` beside it. A spec or story about one aspect of a file is named `<file>.<focus>` (`Foo.download.spec.ts`, `flow.draftSave.spec.ts`) so it sorts next to its subject; one that exercises several modules together keeps a descriptive name in their folder (`entityLoadRace.spec.ts`). `tests/unit/` keeps only the setup, the shared helpers (`i18nMount.ts`) and the repo-wide guards; `tests/storybook/` keeps only shared fixtures and the stories with no single source file. The `design-system` and `topology` packages follow the same rule with `*.test.ts`, their `tests/` keeping only setup and helpers. `tests/unit/specLocationGuard.spec.ts` fails on a spec or story left in any of these `tests/` folders for a file that exists in `src/`. Playwright specs stay in the `e2e/` package: they exercise the running app, not one source file.\n- Mount through `i18nMount` or `i18nShallowMount` rather than calling `mount` with your own `createI18n`. Pass `messages` for the keys the spec asserts on, or `locales: en` when it needs the real `en.json`; with neither, `t(\"key\")` renders the key. Missing-key warnings are off in both helpers, so a spec asserting on raw keys stays quiet. Two copies exist and behave the same: specs under `src/`, `tests/unit/` and `packages/topology/src/` import `tests/unit/i18nMount.ts`, and design-system specs import `packages/design-system/tests/units/i18nMount.ts`. Both install the design system, as the app does at bootstrap, so a mounted `Ks*` component resolves instead of warning; a spec that mocks `@kestra-io/design-system` away has to keep a `default` export for that install, such as `default: {install: () => {}}`. Only a spec that never mounts anything, such as one testing pluralisation on the instance itself, builds its own i18n.\n- `i18nMount` also installs a fresh `memoryRouter()` (`tests/unit/memoryRouter.ts`), so `useRoute()` / `useRouter()` resolve instead of warning; a spec that asserts on navigation passes its own router in `global.plugins`, which replaces the default, or mocks `vue-router`. A router already in test-utils' global `config.global.plugins` (ui-ee's setup installs one) also replaces it. A spec mounting with plain `mount` passes `memoryRouter()` itself. A spec that creates a store outside a component activates `routedPinia()` from the same file instead of a bare `createPinia()`, so a store calling `useRoute()` at setup resolves it there too.\n- A spec that mounts a no-code field (`TaskObject`, `TaskObjectField`, `TaskArray`, `Add`, `TaskEdit`) ends with `afterAll(() => taskComponentsReady())` from `useBlockComponent.ts`: the block components are imported on mount, and with `isolate: false` an import still in flight when the file ends is reported as an `EnvironmentTeardownError` for the whole run.\n- A component passed inside `props` (a tab's `component`, an `icon`) is wrapped in `markRaw`: test-utils makes the props object reactive, and Vue warns about a component that was made reactive on every render.\n- Check a spec's console with `npx vitest run --project=unit --silent=false <file>`. Vitest picks a minimal reporter in agent and non-interactive shells that hides the output of passing tests, so a warning CI prints can be invisible locally.\n- A test that exercises a failure path the code reports with `console.error` stubs it (`vi.spyOn(console, \"error\").mockImplementation(() => {})`) and restores it, and ideally asserts on the call; output from a passing test is noise that hides the warning worth reading.\n- 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.\n- 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.\n- Visual regressions caught in Storybook are cheaper to fix than caught in production.\n- A test only earns its place when it can fail for a reason a reviewer would care about. Don't test that a prop is passed through, that a computed returns its own input, or that a `Ks*` component renders: the template is only restated by those assertions, and they are broken by every refactor. Delete a Vitest test once a story covers the same behavior, and say so in the PR description.\n\n### Checking a frontend change before pushing\n\n```bash\nnpm run check:types && npm run test:unit && npm run lint\n```\n\n`npm run lint` is not optional. Without it, one PR comment per eslint violation is posted by reviewdog (missing trailing commas, mostly) and the human review is buried underneath them.\n\n`npm run check:ts-any` compares the explicit `any` per file against `scripts/explicit-any/baseline.json`. It fails when a file gains one, so type it instead of raising the number. It also fails when a file loses one, because the baseline has to come down with the code: run `npm run check:ts-any -- --write` and commit the smaller numbers, or install the repo's git hooks (`.github/.hooks/setup_hooks.sh`) and the pre-commit hook does it for you. `--write` only ever lowers; it refuses to raise a count.\n\nColour is gated automatically. `tests/unit/designSystem/colorGuard.spec.ts` fails on any hex, `rgb()`, `hsl()`, `oklch()`, `--el-*`, `--bs-*`, SCSS colour variable or bare keyword colour reaching a colour property anywhere under `ui/src` and `ui/packages/topology/src`, skipping `*.spec.ts`, `*.test.ts` and `*.stories.ts`, and prints `path:line (colours)` for every offending line. It runs with `npm run test:unit` and on every PR. Replace what it reports with a `--ks-*` token, a `Ks*` prop, or a change in the design system.\n\nTwo escape hatches exist, each taking a reason, for the case the guard cannot judge: artwork whose colours **are** the asset (a brand mark, a third-party logo) rather than a themed surface. Nothing else qualifies, and a hardcoded colour that a token could carry is a bug whether or not the guard is silenced. There is deliberately no whole-file opt-out, and a `-start` with no `-end` is reported instead of muting the rest of the file.\n\n```scss\n/* design-system-disable-next-line: the Kestra mark is fixed artwork */\n/* design-system-disable-start: … */  /* … */  /* design-system-disable-end */\n```\n\nThe guard reads `.vue`, `.scss`, `.css`, `.ts` and `.js`; in a `.vue` file it reads `<style>` blocks plus `fill=` / `stroke=` attributes, and in a script it reads a hex under a colour-named key (`colorHex: \"#…\"`), so a `//`, `/* */` or `<!-- -->` comment all work.\n\nSpacing, radii and `:deep()` are not gated, so still read your own diff for those:\n\n```bash\ngit diff -U0 develop... -- '*.vue' '*.scss' \\\n  | grep -nE '^\\+.*(:deep\\(|(padding|margin|gap|border-radius|font-size)[[:space:]]*:[[:space:]]*[0-9]+px)'\n```\n\nFor a user-visible change, also check it in light **and** dark mode, and attach a screenshot to the PR.\n\n### Deprecation contract\n\nWhen retiring a `Ks*` component, prop, or token:\n\n1. Mark with a `@deprecated` JSDoc tag *and* a one-line replacement path: `@deprecated since 0.x — use <KsNewThing> instead`.\n2. Keep it working for at least one minor release; add a `console.warn` in dev mode if the cost is reasonable.\n3. Migrate all callers in the same release where feasible — don't leave half-migrations.\n4. Only delete after the deprecation window. A silent removal breaks downstream EE / plugin code.\n\n## Anti-patterns (do not write these)\n\n```vue\n<!-- Wrong: raw element-plus, hex color, :deep, SCSS variable in feature code -->\n<template>\n    <el-button class=\"my-btn\">Save</el-button>\n</template>\n<style lang=\"scss\" scoped>\n    .my-btn {\n        background: #8405ff;\n        font-size: $font-size-md;\n    }\n    :deep(.el-button__text) { color: white; }\n</style>\n```\n\n```vue\n<!-- Right: Ks component, semantic tokens, no deep selector, i18n -->\n<template>\n    <KsButton type=\"primary\">{{ t(\"save\") }}</KsButton>\n</template>\n<style scoped>\n    /* Almost always: no custom CSS is needed at all. */\n</style>\n```\n\nIf your `<style>` block needs to exist:\n\n```css\n/* Right: --ks-* tokens, no SCSS vars in feature code, no :deep */\n.my-feature {\n    background: var(--ks-bg-surface);\n    color: var(--ks-text-primary);\n    border: 1px solid var(--ks-border-default);\n}\n```\n\n## Components\n\n### Basic / Layout\n\n| Component | Purpose |\n|-----------|---------|\n| `KsButton` / `KsButtonGroup` | Primary action button and grouped buttons; `--ks-button-content-width` stretches the content (default `auto`); `block` stretches the button to its container's width |\n| `KsIcon` / `KsIconButton` | Material Design icon display; icon-only button (always with `aria-label`) |\n| `KsLink` | Styled hyperlink |\n| `KsText` | Typography wrapper — preferred over raw `<span>` / `<p>` for theme-aware text |\n| `KsScrollbar` | Custom-styled scrollbar wrapper |\n| `KsContainer` / `KsHeader` / `KsMain` | Page layout shell |\n| `KsRow` / `KsCol` | Responsive grid |\n| `KsSplitter` / `KsSplitterPanel` | Resizable split-pane layout |\n\n### Feedback\n\n| Component | Purpose |\n|-----------|---------|\n| `KsAlert` | Alert banner for messages and status feedback. Pass `banner` for a full-width system bar (licence, impersonation): square, bottom rule only, body text in `--ks-text-primary` so it reads at AA on the tint |\n| `KsDialog` | Modal dialog (handles focus trap + Escape); `dirty` asks before an accidental close; content that cannot fit the default width widens it to 1024px with a `data-dialog-wide` attribute on an element inside it, so a step that renders it is wide and the others are not |\n| `KsDrawer` | Side drawer / panel; `dirty` asks before an accidental close |\n| `KsTooltip` | Hover tooltip |\n| `KsPopover` | Popover for contextual content |\n| `KsLoading` (`vKsLoading`) | Loading spinner directive |\n| `KsMessage` | Toast notification service |\n| `KsNotification` | Notification service |\n| `KsMessageBox` | Confirmation dialog service |\n\n### Form\n\n| Component | Purpose |\n|-----------|---------|\n| `KsInput` / `KsPassword` | Text and password inputs |\n| `KsInputNumber` | Numeric input with increment / decrement |\n| `KsSelect` / `KsOption` / `KsOptionGroup` | Dropdown select |\n| `KsAutocomplete` | Autocomplete input with suggestions |\n| `KsCheckbox` / `KsCheckboxGroup` / `KsCheckboxButton` | Checkbox variants |\n| `KsRadio` / `KsRadioGroup` / `KsRadioButton` | Radio button variants |\n| `KsRadioCardGroup` | Single-select radio group rendered as option cards (title + optional hint/icon/disabled); options-driven via `:options` + `v-model` |\n| `KsSwitch` | Toggle switch |\n| `KsThemePicker` | Theme chooser, one miniature of the app per theme painted in that theme's own colours; options-driven via `:options` + `v-model`. A `#preview=\"{option}\"` slot replaces the built-in miniature, `columns` lays the options out as a grid, `ariaLabel` names the radiogroup |\n| `KsDatePicker` / `KsTimePicker` | Date and time pickers |\n| `KsColorPicker` | Color picker; `alphaOnSurface` shows translucent colours over the page surface instead of a checkerboard |\n| `KsDurationPicker` | ISO 8601 duration picker |\n| `KsCascaderPanel` | Cascading hierarchical selector |\n| `KsUpload` | File upload |\n| `KsForm` / `KsFormItem` | Form container with validation |\n\n### Data Display\n\n| Component | Purpose |\n|-----------|---------|\n| `KsCard` | Card container |\n| `KsTable` / `KsTableColumn` | Basic table |\n| `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\". Pass `mobileColumns` (`{leading?, title, titleAside?, meta?, metaAside?, actions?}`, each naming a column `prop`, or the `columnKey` of a prop-less actions column) to render a card per row below 768px: `leading` is a status dot, `title` is bold, `titleAside` sits on its right, `meta` columns share the second line joined by a dot and `metaAside` closes it; `actions` is a corner cell on the first line; unlisted columns are hidden, bulk selection is desktop-only (the selection column is not shown on cards), and the regular table returns from 768px. |\n| `KsEntityLink` | Clickable cross-entity reference (namespace / flow) for table cells — neutral tag with leading icon, violet on hover. Pass `noIcon` in dense embedded tables (e.g. dashboard chart tables, ~90px columns) where the icon's 20px costs more than it tells |\n| `KsBadge` | Small indicator badge; `size=\"small\"` shrinks the pill so it sits on an icon button instead of covering it |\n| `KsNewBadge` | Compact uppercase \"NEW\" pill flagging a newly shipped feature — caller supplies the label via the default slot |\n| `KsTag` / `KsCheckTag` | Tag / label; clickable checkbox-style tag. Pass `truncate` to clip a long label with an ellipsis instead of letting the tag outgrow its container |\n| `KsAvatar` | Avatar with fallback |\n| `KsProgress` | Progress bar |\n| `KsPagination` | Pagination controls |\n| `KsEmpty` | Empty state placeholder |\n| `KsSkeleton` | Skeleton loader |\n| `KsId` | Copyable ID display |\n| `KsDateAgo` | Relative time display (\"2 hours ago\") |\n| `KsSegmented` | Segmented control; object options may carry an `icon` component, rendered before the label. `variant=\"neutral\"` swaps the primary-coloured selection for a grey one with wider segments |\n| `KsCollapse` / `KsCollapseItem` | Collapsible sections |\n| `KsFileTag` | Storage URI rendered as a file reference: a `KsTag` whose symbol comes from the extension, with a readable name (`name`, defaulting to the URI's last segment); the full URI stays in the tooltip |\n| `KsTree` | Hierarchical tree view |\n| `KsJsonTree` | Read-only JSON tree viewer; leaves holding a storage URI render through `KsFileTag` |\n| `KsTimeline` / `KsTimelineItem` | Timeline visualization |\n| `KsExecutionStatus` | Execution / task status badge with icon and color; `dot` collapses it to a coloured dot (label kept for screen readers) for dense layouts such as mobile cards |\n| `KsCodeStatus` | Compact validity badge with icon (`valid` / `error`) — caller supplies the label |\n| `KsMarkdown` | Markdown renderer (lazy-load on heavy surfaces) |\n\n### Charts\n\n| Component | Purpose |\n|-----------|---------|\n| `KsEchart` | ECharts base wrapper (lazy-load) |\n| `KsLine` / `KsBar` / `KsPie` | Line, bar, and pie charts (lazy-load) |\n| `KsGraph` | Graph / network visualization (lazy-load) |\n\n### Navigation\n\n| Component | Purpose |\n|-----------|---------|\n| `KsTabs` / `KsTabPane` | Tabbed interface |\n| `KsMenu` / `KsMenuItem` | Hierarchical menu |\n| `KsDropdown` / `KsDropdownMenu` / `KsDropdownItem` | Dropdown menu; pass `danger` on an item to give a destructive or exit action (delete, log out) the error-coloured hover |\n| `KsTopNavBar` | Top navigation bar; `titleSiblings` reaches the breadcrumb's current page |\n| `KsSideBar` / `KsSideBarSection` / `KsSideBarItem` | Left sidebar shell (header / scrollable body / footer slots), section with title, and styled link primitive with icon, active and locked states |\n| `KsBreadcrumb` / `KsBreadcrumbItem` | Breadcrumb navigation. An item's `siblings` loader (or `children` on the first item, and the `titleSiblings` prop for the current page) adds a chevron whose hover menu lists that level, headed by the level above (its `scope` when the label is not the right name for its content); an entry with a `children` loader flies out its own content to the right, as deep as the tree goes. Level loaders run as soon as the bar renders and an empty result shows no chevron; fly-outs load on hover. Namespace and flow pages build theirs with `useNamespaceBreadcrumb` |\n| `KsSteps` / `KsStep` | Step / wizard progress indicator |\n\n## Utilities (import from the design system)\n\n- `State`, `STATES`, `LOG_LEVELS` — execution state constants and icons. A state's colour is not a property of the state: read it with `State.getStateColor(name)`, which resolves `--ks-status-*`\n- `cssVar(name, opacity?)` — read a `--ks-*` CSS custom property at runtime (use this in JS / chart configs instead of hardcoding hex)\n- `dayjs` — the one configured dayjs instance (utc, timezone, duration, advancedFormat, calendar, isoWeek, localizedFormat, minMax, relativeTime, weekOfYear, isSameOrBefore). Never `import dayjs from \"dayjs\"` in feature code: plugins are registered on this instance, so a bare import silently lacks them\n- `dateUtils` — `dateFilter()`, `parseIso()`, `toIsoKeepOffset()`, `currentTimezone()`, `timezonesWithOffset()`, `currentLocale()`, `setLocale()`, `DATE_FORMAT_STORAGE_KEY`, `TIMEZONE_STORAGE_KEY`\n- `durationUtils` — `duration()`, `isValidDuration()`, `humanDuration()` — ISO 8601 ↔ ms and human-readable\n- `stringUtils` — `afterLastDot()`\n- `escapeHtml()`, `cloneDeep()`, `deepMerge()`, `isDeepEqual()`, `isPlainObject()`, `getPath()`, `setPath()`, `groupBy()`, `mapValues()`, `debounce()`, `throttle()` — the general-purpose helpers that replaced lodash. `debounce`/`throttle` return a function carrying `cancel()` and `flush()`. Do not add `lodash` back for any of these\n- `fileUtils` — `isFileUri()`, `fileName()`, `fileExtension()`, `fileIcon()` — storage-URI detection and the file symbol used by `KsFileTag`\n- `flowYamlUtils` — YAML parsing / manipulation for flow definitions\n- `Comparators` — enum of filter comparison operators\n- Filter helpers — `decodeSearchParams()`, `encodeFiltersToQuery()`, `getUniqueFilters()`, etc.\n- `applyDefaultFilters()`, `useRouteFilterPolicy()` — filter composables\n- `designSystemLocale`, `setDesignSystemLocale`, `registerDesignSystemI18n` — i18n\n- `designSystemI18nReady()` — the locale registration the plugin's `install` started, to await instead of leaving it in flight (the unit setup awaits it after each test)\n\n## Composables\n\n- `useTheme()` — detects and tracks dark / light mode via MutationObserver. Use this instead of reading `document.documentElement` yourself.\n- `useFilters`, `useSavedFilters`, `useDefaultFilter`, `usePreAppliedFilters`, `useRouteFilterPolicy`, `useTableColumns`, `useDataOptions`, `useDragAndDrop`, `usePeriodicRefresh` — data-table filter composables\n- `useDiscardGuard(isDirty, {message?})` — confirm-before-discard behind the `dirty` prop of `KsDialog` / `KsDrawer`; see \"Unsaved input in modals (discard guard)\"\n- `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.\n\n## Design tokens\n\nTokens are CSS custom properties declared in [`ks-theme-light.css`](packages/design-system/src/assets/styles/ks-the","isInternal":false,"tokens":12503,"sizeBytes":50152},{"name":"package.json","path":"ui/packages/design-system/package.json","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/package.json","title":"Design-system Skill","category":"anthropic-skill","format":"json","content":"# Design-system Skill\n\nPath: `ui/packages/design-system/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/package.json)","tokens":300,"sizeBytes":195},{"name":"Figma.json","path":"ui/packages/design-system/scripts/Figma.json","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/scripts/Figma.json","title":"Scripts Skill","category":"anthropic-skill","format":"json","content":"# Scripts Skill\n\nPath: `ui/packages/design-system/scripts/Figma.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/scripts/Figma.json)","tokens":300,"sizeBytes":201},{"name":"Color-variables.json","path":"ui/packages/design-system/tests/storybook/Basic/Color-variables.json","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tests/storybook/Basic/Color-variables.json","title":"Basic Skill","category":"anthropic-skill","format":"json","content":"# Basic Skill\n\nPath: `ui/packages/design-system/tests/storybook/Basic/Color-variables.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tests/storybook/Basic/Color-variables.json)","tokens":300,"sizeBytes":247},{"name":"tsconfig.app.json","path":"ui/packages/design-system/tsconfig.app.json","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.app.json","title":"Design-system Skill","category":"anthropic-skill","format":"json","content":"# Design-system Skill\n\nPath: `ui/packages/design-system/tsconfig.app.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.app.json)","tokens":300,"sizeBytes":205},{"name":"tsconfig.json","path":"ui/packages/design-system/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.json","title":"Design-system Skill","category":"anthropic-skill","format":"json","content":"# Design-system Skill\n\nPath: `ui/packages/design-system/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.json)","tokens":300,"sizeBytes":197},{"name":"tsconfig.node.json","path":"ui/packages/design-system/tsconfig.node.json","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.node.json","title":"Design-system Skill","category":"anthropic-skill","format":"json","content":"# Design-system Skill\n\nPath: `ui/packages/design-system/tsconfig.node.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.node.json)","tokens":300,"sizeBytes":207},{"name":"tsconfig.storybook.json","path":"ui/packages/design-system/tsconfig.storybook.json","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.storybook.json","title":"Design-system Skill","category":"anthropic-skill","format":"json","content":"# Design-system Skill\n\nPath: `ui/packages/design-system/tsconfig.storybook.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.storybook.json)","tokens":300,"sizeBytes":217},{"name":"web-types.json","path":"ui/packages/design-system/web-types.json","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/web-types.json","title":"Design-system Skill","category":"anthropic-skill","format":"json","content":"# Design-system Skill\n\nPath: `ui/packages/design-system/web-types.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/web-types.json)","tokens":300,"sizeBytes":199},{"name":"package.json","path":"ui/packages/hey-api-plugin/package.json","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/hey-api-plugin/package.json","title":"@kestra-io/hey-api-plugin","category":"anthropic-skill","format":"json","content":"{\n  \"name\": \"@kestra-io/hey-api-plugin\",\n  \"version\": \"0.4.0\",\n  \"description\": \"Kestra's shared SDK tooling. Two entry points: the '.' entry is the @hey-api/openapi-ts generator plugin (generation-time) that turns a Kestra OpenAPI spec into tenant-aware, human-friendly SDK wrappers; the './runtime' entry is createConfigureClient — the universal fetch-based client setup every Kestra SDK ships. Single source of truth used by the OSS UI SDK, the EE UI SDK, and the client-sdk repo.\",\n  \"license\": \"Apache-2.0\",\n  \"type\": \"module\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.mts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.mts\",\n      \"default\": \"./dist/index.mjs\"\n    },\n    \"./runtime\": {\n      \"types\": \"./dist/runtime.d.ts\",\n      \"default\": \"./dist/runtime.js\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"tsdown\",\n    \"prepack\": \"tsdown\",\n    \"check:types\": \"tsc --noEmit -p tsconfig.json\"\n  },\n  \"peerDependencies\": {\n    \"@hey-api/openapi-ts\": \">=0.97.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"@hey-api/openapi-ts\": {\n      \"optional\": true\n    }\n  },\n  \"devDependencies\": {\n    \"@hey-api/openapi-ts\": \"^0.99.0\",\n    \"@types/node\": \"^26.6.5\",\n    \"tsdown\": \"^0.23.1\",\n    \"typescript\": \"^6\",\n    \"unrun\": \"^0.3.1\"\n  }\n}\n","frontmatter":{"name":"@kestra-io/hey-api-plugin","version":"0.4.0","description":"Kestra's shared SDK tooling. Two entry points: the '.' entry is the @hey-api/openapi-ts generator plugin (generation-time) that turns a Kestra OpenAPI spec into tenant-aware, human-friendly SDK wrappers; the './runtime' entry is createConfigureClient — the universal fetch-based client setup every Kestra SDK ships. Single source of truth used by the OSS UI SDK, the EE UI SDK, and the client-sdk repo.","license":"Apache-2.0","type":"module","publishConfig":{"access":"public"},"files":["dist"],"main":"./dist/index.mjs","types":"./dist/index.d.mts","exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./runtime":{"types":"./dist/runtime.d.ts","default":"./dist/runtime.js"}},"scripts":{"build":"tsdown","prepack":"tsdown","check:types":"tsc --noEmit -p tsconfig.json"},"peerDependencies":{"@hey-api/openapi-ts":">=0.97.0"},"peerDependenciesMeta":{"@hey-api/openapi-ts":{"optional":true}},"devDependencies":{"@hey-api/openapi-ts":"^0.99.0","@types/node":"^26.6.5","tsdown":"^0.23.1","typescript":"^6","unrun":"^0.3.1"}},"isInternal":false,"tokens":408,"sizeBytes":1332},{"name":"tsconfig.json","path":"ui/packages/hey-api-plugin/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/hey-api-plugin/tsconfig.json","title":"Hey-api-plugin Skill","category":"anthropic-skill","format":"json","content":"# Hey-api-plugin Skill\n\nPath: `ui/packages/hey-api-plugin/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/hey-api-plugin/tsconfig.json)","tokens":300,"sizeBytes":200},{"name":"package.json","path":"ui/packages/kestra-sdk/package.json","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/kestra-sdk/package.json","title":"@kestra-io/kestra-sdk","category":"anthropic-skill","format":"json","content":"{\n  \"name\": \"@kestra-io/kestra-sdk\",\n  \"version\": \"2.0.0\",\n  \"private\": true,\n  \"description\": \"JS/TS client generated from the OSS OpenAPI spec (io.kestra:webserver), in-repo, on the same commit as the backend it describes. The generated code under src/openapi is committed to git. Keeps the client's name so plugins import the same specifier the app does (auth + routing). See README.md.\",\n  \"license\": \"Apache-2.0\",\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./dist/index.js\",\n    \"./ai\": \"./dist/ai.js\",\n    \"./all\": \"./dist/all.js\",\n    \"./blueprint-tags\": \"./dist/blueprint-tags.js\",\n    \"./blueprints\": \"./dist/blueprints.js\",\n    \"./client\": \"./dist/client.js\",\n    \"./dashboards\": \"./dist/dashboards.js\",\n    \"./default\": \"./dist/default.js\",\n    \"./default-admin\": \"./dist/default-admin.js\",\n    \"./executions\": \"./dist/executions.js\",\n    \"./expressions\": \"./dist/expressions.js\",\n    \"./files\": \"./dist/files.js\",\n    \"./flows\": \"./dist/flows.js\",\n    \"./kv\": \"./dist/kv.js\",\n    \"./logs\": \"./dist/logs.js\",\n    \"./mcp\": \"./dist/mcp.js\",\n    \"./metrics\": \"./dist/metrics.js\",\n    \"./misc\": \"./dist/misc.js\",\n    \"./namespaces\": \"./dist/namespaces.js\",\n    \"./notifications\": \"./dist/notifications.js\",\n    \"./outputs\": \"./dist/outputs.js\",\n    \"./plugins\": \"./dist/plugins.js\",\n    \"./secrets\": \"./dist/secrets.js\",\n    \"./services\": \"./dist/services.js\",\n    \"./shared\": \"./dist/shared.js\",\n    \"./triggers\": \"./dist/triggers.js\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"generate:openapi\": \"openapi-ts\",\n    \"build\": \"tsdown\",\n    \"check:types\": \"tsc --noEmit -p tsconfig.json\"\n  },\n  \"devDependencies\": {\n    \"@hey-api/openapi-ts\": \"^0.99.0\",\n    \"@kestra-io/hey-api-plugin\": \"*\",\n    \"@types/node\": \"^26.6.5\",\n    \"tsdown\": \"^0.23.1\",\n    \"typescript\": \"^6\",\n    \"unrun\": \"^0.3.1\"\n  }\n}\n","frontmatter":{"name":"@kestra-io/kestra-sdk","version":"2.0.0","private":true,"description":"JS/TS client generated from the OSS OpenAPI spec (io.kestra:webserver), in-repo, on the same commit as the backend it describes. The generated code under src/openapi is committed to git. Keeps the client's name so plugins import the same specifier the app does (auth + routing). See README.md.","license":"Apache-2.0","type":"module","exports":{".":"./dist/index.js","./ai":"./dist/ai.js","./all":"./dist/all.js","./blueprint-tags":"./dist/blueprint-tags.js","./blueprints":"./dist/blueprints.js","./client":"./dist/client.js","./dashboards":"./dist/dashboards.js","./default":"./dist/default.js","./default-admin":"./dist/default-admin.js","./executions":"./dist/executions.js","./expressions":"./dist/expressions.js","./files":"./dist/files.js","./flows":"./dist/flows.js","./kv":"./dist/kv.js","./logs":"./dist/logs.js","./mcp":"./dist/mcp.js","./metrics":"./dist/metrics.js","./misc":"./dist/misc.js","./namespaces":"./dist/namespaces.js","./notifications":"./dist/notifications.js","./outputs":"./dist/outputs.js","./plugins":"./dist/plugins.js","./secrets":"./dist/secrets.js","./services":"./dist/services.js","./shared":"./dist/shared.js","./triggers":"./dist/triggers.js","./package.json":"./package.json"},"scripts":{"generate:openapi":"openapi-ts","build":"tsdown","check:types":"tsc --noEmit -p tsconfig.json"},"devDependencies":{"@hey-api/openapi-ts":"^0.99.0","@kestra-io/hey-api-plugin":"*","@types/node":"^26.6.5","tsdown":"^0.23.1","typescript":"^6","unrun":"^0.3.1"}},"isInternal":false,"tokens":527,"sizeBytes":1828},{"name":"tsconfig.json","path":"ui/packages/kestra-sdk/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/kestra-sdk/tsconfig.json","title":"Kestra-sdk Skill","category":"anthropic-skill","format":"json","content":"{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"lib\": [\"es2023\", \"dom\"],\n    \"moduleDetection\": \"force\",\n    \"module\": \"preserve\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"esModuleInterop\": true,\n    \"isolatedModules\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src\"],\n  \"references\": [\n    { \"path\": \"./tsconfig.node.json\" }\n  ]\n}\n","frontmatter":{"compilerOptions":{"target":"esnext","lib":["es2023","dom"],"moduleDetection":"force","module":"preserve","moduleResolution":"bundler","resolveJsonModule":true,"strict":true,"noUnusedLocals":true,"declaration":true,"emitDeclarationOnly":true,"esModuleInterop":true,"isolatedModules":true,"verbatimModuleSyntax":true,"skipLibCheck":true},"include":["src"],"references":[{"path":"./tsconfig.node.json"}]},"isInternal":false,"tokens":148,"sizeBytes":516},{"name":"tsconfig.node.json","path":"ui/packages/kestra-sdk/tsconfig.node.json","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/kestra-sdk/tsconfig.node.json","title":"Kestra-sdk Skill","category":"anthropic-skill","format":"json","content":"{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"lib\": [\"es2023\"],\n    \"moduleDetection\": \"force\",\n    \"module\": \"preserve\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"types\": [\"node\"],\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"esModuleInterop\": true,\n    \"isolatedModules\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"composite\": true\n  },\n  \"include\": [\"tsdown.config.ts\", \"openapi-ts.config.ts\"]\n}\n","frontmatter":{"compilerOptions":{"target":"esnext","lib":["es2023"],"moduleDetection":"force","module":"preserve","moduleResolution":"bundler","resolveJsonModule":true,"types":["node"],"strict":true,"noUnusedLocals":true,"esModuleInterop":true,"isolatedModules":true,"verbatimModuleSyntax":true,"skipLibCheck":true,"composite":true},"include":["tsdown.config.ts","openapi-ts.config.ts"]},"isInternal":false,"tokens":135,"sizeBytes":472},{"name":"package.json","path":"ui/packages/slot-contracts/package.json","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/slot-contracts/package.json","title":"@kestra-io/slot-contracts","category":"anthropic-skill","format":"json","content":"{\n    \"name\": \"@kestra-io/slot-contracts\",\n    \"version\": \"0.0.0-dev\",\n    \"description\": \"Kestra slot prop contracts – shared type definitions for federated plugin UI slots\",\n    \"type\": \"module\",\n    \"private\": true,\n    \"homepage\": \"https://github.com/kestra-io/kestra#readme\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"git+https://github.com/kestra-io/kestra.git\"\n    },\n    \"bugs\": {\n        \"url\": \"https://github.com/kestra-io/kestra/issues\"\n    },\n    \"exports\": {\n        \".\": \"./src/index.ts\"\n    },\n    \"main\": \"./dist/index.js\",\n    \"types\": \"./dist/index.d.ts\",\n    \"files\": [\n        \"dist\"\n    ],\n    \"scripts\": {\n        \"build\": \"tsdown\",\n        \"dev\": \"tsdown --watch\",\n        \"check:types\": \"vue-tsc --noEmit\",\n        \"prepublishOnly\": \"npm run build\"\n    },\n    \"peerDependencies\": {\n        \"@kestra-io/kestra-sdk\": \"*\"\n    },\n    \"devDependencies\": {\n        \"tsdown\": \"^0.23.1\",\n        \"ts-morph\": \"^28.0.0\",\n        \"unrun\": \"^0.3.1\"\n    },\n    \"dependencies\": {\n        \"zod\": \"^4.6.7\"\n    }\n}\n","frontmatter":{"name":"@kestra-io/slot-contracts","version":"0.0.0-dev","description":"Kestra slot prop contracts – shared type definitions for federated plugin UI slots","type":"module","private":true,"homepage":"https://github.com/kestra-io/kestra#readme","repository":{"type":"git","url":"git+https://github.com/kestra-io/kestra.git"},"bugs":{"url":"https://github.com/kestra-io/kestra/issues"},"exports":{".":"./src/index.ts"},"main":"./dist/index.js","types":"./dist/index.d.ts","files":["dist"],"scripts":{"build":"tsdown","dev":"tsdown --watch","check:types":"vue-tsc --noEmit","prepublishOnly":"npm run build"},"peerDependencies":{"@kestra-io/kestra-sdk":"*"},"devDependencies":{"tsdown":"^0.23.1","ts-morph":"^28.0.0","unrun":"^0.3.1"},"dependencies":{"zod":"^4.6.7"}},"isInternal":false,"tokens":321,"sizeBytes":1042},{"name":"tsconfig.json","path":"ui/packages/slot-contracts/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/slot-contracts/tsconfig.json","title":"Slot-contracts Skill","category":"anthropic-skill","format":"json","content":"{\n    \"compilerOptions\": {\n        \"target\": \"esnext\",\n        \"lib\": [\"es2023\", \"DOM\"],\n        \"moduleDetection\": \"force\",\n        \"module\": \"preserve\",\n        \"moduleResolution\": \"bundler\",\n        \"resolveJsonModule\": true,\n        \"types\": [\"node\"],\n        \"strict\": true,\n        \"noUnusedLocals\": true,\n        \"declaration\": true,\n        \"emitDeclarationOnly\": true,\n        \"esModuleInterop\": true,\n        \"isolatedModules\": true,\n        \"verbatimModuleSyntax\": true,\n        \"skipLibCheck\": true,\n        \"allowImportingTsExtensions\": true\n    },\n    \"include\": [\"src\", \"tsdown.config.ts\"]\n}\n","frontmatter":{"compilerOptions":{"target":"esnext","lib":["es2023","DOM"],"moduleDetection":"force","module":"preserve","moduleResolution":"bundler","resolveJsonModule":true,"types":["node"],"strict":true,"noUnusedLocals":true,"declaration":true,"emitDeclarationOnly":true,"esModuleInterop":true,"isolatedModules":true,"verbatimModuleSyntax":true,"skipLibCheck":true,"allowImportingTsExtensions":true},"include":["src","tsdown.config.ts"]},"isInternal":false,"tokens":152,"sizeBytes":607},{"name":"package.json","path":"ui/packages/topology/package.json","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/topology/package.json","title":"@kestra-io/topology","category":"anthropic-skill","format":"json","content":"{\n    \"name\": \"@kestra-io/topology\",\n    \"version\": \"0.0.0-dev\",\n    \"description\": \"Kestra flow topology graph component – powered by Vue Flow\",\n    \"type\": \"module\",\n    \"homepage\": \"https://github.com/kestra-io/kestra#readme\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"git+https://github.com/kestra-io/kestra.git\"\n    },\n    \"bugs\": {\n        \"url\": \"https://github.com/kestra-io/kestra/issues\"\n    },\n    \"exports\": {\n        \".\": \"./src/index.ts\",\n        \"./vue-flow-utils\": \"./src/vue-flow-utils.ts\",\n        \"./flow-yaml-utils\": \"./src/utils/flowYamlUtils.ts\"\n    },\n    \"main\": \"./dist/index.js\",\n    \"types\": \"./dist/index.d.ts\",\n    \"files\": [\n        \"dist\"\n    ],\n    \"sideEffects\": [\n        \"**/*.css\",\n        \"./dist/style.css\",\n        \"./src/index.ts\"\n    ],\n    \"publishConfig\": {\n        \"access\": \"public\"\n    },\n    \"scripts\": {\n        \"build\": \"tsdown\",\n        \"dev\": \"tsdown --watch\",\n        \"check:types\": \"vue-tsc --noEmit\",\n        \"test\": \"npm run test:unit\",\n        \"test:unit\": \"vitest run --config vitest.unit.config.ts\",\n        \"test:unit:watch\": \"vitest --config vitest.unit.config.ts\",\n        \"lint\": \"eslint --fix\",\n        \"test:lint\": \"eslint\",\n        \"prepublishOnly\": \"npm run build\"\n    },\n    \"dependencies\": {\n        \"dagre\": \"^0.8.5\",\n        \"html-to-image\": \"^1.11.13\",\n        \"js-yaml\": \"^5.5.0\",\n        \"vue-material-design-icons\": \"^5.4.0\",\n        \"yaml\": \"^2\"\n    },\n    \"peerDependencies\": {\n        \"@kestra-io/design-system\": \"*\",\n        \"@vue-flow/background\": \"^1.3.2\",\n        \"@vue-flow/controls\": \"^1.1.3\",\n        \"@vue-flow/core\": \"^1.48.2\",\n        \"vue\": \"^3.5.0\",\n        \"vue-i18n\": \"^11.4.4\"\n    },\n    \"peerDependenciesMeta\": {\n        \"vue-i18n\": {\n            \"optional\": true\n        }\n    },\n    \"devDependencies\": {\n        \"@tsdown/css\": \"^0.23.1\",\n        \"@types/dagre\": \"^0.7.54\",\n        \"@types/js-yaml\": \"^4.0.9\",\n        \"@types/node\": \"^26.6.5\",\n        \"@vitejs/plugin-vue\": \"^6.0.10\",\n        \"sass-embedded\": \"^1.105.2\",\n        \"tsdown\": \"^0.23.1\",\n        \"typescript\": \"^6\",\n        \"unrun\": \"^0.3.1\",\n        \"vite\": \"^8.3.4\",\n        \"vitest\": \"^5.0.5\",\n        \"vue\": \"^3.5.44\",\n        \"vue-i18n\": \"^11.4.16\",\n        \"vue-tsc\": \"^3.3.13\"\n    }\n}\n","frontmatter":{"name":"@kestra-io/topology","version":"0.0.0-dev","description":"Kestra flow topology graph component – powered by Vue Flow","type":"module","homepage":"https://github.com/kestra-io/kestra#readme","repository":{"type":"git","url":"git+https://github.com/kestra-io/kestra.git"},"bugs":{"url":"https://github.com/kestra-io/kestra/issues"},"exports":{".":"./src/index.ts","./vue-flow-utils":"./src/vue-flow-utils.ts","./flow-yaml-utils":"./src/utils/flowYamlUtils.ts"},"main":"./dist/index.js","types":"./dist/index.d.ts","files":["dist"],"sideEffects":["**/*.css","./dist/style.css","./src/index.ts"],"publishConfig":{"access":"public"},"scripts":{"build":"tsdown","dev":"tsdown --watch","check:types":"vue-tsc --noEmit","test":"npm run test:unit","test:unit":"vitest run --config vitest.unit.config.ts","test:unit:watch":"vitest --config vitest.unit.config.ts","lint":"eslint --fix","test:lint":"eslint","prepublishOnly":"npm run build"},"dependencies":{"dagre":"^0.8.5","html-to-image":"^1.11.13","js-yaml":"^5.5.0","vue-material-design-icons":"^5.4.0","yaml":"^2"},"peerDependencies":{"@kestra-io/design-system":"*","@vue-flow/background":"^1.3.2","@vue-flow/controls":"^1.1.3","@vue-flow/core":"^1.48.2","vue":"^3.5.0","vue-i18n":"^11.4.4"},"peerDependenciesMeta":{"vue-i18n":{"optional":true}},"devDependencies":{"@tsdown/css":"^0.23.1","@types/dagre":"^0.7.54","@types/js-yaml":"^4.0.9","@types/node":"^26.6.5","@vitejs/plugin-vue":"^6.0.10","sass-embedded":"^1.105.2","tsdown":"^0.23.1","typescript":"^6","unrun":"^0.3.1","vite":"^8.3.4","vitest":"^5.0.5","vue":"^3.5.44","vue-i18n":"^11.4.16","vue-tsc":"^3.3.13"}},"isInternal":false,"tokens":718,"sizeBytes":2268},{"name":"tsconfig.json","path":"ui/packages/topology/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/topology/tsconfig.json","title":"Topology Skill","category":"anthropic-skill","format":"json","content":"{\n    \"compilerOptions\": {\n        \"target\": \"esnext\",\n        \"lib\": [\"es2023\", \"DOM\", \"DOM.Iterable\"],\n        \"moduleDetection\": \"force\",\n        \"module\": \"preserve\",\n        \"moduleResolution\": \"bundler\",\n        \"resolveJsonModule\": true,\n        \"types\": [\"node\"],\n        \"strict\": true,\n        \"noUnusedLocals\": true,\n        \"declaration\": true,\n        \"emitDeclarationOnly\": true,\n        \"esModuleInterop\": true,\n        \"isolatedModules\": true,\n        \"verbatimModuleSyntax\": true,\n        \"skipLibCheck\": true,\n        \"allowImportingTsExtensions\": true\n    },\n    \"include\": [\"src\", \"tsdown.config.ts\"],\n    \"exclude\": [\"src/**/*.test.ts\"]\n}\n","frontmatter":{"compilerOptions":{"target":"esnext","lib":["es2023","DOM","DOM.Iterable"],"moduleDetection":"force","module":"preserve","moduleResolution":"bundler","resolveJsonModule":true,"types":["node"],"strict":true,"noUnusedLocals":true,"declaration":true,"emitDeclarationOnly":true,"esModuleInterop":true,"isolatedModules":true,"verbatimModuleSyntax":true,"skipLibCheck":true,"allowImportingTsExtensions":true},"include":["src","tsdown.config.ts"],"exclude":["src/**/*.test.ts"]},"isInternal":false,"tokens":167,"sizeBytes":660},{"name":"README.md","path":"ui/packages/hey-api-plugin/README.md","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/hey-api-plugin/README.md","title":"hey-api-plugin Documentation","category":"plugin-manifest","format":"markdown","content":"# hey-api-plugin Documentation\n\nPath: `ui/packages/hey-api-plugin/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/hey-api-plugin/README.md)","tokens":300,"sizeBytes":200},{"name":"README.md","path":"ui/packages/kestra-sdk/README.md","rawUrl":"https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/kestra-sdk/README.md","title":"kestra-sdk Documentation","category":"plugin-manifest","format":"markdown","content":"# @kestra-io/kestra-sdk (OSS)\n\nThe JS/TS client for the Kestra **OSS** API, generated from the backend's own OpenAPI spec and\nliving **in the same repo, on the same commit** as the backend it describes.\n\n- **Generated from:** `io.kestra:webserver` → `./gradlew :webserver:generateOpenapiSpec` → `openapi.yml`\n- **Generator:** [`@hey-api/openapi-ts`](https://heyapi.dev/) with `@hey-api/client-fetch` + the shared\n  [`@kestra-io/hey-api-plugin`](../hey-api-plugin) (tenant-aware, human-friendly wrappers).\n- **Committed:** the generated code under [`src/openapi`](src/openapi) **is checked into git**. This\n  decouples the fast (npm) build from the Gradle/backend build — `npm run dev`, `build`, and\n  `check:types` never need a Java toolchain.\n\nThe package keeps the name `@kestra-io/kestra-sdk` because that is the specifier plugins import to\nreach the app's client (auth + routing).\n\n## Entry points\n\n| Import | Contents |\n| --- | --- |\n| `@kestra-io/kestra-sdk` | `useClient` / `configureClient` / `setMockClient`, and every generated **type** |\n| `@kestra-io/kestra-sdk/<tag>` | the operations of one tag, e.g. `/flows`, `/executions` |\n| `@kestra-io/kestra-sdk/all` | every operation at once — named exports, plus the namespace as `default` |\n\nThe root entry exports **no operations**: re-exporting the generated operations there put all of them\nin the app's initial graph. Reach an operation through its tag, or through `/all` if one import is\npreferable to several.\n\n## Regenerating the SDK\n\nOnly needed when the OSS API changes. From `ui/`:\n\n```bash\nnpm run generate:sdk\n```\n\nThat is the **only** path that invokes Gradle. It: generates `openapi.yml` (Gradle) → hashes it and\ncompares against the `OPENAPI_SPEC_HASH` already committed in `src/openapi/sdk/shared.gen.ts` → if\nthey match, the committed SDK is already correct for the current spec and the rest is skipped; if\nthey differ, builds the shared plugin → runs `openapi-ts` (generate + convert + hash-stamp) →\nbundles `dist/`. This makes `generate:sdk` cheap to run speculatively (e.g. after any backend change)\nsince a no-op spec diff short-circuits before the expensive steps. Commit the resulting\n`src/openapi/` changes (and the regenerated `package.json` `exports` map) when it does regenerate.\n\nEveryday commands never regenerate: `ui/scripts/ensure-sdk.mjs` (the `predev` / `prebuild` /\n`precheck:types` hook) only bundles the already-committed `src/openapi` into `dist/` when `dist/` is\nmissing.\n\n## Drift detection\n\nThe generated SDK exports `OPENAPI_SPEC_HASH` (`sha256(openapi.yml)[:16]`, stamped by the shared\nplugin at generation time — no external bin). Drift is caught **at dev time, not in CI**: on the\nfirst `configureClient` call in a dev build, `dev-freshness.ts` fetches the backend's live spec,\nhashes it the same way, and warns if the committed SDK is behind. The check is guarded by\n`import.meta.env.DEV` + a dynamic import, so it is tree-shaken out of production builds entirely.\n\n## Runtime\n\n`src/index.ts` binds the shared `createConfigureClient(client, formDataBodySerializer)` from\n`@kestra-io/hey-api-plugin/runtime` (bundled into `dist/`, not a runtime dependency) and keeps the\napp-only `useClient()` / `setMockClient()` — an axios-like facade over fetch that shares the same\ninterceptors, so existing `useClient().get/post(...)` call sites are unchanged. The EE SDK reuses\nthese by relative import.\n","isInternal":false,"tokens":896,"sizeBytes":3415}],"systemPromptSnippet":"<yakaai_skills repo=\"kestra-io/kestra\">\n<!-- File: AGENTS.md (Tokens: ~7258 | Category: root-instruction) -->\n# Coding Agent Guidelines for Kestra Open Source Edition\n\nThis document provides essential information for AI coding agents working on the Kestra codebase.\n\n**IMPORTANT — READ FIRST**\n\n- **Act as a Senior Software Engineer and Software Architect.** Approach software development with:\n    - **Pragmatism**: Favor simple solutions over clever ones\n    - **Skepticism**: Question decisions that could cause technical debt or scalability issues\n    - **Efficiency**: Only challenge when it genuinely matters\n- **Think before coding**: explicitly state assumptions, compare alternatives, and justify choices.\n- **Simplicity first (KISS)**: overengineering and \"gas factories\" are strictly forbidden.\n- **Surgical changes only**: touch **only** what is strictly necessary to achieve the goal.\n- **Goal-driven execution**: define what success looks like *before* writing the first line of code.\n- **Reuse before writing**: the pattern you need almost always exists already. Find the closest equivalent in the codebase and follow it instead of inventing a second way to do the same thing.\n- **No comments by default**: see [Comments](#comments). Over-commenting is one of the most common reasons a PR written by an agent has to be revised.\n- **How Kestra looks is decided by the design system**: see [UI Design System](#ui-design-system). No color, spacing, radius or component pattern may be invented in feature code.\n- **A test has to be able to fail for a reason a reviewer would care about**: see [When a test earns its place](#when-a-test-earns-its-place). More tests do not make a change safer.\n- **Build and test only what you touched** rather than the whole repo: see [What to run after a change](#what-to-run-after-a-change).\n\n## Comments\n\n**The default is no comment.** Small methods with accurate names need no explanation, and every comment is a second thing that has to be kept in sync with the code. Over-commenting is easy to slip into: the next line gets narrated, the method name gets restated above the method, and `// Step 1` / `// Step 2` scaffolding is left behind. None of that will be accepted in review.\n\nThe reader to write for is the developer opening the file in a month. They do not have time to read a paragraph, and most of what a paragraph would tell them is already in the code.\n\nWrite a comment only when a reviewer would otherwise have to ask **why** the code exists:\n\n- a non-obvious constraint, or an upstream bug being worked around (link the issue)\n- an ordering, locking or concurrency requirement that the code itself cannot express\n- a deliberate omission, so that the next person does not \"fix\" it back\n\nKeep it to **one sentence**. When a paragraph seems necessary, make the code clearer instead.\n\nNever write:\n\n- a restatement of the line below it (`// increment the counter`, `// return the result`)\n- a narration of a self-explanatory getter, loop, or well-named call\n- section banners (`// ---- helpers ----`), changelog notes, or a `// TODO` with no issue link\n- a comment on every entry of a list when the rule is already stated once above the list\n\n**Existing comments** should be left alone unless the code they describe changed. When that code is deleted, delete the comment with it rather than rewording it around the gap. When the code changed, either correct the comment or remove it.\n\n**Javadoc** describes the contract rather than the body. Add it on public API called from other modules or from plugins, and on anything whose behavior cannot be seen from the signature (nullability, thread-safety, side effects, units, ownership of a returned collection). Skip it on getters, setters, builders, an override that adds nothing (a bare `{@inheritDoc}` is noise), and any method whose name already says everything.\n\n## Project\n\nMonorepo built with Java (backend) and Vue (frontend), using Gradle as the build system.\n\n## Tech Stack\n- **Backend:** Java 25, Micronaut Framework, Lombok\n- **Frontend:** Vue 3, TypeScript, Vite, Element Plus, Pinia\n- **Build:** Gradle, multi-project. The version is pinned in `gradle/wrapper/gradle-wrapper.properties`, and `settings.gradle` is the list of modules\n- **Testing:** JUnit 5, Mockito, AssertJ, Vitest, Playwright\n\n## Critical Code Patterns\n\n### Dependency Injection\n\n**DO**: Use constructor injection with final fields.\n\n```java\n@Singleton\npublic class MyService {\n    private final SomeDependency dependency;\n\n    @Inject\n    public MyService(SomeDependency dependency) {\n        this.dependency = Objects.requireNonNull(dependency);\n    }\n}\n```\n\n**DON'T**: Use field injection (`@Inject` on fields directly). Always prefer constructor injection.\n\n### Class Structure\n\n```java\n// 1. Package declaration and imports\n// 2. Class-level annotations (@Slf4j, @Singleton, etc.)\n// 3. Class declaration with Javadoc\n// 4. Static constants (UPPER_SNAKE_CASE)\n// 5. Injected fields (@Inject)\n// 6. Constructors\n// 7. Public methods\n// 8. Protected methods\n// 9. Private methods\n// 10. Inner classes/records\n```\n\n### Annotations\n- **Micronaut:** `@Singleton`, `@Inject`, `@Controller`, `@Replaces`, `@Requires`\n- **Validation:** `@Valid`, `@NotNull`, `@Nullable`\n- **Lombok:** `@Slf4j`, `@Getter`, `@NoArgsConstructor`, `@AllArgsConstructor`\n- Use `@Builder` for complex object creation\n\n### Error Handling\n\n**DO**:\n- Use specific exception types — extend `KestraException` or `KestraRuntimeException`\n- Use `Optional<T>` for potentially absent returned values\n- Return empty collections (e.g., `List.of()`, `Collections.emptyList()`) for absent values\n- Use try-with-resources for resource management\n- Log errors before re-throwing: `log.error(\"message\", exception)`\n- 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)`\n\n**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.\n\n### Java Language Features\n- Use java records for simple data carriers\n\n### Naming Conventions\n- Follow Java naming-convention best practices for Classes, Methods, Variables, Constants.\n- Boolean methods: Start with `is`, `has`, `should`, `can` (e.g., `isReadOnly()`).\n\n### File Organization\n- Use 4-space indentation (configured in .editorconfig)\n- UTF-8 encoding with LF line endings\n- No trailing whitespace\n\n### Utility Classes\n* Mark utility classes as `final` with a private constructor\n* Use static methods only\n* Use existing utility classes (e.g., `ListUtils`, `MapUtils`) instead of creating new ones (`io.kestra.core.utils.*`)\n\n**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.\n\n### Enums\n- 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\n- Use `@JsonValue` for custom serialization if needed\n- Use `UNKNOWN` enum value for unknown cases in deserialization\n- Compare Constants From The Left (a.k.a., Yoda conditions)\n- Use a static `fromString` method for case-insensitive lookups using `Enums` class.\n\ne.g.:\n```java\npublic enum MyEnum {\n    VALUE_ONE,\n    VALUE_TWO,\n    UNKNOWN;\n\n    @JsonCreator\n    public static ResourceType fromString(final String value) {\n        return Enums.getForNameIgnoreCase(value, MyEnum.class, UNKNOWN);\n    }\n}\n```\n\n### Documentation\n\nSee [Comments](#comments) for when a Javadoc block is worth writing at all. Once you have decided that it is:\n\n- Keep it to the contract: what the caller has to know that the signature does not already say\n- Use `@param`, `@return` and `@throws` only where each adds something. `@param name the name` is noise\n- Skip a bare `{@inheritDoc}` on an override that changes nothing\n- Add a usage example only when the call site would otherwise be guessed wrong\n\n## Webserver Constraints\n- Put classes used by only controllers in the webserver module (not core)\n- No business code/rule inside controllers - instead use a Service class\n- All APIs must return a valid JSON object\n- APIs should not return a response being a JSON array which cannot be evolved in a backwards-compatible way\n- Any endpoint returning a collection should be paged through `PagedResults` with `page` and `size` parameters, like the rest of the API\n- A DTO used by a single controller belongs as an inner record of that controller. Promote it to `io.kestra.webserver.models.api` only once a second caller exists, and name it with the `Api` prefix there (`ApiExecution`, `ApiTriggerState`, `ApiPluginArtifact`)\n- Import a class rather than writing its fully-qualified name, as soon as it is used more than once in the same file\n- Unit tests must assert that a user can only access a given API if authorized to do so, and that access is denied otherwise\n- APIs must be documented with OpenAPI annotations\n- Use DTOs for requests/responses\n- A dashboard chart export must use the chart's declared columns for its header, in declaration order, whether or not there are rows\n- Always validate input parameters with `@Valid`\n- Use `@ExecuteOn(TaskExecutors.IO)` for blocking operations\n- Return meaningful error responses in controllers\n\n## Worker Constraints\n- Never depend on repositories for code called by the workers - instead use MetaStore/StateStore facades\n\n## Executor Constraints\n- Run the `H2RunnerTest` whenever you update part of the executor\n\n## Testing Guidelines\n\n### When a test earns its place\n\nMore tests do not make a change safer. Every test is code that has to be read, kept passing, and understood by whoever changes that behavior next, so a test is only worth its cost when it can fail for a reason a reviewer would care about.\n\n**Add a test when:**\n\n- new behavior is introduced, or a bug is fixed. The test should fail on the commit before yours; if it passes there, it is testing something else\n- an edge case would be easy to get wrong later: an empty collection, a null, a boundary, a race, a permission check\n- the change is in the executor, the scheduler, the queue, or an ACL path, where a regression is expensive and hard to notice\n\n**Don't add a test when:**\n\n- it only exercises getters, setters, builders, or a Lombok-generated method\n- it asserts behavior that the framework already documents (Jackson serializing a field, Micronaut injecting a bean)\n- it repeats an existing test with one property changed. One test per behavior, rather than one per property, since otherwise there would be hundreds of them\n- the mocks would have to restate the implementation for the assertions to pass. Such a test is pinned to the current code rather than to the behavior, so it is broken by every legitimate refactor\n- it exists only to move a coverage number\n\n**Delete tests too.** Removing a feature should reduce the test count rather than raise it. When a test no longer describes behavior anyone relies on, or when it duplicates another one, delete it in the same PR and say so in the description. A redundant test that is always green is still maintenance cost.\n\n### Java Tests\n\n**DO**:\n- Place tests in same package structure as source code\n- Simple unit test with mocks over complex integration tests when possible\n- Structure the body as given / when / then. The three marker comments are optional and should be dropped when the test is only a few obvious lines\n- Test method naming: `should<ExpectedBehavior>When<ConditionOrAction>` (also `...Given<Input>`, `...For<Condition>`, `...If<Condition>`), e.g. `shouldThrowExceptionWhenDividingByZero()`\n- Use `@MicronautTest` for tests that require Micronaut beans\n- Use `@KestraTest` for tests that require running Kestra services (e.g., Executor, Scheduler)\n-\n```java\n@KestraTest\nclass ServiceTest {\n    @Inject\n    private ServiceClass service;\n\n    @Test\n    void shouldPerformActionWhenCondition() {\n        // Given (setup)\n\n        // When (action)\n\n        // Then (assertions)\n        assertThat(result).isNotNull();\n    }\n}\n```\n\n**DON'T**: Use Nested classes for test organization. Avoid complex test hierarchies.\n\n**Assertions:**\n- Use AssertJ: `assertThat().isEqualTo()`, `assertThat().isNotNull()`, `assertThatThrownBy()`, `assertThatObject()`\n- Prefer descriptive assertion methods\n- Use `@MockBean` for mocking dependencies\n\n**Test Categories:**\n- Unit tests: Fast, isolated, no external dependencies\n- Integration tests: Test component interaction, use `@Tag(\"integration\")`\n- Flaky tests: Use `@Tag(\"flaky\")` for unreliable tests\n\n### Frontend Tests\n- Unit tests with Vitest and `@vue/test-utils`, next to the file they cover (`Foo.vue` beside `Foo.spec.ts` and `Foo.stories.ts`), see [ui/AGENTS.md](ui/AGENTS.md)\n- E2E tests with Playwright\n- Storybook component tests\n- Use JSdom environment for DOM testing\n- **Prefer Storybook component tests over Vitest unit tests whenever possible** — components render through their real story setup (props, slots, design-system deps) instead of being stubbed out, catching regressions unit mocks miss. Fall back to a Vitest unit test only when the logic under test isn't component-rendering behavior (e.g. a pure helper/composable) or no story exists and adding one isn't practical.\n- Don't keep a Vitest test that only re-asserts what a story already covers. Delete it when you add the story.\n- Assert on `data-test` attributes and rendered text. A test written against `.el-*` or `.ks-*` class names is broken by the next Element Plus or design-system upgrade, without any behavior having changed.\n\n## UI Design System\n\nThe design system at [ui/packages/design-system/](ui/packages/design-system/) is the **single source of truth for every design decision** in Kestra: colors, typography, spacing, radii, shadows, and the component vocabulary. Nothing rendered to a user may be styled outside it.\n\nIn practice that means no hex codes, no `rgb()`, no `--el-*` or `--bs-*` variables, no hardcoded pixel spacing, no `:deep()` into another component, and no locally built equivalent of a component that already exists. When the design you need cannot be expressed with the existing `Ks*` components and `--ks-*` tokens, that is a gap in the design system: raise it with design and add the token, the prop or the component upstream. Approximating it with custom CSS in a feature component is not an option, since the result will only be right in the one theme you checked it in.\n\nThe full rules, the component catalogue, the token reference and the frontend best practices live in [ui/AGENTS.md](ui/AGENTS.md), which is auto-loaded for work under `ui/` in OSS and `ui-ee/` in Enterprise Edition. Read it before any frontend change, and update it in the same PR whenever a component, a prop or a token is added.\n\n@ui/AGENTS.md\n\n## Frontend Code Style (Vue 3)\n\n**File Organization:**\n- Use 2-space indentation for Vue, JSON, YAML, CSS\n- Use 4-space indentation for JavaScript/TypeScript\n- Follow Vue 3 Composition API patterns\n- Organize imports: Vue/framework → third-party → local modules\n\n**Naming Conventions:**\n- Components: `PascalCase` files (e.g., `MyComponent.vue`)\n- Variables/functions: `camelCase`\n- Constants: `UPPER_SNAKE_CASE`\n- CSS classes: Follow Element Plus conventions\n\n**TypeScript:**\n- Use strict TypeScript configuration\n- Prefer type definitions over `any`\n- Use interfaces for object shapes\n- Use enums for fixed sets of values\n\n## Build Commands\n\n### What to run after a change\n\nA whole-repo `./gradlew build` can take tens of minutes, and almost nothing about a three-line change can be learned from it. Start with the narrowest command that covers what you touched, and widen it only once that one passes.\n\n| What changed | Run |\n|---|---|\n| One backend module | `./gradlew :<module>:test --tests \"TheClassName\"`, then `./gradlew :<module>:test` |\n| Anything in the executor | `./gradlew :jdbc-h2:test --tests \"H2RunnerTest\"` (mandatory) |\n| Something crossing module boundaries | `./gradlew build -x integrationTest` |\n| A Vue component or composable | `cd ui && npm run check:types && npm run test:unit && npm run lint` |\n| A design-system component | the above, plus `npm run test:storybook` |\n| An e2e spec or fixture | `cd e2e && npm run check:types && npm run test:lint` |\n| `en.json`, whether a key was added or a value edited | `cd ui && npm run translations:generate && npm run translations:check` |\n| A controller or its DTOs | that module's tests, including the authorization tests for the route |\n\n### Java Backend\n\n```bash\n# Clean build\n./gradlew clean\n\n# Full build (includes tests)\n./gradlew build\n\n# Build without tests (faster)\n./gradlew build -x test -x integrationTest -x testCodeCoverageReport --refresh-dependencies --no-daemon --parallel\n```\n\n### Test Commands\n\n```bash\n# Run all tests (excludes flaky tests)\n./gradlew test\n\n# Run only unit tests (fastest)\n./gradlew unitTest\n\n# Run integration tests\n./gradlew integrationTest\n\n# Run flaky tests (separate from build)\n./gradlew flakyTest\n\n# Run tests for specific module\n./gradlew :core:test\n\n# Run single test class\n./gradlew :module-name:test --tests \"ClassName\"\n\n# Run single test method\n./gradlew :module-name:test --tests \"ClassName.methodName\"\n\n# After running tests: generate a markdown summary of failures only\nnpx --yes @kestra-io/kestra-devtools generateTestReportSummary --only-errors $(pwd)\n```\n\n### Frontend (UI)\n\n```bash\ncd ui\n\n# Install dependencies\nnpm install\n\n# Development server\nnpm run dev\n\n# Type checking\nnpm run check:types\n\n# Build for production\nnpm run build\n\n# Run tests\nnpm run test:all        # All tests with coverage\nnpm run test:unit       # Unit tests only\nnpm run test:storybook  # Storybook tests\n\n# Linting\nnpm run lint            # Fix linting issues\nnpm run test:lint       # Check linting only\n\n# Storybook\nnpm run storybook       # Development\nnpm run build-storybook # Build\n```\n\n### End-to-end tests\n\nThe Playwright suite is its own npm package at [e2e/](e2e/), installed separately from `ui/` so that\na spec change pulls in neither the frontend dependency tree nor the frontend CI jobs.\n\n```bash\ncd e2e\nnpm ci\nnpx playwright install chromium\n\nnpm run test:e2e                          # starts a Kestra docker image, runs the suite, stops it\nnpm run test:e2e-without-starting-backend # against an instance you already have running\nnpm run check:types && npm run test:lint\n```\n\n## Development Workflow\n\n### Running Locally\n\n1. **Start/stop backends:**\n```bash\n# Start databases with Docker Compose\ndocker compose -f docker-compose-ci.yml up\n\n# Stop databases with Docker Compose\ndocker compose -f docker-compose-ci.yml down\n```\n\n2. **Access application:** http://localhost:8080\n\n### Worktree setup\n\nWhen working in an OSS worktree (the working directory is under a `worktrees/` directory):\n```bash\ndev-tools/setup-worktree.sh ../worktrees/foo\n```\nThis 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.\n\n### Security Considerations\n- Use tenant isolation for multi-tenant features\n- Implement proper authorization with `@HasAnyPermission`\n- Handle secrets securely (never log sensitive data)\n\n### Performance Best Practices\n- Implement pagination for large datasets\n- Use streaming for large file operations\n- Cache frequently accessed data appropriately\n- Initialize collections with the expected size to avoid resizing overhead\n\n## Troubleshooting\n\n- **A Gradle build fails with no visible cause:** run `./gradlew clean` and retry once. When it repeats, read the actual error instead of retrying again.\n- **Tests fail on connection errors:** the databases are not running. `docker compose -f docker-compose-ci.yml up -d`.\n- **The frontend build fails on dependency resolution:** check the Node and npm versions against `package.json` engines before touching anything else.\n- **`npm install` fails with `EBADENGINE`:** the UI workspaces require npm >= 11.7 and set `engine-strict=true`. Older npm mislabels the `sass`/`sass-embedded` platform binaries as peer dependencies and rewrites `package-lock.json` on every install. Install the pinned npm (`npm i -g npm@11.16.0`) or use the Node release named in `ui/.nvmrc`.\n- **The UI builds but the app never mounts:** module federation, so the cause is usually workspace resolution rather than the change itself. Verify against a running instance rather than against the dev server.\n- **Debug logging:** `--logging.level.io.kestra=DEBUG`.\n\n## Module Structure\n\n**Core Modules:**\n- `cli` - Command Line Interface\n- `core` - Core functionality\n- `webserver` - Web server\n- `ui` - Vue 3 frontend application\n- `executor` - The component responsible for managing execution state\n- `scheduler` - The component responsible for scheduling polling and schedule triggers\n- `worker` - The component that executes tasks and manages worker instances\n- `worker-controller` - The component that manages worker instances and job distribution\n- `indexer` - The component responsible for indexing executions\n- `platform` - provides the Platform Bill of Materials (BOM) for dependency management\n\n**Queuing Layer:**\n- `queue` - Core API for queue implementations\n- `queue-jdbc` - JDBC-based queue implementation\n\n**Data Layer:**\n- `jdbc-*` - Database implementations (H2, Postgres, MySQL)\n\n**Testing Modules:**\n- `e2e` - Playwright end-to-end suite, run against a Kestra docker image\n- `tests` - Common test utilities and base classes\n- `jmh-benchmark` - JMH benchmarks for performance testing\n\n**Key Patterns:**\n- Repository pattern for data access\n- Service layer for business logic\n- Controller layer for HTTP endpoints\n- Builder pattern for object construction (often with Lombok `@Builder`)\n\n## Pull request guidelines\n\n- **One PR, one scope.** When you cannot pick a single conventional-commit scope for the title, the PR is doing too much and should be split.\n- Title format `type(scope): lowercase description`, following https://www.conventionalcommits.org/en/v1.0.0. Name the changed things rather than the actions taken on them: `fix(cases): assignees header, missing Acknowledged status, auto-link toggle`.\n- Use types: chore, feat, fix, refactor, test, docs, build\n- 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\n- Put the closing keyword on the first line of the description, as a full URL so that the link is not lost in a squash merge: `Closes https://github.com/kestra-io/kestra/issues/<id>.`\n- Cover new behavior with tests that could fail, following [When a test earns its place](#when-a-test-earns-its-place). \"Add tests\" never means one test per changed line.\n- Run `npm run lint` in `ui/` before pushing any frontend change. Otherwise one comment per violation is posted by reviewdog, and the human review is buried under trailing-comma suggestions.\n- Attach a screenshot or a short recording for every user-visible change, taken against a running instance.\n- Keep the branch rebased on `develop` rather than merged.\n- Fill in `.github/pull_request_template.md`, and delete the checklist section that does not apply instead of leaving it unticked.\n- Write the description as problem, fix and evidence, once each. No table restating the diff, and no checklist of tests that all passed.\n- Never commit generated output by hand: the twelve non-English translation files, `fingerprints.json`, and the generated SDK are produced by their scripts or by CI.\n\n## Issue guidelines\n- **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`).\n- **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.\n- Leave triage labels such as `kind/cooldown` to `kestrabot`; it applies them automatically on new issues.\n\n## UI Translations\n\n**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. When the string embeds markup or a component (`<code>`, `router-link`, `KsId`), render it with `<i18n-t>` (vue-i18n's component interpolation) and one named slot per placeholder, keeping the markup out of the translation instead of rendering it with `v-html`. 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.\n\nTranslation 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`.\n\n### Checking for missing translations\n\nRun the check script from the `ui/` directory:\n\n```bash\ncd ui && npm run translations:check\n```\n\nA clean run reports `No missing keys.`, `No extra keys.` and `No stale keys.` for every language. Anything listed must be fixed before merging — the same check runs as a PR gate.\n\n> **Enterprise Edition:** EE-only keys live in `ui-ee/src/translations/ee_translations/en.json`, and a tenant type's own keys in `ui-ee/src/tenantTypes/<type>/translations/`, unprefixed and rooted under `tenantTypes.<type>` at runtime. Both are checked separately — run `npm run translations:check` in `ui-ee` as well (see `kestra-ee/AGENTS.md` → \"Frontend i18n\").\n\n### Editing English strings\n\n**Changing an existing English value is a translation change.** Every key carries a fingerprint of the English text its translations were generated from, so editing `en.json` — even just the capitalisation — marks that key stale in all twelve languages and fails `translations:check` until it is regenerated. Run `npm run translations:generate` and commit the result alongside your change.\n\nThis is deliberate: before it existed, edited values were never propagated, and a rename of \"SuperAdmin\" to \"Superadmin\" sat un-translated in eleven locales for a year (kestra-io/kestra#10656).\n\n### Adding or regenerating translations\n\nPrefer `npm run translations:generate` (needs `GOOGLE_CLOUD_PROJECT` and Application Default Credentials, since Gemini is reached through Vertex AI: run `gcloud auth application-default login` once); it fills missing keys and re-translates stale ones on its own, with no flag to remember. Pass `true` to force a full re-translation of everything.\n\nIf you must write a translation by hand:\n\n1. Identify gaps by running `npm run translations:check`.\n2. Follow these translation rules (mirroring `ui/scripts/translations/generateTranslations.ts`, the generator shared by OSS and EE):\n   - **Reserved English terms — never translate:** `kv store`, `namespace`, `tenant`, `flow`, `subflow`, `task`, `log`, `blueprint`, `id`, `trigger`, `label`, `key`, `value`, `input`, `output`, `port`, `worker`, `backfill`, `healthcheck`, `artifact`, `min`, `max`.\n   - **ALL-CAPS status labels stay in English:** `WARNING`, `FAILED`, `SUCCESS`, `PAUSED`, `RUNNING`, etc.\n   - **Preserve `{placeholder}` variables** exactly — vue-i18n uses a **single** pair of braces. Do not translate the name inside the braces, do not rename it, and never write `{{placeholder}}`: double braces are a compile error (`Not allowed nest placeholder`) and make `t()` throw at render time. Each translation must carry exactly the same placeholders as the English source — no invented ones, none dropped.\n   - **Use natural UI terminology** — avoid false friends or overly literal translations (e.g. German: Execution → Ausführung, Theme → Modus, State → Zustand).\n3. Insert the translated keys into the correct position in the target language JSON, mirroring the key order of `en.json`.\n4. Re-run `npm run translations:check` to confirm everything is clean before committing.\n\nThe tooling itself lives in `ui/scripts/translations/` and is shared with EE, which keeps only thin entry points. Rules live in `.mjs` so the dependency-free PR gate can apply them; file IO and orchestration stay in `.ts`.\n\n### Conflicts in `fingerprints.json`\n\nTwo branches that both touch `en.json` will both regenerate `ui/scripts/translations/fingerprints.json`, so it conflicts often. **Never hand-merge the hashes and never pick a side** — a hash says \"this English text is what the twelve translations were generated from\", so choosing the wrong one silently marks a drifted key as current and the drift becomes invisible again.\n\nResolve it the same way as a `kestra-sdk` conflict — regenerate:\n\n```bash\ngit checkout --ours ui/src/translations/*.json ui/scripts/translations/fingerprints*.json\ncd ui && npm run translations:generate   # fills whatever the other branch added\nnpm run translations:check               # must report no missing / extra / stale keys\n```\n\n`en.json` itself normally merges cleanly, since branches usually add different keys; it is the generated files that collide.\n\n## Keeping this file up to date\n\nWhen a review comment corrects something that is not written here, and the correction is a rule rather than a one-off, add it in the same PR: one or two lines, with the example next to the rule. Delete a rule once it no longer matches the codebase. A guideline that no longer matches the code is worse than no guideline, since it will be followed confidently.\n\nWhen in doubt, follow the existing patterns in the codebase and stay consistent with them.\n\n<!-- File: ui/AGENTS.md (Tokens: ~12503 | Category: root-instruction) -->\n# UI Design System Guidelines\n\nScope: 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.\n\nThe 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.\n\n## What this is, in plain terms\n\nThink of the design system as the product's **visual vocabulary**:\n\n- A short list of agreed-upon **colors**, **fonts**, and **spacings** (called *design tokens*).\n- A library of pre-built **components** (`KsButton`, `KsTable`, `KsDialog`, …) that already use those tokens.\n- 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.\n\nIf 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.\n\nUnder 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/`.\n\n> **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.\n\n## Golden rules (non-negotiable)\n\nThese rules are what keep the UI maintainable as it grows. Treat any deviation as a bug.\n\n**How Kestra looks is decided by the design system; what it does is decided by feature code.** A new visual pattern (a new card treatment, a different spacing rhythm, another status color, a bespoke empty state, a one-off modal layout) counts as a design-system change and belongs in `ui/packages/design-system/`, after a conversation with design. Approximating one inside a feature component is the fastest route to a screen that is fine in the one theme you checked it in and wrong everywhere else. When nothing in the catalogue below covers what you need, raise it as a gap in the design system instead of improvising around it.\n\n1. **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.\n2. **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.css` / `ks-theme-dark.css` / `ks-theme-dark-2.css` — do not pick a one-off color.\n3. **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 `--ks-font-family-*` and `--ks-font-size-*` tokens; feature code should not redefine them.\n4. **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.\n5. **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.\n6. **No magic numbers for theme values.** Spacing, radii, font sizes, and shadows must reference tokens — 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.css`](packages/design-system/src/assets/styles/ks-tokens.css)). 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)`).\n7. **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.\n8. **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.\n9. **Every new `Ks*` component needs a Storybook story and a unit test.** Stories double as living documentation for design and product reviewers.\n10. **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`.\n11. **Check that a token exists before using it.** With an invalid `var(--ks-…)` and no fallback, the property is silently inherited instead, so the mistake remains invisible until the computed style is measured. The cases feature was released using `--ks-font-size-medium`, `--ks-font-size-small`, `--ks-radius-2` and `--ks-border-active`, none of which are declared anywhere, and the interval filter's Apply-to row shipped with no visible selection for the same reason (kestra-io/kestra#18777).\n\n    Your editor underlines it: `kestra-tokens/no-undeclared-css-token` (eslint, `@eslint/css`) covers `.css` and plain `<style>` blocks, `kestra-tokens/no-undeclared-ks-token` (eslint) covers `cssVar(\"--ks-…\")` and tokens written inside strings, and `ks/no-undeclared-custom-property` (stylelint) covers the design system's remaining SCSS. All three propose a replacement, and `npm run lint` runs them all. See [scripts/tokens/README.md](scripts/tokens/README.md).\n12. **Copying an existing rule is not proof that it is correct.** Several hundred `:deep()` selectors, some hex codes and some raw pixel values are older than these rules and are being removed over time. Treat them as debt rather than as precedent: don't add more, and clean up the ones in the component you are already editing.\n13. **Bootstrap is gone; don't use its utility classes.** The `d-flex`, `mb-3` and similar classes still in the templates are kept alive by `packages/design-system/src/assets/styles/_utilities.css` until their migration issues land, and that file only shrinks: replace a utility with scoped CSS on `--ks-*` tokens and delete its rule once the last use is gone. `tests/unit/designSystem/bootstrapGuard.spec.ts` fails on a utility the file does not define and on any `--kbs-*` variable.\n\n## Best practices for keeping the design system healthy\n\nA 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.\n\n### Before you write code\n\n- Search the component tables and Storybook first. The most common waste in this codebase is rebuilding something that already exists.\n- 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.\"\n- For anything visible to a user, check both light and dark mode in Storybook before merging.\n\n### While you write code\n\n- 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.\n- Keep the style **inside** the SFC. `<style scoped src=\"./x.css\">` is valid and `scoped` still applies, but an external file separates the CSS from the markup it describes for no gain, and it is one more file to open. Block order is `template`, `script`, `style`, enforced by `vue/block-order` in `ui/eslint.config.js`.\n- Write styles as plain CSS: no `lang=\"scss\"` and no new `.scss` file outside `packages/design-system/` (kestra-io/kestra#21346). Native nesting works with `scoped`, but start a nested selector with `&` (`& :deep(.x)`, `& > *`), or Vue puts `[data-v-*]` on the nested part instead of the parent.\n- 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.\n- 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.\n- 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 }`).\n- Use semantic tokens, not raw colors. `var(--ks-text-link)` communicates intent; `var(--ks-text-blue-500)` does not exist for a reason.\n- Co-locate component-specific tokens (e.g. `--ks-card-shadow`) in the component's SCSS, but always derive them from semantic tokens.\n\n### When extending the design system\n\n- Component styles are plain CSS. A component that needs Element Plus's base styles keeps them in a `<style lang=\"scss\">` block holding only the `el-ns` and `theme-chalk` `@use` lines, placed before the plain `<style>` with its own rules, so each published component entry still ships its chalk CSS. Palette colours are `var(--ks-<family>-<step>)`, not `palette.$base-*`.\n- Don't write one rule per variant. When the tokens follow the variant's name (`--ks-status-<status>`), bind them with `v-bind()` and add a test that every name has its token, since the token lint cannot see a built name (`KsExecutionStatus`, `KsAlert`). When they don't, keep a typed table of `var(--ks-*)` strings in the component's `<script>`, where `kestra-tokens/no-undeclared-ks-token` still checks them, and bind that (`KsTag`, `KsButton`).\n- Only expose props that are actually used somewhere in the codebase. Speculative props rot.\n- Mirror Element Plus prop names where possible — predictability is a feature.\n- Pass `v-bind=\"$attrs\"` and forward slots so wrappers don't trap consumer extension points.\n- 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.\n- Document tokens in code comments next to where they're declared in `ks-theme-*.css`. The `scripts/generate-palette.mjs` file is auto-generated — don't hand-edit it.\n\n### When reviewing a UI PR\n\nReject (or ask to fix) anything that:\n\n- Imports from `element-plus` directly into `ui/src/`.\n- Uses a hex code, `rgb(...)`, `--el-*`, or `--bs-*` for color.\n- Uses `:deep()` to reach into a `Ks*` or `el-*` component.\n- Hardcodes pixel values for padding, margin, radii, font sizes, or shadows.\n- Adds a CSS class that overrides `.el-...` selectors.\n- Duplicates a component that already exists in the design system.\n- Adds a `Ks*` component without a Storybook story or test.\n- Mounts `KsDataTable` without binding `:currentPage` / `:pageSize` (or `v-model:currentPage` / `v-model:pageSize`) — pagination is controlled; see \"Data tables & pagination state\".\n- 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\".\n- Adds a modal/drawer where the user enters data without guarding accidental dismissal — see \"Unsaved input in modals (discard guard)\".\n\n### Accessibility\n\n- Every icon-only `KsIconButton` must have an accessible label (`aria-label` or `title`). Screen readers do not see the icon glyph.\n- Never convey state with color alone — pair status colors with an icon (`KsExecutionStatus` already does this) or a text label.\n- Use semantic HTML inside slots: real `<button>`, `<a>`, `<label>`, headings in document order. Don't fake interactivity with `<div @click>`.\n- `KsDialog`, `KsDrawer`, `KsPopover` already manage focus trap and `Escape`-to-close — don't reimplement these in feature code.\n- Keep tab order logical; rely on the DOM order rather than `tabindex` hacks.\n- 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.\n\n### Internationalization\n\n- No hardcoded user-facing strings. Always go through i18n.\n- **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.\n- Pass plurals and interpolation as named arguments to `$t(...)`, never string-concatenate.\n- A string that embeds markup or a component (`<code>`, `router-link`, `KsId`) renders through `<i18n-t keypath=\"...\" tag=\"span\" scope=\"global\">` (vue-i18n's component interpolation, registered globally, so there is nothing to import; `scope=\"global\"` names the scope every message lives in, or the component looks for a local one up the tree and warns; `kestra-i18n/i18n-t-scope` fails a tag without it and `npm run lint` adds it), with one named slot per placeholder: `Mark as {status}` becomes `<template #status><code>{{ item.code }}</code></template>`. Keep the markup out of the translation, so no `v-html` and no `escapeHtml` are needed, and translators can reorder the placeholders.\n- `splitTranslation(t, key, slot)` and markup passed as a named argument to `v-html` are older patterns for the same job. Don't add new uses; move a string to `<i18n-t>` when you touch it.\n- 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. `tests/unit/dateFormatGuard.spec.ts` (and its `ui-ee` twin) fails on `dayjs(...).format(`, `toLocaleString()` on a `Date`, and `Intl.DateTimeFormat` in `src/`; a file that serializes a date rather than displaying it goes in its `ALLOWED` list with the reason.\n- Date arithmetic goes through the `dayjs` re-exported by the design system. `dayjs` objects are **immutable**, so `d.add(1, \"day\")` returns a new instance and mutating idioms (`cursor.add(...)` in a loop) silently no-op — reassign instead. Parse untrusted input with `dateUtils.parseIso()`: bare `dayjs()` happily reads an epoch number or a bare year as a date.\n- 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.\n\n### Loading, empty, and error states\n\nEvery async surface must render all four states. \"Happy path only\" is a bug.\n\n- **Loading:** `KsSkeleton` for content placeholders; `vKsLoading` directive for sections that already have layout; `KsLoading` component for full-page or container-level spinners.\n- **Empty:** `KsEmpty` with an action where possible — never a blank screen.\n- **Error:** `KsAlert type=\"error\"` with retry affordance, or `KsMessage` for transient errors.\n- **Success / data:** the actual content.\n\n### Data tables & pagination state\n\n`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.\n\n**The contract:**\n\n- Bind `:currentPage` / `:pageSize` (one-way) OR use `v-model:currentPage` / `v-model:pageSize` (two-way).\n- 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)})`.\n- 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.\n- `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.\n- When a column's `prop` is not the backend sort key (e.g. `auditLog.detail.resourceType` vs `detail.resourceType`), pass `:sortKeyMapper` (`(key: string) => string`); a sort click reloads immediately with the raw column prop otherwise, and the backend rejects it with a 422.\n\n**URL-driven pattern** — the default for top-level list pages (Logs, Flows, Executions, KV, Secrets, Triggers, FlowsSearch, Blueprints):\n\n```vue\n<KsDataTable\n    :loadData=\"loadData\"\n    :currentPage=\"urlPage\"\n    :pageSize=\"urlSize\"\n    :total=\"store.total\"\n    @page-changed=\"({page, size}) => router.push({query: {...route.query, page: String(page), size: String(size)}})\"\n/>\n\n<script setup>\nconst urlPage = computed(() => Number(route.query.page) || 1)\nconst urlSize = computed(() => Number(route.query.size) || 25)\n</script>\n```\n\n**Local-state pattern** — for embedded tables that should not appear in the URL (MetricsTable, side-panel views):\n\n```vue\n<KsDataTable\n    v-model:currentPage=\"currentPage\"\n    v-model:pageSize=\"pageSize\"\n    :loadData=\"loadData\"\n    :total=\"...\"\n/>\n\n<script setup>\nconst currentPage = ref(1)\nconst pageSize = ref(25)\n</script>\n```\n\n**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.\n\n**Defaults that land in the URL after mount** — a page whose filter defaults are written by a `router.replace` (e.g. the Logs page's default level, via `useRouteFilterPolicy`) must gate its first `loadData` on `isRouteSettled`. `KsDataTable` loads on mount, so without the gate the page fires a request for the bare query, throws that result away when the defaults arrive, and leaves the two responses racing each other. Gating means the query change is what triggers the first load, so also handle the navigation never landing: `isRouteSettled` gives up after a timeout, and the page must reload when it does — otherwise the list stays empty for good, with no request in flight and nothing to retry it.\n\n**Stores that back a filterable list** must ignore superseded responses: keep a sequence number per search and drop a response whose sequence is no longer the newest (see `stores/logs.ts`). Otherwise the response that happens to land last wins and the list can show results for filters the user already left — or nothing at all, when the stale search matched nothing.\n\n### The deep-watch / computed-spread trap\n\nA `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.\n\nThis 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.\n\n**Don't:**\n\n```ts\nconst filterQuery = computed(() => {\n    const {page: _p, size: _s, sort: _so, ...filters} = route.query\n    return filters  // new object reference on every route.query change\n})\nwatch(filterQuery, () => dataTable.value?.resetAndReload(), {deep: true})\n// Fires on every route.query change — page clicks, sort clicks, anything —\n// and bounces the user back to page 1.\n```\n\n**Do:**\n\n```ts\nconst filterQueryKey = computed(() => {\n    const {page: _p, size: _s, sort: _so, ...filters} = route.query\n    return JSON.stringify(filters)  // stable string — same content, same value\n})\nwatch(filterQueryKey, () => dataTable.value?.resetAndReload())\n// Fires only when filter content actually changes.\n```\n\nThe 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.\n\n### Router guards and `initApp`\n\nGuards are wired through `initApp`, and three things about that are easy to get wrong.\n\n**Register before the router installs.** `app.use(router)` starts the first navigation, and `vue-router` reads `beforeGuards.list()` once that navigation reaches its guard phase. A guard registered synchronously after `app.use(router)` is still picked up; one registered after an `await` is not, and `initApp` awaits i18n and the moment locale between installing the router and returning. A guard added in `initApp(...).then(...)` therefore misses the first navigation, which on a cold load is the only navigation there is. Pass it through the `guards` argument instead.\n\n`afterEach` is in the same window, for a less obvious reason: `afterGuards.list()` is read when a navigation *finishes*, and the first navigation can finish while `initApp` is still awaiting. An `afterEach` registered after those awaits therefore never sees it either, which silently costs anything keyed on the first navigation (`eventsRouter` attributes the landing page to `document.referrer` on exactly that hop).\n\n**A guard's arity decides how `vue-router` reads it.** `initApp` registers guards as `guards.beforeEach.bind(null, router)`, so a `(router, to, from)` guard arrives bound with `length` 2 and stays in return-value mode, where returning a route object redirects. Add a fourth parameter and the bound length becomes 3, which flips `vue-router` into `next()`-callback mode: the returned redirect is ignored and the navigation hangs with no error. Keep guards at `(router, to, from)`.\n\n**There is one `beforeEach` slot.** OSS spends it on `tenantGuard` and can only do so because its auth guard is a `beforeResolve`; EE spends it on `authGuard`. A second `beforeEach` has to be composed into the existing one, or `initApp` has to grow to accept a list, rather than being registered afterwards, since \"afterwards\" is exactly the case that misses the first navigation.\n\n### Session loss and an unreachable server\n\nA 401 on a logged-out session opens `ReauthDialog` through `requestReauth(signIn)` and replays the failed request once the user signs in, so the page and its unsaved state survive; the edition supplies its own `signIn` from `onUnauthorized`. A request that fails without a response (or behind a 502/503/504) raises `ServerUnreachableBanner`, which clears on the next response. Don't add per-page handling for either, and don't clear `unsavedChange` before the user has chosen to leave. The native basic-auth dialog is suppressed for fetch and XHR only: `EventSource` cannot set headers and relies on `Sec-Fetch-Dest`, which browsers send only over HTTPS or localhost, so on plain HTTP its streams can still open the dialog.\n\n### Unsaved input in modals (discard guard)\n\nAny modal/drawer where the user **enters data** must not silently lose it on an accidental dismissal. `KsDialog` and `KsDrawer` take a `dirty` prop and ask for confirmation themselves; never reimplement the confirm-before-discard logic per modal.\n\n```vue\n<KsDialog v-model=\"visible\" :dirty=\"isDirty\" ... />\n<KsDrawer  v-model=\"visible\" :dirty=\"isDirty\" ... />\n```\n\n```ts\n// isDirty: true when there is unsaved input worth a prompt, usually a comparison against a snapshot taken on open\nconst baseline = ref(\"\")\nconst isDirty = computed(() => JSON.stringify(form.value) !== baseline.value)\n```\n\n`dirtyMessage` replaces the default confirmation text when the discarded thing is not a form (`TriggerFlow.vue` uses it for an unsubmitted execution). When the close also has to run cleanup, keep `:beforeClose=\"(done) => { reset(); done() }\"` next to `:dirty`: the confirmation runs first, `beforeClose` only once the user agreed. The composable behind the prop, `useDiscardGuard(isDirty, {message?})` from `@kestra-io/design-system`, stays available for closes that do not go through a `Ks*` container.\n\nRules:\n- **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.\n- **`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.\n- **`KsDialog` and `KsDrawer` both expose `dirty` and 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.)\n- **Don't guard** read-only viewers, action/confirmation dialogs, or ephemeral forms that reset on every open.\n\n### Icons\n\n- All icons come from [`vue-material-design-icons`](https://github.com/robcresswell/vue-material-design-icons) via `<KsIcon>` (or `<KsIconButton>` for clickable icons).\n- 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.\n- Pass `name` (the kebab-case Material name); size and color come from props or the surrounding token context — don't override with inline `style`.\n- **Two file-type icon sets coexist on purpose, so don't merge them.** `fileUtils.fileIcon()` (behind `KsFileTag`) maps an extension to a monochrome `vue-material-design-icons` component, which inherits `--ks-icon-*` and so recolors per theme and per tag variant. `ui/src/components/utils/icons/Type.vue` renders the colored `material-file-icons` SVGs for the namespace file explorer, where the brand colors are the point. That package is a `ui/` dependency the design system does not have, and it bakes its colors into a base64 `<img>` that no token can reach, so it cannot be used from a `Ks*` component.\n\n### Performance\n\n- Lazy-load heavy surfaces: `KsEchart`, `KsLine`, `KsBar`, `KsPie`, `KsGraph`, `KsMarkdown`, code-editor surfaces. Use `defineAsyncComponent` or route-level code splitting.\n- Prefer `v-show` for frequent toggles (tabs, filters), `v-if` for rare/heavy mounts (modals, big tables).\n- Pass stable `key` props in lists. Avoid index-based keys when items have IDs.\n- Don't render giant tables without `KsDataTable`'s pagination/virtualization — server-side paging is the default for anything that can grow.\n- Watch out for `watch(..., { deep: true })` and `computed` with object identity — they often re-run more than you expect.\n\n### Types\n\n**Never write `any`.** TypeScript stops checking a value the moment it is typed `any`, so a typo or a renamed field is found by whoever opens the page instead of by the compiler. The rule covers every spelling: `(row: any)`, `x as any`, `any[]`, `Record<string, any>`, in the `<script>` block and in template expressions alike.\n\nWhere the type comes from, in this order:\n\n1. **Data from the backend: `kestra-sdk`.** It is generated from our OpenAPI spec, so the type already exists and stays in step with the API. `import type {Execution} from \"@kestra-io/kestra-sdk\"`.\n2. **Data from a library** (vue-router, element-plus, monaco, echarts, the DOM): the library's own types. When a package ships none, add a `declare module` file next to `src/material-icons.d.ts` rather than reaching for `any`.\n3. **An interface the app already has.** Search for one before writing another.\n4. **A new interface**, only when none of the above fit.\n\nFor a value whose shape really is not known yet, `unknown` with a narrowing check is the honest escape hatch. `any` is not.\n\n**A legitimate `any`, or a package with no types?** Give the package a small `.d.ts` shim next to `src/material-icons.d.ts`. An `any` that truly cannot be avoided is a conversation with a maintainer, not a number you change on your own.\n\n**MANDATORY — never raise the baseline to make the check pass.** A bigger number hides the new `any` from every later run, which is the one thing the baseline exists to prevent, and it will be treated as a bug in review. Specifically, and this applies to coding agents as much as to people:\n\n- Do not hand-edit `scripts/explicit-any/baseline.json`. The only writes to it come from the check itself.\n- Do not run `--accept-new-any`. A maintainer who has already agreed to a raise records it with `npm run check:ts-any -- --write --accept-new-any`; it is never a way to get a green check.\n- A red `check:ts-any` is fixed by typing the value, not by making the check agree with the code.\n- When you cannot type it, stop. Leave the check red, say in the PR which value defeated you and why, and let a maintainer decide. An unfinished PR is fine; a silently raised baseline is not.\n\n**The check.** `npm run check:ts-any` counts the explicit `any` per file, oxlint for the `<script>` block and the Vue compiler for template expressions, and compares the counts with `scripts/explicit-any/baseline.json`, which records what was already in the tree when the rule came in. The same check runs on every PR, as `Npm - check ts-any`. It fails in two directions and the message says which:\n\n- ``New `any` in 1 file(s)`` with a line like `src/utils/filters.ts: 1 -> 2`. Your change added one. Type it with the order above.\n- `The baseline is out of date` with `src/utils/filters.ts: 3 -> 2`, or `moved from …` when you renamed a file. Nothing got worse, the baseline just has to follow the code. Run `npm run check:ts-any -- --write` and commit it alongside your change. A rename is followed whether it is staged or already committed on your branch, so the count travels with the file either way.\n\nInstall the repo hooks once with `.github/.hooks/setup_hooks.sh` and the second case stops happening: the pre-commit hook lowers the baseline and stages it with the rest of your commit.\n\n### Testing UI\n\n- Unit tests with **Vitest** + `@vue/test-utils`, and Storybook stories, live next to the file they cover: `src/components/logs/Foo.vue` has `Foo.spec.ts` and `Foo.stories.ts` beside it. A spec or story about one aspect of a file is named `<file>.<focus>` (`Foo.download.spec.ts`, `flow.draftSave.spec.ts`) so it sorts next to its subject; one that exercises several modules together keeps a descriptive name in their folder (`entityLoadRace.spec.ts`). `tests/unit/` keeps only the setup, the shared helpers (`i18nMount.ts`) and the repo-wide guards; `tests/storybook/` keeps only shared fixtures and the stories with no single source file. The `design-system` and `topology` packages follow the same rule with `*.test.ts`, their `tests/` keeping only setup and helpers. `tests/unit/specLocationGuard.spec.ts` fails on a spec or story left in any of these `tests/` folders for a file that exists in `src/`. Playwright specs stay in the `e2e/` package: they exercise the running app, not one source file.\n- Mount through `i18nMount` or `i18nShallowMount` rather than calling `mount` with your own `createI18n`. Pass `messages` for the keys the spec asserts on, or `locales: en` when it needs the real `en.json`; with neither, `t(\"key\")` renders the key. Missing-key warnings are off in both helpers, so a spec asserting on raw keys stays quiet. Two copies exist and behave the same: specs under `src/`, `tests/unit/` and `packages/topology/src/` import `tests/unit/i18nMount.ts`, and design-system specs import `packages/design-system/tests/units/i18nMount.ts`. Both install the design system, as the app does at bootstrap, so a mounted `Ks*` component resolves instead of warning; a spec that mocks `@kestra-io/design-system` away has to keep a `default` export for that install, such as `default: {install: () => {}}`. Only a spec that never mounts anything, such as one testing pluralisation on the instance itself, builds its own i18n.\n- `i18nMount` also installs a fresh `memoryRouter()` (`tests/unit/memoryRouter.ts`), so `useRoute()` / `useRouter()` resolve instead of warning; a spec that asserts on navigation passes its own router in `global.plugins`, which replaces the default, or mocks `vue-router`. A router already in test-utils' global `config.global.plugins` (ui-ee's setup installs one) also replaces it. A spec mounting with plain `mount` passes `memoryRouter()` itself. A spec that creates a store outside a component activates `routedPinia()` from the same file instead of a bare `createPinia()`, so a store calling `useRoute()` at setup resolves it there too.\n- A spec that mounts a no-code field (`TaskObject`, `TaskObjectField`, `TaskArray`, `Add`, `TaskEdit`) ends with `afterAll(() => taskComponentsReady())` from `useBlockComponent.ts`: the block components are imported on mount, and with `isolate: false` an import still in flight when the file ends is reported as an `EnvironmentTeardownError` for the whole run.\n- A component passed inside `props` (a tab's `component`, an `icon`) is wrapped in `markRaw`: test-utils makes the props object reactive, and Vue warns about a component that was made reactive on every render.\n- Check a spec's console with `npx vitest run --project=unit --silent=false <file>`. Vitest picks a minimal reporter in agent and non-interactive shells that hides the output of passing tests, so a warning CI prints can be invisible locally.\n- A test that exercises a failure path the code reports with `console.error` stubs it (`vi.spyOn(console, \"error\").mockImplementation(() => {})`) and restores it, and ideally asserts on the call; output from a passing test is noise that hides the warning worth reading.\n- 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.\n- 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.\n- Visual regressions caught in Storybook are cheaper to fix than caught in production.\n- A test only earns its place when it can fail for a reason a reviewer would care about. Don't test that a prop is passed through, that a computed returns its own input, or that a `Ks*` component renders: the template is only restated by those assertions, and they are broken by every refactor. Delete a Vitest test once a story covers the same behavior, and say so in the PR description.\n\n### Checking a frontend change before pushing\n\n```bash\nnpm run check:types && npm run test:unit && npm run lint\n```\n\n`npm run lint` is not optional. Without it, one PR comment per eslint violation is posted by reviewdog (missing trailing commas, mostly) and the human review is buried underneath them.\n\n`npm run check:ts-any` compares the explicit `any` per file against `scripts/explicit-any/baseline.json`. It fails when a file gains one, so type it instead of raising the number. It also fails when a file loses one, because the baseline has to come down with the code: run `npm run check:ts-any -- --write` and commit the smaller numbers, or install the repo's git hooks (`.github/.hooks/setup_hooks.sh`) and the pre-commit hook does it for you. `--write` only ever lowers; it refuses to raise a count.\n\nColour is gated automatically. `tests/unit/designSystem/colorGuard.spec.ts` fails on any hex, `rgb()`, `hsl()`, `oklch()`, `--el-*`, `--bs-*`, SCSS colour variable or bare keyword colour reaching a colour property anywhere under `ui/src` and `ui/packages/topology/src`, skipping `*.spec.ts`, `*.test.ts` and `*.stories.ts`, and prints `path:line (colours)` for every offending line. It runs with `npm run test:unit` and on every PR. Replace what it reports with a `--ks-*` token, a `Ks*` prop, or a change in the design system.\n\nTwo escape hatches exist, each taking a reason, for the case the guard cannot judge: artwork whose colours **are** the asset (a brand mark, a third-party logo) rather than a themed surface. Nothing else qualifies, and a hardcoded colour that a token could carry is a bug whether or not the guard is silenced. There is deliberately no whole-file opt-out, and a `-start` with no `-end` is reported instead of muting the rest of the file.\n\n```scss\n/* design-system-disable-next-line: the Kestra mark is fixed artwork */\n/* design-system-disable-start: … */  /* … */  /* design-system-disable-end */\n```\n\nThe guard reads `.vue`, `.scss`, `.css`, `.ts` and `.js`; in a `.vue` file it reads `<style>` blocks plus `fill=` / `stroke=` attributes, and in a script it reads a hex under a colour-named key (`colorHex: \"#…\"`), so a `//`, `/* */` or `<!-- -->` comment all work.\n\nSpacing, radii and `:deep()` are not gated, so still read your own diff for those:\n\n```bash\ngit diff -U0 develop... -- '*.vue' '*.scss' \\\n  | grep -nE '^\\+.*(:deep\\(|(padding|margin|gap|border-radius|font-size)[[:space:]]*:[[:space:]]*[0-9]+px)'\n```\n\nFor a user-visible change, also check it in light **and** dark mode, and attach a screenshot to the PR.\n\n### Deprecation contract\n\nWhen retiring a `Ks*` component, prop, or token:\n\n1. Mark with a `@deprecated` JSDoc tag *and* a one-line replacement path: `@deprecated since 0.x — use <KsNewThing> instead`.\n2. Keep it working for at least one minor release; add a `console.warn` in dev mode if the cost is reasonable.\n3. Migrate all callers in the same release where feasible — don't leave half-migrations.\n4. Only delete after the deprecation window. A silent removal breaks downstream EE / plugin code.\n\n## Anti-patterns (do not write these)\n\n```vue\n<!-- Wrong: raw element-plus, hex color, :deep, SCSS variable in feature code -->\n<template>\n    <el-button class=\"my-btn\">Save</el-button>\n</template>\n<style lang=\"scss\" scoped>\n    .my-btn {\n        background: #8405ff;\n        font-size: $font-size-md;\n    }\n    :deep(.el-button__text) { color: white; }\n</style>\n```\n\n```vue\n<!-- Right: Ks component, semantic tokens, no deep selector, i18n -->\n<template>\n    <KsButton type=\"primary\">{{ t(\"save\") }}</KsButton>\n</template>\n<style scoped>\n    /* Almost always: no custom CSS is needed at all. */\n</style>\n```\n\nIf your `<style>` block needs to exist:\n\n```css\n/* Right: --ks-* tokens, no SCSS vars in feature code, no :deep */\n.my-feature {\n    background: var(--ks-bg-surface);\n    color: var(--ks-text-primary);\n    border: 1px solid var(--ks-border-default);\n}\n```\n\n## Components\n\n### Basic / Layout\n\n| Component | Purpose |\n|-----------|---------|\n| `KsButton` / `KsButtonGroup` | Primary action button and grouped buttons; `--ks-button-content-width` stretches the content (default `auto`); `block` stretches the button to its container's width |\n| `KsIcon` / `KsIconButton` | Material Design icon display; icon-only button (always with `aria-label`) |\n| `KsLink` | Styled hyperlink |\n| `KsText` | Typography wrapper — preferred over raw `<span>` / `<p>` for theme-aware text |\n| `KsScrollbar` | Custom-styled scrollbar wrapper |\n| `KsContainer` / `KsHeader` / `KsMain` | Page layout shell |\n| `KsRow` / `KsCol` | Responsive grid |\n| `KsSplitter` / `KsSplitterPanel` | Resizable split-pane layout |\n\n### Feedback\n\n| Component | Purpose |\n|-----------|---------|\n| `KsAlert` | Alert banner for messages and status feedback. Pass `banner` for a full-width system bar (licence, impersonation): square, bottom rule only, body text in `--ks-text-primary` so it reads at AA on the tint |\n| `KsDialog` | Modal dialog (handles focus trap + Escape); `dirty` asks before an accidental close; content that cannot fit the default width widens it to 1024px with a `data-dialog-wide` attribute on an element inside it, so a step that renders it is wide and the others are not |\n| `KsDrawer` | Side drawer / panel; `dirty` asks before an accidental close |\n| `KsTooltip` | Hover tooltip |\n| `KsPopover` | Popover for contextual content |\n| `KsLoading` (`vKsLoading`) | Loading spinner directive |\n| `KsMessage` | Toast notification service |\n| `KsNotification` | Notification service |\n| `KsMessageBox` | Confirmation dialog service |\n\n### Form\n\n| Component | Purpose |\n|-----------|---------|\n| `KsInput` / `KsPassword` | Text and password inputs |\n| `KsInputNumber` | Numeric input with increment / decrement |\n| `KsSelect` / `KsOption` / `KsOptionGroup` | Dropdown select |\n| `KsAutocomplete` | Autocomplete input with suggestions |\n| `KsCheckbox` / `KsCheckboxGroup` / `KsCheckboxButton` | Checkbox variants |\n| `KsRadio` / `KsRadioGroup` / `KsRadioButton` | Radio button variants |\n| `KsRadioCardGroup` | Single-select radio group rendered as option cards (title + optional hint/icon/disabled); options-driven via `:options` + `v-model` |\n| `KsSwitch` | Toggle switch |\n| `KsThemePicker` | Theme chooser, one miniature of the app per theme painted in that theme's own colours; options-driven via `:options` + `v-model`. A `#preview=\"{option}\"` slot replaces the built-in miniature, `columns` lays the options out as a grid, `ariaLabel` names the radiogroup |\n| `KsDatePicker` / `KsTimePicker` | Date and time pickers |\n| `KsColorPicker` | Color picker; `alphaOnSurface` shows translucent colours over the page surface instead of a checkerboard |\n| `KsDurationPicker` | ISO 8601 duration picker |\n| `KsCascaderPanel` | Cascading hierarchical selector |\n| `KsUpload` | File upload |\n| `KsForm` / `KsFormItem` | Form container with validation |\n\n### Data Display\n\n| Component | Purpose |\n|-----------|---------|\n| `KsCard` | Card container |\n| `KsTable` / `KsTableColumn` | Basic table |\n| `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\". Pass `mobileColumns` (`{leading?, title, titleAside?, meta?, metaAside?, actions?}`, each naming a column `prop`, or the `columnKey` of a prop-less actions column) to render a card per row below 768px: `leading` is a status dot, `title` is bold, `titleAside` sits on its right, `meta` columns share the second line joined by a dot and `metaAside` closes it; `actions` is a corner cell on the first line; unlisted columns are hidden, bulk selection is desktop-only (the selection column is not shown on cards), and the regular table returns from 768px. |\n| `KsEntityLink` | Clickable cross-entity reference (namespace / flow) for table cells — neutral tag with leading icon, violet on hover. Pass `noIcon` in dense embedded tables (e.g. dashboard chart tables, ~90px columns) where the icon's 20px costs more than it tells |\n| `KsBadge` | Small indicator badge; `size=\"small\"` shrinks the pill so it sits on an icon button instead of covering it |\n| `KsNewBadge` | Compact uppercase \"NEW\" pill flagging a newly shipped feature — caller supplies the label via the default slot |\n| `KsTag` / `KsCheckTag` | Tag / label; clickable checkbox-style tag. Pass `truncate` to clip a long label with an ellipsis instead of letting the tag outgrow its container |\n| `KsAvatar` | Avatar with fallback |\n| `KsProgress` | Progress bar |\n| `KsPagination` | Pagination controls |\n| `KsEmpty` | Empty state placeholder |\n| `KsSkeleton` | Skeleton loader |\n| `KsId` | Copyable ID display |\n| `KsDateAgo` | Relative time display (\"2 hours ago\") |\n| `KsSegmented` | Segmented control; object options may carry an `icon` component, rendered before the label. `variant=\"neutral\"` swaps the primary-coloured selection for a grey one with wider segments |\n| `KsCollapse` / `KsCollapseItem` | Collapsible sections |\n| `KsFileTag` | Storage URI rendered as a file reference: a `KsTag` whose symbol comes from the extension, with a readable name (`name`, defaulting to the URI's last segment); the full URI stays in the tooltip |\n| `KsTree` | Hierarchical tree view |\n| `KsJsonTree` | Read-only JSON tree viewer; leaves holding a storage URI render through `KsFileTag` |\n| `KsTimeline` / `KsTimelineItem` | Timeline visualization |\n| `KsExecutionStatus` | Execution / task status badge with icon and color; `dot` collapses it to a coloured dot (label kept for screen readers) for dense layouts such as mobile cards |\n| `KsCodeStatus` | Compact validity badge with icon (`valid` / `error`) — caller supplies the label |\n| `KsMarkdown` | Markdown renderer (lazy-load on heavy surfaces) |\n\n### Charts\n\n| Component | Purpose |\n|-----------|---------|\n| `KsEchart` | ECharts base wrapper (lazy-load) |\n| `KsLine` / `KsBar` / `KsPie` | Line, bar, and pie charts (lazy-load) |\n| `KsGraph` | Graph / network visualization (lazy-load) |\n\n### Navigation\n\n| Component | Purpose |\n|-----------|---------|\n| `KsTabs` / `KsTabPane` | Tabbed interface |\n| `KsMenu` / `KsMenuItem` | Hierarchical menu |\n| `KsDropdown` / `KsDropdownMenu` / `KsDropdownItem` | Dropdown menu; pass `danger` on an item to give a destructive or exit action (delete, log out) the error-coloured hover |\n| `KsTopNavBar` | Top navigation bar; `titleSiblings` reaches the breadcrumb's current page |\n| `KsSideBar` / `KsSideBarSection` / `KsSideBarItem` | Left sidebar shell (header / scrollable body / footer slots), section with title, and styled link primitive with icon, active and locked states |\n| `KsBreadcrumb` / `KsBreadcrumbItem` | Breadcrumb navigation. An item's `siblings` loader (or `children` on the first item, and the `titleSiblings` prop for the current page) adds a chevron whose hover menu lists that level, headed by the level above (its `scope` when the label is not the right name for its content); an entry with a `children` loader flies out its own content to the right, as deep as the tree goes. Level loaders run as soon as the bar renders and an empty result shows no chevron; fly-outs load on hover. Namespace and flow pages build theirs with `useNamespaceBreadcrumb` |\n| `KsSteps` / `KsStep` | Step / wizard progress indicator |\n\n## Utilities (import from the design system)\n\n- `State`, `STATES`, `LOG_LEVELS` — execution state constants and icons. A state's colour is not a property of the state: read it with `State.getStateColor(name)`, which resolves `--ks-status-*`\n- `cssVar(name, opacity?)` — read a `--ks-*` CSS custom property at runtime (use this in JS / chart configs instead of hardcoding hex)\n- `dayjs` — the one configured dayjs instance (utc, timezone, duration, advancedFormat, calendar, isoWeek, localizedFormat, minMax, relativeTime, weekOfYear, isSameOrBefore). Never `import dayjs from \"dayjs\"` in feature code: plugins are registered on this instance, so a bare import silently lacks them\n- `dateUtils` — `dateFilter()`, `parseIso()`, `toIsoKeepOffset()`, `currentTimezone()`, `timezonesWithOffset()`, `currentLocale()`, `setLocale()`, `DATE_FORMAT_STORAGE_KEY`, `TIMEZONE_STORAGE_KEY`\n- `durationUtils` — `duration()`, `isValidDuration()`, `humanDuration()` — ISO 8601 ↔ ms and human-readable\n- `stringUtils` — `afterLastDot()`\n- `escapeHtml()`, `cloneDeep()`, `deepMerge()`, `isDeepEqual()`, `isPlainObject()`, `getPath()`, `setPath()`, `groupBy()`, `mapValues()`, `debounce()`, `throttle()` — the general-purpose helpers that replaced lodash. `debounce`/`throttle` return a function carrying `cancel()` and `flush()`. Do not add `lodash` back for any of these\n- `fileUtils` — `isFileUri()`, `fileName()`, `fileExtension()`, `fileIcon()` — storage-URI detection and the file symbol used by `KsFileTag`\n- `flowYamlUtils` — YAML parsing / manipulation for flow definitions\n- `Comparators` — enum of filter comparison operators\n- Filter helpers — `decodeSearchParams()`, `encodeFiltersToQuery()`, `getUniqueFilters()`, etc.\n- `applyDefaultFilters()`, `useRouteFilterPolicy()` — filter composables\n- `designSystemLocale`, `setDesignSystemLocale`, `registerDesignSystemI18n` — i18n\n- `designSystemI18nReady()` — the locale registration the plugin's `install` started, to await instead of leaving it in flight (the unit setup awaits it after each test)\n\n## Composables\n\n- `useTheme()` — detects and tracks dark / light mode via MutationObserver. Use this instead of reading `document.documentElement` yourself.\n- `useFilters`, `useSavedFilters`, `useDefaultFilter`, `usePreAppliedFilters`, `useRouteFilterPolicy`, `useTableColumns`, `useDataOptions`, `useDragAndDrop`, `usePeriodicRefresh` — data-table filter composables\n- `useDiscardGuard(isDirty, {message?})` — confirm-before-discard behind the `dirty` prop of `KsDialog` / `KsDrawer`; see \"Unsaved input in modals (discard guard)\"\n- `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.\n\n## Design tokens\n\nTokens are CSS custom properties declared in [`ks-theme-light.css`](packages/design-system/src/assets/styles/ks-the\n\n<!-- File: ui/packages/design-system/package.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Design-system Skill\n\nPath: `ui/packages/design-system/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/package.json)\n\n<!-- File: ui/packages/design-system/scripts/Figma.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Scripts Skill\n\nPath: `ui/packages/design-system/scripts/Figma.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/scripts/Figma.json)\n\n<!-- File: ui/packages/design-system/tests/storybook/Basic/Color-variables.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Basic Skill\n\nPath: `ui/packages/design-system/tests/storybook/Basic/Color-variables.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tests/storybook/Basic/Color-variables.json)\n\n<!-- File: ui/packages/design-system/tsconfig.app.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Design-system Skill\n\nPath: `ui/packages/design-system/tsconfig.app.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.app.json)\n\n<!-- File: ui/packages/design-system/tsconfig.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Design-system Skill\n\nPath: `ui/packages/design-system/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.json)\n\n<!-- File: ui/packages/design-system/tsconfig.node.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Design-system Skill\n\nPath: `ui/packages/design-system/tsconfig.node.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.node.json)\n\n<!-- File: ui/packages/design-system/tsconfig.storybook.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Design-system Skill\n\nPath: `ui/packages/design-system/tsconfig.storybook.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/tsconfig.storybook.json)\n\n<!-- File: ui/packages/design-system/web-types.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Design-system Skill\n\nPath: `ui/packages/design-system/web-types.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/design-system/web-types.json)\n\n<!-- File: ui/packages/hey-api-plugin/package.json (Tokens: ~408 | Category: anthropic-skill) -->\n{\n  \"name\": \"@kestra-io/hey-api-plugin\",\n  \"version\": \"0.4.0\",\n  \"description\": \"Kestra's shared SDK tooling. Two entry points: the '.' entry is the @hey-api/openapi-ts generator plugin (generation-time) that turns a Kestra OpenAPI spec into tenant-aware, human-friendly SDK wrappers; the './runtime' entry is createConfigureClient — the universal fetch-based client setup every Kestra SDK ships. Single source of truth used by the OSS UI SDK, the EE UI SDK, and the client-sdk repo.\",\n  \"license\": \"Apache-2.0\",\n  \"type\": \"module\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.mts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.mts\",\n      \"default\": \"./dist/index.mjs\"\n    },\n    \"./runtime\": {\n      \"types\": \"./dist/runtime.d.ts\",\n      \"default\": \"./dist/runtime.js\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"tsdown\",\n    \"prepack\": \"tsdown\",\n    \"check:types\": \"tsc --noEmit -p tsconfig.json\"\n  },\n  \"peerDependencies\": {\n    \"@hey-api/openapi-ts\": \">=0.97.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"@hey-api/openapi-ts\": {\n      \"optional\": true\n    }\n  },\n  \"devDependencies\": {\n    \"@hey-api/openapi-ts\": \"^0.99.0\",\n    \"@types/node\": \"^26.6.5\",\n    \"tsdown\": \"^0.23.1\",\n    \"typescript\": \"^6\",\n    \"unrun\": \"^0.3.1\"\n  }\n}\n\n<!-- File: ui/packages/hey-api-plugin/tsconfig.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Hey-api-plugin Skill\n\nPath: `ui/packages/hey-api-plugin/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/hey-api-plugin/tsconfig.json)\n\n<!-- File: ui/packages/kestra-sdk/package.json (Tokens: ~527 | Category: anthropic-skill) -->\n{\n  \"name\": \"@kestra-io/kestra-sdk\",\n  \"version\": \"2.0.0\",\n  \"private\": true,\n  \"description\": \"JS/TS client generated from the OSS OpenAPI spec (io.kestra:webserver), in-repo, on the same commit as the backend it describes. The generated code under src/openapi is committed to git. Keeps the client's name so plugins import the same specifier the app does (auth + routing). See README.md.\",\n  \"license\": \"Apache-2.0\",\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./dist/index.js\",\n    \"./ai\": \"./dist/ai.js\",\n    \"./all\": \"./dist/all.js\",\n    \"./blueprint-tags\": \"./dist/blueprint-tags.js\",\n    \"./blueprints\": \"./dist/blueprints.js\",\n    \"./client\": \"./dist/client.js\",\n    \"./dashboards\": \"./dist/dashboards.js\",\n    \"./default\": \"./dist/default.js\",\n    \"./default-admin\": \"./dist/default-admin.js\",\n    \"./executions\": \"./dist/executions.js\",\n    \"./expressions\": \"./dist/expressions.js\",\n    \"./files\": \"./dist/files.js\",\n    \"./flows\": \"./dist/flows.js\",\n    \"./kv\": \"./dist/kv.js\",\n    \"./logs\": \"./dist/logs.js\",\n    \"./mcp\": \"./dist/mcp.js\",\n    \"./metrics\": \"./dist/metrics.js\",\n    \"./misc\": \"./dist/misc.js\",\n    \"./namespaces\": \"./dist/namespaces.js\",\n    \"./notifications\": \"./dist/notifications.js\",\n    \"./outputs\": \"./dist/outputs.js\",\n    \"./plugins\": \"./dist/plugins.js\",\n    \"./secrets\": \"./dist/secrets.js\",\n    \"./services\": \"./dist/services.js\",\n    \"./shared\": \"./dist/shared.js\",\n    \"./triggers\": \"./dist/triggers.js\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"generate:openapi\": \"openapi-ts\",\n    \"build\": \"tsdown\",\n    \"check:types\": \"tsc --noEmit -p tsconfig.json\"\n  },\n  \"devDependencies\": {\n    \"@hey-api/openapi-ts\": \"^0.99.0\",\n    \"@kestra-io/hey-api-plugin\": \"*\",\n    \"@types/node\": \"^26.6.5\",\n    \"tsdown\": \"^0.23.1\",\n    \"typescript\": \"^6\",\n    \"unrun\": \"^0.3.1\"\n  }\n}\n\n<!-- File: ui/packages/kestra-sdk/tsconfig.json (Tokens: ~148 | Category: anthropic-skill) -->\n{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"lib\": [\"es2023\", \"dom\"],\n    \"moduleDetection\": \"force\",\n    \"module\": \"preserve\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"esModuleInterop\": true,\n    \"isolatedModules\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src\"],\n  \"references\": [\n    { \"path\": \"./tsconfig.node.json\" }\n  ]\n}\n\n<!-- File: ui/packages/kestra-sdk/tsconfig.node.json (Tokens: ~135 | Category: anthropic-skill) -->\n{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"lib\": [\"es2023\"],\n    \"moduleDetection\": \"force\",\n    \"module\": \"preserve\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"types\": [\"node\"],\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"esModuleInterop\": true,\n    \"isolatedModules\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"composite\": true\n  },\n  \"include\": [\"tsdown.config.ts\", \"openapi-ts.config.ts\"]\n}\n\n<!-- File: ui/packages/slot-contracts/package.json (Tokens: ~321 | Category: anthropic-skill) -->\n{\n    \"name\": \"@kestra-io/slot-contracts\",\n    \"version\": \"0.0.0-dev\",\n    \"description\": \"Kestra slot prop contracts – shared type definitions for federated plugin UI slots\",\n    \"type\": \"module\",\n    \"private\": true,\n    \"homepage\": \"https://github.com/kestra-io/kestra#readme\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"git+https://github.com/kestra-io/kestra.git\"\n    },\n    \"bugs\": {\n        \"url\": \"https://github.com/kestra-io/kestra/issues\"\n    },\n    \"exports\": {\n        \".\": \"./src/index.ts\"\n    },\n    \"main\": \"./dist/index.js\",\n    \"types\": \"./dist/index.d.ts\",\n    \"files\": [\n        \"dist\"\n    ],\n    \"scripts\": {\n        \"build\": \"tsdown\",\n        \"dev\": \"tsdown --watch\",\n        \"check:types\": \"vue-tsc --noEmit\",\n        \"prepublishOnly\": \"npm run build\"\n    },\n    \"peerDependencies\": {\n        \"@kestra-io/kestra-sdk\": \"*\"\n    },\n    \"devDependencies\": {\n        \"tsdown\": \"^0.23.1\",\n        \"ts-morph\": \"^28.0.0\",\n        \"unrun\": \"^0.3.1\"\n    },\n    \"dependencies\": {\n        \"zod\": \"^4.6.7\"\n    }\n}\n\n<!-- File: ui/packages/slot-contracts/tsconfig.json (Tokens: ~152 | Category: anthropic-skill) -->\n{\n    \"compilerOptions\": {\n        \"target\": \"esnext\",\n        \"lib\": [\"es2023\", \"DOM\"],\n        \"moduleDetection\": \"force\",\n        \"module\": \"preserve\",\n        \"moduleResolution\": \"bundler\",\n        \"resolveJsonModule\": true,\n        \"types\": [\"node\"],\n        \"strict\": true,\n        \"noUnusedLocals\": true,\n        \"declaration\": true,\n        \"emitDeclarationOnly\": true,\n        \"esModuleInterop\": true,\n        \"isolatedModules\": true,\n        \"verbatimModuleSyntax\": true,\n        \"skipLibCheck\": true,\n        \"allowImportingTsExtensions\": true\n    },\n    \"include\": [\"src\", \"tsdown.config.ts\"]\n}\n\n<!-- File: ui/packages/topology/package.json (Tokens: ~718 | Category: anthropic-skill) -->\n{\n    \"name\": \"@kestra-io/topology\",\n    \"version\": \"0.0.0-dev\",\n    \"description\": \"Kestra flow topology graph component – powered by Vue Flow\",\n    \"type\": \"module\",\n    \"homepage\": \"https://github.com/kestra-io/kestra#readme\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"git+https://github.com/kestra-io/kestra.git\"\n    },\n    \"bugs\": {\n        \"url\": \"https://github.com/kestra-io/kestra/issues\"\n    },\n    \"exports\": {\n        \".\": \"./src/index.ts\",\n        \"./vue-flow-utils\": \"./src/vue-flow-utils.ts\",\n        \"./flow-yaml-utils\": \"./src/utils/flowYamlUtils.ts\"\n    },\n    \"main\": \"./dist/index.js\",\n    \"types\": \"./dist/index.d.ts\",\n    \"files\": [\n        \"dist\"\n    ],\n    \"sideEffects\": [\n        \"**/*.css\",\n        \"./dist/style.css\",\n        \"./src/index.ts\"\n    ],\n    \"publishConfig\": {\n        \"access\": \"public\"\n    },\n    \"scripts\": {\n        \"build\": \"tsdown\",\n        \"dev\": \"tsdown --watch\",\n        \"check:types\": \"vue-tsc --noEmit\",\n        \"test\": \"npm run test:unit\",\n        \"test:unit\": \"vitest run --config vitest.unit.config.ts\",\n        \"test:unit:watch\": \"vitest --config vitest.unit.config.ts\",\n        \"lint\": \"eslint --fix\",\n        \"test:lint\": \"eslint\",\n        \"prepublishOnly\": \"npm run build\"\n    },\n    \"dependencies\": {\n        \"dagre\": \"^0.8.5\",\n        \"html-to-image\": \"^1.11.13\",\n        \"js-yaml\": \"^5.5.0\",\n        \"vue-material-design-icons\": \"^5.4.0\",\n        \"yaml\": \"^2\"\n    },\n    \"peerDependencies\": {\n        \"@kestra-io/design-system\": \"*\",\n        \"@vue-flow/background\": \"^1.3.2\",\n        \"@vue-flow/controls\": \"^1.1.3\",\n        \"@vue-flow/core\": \"^1.48.2\",\n        \"vue\": \"^3.5.0\",\n        \"vue-i18n\": \"^11.4.4\"\n    },\n    \"peerDependenciesMeta\": {\n        \"vue-i18n\": {\n            \"optional\": true\n        }\n    },\n    \"devDependencies\": {\n        \"@tsdown/css\": \"^0.23.1\",\n        \"@types/dagre\": \"^0.7.54\",\n        \"@types/js-yaml\": \"^4.0.9\",\n        \"@types/node\": \"^26.6.5\",\n        \"@vitejs/plugin-vue\": \"^6.0.10\",\n        \"sass-embedded\": \"^1.105.2\",\n        \"tsdown\": \"^0.23.1\",\n        \"typescript\": \"^6\",\n        \"unrun\": \"^0.3.1\",\n        \"vite\": \"^8.3.4\",\n        \"vitest\": \"^5.0.5\",\n        \"vue\": \"^3.5.44\",\n        \"vue-i18n\": \"^11.4.16\",\n        \"vue-tsc\": \"^3.3.13\"\n    }\n}\n\n<!-- File: ui/packages/topology/tsconfig.json (Tokens: ~167 | Category: anthropic-skill) -->\n{\n    \"compilerOptions\": {\n        \"target\": \"esnext\",\n        \"lib\": [\"es2023\", \"DOM\", \"DOM.Iterable\"],\n        \"moduleDetection\": \"force\",\n        \"module\": \"preserve\",\n        \"moduleResolution\": \"bundler\",\n        \"resolveJsonModule\": true,\n        \"types\": [\"node\"],\n        \"strict\": true,\n        \"noUnusedLocals\": true,\n        \"declaration\": true,\n        \"emitDeclarationOnly\": true,\n        \"esModuleInterop\": true,\n        \"isolatedModules\": true,\n        \"verbatimModuleSyntax\": true,\n        \"skipLibCheck\": true,\n        \"allowImportingTsExtensions\": true\n    },\n    \"include\": [\"src\", \"tsdown.config.ts\"],\n    \"exclude\": [\"src/**/*.test.ts\"]\n}\n\n<!-- File: ui/packages/hey-api-plugin/README.md (Tokens: ~300 | Category: plugin-manifest) -->\n# hey-api-plugin Documentation\n\nPath: `ui/packages/hey-api-plugin/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/kestra-io/kestra/HEAD/ui/packages/hey-api-plugin/README.md)\n\n<!-- File: ui/packages/kestra-sdk/README.md (Tokens: ~896 | Category: plugin-manifest) -->\n# @kestra-io/kestra-sdk (OSS)\n\nThe JS/TS client for the Kestra **OSS** API, generated from the backend's own OpenAPI spec and\nliving **in the same repo, on the same commit** as the backend it describes.\n\n- **Generated from:** `io.kestra:webserver` → `./gradlew :webserver:generateOpenapiSpec` → `openapi.yml`\n- **Generator:** [`@hey-api/openapi-ts`](https://heyapi.dev/) with `@hey-api/client-fetch` + the shared\n  [`@kestra-io/hey-api-plugin`](../hey-api-plugin) (tenant-aware, human-friendly wrappers).\n- **Committed:** the generated code under [`src/openapi`](src/openapi) **is checked into git**. This\n  decouples the fast (npm) build from the Gradle/backend build — `npm run dev`, `build`, and\n  `check:types` never need a Java toolchain.\n\nThe package keeps the name `@kestra-io/kestra-sdk` because that is the specifier plugins import to\nreach the app's client (auth + routing).\n\n## Entry points\n\n| Import | Contents |\n| --- | --- |\n| `@kestra-io/kestra-sdk` | `useClient` / `configureClient` / `setMockClient`, and every generated **type** |\n| `@kestra-io/kestra-sdk/<tag>` | the operations of one tag, e.g. `/flows`, `/executions` |\n| `@kestra-io/kestra-sdk/all` | every operation at once — named exports, plus the namespace as `default` |\n\nThe root entry exports **no operations**: re-exporting the generated operations there put all of them\nin the app's initial graph. Reach an operation through its tag, or through `/all` if one import is\npreferable to several.\n\n## Regenerating the SDK\n\nOnly needed when the OSS API changes. From `ui/`:\n\n```bash\nnpm run generate:sdk\n```\n\nThat is the **only** path that invokes Gradle. It: generates `openapi.yml` (Gradle) → hashes it and\ncompares against the `OPENAPI_SPEC_HASH` already committed in `src/openapi/sdk/shared.gen.ts` → if\nthey match, the committed SDK is already correct for the current spec and the rest is skipped; if\nthey differ, builds the shared plugin → runs `openapi-ts` (generate + convert + hash-stamp) →\nbundles `dist/`. This makes `generate:sdk` cheap to run speculatively (e.g. after any backend change)\nsince a no-op spec diff short-circuits before the expensive steps. Commit the resulting\n`src/openapi/` changes (and the regenerated `package.json` `exports` map) when it does regenerate.\n\nEveryday commands never regenerate: `ui/scripts/ensure-sdk.mjs` (the `predev` / `prebuild` /\n`precheck:types` hook) only bundles the already-committed `src/openapi` into `dist/` when `dist/` is\nmissing.\n\n## Drift detection\n\nThe generated SDK exports `OPENAPI_SPEC_HASH` (`sha256(openapi.yml)[:16]`, stamped by the shared\nplugin at generation time — no external bin). Drift is caught **at dev time, not in CI**: on the\nfirst `configureClient` call in a dev build, `dev-freshness.ts` fetches the backend's live spec,\nhashes it the same way, and warns if the committed SDK is behind. The check is guarded by\n`import.meta.env.DEV` + a dynamic import, so it is tree-shaken out of production builds entirely.\n\n## Runtime\n\n`src/index.ts` binds the shared `createConfigureClient(client, formDataBodySerializer)` from\n`@kestra-io/hey-api-plugin/runtime` (bundled into `dist/`, not a runtime dependency) and keeps the\napp-only `useClient()` / `setMockClient()` — an axios-like facade over fetch that shares the same\ninterceptors, so existing `useClient().get/post(...)` call sites are unchanged. The EE SDK reuses\nthese by relative import.\n</yakaai_skills>"}