{"owner":"redis","repo":"RedisInsight","hasSkills":true,"totalSkillsCount":45,"totalTokensCount":44081,"categories":["anthropic-skill","subagent-persona","mcp-config","copilot-instructions","root-instruction","claude-rule","plugin-manifest"],"hasMcp":true,"mcpConfig":null,"found":[".ai/skills/backend/SKILL.md",".ai/skills/branches/SKILL.md",".ai/skills/code-quality/SKILL.md",".ai/skills/commits/SKILL.md",".ai/skills/dead-dependencies/SKILL.md",".ai/skills/e2e-testing/SKILL.md",".ai/skills/feature-flags/SKILL.md",".ai/skills/frontend/SKILL.md",".ai/skills/git-safety/SKILL.md",".ai/skills/pull-requests/SKILL.md",".ai/skills/redis-insight-plugin/SKILL.md",".ai/skills/redis-insight-plugin/references/error-handling.md",".ai/skills/redis-insight-plugin/references/external-parcel-plugin.md",".ai/skills/redis-insight-plugin/references/internal-vite-plugin.md",".ai/skills/redis-insight-plugin/references/iterative-development.md",".ai/skills/redis-insight-plugin/references/official-docs-summary.md",".ai/skills/redis-insight-plugin/references/plugin-manifest.md",".ai/skills/redis-insight-plugin/references/redis-command-parsing.md",".ai/skills/redis-insight-plugin/references/redis-insight-plugin-guidelines.md",".ai/skills/redis-insight-plugin/references/redisinsight-product-ui.md",".ai/skills/redis-insight-plugin/references/review-hardening.md",".ai/skills/redis-insight-plugin/references/testing-and-deployment.md",".ai/skills/redis-insight-plugin/references/third-party-libraries.md",".ai/skills/redis-insight-plugin/templates/external-parcel-package.json",".ai/skills/testing/SKILL.md",".ai/skills/tsconfigs/SKILL.md",".ai/skills/type-check-baselines/SKILL.md",".cursor/agents/test-runner.md",".cursor/mcp.json",".cursor/skills/branches/SKILL.md",".cursor/skills/commits/SKILL.md",".cursor/skills/feature-flags/SKILL.md",".cursor/skills/pull-requests/SKILL.md",".github/copilot-instructions.md","AGENTS.md","CLAUDE.md","docs/plugins/development.md","docs/plugins/installation.md","docs/plugins/introduction.md","redisinsight/ui/src/packages/clients-list/README.md","redisinsight/ui/src/packages/geodata/README.md","redisinsight/ui/src/packages/redisgraph/README.md","redisinsight/ui/src/packages/redisinsight-plugin-sdk/README.md","redisinsight/ui/src/packages/redistimeseries-app/README.md","redisinsight/ui/src/packages/ri-explain/README.md"],"skills":{".ai/skills/backend/SKILL.md":"---\nname: backend\ndescription: >-\n  NestJS backend development patterns for the RedisInsight API:\n  module structure, services, controllers, DTOs, dependency injection,\n  and error handling. Use when editing any file under\n  redisinsight/api/**, writing or modifying NestJS modules,\n  controllers, services, DTOs, providers, or when the user mentions\n  the backend, API, or NestJS.\n---\n\n\n# Backend Development (NestJS/API)\n\n## Module Structure\n\n### NestJS Architecture\n\n- Follow **modular architecture** (feature-based modules)\n- Use **dependency injection** throughout\n- **Separate concerns**: Controllers, Services, Repositories\n- Use **DTOs** for validation and data transfer\n- Apply **proper error handling** with NestJS exceptions\n\n### Module Folder Structure\n\nEach feature module in its own directory under `api/src/`:\n\n```\nfeature/\n├── feature.module.ts           # Module definition\n├── feature.controller.ts       # REST endpoints\n├── feature.service.ts          # Business logic\n├── feature.service.spec.ts     # Service tests\n├── feature.controller.spec.ts  # Controller tests\n├── feature.types.ts            # Interfaces and types related to the feature\n├── dto/                        # Data transfer objects\n│   ├── create-feature.dto.ts\n│   ├── update-feature.dto.ts\n│   └── feature.dto.ts\n├── entities/                   # TypeORM entities\n├── repositories/               # Custom repositories\n├── exceptions/                 # Custom exceptions\n├── guards/                     # Feature-specific guards\n├── decorators/                 # Custom decorators\n└── constants/                  # Feature constants\n```\n\n### File Naming\n\n- **Modules**: `feature.module.ts`\n- **Controllers**: `feature.controller.ts`\n- **Services**: `feature.service.ts`\n- **DTOs**: `create-feature.dto.ts`, `update-feature.dto.ts`\n- **Entities**: `feature.entity.ts`\n- **Interfaces and types**: `feature.types.ts`\n- **Tests**: `feature.service.spec.ts`\n- **Constants**: `feature.constants.ts`\n- **Exceptions**: `feature-not-found.exception.ts`\n\n### Constants Organization\n\nStore feature-specific constants in dedicated constants file:\n\n```typescript\nexport const FEATURE_CONSTANTS = {\n  MAX_NAME_LENGTH: 100,\n  DEFAULT_PAGE_SIZE: 20,\n} as const;\n\nexport const FEATURE_ERROR_MESSAGES = {\n  NOT_FOUND: 'Feature not found',\n  INVALID_INPUT: 'Invalid feature data',\n} as const;\n```\n\n### Imports Order\n\n1. Node.js built-in modules\n2. External dependencies (`@nestjs/*`, etc.)\n3. Internal modules (using `src/*` alias — the backend's self-reference into `redisinsight/api/src/*`)\n4. Local relative imports\n\n## Service Layer\n\n### Service Pattern\n\n- Inject dependencies via constructor\n- Use TypeORM repositories\n- Handle errors with NestJS exceptions\n- Use Logger for important operations\n- Keep business logic in services (not controllers)\n\n### Dependency Injection\n\nAlways inject dependencies via constructor with proper decorators:\n\n```typescript\n@Injectable()\nexport class UserService {\n  constructor(\n    @InjectRepository(User)\n    private readonly userRepository: Repository<User>,\n    private readonly emailService: EmailService,\n  ) {}\n}\n```\n\n## Controller Layer\n\n### Controller Pattern\n\n- Keep controllers thin (delegate to services)\n- Use proper HTTP decorators (`@Get`, `@Post`, etc.)\n- Use `@Body`, `@Param`, `@Query` for inputs\n- Apply guards with `@UseGuards()`\n- Document with Swagger decorators\n\n### HTTP Status Codes\n\n- Use `@HttpCode()` decorator for non-standard codes\n- Return appropriate status codes (200, 201, 204, 400, 404, etc.)\n\n## Data Transfer Objects (DTOs)\n\n### Validation\n\nUse `class-validator` decorators for validation:\n\n- `@IsString()`, `@IsNumber()`, `@IsEmail()`\n- `@IsNotEmpty()`, `@IsOptional()`\n- `@MinLength()`, `@MaxLength()`\n- `@Min()`, `@Max()`\n\n### Swagger Documentation\n\nUse `@ApiProperty()` and `@ApiPropertyOptional()` for Swagger docs.\n\n## Error Handling\n\n### NestJS Exceptions\n\nUse appropriate exception types:\n\n- `NotFoundException` - 404\n- `BadRequestException` - 400\n- `UnauthorizedException` - 401\n- `ForbiddenException` - 403\n- `ConflictException` - 409\n- `InternalServerErrorException` - 500\n\n### Custom Exceptions\n\n**Prefer custom exceptions over generic ones** when you need:\n\n- Consistent error codes for frontend handling\n- Specific error messages from constants\n- Consistent error structure across the codebase\n\nCreate custom exceptions following existing patterns:\n\n```typescript\n// src/modules/feature/exceptions/feature-invalid.exception.ts\nimport {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class FeatureInvalidException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.FEATURE_INVALID,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'FeatureInvalid',\n      errorCode: CustomErrorCodes.FeatureInvalid,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n```\n\n### Error Logging\n\n```typescript\nprivate readonly logger = new Logger(ServiceName.name)\n\nthis.logger.error('Error message', error.stack, { context })\n```\n\n## Redis Integration\n\n### Redis Service Pattern\n\n- Use RedisClient from `src/modules/redis`\n- Handle errors gracefully\n- Log Redis operations\n- Use try-catch for error handling\n\n## Code Quality\n\n### Cognitive Complexity (≤ 15)\n\n- Use early returns to reduce nesting\n- Extract complex logic to separate functions\n- Avoid deeply nested conditions\n\n### No Duplicate Strings\n\nExtract repeated strings to constants in constants file.\n\n## API Documentation (Swagger)\n\n### Required Decorators\n\n- `@ApiTags()` - Group endpoints\n- `@ApiOperation()` - Describe operation\n- `@ApiResponse()` - Document responses\n- `@ApiParam()` - Document params\n- `@ApiQuery()` - Document query params\n- `@ApiBearerAuth()` - Auth requirement\n\n## Checklist\n\n- [ ] Services use dependency injection\n- [ ] DTOs have validation decorators\n- [ ] Controllers have Swagger documentation\n- [ ] Proper HTTP status codes used\n- [ ] Error handling with appropriate exceptions\n- [ ] Logging for important operations\n- [ ] Transactions for related DB operations\n- [ ] Configuration via ConfigService\n- [ ] Guards for authentication/authorization\n- [ ] Cognitive complexity ≤ 15\n",".ai/skills/branches/SKILL.md":"---\nname: branches\ndescription: >-\n  Create and name git branches following project conventions. Use when\n  creating branches, checking out new branches, or the user mentions\n  branch naming.\n---\n\n# Branch Naming Conventions\n\nUse lowercase kebab-case with type prefix and issue/ticket identifier.\n\n```bash\n# Pattern: <type>/<issue-ref>/<short-title>\n\n# INTERNAL (JIRA - RI-XXX)\nfeature/RI-123/add-user-profile\nbugfix/RI-789/memory-leak\nfe/feature/RI-567/add-dark-mode\nbe/bugfix/RI-345/fix-redis-connection\ndocs/RI-333/update-docs\ntest/RI-444/add-unit-tests\ne2e/RI-555/add-integration-tests\n\n# OPEN SOURCE (GitHub - XXX)\nfeature/123/add-export-feature\nbugfix/789/fix-connection-timeout\n\n# Special branches\nrelease/v2.0.0\nric/RI-666/custom-prefix\n```\n\n## Branch Types\n\n- `feature/` - New features and refactoring (affects multiple areas)\n- `bugfix/` - Bug fixes (affects multiple areas)\n- `fe/feature/` - Frontend-only features (only `redisinsight/ui/` folder)\n- `fe/bugfix/` - Frontend-only bug fixes (only `redisinsight/ui/` folder)\n- `be/feature/` - Backend-only features (only `redisinsight/api/` folder)\n- `be/bugfix/` - Backend-only bug fixes (only `redisinsight/api/` folder)\n- `docs/` - Documentation changes\n- `test/` - Test-related changes\n- `e2e/` - End-to-end test changes\n- `release/` - Release branches\n- `ric/` - Custom prefix for special cases\n\n**Note:** When a bug fix affects only the `redisinsight/ui/` folder, use `fe/bugfix/` prefix instead of `bugfix/`.\n\n## Issue References\n\n- **Internal**: `RI-XXX` (JIRA ticket)\n- **Open Source**: `XXX` (GitHub issue number)\n- Use `#` only in commit messages, not branch names\n",".ai/skills/code-quality/SKILL.md":"---\nname: code-quality\ndescription: >-\n  Code-quality standards for RedisInsight: TypeScript strictness,\n  naming conventions (camelCase, PascalCase, UPPER_SNAKE_CASE),\n  linting rules, no `any` without reason, no `!important` in styles,\n  and constant extraction. Use when writing or refactoring any\n  TypeScript/JavaScript code in this repo, when ESLint or Prettier\n  issues come up, or when the user mentions code style, lint, naming\n  conventions, or general code quality.\n---\n\n\n# Code Quality Standards\n\n## Critical Rules\n\n- **ALWAYS run linter** after code changes: `npm run lint`\n- Linter must pass before committing\n- No console.log in production code (use console.warn/error only)\n\n## TypeScript Standards\n\n### Essential Rules\n\n- Use TypeScript for all new code\n- **Avoid `any`** - use proper types or `unknown`\n- **Prefer interfaces** for object shapes\n- Use **type** for unions, intersections, primitives\n- Add explicit return types for non-obvious functions\n- Leverage type inference where clear\n\n## Import Organization\n\n### Required Order (enforced by ESLint)\n\n1. External libraries (`react`, `lodash`, etc.)\n2. Built-in Node modules (`path`, `fs` - backend only)\n3. Internal modules with aliases (`uiSrc/*`, `apiClient`)\n4. Sibling/parent relative imports\n5. Style imports (ALWAYS LAST)\n\n### Module Aliases\n\n- `uiSrc/*` → `redisinsight/ui/src/*` (UI workspace)\n- `apiClient` → `redisinsight/api-client` (auto-generated OpenAPI types — the UI's only entry point into BE-defined shapes)\n- `desktopSrc/*` → `redisinsight/desktop/src/*` (desktop workspace)\n\nThe UI workspace must not import from the backend codebase directly. Use `apiClient` for types and the existing service layer (`uiSrc/services`) for HTTP calls.\n\n✅ **Use aliases**: `import { Button } from 'uiSrc/components/Button'`  \n❌ **Avoid relative**: `import { Button } from '../../../ui/src/components/Button'`\n\n## Naming Conventions\n\n- **Components**: `PascalCase` - `UserProfile`\n- **Functions/Variables**: `camelCase` - `fetchUserProfile`\n- **Constants**: `UPPER_SNAKE_CASE` - `MAX_RETRY_ATTEMPTS`\n- **Booleans**: Use `is/has/should` prefix - `isLoading`, `hasError`\n\n## Comments\n\n**Default to fewer comments.** Clear names, small functions, and good\ntests should carry the meaning. Write a comment only when the code\ngenuinely can't explain itself — or when the user explicitly asks for\nmore (for example, on a piece of complex logic).\n\nWhen you do write one, keep it short and use plain words to say\n**what** the code is doing or **why** it has to exist — not how it\nworks under the hood, and not the story of why you made the change\n(that belongs in the commit message or PR description).\n\n✅ **Good** — plain, names the situation:\n\n```ts\n// When switching keys, hide the previous key's loader/error/result\n// until the new key is ready.\nconst showLoader = isArrayKeyReady && loading\n```\n\n❌ **Avoid** — long, mechanism-heavy, re-derives what the code shows:\n\n```ts\n// Gate every aggregate-slice surface (loader, error, result) on\n// `isArrayKeyReady`: when the user switches keys, `keyProp` flips\n// immediately but `selectedKeyData` lags by a round-trip, so the\n// prior key's slice state would otherwise paint under the newly\n// selected (or empty) key for one frame before the hook's reset\n// effect fires.\nconst showLoader = isArrayKeyReady && loading\n```\n\nAlso avoid:\n\n- Restating identifier names in prose (`// set loading to true`).\n- Comments that duplicate what a clearly-named test already asserts.\n- Block comments that summarize obvious blocks (`// loop over items`).\n\n## SonarJS Rules\n\n- Keep cognitive complexity low (refactor complex functions)\n- Extract duplicate strings to constants\n- Follow DRY principle - no duplicate code\n- Use immediate return (avoid unnecessary intermediate variables)\n\n## Best Practices\n\n- Use destructuring for objects and arrays\n- Use template literals over string concatenation\n- Use `const` by default, `let` only when reassignment needed\n- Use descriptive variable names\n- Handle errors properly\n- Clean up subscriptions and timers\n- Use constants instead of magic numbers\n\n## Vite Cache Management\n\nWhen updating npm packages (especially `@redis-ui/*` packages):\n\n1. **Clear Vite cache** after `npm install`:\n\n   ```bash\n   rm -rf node_modules/.vite\n   rm -rf redisinsight/ui/node_modules/.vite\n   ```\n\n2. **Restart dev server** to rebuild dependencies\n\n3. This ensures new package versions are properly loaded\n\n## Pre-Commit Checklist\n\n- [ ] `npm run lint` passes\n- [ ] No TypeScript errors\n- [ ] Import order is correct\n- [ ] No `any` types without reason\n- [ ] No console.log statements\n- [ ] No magic numbers\n- [ ] Descriptive variable names\n- [ ] Low cognitive complexity\n- [ ] No duplicate code\n- [ ] Comments kept to a minimum; any present are short and plain-language\n- [ ] Vite cache cleared (if updated dependencies)\n",".ai/skills/commits/SKILL.md":"---\nname: commits\ndescription: >-\n  Generate commit messages following Conventional Commits format. Use when\n  creating commits, writing commit messages, or the user asks to commit changes.\n---\n\n# Commit Message Guidelines\n\nFollow **Conventional Commits** format:\n\n```\n<type>(<scope>): <subject>\n\n<body>\n\n<footer>\n```\n\n## Commit Types\n\n- `feat` - New feature\n- `fix` - Bug fix\n- `refactor` - Code refactoring\n- `test` - Adding or updating tests\n- `docs` - Documentation changes\n- `chore` - Maintenance tasks\n- `style` - Code style changes (formatting)\n- `perf` - Performance improvements\n- `ci` - CI/CD changes\n\n## Examples\n\n```bash\nfeat(ui): add user search functionality\n\nImplements real-time search with debouncing.\n\nReferences: #RI-123\n\n---\n\nfix(api): resolve memory leak in connection pool\n\nProperly cleanup subscriptions on unmount.\n\nFixes #456\n\n---\n\ntest(ui): add tests for data serialization\n\nrefactor(api): extract common validation logic\n\ndocs: update API endpoint documentation\n\nchore: upgrade React to version 18.2\n```\n\n## Best Practices\n\n### ✅ Good Commits\n\n- Clear, descriptive subject line\n- Atomic changes (one logical change per commit)\n- Reference issue/ticket in body\n- Explain **why**, not just **what**\n- **Keep it concise** - Don't list every file change in the body\n\n```bash\nfeat(ui): add user profile editing\n\nAllows users to update their profile information including\nname, email, and avatar. Includes validation and error handling.\n\nReferences: #RI-123\n```\n\n### ❌ Bad Commits\n\n```bash\n# Too vague\nfix stuff\nWIP\nupdate\n\n# Too broad\nadd feature, fix bugs, refactor code, update tests\n```\n\n## Issue References\n\n- **JIRA (internal)**: `References: #RI-123` or `Fixes #RI-123`\n- **GitHub (open source)**: `Fixes #456` or `Closes #456`\n- Use `#` for auto-linking in commit messages\n",".ai/skills/dead-dependencies/SKILL.md":"---\nname: dead-dependencies\ndescription: >-\n  Find and safely remove unused (\"dead\") npm dependencies in RedisInsight\n  using a grep + leaf-check + build-gate recipe. Use when cleaning up\n  dependencies, investigating whether a package is still used, removing a\n  suspected leftover, or when the user mentions dead deps, unused\n  dependencies, dependency cleanup, leftover packages, or \"is this safe to\n  remove\". Complements the weekly vulnerability audit\n  (`scripts/dependency-audit-report.mjs`), which only reports vulnerabilities.\n---\n\n# Dead Dependencies\n\nIdentify and safely remove unused npm dependencies. This is a **local,\ninteractive** workflow — a scheduled report can only ever guess; confirming a\ndependency is dead requires grepping real usage and running the build.\n\n## Why manual (not knip/depcheck)\n\nStatic tools over-report badly in this repo and must not be trusted blindly:\n\n- The root `package.json` is a **mega-manifest** (UI + desktop + build + test\n  + storybook + electron), so a tool scanning one area flags everything used\n  elsewhere as \"unused\".\n- Lots of dependencies are referenced **without an `import`**: webpack loaders\n  and eslint/jest/babel plugins by string in config, tools invoked from\n  `package.json` `scripts`, runtime `-r` preloads, dynamic `require`, and\n  ambient `@types/*`.\n- The webpack→Vite migration left **genuinely dead** build tooling behind,\n  mixed in with false positives.\n\nThe reliable signal is: **grep for usage → confirm it's a leaf → remove it and\nrun the build gate.** That is exactly how `jsonpath` was confirmed dead and\nremoved (worked example at the bottom).\n\n## The recipe (per candidate)\n\n### 1. Grep the whole repo for real usage\n\n```bash\nPKG=jsonpath   # the dependency to check\ngrep -rn \"$PKG\" \\\n  redisinsight configs scripts tests .storybook \\\n  --include=*.ts --include=*.tsx --include=*.js --include=*.jsx \\\n  --include=*.mjs --include=*.cjs --include=*.json \\\n  2>/dev/null | grep -v node_modules\n```\n\nLook for `import ... from '$PKG'`, `require('$PKG')`, `import('$PKG')`, and\nbare references. Ignore unrelated substring hits (e.g. `nestjs-form-data` when\nchecking `form-data`, or a package name appearing only in tutorial/`manifest`\ntext). **Zero real references → candidate for the next steps.**\n\n### 2. Rule out no-import usage\n\nA clean grep is **necessary but not sufficient**. Check the ways a package is\nused without an `import`:\n\n- **Config string references** — search the build/test config for the bare\n  name: `.eslintrc.js`, `configs/webpack.config.*.ts`,\n  `redisinsight/ui/vite.config.mjs`, `jest.config.cjs`, `babel.config.cjs`,\n  `electron-builder.json`, `.mocharc*`. eslint plugins, webpack loaders, and\n  jest/mocha reporters live here.\n- **`package.json` scripts** — a tool like `concurrently`, `lint-staged`, or a\n  reporter is \"used\" if a `scripts` entry (any workspace) invokes it.\n- **Runtime preloads / dynamic** — `node -r <pkg>`, `require(variable)`, or a\n  wasm/worker loader with a computed path.\n\n### 3. Leaf check\n\n```bash\nnpm ls \"$PKG\"        # in the workspace that declares it\n```\n\nConfirm nothing else in the tree depends on it, and note any transitive deps it\nuniquely pulls (removing `jsonpath` also dropped `underscore`).\n\n### 4. Classify before acting\n\n| Situation | Action |\n| --- | --- |\n| Plain runtime/dev dep, zero references anywhere | **Delete** (after the gate below) |\n| `@types/x` where base `x` bundles its own types (`node_modules/x/package.json` has `types`/`typings`) | **Delete** — the DefinitelyTyped package is obsolete |\n| `@types/x` that's ambient/global-only (e.g. `@types/webpack-env`) | **Keep** — never imported by design |\n| Declared in more than one workspace (`package.json`), used in only one | **Relocate/dedupe** — remove the *unused* declaration, never the used copy |\n| Shadows a Node builtin (`buffer`, `assert`, …) | **Keep** — usually a false positive |\n| Referenced only in a config/`scripts` (step 2 hit) | **Keep** |\n\n### 5. The gate — the only real proof\n\nRemove it, reinstall (per the repo's dependency rules — never hand-edit\n`package.json`/lockfile, never `--ignore-scripts`), and verify the affected\narea builds:\n\n```bash\nnpm uninstall \"$PKG\"          # in the declaring workspace; updates the lockfile\nnpm run type-check            # or the affected workspace's type-check\nnpm run test                  # / test:api, as relevant\nnpm run build                 # if it's a build-time dep\n```\n\nGreen across the relevant checks = safe. Commit the `package.json` +\n`package-lock.json` change (see the git-safety / dependency rules in\n`CLAUDE.md`). If anything goes red, it wasn't dead — restore it.\n\n## Optional: enumerate candidates to sweep\n\nTo triage the whole surface rather than one package, list declared deps and\ngrep each for an import, then apply steps 2–5 to the ones with zero hits:\n\n```bash\nnode -e \"const p=require('./package.json');console.log([...Object.keys(p.dependencies||{}),...Object.keys(p.devDependencies||{})].join('\\n'))\" \\\n| while read PKG; do\n    hits=$(grep -rl --include=*.ts --include=*.tsx --include=*.js --include=*.jsx --include=*.mjs \\\n      -e \"from ['\\\"]$PKG\" -e \"require(['\\\"]$PKG\" redisinsight configs scripts tests 2>/dev/null | grep -vc node_modules)\n    [ \"$hits\" = \"0\" ] && echo \"candidate: $PKG\"\n  done\n```\n\nThis is a **first filter only** — every candidate still goes through steps 2–5.\nExpect false positives (config/string/dynamic/ambient use). Never delete straight\nfrom this list.\n\n## Worked example — `jsonpath`\n\n1. Grep across the repo → the only hits were unrelated tutorial text in a\n   `manifest.json`; **no `import`/`require`**.\n2. Not referenced in any config or script.\n3. `npm ls jsonpath` → a leaf; it was also the sole reason `underscore` was\n   installed.\n4. Plain runtime dep, zero references → likely deletable.\n5. Removed `jsonpath` + `@types/jsonpath`, `npm install`, `npm run type-check`\n   → green. Confirmed dead; committed. (`underscore` dropped with it.)\n",".ai/skills/e2e-testing/SKILL.md":"---\nname: e2e-testing\ndescription: >-\n  Playwright end-to-end testing standards for RedisInsight: page\n  object models, test structure, fixtures, navigation patterns, and\n  flaky-test prevention. Use when editing files under\n  tests/e2e-playwright/**, writing Playwright tests, adding page\n  objects, or when the user mentions Playwright, E2E tests, page\n  objects, or end-to-end testing.\n---\n\n\n# E2E Testing Standards (Playwright)\n\n## Location\n\nAll E2E tests are in `tests/e2e-playwright/`. This is a **standalone package** - no imports from `redisinsight/ui/` or `redisinsight/api/`.\n\n## Test Plan\n\n**Always refer to `tests/e2e-playwright/TEST_PLAN.md`** for:\n- Test coverage status (✅ implemented, 🔲 not implemented)\n- Feature implementation order\n- Test data requirements\n\n**After implementing tests, update TEST_PLAN.md** to mark tests as ✅.\n\n## Project Structure\n\n```\ntests/e2e-playwright/\n├── TEST_PLAN.md         # Master test plan with coverage status\n├── config/              # Configuration (env, databases)\n│   └── databases/       # Database configs by type\n├── fixtures/            # Playwright fixtures\n├── helpers/             # API helpers for setup/teardown\n├── pages/               # Page Object Models\n│   ├── BasePage.ts      # Base class for all pages\n│   ├── InstancePage.ts  # Base class for database instance pages\n│   ├── components/      # Shared components (InstanceHeader, NavigationTabs, BottomPanel)\n│   └── {feature}/       # Feature-specific pages (browser/, cli/, etc.)\n├── test-data/           # Test data factories\n├── tests/               # Test specs organized by project\n│   ├── main/            # Default parallel tests\n│   │   └── {feature}/\n│   │       └── {action}/\n│   ├── auto-update/     # Serial tests with special setup\n│   └── electron/        # Electron-specific tests\n└── types/               # TypeScript types\n```\n\n## Playwright Projects\n\nThe folder a test lives in determines its execution mode. Each browser platform has a parallel project and a serial project:\n\n| Project             | Folder            | Parallelism | Use Case |\n|---------------------|-------------------|-------------|----------|\n| `chromium-parallel` | `tests/parallel/` | Parallel (4 workers) | Standard chromium tests |\n| `chromium-serial`   | `tests/serial/`   | Serial (1 worker)    | Sequential chromium tests |\n| `electron-parallel` | `tests/parallel/` | Serial (1 worker)*   | Standard electron tests |\n| `electron-serial`   | `tests/serial/`   | Serial (1 worker)    | Sequential electron tests |\n\n\\* Electron uses one worker today because there is a single app instance.\n\n### Execution order\n\nFor each platform, the serial project depends on the parallel project via `dependencies: ['<platform>-parallel']` in [playwright.config.ts](../../tests/e2e-playwright/playwright.config.ts), so the order is always:\n\n1. `<platform>-parallel` runs first (with up to N workers)\n2. `<platform>-serial` runs after, one worker at a time\n\nSerial tests perform destructive operations on the shared RTE Redis (`FLUSHDB`, broad `deleteAllIndexes`, dangerous commands) so they can't safely run alongside parallel tests on the same RTE. Electron also can't run its two projects concurrently because the desktop app binds its embedded API on a fixed port (`5530`).\n\nIf serial grows large enough to become a CI bottleneck, the next step is to give serial tests a dedicated Redis instance (or split into a separate CI job) — not to revert the ordering.\n\n### Running Projects\n\n```bash\n# Full platform run (parallel + serial)\nnpx playwright test --project=chromium-parallel --project=chromium-serial\nnpx playwright test --project=electron-parallel --project=electron-serial\n\n# Just parallel\nnpx playwright test --project=chromium-parallel\n\n# Just serial — needs --no-deps, otherwise parallel runs first (it's a\n# project dependency). --no-deps also skips browser-setup.\nnpx playwright test --project=chromium-serial --no-deps\n\nnpx playwright test                           # All projects\n```\n\n### When to put a test in `tests/serial/`\n\nPut a test in `tests/serial/` when it:\n- Shares database state across tests via `beforeAll`\n- Runs dangerous commands or mutates global app state. Settings are global:\n  `scanThreshold`, `dateFormat`, `timezone` and `batchSize` are one shared resource,\n  so a spec that changes one and holds it across other work breaks every concurrent\n  test that reads it\n- Cannot tolerate concurrent execution with other tests\n- Would cause flakiness when run with other tests\n- Require special environment configuration\n\n### Adding a New Project\n\n1. Create folder under `tests/` (e.g., `tests/my-feature/`)\n2. Add project configuration in `playwright.config.ts`:\n\n```typescript\n{\n  name: 'my-feature',\n  testDir: './tests/my-feature',\n  fullyParallel: false, // or true\n  workers: 1,\n  timeout: 120000,\n  // Optional: different setup\n  // globalSetup: './my-feature-setup.ts',\n}\n```\n\n## Page Objects\n\n### Page Object Hierarchy\n\n```\nBasePage (abstract)\n  ├── DatabasesPage           # Databases list page\n  ├── SettingsPage            # Settings page\n  └── InstancePage (abstract) # Base for all database instance pages\n        ├── instanceHeader    # Database name, stats, breadcrumb\n        ├── navigationTabs    # Browse, Workbench, Analyze, Pub/Sub\n        ├── bottomPanel       # CLI, Command Helper, Profiler\n        └── BrowserPage       # Browser-specific (extends InstancePage)\n              └── WorkbenchPage (future)\n              └── AnalyzePage (future)\n              └── PubSubPage (future)\n```\n\n### Extend the Appropriate Base Class\n\n- **BasePage** - For standalone pages (DatabasesPage, SettingsPage)\n- **InstancePage** - For pages within a connected database (BrowserPage, WorkbenchPage, etc.)\n\nPage objects are **stateless** - they don't store database objects. Pass `databaseId` to navigation methods.\n\n```typescript\n// For database instance pages - extend InstancePage\nimport { Page, Locator } from '@playwright/test';\nimport { InstancePage } from '../InstancePage';\n\nexport class WorkbenchPage extends InstancePage {\n  readonly editor: Locator;\n\n  constructor(page: Page) {\n    super(page);\n    this.editor = page.getByTestId('workbench-editor');\n  }\n\n  // InstancePage provides: instanceHeader, navigationTabs, bottomPanel\n  // Plus navigation methods: navigateToBrowser(), openCli(), etc.\n\n  async goto(databaseId: string): Promise<void> {\n    await this.gotoDatabase(databaseId);\n    await this.navigationTabs.gotoWorkbench();\n    await this.waitForLoad();\n  }\n}\n```\n\n### Component-Based Structure\n\nBreak large pages into components:\n\n```typescript\n// pages/feature/FeaturePage.ts\nexport class FeaturePage extends InstancePage {\n  readonly dialog: FeatureDialog;\n  readonly list: FeatureList;\n\n  constructor(page: Page) {\n    super(page);\n    this.dialog = new FeatureDialog(page);\n    this.list = new FeatureList(page);\n  }\n}\n```\n\n## Test Structure\n\n### File Organization\n\n```\ntests/\n├── main/                # Default parallel tests (most tests go here)\n│   └── {feature}/       # e.g., databases, browser, workbench\n│       └── {action}/    # e.g., add, edit, delete\n│           ├── standalone.spec.ts\n│           └── cluster.spec.ts\n├── auto-update/         # Serial tests with special setup\n└── electron/            # Electron-specific tests\n```\n\n### Test Setup Pattern\n\nUse simple, explicit setup with clear separation of concerns. **Page objects are fixtures** - they don't store database state. Pass `databaseId` to `goto()` methods.\n\n```typescript\nimport { test, expect } from '../../../fixtures/base';\nimport { standaloneConfig } from '../../../config/databases/standalone';\nimport { DatabaseInstance } from '../../../types';\n\ntest.describe('Feature > Action', () => {\n  let database: DatabaseInstance;\n\n  // Setup: Create database once for all tests\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase({\n      name: 'test-feature-db',\n      host: standaloneConfig.host,\n      port: standaloneConfig.port,\n    });\n  });\n\n  // Teardown: Clean up database after all tests\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  test.describe('Sub-feature', () => {\n    // Navigation: Pass databaseId to goto() - page is a fixture\n    test.beforeEach(async ({ featurePage }) => {\n      await featurePage.goto(database.id);\n    });\n\n    // Tests receive page fixtures they need\n    test('should do something', async ({ featurePage }) => {\n      await featurePage.doAction();\n      await expect(featurePage.result).toBeVisible();\n    });\n\n    // Tests that need both page and apiHelper\n    test('should create and verify', async ({ featurePage, apiHelper }) => {\n      await apiHelper.createKey(database.id, 'test-key', 'value');\n      await featurePage.refresh();\n      await expect(featurePage.keyList).toContainText('test-key');\n    });\n  });\n});\n```\n\n### Key Principles\n\n1. **`beforeAll`** - Create database/test data via API (runs once)\n2. **`afterAll`** - Clean up database/test data via API (runs once)\n3. **`beforeEach`** - Navigate to page via UI using `goto(databaseId)` (runs before each test)\n4. **Individual tests** - Receive page fixtures they need in the signature\n5. **Page objects are stateless** - Don't store database objects in pages, pass IDs to methods\n\n### Avoid These Anti-Patterns\n\n```typescript\n// ❌ BAD: Storing database in page object\nconst browserPage = createBrowserPage(database);  // OLD pattern - don't use\n\n// ✅ GOOD: Pass databaseId to goto()\nawait browserPage.goto(database.id);\n\n// ❌ BAD: Using page fixture without declaring it in test signature\ntest('should work', async () => {\n  await browserPage.doSomething();  // browserPage is undefined!\n});\n\n// ✅ GOOD: Declare fixtures in test signature\ntest('should work', async ({ browserPage }) => {\n  await browserPage.doSomething();\n});\n\n// ❌ BAD: Navigation inside each test\ntest('should work', async ({ browserPage }) => {\n  await browserPage.goto(database.id);  // Should be in beforeEach\n  // ...\n});\n\n// ❌ BAD: Using test.describe.serial when not needed\ntest.describe.serial('Feature', () => { // Use regular describe unless tests truly depend on each other\n  // ...\n});\n```\n\n## Test Data\n\n### Use Fishery Factories with Faker\n\nUse the [fishery](https://github.com/thoughtbot/fishery) library for test data factories:\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nexport const TEST_PREFIX = 'test-';\n\nexport const ConfigFactory = Factory.define<Config>(() => ({\n  name: `${TEST_PREFIX}${faker.string.alphanumeric(8)}`,\n  host: '127.0.0.1',\n  port: 6379,\n}));\n\n// Usage in tests\nconst config = ConfigFactory.build();\nconst config = ConfigFactory.build({ name: 'custom-name' });\n```\n\n### Cleanup Pattern\n\nPrefix test data with `test-` so leftovers are identifiable, but **never clean up by\nthat prefix**. Specs share a Redis instance and different files run on different\nworkers, so deleting every `test-*` key also removes keys the specs alongside it are\nstill asserting on, and their rows disappear mid-test.\n\nTrack what a test creates and delete only that:\n\n```typescript\nimport { createKeyTracker } from 'e2eSrc/helpers';\n\ntest.describe('Browser > Add Key', () => {\n  const keys = createKeyTracker();\n\n  test.afterEach(async ({ apiHelper }) => {\n    await keys.cleanup(apiHelper, database.id);\n  });\n\n  test('should add a Hash key', async ({ browserPage }) => {\n    const keyData = keys.track(HashKeyFactory.build());\n    // keys.add(name) records a name built without a factory, e.g. a rename target.\n  });\n});\n```\n\nMiss a `track()` and the key is never deleted, which is harmless in CI (the test\nenvironment is recreated per run) but accumulates locally until the RTE restarts.\n\nDatabases need no such care: each spec deletes its own in `afterAll`, and\n`browser.setup` clears leftover `test-` databases once before the run.\n\n## Fixtures\n\n### Add New Fixtures to base.ts\n\n```typescript\n// fixtures/base.ts\ntype Fixtures = {\n  myPage: MyPage;\n  apiHelper: ApiHelper;\n};\n\nexport const test = base.extend<Fixtures>({\n  myPage: async ({ page }, use) => {\n    await use(new MyPage(page));\n  },\n  apiHelper: async ({}, use) => {\n    const helper = new ApiHelper();\n    await use(helper);\n    await helper.dispose();\n  },\n});\n```\n\n## UI Exploration with Playwright MCP\n\n**Before writing tests, ALWAYS use Playwright MCP to explore the UI:**\n\n### Why Explore First?\n- Discover actual `data-testid` attributes used in the application\n- Understand element roles and accessible names for `getByRole()`\n- See page structure and component hierarchy\n- Avoid trial-and-error test writing\n\n### Exploration Workflow\n\n1. **Navigate to the page**: `browser_navigate_Playwright` to target URL\n2. **Take snapshot**: `browser_snapshot_Playwright` to see element tree\n3. **Interact with elements**: `browser_click_Playwright` to trigger dialogs, dropdowns, etc.\n4. **Wait for async content**: `browser_wait_for_Playwright` for dynamic content\n5. **Document findings**: Add discovered UI patterns to `TEST_PLAN.md` under the feature section\n\n### What to Look For\n\n- `data-testid` attributes → use with `page.getByTestId()`\n- Element roles (button, combobox, grid, treeitem) → use with `page.getByRole()`\n- Accessible names → use with `{ name: 'text' }` option\n- Form field placeholders → use with `page.getByPlaceholder()`\n- Text content patterns → use with `page.getByText()`\n\n### Use Discovered Patterns in Page Objects\n\nAfter exploring, use discovered patterns directly in Page Object locators:\n\n```typescript\n// Use data-testid when available\nthis.addButton = page.getByTestId('btn-add-key');\n\n// Use role + name for accessible elements\nthis.submitButton = page.getByRole('button', { name: 'Submit' });\n\n// Use placeholder for form fields\nthis.searchInput = page.getByPlaceholder('Search...');\n```\n\n**Note**: Keep TEST_PLAN.md as a simple visual list of test cases. Document UI patterns in Page Object comments if needed.\n\n## Best Practices\n\n### ✅ DO\n\n- **Explore UI with Playwright MCP before writing tests**\n- **Use Page Object navigation methods** (e.g., `browserPage.goto()`, `workbenchPage.goto()`)\n- Use `data-testid` attributes for stable selectors\n- Use `getByRole`, `getByLabel` for accessible elements\n- Wait for elements with `waitFor({ state: 'visible' })`\n- Clean up test data in `afterEach`\n- Use API for setup, UI for assertions\n- Handle both List view and Tree view in key assertions\n\n### ❌ DON'T\n\n- **NEVER use `page.goto()` directly** - tests must work in both browser and Electron\n- Write tests without exploring the actual UI first\n- Use fixed timeouts (`waitForTimeout`)\n- Use CSS selectors for dynamic content\n- Leave test data after tests\n- Import from `redisinsight/ui/` or `redisinsight/api/`\n- Hardcode test data (use faker)\n- Assume element structure without verification\n\n## Navigation (IMPORTANT)\n\n**All navigation must use UI-based methods, NOT URL navigation.**\n\nTests must work in both browser mode (http://localhost:8080) and Electron mode (no baseURL). Direct `page.goto()` calls fail in Electron because there's no baseURL.\n\n### Navigation Architecture\n\n**BasePage** provides only fundamental navigation:\n```typescript\nawait this.gotoHome();              // Click Redis logo → databases list\nawait this.gotoDatabase(dbId);      // Click database → Browser page (default)\n```\n\n**Each page owns its navigation** via its `goto()` method:\n```typescript\nawait settingsPage.goto();           // Settings page\nawait browserPage.goto(dbId);        // Browser page for database\nawait workbenchPage.goto(dbId);      // Workbench page for database\nawait analyticsPage.goto(dbId);      // Analytics page for database\nawait pubSubPage.goto(dbId);         // Pub/Sub page for database\n```\n\n**NavigationTabs component** handles tab switching within a connected database:\n```typescript\nawait browserPage.navigationTabs.gotoBrowser();\nawait browserPage.navigationTabs.gotoWorkbench();\nawait browserPage.navigationTabs.gotoAnalyze();\nawait browserPage.navigationTabs.gotoPubSub();\n```\n\n### ✅ Correct Navigation Pattern\n\n```typescript\n// Use Page Object's goto() method in beforeEach\ntest.beforeEach(async ({ browserPage }) => {\n  await browserPage.goto(database.id);  // Navigates and waits for page load\n});\n\n// Switch tabs when already connected\nawait browserPage.navigationTabs.gotoWorkbench();\n```\n\n### ❌ Incorrect Navigation Pattern\n\n```typescript\n// NEVER do this - fails in Electron\nawait page.goto(`/${database.id}/browser`);\nawait page.goto('/settings');\nawait page.goto('/');\n```\n\n## Running Tests\n\nRun these commands from the E2E package directory:\n\n```bash\ncd tests/e2e-playwright\n\nnpx playwright test                                              # All Playwright projects\nnpx playwright test --project=chromium-parallel                  # Chromium parallel tests\nnpx playwright test --project=chromium-serial --no-deps          # Chromium serial only (skips parallel + setup)\nnpx playwright test --project=electron-parallel                  # Electron parallel tests\nnpx playwright test --project=electron-serial --no-deps          # Electron serial only (skips parallel + setup)\nENV=ci npx playwright test                             # CI environment\nENV=staging npx playwright test                        # Staging environment\n```\n\n## Code Quality (IMPORTANT)\n\n**Always run linter and type checker after making changes:**\n\n```bash\nnpm run lint                # ESLint check\nnpm run type-check          # TypeScript type check\n```\n\nBoth must pass before committing. Common issues:\n- Unused variables/imports\n- Missing return types\n- `any` types (avoid when possible)\n- Null/undefined handling (use proper types like `Promise<string | null>`)\n\n## Test Isolation (IMPORTANT)\n\nTests should be isolated and not depend on execution order:\n\n### 1. Shared Database with beforeAll/afterAll\n\n```typescript\ntest.describe('Feature Name', () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase({ name: 'test-feature-db', ... });\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  // Tests can run in parallel - they share the database but don't modify shared state\n});\n```\n\n### 2. Use Serial Only When Tests Truly Depend on Each Other\n\n```typescript\n// Only use .serial when tests modify state that subsequent tests depend on\ntest.describe.serial('Workflow that modifies state', () => {\n  test('step 1: create item', ...);\n  test('step 2: modify item created in step 1', ...);\n  test('step 3: delete item', ...);\n});\n```\n\n### 3. Unique Test Data Per Test (when needed)\n\n```typescript\ntest('should create unique item', async ({ apiHelper }) => {\n  const uniqueName = `test-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;\n  // Use uniqueName for this test's data\n});\n```\n\n## Feature-to-Path Mapping\n\nFollow this naming convention for test and page object paths:\n\n| Feature | Test Path | Page Object Path |\n|---------|-----------|------------------|\n| Database List | `tests/parallel/databases/list/` | `pages/databases/` |\n| Add Database | `tests/parallel/databases/add/` | `pages/databases/` |\n| Import Database | `tests/parallel/databases/import/` | `pages/databases/` |\n| Browser - Key List | `tests/parallel/browser/key-list/` | `pages/browser/` |\n| Browser - Add Key | `tests/parallel/browser/add-key/` | `pages/browser/` |\n| Browser - Key Details | `tests/parallel/browser/key-details/` | `pages/browser/` |\n| Workbench | `tests/parallel/workbench/` | `pages/workbench/` |\n| CLI | `tests/parallel/cli/` | `pages/cli/` |\n| Pub/Sub | `tests/parallel/pubsub/` | `pages/pubsub/` |\n| Slow Log | `tests/parallel/analytics/slow-log/` | `pages/analytics/` |\n| DB Analysis | `tests/parallel/analytics/analysis/` | `pages/analytics/` |\n| Settings | `tests/parallel/settings/` | `pages/settings/` |\n| Navigation | `tests/parallel/navigation/` | `pages/navigation/` |\n| Auto-Update | `tests/auto-update/` | `pages/` (shared) |\n| Deep Links | `tests/electron/deep-links/` | `pages/` (shared) |\n\n**Note**: Most tests go in `tests/parallel/`. Only use other project folders for tests with special requirements (serial execution, different setup, etc.).\n",".ai/skills/feature-flags/SKILL.md":"---\nname: feature-flags\ndescription: >-\n  Create, modify, and remove feature flags in RedisInsight. Use when adding a\n  new feature flag, introducing a dev flag, promoting a dev flag to regular,\n  cleaning up old flags, or the user mentions feature flags, feature toggles,\n  or gating features.\n---\n\n# Feature Flags\n\nRedisInsight has its own feature flag system. Flags are defined in a remote JSON config, fetched by the backend, and served to the frontend via API. This skill covers how to add, promote, and remove flags.\n\n## Flag Types\n\n| Type                         | Naming                            | `flag` value | Strategy                 | Purpose                                                      |\n| ---------------------------- | --------------------------------- | ------------ | ------------------------ | ------------------------------------------------------------ |\n| **Dev flag**                 | `dev-<name>` (e.g. `dev-browser`) | `false`      | `CommonFlagStrategy`     | Hide incomplete features during development                  |\n| **Regular flag**             | `camelCase` (e.g. `azureEntraId`) | `true`       | `CommonFlagStrategy`     | Standard on/off toggle                                       |\n| **Regular with data**        | `camelCase`                       | `true`       | `WithDataFlagStrategy`   | Flag + extra config payload in `data`                        |\n| **Switchable (overridable)** | `camelCase`                       | `true`       | `SwitchableFlagStrategy` | User can override locally via `~/.redis-insight/config.json` |\n\n## Files to Change\n\nEvery new flag touches these files (in order):\n\n### Backend (required)\n\n1. **`redisinsight/api/config/features-config.json`**\n   Add the flag entry with `flag`, `perc`, optional `filters` and `data`. Bump the `version` number.\n\n2. **`redisinsight/api/src/modules/feature/constants/index.ts`**\n   Add to the `KnownFeatures` enum.\n\n3. **`redisinsight/api/src/modules/feature/constants/known-features.ts`**\n   Add entry to the `knownFeatures` record with `name` and `storage` (usually `FeatureStorage.Database`).\n\n4. **`redisinsight/api/src/modules/feature/providers/feature-flag/feature-flag.provider.ts`**\n   Register the flag with its strategy (see Strategy Types below).\n\n### Frontend (required if the flag gates UI)\n\n5. **`redisinsight/ui/src/constants/featureFlags.ts`**\n   Add to the `FeatureFlags` enum.\n\n6. **`redisinsight/ui/src/slices/app/features.ts`**\n   Add default state entry in `initialState.featureFlags.features` with `{ flag: false }`.\n\n### Consuming code\n\n7. Use the flag in components/hooks to gate functionality.\n\n## Strategy Selection\n\nChoose the strategy based on what the flag needs:\n\n```\nCommonFlagStrategy        → Most flags (dev and regular on/off)\nWithDataFlagStrategy      → Flag needs to carry extra data payload\nSwitchableFlagStrategy    → Flag should be overridable via local config.json\n```\n\nRegister in `feature-flag.provider.ts`:\n\n```typescript\nthis.strategies.set(\n  KnownFeatures.YourFeature,\n  new CommonFlagStrategy(this.featuresConfigService, this.settingsService),\n);\n```\n\n## Config JSON Structure\n\n### Minimal (dev flag)\n\n```json\n\"dev-myFeature\": {\n  \"flag\": false,\n  \"perc\": [[0, 100]]\n}\n```\n\n### With filters (Electron-only)\n\n```json\n\"myFeature\": {\n  \"flag\": true,\n  \"perc\": [[0, 100]],\n  \"filters\": [\n    { \"name\": \"config.server.buildType\", \"value\": \"ELECTRON\", \"cond\": \"eq\" }\n  ]\n}\n```\n\n### Gradual rollout (10% of users)\n\n```json\n\"myFeature\": {\n  \"flag\": true,\n  \"perc\": [[0, 10]]\n}\n```\n\n### With data payload\n\n```json\n\"myFeature\": {\n  \"flag\": true,\n  \"perc\": [[0, 100]],\n  \"data\": { \"strategy\": \"ioredis\" }\n}\n```\n\n## Filter Conditions\n\nFilters compare a value from server state against the filter value.\n\n| Condition    | Meaning                         |\n| ------------ | ------------------------------- |\n| `eq`         | equals                          |\n| `neq`        | not equals                      |\n| `gt` / `gte` | greater than / greater or equal |\n| `lt` / `lte` | less than / less or equal       |\n\nCommon `name` paths: `config.server.buildType` (ELECTRON, DOCKER_ON_PREMISE, REDIS_STACK), `config.server.packageVersion` (uses semver), `agreements.analytics`, `env.<VAR_NAME>`.\n\nFilters support `and`/`or` composition for complex conditions.\n\n## Workflows\n\n### Add a dev feature flag\n\nUse for features under active development that should not be visible in production.\n\n1. `features-config.json` → add `\"dev-myFeature\": { \"flag\": false, \"perc\": [[0, 100]] }`\n2. `constants/index.ts` → add `DevMyFeature = 'dev-myFeature'` to `KnownFeatures`\n3. `constants/known-features.ts` → add record entry\n4. `feature-flag.provider.ts` → register with `CommonFlagStrategy`\n5. `ui/src/constants/featureFlags.ts` → add `devMyFeature = 'dev-myFeature'`\n6. `ui/src/slices/app/features.ts` → add default `{ flag: false }`\n\n### Promote dev flag to regular flag\n\nWhen the feature is complete and ready for rollout.\n\n1. Rename `dev-myFeature` → `myFeature` in all the files above\n2. Set `flag: true` in `features-config.json`\n3. Optionally set `perc` for gradual rollout (e.g. `[[0, 10]]`)\n4. Change strategy if needed (e.g. to `SwitchableFlagStrategy` for overridable)\n5. Bump config `version`\n\n### Clean up a flag\n\nWhen a feature is fully rolled out and the flag is no longer needed.\n\n1. Remove from `features-config.json`\n2. Remove from `KnownFeatures` enum\n3. Remove from `knownFeatures` record\n4. Remove strategy registration from `feature-flag.provider.ts`\n5. Remove from FE `FeatureFlags` enum\n6. Remove default state from `features.ts`\n7. Remove all gating code (conditionals, `FeatureFlagComponent` wrappers) in consuming components\n\n## FE Usage Patterns\n\n### Check flag in a component\n\n```typescript\nimport { FeatureFlags } from 'uiSrc/constants';\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features';\n\nconst features = useSelector(appFeatureFlagsFeaturesSelector);\nconst isEnabled = features[FeatureFlags.myFeature]?.flag;\n```\n\n### Custom selector for complex logic\n\n```typescript\nexport const isMyFeatureEnabledSelector = (state: RootState): boolean => {\n  const features = state.app.features.featureFlags.features;\n  return features[FeatureFlags.myFeature]?.flag ?? false;\n};\n```\n",".ai/skills/frontend/SKILL.md":"---\nname: frontend\ndescription: >-\n  React/Redux frontend development patterns for RedisInsight UI:\n  component folder structure, styled-components, hooks, named exports,\n  barrel files, layout components, and theme usage. Use when editing\n  any file under redisinsight/ui/**, writing or modifying React\n  components, Redux slices, styled-components, custom hooks, or when\n  the user mentions UI, frontend, React, Redux, or styled-components.\n---\n\n# Frontend Development (React/Redux)\n\n## Component Structure\n\n### Functional Components\n\n- Use **functional components with hooks** (no class components)\n- **Prefer named exports** over default exports\n- Keep components focused and single-responsibility\n- Extract complex logic into custom hooks\n\n### Component Folder Structure\n\nEach component in its own directory under `**/ComponentName`:\n\n```\nComponentName/\n  ComponentName.tsx          # Main component\n  ComponentName.styles.ts    # Styled-components styles (PascalCase)\n  ComponentName.types.ts     # TypeScript interfaces\n  ComponentName.spec.tsx     # Tests\n  ComponentName.constants.ts # Constants\n  ComponentName.story.tsx    # Storybook examples\n  hooks/                     # Custom hooks\n  components/                # Sub-components\n  utils/                     # Utility functions\n```\n\n### Props Interface\n\n- Name as `ComponentNameProps`\n- Use separate interfaces for complex prop objects\n- Always use proper TypeScript types, never `any`\n\n### Imports Order in Components\n\n1. External dependencies (`react`, `redux`, etc.)\n2. Internal modules (aliases)\n3. Local imports (types, constants, hooks)\n4. Styles (always last: `import { Container } from './Component.styles'`)\n\n### Barrel Files\n\nUse barrel files (`index.ts`) only when exporting **3 or more** items. Make sure exports appear in only one barrel file, not propagated up the chain.\n\n## Styled Components\n\n**We are migrating to styled-components** (deprecating SCSS modules).\n\n### Encapsulate Styles in .styles.ts\n\nKeep all component styles in dedicated `.styles.ts` files using styled-components. Use PascalCase for the filename to match the component name:\n\n```\nComponentName/\n  ComponentName.tsx\n  ComponentName.styles.ts  # ✅ PascalCase\n  # Not component-name.styles.ts ❌\n```\n\n### Import Pattern\n\nKeep all component styles in dedicated .style.ts files and import them with a namespace.\n\n**CRITICAL: `import * as S` is reserved for local styles only** (e.g., from `ComponentName.styles.ts`). When you need to use styled components from external components, create a local styles file that re-exports them.\n\n#### ✅ Good\n\n```typescript\n// ComponentName.tsx\nimport * as S from './ComponentName.styles'\n\nreturn (\n  <S.Container>\n    <S.Title>Title</S.Title>\n    <S.Content>Content</S.Content>\n  </S.Container>\n)\n\n// ComponentName.styles.ts (when re-exporting from external component)\nexport { ExternalStyledComponent } from '../ExternalComponent/ExternalComponent.styles'\n```\n\n#### ❌ Bad\n\n```typescript\n// ❌ BAD: Importing styled components directly from external component\nimport * as S from '../ExternalComponent/ExternalComponent.styles';\n\n// ❌ BAD: Named imports instead of namespace\nimport { Container, Title, Content } from './Component.styles';\n```\n\n### Use Layout Components Instead of div\n\n**Prefer `FlexGroup` over `div`** when creating flex containers:\n\n```typescript\n// ✅ GOOD: Use FlexGroup\nimport { FlexGroup } from 'uiSrc/components/base/layout/flex'\n\nexport const Wrapper = styled(FlexGroup)`\n  user-select: none;\n`\n\n// Usage: Pass layout props as component props\n<Wrapper align=\"center\" justify=\"end\">\n  {children}\n</Wrapper>\n\n// ❌ BAD: Using div with hardcoded flex properties\nexport const Wrapper = styled.div`\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n`\n```\n\n### Pass Layout Props as Component Props\n\n**Don't hardcode layout properties** in styled components when using layout components like `FlexGroup`. Pass them as props instead:\n\n```typescript\n// ✅ GOOD: Pass props in JSX\nexport const Wrapper = styled(FlexGroup)`\n  user-select: none;\n`\n\n<Wrapper align=\"center\" justify=\"end\">\n  {children}\n</Wrapper>\n\n// ❌ BAD: Hardcoding in styled component\nexport const Wrapper = styled(FlexGroup)`\n  align-items: center;\n  justify-content: flex-end;\n  user-select: none;\n`\n```\n\n### Use Gap Prop Instead of Custom Margins\n\n**Prefer `gap` prop on layout components** instead of custom margins for spacing between elements:\n\n```typescript\n// ✅ GOOD: Use gap prop\n<Row align=\"center\" justify=\"between\" gap=\"l\">\n  <FlexItem>Item 1</FlexItem>\n  <FlexItem>Item 2</FlexItem>\n</Row>\n```\n\n### Use Theme Spacing Instead of Magic Numbers\n\n**Always use theme spacing values** instead of hardcoded pixel values:\n\n```typescript\n// ✅ GOOD: Use theme spacing\nexport const Container = styled(Row)`\n  height: ${({ theme }) => theme.core.space.space500};\n  padding: 0 ${({ theme }) => theme.core.space.space200};\n  margin-bottom: ${({ theme }) => theme.core.space.space200};\n`;\n\n// ❌ BAD: Using magic numbers\nexport const Container = styled(Row)`\n  height: 64px;\n  padding: 0 16px;\n  margin-bottom: 16px;\n`;\n```\n\n### Use Semantic Colors from Theme\n\n**Always use semantic colors** from the theme instead of CSS variables or hardcoded colors:\n\n```typescript\n// ✅ GOOD: Use semantic colors\nexport const Header = styled(Row)`\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral100};\n  border-bottom: 1px solid\n    ${({ theme }) => theme.semantic.color.border.neutral500};\n`;\n\n// ❌ BAD: Using deprecated EUI CSS variables\nexport const Header = styled(Row)`\n  background-color: var(--euiColorEmptyShade);\n  border-bottom: 1px solid var(--separatorColor);\n`;\n```\n\n### Use Layout Components (Row/Col/FlexGroup) Instead of div\n\n**Prefer layout components** from the layout system instead of regular `div` elements:\n\n```typescript\n// ✅ GOOD: Use Row component\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const PageHeader = styled(Row)`\n  height: ${({ theme }) => theme.core.space.space500};\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral100};\n`\n\n<PageHeader align=\"center\" justify=\"between\" gap=\"l\">\n  {children}\n</PageHeader>\n\n// ❌ BAD: Using div with flex properties\nexport const PageHeader = styled.div`\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  height: 64px;\n`\n```\n\n### Conditional Styling\n\nUse `$` prefix for transient props that shouldn't pass to DOM:\n\n```typescript\nexport const Button = styled.button<{ $isActive?: boolean }>`\n  background-color: ${({ $isActive }) => ($isActive ? '#007bff' : '#6c757d')};\n`;\n```\n\n### Avoid !important\n\n**Never use `!important` in styled-components**. Styled-components handles CSS specificity through component hierarchy. If you need to override styles, use more specific selectors or adjust the component structure:\n\n```typescript\n// ✅ GOOD: Rely on CSS specificity\nexport const IconButton = styled(IconButton)<{ isOpen: boolean }>`\n  ${({ isOpen }) =>\n    isOpen &&\n    css`\n      background-color: ${({ theme }) =>\n        theme.semantic.color.background.primary200};\n    `}\n`;\n\n// ❌ BAD: Using !important\nexport const IconButton = styled(IconButton)`\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.primary200} !important;\n`;\n```\n\n### Verify Type System Compatibility\n\nWhen using layout components or other typed components, verify your prop values match the type system:\n\n```typescript\n// Check the component's type definitions\n// FlexGroup accepts: align?: 'center' | 'stretch' | 'baseline' | 'start' | 'end'\n// Use valid values from the type system\n```\n\n## State Management (Redux)\n\n### When to Use What\n\n- **Global State (Redux)**:\n\n  - Data shared across multiple components\n  - Data persisting across routes\n  - Server state (API data)\n  - User preferences/settings\n\n- **Local State (useState)**:\n\n  - UI state (modals, dropdowns, tabs)\n  - Form inputs before submission\n  - Component-specific temporary data\n\n- **Derived State (Selectors)**:\n  - Computed values from Redux state\n  - Filtered/sorted lists\n  - Aggregated data\n\n### Redux Toolkit Patterns\n\n#### Slice Structure\n\n- Use `createSlice` from Redux Toolkit\n- Define proper TypeScript types for state\n- Use `PayloadAction<T>` for action typing\n- Handle async with `extraReducers` and thunks\n\n#### Thunks\n\n- Use `createAsyncThunk` for async operations\n- Handle pending, fulfilled, and rejected states\n- Use `rejectWithValue` for error handling\n\n#### Selectors\n\n- Create basic selectors for direct state access\n- Use `createSelector` from `reselect` for memoized/computed values\n- Keep selectors in separate `selectors.ts` file\n\n## React Best Practices\n\n### Performance\n\n- Use `useCallback` for functions passed as props\n- Use `useMemo` for expensive computations\n- Use `React.memo` for expensive components\n- Avoid inline arrow functions in JSX props\n\n### Effect Cleanup\n\nAlways clean up subscriptions, timers, and event listeners in `useEffect` return function.\n\n### Keys in Lists\n\n- Use unique, stable IDs (not array indices)\n- Only use indices if list never reorders and items have no IDs\n\n### Conditional Rendering\n\n- Use early returns for loading/error states\n- Avoid deeply nested ternaries - extract to functions\n\n## Custom Hooks\n\n### Extract Reusable Logic\n\nCreate custom hooks for reusable stateful logic. Store component-specific hooks in the component's `/hooks` directory.\n\n## Form Handling\n\nUse Formik with Yup for validation. Keep form logic in custom hooks when complex.\n\n## UI Components\n\n**⚠️ IMPORTANT**:\n\n- We are **deprecating Elastic UI** components\n- Migrating to **Redis UI** (`@redis-ui/*`)\n- **Use internal wrappers** from `uiSrc/components/ui`\n- **DO NOT import directly** from `@redis-ui/*`\n\n### Component Usage\n\n```typescript\n// ✅ GOOD: Import from internal wrappers\nimport { Button, Input, FlexGroup } from 'uiSrc/components/ui';\n\n// ❌ BAD: Don't import directly from @redis-ui\nimport { Button } from '@redis-ui/components';\n\n// ❌ DEPRECATED: Don't use Elastic UI for new code\nimport { EuiButton } from '@elastic/eui';\n```\n\n### Migration Guidelines\n\n- ✅ Use internal wrappers from `uiSrc/components/ui` for all new features\n- ✅ Create internal wrappers for Redis UI components as needed\n- ✅ Replace Elastic UI when touching existing code\n- ❌ Do not import directly from `@redis-ui/*`\n- ❌ Do not add new Elastic UI imports\n\n## Icons\n\n**⚠️ IMPORTANT**: Always use icons from Redis UI library instead of custom SVGs.\n\n### Icon Usage\n\n- **Use Redis UI icons** from `@redis-ui/icons` via `iconRegistry.tsx`\n- Icons are automatically exported via `export * from '@redis-ui/icons'` in `iconRegistry.tsx`\n- Use `RiIcon` component with icon type: `<RiIcon type=\"FolderOpenIcon\" />`\n- **DO NOT create custom SVG icons** - check if the icon exists in Redis UI library first\n\n### Custom Icons (Exception)\n\nOnly create custom SVG icons if:\n\n- The icon doesn't exist in Redis UI library\n- It's a project-specific icon that won't be added to the library\n\n## Testing Components\n\n### Always Use Shared `renderComponent` Helper\n\n**CRITICAL**: Create a `renderComponent` helper function for each component test file:\n\nPull entity data from a shared Fishery factory in `redisinsight/ui/src/mocks/factories/`\n(reuse one, or add a new `<domain>/<TypeName>.factory.ts`); inline only callbacks/primitives.\n\n```typescript\ndescribe('MyComponent', () => {\n  const mockUser = UserFactory.build();\n  const defaultProps: MyComponentProps = {\n    user: mockUser,\n    onComplete: jest.fn(),\n  }\n\n  const renderComponent = (propsOverride?: Partial<MyComponentProps>) => {\n    const props = { ...defaultProps, ...propsOverride }\n\n    return render(\n      <Provider store={store}>\n        <MyComponent {...props} />\n      </Provider>\n    )\n  }\n\n  it('should render', () => {\n    renderComponent()\n    // assertions\n  })\n})\n```\n\nBenefits:\n\n- Centralized setup and providers\n- Default props in one place\n- Easy prop overrides per test\n- No duplicate render logic\n\n### Testing Redux\n\nCreate a test store with `configureStore` for components connected to Redux.\n\n## Key Principles\n\n1. **Separation of Concerns**: Keep styles, types, constants, logic separate\n2. **Colocate Related Code**: Keep sub-components and hooks close to usage\n3. **Consistent Naming**: Follow conventions across all components\n4. **Type Safety**: Always define proper types, never `any`\n5. **Testability**: Structure for easy testing with `renderComponent` helper\n6. **Styled Components**: Prefer styled-components over SCSS modules\n7. **Layout Components**: Use FlexGroup instead of div for flex containers, pass layout props as component props\n8. **Type Safety**: Verify prop values match component type definitions (e.g., FlexGroup's align/justify values)\n",".ai/skills/git-safety/SKILL.md":"---\nname: git-safety\ndescription: >-\n  Critical safety guardrails for protected branches: never commit,\n  push, or force-push directly to main, latest, or release branches;\n  no destructive history rewrites without explicit user approval.\n  Use before any git operation that touches protected branches,\n  before force-push, reset --hard, history rewrite, or branch\n  deletion, or whenever the user asks about merging, pushing, or\n  release branches.\n---\n\n\n# Git Safety Rules for AI Agents\n\n## 🚫 CRITICAL: Protected Branch Rules\n\n**AI agents must NEVER commit to or push to protected branches under any circumstances.**\n\n### Protected Branches\n\n- `main` - Primary production branch\n- `latest` - Latest stable release\n- `release/*` - Release branches (e.g., `release/v2.0.0`)\n\nThis is a non-negotiable rule that applies to all scenarios:\n\n### Prohibited Actions\n\n- ❌ **Direct commits** - Never run `git commit` while on a protected branch\n- ❌ **Direct pushes** - Never run `git push origin <protected-branch>` or `git push` while on a protected branch\n- ❌ **Force pushes** - Never run `git push --force` or `git push -f` targeting protected branches\n- ❌ **Merging into protected branches locally** - Never run `git merge <branch>` while on a protected branch\n- ❌ **Rebasing protected branches** - Never run `git rebase` while on a protected branch\n- ❌ **Resetting protected branches** - Never run `git reset` while on a protected branch\n\n### Required Workflow\n\n1. **Always create a feature branch** before making any changes\n2. **Verify current branch** before any git operation using `git branch --show-current`\n3. **Create Pull Requests** for all changes - let the review process handle merging\n\n### Pre-Push Checklist\n\nBefore executing any push command, AI agents must:\n\n1. ✅ Confirm current branch is NOT a protected branch (`main`, `latest`, `release/*`)\n2. ✅ Verify the remote and branch target\n\n### Error Recovery\n\nIf accidentally on a protected branch with uncommitted changes:\n\n1. Stash changes: `git stash`\n2. Create new branch: `git checkout -b <appropriate-branch-name>`\n3. Apply changes: `git stash pop`\n4. Continue work on the new branch\n\n## Rationale\n\n- Protected branches represent production-ready or release code\n- All changes must go through code review via Pull Requests\n- Direct pushes bypass CI/CD checks and team review\n- Mistakes on protected branches can affect the entire team and deployment pipeline\n",".ai/skills/pull-requests/SKILL.md":"---\nname: pull-requests\ndescription: >-\n  Create and review pull requests following project standards including title\n  format, description template, and review checklist. Use when creating PRs,\n  writing PR descriptions, or reviewing pull requests.\n---\n\n# Pull Request Guidelines\n\n## Creating a PR\n\n### PR Title\n\nInclude issue number at the start:\n\n```\nRI-123 Add user profile editing\n#456 Fix memory leak in connection pool\n```\n\n### PR Description Template\n\n```markdown\n# What\n\nDescribe what was changed.\n\n# Testing\n\nDescribe how to test the changes.\n\n---\n\nRefs #RI-123\n```\n\n**PR Description Guidelines:**\n\n- **Keep it concise** - Avoid verbose descriptions\n- **Focus on high-level changes** - Don't list every code change in the #What section\n- **Brief and to the point** - The diff shows the details; describe the \"why\" and \"what\" at a high level\n- **Technical decisions** - Only mention significant architectural or design decisions if relevant\n- **Link, don't auto-close** - Use `Refs #RI-123` / `Addresses #RI-123`, not `Closes`/`Fixes`/`Resolves` - those keywords auto-close the issue when the PR merges, and tickets should be closed manually, not by the merge\n\n## Review Process\n\n### As PR Author\n\n- **Respond to all comments** - Address every piece of feedback\n- **Don't take feedback personally** - Reviews improve code quality\n- **Update code based on feedback** - Make requested changes\n- **Mark conversations as resolved** - After addressing feedback\n- **Keep PR up to date** - Rebase on main regularly\n\n### As PR Reviewer\n\n- **Be constructive and respectful** - Focus on improvement\n- **Focus on logic, not style** - Linter handles formatting\n- **Check for**:\n  - Logic errors and edge cases\n  - Performance issues\n  - Security concerns\n  - Test coverage\n  - Missing documentation\n  - Architectural concerns\n\n### Review Checklist\n\n- [ ] Code follows project patterns\n- [ ] Tests are comprehensive\n- [ ] No console.log or debug code\n- [ ] TypeScript types are proper\n- [ ] Error handling is adequate\n- [ ] Documentation is updated\n- [ ] No security vulnerabilities\n- [ ] Performance is acceptable\n",".ai/skills/redis-insight-plugin/SKILL.md":"---\nname: redis-insight-plugin\ndescription: Use when creating, modifying, debugging, deploying, or testing Redis Insight Workbench visualization plugins, plugin manifests, package.json visualizations, activationMethod functions, redisinsight-plugin-sdk usage, Parcel/Vite plugin builds, iframe rendering, Redis command parsing, Docker RedisInsight deployment, /api/plugins verification, or Playwright plugin validation.\nlicense: MIT\nmetadata:\n  author: redis\n  version: \"1.0.0\"\n---\n# Redis Insight Workbench Plugin\n\nBuild, deploy, and validate Redis Insight Workbench visualization plugins. Plugins render inside an iframe in Workbench and visualize the result of a Redis command. Trigger this skill for plugin manifests, `package.json` `visualizations`, `activationMethod` functions, `redisinsight-plugin-sdk` usage, Parcel/Vite plugin builds, iframe rendering, Redis command parsing, Docker RedisInsight deployment, `/api/plugins` verification, and Playwright plugin tests.\n\nUse the `redis-ui-components` skill for every visual plugin UI. RedisInsight plugins use the RedisInsight product theme pair: `light` / `dark`, not `light2` / `dark2`.\n\nOfficial source-of-truth references:\n\n- In-repo canonical docs: [docs/plugins/development.md](../../../docs/plugins/development.md), [docs/plugins/installation.md](../../../docs/plugins/installation.md), [docs/plugins/introduction.md](../../../docs/plugins/introduction.md)\n- Upstream: https://github.com/redis/RedisInsight/tree/main/docs/plugins\n\nSee [references/official-docs-summary.md](references/official-docs-summary.md) for a condensed summary.\n\n## Repo Code Conventions (internal plugins)\n\n> **You are inside the RedisInsight repo.** An internal plugin lives in the `redisinsight/ui/`\n> tree, so its code must follow the same styleguides as the rest of the UI — not ad hoc plugin\n> code. These rules are **mandatory** for any plugin code written here and override generic\n> external-plugin guidance below where they conflict:\n>\n> - **[frontend](../frontend/SKILL.md)** — component folder structure\n>   (`ComponentName/ComponentName.tsx` + `.styles.ts` + `.types.ts` + `.spec.tsx`), functional\n>   components with hooks, **named exports**, barrel files, layout components\n>   (`Row` / `Col` / `FlexGroup`) instead of raw `div`, and theme usage.\n> - **[redis-ui-components](../redis-ui-components/)** — build all plugin UI from Redis UI\n>   components. Import the internal `uiSrc/components/ui` wrappers; **never** import raw\n>   `@redis-ui/*`. (This skill is a symlink into the installed `@redis-ui/components` package,\n>   so it resolves after `npm install`; if it is missing, run install — the canonical source is\n>   `node_modules/@redis-ui/components/skills/redis-ui-components/`.)\n> - **[code-quality](../code-quality/SKILL.md)** — TypeScript everywhere (no `any`), naming\n>   (`PascalCase` / `camelCase` / `UPPER_SNAKE_CASE`), import order, no magic numbers, no\n>   `!important` in styles, semantic theme colors over CSS variables.\n> - **[testing](../testing/SKILL.md)** — Jest + Testing Library, the `renderComponent` helper,\n>   `faker` for test data, `waitFor` instead of fixed time waits.\n> - **[e2e-testing](../e2e-testing/SKILL.md)** — any Playwright/E2E test follows this skill\n>   (tests in `tests/e2e-playwright/`, page objects, fixtures, UI navigation; never `page.goto()`\n>   directly, no CSS selectors or fixed waits).\n>\n> External standalone plugins (below) are bundled in isolation and **cannot** import these\n> internals; they emulate the conventions with local code instead.\n\n## First Decision: Plugin Type\n\nDecide before scaffolding anything else.\n\n- **Internal monorepo plugin** — lives inside `redisinsight/ui/src/packages/<plugin-name>/` and ships with Redis Insight itself. Build with **Vite** (shared config). Follow the repo styleguides above. **This is the default for any contribution to this repo** — copy a sibling package such as `geodata` or `redisearch` rather than diverging.\n- **External standalone plugin** — installed by a user into `~/.redis-insight/plugins/<name>/`. Build with **Parcel**. Bundle all dependencies. Do not import from `uiSrc/` or any RedisInsight monorepo internal. Use this only for customer/field/demo plugins that ship *outside* this repo.\n\nSee [references/internal-vite-plugin.md](references/internal-vite-plugin.md) and [references/external-parcel-plugin.md](references/external-parcel-plugin.md).\n\n## External Plugin Structure\n\n```\n<plugin-name>/\n  package.json              # manifest + build scripts\n  src/\n    index.html              # iframe entry, has #app\n    main.tsx                # activation functions, default export\n    components/\n    styles/\n      styles.scss\n  dist/\n    index.js                # built bundle (referenced by manifest \"main\")\n    styles.css              # built styles (referenced by manifest \"styles\")\n```\n\n## Required Manifest\n\nTop-level `package.json` fields:\n\n- `name`\n- `version`\n- `description`\n- `main` — path to built JS (e.g. `./dist/index.js`).\n- `styles` — path to built CSS (e.g. `./dist/styles.css`).\n- `visualizations` — array of visualization descriptors.\n\nEach visualization descriptor must include:\n\n- `id`\n- `name`\n- `activationMethod`\n- `matchCommands`\n- `description`\n- `default`\n\nSet `default: false` unless the user explicitly asks for it to be the default visualization.\n\nThe `activationMethod` value must **exactly** match an exported function name in the bundle. The plugin entry must export that function via the default export:\n\n```ts\nexport default { renderMyView };\n```\n\nMultiple visualizations:\n\n```ts\nexport default {\n  renderTableView,\n  renderChartView,\n};\n```\n\nSee [references/plugin-manifest.md](references/plugin-manifest.md) for full examples and how to strip dev-only fields from the deployed manifest.\n\n## Activation Function Contract\n\nEvery activation function must:\n\n1. Get the host element: `const root = document.getElementById('app');`\n2. Defensively validate props (`command`, `data`, modules, theme).\n3. Wrap render logic in `try/catch` and render an error state on failure.\n4. Render an empty state when `data` is missing or empty.\n5. Log with a plugin-specific prefix, e.g. `[MY_PLUGIN]`, never bare `console.log`.\n\nSee [references/error-handling.md](references/error-handling.md).\n\n## RedisInsight Product UI Contract\n\nEvery plugin UI must follow RedisInsight product styling:\n\n- For **internal** plugins, build the UI from the [redis-ui-components](../redis-ui-components/) skill and the `uiSrc/components/ui` wrappers, following the [frontend](../frontend/SKILL.md) styleguide.\n- For **external** plugins, mirror the same Redis UI look with local code (you cannot import the internals).\n- Use RedisInsight `light` / `dark` product themes.\n- For standalone external plugins, emulate the RedisInsight product tokens with local CSS variables; do not import RedisInsight monorepo internals or `@redis-ui/*`.\n- Detect iframe theme through `theme_LIGHT` / `theme_DARK` body classes or SDK theme helpers.\n- Use compact product components: table, toolbar, segmented states, empty/error/loading panels, badges, and inspector-style details.\n- Keep Redis brand red for brand moments only; do not use it as the default plugin CTA, heading, error, or status color.\n\nSee [references/redisinsight-product-ui.md](references/redisinsight-product-ui.md) and use [templates/external-styles.scss](templates/external-styles.scss) as the baseline `src/styles/styles.scss`.\n\n## Mandatory Phased Workflow\n\nBuild every new plugin in three phases. Do not skip phases — the failure mode in each phase tells you exactly what is wrong.\n\n- **Phase 1 — Vanilla wiring.** No React, no third-party libraries. Render plain DOM that proves activation, props, and iframe rendering work end-to-end.\n- **Phase 2 — React rendering.** Add React + ReactDOM. Render a typed component that displays `command`, `status`, and the raw response.\n- **Phase 3 — Full feature.** Add the actual visualization library (charting, mapping, grid, etc.) and the real UX.\n\nSee [references/iterative-development.md](references/iterative-development.md) and the templates in `templates/`.\n\n## Review Hardening Loop\n\nBefore asking for review, run a small adversarial pass against the exact surfaces the plugin touches:\n\n- Manifest matching/defaults: exact command boundaries, no default visualization conflicts, no regex backtracking traps.\n- Redis command parsing: token-aware option handling, raw-unit preservation, malformed rows, empty rows, keyword-like key/member names.\n- Visualization state: stale closures, safe library bounds/inputs, large result sets, mode-specific empty/error copy.\n- Tests: red regression first, then the smallest package/component/parser test set that proves the fix.\n\nSee [references/review-hardening.md](references/review-hardening.md).\n\n## Build and Verify\n\n```bash\nnpm run build\ntest -f dist/index.js\ntest -f dist/styles.css                       # if \"styles\" is declared\ngrep -c \"process.env\" dist/index.js           # must be 0 in a Parcel build\n```\n\nConfirm each `activationMethod` name appears in the bundle:\n\n```bash\ngrep -o \"renderMyView\" dist/index.js | head\n```\n\n## Deploy\n\nExternal plugin → user plugins folder:\n\n```bash\nmkdir -p ~/.redis-insight/plugins/<plugin-name>\ncp package.json ~/.redis-insight/plugins/<plugin-name>/\ncp -R dist ~/.redis-insight/plugins/<plugin-name>/dist\n```\n\nRestart Redis Insight, then verify:\n\n```bash\ncurl -s http://localhost:5540/api/plugins\n```\n\nThe response must include the plugin `name` and its visualizations. See [references/testing-and-deployment.md](references/testing-and-deployment.md) for Docker workarounds, Playwright smoke tests, and the static-plugin path inside the Docker image.\n\n## Security Rules\n\n- Plugins execute code in the Insight UI process. Only ship code you control or trust.\n- Never embed secrets, tokens, or credentials in the bundle.\n- No hidden network calls. Document any outbound HTTP and prefer none.\n- Default to read-only Redis commands (`HGETALL`, `LRANGE`, `XRANGE`, `INFO`, `FT.SEARCH`). Never run destructive commands (`FLUSHDB`, `DEL`, `UNLINK`, `XTRIM`, `CLUSTER RESET`, `CONFIG SET`) without explicit user request and confirmation.\n\n## DO NOT\n\n- DO NOT skip Phase 1 or Phase 2; each catches a different class of failure.\n- DO NOT deploy `index.js` / `styles.css` at the plugin root. They live in `dist/` and the manifest points at `./dist/...`.\n- DO NOT set `default: true` on a visualization unless the user asked for it.\n- DO NOT include `scripts` or `devDependencies` in the deployed manifest. Strip them before copying.\n- DO NOT use Vite for standalone external plugins. Use Parcel.\n- DO NOT import from `uiSrc/`, `@redis-ui/*`, or any RedisInsight monorepo internal in a standalone plugin.\n- DO NOT externalize React in a standalone plugin bundle. Bundle React and ReactDOM.\n- DO NOT style plugin UI with ad hoc inline brand colors. Use RedisInsight product UI variables/classes.\n- DO NOT use `light2` / `dark2` for RedisInsight plugins; those are for other Redis product UIs.\n- DO NOT skip `/api/plugins` verification after deploying.\n- DO NOT ship `process.env.*` references in the bundle. Replace at build time.\n- DO NOT assume one Redis response shape — different commands, and even the same command with different flags (e.g. a `WITH...` modifier), return very different structures. See [references/redis-command-parsing.md](references/redis-command-parsing.md).\n\n## Final Checklist\n\n- Plugin type chosen and matches build tool (Parcel = external, Vite = internal).\n- `package.json` declares `main`, `styles`, and `visualizations` with required fields.\n- Every `activationMethod` matches a default-exported function.\n- Phases 1, 2, 3 each rendered successfully before moving on.\n- RedisInsight product UI applied via the `redis-ui-components` skill (internal plugins) or emulated locally (external plugins), with `light` / `dark` theme handling.\n- Review hardening pass completed for manifest matching, command parsing, visualization state, and scoped tests.\n- Bundle verified: `dist/index.js`, `dist/styles.css`, no `process.env`.\n- Plugin deployed to `~/.redis-insight/plugins/<name>/` (or via the Docker workaround).\n- `curl http://localhost:5540/api/plugins` lists the plugin.\n- Workbench runs a matching command and renders the visualization.\n- Defensive empty/error states verified.\n- Optional Playwright smoke test passes (written per the [e2e-testing](../e2e-testing/SKILL.md) skill).\n\n## Reference Index\n\n| File | Load When |\n|------|-----------|\n| [official-docs-summary.md](references/official-docs-summary.md) | Need the canonical contract from Redis Insight docs. |\n| [redis-insight-plugin-guidelines.md](references/redis-insight-plugin-guidelines.md) | Need the long-form operational reference. |\n| [external-parcel-plugin.md](references/external-parcel-plugin.md) | Building a standalone plugin with Parcel. |\n| [internal-vite-plugin.md](references/internal-vite-plugin.md) | Building inside the RedisInsight monorepo with Vite. |\n| [plugin-manifest.md](references/plugin-manifest.md) | Writing or stripping `package.json` manifests. |\n| [iterative-development.md](references/iterative-development.md) | Phase 1/2/3 templates and pipeline. |\n| [redisinsight-product-ui.md](references/redisinsight-product-ui.md) | Applying RedisInsight product UI inside plugin iframes. |\n| [review-hardening.md](references/review-hardening.md) | Pre-review checklist for matcher, parser, visualization state, and scoped regression tests. |\n| [testing-and-deployment.md](references/testing-and-deployment.md) | Deploy paths, Docker workaround, `/api/plugins`, Playwright. |\n| [redis-command-parsing.md](references/redis-command-parsing.md) | Parsing raw Redis command responses defensively. |\n| [third-party-libraries.md](references/third-party-libraries.md) | Integrating a visualization library, custom `.d.ts`, bundle size. |\n| [error-handling.md](references/error-handling.md) | Defensive render, ErrorBoundary, log prefixes. |\n",".ai/skills/redis-insight-plugin/references/error-handling.md":"# Error Handling and Stability\n\nPlugins run inside the Insight iframe. A thrown exception during activation produces a blank panel with no visible cause. Defensive rendering is mandatory.\n\n## Defensive Render Pattern\n\n```ts\nconst PREFIX = '[MY_PLUGIN]';\n\nexport function renderView(props: any) {\n  const host = document.getElementById('app');\n  if (!host) {\n    console.error(PREFIX, '#app missing');\n    return;\n  }\n  try {\n    const rows = parseData(props?.data ?? []);\n    if (rows.length === 0) {\n      host.innerHTML = '<div class=\"empty\">No data to display.</div>';\n      return;\n    }\n    renderTable(host, rows, props);\n  } catch (err) {\n    console.error(PREFIX, 'render failed', err);\n    host.innerHTML = `\n      <div class=\"error\">\n        <p>Plugin failed to render. See devtools console.</p>\n        <pre>${escape(String((err as Error)?.message ?? err))}</pre>\n      </div>\n    `;\n  }\n}\n```\n\nRules:\n\n- Always check `#app` exists.\n- Always check the data shape before rendering.\n- Always wrap render logic in `try/catch`.\n- Render an empty state and an error state — never a blank iframe.\n- Make empty/error copy match the active visualization mode. Each mode in a multi-visualization plugin should show its own message, not a single generic one.\n- Remove unreachable fallback branches after parser state is narrowed. Dead defensive checks hide the true state model.\n\n## React ErrorBoundary\n\nFor React-rendered plugins, wrap the root component:\n\n```tsx\nclass PluginErrorBoundary extends React.Component<{ children: React.ReactNode }, { error: Error | null }> {\n  state = { error: null as Error | null };\n  static getDerivedStateFromError(error: Error) { return { error }; }\n  componentDidCatch(error: Error, info: React.ErrorInfo) {\n    console.error('[PLUGIN]', 'render error', error, info);\n  }\n  render() {\n    if (this.state.error) {\n      return <div className=\"error\"><pre>{String(this.state.error.message)}</pre></div>;\n    }\n    return this.props.children;\n  }\n}\n```\n\nWrap once at the plugin root, not around every leaf component.\n\n## Logging Prefixes\n\nPrefix every log line with a stable, unique tag:\n\n- `[MY_PLUGIN]`\n- `[STREAM_PLUGIN]`\n- `[SEARCH_PLUGIN]`\n\nWhy:\n\n- Devtools filtering becomes a one-step regex.\n- Multiple plugins in the same Insight session can be told apart.\n- Customer logs forwarded to support are immediately attributable.\n\nNever use bare `console.log(data)` in shipped plugins.\n\n## State Persistence\n\nPrefer the SDK first, `localStorage` fallback:\n\n```ts\nimport { getState, setState } from 'redisinsight-plugin-sdk';\n\nasync function loadSettings() {\n  try {\n    return (await getState()) ?? {};\n  } catch {\n    try {\n      return JSON.parse(localStorage.getItem('ri:my-plugin:settings') ?? '{}');\n    } catch {\n      return {};\n    }\n  }\n}\n\nasync function saveSettings(s: object) {\n  try { await setState(s); }\n  catch { localStorage.setItem('ri:my-plugin:settings', JSON.stringify(s)); }\n}\n```\n\nValidate persisted state before applying — fields can disappear or change shape between plugin versions.\n\n## Common Issues\n\n| Issue | Likely cause | Fix |\n|-------|--------------|-----|\n| Blank iframe, no logs | Activation function not exported | Ensure `export default { fn }` and name matches manifest. |\n| `process.env is not defined` | Bundler left env refs | Replace at build time; verify `grep -c process.env dist/index.js` is `0`. |\n| `require is not defined` | Externalized React | `targets.module.includeNodeModules: true`; rebuild. |\n| Visualization renders but is empty | Parser returned empty | Log raw `props.data`; check command modifier flags. |\n| Values mapped to wrong field/axis | Response shape assumption wrong | Branch on the actual runtime shape, not the command name. |\n| Theme looks wrong | No `theme_DARK` handling | Read `document.body.classList` or `getTheme()` from SDK. |\n| Plugin disappears after Insight upgrade | Manifest field changed | Re-read official docs and update `package.json`. |\n| Large bundle, slow load | Missing minify, full lodash | Run `npm run minify:js`; switch to scoped imports. |\n",".ai/skills/redis-insight-plugin/references/external-parcel-plugin.md":"# External Parcel Plugin\n\nBuild a standalone Redis Insight plugin with Parcel.\n\n## Folder Layout\n\n```\n<plugin-name>/\n  package.json\n  src/\n    index.html\n    main.tsx\n    components/\n    styles/\n      styles.scss\n  dist/                     # build output, gitignored except when deploying\n    index.js\n    styles.css\n  scripts/\n    verify-plugin.sh\n    deploy-external.sh\n```\n\n## Build Tool: Parcel\n\nWhy Parcel:\n\n- Zero-config TS/SCSS/asset handling.\n- Simple `targets` model maps cleanly to \"single bundled JS + CSS\".\n- `targets.module.includeNodeModules: true` bundles every dep, which is what an external plugin needs.\n\nWhy **not** Vite for external plugins:\n\n- Vite assumes ESM consumers and multi-file output. Insight expects a single `dist/index.js`.\n- Vite's externalization defaults can leave React unbundled — fatal for an iframe with no shared deps.\n\n## Package Scripts\n\nRecommended scripts (see [../templates/external-parcel-package.json](../templates/external-parcel-package.json)):\n\n```json\n\"scripts\": {\n  \"start\":   \"parcel src/index.html\",\n  \"build\":   \"concurrently \\\"npm run build:js\\\" \\\"npm run build:css\\\"\",\n  \"build:js\":\"parcel build src/main.tsx --no-source-maps --dist-dir dist --target module\",\n  \"build:css\":\"parcel build src/styles/styles.scss --no-source-maps --dist-dir dist\",\n  \"minify:js\":\"terser dist/index.js -o dist/index.js -c -m\",\n  \"clean\":   \"rimraf dist\",\n  \"verify\":  \"bash scripts/verify-plugin.sh\",\n  \"deploy:external\":\"bash scripts/deploy-external.sh\",\n  \"deploy:internal\":\"bash scripts/deploy-internal-docker.sh\"\n}\n```\n\n## Targets\n\n```json\n\"targets\": {\n  \"main\": false,\n  \"module\": {\n    \"includeNodeModules\": true,\n    \"outputFormat\": \"esmodule\",\n    \"isLibrary\": false\n  }\n}\n```\n\n- `main: false` disables Parcel's CommonJS output.\n- `includeNodeModules: true` inlines every dependency.\n- Do **not** add `\"engines\": { \"browsers\": \"...\" }` so narrow that React or your visualization library break — Insight bundles a modern Chromium.\n\n## Bundling Rules\n\n- Bundle React, ReactDOM, and every other runtime dependency (including the visualization library).\n- No `peerDependencies`. Insight does not provide shared deps to plugins.\n- No imports from `uiSrc/`, `@redis-ui/*`, or any RedisInsight monorepo package.\n- No `process.env.*` references in the bundle. Replace with constants at build time. Verify with:\n\n  ```bash\n  grep -c \"process.env\" dist/index.js   # must be 0\n  ```\n\n- Minify before deploying customer-facing plugins; skip in dev for readable stack traces.\n\n## Source vs Deployed Manifest\n\nThe source `package.json` includes `scripts`, `devDependencies`, `targets`, etc. The deployed manifest must include only:\n\n- `name`, `version`, `description`\n- `main`, `styles`\n- `visualizations`\n- runtime `dependencies` (optional, informational)\n\nStrip the rest before copying into `~/.redis-insight/plugins/<name>/`. The deploy script in `templates/deploy-external.sh` does this for you (or use `jq` to filter).\n\n## Outputs\n\nAfter `npm run build`:\n\n- `dist/index.js` — single bundled module.\n- `dist/styles.css` — single stylesheet.\n\nIf your plugin has no styles, you can omit the `styles` manifest field and the SCSS source — but most plugins need styles.\n\nFor RedisInsight product UI fidelity, copy `templates/external-styles.scss` to `src/styles/styles.scss`. Keep plugin styles scoped under `.ri-plugin-*` classes and use `theme_LIGHT` / `theme_DARK` body classes for light/dark mode.\n\n## Verification\n\n```bash\nnpm run build\nnpm run verify     # runs templates/verify-plugin.sh\n```\n\n`verify` should report:\n\n- `package.json` present and parseable.\n- `dist/index.js` present and non-empty.\n- `dist/styles.css` present (if `styles` is declared).\n- Zero `process.env` references in the bundle.\n- Each declared `activationMethod` name appears in the bundle.\n- Bundle size under your project's threshold.\n",".ai/skills/redis-insight-plugin/references/internal-vite-plugin.md":"# Internal Vite Plugin\n\nBuild a Redis Insight plugin **inside** the RedisInsight monorepo. **This is the default for\nany contribution to this repo.** If you are not working inside the RedisInsight repo, use\n[external-parcel-plugin.md](external-parcel-plugin.md) instead.\n\n> **Start by copying a sibling.** Do not scaffold from scratch — copy an existing package such\n> as `geodata` or `redisearch` from `redisinsight/ui/src/packages/` and adapt it. Match the\n> surrounding conventions exactly rather than diverging.\n\n## Follow the repo styleguides\n\nInternal plugin code lives in the `redisinsight/ui/` tree, so it must follow the same\nstyleguides as the rest of the UI (these are mandatory, not optional):\n\n- **[frontend](../../frontend/SKILL.md)** — component folder structure\n  (`ComponentName/ComponentName.tsx` + `.styles.ts` + `.types.ts` + `.spec.tsx`), functional\n  components with hooks, **named exports**, barrel files (`src/components/index.ts`), layout\n  components (`Row` / `Col` / `FlexGroup`) instead of raw `div`, and theme usage.\n- **[redis-ui-components](../../redis-ui-components/)** — build all plugin UI from Redis UI\n  components via the `uiSrc/components/ui` wrappers; never import raw `@redis-ui/*`.\n- **[code-quality](../../code-quality/SKILL.md)** — no `any`, naming conventions, import order,\n  no magic numbers, no `!important`, semantic theme colors.\n- **[testing](../../testing/SKILL.md)** — Jest + Testing Library, `renderComponent`, `faker`,\n  `waitFor` over fixed waits.\n\n## Path\n\n```\nredisinsight/ui/src/packages/<plugin-name>/\n  package.json              # manifest (source/main/styles/visualizations) + scripts\n  index.html                # iframe entry with <div id=\"app\">\n  jest.config.cjs\n  tsconfig.json\n  public/                   # icon SVGs (copied to dist by the shared build)\n  src/\n    main.tsx                # exports activation functions (default export)\n    App.tsx / App.spec.tsx\n    components/             # ComponentName/ dirs + index.ts barrel\n    constants/             # index.ts barrel\n    types/                 # index.ts barrel\n    utils/                 # parsers + *.spec.ts\n    styles/styles.scss\n    global.d.ts\n    jest.setup.ts\n```\n\nSee any sibling under `redisinsight/ui/src/packages/` for a complete, current example\n(`redisearch` is a good table plugin; `geodata` is a good multi-visualization one).\n\n## Build Tool: Vite (shared config)\n\nThere is **no per-package `vite.config.ts`**. All internal plugins share a single config at\n`redisinsight/ui/src/packages/vite.config.mjs` and are listed in its `riPlugins` array. To add\na new plugin you register it there:\n\n```js\n// redisinsight/ui/src/packages/vite.config.mjs\nconst riPlugins = [\n  { name: 'redisearch', entry: 'src/main.tsx' },\n  // ...\n  { name: '<plugin-name>', entry: 'src/main.tsx' },  // add your plugin\n];\n```\n\nEach package's `package.json` wires the standard scripts:\n\n```json\n{\n  \"source\": \"./src/main.tsx\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"scripts\": {\n    \"dev\": \"vite -c ../vite.config.mjs\",\n    \"test\": \"node ../../../../../node_modules/.bin/jest -c jest.config.cjs\",\n    \"typecheck\": \"node ../../../../../node_modules/.bin/tsc --project tsconfig.json --noEmit\"\n  }\n}\n```\n\nThe shared build produces `dist/index.js` and `dist/styles.css` (and copies `public/` icons to\n`dist/`), all referenced from the manifest.\n\n## Manifest fields used in-repo\n\nIn addition to the base fields in [plugin-manifest.md](plugin-manifest.md), in-repo\nvisualizations commonly use:\n\n- `matchQuery.anyRegex` / `matchQuery.noneRegex` — refine matching beyond `matchCommands` (e.g.\n  only activate when a specific modifier flag is present in the command text).\n- `iconDark` / `iconLight` — `./dist/<icon>.svg` paths (source SVGs live in `public/`).\n\n## index.html\n\n```html\n<!doctype html>\n<html>\n  <head><meta charset=\"utf-8\" /></head>\n  <body class=\"theme_LIGHT\">\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./src/main.tsx\"></script>\n  </body>\n</html>\n```\n\n## Theme and Shared UI Caveats\n\n- Use Redis UI components and the theme tokens via the repo's `uiSrc/components/ui` wrappers per\n  the [redis-ui-components](../../redis-ui-components/) and [frontend](../../frontend/SKILL.md)\n  skills.\n- Detect iframe theme through the `theme_LIGHT` / `theme_DARK` body classes (or the\n  `redisinsight-plugin-sdk` theme helper).\n- Do not import from deep relative paths (`../../../../../`); use the monorepo's package alias.\n\n## Internal Plugin DO NOT\n\n- DO NOT introduce a Parcel build or a per-package `vite.config.ts` inside the monorepo. Register\n  in the shared `vite.config.mjs`.\n- DO NOT diverge from the runtime-dependency handling of sibling packages — copy how `geodata`\n  declares `react`/`react-dom` and externalization, don't invent your own.\n- DO NOT import from a sibling internal plugin — use shared utilities (`packages/common`) only.\n- DO NOT mutate global window state outside of the documented `window.state` surface.\n\n## When to Convert Internal → External\n\nIf the plugin is meant to ship outside RedisInsight (customer demo, field use, GitHub release),\nport it to an external Parcel layout. Keep the React component code; replace the build tool,\nbundle every dependency, and follow [external-parcel-plugin.md](external-parcel-plugin.md).\n",".ai/skills/redis-insight-plugin/references/iterative-development.md":"# Iterative Plugin Development\n\nBuild every Redis Insight plugin in three phases. The phases exist because each one isolates a different class of failure. Skipping is how you end up with a blank iframe and no idea why.\n\n## Phase 1 — Vanilla JS Wiring\n\nGoal: prove activation, props, and iframe rendering work. No React, no third-party libraries.\n\nUse the RedisInsight product UI baseline from `templates/external-styles.scss`; Phase 1 can be vanilla DOM without falling back to inline brand colors.\n\n```ts\n// src/main.tsx (or main.js)\nconst PREFIX = '[EXAMPLE_PLUGIN]';\n\nexport function renderExampleView(props: any) {\n  console.log(PREFIX, 'activated', props);\n  const root = document.getElementById('app');\n  if (!root) {\n    console.error(PREFIX, '#app missing');\n    return;\n  }\n  root.innerHTML = `\n    <section class=\"ri-plugin-shell\">\n      <div class=\"ri-plugin-panel\">\n        <header class=\"ri-plugin-header\">\n          <h2 class=\"ri-plugin-title\">Plugin works</h2>\n          <span class=\"ri-plugin-badge ri-plugin-badge--success\">Active</span>\n        </header>\n        <div class=\"ri-plugin-content\">\n          <code class=\"ri-plugin-command\">${escape(String(props?.command ?? ''))}</code>\n          <pre class=\"ri-plugin-code\">${escape(JSON.stringify(props?.data, null, 2))}</pre>\n        </div>\n      </div>\n    </section>\n  `;\n}\n\nfunction escape(s: string) {\n  return s.replace(/[&<>]/g, c => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;' }[c] as string));\n}\n\nexport default { renderExampleView };\n```\n\nFailure modes Phase 1 surfaces:\n\n- `activationMethod` name typo in the manifest.\n- Wrong `main` path or missing `dist/index.js`.\n- `process.env` references in the bundle.\n- Plugin folder not at `~/.redis-insight/plugins/<name>/`.\n- Insight not seeing the plugin (`/api/plugins`).\n\nIf Phase 1 doesn't render \"PLUGIN WORKS\" with the command and raw JSON, do not move on.\n\n## Phase 2 — React Rendering\n\nGoal: prove React mounts inside the iframe and displays props.\n\n```tsx\nimport * as React from 'react';\nimport { createRoot, type Root } from 'react-dom/client';\n\nconst PREFIX = '[EXAMPLE_PLUGIN]';\n\n// Workbench can re-activate the same iframe. Reuse one root per host element;\n// calling createRoot() twice on the same node warns and breaks rendering.\nconst roots = new WeakMap<HTMLElement, Root>();\n\nfunction getRoot(host: HTMLElement): Root {\n  let root = roots.get(host);\n  if (!root) {\n    root = createRoot(host);\n    roots.set(host, root);\n  }\n  return root;\n}\n\nfunction ExampleApp({ command, data }: { command: string; data: unknown }) {\n  return (\n    <section className=\"ri-plugin-shell\">\n      <div className=\"ri-plugin-panel\">\n        <header className=\"ri-plugin-header\">\n          <h2 className=\"ri-plugin-title\">Example Plugin</h2>\n          <span className=\"ri-plugin-badge ri-plugin-badge--success\">Active</span>\n        </header>\n        <div className=\"ri-plugin-content\">\n          <code className=\"ri-plugin-command\">{command}</code>\n          <pre className=\"ri-plugin-code\">{JSON.stringify(data, null, 2)}</pre>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport function renderExampleView(props: any) {\n  console.log(PREFIX, 'activated', props);\n  const host = document.getElementById('app');\n  if (!host) return;\n  try {\n    const root = getRoot(host);\n    root.render(<ExampleApp command={String(props?.command ?? '')} data={props?.data} />);\n  } catch (err) {\n    console.error(PREFIX, 'render failed', err);\n    host.innerHTML = '<div class=\"ri-plugin-error\">Plugin failed. See console.</div>';\n  }\n}\n\nexport default { renderExampleView };\n```\n\nIf you are on React 17, use `ReactDOM.render` instead of `createRoot`. Failure modes Phase 2 surfaces:\n\n- Wrong React/ReactDOM version pairing (React 17 + `createRoot` is the classic).\n- Externalized React in the bundle (`require is not defined`).\n- Missing `index.html` `#app` host.\n- Type errors not caught by the build.\n\n## Phase 3 — Full Feature\n\nGoal: real visualization library, real UX, real error/empty states.\n\nIn Phase 3:\n\n- Add the actual visualization library (charting, mapping, grid, etc.). See [third-party-libraries.md](third-party-libraries.md).\n- Parse the Redis response defensively. See [redis-command-parsing.md](redis-command-parsing.md).\n- Render empty and error states explicitly.\n- Persist user settings via SDK or `localStorage`.\n- Handle `theme_DARK` / `theme_LIGHT` switches.\n\nPhase 3 failures usually point to:\n\n- Unexpected response shape (use the parser patterns).\n- Library asset paths broken by bundling (icons, fonts, or images that resolve via bundler-relative URLs).\n- Missing CSS (`styles` field, CSS not bundled).\n- Cluster/multi-shard responses you didn't account for.\n\n## CI Pipeline Sketch\n\n```yaml\n# .github/workflows/plugin.yml (sketch)\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with: { node-version: '20' }\n      - run: npm ci\n      - run: npm run build\n      - run: bash scripts/verify-plugin.sh\n      - uses: actions/upload-artifact@v4\n        with:\n          name: plugin-bundle\n          path: |\n            package.json\n            dist/\n```\n\nOptionally extend with a Playwright job that boots a Redis Insight container, copies the artifact in, and runs a Workbench smoke test written per the repo's [e2e-testing](../../e2e-testing/SKILL.md) skill.\n",".ai/skills/redis-insight-plugin/references/official-docs-summary.md":"# Redis Insight Plugin Docs — Summary\n\nCondensed from the official Redis Insight plugin docs:\n\n- https://github.com/redis/RedisInsight/tree/main/docs/plugins\n- https://github.com/redis/RedisInsight/blob/main/docs/plugins/development.md\n- https://github.com/redis/RedisInsight/blob/main/docs/plugins/installation.md\n\nAlways re-read the upstream docs when in doubt — the contract may evolve.\n\n## Where Plugins Run\n\n- Workbench renders a plugin inside an **iframe**.\n- The iframe loads the plugin's `index.html` (or equivalent), which loads the bundle declared in the manifest's `main`.\n- The iframe DOM has a `<div id=\"app\"></div>` host element. Activation functions render into `#app`.\n- Stylesheets declared by `styles` are injected into the iframe.\n\n## Manifest (`package.json`)\n\nA plugin's root `package.json` is the manifest. Required fields:\n\n- `name`, `version`, `description`\n- `main` — relative path to the built JS bundle (e.g. `./dist/index.js`).\n- `styles` — relative path to the built CSS (e.g. `./dist/styles.css`).\n- `visualizations[]` — list of visualization descriptors:\n  - `id` — stable identifier inside the plugin.\n  - `name` — visible label in the Workbench result tabs.\n  - `activationMethod` — exact name of the exported function called to render this view.\n  - `matchCommands` — array of Redis command names this visualization supports (e.g. `[\"XRANGE\", \"XREVRANGE\"]`).\n  - `description` — short summary shown in the UI.\n  - `default` — `true` if this should be the default tab; otherwise `false`.\n\n## Activation Function\n\nThe plugin's main script exports an object whose keys are activation function names:\n\n```ts\nexport default { renderMyView };\n```\n\nEach activation function is invoked by the Workbench host and receives a props object containing:\n\n- The Redis `command` that was executed.\n- The data **result array** (raw Redis reply).\n- Helpers via `redisinsight-plugin-sdk` (config, modules, theme, base URL, app version).\n\nThe `<body>` of the iframe carries a `theme_DARK` or `theme_LIGHT` class, and `window.state` exposes plugin-relevant config such as connected modules, base URL, and app version.\n\n## Installation Paths\n\nUser-installed plugins live under:\n\n- macOS / Linux: `~/.redis-insight/plugins/`\n- Windows: `C:\\Users\\{Username}\\.redis-insight\\plugins\\`\n\nEach plugin lives in its own subdirectory (folder name should match `name`).\n\n## Trust Warning\n\nPlugins execute arbitrary code inside Redis Insight. **Only install plugins from trusted sources.** Treat plugin code review with the same care you would use for any code that runs in your dev environment.\n\n## What This Skill Adds\n\nThe official docs describe the contract. This skill adds operational practice:\n\n- Phased Phase 1/2/3 development to surface failure modes early.\n- Parcel for external plugins, Vite for internal monorepo plugins.\n- Defensive command parsing across different commands and flag-dependent response shapes.\n- `curl /api/plugins` and Playwright smoke validation after every deploy.\n- Docker static-plugin workaround for `redis/redisinsight` images.\n",".ai/skills/redis-insight-plugin/references/plugin-manifest.md":"# Plugin Manifest (`package.json`)\n\nThe plugin manifest is `package.json` at the plugin root.\n\n## Single Visualization\n\n```json\n{\n  \"name\": \"ri-plugin-xrange-table\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Render XRANGE results as a sortable table.\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"visualizations\": [\n    {\n      \"id\": \"xrange-table\",\n      \"name\": \"Stream Entries\",\n      \"activationMethod\": \"renderXRangeTable\",\n      \"matchCommands\": [\"XRANGE\", \"XREVRANGE\"],\n      \"description\": \"Tabular view of stream entries.\",\n      \"default\": false\n    }\n  ]\n}\n```\n\n## Multiple Visualizations\n\n```json\n{\n  \"name\": \"ri-plugin-stream-views\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Table and chart visualizations for Redis stream entries.\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"visualizations\": [\n    {\n      \"id\": \"stream-table\",\n      \"name\": \"Table\",\n      \"activationMethod\": \"renderTableView\",\n      \"matchCommands\": [\"XRANGE\", \"XREVRANGE\"],\n      \"description\": \"Tabular view of stream entries.\",\n      \"default\": false\n    },\n    {\n      \"id\": \"stream-chart\",\n      \"name\": \"Chart\",\n      \"activationMethod\": \"renderChartView\",\n      \"matchCommands\": [\"XRANGE\", \"XREVRANGE\"],\n      \"description\": \"Chart numeric fields over the stream's time range.\",\n      \"default\": false\n    }\n  ]\n}\n```\n\nThe plugin's main bundle exports both functions:\n\n```ts\nexport default {\n  renderTableView,\n  renderChartView,\n};\n```\n\n## Matching and Defaults\n\nWhen several visualizations share commands, make their defaults mutually exclusive by command shape, not just by command name. For example, one visualization can be the default when the result has a particular shape (numeric series, coordinates, nested rows, …), while another is available for the same commands but defaults only for a different shape (scalar, store-style, or empty results).\n\nRules for matcher definitions:\n\n- Match whole command tokens. Do not let a command match a longer command with the same prefix (e.g. a `*STORE` or `*_RO` variant).\n- Keep `matchCommands` broad only when `matchQuery` narrows the result shape safely.\n- Use `noneRegex` for exclusion only after checking how the platform normalizes command text.\n- Keep regexes bounded and linear. Avoid broad repeated token alternatives plus a trailing `[\\s\\S]{0,N}` window.\n- Test overlapping visualizations so only one `default: true` candidate remains for each command shape.\n\n## Icons\n\nOptional but supported:\n\n```json\n{\n  \"iconDark\": \"./dist/icon-dark.svg\",\n  \"iconLight\": \"./dist/icon-light.svg\"\n}\n```\n\nPlace icons under `dist/` (or another path your manifest points at) and copy them as part of deployment.\n\n## Stripping Dev-Only Fields\n\nThe deployed manifest must **not** include:\n\n- `scripts`\n- `devDependencies`\n- `targets` (Parcel-only build hints)\n- `husky`, `lint-staged`, `eslintConfig`, `prettier`, etc.\n\nEither keep a separate `package.deploy.json` or strip with `jq`:\n\n```bash\njq 'del(.scripts, .devDependencies, .targets, .husky, .[\"lint-staged\"])' \\\n  package.json > /tmp/package.deploy.json\n```\n\n`templates/deploy-external.sh` does this automatically.\n\n## Required Fields Recap\n\n- `name` — globally unique within `~/.redis-insight/plugins/`.\n- `version` — semver.\n- `description` — one sentence shown in Insight.\n- `main` — built JS bundle path.\n- `styles` — built CSS path (omit only if no styles).\n- `visualizations[]` — at least one, each with `id`, `name`, `activationMethod`, `matchCommands`, `description`, `default`.\n\nIf any required field is missing, Insight silently drops the plugin. Always run `curl /api/plugins` after deploying.\n",".ai/skills/redis-insight-plugin/references/redis-command-parsing.md":"# Redis Command Parsing\n\nPlugins must defensively parse raw Redis responses. Different commands return very different\nshapes, and even the same command with different flags returns different shapes.\n\n## Same Command, Different Shapes by Flag\n\nA command's optional modifiers change the response shape mid-flight. A classic pattern: without\nmodifiers a command returns a flat array of values, but adding `WITH...`-style flags returns a\nnested array per entry:\n\n```json\n// no modifiers — flat list\n[\"member1\", \"member2\", \"member3\"]\n```\n\n```json\n// with extra-data flags — nested per entry\n[\n  [\"member1\", \"12.34\", [\"extra\", \"data\"]],\n  [\"member2\", \"56.78\", [\"extra\", \"data\"]]\n]\n```\n\nNever key parsing off the command name alone — branch on the actual runtime shape, and read the\ncommand string from the activation props to choose the right header/label.\n\n## Worked Example: XRANGE / XREVRANGE\n\n```\nXRANGE stream - +\n```\n\nReturns an array of `[id, [field, value, field, value, ...]]`:\n\n```json\n[\n  [\"1700000000000-0\", [\"temp\", \"22\", \"humidity\", \"55\"]],\n  [\"1700000001000-0\", [\"temp\", \"23\"]]\n]\n```\n\nParser sketch:\n\n```ts\nexport function parseXRange(data: any[]): { id: string; fields: Record<string, string> }[] {\n  if (!Array.isArray(data)) return [];\n  return data.flatMap(entry => {\n    if (!Array.isArray(entry) || entry.length < 2) return [];\n    const [id, kv] = entry;\n    if (typeof id !== 'string' || !Array.isArray(kv)) return [];\n    const fields: Record<string, string> = {};\n    for (let i = 0; i + 1 < kv.length; i += 2) {\n      const k = kv[i];\n      const v = kv[i + 1];\n      if (typeof k === 'string') fields[k] = String(v);\n    }\n    return [{ id, fields }];\n  });\n}\n```\n\nThe same defensive structure applies to any command: start from `Array.isArray`, destructure\nsafely, coerce-and-validate numbers, and drop malformed entries instead of throwing.\n\n## Preserve Raw Values and Units\n\nSome commands accept a unit or format argument and return values in that same unit/format (a\nradius unit, a count, a score precision, etc.). If the visualization assumes one unit but the\ncommand used another, labels and scaling will be wrong. Read the relevant token from the command\nstring and preserve the raw value before any internal normalization.\n\n## Defensive Parsing Rules\n\n- Always start with `if (!Array.isArray(data)) return [];`.\n- Never assume nested array length — destructure with `const [a, b, ...rest] = ...`.\n- Preserve argument positions when tokenizing commands. Empty quoted strings (`\"\"`, `''`) should become empty-string tokens, not disappear.\n- Treat numeric fields as strings; convert with `Number(...)` and validate with `Number.isFinite(...)`.\n- Reject blank numeric strings before conversion; `Number('')` is `0`.\n- Drop malformed entries silently; surface the count (`\"3 of 25 entries skipped\"`) in the UI rather than crashing.\n- Never throw inside a parser — return an empty array and let the activation function render an empty/error state.\n- Avoid `Math.max(...largeArray)` or other spreading of full result sets. Use `reduce` or loops for large responses.\n- When parsing `FT.*` or other option-rich command strings, tokenize first and honor command grammar. Do not strip option keywords (`PARAMS`, `SEARCH`, `FILTER`, …) just because those words appear inside a key, member, index name, or quoted query.\n\n## Response Shape Caveats\n\n- **Cluster mode** can return wrapped responses depending on Insight's transport.\n- **Modifier flags** change the response shape mid-flight — never key off command name alone.\n- **Empty results** are `[]`, but on some commands a missing key returns `null`. Treat both as empty.\n- **Module commands** (RedisJSON, RedisTimeSeries, RedisSearch, etc.) return their own shapes — only support what your `matchCommands` declares.\n- **Redis 8 / module responses** can be array-like, aggregate-like, or object/map-style. Handle array-specific branches before generic object branches because arrays are objects in JavaScript.\n",".ai/skills/redis-insight-plugin/references/redis-insight-plugin-guidelines.md":"# Redis Insight Plugin — Operational Guidelines\n\nLong-form operational reference. SKILL.md is the entry point; this file is for anything that doesn't fit there.\n\n## Plugin Types Overview\n\nTwo plugin shapes, two build tools, two deployment models.\n\n- **External standalone plugin** — bundles everything with **Parcel**. Ships as a folder you drop into `~/.redis-insight/plugins/<name>/`. No knowledge of the RedisInsight monorepo. Default for customer demos and field plugins.\n- **Internal monorepo plugin** — lives at `RedisInsight/redisinsight/ui/src/packages/<plugin-name>/`. Built with **Vite** as part of the RedisInsight repo. Ships embedded in Insight builds.\n\nIf you are unsure, build external. External works in every Redis Insight install; internal only ships with the Insight binary you build.\n\n## Official References\n\n- https://github.com/redis/RedisInsight/tree/main/docs/plugins\n- https://github.com/redis/RedisInsight/blob/main/docs/plugins/development.md\n- https://github.com/redis/RedisInsight/blob/main/docs/plugins/installation.md\n\n## Internal Plugin Development with Vite\n\n- Path: `RedisInsight/redisinsight/ui/src/packages/<plugin-name>/`.\n- Use Vite, not Parcel. Inherit RedisInsight's shared TS, theme, and component conventions.\n- Internal plugins **may** import from `uiSrc/` and shared components, but only when the import has a stable contract — internal layout helpers move often.\n- Avoid hard dependencies on `ThemeProvider` or other Insight-only providers in plugin code that might be reused externally later.\n\n## External Plugin Development with Parcel\n\n- Path: anywhere; deployed to `~/.redis-insight/plugins/<plugin-name>/`.\n- Bundle every dependency. Do not externalize React or any runtime library.\n- `targets.module.includeNodeModules: true` so Parcel inlines deps.\n- No imports from `uiSrc/`, `@redis-ui/*`, or any RedisInsight internal package.\n- Output to `dist/index.js` and `dist/styles.css`.\n\n## Package.json Requirements\n\nSee [plugin-manifest.md](plugin-manifest.md) for full examples. Minimum:\n\n```json\n{\n  \"name\": \"ri-plugin-example\",\n  \"version\": \"0.0.1\",\n  \"description\": \"...\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"visualizations\": [\n    {\n      \"id\": \"example-view\",\n      \"name\": \"Example View\",\n      \"activationMethod\": \"renderExampleView\",\n      \"matchCommands\": [\"XRANGE\"],\n      \"description\": \"Shows XRANGE results.\",\n      \"default\": false\n    }\n  ]\n}\n```\n\n## Plugin Entry Point\n\n```ts\nfunction renderExampleView(props) {\n  const root = document.getElementById('app');\n  if (!root) {\n    console.error('[EXAMPLE_PLUGIN] #app element missing');\n    return;\n  }\n  try {\n    // Render here. Show empty state when data is empty.\n  } catch (err) {\n    console.error('[EXAMPLE_PLUGIN] render failed', err);\n    root.innerHTML = '<div class=\"ri-plugin-error\">Plugin failed. See console.</div>';\n  }\n}\n\nexport default { renderExampleView };\n```\n\n## Testing & Verification\n\n- `npm run build` produces `dist/index.js` and `dist/styles.css`.\n- `templates/verify-plugin.sh` confirms file presence, no `process.env`, and that activation method names appear in the bundle.\n- Deploy with `templates/deploy-external.sh` (folder install) or `templates/deploy-internal-docker.sh` (Docker container).\n- After deploy, `curl http://localhost:5540/api/plugins` and grep for the plugin name.\n- Optional: add a Workbench smoke test following the repo's [e2e-testing](../../e2e-testing/SKILL.md) skill (page objects, fixtures, UI navigation) to validate iframe rendering.\n\n## Iterative Plugin Development\n\nSee [iterative-development.md](iterative-development.md). Always run Phase 1, then Phase 2, then Phase 3.\n\n## DO NOT Rules\n\nReproduced from SKILL.md for convenience:\n\n- DO NOT skip Phase 1 or Phase 2.\n- DO NOT deploy `index.js`/`styles.css` at the plugin root — they live in `dist/`.\n- DO NOT set `default: true` by default.\n- DO NOT include `scripts` or `devDependencies` in the deployed manifest.\n- DO NOT use Vite for standalone plugins.\n- DO NOT import `uiSrc` in standalone plugins.\n- DO NOT externalize React in standalone bundles.\n- DO NOT skip `/api/plugins` verification.\n- DO NOT ship `process.env.*` references.\n- DO NOT assume one Redis response shape.\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---------|--------------|-----|\n| Plugin missing from `/api/plugins` | Manifest not deployed, or wrong folder | Check folder is at `~/.redis-insight/plugins/<name>/` and contains `package.json` + `dist/`. |\n| Iframe blank | `activationMethod` name mismatch | Confirm exported function name == manifest field. |\n| `process.env is not defined` | `process.env.*` left in bundle | Replace at build time; rebuild; verify with `grep -c process.env dist/index.js`. |\n| `require is not defined` | Externalized React or wrong target | Use Parcel `targets.module` with `includeNodeModules: true`. |\n| Styles not applied | `styles` field missing or wrong path | Add `\"styles\": \"./dist/styles.css\"`; confirm file exists. |\n| Console error: `#app is null` | Activation ran before DOM | Ensure script is loaded as a module and renders into `document.getElementById('app')`. |\n| Visualization tab missing | `matchCommands` mismatch | Confirm command name (uppercase) appears in `matchCommands`. |\n\n## Lessons Learned\n\n- Phase 1 catches `activationMethod` name/manifest mismatches before any React is wired.\n- Phase 2 surfaces React/ReactDOM version and `createRoot` mismatches early.\n- Phase 3 exposes library asset-path bugs (icons/fonts/images broken by bundler-relative URLs) — fix once at init.\n- A command with extra-data flags often returns nested arrays, not objects — guard against shape changes.\n- Always `curl /api/plugins` after deploy. Cached menus lie.\n\n## Plugin API Reference\n\nActivation function props (subject to upstream changes — re-check official docs):\n\n- `command: string` — the executed Redis command.\n- `data: any[]` — the raw Redis result array.\n- `redisInsightPlugin` (via SDK) — `getState`, `setState`, `getModules`, `getTheme`, `getBaseUrl`, `getAppVersion`.\n\nUse `redisinsight-plugin-sdk` for state and theme rather than reading `window.state` directly when possible.\n\n## Command Parsing Patterns\n\nSee [redis-command-parsing.md](redis-command-parsing.md).\n\n## Third-Party Library Integration\n\nSee [third-party-libraries.md](third-party-libraries.md).\n\n## Interactive UI Patterns\n\n- Keep heavy UI inside the iframe; do not assume access to the host page.\n- Apply the [redis-ui-components](../../redis-ui-components/) skill with RedisInsight `light` / `dark` themes for visual plugin work (internal plugins use the `uiSrc/components/ui` wrappers per the [frontend](../../frontend/SKILL.md) styleguide).\n- Standalone plugins emulate product tokens locally with `src/styles/styles.scss`; copy `templates/external-styles.scss` as the baseline.\n- Use `document.body.classList.contains('theme_DARK')` to switch palettes.\n- Persist user settings via SDK state, falling back to `localStorage` keyed under your plugin id.\n- Avoid global CSS resets that fight Insight's iframe styles.\n\n## State Persistence\n\n- Prefer `redisinsight-plugin-sdk` `getState` / `setState` for cross-session persistence.\n- Fall back to `localStorage` with a plugin-prefixed key (`ri:<plugin-name>:settings`) when SDK state is unavailable.\n- Treat persisted state as untrusted; validate before use.\n\n## Error Handling & Stability\n\nSee [error-handling.md](error-handling.md).\n\n## Debugging & Development Tips\n\n- Open the Workbench iframe in browser devtools to inspect logs and network calls.\n- Look for `[<PLUGIN_PREFIX>]` log lines to filter your output.\n- Use the Phase 1 vanilla bundle to A/B test whether failures are plugin-host or library-related.\n- For Docker installs, `docker exec` into the container and inspect `/usr/src/app/redisinsight/api/dist/static/plugins/<name>/`.\n",".ai/skills/redis-insight-plugin/references/redisinsight-product-ui.md":"# RedisInsight Product UI For Plugins\n\nUse the [redis-ui-components](../../redis-ui-components/) skill for visual decisions. This file\nonly adapts it to RedisInsight Workbench plugin iframes.\n\n> **Internal vs external.** For **internal** plugins (the default in this repo), build the UI\n> from Redis UI components via the `uiSrc/components/ui` wrappers and follow the\n> [frontend](../../frontend/SKILL.md) styleguide — use layout components, theme colors, and the\n> standard component folder structure. The \"Standalone Constraints\" below (no `@redis-ui/*` /\n> `uiSrc/` imports, local CSS variables) apply to **external standalone** plugins only, which\n> are bundled in isolation and must emulate the same look with local code.\n\n## Theme\n\n- RedisInsight plugins use `light` / `dark`, never `light2` / `dark2`.\n- Standalone external plugins cannot import RedisInsight internals, so define local CSS variables that emulate RedisInsight product tokens.\n- Read the iframe body class:\n  - `theme_LIGHT` => light theme\n  - `theme_DARK` => dark theme\n- If using `redisinsight-plugin-sdk`, prefer its theme helper when available, then mirror the same mode into local classes only when needed.\n\n## Baseline Files\n\nFor external Parcel plugins:\n\n- Copy `templates/external-styles.scss` to `src/styles/styles.scss`.\n- Keep `package.json` `\"styles\": \"./dist/styles.css\"`.\n- Keep the stylesheet bundled into `dist/styles.css`; RedisInsight injects it into the plugin iframe.\n\n## UI Rules\n\n- Build compact Workbench-like surfaces: toolbar, table, inspector, code/value panel, empty/error/loading states.\n- Prefer table-plus-detail layouts for Redis command responses.\n- Use `Source Code Pro` or monospace stacks for Redis keys, stream IDs, commands, raw values, and timestamps.\n- Use status badges with text and color; never rely on color alone.\n- Use product semantic colors for success, warning, danger, selected, and neutral states.\n- Keep controls stable: long keys and JSON must scroll, wrap intentionally, or ellipsize with a title/tooltip.\n\n## Standalone Constraints\n\n- Do not import `uiSrc/`, `@redis-ui/*`, RedisInsight monorepo packages, or private source.\n- Do not use global CSS resets that can fight the iframe host.\n- Scope styles under plugin classes such as `.ri-plugin-shell`.\n- Do not use Redis brand red as the universal accent, heading, CTA, error, or status color.\n\n## Verification\n\nCheck both themes:\n\n```bash\ncurl -s http://localhost:5540/api/plugins\n```\n\nThen run a matching Redis command in Workbench and verify:\n\n- `theme_LIGHT` and `theme_DARK` render correctly.\n- Empty, loading, error, and success states are styled.\n- Tables and `pre` blocks do not overflow the iframe incoherently.\n- Browser console has no style/runtime errors.\n",".ai/skills/redis-insight-plugin/references/review-hardening.md":"# Review Hardening\n\nUse this before opening or updating a Redis Insight plugin PR. It captures review issues that are easy for coding agents to miss because the happy path works.\n\n## Start From Review Discipline\n\n- Treat every bot or human comment as a hypothesis. Confirm it against code, tests, and Redis/Insight behavior before changing code.\n- Reproduce the failing shape with a focused test first. If the issue is valid, keep the regression test.\n- Prefer the narrowest verification scope that proves the change: package tests, parser tests, manifest matcher tests, and typecheck. Do not run a full RedisInsight build for every comment unless the changed surface requires it.\n- When replying to PR comments, state the behavior fixed, the test added, and the commit that contains it.\n\n## Manifest and Matcher Checklist\n\n- Match whole Redis command tokens, not prefixes. A command must not match a longer command with the same prefix (e.g. a `*STORE` or `*_RO` variant).\n- Keep multi-visualization defaults mutually exclusive for overlapping commands. If one view is the default for one result shape, another can be available but must not also be the blanket default for the same command shape.\n- If `matchQuery.anyRegex` / `noneRegex` is used, keep regexes bounded and linear. Avoid repeated broad alternatives followed by trailing `[\\s\\S]{0,N}`.\n- Make token alternatives disjoint: quoted strings, double-quoted strings, and unquoted tokens should not overlap in a way that creates backtracking spikes.\n- Test native command names, module command names, and store/scalar variants separately. Query/result rows and scalar/store commands often need different visualization defaults.\n\n## Parser Checklist\n\n- Tokenize the Redis command before inspecting options. Raw `indexOf(\"PARAMS\")`, `indexOf(\"SEARCH\")`, or `indexOf(\"FILTER\")` will misread keys, members, index names, and quoted query text.\n- Preserve empty quoted tokens in the tokenizer. `\"\"` and `''` should still occupy an argument position, even if the downstream parser later rejects that command shape.\n- Keyword positions matter. For `FT.HYBRID`, skip the command and index tokens when detecting `SEARCH` / `FILTER`, and skip `PARAMS` key/value ranges when parsing predicates.\n- Strip or ignore large option payloads only when the token is the actual option for the command grammar. Do not strip a key/member named like an option keyword, an index named like one, or query text containing one.\n- Preserve the raw unit/format returned by Redis (the token that follows the relevant option in the command string) before any internal normalization.\n- Validate blank numeric parts before calling `Number(...)`; JavaScript treats `Number(\"\")` as `0`.\n- Differentiate empty result sets from malformed/incomplete rows so the UI can show the right guidance.\n- Handle search-like arrays, aggregate-like arrays, and Redis 8 map-style objects before generic object fallback. Arrays are objects in JavaScript, so order matters.\n- Avoid `Math.max(...largeArray)` or similar spread over result sets. Reduce or loop to avoid call stack and argument limits.\n\n## Visualization Library Checklist\n\n- Validate library preconditions before rendering (non-empty series, valid bounds/ranges, sized container). Bail to an empty/error state instead of calling render APIs with bad input.\n- Do not let library callbacks (cell renderers, tooltip/popup builders, icon factories) capture stale React state. Keep live values in refs or recreate the instance when the callback dependencies change.\n- Build tooltip/popup/cell DOM with `textContent` or escaped React output, not interpolated HTML from Redis data.\n- Destroy/dispose the library instance before re-creating it on re-render to avoid leaks.\n- For large result sets, use loops/reductions and avoid per-render expensive parsing unless the input changed.\n- Use mode-aware empty/error titles. Different visualizations in the same plugin should not all show the same generic failure message.\n\n## Code Hygiene Checklist\n\n- Remove dead plugin config files and unused exports instead of leaving them for reviewers to rediscover.\n- Enforce declared limits or delete them. A `maxRows` / `maxPoints` constant that nobody reads is a bug magnet.\n- Centralize shared conversion constants/helpers so parsers and visualizations cannot drift.\n- Keep component-local types and large constants in sibling `.types.ts` and `.constants.ts` files when the component is already large.\n- Keep return types honest and simple. Do not use a tuple type when the function returns arbitrary-length arrays.\n- After discriminated-union narrowing, remove unreachable null/error branches. A defensive check that cannot run makes the real state model harder to review.\n- Memoize parsed command/results when parsing is nontrivial or feeds the visualization render.\n\n## Test Matrix\n\nAdd focused tests for any surface you touch:\n\n- Manifest matcher: exact command boundaries, default exclusivity, distinct result shapes (scalar vs row vs nested).\n- Command parser: keyword-like key/member/index names, empty quoted tokens, option keywords appearing in native commands and in query text, large payloads, empty rows, malformed rows, Redis 8 object responses.\n- Unit/format handling: commands that take a unit or format argument return values in that same unit/format; cover each variant the plugin supports.\n- Visualization state: control updates while rendered, empty/invalid input sets, distinct per-mode error titles.\n- Performance guardrails: large result counts, large payloads, and no spread into `Math.max`.\n",".ai/skills/redis-insight-plugin/references/testing-and-deployment.md":"# Testing and Deployment\n\nHow to deploy and verify a Redis Insight plugin, and how to smoke test it with Playwright (following the repo's [e2e-testing](../../e2e-testing/SKILL.md) skill).\n\n## Correct External Deployment Structure\n\n```\n~/.redis-insight/plugins/<plugin-name>/\n  package.json               # deployed manifest (no scripts, no devDependencies)\n  dist/\n    index.js\n    styles.css\n```\n\nThat's it. Nothing else is required at deploy time.\n\n## Wrong Structures (Common Mistakes)\n\n```\n# WRONG — bundle at plugin root\n~/.redis-insight/plugins/<plugin-name>/\n  package.json\n  index.js                   # should be in dist/\n  styles.css                 # should be in dist/\n```\n\n```\n# WRONG — nested package folder\n~/.redis-insight/plugins/<plugin-name>/\n  <plugin-name>/             # extra directory\n    package.json\n    dist/\n```\n\n```\n# WRONG — manifest still has dev/scripts\n{\n  \"main\": \"./dist/index.js\",\n  \"scripts\": { \"build\": \"...\" },        # remove\n  \"devDependencies\": { \"parcel\": \"...\" } # remove\n}\n```\n\n## Deploy Commands\n\nExternal (host install):\n\n```bash\nnpm run build\nbash templates/verify-plugin.sh\nbash templates/deploy-external.sh\n```\n\nInside Docker RedisInsight:\n\n```bash\nnpm run build\nbash templates/verify-plugin.sh\nbash templates/deploy-internal-docker.sh\n```\n\n## Docker RedisInsight: Static Plugin Path\n\nExternal plugin deployment to `~/.redis-insight/plugins/<name>/` works for the host install — that is the canonical path and the one to default to.\n\nFor Docker, the host plugins folder is not visible inside the container. To install a plugin into a running `redis/redisinsight` container, copy it into the container's static plugins folder:\n\n```\n/usr/src/app/redisinsight/api/dist/static/plugins/<plugin-name>/\n```\n\nWorkflow:\n\n```bash\ndocker cp dist redisinsight-test:/usr/src/app/redisinsight/api/dist/static/plugins/<plugin-name>/dist\ndocker cp package.json redisinsight-test:/usr/src/app/redisinsight/api/dist/static/plugins/<plugin-name>/package.json\ndocker restart redisinsight-test\n```\n\nThen verify via `/api/plugins`. The deploy script in `templates/deploy-internal-docker.sh` automates this.\n\n## /api/plugins Verification\n\nAfter every deploy:\n\n```bash\ncurl -s http://localhost:5540/api/plugins | jq '.[] | {name, visualizations}'\n```\n\nThe plugin must appear by `name`, with each declared `visualizations[*].id`. If absent:\n\n- The plugin folder is wrong, or\n- The manifest is invalid (missing required field), or\n- Insight wasn't restarted (Docker case).\n\n## Browser Console Verification\n\nOpen the Workbench iframe in browser devtools:\n\n- Filter logs by your `[<PLUGIN_PREFIX>]` prefix.\n- Confirm `activated` log fires when running a matching command.\n- Confirm no `process.env` errors, no `require is not defined`, no React mismatch.\n- Inspect network calls — there should be none unless your plugin explicitly makes them.\n\n## Playwright Smoke Test Strategy\n\nFor any Playwright E2E test, **follow the repo's [e2e-testing](../../e2e-testing/SKILL.md)\nskill** — it owns the conventions for this repo (tests live in `tests/e2e-playwright/`, use page\nobjects, fixtures, and `apiHelper`; never call `page.goto()` directly; no CSS selectors or fixed\nwaits). Do not hand-roll a standalone spec that bypasses those patterns.\n\nA plugin smoke test should cover the same flow, expressed through those conventions:\n\n1. Create/seed the database via `apiHelper` in `beforeAll`.\n2. Navigate to Workbench through a page object's `goto()` method (UI navigation, not `page.goto()`).\n3. Run a command that matches the plugin's `matchCommands`.\n4. Select the plugin's visualization tab.\n5. Assert the iframe is present and renders expected content.\n\nUse Playwright MCP to discover the actual `data-testid`/roles first, as the e2e-testing skill\ndescribes. The one plugin-specific check that does not need the UI is registration —\n`curl /api/plugins` (or `page.request.get('/api/plugins')`) must list the plugin.\n\n## Internal PR Verification Scope\n\nFor RedisInsight monorepo plugin changes, keep verification proportional to the changed surface:\n\n- Parser or command-shape fix: run the parser spec or package test file first, then the plugin package test command.\n- Manifest or matcher fix: run the matcher/plugin utility spec that exercises `matchCommands`, `matchQuery`, `anyRegex`, `noneRegex`, and `default` selection.\n- React / visualization-library fix: run the component spec for that visualization plus the package typecheck.\n- Code hygiene fix: run the closest affected spec plus typecheck, especially when moving types/constants or centralizing helpers.\n- Shared utility change under `ui/src/`: add the focused utility spec and a changed-file lint/typecheck check.\n- Before final commit on a PR batch, run the package tests, package typecheck, and `git diff --check`. Run a full RedisInsight build only when build configuration, package exports, bundling, or shared application wiring changed.\n",".ai/skills/redis-insight-plugin/references/third-party-libraries.md":"# Third-Party Libraries\n\nNotes on integrating a visualization library (charting, mapping, grid, diagram, etc.) inside a\nRedis Insight plugin iframe. The principles are library-agnostic; apply them to whatever library\nthe plugin needs.\n\n## General Integration Rules\n\n- **Bundle the library fully** in an external (Parcel) plugin — do not externalize it. Internal\n  (Vite) plugins follow the shared build's dependency handling instead.\n- **Add the library's CSS to the bundle** (`dist/styles.css`); do not rely on a global `@import`\n  or a CDN. RedisInsight injects the bundled stylesheet into the plugin iframe.\n- **Initialize once, clean up on re-render.** Libraries that own a canvas, map, or grid instance\n  must be destroyed/disposed before re-creating, or they leak:\n\n  ```ts\n  instanceRef.current?.destroy();\n  instanceRef.current = createInstance(container, config);\n  ```\n\n- **Size the container explicitly.** Many libraries measure their parent on init; put the\n  render target in a sized flex/grid container so it lays out correctly inside the iframe.\n- **Validate inputs before handing them to the library.** Never assume parsed Redis data is\n  well-formed — guard for empty/NaN/out-of-range values, and check any library precondition\n  (valid bounds, non-empty series, etc.) before calling its render APIs.\n- **Avoid stale closures in library callbacks.** If a callback (cell renderer, tooltip builder,\n  icon factory) depends on React state, keep the live value in a ref or recreate the instance\n  when the dependency changes — captured state goes stale otherwise.\n- **Never interpolate Redis data into raw HTML.** Build tooltip/popup/cell DOM with\n  `textContent` or escaped React output so keys, members, and field values cannot inject markup.\n\n## Untyped Libraries: Custom `.d.ts`\n\nSome libraries (or their sub-plugins) ship without types. Add a local declaration instead of\nusing `any`:\n\n```ts\n// src/types/<lib-name>.d.ts\ndeclare module '<lib-name>' {\n  export interface Options {\n    /* the options the plugin actually uses */\n  }\n  export function createThing(target: HTMLElement, options?: Options): unknown;\n}\n```\n\nKeep the declaration minimal — only the surface the plugin uses.\n\n## Bundle Size Guidance\n\n- Aim for `dist/index.js` under ~1.5 MB minified for a snappy first render.\n- Use the bundler's analyzer (or `terser`'s reports) to find heavy dependencies.\n- Avoid `moment`; use `date-fns` or native `Intl.DateTimeFormat`.\n- Avoid full `lodash`; use targeted imports (`lodash/get`) or stdlib.\n- Tree-shaking only helps when imports are scoped (`import { sum } from 'lodash-es'`).\n- Cap row/point counts (paginate or virtualize) for large command results rather than handing\n  the whole set to the library at once.\n\n## Forbidden Imports\n\n- No `uiSrc/`, `@redis-ui/*`, or any RedisInsight monorepo internal in standalone plugins.\n- No Node-only modules (`fs`, `path`, `child_process`).\n- No CommonJS-only deps that fail in Parcel's `module` target without shims.\n",".ai/skills/redis-insight-plugin/templates/external-parcel-package.json":"{\n  \"name\": \"ri-plugin-example\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Example external Redis Insight Workbench plugin built with Parcel.\",\n  \"source\": \"./src/main.tsx\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"scripts\": {\n    \"start\": \"parcel src/index.html\",\n    \"build\": \"concurrently \\\"npm run build:js\\\" \\\"npm run build:css\\\"\",\n    \"build:js\": \"parcel build src/main.tsx --no-source-maps --dist-dir dist --target module\",\n    \"build:css\": \"parcel build src/styles/styles.scss --no-source-maps --dist-dir dist\",\n    \"minify:js\": \"terser dist/index.js -o dist/index.js -c -m\",\n    \"clean\": \"rimraf dist\",\n    \"verify\": \"bash scripts/verify-plugin.sh\",\n    \"deploy:external\": \"bash scripts/deploy-external.sh\",\n    \"deploy:internal\": \"bash scripts/deploy-internal-docker.sh\"\n  },\n  \"targets\": {\n    \"main\": false,\n    \"module\": {\n      \"includeNodeModules\": true,\n      \"outputFormat\": \"esmodule\",\n      \"isLibrary\": false\n    }\n  },\n  \"visualizations\": [\n    {\n      \"id\": \"example-view\",\n      \"name\": \"Example View\",\n      \"activationMethod\": \"renderExampleView\",\n      \"matchCommands\": [\"XRANGE\"],\n      \"description\": \"Example visualization rendered for XRANGE results.\",\n      \"default\": false\n    }\n  ],\n  \"dependencies\": {\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\",\n    \"redisinsight-plugin-sdk\": \"^1.1.0\"\n  },\n  \"devDependencies\": {\n    \"parcel\": \"^2.12.0\",\n    \"terser\": \"^5.30.0\",\n    \"rimraf\": \"^5.0.5\",\n    \"concurrently\": \"^8.2.2\",\n    \"cross-env\": \"^7.0.3\",\n    \"typescript\": \"^5.4.0\",\n    \"@types/react\": \"^17.0.79\",\n    \"@types/react-dom\": \"^17.0.25\"\n  }\n}\n",".ai/skills/testing/SKILL.md":"---\nname: testing\ndescription: >-\n  Unit/integration testing standards for RedisInsight using Jest and\n  Testing Library: test structure, the `renderComponent` helper, faker\n  for test data, mocking patterns, and `waitFor` instead of fixed\n  time waits. Use when writing or modifying any `*.spec.ts` or\n  `*.spec.tsx` file, when adding component or slice tests, when\n  debugging flaky tests, or when the user mentions jest, testing\n  library, faker, or test patterns.\n---\n\n# Testing Standards and Practices\n\n## Core Principles\n\n- **Write tests for all new features**\n- Follow **AAA pattern**: Arrange, Act, Assert\n- Use **descriptive test names**: \"should do X when Y\"\n- **CRITICAL**: Never use fixed time waits - tests must be deterministic\n- **CRITICAL**: Use faker library (@faker-js/faker) for test data\n\n## Test Organization\n\n```typescript\ndescribe('FeatureService', () => {\n  describe('findById', () => {\n    it('should return entity when found', () => {});\n    it('should throw NotFoundException when not found', () => {});\n  });\n\n  describe('create', () => {\n    it('should create entity with valid data', () => {});\n    it('should throw error with invalid data', () => {});\n  });\n});\n```\n\n## Frontend Testing (Jest + Testing Library)\n\n### Running Specific Tests\n\n```bash\n# Run a specific test file\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs'\n\n# Run a specific test by name (use -t flag)\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs' -t 'test name pattern'\n\n# Example:\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/slices/tests/browser/keys.spec.ts' -c 'jest.config.cjs' -t 'refreshKeyInfoAction'\n```\n\n### CRITICAL: Always Use Shared `renderComponent` Helper\n\n**Create a `renderComponent` helper for each component test file**:\n\n```typescript\nimport { faker } from '@faker-js/faker';\n// Pull structured data from a shared factory (redisinsight/ui/src/mocks/factories/);\n// only build ad-hoc primitives/callbacks inline.\n\ndescribe('MyComponent', () => {\n  // Default props: factory for the entity, inline only for callbacks/primitives\n  const mockUser = UserFactory.build();\n  const defaultProps: MyComponentProps = {\n    user: mockUser,\n    onComplete: jest.fn(),\n  };\n\n  // Shared render helper\n  const renderComponent = (propsOverride?: Partial<MyComponentProps>) => {\n    const props = { ...defaultProps, ...propsOverride };\n\n    return render(\n      <Provider store={store}>\n        <MyComponent {...props} />\n      </Provider>\n    );\n  };\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n  });\n\n  it('should render component', () => {\n    renderComponent();\n    expect(screen.getByText(defaultProps.name)).toBeInTheDocument();\n  });\n\n  it('should handle click', async () => {\n    const mockOnComplete = jest.fn();\n    renderComponent({ onComplete: mockOnComplete });\n\n    fireEvent.click(screen.getByRole('button'));\n\n    await waitFor(() => {\n      expect(mockOnComplete).toHaveBeenCalledTimes(1);\n    });\n  });\n});\n```\n\n**Benefits**:\n\n- Centralized setup (providers, router, theme)\n- Default props defined once\n- Easy prop overrides per test\n- No duplicate setup code\n\n### Complex Component Setup\n\nFor components requiring Router, ThemeProvider, etc., include them in `renderComponent`:\n\n```typescript\nconst renderComponent = (propsOverride?: Partial<Props>) => {\n  const props = { ...defaultProps, ...propsOverride }\n\n  return render(\n    <Provider store={store}>\n      <BrowserRouter>\n        <ThemeProvider theme={theme}>\n          <Component {...props} />\n        </ThemeProvider>\n      </BrowserRouter>\n    </Provider>\n  )\n}\n```\n\n### Testing with Redux\n\nCreate a test store with `configureStore` for Redux-connected components:\n\n```typescript\nconst createTestStore = (initialState = {}) => {\n  return configureStore({\n    reducer: { user: userSlice.reducer },\n    preloadedState: initialState,\n  });\n};\n\nconst renderComponent = (propsOverride?: Partial<Props>, storeState = {}) => {\n  const testStore = createTestStore(storeState);\n  // render with testStore\n};\n```\n\n### Query Priorities (Testing Library)\n\n**Prefer accessible queries** (as users would interact):\n\n```typescript\n// ✅ PREFERRED\nscreen.getByRole('button', { name: /submit/i });\nscreen.getByLabelText('Email');\nscreen.getByPlaceholderText('Enter name');\n\n// ⚠️ LAST RESORT\nscreen.getByTestId('user-profile');\n\n// ❌ AVOID\nwrapper.find('.button-class');\n```\n\n### Testing Async Behavior\n\n```typescript\n// ✅ GOOD: waitFor with proper queries\nawait waitFor(() => {\n  expect(screen.getByText('Data loaded')).toBeInTheDocument();\n});\n\n// ✅ GOOD: waitForElementToBeRemoved\nawait waitForElementToBeRemoved(() => screen.queryByText('Loading...'));\n\n// ✅ GOOD: findBy queries (built-in waiting)\nconst element = await screen.findByText('Async content');\n\n// ❌ BAD: Fixed timeouts (flaky tests)\nawait new Promise((resolve) => setTimeout(resolve, 1000));\n```\n\n### Mocking API Calls (MSW)\n\nUse Mock Service Worker for API mocking:\n\n```typescript\nimport { rest } from 'msw';\nimport { setupServer } from 'msw/node';\n\nconst server = setupServer(\n  rest.get('/api/users/:id', (req, res, ctx) => {\n    return res(\n      ctx.json({\n        id: req.params.id,\n        name: faker.person.fullName(),\n      }),\n    );\n  }),\n);\n\nbeforeAll(() => server.listen());\nafterEach(() => server.resetHandlers());\nafterAll(() => server.close());\n```\n\n## Backend Testing (NestJS/Jest)\n\n### Service Test Pattern\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\n// Define factory for User entity\nconst userFactory = Factory.define<User>(() => ({\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n}));\n\ndescribe('UserService', () => {\n  let service: UserService;\n  let repository: Repository<User>;\n\n  const mockRepository = {\n    find: jest.fn(),\n    findOne: jest.fn(),\n    save: jest.fn(),\n    update: jest.fn(),\n    delete: jest.fn(),\n  };\n\n  beforeEach(async () => {\n    const module = await Test.createTestingModule({\n      providers: [\n        UserService,\n        {\n          provide: getRepositoryToken(User),\n          useValue: mockRepository,\n        },\n      ],\n    }).compile();\n\n    service = module.get<UserService>(UserService);\n    repository = module.get<Repository<User>>(getRepositoryToken(User));\n  });\n\n  afterEach(() => {\n    jest.clearAllMocks();\n  });\n\n  it('should return user when found', async () => {\n    const mockUser = userFactory.build();\n    mockRepository.findOne.mockResolvedValue(mockUser);\n\n    const result = await service.findById(mockUser.id);\n\n    expect(result).toEqual(mockUser);\n  });\n});\n```\n\n### Controller Test Pattern\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nconst userFactory = Factory.define<User>(() => ({\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n}));\n\ndescribe('UserController', () => {\n  let controller: UserController;\n  let service: UserService;\n\n  const mockService = {\n    findAll: jest.fn(),\n    findById: jest.fn(),\n    create: jest.fn(),\n  };\n\n  beforeEach(async () => {\n    const module = await Test.createTestingModule({\n      controllers: [UserController],\n      providers: [{ provide: UserService, useValue: mockService }],\n    }).compile();\n\n    controller = module.get<UserController>(UserController);\n  });\n\n  it('should return user from service', async () => {\n    const mockUser = userFactory.build();\n    mockService.findById.mockResolvedValue(mockUser);\n\n    const result = await controller.findById(mockUser.id);\n\n    expect(result).toEqual(mockUser);\n  });\n});\n```\n\n### Integration Tests (E2E)\n\n```typescript\ndescribe('UserController (e2e)', () => {\n  let app: INestApplication;\n\n  beforeAll(async () => {\n    const module = await Test.createTestingModule({\n      imports: [AppModule],\n    }).compile();\n\n    app = module.createNestApplication();\n    await app.init();\n  });\n\n  afterAll(async () => {\n    await app.close();\n  });\n\n  it('/users (GET)', () => {\n    return request(app.getHttpServer())\n      .get('/users')\n      .expect(200)\n      .expect((res) => {\n        expect(Array.isArray(res.body)).toBe(true);\n      });\n  });\n});\n```\n\n## E2E Testing (Playwright)\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nconst userDataFactory = Factory.define(() => ({\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n}));\n\ntest.describe('User Management', () => {\n  test('should create new user', async ({ page }) => {\n    const userData = userDataFactory.build();\n\n    await page.goto('/users');\n    await page.click('text=Add User');\n    await page.fill('[name=\"name\"]', userData.name);\n    await page.fill('[name=\"email\"]', userData.email);\n    await page.click('text=Submit');\n\n    // ✅ Use proper waits\n    await expect(page.locator(`text=${userData.name}`)).toBeVisible();\n  });\n});\n```\n\n## Best Practices\n\n### Always Use Faker for Test Data\n\n```typescript\n// ✅ GOOD: Use faker\nconst user = {\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n  age: faker.number.int({ min: 18, max: 100 }),\n};\n\n// ❌ BAD: Hardcoded data\nconst user = { id: '123', name: 'Test User' };\n```\n\n### Use Factories Instead of Static Mocks\n\n**Use Fishery** for creating test data factories with sensible defaults and overrides.\n\n**CRITICAL — UI factories live in `redisinsight/ui/src/mocks/factories/`**, organized by\ndomain (`<domain>/<TypeName>.factory.ts`), exported as `<TypeName>Factory`. Before writing\ntest data in a UI spec, **check for an existing factory there and reuse it**; if none fits,\n**add a new factory file in that directory** rather than inlining mocks in the spec.\n\n```typescript\n// ✅ GOOD: reuse the shared factory (redisinsight/ui/src/mocks/factories/database/DBInstance.factory.ts)\nimport { DBInstanceFactory } from 'uiSrc/mocks/factories/database/DBInstance.factory';\n\nconst instance = DBInstanceFactory.build();\nconst cluster = DBInstanceFactory.build({\n  connectionType: ConnectionType.Cluster,\n});\nconst many = DBInstanceFactory.buildList(5);\n\n// ✅ GOOD: a new factory file, when none exists yet\n// redisinsight/ui/src/mocks/factories/<domain>/<TypeName>.factory.ts\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nconst userFactory = Factory.define<User>(({ sequence }) => ({\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n  age: faker.number.int({ min: 18, max: 100 }),\n}));\n\n// Usage - flexible and reusable\nconst user1 = userFactory.build();\nconst user2 = userFactory.build({ age: 25 });\nconst user3 = userFactory.build({ name: 'Specific Name' });\nconst users = userFactory.buildList(5); // Create multiple\n\n// ❌ BAD: Static mock objects\nconst mockUser1 = {\n  id: '123',\n  name: 'User 1',\n  email: 'user1@test.com',\n  age: 30,\n};\n```\n\nBenefits of Fishery factories:\n\n- Easy to override specific properties per test\n- Consistent default values across tests\n- Single source of truth for mock structure\n- Better maintainability when types change\n- Built-in support for sequences and traits\n\n### Never Use Fixed Timeouts\n\n```typescript\n// ❌ BAD: Fixed timeout\nawait new Promise((resolve) => setTimeout(resolve, 1000));\nawait page.waitForTimeout(2000);\n\n// ✅ GOOD: Wait for condition\nawait waitFor(() => {\n  expect(element).toBeInTheDocument();\n});\n\nawait page.waitForSelector('[data-test=\"result\"]');\n```\n\n### Mock External Dependencies\n\n```typescript\n// ✅ GOOD: Mock services\njest.mock('uiSrc/services/api', () => ({\n  apiService: {\n    get: jest.fn(),\n    post: jest.fn(),\n  },\n}));\n```\n\n### Parameterized Tests with `it.each`\n\n**Use `it.each` for multiple tests with the same body but different inputs:**\n\n```typescript\n// ✅ GOOD: Parameterized tests\nit.each([\n  { description: 'null', value: null },\n  { description: 'undefined', value: undefined },\n  { description: 'empty string', value: '' },\n  { description: 'whitespace only', value: '   ' },\n])('should return error when input is $description', async ({ value }) => {\n  const result = await service.processInput(value);\n  expect(result.status).toBe('error');\n});\n```\n\n**Benefits:**\n\n- DRY: Single test body shared across all cases\n- Maintainability: Changes to test logic only need to be made once\n- Readability: Test cases are clearly defined in a table\n- Easier to extend: Adding new test cases is just adding a new row\n\n### Test Edge Cases\n\nAlways test:\n\n- Empty arrays/objects\n- Null/undefined values\n- Error scenarios\n- Boundary conditions\n- Loading states\n\n## Testing Checklist\n\n- [ ] All new features have tests\n- [ ] Tests use faker for data generation\n- [ ] No fixed timeouts (use waitFor)\n- [ ] Tests follow AAA pattern\n- [ ] Descriptive test names\n- [ ] Shared `renderComponent` helper used\n- [ ] Default props defined\n- [ ] Edge cases covered\n- [ ] Error scenarios tested\n- [ ] Mocks cleaned up between tests\n- [ ] Integration tests for API endpoints\n- [ ] E2E tests for critical flows\n- [ ] Coverage meets thresholds (80%+)\n",".ai/skills/tsconfigs/SKILL.md":"---\nname: tsconfigs\ndescription: >-\n  Locate and modify TypeScript configuration in RedisInsight. Use when adding\n  path aliases, introducing a new TS area, debugging webpack/ts-node/ESLint\n  path resolution, or the user asks about tsconfig, path mappings, or where\n  TypeScript is configured.\n---\n\n# TypeScript Configuration\n\nRedisInsight has no root `tsconfig.json`. Config is split per area, each owning its own paths, includes, and consumers. ESLint uses `parserOptions.project: true` to auto-discover the nearest tsconfig for each linted file.\n\n## Layout\n\n| File | Owns | Consumers |\n| - | - | - |\n| `redisinsight/ui/tsconfig.json` | UI source, `uiSrc/*`, `apiClient` paths | Vite (UI build), ESLint UI override, `npm run type-check:ui` |\n| `redisinsight/api/tsconfig.json` | API source, `src/*`, `tests/*` paths | NestJS build, ESLint API override |\n| `redisinsight/api/tsconfig.check.json` | Same as base + `strict: true` (with `strictPropertyInitialization` and `useUnknownInCatchVariables` off) and `noEmit: true` | `npm run type-check:api` only — kept separate so strict mode doesn't break `nest build`. See the `type-check-baselines` skill. |\n| `redisinsight/desktop/tsconfig.json` | Desktop source. Paths `desktopSrc/*`, `apiSrc/*`, `uiSrc/*`, `apiClient`, `apiClient/*` for TypeScript / IDE intellisense | ESLint for desktop files, TS language server |\n| `configs/tsconfig.json` | Compiler options (`module: CommonJS`, `esModuleInterop`) used by `ts-node` to load the `.ts` webpack configs | `ts-node` via `TS_NODE_PROJECT` set in `build:main` / `build:main:stage` / `build:stage` |\n| `.storybook/tsconfig.json` | Storybook framework files, extends UI tsconfig | Storybook + ESLint |\n| `stories/tsconfig.json` | Story files. Extends UI tsconfig; rewires `uiSrc/*` to resolve from `redisinsight/ui` | Storybook + ESLint |\n| `tests/e2e-playwright/tsconfig.json` | Playwright E2E suite | E2E runner (sub-project with own package.json, eslint-ignored at root) |\n\n## Webpack path resolution (`configs/webpack.config.base.ts`)\n\nWebpack uses explicit `resolve.alias` entries (not `tsconfig-paths-webpack-plugin`):\n\n```ts\nalias: {\n  desktopSrc: webpackPaths.desktopSrcPath,\n  apiSrc: resolve(webpackPaths.apiPath, 'src'),\n  uiSrc: webpackPaths.uiSrcPath,\n  apiClient: resolve(webpackPaths.riPath, 'api-client'),\n  // ...\n}\n```\n\nIf you add a new path alias used by desktop code, add it in **both** places: `redisinsight/desktop/tsconfig.json` (for TypeScript / ESLint / IDE) and `configs/webpack.config.base.ts` `resolve.alias` (for the electron bundle).\n\n## ts-node and webpack TS configs\n\nThe webpack configs in `configs/` are `.ts` files with type annotations. When `webpack --config ./configs/*.ts` runs, `ts-node` compiles them. Three package.json scripts set `TS_NODE_PROJECT=./configs/tsconfig.json` so ts-node uses the right compiler options:\n\n- `build:main`\n- `build:main:stage`\n- `build:stage`\n\n`configs/tsconfig.json` deliberately does not set `allowJs`. Any `.js` file inside `scripts/` or `configs/` must be CommonJS (`require` / `module.exports`) — Node 22's module syntax detection will treat ESM-syntax `.js` files as ESM and break references to `__dirname`. Two scripts converted for this reason: `scripts/prebuild.js`, `scripts/DeleteSourceMaps.js`.\n\n## ESLint\n\n`.eslintrc.js`:\n\n- Root `parserOptions.project: true` — auto-discovers the nearest tsconfig per file.\n- API override: `parserOptions.project: redisinsight/api/tsconfig.json` (explicit).\n- UI override: `parserOptions.project: redisinsight/ui/tsconfig.json` (explicit).\n- Files in `.storybook/`, `.github/`, etc. are not linted by `eslint .` because ESLint skips dot-prefixed paths during directory expansion.\n\nIf you add a top-level TS folder that ESLint will reach, drop a tsconfig in it or `parserOptions.project: true` will fail to find one.\n\n## Common tasks\n\n### Adding a path alias used by desktop / electron\n\n1. Add the alias to `redisinsight/desktop/tsconfig.json` `compilerOptions.paths`.\n2. If the alias is also used by UI code, add it to `redisinsight/ui/tsconfig.json`.\n3. If the alias is also used by API code, add it to `redisinsight/api/tsconfig.json`.\n4. For aliases used by the **electron bundle** (anything imported from `redisinsight/desktop/**`), also mirror the alias into `configs/webpack.config.base.ts` `resolve.alias`. Vite resolves UI aliases from `redisinsight/ui/tsconfig.json` automatically.\n\n### Adding a new top-level TS folder\n\nCreate a `tsconfig.json` in it. Bare minimum:\n\n```json\n{\n  \"extends\": \"../redisinsight/ui/tsconfig.json\",\n  \"include\": [\"**/*\"]\n}\n```\n\nWithout it, ESLint's `parserOptions.project: true` will error on those files.\n\n### Adding a new `.ts` webpack config under `configs/`\n\nNo action needed — `configs/tsconfig.json` includes `**/*`.\n\n### Adding a Node script that needs to run without ts-node\n\nPlace under `scripts/` as CommonJS (`require`/`module.exports`). Do not use ESM syntax — `configs/tsconfig.json` no longer has `allowJs` and Node 22 will load ESM-syntax `.js` files as ESM (no `__dirname`).\n",".ai/skills/type-check-baselines/SKILL.md":"---\nname: type-check-baselines\ndescription: >-\n  Run, refresh, or recover from RedisInsight's per-project TypeScript error\n  baselines (.tscheck.rec.json). Use when CI reports \"baseline is outdated\" or\n  \"more TS errors than previously recorded\", when the user mentions tscheck /\n  type-check / .tscheck.rec.json, when introducing or fixing TS errors, or when\n  reviewing PRs that touch baseline files.\n---\n\n# TS error baselines\n\nRedisInsight gates TypeScript errors per project via a one-way ratchet: current error counts are recorded in `.tscheck.rec.json` files, and CI fails if any (file × error-code) count increases. Counts can only go down.\n\n## Projects and commands\n\n| Project | tsconfig used | Baseline file | Per-project compare |\n| - | - | - | - |\n| UI | `redisinsight/ui/tsconfig.json` | `redisinsight/ui/.tscheck.rec.json` | `npm run type-check --prefix redisinsight/ui` |\n| API | `redisinsight/api/tsconfig.check.json` (strict, extends base) | `redisinsight/api/.tscheck.rec.json` | `npm run type-check --prefix redisinsight/api` |\n| Desktop | `redisinsight/desktop/tsconfig.json` | `redisinsight/desktop/.tscheck.rec.json` | `npm run type-check --prefix redisinsight/desktop` |\n| Configs | `configs/tsconfig.json` | — (must stay at 0 errors) | `npx tsc --project configs/tsconfig.json --noEmit` |\n\nRun all checks together from the repo root:\n\n- `npm run type-check` — compare against baselines (all four projects). E2E Playwright is type-checked by a separate workflow (`tests-e2e-playwright-lint.yml`) — not part of this.\n- `npm run tscheck` — refresh baselines for ui/api/desktop after fixing errors. Projects whose error count didn't change produce no diff.\n- `npm run tscheck:force` — force-overwrite baselines for ui/api/desktop. Emergencies only.\n\n**Always run refresh commands through the root `npm run tscheck` / `npm run tscheck:force` wrappers.** The per-workspace refresh scripts (`npm run tscheck --prefix redisinsight/<ws>`) shell out to `tsc`, `tsx`, and `tsc-output-parser`, which are installed only in the **root** `node_modules/.bin/` — this repo is not an npm workspace, so `npm run --prefix` only exposes the sub-dir's bin, not the root's. The root wrappers exist precisely to avoid that trap by running in the root context first. If you must invoke the per-package script directly, prepend the root bin dir manually: `PATH=\"$PWD/node_modules/.bin:$PATH\" npm run tscheck --prefix redisinsight/ui`.\n\n## API has a dedicated check tsconfig\n\n`redisinsight/api/tsconfig.check.json` extends the base `tsconfig.json` and adds:\n\n```json\n\"strict\": true,\n\"strictPropertyInitialization\": false,\n\"useUnknownInCatchVariables\": false,\n\"noEmit\": true\n```\n\nThe base `tsconfig.json` stays as-is so `nest build` is unaffected. **Never enable `strict` in the base** — it breaks the production build.\n\n## Workflows\n\n### CI says \"more TS errors than previously recorded\"\n\nYou introduced new errors. Fix them. Read the script output — it lists the file, error code (TSxxxx), and message for each new error. Do **not** run `tscheck:force` in any workspace to paper over them.\n\n### CI says \"baseline is outdated\"\n\nYou fixed errors (good). Refresh baselines from the repo root:\n\n```sh\nnpm run tscheck\n```\n\nThis runs the refresh for ui, api, and desktop; only the project whose count changed will produce a diff. Commit the updated `.tscheck.rec.json`.\n\n### Adding a brand-new file with TS errors\n\nSame rule: the file × error-code counts went from 0 to N — that's \"new errors\". Fix them before merging. Strict-mode escape hatches (`as any`, `// @ts-expect-error` with justification) are acceptable when fixing legitimately is out of scope, but prefer real fixes.\n\n### Bootstrapping a fresh baseline\n\nOnly needed once per project (already done for ui/api/desktop). The non-force `npm run tscheck --prefix redisinsight/<workspace>` calls `compare` first, which fails against an empty baseline. Use `npm run tscheck:force --prefix redisinsight/<workspace>` for the very first baseline only.\n\n### After `npm install` in `redisinsight/api/`\n\nThe api postinstall regenerates `redisinsight/api-client/`. That can shift UI and Desktop error counts (they both import from `apiClient`). If `npm run type-check:ui` or `npm run type-check:desktop` reports drift after an api install, refresh those baselines.\n\n### Local UI check disagrees with CI\n\nUI plugins under `redisinsight/ui/src/packages/{redisearch, redisgraph, redistimeseries-app, ri-explain, clients-list}` are sub-projects whose source gets type-checked via the UI tsconfig. Their deps live in nested `node_modules` populated by `npm run build:statics` (or by running `npm install --prefix redisinsight/ui/src/packages/<plugin>`). CI runs `npm run build:statics` before `npm run type-check:ui`, so the baseline reflects \"plugin deps installed.\"\n\nIf `npm run type-check:ui` shows TS7016 (\"Could not find a declaration file for module ...\") errors that CI doesn't, you're missing plugin deps. Run `npm run build:statics` once, then re-run the check. Don't refresh the baseline to your local state — CI runs with plugin deps installed.\n\n### Local Desktop check disagrees with CI\n\nDesktop type-check needs `redisinsight/api/dist/` populated with the **dev** nest build (`npm run build --prefix redisinsight/api`, not `build:prod` — prod skips `.d.ts` emission). CI does this automatically. Locally, build api once before generating or refreshing the desktop baseline.\n\n## Reviewing PRs\n\nReject PRs that:\n\n- Bump a `.tscheck.rec.json` file × code count **up** without a corresponding code fix.\n- Use `tscheck:force` in any workspace (look for the diff: a force overwrite typically touches many lines in the rec file with no related TS changes).\n- Enable `strict` in `redisinsight/api/tsconfig.json` (the base). Strict for api belongs only in `tsconfig.check.json`.\n\nApprove PRs that:\n\n- Leave counts unchanged.\n- Decrease counts (with the rec file updated and committed).\n",".cursor/agents/test-runner.md":"---\nname: test-runner\nmodel: composer-1.5\ndescription: Test automation expert. Use proactively to run tests and fix failures.\n---\n\nYou are a test automation expert.\n\nWhen you see code changes, proactively run appropriate tests.\n\nWhen completing a major task, run all tests to ensure the task is complete.\n\nIf tests fail:\n\n1. Analyze the failure output\n2. Identify the root cause\n3. Fix the issue while preserving test intent\n4. Re-run to verify\n\nReport test results with:\n\n- Number of tests passed/failed\n- Summary of any failures\n- Changes made to fix issues\n",".cursor/mcp.json":"../mcp.json",".cursor/skills/branches/SKILL.md":"../../../.ai/skills/branches/SKILL.md",".cursor/skills/commits/SKILL.md":"../../../.ai/skills/commits/SKILL.md",".cursor/skills/feature-flags/SKILL.md":"../../../.ai/skills/feature-flags/SKILL.md",".cursor/skills/pull-requests/SKILL.md":"../../../.ai/skills/pull-requests/SKILL.md",".github/copilot-instructions.md":"# GitHub Copilot Instructions for RedisInsight\n\n> **🎯 Start here: Read `AGENTS.md` at the repository root for essential commands, testing instructions, and quick reference**\n\nThis project uses a centralized AI rules structure:\n\n- **`AGENTS.md`** (repository root) - Entry point with commands, testing, and boundaries\n- **`.ai/skills/`** - Detailed development standards as skill files (one folder per topic, each with a `SKILL.md`)\n- **`.ai/commands/`** - AI workflow commands and templates\n\n## 📂 Skills Structure\n\n### Core Development Skills\n\n- **Code Quality**: `.ai/skills/code-quality/SKILL.md`\n\n  - TypeScript best practices\n  - Import organization\n  - SonarJS complexity rules\n\n- **Frontend Development**: `.ai/skills/frontend/SKILL.md`\n\n  - React 18 patterns and best practices\n  - Redux Toolkit state management\n  - Styled-components (SCSS deprecated)\n  - Component folder structure\n  - Internal UI component wrappers (never import from @redis-ui directly)\n  - Elastic UI deprecation (use Redis UI wrappers)\n\n- **Backend Development**: `.ai/skills/backend/SKILL.md`\n\n  - NestJS module architecture\n  - Service and controller patterns\n  - DTOs and validation\n  - Error handling\n  - Redis integration patterns\n\n- **Testing Standards**: `.ai/skills/testing/SKILL.md`\n\n  - Jest and Testing Library patterns\n  - Component testing with renderComponent helper\n  - Faker for test data generation\n  - No fixed timeouts (use waitFor)\n  - Backend testing with NestJS\n\n- **E2E Testing**: `.ai/skills/e2e-testing/SKILL.md`\n\n  - Playwright standards, page object models, fixtures\n\n- **Git Safety**: `.ai/skills/git-safety/SKILL.md`\n\n  - Protected-branch guardrails (no direct commits to main/latest/release)\n\n- **Commit Messages**: `.ai/skills/commits/SKILL.md`\n\n  - Commit message format (Conventional Commits)\n\n- **Pull Requests**: `.ai/skills/pull-requests/SKILL.md`\n  - PR process and review guidelines\n  - Pre-commit checklist\n\n### Commands and Workflows\n\n- **PR Plan**: `.ai/commands/pr-plan.md` - Analyze JIRA tickets (RI-XXX) and create detailed implementation plans\n- **Commit Message Generation**: `.ai/commands/commit-message.md` - Generate commit messages following Conventional Commits\n- **PR Review**: `.ai/commands/pull-request-review.md` - Review pull requests and provide feedback\n\n## 🎯 Project Overview\n\n**Tech Stack:**\n\n- Frontend: React 18, TypeScript, Redux Toolkit, styled-components, Vite\n- Backend: NestJS, TypeScript, Node.js\n- Desktop: Electron\n- Testing: Jest, Testing Library, Playwright\n\n**Module Aliases:**\n\n- `uiSrc/*` → `redisinsight/ui/src/*`\n- `apiClient` → `redisinsight/api-client` (auto-generated OpenAPI types consumed by the UI)\n- `desktopSrc/*` → `redisinsight/desktop/src/*`\n\n## 📖 Additional Documentation\n\n- **For AI agents**: Start with `AGENTS.md` at repository root\n- **For human developers**: See `.ai/README.md` for setup and overview\n\n---\n\n**Note**: This is a minimal reference file. GitHub Copilot cannot read the referenced files directly, but developers can access the full guidelines. Claude Code and Codex read `AGENTS.md` directly; Claude Code additionally auto-discovers skills under `.ai/skills/` (Codex via the `.agents/skills` symlink).\n","AGENTS.md":"# RedisInsight AI Agent Instructions\n\nThis file provides essential context and instructions for AI coding agents working on RedisInsight.\n\n## Project Overview\n\n**RedisInsight** is a desktop application for Redis database management built with:\n\n- **Frontend**: React 18, TypeScript, Redux Toolkit, styled-components, Monaco Editor, Vite\n- **Backend**: NestJS, TypeScript, Node.js\n- **Desktop**: Electron for cross-platform distribution\n- **Testing**: Jest, Testing Library, Playwright\n\n**Architecture**:\n\n```\nredisinsight/\n├── ui/          # React frontend (Vite + TypeScript)\n├── api/         # NestJS backend (TypeScript)\n├── desktop/     # Electron main process\n└── tests/       # E2E tests (Playwright)\n```\n\n## Setup Commands\n\n### Development\n\n```bash\n# Frontend development (web)\nnpm run dev:ui\n\n# Backend development\nnpm run dev:api\n\n# Desktop app development (runs all: API + UI + Electron)\nnpm run dev:desktop\n\n# Frontend with coverage\nnpm run dev:ui:coverage\n```\n\n## Testing Instructions\n\n### Run Tests\n\n```bash\n# Frontend tests\nnpm test               # Run all UI tests\n\n# Backend tests\nnpm run test:api       # Run all API tests\n\n# E2E tests\nnpm test --prefix tests/e2e-playwright\n```\n\n### Run Specific Frontend Tests\n\n```bash\n# Run a specific test file\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs'\n\n# Run a specific test by name (use -t flag)\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs' -t 'test name pattern'\n\n# Example:\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/slices/tests/browser/keys.spec.ts' -c 'jest.config.cjs' -t 'refreshKeyInfoAction'\n```\n\n### Before Committing\n\n**ALWAYS run these before committing:**\n\n```bash\n# Lint check\nnpm run lint           # All code\nnpm run lint:ui        # Frontend only\nnpm run lint:api       # Backend only\n\n# Type checking (compares against .tscheck.rec.json baselines for ui/api/desktop + configs)\nnpm run type-check\n\n# Refresh baselines after intentionally adding or fixing TS errors (do not run casually)\nnpm run tscheck\n\n# Tests\nnpm test               # Frontend tests\nnpm run test:api       # Backend tests\n```\n\n`npm run type-check` is the gate — CI fails if any (file × error-code) TS-error count increases. If you intentionally changed TS-error counts, run `npm run tscheck` to refresh the baselines and commit the updated `.tscheck.rec.json` files. See `.ai/skills/type-check-baselines/SKILL.md` for details (including the `npm run tscheck:force` escape hatch).\n\n**Fix any linting errors, type errors, or test failures before committing.**\n\n## Always-On Rules\n\nThese apply to every change in the repo. Skill files contain the full detail; the essentials are listed here so they're never missed.\n\n### Code quality (always)\n\n- Run `npm run lint` and `npm run type-check` before committing — both must pass.\n- TypeScript everywhere. Avoid `any`; use `unknown` if you must.\n- Naming: `PascalCase` components, `camelCase` functions/variables, `UPPER_SNAKE_CASE` constants, `is/has/should` prefix for booleans.\n- No `console.log` in production code (use `console.warn`/`error`).\n- Imports: external → built-ins → internal aliases (`uiSrc/*`, `apiClient`, `desktopSrc/*`) → relative → styles last. UI must not import from backend directly — use `apiClient`.\n- No magic numbers; extract duplicated strings to constants.\n\n### Git safety (always)\n\n- **Never commit, push, or force-push directly to `main`, `latest`, or `release/*`.** Always create a feature branch first.\n- Verify current branch with `git branch --show-current` before any push.\n- All changes go through pull requests.\n\n### Dependency / lockfile management (always)\n\n- After modifying any `package.json` (root, `redisinsight/`, `redisinsight/api/`, or a `redisinsight/ui/src/packages/*` plugin), run `npm install` from that directory and commit the resulting `package-lock.json` changes.\n- Never edit `package-lock.json` files by hand and never run `npm install --ignore-scripts` (or otherwise skip `postinstall`) when preparing a commit — the `postinstall` applies `patch-package` patches, and the lockfile shipped to CI must match what `npm install` produces locally.\n- CI runs `npm ci`, which installs strictly from `package-lock.json` and fails if it is out of sync with `package.json`. A green local install in every changed package's directory is required before pushing.\n- Use the right package manager for the change: `npm install <pkg>` / `npm uninstall <pkg>` (or `npm update`) for dependency changes, never manual edits to `package.json` versions without re-running install.\n\n## Skills\n\nAll detailed development standards are exposed as skills under `.ai/skills/`. Claude Code auto-discovers them; each skill triggers when its description matches the task.\n\n- **Code Quality**: `.ai/skills/code-quality/SKILL.md` - Linting, TypeScript standards, naming, import order\n- **Frontend**: `.ai/skills/frontend/SKILL.md` - React, Redux, UI patterns, styled-components\n- **i18n**: `.ai/skills/i18n/SKILL.md` - i18next translations, locale key conventions, translating backend errors and notifications\n- **Backend**: `.ai/skills/backend/SKILL.md` - NestJS, API patterns, dependency injection\n- **Testing**: `.ai/skills/testing/SKILL.md` - Jest/Testing Library standards, faker, test patterns\n- **E2E Testing**: `.ai/skills/e2e-testing/SKILL.md` - Playwright standards, page objects\n- **Git Safety**: `.ai/skills/git-safety/SKILL.md` - Protected-branch guardrails (detail)\n- **Branches**: `.ai/skills/branches/SKILL.md` - Branch naming conventions\n- **Commits**: `.ai/skills/commits/SKILL.md` - Commit message guidelines\n- **Pull Requests**: `.ai/skills/pull-requests/SKILL.md` - PR process and review guidelines\n- **Feature Flags**: `.ai/skills/feature-flags/SKILL.md` - Adding, promoting, and removing feature flags\n- **TS Error Baselines**: `.ai/skills/type-check-baselines/SKILL.md` - Running and refreshing TypeScript error baselines\n- **Redis UI Components**: `.ai/skills/redis-ui-components/SKILL.md` - Component API references, props, and usage examples (from `@redis-ui/components` package)\n- **RedisInsight Plugins**: `.ai/skills/redis-insight-plugin/SKILL.md` - Building, deploying, and validating Workbench visualization plugins (manifests, `activationMethod`, phased workflow, command parsing); internal plugins under `redisinsight/ui/src/packages/` must also follow the Frontend, Code Quality, Redis UI Components, and Testing skills\n\n**Refer to these files for comprehensive guidelines on each topic.**\n\n## Boundaries\n\n### ✅ Always Do\n\n- Ensure the current branch name follows `.ai/skills/branches/SKILL.md` before opening a PR; rename it if it doesn't\n- Write to `src/` and `tests/` directories\n- Run `npm run lint` and `npm test` before commits\n- Follow naming conventions (camelCase, PascalCase, UPPER_SNAKE_CASE)\n- Use faker library for test data generation\n- Use `renderComponent` helper in component tests\n- Extract duplicate strings to constants\n- Use semantic colors from theme (not CSS variables)\n- Use layout components (Row/Col/FlexGroup) instead of div\n- Pass layout props as component props (not hardcoded in styles)\n- Consult the `redis-ui-components` skill (`.ai/skills/redis-ui-components/`) for component APIs when writing frontend code\n- Consult the `redis-insight-plugin` skill (`.ai/skills/redis-insight-plugin/`) when creating or modifying anything under `redisinsight/ui/src/packages/**` or any plugin manifest with a `visualizations` field\n\n### ⚠️ Ask First\n\n- Database schema changes\n- Adding new dependencies\n- Modifying CI/CD configuration (`.github/workflows/`)\n- Changes to build configuration\n- Breaking changes to APIs\n\n### 🚫 Never Do\n\n- Commit secrets or API keys\n- Edit `node_modules/` or `vendor/` directories\n- Edit `package-lock.json` by hand or commit a lockfile produced with `--ignore-scripts` / a skipped `postinstall`\n- Use fixed time waits in tests (use `waitFor` instead)\n- Use `!important` in styled-components\n- Import directly from `@redis-ui/*` (use internal wrappers from `uiSrc/components/ui`)\n- Use Elastic UI for new code (migrating to Redis UI)\n- Use hardcoded pixel values (use theme spacing)\n- Use `any` type without reason\n","CLAUDE.md":"AGENTS.md","docs/plugins/development.md":"# Plugin development\n\nThis document describes the guides to develop your own plugin for the Redis Insight Workbench.\n\n## How it works\n\nPlugin visualization in the Workbench is rendered using Iframe to encapsulate plugin scripts and styles, described in\nthe main plugin script and the stylesheet (if it has been specified in the `package.json`),\niframe includes basic styles as well.\n\n## Plugin structure\n\nEach plugin should have a unique name with all its files [loaded](installation.md) to\na separate folder inside the default `plugins` folder.\n\n> Default plugins are located inside the application.\n\n### Files\n\n`package.json` should be located in the root folder of your plugins, all other files can be included into a subfolder.\n\n- **pluginName/package.json** _(required)_ - Manifest of the plugin\n- **pluginName/{anyName}.js** _(required)_ - Core script of the plugin\n- **pluginName/{anyName}.css** _(optional)_ - File with styles for the plugin visualizations\n- **pluginName/{anyFileOrFolder}** _(optional)_ - Specify any other file or folder inside the plugin folder\n  to use by the core module script. _For example_: pluginName/images/image.png.\n\n## `package.json` structure\n\nThis is the required manifest to use the plugin. `package.json` file should include\nthe following **required** fields:\n\n<table>\n  <tr>\n    <td><i>name</i></td>\n    <td>Plugin name. It is recommended to use the folder name as the plugin name in the package.json.</td>\n  </tr>\n  <tr>\n    <td><i>main</i></td>\n    <td>Relative path to the core script of the plugin. <i>Example: </i> \"./dist/index.js\"</td>\n  </tr>\n  <tr>\n    <td><i>visualizations</i></td>\n    <td>\n      Array of visualizations (objects) to visualize the results in the Workbench.\n      <br><br>\n      Required fields in visualizations:\n      <ul>\n        <li><strong><i>id</i></strong> - visualization id</li>\n        <li><strong><i>name</i></strong> - visualization name to display in the Workbench</li>\n        <li><strong><i>activationMethod</i></strong> - name of the exported function to call when \nthis visualization is selected in the Workbench</li>\n        <li>\n          <strong><i>matchCommands</i></strong> - array of commands to use the visualization for. Supports regex string. \n          <i>Example: </i> [\"CLIENT LIST\", \"FT.*\"]\n        </li>\n      </ul>\n    </td>\n  </tr>\n</table>\n\nYou can specify the path to a css file in the `styles` field. If specified,\nthis file will be included inside the iframe plugin.\n\nSimple example of the `package.json` file with required and optional fields:\n\n```json\n{\n  \"author\": {\n    \"name\": \"Redis Ltd.\",\n    \"email\": \"support@redis.com\",\n    \"url\": \"https://redis.com/redis-enterprise/redis-insight\"\n  },\n  \"description\": \"Show client list as table\",\n  \"styles\": \"./dist/styles.css\",\n  \"main\": \"./dist/index.js\",\n  \"name\": \"client-list\",\n  \"version\": \"0.0.1\",\n  \"scripts\": {},\n  \"visualizations\": [\n    {\n      \"id\": \"clients-list\",\n      \"name\": \"Table\",\n      \"activationMethod\": \"renderClientsList\",\n      \"matchCommands\": [\"CLIENT LIST\"],\n      \"description\": \"Example of client list plugin\",\n      \"default\": true\n    }\n  ],\n  \"devDependencies\": {},\n  \"dependencies\": {}\n}\n```\n\n## Core script of the plugin\n\nThis is the required script with defined visualization methods.\nThe core script contains function and its export (functions - for multiple visualizations),\nwhich is run after the relevant visualization is selected in the Workbench.\n\nThe following function receives props of the executed commands:\n\n```typescript\ninterface Props {\n  command: string; // executed command\n  data: Result[]; // array of results (one item for Standalone)\n}\n\ninterface Result {\n  response: any; // response of the executed command\n  status: 'success' | 'fail'; // response status of the executed command\n}\n\nconst renderVisualization = (props: Props) => {\n  // Do your magic\n};\n\nexport default { renderVisualization };\n```\n\nEach plugin iframe has basic styles of Redis Insight application, including fonts and color schemes.\n\nIt is recommended to use the React & [Elastic UI library](https://elastic.github.io/eui/#/) for\nconsistency with plugin visualisations and the entire application.\n\nFind the example of the plugin here.\n\n- [Client List Plugin README](https://github.com/RedisInsight/Packages/blob/main/clients-list-example/README.md)\n- [Client List Plugin dir](https://github.com/RedisInsight/Packages/blob/main/clients-list-example/)\n\n### Available parameters\n\nAdditional information provided to the plugin iframe is included in the `window.state`\ninside of the plugin script.\n\n```javascript\nconst { config, modules } = window.state;\nconst { baseUrl, appVersion } = config;\n\n// modules - the list of modules of the current database\n// baseUrl - url for your plugin folder - can be used to include your assets\n// appVersion - version of the Redis Insight application\n```\n\n### Plugin rendering\n\nTo render the plugin visualization, the iframe with basic html is generated which is\nthen populated with relevant scripts and styles. To render the html data, use existing\nDOM Element `#app` or create your own DOM Elements.\nRendered iframe also includes `theme_DARK` or `theme_LIGHT` className on `body` to indicate the application theme used.\n\n_Javascript Example:_\n\n```javascript\nconst renderVisualization = (props) => {\n  const { command, data = [] } = props;\n  const [{ result, status }] = data;\n  document.getElementById('app').innerHTML = `\n        <h3>Executed command:<h3>\n        <p>${command}</p>\n        <h4>Result of the command</h4>\n        <p>${result}</p>\n        <h4>Status of the command</h4>\n        <p>${status}</p>\n      `;\n};\n\nexport default { renderVisualization };\n```\n\n_React Example:_\n\n```javascript\nimport { render } from 'react-dom';\nimport App from './App';\n\nconst renderVisualization = (props) => {\n  const { command, data = [] } = props;\n  const [{ result, status }] = data;\n  render(\n    <App command={command} response={result} status={status} />,\n    document.getElementById('app'),\n  );\n};\n\n// This is a required action - export the main function for execution of the visualization\nexport default { renderVisualization };\n```\n\n## Plugin communication\n\nUse the [redisinsight-plugin-sdk](https://www.npmjs.com/package/redisinsight-plugin-sdk), which is a third party library,\nto communicate with the main app.\n\nFind the list and\ndescription of methods called in the\n[README.md](../../redisinsight/ui/src/packages/redisinsight-plugin-sdk/README.md).\n","docs/plugins/installation.md":"# Plugin installation & Usage\n\nThis document describes the guides to add `plugins` for the Workbench to Redis Insight.\n\n## Installation guide\n\n**Note**: While adding new plugins for Workbench, use files only from trusted\nauthors to avoid automatic execution of malicious code.\n\n1. Download the plugin for the Workbench.\n2. Open the `plugins` folder with the following path\n   - For MacOs: `<usersHomeDir>/.redis-insight/plugins`\n   - For Windows: `C:/Users/{Username}/.redis-insight/plugins`\n   - For Linux: `<usersHomeDir>/.redis-insight/plugins`\n3. Add the folder with plugin to the `plugins` folder\n\nTo see the uploaded plugin visualizations in the command results, reload the Workbench\npage and run Redis command relevant for this visualization.\n\n## Usage\n\nThe plugin may contain different visualizations for any Redis commands.\nBelow you can find a guide to see command results in the uploaded plugin visualization:\n\n1. Open Redis Insight\n2. Open a database added\n3. Open the Workbench\n4. Run the Redis command relevant for the plugin visualization\n5. Select the plugin visualization to display results in (if this visualization has not been set by default)\n","docs/plugins/introduction.md":"# Introduction to plugins for the Workbench\n\n## Introduction\n\nRedis can hold a range of different data types. Visualizing these in a\nformat that’s convenient to you for validation and debugging is paramount.\nYou can now easily extend the core functionality of Redis Insight independently by\nbuilding your own custom visualization plugin.\n\nData visualization provided by the plugin is rendered within the\nWorkbench results area and is based on the executed command, ie. a certain\nRedis command can generate its own custom data visualization.\n\nWe have included the following [plugin package example](https://github.com/RedisInsight/RedisInsight/tree/main/redisinsight/ui/src/packages/clients-list) for your reference: running the CLIENT LIST command presents the output in a tabular format for easier reading.\n\n## Wiki\n\n- [Installation and Usage](installation.md)\n- [Plugin Development](development.md)\n","redisinsight/ui/src/packages/clients-list/README.md":"# Plugin for the “Client List” command\n\nThe plugin has been created using React, TypeScript, and [Elastic UI](https://elastic.github.io/eui/#/).\n[Parcel](https://parceljs.org/) is used to build the plugin.\n\n## Running locally\n\nThe following commands will install dependencies and start the server to run the plugin locally:\n\n```\nnpm install\nnpm start\n```\n\nThese commands will install dependencies and start the server.\n\n_Note_: Base styles are included to `index.html`\nfrom [RedisInsight](https://github.com/RedisInsight/RedisInsight) repository.\n\n_From Redis Insight Repo_:\nThis command will generate the `vendor` folder with styles and fonts of the core app. Add this folder\ninside the folder for your plugin and include appropriate styles to the `index.html` file.\n\n```\nnpm install\nnpm install --prefix redisinsight/api\n\nnpm run build:statics - for Linux or MacOs\nnpm run build:statics:win - for Windows\n```\n\n## Build plugin\n\nThe following commands will build plugins to be used in Redis Insight:\n\n```\nnpm install\nnpm run build\n```\n\n[Add](https://github.com/RedisInsight/RedisInsight/blob/main/docs/plugins/installation.md) the package.json file and the\n`dist` folder to the folder with your plugin, which should be located in the `plugins` folder.\n","redisinsight/ui/src/packages/geodata/README.md":"# Geodata Workbench Plugin\n\nInternal RedisInsight Workbench plugin for Redis GEO commands.\n\n## Visualizations\n\n- `Geospatial map` plots coordinate-bearing search results for `GEOSEARCH`, `GEORADIUS`, and `GEORADIUSBYMEMBER`.\n- `Geospatial heatmap` renders the same coordinate-bearing search results as density.\n- `Geospatial details` summarizes every Redis GEO command family, including write and store commands.\n\nThe map views require `WITHCOORD` in command output. The inspector can render scalar, integer, hash, position, search, and search-store responses without coordinates.\n\n`Geospatial map` is the default visualization for coordinate search commands that include `WITHCOORD`. `Geospatial details` is the default for scalar, store, and non-coordinate GEO search commands, and remains selectable for coordinate search commands.\n\n## Tile Policy\n\nThe plugin loads OSM tiles from the runtime default config; fallback renders unavailable state.\n\nFor an official upstream PR, maintainers may still require the default tile provider to be disabled or replaced by a Redis-approved tile backend.\n\n## Development\n\n```sh\nnpm install --prefix redisinsight/ui/src/packages/geodata\nnpm test --prefix redisinsight/ui/src/packages/geodata\nnpm run build --prefix redisinsight/ui/src/packages\n```\n","redisinsight/ui/src/packages/redisgraph/README.md":"# RedisGraph Plugin for Redis Insight v2\n\nThe example has been created using React, TypeScript, and [Elastic UI](https://elastic.github.io/eui/#/).\n[Parcel](https://parceljs.org/) is used to build the plugin.\n\n## Running locally\n\nThe following commands will install dependencies and start the server to run the plugin locally:\n\n```\nnpm install\nnpm start\n```\n\nThese commands will install dependencies and start the server.\n\n_Note_: Base styles are included to `index.html` from the repository.\n\nThis command will generate the `vendor` folder with styles and fonts of the core app. Add this folder\ninside the folder for your plugin and include appropriate styles to the `index.html` file.\n\n```\nnpm run build:statics - for Linux or MacOs\nnpm run build:statics:win - for Windows\n```\n\n## Build plugin\n\nThe following commands will build plugins to be used in Redis Insight:\n\n```\nnpm install\nnpm run build\n```\n\n[Add](../../../../../docs/plugins/installation.md) the package.json file and the\n`dist` folder to the folder with your plugin, which should be located in the `plugins` folder.\n","redisinsight/ui/src/packages/redisinsight-plugin-sdk/README.md":"# RedisInsight-plugin-sdk\n\nThe high-level API for communication between Redis Insight\nplugin and Redis Insight application.\n\n## Usage\n\n```\nnpm install redisinsight-plugin-sdk\nor\nnpm install redisinsight-plugin-sdk\n```\n\n## Available methods\n\n### setHeaderText(text)\n\nSets any custom text to the header of the command result\n\n**Parameters:**\n\n- `text` **{String}**\n\n**Example:**\n\n```js\nimport { setHeaderText } from 'redisinsight-plugin-sdk';\nsetHeaderText('Matched: 10');\n```\n\n### executeRedisCommand(command)\n\nExecutes a Redis command _(currently, only read-only commands are supported)_.\n\n**Parameters:**\n\n- `command` **{String}** - command to execute\n\n**Returns:**\n\n- `Promise<[{ response, status }]>`\n\n```js\n/**\n * @async\n * @param {String} command\n * @returns {Promise.<[{ response, status }]>}\n * @throws {Error}\n */\n```\n\n**Example:**\n\n```js\nimport { executeRedisCommand } from 'redisinsight-plugin-sdk';\ntry {\n  const result = await executeRedisCommand('GET foo');\n  const [{ response, status }] = result;\n  if (status === 'success') {\n    // Do smth\n  }\n} catch (e) {\n  console.error(e);\n}\n```\n\n### getState()\n\nReturns saved state for the command visualization.\n\nThrow an error if the state has not been saved.\n\n**Parameters:**\n\n- `state` **{any}** - any data to save\n\n**Returns:**\n\n- `Promise<any>`\n\n```js\n/**\n * @async\n * @returns {Promise.<any>} state\n * @throws {Error}\n */\n```\n\n**Example:**\n\n```js\nimport { getState } from 'redisinsight-plugin-sdk';\ntry {\n  const result = await getState();\n} catch (e) {\n  console.error(e);\n}\n```\n\n### setState(state)\n\nSave the state for the command visualization.\n\n**Returns:**\n\n- `Promise<any>`\n\n```js\n/**\n * @async\n * @param {any} state\n * @returns {Promise.<any>} state\n * @throws {Error}\n */\n```\n\n**Example:**\n\n```js\nimport { setState } from 'redisinsight-plugin-sdk';\ntry {\n  await setState({ a: 1, b: 2 });\n} catch (e) {\n  console.error(e);\n}\n```\n\n### formatRedisReply(response, command)\n\nUtil function to parse Redis response\n\nReturns string with parsed cli-like response\n\n**Returns:**\n\n- `Promise<string>`\n\n```js\n/**\n * @async\n * @param {any} response\n * @param {String} command\n * @returns {Promise.<string>} data\n * @throws {Error}\n */\n```\n\n**Example:**\n\n```js\nimport { formatRedisReply } from 'redisinsight-plugin-sdk';\n\ntry {\n  const parsedReply = await formatRedisReply(data[0].response, command);\n\n  /*\n    parsedReply:\n    \n    1) 1) \"COUNT(a)\"\n    2) 1) 1) \"0\"\n    3) 1) \"Cached execution: 1\"\n       2) \"Query internal execution time: 3.134125 milliseconds\"\n   */\n} catch (e) {\n  console.error(e);\n}\n```\n","redisinsight/ui/src/packages/redistimeseries-app/README.md":"# RedisTimeseries Plugin for Redis Insight\n\nThe example has been created using React, TypeScript, and [Elastic UI](https://elastic.github.io/eui/#/).\n[Parcel](https://parceljs.org/) is used to build the plugin.\n\n## Running locally\n\nThe following commands will install dependencies and start the server to run the plugin locally:\n\n```\nnpm install\nnpm start\n```\n\nThese commands will install dependencies and start the server.\n\n_Note_: Base styles are included to `index.html` from the repository.\n\nThis command will generate the `vendor` folder with styles and fonts of the core app. Add this folder\ninside the folder for your plugin and include appropriate styles to the `index.html` file.\n\n```\nnpm run build:statics - for Linux or MacOs\nnpm run build:statics:win - for Windows\n```\n\n## Build plugin\n\nThe following commands will build plugins to be used in Redis Insight:\n\n```\nnpm install\nnpm run build\n```\n\n[Add](../../../../../docs/plugins/installation.md) the package.json file and the\n`dist` folder to the folder with your plugin, which should be located in the `plugins` folder.\n","redisinsight/ui/src/packages/ri-explain/README.md":"# RI-Explain plugin\n"},"files":{".ai/skills/backend/SKILL.md":"---\nname: backend\ndescription: >-\n  NestJS backend development patterns for the RedisInsight API:\n  module structure, services, controllers, DTOs, dependency injection,\n  and error handling. Use when editing any file under\n  redisinsight/api/**, writing or modifying NestJS modules,\n  controllers, services, DTOs, providers, or when the user mentions\n  the backend, API, or NestJS.\n---\n\n\n# Backend Development (NestJS/API)\n\n## Module Structure\n\n### NestJS Architecture\n\n- Follow **modular architecture** (feature-based modules)\n- Use **dependency injection** throughout\n- **Separate concerns**: Controllers, Services, Repositories\n- Use **DTOs** for validation and data transfer\n- Apply **proper error handling** with NestJS exceptions\n\n### Module Folder Structure\n\nEach feature module in its own directory under `api/src/`:\n\n```\nfeature/\n├── feature.module.ts           # Module definition\n├── feature.controller.ts       # REST endpoints\n├── feature.service.ts          # Business logic\n├── feature.service.spec.ts     # Service tests\n├── feature.controller.spec.ts  # Controller tests\n├── feature.types.ts            # Interfaces and types related to the feature\n├── dto/                        # Data transfer objects\n│   ├── create-feature.dto.ts\n│   ├── update-feature.dto.ts\n│   └── feature.dto.ts\n├── entities/                   # TypeORM entities\n├── repositories/               # Custom repositories\n├── exceptions/                 # Custom exceptions\n├── guards/                     # Feature-specific guards\n├── decorators/                 # Custom decorators\n└── constants/                  # Feature constants\n```\n\n### File Naming\n\n- **Modules**: `feature.module.ts`\n- **Controllers**: `feature.controller.ts`\n- **Services**: `feature.service.ts`\n- **DTOs**: `create-feature.dto.ts`, `update-feature.dto.ts`\n- **Entities**: `feature.entity.ts`\n- **Interfaces and types**: `feature.types.ts`\n- **Tests**: `feature.service.spec.ts`\n- **Constants**: `feature.constants.ts`\n- **Exceptions**: `feature-not-found.exception.ts`\n\n### Constants Organization\n\nStore feature-specific constants in dedicated constants file:\n\n```typescript\nexport const FEATURE_CONSTANTS = {\n  MAX_NAME_LENGTH: 100,\n  DEFAULT_PAGE_SIZE: 20,\n} as const;\n\nexport const FEATURE_ERROR_MESSAGES = {\n  NOT_FOUND: 'Feature not found',\n  INVALID_INPUT: 'Invalid feature data',\n} as const;\n```\n\n### Imports Order\n\n1. Node.js built-in modules\n2. External dependencies (`@nestjs/*`, etc.)\n3. Internal modules (using `src/*` alias — the backend's self-reference into `redisinsight/api/src/*`)\n4. Local relative imports\n\n## Service Layer\n\n### Service Pattern\n\n- Inject dependencies via constructor\n- Use TypeORM repositories\n- Handle errors with NestJS exceptions\n- Use Logger for important operations\n- Keep business logic in services (not controllers)\n\n### Dependency Injection\n\nAlways inject dependencies via constructor with proper decorators:\n\n```typescript\n@Injectable()\nexport class UserService {\n  constructor(\n    @InjectRepository(User)\n    private readonly userRepository: Repository<User>,\n    private readonly emailService: EmailService,\n  ) {}\n}\n```\n\n## Controller Layer\n\n### Controller Pattern\n\n- Keep controllers thin (delegate to services)\n- Use proper HTTP decorators (`@Get`, `@Post`, etc.)\n- Use `@Body`, `@Param`, `@Query` for inputs\n- Apply guards with `@UseGuards()`\n- Document with Swagger decorators\n\n### HTTP Status Codes\n\n- Use `@HttpCode()` decorator for non-standard codes\n- Return appropriate status codes (200, 201, 204, 400, 404, etc.)\n\n## Data Transfer Objects (DTOs)\n\n### Validation\n\nUse `class-validator` decorators for validation:\n\n- `@IsString()`, `@IsNumber()`, `@IsEmail()`\n- `@IsNotEmpty()`, `@IsOptional()`\n- `@MinLength()`, `@MaxLength()`\n- `@Min()`, `@Max()`\n\n### Swagger Documentation\n\nUse `@ApiProperty()` and `@ApiPropertyOptional()` for Swagger docs.\n\n## Error Handling\n\n### NestJS Exceptions\n\nUse appropriate exception types:\n\n- `NotFoundException` - 404\n- `BadRequestException` - 400\n- `UnauthorizedException` - 401\n- `ForbiddenException` - 403\n- `ConflictException` - 409\n- `InternalServerErrorException` - 500\n\n### Custom Exceptions\n\n**Prefer custom exceptions over generic ones** when you need:\n\n- Consistent error codes for frontend handling\n- Specific error messages from constants\n- Consistent error structure across the codebase\n\nCreate custom exceptions following existing patterns:\n\n```typescript\n// src/modules/feature/exceptions/feature-invalid.exception.ts\nimport {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class FeatureInvalidException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.FEATURE_INVALID,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'FeatureInvalid',\n      errorCode: CustomErrorCodes.FeatureInvalid,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n```\n\n### Error Logging\n\n```typescript\nprivate readonly logger = new Logger(ServiceName.name)\n\nthis.logger.error('Error message', error.stack, { context })\n```\n\n## Redis Integration\n\n### Redis Service Pattern\n\n- Use RedisClient from `src/modules/redis`\n- Handle errors gracefully\n- Log Redis operations\n- Use try-catch for error handling\n\n## Code Quality\n\n### Cognitive Complexity (≤ 15)\n\n- Use early returns to reduce nesting\n- Extract complex logic to separate functions\n- Avoid deeply nested conditions\n\n### No Duplicate Strings\n\nExtract repeated strings to constants in constants file.\n\n## API Documentation (Swagger)\n\n### Required Decorators\n\n- `@ApiTags()` - Group endpoints\n- `@ApiOperation()` - Describe operation\n- `@ApiResponse()` - Document responses\n- `@ApiParam()` - Document params\n- `@ApiQuery()` - Document query params\n- `@ApiBearerAuth()` - Auth requirement\n\n## Checklist\n\n- [ ] Services use dependency injection\n- [ ] DTOs have validation decorators\n- [ ] Controllers have Swagger documentation\n- [ ] Proper HTTP status codes used\n- [ ] Error handling with appropriate exceptions\n- [ ] Logging for important operations\n- [ ] Transactions for related DB operations\n- [ ] Configuration via ConfigService\n- [ ] Guards for authentication/authorization\n- [ ] Cognitive complexity ≤ 15\n",".ai/skills/branches/SKILL.md":"---\nname: branches\ndescription: >-\n  Create and name git branches following project conventions. Use when\n  creating branches, checking out new branches, or the user mentions\n  branch naming.\n---\n\n# Branch Naming Conventions\n\nUse lowercase kebab-case with type prefix and issue/ticket identifier.\n\n```bash\n# Pattern: <type>/<issue-ref>/<short-title>\n\n# INTERNAL (JIRA - RI-XXX)\nfeature/RI-123/add-user-profile\nbugfix/RI-789/memory-leak\nfe/feature/RI-567/add-dark-mode\nbe/bugfix/RI-345/fix-redis-connection\ndocs/RI-333/update-docs\ntest/RI-444/add-unit-tests\ne2e/RI-555/add-integration-tests\n\n# OPEN SOURCE (GitHub - XXX)\nfeature/123/add-export-feature\nbugfix/789/fix-connection-timeout\n\n# Special branches\nrelease/v2.0.0\nric/RI-666/custom-prefix\n```\n\n## Branch Types\n\n- `feature/` - New features and refactoring (affects multiple areas)\n- `bugfix/` - Bug fixes (affects multiple areas)\n- `fe/feature/` - Frontend-only features (only `redisinsight/ui/` folder)\n- `fe/bugfix/` - Frontend-only bug fixes (only `redisinsight/ui/` folder)\n- `be/feature/` - Backend-only features (only `redisinsight/api/` folder)\n- `be/bugfix/` - Backend-only bug fixes (only `redisinsight/api/` folder)\n- `docs/` - Documentation changes\n- `test/` - Test-related changes\n- `e2e/` - End-to-end test changes\n- `release/` - Release branches\n- `ric/` - Custom prefix for special cases\n\n**Note:** When a bug fix affects only the `redisinsight/ui/` folder, use `fe/bugfix/` prefix instead of `bugfix/`.\n\n## Issue References\n\n- **Internal**: `RI-XXX` (JIRA ticket)\n- **Open Source**: `XXX` (GitHub issue number)\n- Use `#` only in commit messages, not branch names\n",".ai/skills/code-quality/SKILL.md":"---\nname: code-quality\ndescription: >-\n  Code-quality standards for RedisInsight: TypeScript strictness,\n  naming conventions (camelCase, PascalCase, UPPER_SNAKE_CASE),\n  linting rules, no `any` without reason, no `!important` in styles,\n  and constant extraction. Use when writing or refactoring any\n  TypeScript/JavaScript code in this repo, when ESLint or Prettier\n  issues come up, or when the user mentions code style, lint, naming\n  conventions, or general code quality.\n---\n\n\n# Code Quality Standards\n\n## Critical Rules\n\n- **ALWAYS run linter** after code changes: `npm run lint`\n- Linter must pass before committing\n- No console.log in production code (use console.warn/error only)\n\n## TypeScript Standards\n\n### Essential Rules\n\n- Use TypeScript for all new code\n- **Avoid `any`** - use proper types or `unknown`\n- **Prefer interfaces** for object shapes\n- Use **type** for unions, intersections, primitives\n- Add explicit return types for non-obvious functions\n- Leverage type inference where clear\n\n## Import Organization\n\n### Required Order (enforced by ESLint)\n\n1. External libraries (`react`, `lodash`, etc.)\n2. Built-in Node modules (`path`, `fs` - backend only)\n3. Internal modules with aliases (`uiSrc/*`, `apiClient`)\n4. Sibling/parent relative imports\n5. Style imports (ALWAYS LAST)\n\n### Module Aliases\n\n- `uiSrc/*` → `redisinsight/ui/src/*` (UI workspace)\n- `apiClient` → `redisinsight/api-client` (auto-generated OpenAPI types — the UI's only entry point into BE-defined shapes)\n- `desktopSrc/*` → `redisinsight/desktop/src/*` (desktop workspace)\n\nThe UI workspace must not import from the backend codebase directly. Use `apiClient` for types and the existing service layer (`uiSrc/services`) for HTTP calls.\n\n✅ **Use aliases**: `import { Button } from 'uiSrc/components/Button'`  \n❌ **Avoid relative**: `import { Button } from '../../../ui/src/components/Button'`\n\n## Naming Conventions\n\n- **Components**: `PascalCase` - `UserProfile`\n- **Functions/Variables**: `camelCase` - `fetchUserProfile`\n- **Constants**: `UPPER_SNAKE_CASE` - `MAX_RETRY_ATTEMPTS`\n- **Booleans**: Use `is/has/should` prefix - `isLoading`, `hasError`\n\n## Comments\n\n**Default to fewer comments.** Clear names, small functions, and good\ntests should carry the meaning. Write a comment only when the code\ngenuinely can't explain itself — or when the user explicitly asks for\nmore (for example, on a piece of complex logic).\n\nWhen you do write one, keep it short and use plain words to say\n**what** the code is doing or **why** it has to exist — not how it\nworks under the hood, and not the story of why you made the change\n(that belongs in the commit message or PR description).\n\n✅ **Good** — plain, names the situation:\n\n```ts\n// When switching keys, hide the previous key's loader/error/result\n// until the new key is ready.\nconst showLoader = isArrayKeyReady && loading\n```\n\n❌ **Avoid** — long, mechanism-heavy, re-derives what the code shows:\n\n```ts\n// Gate every aggregate-slice surface (loader, error, result) on\n// `isArrayKeyReady`: when the user switches keys, `keyProp` flips\n// immediately but `selectedKeyData` lags by a round-trip, so the\n// prior key's slice state would otherwise paint under the newly\n// selected (or empty) key for one frame before the hook's reset\n// effect fires.\nconst showLoader = isArrayKeyReady && loading\n```\n\nAlso avoid:\n\n- Restating identifier names in prose (`// set loading to true`).\n- Comments that duplicate what a clearly-named test already asserts.\n- Block comments that summarize obvious blocks (`// loop over items`).\n\n## SonarJS Rules\n\n- Keep cognitive complexity low (refactor complex functions)\n- Extract duplicate strings to constants\n- Follow DRY principle - no duplicate code\n- Use immediate return (avoid unnecessary intermediate variables)\n\n## Best Practices\n\n- Use destructuring for objects and arrays\n- Use template literals over string concatenation\n- Use `const` by default, `let` only when reassignment needed\n- Use descriptive variable names\n- Handle errors properly\n- Clean up subscriptions and timers\n- Use constants instead of magic numbers\n\n## Vite Cache Management\n\nWhen updating npm packages (especially `@redis-ui/*` packages):\n\n1. **Clear Vite cache** after `npm install`:\n\n   ```bash\n   rm -rf node_modules/.vite\n   rm -rf redisinsight/ui/node_modules/.vite\n   ```\n\n2. **Restart dev server** to rebuild dependencies\n\n3. This ensures new package versions are properly loaded\n\n## Pre-Commit Checklist\n\n- [ ] `npm run lint` passes\n- [ ] No TypeScript errors\n- [ ] Import order is correct\n- [ ] No `any` types without reason\n- [ ] No console.log statements\n- [ ] No magic numbers\n- [ ] Descriptive variable names\n- [ ] Low cognitive complexity\n- [ ] No duplicate code\n- [ ] Comments kept to a minimum; any present are short and plain-language\n- [ ] Vite cache cleared (if updated dependencies)\n",".ai/skills/commits/SKILL.md":"---\nname: commits\ndescription: >-\n  Generate commit messages following Conventional Commits format. Use when\n  creating commits, writing commit messages, or the user asks to commit changes.\n---\n\n# Commit Message Guidelines\n\nFollow **Conventional Commits** format:\n\n```\n<type>(<scope>): <subject>\n\n<body>\n\n<footer>\n```\n\n## Commit Types\n\n- `feat` - New feature\n- `fix` - Bug fix\n- `refactor` - Code refactoring\n- `test` - Adding or updating tests\n- `docs` - Documentation changes\n- `chore` - Maintenance tasks\n- `style` - Code style changes (formatting)\n- `perf` - Performance improvements\n- `ci` - CI/CD changes\n\n## Examples\n\n```bash\nfeat(ui): add user search functionality\n\nImplements real-time search with debouncing.\n\nReferences: #RI-123\n\n---\n\nfix(api): resolve memory leak in connection pool\n\nProperly cleanup subscriptions on unmount.\n\nFixes #456\n\n---\n\ntest(ui): add tests for data serialization\n\nrefactor(api): extract common validation logic\n\ndocs: update API endpoint documentation\n\nchore: upgrade React to version 18.2\n```\n\n## Best Practices\n\n### ✅ Good Commits\n\n- Clear, descriptive subject line\n- Atomic changes (one logical change per commit)\n- Reference issue/ticket in body\n- Explain **why**, not just **what**\n- **Keep it concise** - Don't list every file change in the body\n\n```bash\nfeat(ui): add user profile editing\n\nAllows users to update their profile information including\nname, email, and avatar. Includes validation and error handling.\n\nReferences: #RI-123\n```\n\n### ❌ Bad Commits\n\n```bash\n# Too vague\nfix stuff\nWIP\nupdate\n\n# Too broad\nadd feature, fix bugs, refactor code, update tests\n```\n\n## Issue References\n\n- **JIRA (internal)**: `References: #RI-123` or `Fixes #RI-123`\n- **GitHub (open source)**: `Fixes #456` or `Closes #456`\n- Use `#` for auto-linking in commit messages\n",".ai/skills/dead-dependencies/SKILL.md":"---\nname: dead-dependencies\ndescription: >-\n  Find and safely remove unused (\"dead\") npm dependencies in RedisInsight\n  using a grep + leaf-check + build-gate recipe. Use when cleaning up\n  dependencies, investigating whether a package is still used, removing a\n  suspected leftover, or when the user mentions dead deps, unused\n  dependencies, dependency cleanup, leftover packages, or \"is this safe to\n  remove\". Complements the weekly vulnerability audit\n  (`scripts/dependency-audit-report.mjs`), which only reports vulnerabilities.\n---\n\n# Dead Dependencies\n\nIdentify and safely remove unused npm dependencies. This is a **local,\ninteractive** workflow — a scheduled report can only ever guess; confirming a\ndependency is dead requires grepping real usage and running the build.\n\n## Why manual (not knip/depcheck)\n\nStatic tools over-report badly in this repo and must not be trusted blindly:\n\n- The root `package.json` is a **mega-manifest** (UI + desktop + build + test\n  + storybook + electron), so a tool scanning one area flags everything used\n  elsewhere as \"unused\".\n- Lots of dependencies are referenced **without an `import`**: webpack loaders\n  and eslint/jest/babel plugins by string in config, tools invoked from\n  `package.json` `scripts`, runtime `-r` preloads, dynamic `require`, and\n  ambient `@types/*`.\n- The webpack→Vite migration left **genuinely dead** build tooling behind,\n  mixed in with false positives.\n\nThe reliable signal is: **grep for usage → confirm it's a leaf → remove it and\nrun the build gate.** That is exactly how `jsonpath` was confirmed dead and\nremoved (worked example at the bottom).\n\n## The recipe (per candidate)\n\n### 1. Grep the whole repo for real usage\n\n```bash\nPKG=jsonpath   # the dependency to check\ngrep -rn \"$PKG\" \\\n  redisinsight configs scripts tests .storybook \\\n  --include=*.ts --include=*.tsx --include=*.js --include=*.jsx \\\n  --include=*.mjs --include=*.cjs --include=*.json \\\n  2>/dev/null | grep -v node_modules\n```\n\nLook for `import ... from '$PKG'`, `require('$PKG')`, `import('$PKG')`, and\nbare references. Ignore unrelated substring hits (e.g. `nestjs-form-data` when\nchecking `form-data`, or a package name appearing only in tutorial/`manifest`\ntext). **Zero real references → candidate for the next steps.**\n\n### 2. Rule out no-import usage\n\nA clean grep is **necessary but not sufficient**. Check the ways a package is\nused without an `import`:\n\n- **Config string references** — search the build/test config for the bare\n  name: `.eslintrc.js`, `configs/webpack.config.*.ts`,\n  `redisinsight/ui/vite.config.mjs`, `jest.config.cjs`, `babel.config.cjs`,\n  `electron-builder.json`, `.mocharc*`. eslint plugins, webpack loaders, and\n  jest/mocha reporters live here.\n- **`package.json` scripts** — a tool like `concurrently`, `lint-staged`, or a\n  reporter is \"used\" if a `scripts` entry (any workspace) invokes it.\n- **Runtime preloads / dynamic** — `node -r <pkg>`, `require(variable)`, or a\n  wasm/worker loader with a computed path.\n\n### 3. Leaf check\n\n```bash\nnpm ls \"$PKG\"        # in the workspace that declares it\n```\n\nConfirm nothing else in the tree depends on it, and note any transitive deps it\nuniquely pulls (removing `jsonpath` also dropped `underscore`).\n\n### 4. Classify before acting\n\n| Situation | Action |\n| --- | --- |\n| Plain runtime/dev dep, zero references anywhere | **Delete** (after the gate below) |\n| `@types/x` where base `x` bundles its own types (`node_modules/x/package.json` has `types`/`typings`) | **Delete** — the DefinitelyTyped package is obsolete |\n| `@types/x` that's ambient/global-only (e.g. `@types/webpack-env`) | **Keep** — never imported by design |\n| Declared in more than one workspace (`package.json`), used in only one | **Relocate/dedupe** — remove the *unused* declaration, never the used copy |\n| Shadows a Node builtin (`buffer`, `assert`, …) | **Keep** — usually a false positive |\n| Referenced only in a config/`scripts` (step 2 hit) | **Keep** |\n\n### 5. The gate — the only real proof\n\nRemove it, reinstall (per the repo's dependency rules — never hand-edit\n`package.json`/lockfile, never `--ignore-scripts`), and verify the affected\narea builds:\n\n```bash\nnpm uninstall \"$PKG\"          # in the declaring workspace; updates the lockfile\nnpm run type-check            # or the affected workspace's type-check\nnpm run test                  # / test:api, as relevant\nnpm run build                 # if it's a build-time dep\n```\n\nGreen across the relevant checks = safe. Commit the `package.json` +\n`package-lock.json` change (see the git-safety / dependency rules in\n`CLAUDE.md`). If anything goes red, it wasn't dead — restore it.\n\n## Optional: enumerate candidates to sweep\n\nTo triage the whole surface rather than one package, list declared deps and\ngrep each for an import, then apply steps 2–5 to the ones with zero hits:\n\n```bash\nnode -e \"const p=require('./package.json');console.log([...Object.keys(p.dependencies||{}),...Object.keys(p.devDependencies||{})].join('\\n'))\" \\\n| while read PKG; do\n    hits=$(grep -rl --include=*.ts --include=*.tsx --include=*.js --include=*.jsx --include=*.mjs \\\n      -e \"from ['\\\"]$PKG\" -e \"require(['\\\"]$PKG\" redisinsight configs scripts tests 2>/dev/null | grep -vc node_modules)\n    [ \"$hits\" = \"0\" ] && echo \"candidate: $PKG\"\n  done\n```\n\nThis is a **first filter only** — every candidate still goes through steps 2–5.\nExpect false positives (config/string/dynamic/ambient use). Never delete straight\nfrom this list.\n\n## Worked example — `jsonpath`\n\n1. Grep across the repo → the only hits were unrelated tutorial text in a\n   `manifest.json`; **no `import`/`require`**.\n2. Not referenced in any config or script.\n3. `npm ls jsonpath` → a leaf; it was also the sole reason `underscore` was\n   installed.\n4. Plain runtime dep, zero references → likely deletable.\n5. Removed `jsonpath` + `@types/jsonpath`, `npm install`, `npm run type-check`\n   → green. Confirmed dead; committed. (`underscore` dropped with it.)\n",".ai/skills/e2e-testing/SKILL.md":"---\nname: e2e-testing\ndescription: >-\n  Playwright end-to-end testing standards for RedisInsight: page\n  object models, test structure, fixtures, navigation patterns, and\n  flaky-test prevention. Use when editing files under\n  tests/e2e-playwright/**, writing Playwright tests, adding page\n  objects, or when the user mentions Playwright, E2E tests, page\n  objects, or end-to-end testing.\n---\n\n\n# E2E Testing Standards (Playwright)\n\n## Location\n\nAll E2E tests are in `tests/e2e-playwright/`. This is a **standalone package** - no imports from `redisinsight/ui/` or `redisinsight/api/`.\n\n## Test Plan\n\n**Always refer to `tests/e2e-playwright/TEST_PLAN.md`** for:\n- Test coverage status (✅ implemented, 🔲 not implemented)\n- Feature implementation order\n- Test data requirements\n\n**After implementing tests, update TEST_PLAN.md** to mark tests as ✅.\n\n## Project Structure\n\n```\ntests/e2e-playwright/\n├── TEST_PLAN.md         # Master test plan with coverage status\n├── config/              # Configuration (env, databases)\n│   └── databases/       # Database configs by type\n├── fixtures/            # Playwright fixtures\n├── helpers/             # API helpers for setup/teardown\n├── pages/               # Page Object Models\n│   ├── BasePage.ts      # Base class for all pages\n│   ├── InstancePage.ts  # Base class for database instance pages\n│   ├── components/      # Shared components (InstanceHeader, NavigationTabs, BottomPanel)\n│   └── {feature}/       # Feature-specific pages (browser/, cli/, etc.)\n├── test-data/           # Test data factories\n├── tests/               # Test specs organized by project\n│   ├── main/            # Default parallel tests\n│   │   └── {feature}/\n│   │       └── {action}/\n│   ├── auto-update/     # Serial tests with special setup\n│   └── electron/        # Electron-specific tests\n└── types/               # TypeScript types\n```\n\n## Playwright Projects\n\nThe folder a test lives in determines its execution mode. Each browser platform has a parallel project and a serial project:\n\n| Project             | Folder            | Parallelism | Use Case |\n|---------------------|-------------------|-------------|----------|\n| `chromium-parallel` | `tests/parallel/` | Parallel (4 workers) | Standard chromium tests |\n| `chromium-serial`   | `tests/serial/`   | Serial (1 worker)    | Sequential chromium tests |\n| `electron-parallel` | `tests/parallel/` | Serial (1 worker)*   | Standard electron tests |\n| `electron-serial`   | `tests/serial/`   | Serial (1 worker)    | Sequential electron tests |\n\n\\* Electron uses one worker today because there is a single app instance.\n\n### Execution order\n\nFor each platform, the serial project depends on the parallel project via `dependencies: ['<platform>-parallel']` in [playwright.config.ts](../../tests/e2e-playwright/playwright.config.ts), so the order is always:\n\n1. `<platform>-parallel` runs first (with up to N workers)\n2. `<platform>-serial` runs after, one worker at a time\n\nSerial tests perform destructive operations on the shared RTE Redis (`FLUSHDB`, broad `deleteAllIndexes`, dangerous commands) so they can't safely run alongside parallel tests on the same RTE. Electron also can't run its two projects concurrently because the desktop app binds its embedded API on a fixed port (`5530`).\n\nIf serial grows large enough to become a CI bottleneck, the next step is to give serial tests a dedicated Redis instance (or split into a separate CI job) — not to revert the ordering.\n\n### Running Projects\n\n```bash\n# Full platform run (parallel + serial)\nnpx playwright test --project=chromium-parallel --project=chromium-serial\nnpx playwright test --project=electron-parallel --project=electron-serial\n\n# Just parallel\nnpx playwright test --project=chromium-parallel\n\n# Just serial — needs --no-deps, otherwise parallel runs first (it's a\n# project dependency). --no-deps also skips browser-setup.\nnpx playwright test --project=chromium-serial --no-deps\n\nnpx playwright test                           # All projects\n```\n\n### When to put a test in `tests/serial/`\n\nPut a test in `tests/serial/` when it:\n- Shares database state across tests via `beforeAll`\n- Runs dangerous commands or mutates global app state. Settings are global:\n  `scanThreshold`, `dateFormat`, `timezone` and `batchSize` are one shared resource,\n  so a spec that changes one and holds it across other work breaks every concurrent\n  test that reads it\n- Cannot tolerate concurrent execution with other tests\n- Would cause flakiness when run with other tests\n- Require special environment configuration\n\n### Adding a New Project\n\n1. Create folder under `tests/` (e.g., `tests/my-feature/`)\n2. Add project configuration in `playwright.config.ts`:\n\n```typescript\n{\n  name: 'my-feature',\n  testDir: './tests/my-feature',\n  fullyParallel: false, // or true\n  workers: 1,\n  timeout: 120000,\n  // Optional: different setup\n  // globalSetup: './my-feature-setup.ts',\n}\n```\n\n## Page Objects\n\n### Page Object Hierarchy\n\n```\nBasePage (abstract)\n  ├── DatabasesPage           # Databases list page\n  ├── SettingsPage            # Settings page\n  └── InstancePage (abstract) # Base for all database instance pages\n        ├── instanceHeader    # Database name, stats, breadcrumb\n        ├── navigationTabs    # Browse, Workbench, Analyze, Pub/Sub\n        ├── bottomPanel       # CLI, Command Helper, Profiler\n        └── BrowserPage       # Browser-specific (extends InstancePage)\n              └── WorkbenchPage (future)\n              └── AnalyzePage (future)\n              └── PubSubPage (future)\n```\n\n### Extend the Appropriate Base Class\n\n- **BasePage** - For standalone pages (DatabasesPage, SettingsPage)\n- **InstancePage** - For pages within a connected database (BrowserPage, WorkbenchPage, etc.)\n\nPage objects are **stateless** - they don't store database objects. Pass `databaseId` to navigation methods.\n\n```typescript\n// For database instance pages - extend InstancePage\nimport { Page, Locator } from '@playwright/test';\nimport { InstancePage } from '../InstancePage';\n\nexport class WorkbenchPage extends InstancePage {\n  readonly editor: Locator;\n\n  constructor(page: Page) {\n    super(page);\n    this.editor = page.getByTestId('workbench-editor');\n  }\n\n  // InstancePage provides: instanceHeader, navigationTabs, bottomPanel\n  // Plus navigation methods: navigateToBrowser(), openCli(), etc.\n\n  async goto(databaseId: string): Promise<void> {\n    await this.gotoDatabase(databaseId);\n    await this.navigationTabs.gotoWorkbench();\n    await this.waitForLoad();\n  }\n}\n```\n\n### Component-Based Structure\n\nBreak large pages into components:\n\n```typescript\n// pages/feature/FeaturePage.ts\nexport class FeaturePage extends InstancePage {\n  readonly dialog: FeatureDialog;\n  readonly list: FeatureList;\n\n  constructor(page: Page) {\n    super(page);\n    this.dialog = new FeatureDialog(page);\n    this.list = new FeatureList(page);\n  }\n}\n```\n\n## Test Structure\n\n### File Organization\n\n```\ntests/\n├── main/                # Default parallel tests (most tests go here)\n│   └── {feature}/       # e.g., databases, browser, workbench\n│       └── {action}/    # e.g., add, edit, delete\n│           ├── standalone.spec.ts\n│           └── cluster.spec.ts\n├── auto-update/         # Serial tests with special setup\n└── electron/            # Electron-specific tests\n```\n\n### Test Setup Pattern\n\nUse simple, explicit setup with clear separation of concerns. **Page objects are fixtures** - they don't store database state. Pass `databaseId` to `goto()` methods.\n\n```typescript\nimport { test, expect } from '../../../fixtures/base';\nimport { standaloneConfig } from '../../../config/databases/standalone';\nimport { DatabaseInstance } from '../../../types';\n\ntest.describe('Feature > Action', () => {\n  let database: DatabaseInstance;\n\n  // Setup: Create database once for all tests\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase({\n      name: 'test-feature-db',\n      host: standaloneConfig.host,\n      port: standaloneConfig.port,\n    });\n  });\n\n  // Teardown: Clean up database after all tests\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  test.describe('Sub-feature', () => {\n    // Navigation: Pass databaseId to goto() - page is a fixture\n    test.beforeEach(async ({ featurePage }) => {\n      await featurePage.goto(database.id);\n    });\n\n    // Tests receive page fixtures they need\n    test('should do something', async ({ featurePage }) => {\n      await featurePage.doAction();\n      await expect(featurePage.result).toBeVisible();\n    });\n\n    // Tests that need both page and apiHelper\n    test('should create and verify', async ({ featurePage, apiHelper }) => {\n      await apiHelper.createKey(database.id, 'test-key', 'value');\n      await featurePage.refresh();\n      await expect(featurePage.keyList).toContainText('test-key');\n    });\n  });\n});\n```\n\n### Key Principles\n\n1. **`beforeAll`** - Create database/test data via API (runs once)\n2. **`afterAll`** - Clean up database/test data via API (runs once)\n3. **`beforeEach`** - Navigate to page via UI using `goto(databaseId)` (runs before each test)\n4. **Individual tests** - Receive page fixtures they need in the signature\n5. **Page objects are stateless** - Don't store database objects in pages, pass IDs to methods\n\n### Avoid These Anti-Patterns\n\n```typescript\n// ❌ BAD: Storing database in page object\nconst browserPage = createBrowserPage(database);  // OLD pattern - don't use\n\n// ✅ GOOD: Pass databaseId to goto()\nawait browserPage.goto(database.id);\n\n// ❌ BAD: Using page fixture without declaring it in test signature\ntest('should work', async () => {\n  await browserPage.doSomething();  // browserPage is undefined!\n});\n\n// ✅ GOOD: Declare fixtures in test signature\ntest('should work', async ({ browserPage }) => {\n  await browserPage.doSomething();\n});\n\n// ❌ BAD: Navigation inside each test\ntest('should work', async ({ browserPage }) => {\n  await browserPage.goto(database.id);  // Should be in beforeEach\n  // ...\n});\n\n// ❌ BAD: Using test.describe.serial when not needed\ntest.describe.serial('Feature', () => { // Use regular describe unless tests truly depend on each other\n  // ...\n});\n```\n\n## Test Data\n\n### Use Fishery Factories with Faker\n\nUse the [fishery](https://github.com/thoughtbot/fishery) library for test data factories:\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nexport const TEST_PREFIX = 'test-';\n\nexport const ConfigFactory = Factory.define<Config>(() => ({\n  name: `${TEST_PREFIX}${faker.string.alphanumeric(8)}`,\n  host: '127.0.0.1',\n  port: 6379,\n}));\n\n// Usage in tests\nconst config = ConfigFactory.build();\nconst config = ConfigFactory.build({ name: 'custom-name' });\n```\n\n### Cleanup Pattern\n\nPrefix test data with `test-` so leftovers are identifiable, but **never clean up by\nthat prefix**. Specs share a Redis instance and different files run on different\nworkers, so deleting every `test-*` key also removes keys the specs alongside it are\nstill asserting on, and their rows disappear mid-test.\n\nTrack what a test creates and delete only that:\n\n```typescript\nimport { createKeyTracker } from 'e2eSrc/helpers';\n\ntest.describe('Browser > Add Key', () => {\n  const keys = createKeyTracker();\n\n  test.afterEach(async ({ apiHelper }) => {\n    await keys.cleanup(apiHelper, database.id);\n  });\n\n  test('should add a Hash key', async ({ browserPage }) => {\n    const keyData = keys.track(HashKeyFactory.build());\n    // keys.add(name) records a name built without a factory, e.g. a rename target.\n  });\n});\n```\n\nMiss a `track()` and the key is never deleted, which is harmless in CI (the test\nenvironment is recreated per run) but accumulates locally until the RTE restarts.\n\nDatabases need no such care: each spec deletes its own in `afterAll`, and\n`browser.setup` clears leftover `test-` databases once before the run.\n\n## Fixtures\n\n### Add New Fixtures to base.ts\n\n```typescript\n// fixtures/base.ts\ntype Fixtures = {\n  myPage: MyPage;\n  apiHelper: ApiHelper;\n};\n\nexport const test = base.extend<Fixtures>({\n  myPage: async ({ page }, use) => {\n    await use(new MyPage(page));\n  },\n  apiHelper: async ({}, use) => {\n    const helper = new ApiHelper();\n    await use(helper);\n    await helper.dispose();\n  },\n});\n```\n\n## UI Exploration with Playwright MCP\n\n**Before writing tests, ALWAYS use Playwright MCP to explore the UI:**\n\n### Why Explore First?\n- Discover actual `data-testid` attributes used in the application\n- Understand element roles and accessible names for `getByRole()`\n- See page structure and component hierarchy\n- Avoid trial-and-error test writing\n\n### Exploration Workflow\n\n1. **Navigate to the page**: `browser_navigate_Playwright` to target URL\n2. **Take snapshot**: `browser_snapshot_Playwright` to see element tree\n3. **Interact with elements**: `browser_click_Playwright` to trigger dialogs, dropdowns, etc.\n4. **Wait for async content**: `browser_wait_for_Playwright` for dynamic content\n5. **Document findings**: Add discovered UI patterns to `TEST_PLAN.md` under the feature section\n\n### What to Look For\n\n- `data-testid` attributes → use with `page.getByTestId()`\n- Element roles (button, combobox, grid, treeitem) → use with `page.getByRole()`\n- Accessible names → use with `{ name: 'text' }` option\n- Form field placeholders → use with `page.getByPlaceholder()`\n- Text content patterns → use with `page.getByText()`\n\n### Use Discovered Patterns in Page Objects\n\nAfter exploring, use discovered patterns directly in Page Object locators:\n\n```typescript\n// Use data-testid when available\nthis.addButton = page.getByTestId('btn-add-key');\n\n// Use role + name for accessible elements\nthis.submitButton = page.getByRole('button', { name: 'Submit' });\n\n// Use placeholder for form fields\nthis.searchInput = page.getByPlaceholder('Search...');\n```\n\n**Note**: Keep TEST_PLAN.md as a simple visual list of test cases. Document UI patterns in Page Object comments if needed.\n\n## Best Practices\n\n### ✅ DO\n\n- **Explore UI with Playwright MCP before writing tests**\n- **Use Page Object navigation methods** (e.g., `browserPage.goto()`, `workbenchPage.goto()`)\n- Use `data-testid` attributes for stable selectors\n- Use `getByRole`, `getByLabel` for accessible elements\n- Wait for elements with `waitFor({ state: 'visible' })`\n- Clean up test data in `afterEach`\n- Use API for setup, UI for assertions\n- Handle both List view and Tree view in key assertions\n\n### ❌ DON'T\n\n- **NEVER use `page.goto()` directly** - tests must work in both browser and Electron\n- Write tests without exploring the actual UI first\n- Use fixed timeouts (`waitForTimeout`)\n- Use CSS selectors for dynamic content\n- Leave test data after tests\n- Import from `redisinsight/ui/` or `redisinsight/api/`\n- Hardcode test data (use faker)\n- Assume element structure without verification\n\n## Navigation (IMPORTANT)\n\n**All navigation must use UI-based methods, NOT URL navigation.**\n\nTests must work in both browser mode (http://localhost:8080) and Electron mode (no baseURL). Direct `page.goto()` calls fail in Electron because there's no baseURL.\n\n### Navigation Architecture\n\n**BasePage** provides only fundamental navigation:\n```typescript\nawait this.gotoHome();              // Click Redis logo → databases list\nawait this.gotoDatabase(dbId);      // Click database → Browser page (default)\n```\n\n**Each page owns its navigation** via its `goto()` method:\n```typescript\nawait settingsPage.goto();           // Settings page\nawait browserPage.goto(dbId);        // Browser page for database\nawait workbenchPage.goto(dbId);      // Workbench page for database\nawait analyticsPage.goto(dbId);      // Analytics page for database\nawait pubSubPage.goto(dbId);         // Pub/Sub page for database\n```\n\n**NavigationTabs component** handles tab switching within a connected database:\n```typescript\nawait browserPage.navigationTabs.gotoBrowser();\nawait browserPage.navigationTabs.gotoWorkbench();\nawait browserPage.navigationTabs.gotoAnalyze();\nawait browserPage.navigationTabs.gotoPubSub();\n```\n\n### ✅ Correct Navigation Pattern\n\n```typescript\n// Use Page Object's goto() method in beforeEach\ntest.beforeEach(async ({ browserPage }) => {\n  await browserPage.goto(database.id);  // Navigates and waits for page load\n});\n\n// Switch tabs when already connected\nawait browserPage.navigationTabs.gotoWorkbench();\n```\n\n### ❌ Incorrect Navigation Pattern\n\n```typescript\n// NEVER do this - fails in Electron\nawait page.goto(`/${database.id}/browser`);\nawait page.goto('/settings');\nawait page.goto('/');\n```\n\n## Running Tests\n\nRun these commands from the E2E package directory:\n\n```bash\ncd tests/e2e-playwright\n\nnpx playwright test                                              # All Playwright projects\nnpx playwright test --project=chromium-parallel                  # Chromium parallel tests\nnpx playwright test --project=chromium-serial --no-deps          # Chromium serial only (skips parallel + setup)\nnpx playwright test --project=electron-parallel                  # Electron parallel tests\nnpx playwright test --project=electron-serial --no-deps          # Electron serial only (skips parallel + setup)\nENV=ci npx playwright test                             # CI environment\nENV=staging npx playwright test                        # Staging environment\n```\n\n## Code Quality (IMPORTANT)\n\n**Always run linter and type checker after making changes:**\n\n```bash\nnpm run lint                # ESLint check\nnpm run type-check          # TypeScript type check\n```\n\nBoth must pass before committing. Common issues:\n- Unused variables/imports\n- Missing return types\n- `any` types (avoid when possible)\n- Null/undefined handling (use proper types like `Promise<string | null>`)\n\n## Test Isolation (IMPORTANT)\n\nTests should be isolated and not depend on execution order:\n\n### 1. Shared Database with beforeAll/afterAll\n\n```typescript\ntest.describe('Feature Name', () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase({ name: 'test-feature-db', ... });\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  // Tests can run in parallel - they share the database but don't modify shared state\n});\n```\n\n### 2. Use Serial Only When Tests Truly Depend on Each Other\n\n```typescript\n// Only use .serial when tests modify state that subsequent tests depend on\ntest.describe.serial('Workflow that modifies state', () => {\n  test('step 1: create item', ...);\n  test('step 2: modify item created in step 1', ...);\n  test('step 3: delete item', ...);\n});\n```\n\n### 3. Unique Test Data Per Test (when needed)\n\n```typescript\ntest('should create unique item', async ({ apiHelper }) => {\n  const uniqueName = `test-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;\n  // Use uniqueName for this test's data\n});\n```\n\n## Feature-to-Path Mapping\n\nFollow this naming convention for test and page object paths:\n\n| Feature | Test Path | Page Object Path |\n|---------|-----------|------------------|\n| Database List | `tests/parallel/databases/list/` | `pages/databases/` |\n| Add Database | `tests/parallel/databases/add/` | `pages/databases/` |\n| Import Database | `tests/parallel/databases/import/` | `pages/databases/` |\n| Browser - Key List | `tests/parallel/browser/key-list/` | `pages/browser/` |\n| Browser - Add Key | `tests/parallel/browser/add-key/` | `pages/browser/` |\n| Browser - Key Details | `tests/parallel/browser/key-details/` | `pages/browser/` |\n| Workbench | `tests/parallel/workbench/` | `pages/workbench/` |\n| CLI | `tests/parallel/cli/` | `pages/cli/` |\n| Pub/Sub | `tests/parallel/pubsub/` | `pages/pubsub/` |\n| Slow Log | `tests/parallel/analytics/slow-log/` | `pages/analytics/` |\n| DB Analysis | `tests/parallel/analytics/analysis/` | `pages/analytics/` |\n| Settings | `tests/parallel/settings/` | `pages/settings/` |\n| Navigation | `tests/parallel/navigation/` | `pages/navigation/` |\n| Auto-Update | `tests/auto-update/` | `pages/` (shared) |\n| Deep Links | `tests/electron/deep-links/` | `pages/` (shared) |\n\n**Note**: Most tests go in `tests/parallel/`. Only use other project folders for tests with special requirements (serial execution, different setup, etc.).\n",".ai/skills/feature-flags/SKILL.md":"---\nname: feature-flags\ndescription: >-\n  Create, modify, and remove feature flags in RedisInsight. Use when adding a\n  new feature flag, introducing a dev flag, promoting a dev flag to regular,\n  cleaning up old flags, or the user mentions feature flags, feature toggles,\n  or gating features.\n---\n\n# Feature Flags\n\nRedisInsight has its own feature flag system. Flags are defined in a remote JSON config, fetched by the backend, and served to the frontend via API. This skill covers how to add, promote, and remove flags.\n\n## Flag Types\n\n| Type                         | Naming                            | `flag` value | Strategy                 | Purpose                                                      |\n| ---------------------------- | --------------------------------- | ------------ | ------------------------ | ------------------------------------------------------------ |\n| **Dev flag**                 | `dev-<name>` (e.g. `dev-browser`) | `false`      | `CommonFlagStrategy`     | Hide incomplete features during development                  |\n| **Regular flag**             | `camelCase` (e.g. `azureEntraId`) | `true`       | `CommonFlagStrategy`     | Standard on/off toggle                                       |\n| **Regular with data**        | `camelCase`                       | `true`       | `WithDataFlagStrategy`   | Flag + extra config payload in `data`                        |\n| **Switchable (overridable)** | `camelCase`                       | `true`       | `SwitchableFlagStrategy` | User can override locally via `~/.redis-insight/config.json` |\n\n## Files to Change\n\nEvery new flag touches these files (in order):\n\n### Backend (required)\n\n1. **`redisinsight/api/config/features-config.json`**\n   Add the flag entry with `flag`, `perc`, optional `filters` and `data`. Bump the `version` number.\n\n2. **`redisinsight/api/src/modules/feature/constants/index.ts`**\n   Add to the `KnownFeatures` enum.\n\n3. **`redisinsight/api/src/modules/feature/constants/known-features.ts`**\n   Add entry to the `knownFeatures` record with `name` and `storage` (usually `FeatureStorage.Database`).\n\n4. **`redisinsight/api/src/modules/feature/providers/feature-flag/feature-flag.provider.ts`**\n   Register the flag with its strategy (see Strategy Types below).\n\n### Frontend (required if the flag gates UI)\n\n5. **`redisinsight/ui/src/constants/featureFlags.ts`**\n   Add to the `FeatureFlags` enum.\n\n6. **`redisinsight/ui/src/slices/app/features.ts`**\n   Add default state entry in `initialState.featureFlags.features` with `{ flag: false }`.\n\n### Consuming code\n\n7. Use the flag in components/hooks to gate functionality.\n\n## Strategy Selection\n\nChoose the strategy based on what the flag needs:\n\n```\nCommonFlagStrategy        → Most flags (dev and regular on/off)\nWithDataFlagStrategy      → Flag needs to carry extra data payload\nSwitchableFlagStrategy    → Flag should be overridable via local config.json\n```\n\nRegister in `feature-flag.provider.ts`:\n\n```typescript\nthis.strategies.set(\n  KnownFeatures.YourFeature,\n  new CommonFlagStrategy(this.featuresConfigService, this.settingsService),\n);\n```\n\n## Config JSON Structure\n\n### Minimal (dev flag)\n\n```json\n\"dev-myFeature\": {\n  \"flag\": false,\n  \"perc\": [[0, 100]]\n}\n```\n\n### With filters (Electron-only)\n\n```json\n\"myFeature\": {\n  \"flag\": true,\n  \"perc\": [[0, 100]],\n  \"filters\": [\n    { \"name\": \"config.server.buildType\", \"value\": \"ELECTRON\", \"cond\": \"eq\" }\n  ]\n}\n```\n\n### Gradual rollout (10% of users)\n\n```json\n\"myFeature\": {\n  \"flag\": true,\n  \"perc\": [[0, 10]]\n}\n```\n\n### With data payload\n\n```json\n\"myFeature\": {\n  \"flag\": true,\n  \"perc\": [[0, 100]],\n  \"data\": { \"strategy\": \"ioredis\" }\n}\n```\n\n## Filter Conditions\n\nFilters compare a value from server state against the filter value.\n\n| Condition    | Meaning                         |\n| ------------ | ------------------------------- |\n| `eq`         | equals                          |\n| `neq`        | not equals                      |\n| `gt` / `gte` | greater than / greater or equal |\n| `lt` / `lte` | less than / less or equal       |\n\nCommon `name` paths: `config.server.buildType` (ELECTRON, DOCKER_ON_PREMISE, REDIS_STACK), `config.server.packageVersion` (uses semver), `agreements.analytics`, `env.<VAR_NAME>`.\n\nFilters support `and`/`or` composition for complex conditions.\n\n## Workflows\n\n### Add a dev feature flag\n\nUse for features under active development that should not be visible in production.\n\n1. `features-config.json` → add `\"dev-myFeature\": { \"flag\": false, \"perc\": [[0, 100]] }`\n2. `constants/index.ts` → add `DevMyFeature = 'dev-myFeature'` to `KnownFeatures`\n3. `constants/known-features.ts` → add record entry\n4. `feature-flag.provider.ts` → register with `CommonFlagStrategy`\n5. `ui/src/constants/featureFlags.ts` → add `devMyFeature = 'dev-myFeature'`\n6. `ui/src/slices/app/features.ts` → add default `{ flag: false }`\n\n### Promote dev flag to regular flag\n\nWhen the feature is complete and ready for rollout.\n\n1. Rename `dev-myFeature` → `myFeature` in all the files above\n2. Set `flag: true` in `features-config.json`\n3. Optionally set `perc` for gradual rollout (e.g. `[[0, 10]]`)\n4. Change strategy if needed (e.g. to `SwitchableFlagStrategy` for overridable)\n5. Bump config `version`\n\n### Clean up a flag\n\nWhen a feature is fully rolled out and the flag is no longer needed.\n\n1. Remove from `features-config.json`\n2. Remove from `KnownFeatures` enum\n3. Remove from `knownFeatures` record\n4. Remove strategy registration from `feature-flag.provider.ts`\n5. Remove from FE `FeatureFlags` enum\n6. Remove default state from `features.ts`\n7. Remove all gating code (conditionals, `FeatureFlagComponent` wrappers) in consuming components\n\n## FE Usage Patterns\n\n### Check flag in a component\n\n```typescript\nimport { FeatureFlags } from 'uiSrc/constants';\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features';\n\nconst features = useSelector(appFeatureFlagsFeaturesSelector);\nconst isEnabled = features[FeatureFlags.myFeature]?.flag;\n```\n\n### Custom selector for complex logic\n\n```typescript\nexport const isMyFeatureEnabledSelector = (state: RootState): boolean => {\n  const features = state.app.features.featureFlags.features;\n  return features[FeatureFlags.myFeature]?.flag ?? false;\n};\n```\n",".ai/skills/frontend/SKILL.md":"---\nname: frontend\ndescription: >-\n  React/Redux frontend development patterns for RedisInsight UI:\n  component folder structure, styled-components, hooks, named exports,\n  barrel files, layout components, and theme usage. Use when editing\n  any file under redisinsight/ui/**, writing or modifying React\n  components, Redux slices, styled-components, custom hooks, or when\n  the user mentions UI, frontend, React, Redux, or styled-components.\n---\n\n# Frontend Development (React/Redux)\n\n## Component Structure\n\n### Functional Components\n\n- Use **functional components with hooks** (no class components)\n- **Prefer named exports** over default exports\n- Keep components focused and single-responsibility\n- Extract complex logic into custom hooks\n\n### Component Folder Structure\n\nEach component in its own directory under `**/ComponentName`:\n\n```\nComponentName/\n  ComponentName.tsx          # Main component\n  ComponentName.styles.ts    # Styled-components styles (PascalCase)\n  ComponentName.types.ts     # TypeScript interfaces\n  ComponentName.spec.tsx     # Tests\n  ComponentName.constants.ts # Constants\n  ComponentName.story.tsx    # Storybook examples\n  hooks/                     # Custom hooks\n  components/                # Sub-components\n  utils/                     # Utility functions\n```\n\n### Props Interface\n\n- Name as `ComponentNameProps`\n- Use separate interfaces for complex prop objects\n- Always use proper TypeScript types, never `any`\n\n### Imports Order in Components\n\n1. External dependencies (`react`, `redux`, etc.)\n2. Internal modules (aliases)\n3. Local imports (types, constants, hooks)\n4. Styles (always last: `import { Container } from './Component.styles'`)\n\n### Barrel Files\n\nUse barrel files (`index.ts`) only when exporting **3 or more** items. Make sure exports appear in only one barrel file, not propagated up the chain.\n\n## Styled Components\n\n**We are migrating to styled-components** (deprecating SCSS modules).\n\n### Encapsulate Styles in .styles.ts\n\nKeep all component styles in dedicated `.styles.ts` files using styled-components. Use PascalCase for the filename to match the component name:\n\n```\nComponentName/\n  ComponentName.tsx\n  ComponentName.styles.ts  # ✅ PascalCase\n  # Not component-name.styles.ts ❌\n```\n\n### Import Pattern\n\nKeep all component styles in dedicated .style.ts files and import them with a namespace.\n\n**CRITICAL: `import * as S` is reserved for local styles only** (e.g., from `ComponentName.styles.ts`). When you need to use styled components from external components, create a local styles file that re-exports them.\n\n#### ✅ Good\n\n```typescript\n// ComponentName.tsx\nimport * as S from './ComponentName.styles'\n\nreturn (\n  <S.Container>\n    <S.Title>Title</S.Title>\n    <S.Content>Content</S.Content>\n  </S.Container>\n)\n\n// ComponentName.styles.ts (when re-exporting from external component)\nexport { ExternalStyledComponent } from '../ExternalComponent/ExternalComponent.styles'\n```\n\n#### ❌ Bad\n\n```typescript\n// ❌ BAD: Importing styled components directly from external component\nimport * as S from '../ExternalComponent/ExternalComponent.styles';\n\n// ❌ BAD: Named imports instead of namespace\nimport { Container, Title, Content } from './Component.styles';\n```\n\n### Use Layout Components Instead of div\n\n**Prefer `FlexGroup` over `div`** when creating flex containers:\n\n```typescript\n// ✅ GOOD: Use FlexGroup\nimport { FlexGroup } from 'uiSrc/components/base/layout/flex'\n\nexport const Wrapper = styled(FlexGroup)`\n  user-select: none;\n`\n\n// Usage: Pass layout props as component props\n<Wrapper align=\"center\" justify=\"end\">\n  {children}\n</Wrapper>\n\n// ❌ BAD: Using div with hardcoded flex properties\nexport const Wrapper = styled.div`\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n`\n```\n\n### Pass Layout Props as Component Props\n\n**Don't hardcode layout properties** in styled components when using layout components like `FlexGroup`. Pass them as props instead:\n\n```typescript\n// ✅ GOOD: Pass props in JSX\nexport const Wrapper = styled(FlexGroup)`\n  user-select: none;\n`\n\n<Wrapper align=\"center\" justify=\"end\">\n  {children}\n</Wrapper>\n\n// ❌ BAD: Hardcoding in styled component\nexport const Wrapper = styled(FlexGroup)`\n  align-items: center;\n  justify-content: flex-end;\n  user-select: none;\n`\n```\n\n### Use Gap Prop Instead of Custom Margins\n\n**Prefer `gap` prop on layout components** instead of custom margins for spacing between elements:\n\n```typescript\n// ✅ GOOD: Use gap prop\n<Row align=\"center\" justify=\"between\" gap=\"l\">\n  <FlexItem>Item 1</FlexItem>\n  <FlexItem>Item 2</FlexItem>\n</Row>\n```\n\n### Use Theme Spacing Instead of Magic Numbers\n\n**Always use theme spacing values** instead of hardcoded pixel values:\n\n```typescript\n// ✅ GOOD: Use theme spacing\nexport const Container = styled(Row)`\n  height: ${({ theme }) => theme.core.space.space500};\n  padding: 0 ${({ theme }) => theme.core.space.space200};\n  margin-bottom: ${({ theme }) => theme.core.space.space200};\n`;\n\n// ❌ BAD: Using magic numbers\nexport const Container = styled(Row)`\n  height: 64px;\n  padding: 0 16px;\n  margin-bottom: 16px;\n`;\n```\n\n### Use Semantic Colors from Theme\n\n**Always use semantic colors** from the theme instead of CSS variables or hardcoded colors:\n\n```typescript\n// ✅ GOOD: Use semantic colors\nexport const Header = styled(Row)`\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral100};\n  border-bottom: 1px solid\n    ${({ theme }) => theme.semantic.color.border.neutral500};\n`;\n\n// ❌ BAD: Using deprecated EUI CSS variables\nexport const Header = styled(Row)`\n  background-color: var(--euiColorEmptyShade);\n  border-bottom: 1px solid var(--separatorColor);\n`;\n```\n\n### Use Layout Components (Row/Col/FlexGroup) Instead of div\n\n**Prefer layout components** from the layout system instead of regular `div` elements:\n\n```typescript\n// ✅ GOOD: Use Row component\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const PageHeader = styled(Row)`\n  height: ${({ theme }) => theme.core.space.space500};\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral100};\n`\n\n<PageHeader align=\"center\" justify=\"between\" gap=\"l\">\n  {children}\n</PageHeader>\n\n// ❌ BAD: Using div with flex properties\nexport const PageHeader = styled.div`\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  height: 64px;\n`\n```\n\n### Conditional Styling\n\nUse `$` prefix for transient props that shouldn't pass to DOM:\n\n```typescript\nexport const Button = styled.button<{ $isActive?: boolean }>`\n  background-color: ${({ $isActive }) => ($isActive ? '#007bff' : '#6c757d')};\n`;\n```\n\n### Avoid !important\n\n**Never use `!important` in styled-components**. Styled-components handles CSS specificity through component hierarchy. If you need to override styles, use more specific selectors or adjust the component structure:\n\n```typescript\n// ✅ GOOD: Rely on CSS specificity\nexport const IconButton = styled(IconButton)<{ isOpen: boolean }>`\n  ${({ isOpen }) =>\n    isOpen &&\n    css`\n      background-color: ${({ theme }) =>\n        theme.semantic.color.background.primary200};\n    `}\n`;\n\n// ❌ BAD: Using !important\nexport const IconButton = styled(IconButton)`\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.primary200} !important;\n`;\n```\n\n### Verify Type System Compatibility\n\nWhen using layout components or other typed components, verify your prop values match the type system:\n\n```typescript\n// Check the component's type definitions\n// FlexGroup accepts: align?: 'center' | 'stretch' | 'baseline' | 'start' | 'end'\n// Use valid values from the type system\n```\n\n## State Management (Redux)\n\n### When to Use What\n\n- **Global State (Redux)**:\n\n  - Data shared across multiple components\n  - Data persisting across routes\n  - Server state (API data)\n  - User preferences/settings\n\n- **Local State (useState)**:\n\n  - UI state (modals, dropdowns, tabs)\n  - Form inputs before submission\n  - Component-specific temporary data\n\n- **Derived State (Selectors)**:\n  - Computed values from Redux state\n  - Filtered/sorted lists\n  - Aggregated data\n\n### Redux Toolkit Patterns\n\n#### Slice Structure\n\n- Use `createSlice` from Redux Toolkit\n- Define proper TypeScript types for state\n- Use `PayloadAction<T>` for action typing\n- Handle async with `extraReducers` and thunks\n\n#### Thunks\n\n- Use `createAsyncThunk` for async operations\n- Handle pending, fulfilled, and rejected states\n- Use `rejectWithValue` for error handling\n\n#### Selectors\n\n- Create basic selectors for direct state access\n- Use `createSelector` from `reselect` for memoized/computed values\n- Keep selectors in separate `selectors.ts` file\n\n## React Best Practices\n\n### Performance\n\n- Use `useCallback` for functions passed as props\n- Use `useMemo` for expensive computations\n- Use `React.memo` for expensive components\n- Avoid inline arrow functions in JSX props\n\n### Effect Cleanup\n\nAlways clean up subscriptions, timers, and event listeners in `useEffect` return function.\n\n### Keys in Lists\n\n- Use unique, stable IDs (not array indices)\n- Only use indices if list never reorders and items have no IDs\n\n### Conditional Rendering\n\n- Use early returns for loading/error states\n- Avoid deeply nested ternaries - extract to functions\n\n## Custom Hooks\n\n### Extract Reusable Logic\n\nCreate custom hooks for reusable stateful logic. Store component-specific hooks in the component's `/hooks` directory.\n\n## Form Handling\n\nUse Formik with Yup for validation. Keep form logic in custom hooks when complex.\n\n## UI Components\n\n**⚠️ IMPORTANT**:\n\n- We are **deprecating Elastic UI** components\n- Migrating to **Redis UI** (`@redis-ui/*`)\n- **Use internal wrappers** from `uiSrc/components/ui`\n- **DO NOT import directly** from `@redis-ui/*`\n\n### Component Usage\n\n```typescript\n// ✅ GOOD: Import from internal wrappers\nimport { Button, Input, FlexGroup } from 'uiSrc/components/ui';\n\n// ❌ BAD: Don't import directly from @redis-ui\nimport { Button } from '@redis-ui/components';\n\n// ❌ DEPRECATED: Don't use Elastic UI for new code\nimport { EuiButton } from '@elastic/eui';\n```\n\n### Migration Guidelines\n\n- ✅ Use internal wrappers from `uiSrc/components/ui` for all new features\n- ✅ Create internal wrappers for Redis UI components as needed\n- ✅ Replace Elastic UI when touching existing code\n- ❌ Do not import directly from `@redis-ui/*`\n- ❌ Do not add new Elastic UI imports\n\n## Icons\n\n**⚠️ IMPORTANT**: Always use icons from Redis UI library instead of custom SVGs.\n\n### Icon Usage\n\n- **Use Redis UI icons** from `@redis-ui/icons` via `iconRegistry.tsx`\n- Icons are automatically exported via `export * from '@redis-ui/icons'` in `iconRegistry.tsx`\n- Use `RiIcon` component with icon type: `<RiIcon type=\"FolderOpenIcon\" />`\n- **DO NOT create custom SVG icons** - check if the icon exists in Redis UI library first\n\n### Custom Icons (Exception)\n\nOnly create custom SVG icons if:\n\n- The icon doesn't exist in Redis UI library\n- It's a project-specific icon that won't be added to the library\n\n## Testing Components\n\n### Always Use Shared `renderComponent` Helper\n\n**CRITICAL**: Create a `renderComponent` helper function for each component test file:\n\nPull entity data from a shared Fishery factory in `redisinsight/ui/src/mocks/factories/`\n(reuse one, or add a new `<domain>/<TypeName>.factory.ts`); inline only callbacks/primitives.\n\n```typescript\ndescribe('MyComponent', () => {\n  const mockUser = UserFactory.build();\n  const defaultProps: MyComponentProps = {\n    user: mockUser,\n    onComplete: jest.fn(),\n  }\n\n  const renderComponent = (propsOverride?: Partial<MyComponentProps>) => {\n    const props = { ...defaultProps, ...propsOverride }\n\n    return render(\n      <Provider store={store}>\n        <MyComponent {...props} />\n      </Provider>\n    )\n  }\n\n  it('should render', () => {\n    renderComponent()\n    // assertions\n  })\n})\n```\n\nBenefits:\n\n- Centralized setup and providers\n- Default props in one place\n- Easy prop overrides per test\n- No duplicate render logic\n\n### Testing Redux\n\nCreate a test store with `configureStore` for components connected to Redux.\n\n## Key Principles\n\n1. **Separation of Concerns**: Keep styles, types, constants, logic separate\n2. **Colocate Related Code**: Keep sub-components and hooks close to usage\n3. **Consistent Naming**: Follow conventions across all components\n4. **Type Safety**: Always define proper types, never `any`\n5. **Testability**: Structure for easy testing with `renderComponent` helper\n6. **Styled Components**: Prefer styled-components over SCSS modules\n7. **Layout Components**: Use FlexGroup instead of div for flex containers, pass layout props as component props\n8. **Type Safety**: Verify prop values match component type definitions (e.g., FlexGroup's align/justify values)\n",".ai/skills/git-safety/SKILL.md":"---\nname: git-safety\ndescription: >-\n  Critical safety guardrails for protected branches: never commit,\n  push, or force-push directly to main, latest, or release branches;\n  no destructive history rewrites without explicit user approval.\n  Use before any git operation that touches protected branches,\n  before force-push, reset --hard, history rewrite, or branch\n  deletion, or whenever the user asks about merging, pushing, or\n  release branches.\n---\n\n\n# Git Safety Rules for AI Agents\n\n## 🚫 CRITICAL: Protected Branch Rules\n\n**AI agents must NEVER commit to or push to protected branches under any circumstances.**\n\n### Protected Branches\n\n- `main` - Primary production branch\n- `latest` - Latest stable release\n- `release/*` - Release branches (e.g., `release/v2.0.0`)\n\nThis is a non-negotiable rule that applies to all scenarios:\n\n### Prohibited Actions\n\n- ❌ **Direct commits** - Never run `git commit` while on a protected branch\n- ❌ **Direct pushes** - Never run `git push origin <protected-branch>` or `git push` while on a protected branch\n- ❌ **Force pushes** - Never run `git push --force` or `git push -f` targeting protected branches\n- ❌ **Merging into protected branches locally** - Never run `git merge <branch>` while on a protected branch\n- ❌ **Rebasing protected branches** - Never run `git rebase` while on a protected branch\n- ❌ **Resetting protected branches** - Never run `git reset` while on a protected branch\n\n### Required Workflow\n\n1. **Always create a feature branch** before making any changes\n2. **Verify current branch** before any git operation using `git branch --show-current`\n3. **Create Pull Requests** for all changes - let the review process handle merging\n\n### Pre-Push Checklist\n\nBefore executing any push command, AI agents must:\n\n1. ✅ Confirm current branch is NOT a protected branch (`main`, `latest`, `release/*`)\n2. ✅ Verify the remote and branch target\n\n### Error Recovery\n\nIf accidentally on a protected branch with uncommitted changes:\n\n1. Stash changes: `git stash`\n2. Create new branch: `git checkout -b <appropriate-branch-name>`\n3. Apply changes: `git stash pop`\n4. Continue work on the new branch\n\n## Rationale\n\n- Protected branches represent production-ready or release code\n- All changes must go through code review via Pull Requests\n- Direct pushes bypass CI/CD checks and team review\n- Mistakes on protected branches can affect the entire team and deployment pipeline\n",".ai/skills/pull-requests/SKILL.md":"---\nname: pull-requests\ndescription: >-\n  Create and review pull requests following project standards including title\n  format, description template, and review checklist. Use when creating PRs,\n  writing PR descriptions, or reviewing pull requests.\n---\n\n# Pull Request Guidelines\n\n## Creating a PR\n\n### PR Title\n\nInclude issue number at the start:\n\n```\nRI-123 Add user profile editing\n#456 Fix memory leak in connection pool\n```\n\n### PR Description Template\n\n```markdown\n# What\n\nDescribe what was changed.\n\n# Testing\n\nDescribe how to test the changes.\n\n---\n\nRefs #RI-123\n```\n\n**PR Description Guidelines:**\n\n- **Keep it concise** - Avoid verbose descriptions\n- **Focus on high-level changes** - Don't list every code change in the #What section\n- **Brief and to the point** - The diff shows the details; describe the \"why\" and \"what\" at a high level\n- **Technical decisions** - Only mention significant architectural or design decisions if relevant\n- **Link, don't auto-close** - Use `Refs #RI-123` / `Addresses #RI-123`, not `Closes`/`Fixes`/`Resolves` - those keywords auto-close the issue when the PR merges, and tickets should be closed manually, not by the merge\n\n## Review Process\n\n### As PR Author\n\n- **Respond to all comments** - Address every piece of feedback\n- **Don't take feedback personally** - Reviews improve code quality\n- **Update code based on feedback** - Make requested changes\n- **Mark conversations as resolved** - After addressing feedback\n- **Keep PR up to date** - Rebase on main regularly\n\n### As PR Reviewer\n\n- **Be constructive and respectful** - Focus on improvement\n- **Focus on logic, not style** - Linter handles formatting\n- **Check for**:\n  - Logic errors and edge cases\n  - Performance issues\n  - Security concerns\n  - Test coverage\n  - Missing documentation\n  - Architectural concerns\n\n### Review Checklist\n\n- [ ] Code follows project patterns\n- [ ] Tests are comprehensive\n- [ ] No console.log or debug code\n- [ ] TypeScript types are proper\n- [ ] Error handling is adequate\n- [ ] Documentation is updated\n- [ ] No security vulnerabilities\n- [ ] Performance is acceptable\n",".ai/skills/redis-insight-plugin/SKILL.md":"---\nname: redis-insight-plugin\ndescription: Use when creating, modifying, debugging, deploying, or testing Redis Insight Workbench visualization plugins, plugin manifests, package.json visualizations, activationMethod functions, redisinsight-plugin-sdk usage, Parcel/Vite plugin builds, iframe rendering, Redis command parsing, Docker RedisInsight deployment, /api/plugins verification, or Playwright plugin validation.\nlicense: MIT\nmetadata:\n  author: redis\n  version: \"1.0.0\"\n---\n# Redis Insight Workbench Plugin\n\nBuild, deploy, and validate Redis Insight Workbench visualization plugins. Plugins render inside an iframe in Workbench and visualize the result of a Redis command. Trigger this skill for plugin manifests, `package.json` `visualizations`, `activationMethod` functions, `redisinsight-plugin-sdk` usage, Parcel/Vite plugin builds, iframe rendering, Redis command parsing, Docker RedisInsight deployment, `/api/plugins` verification, and Playwright plugin tests.\n\nUse the `redis-ui-components` skill for every visual plugin UI. RedisInsight plugins use the RedisInsight product theme pair: `light` / `dark`, not `light2` / `dark2`.\n\nOfficial source-of-truth references:\n\n- In-repo canonical docs: [docs/plugins/development.md](../../../docs/plugins/development.md), [docs/plugins/installation.md](../../../docs/plugins/installation.md), [docs/plugins/introduction.md](../../../docs/plugins/introduction.md)\n- Upstream: https://github.com/redis/RedisInsight/tree/main/docs/plugins\n\nSee [references/official-docs-summary.md](references/official-docs-summary.md) for a condensed summary.\n\n## Repo Code Conventions (internal plugins)\n\n> **You are inside the RedisInsight repo.** An internal plugin lives in the `redisinsight/ui/`\n> tree, so its code must follow the same styleguides as the rest of the UI — not ad hoc plugin\n> code. These rules are **mandatory** for any plugin code written here and override generic\n> external-plugin guidance below where they conflict:\n>\n> - **[frontend](../frontend/SKILL.md)** — component folder structure\n>   (`ComponentName/ComponentName.tsx` + `.styles.ts` + `.types.ts` + `.spec.tsx`), functional\n>   components with hooks, **named exports**, barrel files, layout components\n>   (`Row` / `Col` / `FlexGroup`) instead of raw `div`, and theme usage.\n> - **[redis-ui-components](../redis-ui-components/)** — build all plugin UI from Redis UI\n>   components. Import the internal `uiSrc/components/ui` wrappers; **never** import raw\n>   `@redis-ui/*`. (This skill is a symlink into the installed `@redis-ui/components` package,\n>   so it resolves after `npm install`; if it is missing, run install — the canonical source is\n>   `node_modules/@redis-ui/components/skills/redis-ui-components/`.)\n> - **[code-quality](../code-quality/SKILL.md)** — TypeScript everywhere (no `any`), naming\n>   (`PascalCase` / `camelCase` / `UPPER_SNAKE_CASE`), import order, no magic numbers, no\n>   `!important` in styles, semantic theme colors over CSS variables.\n> - **[testing](../testing/SKILL.md)** — Jest + Testing Library, the `renderComponent` helper,\n>   `faker` for test data, `waitFor` instead of fixed time waits.\n> - **[e2e-testing](../e2e-testing/SKILL.md)** — any Playwright/E2E test follows this skill\n>   (tests in `tests/e2e-playwright/`, page objects, fixtures, UI navigation; never `page.goto()`\n>   directly, no CSS selectors or fixed waits).\n>\n> External standalone plugins (below) are bundled in isolation and **cannot** import these\n> internals; they emulate the conventions with local code instead.\n\n## First Decision: Plugin Type\n\nDecide before scaffolding anything else.\n\n- **Internal monorepo plugin** — lives inside `redisinsight/ui/src/packages/<plugin-name>/` and ships with Redis Insight itself. Build with **Vite** (shared config). Follow the repo styleguides above. **This is the default for any contribution to this repo** — copy a sibling package such as `geodata` or `redisearch` rather than diverging.\n- **External standalone plugin** — installed by a user into `~/.redis-insight/plugins/<name>/`. Build with **Parcel**. Bundle all dependencies. Do not import from `uiSrc/` or any RedisInsight monorepo internal. Use this only for customer/field/demo plugins that ship *outside* this repo.\n\nSee [references/internal-vite-plugin.md](references/internal-vite-plugin.md) and [references/external-parcel-plugin.md](references/external-parcel-plugin.md).\n\n## External Plugin Structure\n\n```\n<plugin-name>/\n  package.json              # manifest + build scripts\n  src/\n    index.html              # iframe entry, has #app\n    main.tsx                # activation functions, default export\n    components/\n    styles/\n      styles.scss\n  dist/\n    index.js                # built bundle (referenced by manifest \"main\")\n    styles.css              # built styles (referenced by manifest \"styles\")\n```\n\n## Required Manifest\n\nTop-level `package.json` fields:\n\n- `name`\n- `version`\n- `description`\n- `main` — path to built JS (e.g. `./dist/index.js`).\n- `styles` — path to built CSS (e.g. `./dist/styles.css`).\n- `visualizations` — array of visualization descriptors.\n\nEach visualization descriptor must include:\n\n- `id`\n- `name`\n- `activationMethod`\n- `matchCommands`\n- `description`\n- `default`\n\nSet `default: false` unless the user explicitly asks for it to be the default visualization.\n\nThe `activationMethod` value must **exactly** match an exported function name in the bundle. The plugin entry must export that function via the default export:\n\n```ts\nexport default { renderMyView };\n```\n\nMultiple visualizations:\n\n```ts\nexport default {\n  renderTableView,\n  renderChartView,\n};\n```\n\nSee [references/plugin-manifest.md](references/plugin-manifest.md) for full examples and how to strip dev-only fields from the deployed manifest.\n\n## Activation Function Contract\n\nEvery activation function must:\n\n1. Get the host element: `const root = document.getElementById('app');`\n2. Defensively validate props (`command`, `data`, modules, theme).\n3. Wrap render logic in `try/catch` and render an error state on failure.\n4. Render an empty state when `data` is missing or empty.\n5. Log with a plugin-specific prefix, e.g. `[MY_PLUGIN]`, never bare `console.log`.\n\nSee [references/error-handling.md](references/error-handling.md).\n\n## RedisInsight Product UI Contract\n\nEvery plugin UI must follow RedisInsight product styling:\n\n- For **internal** plugins, build the UI from the [redis-ui-components](../redis-ui-components/) skill and the `uiSrc/components/ui` wrappers, following the [frontend](../frontend/SKILL.md) styleguide.\n- For **external** plugins, mirror the same Redis UI look with local code (you cannot import the internals).\n- Use RedisInsight `light` / `dark` product themes.\n- For standalone external plugins, emulate the RedisInsight product tokens with local CSS variables; do not import RedisInsight monorepo internals or `@redis-ui/*`.\n- Detect iframe theme through `theme_LIGHT` / `theme_DARK` body classes or SDK theme helpers.\n- Use compact product components: table, toolbar, segmented states, empty/error/loading panels, badges, and inspector-style details.\n- Keep Redis brand red for brand moments only; do not use it as the default plugin CTA, heading, error, or status color.\n\nSee [references/redisinsight-product-ui.md](references/redisinsight-product-ui.md) and use [templates/external-styles.scss](templates/external-styles.scss) as the baseline `src/styles/styles.scss`.\n\n## Mandatory Phased Workflow\n\nBuild every new plugin in three phases. Do not skip phases — the failure mode in each phase tells you exactly what is wrong.\n\n- **Phase 1 — Vanilla wiring.** No React, no third-party libraries. Render plain DOM that proves activation, props, and iframe rendering work end-to-end.\n- **Phase 2 — React rendering.** Add React + ReactDOM. Render a typed component that displays `command`, `status`, and the raw response.\n- **Phase 3 — Full feature.** Add the actual visualization library (charting, mapping, grid, etc.) and the real UX.\n\nSee [references/iterative-development.md](references/iterative-development.md) and the templates in `templates/`.\n\n## Review Hardening Loop\n\nBefore asking for review, run a small adversarial pass against the exact surfaces the plugin touches:\n\n- Manifest matching/defaults: exact command boundaries, no default visualization conflicts, no regex backtracking traps.\n- Redis command parsing: token-aware option handling, raw-unit preservation, malformed rows, empty rows, keyword-like key/member names.\n- Visualization state: stale closures, safe library bounds/inputs, large result sets, mode-specific empty/error copy.\n- Tests: red regression first, then the smallest package/component/parser test set that proves the fix.\n\nSee [references/review-hardening.md](references/review-hardening.md).\n\n## Build and Verify\n\n```bash\nnpm run build\ntest -f dist/index.js\ntest -f dist/styles.css                       # if \"styles\" is declared\ngrep -c \"process.env\" dist/index.js           # must be 0 in a Parcel build\n```\n\nConfirm each `activationMethod` name appears in the bundle:\n\n```bash\ngrep -o \"renderMyView\" dist/index.js | head\n```\n\n## Deploy\n\nExternal plugin → user plugins folder:\n\n```bash\nmkdir -p ~/.redis-insight/plugins/<plugin-name>\ncp package.json ~/.redis-insight/plugins/<plugin-name>/\ncp -R dist ~/.redis-insight/plugins/<plugin-name>/dist\n```\n\nRestart Redis Insight, then verify:\n\n```bash\ncurl -s http://localhost:5540/api/plugins\n```\n\nThe response must include the plugin `name` and its visualizations. See [references/testing-and-deployment.md](references/testing-and-deployment.md) for Docker workarounds, Playwright smoke tests, and the static-plugin path inside the Docker image.\n\n## Security Rules\n\n- Plugins execute code in the Insight UI process. Only ship code you control or trust.\n- Never embed secrets, tokens, or credentials in the bundle.\n- No hidden network calls. Document any outbound HTTP and prefer none.\n- Default to read-only Redis commands (`HGETALL`, `LRANGE`, `XRANGE`, `INFO`, `FT.SEARCH`). Never run destructive commands (`FLUSHDB`, `DEL`, `UNLINK`, `XTRIM`, `CLUSTER RESET`, `CONFIG SET`) without explicit user request and confirmation.\n\n## DO NOT\n\n- DO NOT skip Phase 1 or Phase 2; each catches a different class of failure.\n- DO NOT deploy `index.js` / `styles.css` at the plugin root. They live in `dist/` and the manifest points at `./dist/...`.\n- DO NOT set `default: true` on a visualization unless the user asked for it.\n- DO NOT include `scripts` or `devDependencies` in the deployed manifest. Strip them before copying.\n- DO NOT use Vite for standalone external plugins. Use Parcel.\n- DO NOT import from `uiSrc/`, `@redis-ui/*`, or any RedisInsight monorepo internal in a standalone plugin.\n- DO NOT externalize React in a standalone plugin bundle. Bundle React and ReactDOM.\n- DO NOT style plugin UI with ad hoc inline brand colors. Use RedisInsight product UI variables/classes.\n- DO NOT use `light2` / `dark2` for RedisInsight plugins; those are for other Redis product UIs.\n- DO NOT skip `/api/plugins` verification after deploying.\n- DO NOT ship `process.env.*` references in the bundle. Replace at build time.\n- DO NOT assume one Redis response shape — different commands, and even the same command with different flags (e.g. a `WITH...` modifier), return very different structures. See [references/redis-command-parsing.md](references/redis-command-parsing.md).\n\n## Final Checklist\n\n- Plugin type chosen and matches build tool (Parcel = external, Vite = internal).\n- `package.json` declares `main`, `styles`, and `visualizations` with required fields.\n- Every `activationMethod` matches a default-exported function.\n- Phases 1, 2, 3 each rendered successfully before moving on.\n- RedisInsight product UI applied via the `redis-ui-components` skill (internal plugins) or emulated locally (external plugins), with `light` / `dark` theme handling.\n- Review hardening pass completed for manifest matching, command parsing, visualization state, and scoped tests.\n- Bundle verified: `dist/index.js`, `dist/styles.css`, no `process.env`.\n- Plugin deployed to `~/.redis-insight/plugins/<name>/` (or via the Docker workaround).\n- `curl http://localhost:5540/api/plugins` lists the plugin.\n- Workbench runs a matching command and renders the visualization.\n- Defensive empty/error states verified.\n- Optional Playwright smoke test passes (written per the [e2e-testing](../e2e-testing/SKILL.md) skill).\n\n## Reference Index\n\n| File | Load When |\n|------|-----------|\n| [official-docs-summary.md](references/official-docs-summary.md) | Need the canonical contract from Redis Insight docs. |\n| [redis-insight-plugin-guidelines.md](references/redis-insight-plugin-guidelines.md) | Need the long-form operational reference. |\n| [external-parcel-plugin.md](references/external-parcel-plugin.md) | Building a standalone plugin with Parcel. |\n| [internal-vite-plugin.md](references/internal-vite-plugin.md) | Building inside the RedisInsight monorepo with Vite. |\n| [plugin-manifest.md](references/plugin-manifest.md) | Writing or stripping `package.json` manifests. |\n| [iterative-development.md](references/iterative-development.md) | Phase 1/2/3 templates and pipeline. |\n| [redisinsight-product-ui.md](references/redisinsight-product-ui.md) | Applying RedisInsight product UI inside plugin iframes. |\n| [review-hardening.md](references/review-hardening.md) | Pre-review checklist for matcher, parser, visualization state, and scoped regression tests. |\n| [testing-and-deployment.md](references/testing-and-deployment.md) | Deploy paths, Docker workaround, `/api/plugins`, Playwright. |\n| [redis-command-parsing.md](references/redis-command-parsing.md) | Parsing raw Redis command responses defensively. |\n| [third-party-libraries.md](references/third-party-libraries.md) | Integrating a visualization library, custom `.d.ts`, bundle size. |\n| [error-handling.md](references/error-handling.md) | Defensive render, ErrorBoundary, log prefixes. |\n",".ai/skills/redis-insight-plugin/references/error-handling.md":"# Error Handling and Stability\n\nPlugins run inside the Insight iframe. A thrown exception during activation produces a blank panel with no visible cause. Defensive rendering is mandatory.\n\n## Defensive Render Pattern\n\n```ts\nconst PREFIX = '[MY_PLUGIN]';\n\nexport function renderView(props: any) {\n  const host = document.getElementById('app');\n  if (!host) {\n    console.error(PREFIX, '#app missing');\n    return;\n  }\n  try {\n    const rows = parseData(props?.data ?? []);\n    if (rows.length === 0) {\n      host.innerHTML = '<div class=\"empty\">No data to display.</div>';\n      return;\n    }\n    renderTable(host, rows, props);\n  } catch (err) {\n    console.error(PREFIX, 'render failed', err);\n    host.innerHTML = `\n      <div class=\"error\">\n        <p>Plugin failed to render. See devtools console.</p>\n        <pre>${escape(String((err as Error)?.message ?? err))}</pre>\n      </div>\n    `;\n  }\n}\n```\n\nRules:\n\n- Always check `#app` exists.\n- Always check the data shape before rendering.\n- Always wrap render logic in `try/catch`.\n- Render an empty state and an error state — never a blank iframe.\n- Make empty/error copy match the active visualization mode. Each mode in a multi-visualization plugin should show its own message, not a single generic one.\n- Remove unreachable fallback branches after parser state is narrowed. Dead defensive checks hide the true state model.\n\n## React ErrorBoundary\n\nFor React-rendered plugins, wrap the root component:\n\n```tsx\nclass PluginErrorBoundary extends React.Component<{ children: React.ReactNode }, { error: Error | null }> {\n  state = { error: null as Error | null };\n  static getDerivedStateFromError(error: Error) { return { error }; }\n  componentDidCatch(error: Error, info: React.ErrorInfo) {\n    console.error('[PLUGIN]', 'render error', error, info);\n  }\n  render() {\n    if (this.state.error) {\n      return <div className=\"error\"><pre>{String(this.state.error.message)}</pre></div>;\n    }\n    return this.props.children;\n  }\n}\n```\n\nWrap once at the plugin root, not around every leaf component.\n\n## Logging Prefixes\n\nPrefix every log line with a stable, unique tag:\n\n- `[MY_PLUGIN]`\n- `[STREAM_PLUGIN]`\n- `[SEARCH_PLUGIN]`\n\nWhy:\n\n- Devtools filtering becomes a one-step regex.\n- Multiple plugins in the same Insight session can be told apart.\n- Customer logs forwarded to support are immediately attributable.\n\nNever use bare `console.log(data)` in shipped plugins.\n\n## State Persistence\n\nPrefer the SDK first, `localStorage` fallback:\n\n```ts\nimport { getState, setState } from 'redisinsight-plugin-sdk';\n\nasync function loadSettings() {\n  try {\n    return (await getState()) ?? {};\n  } catch {\n    try {\n      return JSON.parse(localStorage.getItem('ri:my-plugin:settings') ?? '{}');\n    } catch {\n      return {};\n    }\n  }\n}\n\nasync function saveSettings(s: object) {\n  try { await setState(s); }\n  catch { localStorage.setItem('ri:my-plugin:settings', JSON.stringify(s)); }\n}\n```\n\nValidate persisted state before applying — fields can disappear or change shape between plugin versions.\n\n## Common Issues\n\n| Issue | Likely cause | Fix |\n|-------|--------------|-----|\n| Blank iframe, no logs | Activation function not exported | Ensure `export default { fn }` and name matches manifest. |\n| `process.env is not defined` | Bundler left env refs | Replace at build time; verify `grep -c process.env dist/index.js` is `0`. |\n| `require is not defined` | Externalized React | `targets.module.includeNodeModules: true`; rebuild. |\n| Visualization renders but is empty | Parser returned empty | Log raw `props.data`; check command modifier flags. |\n| Values mapped to wrong field/axis | Response shape assumption wrong | Branch on the actual runtime shape, not the command name. |\n| Theme looks wrong | No `theme_DARK` handling | Read `document.body.classList` or `getTheme()` from SDK. |\n| Plugin disappears after Insight upgrade | Manifest field changed | Re-read official docs and update `package.json`. |\n| Large bundle, slow load | Missing minify, full lodash | Run `npm run minify:js`; switch to scoped imports. |\n",".ai/skills/redis-insight-plugin/references/external-parcel-plugin.md":"# External Parcel Plugin\n\nBuild a standalone Redis Insight plugin with Parcel.\n\n## Folder Layout\n\n```\n<plugin-name>/\n  package.json\n  src/\n    index.html\n    main.tsx\n    components/\n    styles/\n      styles.scss\n  dist/                     # build output, gitignored except when deploying\n    index.js\n    styles.css\n  scripts/\n    verify-plugin.sh\n    deploy-external.sh\n```\n\n## Build Tool: Parcel\n\nWhy Parcel:\n\n- Zero-config TS/SCSS/asset handling.\n- Simple `targets` model maps cleanly to \"single bundled JS + CSS\".\n- `targets.module.includeNodeModules: true` bundles every dep, which is what an external plugin needs.\n\nWhy **not** Vite for external plugins:\n\n- Vite assumes ESM consumers and multi-file output. Insight expects a single `dist/index.js`.\n- Vite's externalization defaults can leave React unbundled — fatal for an iframe with no shared deps.\n\n## Package Scripts\n\nRecommended scripts (see [../templates/external-parcel-package.json](../templates/external-parcel-package.json)):\n\n```json\n\"scripts\": {\n  \"start\":   \"parcel src/index.html\",\n  \"build\":   \"concurrently \\\"npm run build:js\\\" \\\"npm run build:css\\\"\",\n  \"build:js\":\"parcel build src/main.tsx --no-source-maps --dist-dir dist --target module\",\n  \"build:css\":\"parcel build src/styles/styles.scss --no-source-maps --dist-dir dist\",\n  \"minify:js\":\"terser dist/index.js -o dist/index.js -c -m\",\n  \"clean\":   \"rimraf dist\",\n  \"verify\":  \"bash scripts/verify-plugin.sh\",\n  \"deploy:external\":\"bash scripts/deploy-external.sh\",\n  \"deploy:internal\":\"bash scripts/deploy-internal-docker.sh\"\n}\n```\n\n## Targets\n\n```json\n\"targets\": {\n  \"main\": false,\n  \"module\": {\n    \"includeNodeModules\": true,\n    \"outputFormat\": \"esmodule\",\n    \"isLibrary\": false\n  }\n}\n```\n\n- `main: false` disables Parcel's CommonJS output.\n- `includeNodeModules: true` inlines every dependency.\n- Do **not** add `\"engines\": { \"browsers\": \"...\" }` so narrow that React or your visualization library break — Insight bundles a modern Chromium.\n\n## Bundling Rules\n\n- Bundle React, ReactDOM, and every other runtime dependency (including the visualization library).\n- No `peerDependencies`. Insight does not provide shared deps to plugins.\n- No imports from `uiSrc/`, `@redis-ui/*`, or any RedisInsight monorepo package.\n- No `process.env.*` references in the bundle. Replace with constants at build time. Verify with:\n\n  ```bash\n  grep -c \"process.env\" dist/index.js   # must be 0\n  ```\n\n- Minify before deploying customer-facing plugins; skip in dev for readable stack traces.\n\n## Source vs Deployed Manifest\n\nThe source `package.json` includes `scripts`, `devDependencies`, `targets`, etc. The deployed manifest must include only:\n\n- `name`, `version`, `description`\n- `main`, `styles`\n- `visualizations`\n- runtime `dependencies` (optional, informational)\n\nStrip the rest before copying into `~/.redis-insight/plugins/<name>/`. The deploy script in `templates/deploy-external.sh` does this for you (or use `jq` to filter).\n\n## Outputs\n\nAfter `npm run build`:\n\n- `dist/index.js` — single bundled module.\n- `dist/styles.css` — single stylesheet.\n\nIf your plugin has no styles, you can omit the `styles` manifest field and the SCSS source — but most plugins need styles.\n\nFor RedisInsight product UI fidelity, copy `templates/external-styles.scss` to `src/styles/styles.scss`. Keep plugin styles scoped under `.ri-plugin-*` classes and use `theme_LIGHT` / `theme_DARK` body classes for light/dark mode.\n\n## Verification\n\n```bash\nnpm run build\nnpm run verify     # runs templates/verify-plugin.sh\n```\n\n`verify` should report:\n\n- `package.json` present and parseable.\n- `dist/index.js` present and non-empty.\n- `dist/styles.css` present (if `styles` is declared).\n- Zero `process.env` references in the bundle.\n- Each declared `activationMethod` name appears in the bundle.\n- Bundle size under your project's threshold.\n",".ai/skills/redis-insight-plugin/references/internal-vite-plugin.md":"# Internal Vite Plugin\n\nBuild a Redis Insight plugin **inside** the RedisInsight monorepo. **This is the default for\nany contribution to this repo.** If you are not working inside the RedisInsight repo, use\n[external-parcel-plugin.md](external-parcel-plugin.md) instead.\n\n> **Start by copying a sibling.** Do not scaffold from scratch — copy an existing package such\n> as `geodata` or `redisearch` from `redisinsight/ui/src/packages/` and adapt it. Match the\n> surrounding conventions exactly rather than diverging.\n\n## Follow the repo styleguides\n\nInternal plugin code lives in the `redisinsight/ui/` tree, so it must follow the same\nstyleguides as the rest of the UI (these are mandatory, not optional):\n\n- **[frontend](../../frontend/SKILL.md)** — component folder structure\n  (`ComponentName/ComponentName.tsx` + `.styles.ts` + `.types.ts` + `.spec.tsx`), functional\n  components with hooks, **named exports**, barrel files (`src/components/index.ts`), layout\n  components (`Row` / `Col` / `FlexGroup`) instead of raw `div`, and theme usage.\n- **[redis-ui-components](../../redis-ui-components/)** — build all plugin UI from Redis UI\n  components via the `uiSrc/components/ui` wrappers; never import raw `@redis-ui/*`.\n- **[code-quality](../../code-quality/SKILL.md)** — no `any`, naming conventions, import order,\n  no magic numbers, no `!important`, semantic theme colors.\n- **[testing](../../testing/SKILL.md)** — Jest + Testing Library, `renderComponent`, `faker`,\n  `waitFor` over fixed waits.\n\n## Path\n\n```\nredisinsight/ui/src/packages/<plugin-name>/\n  package.json              # manifest (source/main/styles/visualizations) + scripts\n  index.html                # iframe entry with <div id=\"app\">\n  jest.config.cjs\n  tsconfig.json\n  public/                   # icon SVGs (copied to dist by the shared build)\n  src/\n    main.tsx                # exports activation functions (default export)\n    App.tsx / App.spec.tsx\n    components/             # ComponentName/ dirs + index.ts barrel\n    constants/             # index.ts barrel\n    types/                 # index.ts barrel\n    utils/                 # parsers + *.spec.ts\n    styles/styles.scss\n    global.d.ts\n    jest.setup.ts\n```\n\nSee any sibling under `redisinsight/ui/src/packages/` for a complete, current example\n(`redisearch` is a good table plugin; `geodata` is a good multi-visualization one).\n\n## Build Tool: Vite (shared config)\n\nThere is **no per-package `vite.config.ts`**. All internal plugins share a single config at\n`redisinsight/ui/src/packages/vite.config.mjs` and are listed in its `riPlugins` array. To add\na new plugin you register it there:\n\n```js\n// redisinsight/ui/src/packages/vite.config.mjs\nconst riPlugins = [\n  { name: 'redisearch', entry: 'src/main.tsx' },\n  // ...\n  { name: '<plugin-name>', entry: 'src/main.tsx' },  // add your plugin\n];\n```\n\nEach package's `package.json` wires the standard scripts:\n\n```json\n{\n  \"source\": \"./src/main.tsx\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"scripts\": {\n    \"dev\": \"vite -c ../vite.config.mjs\",\n    \"test\": \"node ../../../../../node_modules/.bin/jest -c jest.config.cjs\",\n    \"typecheck\": \"node ../../../../../node_modules/.bin/tsc --project tsconfig.json --noEmit\"\n  }\n}\n```\n\nThe shared build produces `dist/index.js` and `dist/styles.css` (and copies `public/` icons to\n`dist/`), all referenced from the manifest.\n\n## Manifest fields used in-repo\n\nIn addition to the base fields in [plugin-manifest.md](plugin-manifest.md), in-repo\nvisualizations commonly use:\n\n- `matchQuery.anyRegex` / `matchQuery.noneRegex` — refine matching beyond `matchCommands` (e.g.\n  only activate when a specific modifier flag is present in the command text).\n- `iconDark` / `iconLight` — `./dist/<icon>.svg` paths (source SVGs live in `public/`).\n\n## index.html\n\n```html\n<!doctype html>\n<html>\n  <head><meta charset=\"utf-8\" /></head>\n  <body class=\"theme_LIGHT\">\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./src/main.tsx\"></script>\n  </body>\n</html>\n```\n\n## Theme and Shared UI Caveats\n\n- Use Redis UI components and the theme tokens via the repo's `uiSrc/components/ui` wrappers per\n  the [redis-ui-components](../../redis-ui-components/) and [frontend](../../frontend/SKILL.md)\n  skills.\n- Detect iframe theme through the `theme_LIGHT` / `theme_DARK` body classes (or the\n  `redisinsight-plugin-sdk` theme helper).\n- Do not import from deep relative paths (`../../../../../`); use the monorepo's package alias.\n\n## Internal Plugin DO NOT\n\n- DO NOT introduce a Parcel build or a per-package `vite.config.ts` inside the monorepo. Register\n  in the shared `vite.config.mjs`.\n- DO NOT diverge from the runtime-dependency handling of sibling packages — copy how `geodata`\n  declares `react`/`react-dom` and externalization, don't invent your own.\n- DO NOT import from a sibling internal plugin — use shared utilities (`packages/common`) only.\n- DO NOT mutate global window state outside of the documented `window.state` surface.\n\n## When to Convert Internal → External\n\nIf the plugin is meant to ship outside RedisInsight (customer demo, field use, GitHub release),\nport it to an external Parcel layout. Keep the React component code; replace the build tool,\nbundle every dependency, and follow [external-parcel-plugin.md](external-parcel-plugin.md).\n",".ai/skills/redis-insight-plugin/references/iterative-development.md":"# Iterative Plugin Development\n\nBuild every Redis Insight plugin in three phases. The phases exist because each one isolates a different class of failure. Skipping is how you end up with a blank iframe and no idea why.\n\n## Phase 1 — Vanilla JS Wiring\n\nGoal: prove activation, props, and iframe rendering work. No React, no third-party libraries.\n\nUse the RedisInsight product UI baseline from `templates/external-styles.scss`; Phase 1 can be vanilla DOM without falling back to inline brand colors.\n\n```ts\n// src/main.tsx (or main.js)\nconst PREFIX = '[EXAMPLE_PLUGIN]';\n\nexport function renderExampleView(props: any) {\n  console.log(PREFIX, 'activated', props);\n  const root = document.getElementById('app');\n  if (!root) {\n    console.error(PREFIX, '#app missing');\n    return;\n  }\n  root.innerHTML = `\n    <section class=\"ri-plugin-shell\">\n      <div class=\"ri-plugin-panel\">\n        <header class=\"ri-plugin-header\">\n          <h2 class=\"ri-plugin-title\">Plugin works</h2>\n          <span class=\"ri-plugin-badge ri-plugin-badge--success\">Active</span>\n        </header>\n        <div class=\"ri-plugin-content\">\n          <code class=\"ri-plugin-command\">${escape(String(props?.command ?? ''))}</code>\n          <pre class=\"ri-plugin-code\">${escape(JSON.stringify(props?.data, null, 2))}</pre>\n        </div>\n      </div>\n    </section>\n  `;\n}\n\nfunction escape(s: string) {\n  return s.replace(/[&<>]/g, c => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;' }[c] as string));\n}\n\nexport default { renderExampleView };\n```\n\nFailure modes Phase 1 surfaces:\n\n- `activationMethod` name typo in the manifest.\n- Wrong `main` path or missing `dist/index.js`.\n- `process.env` references in the bundle.\n- Plugin folder not at `~/.redis-insight/plugins/<name>/`.\n- Insight not seeing the plugin (`/api/plugins`).\n\nIf Phase 1 doesn't render \"PLUGIN WORKS\" with the command and raw JSON, do not move on.\n\n## Phase 2 — React Rendering\n\nGoal: prove React mounts inside the iframe and displays props.\n\n```tsx\nimport * as React from 'react';\nimport { createRoot, type Root } from 'react-dom/client';\n\nconst PREFIX = '[EXAMPLE_PLUGIN]';\n\n// Workbench can re-activate the same iframe. Reuse one root per host element;\n// calling createRoot() twice on the same node warns and breaks rendering.\nconst roots = new WeakMap<HTMLElement, Root>();\n\nfunction getRoot(host: HTMLElement): Root {\n  let root = roots.get(host);\n  if (!root) {\n    root = createRoot(host);\n    roots.set(host, root);\n  }\n  return root;\n}\n\nfunction ExampleApp({ command, data }: { command: string; data: unknown }) {\n  return (\n    <section className=\"ri-plugin-shell\">\n      <div className=\"ri-plugin-panel\">\n        <header className=\"ri-plugin-header\">\n          <h2 className=\"ri-plugin-title\">Example Plugin</h2>\n          <span className=\"ri-plugin-badge ri-plugin-badge--success\">Active</span>\n        </header>\n        <div className=\"ri-plugin-content\">\n          <code className=\"ri-plugin-command\">{command}</code>\n          <pre className=\"ri-plugin-code\">{JSON.stringify(data, null, 2)}</pre>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport function renderExampleView(props: any) {\n  console.log(PREFIX, 'activated', props);\n  const host = document.getElementById('app');\n  if (!host) return;\n  try {\n    const root = getRoot(host);\n    root.render(<ExampleApp command={String(props?.command ?? '')} data={props?.data} />);\n  } catch (err) {\n    console.error(PREFIX, 'render failed', err);\n    host.innerHTML = '<div class=\"ri-plugin-error\">Plugin failed. See console.</div>';\n  }\n}\n\nexport default { renderExampleView };\n```\n\nIf you are on React 17, use `ReactDOM.render` instead of `createRoot`. Failure modes Phase 2 surfaces:\n\n- Wrong React/ReactDOM version pairing (React 17 + `createRoot` is the classic).\n- Externalized React in the bundle (`require is not defined`).\n- Missing `index.html` `#app` host.\n- Type errors not caught by the build.\n\n## Phase 3 — Full Feature\n\nGoal: real visualization library, real UX, real error/empty states.\n\nIn Phase 3:\n\n- Add the actual visualization library (charting, mapping, grid, etc.). See [third-party-libraries.md](third-party-libraries.md).\n- Parse the Redis response defensively. See [redis-command-parsing.md](redis-command-parsing.md).\n- Render empty and error states explicitly.\n- Persist user settings via SDK or `localStorage`.\n- Handle `theme_DARK` / `theme_LIGHT` switches.\n\nPhase 3 failures usually point to:\n\n- Unexpected response shape (use the parser patterns).\n- Library asset paths broken by bundling (icons, fonts, or images that resolve via bundler-relative URLs).\n- Missing CSS (`styles` field, CSS not bundled).\n- Cluster/multi-shard responses you didn't account for.\n\n## CI Pipeline Sketch\n\n```yaml\n# .github/workflows/plugin.yml (sketch)\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with: { node-version: '20' }\n      - run: npm ci\n      - run: npm run build\n      - run: bash scripts/verify-plugin.sh\n      - uses: actions/upload-artifact@v4\n        with:\n          name: plugin-bundle\n          path: |\n            package.json\n            dist/\n```\n\nOptionally extend with a Playwright job that boots a Redis Insight container, copies the artifact in, and runs a Workbench smoke test written per the repo's [e2e-testing](../../e2e-testing/SKILL.md) skill.\n",".ai/skills/redis-insight-plugin/references/official-docs-summary.md":"# Redis Insight Plugin Docs — Summary\n\nCondensed from the official Redis Insight plugin docs:\n\n- https://github.com/redis/RedisInsight/tree/main/docs/plugins\n- https://github.com/redis/RedisInsight/blob/main/docs/plugins/development.md\n- https://github.com/redis/RedisInsight/blob/main/docs/plugins/installation.md\n\nAlways re-read the upstream docs when in doubt — the contract may evolve.\n\n## Where Plugins Run\n\n- Workbench renders a plugin inside an **iframe**.\n- The iframe loads the plugin's `index.html` (or equivalent), which loads the bundle declared in the manifest's `main`.\n- The iframe DOM has a `<div id=\"app\"></div>` host element. Activation functions render into `#app`.\n- Stylesheets declared by `styles` are injected into the iframe.\n\n## Manifest (`package.json`)\n\nA plugin's root `package.json` is the manifest. Required fields:\n\n- `name`, `version`, `description`\n- `main` — relative path to the built JS bundle (e.g. `./dist/index.js`).\n- `styles` — relative path to the built CSS (e.g. `./dist/styles.css`).\n- `visualizations[]` — list of visualization descriptors:\n  - `id` — stable identifier inside the plugin.\n  - `name` — visible label in the Workbench result tabs.\n  - `activationMethod` — exact name of the exported function called to render this view.\n  - `matchCommands` — array of Redis command names this visualization supports (e.g. `[\"XRANGE\", \"XREVRANGE\"]`).\n  - `description` — short summary shown in the UI.\n  - `default` — `true` if this should be the default tab; otherwise `false`.\n\n## Activation Function\n\nThe plugin's main script exports an object whose keys are activation function names:\n\n```ts\nexport default { renderMyView };\n```\n\nEach activation function is invoked by the Workbench host and receives a props object containing:\n\n- The Redis `command` that was executed.\n- The data **result array** (raw Redis reply).\n- Helpers via `redisinsight-plugin-sdk` (config, modules, theme, base URL, app version).\n\nThe `<body>` of the iframe carries a `theme_DARK` or `theme_LIGHT` class, and `window.state` exposes plugin-relevant config such as connected modules, base URL, and app version.\n\n## Installation Paths\n\nUser-installed plugins live under:\n\n- macOS / Linux: `~/.redis-insight/plugins/`\n- Windows: `C:\\Users\\{Username}\\.redis-insight\\plugins\\`\n\nEach plugin lives in its own subdirectory (folder name should match `name`).\n\n## Trust Warning\n\nPlugins execute arbitrary code inside Redis Insight. **Only install plugins from trusted sources.** Treat plugin code review with the same care you would use for any code that runs in your dev environment.\n\n## What This Skill Adds\n\nThe official docs describe the contract. This skill adds operational practice:\n\n- Phased Phase 1/2/3 development to surface failure modes early.\n- Parcel for external plugins, Vite for internal monorepo plugins.\n- Defensive command parsing across different commands and flag-dependent response shapes.\n- `curl /api/plugins` and Playwright smoke validation after every deploy.\n- Docker static-plugin workaround for `redis/redisinsight` images.\n",".ai/skills/redis-insight-plugin/references/plugin-manifest.md":"# Plugin Manifest (`package.json`)\n\nThe plugin manifest is `package.json` at the plugin root.\n\n## Single Visualization\n\n```json\n{\n  \"name\": \"ri-plugin-xrange-table\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Render XRANGE results as a sortable table.\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"visualizations\": [\n    {\n      \"id\": \"xrange-table\",\n      \"name\": \"Stream Entries\",\n      \"activationMethod\": \"renderXRangeTable\",\n      \"matchCommands\": [\"XRANGE\", \"XREVRANGE\"],\n      \"description\": \"Tabular view of stream entries.\",\n      \"default\": false\n    }\n  ]\n}\n```\n\n## Multiple Visualizations\n\n```json\n{\n  \"name\": \"ri-plugin-stream-views\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Table and chart visualizations for Redis stream entries.\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"visualizations\": [\n    {\n      \"id\": \"stream-table\",\n      \"name\": \"Table\",\n      \"activationMethod\": \"renderTableView\",\n      \"matchCommands\": [\"XRANGE\", \"XREVRANGE\"],\n      \"description\": \"Tabular view of stream entries.\",\n      \"default\": false\n    },\n    {\n      \"id\": \"stream-chart\",\n      \"name\": \"Chart\",\n      \"activationMethod\": \"renderChartView\",\n      \"matchCommands\": [\"XRANGE\", \"XREVRANGE\"],\n      \"description\": \"Chart numeric fields over the stream's time range.\",\n      \"default\": false\n    }\n  ]\n}\n```\n\nThe plugin's main bundle exports both functions:\n\n```ts\nexport default {\n  renderTableView,\n  renderChartView,\n};\n```\n\n## Matching and Defaults\n\nWhen several visualizations share commands, make their defaults mutually exclusive by command shape, not just by command name. For example, one visualization can be the default when the result has a particular shape (numeric series, coordinates, nested rows, …), while another is available for the same commands but defaults only for a different shape (scalar, store-style, or empty results).\n\nRules for matcher definitions:\n\n- Match whole command tokens. Do not let a command match a longer command with the same prefix (e.g. a `*STORE` or `*_RO` variant).\n- Keep `matchCommands` broad only when `matchQuery` narrows the result shape safely.\n- Use `noneRegex` for exclusion only after checking how the platform normalizes command text.\n- Keep regexes bounded and linear. Avoid broad repeated token alternatives plus a trailing `[\\s\\S]{0,N}` window.\n- Test overlapping visualizations so only one `default: true` candidate remains for each command shape.\n\n## Icons\n\nOptional but supported:\n\n```json\n{\n  \"iconDark\": \"./dist/icon-dark.svg\",\n  \"iconLight\": \"./dist/icon-light.svg\"\n}\n```\n\nPlace icons under `dist/` (or another path your manifest points at) and copy them as part of deployment.\n\n## Stripping Dev-Only Fields\n\nThe deployed manifest must **not** include:\n\n- `scripts`\n- `devDependencies`\n- `targets` (Parcel-only build hints)\n- `husky`, `lint-staged`, `eslintConfig`, `prettier`, etc.\n\nEither keep a separate `package.deploy.json` or strip with `jq`:\n\n```bash\njq 'del(.scripts, .devDependencies, .targets, .husky, .[\"lint-staged\"])' \\\n  package.json > /tmp/package.deploy.json\n```\n\n`templates/deploy-external.sh` does this automatically.\n\n## Required Fields Recap\n\n- `name` — globally unique within `~/.redis-insight/plugins/`.\n- `version` — semver.\n- `description` — one sentence shown in Insight.\n- `main` — built JS bundle path.\n- `styles` — built CSS path (omit only if no styles).\n- `visualizations[]` — at least one, each with `id`, `name`, `activationMethod`, `matchCommands`, `description`, `default`.\n\nIf any required field is missing, Insight silently drops the plugin. Always run `curl /api/plugins` after deploying.\n",".ai/skills/redis-insight-plugin/references/redis-command-parsing.md":"# Redis Command Parsing\n\nPlugins must defensively parse raw Redis responses. Different commands return very different\nshapes, and even the same command with different flags returns different shapes.\n\n## Same Command, Different Shapes by Flag\n\nA command's optional modifiers change the response shape mid-flight. A classic pattern: without\nmodifiers a command returns a flat array of values, but adding `WITH...`-style flags returns a\nnested array per entry:\n\n```json\n// no modifiers — flat list\n[\"member1\", \"member2\", \"member3\"]\n```\n\n```json\n// with extra-data flags — nested per entry\n[\n  [\"member1\", \"12.34\", [\"extra\", \"data\"]],\n  [\"member2\", \"56.78\", [\"extra\", \"data\"]]\n]\n```\n\nNever key parsing off the command name alone — branch on the actual runtime shape, and read the\ncommand string from the activation props to choose the right header/label.\n\n## Worked Example: XRANGE / XREVRANGE\n\n```\nXRANGE stream - +\n```\n\nReturns an array of `[id, [field, value, field, value, ...]]`:\n\n```json\n[\n  [\"1700000000000-0\", [\"temp\", \"22\", \"humidity\", \"55\"]],\n  [\"1700000001000-0\", [\"temp\", \"23\"]]\n]\n```\n\nParser sketch:\n\n```ts\nexport function parseXRange(data: any[]): { id: string; fields: Record<string, string> }[] {\n  if (!Array.isArray(data)) return [];\n  return data.flatMap(entry => {\n    if (!Array.isArray(entry) || entry.length < 2) return [];\n    const [id, kv] = entry;\n    if (typeof id !== 'string' || !Array.isArray(kv)) return [];\n    const fields: Record<string, string> = {};\n    for (let i = 0; i + 1 < kv.length; i += 2) {\n      const k = kv[i];\n      const v = kv[i + 1];\n      if (typeof k === 'string') fields[k] = String(v);\n    }\n    return [{ id, fields }];\n  });\n}\n```\n\nThe same defensive structure applies to any command: start from `Array.isArray`, destructure\nsafely, coerce-and-validate numbers, and drop malformed entries instead of throwing.\n\n## Preserve Raw Values and Units\n\nSome commands accept a unit or format argument and return values in that same unit/format (a\nradius unit, a count, a score precision, etc.). If the visualization assumes one unit but the\ncommand used another, labels and scaling will be wrong. Read the relevant token from the command\nstring and preserve the raw value before any internal normalization.\n\n## Defensive Parsing Rules\n\n- Always start with `if (!Array.isArray(data)) return [];`.\n- Never assume nested array length — destructure with `const [a, b, ...rest] = ...`.\n- Preserve argument positions when tokenizing commands. Empty quoted strings (`\"\"`, `''`) should become empty-string tokens, not disappear.\n- Treat numeric fields as strings; convert with `Number(...)` and validate with `Number.isFinite(...)`.\n- Reject blank numeric strings before conversion; `Number('')` is `0`.\n- Drop malformed entries silently; surface the count (`\"3 of 25 entries skipped\"`) in the UI rather than crashing.\n- Never throw inside a parser — return an empty array and let the activation function render an empty/error state.\n- Avoid `Math.max(...largeArray)` or other spreading of full result sets. Use `reduce` or loops for large responses.\n- When parsing `FT.*` or other option-rich command strings, tokenize first and honor command grammar. Do not strip option keywords (`PARAMS`, `SEARCH`, `FILTER`, …) just because those words appear inside a key, member, index name, or quoted query.\n\n## Response Shape Caveats\n\n- **Cluster mode** can return wrapped responses depending on Insight's transport.\n- **Modifier flags** change the response shape mid-flight — never key off command name alone.\n- **Empty results** are `[]`, but on some commands a missing key returns `null`. Treat both as empty.\n- **Module commands** (RedisJSON, RedisTimeSeries, RedisSearch, etc.) return their own shapes — only support what your `matchCommands` declares.\n- **Redis 8 / module responses** can be array-like, aggregate-like, or object/map-style. Handle array-specific branches before generic object branches because arrays are objects in JavaScript.\n",".ai/skills/redis-insight-plugin/references/redis-insight-plugin-guidelines.md":"# Redis Insight Plugin — Operational Guidelines\n\nLong-form operational reference. SKILL.md is the entry point; this file is for anything that doesn't fit there.\n\n## Plugin Types Overview\n\nTwo plugin shapes, two build tools, two deployment models.\n\n- **External standalone plugin** — bundles everything with **Parcel**. Ships as a folder you drop into `~/.redis-insight/plugins/<name>/`. No knowledge of the RedisInsight monorepo. Default for customer demos and field plugins.\n- **Internal monorepo plugin** — lives at `RedisInsight/redisinsight/ui/src/packages/<plugin-name>/`. Built with **Vite** as part of the RedisInsight repo. Ships embedded in Insight builds.\n\nIf you are unsure, build external. External works in every Redis Insight install; internal only ships with the Insight binary you build.\n\n## Official References\n\n- https://github.com/redis/RedisInsight/tree/main/docs/plugins\n- https://github.com/redis/RedisInsight/blob/main/docs/plugins/development.md\n- https://github.com/redis/RedisInsight/blob/main/docs/plugins/installation.md\n\n## Internal Plugin Development with Vite\n\n- Path: `RedisInsight/redisinsight/ui/src/packages/<plugin-name>/`.\n- Use Vite, not Parcel. Inherit RedisInsight's shared TS, theme, and component conventions.\n- Internal plugins **may** import from `uiSrc/` and shared components, but only when the import has a stable contract — internal layout helpers move often.\n- Avoid hard dependencies on `ThemeProvider` or other Insight-only providers in plugin code that might be reused externally later.\n\n## External Plugin Development with Parcel\n\n- Path: anywhere; deployed to `~/.redis-insight/plugins/<plugin-name>/`.\n- Bundle every dependency. Do not externalize React or any runtime library.\n- `targets.module.includeNodeModules: true` so Parcel inlines deps.\n- No imports from `uiSrc/`, `@redis-ui/*`, or any RedisInsight internal package.\n- Output to `dist/index.js` and `dist/styles.css`.\n\n## Package.json Requirements\n\nSee [plugin-manifest.md](plugin-manifest.md) for full examples. Minimum:\n\n```json\n{\n  \"name\": \"ri-plugin-example\",\n  \"version\": \"0.0.1\",\n  \"description\": \"...\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"visualizations\": [\n    {\n      \"id\": \"example-view\",\n      \"name\": \"Example View\",\n      \"activationMethod\": \"renderExampleView\",\n      \"matchCommands\": [\"XRANGE\"],\n      \"description\": \"Shows XRANGE results.\",\n      \"default\": false\n    }\n  ]\n}\n```\n\n## Plugin Entry Point\n\n```ts\nfunction renderExampleView(props) {\n  const root = document.getElementById('app');\n  if (!root) {\n    console.error('[EXAMPLE_PLUGIN] #app element missing');\n    return;\n  }\n  try {\n    // Render here. Show empty state when data is empty.\n  } catch (err) {\n    console.error('[EXAMPLE_PLUGIN] render failed', err);\n    root.innerHTML = '<div class=\"ri-plugin-error\">Plugin failed. See console.</div>';\n  }\n}\n\nexport default { renderExampleView };\n```\n\n## Testing & Verification\n\n- `npm run build` produces `dist/index.js` and `dist/styles.css`.\n- `templates/verify-plugin.sh` confirms file presence, no `process.env`, and that activation method names appear in the bundle.\n- Deploy with `templates/deploy-external.sh` (folder install) or `templates/deploy-internal-docker.sh` (Docker container).\n- After deploy, `curl http://localhost:5540/api/plugins` and grep for the plugin name.\n- Optional: add a Workbench smoke test following the repo's [e2e-testing](../../e2e-testing/SKILL.md) skill (page objects, fixtures, UI navigation) to validate iframe rendering.\n\n## Iterative Plugin Development\n\nSee [iterative-development.md](iterative-development.md). Always run Phase 1, then Phase 2, then Phase 3.\n\n## DO NOT Rules\n\nReproduced from SKILL.md for convenience:\n\n- DO NOT skip Phase 1 or Phase 2.\n- DO NOT deploy `index.js`/`styles.css` at the plugin root — they live in `dist/`.\n- DO NOT set `default: true` by default.\n- DO NOT include `scripts` or `devDependencies` in the deployed manifest.\n- DO NOT use Vite for standalone plugins.\n- DO NOT import `uiSrc` in standalone plugins.\n- DO NOT externalize React in standalone bundles.\n- DO NOT skip `/api/plugins` verification.\n- DO NOT ship `process.env.*` references.\n- DO NOT assume one Redis response shape.\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---------|--------------|-----|\n| Plugin missing from `/api/plugins` | Manifest not deployed, or wrong folder | Check folder is at `~/.redis-insight/plugins/<name>/` and contains `package.json` + `dist/`. |\n| Iframe blank | `activationMethod` name mismatch | Confirm exported function name == manifest field. |\n| `process.env is not defined` | `process.env.*` left in bundle | Replace at build time; rebuild; verify with `grep -c process.env dist/index.js`. |\n| `require is not defined` | Externalized React or wrong target | Use Parcel `targets.module` with `includeNodeModules: true`. |\n| Styles not applied | `styles` field missing or wrong path | Add `\"styles\": \"./dist/styles.css\"`; confirm file exists. |\n| Console error: `#app is null` | Activation ran before DOM | Ensure script is loaded as a module and renders into `document.getElementById('app')`. |\n| Visualization tab missing | `matchCommands` mismatch | Confirm command name (uppercase) appears in `matchCommands`. |\n\n## Lessons Learned\n\n- Phase 1 catches `activationMethod` name/manifest mismatches before any React is wired.\n- Phase 2 surfaces React/ReactDOM version and `createRoot` mismatches early.\n- Phase 3 exposes library asset-path bugs (icons/fonts/images broken by bundler-relative URLs) — fix once at init.\n- A command with extra-data flags often returns nested arrays, not objects — guard against shape changes.\n- Always `curl /api/plugins` after deploy. Cached menus lie.\n\n## Plugin API Reference\n\nActivation function props (subject to upstream changes — re-check official docs):\n\n- `command: string` — the executed Redis command.\n- `data: any[]` — the raw Redis result array.\n- `redisInsightPlugin` (via SDK) — `getState`, `setState`, `getModules`, `getTheme`, `getBaseUrl`, `getAppVersion`.\n\nUse `redisinsight-plugin-sdk` for state and theme rather than reading `window.state` directly when possible.\n\n## Command Parsing Patterns\n\nSee [redis-command-parsing.md](redis-command-parsing.md).\n\n## Third-Party Library Integration\n\nSee [third-party-libraries.md](third-party-libraries.md).\n\n## Interactive UI Patterns\n\n- Keep heavy UI inside the iframe; do not assume access to the host page.\n- Apply the [redis-ui-components](../../redis-ui-components/) skill with RedisInsight `light` / `dark` themes for visual plugin work (internal plugins use the `uiSrc/components/ui` wrappers per the [frontend](../../frontend/SKILL.md) styleguide).\n- Standalone plugins emulate product tokens locally with `src/styles/styles.scss`; copy `templates/external-styles.scss` as the baseline.\n- Use `document.body.classList.contains('theme_DARK')` to switch palettes.\n- Persist user settings via SDK state, falling back to `localStorage` keyed under your plugin id.\n- Avoid global CSS resets that fight Insight's iframe styles.\n\n## State Persistence\n\n- Prefer `redisinsight-plugin-sdk` `getState` / `setState` for cross-session persistence.\n- Fall back to `localStorage` with a plugin-prefixed key (`ri:<plugin-name>:settings`) when SDK state is unavailable.\n- Treat persisted state as untrusted; validate before use.\n\n## Error Handling & Stability\n\nSee [error-handling.md](error-handling.md).\n\n## Debugging & Development Tips\n\n- Open the Workbench iframe in browser devtools to inspect logs and network calls.\n- Look for `[<PLUGIN_PREFIX>]` log lines to filter your output.\n- Use the Phase 1 vanilla bundle to A/B test whether failures are plugin-host or library-related.\n- For Docker installs, `docker exec` into the container and inspect `/usr/src/app/redisinsight/api/dist/static/plugins/<name>/`.\n",".ai/skills/redis-insight-plugin/references/redisinsight-product-ui.md":"# RedisInsight Product UI For Plugins\n\nUse the [redis-ui-components](../../redis-ui-components/) skill for visual decisions. This file\nonly adapts it to RedisInsight Workbench plugin iframes.\n\n> **Internal vs external.** For **internal** plugins (the default in this repo), build the UI\n> from Redis UI components via the `uiSrc/components/ui` wrappers and follow the\n> [frontend](../../frontend/SKILL.md) styleguide — use layout components, theme colors, and the\n> standard component folder structure. The \"Standalone Constraints\" below (no `@redis-ui/*` /\n> `uiSrc/` imports, local CSS variables) apply to **external standalone** plugins only, which\n> are bundled in isolation and must emulate the same look with local code.\n\n## Theme\n\n- RedisInsight plugins use `light` / `dark`, never `light2` / `dark2`.\n- Standalone external plugins cannot import RedisInsight internals, so define local CSS variables that emulate RedisInsight product tokens.\n- Read the iframe body class:\n  - `theme_LIGHT` => light theme\n  - `theme_DARK` => dark theme\n- If using `redisinsight-plugin-sdk`, prefer its theme helper when available, then mirror the same mode into local classes only when needed.\n\n## Baseline Files\n\nFor external Parcel plugins:\n\n- Copy `templates/external-styles.scss` to `src/styles/styles.scss`.\n- Keep `package.json` `\"styles\": \"./dist/styles.css\"`.\n- Keep the stylesheet bundled into `dist/styles.css`; RedisInsight injects it into the plugin iframe.\n\n## UI Rules\n\n- Build compact Workbench-like surfaces: toolbar, table, inspector, code/value panel, empty/error/loading states.\n- Prefer table-plus-detail layouts for Redis command responses.\n- Use `Source Code Pro` or monospace stacks for Redis keys, stream IDs, commands, raw values, and timestamps.\n- Use status badges with text and color; never rely on color alone.\n- Use product semantic colors for success, warning, danger, selected, and neutral states.\n- Keep controls stable: long keys and JSON must scroll, wrap intentionally, or ellipsize with a title/tooltip.\n\n## Standalone Constraints\n\n- Do not import `uiSrc/`, `@redis-ui/*`, RedisInsight monorepo packages, or private source.\n- Do not use global CSS resets that can fight the iframe host.\n- Scope styles under plugin classes such as `.ri-plugin-shell`.\n- Do not use Redis brand red as the universal accent, heading, CTA, error, or status color.\n\n## Verification\n\nCheck both themes:\n\n```bash\ncurl -s http://localhost:5540/api/plugins\n```\n\nThen run a matching Redis command in Workbench and verify:\n\n- `theme_LIGHT` and `theme_DARK` render correctly.\n- Empty, loading, error, and success states are styled.\n- Tables and `pre` blocks do not overflow the iframe incoherently.\n- Browser console has no style/runtime errors.\n",".ai/skills/redis-insight-plugin/references/review-hardening.md":"# Review Hardening\n\nUse this before opening or updating a Redis Insight plugin PR. It captures review issues that are easy for coding agents to miss because the happy path works.\n\n## Start From Review Discipline\n\n- Treat every bot or human comment as a hypothesis. Confirm it against code, tests, and Redis/Insight behavior before changing code.\n- Reproduce the failing shape with a focused test first. If the issue is valid, keep the regression test.\n- Prefer the narrowest verification scope that proves the change: package tests, parser tests, manifest matcher tests, and typecheck. Do not run a full RedisInsight build for every comment unless the changed surface requires it.\n- When replying to PR comments, state the behavior fixed, the test added, and the commit that contains it.\n\n## Manifest and Matcher Checklist\n\n- Match whole Redis command tokens, not prefixes. A command must not match a longer command with the same prefix (e.g. a `*STORE` or `*_RO` variant).\n- Keep multi-visualization defaults mutually exclusive for overlapping commands. If one view is the default for one result shape, another can be available but must not also be the blanket default for the same command shape.\n- If `matchQuery.anyRegex` / `noneRegex` is used, keep regexes bounded and linear. Avoid repeated broad alternatives followed by trailing `[\\s\\S]{0,N}`.\n- Make token alternatives disjoint: quoted strings, double-quoted strings, and unquoted tokens should not overlap in a way that creates backtracking spikes.\n- Test native command names, module command names, and store/scalar variants separately. Query/result rows and scalar/store commands often need different visualization defaults.\n\n## Parser Checklist\n\n- Tokenize the Redis command before inspecting options. Raw `indexOf(\"PARAMS\")`, `indexOf(\"SEARCH\")`, or `indexOf(\"FILTER\")` will misread keys, members, index names, and quoted query text.\n- Preserve empty quoted tokens in the tokenizer. `\"\"` and `''` should still occupy an argument position, even if the downstream parser later rejects that command shape.\n- Keyword positions matter. For `FT.HYBRID`, skip the command and index tokens when detecting `SEARCH` / `FILTER`, and skip `PARAMS` key/value ranges when parsing predicates.\n- Strip or ignore large option payloads only when the token is the actual option for the command grammar. Do not strip a key/member named like an option keyword, an index named like one, or query text containing one.\n- Preserve the raw unit/format returned by Redis (the token that follows the relevant option in the command string) before any internal normalization.\n- Validate blank numeric parts before calling `Number(...)`; JavaScript treats `Number(\"\")` as `0`.\n- Differentiate empty result sets from malformed/incomplete rows so the UI can show the right guidance.\n- Handle search-like arrays, aggregate-like arrays, and Redis 8 map-style objects before generic object fallback. Arrays are objects in JavaScript, so order matters.\n- Avoid `Math.max(...largeArray)` or similar spread over result sets. Reduce or loop to avoid call stack and argument limits.\n\n## Visualization Library Checklist\n\n- Validate library preconditions before rendering (non-empty series, valid bounds/ranges, sized container). Bail to an empty/error state instead of calling render APIs with bad input.\n- Do not let library callbacks (cell renderers, tooltip/popup builders, icon factories) capture stale React state. Keep live values in refs or recreate the instance when the callback dependencies change.\n- Build tooltip/popup/cell DOM with `textContent` or escaped React output, not interpolated HTML from Redis data.\n- Destroy/dispose the library instance before re-creating it on re-render to avoid leaks.\n- For large result sets, use loops/reductions and avoid per-render expensive parsing unless the input changed.\n- Use mode-aware empty/error titles. Different visualizations in the same plugin should not all show the same generic failure message.\n\n## Code Hygiene Checklist\n\n- Remove dead plugin config files and unused exports instead of leaving them for reviewers to rediscover.\n- Enforce declared limits or delete them. A `maxRows` / `maxPoints` constant that nobody reads is a bug magnet.\n- Centralize shared conversion constants/helpers so parsers and visualizations cannot drift.\n- Keep component-local types and large constants in sibling `.types.ts` and `.constants.ts` files when the component is already large.\n- Keep return types honest and simple. Do not use a tuple type when the function returns arbitrary-length arrays.\n- After discriminated-union narrowing, remove unreachable null/error branches. A defensive check that cannot run makes the real state model harder to review.\n- Memoize parsed command/results when parsing is nontrivial or feeds the visualization render.\n\n## Test Matrix\n\nAdd focused tests for any surface you touch:\n\n- Manifest matcher: exact command boundaries, default exclusivity, distinct result shapes (scalar vs row vs nested).\n- Command parser: keyword-like key/member/index names, empty quoted tokens, option keywords appearing in native commands and in query text, large payloads, empty rows, malformed rows, Redis 8 object responses.\n- Unit/format handling: commands that take a unit or format argument return values in that same unit/format; cover each variant the plugin supports.\n- Visualization state: control updates while rendered, empty/invalid input sets, distinct per-mode error titles.\n- Performance guardrails: large result counts, large payloads, and no spread into `Math.max`.\n",".ai/skills/redis-insight-plugin/references/testing-and-deployment.md":"# Testing and Deployment\n\nHow to deploy and verify a Redis Insight plugin, and how to smoke test it with Playwright (following the repo's [e2e-testing](../../e2e-testing/SKILL.md) skill).\n\n## Correct External Deployment Structure\n\n```\n~/.redis-insight/plugins/<plugin-name>/\n  package.json               # deployed manifest (no scripts, no devDependencies)\n  dist/\n    index.js\n    styles.css\n```\n\nThat's it. Nothing else is required at deploy time.\n\n## Wrong Structures (Common Mistakes)\n\n```\n# WRONG — bundle at plugin root\n~/.redis-insight/plugins/<plugin-name>/\n  package.json\n  index.js                   # should be in dist/\n  styles.css                 # should be in dist/\n```\n\n```\n# WRONG — nested package folder\n~/.redis-insight/plugins/<plugin-name>/\n  <plugin-name>/             # extra directory\n    package.json\n    dist/\n```\n\n```\n# WRONG — manifest still has dev/scripts\n{\n  \"main\": \"./dist/index.js\",\n  \"scripts\": { \"build\": \"...\" },        # remove\n  \"devDependencies\": { \"parcel\": \"...\" } # remove\n}\n```\n\n## Deploy Commands\n\nExternal (host install):\n\n```bash\nnpm run build\nbash templates/verify-plugin.sh\nbash templates/deploy-external.sh\n```\n\nInside Docker RedisInsight:\n\n```bash\nnpm run build\nbash templates/verify-plugin.sh\nbash templates/deploy-internal-docker.sh\n```\n\n## Docker RedisInsight: Static Plugin Path\n\nExternal plugin deployment to `~/.redis-insight/plugins/<name>/` works for the host install — that is the canonical path and the one to default to.\n\nFor Docker, the host plugins folder is not visible inside the container. To install a plugin into a running `redis/redisinsight` container, copy it into the container's static plugins folder:\n\n```\n/usr/src/app/redisinsight/api/dist/static/plugins/<plugin-name>/\n```\n\nWorkflow:\n\n```bash\ndocker cp dist redisinsight-test:/usr/src/app/redisinsight/api/dist/static/plugins/<plugin-name>/dist\ndocker cp package.json redisinsight-test:/usr/src/app/redisinsight/api/dist/static/plugins/<plugin-name>/package.json\ndocker restart redisinsight-test\n```\n\nThen verify via `/api/plugins`. The deploy script in `templates/deploy-internal-docker.sh` automates this.\n\n## /api/plugins Verification\n\nAfter every deploy:\n\n```bash\ncurl -s http://localhost:5540/api/plugins | jq '.[] | {name, visualizations}'\n```\n\nThe plugin must appear by `name`, with each declared `visualizations[*].id`. If absent:\n\n- The plugin folder is wrong, or\n- The manifest is invalid (missing required field), or\n- Insight wasn't restarted (Docker case).\n\n## Browser Console Verification\n\nOpen the Workbench iframe in browser devtools:\n\n- Filter logs by your `[<PLUGIN_PREFIX>]` prefix.\n- Confirm `activated` log fires when running a matching command.\n- Confirm no `process.env` errors, no `require is not defined`, no React mismatch.\n- Inspect network calls — there should be none unless your plugin explicitly makes them.\n\n## Playwright Smoke Test Strategy\n\nFor any Playwright E2E test, **follow the repo's [e2e-testing](../../e2e-testing/SKILL.md)\nskill** — it owns the conventions for this repo (tests live in `tests/e2e-playwright/`, use page\nobjects, fixtures, and `apiHelper`; never call `page.goto()` directly; no CSS selectors or fixed\nwaits). Do not hand-roll a standalone spec that bypasses those patterns.\n\nA plugin smoke test should cover the same flow, expressed through those conventions:\n\n1. Create/seed the database via `apiHelper` in `beforeAll`.\n2. Navigate to Workbench through a page object's `goto()` method (UI navigation, not `page.goto()`).\n3. Run a command that matches the plugin's `matchCommands`.\n4. Select the plugin's visualization tab.\n5. Assert the iframe is present and renders expected content.\n\nUse Playwright MCP to discover the actual `data-testid`/roles first, as the e2e-testing skill\ndescribes. The one plugin-specific check that does not need the UI is registration —\n`curl /api/plugins` (or `page.request.get('/api/plugins')`) must list the plugin.\n\n## Internal PR Verification Scope\n\nFor RedisInsight monorepo plugin changes, keep verification proportional to the changed surface:\n\n- Parser or command-shape fix: run the parser spec or package test file first, then the plugin package test command.\n- Manifest or matcher fix: run the matcher/plugin utility spec that exercises `matchCommands`, `matchQuery`, `anyRegex`, `noneRegex`, and `default` selection.\n- React / visualization-library fix: run the component spec for that visualization plus the package typecheck.\n- Code hygiene fix: run the closest affected spec plus typecheck, especially when moving types/constants or centralizing helpers.\n- Shared utility change under `ui/src/`: add the focused utility spec and a changed-file lint/typecheck check.\n- Before final commit on a PR batch, run the package tests, package typecheck, and `git diff --check`. Run a full RedisInsight build only when build configuration, package exports, bundling, or shared application wiring changed.\n",".ai/skills/redis-insight-plugin/references/third-party-libraries.md":"# Third-Party Libraries\n\nNotes on integrating a visualization library (charting, mapping, grid, diagram, etc.) inside a\nRedis Insight plugin iframe. The principles are library-agnostic; apply them to whatever library\nthe plugin needs.\n\n## General Integration Rules\n\n- **Bundle the library fully** in an external (Parcel) plugin — do not externalize it. Internal\n  (Vite) plugins follow the shared build's dependency handling instead.\n- **Add the library's CSS to the bundle** (`dist/styles.css`); do not rely on a global `@import`\n  or a CDN. RedisInsight injects the bundled stylesheet into the plugin iframe.\n- **Initialize once, clean up on re-render.** Libraries that own a canvas, map, or grid instance\n  must be destroyed/disposed before re-creating, or they leak:\n\n  ```ts\n  instanceRef.current?.destroy();\n  instanceRef.current = createInstance(container, config);\n  ```\n\n- **Size the container explicitly.** Many libraries measure their parent on init; put the\n  render target in a sized flex/grid container so it lays out correctly inside the iframe.\n- **Validate inputs before handing them to the library.** Never assume parsed Redis data is\n  well-formed — guard for empty/NaN/out-of-range values, and check any library precondition\n  (valid bounds, non-empty series, etc.) before calling its render APIs.\n- **Avoid stale closures in library callbacks.** If a callback (cell renderer, tooltip builder,\n  icon factory) depends on React state, keep the live value in a ref or recreate the instance\n  when the dependency changes — captured state goes stale otherwise.\n- **Never interpolate Redis data into raw HTML.** Build tooltip/popup/cell DOM with\n  `textContent` or escaped React output so keys, members, and field values cannot inject markup.\n\n## Untyped Libraries: Custom `.d.ts`\n\nSome libraries (or their sub-plugins) ship without types. Add a local declaration instead of\nusing `any`:\n\n```ts\n// src/types/<lib-name>.d.ts\ndeclare module '<lib-name>' {\n  export interface Options {\n    /* the options the plugin actually uses */\n  }\n  export function createThing(target: HTMLElement, options?: Options): unknown;\n}\n```\n\nKeep the declaration minimal — only the surface the plugin uses.\n\n## Bundle Size Guidance\n\n- Aim for `dist/index.js` under ~1.5 MB minified for a snappy first render.\n- Use the bundler's analyzer (or `terser`'s reports) to find heavy dependencies.\n- Avoid `moment`; use `date-fns` or native `Intl.DateTimeFormat`.\n- Avoid full `lodash`; use targeted imports (`lodash/get`) or stdlib.\n- Tree-shaking only helps when imports are scoped (`import { sum } from 'lodash-es'`).\n- Cap row/point counts (paginate or virtualize) for large command results rather than handing\n  the whole set to the library at once.\n\n## Forbidden Imports\n\n- No `uiSrc/`, `@redis-ui/*`, or any RedisInsight monorepo internal in standalone plugins.\n- No Node-only modules (`fs`, `path`, `child_process`).\n- No CommonJS-only deps that fail in Parcel's `module` target without shims.\n",".ai/skills/redis-insight-plugin/templates/external-parcel-package.json":"{\n  \"name\": \"ri-plugin-example\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Example external Redis Insight Workbench plugin built with Parcel.\",\n  \"source\": \"./src/main.tsx\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"scripts\": {\n    \"start\": \"parcel src/index.html\",\n    \"build\": \"concurrently \\\"npm run build:js\\\" \\\"npm run build:css\\\"\",\n    \"build:js\": \"parcel build src/main.tsx --no-source-maps --dist-dir dist --target module\",\n    \"build:css\": \"parcel build src/styles/styles.scss --no-source-maps --dist-dir dist\",\n    \"minify:js\": \"terser dist/index.js -o dist/index.js -c -m\",\n    \"clean\": \"rimraf dist\",\n    \"verify\": \"bash scripts/verify-plugin.sh\",\n    \"deploy:external\": \"bash scripts/deploy-external.sh\",\n    \"deploy:internal\": \"bash scripts/deploy-internal-docker.sh\"\n  },\n  \"targets\": {\n    \"main\": false,\n    \"module\": {\n      \"includeNodeModules\": true,\n      \"outputFormat\": \"esmodule\",\n      \"isLibrary\": false\n    }\n  },\n  \"visualizations\": [\n    {\n      \"id\": \"example-view\",\n      \"name\": \"Example View\",\n      \"activationMethod\": \"renderExampleView\",\n      \"matchCommands\": [\"XRANGE\"],\n      \"description\": \"Example visualization rendered for XRANGE results.\",\n      \"default\": false\n    }\n  ],\n  \"dependencies\": {\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\",\n    \"redisinsight-plugin-sdk\": \"^1.1.0\"\n  },\n  \"devDependencies\": {\n    \"parcel\": \"^2.12.0\",\n    \"terser\": \"^5.30.0\",\n    \"rimraf\": \"^5.0.5\",\n    \"concurrently\": \"^8.2.2\",\n    \"cross-env\": \"^7.0.3\",\n    \"typescript\": \"^5.4.0\",\n    \"@types/react\": \"^17.0.79\",\n    \"@types/react-dom\": \"^17.0.25\"\n  }\n}\n",".ai/skills/testing/SKILL.md":"---\nname: testing\ndescription: >-\n  Unit/integration testing standards for RedisInsight using Jest and\n  Testing Library: test structure, the `renderComponent` helper, faker\n  for test data, mocking patterns, and `waitFor` instead of fixed\n  time waits. Use when writing or modifying any `*.spec.ts` or\n  `*.spec.tsx` file, when adding component or slice tests, when\n  debugging flaky tests, or when the user mentions jest, testing\n  library, faker, or test patterns.\n---\n\n# Testing Standards and Practices\n\n## Core Principles\n\n- **Write tests for all new features**\n- Follow **AAA pattern**: Arrange, Act, Assert\n- Use **descriptive test names**: \"should do X when Y\"\n- **CRITICAL**: Never use fixed time waits - tests must be deterministic\n- **CRITICAL**: Use faker library (@faker-js/faker) for test data\n\n## Test Organization\n\n```typescript\ndescribe('FeatureService', () => {\n  describe('findById', () => {\n    it('should return entity when found', () => {});\n    it('should throw NotFoundException when not found', () => {});\n  });\n\n  describe('create', () => {\n    it('should create entity with valid data', () => {});\n    it('should throw error with invalid data', () => {});\n  });\n});\n```\n\n## Frontend Testing (Jest + Testing Library)\n\n### Running Specific Tests\n\n```bash\n# Run a specific test file\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs'\n\n# Run a specific test by name (use -t flag)\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs' -t 'test name pattern'\n\n# Example:\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/slices/tests/browser/keys.spec.ts' -c 'jest.config.cjs' -t 'refreshKeyInfoAction'\n```\n\n### CRITICAL: Always Use Shared `renderComponent` Helper\n\n**Create a `renderComponent` helper for each component test file**:\n\n```typescript\nimport { faker } from '@faker-js/faker';\n// Pull structured data from a shared factory (redisinsight/ui/src/mocks/factories/);\n// only build ad-hoc primitives/callbacks inline.\n\ndescribe('MyComponent', () => {\n  // Default props: factory for the entity, inline only for callbacks/primitives\n  const mockUser = UserFactory.build();\n  const defaultProps: MyComponentProps = {\n    user: mockUser,\n    onComplete: jest.fn(),\n  };\n\n  // Shared render helper\n  const renderComponent = (propsOverride?: Partial<MyComponentProps>) => {\n    const props = { ...defaultProps, ...propsOverride };\n\n    return render(\n      <Provider store={store}>\n        <MyComponent {...props} />\n      </Provider>\n    );\n  };\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n  });\n\n  it('should render component', () => {\n    renderComponent();\n    expect(screen.getByText(defaultProps.name)).toBeInTheDocument();\n  });\n\n  it('should handle click', async () => {\n    const mockOnComplete = jest.fn();\n    renderComponent({ onComplete: mockOnComplete });\n\n    fireEvent.click(screen.getByRole('button'));\n\n    await waitFor(() => {\n      expect(mockOnComplete).toHaveBeenCalledTimes(1);\n    });\n  });\n});\n```\n\n**Benefits**:\n\n- Centralized setup (providers, router, theme)\n- Default props defined once\n- Easy prop overrides per test\n- No duplicate setup code\n\n### Complex Component Setup\n\nFor components requiring Router, ThemeProvider, etc., include them in `renderComponent`:\n\n```typescript\nconst renderComponent = (propsOverride?: Partial<Props>) => {\n  const props = { ...defaultProps, ...propsOverride }\n\n  return render(\n    <Provider store={store}>\n      <BrowserRouter>\n        <ThemeProvider theme={theme}>\n          <Component {...props} />\n        </ThemeProvider>\n      </BrowserRouter>\n    </Provider>\n  )\n}\n```\n\n### Testing with Redux\n\nCreate a test store with `configureStore` for Redux-connected components:\n\n```typescript\nconst createTestStore = (initialState = {}) => {\n  return configureStore({\n    reducer: { user: userSlice.reducer },\n    preloadedState: initialState,\n  });\n};\n\nconst renderComponent = (propsOverride?: Partial<Props>, storeState = {}) => {\n  const testStore = createTestStore(storeState);\n  // render with testStore\n};\n```\n\n### Query Priorities (Testing Library)\n\n**Prefer accessible queries** (as users would interact):\n\n```typescript\n// ✅ PREFERRED\nscreen.getByRole('button', { name: /submit/i });\nscreen.getByLabelText('Email');\nscreen.getByPlaceholderText('Enter name');\n\n// ⚠️ LAST RESORT\nscreen.getByTestId('user-profile');\n\n// ❌ AVOID\nwrapper.find('.button-class');\n```\n\n### Testing Async Behavior\n\n```typescript\n// ✅ GOOD: waitFor with proper queries\nawait waitFor(() => {\n  expect(screen.getByText('Data loaded')).toBeInTheDocument();\n});\n\n// ✅ GOOD: waitForElementToBeRemoved\nawait waitForElementToBeRemoved(() => screen.queryByText('Loading...'));\n\n// ✅ GOOD: findBy queries (built-in waiting)\nconst element = await screen.findByText('Async content');\n\n// ❌ BAD: Fixed timeouts (flaky tests)\nawait new Promise((resolve) => setTimeout(resolve, 1000));\n```\n\n### Mocking API Calls (MSW)\n\nUse Mock Service Worker for API mocking:\n\n```typescript\nimport { rest } from 'msw';\nimport { setupServer } from 'msw/node';\n\nconst server = setupServer(\n  rest.get('/api/users/:id', (req, res, ctx) => {\n    return res(\n      ctx.json({\n        id: req.params.id,\n        name: faker.person.fullName(),\n      }),\n    );\n  }),\n);\n\nbeforeAll(() => server.listen());\nafterEach(() => server.resetHandlers());\nafterAll(() => server.close());\n```\n\n## Backend Testing (NestJS/Jest)\n\n### Service Test Pattern\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\n// Define factory for User entity\nconst userFactory = Factory.define<User>(() => ({\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n}));\n\ndescribe('UserService', () => {\n  let service: UserService;\n  let repository: Repository<User>;\n\n  const mockRepository = {\n    find: jest.fn(),\n    findOne: jest.fn(),\n    save: jest.fn(),\n    update: jest.fn(),\n    delete: jest.fn(),\n  };\n\n  beforeEach(async () => {\n    const module = await Test.createTestingModule({\n      providers: [\n        UserService,\n        {\n          provide: getRepositoryToken(User),\n          useValue: mockRepository,\n        },\n      ],\n    }).compile();\n\n    service = module.get<UserService>(UserService);\n    repository = module.get<Repository<User>>(getRepositoryToken(User));\n  });\n\n  afterEach(() => {\n    jest.clearAllMocks();\n  });\n\n  it('should return user when found', async () => {\n    const mockUser = userFactory.build();\n    mockRepository.findOne.mockResolvedValue(mockUser);\n\n    const result = await service.findById(mockUser.id);\n\n    expect(result).toEqual(mockUser);\n  });\n});\n```\n\n### Controller Test Pattern\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nconst userFactory = Factory.define<User>(() => ({\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n}));\n\ndescribe('UserController', () => {\n  let controller: UserController;\n  let service: UserService;\n\n  const mockService = {\n    findAll: jest.fn(),\n    findById: jest.fn(),\n    create: jest.fn(),\n  };\n\n  beforeEach(async () => {\n    const module = await Test.createTestingModule({\n      controllers: [UserController],\n      providers: [{ provide: UserService, useValue: mockService }],\n    }).compile();\n\n    controller = module.get<UserController>(UserController);\n  });\n\n  it('should return user from service', async () => {\n    const mockUser = userFactory.build();\n    mockService.findById.mockResolvedValue(mockUser);\n\n    const result = await controller.findById(mockUser.id);\n\n    expect(result).toEqual(mockUser);\n  });\n});\n```\n\n### Integration Tests (E2E)\n\n```typescript\ndescribe('UserController (e2e)', () => {\n  let app: INestApplication;\n\n  beforeAll(async () => {\n    const module = await Test.createTestingModule({\n      imports: [AppModule],\n    }).compile();\n\n    app = module.createNestApplication();\n    await app.init();\n  });\n\n  afterAll(async () => {\n    await app.close();\n  });\n\n  it('/users (GET)', () => {\n    return request(app.getHttpServer())\n      .get('/users')\n      .expect(200)\n      .expect((res) => {\n        expect(Array.isArray(res.body)).toBe(true);\n      });\n  });\n});\n```\n\n## E2E Testing (Playwright)\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nconst userDataFactory = Factory.define(() => ({\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n}));\n\ntest.describe('User Management', () => {\n  test('should create new user', async ({ page }) => {\n    const userData = userDataFactory.build();\n\n    await page.goto('/users');\n    await page.click('text=Add User');\n    await page.fill('[name=\"name\"]', userData.name);\n    await page.fill('[name=\"email\"]', userData.email);\n    await page.click('text=Submit');\n\n    // ✅ Use proper waits\n    await expect(page.locator(`text=${userData.name}`)).toBeVisible();\n  });\n});\n```\n\n## Best Practices\n\n### Always Use Faker for Test Data\n\n```typescript\n// ✅ GOOD: Use faker\nconst user = {\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n  age: faker.number.int({ min: 18, max: 100 }),\n};\n\n// ❌ BAD: Hardcoded data\nconst user = { id: '123', name: 'Test User' };\n```\n\n### Use Factories Instead of Static Mocks\n\n**Use Fishery** for creating test data factories with sensible defaults and overrides.\n\n**CRITICAL — UI factories live in `redisinsight/ui/src/mocks/factories/`**, organized by\ndomain (`<domain>/<TypeName>.factory.ts`), exported as `<TypeName>Factory`. Before writing\ntest data in a UI spec, **check for an existing factory there and reuse it**; if none fits,\n**add a new factory file in that directory** rather than inlining mocks in the spec.\n\n```typescript\n// ✅ GOOD: reuse the shared factory (redisinsight/ui/src/mocks/factories/database/DBInstance.factory.ts)\nimport { DBInstanceFactory } from 'uiSrc/mocks/factories/database/DBInstance.factory';\n\nconst instance = DBInstanceFactory.build();\nconst cluster = DBInstanceFactory.build({\n  connectionType: ConnectionType.Cluster,\n});\nconst many = DBInstanceFactory.buildList(5);\n\n// ✅ GOOD: a new factory file, when none exists yet\n// redisinsight/ui/src/mocks/factories/<domain>/<TypeName>.factory.ts\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nconst userFactory = Factory.define<User>(({ sequence }) => ({\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n  age: faker.number.int({ min: 18, max: 100 }),\n}));\n\n// Usage - flexible and reusable\nconst user1 = userFactory.build();\nconst user2 = userFactory.build({ age: 25 });\nconst user3 = userFactory.build({ name: 'Specific Name' });\nconst users = userFactory.buildList(5); // Create multiple\n\n// ❌ BAD: Static mock objects\nconst mockUser1 = {\n  id: '123',\n  name: 'User 1',\n  email: 'user1@test.com',\n  age: 30,\n};\n```\n\nBenefits of Fishery factories:\n\n- Easy to override specific properties per test\n- Consistent default values across tests\n- Single source of truth for mock structure\n- Better maintainability when types change\n- Built-in support for sequences and traits\n\n### Never Use Fixed Timeouts\n\n```typescript\n// ❌ BAD: Fixed timeout\nawait new Promise((resolve) => setTimeout(resolve, 1000));\nawait page.waitForTimeout(2000);\n\n// ✅ GOOD: Wait for condition\nawait waitFor(() => {\n  expect(element).toBeInTheDocument();\n});\n\nawait page.waitForSelector('[data-test=\"result\"]');\n```\n\n### Mock External Dependencies\n\n```typescript\n// ✅ GOOD: Mock services\njest.mock('uiSrc/services/api', () => ({\n  apiService: {\n    get: jest.fn(),\n    post: jest.fn(),\n  },\n}));\n```\n\n### Parameterized Tests with `it.each`\n\n**Use `it.each` for multiple tests with the same body but different inputs:**\n\n```typescript\n// ✅ GOOD: Parameterized tests\nit.each([\n  { description: 'null', value: null },\n  { description: 'undefined', value: undefined },\n  { description: 'empty string', value: '' },\n  { description: 'whitespace only', value: '   ' },\n])('should return error when input is $description', async ({ value }) => {\n  const result = await service.processInput(value);\n  expect(result.status).toBe('error');\n});\n```\n\n**Benefits:**\n\n- DRY: Single test body shared across all cases\n- Maintainability: Changes to test logic only need to be made once\n- Readability: Test cases are clearly defined in a table\n- Easier to extend: Adding new test cases is just adding a new row\n\n### Test Edge Cases\n\nAlways test:\n\n- Empty arrays/objects\n- Null/undefined values\n- Error scenarios\n- Boundary conditions\n- Loading states\n\n## Testing Checklist\n\n- [ ] All new features have tests\n- [ ] Tests use faker for data generation\n- [ ] No fixed timeouts (use waitFor)\n- [ ] Tests follow AAA pattern\n- [ ] Descriptive test names\n- [ ] Shared `renderComponent` helper used\n- [ ] Default props defined\n- [ ] Edge cases covered\n- [ ] Error scenarios tested\n- [ ] Mocks cleaned up between tests\n- [ ] Integration tests for API endpoints\n- [ ] E2E tests for critical flows\n- [ ] Coverage meets thresholds (80%+)\n",".ai/skills/tsconfigs/SKILL.md":"---\nname: tsconfigs\ndescription: >-\n  Locate and modify TypeScript configuration in RedisInsight. Use when adding\n  path aliases, introducing a new TS area, debugging webpack/ts-node/ESLint\n  path resolution, or the user asks about tsconfig, path mappings, or where\n  TypeScript is configured.\n---\n\n# TypeScript Configuration\n\nRedisInsight has no root `tsconfig.json`. Config is split per area, each owning its own paths, includes, and consumers. ESLint uses `parserOptions.project: true` to auto-discover the nearest tsconfig for each linted file.\n\n## Layout\n\n| File | Owns | Consumers |\n| - | - | - |\n| `redisinsight/ui/tsconfig.json` | UI source, `uiSrc/*`, `apiClient` paths | Vite (UI build), ESLint UI override, `npm run type-check:ui` |\n| `redisinsight/api/tsconfig.json` | API source, `src/*`, `tests/*` paths | NestJS build, ESLint API override |\n| `redisinsight/api/tsconfig.check.json` | Same as base + `strict: true` (with `strictPropertyInitialization` and `useUnknownInCatchVariables` off) and `noEmit: true` | `npm run type-check:api` only — kept separate so strict mode doesn't break `nest build`. See the `type-check-baselines` skill. |\n| `redisinsight/desktop/tsconfig.json` | Desktop source. Paths `desktopSrc/*`, `apiSrc/*`, `uiSrc/*`, `apiClient`, `apiClient/*` for TypeScript / IDE intellisense | ESLint for desktop files, TS language server |\n| `configs/tsconfig.json` | Compiler options (`module: CommonJS`, `esModuleInterop`) used by `ts-node` to load the `.ts` webpack configs | `ts-node` via `TS_NODE_PROJECT` set in `build:main` / `build:main:stage` / `build:stage` |\n| `.storybook/tsconfig.json` | Storybook framework files, extends UI tsconfig | Storybook + ESLint |\n| `stories/tsconfig.json` | Story files. Extends UI tsconfig; rewires `uiSrc/*` to resolve from `redisinsight/ui` | Storybook + ESLint |\n| `tests/e2e-playwright/tsconfig.json` | Playwright E2E suite | E2E runner (sub-project with own package.json, eslint-ignored at root) |\n\n## Webpack path resolution (`configs/webpack.config.base.ts`)\n\nWebpack uses explicit `resolve.alias` entries (not `tsconfig-paths-webpack-plugin`):\n\n```ts\nalias: {\n  desktopSrc: webpackPaths.desktopSrcPath,\n  apiSrc: resolve(webpackPaths.apiPath, 'src'),\n  uiSrc: webpackPaths.uiSrcPath,\n  apiClient: resolve(webpackPaths.riPath, 'api-client'),\n  // ...\n}\n```\n\nIf you add a new path alias used by desktop code, add it in **both** places: `redisinsight/desktop/tsconfig.json` (for TypeScript / ESLint / IDE) and `configs/webpack.config.base.ts` `resolve.alias` (for the electron bundle).\n\n## ts-node and webpack TS configs\n\nThe webpack configs in `configs/` are `.ts` files with type annotations. When `webpack --config ./configs/*.ts` runs, `ts-node` compiles them. Three package.json scripts set `TS_NODE_PROJECT=./configs/tsconfig.json` so ts-node uses the right compiler options:\n\n- `build:main`\n- `build:main:stage`\n- `build:stage`\n\n`configs/tsconfig.json` deliberately does not set `allowJs`. Any `.js` file inside `scripts/` or `configs/` must be CommonJS (`require` / `module.exports`) — Node 22's module syntax detection will treat ESM-syntax `.js` files as ESM and break references to `__dirname`. Two scripts converted for this reason: `scripts/prebuild.js`, `scripts/DeleteSourceMaps.js`.\n\n## ESLint\n\n`.eslintrc.js`:\n\n- Root `parserOptions.project: true` — auto-discovers the nearest tsconfig per file.\n- API override: `parserOptions.project: redisinsight/api/tsconfig.json` (explicit).\n- UI override: `parserOptions.project: redisinsight/ui/tsconfig.json` (explicit).\n- Files in `.storybook/`, `.github/`, etc. are not linted by `eslint .` because ESLint skips dot-prefixed paths during directory expansion.\n\nIf you add a top-level TS folder that ESLint will reach, drop a tsconfig in it or `parserOptions.project: true` will fail to find one.\n\n## Common tasks\n\n### Adding a path alias used by desktop / electron\n\n1. Add the alias to `redisinsight/desktop/tsconfig.json` `compilerOptions.paths`.\n2. If the alias is also used by UI code, add it to `redisinsight/ui/tsconfig.json`.\n3. If the alias is also used by API code, add it to `redisinsight/api/tsconfig.json`.\n4. For aliases used by the **electron bundle** (anything imported from `redisinsight/desktop/**`), also mirror the alias into `configs/webpack.config.base.ts` `resolve.alias`. Vite resolves UI aliases from `redisinsight/ui/tsconfig.json` automatically.\n\n### Adding a new top-level TS folder\n\nCreate a `tsconfig.json` in it. Bare minimum:\n\n```json\n{\n  \"extends\": \"../redisinsight/ui/tsconfig.json\",\n  \"include\": [\"**/*\"]\n}\n```\n\nWithout it, ESLint's `parserOptions.project: true` will error on those files.\n\n### Adding a new `.ts` webpack config under `configs/`\n\nNo action needed — `configs/tsconfig.json` includes `**/*`.\n\n### Adding a Node script that needs to run without ts-node\n\nPlace under `scripts/` as CommonJS (`require`/`module.exports`). Do not use ESM syntax — `configs/tsconfig.json` no longer has `allowJs` and Node 22 will load ESM-syntax `.js` files as ESM (no `__dirname`).\n",".ai/skills/type-check-baselines/SKILL.md":"---\nname: type-check-baselines\ndescription: >-\n  Run, refresh, or recover from RedisInsight's per-project TypeScript error\n  baselines (.tscheck.rec.json). Use when CI reports \"baseline is outdated\" or\n  \"more TS errors than previously recorded\", when the user mentions tscheck /\n  type-check / .tscheck.rec.json, when introducing or fixing TS errors, or when\n  reviewing PRs that touch baseline files.\n---\n\n# TS error baselines\n\nRedisInsight gates TypeScript errors per project via a one-way ratchet: current error counts are recorded in `.tscheck.rec.json` files, and CI fails if any (file × error-code) count increases. Counts can only go down.\n\n## Projects and commands\n\n| Project | tsconfig used | Baseline file | Per-project compare |\n| - | - | - | - |\n| UI | `redisinsight/ui/tsconfig.json` | `redisinsight/ui/.tscheck.rec.json` | `npm run type-check --prefix redisinsight/ui` |\n| API | `redisinsight/api/tsconfig.check.json` (strict, extends base) | `redisinsight/api/.tscheck.rec.json` | `npm run type-check --prefix redisinsight/api` |\n| Desktop | `redisinsight/desktop/tsconfig.json` | `redisinsight/desktop/.tscheck.rec.json` | `npm run type-check --prefix redisinsight/desktop` |\n| Configs | `configs/tsconfig.json` | — (must stay at 0 errors) | `npx tsc --project configs/tsconfig.json --noEmit` |\n\nRun all checks together from the repo root:\n\n- `npm run type-check` — compare against baselines (all four projects). E2E Playwright is type-checked by a separate workflow (`tests-e2e-playwright-lint.yml`) — not part of this.\n- `npm run tscheck` — refresh baselines for ui/api/desktop after fixing errors. Projects whose error count didn't change produce no diff.\n- `npm run tscheck:force` — force-overwrite baselines for ui/api/desktop. Emergencies only.\n\n**Always run refresh commands through the root `npm run tscheck` / `npm run tscheck:force` wrappers.** The per-workspace refresh scripts (`npm run tscheck --prefix redisinsight/<ws>`) shell out to `tsc`, `tsx`, and `tsc-output-parser`, which are installed only in the **root** `node_modules/.bin/` — this repo is not an npm workspace, so `npm run --prefix` only exposes the sub-dir's bin, not the root's. The root wrappers exist precisely to avoid that trap by running in the root context first. If you must invoke the per-package script directly, prepend the root bin dir manually: `PATH=\"$PWD/node_modules/.bin:$PATH\" npm run tscheck --prefix redisinsight/ui`.\n\n## API has a dedicated check tsconfig\n\n`redisinsight/api/tsconfig.check.json` extends the base `tsconfig.json` and adds:\n\n```json\n\"strict\": true,\n\"strictPropertyInitialization\": false,\n\"useUnknownInCatchVariables\": false,\n\"noEmit\": true\n```\n\nThe base `tsconfig.json` stays as-is so `nest build` is unaffected. **Never enable `strict` in the base** — it breaks the production build.\n\n## Workflows\n\n### CI says \"more TS errors than previously recorded\"\n\nYou introduced new errors. Fix them. Read the script output — it lists the file, error code (TSxxxx), and message for each new error. Do **not** run `tscheck:force` in any workspace to paper over them.\n\n### CI says \"baseline is outdated\"\n\nYou fixed errors (good). Refresh baselines from the repo root:\n\n```sh\nnpm run tscheck\n```\n\nThis runs the refresh for ui, api, and desktop; only the project whose count changed will produce a diff. Commit the updated `.tscheck.rec.json`.\n\n### Adding a brand-new file with TS errors\n\nSame rule: the file × error-code counts went from 0 to N — that's \"new errors\". Fix them before merging. Strict-mode escape hatches (`as any`, `// @ts-expect-error` with justification) are acceptable when fixing legitimately is out of scope, but prefer real fixes.\n\n### Bootstrapping a fresh baseline\n\nOnly needed once per project (already done for ui/api/desktop). The non-force `npm run tscheck --prefix redisinsight/<workspace>` calls `compare` first, which fails against an empty baseline. Use `npm run tscheck:force --prefix redisinsight/<workspace>` for the very first baseline only.\n\n### After `npm install` in `redisinsight/api/`\n\nThe api postinstall regenerates `redisinsight/api-client/`. That can shift UI and Desktop error counts (they both import from `apiClient`). If `npm run type-check:ui` or `npm run type-check:desktop` reports drift after an api install, refresh those baselines.\n\n### Local UI check disagrees with CI\n\nUI plugins under `redisinsight/ui/src/packages/{redisearch, redisgraph, redistimeseries-app, ri-explain, clients-list}` are sub-projects whose source gets type-checked via the UI tsconfig. Their deps live in nested `node_modules` populated by `npm run build:statics` (or by running `npm install --prefix redisinsight/ui/src/packages/<plugin>`). CI runs `npm run build:statics` before `npm run type-check:ui`, so the baseline reflects \"plugin deps installed.\"\n\nIf `npm run type-check:ui` shows TS7016 (\"Could not find a declaration file for module ...\") errors that CI doesn't, you're missing plugin deps. Run `npm run build:statics` once, then re-run the check. Don't refresh the baseline to your local state — CI runs with plugin deps installed.\n\n### Local Desktop check disagrees with CI\n\nDesktop type-check needs `redisinsight/api/dist/` populated with the **dev** nest build (`npm run build --prefix redisinsight/api`, not `build:prod` — prod skips `.d.ts` emission). CI does this automatically. Locally, build api once before generating or refreshing the desktop baseline.\n\n## Reviewing PRs\n\nReject PRs that:\n\n- Bump a `.tscheck.rec.json` file × code count **up** without a corresponding code fix.\n- Use `tscheck:force` in any workspace (look for the diff: a force overwrite typically touches many lines in the rec file with no related TS changes).\n- Enable `strict` in `redisinsight/api/tsconfig.json` (the base). Strict for api belongs only in `tsconfig.check.json`.\n\nApprove PRs that:\n\n- Leave counts unchanged.\n- Decrease counts (with the rec file updated and committed).\n",".cursor/agents/test-runner.md":"---\nname: test-runner\nmodel: composer-1.5\ndescription: Test automation expert. Use proactively to run tests and fix failures.\n---\n\nYou are a test automation expert.\n\nWhen you see code changes, proactively run appropriate tests.\n\nWhen completing a major task, run all tests to ensure the task is complete.\n\nIf tests fail:\n\n1. Analyze the failure output\n2. Identify the root cause\n3. Fix the issue while preserving test intent\n4. Re-run to verify\n\nReport test results with:\n\n- Number of tests passed/failed\n- Summary of any failures\n- Changes made to fix issues\n",".cursor/mcp.json":"../mcp.json",".cursor/skills/branches/SKILL.md":"../../../.ai/skills/branches/SKILL.md",".cursor/skills/commits/SKILL.md":"../../../.ai/skills/commits/SKILL.md",".cursor/skills/feature-flags/SKILL.md":"../../../.ai/skills/feature-flags/SKILL.md",".cursor/skills/pull-requests/SKILL.md":"../../../.ai/skills/pull-requests/SKILL.md",".github/copilot-instructions.md":"# GitHub Copilot Instructions for RedisInsight\n\n> **🎯 Start here: Read `AGENTS.md` at the repository root for essential commands, testing instructions, and quick reference**\n\nThis project uses a centralized AI rules structure:\n\n- **`AGENTS.md`** (repository root) - Entry point with commands, testing, and boundaries\n- **`.ai/skills/`** - Detailed development standards as skill files (one folder per topic, each with a `SKILL.md`)\n- **`.ai/commands/`** - AI workflow commands and templates\n\n## 📂 Skills Structure\n\n### Core Development Skills\n\n- **Code Quality**: `.ai/skills/code-quality/SKILL.md`\n\n  - TypeScript best practices\n  - Import organization\n  - SonarJS complexity rules\n\n- **Frontend Development**: `.ai/skills/frontend/SKILL.md`\n\n  - React 18 patterns and best practices\n  - Redux Toolkit state management\n  - Styled-components (SCSS deprecated)\n  - Component folder structure\n  - Internal UI component wrappers (never import from @redis-ui directly)\n  - Elastic UI deprecation (use Redis UI wrappers)\n\n- **Backend Development**: `.ai/skills/backend/SKILL.md`\n\n  - NestJS module architecture\n  - Service and controller patterns\n  - DTOs and validation\n  - Error handling\n  - Redis integration patterns\n\n- **Testing Standards**: `.ai/skills/testing/SKILL.md`\n\n  - Jest and Testing Library patterns\n  - Component testing with renderComponent helper\n  - Faker for test data generation\n  - No fixed timeouts (use waitFor)\n  - Backend testing with NestJS\n\n- **E2E Testing**: `.ai/skills/e2e-testing/SKILL.md`\n\n  - Playwright standards, page object models, fixtures\n\n- **Git Safety**: `.ai/skills/git-safety/SKILL.md`\n\n  - Protected-branch guardrails (no direct commits to main/latest/release)\n\n- **Commit Messages**: `.ai/skills/commits/SKILL.md`\n\n  - Commit message format (Conventional Commits)\n\n- **Pull Requests**: `.ai/skills/pull-requests/SKILL.md`\n  - PR process and review guidelines\n  - Pre-commit checklist\n\n### Commands and Workflows\n\n- **PR Plan**: `.ai/commands/pr-plan.md` - Analyze JIRA tickets (RI-XXX) and create detailed implementation plans\n- **Commit Message Generation**: `.ai/commands/commit-message.md` - Generate commit messages following Conventional Commits\n- **PR Review**: `.ai/commands/pull-request-review.md` - Review pull requests and provide feedback\n\n## 🎯 Project Overview\n\n**Tech Stack:**\n\n- Frontend: React 18, TypeScript, Redux Toolkit, styled-components, Vite\n- Backend: NestJS, TypeScript, Node.js\n- Desktop: Electron\n- Testing: Jest, Testing Library, Playwright\n\n**Module Aliases:**\n\n- `uiSrc/*` → `redisinsight/ui/src/*`\n- `apiClient` → `redisinsight/api-client` (auto-generated OpenAPI types consumed by the UI)\n- `desktopSrc/*` → `redisinsight/desktop/src/*`\n\n## 📖 Additional Documentation\n\n- **For AI agents**: Start with `AGENTS.md` at repository root\n- **For human developers**: See `.ai/README.md` for setup and overview\n\n---\n\n**Note**: This is a minimal reference file. GitHub Copilot cannot read the referenced files directly, but developers can access the full guidelines. Claude Code and Codex read `AGENTS.md` directly; Claude Code additionally auto-discovers skills under `.ai/skills/` (Codex via the `.agents/skills` symlink).\n","AGENTS.md":"# RedisInsight AI Agent Instructions\n\nThis file provides essential context and instructions for AI coding agents working on RedisInsight.\n\n## Project Overview\n\n**RedisInsight** is a desktop application for Redis database management built with:\n\n- **Frontend**: React 18, TypeScript, Redux Toolkit, styled-components, Monaco Editor, Vite\n- **Backend**: NestJS, TypeScript, Node.js\n- **Desktop**: Electron for cross-platform distribution\n- **Testing**: Jest, Testing Library, Playwright\n\n**Architecture**:\n\n```\nredisinsight/\n├── ui/          # React frontend (Vite + TypeScript)\n├── api/         # NestJS backend (TypeScript)\n├── desktop/     # Electron main process\n└── tests/       # E2E tests (Playwright)\n```\n\n## Setup Commands\n\n### Development\n\n```bash\n# Frontend development (web)\nnpm run dev:ui\n\n# Backend development\nnpm run dev:api\n\n# Desktop app development (runs all: API + UI + Electron)\nnpm run dev:desktop\n\n# Frontend with coverage\nnpm run dev:ui:coverage\n```\n\n## Testing Instructions\n\n### Run Tests\n\n```bash\n# Frontend tests\nnpm test               # Run all UI tests\n\n# Backend tests\nnpm run test:api       # Run all API tests\n\n# E2E tests\nnpm test --prefix tests/e2e-playwright\n```\n\n### Run Specific Frontend Tests\n\n```bash\n# Run a specific test file\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs'\n\n# Run a specific test by name (use -t flag)\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs' -t 'test name pattern'\n\n# Example:\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/slices/tests/browser/keys.spec.ts' -c 'jest.config.cjs' -t 'refreshKeyInfoAction'\n```\n\n### Before Committing\n\n**ALWAYS run these before committing:**\n\n```bash\n# Lint check\nnpm run lint           # All code\nnpm run lint:ui        # Frontend only\nnpm run lint:api       # Backend only\n\n# Type checking (compares against .tscheck.rec.json baselines for ui/api/desktop + configs)\nnpm run type-check\n\n# Refresh baselines after intentionally adding or fixing TS errors (do not run casually)\nnpm run tscheck\n\n# Tests\nnpm test               # Frontend tests\nnpm run test:api       # Backend tests\n```\n\n`npm run type-check` is the gate — CI fails if any (file × error-code) TS-error count increases. If you intentionally changed TS-error counts, run `npm run tscheck` to refresh the baselines and commit the updated `.tscheck.rec.json` files. See `.ai/skills/type-check-baselines/SKILL.md` for details (including the `npm run tscheck:force` escape hatch).\n\n**Fix any linting errors, type errors, or test failures before committing.**\n\n## Always-On Rules\n\nThese apply to every change in the repo. Skill files contain the full detail; the essentials are listed here so they're never missed.\n\n### Code quality (always)\n\n- Run `npm run lint` and `npm run type-check` before committing — both must pass.\n- TypeScript everywhere. Avoid `any`; use `unknown` if you must.\n- Naming: `PascalCase` components, `camelCase` functions/variables, `UPPER_SNAKE_CASE` constants, `is/has/should` prefix for booleans.\n- No `console.log` in production code (use `console.warn`/`error`).\n- Imports: external → built-ins → internal aliases (`uiSrc/*`, `apiClient`, `desktopSrc/*`) → relative → styles last. UI must not import from backend directly — use `apiClient`.\n- No magic numbers; extract duplicated strings to constants.\n\n### Git safety (always)\n\n- **Never commit, push, or force-push directly to `main`, `latest`, or `release/*`.** Always create a feature branch first.\n- Verify current branch with `git branch --show-current` before any push.\n- All changes go through pull requests.\n\n### Dependency / lockfile management (always)\n\n- After modifying any `package.json` (root, `redisinsight/`, `redisinsight/api/`, or a `redisinsight/ui/src/packages/*` plugin), run `npm install` from that directory and commit the resulting `package-lock.json` changes.\n- Never edit `package-lock.json` files by hand and never run `npm install --ignore-scripts` (or otherwise skip `postinstall`) when preparing a commit — the `postinstall` applies `patch-package` patches, and the lockfile shipped to CI must match what `npm install` produces locally.\n- CI runs `npm ci`, which installs strictly from `package-lock.json` and fails if it is out of sync with `package.json`. A green local install in every changed package's directory is required before pushing.\n- Use the right package manager for the change: `npm install <pkg>` / `npm uninstall <pkg>` (or `npm update`) for dependency changes, never manual edits to `package.json` versions without re-running install.\n\n## Skills\n\nAll detailed development standards are exposed as skills under `.ai/skills/`. Claude Code auto-discovers them; each skill triggers when its description matches the task.\n\n- **Code Quality**: `.ai/skills/code-quality/SKILL.md` - Linting, TypeScript standards, naming, import order\n- **Frontend**: `.ai/skills/frontend/SKILL.md` - React, Redux, UI patterns, styled-components\n- **i18n**: `.ai/skills/i18n/SKILL.md` - i18next translations, locale key conventions, translating backend errors and notifications\n- **Backend**: `.ai/skills/backend/SKILL.md` - NestJS, API patterns, dependency injection\n- **Testing**: `.ai/skills/testing/SKILL.md` - Jest/Testing Library standards, faker, test patterns\n- **E2E Testing**: `.ai/skills/e2e-testing/SKILL.md` - Playwright standards, page objects\n- **Git Safety**: `.ai/skills/git-safety/SKILL.md` - Protected-branch guardrails (detail)\n- **Branches**: `.ai/skills/branches/SKILL.md` - Branch naming conventions\n- **Commits**: `.ai/skills/commits/SKILL.md` - Commit message guidelines\n- **Pull Requests**: `.ai/skills/pull-requests/SKILL.md` - PR process and review guidelines\n- **Feature Flags**: `.ai/skills/feature-flags/SKILL.md` - Adding, promoting, and removing feature flags\n- **TS Error Baselines**: `.ai/skills/type-check-baselines/SKILL.md` - Running and refreshing TypeScript error baselines\n- **Redis UI Components**: `.ai/skills/redis-ui-components/SKILL.md` - Component API references, props, and usage examples (from `@redis-ui/components` package)\n- **RedisInsight Plugins**: `.ai/skills/redis-insight-plugin/SKILL.md` - Building, deploying, and validating Workbench visualization plugins (manifests, `activationMethod`, phased workflow, command parsing); internal plugins under `redisinsight/ui/src/packages/` must also follow the Frontend, Code Quality, Redis UI Components, and Testing skills\n\n**Refer to these files for comprehensive guidelines on each topic.**\n\n## Boundaries\n\n### ✅ Always Do\n\n- Ensure the current branch name follows `.ai/skills/branches/SKILL.md` before opening a PR; rename it if it doesn't\n- Write to `src/` and `tests/` directories\n- Run `npm run lint` and `npm test` before commits\n- Follow naming conventions (camelCase, PascalCase, UPPER_SNAKE_CASE)\n- Use faker library for test data generation\n- Use `renderComponent` helper in component tests\n- Extract duplicate strings to constants\n- Use semantic colors from theme (not CSS variables)\n- Use layout components (Row/Col/FlexGroup) instead of div\n- Pass layout props as component props (not hardcoded in styles)\n- Consult the `redis-ui-components` skill (`.ai/skills/redis-ui-components/`) for component APIs when writing frontend code\n- Consult the `redis-insight-plugin` skill (`.ai/skills/redis-insight-plugin/`) when creating or modifying anything under `redisinsight/ui/src/packages/**` or any plugin manifest with a `visualizations` field\n\n### ⚠️ Ask First\n\n- Database schema changes\n- Adding new dependencies\n- Modifying CI/CD configuration (`.github/workflows/`)\n- Changes to build configuration\n- Breaking changes to APIs\n\n### 🚫 Never Do\n\n- Commit secrets or API keys\n- Edit `node_modules/` or `vendor/` directories\n- Edit `package-lock.json` by hand or commit a lockfile produced with `--ignore-scripts` / a skipped `postinstall`\n- Use fixed time waits in tests (use `waitFor` instead)\n- Use `!important` in styled-components\n- Import directly from `@redis-ui/*` (use internal wrappers from `uiSrc/components/ui`)\n- Use Elastic UI for new code (migrating to Redis UI)\n- Use hardcoded pixel values (use theme spacing)\n- Use `any` type without reason\n","CLAUDE.md":"AGENTS.md","docs/plugins/development.md":"# Plugin development\n\nThis document describes the guides to develop your own plugin for the Redis Insight Workbench.\n\n## How it works\n\nPlugin visualization in the Workbench is rendered using Iframe to encapsulate plugin scripts and styles, described in\nthe main plugin script and the stylesheet (if it has been specified in the `package.json`),\niframe includes basic styles as well.\n\n## Plugin structure\n\nEach plugin should have a unique name with all its files [loaded](installation.md) to\na separate folder inside the default `plugins` folder.\n\n> Default plugins are located inside the application.\n\n### Files\n\n`package.json` should be located in the root folder of your plugins, all other files can be included into a subfolder.\n\n- **pluginName/package.json** _(required)_ - Manifest of the plugin\n- **pluginName/{anyName}.js** _(required)_ - Core script of the plugin\n- **pluginName/{anyName}.css** _(optional)_ - File with styles for the plugin visualizations\n- **pluginName/{anyFileOrFolder}** _(optional)_ - Specify any other file or folder inside the plugin folder\n  to use by the core module script. _For example_: pluginName/images/image.png.\n\n## `package.json` structure\n\nThis is the required manifest to use the plugin. `package.json` file should include\nthe following **required** fields:\n\n<table>\n  <tr>\n    <td><i>name</i></td>\n    <td>Plugin name. It is recommended to use the folder name as the plugin name in the package.json.</td>\n  </tr>\n  <tr>\n    <td><i>main</i></td>\n    <td>Relative path to the core script of the plugin. <i>Example: </i> \"./dist/index.js\"</td>\n  </tr>\n  <tr>\n    <td><i>visualizations</i></td>\n    <td>\n      Array of visualizations (objects) to visualize the results in the Workbench.\n      <br><br>\n      Required fields in visualizations:\n      <ul>\n        <li><strong><i>id</i></strong> - visualization id</li>\n        <li><strong><i>name</i></strong> - visualization name to display in the Workbench</li>\n        <li><strong><i>activationMethod</i></strong> - name of the exported function to call when \nthis visualization is selected in the Workbench</li>\n        <li>\n          <strong><i>matchCommands</i></strong> - array of commands to use the visualization for. Supports regex string. \n          <i>Example: </i> [\"CLIENT LIST\", \"FT.*\"]\n        </li>\n      </ul>\n    </td>\n  </tr>\n</table>\n\nYou can specify the path to a css file in the `styles` field. If specified,\nthis file will be included inside the iframe plugin.\n\nSimple example of the `package.json` file with required and optional fields:\n\n```json\n{\n  \"author\": {\n    \"name\": \"Redis Ltd.\",\n    \"email\": \"support@redis.com\",\n    \"url\": \"https://redis.com/redis-enterprise/redis-insight\"\n  },\n  \"description\": \"Show client list as table\",\n  \"styles\": \"./dist/styles.css\",\n  \"main\": \"./dist/index.js\",\n  \"name\": \"client-list\",\n  \"version\": \"0.0.1\",\n  \"scripts\": {},\n  \"visualizations\": [\n    {\n      \"id\": \"clients-list\",\n      \"name\": \"Table\",\n      \"activationMethod\": \"renderClientsList\",\n      \"matchCommands\": [\"CLIENT LIST\"],\n      \"description\": \"Example of client list plugin\",\n      \"default\": true\n    }\n  ],\n  \"devDependencies\": {},\n  \"dependencies\": {}\n}\n```\n\n## Core script of the plugin\n\nThis is the required script with defined visualization methods.\nThe core script contains function and its export (functions - for multiple visualizations),\nwhich is run after the relevant visualization is selected in the Workbench.\n\nThe following function receives props of the executed commands:\n\n```typescript\ninterface Props {\n  command: string; // executed command\n  data: Result[]; // array of results (one item for Standalone)\n}\n\ninterface Result {\n  response: any; // response of the executed command\n  status: 'success' | 'fail'; // response status of the executed command\n}\n\nconst renderVisualization = (props: Props) => {\n  // Do your magic\n};\n\nexport default { renderVisualization };\n```\n\nEach plugin iframe has basic styles of Redis Insight application, including fonts and color schemes.\n\nIt is recommended to use the React & [Elastic UI library](https://elastic.github.io/eui/#/) for\nconsistency with plugin visualisations and the entire application.\n\nFind the example of the plugin here.\n\n- [Client List Plugin README](https://github.com/RedisInsight/Packages/blob/main/clients-list-example/README.md)\n- [Client List Plugin dir](https://github.com/RedisInsight/Packages/blob/main/clients-list-example/)\n\n### Available parameters\n\nAdditional information provided to the plugin iframe is included in the `window.state`\ninside of the plugin script.\n\n```javascript\nconst { config, modules } = window.state;\nconst { baseUrl, appVersion } = config;\n\n// modules - the list of modules of the current database\n// baseUrl - url for your plugin folder - can be used to include your assets\n// appVersion - version of the Redis Insight application\n```\n\n### Plugin rendering\n\nTo render the plugin visualization, the iframe with basic html is generated which is\nthen populated with relevant scripts and styles. To render the html data, use existing\nDOM Element `#app` or create your own DOM Elements.\nRendered iframe also includes `theme_DARK` or `theme_LIGHT` className on `body` to indicate the application theme used.\n\n_Javascript Example:_\n\n```javascript\nconst renderVisualization = (props) => {\n  const { command, data = [] } = props;\n  const [{ result, status }] = data;\n  document.getElementById('app').innerHTML = `\n        <h3>Executed command:<h3>\n        <p>${command}</p>\n        <h4>Result of the command</h4>\n        <p>${result}</p>\n        <h4>Status of the command</h4>\n        <p>${status}</p>\n      `;\n};\n\nexport default { renderVisualization };\n```\n\n_React Example:_\n\n```javascript\nimport { render } from 'react-dom';\nimport App from './App';\n\nconst renderVisualization = (props) => {\n  const { command, data = [] } = props;\n  const [{ result, status }] = data;\n  render(\n    <App command={command} response={result} status={status} />,\n    document.getElementById('app'),\n  );\n};\n\n// This is a required action - export the main function for execution of the visualization\nexport default { renderVisualization };\n```\n\n## Plugin communication\n\nUse the [redisinsight-plugin-sdk](https://www.npmjs.com/package/redisinsight-plugin-sdk), which is a third party library,\nto communicate with the main app.\n\nFind the list and\ndescription of methods called in the\n[README.md](../../redisinsight/ui/src/packages/redisinsight-plugin-sdk/README.md).\n","docs/plugins/installation.md":"# Plugin installation & Usage\n\nThis document describes the guides to add `plugins` for the Workbench to Redis Insight.\n\n## Installation guide\n\n**Note**: While adding new plugins for Workbench, use files only from trusted\nauthors to avoid automatic execution of malicious code.\n\n1. Download the plugin for the Workbench.\n2. Open the `plugins` folder with the following path\n   - For MacOs: `<usersHomeDir>/.redis-insight/plugins`\n   - For Windows: `C:/Users/{Username}/.redis-insight/plugins`\n   - For Linux: `<usersHomeDir>/.redis-insight/plugins`\n3. Add the folder with plugin to the `plugins` folder\n\nTo see the uploaded plugin visualizations in the command results, reload the Workbench\npage and run Redis command relevant for this visualization.\n\n## Usage\n\nThe plugin may contain different visualizations for any Redis commands.\nBelow you can find a guide to see command results in the uploaded plugin visualization:\n\n1. Open Redis Insight\n2. Open a database added\n3. Open the Workbench\n4. Run the Redis command relevant for the plugin visualization\n5. Select the plugin visualization to display results in (if this visualization has not been set by default)\n","docs/plugins/introduction.md":"# Introduction to plugins for the Workbench\n\n## Introduction\n\nRedis can hold a range of different data types. Visualizing these in a\nformat that’s convenient to you for validation and debugging is paramount.\nYou can now easily extend the core functionality of Redis Insight independently by\nbuilding your own custom visualization plugin.\n\nData visualization provided by the plugin is rendered within the\nWorkbench results area and is based on the executed command, ie. a certain\nRedis command can generate its own custom data visualization.\n\nWe have included the following [plugin package example](https://github.com/RedisInsight/RedisInsight/tree/main/redisinsight/ui/src/packages/clients-list) for your reference: running the CLIENT LIST command presents the output in a tabular format for easier reading.\n\n## Wiki\n\n- [Installation and Usage](installation.md)\n- [Plugin Development](development.md)\n","redisinsight/ui/src/packages/clients-list/README.md":"# Plugin for the “Client List” command\n\nThe plugin has been created using React, TypeScript, and [Elastic UI](https://elastic.github.io/eui/#/).\n[Parcel](https://parceljs.org/) is used to build the plugin.\n\n## Running locally\n\nThe following commands will install dependencies and start the server to run the plugin locally:\n\n```\nnpm install\nnpm start\n```\n\nThese commands will install dependencies and start the server.\n\n_Note_: Base styles are included to `index.html`\nfrom [RedisInsight](https://github.com/RedisInsight/RedisInsight) repository.\n\n_From Redis Insight Repo_:\nThis command will generate the `vendor` folder with styles and fonts of the core app. Add this folder\ninside the folder for your plugin and include appropriate styles to the `index.html` file.\n\n```\nnpm install\nnpm install --prefix redisinsight/api\n\nnpm run build:statics - for Linux or MacOs\nnpm run build:statics:win - for Windows\n```\n\n## Build plugin\n\nThe following commands will build plugins to be used in Redis Insight:\n\n```\nnpm install\nnpm run build\n```\n\n[Add](https://github.com/RedisInsight/RedisInsight/blob/main/docs/plugins/installation.md) the package.json file and the\n`dist` folder to the folder with your plugin, which should be located in the `plugins` folder.\n","redisinsight/ui/src/packages/geodata/README.md":"# Geodata Workbench Plugin\n\nInternal RedisInsight Workbench plugin for Redis GEO commands.\n\n## Visualizations\n\n- `Geospatial map` plots coordinate-bearing search results for `GEOSEARCH`, `GEORADIUS`, and `GEORADIUSBYMEMBER`.\n- `Geospatial heatmap` renders the same coordinate-bearing search results as density.\n- `Geospatial details` summarizes every Redis GEO command family, including write and store commands.\n\nThe map views require `WITHCOORD` in command output. The inspector can render scalar, integer, hash, position, search, and search-store responses without coordinates.\n\n`Geospatial map` is the default visualization for coordinate search commands that include `WITHCOORD`. `Geospatial details` is the default for scalar, store, and non-coordinate GEO search commands, and remains selectable for coordinate search commands.\n\n## Tile Policy\n\nThe plugin loads OSM tiles from the runtime default config; fallback renders unavailable state.\n\nFor an official upstream PR, maintainers may still require the default tile provider to be disabled or replaced by a Redis-approved tile backend.\n\n## Development\n\n```sh\nnpm install --prefix redisinsight/ui/src/packages/geodata\nnpm test --prefix redisinsight/ui/src/packages/geodata\nnpm run build --prefix redisinsight/ui/src/packages\n```\n","redisinsight/ui/src/packages/redisgraph/README.md":"# RedisGraph Plugin for Redis Insight v2\n\nThe example has been created using React, TypeScript, and [Elastic UI](https://elastic.github.io/eui/#/).\n[Parcel](https://parceljs.org/) is used to build the plugin.\n\n## Running locally\n\nThe following commands will install dependencies and start the server to run the plugin locally:\n\n```\nnpm install\nnpm start\n```\n\nThese commands will install dependencies and start the server.\n\n_Note_: Base styles are included to `index.html` from the repository.\n\nThis command will generate the `vendor` folder with styles and fonts of the core app. Add this folder\ninside the folder for your plugin and include appropriate styles to the `index.html` file.\n\n```\nnpm run build:statics - for Linux or MacOs\nnpm run build:statics:win - for Windows\n```\n\n## Build plugin\n\nThe following commands will build plugins to be used in Redis Insight:\n\n```\nnpm install\nnpm run build\n```\n\n[Add](../../../../../docs/plugins/installation.md) the package.json file and the\n`dist` folder to the folder with your plugin, which should be located in the `plugins` folder.\n","redisinsight/ui/src/packages/redisinsight-plugin-sdk/README.md":"# RedisInsight-plugin-sdk\n\nThe high-level API for communication between Redis Insight\nplugin and Redis Insight application.\n\n## Usage\n\n```\nnpm install redisinsight-plugin-sdk\nor\nnpm install redisinsight-plugin-sdk\n```\n\n## Available methods\n\n### setHeaderText(text)\n\nSets any custom text to the header of the command result\n\n**Parameters:**\n\n- `text` **{String}**\n\n**Example:**\n\n```js\nimport { setHeaderText } from 'redisinsight-plugin-sdk';\nsetHeaderText('Matched: 10');\n```\n\n### executeRedisCommand(command)\n\nExecutes a Redis command _(currently, only read-only commands are supported)_.\n\n**Parameters:**\n\n- `command` **{String}** - command to execute\n\n**Returns:**\n\n- `Promise<[{ response, status }]>`\n\n```js\n/**\n * @async\n * @param {String} command\n * @returns {Promise.<[{ response, status }]>}\n * @throws {Error}\n */\n```\n\n**Example:**\n\n```js\nimport { executeRedisCommand } from 'redisinsight-plugin-sdk';\ntry {\n  const result = await executeRedisCommand('GET foo');\n  const [{ response, status }] = result;\n  if (status === 'success') {\n    // Do smth\n  }\n} catch (e) {\n  console.error(e);\n}\n```\n\n### getState()\n\nReturns saved state for the command visualization.\n\nThrow an error if the state has not been saved.\n\n**Parameters:**\n\n- `state` **{any}** - any data to save\n\n**Returns:**\n\n- `Promise<any>`\n\n```js\n/**\n * @async\n * @returns {Promise.<any>} state\n * @throws {Error}\n */\n```\n\n**Example:**\n\n```js\nimport { getState } from 'redisinsight-plugin-sdk';\ntry {\n  const result = await getState();\n} catch (e) {\n  console.error(e);\n}\n```\n\n### setState(state)\n\nSave the state for the command visualization.\n\n**Returns:**\n\n- `Promise<any>`\n\n```js\n/**\n * @async\n * @param {any} state\n * @returns {Promise.<any>} state\n * @throws {Error}\n */\n```\n\n**Example:**\n\n```js\nimport { setState } from 'redisinsight-plugin-sdk';\ntry {\n  await setState({ a: 1, b: 2 });\n} catch (e) {\n  console.error(e);\n}\n```\n\n### formatRedisReply(response, command)\n\nUtil function to parse Redis response\n\nReturns string with parsed cli-like response\n\n**Returns:**\n\n- `Promise<string>`\n\n```js\n/**\n * @async\n * @param {any} response\n * @param {String} command\n * @returns {Promise.<string>} data\n * @throws {Error}\n */\n```\n\n**Example:**\n\n```js\nimport { formatRedisReply } from 'redisinsight-plugin-sdk';\n\ntry {\n  const parsedReply = await formatRedisReply(data[0].response, command);\n\n  /*\n    parsedReply:\n    \n    1) 1) \"COUNT(a)\"\n    2) 1) 1) \"0\"\n    3) 1) \"Cached execution: 1\"\n       2) \"Query internal execution time: 3.134125 milliseconds\"\n   */\n} catch (e) {\n  console.error(e);\n}\n```\n","redisinsight/ui/src/packages/redistimeseries-app/README.md":"# RedisTimeseries Plugin for Redis Insight\n\nThe example has been created using React, TypeScript, and [Elastic UI](https://elastic.github.io/eui/#/).\n[Parcel](https://parceljs.org/) is used to build the plugin.\n\n## Running locally\n\nThe following commands will install dependencies and start the server to run the plugin locally:\n\n```\nnpm install\nnpm start\n```\n\nThese commands will install dependencies and start the server.\n\n_Note_: Base styles are included to `index.html` from the repository.\n\nThis command will generate the `vendor` folder with styles and fonts of the core app. Add this folder\ninside the folder for your plugin and include appropriate styles to the `index.html` file.\n\n```\nnpm run build:statics - for Linux or MacOs\nnpm run build:statics:win - for Windows\n```\n\n## Build plugin\n\nThe following commands will build plugins to be used in Redis Insight:\n\n```\nnpm install\nnpm run build\n```\n\n[Add](../../../../../docs/plugins/installation.md) the package.json file and the\n`dist` folder to the folder with your plugin, which should be located in the `plugins` folder.\n","redisinsight/ui/src/packages/ri-explain/README.md":"# RI-Explain plugin\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# RedisInsight AI Agent Instructions\n\nThis file provides essential context and instructions for AI coding agents working on RedisInsight.\n\n## Project Overview\n\n**RedisInsight** is a desktop application for Redis database management built with:\n\n- **Frontend**: React 18, TypeScript, Redux Toolkit, styled-components, Monaco Editor, Vite\n- **Backend**: NestJS, TypeScript, Node.js\n- **Desktop**: Electron for cross-platform distribution\n- **Testing**: Jest, Testing Library, Playwright\n\n**Architecture**:\n\n```\nredisinsight/\n├── ui/          # React frontend (Vite + TypeScript)\n├── api/         # NestJS backend (TypeScript)\n├── desktop/     # Electron main process\n└── tests/       # E2E tests (Playwright)\n```\n\n## Setup Commands\n\n### Development\n\n```bash\n# Frontend development (web)\nnpm run dev:ui\n\n# Backend development\nnpm run dev:api\n\n# Desktop app development (runs all: API + UI + Electron)\nnpm run dev:desktop\n\n# Frontend with coverage\nnpm run dev:ui:coverage\n```\n\n## Testing Instructions\n\n### Run Tests\n\n```bash\n# Frontend tests\nnpm test               # Run all UI tests\n\n# Backend tests\nnpm run test:api       # Run all API tests\n\n# E2E tests\nnpm test --prefix tests/e2e-playwright\n```\n\n### Run Specific Frontend Tests\n\n```bash\n# Run a specific test file\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs'\n\n# Run a specific test by name (use -t flag)\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs' -t 'test name pattern'\n\n# Example:\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/slices/tests/browser/keys.spec.ts' -c 'jest.config.cjs' -t 'refreshKeyInfoAction'\n```\n\n### Before Committing\n\n**ALWAYS run these before committing:**\n\n```bash\n# Lint check\nnpm run lint           # All code\nnpm run lint:ui        # Frontend only\nnpm run lint:api       # Backend only\n\n# Type checking (compares against .tscheck.rec.json baselines for ui/api/desktop + configs)\nnpm run type-check\n\n# Refresh baselines after intentionally adding or fixing TS errors (do not run casually)\nnpm run tscheck\n\n# Tests\nnpm test               # Frontend tests\nnpm run test:api       # Backend tests\n```\n\n`npm run type-check` is the gate — CI fails if any (file × error-code) TS-error count increases. If you intentionally changed TS-error counts, run `npm run tscheck` to refresh the baselines and commit the updated `.tscheck.rec.json` files. See `.ai/skills/type-check-baselines/SKILL.md` for details (including the `npm run tscheck:force` escape hatch).\n\n**Fix any linting errors, type errors, or test failures before committing.**\n\n## Always-On Rules\n\nThese apply to every change in the repo. Skill files contain the full detail; the essentials are listed here so they're never missed.\n\n### Code quality (always)\n\n- Run `npm run lint` and `npm run type-check` before committing — both must pass.\n- TypeScript everywhere. Avoid `any`; use `unknown` if you must.\n- Naming: `PascalCase` components, `camelCase` functions/variables, `UPPER_SNAKE_CASE` constants, `is/has/should` prefix for booleans.\n- No `console.log` in production code (use `console.warn`/`error`).\n- Imports: external → built-ins → internal aliases (`uiSrc/*`, `apiClient`, `desktopSrc/*`) → relative → styles last. UI must not import from backend directly — use `apiClient`.\n- No magic numbers; extract duplicated strings to constants.\n\n### Git safety (always)\n\n- **Never commit, push, or force-push directly to `main`, `latest`, or `release/*`.** Always create a feature branch first.\n- Verify current branch with `git branch --show-current` before any push.\n- All changes go through pull requests.\n\n### Dependency / lockfile management (always)\n\n- After modifying any `package.json` (root, `redisinsight/`, `redisinsight/api/`, or a `redisinsight/ui/src/packages/*` plugin), run `npm install` from that directory and commit the resulting `package-lock.json` changes.\n- Never edit `package-lock.json` files by hand and never run `npm install --ignore-scripts` (or otherwise skip `postinstall`) when preparing a commit — the `postinstall` applies `patch-package` patches, and the lockfile shipped to CI must match what `npm install` produces locally.\n- CI runs `npm ci`, which installs strictly from `package-lock.json` and fails if it is out of sync with `package.json`. A green local install in every changed package's directory is required before pushing.\n- Use the right package manager for the change: `npm install <pkg>` / `npm uninstall <pkg>` (or `npm update`) for dependency changes, never manual edits to `package.json` versions without re-running install.\n\n## Skills\n\nAll detailed development standards are exposed as skills under `.ai/skills/`. Claude Code auto-discovers them; each skill triggers when its description matches the task.\n\n- **Code Quality**: `.ai/skills/code-quality/SKILL.md` - Linting, TypeScript standards, naming, import order\n- **Frontend**: `.ai/skills/frontend/SKILL.md` - React, Redux, UI patterns, styled-components\n- **i18n**: `.ai/skills/i18n/SKILL.md` - i18next translations, locale key conventions, translating backend errors and notifications\n- **Backend**: `.ai/skills/backend/SKILL.md` - NestJS, API patterns, dependency injection\n- **Testing**: `.ai/skills/testing/SKILL.md` - Jest/Testing Library standards, faker, test patterns\n- **E2E Testing**: `.ai/skills/e2e-testing/SKILL.md` - Playwright standards, page objects\n- **Git Safety**: `.ai/skills/git-safety/SKILL.md` - Protected-branch guardrails (detail)\n- **Branches**: `.ai/skills/branches/SKILL.md` - Branch naming conventions\n- **Commits**: `.ai/skills/commits/SKILL.md` - Commit message guidelines\n- **Pull Requests**: `.ai/skills/pull-requests/SKILL.md` - PR process and review guidelines\n- **Feature Flags**: `.ai/skills/feature-flags/SKILL.md` - Adding, promoting, and removing feature flags\n- **TS Error Baselines**: `.ai/skills/type-check-baselines/SKILL.md` - Running and refreshing TypeScript error baselines\n- **Redis UI Components**: `.ai/skills/redis-ui-components/SKILL.md` - Component API references, props, and usage examples (from `@redis-ui/components` package)\n- **RedisInsight Plugins**: `.ai/skills/redis-insight-plugin/SKILL.md` - Building, deploying, and validating Workbench visualization plugins (manifests, `activationMethod`, phased workflow, command parsing); internal plugins under `redisinsight/ui/src/packages/` must also follow the Frontend, Code Quality, Redis UI Components, and Testing skills\n\n**Refer to these files for comprehensive guidelines on each topic.**\n\n## Boundaries\n\n### ✅ Always Do\n\n- Ensure the current branch name follows `.ai/skills/branches/SKILL.md` before opening a PR; rename it if it doesn't\n- Write to `src/` and `tests/` directories\n- Run `npm run lint` and `npm test` before commits\n- Follow naming conventions (camelCase, PascalCase, UPPER_SNAKE_CASE)\n- Use faker library for test data generation\n- Use `renderComponent` helper in component tests\n- Extract duplicate strings to constants\n- Use semantic colors from theme (not CSS variables)\n- Use layout components (Row/Col/FlexGroup) instead of div\n- Pass layout props as component props (not hardcoded in styles)\n- Consult the `redis-ui-components` skill (`.ai/skills/redis-ui-components/`) for component APIs when writing frontend code\n- Consult the `redis-insight-plugin` skill (`.ai/skills/redis-insight-plugin/`) when creating or modifying anything under `redisinsight/ui/src/packages/**` or any plugin manifest with a `visualizations` field\n\n### ⚠️ Ask First\n\n- Database schema changes\n- Adding new dependencies\n- Modifying CI/CD configuration (`.github/workflows/`)\n- Changes to build configuration\n- Breaking changes to APIs\n\n### 🚫 Never Do\n\n- Commit secrets or API keys\n- Edit `node_modules/` or `vendor/` directories\n- Edit `package-lock.json` by hand or commit a lockfile produced with `--ignore-scripts` / a skipped `postinstall`\n- Use fixed time waits in tests (use `waitFor` instead)\n- Use `!important` in styled-components\n- Import directly from `@redis-ui/*` (use internal wrappers from `uiSrc/components/ui`)\n- Use Elastic UI for new code (migrating to Redis UI)\n- Use hardcoded pixel values (use theme spacing)\n- Use `any` type without reason\n","isInternal":false,"tokens":2029,"sizeBytes":8281},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"AGENTS.md","isInternal":false,"tokens":3,"sizeBytes":9},{"name":"copilot-instructions.md","path":".github/copilot-instructions.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.github/copilot-instructions.md","title":"GitHub Copilot Instructions","category":"copilot-instructions","format":"markdown","content":"# GitHub Copilot Instructions for RedisInsight\n\n> **🎯 Start here: Read `AGENTS.md` at the repository root for essential commands, testing instructions, and quick reference**\n\nThis project uses a centralized AI rules structure:\n\n- **`AGENTS.md`** (repository root) - Entry point with commands, testing, and boundaries\n- **`.ai/skills/`** - Detailed development standards as skill files (one folder per topic, each with a `SKILL.md`)\n- **`.ai/commands/`** - AI workflow commands and templates\n\n## 📂 Skills Structure\n\n### Core Development Skills\n\n- **Code Quality**: `.ai/skills/code-quality/SKILL.md`\n\n  - TypeScript best practices\n  - Import organization\n  - SonarJS complexity rules\n\n- **Frontend Development**: `.ai/skills/frontend/SKILL.md`\n\n  - React 18 patterns and best practices\n  - Redux Toolkit state management\n  - Styled-components (SCSS deprecated)\n  - Component folder structure\n  - Internal UI component wrappers (never import from @redis-ui directly)\n  - Elastic UI deprecation (use Redis UI wrappers)\n\n- **Backend Development**: `.ai/skills/backend/SKILL.md`\n\n  - NestJS module architecture\n  - Service and controller patterns\n  - DTOs and validation\n  - Error handling\n  - Redis integration patterns\n\n- **Testing Standards**: `.ai/skills/testing/SKILL.md`\n\n  - Jest and Testing Library patterns\n  - Component testing with renderComponent helper\n  - Faker for test data generation\n  - No fixed timeouts (use waitFor)\n  - Backend testing with NestJS\n\n- **E2E Testing**: `.ai/skills/e2e-testing/SKILL.md`\n\n  - Playwright standards, page object models, fixtures\n\n- **Git Safety**: `.ai/skills/git-safety/SKILL.md`\n\n  - Protected-branch guardrails (no direct commits to main/latest/release)\n\n- **Commit Messages**: `.ai/skills/commits/SKILL.md`\n\n  - Commit message format (Conventional Commits)\n\n- **Pull Requests**: `.ai/skills/pull-requests/SKILL.md`\n  - PR process and review guidelines\n  - Pre-commit checklist\n\n### Commands and Workflows\n\n- **PR Plan**: `.ai/commands/pr-plan.md` - Analyze JIRA tickets (RI-XXX) and create detailed implementation plans\n- **Commit Message Generation**: `.ai/commands/commit-message.md` - Generate commit messages following Conventional Commits\n- **PR Review**: `.ai/commands/pull-request-review.md` - Review pull requests and provide feedback\n\n## 🎯 Project Overview\n\n**Tech Stack:**\n\n- Frontend: React 18, TypeScript, Redux Toolkit, styled-components, Vite\n- Backend: NestJS, TypeScript, Node.js\n- Desktop: Electron\n- Testing: Jest, Testing Library, Playwright\n\n**Module Aliases:**\n\n- `uiSrc/*` → `redisinsight/ui/src/*`\n- `apiClient` → `redisinsight/api-client` (auto-generated OpenAPI types consumed by the UI)\n- `desktopSrc/*` → `redisinsight/desktop/src/*`\n\n## 📖 Additional Documentation\n\n- **For AI agents**: Start with `AGENTS.md` at repository root\n- **For human developers**: See `.ai/README.md` for setup and overview\n\n---\n\n**Note**: This is a minimal reference file. GitHub Copilot cannot read the referenced files directly, but developers can access the full guidelines. Claude Code and Codex read `AGENTS.md` directly; Claude Code additionally auto-discovers skills under `.ai/skills/` (Codex via the `.agents/skills` symlink).\n","isInternal":false,"tokens":780,"sizeBytes":3207},{"name":"SKILL.md","path":".ai/skills/backend/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/backend/SKILL.md","title":"backend","category":"anthropic-skill","format":"markdown","content":"---\nname: backend\ndescription: >-\n  NestJS backend development patterns for the RedisInsight API:\n  module structure, services, controllers, DTOs, dependency injection,\n  and error handling. Use when editing any file under\n  redisinsight/api/**, writing or modifying NestJS modules,\n  controllers, services, DTOs, providers, or when the user mentions\n  the backend, API, or NestJS.\n---\n\n\n# Backend Development (NestJS/API)\n\n## Module Structure\n\n### NestJS Architecture\n\n- Follow **modular architecture** (feature-based modules)\n- Use **dependency injection** throughout\n- **Separate concerns**: Controllers, Services, Repositories\n- Use **DTOs** for validation and data transfer\n- Apply **proper error handling** with NestJS exceptions\n\n### Module Folder Structure\n\nEach feature module in its own directory under `api/src/`:\n\n```\nfeature/\n├── feature.module.ts           # Module definition\n├── feature.controller.ts       # REST endpoints\n├── feature.service.ts          # Business logic\n├── feature.service.spec.ts     # Service tests\n├── feature.controller.spec.ts  # Controller tests\n├── feature.types.ts            # Interfaces and types related to the feature\n├── dto/                        # Data transfer objects\n│   ├── create-feature.dto.ts\n│   ├── update-feature.dto.ts\n│   └── feature.dto.ts\n├── entities/                   # TypeORM entities\n├── repositories/               # Custom repositories\n├── exceptions/                 # Custom exceptions\n├── guards/                     # Feature-specific guards\n├── decorators/                 # Custom decorators\n└── constants/                  # Feature constants\n```\n\n### File Naming\n\n- **Modules**: `feature.module.ts`\n- **Controllers**: `feature.controller.ts`\n- **Services**: `feature.service.ts`\n- **DTOs**: `create-feature.dto.ts`, `update-feature.dto.ts`\n- **Entities**: `feature.entity.ts`\n- **Interfaces and types**: `feature.types.ts`\n- **Tests**: `feature.service.spec.ts`\n- **Constants**: `feature.constants.ts`\n- **Exceptions**: `feature-not-found.exception.ts`\n\n### Constants Organization\n\nStore feature-specific constants in dedicated constants file:\n\n```typescript\nexport const FEATURE_CONSTANTS = {\n  MAX_NAME_LENGTH: 100,\n  DEFAULT_PAGE_SIZE: 20,\n} as const;\n\nexport const FEATURE_ERROR_MESSAGES = {\n  NOT_FOUND: 'Feature not found',\n  INVALID_INPUT: 'Invalid feature data',\n} as const;\n```\n\n### Imports Order\n\n1. Node.js built-in modules\n2. External dependencies (`@nestjs/*`, etc.)\n3. Internal modules (using `src/*` alias — the backend's self-reference into `redisinsight/api/src/*`)\n4. Local relative imports\n\n## Service Layer\n\n### Service Pattern\n\n- Inject dependencies via constructor\n- Use TypeORM repositories\n- Handle errors with NestJS exceptions\n- Use Logger for important operations\n- Keep business logic in services (not controllers)\n\n### Dependency Injection\n\nAlways inject dependencies via constructor with proper decorators:\n\n```typescript\n@Injectable()\nexport class UserService {\n  constructor(\n    @InjectRepository(User)\n    private readonly userRepository: Repository<User>,\n    private readonly emailService: EmailService,\n  ) {}\n}\n```\n\n## Controller Layer\n\n### Controller Pattern\n\n- Keep controllers thin (delegate to services)\n- Use proper HTTP decorators (`@Get`, `@Post`, etc.)\n- Use `@Body`, `@Param`, `@Query` for inputs\n- Apply guards with `@UseGuards()`\n- Document with Swagger decorators\n\n### HTTP Status Codes\n\n- Use `@HttpCode()` decorator for non-standard codes\n- Return appropriate status codes (200, 201, 204, 400, 404, etc.)\n\n## Data Transfer Objects (DTOs)\n\n### Validation\n\nUse `class-validator` decorators for validation:\n\n- `@IsString()`, `@IsNumber()`, `@IsEmail()`\n- `@IsNotEmpty()`, `@IsOptional()`\n- `@MinLength()`, `@MaxLength()`\n- `@Min()`, `@Max()`\n\n### Swagger Documentation\n\nUse `@ApiProperty()` and `@ApiPropertyOptional()` for Swagger docs.\n\n## Error Handling\n\n### NestJS Exceptions\n\nUse appropriate exception types:\n\n- `NotFoundException` - 404\n- `BadRequestException` - 400\n- `UnauthorizedException` - 401\n- `ForbiddenException` - 403\n- `ConflictException` - 409\n- `InternalServerErrorException` - 500\n\n### Custom Exceptions\n\n**Prefer custom exceptions over generic ones** when you need:\n\n- Consistent error codes for frontend handling\n- Specific error messages from constants\n- Consistent error structure across the codebase\n\nCreate custom exceptions following existing patterns:\n\n```typescript\n// src/modules/feature/exceptions/feature-invalid.exception.ts\nimport {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class FeatureInvalidException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.FEATURE_INVALID,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'FeatureInvalid',\n      errorCode: CustomErrorCodes.FeatureInvalid,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n```\n\n### Error Logging\n\n```typescript\nprivate readonly logger = new Logger(ServiceName.name)\n\nthis.logger.error('Error message', error.stack, { context })\n```\n\n## Redis Integration\n\n### Redis Service Pattern\n\n- Use RedisClient from `src/modules/redis`\n- Handle errors gracefully\n- Log Redis operations\n- Use try-catch for error handling\n\n## Code Quality\n\n### Cognitive Complexity (≤ 15)\n\n- Use early returns to reduce nesting\n- Extract complex logic to separate functions\n- Avoid deeply nested conditions\n\n### No Duplicate Strings\n\nExtract repeated strings to constants in constants file.\n\n## API Documentation (Swagger)\n\n### Required Decorators\n\n- `@ApiTags()` - Group endpoints\n- `@ApiOperation()` - Describe operation\n- `@ApiResponse()` - Document responses\n- `@ApiParam()` - Document params\n- `@ApiQuery()` - Document query params\n- `@ApiBearerAuth()` - Auth requirement\n\n## Checklist\n\n- [ ] Services use dependency injection\n- [ ] DTOs have validation decorators\n- [ ] Controllers have Swagger documentation\n- [ ] Proper HTTP status codes used\n- [ ] Error handling with appropriate exceptions\n- [ ] Logging for important operations\n- [ ] Transactions for related DB operations\n- [ ] Configuration via ConfigService\n- [ ] Guards for authentication/authorization\n- [ ] Cognitive complexity ≤ 15\n","frontmatter":{"name":"backend","description":"NestJS backend development patterns for the RedisInsight API: module structure, services, controllers, DTOs, dependency injection, and error handling. Use when editing any file under redisinsight/api/**, writing or modifying NestJS modules, controllers, services, DTOs, providers, or when the user mentions the backend, API, or NestJS."},"isInternal":false,"tokens":1421,"sizeBytes":6484},{"name":"SKILL.md","path":".ai/skills/branches/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/branches/SKILL.md","title":"branches","category":"anthropic-skill","format":"markdown","content":"---\nname: branches\ndescription: >-\n  Create and name git branches following project conventions. Use when\n  creating branches, checking out new branches, or the user mentions\n  branch naming.\n---\n\n# Branch Naming Conventions\n\nUse lowercase kebab-case with type prefix and issue/ticket identifier.\n\n```bash\n# Pattern: <type>/<issue-ref>/<short-title>\n\n# INTERNAL (JIRA - RI-XXX)\nfeature/RI-123/add-user-profile\nbugfix/RI-789/memory-leak\nfe/feature/RI-567/add-dark-mode\nbe/bugfix/RI-345/fix-redis-connection\ndocs/RI-333/update-docs\ntest/RI-444/add-unit-tests\ne2e/RI-555/add-integration-tests\n\n# OPEN SOURCE (GitHub - XXX)\nfeature/123/add-export-feature\nbugfix/789/fix-connection-timeout\n\n# Special branches\nrelease/v2.0.0\nric/RI-666/custom-prefix\n```\n\n## Branch Types\n\n- `feature/` - New features and refactoring (affects multiple areas)\n- `bugfix/` - Bug fixes (affects multiple areas)\n- `fe/feature/` - Frontend-only features (only `redisinsight/ui/` folder)\n- `fe/bugfix/` - Frontend-only bug fixes (only `redisinsight/ui/` folder)\n- `be/feature/` - Backend-only features (only `redisinsight/api/` folder)\n- `be/bugfix/` - Backend-only bug fixes (only `redisinsight/api/` folder)\n- `docs/` - Documentation changes\n- `test/` - Test-related changes\n- `e2e/` - End-to-end test changes\n- `release/` - Release branches\n- `ric/` - Custom prefix for special cases\n\n**Note:** When a bug fix affects only the `redisinsight/ui/` folder, use `fe/bugfix/` prefix instead of `bugfix/`.\n\n## Issue References\n\n- **Internal**: `RI-XXX` (JIRA ticket)\n- **Open Source**: `XXX` (GitHub issue number)\n- Use `#` only in commit messages, not branch names\n","frontmatter":{"name":"branches","description":"Create and name git branches following project conventions. Use when creating branches, checking out new branches, or the user mentions branch naming."},"isInternal":false,"tokens":473,"sizeBytes":1634},{"name":"SKILL.md","path":".ai/skills/code-quality/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/code-quality/SKILL.md","title":"code-quality","category":"anthropic-skill","format":"markdown","content":"---\nname: code-quality\ndescription: >-\n  Code-quality standards for RedisInsight: TypeScript strictness,\n  naming conventions (camelCase, PascalCase, UPPER_SNAKE_CASE),\n  linting rules, no `any` without reason, no `!important` in styles,\n  and constant extraction. Use when writing or refactoring any\n  TypeScript/JavaScript code in this repo, when ESLint or Prettier\n  issues come up, or when the user mentions code style, lint, naming\n  conventions, or general code quality.\n---\n\n\n# Code Quality Standards\n\n## Critical Rules\n\n- **ALWAYS run linter** after code changes: `npm run lint`\n- Linter must pass before committing\n- No console.log in production code (use console.warn/error only)\n\n## TypeScript Standards\n\n### Essential Rules\n\n- Use TypeScript for all new code\n- **Avoid `any`** - use proper types or `unknown`\n- **Prefer interfaces** for object shapes\n- Use **type** for unions, intersections, primitives\n- Add explicit return types for non-obvious functions\n- Leverage type inference where clear\n\n## Import Organization\n\n### Required Order (enforced by ESLint)\n\n1. External libraries (`react`, `lodash`, etc.)\n2. Built-in Node modules (`path`, `fs` - backend only)\n3. Internal modules with aliases (`uiSrc/*`, `apiClient`)\n4. Sibling/parent relative imports\n5. Style imports (ALWAYS LAST)\n\n### Module Aliases\n\n- `uiSrc/*` → `redisinsight/ui/src/*` (UI workspace)\n- `apiClient` → `redisinsight/api-client` (auto-generated OpenAPI types — the UI's only entry point into BE-defined shapes)\n- `desktopSrc/*` → `redisinsight/desktop/src/*` (desktop workspace)\n\nThe UI workspace must not import from the backend codebase directly. Use `apiClient` for types and the existing service layer (`uiSrc/services`) for HTTP calls.\n\n✅ **Use aliases**: `import { Button } from 'uiSrc/components/Button'`  \n❌ **Avoid relative**: `import { Button } from '../../../ui/src/components/Button'`\n\n## Naming Conventions\n\n- **Components**: `PascalCase` - `UserProfile`\n- **Functions/Variables**: `camelCase` - `fetchUserProfile`\n- **Constants**: `UPPER_SNAKE_CASE` - `MAX_RETRY_ATTEMPTS`\n- **Booleans**: Use `is/has/should` prefix - `isLoading`, `hasError`\n\n## Comments\n\n**Default to fewer comments.** Clear names, small functions, and good\ntests should carry the meaning. Write a comment only when the code\ngenuinely can't explain itself — or when the user explicitly asks for\nmore (for example, on a piece of complex logic).\n\nWhen you do write one, keep it short and use plain words to say\n**what** the code is doing or **why** it has to exist — not how it\nworks under the hood, and not the story of why you made the change\n(that belongs in the commit message or PR description).\n\n✅ **Good** — plain, names the situation:\n\n```ts\n// When switching keys, hide the previous key's loader/error/result\n// until the new key is ready.\nconst showLoader = isArrayKeyReady && loading\n```\n\n❌ **Avoid** — long, mechanism-heavy, re-derives what the code shows:\n\n```ts\n// Gate every aggregate-slice surface (loader, error, result) on\n// `isArrayKeyReady`: when the user switches keys, `keyProp` flips\n// immediately but `selectedKeyData` lags by a round-trip, so the\n// prior key's slice state would otherwise paint under the newly\n// selected (or empty) key for one frame before the hook's reset\n// effect fires.\nconst showLoader = isArrayKeyReady && loading\n```\n\nAlso avoid:\n\n- Restating identifier names in prose (`// set loading to true`).\n- Comments that duplicate what a clearly-named test already asserts.\n- Block comments that summarize obvious blocks (`// loop over items`).\n\n## SonarJS Rules\n\n- Keep cognitive complexity low (refactor complex functions)\n- Extract duplicate strings to constants\n- Follow DRY principle - no duplicate code\n- Use immediate return (avoid unnecessary intermediate variables)\n\n## Best Practices\n\n- Use destructuring for objects and arrays\n- Use template literals over string concatenation\n- Use `const` by default, `let` only when reassignment needed\n- Use descriptive variable names\n- Handle errors properly\n- Clean up subscriptions and timers\n- Use constants instead of magic numbers\n\n## Vite Cache Management\n\nWhen updating npm packages (especially `@redis-ui/*` packages):\n\n1. **Clear Vite cache** after `npm install`:\n\n   ```bash\n   rm -rf node_modules/.vite\n   rm -rf redisinsight/ui/node_modules/.vite\n   ```\n\n2. **Restart dev server** to rebuild dependencies\n\n3. This ensures new package versions are properly loaded\n\n## Pre-Commit Checklist\n\n- [ ] `npm run lint` passes\n- [ ] No TypeScript errors\n- [ ] Import order is correct\n- [ ] No `any` types without reason\n- [ ] No console.log statements\n- [ ] No magic numbers\n- [ ] Descriptive variable names\n- [ ] Low cognitive complexity\n- [ ] No duplicate code\n- [ ] Comments kept to a minimum; any present are short and plain-language\n- [ ] Vite cache cleared (if updated dependencies)\n","frontmatter":{"name":"code-quality","description":"Code-quality standards for RedisInsight: TypeScript strictness, naming conventions (camelCase, PascalCase, UPPER_SNAKE_CASE), linting rules, no `any` without reason, no `!important` in styles, and constant extraction. Use when writing or refactoring any TypeScript/JavaScript code in this repo, when ESLint or Prettier issues come up, or when the user mentions code style, lint, naming conventions, or general code quality."},"isInternal":false,"tokens":1175,"sizeBytes":4878},{"name":"SKILL.md","path":".ai/skills/commits/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/commits/SKILL.md","title":"commits","category":"anthropic-skill","format":"markdown","content":"---\nname: commits\ndescription: >-\n  Generate commit messages following Conventional Commits format. Use when\n  creating commits, writing commit messages, or the user asks to commit changes.\n---\n\n# Commit Message Guidelines\n\nFollow **Conventional Commits** format:\n\n```\n<type>(<scope>): <subject>\n\n<body>\n\n<footer>\n```\n\n## Commit Types\n\n- `feat` - New feature\n- `fix` - Bug fix\n- `refactor` - Code refactoring\n- `test` - Adding or updating tests\n- `docs` - Documentation changes\n- `chore` - Maintenance tasks\n- `style` - Code style changes (formatting)\n- `perf` - Performance improvements\n- `ci` - CI/CD changes\n\n## Examples\n\n```bash\nfeat(ui): add user search functionality\n\nImplements real-time search with debouncing.\n\nReferences: #RI-123\n\n---\n\nfix(api): resolve memory leak in connection pool\n\nProperly cleanup subscriptions on unmount.\n\nFixes #456\n\n---\n\ntest(ui): add tests for data serialization\n\nrefactor(api): extract common validation logic\n\ndocs: update API endpoint documentation\n\nchore: upgrade React to version 18.2\n```\n\n## Best Practices\n\n### ✅ Good Commits\n\n- Clear, descriptive subject line\n- Atomic changes (one logical change per commit)\n- Reference issue/ticket in body\n- Explain **why**, not just **what**\n- **Keep it concise** - Don't list every file change in the body\n\n```bash\nfeat(ui): add user profile editing\n\nAllows users to update their profile information including\nname, email, and avatar. Includes validation and error handling.\n\nReferences: #RI-123\n```\n\n### ❌ Bad Commits\n\n```bash\n# Too vague\nfix stuff\nWIP\nupdate\n\n# Too broad\nadd feature, fix bugs, refactor code, update tests\n```\n\n## Issue References\n\n- **JIRA (internal)**: `References: #RI-123` or `Fixes #RI-123`\n- **GitHub (open source)**: `Fixes #456` or `Closes #456`\n- Use `#` for auto-linking in commit messages\n","frontmatter":{"name":"commits","description":"Generate commit messages following Conventional Commits format. Use when creating commits, writing commit messages, or the user asks to commit changes."},"isInternal":false,"tokens":461,"sizeBytes":1806},{"name":"SKILL.md","path":".ai/skills/dead-dependencies/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/dead-dependencies/SKILL.md","title":"dead-dependencies","category":"anthropic-skill","format":"markdown","content":"---\nname: dead-dependencies\ndescription: >-\n  Find and safely remove unused (\"dead\") npm dependencies in RedisInsight\n  using a grep + leaf-check + build-gate recipe. Use when cleaning up\n  dependencies, investigating whether a package is still used, removing a\n  suspected leftover, or when the user mentions dead deps, unused\n  dependencies, dependency cleanup, leftover packages, or \"is this safe to\n  remove\". Complements the weekly vulnerability audit\n  (`scripts/dependency-audit-report.mjs`), which only reports vulnerabilities.\n---\n\n# Dead Dependencies\n\nIdentify and safely remove unused npm dependencies. This is a **local,\ninteractive** workflow — a scheduled report can only ever guess; confirming a\ndependency is dead requires grepping real usage and running the build.\n\n## Why manual (not knip/depcheck)\n\nStatic tools over-report badly in this repo and must not be trusted blindly:\n\n- The root `package.json` is a **mega-manifest** (UI + desktop + build + test\n  + storybook + electron), so a tool scanning one area flags everything used\n  elsewhere as \"unused\".\n- Lots of dependencies are referenced **without an `import`**: webpack loaders\n  and eslint/jest/babel plugins by string in config, tools invoked from\n  `package.json` `scripts`, runtime `-r` preloads, dynamic `require`, and\n  ambient `@types/*`.\n- The webpack→Vite migration left **genuinely dead** build tooling behind,\n  mixed in with false positives.\n\nThe reliable signal is: **grep for usage → confirm it's a leaf → remove it and\nrun the build gate.** That is exactly how `jsonpath` was confirmed dead and\nremoved (worked example at the bottom).\n\n## The recipe (per candidate)\n\n### 1. Grep the whole repo for real usage\n\n```bash\nPKG=jsonpath   # the dependency to check\ngrep -rn \"$PKG\" \\\n  redisinsight configs scripts tests .storybook \\\n  --include=*.ts --include=*.tsx --include=*.js --include=*.jsx \\\n  --include=*.mjs --include=*.cjs --include=*.json \\\n  2>/dev/null | grep -v node_modules\n```\n\nLook for `import ... from '$PKG'`, `require('$PKG')`, `import('$PKG')`, and\nbare references. Ignore unrelated substring hits (e.g. `nestjs-form-data` when\nchecking `form-data`, or a package name appearing only in tutorial/`manifest`\ntext). **Zero real references → candidate for the next steps.**\n\n### 2. Rule out no-import usage\n\nA clean grep is **necessary but not sufficient**. Check the ways a package is\nused without an `import`:\n\n- **Config string references** — search the build/test config for the bare\n  name: `.eslintrc.js`, `configs/webpack.config.*.ts`,\n  `redisinsight/ui/vite.config.mjs`, `jest.config.cjs`, `babel.config.cjs`,\n  `electron-builder.json`, `.mocharc*`. eslint plugins, webpack loaders, and\n  jest/mocha reporters live here.\n- **`package.json` scripts** — a tool like `concurrently`, `lint-staged`, or a\n  reporter is \"used\" if a `scripts` entry (any workspace) invokes it.\n- **Runtime preloads / dynamic** — `node -r <pkg>`, `require(variable)`, or a\n  wasm/worker loader with a computed path.\n\n### 3. Leaf check\n\n```bash\nnpm ls \"$PKG\"        # in the workspace that declares it\n```\n\nConfirm nothing else in the tree depends on it, and note any transitive deps it\nuniquely pulls (removing `jsonpath` also dropped `underscore`).\n\n### 4. Classify before acting\n\n| Situation | Action |\n| --- | --- |\n| Plain runtime/dev dep, zero references anywhere | **Delete** (after the gate below) |\n| `@types/x` where base `x` bundles its own types (`node_modules/x/package.json` has `types`/`typings`) | **Delete** — the DefinitelyTyped package is obsolete |\n| `@types/x` that's ambient/global-only (e.g. `@types/webpack-env`) | **Keep** — never imported by design |\n| Declared in more than one workspace (`package.json`), used in only one | **Relocate/dedupe** — remove the *unused* declaration, never the used copy |\n| Shadows a Node builtin (`buffer`, `assert`, …) | **Keep** — usually a false positive |\n| Referenced only in a config/`scripts` (step 2 hit) | **Keep** |\n\n### 5. The gate — the only real proof\n\nRemove it, reinstall (per the repo's dependency rules — never hand-edit\n`package.json`/lockfile, never `--ignore-scripts`), and verify the affected\narea builds:\n\n```bash\nnpm uninstall \"$PKG\"          # in the declaring workspace; updates the lockfile\nnpm run type-check            # or the affected workspace's type-check\nnpm run test                  # / test:api, as relevant\nnpm run build                 # if it's a build-time dep\n```\n\nGreen across the relevant checks = safe. Commit the `package.json` +\n`package-lock.json` change (see the git-safety / dependency rules in\n`CLAUDE.md`). If anything goes red, it wasn't dead — restore it.\n\n## Optional: enumerate candidates to sweep\n\nTo triage the whole surface rather than one package, list declared deps and\ngrep each for an import, then apply steps 2–5 to the ones with zero hits:\n\n```bash\nnode -e \"const p=require('./package.json');console.log([...Object.keys(p.dependencies||{}),...Object.keys(p.devDependencies||{})].join('\\n'))\" \\\n| while read PKG; do\n    hits=$(grep -rl --include=*.ts --include=*.tsx --include=*.js --include=*.jsx --include=*.mjs \\\n      -e \"from ['\\\"]$PKG\" -e \"require(['\\\"]$PKG\" redisinsight configs scripts tests 2>/dev/null | grep -vc node_modules)\n    [ \"$hits\" = \"0\" ] && echo \"candidate: $PKG\"\n  done\n```\n\nThis is a **first filter only** — every candidate still goes through steps 2–5.\nExpect false positives (config/string/dynamic/ambient use). Never delete straight\nfrom this list.\n\n## Worked example — `jsonpath`\n\n1. Grep across the repo → the only hits were unrelated tutorial text in a\n   `manifest.json`; **no `import`/`require`**.\n2. Not referenced in any config or script.\n3. `npm ls jsonpath` → a leaf; it was also the sole reason `underscore` was\n   installed.\n4. Plain runtime dep, zero references → likely deletable.\n5. Removed `jsonpath` + `@types/jsonpath`, `npm install`, `npm run type-check`\n   → green. Confirmed dead; committed. (`underscore` dropped with it.)\n","frontmatter":{"name":"dead-dependencies","description":"Find and safely remove unused (\"dead\") npm dependencies in RedisInsight using a grep + leaf-check + build-gate recipe. Use when cleaning up dependencies, investigating whether a package is still used, removing a suspected leftover, or when the user mentions dead deps, unused dependencies, dependency cleanup, leftover packages, or \"is this safe to remove\". Complements the weekly vulnerability audit (`scripts/dependency-audit-report.mjs`), which only reports vulnerabilities."},"isInternal":false,"tokens":1527,"sizeBytes":6025},{"name":"SKILL.md","path":".ai/skills/e2e-testing/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/e2e-testing/SKILL.md","title":"e2e-testing","category":"anthropic-skill","format":"markdown","content":"---\nname: e2e-testing\ndescription: >-\n  Playwright end-to-end testing standards for RedisInsight: page\n  object models, test structure, fixtures, navigation patterns, and\n  flaky-test prevention. Use when editing files under\n  tests/e2e-playwright/**, writing Playwright tests, adding page\n  objects, or when the user mentions Playwright, E2E tests, page\n  objects, or end-to-end testing.\n---\n\n\n# E2E Testing Standards (Playwright)\n\n## Location\n\nAll E2E tests are in `tests/e2e-playwright/`. This is a **standalone package** - no imports from `redisinsight/ui/` or `redisinsight/api/`.\n\n## Test Plan\n\n**Always refer to `tests/e2e-playwright/TEST_PLAN.md`** for:\n- Test coverage status (✅ implemented, 🔲 not implemented)\n- Feature implementation order\n- Test data requirements\n\n**After implementing tests, update TEST_PLAN.md** to mark tests as ✅.\n\n## Project Structure\n\n```\ntests/e2e-playwright/\n├── TEST_PLAN.md         # Master test plan with coverage status\n├── config/              # Configuration (env, databases)\n│   └── databases/       # Database configs by type\n├── fixtures/            # Playwright fixtures\n├── helpers/             # API helpers for setup/teardown\n├── pages/               # Page Object Models\n│   ├── BasePage.ts      # Base class for all pages\n│   ├── InstancePage.ts  # Base class for database instance pages\n│   ├── components/      # Shared components (InstanceHeader, NavigationTabs, BottomPanel)\n│   └── {feature}/       # Feature-specific pages (browser/, cli/, etc.)\n├── test-data/           # Test data factories\n├── tests/               # Test specs organized by project\n│   ├── main/            # Default parallel tests\n│   │   └── {feature}/\n│   │       └── {action}/\n│   ├── auto-update/     # Serial tests with special setup\n│   └── electron/        # Electron-specific tests\n└── types/               # TypeScript types\n```\n\n## Playwright Projects\n\nThe folder a test lives in determines its execution mode. Each browser platform has a parallel project and a serial project:\n\n| Project             | Folder            | Parallelism | Use Case |\n|---------------------|-------------------|-------------|----------|\n| `chromium-parallel` | `tests/parallel/` | Parallel (4 workers) | Standard chromium tests |\n| `chromium-serial`   | `tests/serial/`   | Serial (1 worker)    | Sequential chromium tests |\n| `electron-parallel` | `tests/parallel/` | Serial (1 worker)*   | Standard electron tests |\n| `electron-serial`   | `tests/serial/`   | Serial (1 worker)    | Sequential electron tests |\n\n\\* Electron uses one worker today because there is a single app instance.\n\n### Execution order\n\nFor each platform, the serial project depends on the parallel project via `dependencies: ['<platform>-parallel']` in [playwright.config.ts](../../tests/e2e-playwright/playwright.config.ts), so the order is always:\n\n1. `<platform>-parallel` runs first (with up to N workers)\n2. `<platform>-serial` runs after, one worker at a time\n\nSerial tests perform destructive operations on the shared RTE Redis (`FLUSHDB`, broad `deleteAllIndexes`, dangerous commands) so they can't safely run alongside parallel tests on the same RTE. Electron also can't run its two projects concurrently because the desktop app binds its embedded API on a fixed port (`5530`).\n\nIf serial grows large enough to become a CI bottleneck, the next step is to give serial tests a dedicated Redis instance (or split into a separate CI job) — not to revert the ordering.\n\n### Running Projects\n\n```bash\n# Full platform run (parallel + serial)\nnpx playwright test --project=chromium-parallel --project=chromium-serial\nnpx playwright test --project=electron-parallel --project=electron-serial\n\n# Just parallel\nnpx playwright test --project=chromium-parallel\n\n# Just serial — needs --no-deps, otherwise parallel runs first (it's a\n# project dependency). --no-deps also skips browser-setup.\nnpx playwright test --project=chromium-serial --no-deps\n\nnpx playwright test                           # All projects\n```\n\n### When to put a test in `tests/serial/`\n\nPut a test in `tests/serial/` when it:\n- Shares database state across tests via `beforeAll`\n- Runs dangerous commands or mutates global app state. Settings are global:\n  `scanThreshold`, `dateFormat`, `timezone` and `batchSize` are one shared resource,\n  so a spec that changes one and holds it across other work breaks every concurrent\n  test that reads it\n- Cannot tolerate concurrent execution with other tests\n- Would cause flakiness when run with other tests\n- Require special environment configuration\n\n### Adding a New Project\n\n1. Create folder under `tests/` (e.g., `tests/my-feature/`)\n2. Add project configuration in `playwright.config.ts`:\n\n```typescript\n{\n  name: 'my-feature',\n  testDir: './tests/my-feature',\n  fullyParallel: false, // or true\n  workers: 1,\n  timeout: 120000,\n  // Optional: different setup\n  // globalSetup: './my-feature-setup.ts',\n}\n```\n\n## Page Objects\n\n### Page Object Hierarchy\n\n```\nBasePage (abstract)\n  ├── DatabasesPage           # Databases list page\n  ├── SettingsPage            # Settings page\n  └── InstancePage (abstract) # Base for all database instance pages\n        ├── instanceHeader    # Database name, stats, breadcrumb\n        ├── navigationTabs    # Browse, Workbench, Analyze, Pub/Sub\n        ├── bottomPanel       # CLI, Command Helper, Profiler\n        └── BrowserPage       # Browser-specific (extends InstancePage)\n              └── WorkbenchPage (future)\n              └── AnalyzePage (future)\n              └── PubSubPage (future)\n```\n\n### Extend the Appropriate Base Class\n\n- **BasePage** - For standalone pages (DatabasesPage, SettingsPage)\n- **InstancePage** - For pages within a connected database (BrowserPage, WorkbenchPage, etc.)\n\nPage objects are **stateless** - they don't store database objects. Pass `databaseId` to navigation methods.\n\n```typescript\n// For database instance pages - extend InstancePage\nimport { Page, Locator } from '@playwright/test';\nimport { InstancePage } from '../InstancePage';\n\nexport class WorkbenchPage extends InstancePage {\n  readonly editor: Locator;\n\n  constructor(page: Page) {\n    super(page);\n    this.editor = page.getByTestId('workbench-editor');\n  }\n\n  // InstancePage provides: instanceHeader, navigationTabs, bottomPanel\n  // Plus navigation methods: navigateToBrowser(), openCli(), etc.\n\n  async goto(databaseId: string): Promise<void> {\n    await this.gotoDatabase(databaseId);\n    await this.navigationTabs.gotoWorkbench();\n    await this.waitForLoad();\n  }\n}\n```\n\n### Component-Based Structure\n\nBreak large pages into components:\n\n```typescript\n// pages/feature/FeaturePage.ts\nexport class FeaturePage extends InstancePage {\n  readonly dialog: FeatureDialog;\n  readonly list: FeatureList;\n\n  constructor(page: Page) {\n    super(page);\n    this.dialog = new FeatureDialog(page);\n    this.list = new FeatureList(page);\n  }\n}\n```\n\n## Test Structure\n\n### File Organization\n\n```\ntests/\n├── main/                # Default parallel tests (most tests go here)\n│   └── {feature}/       # e.g., databases, browser, workbench\n│       └── {action}/    # e.g., add, edit, delete\n│           ├── standalone.spec.ts\n│           └── cluster.spec.ts\n├── auto-update/         # Serial tests with special setup\n└── electron/            # Electron-specific tests\n```\n\n### Test Setup Pattern\n\nUse simple, explicit setup with clear separation of concerns. **Page objects are fixtures** - they don't store database state. Pass `databaseId` to `goto()` methods.\n\n```typescript\nimport { test, expect } from '../../../fixtures/base';\nimport { standaloneConfig } from '../../../config/databases/standalone';\nimport { DatabaseInstance } from '../../../types';\n\ntest.describe('Feature > Action', () => {\n  let database: DatabaseInstance;\n\n  // Setup: Create database once for all tests\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase({\n      name: 'test-feature-db',\n      host: standaloneConfig.host,\n      port: standaloneConfig.port,\n    });\n  });\n\n  // Teardown: Clean up database after all tests\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  test.describe('Sub-feature', () => {\n    // Navigation: Pass databaseId to goto() - page is a fixture\n    test.beforeEach(async ({ featurePage }) => {\n      await featurePage.goto(database.id);\n    });\n\n    // Tests receive page fixtures they need\n    test('should do something', async ({ featurePage }) => {\n      await featurePage.doAction();\n      await expect(featurePage.result).toBeVisible();\n    });\n\n    // Tests that need both page and apiHelper\n    test('should create and verify', async ({ featurePage, apiHelper }) => {\n      await apiHelper.createKey(database.id, 'test-key', 'value');\n      await featurePage.refresh();\n      await expect(featurePage.keyList).toContainText('test-key');\n    });\n  });\n});\n```\n\n### Key Principles\n\n1. **`beforeAll`** - Create database/test data via API (runs once)\n2. **`afterAll`** - Clean up database/test data via API (runs once)\n3. **`beforeEach`** - Navigate to page via UI using `goto(databaseId)` (runs before each test)\n4. **Individual tests** - Receive page fixtures they need in the signature\n5. **Page objects are stateless** - Don't store database objects in pages, pass IDs to methods\n\n### Avoid These Anti-Patterns\n\n```typescript\n// ❌ BAD: Storing database in page object\nconst browserPage = createBrowserPage(database);  // OLD pattern - don't use\n\n// ✅ GOOD: Pass databaseId to goto()\nawait browserPage.goto(database.id);\n\n// ❌ BAD: Using page fixture without declaring it in test signature\ntest('should work', async () => {\n  await browserPage.doSomething();  // browserPage is undefined!\n});\n\n// ✅ GOOD: Declare fixtures in test signature\ntest('should work', async ({ browserPage }) => {\n  await browserPage.doSomething();\n});\n\n// ❌ BAD: Navigation inside each test\ntest('should work', async ({ browserPage }) => {\n  await browserPage.goto(database.id);  // Should be in beforeEach\n  // ...\n});\n\n// ❌ BAD: Using test.describe.serial when not needed\ntest.describe.serial('Feature', () => { // Use regular describe unless tests truly depend on each other\n  // ...\n});\n```\n\n## Test Data\n\n### Use Fishery Factories with Faker\n\nUse the [fishery](https://github.com/thoughtbot/fishery) library for test data factories:\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nexport const TEST_PREFIX = 'test-';\n\nexport const ConfigFactory = Factory.define<Config>(() => ({\n  name: `${TEST_PREFIX}${faker.string.alphanumeric(8)}`,\n  host: '127.0.0.1',\n  port: 6379,\n}));\n\n// Usage in tests\nconst config = ConfigFactory.build();\nconst config = ConfigFactory.build({ name: 'custom-name' });\n```\n\n### Cleanup Pattern\n\nPrefix test data with `test-` so leftovers are identifiable, but **never clean up by\nthat prefix**. Specs share a Redis instance and different files run on different\nworkers, so deleting every `test-*` key also removes keys the specs alongside it are\nstill asserting on, and their rows disappear mid-test.\n\nTrack what a test creates and delete only that:\n\n```typescript\nimport { createKeyTracker } from 'e2eSrc/helpers';\n\ntest.describe('Browser > Add Key', () => {\n  const keys = createKeyTracker();\n\n  test.afterEach(async ({ apiHelper }) => {\n    await keys.cleanup(apiHelper, database.id);\n  });\n\n  test('should add a Hash key', async ({ browserPage }) => {\n    const keyData = keys.track(HashKeyFactory.build());\n    // keys.add(name) records a name built without a factory, e.g. a rename target.\n  });\n});\n```\n\nMiss a `track()` and the key is never deleted, which is harmless in CI (the test\nenvironment is recreated per run) but accumulates locally until the RTE restarts.\n\nDatabases need no such care: each spec deletes its own in `afterAll`, and\n`browser.setup` clears leftover `test-` databases once before the run.\n\n## Fixtures\n\n### Add New Fixtures to base.ts\n\n```typescript\n// fixtures/base.ts\ntype Fixtures = {\n  myPage: MyPage;\n  apiHelper: ApiHelper;\n};\n\nexport const test = base.extend<Fixtures>({\n  myPage: async ({ page }, use) => {\n    await use(new MyPage(page));\n  },\n  apiHelper: async ({}, use) => {\n    const helper = new ApiHelper();\n    await use(helper);\n    await helper.dispose();\n  },\n});\n```\n\n## UI Exploration with Playwright MCP\n\n**Before writing tests, ALWAYS use Playwright MCP to explore the UI:**\n\n### Why Explore First?\n- Discover actual `data-testid` attributes used in the application\n- Understand element roles and accessible names for `getByRole()`\n- See page structure and component hierarchy\n- Avoid trial-and-error test writing\n\n### Exploration Workflow\n\n1. **Navigate to the page**: `browser_navigate_Playwright` to target URL\n2. **Take snapshot**: `browser_snapshot_Playwright` to see element tree\n3. **Interact with elements**: `browser_click_Playwright` to trigger dialogs, dropdowns, etc.\n4. **Wait for async content**: `browser_wait_for_Playwright` for dynamic content\n5. **Document findings**: Add discovered UI patterns to `TEST_PLAN.md` under the feature section\n\n### What to Look For\n\n- `data-testid` attributes → use with `page.getByTestId()`\n- Element roles (button, combobox, grid, treeitem) → use with `page.getByRole()`\n- Accessible names → use with `{ name: 'text' }` option\n- Form field placeholders → use with `page.getByPlaceholder()`\n- Text content patterns → use with `page.getByText()`\n\n### Use Discovered Patterns in Page Objects\n\nAfter exploring, use discovered patterns directly in Page Object locators:\n\n```typescript\n// Use data-testid when available\nthis.addButton = page.getByTestId('btn-add-key');\n\n// Use role + name for accessible elements\nthis.submitButton = page.getByRole('button', { name: 'Submit' });\n\n// Use placeholder for form fields\nthis.searchInput = page.getByPlaceholder('Search...');\n```\n\n**Note**: Keep TEST_PLAN.md as a simple visual list of test cases. Document UI patterns in Page Object comments if needed.\n\n## Best Practices\n\n### ✅ DO\n\n- **Explore UI with Playwright MCP before writing tests**\n- **Use Page Object navigation methods** (e.g., `browserPage.goto()`, `workbenchPage.goto()`)\n- Use `data-testid` attributes for stable selectors\n- Use `getByRole`, `getByLabel` for accessible elements\n- Wait for elements with `waitFor({ state: 'visible' })`\n- Clean up test data in `afterEach`\n- Use API for setup, UI for assertions\n- Handle both List view and Tree view in key assertions\n\n### ❌ DON'T\n\n- **NEVER use `page.goto()` directly** - tests must work in both browser and Electron\n- Write tests without exploring the actual UI first\n- Use fixed timeouts (`waitForTimeout`)\n- Use CSS selectors for dynamic content\n- Leave test data after tests\n- Import from `redisinsight/ui/` or `redisinsight/api/`\n- Hardcode test data (use faker)\n- Assume element structure without verification\n\n## Navigation (IMPORTANT)\n\n**All navigation must use UI-based methods, NOT URL navigation.**\n\nTests must work in both browser mode (http://localhost:8080) and Electron mode (no baseURL). Direct `page.goto()` calls fail in Electron because there's no baseURL.\n\n### Navigation Architecture\n\n**BasePage** provides only fundamental navigation:\n```typescript\nawait this.gotoHome();              // Click Redis logo → databases list\nawait this.gotoDatabase(dbId);      // Click database → Browser page (default)\n```\n\n**Each page owns its navigation** via its `goto()` method:\n```typescript\nawait settingsPage.goto();           // Settings page\nawait browserPage.goto(dbId);        // Browser page for database\nawait workbenchPage.goto(dbId);      // Workbench page for database\nawait analyticsPage.goto(dbId);      // Analytics page for database\nawait pubSubPage.goto(dbId);         // Pub/Sub page for database\n```\n\n**NavigationTabs component** handles tab switching within a connected database:\n```typescript\nawait browserPage.navigationTabs.gotoBrowser();\nawait browserPage.navigationTabs.gotoWorkbench();\nawait browserPage.navigationTabs.gotoAnalyze();\nawait browserPage.navigationTabs.gotoPubSub();\n```\n\n### ✅ Correct Navigation Pattern\n\n```typescript\n// Use Page Object's goto() method in beforeEach\ntest.beforeEach(async ({ browserPage }) => {\n  await browserPage.goto(database.id);  // Navigates and waits for page load\n});\n\n// Switch tabs when already connected\nawait browserPage.navigationTabs.gotoWorkbench();\n```\n\n### ❌ Incorrect Navigation Pattern\n\n```typescript\n// NEVER do this - fails in Electron\nawait page.goto(`/${database.id}/browser`);\nawait page.goto('/settings');\nawait page.goto('/');\n```\n\n## Running Tests\n\nRun these commands from the E2E package directory:\n\n```bash\ncd tests/e2e-playwright\n\nnpx playwright test                                              # All Playwright projects\nnpx playwright test --project=chromium-parallel                  # Chromium parallel tests\nnpx playwright test --project=chromium-serial --no-deps          # Chromium serial only (skips parallel + setup)\nnpx playwright test --project=electron-parallel                  # Electron parallel tests\nnpx playwright test --project=electron-serial --no-deps          # Electron serial only (skips parallel + setup)\nENV=ci npx playwright test                             # CI environment\nENV=staging npx playwright test                        # Staging environment\n```\n\n## Code Quality (IMPORTANT)\n\n**Always run linter and type checker after making changes:**\n\n```bash\nnpm run lint                # ESLint check\nnpm run type-check          # TypeScript type check\n```\n\nBoth must pass before committing. Common issues:\n- Unused variables/imports\n- Missing return types\n- `any` types (avoid when possible)\n- Null/undefined handling (use proper types like `Promise<string | null>`)\n\n## Test Isolation (IMPORTANT)\n\nTests should be isolated and not depend on execution order:\n\n### 1. Shared Database with beforeAll/afterAll\n\n```typescript\ntest.describe('Feature Name', () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase({ name: 'test-feature-db', ... });\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  // Tests can run in parallel - they share the database but don't modify shared state\n});\n```\n\n### 2. Use Serial Only When Tests Truly Depend on Each Other\n\n```typescript\n// Only use .serial when tests modify state that subsequent tests depend on\ntest.describe.serial('Workflow that modifies state', () => {\n  test('step 1: create item', ...);\n  test('step 2: modify item created in step 1', ...);\n  test('step 3: delete item', ...);\n});\n```\n\n### 3. Unique Test Data Per Test (when needed)\n\n```typescript\ntest('should create unique item', async ({ apiHelper }) => {\n  const uniqueName = `test-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;\n  // Use uniqueName for this test's data\n});\n```\n\n## Feature-to-Path Mapping\n\nFollow this naming convention for test and page object paths:\n\n| Feature | Test Path | Page Object Path |\n|---------|-----------|------------------|\n| Database List | `tests/parallel/databases/list/` | `pages/databases/` |\n| Add Database | `tests/parallel/databases/add/` | `pages/databases/` |\n| Import Database | `tests/parallel/databases/import/` | `pages/databases/` |\n| Browser - Key List | `tests/parallel/browser/key-list/` | `pages/browser/` |\n| Browser - Add Key | `tests/parallel/browser/add-key/` | `pages/browser/` |\n| Browser - Key Details | `tests/parallel/browser/key-details/` | `pages/browser/` |\n| Workbench | `tests/parallel/workbench/` | `pages/workbench/` |\n| CLI | `tests/parallel/cli/` | `pages/cli/` |\n| Pub/Sub | `tests/parallel/pubsub/` | `pages/pubsub/` |\n| Slow Log | `tests/parallel/analytics/slow-log/` | `pages/analytics/` |\n| DB Analysis | `tests/parallel/analytics/analysis/` | `pages/analytics/` |\n| Settings | `tests/parallel/settings/` | `pages/settings/` |\n| Navigation | `tests/parallel/navigation/` | `pages/navigation/` |\n| Auto-Update | `tests/auto-update/` | `pages/` (shared) |\n| Deep Links | `tests/electron/deep-links/` | `pages/` (shared) |\n\n**Note**: Most tests go in `tests/parallel/`. Only use other project folders for tests with special requirements (serial execution, different setup, etc.).\n","frontmatter":{"name":"e2e-testing","description":"Playwright end-to-end testing standards for RedisInsight: page object models, test structure, fixtures, navigation patterns, and flaky-test prevention. Use when editing files under tests/e2e-playwright/**, writing Playwright tests, adding page objects, or when the user mentions Playwright, E2E tests, page objects, or end-to-end testing."},"isInternal":false,"tokens":4768,"sizeBytes":20562},{"name":"SKILL.md","path":".ai/skills/feature-flags/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/feature-flags/SKILL.md","title":"feature-flags","category":"anthropic-skill","format":"markdown","content":"---\nname: feature-flags\ndescription: >-\n  Create, modify, and remove feature flags in RedisInsight. Use when adding a\n  new feature flag, introducing a dev flag, promoting a dev flag to regular,\n  cleaning up old flags, or the user mentions feature flags, feature toggles,\n  or gating features.\n---\n\n# Feature Flags\n\nRedisInsight has its own feature flag system. Flags are defined in a remote JSON config, fetched by the backend, and served to the frontend via API. This skill covers how to add, promote, and remove flags.\n\n## Flag Types\n\n| Type                         | Naming                            | `flag` value | Strategy                 | Purpose                                                      |\n| ---------------------------- | --------------------------------- | ------------ | ------------------------ | ------------------------------------------------------------ |\n| **Dev flag**                 | `dev-<name>` (e.g. `dev-browser`) | `false`      | `CommonFlagStrategy`     | Hide incomplete features during development                  |\n| **Regular flag**             | `camelCase` (e.g. `azureEntraId`) | `true`       | `CommonFlagStrategy`     | Standard on/off toggle                                       |\n| **Regular with data**        | `camelCase`                       | `true`       | `WithDataFlagStrategy`   | Flag + extra config payload in `data`                        |\n| **Switchable (overridable)** | `camelCase`                       | `true`       | `SwitchableFlagStrategy` | User can override locally via `~/.redis-insight/config.json` |\n\n## Files to Change\n\nEvery new flag touches these files (in order):\n\n### Backend (required)\n\n1. **`redisinsight/api/config/features-config.json`**\n   Add the flag entry with `flag`, `perc`, optional `filters` and `data`. Bump the `version` number.\n\n2. **`redisinsight/api/src/modules/feature/constants/index.ts`**\n   Add to the `KnownFeatures` enum.\n\n3. **`redisinsight/api/src/modules/feature/constants/known-features.ts`**\n   Add entry to the `knownFeatures` record with `name` and `storage` (usually `FeatureStorage.Database`).\n\n4. **`redisinsight/api/src/modules/feature/providers/feature-flag/feature-flag.provider.ts`**\n   Register the flag with its strategy (see Strategy Types below).\n\n### Frontend (required if the flag gates UI)\n\n5. **`redisinsight/ui/src/constants/featureFlags.ts`**\n   Add to the `FeatureFlags` enum.\n\n6. **`redisinsight/ui/src/slices/app/features.ts`**\n   Add default state entry in `initialState.featureFlags.features` with `{ flag: false }`.\n\n### Consuming code\n\n7. Use the flag in components/hooks to gate functionality.\n\n## Strategy Selection\n\nChoose the strategy based on what the flag needs:\n\n```\nCommonFlagStrategy        → Most flags (dev and regular on/off)\nWithDataFlagStrategy      → Flag needs to carry extra data payload\nSwitchableFlagStrategy    → Flag should be overridable via local config.json\n```\n\nRegister in `feature-flag.provider.ts`:\n\n```typescript\nthis.strategies.set(\n  KnownFeatures.YourFeature,\n  new CommonFlagStrategy(this.featuresConfigService, this.settingsService),\n);\n```\n\n## Config JSON Structure\n\n### Minimal (dev flag)\n\n```json\n\"dev-myFeature\": {\n  \"flag\": false,\n  \"perc\": [[0, 100]]\n}\n```\n\n### With filters (Electron-only)\n\n```json\n\"myFeature\": {\n  \"flag\": true,\n  \"perc\": [[0, 100]],\n  \"filters\": [\n    { \"name\": \"config.server.buildType\", \"value\": \"ELECTRON\", \"cond\": \"eq\" }\n  ]\n}\n```\n\n### Gradual rollout (10% of users)\n\n```json\n\"myFeature\": {\n  \"flag\": true,\n  \"perc\": [[0, 10]]\n}\n```\n\n### With data payload\n\n```json\n\"myFeature\": {\n  \"flag\": true,\n  \"perc\": [[0, 100]],\n  \"data\": { \"strategy\": \"ioredis\" }\n}\n```\n\n## Filter Conditions\n\nFilters compare a value from server state against the filter value.\n\n| Condition    | Meaning                         |\n| ------------ | ------------------------------- |\n| `eq`         | equals                          |\n| `neq`        | not equals                      |\n| `gt` / `gte` | greater than / greater or equal |\n| `lt` / `lte` | less than / less or equal       |\n\nCommon `name` paths: `config.server.buildType` (ELECTRON, DOCKER_ON_PREMISE, REDIS_STACK), `config.server.packageVersion` (uses semver), `agreements.analytics`, `env.<VAR_NAME>`.\n\nFilters support `and`/`or` composition for complex conditions.\n\n## Workflows\n\n### Add a dev feature flag\n\nUse for features under active development that should not be visible in production.\n\n1. `features-config.json` → add `\"dev-myFeature\": { \"flag\": false, \"perc\": [[0, 100]] }`\n2. `constants/index.ts` → add `DevMyFeature = 'dev-myFeature'` to `KnownFeatures`\n3. `constants/known-features.ts` → add record entry\n4. `feature-flag.provider.ts` → register with `CommonFlagStrategy`\n5. `ui/src/constants/featureFlags.ts` → add `devMyFeature = 'dev-myFeature'`\n6. `ui/src/slices/app/features.ts` → add default `{ flag: false }`\n\n### Promote dev flag to regular flag\n\nWhen the feature is complete and ready for rollout.\n\n1. Rename `dev-myFeature` → `myFeature` in all the files above\n2. Set `flag: true` in `features-config.json`\n3. Optionally set `perc` for gradual rollout (e.g. `[[0, 10]]`)\n4. Change strategy if needed (e.g. to `SwitchableFlagStrategy` for overridable)\n5. Bump config `version`\n\n### Clean up a flag\n\nWhen a feature is fully rolled out and the flag is no longer needed.\n\n1. Remove from `features-config.json`\n2. Remove from `KnownFeatures` enum\n3. Remove from `knownFeatures` record\n4. Remove strategy registration from `feature-flag.provider.ts`\n5. Remove from FE `FeatureFlags` enum\n6. Remove default state from `features.ts`\n7. Remove all gating code (conditionals, `FeatureFlagComponent` wrappers) in consuming components\n\n## FE Usage Patterns\n\n### Check flag in a component\n\n```typescript\nimport { FeatureFlags } from 'uiSrc/constants';\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features';\n\nconst features = useSelector(appFeatureFlagsFeaturesSelector);\nconst isEnabled = features[FeatureFlags.myFeature]?.flag;\n```\n\n### Custom selector for complex logic\n\n```typescript\nexport const isMyFeatureEnabledSelector = (state: RootState): boolean => {\n  const features = state.app.features.featureFlags.features;\n  return features[FeatureFlags.myFeature]?.flag ?? false;\n};\n```\n","frontmatter":{"name":"feature-flags","description":"Create, modify, and remove feature flags in RedisInsight. Use when adding a new feature flag, introducing a dev flag, promoting a dev flag to regular, cleaning up old flags, or the user mentions feature flags, feature toggles, or gating features."},"isInternal":false,"tokens":1501,"sizeBytes":6281},{"name":"SKILL.md","path":".ai/skills/frontend/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/frontend/SKILL.md","title":"frontend","category":"anthropic-skill","format":"markdown","content":"---\nname: frontend\ndescription: >-\n  React/Redux frontend development patterns for RedisInsight UI:\n  component folder structure, styled-components, hooks, named exports,\n  barrel files, layout components, and theme usage. Use when editing\n  any file under redisinsight/ui/**, writing or modifying React\n  components, Redux slices, styled-components, custom hooks, or when\n  the user mentions UI, frontend, React, Redux, or styled-components.\n---\n\n# Frontend Development (React/Redux)\n\n## Component Structure\n\n### Functional Components\n\n- Use **functional components with hooks** (no class components)\n- **Prefer named exports** over default exports\n- Keep components focused and single-responsibility\n- Extract complex logic into custom hooks\n\n### Component Folder Structure\n\nEach component in its own directory under `**/ComponentName`:\n\n```\nComponentName/\n  ComponentName.tsx          # Main component\n  ComponentName.styles.ts    # Styled-components styles (PascalCase)\n  ComponentName.types.ts     # TypeScript interfaces\n  ComponentName.spec.tsx     # Tests\n  ComponentName.constants.ts # Constants\n  ComponentName.story.tsx    # Storybook examples\n  hooks/                     # Custom hooks\n  components/                # Sub-components\n  utils/                     # Utility functions\n```\n\n### Props Interface\n\n- Name as `ComponentNameProps`\n- Use separate interfaces for complex prop objects\n- Always use proper TypeScript types, never `any`\n\n### Imports Order in Components\n\n1. External dependencies (`react`, `redux`, etc.)\n2. Internal modules (aliases)\n3. Local imports (types, constants, hooks)\n4. Styles (always last: `import { Container } from './Component.styles'`)\n\n### Barrel Files\n\nUse barrel files (`index.ts`) only when exporting **3 or more** items. Make sure exports appear in only one barrel file, not propagated up the chain.\n\n## Styled Components\n\n**We are migrating to styled-components** (deprecating SCSS modules).\n\n### Encapsulate Styles in .styles.ts\n\nKeep all component styles in dedicated `.styles.ts` files using styled-components. Use PascalCase for the filename to match the component name:\n\n```\nComponentName/\n  ComponentName.tsx\n  ComponentName.styles.ts  # ✅ PascalCase\n  # Not component-name.styles.ts ❌\n```\n\n### Import Pattern\n\nKeep all component styles in dedicated .style.ts files and import them with a namespace.\n\n**CRITICAL: `import * as S` is reserved for local styles only** (e.g., from `ComponentName.styles.ts`). When you need to use styled components from external components, create a local styles file that re-exports them.\n\n#### ✅ Good\n\n```typescript\n// ComponentName.tsx\nimport * as S from './ComponentName.styles'\n\nreturn (\n  <S.Container>\n    <S.Title>Title</S.Title>\n    <S.Content>Content</S.Content>\n  </S.Container>\n)\n\n// ComponentName.styles.ts (when re-exporting from external component)\nexport { ExternalStyledComponent } from '../ExternalComponent/ExternalComponent.styles'\n```\n\n#### ❌ Bad\n\n```typescript\n// ❌ BAD: Importing styled components directly from external component\nimport * as S from '../ExternalComponent/ExternalComponent.styles';\n\n// ❌ BAD: Named imports instead of namespace\nimport { Container, Title, Content } from './Component.styles';\n```\n\n### Use Layout Components Instead of div\n\n**Prefer `FlexGroup` over `div`** when creating flex containers:\n\n```typescript\n// ✅ GOOD: Use FlexGroup\nimport { FlexGroup } from 'uiSrc/components/base/layout/flex'\n\nexport const Wrapper = styled(FlexGroup)`\n  user-select: none;\n`\n\n// Usage: Pass layout props as component props\n<Wrapper align=\"center\" justify=\"end\">\n  {children}\n</Wrapper>\n\n// ❌ BAD: Using div with hardcoded flex properties\nexport const Wrapper = styled.div`\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n`\n```\n\n### Pass Layout Props as Component Props\n\n**Don't hardcode layout properties** in styled components when using layout components like `FlexGroup`. Pass them as props instead:\n\n```typescript\n// ✅ GOOD: Pass props in JSX\nexport const Wrapper = styled(FlexGroup)`\n  user-select: none;\n`\n\n<Wrapper align=\"center\" justify=\"end\">\n  {children}\n</Wrapper>\n\n// ❌ BAD: Hardcoding in styled component\nexport const Wrapper = styled(FlexGroup)`\n  align-items: center;\n  justify-content: flex-end;\n  user-select: none;\n`\n```\n\n### Use Gap Prop Instead of Custom Margins\n\n**Prefer `gap` prop on layout components** instead of custom margins for spacing between elements:\n\n```typescript\n// ✅ GOOD: Use gap prop\n<Row align=\"center\" justify=\"between\" gap=\"l\">\n  <FlexItem>Item 1</FlexItem>\n  <FlexItem>Item 2</FlexItem>\n</Row>\n```\n\n### Use Theme Spacing Instead of Magic Numbers\n\n**Always use theme spacing values** instead of hardcoded pixel values:\n\n```typescript\n// ✅ GOOD: Use theme spacing\nexport const Container = styled(Row)`\n  height: ${({ theme }) => theme.core.space.space500};\n  padding: 0 ${({ theme }) => theme.core.space.space200};\n  margin-bottom: ${({ theme }) => theme.core.space.space200};\n`;\n\n// ❌ BAD: Using magic numbers\nexport const Container = styled(Row)`\n  height: 64px;\n  padding: 0 16px;\n  margin-bottom: 16px;\n`;\n```\n\n### Use Semantic Colors from Theme\n\n**Always use semantic colors** from the theme instead of CSS variables or hardcoded colors:\n\n```typescript\n// ✅ GOOD: Use semantic colors\nexport const Header = styled(Row)`\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral100};\n  border-bottom: 1px solid\n    ${({ theme }) => theme.semantic.color.border.neutral500};\n`;\n\n// ❌ BAD: Using deprecated EUI CSS variables\nexport const Header = styled(Row)`\n  background-color: var(--euiColorEmptyShade);\n  border-bottom: 1px solid var(--separatorColor);\n`;\n```\n\n### Use Layout Components (Row/Col/FlexGroup) Instead of div\n\n**Prefer layout components** from the layout system instead of regular `div` elements:\n\n```typescript\n// ✅ GOOD: Use Row component\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const PageHeader = styled(Row)`\n  height: ${({ theme }) => theme.core.space.space500};\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral100};\n`\n\n<PageHeader align=\"center\" justify=\"between\" gap=\"l\">\n  {children}\n</PageHeader>\n\n// ❌ BAD: Using div with flex properties\nexport const PageHeader = styled.div`\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  height: 64px;\n`\n```\n\n### Conditional Styling\n\nUse `$` prefix for transient props that shouldn't pass to DOM:\n\n```typescript\nexport const Button = styled.button<{ $isActive?: boolean }>`\n  background-color: ${({ $isActive }) => ($isActive ? '#007bff' : '#6c757d')};\n`;\n```\n\n### Avoid !important\n\n**Never use `!important` in styled-components**. Styled-components handles CSS specificity through component hierarchy. If you need to override styles, use more specific selectors or adjust the component structure:\n\n```typescript\n// ✅ GOOD: Rely on CSS specificity\nexport const IconButton = styled(IconButton)<{ isOpen: boolean }>`\n  ${({ isOpen }) =>\n    isOpen &&\n    css`\n      background-color: ${({ theme }) =>\n        theme.semantic.color.background.primary200};\n    `}\n`;\n\n// ❌ BAD: Using !important\nexport const IconButton = styled(IconButton)`\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.primary200} !important;\n`;\n```\n\n### Verify Type System Compatibility\n\nWhen using layout components or other typed components, verify your prop values match the type system:\n\n```typescript\n// Check the component's type definitions\n// FlexGroup accepts: align?: 'center' | 'stretch' | 'baseline' | 'start' | 'end'\n// Use valid values from the type system\n```\n\n## State Management (Redux)\n\n### When to Use What\n\n- **Global State (Redux)**:\n\n  - Data shared across multiple components\n  - Data persisting across routes\n  - Server state (API data)\n  - User preferences/settings\n\n- **Local State (useState)**:\n\n  - UI state (modals, dropdowns, tabs)\n  - Form inputs before submission\n  - Component-specific temporary data\n\n- **Derived State (Selectors)**:\n  - Computed values from Redux state\n  - Filtered/sorted lists\n  - Aggregated data\n\n### Redux Toolkit Patterns\n\n#### Slice Structure\n\n- Use `createSlice` from Redux Toolkit\n- Define proper TypeScript types for state\n- Use `PayloadAction<T>` for action typing\n- Handle async with `extraReducers` and thunks\n\n#### Thunks\n\n- Use `createAsyncThunk` for async operations\n- Handle pending, fulfilled, and rejected states\n- Use `rejectWithValue` for error handling\n\n#### Selectors\n\n- Create basic selectors for direct state access\n- Use `createSelector` from `reselect` for memoized/computed values\n- Keep selectors in separate `selectors.ts` file\n\n## React Best Practices\n\n### Performance\n\n- Use `useCallback` for functions passed as props\n- Use `useMemo` for expensive computations\n- Use `React.memo` for expensive components\n- Avoid inline arrow functions in JSX props\n\n### Effect Cleanup\n\nAlways clean up subscriptions, timers, and event listeners in `useEffect` return function.\n\n### Keys in Lists\n\n- Use unique, stable IDs (not array indices)\n- Only use indices if list never reorders and items have no IDs\n\n### Conditional Rendering\n\n- Use early returns for loading/error states\n- Avoid deeply nested ternaries - extract to functions\n\n## Custom Hooks\n\n### Extract Reusable Logic\n\nCreate custom hooks for reusable stateful logic. Store component-specific hooks in the component's `/hooks` directory.\n\n## Form Handling\n\nUse Formik with Yup for validation. Keep form logic in custom hooks when complex.\n\n## UI Components\n\n**⚠️ IMPORTANT**:\n\n- We are **deprecating Elastic UI** components\n- Migrating to **Redis UI** (`@redis-ui/*`)\n- **Use internal wrappers** from `uiSrc/components/ui`\n- **DO NOT import directly** from `@redis-ui/*`\n\n### Component Usage\n\n```typescript\n// ✅ GOOD: Import from internal wrappers\nimport { Button, Input, FlexGroup } from 'uiSrc/components/ui';\n\n// ❌ BAD: Don't import directly from @redis-ui\nimport { Button } from '@redis-ui/components';\n\n// ❌ DEPRECATED: Don't use Elastic UI for new code\nimport { EuiButton } from '@elastic/eui';\n```\n\n### Migration Guidelines\n\n- ✅ Use internal wrappers from `uiSrc/components/ui` for all new features\n- ✅ Create internal wrappers for Redis UI components as needed\n- ✅ Replace Elastic UI when touching existing code\n- ❌ Do not import directly from `@redis-ui/*`\n- ❌ Do not add new Elastic UI imports\n\n## Icons\n\n**⚠️ IMPORTANT**: Always use icons from Redis UI library instead of custom SVGs.\n\n### Icon Usage\n\n- **Use Redis UI icons** from `@redis-ui/icons` via `iconRegistry.tsx`\n- Icons are automatically exported via `export * from '@redis-ui/icons'` in `iconRegistry.tsx`\n- Use `RiIcon` component with icon type: `<RiIcon type=\"FolderOpenIcon\" />`\n- **DO NOT create custom SVG icons** - check if the icon exists in Redis UI library first\n\n### Custom Icons (Exception)\n\nOnly create custom SVG icons if:\n\n- The icon doesn't exist in Redis UI library\n- It's a project-specific icon that won't be added to the library\n\n## Testing Components\n\n### Always Use Shared `renderComponent` Helper\n\n**CRITICAL**: Create a `renderComponent` helper function for each component test file:\n\nPull entity data from a shared Fishery factory in `redisinsight/ui/src/mocks/factories/`\n(reuse one, or add a new `<domain>/<TypeName>.factory.ts`); inline only callbacks/primitives.\n\n```typescript\ndescribe('MyComponent', () => {\n  const mockUser = UserFactory.build();\n  const defaultProps: MyComponentProps = {\n    user: mockUser,\n    onComplete: jest.fn(),\n  }\n\n  const renderComponent = (propsOverride?: Partial<MyComponentProps>) => {\n    const props = { ...defaultProps, ...propsOverride }\n\n    return render(\n      <Provider store={store}>\n        <MyComponent {...props} />\n      </Provider>\n    )\n  }\n\n  it('should render', () => {\n    renderComponent()\n    // assertions\n  })\n})\n```\n\nBenefits:\n\n- Centralized setup and providers\n- Default props in one place\n- Easy prop overrides per test\n- No duplicate render logic\n\n### Testing Redux\n\nCreate a test store with `configureStore` for components connected to Redux.\n\n## Key Principles\n\n1. **Separation of Concerns**: Keep styles, types, constants, logic separate\n2. **Colocate Related Code**: Keep sub-components and hooks close to usage\n3. **Consistent Naming**: Follow conventions across all components\n4. **Type Safety**: Always define proper types, never `any`\n5. **Testability**: Structure for easy testing with `renderComponent` helper\n6. **Styled Components**: Prefer styled-components over SCSS modules\n7. **Layout Components**: Use FlexGroup instead of div for flex containers, pass layout props as component props\n8. **Type Safety**: Verify prop values match component type definitions (e.g., FlexGroup's align/justify values)\n","frontmatter":{"name":"frontend","description":"React/Redux frontend development patterns for RedisInsight UI: component folder structure, styled-components, hooks, named exports, barrel files, layout components, and theme usage. Use when editing any file under redisinsight/ui/**, writing or modifying React components, Redux slices, styled-components, custom hooks, or when the user mentions UI, frontend, React, Redux, or styled-components."},"isInternal":false,"tokens":2949,"sizeBytes":12814},{"name":"SKILL.md","path":".ai/skills/git-safety/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/git-safety/SKILL.md","title":"git-safety","category":"anthropic-skill","format":"markdown","content":"---\nname: git-safety\ndescription: >-\n  Critical safety guardrails for protected branches: never commit,\n  push, or force-push directly to main, latest, or release branches;\n  no destructive history rewrites without explicit user approval.\n  Use before any git operation that touches protected branches,\n  before force-push, reset --hard, history rewrite, or branch\n  deletion, or whenever the user asks about merging, pushing, or\n  release branches.\n---\n\n\n# Git Safety Rules for AI Agents\n\n## 🚫 CRITICAL: Protected Branch Rules\n\n**AI agents must NEVER commit to or push to protected branches under any circumstances.**\n\n### Protected Branches\n\n- `main` - Primary production branch\n- `latest` - Latest stable release\n- `release/*` - Release branches (e.g., `release/v2.0.0`)\n\nThis is a non-negotiable rule that applies to all scenarios:\n\n### Prohibited Actions\n\n- ❌ **Direct commits** - Never run `git commit` while on a protected branch\n- ❌ **Direct pushes** - Never run `git push origin <protected-branch>` or `git push` while on a protected branch\n- ❌ **Force pushes** - Never run `git push --force` or `git push -f` targeting protected branches\n- ❌ **Merging into protected branches locally** - Never run `git merge <branch>` while on a protected branch\n- ❌ **Rebasing protected branches** - Never run `git rebase` while on a protected branch\n- ❌ **Resetting protected branches** - Never run `git reset` while on a protected branch\n\n### Required Workflow\n\n1. **Always create a feature branch** before making any changes\n2. **Verify current branch** before any git operation using `git branch --show-current`\n3. **Create Pull Requests** for all changes - let the review process handle merging\n\n### Pre-Push Checklist\n\nBefore executing any push command, AI agents must:\n\n1. ✅ Confirm current branch is NOT a protected branch (`main`, `latest`, `release/*`)\n2. ✅ Verify the remote and branch target\n\n### Error Recovery\n\nIf accidentally on a protected branch with uncommitted changes:\n\n1. Stash changes: `git stash`\n2. Create new branch: `git checkout -b <appropriate-branch-name>`\n3. Apply changes: `git stash pop`\n4. Continue work on the new branch\n\n## Rationale\n\n- Protected branches represent production-ready or release code\n- All changes must go through code review via Pull Requests\n- Direct pushes bypass CI/CD checks and team review\n- Mistakes on protected branches can affect the entire team and deployment pipeline\n","frontmatter":{"name":"git-safety","description":"Critical safety guardrails for protected branches: never commit, push, or force-push directly to main, latest, or release branches; no destructive history rewrites without explicit user approval. Use before any git operation that touches protected branches, before force-push, reset --hard, history rewrite, or branch deletion, or whenever the user asks about merging, pushing, or release branches."},"isInternal":false,"tokens":564,"sizeBytes":2444},{"name":"SKILL.md","path":".ai/skills/pull-requests/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/pull-requests/SKILL.md","title":"pull-requests","category":"anthropic-skill","format":"markdown","content":"---\nname: pull-requests\ndescription: >-\n  Create and review pull requests following project standards including title\n  format, description template, and review checklist. Use when creating PRs,\n  writing PR descriptions, or reviewing pull requests.\n---\n\n# Pull Request Guidelines\n\n## Creating a PR\n\n### PR Title\n\nInclude issue number at the start:\n\n```\nRI-123 Add user profile editing\n#456 Fix memory leak in connection pool\n```\n\n### PR Description Template\n\n```markdown\n# What\n\nDescribe what was changed.\n\n# Testing\n\nDescribe how to test the changes.\n\n---\n\nRefs #RI-123\n```\n\n**PR Description Guidelines:**\n\n- **Keep it concise** - Avoid verbose descriptions\n- **Focus on high-level changes** - Don't list every code change in the #What section\n- **Brief and to the point** - The diff shows the details; describe the \"why\" and \"what\" at a high level\n- **Technical decisions** - Only mention significant architectural or design decisions if relevant\n- **Link, don't auto-close** - Use `Refs #RI-123` / `Addresses #RI-123`, not `Closes`/`Fixes`/`Resolves` - those keywords auto-close the issue when the PR merges, and tickets should be closed manually, not by the merge\n\n## Review Process\n\n### As PR Author\n\n- **Respond to all comments** - Address every piece of feedback\n- **Don't take feedback personally** - Reviews improve code quality\n- **Update code based on feedback** - Make requested changes\n- **Mark conversations as resolved** - After addressing feedback\n- **Keep PR up to date** - Rebase on main regularly\n\n### As PR Reviewer\n\n- **Be constructive and respectful** - Focus on improvement\n- **Focus on logic, not style** - Linter handles formatting\n- **Check for**:\n  - Logic errors and edge cases\n  - Performance issues\n  - Security concerns\n  - Test coverage\n  - Missing documentation\n  - Architectural concerns\n\n### Review Checklist\n\n- [ ] Code follows project patterns\n- [ ] Tests are comprehensive\n- [ ] No console.log or debug code\n- [ ] TypeScript types are proper\n- [ ] Error handling is adequate\n- [ ] Documentation is updated\n- [ ] No security vulnerabilities\n- [ ] Performance is acceptable\n","frontmatter":{"name":"pull-requests","description":"Create and review pull requests following project standards including title format, description template, and review checklist. Use when creating PRs, writing PR descriptions, or reviewing pull requests."},"isInternal":false,"tokens":491,"sizeBytes":2111},{"name":"error-handling.md","path":".ai/skills/redis-insight-plugin/references/error-handling.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/redis-insight-plugin/references/error-handling.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Error Handling and Stability\n\nPlugins run inside the Insight iframe. A thrown exception during activation produces a blank panel with no visible cause. Defensive rendering is mandatory.\n\n## Defensive Render Pattern\n\n```ts\nconst PREFIX = '[MY_PLUGIN]';\n\nexport function renderView(props: any) {\n  const host = document.getElementById('app');\n  if (!host) {\n    console.error(PREFIX, '#app missing');\n    return;\n  }\n  try {\n    const rows = parseData(props?.data ?? []);\n    if (rows.length === 0) {\n      host.innerHTML = '<div class=\"empty\">No data to display.</div>';\n      return;\n    }\n    renderTable(host, rows, props);\n  } catch (err) {\n    console.error(PREFIX, 'render failed', err);\n    host.innerHTML = `\n      <div class=\"error\">\n        <p>Plugin failed to render. See devtools console.</p>\n        <pre>${escape(String((err as Error)?.message ?? err))}</pre>\n      </div>\n    `;\n  }\n}\n```\n\nRules:\n\n- Always check `#app` exists.\n- Always check the data shape before rendering.\n- Always wrap render logic in `try/catch`.\n- Render an empty state and an error state — never a blank iframe.\n- Make empty/error copy match the active visualization mode. Each mode in a multi-visualization plugin should show its own message, not a single generic one.\n- Remove unreachable fallback branches after parser state is narrowed. Dead defensive checks hide the true state model.\n\n## React ErrorBoundary\n\nFor React-rendered plugins, wrap the root component:\n\n```tsx\nclass PluginErrorBoundary extends React.Component<{ children: React.ReactNode }, { error: Error | null }> {\n  state = { error: null as Error | null };\n  static getDerivedStateFromError(error: Error) { return { error }; }\n  componentDidCatch(error: Error, info: React.ErrorInfo) {\n    console.error('[PLUGIN]', 'render error', error, info);\n  }\n  render() {\n    if (this.state.error) {\n      return <div className=\"error\"><pre>{String(this.state.error.message)}</pre></div>;\n    }\n    return this.props.children;\n  }\n}\n```\n\nWrap once at the plugin root, not around every leaf component.\n\n## Logging Prefixes\n\nPrefix every log line with a stable, unique tag:\n\n- `[MY_PLUGIN]`\n- `[STREAM_PLUGIN]`\n- `[SEARCH_PLUGIN]`\n\nWhy:\n\n- Devtools filtering becomes a one-step regex.\n- Multiple plugins in the same Insight session can be told apart.\n- Customer logs forwarded to support are immediately attributable.\n\nNever use bare `console.log(data)` in shipped plugins.\n\n## State Persistence\n\nPrefer the SDK first, `localStorage` fallback:\n\n```ts\nimport { getState, setState } from 'redisinsight-plugin-sdk';\n\nasync function loadSettings() {\n  try {\n    return (await getState()) ?? {};\n  } catch {\n    try {\n      return JSON.parse(localStorage.getItem('ri:my-plugin:settings') ?? '{}');\n    } catch {\n      return {};\n    }\n  }\n}\n\nasync function saveSettings(s: object) {\n  try { await setState(s); }\n  catch { localStorage.setItem('ri:my-plugin:settings', JSON.stringify(s)); }\n}\n```\n\nValidate persisted state before applying — fields can disappear or change shape between plugin versions.\n\n## Common Issues\n\n| Issue | Likely cause | Fix |\n|-------|--------------|-----|\n| Blank iframe, no logs | Activation function not exported | Ensure `export default { fn }` and name matches manifest. |\n| `process.env is not defined` | Bundler left env refs | Replace at build time; verify `grep -c process.env dist/index.js` is `0`. |\n| `require is not defined` | Externalized React | `targets.module.includeNodeModules: true`; rebuild. |\n| Visualization renders but is empty | Parser returned empty | Log raw `props.data`; check command modifier flags. |\n| Values mapped to wrong field/axis | Response shape assumption wrong | Branch on the actual runtime shape, not the command name. |\n| Theme looks wrong | No `theme_DARK` handling | Read `document.body.classList` or `getTheme()` from SDK. |\n| Plugin disappears after Insight upgrade | Manifest field changed | Re-read official docs and update `package.json`. |\n| Large bundle, slow load | Missing minify, full lodash | Run `npm run minify:js`; switch to scoped imports. |\n","isInternal":false,"tokens":926,"sizeBytes":4077},{"name":"external-parcel-plugin.md","path":".ai/skills/redis-insight-plugin/references/external-parcel-plugin.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/redis-insight-plugin/references/external-parcel-plugin.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# External Parcel Plugin\n\nBuild a standalone Redis Insight plugin with Parcel.\n\n## Folder Layout\n\n```\n<plugin-name>/\n  package.json\n  src/\n    index.html\n    main.tsx\n    components/\n    styles/\n      styles.scss\n  dist/                     # build output, gitignored except when deploying\n    index.js\n    styles.css\n  scripts/\n    verify-plugin.sh\n    deploy-external.sh\n```\n\n## Build Tool: Parcel\n\nWhy Parcel:\n\n- Zero-config TS/SCSS/asset handling.\n- Simple `targets` model maps cleanly to \"single bundled JS + CSS\".\n- `targets.module.includeNodeModules: true` bundles every dep, which is what an external plugin needs.\n\nWhy **not** Vite for external plugins:\n\n- Vite assumes ESM consumers and multi-file output. Insight expects a single `dist/index.js`.\n- Vite's externalization defaults can leave React unbundled — fatal for an iframe with no shared deps.\n\n## Package Scripts\n\nRecommended scripts (see [../templates/external-parcel-package.json](../templates/external-parcel-package.json)):\n\n```json\n\"scripts\": {\n  \"start\":   \"parcel src/index.html\",\n  \"build\":   \"concurrently \\\"npm run build:js\\\" \\\"npm run build:css\\\"\",\n  \"build:js\":\"parcel build src/main.tsx --no-source-maps --dist-dir dist --target module\",\n  \"build:css\":\"parcel build src/styles/styles.scss --no-source-maps --dist-dir dist\",\n  \"minify:js\":\"terser dist/index.js -o dist/index.js -c -m\",\n  \"clean\":   \"rimraf dist\",\n  \"verify\":  \"bash scripts/verify-plugin.sh\",\n  \"deploy:external\":\"bash scripts/deploy-external.sh\",\n  \"deploy:internal\":\"bash scripts/deploy-internal-docker.sh\"\n}\n```\n\n## Targets\n\n```json\n\"targets\": {\n  \"main\": false,\n  \"module\": {\n    \"includeNodeModules\": true,\n    \"outputFormat\": \"esmodule\",\n    \"isLibrary\": false\n  }\n}\n```\n\n- `main: false` disables Parcel's CommonJS output.\n- `includeNodeModules: true` inlines every dependency.\n- Do **not** add `\"engines\": { \"browsers\": \"...\" }` so narrow that React or your visualization library break — Insight bundles a modern Chromium.\n\n## Bundling Rules\n\n- Bundle React, ReactDOM, and every other runtime dependency (including the visualization library).\n- No `peerDependencies`. Insight does not provide shared deps to plugins.\n- No imports from `uiSrc/`, `@redis-ui/*`, or any RedisInsight monorepo package.\n- No `process.env.*` references in the bundle. Replace with constants at build time. Verify with:\n\n  ```bash\n  grep -c \"process.env\" dist/index.js   # must be 0\n  ```\n\n- Minify before deploying customer-facing plugins; skip in dev for readable stack traces.\n\n## Source vs Deployed Manifest\n\nThe source `package.json` includes `scripts`, `devDependencies`, `targets`, etc. The deployed manifest must include only:\n\n- `name`, `version`, `description`\n- `main`, `styles`\n- `visualizations`\n- runtime `dependencies` (optional, informational)\n\nStrip the rest before copying into `~/.redis-insight/plugins/<name>/`. The deploy script in `templates/deploy-external.sh` does this for you (or use `jq` to filter).\n\n## Outputs\n\nAfter `npm run build`:\n\n- `dist/index.js` — single bundled module.\n- `dist/styles.css` — single stylesheet.\n\nIf your plugin has no styles, you can omit the `styles` manifest field and the SCSS source — but most plugins need styles.\n\nFor RedisInsight product UI fidelity, copy `templates/external-styles.scss` to `src/styles/styles.scss`. Keep plugin styles scoped under `.ri-plugin-*` classes and use `theme_LIGHT` / `theme_DARK` body classes for light/dark mode.\n\n## Verification\n\n```bash\nnpm run build\nnpm run verify     # runs templates/verify-plugin.sh\n```\n\n`verify` should report:\n\n- `package.json` present and parseable.\n- `dist/index.js` present and non-empty.\n- `dist/styles.css` present (if `styles` is declared).\n- Zero `process.env` references in the bundle.\n- Each declared `activationMethod` name appears in the bundle.\n- Bundle size under your project's threshold.\n","isInternal":false,"tokens":951,"sizeBytes":3859},{"name":"internal-vite-plugin.md","path":".ai/skills/redis-insight-plugin/references/internal-vite-plugin.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/redis-insight-plugin/references/internal-vite-plugin.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Internal Vite Plugin\n\nBuild a Redis Insight plugin **inside** the RedisInsight monorepo. **This is the default for\nany contribution to this repo.** If you are not working inside the RedisInsight repo, use\n[external-parcel-plugin.md](external-parcel-plugin.md) instead.\n\n> **Start by copying a sibling.** Do not scaffold from scratch — copy an existing package such\n> as `geodata` or `redisearch` from `redisinsight/ui/src/packages/` and adapt it. Match the\n> surrounding conventions exactly rather than diverging.\n\n## Follow the repo styleguides\n\nInternal plugin code lives in the `redisinsight/ui/` tree, so it must follow the same\nstyleguides as the rest of the UI (these are mandatory, not optional):\n\n- **[frontend](../../frontend/SKILL.md)** — component folder structure\n  (`ComponentName/ComponentName.tsx` + `.styles.ts` + `.types.ts` + `.spec.tsx`), functional\n  components with hooks, **named exports**, barrel files (`src/components/index.ts`), layout\n  components (`Row` / `Col` / `FlexGroup`) instead of raw `div`, and theme usage.\n- **[redis-ui-components](../../redis-ui-components/)** — build all plugin UI from Redis UI\n  components via the `uiSrc/components/ui` wrappers; never import raw `@redis-ui/*`.\n- **[code-quality](../../code-quality/SKILL.md)** — no `any`, naming conventions, import order,\n  no magic numbers, no `!important`, semantic theme colors.\n- **[testing](../../testing/SKILL.md)** — Jest + Testing Library, `renderComponent`, `faker`,\n  `waitFor` over fixed waits.\n\n## Path\n\n```\nredisinsight/ui/src/packages/<plugin-name>/\n  package.json              # manifest (source/main/styles/visualizations) + scripts\n  index.html                # iframe entry with <div id=\"app\">\n  jest.config.cjs\n  tsconfig.json\n  public/                   # icon SVGs (copied to dist by the shared build)\n  src/\n    main.tsx                # exports activation functions (default export)\n    App.tsx / App.spec.tsx\n    components/             # ComponentName/ dirs + index.ts barrel\n    constants/             # index.ts barrel\n    types/                 # index.ts barrel\n    utils/                 # parsers + *.spec.ts\n    styles/styles.scss\n    global.d.ts\n    jest.setup.ts\n```\n\nSee any sibling under `redisinsight/ui/src/packages/` for a complete, current example\n(`redisearch` is a good table plugin; `geodata` is a good multi-visualization one).\n\n## Build Tool: Vite (shared config)\n\nThere is **no per-package `vite.config.ts`**. All internal plugins share a single config at\n`redisinsight/ui/src/packages/vite.config.mjs` and are listed in its `riPlugins` array. To add\na new plugin you register it there:\n\n```js\n// redisinsight/ui/src/packages/vite.config.mjs\nconst riPlugins = [\n  { name: 'redisearch', entry: 'src/main.tsx' },\n  // ...\n  { name: '<plugin-name>', entry: 'src/main.tsx' },  // add your plugin\n];\n```\n\nEach package's `package.json` wires the standard scripts:\n\n```json\n{\n  \"source\": \"./src/main.tsx\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"scripts\": {\n    \"dev\": \"vite -c ../vite.config.mjs\",\n    \"test\": \"node ../../../../../node_modules/.bin/jest -c jest.config.cjs\",\n    \"typecheck\": \"node ../../../../../node_modules/.bin/tsc --project tsconfig.json --noEmit\"\n  }\n}\n```\n\nThe shared build produces `dist/index.js` and `dist/styles.css` (and copies `public/` icons to\n`dist/`), all referenced from the manifest.\n\n## Manifest fields used in-repo\n\nIn addition to the base fields in [plugin-manifest.md](plugin-manifest.md), in-repo\nvisualizations commonly use:\n\n- `matchQuery.anyRegex` / `matchQuery.noneRegex` — refine matching beyond `matchCommands` (e.g.\n  only activate when a specific modifier flag is present in the command text).\n- `iconDark` / `iconLight` — `./dist/<icon>.svg` paths (source SVGs live in `public/`).\n\n## index.html\n\n```html\n<!doctype html>\n<html>\n  <head><meta charset=\"utf-8\" /></head>\n  <body class=\"theme_LIGHT\">\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./src/main.tsx\"></script>\n  </body>\n</html>\n```\n\n## Theme and Shared UI Caveats\n\n- Use Redis UI components and the theme tokens via the repo's `uiSrc/components/ui` wrappers per\n  the [redis-ui-components](../../redis-ui-components/) and [frontend](../../frontend/SKILL.md)\n  skills.\n- Detect iframe theme through the `theme_LIGHT` / `theme_DARK` body classes (or the\n  `redisinsight-plugin-sdk` theme helper).\n- Do not import from deep relative paths (`../../../../../`); use the monorepo's package alias.\n\n## Internal Plugin DO NOT\n\n- DO NOT introduce a Parcel build or a per-package `vite.config.ts` inside the monorepo. Register\n  in the shared `vite.config.mjs`.\n- DO NOT diverge from the runtime-dependency handling of sibling packages — copy how `geodata`\n  declares `react`/`react-dom` and externalization, don't invent your own.\n- DO NOT import from a sibling internal plugin — use shared utilities (`packages/common`) only.\n- DO NOT mutate global window state outside of the documented `window.state` surface.\n\n## When to Convert Internal → External\n\nIf the plugin is meant to ship outside RedisInsight (customer demo, field use, GitHub release),\nport it to an external Parcel layout. Keep the React component code; replace the build tool,\nbundle every dependency, and follow [external-parcel-plugin.md](external-parcel-plugin.md).\n","isInternal":false,"tokens":1339,"sizeBytes":5326},{"name":"iterative-development.md","path":".ai/skills/redis-insight-plugin/references/iterative-development.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/redis-insight-plugin/references/iterative-development.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Iterative Plugin Development\n\nBuild every Redis Insight plugin in three phases. The phases exist because each one isolates a different class of failure. Skipping is how you end up with a blank iframe and no idea why.\n\n## Phase 1 — Vanilla JS Wiring\n\nGoal: prove activation, props, and iframe rendering work. No React, no third-party libraries.\n\nUse the RedisInsight product UI baseline from `templates/external-styles.scss`; Phase 1 can be vanilla DOM without falling back to inline brand colors.\n\n```ts\n// src/main.tsx (or main.js)\nconst PREFIX = '[EXAMPLE_PLUGIN]';\n\nexport function renderExampleView(props: any) {\n  console.log(PREFIX, 'activated', props);\n  const root = document.getElementById('app');\n  if (!root) {\n    console.error(PREFIX, '#app missing');\n    return;\n  }\n  root.innerHTML = `\n    <section class=\"ri-plugin-shell\">\n      <div class=\"ri-plugin-panel\">\n        <header class=\"ri-plugin-header\">\n          <h2 class=\"ri-plugin-title\">Plugin works</h2>\n          <span class=\"ri-plugin-badge ri-plugin-badge--success\">Active</span>\n        </header>\n        <div class=\"ri-plugin-content\">\n          <code class=\"ri-plugin-command\">${escape(String(props?.command ?? ''))}</code>\n          <pre class=\"ri-plugin-code\">${escape(JSON.stringify(props?.data, null, 2))}</pre>\n        </div>\n      </div>\n    </section>\n  `;\n}\n\nfunction escape(s: string) {\n  return s.replace(/[&<>]/g, c => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;' }[c] as string));\n}\n\nexport default { renderExampleView };\n```\n\nFailure modes Phase 1 surfaces:\n\n- `activationMethod` name typo in the manifest.\n- Wrong `main` path or missing `dist/index.js`.\n- `process.env` references in the bundle.\n- Plugin folder not at `~/.redis-insight/plugins/<name>/`.\n- Insight not seeing the plugin (`/api/plugins`).\n\nIf Phase 1 doesn't render \"PLUGIN WORKS\" with the command and raw JSON, do not move on.\n\n## Phase 2 — React Rendering\n\nGoal: prove React mounts inside the iframe and displays props.\n\n```tsx\nimport * as React from 'react';\nimport { createRoot, type Root } from 'react-dom/client';\n\nconst PREFIX = '[EXAMPLE_PLUGIN]';\n\n// Workbench can re-activate the same iframe. Reuse one root per host element;\n// calling createRoot() twice on the same node warns and breaks rendering.\nconst roots = new WeakMap<HTMLElement, Root>();\n\nfunction getRoot(host: HTMLElement): Root {\n  let root = roots.get(host);\n  if (!root) {\n    root = createRoot(host);\n    roots.set(host, root);\n  }\n  return root;\n}\n\nfunction ExampleApp({ command, data }: { command: string; data: unknown }) {\n  return (\n    <section className=\"ri-plugin-shell\">\n      <div className=\"ri-plugin-panel\">\n        <header className=\"ri-plugin-header\">\n          <h2 className=\"ri-plugin-title\">Example Plugin</h2>\n          <span className=\"ri-plugin-badge ri-plugin-badge--success\">Active</span>\n        </header>\n        <div className=\"ri-plugin-content\">\n          <code className=\"ri-plugin-command\">{command}</code>\n          <pre className=\"ri-plugin-code\">{JSON.stringify(data, null, 2)}</pre>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport function renderExampleView(props: any) {\n  console.log(PREFIX, 'activated', props);\n  const host = document.getElementById('app');\n  if (!host) return;\n  try {\n    const root = getRoot(host);\n    root.render(<ExampleApp command={String(props?.command ?? '')} data={props?.data} />);\n  } catch (err) {\n    console.error(PREFIX, 'render failed', err);\n    host.innerHTML = '<div class=\"ri-plugin-error\">Plugin failed. See console.</div>';\n  }\n}\n\nexport default { renderExampleView };\n```\n\nIf you are on React 17, use `ReactDOM.render` instead of `createRoot`. Failure modes Phase 2 surfaces:\n\n- Wrong React/ReactDOM version pairing (React 17 + `createRoot` is the classic).\n- Externalized React in the bundle (`require is not defined`).\n- Missing `index.html` `#app` host.\n- Type errors not caught by the build.\n\n## Phase 3 — Full Feature\n\nGoal: real visualization library, real UX, real error/empty states.\n\nIn Phase 3:\n\n- Add the actual visualization library (charting, mapping, grid, etc.). See [third-party-libraries.md](third-party-libraries.md).\n- Parse the Redis response defensively. See [redis-command-parsing.md](redis-command-parsing.md).\n- Render empty and error states explicitly.\n- Persist user settings via SDK or `localStorage`.\n- Handle `theme_DARK` / `theme_LIGHT` switches.\n\nPhase 3 failures usually point to:\n\n- Unexpected response shape (use the parser patterns).\n- Library asset paths broken by bundling (icons, fonts, or images that resolve via bundler-relative URLs).\n- Missing CSS (`styles` field, CSS not bundled).\n- Cluster/multi-shard responses you didn't account for.\n\n## CI Pipeline Sketch\n\n```yaml\n# .github/workflows/plugin.yml (sketch)\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with: { node-version: '20' }\n      - run: npm ci\n      - run: npm run build\n      - run: bash scripts/verify-plugin.sh\n      - uses: actions/upload-artifact@v4\n        with:\n          name: plugin-bundle\n          path: |\n            package.json\n            dist/\n```\n\nOptionally extend with a Playwright job that boots a Redis Insight container, copies the artifact in, and runs a Workbench smoke test written per the repo's [e2e-testing](../../e2e-testing/SKILL.md) skill.\n","isInternal":false,"tokens":1303,"sizeBytes":5383},{"name":"official-docs-summary.md","path":".ai/skills/redis-insight-plugin/references/official-docs-summary.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/redis-insight-plugin/references/official-docs-summary.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Redis Insight Plugin Docs — Summary\n\nCondensed from the official Redis Insight plugin docs:\n\n- https://github.com/redis/RedisInsight/tree/main/docs/plugins\n- https://github.com/redis/RedisInsight/blob/main/docs/plugins/development.md\n- https://github.com/redis/RedisInsight/blob/main/docs/plugins/installation.md\n\nAlways re-read the upstream docs when in doubt — the contract may evolve.\n\n## Where Plugins Run\n\n- Workbench renders a plugin inside an **iframe**.\n- The iframe loads the plugin's `index.html` (or equivalent), which loads the bundle declared in the manifest's `main`.\n- The iframe DOM has a `<div id=\"app\"></div>` host element. Activation functions render into `#app`.\n- Stylesheets declared by `styles` are injected into the iframe.\n\n## Manifest (`package.json`)\n\nA plugin's root `package.json` is the manifest. Required fields:\n\n- `name`, `version`, `description`\n- `main` — relative path to the built JS bundle (e.g. `./dist/index.js`).\n- `styles` — relative path to the built CSS (e.g. `./dist/styles.css`).\n- `visualizations[]` — list of visualization descriptors:\n  - `id` — stable identifier inside the plugin.\n  - `name` — visible label in the Workbench result tabs.\n  - `activationMethod` — exact name of the exported function called to render this view.\n  - `matchCommands` — array of Redis command names this visualization supports (e.g. `[\"XRANGE\", \"XREVRANGE\"]`).\n  - `description` — short summary shown in the UI.\n  - `default` — `true` if this should be the default tab; otherwise `false`.\n\n## Activation Function\n\nThe plugin's main script exports an object whose keys are activation function names:\n\n```ts\nexport default { renderMyView };\n```\n\nEach activation function is invoked by the Workbench host and receives a props object containing:\n\n- The Redis `command` that was executed.\n- The data **result array** (raw Redis reply).\n- Helpers via `redisinsight-plugin-sdk` (config, modules, theme, base URL, app version).\n\nThe `<body>` of the iframe carries a `theme_DARK` or `theme_LIGHT` class, and `window.state` exposes plugin-relevant config such as connected modules, base URL, and app version.\n\n## Installation Paths\n\nUser-installed plugins live under:\n\n- macOS / Linux: `~/.redis-insight/plugins/`\n- Windows: `C:\\Users\\{Username}\\.redis-insight\\plugins\\`\n\nEach plugin lives in its own subdirectory (folder name should match `name`).\n\n## Trust Warning\n\nPlugins execute arbitrary code inside Redis Insight. **Only install plugins from trusted sources.** Treat plugin code review with the same care you would use for any code that runs in your dev environment.\n\n## What This Skill Adds\n\nThe official docs describe the contract. This skill adds operational practice:\n\n- Phased Phase 1/2/3 development to surface failure modes early.\n- Parcel for external plugins, Vite for internal monorepo plugins.\n- Defensive command parsing across different commands and flag-dependent response shapes.\n- `curl /api/plugins` and Playwright smoke validation after every deploy.\n- Docker static-plugin workaround for `redis/redisinsight` images.\n","isInternal":false,"tokens":705,"sizeBytes":3087},{"name":"plugin-manifest.md","path":".ai/skills/redis-insight-plugin/references/plugin-manifest.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/redis-insight-plugin/references/plugin-manifest.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Plugin Manifest (`package.json`)\n\nThe plugin manifest is `package.json` at the plugin root.\n\n## Single Visualization\n\n```json\n{\n  \"name\": \"ri-plugin-xrange-table\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Render XRANGE results as a sortable table.\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"visualizations\": [\n    {\n      \"id\": \"xrange-table\",\n      \"name\": \"Stream Entries\",\n      \"activationMethod\": \"renderXRangeTable\",\n      \"matchCommands\": [\"XRANGE\", \"XREVRANGE\"],\n      \"description\": \"Tabular view of stream entries.\",\n      \"default\": false\n    }\n  ]\n}\n```\n\n## Multiple Visualizations\n\n```json\n{\n  \"name\": \"ri-plugin-stream-views\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Table and chart visualizations for Redis stream entries.\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"visualizations\": [\n    {\n      \"id\": \"stream-table\",\n      \"name\": \"Table\",\n      \"activationMethod\": \"renderTableView\",\n      \"matchCommands\": [\"XRANGE\", \"XREVRANGE\"],\n      \"description\": \"Tabular view of stream entries.\",\n      \"default\": false\n    },\n    {\n      \"id\": \"stream-chart\",\n      \"name\": \"Chart\",\n      \"activationMethod\": \"renderChartView\",\n      \"matchCommands\": [\"XRANGE\", \"XREVRANGE\"],\n      \"description\": \"Chart numeric fields over the stream's time range.\",\n      \"default\": false\n    }\n  ]\n}\n```\n\nThe plugin's main bundle exports both functions:\n\n```ts\nexport default {\n  renderTableView,\n  renderChartView,\n};\n```\n\n## Matching and Defaults\n\nWhen several visualizations share commands, make their defaults mutually exclusive by command shape, not just by command name. For example, one visualization can be the default when the result has a particular shape (numeric series, coordinates, nested rows, …), while another is available for the same commands but defaults only for a different shape (scalar, store-style, or empty results).\n\nRules for matcher definitions:\n\n- Match whole command tokens. Do not let a command match a longer command with the same prefix (e.g. a `*STORE` or `*_RO` variant).\n- Keep `matchCommands` broad only when `matchQuery` narrows the result shape safely.\n- Use `noneRegex` for exclusion only after checking how the platform normalizes command text.\n- Keep regexes bounded and linear. Avoid broad repeated token alternatives plus a trailing `[\\s\\S]{0,N}` window.\n- Test overlapping visualizations so only one `default: true` candidate remains for each command shape.\n\n## Icons\n\nOptional but supported:\n\n```json\n{\n  \"iconDark\": \"./dist/icon-dark.svg\",\n  \"iconLight\": \"./dist/icon-light.svg\"\n}\n```\n\nPlace icons under `dist/` (or another path your manifest points at) and copy them as part of deployment.\n\n## Stripping Dev-Only Fields\n\nThe deployed manifest must **not** include:\n\n- `scripts`\n- `devDependencies`\n- `targets` (Parcel-only build hints)\n- `husky`, `lint-staged`, `eslintConfig`, `prettier`, etc.\n\nEither keep a separate `package.deploy.json` or strip with `jq`:\n\n```bash\njq 'del(.scripts, .devDependencies, .targets, .husky, .[\"lint-staged\"])' \\\n  package.json > /tmp/package.deploy.json\n```\n\n`templates/deploy-external.sh` does this automatically.\n\n## Required Fields Recap\n\n- `name` — globally unique within `~/.redis-insight/plugins/`.\n- `version` — semver.\n- `description` — one sentence shown in Insight.\n- `main` — built JS bundle path.\n- `styles` — built CSS path (omit only if no styles).\n- `visualizations[]` — at least one, each with `id`, `name`, `activationMethod`, `matchCommands`, `description`, `default`.\n\nIf any required field is missing, Insight silently drops the plugin. Always run `curl /api/plugins` after deploying.\n","isInternal":false,"tokens":899,"sizeBytes":3646},{"name":"redis-command-parsing.md","path":".ai/skills/redis-insight-plugin/references/redis-command-parsing.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/redis-insight-plugin/references/redis-command-parsing.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Redis Command Parsing\n\nPlugins must defensively parse raw Redis responses. Different commands return very different\nshapes, and even the same command with different flags returns different shapes.\n\n## Same Command, Different Shapes by Flag\n\nA command's optional modifiers change the response shape mid-flight. A classic pattern: without\nmodifiers a command returns a flat array of values, but adding `WITH...`-style flags returns a\nnested array per entry:\n\n```json\n// no modifiers — flat list\n[\"member1\", \"member2\", \"member3\"]\n```\n\n```json\n// with extra-data flags — nested per entry\n[\n  [\"member1\", \"12.34\", [\"extra\", \"data\"]],\n  [\"member2\", \"56.78\", [\"extra\", \"data\"]]\n]\n```\n\nNever key parsing off the command name alone — branch on the actual runtime shape, and read the\ncommand string from the activation props to choose the right header/label.\n\n## Worked Example: XRANGE / XREVRANGE\n\n```\nXRANGE stream - +\n```\n\nReturns an array of `[id, [field, value, field, value, ...]]`:\n\n```json\n[\n  [\"1700000000000-0\", [\"temp\", \"22\", \"humidity\", \"55\"]],\n  [\"1700000001000-0\", [\"temp\", \"23\"]]\n]\n```\n\nParser sketch:\n\n```ts\nexport function parseXRange(data: any[]): { id: string; fields: Record<string, string> }[] {\n  if (!Array.isArray(data)) return [];\n  return data.flatMap(entry => {\n    if (!Array.isArray(entry) || entry.length < 2) return [];\n    const [id, kv] = entry;\n    if (typeof id !== 'string' || !Array.isArray(kv)) return [];\n    const fields: Record<string, string> = {};\n    for (let i = 0; i + 1 < kv.length; i += 2) {\n      const k = kv[i];\n      const v = kv[i + 1];\n      if (typeof k === 'string') fields[k] = String(v);\n    }\n    return [{ id, fields }];\n  });\n}\n```\n\nThe same defensive structure applies to any command: start from `Array.isArray`, destructure\nsafely, coerce-and-validate numbers, and drop malformed entries instead of throwing.\n\n## Preserve Raw Values and Units\n\nSome commands accept a unit or format argument and return values in that same unit/format (a\nradius unit, a count, a score precision, etc.). If the visualization assumes one unit but the\ncommand used another, labels and scaling will be wrong. Read the relevant token from the command\nstring and preserve the raw value before any internal normalization.\n\n## Defensive Parsing Rules\n\n- Always start with `if (!Array.isArray(data)) return [];`.\n- Never assume nested array length — destructure with `const [a, b, ...rest] = ...`.\n- Preserve argument positions when tokenizing commands. Empty quoted strings (`\"\"`, `''`) should become empty-string tokens, not disappear.\n- Treat numeric fields as strings; convert with `Number(...)` and validate with `Number.isFinite(...)`.\n- Reject blank numeric strings before conversion; `Number('')` is `0`.\n- Drop malformed entries silently; surface the count (`\"3 of 25 entries skipped\"`) in the UI rather than crashing.\n- Never throw inside a parser — return an empty array and let the activation function render an empty/error state.\n- Avoid `Math.max(...largeArray)` or other spreading of full result sets. Use `reduce` or loops for large responses.\n- When parsing `FT.*` or other option-rich command strings, tokenize first and honor command grammar. Do not strip option keywords (`PARAMS`, `SEARCH`, `FILTER`, …) just because those words appear inside a key, member, index name, or quoted query.\n\n## Response Shape Caveats\n\n- **Cluster mode** can return wrapped responses depending on Insight's transport.\n- **Modifier flags** change the response shape mid-flight — never key off command name alone.\n- **Empty results** are `[]`, but on some commands a missing key returns `null`. Treat both as empty.\n- **Module commands** (RedisJSON, RedisTimeSeries, RedisSearch, etc.) return their own shapes — only support what your `matchCommands` declares.\n- **Redis 8 / module responses** can be array-like, aggregate-like, or object/map-style. Handle array-specific branches before generic object branches because arrays are objects in JavaScript.\n","isInternal":false,"tokens":937,"sizeBytes":3996},{"name":"redis-insight-plugin-guidelines.md","path":".ai/skills/redis-insight-plugin/references/redis-insight-plugin-guidelines.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/redis-insight-plugin/references/redis-insight-plugin-guidelines.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Redis Insight Plugin — Operational Guidelines\n\nLong-form operational reference. SKILL.md is the entry point; this file is for anything that doesn't fit there.\n\n## Plugin Types Overview\n\nTwo plugin shapes, two build tools, two deployment models.\n\n- **External standalone plugin** — bundles everything with **Parcel**. Ships as a folder you drop into `~/.redis-insight/plugins/<name>/`. No knowledge of the RedisInsight monorepo. Default for customer demos and field plugins.\n- **Internal monorepo plugin** — lives at `RedisInsight/redisinsight/ui/src/packages/<plugin-name>/`. Built with **Vite** as part of the RedisInsight repo. Ships embedded in Insight builds.\n\nIf you are unsure, build external. External works in every Redis Insight install; internal only ships with the Insight binary you build.\n\n## Official References\n\n- https://github.com/redis/RedisInsight/tree/main/docs/plugins\n- https://github.com/redis/RedisInsight/blob/main/docs/plugins/development.md\n- https://github.com/redis/RedisInsight/blob/main/docs/plugins/installation.md\n\n## Internal Plugin Development with Vite\n\n- Path: `RedisInsight/redisinsight/ui/src/packages/<plugin-name>/`.\n- Use Vite, not Parcel. Inherit RedisInsight's shared TS, theme, and component conventions.\n- Internal plugins **may** import from `uiSrc/` and shared components, but only when the import has a stable contract — internal layout helpers move often.\n- Avoid hard dependencies on `ThemeProvider` or other Insight-only providers in plugin code that might be reused externally later.\n\n## External Plugin Development with Parcel\n\n- Path: anywhere; deployed to `~/.redis-insight/plugins/<plugin-name>/`.\n- Bundle every dependency. Do not externalize React or any runtime library.\n- `targets.module.includeNodeModules: true` so Parcel inlines deps.\n- No imports from `uiSrc/`, `@redis-ui/*`, or any RedisInsight internal package.\n- Output to `dist/index.js` and `dist/styles.css`.\n\n## Package.json Requirements\n\nSee [plugin-manifest.md](plugin-manifest.md) for full examples. Minimum:\n\n```json\n{\n  \"name\": \"ri-plugin-example\",\n  \"version\": \"0.0.1\",\n  \"description\": \"...\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"visualizations\": [\n    {\n      \"id\": \"example-view\",\n      \"name\": \"Example View\",\n      \"activationMethod\": \"renderExampleView\",\n      \"matchCommands\": [\"XRANGE\"],\n      \"description\": \"Shows XRANGE results.\",\n      \"default\": false\n    }\n  ]\n}\n```\n\n## Plugin Entry Point\n\n```ts\nfunction renderExampleView(props) {\n  const root = document.getElementById('app');\n  if (!root) {\n    console.error('[EXAMPLE_PLUGIN] #app element missing');\n    return;\n  }\n  try {\n    // Render here. Show empty state when data is empty.\n  } catch (err) {\n    console.error('[EXAMPLE_PLUGIN] render failed', err);\n    root.innerHTML = '<div class=\"ri-plugin-error\">Plugin failed. See console.</div>';\n  }\n}\n\nexport default { renderExampleView };\n```\n\n## Testing & Verification\n\n- `npm run build` produces `dist/index.js` and `dist/styles.css`.\n- `templates/verify-plugin.sh` confirms file presence, no `process.env`, and that activation method names appear in the bundle.\n- Deploy with `templates/deploy-external.sh` (folder install) or `templates/deploy-internal-docker.sh` (Docker container).\n- After deploy, `curl http://localhost:5540/api/plugins` and grep for the plugin name.\n- Optional: add a Workbench smoke test following the repo's [e2e-testing](../../e2e-testing/SKILL.md) skill (page objects, fixtures, UI navigation) to validate iframe rendering.\n\n## Iterative Plugin Development\n\nSee [iterative-development.md](iterative-development.md). Always run Phase 1, then Phase 2, then Phase 3.\n\n## DO NOT Rules\n\nReproduced from SKILL.md for convenience:\n\n- DO NOT skip Phase 1 or Phase 2.\n- DO NOT deploy `index.js`/`styles.css` at the plugin root — they live in `dist/`.\n- DO NOT set `default: true` by default.\n- DO NOT include `scripts` or `devDependencies` in the deployed manifest.\n- DO NOT use Vite for standalone plugins.\n- DO NOT import `uiSrc` in standalone plugins.\n- DO NOT externalize React in standalone bundles.\n- DO NOT skip `/api/plugins` verification.\n- DO NOT ship `process.env.*` references.\n- DO NOT assume one Redis response shape.\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---------|--------------|-----|\n| Plugin missing from `/api/plugins` | Manifest not deployed, or wrong folder | Check folder is at `~/.redis-insight/plugins/<name>/` and contains `package.json` + `dist/`. |\n| Iframe blank | `activationMethod` name mismatch | Confirm exported function name == manifest field. |\n| `process.env is not defined` | `process.env.*` left in bundle | Replace at build time; rebuild; verify with `grep -c process.env dist/index.js`. |\n| `require is not defined` | Externalized React or wrong target | Use Parcel `targets.module` with `includeNodeModules: true`. |\n| Styles not applied | `styles` field missing or wrong path | Add `\"styles\": \"./dist/styles.css\"`; confirm file exists. |\n| Console error: `#app is null` | Activation ran before DOM | Ensure script is loaded as a module and renders into `document.getElementById('app')`. |\n| Visualization tab missing | `matchCommands` mismatch | Confirm command name (uppercase) appears in `matchCommands`. |\n\n## Lessons Learned\n\n- Phase 1 catches `activationMethod` name/manifest mismatches before any React is wired.\n- Phase 2 surfaces React/ReactDOM version and `createRoot` mismatches early.\n- Phase 3 exposes library asset-path bugs (icons/fonts/images broken by bundler-relative URLs) — fix once at init.\n- A command with extra-data flags often returns nested arrays, not objects — guard against shape changes.\n- Always `curl /api/plugins` after deploy. Cached menus lie.\n\n## Plugin API Reference\n\nActivation function props (subject to upstream changes — re-check official docs):\n\n- `command: string` — the executed Redis command.\n- `data: any[]` — the raw Redis result array.\n- `redisInsightPlugin` (via SDK) — `getState`, `setState`, `getModules`, `getTheme`, `getBaseUrl`, `getAppVersion`.\n\nUse `redisinsight-plugin-sdk` for state and theme rather than reading `window.state` directly when possible.\n\n## Command Parsing Patterns\n\nSee [redis-command-parsing.md](redis-command-parsing.md).\n\n## Third-Party Library Integration\n\nSee [third-party-libraries.md](third-party-libraries.md).\n\n## Interactive UI Patterns\n\n- Keep heavy UI inside the iframe; do not assume access to the host page.\n- Apply the [redis-ui-components](../../redis-ui-components/) skill with RedisInsight `light` / `dark` themes for visual plugin work (internal plugins use the `uiSrc/components/ui` wrappers per the [frontend](../../frontend/SKILL.md) styleguide).\n- Standalone plugins emulate product tokens locally with `src/styles/styles.scss`; copy `templates/external-styles.scss` as the baseline.\n- Use `document.body.classList.contains('theme_DARK')` to switch palettes.\n- Persist user settings via SDK state, falling back to `localStorage` keyed under your plugin id.\n- Avoid global CSS resets that fight Insight's iframe styles.\n\n## State Persistence\n\n- Prefer `redisinsight-plugin-sdk` `getState` / `setState` for cross-session persistence.\n- Fall back to `localStorage` with a plugin-prefixed key (`ri:<plugin-name>:settings`) when SDK state is unavailable.\n- Treat persisted state as untrusted; validate before use.\n\n## Error Handling & Stability\n\nSee [error-handling.md](error-handling.md).\n\n## Debugging & Development Tips\n\n- Open the Workbench iframe in browser devtools to inspect logs and network calls.\n- Look for `[<PLUGIN_PREFIX>]` log lines to filter your output.\n- Use the Phase 1 vanilla bundle to A/B test whether failures are plugin-host or library-related.\n- For Docker installs, `docker exec` into the container and inspect `/usr/src/app/redisinsight/api/dist/static/plugins/<name>/`.\n","isInternal":false,"tokens":1825,"sizeBytes":7869},{"name":"redisinsight-product-ui.md","path":".ai/skills/redis-insight-plugin/references/redisinsight-product-ui.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/redis-insight-plugin/references/redisinsight-product-ui.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# RedisInsight Product UI For Plugins\n\nUse the [redis-ui-components](../../redis-ui-components/) skill for visual decisions. This file\nonly adapts it to RedisInsight Workbench plugin iframes.\n\n> **Internal vs external.** For **internal** plugins (the default in this repo), build the UI\n> from Redis UI components via the `uiSrc/components/ui` wrappers and follow the\n> [frontend](../../frontend/SKILL.md) styleguide — use layout components, theme colors, and the\n> standard component folder structure. The \"Standalone Constraints\" below (no `@redis-ui/*` /\n> `uiSrc/` imports, local CSS variables) apply to **external standalone** plugins only, which\n> are bundled in isolation and must emulate the same look with local code.\n\n## Theme\n\n- RedisInsight plugins use `light` / `dark`, never `light2` / `dark2`.\n- Standalone external plugins cannot import RedisInsight internals, so define local CSS variables that emulate RedisInsight product tokens.\n- Read the iframe body class:\n  - `theme_LIGHT` => light theme\n  - `theme_DARK` => dark theme\n- If using `redisinsight-plugin-sdk`, prefer its theme helper when available, then mirror the same mode into local classes only when needed.\n\n## Baseline Files\n\nFor external Parcel plugins:\n\n- Copy `templates/external-styles.scss` to `src/styles/styles.scss`.\n- Keep `package.json` `\"styles\": \"./dist/styles.css\"`.\n- Keep the stylesheet bundled into `dist/styles.css`; RedisInsight injects it into the plugin iframe.\n\n## UI Rules\n\n- Build compact Workbench-like surfaces: toolbar, table, inspector, code/value panel, empty/error/loading states.\n- Prefer table-plus-detail layouts for Redis command responses.\n- Use `Source Code Pro` or monospace stacks for Redis keys, stream IDs, commands, raw values, and timestamps.\n- Use status badges with text and color; never rely on color alone.\n- Use product semantic colors for success, warning, danger, selected, and neutral states.\n- Keep controls stable: long keys and JSON must scroll, wrap intentionally, or ellipsize with a title/tooltip.\n\n## Standalone Constraints\n\n- Do not import `uiSrc/`, `@redis-ui/*`, RedisInsight monorepo packages, or private source.\n- Do not use global CSS resets that can fight the iframe host.\n- Scope styles under plugin classes such as `.ri-plugin-shell`.\n- Do not use Redis brand red as the universal accent, heading, CTA, error, or status color.\n\n## Verification\n\nCheck both themes:\n\n```bash\ncurl -s http://localhost:5540/api/plugins\n```\n\nThen run a matching Redis command in Workbench and verify:\n\n- `theme_LIGHT` and `theme_DARK` render correctly.\n- Empty, loading, error, and success states are styled.\n- Tables and `pre` blocks do not overflow the iframe incoherently.\n- Browser console has no style/runtime errors.\n","isInternal":false,"tokens":633,"sizeBytes":2746},{"name":"review-hardening.md","path":".ai/skills/redis-insight-plugin/references/review-hardening.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/redis-insight-plugin/references/review-hardening.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Review Hardening\n\nUse this before opening or updating a Redis Insight plugin PR. It captures review issues that are easy for coding agents to miss because the happy path works.\n\n## Start From Review Discipline\n\n- Treat every bot or human comment as a hypothesis. Confirm it against code, tests, and Redis/Insight behavior before changing code.\n- Reproduce the failing shape with a focused test first. If the issue is valid, keep the regression test.\n- Prefer the narrowest verification scope that proves the change: package tests, parser tests, manifest matcher tests, and typecheck. Do not run a full RedisInsight build for every comment unless the changed surface requires it.\n- When replying to PR comments, state the behavior fixed, the test added, and the commit that contains it.\n\n## Manifest and Matcher Checklist\n\n- Match whole Redis command tokens, not prefixes. A command must not match a longer command with the same prefix (e.g. a `*STORE` or `*_RO` variant).\n- Keep multi-visualization defaults mutually exclusive for overlapping commands. If one view is the default for one result shape, another can be available but must not also be the blanket default for the same command shape.\n- If `matchQuery.anyRegex` / `noneRegex` is used, keep regexes bounded and linear. Avoid repeated broad alternatives followed by trailing `[\\s\\S]{0,N}`.\n- Make token alternatives disjoint: quoted strings, double-quoted strings, and unquoted tokens should not overlap in a way that creates backtracking spikes.\n- Test native command names, module command names, and store/scalar variants separately. Query/result rows and scalar/store commands often need different visualization defaults.\n\n## Parser Checklist\n\n- Tokenize the Redis command before inspecting options. Raw `indexOf(\"PARAMS\")`, `indexOf(\"SEARCH\")`, or `indexOf(\"FILTER\")` will misread keys, members, index names, and quoted query text.\n- Preserve empty quoted tokens in the tokenizer. `\"\"` and `''` should still occupy an argument position, even if the downstream parser later rejects that command shape.\n- Keyword positions matter. For `FT.HYBRID`, skip the command and index tokens when detecting `SEARCH` / `FILTER`, and skip `PARAMS` key/value ranges when parsing predicates.\n- Strip or ignore large option payloads only when the token is the actual option for the command grammar. Do not strip a key/member named like an option keyword, an index named like one, or query text containing one.\n- Preserve the raw unit/format returned by Redis (the token that follows the relevant option in the command string) before any internal normalization.\n- Validate blank numeric parts before calling `Number(...)`; JavaScript treats `Number(\"\")` as `0`.\n- Differentiate empty result sets from malformed/incomplete rows so the UI can show the right guidance.\n- Handle search-like arrays, aggregate-like arrays, and Redis 8 map-style objects before generic object fallback. Arrays are objects in JavaScript, so order matters.\n- Avoid `Math.max(...largeArray)` or similar spread over result sets. Reduce or loop to avoid call stack and argument limits.\n\n## Visualization Library Checklist\n\n- Validate library preconditions before rendering (non-empty series, valid bounds/ranges, sized container). Bail to an empty/error state instead of calling render APIs with bad input.\n- Do not let library callbacks (cell renderers, tooltip/popup builders, icon factories) capture stale React state. Keep live values in refs or recreate the instance when the callback dependencies change.\n- Build tooltip/popup/cell DOM with `textContent` or escaped React output, not interpolated HTML from Redis data.\n- Destroy/dispose the library instance before re-creating it on re-render to avoid leaks.\n- For large result sets, use loops/reductions and avoid per-render expensive parsing unless the input changed.\n- Use mode-aware empty/error titles. Different visualizations in the same plugin should not all show the same generic failure message.\n\n## Code Hygiene Checklist\n\n- Remove dead plugin config files and unused exports instead of leaving them for reviewers to rediscover.\n- Enforce declared limits or delete them. A `maxRows` / `maxPoints` constant that nobody reads is a bug magnet.\n- Centralize shared conversion constants/helpers so parsers and visualizations cannot drift.\n- Keep component-local types and large constants in sibling `.types.ts` and `.constants.ts` files when the component is already large.\n- Keep return types honest and simple. Do not use a tuple type when the function returns arbitrary-length arrays.\n- After discriminated-union narrowing, remove unreachable null/error branches. A defensive check that cannot run makes the real state model harder to review.\n- Memoize parsed command/results when parsing is nontrivial or feeds the visualization render.\n\n## Test Matrix\n\nAdd focused tests for any surface you touch:\n\n- Manifest matcher: exact command boundaries, default exclusivity, distinct result shapes (scalar vs row vs nested).\n- Command parser: keyword-like key/member/index names, empty quoted tokens, option keywords appearing in native commands and in query text, large payloads, empty rows, malformed rows, Redis 8 object responses.\n- Unit/format handling: commands that take a unit or format argument return values in that same unit/format; cover each variant the plugin supports.\n- Visualization state: control updates while rendered, empty/invalid input sets, distinct per-mode error titles.\n- Performance guardrails: large result counts, large payloads, and no spread into `Math.max`.\n","isInternal":false,"tokens":1121,"sizeBytes":5567},{"name":"testing-and-deployment.md","path":".ai/skills/redis-insight-plugin/references/testing-and-deployment.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/redis-insight-plugin/references/testing-and-deployment.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Testing and Deployment\n\nHow to deploy and verify a Redis Insight plugin, and how to smoke test it with Playwright (following the repo's [e2e-testing](../../e2e-testing/SKILL.md) skill).\n\n## Correct External Deployment Structure\n\n```\n~/.redis-insight/plugins/<plugin-name>/\n  package.json               # deployed manifest (no scripts, no devDependencies)\n  dist/\n    index.js\n    styles.css\n```\n\nThat's it. Nothing else is required at deploy time.\n\n## Wrong Structures (Common Mistakes)\n\n```\n# WRONG — bundle at plugin root\n~/.redis-insight/plugins/<plugin-name>/\n  package.json\n  index.js                   # should be in dist/\n  styles.css                 # should be in dist/\n```\n\n```\n# WRONG — nested package folder\n~/.redis-insight/plugins/<plugin-name>/\n  <plugin-name>/             # extra directory\n    package.json\n    dist/\n```\n\n```\n# WRONG — manifest still has dev/scripts\n{\n  \"main\": \"./dist/index.js\",\n  \"scripts\": { \"build\": \"...\" },        # remove\n  \"devDependencies\": { \"parcel\": \"...\" } # remove\n}\n```\n\n## Deploy Commands\n\nExternal (host install):\n\n```bash\nnpm run build\nbash templates/verify-plugin.sh\nbash templates/deploy-external.sh\n```\n\nInside Docker RedisInsight:\n\n```bash\nnpm run build\nbash templates/verify-plugin.sh\nbash templates/deploy-internal-docker.sh\n```\n\n## Docker RedisInsight: Static Plugin Path\n\nExternal plugin deployment to `~/.redis-insight/plugins/<name>/` works for the host install — that is the canonical path and the one to default to.\n\nFor Docker, the host plugins folder is not visible inside the container. To install a plugin into a running `redis/redisinsight` container, copy it into the container's static plugins folder:\n\n```\n/usr/src/app/redisinsight/api/dist/static/plugins/<plugin-name>/\n```\n\nWorkflow:\n\n```bash\ndocker cp dist redisinsight-test:/usr/src/app/redisinsight/api/dist/static/plugins/<plugin-name>/dist\ndocker cp package.json redisinsight-test:/usr/src/app/redisinsight/api/dist/static/plugins/<plugin-name>/package.json\ndocker restart redisinsight-test\n```\n\nThen verify via `/api/plugins`. The deploy script in `templates/deploy-internal-docker.sh` automates this.\n\n## /api/plugins Verification\n\nAfter every deploy:\n\n```bash\ncurl -s http://localhost:5540/api/plugins | jq '.[] | {name, visualizations}'\n```\n\nThe plugin must appear by `name`, with each declared `visualizations[*].id`. If absent:\n\n- The plugin folder is wrong, or\n- The manifest is invalid (missing required field), or\n- Insight wasn't restarted (Docker case).\n\n## Browser Console Verification\n\nOpen the Workbench iframe in browser devtools:\n\n- Filter logs by your `[<PLUGIN_PREFIX>]` prefix.\n- Confirm `activated` log fires when running a matching command.\n- Confirm no `process.env` errors, no `require is not defined`, no React mismatch.\n- Inspect network calls — there should be none unless your plugin explicitly makes them.\n\n## Playwright Smoke Test Strategy\n\nFor any Playwright E2E test, **follow the repo's [e2e-testing](../../e2e-testing/SKILL.md)\nskill** — it owns the conventions for this repo (tests live in `tests/e2e-playwright/`, use page\nobjects, fixtures, and `apiHelper`; never call `page.goto()` directly; no CSS selectors or fixed\nwaits). Do not hand-roll a standalone spec that bypasses those patterns.\n\nA plugin smoke test should cover the same flow, expressed through those conventions:\n\n1. Create/seed the database via `apiHelper` in `beforeAll`.\n2. Navigate to Workbench through a page object's `goto()` method (UI navigation, not `page.goto()`).\n3. Run a command that matches the plugin's `matchCommands`.\n4. Select the plugin's visualization tab.\n5. Assert the iframe is present and renders expected content.\n\nUse Playwright MCP to discover the actual `data-testid`/roles first, as the e2e-testing skill\ndescribes. The one plugin-specific check that does not need the UI is registration —\n`curl /api/plugins` (or `page.request.get('/api/plugins')`) must list the plugin.\n\n## Internal PR Verification Scope\n\nFor RedisInsight monorepo plugin changes, keep verification proportional to the changed surface:\n\n- Parser or command-shape fix: run the parser spec or package test file first, then the plugin package test command.\n- Manifest or matcher fix: run the matcher/plugin utility spec that exercises `matchCommands`, `matchQuery`, `anyRegex`, `noneRegex`, and `default` selection.\n- React / visualization-library fix: run the component spec for that visualization plus the package typecheck.\n- Code hygiene fix: run the closest affected spec plus typecheck, especially when moving types/constants or centralizing helpers.\n- Shared utility change under `ui/src/`: add the focused utility spec and a changed-file lint/typecheck check.\n- Before final commit on a PR batch, run the package tests, package typecheck, and `git diff --check`. Run a full RedisInsight build only when build configuration, package exports, bundling, or shared application wiring changed.\n","isInternal":false,"tokens":1140,"sizeBytes":4939},{"name":"third-party-libraries.md","path":".ai/skills/redis-insight-plugin/references/third-party-libraries.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/redis-insight-plugin/references/third-party-libraries.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Third-Party Libraries\n\nNotes on integrating a visualization library (charting, mapping, grid, diagram, etc.) inside a\nRedis Insight plugin iframe. The principles are library-agnostic; apply them to whatever library\nthe plugin needs.\n\n## General Integration Rules\n\n- **Bundle the library fully** in an external (Parcel) plugin — do not externalize it. Internal\n  (Vite) plugins follow the shared build's dependency handling instead.\n- **Add the library's CSS to the bundle** (`dist/styles.css`); do not rely on a global `@import`\n  or a CDN. RedisInsight injects the bundled stylesheet into the plugin iframe.\n- **Initialize once, clean up on re-render.** Libraries that own a canvas, map, or grid instance\n  must be destroyed/disposed before re-creating, or they leak:\n\n  ```ts\n  instanceRef.current?.destroy();\n  instanceRef.current = createInstance(container, config);\n  ```\n\n- **Size the container explicitly.** Many libraries measure their parent on init; put the\n  render target in a sized flex/grid container so it lays out correctly inside the iframe.\n- **Validate inputs before handing them to the library.** Never assume parsed Redis data is\n  well-formed — guard for empty/NaN/out-of-range values, and check any library precondition\n  (valid bounds, non-empty series, etc.) before calling its render APIs.\n- **Avoid stale closures in library callbacks.** If a callback (cell renderer, tooltip builder,\n  icon factory) depends on React state, keep the live value in a ref or recreate the instance\n  when the dependency changes — captured state goes stale otherwise.\n- **Never interpolate Redis data into raw HTML.** Build tooltip/popup/cell DOM with\n  `textContent` or escaped React output so keys, members, and field values cannot inject markup.\n\n## Untyped Libraries: Custom `.d.ts`\n\nSome libraries (or their sub-plugins) ship without types. Add a local declaration instead of\nusing `any`:\n\n```ts\n// src/types/<lib-name>.d.ts\ndeclare module '<lib-name>' {\n  export interface Options {\n    /* the options the plugin actually uses */\n  }\n  export function createThing(target: HTMLElement, options?: Options): unknown;\n}\n```\n\nKeep the declaration minimal — only the surface the plugin uses.\n\n## Bundle Size Guidance\n\n- Aim for `dist/index.js` under ~1.5 MB minified for a snappy first render.\n- Use the bundler's analyzer (or `terser`'s reports) to find heavy dependencies.\n- Avoid `moment`; use `date-fns` or native `Intl.DateTimeFormat`.\n- Avoid full `lodash`; use targeted imports (`lodash/get`) or stdlib.\n- Tree-shaking only helps when imports are scoped (`import { sum } from 'lodash-es'`).\n- Cap row/point counts (paginate or virtualize) for large command results rather than handing\n  the whole set to the library at once.\n\n## Forbidden Imports\n\n- No `uiSrc/`, `@redis-ui/*`, or any RedisInsight monorepo internal in standalone plugins.\n- No Node-only modules (`fs`, `path`, `child_process`).\n- No CommonJS-only deps that fail in Parcel's `module` target without shims.\n","isInternal":false,"tokens":689,"sizeBytes":2996},{"name":"SKILL.md","path":".ai/skills/redis-insight-plugin/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/redis-insight-plugin/SKILL.md","title":"redis-insight-plugin","category":"anthropic-skill","format":"markdown","content":"---\nname: redis-insight-plugin\ndescription: Use when creating, modifying, debugging, deploying, or testing Redis Insight Workbench visualization plugins, plugin manifests, package.json visualizations, activationMethod functions, redisinsight-plugin-sdk usage, Parcel/Vite plugin builds, iframe rendering, Redis command parsing, Docker RedisInsight deployment, /api/plugins verification, or Playwright plugin validation.\nlicense: MIT\nmetadata:\n  author: redis\n  version: \"1.0.0\"\n---\n# Redis Insight Workbench Plugin\n\nBuild, deploy, and validate Redis Insight Workbench visualization plugins. Plugins render inside an iframe in Workbench and visualize the result of a Redis command. Trigger this skill for plugin manifests, `package.json` `visualizations`, `activationMethod` functions, `redisinsight-plugin-sdk` usage, Parcel/Vite plugin builds, iframe rendering, Redis command parsing, Docker RedisInsight deployment, `/api/plugins` verification, and Playwright plugin tests.\n\nUse the `redis-ui-components` skill for every visual plugin UI. RedisInsight plugins use the RedisInsight product theme pair: `light` / `dark`, not `light2` / `dark2`.\n\nOfficial source-of-truth references:\n\n- In-repo canonical docs: [docs/plugins/development.md](../../../docs/plugins/development.md), [docs/plugins/installation.md](../../../docs/plugins/installation.md), [docs/plugins/introduction.md](../../../docs/plugins/introduction.md)\n- Upstream: https://github.com/redis/RedisInsight/tree/main/docs/plugins\n\nSee [references/official-docs-summary.md](references/official-docs-summary.md) for a condensed summary.\n\n## Repo Code Conventions (internal plugins)\n\n> **You are inside the RedisInsight repo.** An internal plugin lives in the `redisinsight/ui/`\n> tree, so its code must follow the same styleguides as the rest of the UI — not ad hoc plugin\n> code. These rules are **mandatory** for any plugin code written here and override generic\n> external-plugin guidance below where they conflict:\n>\n> - **[frontend](../frontend/SKILL.md)** — component folder structure\n>   (`ComponentName/ComponentName.tsx` + `.styles.ts` + `.types.ts` + `.spec.tsx`), functional\n>   components with hooks, **named exports**, barrel files, layout components\n>   (`Row` / `Col` / `FlexGroup`) instead of raw `div`, and theme usage.\n> - **[redis-ui-components](../redis-ui-components/)** — build all plugin UI from Redis UI\n>   components. Import the internal `uiSrc/components/ui` wrappers; **never** import raw\n>   `@redis-ui/*`. (This skill is a symlink into the installed `@redis-ui/components` package,\n>   so it resolves after `npm install`; if it is missing, run install — the canonical source is\n>   `node_modules/@redis-ui/components/skills/redis-ui-components/`.)\n> - **[code-quality](../code-quality/SKILL.md)** — TypeScript everywhere (no `any`), naming\n>   (`PascalCase` / `camelCase` / `UPPER_SNAKE_CASE`), import order, no magic numbers, no\n>   `!important` in styles, semantic theme colors over CSS variables.\n> - **[testing](../testing/SKILL.md)** — Jest + Testing Library, the `renderComponent` helper,\n>   `faker` for test data, `waitFor` instead of fixed time waits.\n> - **[e2e-testing](../e2e-testing/SKILL.md)** — any Playwright/E2E test follows this skill\n>   (tests in `tests/e2e-playwright/`, page objects, fixtures, UI navigation; never `page.goto()`\n>   directly, no CSS selectors or fixed waits).\n>\n> External standalone plugins (below) are bundled in isolation and **cannot** import these\n> internals; they emulate the conventions with local code instead.\n\n## First Decision: Plugin Type\n\nDecide before scaffolding anything else.\n\n- **Internal monorepo plugin** — lives inside `redisinsight/ui/src/packages/<plugin-name>/` and ships with Redis Insight itself. Build with **Vite** (shared config). Follow the repo styleguides above. **This is the default for any contribution to this repo** — copy a sibling package such as `geodata` or `redisearch` rather than diverging.\n- **External standalone plugin** — installed by a user into `~/.redis-insight/plugins/<name>/`. Build with **Parcel**. Bundle all dependencies. Do not import from `uiSrc/` or any RedisInsight monorepo internal. Use this only for customer/field/demo plugins that ship *outside* this repo.\n\nSee [references/internal-vite-plugin.md](references/internal-vite-plugin.md) and [references/external-parcel-plugin.md](references/external-parcel-plugin.md).\n\n## External Plugin Structure\n\n```\n<plugin-name>/\n  package.json              # manifest + build scripts\n  src/\n    index.html              # iframe entry, has #app\n    main.tsx                # activation functions, default export\n    components/\n    styles/\n      styles.scss\n  dist/\n    index.js                # built bundle (referenced by manifest \"main\")\n    styles.css              # built styles (referenced by manifest \"styles\")\n```\n\n## Required Manifest\n\nTop-level `package.json` fields:\n\n- `name`\n- `version`\n- `description`\n- `main` — path to built JS (e.g. `./dist/index.js`).\n- `styles` — path to built CSS (e.g. `./dist/styles.css`).\n- `visualizations` — array of visualization descriptors.\n\nEach visualization descriptor must include:\n\n- `id`\n- `name`\n- `activationMethod`\n- `matchCommands`\n- `description`\n- `default`\n\nSet `default: false` unless the user explicitly asks for it to be the default visualization.\n\nThe `activationMethod` value must **exactly** match an exported function name in the bundle. The plugin entry must export that function via the default export:\n\n```ts\nexport default { renderMyView };\n```\n\nMultiple visualizations:\n\n```ts\nexport default {\n  renderTableView,\n  renderChartView,\n};\n```\n\nSee [references/plugin-manifest.md](references/plugin-manifest.md) for full examples and how to strip dev-only fields from the deployed manifest.\n\n## Activation Function Contract\n\nEvery activation function must:\n\n1. Get the host element: `const root = document.getElementById('app');`\n2. Defensively validate props (`command`, `data`, modules, theme).\n3. Wrap render logic in `try/catch` and render an error state on failure.\n4. Render an empty state when `data` is missing or empty.\n5. Log with a plugin-specific prefix, e.g. `[MY_PLUGIN]`, never bare `console.log`.\n\nSee [references/error-handling.md](references/error-handling.md).\n\n## RedisInsight Product UI Contract\n\nEvery plugin UI must follow RedisInsight product styling:\n\n- For **internal** plugins, build the UI from the [redis-ui-components](../redis-ui-components/) skill and the `uiSrc/components/ui` wrappers, following the [frontend](../frontend/SKILL.md) styleguide.\n- For **external** plugins, mirror the same Redis UI look with local code (you cannot import the internals).\n- Use RedisInsight `light` / `dark` product themes.\n- For standalone external plugins, emulate the RedisInsight product tokens with local CSS variables; do not import RedisInsight monorepo internals or `@redis-ui/*`.\n- Detect iframe theme through `theme_LIGHT` / `theme_DARK` body classes or SDK theme helpers.\n- Use compact product components: table, toolbar, segmented states, empty/error/loading panels, badges, and inspector-style details.\n- Keep Redis brand red for brand moments only; do not use it as the default plugin CTA, heading, error, or status color.\n\nSee [references/redisinsight-product-ui.md](references/redisinsight-product-ui.md) and use [templates/external-styles.scss](templates/external-styles.scss) as the baseline `src/styles/styles.scss`.\n\n## Mandatory Phased Workflow\n\nBuild every new plugin in three phases. Do not skip phases — the failure mode in each phase tells you exactly what is wrong.\n\n- **Phase 1 — Vanilla wiring.** No React, no third-party libraries. Render plain DOM that proves activation, props, and iframe rendering work end-to-end.\n- **Phase 2 — React rendering.** Add React + ReactDOM. Render a typed component that displays `command`, `status`, and the raw response.\n- **Phase 3 — Full feature.** Add the actual visualization library (charting, mapping, grid, etc.) and the real UX.\n\nSee [references/iterative-development.md](references/iterative-development.md) and the templates in `templates/`.\n\n## Review Hardening Loop\n\nBefore asking for review, run a small adversarial pass against the exact surfaces the plugin touches:\n\n- Manifest matching/defaults: exact command boundaries, no default visualization conflicts, no regex backtracking traps.\n- Redis command parsing: token-aware option handling, raw-unit preservation, malformed rows, empty rows, keyword-like key/member names.\n- Visualization state: stale closures, safe library bounds/inputs, large result sets, mode-specific empty/error copy.\n- Tests: red regression first, then the smallest package/component/parser test set that proves the fix.\n\nSee [references/review-hardening.md](references/review-hardening.md).\n\n## Build and Verify\n\n```bash\nnpm run build\ntest -f dist/index.js\ntest -f dist/styles.css                       # if \"styles\" is declared\ngrep -c \"process.env\" dist/index.js           # must be 0 in a Parcel build\n```\n\nConfirm each `activationMethod` name appears in the bundle:\n\n```bash\ngrep -o \"renderMyView\" dist/index.js | head\n```\n\n## Deploy\n\nExternal plugin → user plugins folder:\n\n```bash\nmkdir -p ~/.redis-insight/plugins/<plugin-name>\ncp package.json ~/.redis-insight/plugins/<plugin-name>/\ncp -R dist ~/.redis-insight/plugins/<plugin-name>/dist\n```\n\nRestart Redis Insight, then verify:\n\n```bash\ncurl -s http://localhost:5540/api/plugins\n```\n\nThe response must include the plugin `name` and its visualizations. See [references/testing-and-deployment.md](references/testing-and-deployment.md) for Docker workarounds, Playwright smoke tests, and the static-plugin path inside the Docker image.\n\n## Security Rules\n\n- Plugins execute code in the Insight UI process. Only ship code you control or trust.\n- Never embed secrets, tokens, or credentials in the bundle.\n- No hidden network calls. Document any outbound HTTP and prefer none.\n- Default to read-only Redis commands (`HGETALL`, `LRANGE`, `XRANGE`, `INFO`, `FT.SEARCH`). Never run destructive commands (`FLUSHDB`, `DEL`, `UNLINK`, `XTRIM`, `CLUSTER RESET`, `CONFIG SET`) without explicit user request and confirmation.\n\n## DO NOT\n\n- DO NOT skip Phase 1 or Phase 2; each catches a different class of failure.\n- DO NOT deploy `index.js` / `styles.css` at the plugin root. They live in `dist/` and the manifest points at `./dist/...`.\n- DO NOT set `default: true` on a visualization unless the user asked for it.\n- DO NOT include `scripts` or `devDependencies` in the deployed manifest. Strip them before copying.\n- DO NOT use Vite for standalone external plugins. Use Parcel.\n- DO NOT import from `uiSrc/`, `@redis-ui/*`, or any RedisInsight monorepo internal in a standalone plugin.\n- DO NOT externalize React in a standalone plugin bundle. Bundle React and ReactDOM.\n- DO NOT style plugin UI with ad hoc inline brand colors. Use RedisInsight product UI variables/classes.\n- DO NOT use `light2` / `dark2` for RedisInsight plugins; those are for other Redis product UIs.\n- DO NOT skip `/api/plugins` verification after deploying.\n- DO NOT ship `process.env.*` references in the bundle. Replace at build time.\n- DO NOT assume one Redis response shape — different commands, and even the same command with different flags (e.g. a `WITH...` modifier), return very different structures. See [references/redis-command-parsing.md](references/redis-command-parsing.md).\n\n## Final Checklist\n\n- Plugin type chosen and matches build tool (Parcel = external, Vite = internal).\n- `package.json` declares `main`, `styles`, and `visualizations` with required fields.\n- Every `activationMethod` matches a default-exported function.\n- Phases 1, 2, 3 each rendered successfully before moving on.\n- RedisInsight product UI applied via the `redis-ui-components` skill (internal plugins) or emulated locally (external plugins), with `light` / `dark` theme handling.\n- Review hardening pass completed for manifest matching, command parsing, visualization state, and scoped tests.\n- Bundle verified: `dist/index.js`, `dist/styles.css`, no `process.env`.\n- Plugin deployed to `~/.redis-insight/plugins/<name>/` (or via the Docker workaround).\n- `curl http://localhost:5540/api/plugins` lists the plugin.\n- Workbench runs a matching command and renders the visualization.\n- Defensive empty/error states verified.\n- Optional Playwright smoke test passes (written per the [e2e-testing](../e2e-testing/SKILL.md) skill).\n\n## Reference Index\n\n| File | Load When |\n|------|-----------|\n| [official-docs-summary.md](references/official-docs-summary.md) | Need the canonical contract from Redis Insight docs. |\n| [redis-insight-plugin-guidelines.md](references/redis-insight-plugin-guidelines.md) | Need the long-form operational reference. |\n| [external-parcel-plugin.md](references/external-parcel-plugin.md) | Building a standalone plugin with Parcel. |\n| [internal-vite-plugin.md](references/internal-vite-plugin.md) | Building inside the RedisInsight monorepo with Vite. |\n| [plugin-manifest.md](references/plugin-manifest.md) | Writing or stripping `package.json` manifests. |\n| [iterative-development.md](references/iterative-development.md) | Phase 1/2/3 templates and pipeline. |\n| [redisinsight-product-ui.md](references/redisinsight-product-ui.md) | Applying RedisInsight product UI inside plugin iframes. |\n| [review-hardening.md](references/review-hardening.md) | Pre-review checklist for matcher, parser, visualization state, and scoped regression tests. |\n| [testing-and-deployment.md](references/testing-and-deployment.md) | Deploy paths, Docker workaround, `/api/plugins`, Playwright. |\n| [redis-command-parsing.md](references/redis-command-parsing.md) | Parsing raw Redis command responses defensively. |\n| [third-party-libraries.md](references/third-party-libraries.md) | Integrating a visualization library, custom `.d.ts`, bundle size. |\n| [error-handling.md](references/error-handling.md) | Defensive render, ErrorBoundary, log prefixes. |\n","frontmatter":{"name":"redis-insight-plugin","description":"Use when creating, modifying, debugging, deploying, or testing Redis Insight Workbench visualization plugins, plugin manifests, package.json visualizations, activationMethod functions, redisinsight-plugin-sdk usage, Parcel/Vite plugin builds, iframe rendering, Redis command parsing, Docker RedisInsight deployment, /api/plugins verification, or Playwright plugin validation.","license":"MIT","metadata":{"author":"redis","version":"1.0.0"}},"isInternal":false,"tokens":3235,"sizeBytes":14036},{"name":"external-parcel-package.json","path":".ai/skills/redis-insight-plugin/templates/external-parcel-package.json","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/redis-insight-plugin/templates/external-parcel-package.json","title":"ri-plugin-example","category":"anthropic-skill","format":"json","content":"{\n  \"name\": \"ri-plugin-example\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Example external Redis Insight Workbench plugin built with Parcel.\",\n  \"source\": \"./src/main.tsx\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"scripts\": {\n    \"start\": \"parcel src/index.html\",\n    \"build\": \"concurrently \\\"npm run build:js\\\" \\\"npm run build:css\\\"\",\n    \"build:js\": \"parcel build src/main.tsx --no-source-maps --dist-dir dist --target module\",\n    \"build:css\": \"parcel build src/styles/styles.scss --no-source-maps --dist-dir dist\",\n    \"minify:js\": \"terser dist/index.js -o dist/index.js -c -m\",\n    \"clean\": \"rimraf dist\",\n    \"verify\": \"bash scripts/verify-plugin.sh\",\n    \"deploy:external\": \"bash scripts/deploy-external.sh\",\n    \"deploy:internal\": \"bash scripts/deploy-internal-docker.sh\"\n  },\n  \"targets\": {\n    \"main\": false,\n    \"module\": {\n      \"includeNodeModules\": true,\n      \"outputFormat\": \"esmodule\",\n      \"isLibrary\": false\n    }\n  },\n  \"visualizations\": [\n    {\n      \"id\": \"example-view\",\n      \"name\": \"Example View\",\n      \"activationMethod\": \"renderExampleView\",\n      \"matchCommands\": [\"XRANGE\"],\n      \"description\": \"Example visualization rendered for XRANGE results.\",\n      \"default\": false\n    }\n  ],\n  \"dependencies\": {\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\",\n    \"redisinsight-plugin-sdk\": \"^1.1.0\"\n  },\n  \"devDependencies\": {\n    \"parcel\": \"^2.12.0\",\n    \"terser\": \"^5.30.0\",\n    \"rimraf\": \"^5.0.5\",\n    \"concurrently\": \"^8.2.2\",\n    \"cross-env\": \"^7.0.3\",\n    \"typescript\": \"^5.4.0\",\n    \"@types/react\": \"^17.0.79\",\n    \"@types/react-dom\": \"^17.0.25\"\n  }\n}\n","frontmatter":{"name":"ri-plugin-example","version":"0.0.1","description":"Example external Redis Insight Workbench plugin built with Parcel.","source":"./src/main.tsx","main":"./dist/index.js","styles":"./dist/styles.css","scripts":{"start":"parcel src/index.html","build":"concurrently \"npm run build:js\" \"npm run build:css\"","build:js":"parcel build src/main.tsx --no-source-maps --dist-dir dist --target module","build:css":"parcel build src/styles/styles.scss --no-source-maps --dist-dir dist","minify:js":"terser dist/index.js -o dist/index.js -c -m","clean":"rimraf dist","verify":"bash scripts/verify-plugin.sh","deploy:external":"bash scripts/deploy-external.sh","deploy:internal":"bash scripts/deploy-internal-docker.sh"},"targets":{"main":false,"module":{"includeNodeModules":true,"outputFormat":"esmodule","isLibrary":false}},"visualizations":[{"id":"example-view","name":"Example View","activationMethod":"renderExampleView","matchCommands":["XRANGE"],"description":"Example visualization rendered for XRANGE results.","default":false}],"dependencies":{"react":"^17.0.2","react-dom":"^17.0.2","redisinsight-plugin-sdk":"^1.1.0"},"devDependencies":{"parcel":"^2.12.0","terser":"^5.30.0","rimraf":"^5.0.5","concurrently":"^8.2.2","cross-env":"^7.0.3","typescript":"^5.4.0","@types/react":"^17.0.79","@types/react-dom":"^17.0.25"}},"isInternal":false,"tokens":487,"sizeBytes":1611},{"name":"SKILL.md","path":".ai/skills/testing/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/testing/SKILL.md","title":"testing","category":"anthropic-skill","format":"markdown","content":"---\nname: testing\ndescription: >-\n  Unit/integration testing standards for RedisInsight using Jest and\n  Testing Library: test structure, the `renderComponent` helper, faker\n  for test data, mocking patterns, and `waitFor` instead of fixed\n  time waits. Use when writing or modifying any `*.spec.ts` or\n  `*.spec.tsx` file, when adding component or slice tests, when\n  debugging flaky tests, or when the user mentions jest, testing\n  library, faker, or test patterns.\n---\n\n# Testing Standards and Practices\n\n## Core Principles\n\n- **Write tests for all new features**\n- Follow **AAA pattern**: Arrange, Act, Assert\n- Use **descriptive test names**: \"should do X when Y\"\n- **CRITICAL**: Never use fixed time waits - tests must be deterministic\n- **CRITICAL**: Use faker library (@faker-js/faker) for test data\n\n## Test Organization\n\n```typescript\ndescribe('FeatureService', () => {\n  describe('findById', () => {\n    it('should return entity when found', () => {});\n    it('should throw NotFoundException when not found', () => {});\n  });\n\n  describe('create', () => {\n    it('should create entity with valid data', () => {});\n    it('should throw error with invalid data', () => {});\n  });\n});\n```\n\n## Frontend Testing (Jest + Testing Library)\n\n### Running Specific Tests\n\n```bash\n# Run a specific test file\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs'\n\n# Run a specific test by name (use -t flag)\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs' -t 'test name pattern'\n\n# Example:\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/slices/tests/browser/keys.spec.ts' -c 'jest.config.cjs' -t 'refreshKeyInfoAction'\n```\n\n### CRITICAL: Always Use Shared `renderComponent` Helper\n\n**Create a `renderComponent` helper for each component test file**:\n\n```typescript\nimport { faker } from '@faker-js/faker';\n// Pull structured data from a shared factory (redisinsight/ui/src/mocks/factories/);\n// only build ad-hoc primitives/callbacks inline.\n\ndescribe('MyComponent', () => {\n  // Default props: factory for the entity, inline only for callbacks/primitives\n  const mockUser = UserFactory.build();\n  const defaultProps: MyComponentProps = {\n    user: mockUser,\n    onComplete: jest.fn(),\n  };\n\n  // Shared render helper\n  const renderComponent = (propsOverride?: Partial<MyComponentProps>) => {\n    const props = { ...defaultProps, ...propsOverride };\n\n    return render(\n      <Provider store={store}>\n        <MyComponent {...props} />\n      </Provider>\n    );\n  };\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n  });\n\n  it('should render component', () => {\n    renderComponent();\n    expect(screen.getByText(defaultProps.name)).toBeInTheDocument();\n  });\n\n  it('should handle click', async () => {\n    const mockOnComplete = jest.fn();\n    renderComponent({ onComplete: mockOnComplete });\n\n    fireEvent.click(screen.getByRole('button'));\n\n    await waitFor(() => {\n      expect(mockOnComplete).toHaveBeenCalledTimes(1);\n    });\n  });\n});\n```\n\n**Benefits**:\n\n- Centralized setup (providers, router, theme)\n- Default props defined once\n- Easy prop overrides per test\n- No duplicate setup code\n\n### Complex Component Setup\n\nFor components requiring Router, ThemeProvider, etc., include them in `renderComponent`:\n\n```typescript\nconst renderComponent = (propsOverride?: Partial<Props>) => {\n  const props = { ...defaultProps, ...propsOverride }\n\n  return render(\n    <Provider store={store}>\n      <BrowserRouter>\n        <ThemeProvider theme={theme}>\n          <Component {...props} />\n        </ThemeProvider>\n      </BrowserRouter>\n    </Provider>\n  )\n}\n```\n\n### Testing with Redux\n\nCreate a test store with `configureStore` for Redux-connected components:\n\n```typescript\nconst createTestStore = (initialState = {}) => {\n  return configureStore({\n    reducer: { user: userSlice.reducer },\n    preloadedState: initialState,\n  });\n};\n\nconst renderComponent = (propsOverride?: Partial<Props>, storeState = {}) => {\n  const testStore = createTestStore(storeState);\n  // render with testStore\n};\n```\n\n### Query Priorities (Testing Library)\n\n**Prefer accessible queries** (as users would interact):\n\n```typescript\n// ✅ PREFERRED\nscreen.getByRole('button', { name: /submit/i });\nscreen.getByLabelText('Email');\nscreen.getByPlaceholderText('Enter name');\n\n// ⚠️ LAST RESORT\nscreen.getByTestId('user-profile');\n\n// ❌ AVOID\nwrapper.find('.button-class');\n```\n\n### Testing Async Behavior\n\n```typescript\n// ✅ GOOD: waitFor with proper queries\nawait waitFor(() => {\n  expect(screen.getByText('Data loaded')).toBeInTheDocument();\n});\n\n// ✅ GOOD: waitForElementToBeRemoved\nawait waitForElementToBeRemoved(() => screen.queryByText('Loading...'));\n\n// ✅ GOOD: findBy queries (built-in waiting)\nconst element = await screen.findByText('Async content');\n\n// ❌ BAD: Fixed timeouts (flaky tests)\nawait new Promise((resolve) => setTimeout(resolve, 1000));\n```\n\n### Mocking API Calls (MSW)\n\nUse Mock Service Worker for API mocking:\n\n```typescript\nimport { rest } from 'msw';\nimport { setupServer } from 'msw/node';\n\nconst server = setupServer(\n  rest.get('/api/users/:id', (req, res, ctx) => {\n    return res(\n      ctx.json({\n        id: req.params.id,\n        name: faker.person.fullName(),\n      }),\n    );\n  }),\n);\n\nbeforeAll(() => server.listen());\nafterEach(() => server.resetHandlers());\nafterAll(() => server.close());\n```\n\n## Backend Testing (NestJS/Jest)\n\n### Service Test Pattern\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\n// Define factory for User entity\nconst userFactory = Factory.define<User>(() => ({\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n}));\n\ndescribe('UserService', () => {\n  let service: UserService;\n  let repository: Repository<User>;\n\n  const mockRepository = {\n    find: jest.fn(),\n    findOne: jest.fn(),\n    save: jest.fn(),\n    update: jest.fn(),\n    delete: jest.fn(),\n  };\n\n  beforeEach(async () => {\n    const module = await Test.createTestingModule({\n      providers: [\n        UserService,\n        {\n          provide: getRepositoryToken(User),\n          useValue: mockRepository,\n        },\n      ],\n    }).compile();\n\n    service = module.get<UserService>(UserService);\n    repository = module.get<Repository<User>>(getRepositoryToken(User));\n  });\n\n  afterEach(() => {\n    jest.clearAllMocks();\n  });\n\n  it('should return user when found', async () => {\n    const mockUser = userFactory.build();\n    mockRepository.findOne.mockResolvedValue(mockUser);\n\n    const result = await service.findById(mockUser.id);\n\n    expect(result).toEqual(mockUser);\n  });\n});\n```\n\n### Controller Test Pattern\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nconst userFactory = Factory.define<User>(() => ({\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n}));\n\ndescribe('UserController', () => {\n  let controller: UserController;\n  let service: UserService;\n\n  const mockService = {\n    findAll: jest.fn(),\n    findById: jest.fn(),\n    create: jest.fn(),\n  };\n\n  beforeEach(async () => {\n    const module = await Test.createTestingModule({\n      controllers: [UserController],\n      providers: [{ provide: UserService, useValue: mockService }],\n    }).compile();\n\n    controller = module.get<UserController>(UserController);\n  });\n\n  it('should return user from service', async () => {\n    const mockUser = userFactory.build();\n    mockService.findById.mockResolvedValue(mockUser);\n\n    const result = await controller.findById(mockUser.id);\n\n    expect(result).toEqual(mockUser);\n  });\n});\n```\n\n### Integration Tests (E2E)\n\n```typescript\ndescribe('UserController (e2e)', () => {\n  let app: INestApplication;\n\n  beforeAll(async () => {\n    const module = await Test.createTestingModule({\n      imports: [AppModule],\n    }).compile();\n\n    app = module.createNestApplication();\n    await app.init();\n  });\n\n  afterAll(async () => {\n    await app.close();\n  });\n\n  it('/users (GET)', () => {\n    return request(app.getHttpServer())\n      .get('/users')\n      .expect(200)\n      .expect((res) => {\n        expect(Array.isArray(res.body)).toBe(true);\n      });\n  });\n});\n```\n\n## E2E Testing (Playwright)\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nconst userDataFactory = Factory.define(() => ({\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n}));\n\ntest.describe('User Management', () => {\n  test('should create new user', async ({ page }) => {\n    const userData = userDataFactory.build();\n\n    await page.goto('/users');\n    await page.click('text=Add User');\n    await page.fill('[name=\"name\"]', userData.name);\n    await page.fill('[name=\"email\"]', userData.email);\n    await page.click('text=Submit');\n\n    // ✅ Use proper waits\n    await expect(page.locator(`text=${userData.name}`)).toBeVisible();\n  });\n});\n```\n\n## Best Practices\n\n### Always Use Faker for Test Data\n\n```typescript\n// ✅ GOOD: Use faker\nconst user = {\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n  age: faker.number.int({ min: 18, max: 100 }),\n};\n\n// ❌ BAD: Hardcoded data\nconst user = { id: '123', name: 'Test User' };\n```\n\n### Use Factories Instead of Static Mocks\n\n**Use Fishery** for creating test data factories with sensible defaults and overrides.\n\n**CRITICAL — UI factories live in `redisinsight/ui/src/mocks/factories/`**, organized by\ndomain (`<domain>/<TypeName>.factory.ts`), exported as `<TypeName>Factory`. Before writing\ntest data in a UI spec, **check for an existing factory there and reuse it**; if none fits,\n**add a new factory file in that directory** rather than inlining mocks in the spec.\n\n```typescript\n// ✅ GOOD: reuse the shared factory (redisinsight/ui/src/mocks/factories/database/DBInstance.factory.ts)\nimport { DBInstanceFactory } from 'uiSrc/mocks/factories/database/DBInstance.factory';\n\nconst instance = DBInstanceFactory.build();\nconst cluster = DBInstanceFactory.build({\n  connectionType: ConnectionType.Cluster,\n});\nconst many = DBInstanceFactory.buildList(5);\n\n// ✅ GOOD: a new factory file, when none exists yet\n// redisinsight/ui/src/mocks/factories/<domain>/<TypeName>.factory.ts\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nconst userFactory = Factory.define<User>(({ sequence }) => ({\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n  age: faker.number.int({ min: 18, max: 100 }),\n}));\n\n// Usage - flexible and reusable\nconst user1 = userFactory.build();\nconst user2 = userFactory.build({ age: 25 });\nconst user3 = userFactory.build({ name: 'Specific Name' });\nconst users = userFactory.buildList(5); // Create multiple\n\n// ❌ BAD: Static mock objects\nconst mockUser1 = {\n  id: '123',\n  name: 'User 1',\n  email: 'user1@test.com',\n  age: 30,\n};\n```\n\nBenefits of Fishery factories:\n\n- Easy to override specific properties per test\n- Consistent default values across tests\n- Single source of truth for mock structure\n- Better maintainability when types change\n- Built-in support for sequences and traits\n\n### Never Use Fixed Timeouts\n\n```typescript\n// ❌ BAD: Fixed timeout\nawait new Promise((resolve) => setTimeout(resolve, 1000));\nawait page.waitForTimeout(2000);\n\n// ✅ GOOD: Wait for condition\nawait waitFor(() => {\n  expect(element).toBeInTheDocument();\n});\n\nawait page.waitForSelector('[data-test=\"result\"]');\n```\n\n### Mock External Dependencies\n\n```typescript\n// ✅ GOOD: Mock services\njest.mock('uiSrc/services/api', () => ({\n  apiService: {\n    get: jest.fn(),\n    post: jest.fn(),\n  },\n}));\n```\n\n### Parameterized Tests with `it.each`\n\n**Use `it.each` for multiple tests with the same body but different inputs:**\n\n```typescript\n// ✅ GOOD: Parameterized tests\nit.each([\n  { description: 'null', value: null },\n  { description: 'undefined', value: undefined },\n  { description: 'empty string', value: '' },\n  { description: 'whitespace only', value: '   ' },\n])('should return error when input is $description', async ({ value }) => {\n  const result = await service.processInput(value);\n  expect(result.status).toBe('error');\n});\n```\n\n**Benefits:**\n\n- DRY: Single test body shared across all cases\n- Maintainability: Changes to test logic only need to be made once\n- Readability: Test cases are clearly defined in a table\n- Easier to extend: Adding new test cases is just adding a new row\n\n### Test Edge Cases\n\nAlways test:\n\n- Empty arrays/objects\n- Null/undefined values\n- Error scenarios\n- Boundary conditions\n- Loading states\n\n## Testing Checklist\n\n- [ ] All new features have tests\n- [ ] Tests use faker for data generation\n- [ ] No fixed timeouts (use waitFor)\n- [ ] Tests follow AAA pattern\n- [ ] Descriptive test names\n- [ ] Shared `renderComponent` helper used\n- [ ] Default props defined\n- [ ] Edge cases covered\n- [ ] Error scenarios tested\n- [ ] Mocks cleaned up between tests\n- [ ] Integration tests for API endpoints\n- [ ] E2E tests for critical flows\n- [ ] Coverage meets thresholds (80%+)\n","frontmatter":{"name":"testing","description":"Unit/integration testing standards for RedisInsight using Jest and Testing Library: test structure, the `renderComponent` helper, faker for test data, mocking patterns, and `waitFor` instead of fixed time waits. Use when writing or modifying any `*.spec.ts` or `*.spec.tsx` file, when adding component or slice tests, when debugging flaky tests, or when the user mentions jest, testing library, faker, or test patterns."},"isInternal":false,"tokens":3089,"sizeBytes":13142},{"name":"SKILL.md","path":".ai/skills/tsconfigs/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/tsconfigs/SKILL.md","title":"tsconfigs","category":"anthropic-skill","format":"markdown","content":"---\nname: tsconfigs\ndescription: >-\n  Locate and modify TypeScript configuration in RedisInsight. Use when adding\n  path aliases, introducing a new TS area, debugging webpack/ts-node/ESLint\n  path resolution, or the user asks about tsconfig, path mappings, or where\n  TypeScript is configured.\n---\n\n# TypeScript Configuration\n\nRedisInsight has no root `tsconfig.json`. Config is split per area, each owning its own paths, includes, and consumers. ESLint uses `parserOptions.project: true` to auto-discover the nearest tsconfig for each linted file.\n\n## Layout\n\n| File | Owns | Consumers |\n| - | - | - |\n| `redisinsight/ui/tsconfig.json` | UI source, `uiSrc/*`, `apiClient` paths | Vite (UI build), ESLint UI override, `npm run type-check:ui` |\n| `redisinsight/api/tsconfig.json` | API source, `src/*`, `tests/*` paths | NestJS build, ESLint API override |\n| `redisinsight/api/tsconfig.check.json` | Same as base + `strict: true` (with `strictPropertyInitialization` and `useUnknownInCatchVariables` off) and `noEmit: true` | `npm run type-check:api` only — kept separate so strict mode doesn't break `nest build`. See the `type-check-baselines` skill. |\n| `redisinsight/desktop/tsconfig.json` | Desktop source. Paths `desktopSrc/*`, `apiSrc/*`, `uiSrc/*`, `apiClient`, `apiClient/*` for TypeScript / IDE intellisense | ESLint for desktop files, TS language server |\n| `configs/tsconfig.json` | Compiler options (`module: CommonJS`, `esModuleInterop`) used by `ts-node` to load the `.ts` webpack configs | `ts-node` via `TS_NODE_PROJECT` set in `build:main` / `build:main:stage` / `build:stage` |\n| `.storybook/tsconfig.json` | Storybook framework files, extends UI tsconfig | Storybook + ESLint |\n| `stories/tsconfig.json` | Story files. Extends UI tsconfig; rewires `uiSrc/*` to resolve from `redisinsight/ui` | Storybook + ESLint |\n| `tests/e2e-playwright/tsconfig.json` | Playwright E2E suite | E2E runner (sub-project with own package.json, eslint-ignored at root) |\n\n## Webpack path resolution (`configs/webpack.config.base.ts`)\n\nWebpack uses explicit `resolve.alias` entries (not `tsconfig-paths-webpack-plugin`):\n\n```ts\nalias: {\n  desktopSrc: webpackPaths.desktopSrcPath,\n  apiSrc: resolve(webpackPaths.apiPath, 'src'),\n  uiSrc: webpackPaths.uiSrcPath,\n  apiClient: resolve(webpackPaths.riPath, 'api-client'),\n  // ...\n}\n```\n\nIf you add a new path alias used by desktop code, add it in **both** places: `redisinsight/desktop/tsconfig.json` (for TypeScript / ESLint / IDE) and `configs/webpack.config.base.ts` `resolve.alias` (for the electron bundle).\n\n## ts-node and webpack TS configs\n\nThe webpack configs in `configs/` are `.ts` files with type annotations. When `webpack --config ./configs/*.ts` runs, `ts-node` compiles them. Three package.json scripts set `TS_NODE_PROJECT=./configs/tsconfig.json` so ts-node uses the right compiler options:\n\n- `build:main`\n- `build:main:stage`\n- `build:stage`\n\n`configs/tsconfig.json` deliberately does not set `allowJs`. Any `.js` file inside `scripts/` or `configs/` must be CommonJS (`require` / `module.exports`) — Node 22's module syntax detection will treat ESM-syntax `.js` files as ESM and break references to `__dirname`. Two scripts converted for this reason: `scripts/prebuild.js`, `scripts/DeleteSourceMaps.js`.\n\n## ESLint\n\n`.eslintrc.js`:\n\n- Root `parserOptions.project: true` — auto-discovers the nearest tsconfig per file.\n- API override: `parserOptions.project: redisinsight/api/tsconfig.json` (explicit).\n- UI override: `parserOptions.project: redisinsight/ui/tsconfig.json` (explicit).\n- Files in `.storybook/`, `.github/`, etc. are not linted by `eslint .` because ESLint skips dot-prefixed paths during directory expansion.\n\nIf you add a top-level TS folder that ESLint will reach, drop a tsconfig in it or `parserOptions.project: true` will fail to find one.\n\n## Common tasks\n\n### Adding a path alias used by desktop / electron\n\n1. Add the alias to `redisinsight/desktop/tsconfig.json` `compilerOptions.paths`.\n2. If the alias is also used by UI code, add it to `redisinsight/ui/tsconfig.json`.\n3. If the alias is also used by API code, add it to `redisinsight/api/tsconfig.json`.\n4. For aliases used by the **electron bundle** (anything imported from `redisinsight/desktop/**`), also mirror the alias into `configs/webpack.config.base.ts` `resolve.alias`. Vite resolves UI aliases from `redisinsight/ui/tsconfig.json` automatically.\n\n### Adding a new top-level TS folder\n\nCreate a `tsconfig.json` in it. Bare minimum:\n\n```json\n{\n  \"extends\": \"../redisinsight/ui/tsconfig.json\",\n  \"include\": [\"**/*\"]\n}\n```\n\nWithout it, ESLint's `parserOptions.project: true` will error on those files.\n\n### Adding a new `.ts` webpack config under `configs/`\n\nNo action needed — `configs/tsconfig.json` includes `**/*`.\n\n### Adding a Node script that needs to run without ts-node\n\nPlace under `scripts/` as CommonJS (`require`/`module.exports`). Do not use ESM syntax — `configs/tsconfig.json` no longer has `allowJs` and Node 22 will load ESM-syntax `.js` files as ESM (no `__dirname`).\n","frontmatter":{"name":"tsconfigs","description":"Locate and modify TypeScript configuration in RedisInsight. Use when adding path aliases, introducing a new TS area, debugging webpack/ts-node/ESLint path resolution, or the user asks about tsconfig, path mappings, or where TypeScript is configured."},"isInternal":false,"tokens":1310,"sizeBytes":5050},{"name":"SKILL.md","path":".ai/skills/type-check-baselines/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.ai/skills/type-check-baselines/SKILL.md","title":"type-check-baselines","category":"anthropic-skill","format":"markdown","content":"---\nname: type-check-baselines\ndescription: >-\n  Run, refresh, or recover from RedisInsight's per-project TypeScript error\n  baselines (.tscheck.rec.json). Use when CI reports \"baseline is outdated\" or\n  \"more TS errors than previously recorded\", when the user mentions tscheck /\n  type-check / .tscheck.rec.json, when introducing or fixing TS errors, or when\n  reviewing PRs that touch baseline files.\n---\n\n# TS error baselines\n\nRedisInsight gates TypeScript errors per project via a one-way ratchet: current error counts are recorded in `.tscheck.rec.json` files, and CI fails if any (file × error-code) count increases. Counts can only go down.\n\n## Projects and commands\n\n| Project | tsconfig used | Baseline file | Per-project compare |\n| - | - | - | - |\n| UI | `redisinsight/ui/tsconfig.json` | `redisinsight/ui/.tscheck.rec.json` | `npm run type-check --prefix redisinsight/ui` |\n| API | `redisinsight/api/tsconfig.check.json` (strict, extends base) | `redisinsight/api/.tscheck.rec.json` | `npm run type-check --prefix redisinsight/api` |\n| Desktop | `redisinsight/desktop/tsconfig.json` | `redisinsight/desktop/.tscheck.rec.json` | `npm run type-check --prefix redisinsight/desktop` |\n| Configs | `configs/tsconfig.json` | — (must stay at 0 errors) | `npx tsc --project configs/tsconfig.json --noEmit` |\n\nRun all checks together from the repo root:\n\n- `npm run type-check` — compare against baselines (all four projects). E2E Playwright is type-checked by a separate workflow (`tests-e2e-playwright-lint.yml`) — not part of this.\n- `npm run tscheck` — refresh baselines for ui/api/desktop after fixing errors. Projects whose error count didn't change produce no diff.\n- `npm run tscheck:force` — force-overwrite baselines for ui/api/desktop. Emergencies only.\n\n**Always run refresh commands through the root `npm run tscheck` / `npm run tscheck:force` wrappers.** The per-workspace refresh scripts (`npm run tscheck --prefix redisinsight/<ws>`) shell out to `tsc`, `tsx`, and `tsc-output-parser`, which are installed only in the **root** `node_modules/.bin/` — this repo is not an npm workspace, so `npm run --prefix` only exposes the sub-dir's bin, not the root's. The root wrappers exist precisely to avoid that trap by running in the root context first. If you must invoke the per-package script directly, prepend the root bin dir manually: `PATH=\"$PWD/node_modules/.bin:$PATH\" npm run tscheck --prefix redisinsight/ui`.\n\n## API has a dedicated check tsconfig\n\n`redisinsight/api/tsconfig.check.json` extends the base `tsconfig.json` and adds:\n\n```json\n\"strict\": true,\n\"strictPropertyInitialization\": false,\n\"useUnknownInCatchVariables\": false,\n\"noEmit\": true\n```\n\nThe base `tsconfig.json` stays as-is so `nest build` is unaffected. **Never enable `strict` in the base** — it breaks the production build.\n\n## Workflows\n\n### CI says \"more TS errors than previously recorded\"\n\nYou introduced new errors. Fix them. Read the script output — it lists the file, error code (TSxxxx), and message for each new error. Do **not** run `tscheck:force` in any workspace to paper over them.\n\n### CI says \"baseline is outdated\"\n\nYou fixed errors (good). Refresh baselines from the repo root:\n\n```sh\nnpm run tscheck\n```\n\nThis runs the refresh for ui, api, and desktop; only the project whose count changed will produce a diff. Commit the updated `.tscheck.rec.json`.\n\n### Adding a brand-new file with TS errors\n\nSame rule: the file × error-code counts went from 0 to N — that's \"new errors\". Fix them before merging. Strict-mode escape hatches (`as any`, `// @ts-expect-error` with justification) are acceptable when fixing legitimately is out of scope, but prefer real fixes.\n\n### Bootstrapping a fresh baseline\n\nOnly needed once per project (already done for ui/api/desktop). The non-force `npm run tscheck --prefix redisinsight/<workspace>` calls `compare` first, which fails against an empty baseline. Use `npm run tscheck:force --prefix redisinsight/<workspace>` for the very first baseline only.\n\n### After `npm install` in `redisinsight/api/`\n\nThe api postinstall regenerates `redisinsight/api-client/`. That can shift UI and Desktop error counts (they both import from `apiClient`). If `npm run type-check:ui` or `npm run type-check:desktop` reports drift after an api install, refresh those baselines.\n\n### Local UI check disagrees with CI\n\nUI plugins under `redisinsight/ui/src/packages/{redisearch, redisgraph, redistimeseries-app, ri-explain, clients-list}` are sub-projects whose source gets type-checked via the UI tsconfig. Their deps live in nested `node_modules` populated by `npm run build:statics` (or by running `npm install --prefix redisinsight/ui/src/packages/<plugin>`). CI runs `npm run build:statics` before `npm run type-check:ui`, so the baseline reflects \"plugin deps installed.\"\n\nIf `npm run type-check:ui` shows TS7016 (\"Could not find a declaration file for module ...\") errors that CI doesn't, you're missing plugin deps. Run `npm run build:statics` once, then re-run the check. Don't refresh the baseline to your local state — CI runs with plugin deps installed.\n\n### Local Desktop check disagrees with CI\n\nDesktop type-check needs `redisinsight/api/dist/` populated with the **dev** nest build (`npm run build --prefix redisinsight/api`, not `build:prod` — prod skips `.d.ts` emission). CI does this automatically. Locally, build api once before generating or refreshing the desktop baseline.\n\n## Reviewing PRs\n\nReject PRs that:\n\n- Bump a `.tscheck.rec.json` file × code count **up** without a corresponding code fix.\n- Use `tscheck:force` in any workspace (look for the diff: a force overwrite typically touches many lines in the rec file with no related TS changes).\n- Enable `strict` in `redisinsight/api/tsconfig.json` (the base). Strict for api belongs only in `tsconfig.check.json`.\n\nApprove PRs that:\n\n- Leave counts unchanged.\n- Decrease counts (with the rec file updated and committed).\n","frontmatter":{"name":"type-check-baselines","description":"Run, refresh, or recover from RedisInsight's per-project TypeScript error baselines (.tscheck.rec.json). Use when CI reports \"baseline is outdated\" or \"more TS errors than previously recorded\", when the user mentions tscheck / type-check / .tscheck.rec.json, when introducing or fixing TS errors, or when reviewing PRs that touch baseline files."},"isInternal":false,"tokens":1445,"sizeBytes":5950},{"name":"SKILL.md","path":".cursor/skills/branches/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.cursor/skills/branches/SKILL.md","title":"branches","category":"anthropic-skill","format":"markdown","content":"../../../.ai/skills/branches/SKILL.md","isInternal":false,"tokens":11,"sizeBytes":37},{"name":"SKILL.md","path":".cursor/skills/commits/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.cursor/skills/commits/SKILL.md","title":"commits","category":"anthropic-skill","format":"markdown","content":"../../../.ai/skills/commits/SKILL.md","isInternal":false,"tokens":12,"sizeBytes":36},{"name":"SKILL.md","path":".cursor/skills/feature-flags/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.cursor/skills/feature-flags/SKILL.md","title":"feature-flags","category":"anthropic-skill","format":"markdown","content":"../../../.ai/skills/feature-flags/SKILL.md","isInternal":false,"tokens":13,"sizeBytes":42},{"name":"SKILL.md","path":".cursor/skills/pull-requests/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.cursor/skills/pull-requests/SKILL.md","title":"pull-requests","category":"anthropic-skill","format":"markdown","content":"../../../.ai/skills/pull-requests/SKILL.md","isInternal":false,"tokens":13,"sizeBytes":42},{"name":"development.md","path":"docs/plugins/development.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/docs/plugins/development.md","title":"Plugins Skill","category":"anthropic-skill","format":"markdown","content":"# Plugin development\n\nThis document describes the guides to develop your own plugin for the Redis Insight Workbench.\n\n## How it works\n\nPlugin visualization in the Workbench is rendered using Iframe to encapsulate plugin scripts and styles, described in\nthe main plugin script and the stylesheet (if it has been specified in the `package.json`),\niframe includes basic styles as well.\n\n## Plugin structure\n\nEach plugin should have a unique name with all its files [loaded](installation.md) to\na separate folder inside the default `plugins` folder.\n\n> Default plugins are located inside the application.\n\n### Files\n\n`package.json` should be located in the root folder of your plugins, all other files can be included into a subfolder.\n\n- **pluginName/package.json** _(required)_ - Manifest of the plugin\n- **pluginName/{anyName}.js** _(required)_ - Core script of the plugin\n- **pluginName/{anyName}.css** _(optional)_ - File with styles for the plugin visualizations\n- **pluginName/{anyFileOrFolder}** _(optional)_ - Specify any other file or folder inside the plugin folder\n  to use by the core module script. _For example_: pluginName/images/image.png.\n\n## `package.json` structure\n\nThis is the required manifest to use the plugin. `package.json` file should include\nthe following **required** fields:\n\n<table>\n  <tr>\n    <td><i>name</i></td>\n    <td>Plugin name. It is recommended to use the folder name as the plugin name in the package.json.</td>\n  </tr>\n  <tr>\n    <td><i>main</i></td>\n    <td>Relative path to the core script of the plugin. <i>Example: </i> \"./dist/index.js\"</td>\n  </tr>\n  <tr>\n    <td><i>visualizations</i></td>\n    <td>\n      Array of visualizations (objects) to visualize the results in the Workbench.\n      <br><br>\n      Required fields in visualizations:\n      <ul>\n        <li><strong><i>id</i></strong> - visualization id</li>\n        <li><strong><i>name</i></strong> - visualization name to display in the Workbench</li>\n        <li><strong><i>activationMethod</i></strong> - name of the exported function to call when \nthis visualization is selected in the Workbench</li>\n        <li>\n          <strong><i>matchCommands</i></strong> - array of commands to use the visualization for. Supports regex string. \n          <i>Example: </i> [\"CLIENT LIST\", \"FT.*\"]\n        </li>\n      </ul>\n    </td>\n  </tr>\n</table>\n\nYou can specify the path to a css file in the `styles` field. If specified,\nthis file will be included inside the iframe plugin.\n\nSimple example of the `package.json` file with required and optional fields:\n\n```json\n{\n  \"author\": {\n    \"name\": \"Redis Ltd.\",\n    \"email\": \"support@redis.com\",\n    \"url\": \"https://redis.com/redis-enterprise/redis-insight\"\n  },\n  \"description\": \"Show client list as table\",\n  \"styles\": \"./dist/styles.css\",\n  \"main\": \"./dist/index.js\",\n  \"name\": \"client-list\",\n  \"version\": \"0.0.1\",\n  \"scripts\": {},\n  \"visualizations\": [\n    {\n      \"id\": \"clients-list\",\n      \"name\": \"Table\",\n      \"activationMethod\": \"renderClientsList\",\n      \"matchCommands\": [\"CLIENT LIST\"],\n      \"description\": \"Example of client list plugin\",\n      \"default\": true\n    }\n  ],\n  \"devDependencies\": {},\n  \"dependencies\": {}\n}\n```\n\n## Core script of the plugin\n\nThis is the required script with defined visualization methods.\nThe core script contains function and its export (functions - for multiple visualizations),\nwhich is run after the relevant visualization is selected in the Workbench.\n\nThe following function receives props of the executed commands:\n\n```typescript\ninterface Props {\n  command: string; // executed command\n  data: Result[]; // array of results (one item for Standalone)\n}\n\ninterface Result {\n  response: any; // response of the executed command\n  status: 'success' | 'fail'; // response status of the executed command\n}\n\nconst renderVisualization = (props: Props) => {\n  // Do your magic\n};\n\nexport default { renderVisualization };\n```\n\nEach plugin iframe has basic styles of Redis Insight application, including fonts and color schemes.\n\nIt is recommended to use the React & [Elastic UI library](https://elastic.github.io/eui/#/) for\nconsistency with plugin visualisations and the entire application.\n\nFind the example of the plugin here.\n\n- [Client List Plugin README](https://github.com/RedisInsight/Packages/blob/main/clients-list-example/README.md)\n- [Client List Plugin dir](https://github.com/RedisInsight/Packages/blob/main/clients-list-example/)\n\n### Available parameters\n\nAdditional information provided to the plugin iframe is included in the `window.state`\ninside of the plugin script.\n\n```javascript\nconst { config, modules } = window.state;\nconst { baseUrl, appVersion } = config;\n\n// modules - the list of modules of the current database\n// baseUrl - url for your plugin folder - can be used to include your assets\n// appVersion - version of the Redis Insight application\n```\n\n### Plugin rendering\n\nTo render the plugin visualization, the iframe with basic html is generated which is\nthen populated with relevant scripts and styles. To render the html data, use existing\nDOM Element `#app` or create your own DOM Elements.\nRendered iframe also includes `theme_DARK` or `theme_LIGHT` className on `body` to indicate the application theme used.\n\n_Javascript Example:_\n\n```javascript\nconst renderVisualization = (props) => {\n  const { command, data = [] } = props;\n  const [{ result, status }] = data;\n  document.getElementById('app').innerHTML = `\n        <h3>Executed command:<h3>\n        <p>${command}</p>\n        <h4>Result of the command</h4>\n        <p>${result}</p>\n        <h4>Status of the command</h4>\n        <p>${status}</p>\n      `;\n};\n\nexport default { renderVisualization };\n```\n\n_React Example:_\n\n```javascript\nimport { render } from 'react-dom';\nimport App from './App';\n\nconst renderVisualization = (props) => {\n  const { command, data = [] } = props;\n  const [{ result, status }] = data;\n  render(\n    <App command={command} response={result} status={status} />,\n    document.getElementById('app'),\n  );\n};\n\n// This is a required action - export the main function for execution of the visualization\nexport default { renderVisualization };\n```\n\n## Plugin communication\n\nUse the [redisinsight-plugin-sdk](https://www.npmjs.com/package/redisinsight-plugin-sdk), which is a third party library,\nto communicate with the main app.\n\nFind the list and\ndescription of methods called in the\n[README.md](../../redisinsight/ui/src/packages/redisinsight-plugin-sdk/README.md).\n","isInternal":false,"tokens":1537,"sizeBytes":6470},{"name":"installation.md","path":"docs/plugins/installation.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/docs/plugins/installation.md","title":"Plugins Skill","category":"anthropic-skill","format":"markdown","content":"# Plugin installation & Usage\n\nThis document describes the guides to add `plugins` for the Workbench to Redis Insight.\n\n## Installation guide\n\n**Note**: While adding new plugins for Workbench, use files only from trusted\nauthors to avoid automatic execution of malicious code.\n\n1. Download the plugin for the Workbench.\n2. Open the `plugins` folder with the following path\n   - For MacOs: `<usersHomeDir>/.redis-insight/plugins`\n   - For Windows: `C:/Users/{Username}/.redis-insight/plugins`\n   - For Linux: `<usersHomeDir>/.redis-insight/plugins`\n3. Add the folder with plugin to the `plugins` folder\n\nTo see the uploaded plugin visualizations in the command results, reload the Workbench\npage and run Redis command relevant for this visualization.\n\n## Usage\n\nThe plugin may contain different visualizations for any Redis commands.\nBelow you can find a guide to see command results in the uploaded plugin visualization:\n\n1. Open Redis Insight\n2. Open a database added\n3. Open the Workbench\n4. Run the Redis command relevant for the plugin visualization\n5. Select the plugin visualization to display results in (if this visualization has not been set by default)\n","isInternal":false,"tokens":253,"sizeBytes":1163},{"name":"introduction.md","path":"docs/plugins/introduction.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/docs/plugins/introduction.md","title":"Plugins Skill","category":"anthropic-skill","format":"markdown","content":"# Introduction to plugins for the Workbench\n\n## Introduction\n\nRedis can hold a range of different data types. Visualizing these in a\nformat that’s convenient to you for validation and debugging is paramount.\nYou can now easily extend the core functionality of Redis Insight independently by\nbuilding your own custom visualization plugin.\n\nData visualization provided by the plugin is rendered within the\nWorkbench results area and is based on the executed command, ie. a certain\nRedis command can generate its own custom data visualization.\n\nWe have included the following [plugin package example](https://github.com/RedisInsight/RedisInsight/tree/main/redisinsight/ui/src/packages/clients-list) for your reference: running the CLIENT LIST command presents the output in a tabular format for easier reading.\n\n## Wiki\n\n- [Installation and Usage](installation.md)\n- [Plugin Development](development.md)\n","isInternal":false,"tokens":176,"sizeBytes":903},{"name":"test-runner.md","path":".cursor/agents/test-runner.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.cursor/agents/test-runner.md","title":"test-runner","category":"subagent-persona","format":"markdown","content":"---\nname: test-runner\nmodel: composer-1.5\ndescription: Test automation expert. Use proactively to run tests and fix failures.\n---\n\nYou are a test automation expert.\n\nWhen you see code changes, proactively run appropriate tests.\n\nWhen completing a major task, run all tests to ensure the task is complete.\n\nIf tests fail:\n\n1. Analyze the failure output\n2. Identify the root cause\n3. Fix the issue while preserving test intent\n4. Re-run to verify\n\nReport test results with:\n\n- Number of tests passed/failed\n- Summary of any failures\n- Changes made to fix issues\n","frontmatter":{"name":"test-runner","model":"composer-1.5","description":"Test automation expert. Use proactively to run tests and fix failures."},"isInternal":false,"tokens":128,"sizeBytes":560},{"name":"mcp.json","path":".cursor/mcp.json","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/.cursor/mcp.json","title":"Model Context Protocol (MCP) Configuration","category":"mcp-config","format":"json","content":"../mcp.json","isInternal":false,"tokens":4,"sizeBytes":11},{"name":"README.md","path":"redisinsight/ui/src/packages/clients-list/README.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/redisinsight/ui/src/packages/clients-list/README.md","title":"clients-list Documentation","category":"plugin-manifest","format":"markdown","content":"# Plugin for the “Client List” command\n\nThe plugin has been created using React, TypeScript, and [Elastic UI](https://elastic.github.io/eui/#/).\n[Parcel](https://parceljs.org/) is used to build the plugin.\n\n## Running locally\n\nThe following commands will install dependencies and start the server to run the plugin locally:\n\n```\nnpm install\nnpm start\n```\n\nThese commands will install dependencies and start the server.\n\n_Note_: Base styles are included to `index.html`\nfrom [RedisInsight](https://github.com/RedisInsight/RedisInsight) repository.\n\n_From Redis Insight Repo_:\nThis command will generate the `vendor` folder with styles and fonts of the core app. Add this folder\ninside the folder for your plugin and include appropriate styles to the `index.html` file.\n\n```\nnpm install\nnpm install --prefix redisinsight/api\n\nnpm run build:statics - for Linux or MacOs\nnpm run build:statics:win - for Windows\n```\n\n## Build plugin\n\nThe following commands will build plugins to be used in Redis Insight:\n\n```\nnpm install\nnpm run build\n```\n\n[Add](https://github.com/RedisInsight/RedisInsight/blob/main/docs/plugins/installation.md) the package.json file and the\n`dist` folder to the folder with your plugin, which should be located in the `plugins` folder.\n","isInternal":false,"tokens":292,"sizeBytes":1256},{"name":"README.md","path":"redisinsight/ui/src/packages/geodata/README.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/redisinsight/ui/src/packages/geodata/README.md","title":"geodata Documentation","category":"plugin-manifest","format":"markdown","content":"# Geodata Workbench Plugin\n\nInternal RedisInsight Workbench plugin for Redis GEO commands.\n\n## Visualizations\n\n- `Geospatial map` plots coordinate-bearing search results for `GEOSEARCH`, `GEORADIUS`, and `GEORADIUSBYMEMBER`.\n- `Geospatial heatmap` renders the same coordinate-bearing search results as density.\n- `Geospatial details` summarizes every Redis GEO command family, including write and store commands.\n\nThe map views require `WITHCOORD` in command output. The inspector can render scalar, integer, hash, position, search, and search-store responses without coordinates.\n\n`Geospatial map` is the default visualization for coordinate search commands that include `WITHCOORD`. `Geospatial details` is the default for scalar, store, and non-coordinate GEO search commands, and remains selectable for coordinate search commands.\n\n## Tile Policy\n\nThe plugin loads OSM tiles from the runtime default config; fallback renders unavailable state.\n\nFor an official upstream PR, maintainers may still require the default tile provider to be disabled or replaced by a Redis-approved tile backend.\n\n## Development\n\n```sh\nnpm install --prefix redisinsight/ui/src/packages/geodata\nnpm test --prefix redisinsight/ui/src/packages/geodata\nnpm run build --prefix redisinsight/ui/src/packages\n```\n","isInternal":false,"tokens":268,"sizeBytes":1287},{"name":"README.md","path":"redisinsight/ui/src/packages/redisgraph/README.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/redisinsight/ui/src/packages/redisgraph/README.md","title":"redisgraph Documentation","category":"plugin-manifest","format":"markdown","content":"# RedisGraph Plugin for Redis Insight v2\n\nThe example has been created using React, TypeScript, and [Elastic UI](https://elastic.github.io/eui/#/).\n[Parcel](https://parceljs.org/) is used to build the plugin.\n\n## Running locally\n\nThe following commands will install dependencies and start the server to run the plugin locally:\n\n```\nnpm install\nnpm start\n```\n\nThese commands will install dependencies and start the server.\n\n_Note_: Base styles are included to `index.html` from the repository.\n\nThis command will generate the `vendor` folder with styles and fonts of the core app. Add this folder\ninside the folder for your plugin and include appropriate styles to the `index.html` file.\n\n```\nnpm run build:statics - for Linux or MacOs\nnpm run build:statics:win - for Windows\n```\n\n## Build plugin\n\nThe following commands will build plugins to be used in Redis Insight:\n\n```\nnpm install\nnpm run build\n```\n\n[Add](../../../../../docs/plugins/installation.md) the package.json file and the\n`dist` folder to the folder with your plugin, which should be located in the `plugins` folder.\n","isInternal":false,"tokens":245,"sizeBytes":1080},{"name":"README.md","path":"redisinsight/ui/src/packages/redisinsight-plugin-sdk/README.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/redisinsight/ui/src/packages/redisinsight-plugin-sdk/README.md","title":"redisinsight-plugin-sdk Documentation","category":"plugin-manifest","format":"markdown","content":"# RedisInsight-plugin-sdk\n\nThe high-level API for communication between Redis Insight\nplugin and Redis Insight application.\n\n## Usage\n\n```\nnpm install redisinsight-plugin-sdk\nor\nnpm install redisinsight-plugin-sdk\n```\n\n## Available methods\n\n### setHeaderText(text)\n\nSets any custom text to the header of the command result\n\n**Parameters:**\n\n- `text` **{String}**\n\n**Example:**\n\n```js\nimport { setHeaderText } from 'redisinsight-plugin-sdk';\nsetHeaderText('Matched: 10');\n```\n\n### executeRedisCommand(command)\n\nExecutes a Redis command _(currently, only read-only commands are supported)_.\n\n**Parameters:**\n\n- `command` **{String}** - command to execute\n\n**Returns:**\n\n- `Promise<[{ response, status }]>`\n\n```js\n/**\n * @async\n * @param {String} command\n * @returns {Promise.<[{ response, status }]>}\n * @throws {Error}\n */\n```\n\n**Example:**\n\n```js\nimport { executeRedisCommand } from 'redisinsight-plugin-sdk';\ntry {\n  const result = await executeRedisCommand('GET foo');\n  const [{ response, status }] = result;\n  if (status === 'success') {\n    // Do smth\n  }\n} catch (e) {\n  console.error(e);\n}\n```\n\n### getState()\n\nReturns saved state for the command visualization.\n\nThrow an error if the state has not been saved.\n\n**Parameters:**\n\n- `state` **{any}** - any data to save\n\n**Returns:**\n\n- `Promise<any>`\n\n```js\n/**\n * @async\n * @returns {Promise.<any>} state\n * @throws {Error}\n */\n```\n\n**Example:**\n\n```js\nimport { getState } from 'redisinsight-plugin-sdk';\ntry {\n  const result = await getState();\n} catch (e) {\n  console.error(e);\n}\n```\n\n### setState(state)\n\nSave the state for the command visualization.\n\n**Returns:**\n\n- `Promise<any>`\n\n```js\n/**\n * @async\n * @param {any} state\n * @returns {Promise.<any>} state\n * @throws {Error}\n */\n```\n\n**Example:**\n\n```js\nimport { setState } from 'redisinsight-plugin-sdk';\ntry {\n  await setState({ a: 1, b: 2 });\n} catch (e) {\n  console.error(e);\n}\n```\n\n### formatRedisReply(response, command)\n\nUtil function to parse Redis response\n\nReturns string with parsed cli-like response\n\n**Returns:**\n\n- `Promise<string>`\n\n```js\n/**\n * @async\n * @param {any} response\n * @param {String} command\n * @returns {Promise.<string>} data\n * @throws {Error}\n */\n```\n\n**Example:**\n\n```js\nimport { formatRedisReply } from 'redisinsight-plugin-sdk';\n\ntry {\n  const parsedReply = await formatRedisReply(data[0].response, command);\n\n  /*\n    parsedReply:\n    \n    1) 1) \"COUNT(a)\"\n    2) 1) 1) \"0\"\n    3) 1) \"Cached execution: 1\"\n       2) \"Query internal execution time: 3.134125 milliseconds\"\n   */\n} catch (e) {\n  console.error(e);\n}\n```\n","isInternal":false,"tokens":702,"sizeBytes":2567},{"name":"README.md","path":"redisinsight/ui/src/packages/redistimeseries-app/README.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/redisinsight/ui/src/packages/redistimeseries-app/README.md","title":"redistimeseries-app Documentation","category":"plugin-manifest","format":"markdown","content":"# RedisTimeseries Plugin for Redis Insight\n\nThe example has been created using React, TypeScript, and [Elastic UI](https://elastic.github.io/eui/#/).\n[Parcel](https://parceljs.org/) is used to build the plugin.\n\n## Running locally\n\nThe following commands will install dependencies and start the server to run the plugin locally:\n\n```\nnpm install\nnpm start\n```\n\nThese commands will install dependencies and start the server.\n\n_Note_: Base styles are included to `index.html` from the repository.\n\nThis command will generate the `vendor` folder with styles and fonts of the core app. Add this folder\ninside the folder for your plugin and include appropriate styles to the `index.html` file.\n\n```\nnpm run build:statics - for Linux or MacOs\nnpm run build:statics:win - for Windows\n```\n\n## Build plugin\n\nThe following commands will build plugins to be used in Redis Insight:\n\n```\nnpm install\nnpm run build\n```\n\n[Add](../../../../../docs/plugins/installation.md) the package.json file and the\n`dist` folder to the folder with your plugin, which should be located in the `plugins` folder.\n","isInternal":false,"tokens":244,"sizeBytes":1082},{"name":"README.md","path":"redisinsight/ui/src/packages/ri-explain/README.md","rawUrl":"https://raw.githubusercontent.com/redis/RedisInsight/HEAD/redisinsight/ui/src/packages/ri-explain/README.md","title":"ri-explain Documentation","category":"plugin-manifest","format":"markdown","content":"# RI-Explain plugin\n","isInternal":false,"tokens":7,"sizeBytes":20}],"systemPromptSnippet":"<yakaai_skills repo=\"redis/RedisInsight\">\n<!-- File: AGENTS.md (Tokens: ~2029 | Category: root-instruction) -->\n# RedisInsight AI Agent Instructions\n\nThis file provides essential context and instructions for AI coding agents working on RedisInsight.\n\n## Project Overview\n\n**RedisInsight** is a desktop application for Redis database management built with:\n\n- **Frontend**: React 18, TypeScript, Redux Toolkit, styled-components, Monaco Editor, Vite\n- **Backend**: NestJS, TypeScript, Node.js\n- **Desktop**: Electron for cross-platform distribution\n- **Testing**: Jest, Testing Library, Playwright\n\n**Architecture**:\n\n```\nredisinsight/\n├── ui/          # React frontend (Vite + TypeScript)\n├── api/         # NestJS backend (TypeScript)\n├── desktop/     # Electron main process\n└── tests/       # E2E tests (Playwright)\n```\n\n## Setup Commands\n\n### Development\n\n```bash\n# Frontend development (web)\nnpm run dev:ui\n\n# Backend development\nnpm run dev:api\n\n# Desktop app development (runs all: API + UI + Electron)\nnpm run dev:desktop\n\n# Frontend with coverage\nnpm run dev:ui:coverage\n```\n\n## Testing Instructions\n\n### Run Tests\n\n```bash\n# Frontend tests\nnpm test               # Run all UI tests\n\n# Backend tests\nnpm run test:api       # Run all API tests\n\n# E2E tests\nnpm test --prefix tests/e2e-playwright\n```\n\n### Run Specific Frontend Tests\n\n```bash\n# Run a specific test file\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs'\n\n# Run a specific test by name (use -t flag)\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs' -t 'test name pattern'\n\n# Example:\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/slices/tests/browser/keys.spec.ts' -c 'jest.config.cjs' -t 'refreshKeyInfoAction'\n```\n\n### Before Committing\n\n**ALWAYS run these before committing:**\n\n```bash\n# Lint check\nnpm run lint           # All code\nnpm run lint:ui        # Frontend only\nnpm run lint:api       # Backend only\n\n# Type checking (compares against .tscheck.rec.json baselines for ui/api/desktop + configs)\nnpm run type-check\n\n# Refresh baselines after intentionally adding or fixing TS errors (do not run casually)\nnpm run tscheck\n\n# Tests\nnpm test               # Frontend tests\nnpm run test:api       # Backend tests\n```\n\n`npm run type-check` is the gate — CI fails if any (file × error-code) TS-error count increases. If you intentionally changed TS-error counts, run `npm run tscheck` to refresh the baselines and commit the updated `.tscheck.rec.json` files. See `.ai/skills/type-check-baselines/SKILL.md` for details (including the `npm run tscheck:force` escape hatch).\n\n**Fix any linting errors, type errors, or test failures before committing.**\n\n## Always-On Rules\n\nThese apply to every change in the repo. Skill files contain the full detail; the essentials are listed here so they're never missed.\n\n### Code quality (always)\n\n- Run `npm run lint` and `npm run type-check` before committing — both must pass.\n- TypeScript everywhere. Avoid `any`; use `unknown` if you must.\n- Naming: `PascalCase` components, `camelCase` functions/variables, `UPPER_SNAKE_CASE` constants, `is/has/should` prefix for booleans.\n- No `console.log` in production code (use `console.warn`/`error`).\n- Imports: external → built-ins → internal aliases (`uiSrc/*`, `apiClient`, `desktopSrc/*`) → relative → styles last. UI must not import from backend directly — use `apiClient`.\n- No magic numbers; extract duplicated strings to constants.\n\n### Git safety (always)\n\n- **Never commit, push, or force-push directly to `main`, `latest`, or `release/*`.** Always create a feature branch first.\n- Verify current branch with `git branch --show-current` before any push.\n- All changes go through pull requests.\n\n### Dependency / lockfile management (always)\n\n- After modifying any `package.json` (root, `redisinsight/`, `redisinsight/api/`, or a `redisinsight/ui/src/packages/*` plugin), run `npm install` from that directory and commit the resulting `package-lock.json` changes.\n- Never edit `package-lock.json` files by hand and never run `npm install --ignore-scripts` (or otherwise skip `postinstall`) when preparing a commit — the `postinstall` applies `patch-package` patches, and the lockfile shipped to CI must match what `npm install` produces locally.\n- CI runs `npm ci`, which installs strictly from `package-lock.json` and fails if it is out of sync with `package.json`. A green local install in every changed package's directory is required before pushing.\n- Use the right package manager for the change: `npm install <pkg>` / `npm uninstall <pkg>` (or `npm update`) for dependency changes, never manual edits to `package.json` versions without re-running install.\n\n## Skills\n\nAll detailed development standards are exposed as skills under `.ai/skills/`. Claude Code auto-discovers them; each skill triggers when its description matches the task.\n\n- **Code Quality**: `.ai/skills/code-quality/SKILL.md` - Linting, TypeScript standards, naming, import order\n- **Frontend**: `.ai/skills/frontend/SKILL.md` - React, Redux, UI patterns, styled-components\n- **i18n**: `.ai/skills/i18n/SKILL.md` - i18next translations, locale key conventions, translating backend errors and notifications\n- **Backend**: `.ai/skills/backend/SKILL.md` - NestJS, API patterns, dependency injection\n- **Testing**: `.ai/skills/testing/SKILL.md` - Jest/Testing Library standards, faker, test patterns\n- **E2E Testing**: `.ai/skills/e2e-testing/SKILL.md` - Playwright standards, page objects\n- **Git Safety**: `.ai/skills/git-safety/SKILL.md` - Protected-branch guardrails (detail)\n- **Branches**: `.ai/skills/branches/SKILL.md` - Branch naming conventions\n- **Commits**: `.ai/skills/commits/SKILL.md` - Commit message guidelines\n- **Pull Requests**: `.ai/skills/pull-requests/SKILL.md` - PR process and review guidelines\n- **Feature Flags**: `.ai/skills/feature-flags/SKILL.md` - Adding, promoting, and removing feature flags\n- **TS Error Baselines**: `.ai/skills/type-check-baselines/SKILL.md` - Running and refreshing TypeScript error baselines\n- **Redis UI Components**: `.ai/skills/redis-ui-components/SKILL.md` - Component API references, props, and usage examples (from `@redis-ui/components` package)\n- **RedisInsight Plugins**: `.ai/skills/redis-insight-plugin/SKILL.md` - Building, deploying, and validating Workbench visualization plugins (manifests, `activationMethod`, phased workflow, command parsing); internal plugins under `redisinsight/ui/src/packages/` must also follow the Frontend, Code Quality, Redis UI Components, and Testing skills\n\n**Refer to these files for comprehensive guidelines on each topic.**\n\n## Boundaries\n\n### ✅ Always Do\n\n- Ensure the current branch name follows `.ai/skills/branches/SKILL.md` before opening a PR; rename it if it doesn't\n- Write to `src/` and `tests/` directories\n- Run `npm run lint` and `npm test` before commits\n- Follow naming conventions (camelCase, PascalCase, UPPER_SNAKE_CASE)\n- Use faker library for test data generation\n- Use `renderComponent` helper in component tests\n- Extract duplicate strings to constants\n- Use semantic colors from theme (not CSS variables)\n- Use layout components (Row/Col/FlexGroup) instead of div\n- Pass layout props as component props (not hardcoded in styles)\n- Consult the `redis-ui-components` skill (`.ai/skills/redis-ui-components/`) for component APIs when writing frontend code\n- Consult the `redis-insight-plugin` skill (`.ai/skills/redis-insight-plugin/`) when creating or modifying anything under `redisinsight/ui/src/packages/**` or any plugin manifest with a `visualizations` field\n\n### ⚠️ Ask First\n\n- Database schema changes\n- Adding new dependencies\n- Modifying CI/CD configuration (`.github/workflows/`)\n- Changes to build configuration\n- Breaking changes to APIs\n\n### 🚫 Never Do\n\n- Commit secrets or API keys\n- Edit `node_modules/` or `vendor/` directories\n- Edit `package-lock.json` by hand or commit a lockfile produced with `--ignore-scripts` / a skipped `postinstall`\n- Use fixed time waits in tests (use `waitFor` instead)\n- Use `!important` in styled-components\n- Import directly from `@redis-ui/*` (use internal wrappers from `uiSrc/components/ui`)\n- Use Elastic UI for new code (migrating to Redis UI)\n- Use hardcoded pixel values (use theme spacing)\n- Use `any` type without reason\n\n<!-- File: CLAUDE.md (Tokens: ~3 | Category: claude-rule) -->\nAGENTS.md\n\n<!-- File: .github/copilot-instructions.md (Tokens: ~780 | Category: copilot-instructions) -->\n# GitHub Copilot Instructions for RedisInsight\n\n> **🎯 Start here: Read `AGENTS.md` at the repository root for essential commands, testing instructions, and quick reference**\n\nThis project uses a centralized AI rules structure:\n\n- **`AGENTS.md`** (repository root) - Entry point with commands, testing, and boundaries\n- **`.ai/skills/`** - Detailed development standards as skill files (one folder per topic, each with a `SKILL.md`)\n- **`.ai/commands/`** - AI workflow commands and templates\n\n## 📂 Skills Structure\n\n### Core Development Skills\n\n- **Code Quality**: `.ai/skills/code-quality/SKILL.md`\n\n  - TypeScript best practices\n  - Import organization\n  - SonarJS complexity rules\n\n- **Frontend Development**: `.ai/skills/frontend/SKILL.md`\n\n  - React 18 patterns and best practices\n  - Redux Toolkit state management\n  - Styled-components (SCSS deprecated)\n  - Component folder structure\n  - Internal UI component wrappers (never import from @redis-ui directly)\n  - Elastic UI deprecation (use Redis UI wrappers)\n\n- **Backend Development**: `.ai/skills/backend/SKILL.md`\n\n  - NestJS module architecture\n  - Service and controller patterns\n  - DTOs and validation\n  - Error handling\n  - Redis integration patterns\n\n- **Testing Standards**: `.ai/skills/testing/SKILL.md`\n\n  - Jest and Testing Library patterns\n  - Component testing with renderComponent helper\n  - Faker for test data generation\n  - No fixed timeouts (use waitFor)\n  - Backend testing with NestJS\n\n- **E2E Testing**: `.ai/skills/e2e-testing/SKILL.md`\n\n  - Playwright standards, page object models, fixtures\n\n- **Git Safety**: `.ai/skills/git-safety/SKILL.md`\n\n  - Protected-branch guardrails (no direct commits to main/latest/release)\n\n- **Commit Messages**: `.ai/skills/commits/SKILL.md`\n\n  - Commit message format (Conventional Commits)\n\n- **Pull Requests**: `.ai/skills/pull-requests/SKILL.md`\n  - PR process and review guidelines\n  - Pre-commit checklist\n\n### Commands and Workflows\n\n- **PR Plan**: `.ai/commands/pr-plan.md` - Analyze JIRA tickets (RI-XXX) and create detailed implementation plans\n- **Commit Message Generation**: `.ai/commands/commit-message.md` - Generate commit messages following Conventional Commits\n- **PR Review**: `.ai/commands/pull-request-review.md` - Review pull requests and provide feedback\n\n## 🎯 Project Overview\n\n**Tech Stack:**\n\n- Frontend: React 18, TypeScript, Redux Toolkit, styled-components, Vite\n- Backend: NestJS, TypeScript, Node.js\n- Desktop: Electron\n- Testing: Jest, Testing Library, Playwright\n\n**Module Aliases:**\n\n- `uiSrc/*` → `redisinsight/ui/src/*`\n- `apiClient` → `redisinsight/api-client` (auto-generated OpenAPI types consumed by the UI)\n- `desktopSrc/*` → `redisinsight/desktop/src/*`\n\n## 📖 Additional Documentation\n\n- **For AI agents**: Start with `AGENTS.md` at repository root\n- **For human developers**: See `.ai/README.md` for setup and overview\n\n---\n\n**Note**: This is a minimal reference file. GitHub Copilot cannot read the referenced files directly, but developers can access the full guidelines. Claude Code and Codex read `AGENTS.md` directly; Claude Code additionally auto-discovers skills under `.ai/skills/` (Codex via the `.agents/skills` symlink).\n\n<!-- File: .ai/skills/backend/SKILL.md (Tokens: ~1421 | Category: anthropic-skill) -->\n---\nname: backend\ndescription: >-\n  NestJS backend development patterns for the RedisInsight API:\n  module structure, services, controllers, DTOs, dependency injection,\n  and error handling. Use when editing any file under\n  redisinsight/api/**, writing or modifying NestJS modules,\n  controllers, services, DTOs, providers, or when the user mentions\n  the backend, API, or NestJS.\n---\n\n\n# Backend Development (NestJS/API)\n\n## Module Structure\n\n### NestJS Architecture\n\n- Follow **modular architecture** (feature-based modules)\n- Use **dependency injection** throughout\n- **Separate concerns**: Controllers, Services, Repositories\n- Use **DTOs** for validation and data transfer\n- Apply **proper error handling** with NestJS exceptions\n\n### Module Folder Structure\n\nEach feature module in its own directory under `api/src/`:\n\n```\nfeature/\n├── feature.module.ts           # Module definition\n├── feature.controller.ts       # REST endpoints\n├── feature.service.ts          # Business logic\n├── feature.service.spec.ts     # Service tests\n├── feature.controller.spec.ts  # Controller tests\n├── feature.types.ts            # Interfaces and types related to the feature\n├── dto/                        # Data transfer objects\n│   ├── create-feature.dto.ts\n│   ├── update-feature.dto.ts\n│   └── feature.dto.ts\n├── entities/                   # TypeORM entities\n├── repositories/               # Custom repositories\n├── exceptions/                 # Custom exceptions\n├── guards/                     # Feature-specific guards\n├── decorators/                 # Custom decorators\n└── constants/                  # Feature constants\n```\n\n### File Naming\n\n- **Modules**: `feature.module.ts`\n- **Controllers**: `feature.controller.ts`\n- **Services**: `feature.service.ts`\n- **DTOs**: `create-feature.dto.ts`, `update-feature.dto.ts`\n- **Entities**: `feature.entity.ts`\n- **Interfaces and types**: `feature.types.ts`\n- **Tests**: `feature.service.spec.ts`\n- **Constants**: `feature.constants.ts`\n- **Exceptions**: `feature-not-found.exception.ts`\n\n### Constants Organization\n\nStore feature-specific constants in dedicated constants file:\n\n```typescript\nexport const FEATURE_CONSTANTS = {\n  MAX_NAME_LENGTH: 100,\n  DEFAULT_PAGE_SIZE: 20,\n} as const;\n\nexport const FEATURE_ERROR_MESSAGES = {\n  NOT_FOUND: 'Feature not found',\n  INVALID_INPUT: 'Invalid feature data',\n} as const;\n```\n\n### Imports Order\n\n1. Node.js built-in modules\n2. External dependencies (`@nestjs/*`, etc.)\n3. Internal modules (using `src/*` alias — the backend's self-reference into `redisinsight/api/src/*`)\n4. Local relative imports\n\n## Service Layer\n\n### Service Pattern\n\n- Inject dependencies via constructor\n- Use TypeORM repositories\n- Handle errors with NestJS exceptions\n- Use Logger for important operations\n- Keep business logic in services (not controllers)\n\n### Dependency Injection\n\nAlways inject dependencies via constructor with proper decorators:\n\n```typescript\n@Injectable()\nexport class UserService {\n  constructor(\n    @InjectRepository(User)\n    private readonly userRepository: Repository<User>,\n    private readonly emailService: EmailService,\n  ) {}\n}\n```\n\n## Controller Layer\n\n### Controller Pattern\n\n- Keep controllers thin (delegate to services)\n- Use proper HTTP decorators (`@Get`, `@Post`, etc.)\n- Use `@Body`, `@Param`, `@Query` for inputs\n- Apply guards with `@UseGuards()`\n- Document with Swagger decorators\n\n### HTTP Status Codes\n\n- Use `@HttpCode()` decorator for non-standard codes\n- Return appropriate status codes (200, 201, 204, 400, 404, etc.)\n\n## Data Transfer Objects (DTOs)\n\n### Validation\n\nUse `class-validator` decorators for validation:\n\n- `@IsString()`, `@IsNumber()`, `@IsEmail()`\n- `@IsNotEmpty()`, `@IsOptional()`\n- `@MinLength()`, `@MaxLength()`\n- `@Min()`, `@Max()`\n\n### Swagger Documentation\n\nUse `@ApiProperty()` and `@ApiPropertyOptional()` for Swagger docs.\n\n## Error Handling\n\n### NestJS Exceptions\n\nUse appropriate exception types:\n\n- `NotFoundException` - 404\n- `BadRequestException` - 400\n- `UnauthorizedException` - 401\n- `ForbiddenException` - 403\n- `ConflictException` - 409\n- `InternalServerErrorException` - 500\n\n### Custom Exceptions\n\n**Prefer custom exceptions over generic ones** when you need:\n\n- Consistent error codes for frontend handling\n- Specific error messages from constants\n- Consistent error structure across the codebase\n\nCreate custom exceptions following existing patterns:\n\n```typescript\n// src/modules/feature/exceptions/feature-invalid.exception.ts\nimport {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class FeatureInvalidException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.FEATURE_INVALID,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'FeatureInvalid',\n      errorCode: CustomErrorCodes.FeatureInvalid,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n```\n\n### Error Logging\n\n```typescript\nprivate readonly logger = new Logger(ServiceName.name)\n\nthis.logger.error('Error message', error.stack, { context })\n```\n\n## Redis Integration\n\n### Redis Service Pattern\n\n- Use RedisClient from `src/modules/redis`\n- Handle errors gracefully\n- Log Redis operations\n- Use try-catch for error handling\n\n## Code Quality\n\n### Cognitive Complexity (≤ 15)\n\n- Use early returns to reduce nesting\n- Extract complex logic to separate functions\n- Avoid deeply nested conditions\n\n### No Duplicate Strings\n\nExtract repeated strings to constants in constants file.\n\n## API Documentation (Swagger)\n\n### Required Decorators\n\n- `@ApiTags()` - Group endpoints\n- `@ApiOperation()` - Describe operation\n- `@ApiResponse()` - Document responses\n- `@ApiParam()` - Document params\n- `@ApiQuery()` - Document query params\n- `@ApiBearerAuth()` - Auth requirement\n\n## Checklist\n\n- [ ] Services use dependency injection\n- [ ] DTOs have validation decorators\n- [ ] Controllers have Swagger documentation\n- [ ] Proper HTTP status codes used\n- [ ] Error handling with appropriate exceptions\n- [ ] Logging for important operations\n- [ ] Transactions for related DB operations\n- [ ] Configuration via ConfigService\n- [ ] Guards for authentication/authorization\n- [ ] Cognitive complexity ≤ 15\n\n<!-- File: .ai/skills/branches/SKILL.md (Tokens: ~473 | Category: anthropic-skill) -->\n---\nname: branches\ndescription: >-\n  Create and name git branches following project conventions. Use when\n  creating branches, checking out new branches, or the user mentions\n  branch naming.\n---\n\n# Branch Naming Conventions\n\nUse lowercase kebab-case with type prefix and issue/ticket identifier.\n\n```bash\n# Pattern: <type>/<issue-ref>/<short-title>\n\n# INTERNAL (JIRA - RI-XXX)\nfeature/RI-123/add-user-profile\nbugfix/RI-789/memory-leak\nfe/feature/RI-567/add-dark-mode\nbe/bugfix/RI-345/fix-redis-connection\ndocs/RI-333/update-docs\ntest/RI-444/add-unit-tests\ne2e/RI-555/add-integration-tests\n\n# OPEN SOURCE (GitHub - XXX)\nfeature/123/add-export-feature\nbugfix/789/fix-connection-timeout\n\n# Special branches\nrelease/v2.0.0\nric/RI-666/custom-prefix\n```\n\n## Branch Types\n\n- `feature/` - New features and refactoring (affects multiple areas)\n- `bugfix/` - Bug fixes (affects multiple areas)\n- `fe/feature/` - Frontend-only features (only `redisinsight/ui/` folder)\n- `fe/bugfix/` - Frontend-only bug fixes (only `redisinsight/ui/` folder)\n- `be/feature/` - Backend-only features (only `redisinsight/api/` folder)\n- `be/bugfix/` - Backend-only bug fixes (only `redisinsight/api/` folder)\n- `docs/` - Documentation changes\n- `test/` - Test-related changes\n- `e2e/` - End-to-end test changes\n- `release/` - Release branches\n- `ric/` - Custom prefix for special cases\n\n**Note:** When a bug fix affects only the `redisinsight/ui/` folder, use `fe/bugfix/` prefix instead of `bugfix/`.\n\n## Issue References\n\n- **Internal**: `RI-XXX` (JIRA ticket)\n- **Open Source**: `XXX` (GitHub issue number)\n- Use `#` only in commit messages, not branch names\n\n<!-- File: .ai/skills/code-quality/SKILL.md (Tokens: ~1175 | Category: anthropic-skill) -->\n---\nname: code-quality\ndescription: >-\n  Code-quality standards for RedisInsight: TypeScript strictness,\n  naming conventions (camelCase, PascalCase, UPPER_SNAKE_CASE),\n  linting rules, no `any` without reason, no `!important` in styles,\n  and constant extraction. Use when writing or refactoring any\n  TypeScript/JavaScript code in this repo, when ESLint or Prettier\n  issues come up, or when the user mentions code style, lint, naming\n  conventions, or general code quality.\n---\n\n\n# Code Quality Standards\n\n## Critical Rules\n\n- **ALWAYS run linter** after code changes: `npm run lint`\n- Linter must pass before committing\n- No console.log in production code (use console.warn/error only)\n\n## TypeScript Standards\n\n### Essential Rules\n\n- Use TypeScript for all new code\n- **Avoid `any`** - use proper types or `unknown`\n- **Prefer interfaces** for object shapes\n- Use **type** for unions, intersections, primitives\n- Add explicit return types for non-obvious functions\n- Leverage type inference where clear\n\n## Import Organization\n\n### Required Order (enforced by ESLint)\n\n1. External libraries (`react`, `lodash`, etc.)\n2. Built-in Node modules (`path`, `fs` - backend only)\n3. Internal modules with aliases (`uiSrc/*`, `apiClient`)\n4. Sibling/parent relative imports\n5. Style imports (ALWAYS LAST)\n\n### Module Aliases\n\n- `uiSrc/*` → `redisinsight/ui/src/*` (UI workspace)\n- `apiClient` → `redisinsight/api-client` (auto-generated OpenAPI types — the UI's only entry point into BE-defined shapes)\n- `desktopSrc/*` → `redisinsight/desktop/src/*` (desktop workspace)\n\nThe UI workspace must not import from the backend codebase directly. Use `apiClient` for types and the existing service layer (`uiSrc/services`) for HTTP calls.\n\n✅ **Use aliases**: `import { Button } from 'uiSrc/components/Button'`  \n❌ **Avoid relative**: `import { Button } from '../../../ui/src/components/Button'`\n\n## Naming Conventions\n\n- **Components**: `PascalCase` - `UserProfile`\n- **Functions/Variables**: `camelCase` - `fetchUserProfile`\n- **Constants**: `UPPER_SNAKE_CASE` - `MAX_RETRY_ATTEMPTS`\n- **Booleans**: Use `is/has/should` prefix - `isLoading`, `hasError`\n\n## Comments\n\n**Default to fewer comments.** Clear names, small functions, and good\ntests should carry the meaning. Write a comment only when the code\ngenuinely can't explain itself — or when the user explicitly asks for\nmore (for example, on a piece of complex logic).\n\nWhen you do write one, keep it short and use plain words to say\n**what** the code is doing or **why** it has to exist — not how it\nworks under the hood, and not the story of why you made the change\n(that belongs in the commit message or PR description).\n\n✅ **Good** — plain, names the situation:\n\n```ts\n// When switching keys, hide the previous key's loader/error/result\n// until the new key is ready.\nconst showLoader = isArrayKeyReady && loading\n```\n\n❌ **Avoid** — long, mechanism-heavy, re-derives what the code shows:\n\n```ts\n// Gate every aggregate-slice surface (loader, error, result) on\n// `isArrayKeyReady`: when the user switches keys, `keyProp` flips\n// immediately but `selectedKeyData` lags by a round-trip, so the\n// prior key's slice state would otherwise paint under the newly\n// selected (or empty) key for one frame before the hook's reset\n// effect fires.\nconst showLoader = isArrayKeyReady && loading\n```\n\nAlso avoid:\n\n- Restating identifier names in prose (`// set loading to true`).\n- Comments that duplicate what a clearly-named test already asserts.\n- Block comments that summarize obvious blocks (`// loop over items`).\n\n## SonarJS Rules\n\n- Keep cognitive complexity low (refactor complex functions)\n- Extract duplicate strings to constants\n- Follow DRY principle - no duplicate code\n- Use immediate return (avoid unnecessary intermediate variables)\n\n## Best Practices\n\n- Use destructuring for objects and arrays\n- Use template literals over string concatenation\n- Use `const` by default, `let` only when reassignment needed\n- Use descriptive variable names\n- Handle errors properly\n- Clean up subscriptions and timers\n- Use constants instead of magic numbers\n\n## Vite Cache Management\n\nWhen updating npm packages (especially `@redis-ui/*` packages):\n\n1. **Clear Vite cache** after `npm install`:\n\n   ```bash\n   rm -rf node_modules/.vite\n   rm -rf redisinsight/ui/node_modules/.vite\n   ```\n\n2. **Restart dev server** to rebuild dependencies\n\n3. This ensures new package versions are properly loaded\n\n## Pre-Commit Checklist\n\n- [ ] `npm run lint` passes\n- [ ] No TypeScript errors\n- [ ] Import order is correct\n- [ ] No `any` types without reason\n- [ ] No console.log statements\n- [ ] No magic numbers\n- [ ] Descriptive variable names\n- [ ] Low cognitive complexity\n- [ ] No duplicate code\n- [ ] Comments kept to a minimum; any present are short and plain-language\n- [ ] Vite cache cleared (if updated dependencies)\n\n<!-- File: .ai/skills/commits/SKILL.md (Tokens: ~461 | Category: anthropic-skill) -->\n---\nname: commits\ndescription: >-\n  Generate commit messages following Conventional Commits format. Use when\n  creating commits, writing commit messages, or the user asks to commit changes.\n---\n\n# Commit Message Guidelines\n\nFollow **Conventional Commits** format:\n\n```\n<type>(<scope>): <subject>\n\n<body>\n\n<footer>\n```\n\n## Commit Types\n\n- `feat` - New feature\n- `fix` - Bug fix\n- `refactor` - Code refactoring\n- `test` - Adding or updating tests\n- `docs` - Documentation changes\n- `chore` - Maintenance tasks\n- `style` - Code style changes (formatting)\n- `perf` - Performance improvements\n- `ci` - CI/CD changes\n\n## Examples\n\n```bash\nfeat(ui): add user search functionality\n\nImplements real-time search with debouncing.\n\nReferences: #RI-123\n\n---\n\nfix(api): resolve memory leak in connection pool\n\nProperly cleanup subscriptions on unmount.\n\nFixes #456\n\n---\n\ntest(ui): add tests for data serialization\n\nrefactor(api): extract common validation logic\n\ndocs: update API endpoint documentation\n\nchore: upgrade React to version 18.2\n```\n\n## Best Practices\n\n### ✅ Good Commits\n\n- Clear, descriptive subject line\n- Atomic changes (one logical change per commit)\n- Reference issue/ticket in body\n- Explain **why**, not just **what**\n- **Keep it concise** - Don't list every file change in the body\n\n```bash\nfeat(ui): add user profile editing\n\nAllows users to update their profile information including\nname, email, and avatar. Includes validation and error handling.\n\nReferences: #RI-123\n```\n\n### ❌ Bad Commits\n\n```bash\n# Too vague\nfix stuff\nWIP\nupdate\n\n# Too broad\nadd feature, fix bugs, refactor code, update tests\n```\n\n## Issue References\n\n- **JIRA (internal)**: `References: #RI-123` or `Fixes #RI-123`\n- **GitHub (open source)**: `Fixes #456` or `Closes #456`\n- Use `#` for auto-linking in commit messages\n\n<!-- File: .ai/skills/dead-dependencies/SKILL.md (Tokens: ~1527 | Category: anthropic-skill) -->\n---\nname: dead-dependencies\ndescription: >-\n  Find and safely remove unused (\"dead\") npm dependencies in RedisInsight\n  using a grep + leaf-check + build-gate recipe. Use when cleaning up\n  dependencies, investigating whether a package is still used, removing a\n  suspected leftover, or when the user mentions dead deps, unused\n  dependencies, dependency cleanup, leftover packages, or \"is this safe to\n  remove\". Complements the weekly vulnerability audit\n  (`scripts/dependency-audit-report.mjs`), which only reports vulnerabilities.\n---\n\n# Dead Dependencies\n\nIdentify and safely remove unused npm dependencies. This is a **local,\ninteractive** workflow — a scheduled report can only ever guess; confirming a\ndependency is dead requires grepping real usage and running the build.\n\n## Why manual (not knip/depcheck)\n\nStatic tools over-report badly in this repo and must not be trusted blindly:\n\n- The root `package.json` is a **mega-manifest** (UI + desktop + build + test\n  + storybook + electron), so a tool scanning one area flags everything used\n  elsewhere as \"unused\".\n- Lots of dependencies are referenced **without an `import`**: webpack loaders\n  and eslint/jest/babel plugins by string in config, tools invoked from\n  `package.json` `scripts`, runtime `-r` preloads, dynamic `require`, and\n  ambient `@types/*`.\n- The webpack→Vite migration left **genuinely dead** build tooling behind,\n  mixed in with false positives.\n\nThe reliable signal is: **grep for usage → confirm it's a leaf → remove it and\nrun the build gate.** That is exactly how `jsonpath` was confirmed dead and\nremoved (worked example at the bottom).\n\n## The recipe (per candidate)\n\n### 1. Grep the whole repo for real usage\n\n```bash\nPKG=jsonpath   # the dependency to check\ngrep -rn \"$PKG\" \\\n  redisinsight configs scripts tests .storybook \\\n  --include=*.ts --include=*.tsx --include=*.js --include=*.jsx \\\n  --include=*.mjs --include=*.cjs --include=*.json \\\n  2>/dev/null | grep -v node_modules\n```\n\nLook for `import ... from '$PKG'`, `require('$PKG')`, `import('$PKG')`, and\nbare references. Ignore unrelated substring hits (e.g. `nestjs-form-data` when\nchecking `form-data`, or a package name appearing only in tutorial/`manifest`\ntext). **Zero real references → candidate for the next steps.**\n\n### 2. Rule out no-import usage\n\nA clean grep is **necessary but not sufficient**. Check the ways a package is\nused without an `import`:\n\n- **Config string references** — search the build/test config for the bare\n  name: `.eslintrc.js`, `configs/webpack.config.*.ts`,\n  `redisinsight/ui/vite.config.mjs`, `jest.config.cjs`, `babel.config.cjs`,\n  `electron-builder.json`, `.mocharc*`. eslint plugins, webpack loaders, and\n  jest/mocha reporters live here.\n- **`package.json` scripts** — a tool like `concurrently`, `lint-staged`, or a\n  reporter is \"used\" if a `scripts` entry (any workspace) invokes it.\n- **Runtime preloads / dynamic** — `node -r <pkg>`, `require(variable)`, or a\n  wasm/worker loader with a computed path.\n\n### 3. Leaf check\n\n```bash\nnpm ls \"$PKG\"        # in the workspace that declares it\n```\n\nConfirm nothing else in the tree depends on it, and note any transitive deps it\nuniquely pulls (removing `jsonpath` also dropped `underscore`).\n\n### 4. Classify before acting\n\n| Situation | Action |\n| --- | --- |\n| Plain runtime/dev dep, zero references anywhere | **Delete** (after the gate below) |\n| `@types/x` where base `x` bundles its own types (`node_modules/x/package.json` has `types`/`typings`) | **Delete** — the DefinitelyTyped package is obsolete |\n| `@types/x` that's ambient/global-only (e.g. `@types/webpack-env`) | **Keep** — never imported by design |\n| Declared in more than one workspace (`package.json`), used in only one | **Relocate/dedupe** — remove the *unused* declaration, never the used copy |\n| Shadows a Node builtin (`buffer`, `assert`, …) | **Keep** — usually a false positive |\n| Referenced only in a config/`scripts` (step 2 hit) | **Keep** |\n\n### 5. The gate — the only real proof\n\nRemove it, reinstall (per the repo's dependency rules — never hand-edit\n`package.json`/lockfile, never `--ignore-scripts`), and verify the affected\narea builds:\n\n```bash\nnpm uninstall \"$PKG\"          # in the declaring workspace; updates the lockfile\nnpm run type-check            # or the affected workspace's type-check\nnpm run test                  # / test:api, as relevant\nnpm run build                 # if it's a build-time dep\n```\n\nGreen across the relevant checks = safe. Commit the `package.json` +\n`package-lock.json` change (see the git-safety / dependency rules in\n`CLAUDE.md`). If anything goes red, it wasn't dead — restore it.\n\n## Optional: enumerate candidates to sweep\n\nTo triage the whole surface rather than one package, list declared deps and\ngrep each for an import, then apply steps 2–5 to the ones with zero hits:\n\n```bash\nnode -e \"const p=require('./package.json');console.log([...Object.keys(p.dependencies||{}),...Object.keys(p.devDependencies||{})].join('\\n'))\" \\\n| while read PKG; do\n    hits=$(grep -rl --include=*.ts --include=*.tsx --include=*.js --include=*.jsx --include=*.mjs \\\n      -e \"from ['\\\"]$PKG\" -e \"require(['\\\"]$PKG\" redisinsight configs scripts tests 2>/dev/null | grep -vc node_modules)\n    [ \"$hits\" = \"0\" ] && echo \"candidate: $PKG\"\n  done\n```\n\nThis is a **first filter only** — every candidate still goes through steps 2–5.\nExpect false positives (config/string/dynamic/ambient use). Never delete straight\nfrom this list.\n\n## Worked example — `jsonpath`\n\n1. Grep across the repo → the only hits were unrelated tutorial text in a\n   `manifest.json`; **no `import`/`require`**.\n2. Not referenced in any config or script.\n3. `npm ls jsonpath` → a leaf; it was also the sole reason `underscore` was\n   installed.\n4. Plain runtime dep, zero references → likely deletable.\n5. Removed `jsonpath` + `@types/jsonpath`, `npm install`, `npm run type-check`\n   → green. Confirmed dead; committed. (`underscore` dropped with it.)\n\n<!-- File: .ai/skills/e2e-testing/SKILL.md (Tokens: ~4768 | Category: anthropic-skill) -->\n---\nname: e2e-testing\ndescription: >-\n  Playwright end-to-end testing standards for RedisInsight: page\n  object models, test structure, fixtures, navigation patterns, and\n  flaky-test prevention. Use when editing files under\n  tests/e2e-playwright/**, writing Playwright tests, adding page\n  objects, or when the user mentions Playwright, E2E tests, page\n  objects, or end-to-end testing.\n---\n\n\n# E2E Testing Standards (Playwright)\n\n## Location\n\nAll E2E tests are in `tests/e2e-playwright/`. This is a **standalone package** - no imports from `redisinsight/ui/` or `redisinsight/api/`.\n\n## Test Plan\n\n**Always refer to `tests/e2e-playwright/TEST_PLAN.md`** for:\n- Test coverage status (✅ implemented, 🔲 not implemented)\n- Feature implementation order\n- Test data requirements\n\n**After implementing tests, update TEST_PLAN.md** to mark tests as ✅.\n\n## Project Structure\n\n```\ntests/e2e-playwright/\n├── TEST_PLAN.md         # Master test plan with coverage status\n├── config/              # Configuration (env, databases)\n│   └── databases/       # Database configs by type\n├── fixtures/            # Playwright fixtures\n├── helpers/             # API helpers for setup/teardown\n├── pages/               # Page Object Models\n│   ├── BasePage.ts      # Base class for all pages\n│   ├── InstancePage.ts  # Base class for database instance pages\n│   ├── components/      # Shared components (InstanceHeader, NavigationTabs, BottomPanel)\n│   └── {feature}/       # Feature-specific pages (browser/, cli/, etc.)\n├── test-data/           # Test data factories\n├── tests/               # Test specs organized by project\n│   ├── main/            # Default parallel tests\n│   │   └── {feature}/\n│   │       └── {action}/\n│   ├── auto-update/     # Serial tests with special setup\n│   └── electron/        # Electron-specific tests\n└── types/               # TypeScript types\n```\n\n## Playwright Projects\n\nThe folder a test lives in determines its execution mode. Each browser platform has a parallel project and a serial project:\n\n| Project             | Folder            | Parallelism | Use Case |\n|---------------------|-------------------|-------------|----------|\n| `chromium-parallel` | `tests/parallel/` | Parallel (4 workers) | Standard chromium tests |\n| `chromium-serial`   | `tests/serial/`   | Serial (1 worker)    | Sequential chromium tests |\n| `electron-parallel` | `tests/parallel/` | Serial (1 worker)*   | Standard electron tests |\n| `electron-serial`   | `tests/serial/`   | Serial (1 worker)    | Sequential electron tests |\n\n\\* Electron uses one worker today because there is a single app instance.\n\n### Execution order\n\nFor each platform, the serial project depends on the parallel project via `dependencies: ['<platform>-parallel']` in [playwright.config.ts](../../tests/e2e-playwright/playwright.config.ts), so the order is always:\n\n1. `<platform>-parallel` runs first (with up to N workers)\n2. `<platform>-serial` runs after, one worker at a time\n\nSerial tests perform destructive operations on the shared RTE Redis (`FLUSHDB`, broad `deleteAllIndexes`, dangerous commands) so they can't safely run alongside parallel tests on the same RTE. Electron also can't run its two projects concurrently because the desktop app binds its embedded API on a fixed port (`5530`).\n\nIf serial grows large enough to become a CI bottleneck, the next step is to give serial tests a dedicated Redis instance (or split into a separate CI job) — not to revert the ordering.\n\n### Running Projects\n\n```bash\n# Full platform run (parallel + serial)\nnpx playwright test --project=chromium-parallel --project=chromium-serial\nnpx playwright test --project=electron-parallel --project=electron-serial\n\n# Just parallel\nnpx playwright test --project=chromium-parallel\n\n# Just serial — needs --no-deps, otherwise parallel runs first (it's a\n# project dependency). --no-deps also skips browser-setup.\nnpx playwright test --project=chromium-serial --no-deps\n\nnpx playwright test                           # All projects\n```\n\n### When to put a test in `tests/serial/`\n\nPut a test in `tests/serial/` when it:\n- Shares database state across tests via `beforeAll`\n- Runs dangerous commands or mutates global app state. Settings are global:\n  `scanThreshold`, `dateFormat`, `timezone` and `batchSize` are one shared resource,\n  so a spec that changes one and holds it across other work breaks every concurrent\n  test that reads it\n- Cannot tolerate concurrent execution with other tests\n- Would cause flakiness when run with other tests\n- Require special environment configuration\n\n### Adding a New Project\n\n1. Create folder under `tests/` (e.g., `tests/my-feature/`)\n2. Add project configuration in `playwright.config.ts`:\n\n```typescript\n{\n  name: 'my-feature',\n  testDir: './tests/my-feature',\n  fullyParallel: false, // or true\n  workers: 1,\n  timeout: 120000,\n  // Optional: different setup\n  // globalSetup: './my-feature-setup.ts',\n}\n```\n\n## Page Objects\n\n### Page Object Hierarchy\n\n```\nBasePage (abstract)\n  ├── DatabasesPage           # Databases list page\n  ├── SettingsPage            # Settings page\n  └── InstancePage (abstract) # Base for all database instance pages\n        ├── instanceHeader    # Database name, stats, breadcrumb\n        ├── navigationTabs    # Browse, Workbench, Analyze, Pub/Sub\n        ├── bottomPanel       # CLI, Command Helper, Profiler\n        └── BrowserPage       # Browser-specific (extends InstancePage)\n              └── WorkbenchPage (future)\n              └── AnalyzePage (future)\n              └── PubSubPage (future)\n```\n\n### Extend the Appropriate Base Class\n\n- **BasePage** - For standalone pages (DatabasesPage, SettingsPage)\n- **InstancePage** - For pages within a connected database (BrowserPage, WorkbenchPage, etc.)\n\nPage objects are **stateless** - they don't store database objects. Pass `databaseId` to navigation methods.\n\n```typescript\n// For database instance pages - extend InstancePage\nimport { Page, Locator } from '@playwright/test';\nimport { InstancePage } from '../InstancePage';\n\nexport class WorkbenchPage extends InstancePage {\n  readonly editor: Locator;\n\n  constructor(page: Page) {\n    super(page);\n    this.editor = page.getByTestId('workbench-editor');\n  }\n\n  // InstancePage provides: instanceHeader, navigationTabs, bottomPanel\n  // Plus navigation methods: navigateToBrowser(), openCli(), etc.\n\n  async goto(databaseId: string): Promise<void> {\n    await this.gotoDatabase(databaseId);\n    await this.navigationTabs.gotoWorkbench();\n    await this.waitForLoad();\n  }\n}\n```\n\n### Component-Based Structure\n\nBreak large pages into components:\n\n```typescript\n// pages/feature/FeaturePage.ts\nexport class FeaturePage extends InstancePage {\n  readonly dialog: FeatureDialog;\n  readonly list: FeatureList;\n\n  constructor(page: Page) {\n    super(page);\n    this.dialog = new FeatureDialog(page);\n    this.list = new FeatureList(page);\n  }\n}\n```\n\n## Test Structure\n\n### File Organization\n\n```\ntests/\n├── main/                # Default parallel tests (most tests go here)\n│   └── {feature}/       # e.g., databases, browser, workbench\n│       └── {action}/    # e.g., add, edit, delete\n│           ├── standalone.spec.ts\n│           └── cluster.spec.ts\n├── auto-update/         # Serial tests with special setup\n└── electron/            # Electron-specific tests\n```\n\n### Test Setup Pattern\n\nUse simple, explicit setup with clear separation of concerns. **Page objects are fixtures** - they don't store database state. Pass `databaseId` to `goto()` methods.\n\n```typescript\nimport { test, expect } from '../../../fixtures/base';\nimport { standaloneConfig } from '../../../config/databases/standalone';\nimport { DatabaseInstance } from '../../../types';\n\ntest.describe('Feature > Action', () => {\n  let database: DatabaseInstance;\n\n  // Setup: Create database once for all tests\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase({\n      name: 'test-feature-db',\n      host: standaloneConfig.host,\n      port: standaloneConfig.port,\n    });\n  });\n\n  // Teardown: Clean up database after all tests\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  test.describe('Sub-feature', () => {\n    // Navigation: Pass databaseId to goto() - page is a fixture\n    test.beforeEach(async ({ featurePage }) => {\n      await featurePage.goto(database.id);\n    });\n\n    // Tests receive page fixtures they need\n    test('should do something', async ({ featurePage }) => {\n      await featurePage.doAction();\n      await expect(featurePage.result).toBeVisible();\n    });\n\n    // Tests that need both page and apiHelper\n    test('should create and verify', async ({ featurePage, apiHelper }) => {\n      await apiHelper.createKey(database.id, 'test-key', 'value');\n      await featurePage.refresh();\n      await expect(featurePage.keyList).toContainText('test-key');\n    });\n  });\n});\n```\n\n### Key Principles\n\n1. **`beforeAll`** - Create database/test data via API (runs once)\n2. **`afterAll`** - Clean up database/test data via API (runs once)\n3. **`beforeEach`** - Navigate to page via UI using `goto(databaseId)` (runs before each test)\n4. **Individual tests** - Receive page fixtures they need in the signature\n5. **Page objects are stateless** - Don't store database objects in pages, pass IDs to methods\n\n### Avoid These Anti-Patterns\n\n```typescript\n// ❌ BAD: Storing database in page object\nconst browserPage = createBrowserPage(database);  // OLD pattern - don't use\n\n// ✅ GOOD: Pass databaseId to goto()\nawait browserPage.goto(database.id);\n\n// ❌ BAD: Using page fixture without declaring it in test signature\ntest('should work', async () => {\n  await browserPage.doSomething();  // browserPage is undefined!\n});\n\n// ✅ GOOD: Declare fixtures in test signature\ntest('should work', async ({ browserPage }) => {\n  await browserPage.doSomething();\n});\n\n// ❌ BAD: Navigation inside each test\ntest('should work', async ({ browserPage }) => {\n  await browserPage.goto(database.id);  // Should be in beforeEach\n  // ...\n});\n\n// ❌ BAD: Using test.describe.serial when not needed\ntest.describe.serial('Feature', () => { // Use regular describe unless tests truly depend on each other\n  // ...\n});\n```\n\n## Test Data\n\n### Use Fishery Factories with Faker\n\nUse the [fishery](https://github.com/thoughtbot/fishery) library for test data factories:\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nexport const TEST_PREFIX = 'test-';\n\nexport const ConfigFactory = Factory.define<Config>(() => ({\n  name: `${TEST_PREFIX}${faker.string.alphanumeric(8)}`,\n  host: '127.0.0.1',\n  port: 6379,\n}));\n\n// Usage in tests\nconst config = ConfigFactory.build();\nconst config = ConfigFactory.build({ name: 'custom-name' });\n```\n\n### Cleanup Pattern\n\nPrefix test data with `test-` so leftovers are identifiable, but **never clean up by\nthat prefix**. Specs share a Redis instance and different files run on different\nworkers, so deleting every `test-*` key also removes keys the specs alongside it are\nstill asserting on, and their rows disappear mid-test.\n\nTrack what a test creates and delete only that:\n\n```typescript\nimport { createKeyTracker } from 'e2eSrc/helpers';\n\ntest.describe('Browser > Add Key', () => {\n  const keys = createKeyTracker();\n\n  test.afterEach(async ({ apiHelper }) => {\n    await keys.cleanup(apiHelper, database.id);\n  });\n\n  test('should add a Hash key', async ({ browserPage }) => {\n    const keyData = keys.track(HashKeyFactory.build());\n    // keys.add(name) records a name built without a factory, e.g. a rename target.\n  });\n});\n```\n\nMiss a `track()` and the key is never deleted, which is harmless in CI (the test\nenvironment is recreated per run) but accumulates locally until the RTE restarts.\n\nDatabases need no such care: each spec deletes its own in `afterAll`, and\n`browser.setup` clears leftover `test-` databases once before the run.\n\n## Fixtures\n\n### Add New Fixtures to base.ts\n\n```typescript\n// fixtures/base.ts\ntype Fixtures = {\n  myPage: MyPage;\n  apiHelper: ApiHelper;\n};\n\nexport const test = base.extend<Fixtures>({\n  myPage: async ({ page }, use) => {\n    await use(new MyPage(page));\n  },\n  apiHelper: async ({}, use) => {\n    const helper = new ApiHelper();\n    await use(helper);\n    await helper.dispose();\n  },\n});\n```\n\n## UI Exploration with Playwright MCP\n\n**Before writing tests, ALWAYS use Playwright MCP to explore the UI:**\n\n### Why Explore First?\n- Discover actual `data-testid` attributes used in the application\n- Understand element roles and accessible names for `getByRole()`\n- See page structure and component hierarchy\n- Avoid trial-and-error test writing\n\n### Exploration Workflow\n\n1. **Navigate to the page**: `browser_navigate_Playwright` to target URL\n2. **Take snapshot**: `browser_snapshot_Playwright` to see element tree\n3. **Interact with elements**: `browser_click_Playwright` to trigger dialogs, dropdowns, etc.\n4. **Wait for async content**: `browser_wait_for_Playwright` for dynamic content\n5. **Document findings**: Add discovered UI patterns to `TEST_PLAN.md` under the feature section\n\n### What to Look For\n\n- `data-testid` attributes → use with `page.getByTestId()`\n- Element roles (button, combobox, grid, treeitem) → use with `page.getByRole()`\n- Accessible names → use with `{ name: 'text' }` option\n- Form field placeholders → use with `page.getByPlaceholder()`\n- Text content patterns → use with `page.getByText()`\n\n### Use Discovered Patterns in Page Objects\n\nAfter exploring, use discovered patterns directly in Page Object locators:\n\n```typescript\n// Use data-testid when available\nthis.addButton = page.getByTestId('btn-add-key');\n\n// Use role + name for accessible elements\nthis.submitButton = page.getByRole('button', { name: 'Submit' });\n\n// Use placeholder for form fields\nthis.searchInput = page.getByPlaceholder('Search...');\n```\n\n**Note**: Keep TEST_PLAN.md as a simple visual list of test cases. Document UI patterns in Page Object comments if needed.\n\n## Best Practices\n\n### ✅ DO\n\n- **Explore UI with Playwright MCP before writing tests**\n- **Use Page Object navigation methods** (e.g., `browserPage.goto()`, `workbenchPage.goto()`)\n- Use `data-testid` attributes for stable selectors\n- Use `getByRole`, `getByLabel` for accessible elements\n- Wait for elements with `waitFor({ state: 'visible' })`\n- Clean up test data in `afterEach`\n- Use API for setup, UI for assertions\n- Handle both List view and Tree view in key assertions\n\n### ❌ DON'T\n\n- **NEVER use `page.goto()` directly** - tests must work in both browser and Electron\n- Write tests without exploring the actual UI first\n- Use fixed timeouts (`waitForTimeout`)\n- Use CSS selectors for dynamic content\n- Leave test data after tests\n- Import from `redisinsight/ui/` or `redisinsight/api/`\n- Hardcode test data (use faker)\n- Assume element structure without verification\n\n## Navigation (IMPORTANT)\n\n**All navigation must use UI-based methods, NOT URL navigation.**\n\nTests must work in both browser mode (http://localhost:8080) and Electron mode (no baseURL). Direct `page.goto()` calls fail in Electron because there's no baseURL.\n\n### Navigation Architecture\n\n**BasePage** provides only fundamental navigation:\n```typescript\nawait this.gotoHome();              // Click Redis logo → databases list\nawait this.gotoDatabase(dbId);      // Click database → Browser page (default)\n```\n\n**Each page owns its navigation** via its `goto()` method:\n```typescript\nawait settingsPage.goto();           // Settings page\nawait browserPage.goto(dbId);        // Browser page for database\nawait workbenchPage.goto(dbId);      // Workbench page for database\nawait analyticsPage.goto(dbId);      // Analytics page for database\nawait pubSubPage.goto(dbId);         // Pub/Sub page for database\n```\n\n**NavigationTabs component** handles tab switching within a connected database:\n```typescript\nawait browserPage.navigationTabs.gotoBrowser();\nawait browserPage.navigationTabs.gotoWorkbench();\nawait browserPage.navigationTabs.gotoAnalyze();\nawait browserPage.navigationTabs.gotoPubSub();\n```\n\n### ✅ Correct Navigation Pattern\n\n```typescript\n// Use Page Object's goto() method in beforeEach\ntest.beforeEach(async ({ browserPage }) => {\n  await browserPage.goto(database.id);  // Navigates and waits for page load\n});\n\n// Switch tabs when already connected\nawait browserPage.navigationTabs.gotoWorkbench();\n```\n\n### ❌ Incorrect Navigation Pattern\n\n```typescript\n// NEVER do this - fails in Electron\nawait page.goto(`/${database.id}/browser`);\nawait page.goto('/settings');\nawait page.goto('/');\n```\n\n## Running Tests\n\nRun these commands from the E2E package directory:\n\n```bash\ncd tests/e2e-playwright\n\nnpx playwright test                                              # All Playwright projects\nnpx playwright test --project=chromium-parallel                  # Chromium parallel tests\nnpx playwright test --project=chromium-serial --no-deps          # Chromium serial only (skips parallel + setup)\nnpx playwright test --project=electron-parallel                  # Electron parallel tests\nnpx playwright test --project=electron-serial --no-deps          # Electron serial only (skips parallel + setup)\nENV=ci npx playwright test                             # CI environment\nENV=staging npx playwright test                        # Staging environment\n```\n\n## Code Quality (IMPORTANT)\n\n**Always run linter and type checker after making changes:**\n\n```bash\nnpm run lint                # ESLint check\nnpm run type-check          # TypeScript type check\n```\n\nBoth must pass before committing. Common issues:\n- Unused variables/imports\n- Missing return types\n- `any` types (avoid when possible)\n- Null/undefined handling (use proper types like `Promise<string | null>`)\n\n## Test Isolation (IMPORTANT)\n\nTests should be isolated and not depend on execution order:\n\n### 1. Shared Database with beforeAll/afterAll\n\n```typescript\ntest.describe('Feature Name', () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase({ name: 'test-feature-db', ... });\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  // Tests can run in parallel - they share the database but don't modify shared state\n});\n```\n\n### 2. Use Serial Only When Tests Truly Depend on Each Other\n\n```typescript\n// Only use .serial when tests modify state that subsequent tests depend on\ntest.describe.serial('Workflow that modifies state', () => {\n  test('step 1: create item', ...);\n  test('step 2: modify item created in step 1', ...);\n  test('step 3: delete item', ...);\n});\n```\n\n### 3. Unique Test Data Per Test (when needed)\n\n```typescript\ntest('should create unique item', async ({ apiHelper }) => {\n  const uniqueName = `test-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;\n  // Use uniqueName for this test's data\n});\n```\n\n## Feature-to-Path Mapping\n\nFollow this naming convention for test and page object paths:\n\n| Feature | Test Path | Page Object Path |\n|---------|-----------|------------------|\n| Database List | `tests/parallel/databases/list/` | `pages/databases/` |\n| Add Database | `tests/parallel/databases/add/` | `pages/databases/` |\n| Import Database | `tests/parallel/databases/import/` | `pages/databases/` |\n| Browser - Key List | `tests/parallel/browser/key-list/` | `pages/browser/` |\n| Browser - Add Key | `tests/parallel/browser/add-key/` | `pages/browser/` |\n| Browser - Key Details | `tests/parallel/browser/key-details/` | `pages/browser/` |\n| Workbench | `tests/parallel/workbench/` | `pages/workbench/` |\n| CLI | `tests/parallel/cli/` | `pages/cli/` |\n| Pub/Sub | `tests/parallel/pubsub/` | `pages/pubsub/` |\n| Slow Log | `tests/parallel/analytics/slow-log/` | `pages/analytics/` |\n| DB Analysis | `tests/parallel/analytics/analysis/` | `pages/analytics/` |\n| Settings | `tests/parallel/settings/` | `pages/settings/` |\n| Navigation | `tests/parallel/navigation/` | `pages/navigation/` |\n| Auto-Update | `tests/auto-update/` | `pages/` (shared) |\n| Deep Links | `tests/electron/deep-links/` | `pages/` (shared) |\n\n**Note**: Most tests go in `tests/parallel/`. Only use other project folders for tests with special requirements (serial execution, different setup, etc.).\n\n<!-- File: .ai/skills/feature-flags/SKILL.md (Tokens: ~1501 | Category: anthropic-skill) -->\n---\nname: feature-flags\ndescription: >-\n  Create, modify, and remove feature flags in RedisInsight. Use when adding a\n  new feature flag, introducing a dev flag, promoting a dev flag to regular,\n  cleaning up old flags, or the user mentions feature flags, feature toggles,\n  or gating features.\n---\n\n# Feature Flags\n\nRedisInsight has its own feature flag system. Flags are defined in a remote JSON config, fetched by the backend, and served to the frontend via API. This skill covers how to add, promote, and remove flags.\n\n## Flag Types\n\n| Type                         | Naming                            | `flag` value | Strategy                 | Purpose                                                      |\n| ---------------------------- | --------------------------------- | ------------ | ------------------------ | ------------------------------------------------------------ |\n| **Dev flag**                 | `dev-<name>` (e.g. `dev-browser`) | `false`      | `CommonFlagStrategy`     | Hide incomplete features during development                  |\n| **Regular flag**             | `camelCase` (e.g. `azureEntraId`) | `true`       | `CommonFlagStrategy`     | Standard on/off toggle                                       |\n| **Regular with data**        | `camelCase`                       | `true`       | `WithDataFlagStrategy`   | Flag + extra config payload in `data`                        |\n| **Switchable (overridable)** | `camelCase`                       | `true`       | `SwitchableFlagStrategy` | User can override locally via `~/.redis-insight/config.json` |\n\n## Files to Change\n\nEvery new flag touches these files (in order):\n\n### Backend (required)\n\n1. **`redisinsight/api/config/features-config.json`**\n   Add the flag entry with `flag`, `perc`, optional `filters` and `data`. Bump the `version` number.\n\n2. **`redisinsight/api/src/modules/feature/constants/index.ts`**\n   Add to the `KnownFeatures` enum.\n\n3. **`redisinsight/api/src/modules/feature/constants/known-features.ts`**\n   Add entry to the `knownFeatures` record with `name` and `storage` (usually `FeatureStorage.Database`).\n\n4. **`redisinsight/api/src/modules/feature/providers/feature-flag/feature-flag.provider.ts`**\n   Register the flag with its strategy (see Strategy Types below).\n\n### Frontend (required if the flag gates UI)\n\n5. **`redisinsight/ui/src/constants/featureFlags.ts`**\n   Add to the `FeatureFlags` enum.\n\n6. **`redisinsight/ui/src/slices/app/features.ts`**\n   Add default state entry in `initialState.featureFlags.features` with `{ flag: false }`.\n\n### Consuming code\n\n7. Use the flag in components/hooks to gate functionality.\n\n## Strategy Selection\n\nChoose the strategy based on what the flag needs:\n\n```\nCommonFlagStrategy        → Most flags (dev and regular on/off)\nWithDataFlagStrategy      → Flag needs to carry extra data payload\nSwitchableFlagStrategy    → Flag should be overridable via local config.json\n```\n\nRegister in `feature-flag.provider.ts`:\n\n```typescript\nthis.strategies.set(\n  KnownFeatures.YourFeature,\n  new CommonFlagStrategy(this.featuresConfigService, this.settingsService),\n);\n```\n\n## Config JSON Structure\n\n### Minimal (dev flag)\n\n```json\n\"dev-myFeature\": {\n  \"flag\": false,\n  \"perc\": [[0, 100]]\n}\n```\n\n### With filters (Electron-only)\n\n```json\n\"myFeature\": {\n  \"flag\": true,\n  \"perc\": [[0, 100]],\n  \"filters\": [\n    { \"name\": \"config.server.buildType\", \"value\": \"ELECTRON\", \"cond\": \"eq\" }\n  ]\n}\n```\n\n### Gradual rollout (10% of users)\n\n```json\n\"myFeature\": {\n  \"flag\": true,\n  \"perc\": [[0, 10]]\n}\n```\n\n### With data payload\n\n```json\n\"myFeature\": {\n  \"flag\": true,\n  \"perc\": [[0, 100]],\n  \"data\": { \"strategy\": \"ioredis\" }\n}\n```\n\n## Filter Conditions\n\nFilters compare a value from server state against the filter value.\n\n| Condition    | Meaning                         |\n| ------------ | ------------------------------- |\n| `eq`         | equals                          |\n| `neq`        | not equals                      |\n| `gt` / `gte` | greater than / greater or equal |\n| `lt` / `lte` | less than / less or equal       |\n\nCommon `name` paths: `config.server.buildType` (ELECTRON, DOCKER_ON_PREMISE, REDIS_STACK), `config.server.packageVersion` (uses semver), `agreements.analytics`, `env.<VAR_NAME>`.\n\nFilters support `and`/`or` composition for complex conditions.\n\n## Workflows\n\n### Add a dev feature flag\n\nUse for features under active development that should not be visible in production.\n\n1. `features-config.json` → add `\"dev-myFeature\": { \"flag\": false, \"perc\": [[0, 100]] }`\n2. `constants/index.ts` → add `DevMyFeature = 'dev-myFeature'` to `KnownFeatures`\n3. `constants/known-features.ts` → add record entry\n4. `feature-flag.provider.ts` → register with `CommonFlagStrategy`\n5. `ui/src/constants/featureFlags.ts` → add `devMyFeature = 'dev-myFeature'`\n6. `ui/src/slices/app/features.ts` → add default `{ flag: false }`\n\n### Promote dev flag to regular flag\n\nWhen the feature is complete and ready for rollout.\n\n1. Rename `dev-myFeature` → `myFeature` in all the files above\n2. Set `flag: true` in `features-config.json`\n3. Optionally set `perc` for gradual rollout (e.g. `[[0, 10]]`)\n4. Change strategy if needed (e.g. to `SwitchableFlagStrategy` for overridable)\n5. Bump config `version`\n\n### Clean up a flag\n\nWhen a feature is fully rolled out and the flag is no longer needed.\n\n1. Remove from `features-config.json`\n2. Remove from `KnownFeatures` enum\n3. Remove from `knownFeatures` record\n4. Remove strategy registration from `feature-flag.provider.ts`\n5. Remove from FE `FeatureFlags` enum\n6. Remove default state from `features.ts`\n7. Remove all gating code (conditionals, `FeatureFlagComponent` wrappers) in consuming components\n\n## FE Usage Patterns\n\n### Check flag in a component\n\n```typescript\nimport { FeatureFlags } from 'uiSrc/constants';\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features';\n\nconst features = useSelector(appFeatureFlagsFeaturesSelector);\nconst isEnabled = features[FeatureFlags.myFeature]?.flag;\n```\n\n### Custom selector for complex logic\n\n```typescript\nexport const isMyFeatureEnabledSelector = (state: RootState): boolean => {\n  const features = state.app.features.featureFlags.features;\n  return features[FeatureFlags.myFeature]?.flag ?? false;\n};\n```\n\n<!-- File: .ai/skills/frontend/SKILL.md (Tokens: ~2949 | Category: anthropic-skill) -->\n---\nname: frontend\ndescription: >-\n  React/Redux frontend development patterns for RedisInsight UI:\n  component folder structure, styled-components, hooks, named exports,\n  barrel files, layout components, and theme usage. Use when editing\n  any file under redisinsight/ui/**, writing or modifying React\n  components, Redux slices, styled-components, custom hooks, or when\n  the user mentions UI, frontend, React, Redux, or styled-components.\n---\n\n# Frontend Development (React/Redux)\n\n## Component Structure\n\n### Functional Components\n\n- Use **functional components with hooks** (no class components)\n- **Prefer named exports** over default exports\n- Keep components focused and single-responsibility\n- Extract complex logic into custom hooks\n\n### Component Folder Structure\n\nEach component in its own directory under `**/ComponentName`:\n\n```\nComponentName/\n  ComponentName.tsx          # Main component\n  ComponentName.styles.ts    # Styled-components styles (PascalCase)\n  ComponentName.types.ts     # TypeScript interfaces\n  ComponentName.spec.tsx     # Tests\n  ComponentName.constants.ts # Constants\n  ComponentName.story.tsx    # Storybook examples\n  hooks/                     # Custom hooks\n  components/                # Sub-components\n  utils/                     # Utility functions\n```\n\n### Props Interface\n\n- Name as `ComponentNameProps`\n- Use separate interfaces for complex prop objects\n- Always use proper TypeScript types, never `any`\n\n### Imports Order in Components\n\n1. External dependencies (`react`, `redux`, etc.)\n2. Internal modules (aliases)\n3. Local imports (types, constants, hooks)\n4. Styles (always last: `import { Container } from './Component.styles'`)\n\n### Barrel Files\n\nUse barrel files (`index.ts`) only when exporting **3 or more** items. Make sure exports appear in only one barrel file, not propagated up the chain.\n\n## Styled Components\n\n**We are migrating to styled-components** (deprecating SCSS modules).\n\n### Encapsulate Styles in .styles.ts\n\nKeep all component styles in dedicated `.styles.ts` files using styled-components. Use PascalCase for the filename to match the component name:\n\n```\nComponentName/\n  ComponentName.tsx\n  ComponentName.styles.ts  # ✅ PascalCase\n  # Not component-name.styles.ts ❌\n```\n\n### Import Pattern\n\nKeep all component styles in dedicated .style.ts files and import them with a namespace.\n\n**CRITICAL: `import * as S` is reserved for local styles only** (e.g., from `ComponentName.styles.ts`). When you need to use styled components from external components, create a local styles file that re-exports them.\n\n#### ✅ Good\n\n```typescript\n// ComponentName.tsx\nimport * as S from './ComponentName.styles'\n\nreturn (\n  <S.Container>\n    <S.Title>Title</S.Title>\n    <S.Content>Content</S.Content>\n  </S.Container>\n)\n\n// ComponentName.styles.ts (when re-exporting from external component)\nexport { ExternalStyledComponent } from '../ExternalComponent/ExternalComponent.styles'\n```\n\n#### ❌ Bad\n\n```typescript\n// ❌ BAD: Importing styled components directly from external component\nimport * as S from '../ExternalComponent/ExternalComponent.styles';\n\n// ❌ BAD: Named imports instead of namespace\nimport { Container, Title, Content } from './Component.styles';\n```\n\n### Use Layout Components Instead of div\n\n**Prefer `FlexGroup` over `div`** when creating flex containers:\n\n```typescript\n// ✅ GOOD: Use FlexGroup\nimport { FlexGroup } from 'uiSrc/components/base/layout/flex'\n\nexport const Wrapper = styled(FlexGroup)`\n  user-select: none;\n`\n\n// Usage: Pass layout props as component props\n<Wrapper align=\"center\" justify=\"end\">\n  {children}\n</Wrapper>\n\n// ❌ BAD: Using div with hardcoded flex properties\nexport const Wrapper = styled.div`\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n`\n```\n\n### Pass Layout Props as Component Props\n\n**Don't hardcode layout properties** in styled components when using layout components like `FlexGroup`. Pass them as props instead:\n\n```typescript\n// ✅ GOOD: Pass props in JSX\nexport const Wrapper = styled(FlexGroup)`\n  user-select: none;\n`\n\n<Wrapper align=\"center\" justify=\"end\">\n  {children}\n</Wrapper>\n\n// ❌ BAD: Hardcoding in styled component\nexport const Wrapper = styled(FlexGroup)`\n  align-items: center;\n  justify-content: flex-end;\n  user-select: none;\n`\n```\n\n### Use Gap Prop Instead of Custom Margins\n\n**Prefer `gap` prop on layout components** instead of custom margins for spacing between elements:\n\n```typescript\n// ✅ GOOD: Use gap prop\n<Row align=\"center\" justify=\"between\" gap=\"l\">\n  <FlexItem>Item 1</FlexItem>\n  <FlexItem>Item 2</FlexItem>\n</Row>\n```\n\n### Use Theme Spacing Instead of Magic Numbers\n\n**Always use theme spacing values** instead of hardcoded pixel values:\n\n```typescript\n// ✅ GOOD: Use theme spacing\nexport const Container = styled(Row)`\n  height: ${({ theme }) => theme.core.space.space500};\n  padding: 0 ${({ theme }) => theme.core.space.space200};\n  margin-bottom: ${({ theme }) => theme.core.space.space200};\n`;\n\n// ❌ BAD: Using magic numbers\nexport const Container = styled(Row)`\n  height: 64px;\n  padding: 0 16px;\n  margin-bottom: 16px;\n`;\n```\n\n### Use Semantic Colors from Theme\n\n**Always use semantic colors** from the theme instead of CSS variables or hardcoded colors:\n\n```typescript\n// ✅ GOOD: Use semantic colors\nexport const Header = styled(Row)`\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral100};\n  border-bottom: 1px solid\n    ${({ theme }) => theme.semantic.color.border.neutral500};\n`;\n\n// ❌ BAD: Using deprecated EUI CSS variables\nexport const Header = styled(Row)`\n  background-color: var(--euiColorEmptyShade);\n  border-bottom: 1px solid var(--separatorColor);\n`;\n```\n\n### Use Layout Components (Row/Col/FlexGroup) Instead of div\n\n**Prefer layout components** from the layout system instead of regular `div` elements:\n\n```typescript\n// ✅ GOOD: Use Row component\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const PageHeader = styled(Row)`\n  height: ${({ theme }) => theme.core.space.space500};\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral100};\n`\n\n<PageHeader align=\"center\" justify=\"between\" gap=\"l\">\n  {children}\n</PageHeader>\n\n// ❌ BAD: Using div with flex properties\nexport const PageHeader = styled.div`\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  height: 64px;\n`\n```\n\n### Conditional Styling\n\nUse `$` prefix for transient props that shouldn't pass to DOM:\n\n```typescript\nexport const Button = styled.button<{ $isActive?: boolean }>`\n  background-color: ${({ $isActive }) => ($isActive ? '#007bff' : '#6c757d')};\n`;\n```\n\n### Avoid !important\n\n**Never use `!important` in styled-components**. Styled-components handles CSS specificity through component hierarchy. If you need to override styles, use more specific selectors or adjust the component structure:\n\n```typescript\n// ✅ GOOD: Rely on CSS specificity\nexport const IconButton = styled(IconButton)<{ isOpen: boolean }>`\n  ${({ isOpen }) =>\n    isOpen &&\n    css`\n      background-color: ${({ theme }) =>\n        theme.semantic.color.background.primary200};\n    `}\n`;\n\n// ❌ BAD: Using !important\nexport const IconButton = styled(IconButton)`\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.primary200} !important;\n`;\n```\n\n### Verify Type System Compatibility\n\nWhen using layout components or other typed components, verify your prop values match the type system:\n\n```typescript\n// Check the component's type definitions\n// FlexGroup accepts: align?: 'center' | 'stretch' | 'baseline' | 'start' | 'end'\n// Use valid values from the type system\n```\n\n## State Management (Redux)\n\n### When to Use What\n\n- **Global State (Redux)**:\n\n  - Data shared across multiple components\n  - Data persisting across routes\n  - Server state (API data)\n  - User preferences/settings\n\n- **Local State (useState)**:\n\n  - UI state (modals, dropdowns, tabs)\n  - Form inputs before submission\n  - Component-specific temporary data\n\n- **Derived State (Selectors)**:\n  - Computed values from Redux state\n  - Filtered/sorted lists\n  - Aggregated data\n\n### Redux Toolkit Patterns\n\n#### Slice Structure\n\n- Use `createSlice` from Redux Toolkit\n- Define proper TypeScript types for state\n- Use `PayloadAction<T>` for action typing\n- Handle async with `extraReducers` and thunks\n\n#### Thunks\n\n- Use `createAsyncThunk` for async operations\n- Handle pending, fulfilled, and rejected states\n- Use `rejectWithValue` for error handling\n\n#### Selectors\n\n- Create basic selectors for direct state access\n- Use `createSelector` from `reselect` for memoized/computed values\n- Keep selectors in separate `selectors.ts` file\n\n## React Best Practices\n\n### Performance\n\n- Use `useCallback` for functions passed as props\n- Use `useMemo` for expensive computations\n- Use `React.memo` for expensive components\n- Avoid inline arrow functions in JSX props\n\n### Effect Cleanup\n\nAlways clean up subscriptions, timers, and event listeners in `useEffect` return function.\n\n### Keys in Lists\n\n- Use unique, stable IDs (not array indices)\n- Only use indices if list never reorders and items have no IDs\n\n### Conditional Rendering\n\n- Use early returns for loading/error states\n- Avoid deeply nested ternaries - extract to functions\n\n## Custom Hooks\n\n### Extract Reusable Logic\n\nCreate custom hooks for reusable stateful logic. Store component-specific hooks in the component's `/hooks` directory.\n\n## Form Handling\n\nUse Formik with Yup for validation. Keep form logic in custom hooks when complex.\n\n## UI Components\n\n**⚠️ IMPORTANT**:\n\n- We are **deprecating Elastic UI** components\n- Migrating to **Redis UI** (`@redis-ui/*`)\n- **Use internal wrappers** from `uiSrc/components/ui`\n- **DO NOT import directly** from `@redis-ui/*`\n\n### Component Usage\n\n```typescript\n// ✅ GOOD: Import from internal wrappers\nimport { Button, Input, FlexGroup } from 'uiSrc/components/ui';\n\n// ❌ BAD: Don't import directly from @redis-ui\nimport { Button } from '@redis-ui/components';\n\n// ❌ DEPRECATED: Don't use Elastic UI for new code\nimport { EuiButton } from '@elastic/eui';\n```\n\n### Migration Guidelines\n\n- ✅ Use internal wrappers from `uiSrc/components/ui` for all new features\n- ✅ Create internal wrappers for Redis UI components as needed\n- ✅ Replace Elastic UI when touching existing code\n- ❌ Do not import directly from `@redis-ui/*`\n- ❌ Do not add new Elastic UI imports\n\n## Icons\n\n**⚠️ IMPORTANT**: Always use icons from Redis UI library instead of custom SVGs.\n\n### Icon Usage\n\n- **Use Redis UI icons** from `@redis-ui/icons` via `iconRegistry.tsx`\n- Icons are automatically exported via `export * from '@redis-ui/icons'` in `iconRegistry.tsx`\n- Use `RiIcon` component with icon type: `<RiIcon type=\"FolderOpenIcon\" />`\n- **DO NOT create custom SVG icons** - check if the icon exists in Redis UI library first\n\n### Custom Icons (Exception)\n\nOnly create custom SVG icons if:\n\n- The icon doesn't exist in Redis UI library\n- It's a project-specific icon that won't be added to the library\n\n## Testing Components\n\n### Always Use Shared `renderComponent` Helper\n\n**CRITICAL**: Create a `renderComponent` helper function for each component test file:\n\nPull entity data from a shared Fishery factory in `redisinsight/ui/src/mocks/factories/`\n(reuse one, or add a new `<domain>/<TypeName>.factory.ts`); inline only callbacks/primitives.\n\n```typescript\ndescribe('MyComponent', () => {\n  const mockUser = UserFactory.build();\n  const defaultProps: MyComponentProps = {\n    user: mockUser,\n    onComplete: jest.fn(),\n  }\n\n  const renderComponent = (propsOverride?: Partial<MyComponentProps>) => {\n    const props = { ...defaultProps, ...propsOverride }\n\n    return render(\n      <Provider store={store}>\n        <MyComponent {...props} />\n      </Provider>\n    )\n  }\n\n  it('should render', () => {\n    renderComponent()\n    // assertions\n  })\n})\n```\n\nBenefits:\n\n- Centralized setup and providers\n- Default props in one place\n- Easy prop overrides per test\n- No duplicate render logic\n\n### Testing Redux\n\nCreate a test store with `configureStore` for components connected to Redux.\n\n## Key Principles\n\n1. **Separation of Concerns**: Keep styles, types, constants, logic separate\n2. **Colocate Related Code**: Keep sub-components and hooks close to usage\n3. **Consistent Naming**: Follow conventions across all components\n4. **Type Safety**: Always define proper types, never `any`\n5. **Testability**: Structure for easy testing with `renderComponent` helper\n6. **Styled Components**: Prefer styled-components over SCSS modules\n7. **Layout Components**: Use FlexGroup instead of div for flex containers, pass layout props as component props\n8. **Type Safety**: Verify prop values match component type definitions (e.g., FlexGroup's align/justify values)\n\n<!-- File: .ai/skills/git-safety/SKILL.md (Tokens: ~564 | Category: anthropic-skill) -->\n---\nname: git-safety\ndescription: >-\n  Critical safety guardrails for protected branches: never commit,\n  push, or force-push directly to main, latest, or release branches;\n  no destructive history rewrites without explicit user approval.\n  Use before any git operation that touches protected branches,\n  before force-push, reset --hard, history rewrite, or branch\n  deletion, or whenever the user asks about merging, pushing, or\n  release branches.\n---\n\n\n# Git Safety Rules for AI Agents\n\n## 🚫 CRITICAL: Protected Branch Rules\n\n**AI agents must NEVER commit to or push to protected branches under any circumstances.**\n\n### Protected Branches\n\n- `main` - Primary production branch\n- `latest` - Latest stable release\n- `release/*` - Release branches (e.g., `release/v2.0.0`)\n\nThis is a non-negotiable rule that applies to all scenarios:\n\n### Prohibited Actions\n\n- ❌ **Direct commits** - Never run `git commit` while on a protected branch\n- ❌ **Direct pushes** - Never run `git push origin <protected-branch>` or `git push` while on a protected branch\n- ❌ **Force pushes** - Never run `git push --force` or `git push -f` targeting protected branches\n- ❌ **Merging into protected branches locally** - Never run `git merge <branch>` while on a protected branch\n- ❌ **Rebasing protected branches** - Never run `git rebase` while on a protected branch\n- ❌ **Resetting protected branches** - Never run `git reset` while on a protected branch\n\n### Required Workflow\n\n1. **Always create a feature branch** before making any changes\n2. **Verify current branch** before any git operation using `git branch --show-current`\n3. **Create Pull Requests** for all changes - let the review process handle merging\n\n### Pre-Push Checklist\n\nBefore executing any push command, AI agents must:\n\n1. ✅ Confirm current branch is NOT a protected branch (`main`, `latest`, `release/*`)\n2. ✅ Verify the remote and branch target\n\n### Error Recovery\n\nIf accidentally on a protected branch with uncommitted changes:\n\n1. Stash changes: `git stash`\n2. Create new branch: `git checkout -b <appropriate-branch-name>`\n3. Apply changes: `git stash pop`\n4. Continue work on the new branch\n\n## Rationale\n\n- Protected branches represent production-ready or release code\n- All changes must go through code review via Pull Requests\n- Direct pushes bypass CI/CD checks and team review\n- Mistakes on protected branches can affect the entire team and deployment pipeline\n\n<!-- File: .ai/skills/pull-requests/SKILL.md (Tokens: ~491 | Category: anthropic-skill) -->\n---\nname: pull-requests\ndescription: >-\n  Create and review pull requests following project standards including title\n  format, description template, and review checklist. Use when creating PRs,\n  writing PR descriptions, or reviewing pull requests.\n---\n\n# Pull Request Guidelines\n\n## Creating a PR\n\n### PR Title\n\nInclude issue number at the start:\n\n```\nRI-123 Add user profile editing\n#456 Fix memory leak in connection pool\n```\n\n### PR Description Template\n\n```markdown\n# What\n\nDescribe what was changed.\n\n# Testing\n\nDescribe how to test the changes.\n\n---\n\nRefs #RI-123\n```\n\n**PR Description Guidelines:**\n\n- **Keep it concise** - Avoid verbose descriptions\n- **Focus on high-level changes** - Don't list every code change in the #What section\n- **Brief and to the point** - The diff shows the details; describe the \"why\" and \"what\" at a high level\n- **Technical decisions** - Only mention significant architectural or design decisions if relevant\n- **Link, don't auto-close** - Use `Refs #RI-123` / `Addresses #RI-123`, not `Closes`/`Fixes`/`Resolves` - those keywords auto-close the issue when the PR merges, and tickets should be closed manually, not by the merge\n\n## Review Process\n\n### As PR Author\n\n- **Respond to all comments** - Address every piece of feedback\n- **Don't take feedback personally** - Reviews improve code quality\n- **Update code based on feedback** - Make requested changes\n- **Mark conversations as resolved** - After addressing feedback\n- **Keep PR up to date** - Rebase on main regularly\n\n### As PR Reviewer\n\n- **Be constructive and respectful** - Focus on improvement\n- **Focus on logic, not style** - Linter handles formatting\n- **Check for**:\n  - Logic errors and edge cases\n  - Performance issues\n  - Security concerns\n  - Test coverage\n  - Missing documentation\n  - Architectural concerns\n\n### Review Checklist\n\n- [ ] Code follows project patterns\n- [ ] Tests are comprehensive\n- [ ] No console.log or debug code\n- [ ] TypeScript types are proper\n- [ ] Error handling is adequate\n- [ ] Documentation is updated\n- [ ] No security vulnerabilities\n- [ ] Performance is acceptable\n\n<!-- File: .ai/skills/redis-insight-plugin/references/error-handling.md (Tokens: ~926 | Category: anthropic-skill) -->\n# Error Handling and Stability\n\nPlugins run inside the Insight iframe. A thrown exception during activation produces a blank panel with no visible cause. Defensive rendering is mandatory.\n\n## Defensive Render Pattern\n\n```ts\nconst PREFIX = '[MY_PLUGIN]';\n\nexport function renderView(props: any) {\n  const host = document.getElementById('app');\n  if (!host) {\n    console.error(PREFIX, '#app missing');\n    return;\n  }\n  try {\n    const rows = parseData(props?.data ?? []);\n    if (rows.length === 0) {\n      host.innerHTML = '<div class=\"empty\">No data to display.</div>';\n      return;\n    }\n    renderTable(host, rows, props);\n  } catch (err) {\n    console.error(PREFIX, 'render failed', err);\n    host.innerHTML = `\n      <div class=\"error\">\n        <p>Plugin failed to render. See devtools console.</p>\n        <pre>${escape(String((err as Error)?.message ?? err))}</pre>\n      </div>\n    `;\n  }\n}\n```\n\nRules:\n\n- Always check `#app` exists.\n- Always check the data shape before rendering.\n- Always wrap render logic in `try/catch`.\n- Render an empty state and an error state — never a blank iframe.\n- Make empty/error copy match the active visualization mode. Each mode in a multi-visualization plugin should show its own message, not a single generic one.\n- Remove unreachable fallback branches after parser state is narrowed. Dead defensive checks hide the true state model.\n\n## React ErrorBoundary\n\nFor React-rendered plugins, wrap the root component:\n\n```tsx\nclass PluginErrorBoundary extends React.Component<{ children: React.ReactNode }, { error: Error | null }> {\n  state = { error: null as Error | null };\n  static getDerivedStateFromError(error: Error) { return { error }; }\n  componentDidCatch(error: Error, info: React.ErrorInfo) {\n    console.error('[PLUGIN]', 'render error', error, info);\n  }\n  render() {\n    if (this.state.error) {\n      return <div className=\"error\"><pre>{String(this.state.error.message)}</pre></div>;\n    }\n    return this.props.children;\n  }\n}\n```\n\nWrap once at the plugin root, not around every leaf component.\n\n## Logging Prefixes\n\nPrefix every log line with a stable, unique tag:\n\n- `[MY_PLUGIN]`\n- `[STREAM_PLUGIN]`\n- `[SEARCH_PLUGIN]`\n\nWhy:\n\n- Devtools filtering becomes a one-step regex.\n- Multiple plugins in the same Insight session can be told apart.\n- Customer logs forwarded to support are immediately attributable.\n\nNever use bare `console.log(data)` in shipped plugins.\n\n## State Persistence\n\nPrefer the SDK first, `localStorage` fallback:\n\n```ts\nimport { getState, setState } from 'redisinsight-plugin-sdk';\n\nasync function loadSettings() {\n  try {\n    return (await getState()) ?? {};\n  } catch {\n    try {\n      return JSON.parse(localStorage.getItem('ri:my-plugin:settings') ?? '{}');\n    } catch {\n      return {};\n    }\n  }\n}\n\nasync function saveSettings(s: object) {\n  try { await setState(s); }\n  catch { localStorage.setItem('ri:my-plugin:settings', JSON.stringify(s)); }\n}\n```\n\nValidate persisted state before applying — fields can disappear or change shape between plugin versions.\n\n## Common Issues\n\n| Issue | Likely cause | Fix |\n|-------|--------------|-----|\n| Blank iframe, no logs | Activation function not exported | Ensure `export default { fn }` and name matches manifest. |\n| `process.env is not defined` | Bundler left env refs | Replace at build time; verify `grep -c process.env dist/index.js` is `0`. |\n| `require is not defined` | Externalized React | `targets.module.includeNodeModules: true`; rebuild. |\n| Visualization renders but is empty | Parser returned empty | Log raw `props.data`; check command modifier flags. |\n| Values mapped to wrong field/axis | Response shape assumption wrong | Branch on the actual runtime shape, not the command name. |\n| Theme looks wrong | No `theme_DARK` handling | Read `document.body.classList` or `getTheme()` from SDK. |\n| Plugin disappears after Insight upgrade | Manifest field changed | Re-read official docs and update `package.json`. |\n| Large bundle, slow load | Missing minify, full lodash | Run `npm run minify:js`; switch to scoped imports. |\n\n<!-- File: .ai/skills/redis-insight-plugin/references/external-parcel-plugin.md (Tokens: ~951 | Category: anthropic-skill) -->\n# External Parcel Plugin\n\nBuild a standalone Redis Insight plugin with Parcel.\n\n## Folder Layout\n\n```\n<plugin-name>/\n  package.json\n  src/\n    index.html\n    main.tsx\n    components/\n    styles/\n      styles.scss\n  dist/                     # build output, gitignored except when deploying\n    index.js\n    styles.css\n  scripts/\n    verify-plugin.sh\n    deploy-external.sh\n```\n\n## Build Tool: Parcel\n\nWhy Parcel:\n\n- Zero-config TS/SCSS/asset handling.\n- Simple `targets` model maps cleanly to \"single bundled JS + CSS\".\n- `targets.module.includeNodeModules: true` bundles every dep, which is what an external plugin needs.\n\nWhy **not** Vite for external plugins:\n\n- Vite assumes ESM consumers and multi-file output. Insight expects a single `dist/index.js`.\n- Vite's externalization defaults can leave React unbundled — fatal for an iframe with no shared deps.\n\n## Package Scripts\n\nRecommended scripts (see [../templates/external-parcel-package.json](../templates/external-parcel-package.json)):\n\n```json\n\"scripts\": {\n  \"start\":   \"parcel src/index.html\",\n  \"build\":   \"concurrently \\\"npm run build:js\\\" \\\"npm run build:css\\\"\",\n  \"build:js\":\"parcel build src/main.tsx --no-source-maps --dist-dir dist --target module\",\n  \"build:css\":\"parcel build src/styles/styles.scss --no-source-maps --dist-dir dist\",\n  \"minify:js\":\"terser dist/index.js -o dist/index.js -c -m\",\n  \"clean\":   \"rimraf dist\",\n  \"verify\":  \"bash scripts/verify-plugin.sh\",\n  \"deploy:external\":\"bash scripts/deploy-external.sh\",\n  \"deploy:internal\":\"bash scripts/deploy-internal-docker.sh\"\n}\n```\n\n## Targets\n\n```json\n\"targets\": {\n  \"main\": false,\n  \"module\": {\n    \"includeNodeModules\": true,\n    \"outputFormat\": \"esmodule\",\n    \"isLibrary\": false\n  }\n}\n```\n\n- `main: false` disables Parcel's CommonJS output.\n- `includeNodeModules: true` inlines every dependency.\n- Do **not** add `\"engines\": { \"browsers\": \"...\" }` so narrow that React or your visualization library break — Insight bundles a modern Chromium.\n\n## Bundling Rules\n\n- Bundle React, ReactDOM, and every other runtime dependency (including the visualization library).\n- No `peerDependencies`. Insight does not provide shared deps to plugins.\n- No imports from `uiSrc/`, `@redis-ui/*`, or any RedisInsight monorepo package.\n- No `process.env.*` references in the bundle. Replace with constants at build time. Verify with:\n\n  ```bash\n  grep -c \"process.env\" dist/index.js   # must be 0\n  ```\n\n- Minify before deploying customer-facing plugins; skip in dev for readable stack traces.\n\n## Source vs Deployed Manifest\n\nThe source `package.json` includes `scripts`, `devDependencies`, `targets`, etc. The deployed manifest must include only:\n\n- `name`, `version`, `description`\n- `main`, `styles`\n- `visualizations`\n- runtime `dependencies` (optional, informational)\n\nStrip the rest before copying into `~/.redis-insight/plugins/<name>/`. The deploy script in `templates/deploy-external.sh` does this for you (or use `jq` to filter).\n\n## Outputs\n\nAfter `npm run build`:\n\n- `dist/index.js` — single bundled module.\n- `dist/styles.css` — single stylesheet.\n\nIf your plugin has no styles, you can omit the `styles` manifest field and the SCSS source — but most plugins need styles.\n\nFor RedisInsight product UI fidelity, copy `templates/external-styles.scss` to `src/styles/styles.scss`. Keep plugin styles scoped under `.ri-plugin-*` classes and use `theme_LIGHT` / `theme_DARK` body classes for light/dark mode.\n\n## Verification\n\n```bash\nnpm run build\nnpm run verify     # runs templates/verify-plugin.sh\n```\n\n`verify` should report:\n\n- `package.json` present and parseable.\n- `dist/index.js` present and non-empty.\n- `dist/styles.css` present (if `styles` is declared).\n- Zero `process.env` references in the bundle.\n- Each declared `activationMethod` name appears in the bundle.\n- Bundle size under your project's threshold.\n\n<!-- File: .ai/skills/redis-insight-plugin/references/internal-vite-plugin.md (Tokens: ~1339 | Category: anthropic-skill) -->\n# Internal Vite Plugin\n\nBuild a Redis Insight plugin **inside** the RedisInsight monorepo. **This is the default for\nany contribution to this repo.** If you are not working inside the RedisInsight repo, use\n[external-parcel-plugin.md](external-parcel-plugin.md) instead.\n\n> **Start by copying a sibling.** Do not scaffold from scratch — copy an existing package such\n> as `geodata` or `redisearch` from `redisinsight/ui/src/packages/` and adapt it. Match the\n> surrounding conventions exactly rather than diverging.\n\n## Follow the repo styleguides\n\nInternal plugin code lives in the `redisinsight/ui/` tree, so it must follow the same\nstyleguides as the rest of the UI (these are mandatory, not optional):\n\n- **[frontend](../../frontend/SKILL.md)** — component folder structure\n  (`ComponentName/ComponentName.tsx` + `.styles.ts` + `.types.ts` + `.spec.tsx`), functional\n  components with hooks, **named exports**, barrel files (`src/components/index.ts`), layout\n  components (`Row` / `Col` / `FlexGroup`) instead of raw `div`, and theme usage.\n- **[redis-ui-components](../../redis-ui-components/)** — build all plugin UI from Redis UI\n  components via the `uiSrc/components/ui` wrappers; never import raw `@redis-ui/*`.\n- **[code-quality](../../code-quality/SKILL.md)** — no `any`, naming conventions, import order,\n  no magic numbers, no `!important`, semantic theme colors.\n- **[testing](../../testing/SKILL.md)** — Jest + Testing Library, `renderComponent`, `faker`,\n  `waitFor` over fixed waits.\n\n## Path\n\n```\nredisinsight/ui/src/packages/<plugin-name>/\n  package.json              # manifest (source/main/styles/visualizations) + scripts\n  index.html                # iframe entry with <div id=\"app\">\n  jest.config.cjs\n  tsconfig.json\n  public/                   # icon SVGs (copied to dist by the shared build)\n  src/\n    main.tsx                # exports activation functions (default export)\n    App.tsx / App.spec.tsx\n    components/             # ComponentName/ dirs + index.ts barrel\n    constants/             # index.ts barrel\n    types/                 # index.ts barrel\n    utils/                 # parsers + *.spec.ts\n    styles/styles.scss\n    global.d.ts\n    jest.setup.ts\n```\n\nSee any sibling under `redisinsight/ui/src/packages/` for a complete, current example\n(`redisearch` is a good table plugin; `geodata` is a good multi-visualization one).\n\n## Build Tool: Vite (shared config)\n\nThere is **no per-package `vite.config.ts`**. All internal plugins share a single config at\n`redisinsight/ui/src/packages/vite.config.mjs` and are listed in its `riPlugins` array. To add\na new plugin you register it there:\n\n```js\n// redisinsight/ui/src/packages/vite.config.mjs\nconst riPlugins = [\n  { name: 'redisearch', entry: 'src/main.tsx' },\n  // ...\n  { name: '<plugin-name>', entry: 'src/main.tsx' },  // add your plugin\n];\n```\n\nEach package's `package.json` wires the standard scripts:\n\n```json\n{\n  \"source\": \"./src/main.tsx\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"scripts\": {\n    \"dev\": \"vite -c ../vite.config.mjs\",\n    \"test\": \"node ../../../../../node_modules/.bin/jest -c jest.config.cjs\",\n    \"typecheck\": \"node ../../../../../node_modules/.bin/tsc --project tsconfig.json --noEmit\"\n  }\n}\n```\n\nThe shared build produces `dist/index.js` and `dist/styles.css` (and copies `public/` icons to\n`dist/`), all referenced from the manifest.\n\n## Manifest fields used in-repo\n\nIn addition to the base fields in [plugin-manifest.md](plugin-manifest.md), in-repo\nvisualizations commonly use:\n\n- `matchQuery.anyRegex` / `matchQuery.noneRegex` — refine matching beyond `matchCommands` (e.g.\n  only activate when a specific modifier flag is present in the command text).\n- `iconDark` / `iconLight` — `./dist/<icon>.svg` paths (source SVGs live in `public/`).\n\n## index.html\n\n```html\n<!doctype html>\n<html>\n  <head><meta charset=\"utf-8\" /></head>\n  <body class=\"theme_LIGHT\">\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./src/main.tsx\"></script>\n  </body>\n</html>\n```\n\n## Theme and Shared UI Caveats\n\n- Use Redis UI components and the theme tokens via the repo's `uiSrc/components/ui` wrappers per\n  the [redis-ui-components](../../redis-ui-components/) and [frontend](../../frontend/SKILL.md)\n  skills.\n- Detect iframe theme through the `theme_LIGHT` / `theme_DARK` body classes (or the\n  `redisinsight-plugin-sdk` theme helper).\n- Do not import from deep relative paths (`../../../../../`); use the monorepo's package alias.\n\n## Internal Plugin DO NOT\n\n- DO NOT introduce a Parcel build or a per-package `vite.config.ts` inside the monorepo. Register\n  in the shared `vite.config.mjs`.\n- DO NOT diverge from the runtime-dependency handling of sibling packages — copy how `geodata`\n  declares `react`/`react-dom` and externalization, don't invent your own.\n- DO NOT import from a sibling internal plugin — use shared utilities (`packages/common`) only.\n- DO NOT mutate global window state outside of the documented `window.state` surface.\n\n## When to Convert Internal → External\n\nIf the plugin is meant to ship outside RedisInsight (customer demo, field use, GitHub release),\nport it to an external Parcel layout. Keep the React component code; replace the build tool,\nbundle every dependency, and follow [external-parcel-plugin.md](external-parcel-plugin.md).\n\n<!-- File: .ai/skills/redis-insight-plugin/references/iterative-development.md (Tokens: ~1303 | Category: anthropic-skill) -->\n# Iterative Plugin Development\n\nBuild every Redis Insight plugin in three phases. The phases exist because each one isolates a different class of failure. Skipping is how you end up with a blank iframe and no idea why.\n\n## Phase 1 — Vanilla JS Wiring\n\nGoal: prove activation, props, and iframe rendering work. No React, no third-party libraries.\n\nUse the RedisInsight product UI baseline from `templates/external-styles.scss`; Phase 1 can be vanilla DOM without falling back to inline brand colors.\n\n```ts\n// src/main.tsx (or main.js)\nconst PREFIX = '[EXAMPLE_PLUGIN]';\n\nexport function renderExampleView(props: any) {\n  console.log(PREFIX, 'activated', props);\n  const root = document.getElementById('app');\n  if (!root) {\n    console.error(PREFIX, '#app missing');\n    return;\n  }\n  root.innerHTML = `\n    <section class=\"ri-plugin-shell\">\n      <div class=\"ri-plugin-panel\">\n        <header class=\"ri-plugin-header\">\n          <h2 class=\"ri-plugin-title\">Plugin works</h2>\n          <span class=\"ri-plugin-badge ri-plugin-badge--success\">Active</span>\n        </header>\n        <div class=\"ri-plugin-content\">\n          <code class=\"ri-plugin-command\">${escape(String(props?.command ?? ''))}</code>\n          <pre class=\"ri-plugin-code\">${escape(JSON.stringify(props?.data, null, 2))}</pre>\n        </div>\n      </div>\n    </section>\n  `;\n}\n\nfunction escape(s: string) {\n  return s.replace(/[&<>]/g, c => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;' }[c] as string));\n}\n\nexport default { renderExampleView };\n```\n\nFailure modes Phase 1 surfaces:\n\n- `activationMethod` name typo in the manifest.\n- Wrong `main` path or missing `dist/index.js`.\n- `process.env` references in the bundle.\n- Plugin folder not at `~/.redis-insight/plugins/<name>/`.\n- Insight not seeing the plugin (`/api/plugins`).\n\nIf Phase 1 doesn't render \"PLUGIN WORKS\" with the command and raw JSON, do not move on.\n\n## Phase 2 — React Rendering\n\nGoal: prove React mounts inside the iframe and displays props.\n\n```tsx\nimport * as React from 'react';\nimport { createRoot, type Root } from 'react-dom/client';\n\nconst PREFIX = '[EXAMPLE_PLUGIN]';\n\n// Workbench can re-activate the same iframe. Reuse one root per host element;\n// calling createRoot() twice on the same node warns and breaks rendering.\nconst roots = new WeakMap<HTMLElement, Root>();\n\nfunction getRoot(host: HTMLElement): Root {\n  let root = roots.get(host);\n  if (!root) {\n    root = createRoot(host);\n    roots.set(host, root);\n  }\n  return root;\n}\n\nfunction ExampleApp({ command, data }: { command: string; data: unknown }) {\n  return (\n    <section className=\"ri-plugin-shell\">\n      <div className=\"ri-plugin-panel\">\n        <header className=\"ri-plugin-header\">\n          <h2 className=\"ri-plugin-title\">Example Plugin</h2>\n          <span className=\"ri-plugin-badge ri-plugin-badge--success\">Active</span>\n        </header>\n        <div className=\"ri-plugin-content\">\n          <code className=\"ri-plugin-command\">{command}</code>\n          <pre className=\"ri-plugin-code\">{JSON.stringify(data, null, 2)}</pre>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport function renderExampleView(props: any) {\n  console.log(PREFIX, 'activated', props);\n  const host = document.getElementById('app');\n  if (!host) return;\n  try {\n    const root = getRoot(host);\n    root.render(<ExampleApp command={String(props?.command ?? '')} data={props?.data} />);\n  } catch (err) {\n    console.error(PREFIX, 'render failed', err);\n    host.innerHTML = '<div class=\"ri-plugin-error\">Plugin failed. See console.</div>';\n  }\n}\n\nexport default { renderExampleView };\n```\n\nIf you are on React 17, use `ReactDOM.render` instead of `createRoot`. Failure modes Phase 2 surfaces:\n\n- Wrong React/ReactDOM version pairing (React 17 + `createRoot` is the classic).\n- Externalized React in the bundle (`require is not defined`).\n- Missing `index.html` `#app` host.\n- Type errors not caught by the build.\n\n## Phase 3 — Full Feature\n\nGoal: real visualization library, real UX, real error/empty states.\n\nIn Phase 3:\n\n- Add the actual visualization library (charting, mapping, grid, etc.). See [third-party-libraries.md](third-party-libraries.md).\n- Parse the Redis response defensively. See [redis-command-parsing.md](redis-command-parsing.md).\n- Render empty and error states explicitly.\n- Persist user settings via SDK or `localStorage`.\n- Handle `theme_DARK` / `theme_LIGHT` switches.\n\nPhase 3 failures usually point to:\n\n- Unexpected response shape (use the parser patterns).\n- Library asset paths broken by bundling (icons, fonts, or images that resolve via bundler-relative URLs).\n- Missing CSS (`styles` field, CSS not bundled).\n- Cluster/multi-shard responses you didn't account for.\n\n## CI Pipeline Sketch\n\n```yaml\n# .github/workflows/plugin.yml (sketch)\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with: { node-version: '20' }\n      - run: npm ci\n      - run: npm run build\n      - run: bash scripts/verify-plugin.sh\n      - uses: actions/upload-artifact@v4\n        with:\n          name: plugin-bundle\n          path: |\n            package.json\n            dist/\n```\n\nOptionally extend with a Playwright job that boots a Redis Insight container, copies the artifact in, and runs a Workbench smoke test written per the repo's [e2e-testing](../../e2e-testing/SKILL.md) skill.\n\n<!-- File: .ai/skills/redis-insight-plugin/references/official-docs-summary.md (Tokens: ~705 | Category: anthropic-skill) -->\n# Redis Insight Plugin Docs — Summary\n\nCondensed from the official Redis Insight plugin docs:\n\n- https://github.com/redis/RedisInsight/tree/main/docs/plugins\n- https://github.com/redis/RedisInsight/blob/main/docs/plugins/development.md\n- https://github.com/redis/RedisInsight/blob/main/docs/plugins/installation.md\n\nAlways re-read the upstream docs when in doubt — the contract may evolve.\n\n## Where Plugins Run\n\n- Workbench renders a plugin inside an **iframe**.\n- The iframe loads the plugin's `index.html` (or equivalent), which loads the bundle declared in the manifest's `main`.\n- The iframe DOM has a `<div id=\"app\"></div>` host element. Activation functions render into `#app`.\n- Stylesheets declared by `styles` are injected into the iframe.\n\n## Manifest (`package.json`)\n\nA plugin's root `package.json` is the manifest. Required fields:\n\n- `name`, `version`, `description`\n- `main` — relative path to the built JS bundle (e.g. `./dist/index.js`).\n- `styles` — relative path to the built CSS (e.g. `./dist/styles.css`).\n- `visualizations[]` — list of visualization descriptors:\n  - `id` — stable identifier inside the plugin.\n  - `name` — visible label in the Workbench result tabs.\n  - `activationMethod` — exact name of the exported function called to render this view.\n  - `matchCommands` — array of Redis command names this visualization supports (e.g. `[\"XRANGE\", \"XREVRANGE\"]`).\n  - `description` — short summary shown in the UI.\n  - `default` — `true` if this should be the default tab; otherwise `false`.\n\n## Activation Function\n\nThe plugin's main script exports an object whose keys are activation function names:\n\n```ts\nexport default { renderMyView };\n```\n\nEach activation function is invoked by the Workbench host and receives a props object containing:\n\n- The Redis `command` that was executed.\n- The data **result array** (raw Redis reply).\n- Helpers via `redisinsight-plugin-sdk` (config, modules, theme, base URL, app version).\n\nThe `<body>` of the iframe carries a `theme_DARK` or `theme_LIGHT` class, and `window.state` exposes plugin-relevant config such as connected modules, base URL, and app version.\n\n## Installation Paths\n\nUser-installed plugins live under:\n\n- macOS / Linux: `~/.redis-insight/plugins/`\n- Windows: `C:\\Users\\{Username}\\.redis-insight\\plugins\\`\n\nEach plugin lives in its own subdirectory (folder name should match `name`).\n\n## Trust Warning\n\nPlugins execute arbitrary code inside Redis Insight. **Only install plugins from trusted sources.** Treat plugin code review with the same care you would use for any code that runs in your dev environment.\n\n## What This Skill Adds\n\nThe official docs describe the contract. This skill adds operational practice:\n\n- Phased Phase 1/2/3 development to surface failure modes early.\n- Parcel for external plugins, Vite for internal monorepo plugins.\n- Defensive command parsing across different commands and flag-dependent response shapes.\n- `curl /api/plugins` and Playwright smoke validation after every deploy.\n- Docker static-plugin workaround for `redis/redisinsight` images.\n\n<!-- File: .ai/skills/redis-insight-plugin/references/plugin-manifest.md (Tokens: ~899 | Category: anthropic-skill) -->\n# Plugin Manifest (`package.json`)\n\nThe plugin manifest is `package.json` at the plugin root.\n\n## Single Visualization\n\n```json\n{\n  \"name\": \"ri-plugin-xrange-table\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Render XRANGE results as a sortable table.\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"visualizations\": [\n    {\n      \"id\": \"xrange-table\",\n      \"name\": \"Stream Entries\",\n      \"activationMethod\": \"renderXRangeTable\",\n      \"matchCommands\": [\"XRANGE\", \"XREVRANGE\"],\n      \"description\": \"Tabular view of stream entries.\",\n      \"default\": false\n    }\n  ]\n}\n```\n\n## Multiple Visualizations\n\n```json\n{\n  \"name\": \"ri-plugin-stream-views\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Table and chart visualizations for Redis stream entries.\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"visualizations\": [\n    {\n      \"id\": \"stream-table\",\n      \"name\": \"Table\",\n      \"activationMethod\": \"renderTableView\",\n      \"matchCommands\": [\"XRANGE\", \"XREVRANGE\"],\n      \"description\": \"Tabular view of stream entries.\",\n      \"default\": false\n    },\n    {\n      \"id\": \"stream-chart\",\n      \"name\": \"Chart\",\n      \"activationMethod\": \"renderChartView\",\n      \"matchCommands\": [\"XRANGE\", \"XREVRANGE\"],\n      \"description\": \"Chart numeric fields over the stream's time range.\",\n      \"default\": false\n    }\n  ]\n}\n```\n\nThe plugin's main bundle exports both functions:\n\n```ts\nexport default {\n  renderTableView,\n  renderChartView,\n};\n```\n\n## Matching and Defaults\n\nWhen several visualizations share commands, make their defaults mutually exclusive by command shape, not just by command name. For example, one visualization can be the default when the result has a particular shape (numeric series, coordinates, nested rows, …), while another is available for the same commands but defaults only for a different shape (scalar, store-style, or empty results).\n\nRules for matcher definitions:\n\n- Match whole command tokens. Do not let a command match a longer command with the same prefix (e.g. a `*STORE` or `*_RO` variant).\n- Keep `matchCommands` broad only when `matchQuery` narrows the result shape safely.\n- Use `noneRegex` for exclusion only after checking how the platform normalizes command text.\n- Keep regexes bounded and linear. Avoid broad repeated token alternatives plus a trailing `[\\s\\S]{0,N}` window.\n- Test overlapping visualizations so only one `default: true` candidate remains for each command shape.\n\n## Icons\n\nOptional but supported:\n\n```json\n{\n  \"iconDark\": \"./dist/icon-dark.svg\",\n  \"iconLight\": \"./dist/icon-light.svg\"\n}\n```\n\nPlace icons under `dist/` (or another path your manifest points at) and copy them as part of deployment.\n\n## Stripping Dev-Only Fields\n\nThe deployed manifest must **not** include:\n\n- `scripts`\n- `devDependencies`\n- `targets` (Parcel-only build hints)\n- `husky`, `lint-staged`, `eslintConfig`, `prettier`, etc.\n\nEither keep a separate `package.deploy.json` or strip with `jq`:\n\n```bash\njq 'del(.scripts, .devDependencies, .targets, .husky, .[\"lint-staged\"])' \\\n  package.json > /tmp/package.deploy.json\n```\n\n`templates/deploy-external.sh` does this automatically.\n\n## Required Fields Recap\n\n- `name` — globally unique within `~/.redis-insight/plugins/`.\n- `version` — semver.\n- `description` — one sentence shown in Insight.\n- `main` — built JS bundle path.\n- `styles` — built CSS path (omit only if no styles).\n- `visualizations[]` — at least one, each with `id`, `name`, `activationMethod`, `matchCommands`, `description`, `default`.\n\nIf any required field is missing, Insight silently drops the plugin. Always run `curl /api/plugins` after deploying.\n\n<!-- File: .ai/skills/redis-insight-plugin/references/redis-command-parsing.md (Tokens: ~937 | Category: anthropic-skill) -->\n# Redis Command Parsing\n\nPlugins must defensively parse raw Redis responses. Different commands return very different\nshapes, and even the same command with different flags returns different shapes.\n\n## Same Command, Different Shapes by Flag\n\nA command's optional modifiers change the response shape mid-flight. A classic pattern: without\nmodifiers a command returns a flat array of values, but adding `WITH...`-style flags returns a\nnested array per entry:\n\n```json\n// no modifiers — flat list\n[\"member1\", \"member2\", \"member3\"]\n```\n\n```json\n// with extra-data flags — nested per entry\n[\n  [\"member1\", \"12.34\", [\"extra\", \"data\"]],\n  [\"member2\", \"56.78\", [\"extra\", \"data\"]]\n]\n```\n\nNever key parsing off the command name alone — branch on the actual runtime shape, and read the\ncommand string from the activation props to choose the right header/label.\n\n## Worked Example: XRANGE / XREVRANGE\n\n```\nXRANGE stream - +\n```\n\nReturns an array of `[id, [field, value, field, value, ...]]`:\n\n```json\n[\n  [\"1700000000000-0\", [\"temp\", \"22\", \"humidity\", \"55\"]],\n  [\"1700000001000-0\", [\"temp\", \"23\"]]\n]\n```\n\nParser sketch:\n\n```ts\nexport function parseXRange(data: any[]): { id: string; fields: Record<string, string> }[] {\n  if (!Array.isArray(data)) return [];\n  return data.flatMap(entry => {\n    if (!Array.isArray(entry) || entry.length < 2) return [];\n    const [id, kv] = entry;\n    if (typeof id !== 'string' || !Array.isArray(kv)) return [];\n    const fields: Record<string, string> = {};\n    for (let i = 0; i + 1 < kv.length; i += 2) {\n      const k = kv[i];\n      const v = kv[i + 1];\n      if (typeof k === 'string') fields[k] = String(v);\n    }\n    return [{ id, fields }];\n  });\n}\n```\n\nThe same defensive structure applies to any command: start from `Array.isArray`, destructure\nsafely, coerce-and-validate numbers, and drop malformed entries instead of throwing.\n\n## Preserve Raw Values and Units\n\nSome commands accept a unit or format argument and return values in that same unit/format (a\nradius unit, a count, a score precision, etc.). If the visualization assumes one unit but the\ncommand used another, labels and scaling will be wrong. Read the relevant token from the command\nstring and preserve the raw value before any internal normalization.\n\n## Defensive Parsing Rules\n\n- Always start with `if (!Array.isArray(data)) return [];`.\n- Never assume nested array length — destructure with `const [a, b, ...rest] = ...`.\n- Preserve argument positions when tokenizing commands. Empty quoted strings (`\"\"`, `''`) should become empty-string tokens, not disappear.\n- Treat numeric fields as strings; convert with `Number(...)` and validate with `Number.isFinite(...)`.\n- Reject blank numeric strings before conversion; `Number('')` is `0`.\n- Drop malformed entries silently; surface the count (`\"3 of 25 entries skipped\"`) in the UI rather than crashing.\n- Never throw inside a parser — return an empty array and let the activation function render an empty/error state.\n- Avoid `Math.max(...largeArray)` or other spreading of full result sets. Use `reduce` or loops for large responses.\n- When parsing `FT.*` or other option-rich command strings, tokenize first and honor command grammar. Do not strip option keywords (`PARAMS`, `SEARCH`, `FILTER`, …) just because those words appear inside a key, member, index name, or quoted query.\n\n## Response Shape Caveats\n\n- **Cluster mode** can return wrapped responses depending on Insight's transport.\n- **Modifier flags** change the response shape mid-flight — never key off command name alone.\n- **Empty results** are `[]`, but on some commands a missing key returns `null`. Treat both as empty.\n- **Module commands** (RedisJSON, RedisTimeSeries, RedisSearch, etc.) return their own shapes — only support what your `matchCommands` declares.\n- **Redis 8 / module responses** can be array-like, aggregate-like, or object/map-style. Handle array-specific branches before generic object branches because arrays are objects in JavaScript.\n\n<!-- File: .ai/skills/redis-insight-plugin/references/redis-insight-plugin-guidelines.md (Tokens: ~1825 | Category: anthropic-skill) -->\n# Redis Insight Plugin — Operational Guidelines\n\nLong-form operational reference. SKILL.md is the entry point; this file is for anything that doesn't fit there.\n\n## Plugin Types Overview\n\nTwo plugin shapes, two build tools, two deployment models.\n\n- **External standalone plugin** — bundles everything with **Parcel**. Ships as a folder you drop into `~/.redis-insight/plugins/<name>/`. No knowledge of the RedisInsight monorepo. Default for customer demos and field plugins.\n- **Internal monorepo plugin** — lives at `RedisInsight/redisinsight/ui/src/packages/<plugin-name>/`. Built with **Vite** as part of the RedisInsight repo. Ships embedded in Insight builds.\n\nIf you are unsure, build external. External works in every Redis Insight install; internal only ships with the Insight binary you build.\n\n## Official References\n\n- https://github.com/redis/RedisInsight/tree/main/docs/plugins\n- https://github.com/redis/RedisInsight/blob/main/docs/plugins/development.md\n- https://github.com/redis/RedisInsight/blob/main/docs/plugins/installation.md\n\n## Internal Plugin Development with Vite\n\n- Path: `RedisInsight/redisinsight/ui/src/packages/<plugin-name>/`.\n- Use Vite, not Parcel. Inherit RedisInsight's shared TS, theme, and component conventions.\n- Internal plugins **may** import from `uiSrc/` and shared components, but only when the import has a stable contract — internal layout helpers move often.\n- Avoid hard dependencies on `ThemeProvider` or other Insight-only providers in plugin code that might be reused externally later.\n\n## External Plugin Development with Parcel\n\n- Path: anywhere; deployed to `~/.redis-insight/plugins/<plugin-name>/`.\n- Bundle every dependency. Do not externalize React or any runtime library.\n- `targets.module.includeNodeModules: true` so Parcel inlines deps.\n- No imports from `uiSrc/`, `@redis-ui/*`, or any RedisInsight internal package.\n- Output to `dist/index.js` and `dist/styles.css`.\n\n## Package.json Requirements\n\nSee [plugin-manifest.md](plugin-manifest.md) for full examples. Minimum:\n\n```json\n{\n  \"name\": \"ri-plugin-example\",\n  \"version\": \"0.0.1\",\n  \"description\": \"...\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"visualizations\": [\n    {\n      \"id\": \"example-view\",\n      \"name\": \"Example View\",\n      \"activationMethod\": \"renderExampleView\",\n      \"matchCommands\": [\"XRANGE\"],\n      \"description\": \"Shows XRANGE results.\",\n      \"default\": false\n    }\n  ]\n}\n```\n\n## Plugin Entry Point\n\n```ts\nfunction renderExampleView(props) {\n  const root = document.getElementById('app');\n  if (!root) {\n    console.error('[EXAMPLE_PLUGIN] #app element missing');\n    return;\n  }\n  try {\n    // Render here. Show empty state when data is empty.\n  } catch (err) {\n    console.error('[EXAMPLE_PLUGIN] render failed', err);\n    root.innerHTML = '<div class=\"ri-plugin-error\">Plugin failed. See console.</div>';\n  }\n}\n\nexport default { renderExampleView };\n```\n\n## Testing & Verification\n\n- `npm run build` produces `dist/index.js` and `dist/styles.css`.\n- `templates/verify-plugin.sh` confirms file presence, no `process.env`, and that activation method names appear in the bundle.\n- Deploy with `templates/deploy-external.sh` (folder install) or `templates/deploy-internal-docker.sh` (Docker container).\n- After deploy, `curl http://localhost:5540/api/plugins` and grep for the plugin name.\n- Optional: add a Workbench smoke test following the repo's [e2e-testing](../../e2e-testing/SKILL.md) skill (page objects, fixtures, UI navigation) to validate iframe rendering.\n\n## Iterative Plugin Development\n\nSee [iterative-development.md](iterative-development.md). Always run Phase 1, then Phase 2, then Phase 3.\n\n## DO NOT Rules\n\nReproduced from SKILL.md for convenience:\n\n- DO NOT skip Phase 1 or Phase 2.\n- DO NOT deploy `index.js`/`styles.css` at the plugin root — they live in `dist/`.\n- DO NOT set `default: true` by default.\n- DO NOT include `scripts` or `devDependencies` in the deployed manifest.\n- DO NOT use Vite for standalone plugins.\n- DO NOT import `uiSrc` in standalone plugins.\n- DO NOT externalize React in standalone bundles.\n- DO NOT skip `/api/plugins` verification.\n- DO NOT ship `process.env.*` references.\n- DO NOT assume one Redis response shape.\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---------|--------------|-----|\n| Plugin missing from `/api/plugins` | Manifest not deployed, or wrong folder | Check folder is at `~/.redis-insight/plugins/<name>/` and contains `package.json` + `dist/`. |\n| Iframe blank | `activationMethod` name mismatch | Confirm exported function name == manifest field. |\n| `process.env is not defined` | `process.env.*` left in bundle | Replace at build time; rebuild; verify with `grep -c process.env dist/index.js`. |\n| `require is not defined` | Externalized React or wrong target | Use Parcel `targets.module` with `includeNodeModules: true`. |\n| Styles not applied | `styles` field missing or wrong path | Add `\"styles\": \"./dist/styles.css\"`; confirm file exists. |\n| Console error: `#app is null` | Activation ran before DOM | Ensure script is loaded as a module and renders into `document.getElementById('app')`. |\n| Visualization tab missing | `matchCommands` mismatch | Confirm command name (uppercase) appears in `matchCommands`. |\n\n## Lessons Learned\n\n- Phase 1 catches `activationMethod` name/manifest mismatches before any React is wired.\n- Phase 2 surfaces React/ReactDOM version and `createRoot` mismatches early.\n- Phase 3 exposes library asset-path bugs (icons/fonts/images broken by bundler-relative URLs) — fix once at init.\n- A command with extra-data flags often returns nested arrays, not objects — guard against shape changes.\n- Always `curl /api/plugins` after deploy. Cached menus lie.\n\n## Plugin API Reference\n\nActivation function props (subject to upstream changes — re-check official docs):\n\n- `command: string` — the executed Redis command.\n- `data: any[]` — the raw Redis result array.\n- `redisInsightPlugin` (via SDK) — `getState`, `setState`, `getModules`, `getTheme`, `getBaseUrl`, `getAppVersion`.\n\nUse `redisinsight-plugin-sdk` for state and theme rather than reading `window.state` directly when possible.\n\n## Command Parsing Patterns\n\nSee [redis-command-parsing.md](redis-command-parsing.md).\n\n## Third-Party Library Integration\n\nSee [third-party-libraries.md](third-party-libraries.md).\n\n## Interactive UI Patterns\n\n- Keep heavy UI inside the iframe; do not assume access to the host page.\n- Apply the [redis-ui-components](../../redis-ui-components/) skill with RedisInsight `light` / `dark` themes for visual plugin work (internal plugins use the `uiSrc/components/ui` wrappers per the [frontend](../../frontend/SKILL.md) styleguide).\n- Standalone plugins emulate product tokens locally with `src/styles/styles.scss`; copy `templates/external-styles.scss` as the baseline.\n- Use `document.body.classList.contains('theme_DARK')` to switch palettes.\n- Persist user settings via SDK state, falling back to `localStorage` keyed under your plugin id.\n- Avoid global CSS resets that fight Insight's iframe styles.\n\n## State Persistence\n\n- Prefer `redisinsight-plugin-sdk` `getState` / `setState` for cross-session persistence.\n- Fall back to `localStorage` with a plugin-prefixed key (`ri:<plugin-name>:settings`) when SDK state is unavailable.\n- Treat persisted state as untrusted; validate before use.\n\n## Error Handling & Stability\n\nSee [error-handling.md](error-handling.md).\n\n## Debugging & Development Tips\n\n- Open the Workbench iframe in browser devtools to inspect logs and network calls.\n- Look for `[<PLUGIN_PREFIX>]` log lines to filter your output.\n- Use the Phase 1 vanilla bundle to A/B test whether failures are plugin-host or library-related.\n- For Docker installs, `docker exec` into the container and inspect `/usr/src/app/redisinsight/api/dist/static/plugins/<name>/`.\n\n<!-- File: .ai/skills/redis-insight-plugin/references/redisinsight-product-ui.md (Tokens: ~633 | Category: anthropic-skill) -->\n# RedisInsight Product UI For Plugins\n\nUse the [redis-ui-components](../../redis-ui-components/) skill for visual decisions. This file\nonly adapts it to RedisInsight Workbench plugin iframes.\n\n> **Internal vs external.** For **internal** plugins (the default in this repo), build the UI\n> from Redis UI components via the `uiSrc/components/ui` wrappers and follow the\n> [frontend](../../frontend/SKILL.md) styleguide — use layout components, theme colors, and the\n> standard component folder structure. The \"Standalone Constraints\" below (no `@redis-ui/*` /\n> `uiSrc/` imports, local CSS variables) apply to **external standalone** plugins only, which\n> are bundled in isolation and must emulate the same look with local code.\n\n## Theme\n\n- RedisInsight plugins use `light` / `dark`, never `light2` / `dark2`.\n- Standalone external plugins cannot import RedisInsight internals, so define local CSS variables that emulate RedisInsight product tokens.\n- Read the iframe body class:\n  - `theme_LIGHT` => light theme\n  - `theme_DARK` => dark theme\n- If using `redisinsight-plugin-sdk`, prefer its theme helper when available, then mirror the same mode into local classes only when needed.\n\n## Baseline Files\n\nFor external Parcel plugins:\n\n- Copy `templates/external-styles.scss` to `src/styles/styles.scss`.\n- Keep `package.json` `\"styles\": \"./dist/styles.css\"`.\n- Keep the stylesheet bundled into `dist/styles.css`; RedisInsight injects it into the plugin iframe.\n\n## UI Rules\n\n- Build compact Workbench-like surfaces: toolbar, table, inspector, code/value panel, empty/error/loading states.\n- Prefer table-plus-detail layouts for Redis command responses.\n- Use `Source Code Pro` or monospace stacks for Redis keys, stream IDs, commands, raw values, and timestamps.\n- Use status badges with text and color; never rely on color alone.\n- Use product semantic colors for success, warning, danger, selected, and neutral states.\n- Keep controls stable: long keys and JSON must scroll, wrap intentionally, or ellipsize with a title/tooltip.\n\n## Standalone Constraints\n\n- Do not import `uiSrc/`, `@redis-ui/*`, RedisInsight monorepo packages, or private source.\n- Do not use global CSS resets that can fight the iframe host.\n- Scope styles under plugin classes such as `.ri-plugin-shell`.\n- Do not use Redis brand red as the universal accent, heading, CTA, error, or status color.\n\n## Verification\n\nCheck both themes:\n\n```bash\ncurl -s http://localhost:5540/api/plugins\n```\n\nThen run a matching Redis command in Workbench and verify:\n\n- `theme_LIGHT` and `theme_DARK` render correctly.\n- Empty, loading, error, and success states are styled.\n- Tables and `pre` blocks do not overflow the iframe incoherently.\n- Browser console has no style/runtime errors.\n\n<!-- File: .ai/skills/redis-insight-plugin/references/review-hardening.md (Tokens: ~1121 | Category: anthropic-skill) -->\n# Review Hardening\n\nUse this before opening or updating a Redis Insight plugin PR. It captures review issues that are easy for coding agents to miss because the happy path works.\n\n## Start From Review Discipline\n\n- Treat every bot or human comment as a hypothesis. Confirm it against code, tests, and Redis/Insight behavior before changing code.\n- Reproduce the failing shape with a focused test first. If the issue is valid, keep the regression test.\n- Prefer the narrowest verification scope that proves the change: package tests, parser tests, manifest matcher tests, and typecheck. Do not run a full RedisInsight build for every comment unless the changed surface requires it.\n- When replying to PR comments, state the behavior fixed, the test added, and the commit that contains it.\n\n## Manifest and Matcher Checklist\n\n- Match whole Redis command tokens, not prefixes. A command must not match a longer command with the same prefix (e.g. a `*STORE` or `*_RO` variant).\n- Keep multi-visualization defaults mutually exclusive for overlapping commands. If one view is the default for one result shape, another can be available but must not also be the blanket default for the same command shape.\n- If `matchQuery.anyRegex` / `noneRegex` is used, keep regexes bounded and linear. Avoid repeated broad alternatives followed by trailing `[\\s\\S]{0,N}`.\n- Make token alternatives disjoint: quoted strings, double-quoted strings, and unquoted tokens should not overlap in a way that creates backtracking spikes.\n- Test native command names, module command names, and store/scalar variants separately. Query/result rows and scalar/store commands often need different visualization defaults.\n\n## Parser Checklist\n\n- Tokenize the Redis command before inspecting options. Raw `indexOf(\"PARAMS\")`, `indexOf(\"SEARCH\")`, or `indexOf(\"FILTER\")` will misread keys, members, index names, and quoted query text.\n- Preserve empty quoted tokens in the tokenizer. `\"\"` and `''` should still occupy an argument position, even if the downstream parser later rejects that command shape.\n- Keyword positions matter. For `FT.HYBRID`, skip the command and index tokens when detecting `SEARCH` / `FILTER`, and skip `PARAMS` key/value ranges when parsing predicates.\n- Strip or ignore large option payloads only when the token is the actual option for the command grammar. Do not strip a key/member named like an option keyword, an index named like one, or query text containing one.\n- Preserve the raw unit/format returned by Redis (the token that follows the relevant option in the command string) before any internal normalization.\n- Validate blank numeric parts before calling `Number(...)`; JavaScript treats `Number(\"\")` as `0`.\n- Differentiate empty result sets from malformed/incomplete rows so the UI can show the right guidance.\n- Handle search-like arrays, aggregate-like arrays, and Redis 8 map-style objects before generic object fallback. Arrays are objects in JavaScript, so order matters.\n- Avoid `Math.max(...largeArray)` or similar spread over result sets. Reduce or loop to avoid call stack and argument limits.\n\n## Visualization Library Checklist\n\n- Validate library preconditions before rendering (non-empty series, valid bounds/ranges, sized container). Bail to an empty/error state instead of calling render APIs with bad input.\n- Do not let library callbacks (cell renderers, tooltip/popup builders, icon factories) capture stale React state. Keep live values in refs or recreate the instance when the callback dependencies change.\n- Build tooltip/popup/cell DOM with `textContent` or escaped React output, not interpolated HTML from Redis data.\n- Destroy/dispose the library instance before re-creating it on re-render to avoid leaks.\n- For large result sets, use loops/reductions and avoid per-render expensive parsing unless the input changed.\n- Use mode-aware empty/error titles. Different visualizations in the same plugin should not all show the same generic failure message.\n\n## Code Hygiene Checklist\n\n- Remove dead plugin config files and unused exports instead of leaving them for reviewers to rediscover.\n- Enforce declared limits or delete them. A `maxRows` / `maxPoints` constant that nobody reads is a bug magnet.\n- Centralize shared conversion constants/helpers so parsers and visualizations cannot drift.\n- Keep component-local types and large constants in sibling `.types.ts` and `.constants.ts` files when the component is already large.\n- Keep return types honest and simple. Do not use a tuple type when the function returns arbitrary-length arrays.\n- After discriminated-union narrowing, remove unreachable null/error branches. A defensive check that cannot run makes the real state model harder to review.\n- Memoize parsed command/results when parsing is nontrivial or feeds the visualization render.\n\n## Test Matrix\n\nAdd focused tests for any surface you touch:\n\n- Manifest matcher: exact command boundaries, default exclusivity, distinct result shapes (scalar vs row vs nested).\n- Command parser: keyword-like key/member/index names, empty quoted tokens, option keywords appearing in native commands and in query text, large payloads, empty rows, malformed rows, Redis 8 object responses.\n- Unit/format handling: commands that take a unit or format argument return values in that same unit/format; cover each variant the plugin supports.\n- Visualization state: control updates while rendered, empty/invalid input sets, distinct per-mode error titles.\n- Performance guardrails: large result counts, large payloads, and no spread into `Math.max`.\n\n<!-- File: .ai/skills/redis-insight-plugin/references/testing-and-deployment.md (Tokens: ~1140 | Category: anthropic-skill) -->\n# Testing and Deployment\n\nHow to deploy and verify a Redis Insight plugin, and how to smoke test it with Playwright (following the repo's [e2e-testing](../../e2e-testing/SKILL.md) skill).\n\n## Correct External Deployment Structure\n\n```\n~/.redis-insight/plugins/<plugin-name>/\n  package.json               # deployed manifest (no scripts, no devDependencies)\n  dist/\n    index.js\n    styles.css\n```\n\nThat's it. Nothing else is required at deploy time.\n\n## Wrong Structures (Common Mistakes)\n\n```\n# WRONG — bundle at plugin root\n~/.redis-insight/plugins/<plugin-name>/\n  package.json\n  index.js                   # should be in dist/\n  styles.css                 # should be in dist/\n```\n\n```\n# WRONG — nested package folder\n~/.redis-insight/plugins/<plugin-name>/\n  <plugin-name>/             # extra directory\n    package.json\n    dist/\n```\n\n```\n# WRONG — manifest still has dev/scripts\n{\n  \"main\": \"./dist/index.js\",\n  \"scripts\": { \"build\": \"...\" },        # remove\n  \"devDependencies\": { \"parcel\": \"...\" } # remove\n}\n```\n\n## Deploy Commands\n\nExternal (host install):\n\n```bash\nnpm run build\nbash templates/verify-plugin.sh\nbash templates/deploy-external.sh\n```\n\nInside Docker RedisInsight:\n\n```bash\nnpm run build\nbash templates/verify-plugin.sh\nbash templates/deploy-internal-docker.sh\n```\n\n## Docker RedisInsight: Static Plugin Path\n\nExternal plugin deployment to `~/.redis-insight/plugins/<name>/` works for the host install — that is the canonical path and the one to default to.\n\nFor Docker, the host plugins folder is not visible inside the container. To install a plugin into a running `redis/redisinsight` container, copy it into the container's static plugins folder:\n\n```\n/usr/src/app/redisinsight/api/dist/static/plugins/<plugin-name>/\n```\n\nWorkflow:\n\n```bash\ndocker cp dist redisinsight-test:/usr/src/app/redisinsight/api/dist/static/plugins/<plugin-name>/dist\ndocker cp package.json redisinsight-test:/usr/src/app/redisinsight/api/dist/static/plugins/<plugin-name>/package.json\ndocker restart redisinsight-test\n```\n\nThen verify via `/api/plugins`. The deploy script in `templates/deploy-internal-docker.sh` automates this.\n\n## /api/plugins Verification\n\nAfter every deploy:\n\n```bash\ncurl -s http://localhost:5540/api/plugins | jq '.[] | {name, visualizations}'\n```\n\nThe plugin must appear by `name`, with each declared `visualizations[*].id`. If absent:\n\n- The plugin folder is wrong, or\n- The manifest is invalid (missing required field), or\n- Insight wasn't restarted (Docker case).\n\n## Browser Console Verification\n\nOpen the Workbench iframe in browser devtools:\n\n- Filter logs by your `[<PLUGIN_PREFIX>]` prefix.\n- Confirm `activated` log fires when running a matching command.\n- Confirm no `process.env` errors, no `require is not defined`, no React mismatch.\n- Inspect network calls — there should be none unless your plugin explicitly makes them.\n\n## Playwright Smoke Test Strategy\n\nFor any Playwright E2E test, **follow the repo's [e2e-testing](../../e2e-testing/SKILL.md)\nskill** — it owns the conventions for this repo (tests live in `tests/e2e-playwright/`, use page\nobjects, fixtures, and `apiHelper`; never call `page.goto()` directly; no CSS selectors or fixed\nwaits). Do not hand-roll a standalone spec that bypasses those patterns.\n\nA plugin smoke test should cover the same flow, expressed through those conventions:\n\n1. Create/seed the database via `apiHelper` in `beforeAll`.\n2. Navigate to Workbench through a page object's `goto()` method (UI navigation, not `page.goto()`).\n3. Run a command that matches the plugin's `matchCommands`.\n4. Select the plugin's visualization tab.\n5. Assert the iframe is present and renders expected content.\n\nUse Playwright MCP to discover the actual `data-testid`/roles first, as the e2e-testing skill\ndescribes. The one plugin-specific check that does not need the UI is registration —\n`curl /api/plugins` (or `page.request.get('/api/plugins')`) must list the plugin.\n\n## Internal PR Verification Scope\n\nFor RedisInsight monorepo plugin changes, keep verification proportional to the changed surface:\n\n- Parser or command-shape fix: run the parser spec or package test file first, then the plugin package test command.\n- Manifest or matcher fix: run the matcher/plugin utility spec that exercises `matchCommands`, `matchQuery`, `anyRegex`, `noneRegex`, and `default` selection.\n- React / visualization-library fix: run the component spec for that visualization plus the package typecheck.\n- Code hygiene fix: run the closest affected spec plus typecheck, especially when moving types/constants or centralizing helpers.\n- Shared utility change under `ui/src/`: add the focused utility spec and a changed-file lint/typecheck check.\n- Before final commit on a PR batch, run the package tests, package typecheck, and `git diff --check`. Run a full RedisInsight build only when build configuration, package exports, bundling, or shared application wiring changed.\n\n<!-- File: .ai/skills/redis-insight-plugin/references/third-party-libraries.md (Tokens: ~689 | Category: anthropic-skill) -->\n# Third-Party Libraries\n\nNotes on integrating a visualization library (charting, mapping, grid, diagram, etc.) inside a\nRedis Insight plugin iframe. The principles are library-agnostic; apply them to whatever library\nthe plugin needs.\n\n## General Integration Rules\n\n- **Bundle the library fully** in an external (Parcel) plugin — do not externalize it. Internal\n  (Vite) plugins follow the shared build's dependency handling instead.\n- **Add the library's CSS to the bundle** (`dist/styles.css`); do not rely on a global `@import`\n  or a CDN. RedisInsight injects the bundled stylesheet into the plugin iframe.\n- **Initialize once, clean up on re-render.** Libraries that own a canvas, map, or grid instance\n  must be destroyed/disposed before re-creating, or they leak:\n\n  ```ts\n  instanceRef.current?.destroy();\n  instanceRef.current = createInstance(container, config);\n  ```\n\n- **Size the container explicitly.** Many libraries measure their parent on init; put the\n  render target in a sized flex/grid container so it lays out correctly inside the iframe.\n- **Validate inputs before handing them to the library.** Never assume parsed Redis data is\n  well-formed — guard for empty/NaN/out-of-range values, and check any library precondition\n  (valid bounds, non-empty series, etc.) before calling its render APIs.\n- **Avoid stale closures in library callbacks.** If a callback (cell renderer, tooltip builder,\n  icon factory) depends on React state, keep the live value in a ref or recreate the instance\n  when the dependency changes — captured state goes stale otherwise.\n- **Never interpolate Redis data into raw HTML.** Build tooltip/popup/cell DOM with\n  `textContent` or escaped React output so keys, members, and field values cannot inject markup.\n\n## Untyped Libraries: Custom `.d.ts`\n\nSome libraries (or their sub-plugins) ship without types. Add a local declaration instead of\nusing `any`:\n\n```ts\n// src/types/<lib-name>.d.ts\ndeclare module '<lib-name>' {\n  export interface Options {\n    /* the options the plugin actually uses */\n  }\n  export function createThing(target: HTMLElement, options?: Options): unknown;\n}\n```\n\nKeep the declaration minimal — only the surface the plugin uses.\n\n## Bundle Size Guidance\n\n- Aim for `dist/index.js` under ~1.5 MB minified for a snappy first render.\n- Use the bundler's analyzer (or `terser`'s reports) to find heavy dependencies.\n- Avoid `moment`; use `date-fns` or native `Intl.DateTimeFormat`.\n- Avoid full `lodash`; use targeted imports (`lodash/get`) or stdlib.\n- Tree-shaking only helps when imports are scoped (`import { sum } from 'lodash-es'`).\n- Cap row/point counts (paginate or virtualize) for large command results rather than handing\n  the whole set to the library at once.\n\n## Forbidden Imports\n\n- No `uiSrc/`, `@redis-ui/*`, or any RedisInsight monorepo internal in standalone plugins.\n- No Node-only modules (`fs`, `path`, `child_process`).\n- No CommonJS-only deps that fail in Parcel's `module` target without shims.\n\n<!-- File: .ai/skills/redis-insight-plugin/SKILL.md (Tokens: ~3235 | Category: anthropic-skill) -->\n---\nname: redis-insight-plugin\ndescription: Use when creating, modifying, debugging, deploying, or testing Redis Insight Workbench visualization plugins, plugin manifests, package.json visualizations, activationMethod functions, redisinsight-plugin-sdk usage, Parcel/Vite plugin builds, iframe rendering, Redis command parsing, Docker RedisInsight deployment, /api/plugins verification, or Playwright plugin validation.\nlicense: MIT\nmetadata:\n  author: redis\n  version: \"1.0.0\"\n---\n# Redis Insight Workbench Plugin\n\nBuild, deploy, and validate Redis Insight Workbench visualization plugins. Plugins render inside an iframe in Workbench and visualize the result of a Redis command. Trigger this skill for plugin manifests, `package.json` `visualizations`, `activationMethod` functions, `redisinsight-plugin-sdk` usage, Parcel/Vite plugin builds, iframe rendering, Redis command parsing, Docker RedisInsight deployment, `/api/plugins` verification, and Playwright plugin tests.\n\nUse the `redis-ui-components` skill for every visual plugin UI. RedisInsight plugins use the RedisInsight product theme pair: `light` / `dark`, not `light2` / `dark2`.\n\nOfficial source-of-truth references:\n\n- In-repo canonical docs: [docs/plugins/development.md](../../../docs/plugins/development.md), [docs/plugins/installation.md](../../../docs/plugins/installation.md), [docs/plugins/introduction.md](../../../docs/plugins/introduction.md)\n- Upstream: https://github.com/redis/RedisInsight/tree/main/docs/plugins\n\nSee [references/official-docs-summary.md](references/official-docs-summary.md) for a condensed summary.\n\n## Repo Code Conventions (internal plugins)\n\n> **You are inside the RedisInsight repo.** An internal plugin lives in the `redisinsight/ui/`\n> tree, so its code must follow the same styleguides as the rest of the UI — not ad hoc plugin\n> code. These rules are **mandatory** for any plugin code written here and override generic\n> external-plugin guidance below where they conflict:\n>\n> - **[frontend](../frontend/SKILL.md)** — component folder structure\n>   (`ComponentName/ComponentName.tsx` + `.styles.ts` + `.types.ts` + `.spec.tsx`), functional\n>   components with hooks, **named exports**, barrel files, layout components\n>   (`Row` / `Col` / `FlexGroup`) instead of raw `div`, and theme usage.\n> - **[redis-ui-components](../redis-ui-components/)** — build all plugin UI from Redis UI\n>   components. Import the internal `uiSrc/components/ui` wrappers; **never** import raw\n>   `@redis-ui/*`. (This skill is a symlink into the installed `@redis-ui/components` package,\n>   so it resolves after `npm install`; if it is missing, run install — the canonical source is\n>   `node_modules/@redis-ui/components/skills/redis-ui-components/`.)\n> - **[code-quality](../code-quality/SKILL.md)** — TypeScript everywhere (no `any`), naming\n>   (`PascalCase` / `camelCase` / `UPPER_SNAKE_CASE`), import order, no magic numbers, no\n>   `!important` in styles, semantic theme colors over CSS variables.\n> - **[testing](../testing/SKILL.md)** — Jest + Testing Library, the `renderComponent` helper,\n>   `faker` for test data, `waitFor` instead of fixed time waits.\n> - **[e2e-testing](../e2e-testing/SKILL.md)** — any Playwright/E2E test follows this skill\n>   (tests in `tests/e2e-playwright/`, page objects, fixtures, UI navigation; never `page.goto()`\n>   directly, no CSS selectors or fixed waits).\n>\n> External standalone plugins (below) are bundled in isolation and **cannot** import these\n> internals; they emulate the conventions with local code instead.\n\n## First Decision: Plugin Type\n\nDecide before scaffolding anything else.\n\n- **Internal monorepo plugin** — lives inside `redisinsight/ui/src/packages/<plugin-name>/` and ships with Redis Insight itself. Build with **Vite** (shared config). Follow the repo styleguides above. **This is the default for any contribution to this repo** — copy a sibling package such as `geodata` or `redisearch` rather than diverging.\n- **External standalone plugin** — installed by a user into `~/.redis-insight/plugins/<name>/`. Build with **Parcel**. Bundle all dependencies. Do not import from `uiSrc/` or any RedisInsight monorepo internal. Use this only for customer/field/demo plugins that ship *outside* this repo.\n\nSee [references/internal-vite-plugin.md](references/internal-vite-plugin.md) and [references/external-parcel-plugin.md](references/external-parcel-plugin.md).\n\n## External Plugin Structure\n\n```\n<plugin-name>/\n  package.json              # manifest + build scripts\n  src/\n    index.html              # iframe entry, has #app\n    main.tsx                # activation functions, default export\n    components/\n    styles/\n      styles.scss\n  dist/\n    index.js                # built bundle (referenced by manifest \"main\")\n    styles.css              # built styles (referenced by manifest \"styles\")\n```\n\n## Required Manifest\n\nTop-level `package.json` fields:\n\n- `name`\n- `version`\n- `description`\n- `main` — path to built JS (e.g. `./dist/index.js`).\n- `styles` — path to built CSS (e.g. `./dist/styles.css`).\n- `visualizations` — array of visualization descriptors.\n\nEach visualization descriptor must include:\n\n- `id`\n- `name`\n- `activationMethod`\n- `matchCommands`\n- `description`\n- `default`\n\nSet `default: false` unless the user explicitly asks for it to be the default visualization.\n\nThe `activationMethod` value must **exactly** match an exported function name in the bundle. The plugin entry must export that function via the default export:\n\n```ts\nexport default { renderMyView };\n```\n\nMultiple visualizations:\n\n```ts\nexport default {\n  renderTableView,\n  renderChartView,\n};\n```\n\nSee [references/plugin-manifest.md](references/plugin-manifest.md) for full examples and how to strip dev-only fields from the deployed manifest.\n\n## Activation Function Contract\n\nEvery activation function must:\n\n1. Get the host element: `const root = document.getElementById('app');`\n2. Defensively validate props (`command`, `data`, modules, theme).\n3. Wrap render logic in `try/catch` and render an error state on failure.\n4. Render an empty state when `data` is missing or empty.\n5. Log with a plugin-specific prefix, e.g. `[MY_PLUGIN]`, never bare `console.log`.\n\nSee [references/error-handling.md](references/error-handling.md).\n\n## RedisInsight Product UI Contract\n\nEvery plugin UI must follow RedisInsight product styling:\n\n- For **internal** plugins, build the UI from the [redis-ui-components](../redis-ui-components/) skill and the `uiSrc/components/ui` wrappers, following the [frontend](../frontend/SKILL.md) styleguide.\n- For **external** plugins, mirror the same Redis UI look with local code (you cannot import the internals).\n- Use RedisInsight `light` / `dark` product themes.\n- For standalone external plugins, emulate the RedisInsight product tokens with local CSS variables; do not import RedisInsight monorepo internals or `@redis-ui/*`.\n- Detect iframe theme through `theme_LIGHT` / `theme_DARK` body classes or SDK theme helpers.\n- Use compact product components: table, toolbar, segmented states, empty/error/loading panels, badges, and inspector-style details.\n- Keep Redis brand red for brand moments only; do not use it as the default plugin CTA, heading, error, or status color.\n\nSee [references/redisinsight-product-ui.md](references/redisinsight-product-ui.md) and use [templates/external-styles.scss](templates/external-styles.scss) as the baseline `src/styles/styles.scss`.\n\n## Mandatory Phased Workflow\n\nBuild every new plugin in three phases. Do not skip phases — the failure mode in each phase tells you exactly what is wrong.\n\n- **Phase 1 — Vanilla wiring.** No React, no third-party libraries. Render plain DOM that proves activation, props, and iframe rendering work end-to-end.\n- **Phase 2 — React rendering.** Add React + ReactDOM. Render a typed component that displays `command`, `status`, and the raw response.\n- **Phase 3 — Full feature.** Add the actual visualization library (charting, mapping, grid, etc.) and the real UX.\n\nSee [references/iterative-development.md](references/iterative-development.md) and the templates in `templates/`.\n\n## Review Hardening Loop\n\nBefore asking for review, run a small adversarial pass against the exact surfaces the plugin touches:\n\n- Manifest matching/defaults: exact command boundaries, no default visualization conflicts, no regex backtracking traps.\n- Redis command parsing: token-aware option handling, raw-unit preservation, malformed rows, empty rows, keyword-like key/member names.\n- Visualization state: stale closures, safe library bounds/inputs, large result sets, mode-specific empty/error copy.\n- Tests: red regression first, then the smallest package/component/parser test set that proves the fix.\n\nSee [references/review-hardening.md](references/review-hardening.md).\n\n## Build and Verify\n\n```bash\nnpm run build\ntest -f dist/index.js\ntest -f dist/styles.css                       # if \"styles\" is declared\ngrep -c \"process.env\" dist/index.js           # must be 0 in a Parcel build\n```\n\nConfirm each `activationMethod` name appears in the bundle:\n\n```bash\ngrep -o \"renderMyView\" dist/index.js | head\n```\n\n## Deploy\n\nExternal plugin → user plugins folder:\n\n```bash\nmkdir -p ~/.redis-insight/plugins/<plugin-name>\ncp package.json ~/.redis-insight/plugins/<plugin-name>/\ncp -R dist ~/.redis-insight/plugins/<plugin-name>/dist\n```\n\nRestart Redis Insight, then verify:\n\n```bash\ncurl -s http://localhost:5540/api/plugins\n```\n\nThe response must include the plugin `name` and its visualizations. See [references/testing-and-deployment.md](references/testing-and-deployment.md) for Docker workarounds, Playwright smoke tests, and the static-plugin path inside the Docker image.\n\n## Security Rules\n\n- Plugins execute code in the Insight UI process. Only ship code you control or trust.\n- Never embed secrets, tokens, or credentials in the bundle.\n- No hidden network calls. Document any outbound HTTP and prefer none.\n- Default to read-only Redis commands (`HGETALL`, `LRANGE`, `XRANGE`, `INFO`, `FT.SEARCH`). Never run destructive commands (`FLUSHDB`, `DEL`, `UNLINK`, `XTRIM`, `CLUSTER RESET`, `CONFIG SET`) without explicit user request and confirmation.\n\n## DO NOT\n\n- DO NOT skip Phase 1 or Phase 2; each catches a different class of failure.\n- DO NOT deploy `index.js` / `styles.css` at the plugin root. They live in `dist/` and the manifest points at `./dist/...`.\n- DO NOT set `default: true` on a visualization unless the user asked for it.\n- DO NOT include `scripts` or `devDependencies` in the deployed manifest. Strip them before copying.\n- DO NOT use Vite for standalone external plugins. Use Parcel.\n- DO NOT import from `uiSrc/`, `@redis-ui/*`, or any RedisInsight monorepo internal in a standalone plugin.\n- DO NOT externalize React in a standalone plugin bundle. Bundle React and ReactDOM.\n- DO NOT style plugin UI with ad hoc inline brand colors. Use RedisInsight product UI variables/classes.\n- DO NOT use `light2` / `dark2` for RedisInsight plugins; those are for other Redis product UIs.\n- DO NOT skip `/api/plugins` verification after deploying.\n- DO NOT ship `process.env.*` references in the bundle. Replace at build time.\n- DO NOT assume one Redis response shape — different commands, and even the same command with different flags (e.g. a `WITH...` modifier), return very different structures. See [references/redis-command-parsing.md](references/redis-command-parsing.md).\n\n## Final Checklist\n\n- Plugin type chosen and matches build tool (Parcel = external, Vite = internal).\n- `package.json` declares `main`, `styles`, and `visualizations` with required fields.\n- Every `activationMethod` matches a default-exported function.\n- Phases 1, 2, 3 each rendered successfully before moving on.\n- RedisInsight product UI applied via the `redis-ui-components` skill (internal plugins) or emulated locally (external plugins), with `light` / `dark` theme handling.\n- Review hardening pass completed for manifest matching, command parsing, visualization state, and scoped tests.\n- Bundle verified: `dist/index.js`, `dist/styles.css`, no `process.env`.\n- Plugin deployed to `~/.redis-insight/plugins/<name>/` (or via the Docker workaround).\n- `curl http://localhost:5540/api/plugins` lists the plugin.\n- Workbench runs a matching command and renders the visualization.\n- Defensive empty/error states verified.\n- Optional Playwright smoke test passes (written per the [e2e-testing](../e2e-testing/SKILL.md) skill).\n\n## Reference Index\n\n| File | Load When |\n|------|-----------|\n| [official-docs-summary.md](references/official-docs-summary.md) | Need the canonical contract from Redis Insight docs. |\n| [redis-insight-plugin-guidelines.md](references/redis-insight-plugin-guidelines.md) | Need the long-form operational reference. |\n| [external-parcel-plugin.md](references/external-parcel-plugin.md) | Building a standalone plugin with Parcel. |\n| [internal-vite-plugin.md](references/internal-vite-plugin.md) | Building inside the RedisInsight monorepo with Vite. |\n| [plugin-manifest.md](references/plugin-manifest.md) | Writing or stripping `package.json` manifests. |\n| [iterative-development.md](references/iterative-development.md) | Phase 1/2/3 templates and pipeline. |\n| [redisinsight-product-ui.md](references/redisinsight-product-ui.md) | Applying RedisInsight product UI inside plugin iframes. |\n| [review-hardening.md](references/review-hardening.md) | Pre-review checklist for matcher, parser, visualization state, and scoped regression tests. |\n| [testing-and-deployment.md](references/testing-and-deployment.md) | Deploy paths, Docker workaround, `/api/plugins`, Playwright. |\n| [redis-command-parsing.md](references/redis-command-parsing.md) | Parsing raw Redis command responses defensively. |\n| [third-party-libraries.md](references/third-party-libraries.md) | Integrating a visualization library, custom `.d.ts`, bundle size. |\n| [error-handling.md](references/error-handling.md) | Defensive render, ErrorBoundary, log prefixes. |\n\n<!-- File: .ai/skills/redis-insight-plugin/templates/external-parcel-package.json (Tokens: ~487 | Category: anthropic-skill) -->\n{\n  \"name\": \"ri-plugin-example\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Example external Redis Insight Workbench plugin built with Parcel.\",\n  \"source\": \"./src/main.tsx\",\n  \"main\": \"./dist/index.js\",\n  \"styles\": \"./dist/styles.css\",\n  \"scripts\": {\n    \"start\": \"parcel src/index.html\",\n    \"build\": \"concurrently \\\"npm run build:js\\\" \\\"npm run build:css\\\"\",\n    \"build:js\": \"parcel build src/main.tsx --no-source-maps --dist-dir dist --target module\",\n    \"build:css\": \"parcel build src/styles/styles.scss --no-source-maps --dist-dir dist\",\n    \"minify:js\": \"terser dist/index.js -o dist/index.js -c -m\",\n    \"clean\": \"rimraf dist\",\n    \"verify\": \"bash scripts/verify-plugin.sh\",\n    \"deploy:external\": \"bash scripts/deploy-external.sh\",\n    \"deploy:internal\": \"bash scripts/deploy-internal-docker.sh\"\n  },\n  \"targets\": {\n    \"main\": false,\n    \"module\": {\n      \"includeNodeModules\": true,\n      \"outputFormat\": \"esmodule\",\n      \"isLibrary\": false\n    }\n  },\n  \"visualizations\": [\n    {\n      \"id\": \"example-view\",\n      \"name\": \"Example View\",\n      \"activationMethod\": \"renderExampleView\",\n      \"matchCommands\": [\"XRANGE\"],\n      \"description\": \"Example visualization rendered for XRANGE results.\",\n      \"default\": false\n    }\n  ],\n  \"dependencies\": {\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\",\n    \"redisinsight-plugin-sdk\": \"^1.1.0\"\n  },\n  \"devDependencies\": {\n    \"parcel\": \"^2.12.0\",\n    \"terser\": \"^5.30.0\",\n    \"rimraf\": \"^5.0.5\",\n    \"concurrently\": \"^8.2.2\",\n    \"cross-env\": \"^7.0.3\",\n    \"typescript\": \"^5.4.0\",\n    \"@types/react\": \"^17.0.79\",\n    \"@types/react-dom\": \"^17.0.25\"\n  }\n}\n\n<!-- File: .ai/skills/testing/SKILL.md (Tokens: ~3089 | Category: anthropic-skill) -->\n---\nname: testing\ndescription: >-\n  Unit/integration testing standards for RedisInsight using Jest and\n  Testing Library: test structure, the `renderComponent` helper, faker\n  for test data, mocking patterns, and `waitFor` instead of fixed\n  time waits. Use when writing or modifying any `*.spec.ts` or\n  `*.spec.tsx` file, when adding component or slice tests, when\n  debugging flaky tests, or when the user mentions jest, testing\n  library, faker, or test patterns.\n---\n\n# Testing Standards and Practices\n\n## Core Principles\n\n- **Write tests for all new features**\n- Follow **AAA pattern**: Arrange, Act, Assert\n- Use **descriptive test names**: \"should do X when Y\"\n- **CRITICAL**: Never use fixed time waits - tests must be deterministic\n- **CRITICAL**: Use faker library (@faker-js/faker) for test data\n\n## Test Organization\n\n```typescript\ndescribe('FeatureService', () => {\n  describe('findById', () => {\n    it('should return entity when found', () => {});\n    it('should throw NotFoundException when not found', () => {});\n  });\n\n  describe('create', () => {\n    it('should create entity with valid data', () => {});\n    it('should throw error with invalid data', () => {});\n  });\n});\n```\n\n## Frontend Testing (Jest + Testing Library)\n\n### Running Specific Tests\n\n```bash\n# Run a specific test file\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs'\n\n# Run a specific test by name (use -t flag)\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs' -t 'test name pattern'\n\n# Example:\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/slices/tests/browser/keys.spec.ts' -c 'jest.config.cjs' -t 'refreshKeyInfoAction'\n```\n\n### CRITICAL: Always Use Shared `renderComponent` Helper\n\n**Create a `renderComponent` helper for each component test file**:\n\n```typescript\nimport { faker } from '@faker-js/faker';\n// Pull structured data from a shared factory (redisinsight/ui/src/mocks/factories/);\n// only build ad-hoc primitives/callbacks inline.\n\ndescribe('MyComponent', () => {\n  // Default props: factory for the entity, inline only for callbacks/primitives\n  const mockUser = UserFactory.build();\n  const defaultProps: MyComponentProps = {\n    user: mockUser,\n    onComplete: jest.fn(),\n  };\n\n  // Shared render helper\n  const renderComponent = (propsOverride?: Partial<MyComponentProps>) => {\n    const props = { ...defaultProps, ...propsOverride };\n\n    return render(\n      <Provider store={store}>\n        <MyComponent {...props} />\n      </Provider>\n    );\n  };\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n  });\n\n  it('should render component', () => {\n    renderComponent();\n    expect(screen.getByText(defaultProps.name)).toBeInTheDocument();\n  });\n\n  it('should handle click', async () => {\n    const mockOnComplete = jest.fn();\n    renderComponent({ onComplete: mockOnComplete });\n\n    fireEvent.click(screen.getByRole('button'));\n\n    await waitFor(() => {\n      expect(mockOnComplete).toHaveBeenCalledTimes(1);\n    });\n  });\n});\n```\n\n**Benefits**:\n\n- Centralized setup (providers, router, theme)\n- Default props defined once\n- Easy prop overrides per test\n- No duplicate setup code\n\n### Complex Component Setup\n\nFor components requiring Router, ThemeProvider, etc., include them in `renderComponent`:\n\n```typescript\nconst renderComponent = (propsOverride?: Partial<Props>) => {\n  const props = { ...defaultProps, ...propsOverride }\n\n  return render(\n    <Provider store={store}>\n      <BrowserRouter>\n        <ThemeProvider theme={theme}>\n          <Component {...props} />\n        </ThemeProvider>\n      </BrowserRouter>\n    </Provider>\n  )\n}\n```\n\n### Testing with Redux\n\nCreate a test store with `configureStore` for Redux-connected components:\n\n```typescript\nconst createTestStore = (initialState = {}) => {\n  return configureStore({\n    reducer: { user: userSlice.reducer },\n    preloadedState: initialState,\n  });\n};\n\nconst renderComponent = (propsOverride?: Partial<Props>, storeState = {}) => {\n  const testStore = createTestStore(storeState);\n  // render with testStore\n};\n```\n\n### Query Priorities (Testing Library)\n\n**Prefer accessible queries** (as users would interact):\n\n```typescript\n// ✅ PREFERRED\nscreen.getByRole('button', { name: /submit/i });\nscreen.getByLabelText('Email');\nscreen.getByPlaceholderText('Enter name');\n\n// ⚠️ LAST RESORT\nscreen.getByTestId('user-profile');\n\n// ❌ AVOID\nwrapper.find('.button-class');\n```\n\n### Testing Async Behavior\n\n```typescript\n// ✅ GOOD: waitFor with proper queries\nawait waitFor(() => {\n  expect(screen.getByText('Data loaded')).toBeInTheDocument();\n});\n\n// ✅ GOOD: waitForElementToBeRemoved\nawait waitForElementToBeRemoved(() => screen.queryByText('Loading...'));\n\n// ✅ GOOD: findBy queries (built-in waiting)\nconst element = await screen.findByText('Async content');\n\n// ❌ BAD: Fixed timeouts (flaky tests)\nawait new Promise((resolve) => setTimeout(resolve, 1000));\n```\n\n### Mocking API Calls (MSW)\n\nUse Mock Service Worker for API mocking:\n\n```typescript\nimport { rest } from 'msw';\nimport { setupServer } from 'msw/node';\n\nconst server = setupServer(\n  rest.get('/api/users/:id', (req, res, ctx) => {\n    return res(\n      ctx.json({\n        id: req.params.id,\n        name: faker.person.fullName(),\n      }),\n    );\n  }),\n);\n\nbeforeAll(() => server.listen());\nafterEach(() => server.resetHandlers());\nafterAll(() => server.close());\n```\n\n## Backend Testing (NestJS/Jest)\n\n### Service Test Pattern\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\n// Define factory for User entity\nconst userFactory = Factory.define<User>(() => ({\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n}));\n\ndescribe('UserService', () => {\n  let service: UserService;\n  let repository: Repository<User>;\n\n  const mockRepository = {\n    find: jest.fn(),\n    findOne: jest.fn(),\n    save: jest.fn(),\n    update: jest.fn(),\n    delete: jest.fn(),\n  };\n\n  beforeEach(async () => {\n    const module = await Test.createTestingModule({\n      providers: [\n        UserService,\n        {\n          provide: getRepositoryToken(User),\n          useValue: mockRepository,\n        },\n      ],\n    }).compile();\n\n    service = module.get<UserService>(UserService);\n    repository = module.get<Repository<User>>(getRepositoryToken(User));\n  });\n\n  afterEach(() => {\n    jest.clearAllMocks();\n  });\n\n  it('should return user when found', async () => {\n    const mockUser = userFactory.build();\n    mockRepository.findOne.mockResolvedValue(mockUser);\n\n    const result = await service.findById(mockUser.id);\n\n    expect(result).toEqual(mockUser);\n  });\n});\n```\n\n### Controller Test Pattern\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nconst userFactory = Factory.define<User>(() => ({\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n}));\n\ndescribe('UserController', () => {\n  let controller: UserController;\n  let service: UserService;\n\n  const mockService = {\n    findAll: jest.fn(),\n    findById: jest.fn(),\n    create: jest.fn(),\n  };\n\n  beforeEach(async () => {\n    const module = await Test.createTestingModule({\n      controllers: [UserController],\n      providers: [{ provide: UserService, useValue: mockService }],\n    }).compile();\n\n    controller = module.get<UserController>(UserController);\n  });\n\n  it('should return user from service', async () => {\n    const mockUser = userFactory.build();\n    mockService.findById.mockResolvedValue(mockUser);\n\n    const result = await controller.findById(mockUser.id);\n\n    expect(result).toEqual(mockUser);\n  });\n});\n```\n\n### Integration Tests (E2E)\n\n```typescript\ndescribe('UserController (e2e)', () => {\n  let app: INestApplication;\n\n  beforeAll(async () => {\n    const module = await Test.createTestingModule({\n      imports: [AppModule],\n    }).compile();\n\n    app = module.createNestApplication();\n    await app.init();\n  });\n\n  afterAll(async () => {\n    await app.close();\n  });\n\n  it('/users (GET)', () => {\n    return request(app.getHttpServer())\n      .get('/users')\n      .expect(200)\n      .expect((res) => {\n        expect(Array.isArray(res.body)).toBe(true);\n      });\n  });\n});\n```\n\n## E2E Testing (Playwright)\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nconst userDataFactory = Factory.define(() => ({\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n}));\n\ntest.describe('User Management', () => {\n  test('should create new user', async ({ page }) => {\n    const userData = userDataFactory.build();\n\n    await page.goto('/users');\n    await page.click('text=Add User');\n    await page.fill('[name=\"name\"]', userData.name);\n    await page.fill('[name=\"email\"]', userData.email);\n    await page.click('text=Submit');\n\n    // ✅ Use proper waits\n    await expect(page.locator(`text=${userData.name}`)).toBeVisible();\n  });\n});\n```\n\n## Best Practices\n\n### Always Use Faker for Test Data\n\n```typescript\n// ✅ GOOD: Use faker\nconst user = {\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n  age: faker.number.int({ min: 18, max: 100 }),\n};\n\n// ❌ BAD: Hardcoded data\nconst user = { id: '123', name: 'Test User' };\n```\n\n### Use Factories Instead of Static Mocks\n\n**Use Fishery** for creating test data factories with sensible defaults and overrides.\n\n**CRITICAL — UI factories live in `redisinsight/ui/src/mocks/factories/`**, organized by\ndomain (`<domain>/<TypeName>.factory.ts`), exported as `<TypeName>Factory`. Before writing\ntest data in a UI spec, **check for an existing factory there and reuse it**; if none fits,\n**add a new factory file in that directory** rather than inlining mocks in the spec.\n\n```typescript\n// ✅ GOOD: reuse the shared factory (redisinsight/ui/src/mocks/factories/database/DBInstance.factory.ts)\nimport { DBInstanceFactory } from 'uiSrc/mocks/factories/database/DBInstance.factory';\n\nconst instance = DBInstanceFactory.build();\nconst cluster = DBInstanceFactory.build({\n  connectionType: ConnectionType.Cluster,\n});\nconst many = DBInstanceFactory.buildList(5);\n\n// ✅ GOOD: a new factory file, when none exists yet\n// redisinsight/ui/src/mocks/factories/<domain>/<TypeName>.factory.ts\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nconst userFactory = Factory.define<User>(({ sequence }) => ({\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n  age: faker.number.int({ min: 18, max: 100 }),\n}));\n\n// Usage - flexible and reusable\nconst user1 = userFactory.build();\nconst user2 = userFactory.build({ age: 25 });\nconst user3 = userFactory.build({ name: 'Specific Name' });\nconst users = userFactory.buildList(5); // Create multiple\n\n// ❌ BAD: Static mock objects\nconst mockUser1 = {\n  id: '123',\n  name: 'User 1',\n  email: 'user1@test.com',\n  age: 30,\n};\n```\n\nBenefits of Fishery factories:\n\n- Easy to override specific properties per test\n- Consistent default values across tests\n- Single source of truth for mock structure\n- Better maintainability when types change\n- Built-in support for sequences and traits\n\n### Never Use Fixed Timeouts\n\n```typescript\n// ❌ BAD: Fixed timeout\nawait new Promise((resolve) => setTimeout(resolve, 1000));\nawait page.waitForTimeout(2000);\n\n// ✅ GOOD: Wait for condition\nawait waitFor(() => {\n  expect(element).toBeInTheDocument();\n});\n\nawait page.waitForSelector('[data-test=\"result\"]');\n```\n\n### Mock External Dependencies\n\n```typescript\n// ✅ GOOD: Mock services\njest.mock('uiSrc/services/api', () => ({\n  apiService: {\n    get: jest.fn(),\n    post: jest.fn(),\n  },\n}));\n```\n\n### Parameterized Tests with `it.each`\n\n**Use `it.each` for multiple tests with the same body but different inputs:**\n\n```typescript\n// ✅ GOOD: Parameterized tests\nit.each([\n  { description: 'null', value: null },\n  { description: 'undefined', value: undefined },\n  { description: 'empty string', value: '' },\n  { description: 'whitespace only', value: '   ' },\n])('should return error when input is $description', async ({ value }) => {\n  const result = await service.processInput(value);\n  expect(result.status).toBe('error');\n});\n```\n\n**Benefits:**\n\n- DRY: Single test body shared across all cases\n- Maintainability: Changes to test logic only need to be made once\n- Readability: Test cases are clearly defined in a table\n- Easier to extend: Adding new test cases is just adding a new row\n\n### Test Edge Cases\n\nAlways test:\n\n- Empty arrays/objects\n- Null/undefined values\n- Error scenarios\n- Boundary conditions\n- Loading states\n\n## Testing Checklist\n\n- [ ] All new features have tests\n- [ ] Tests use faker for data generation\n- [ ] No fixed timeouts (use waitFor)\n- [ ] Tests follow AAA pattern\n- [ ] Descriptive test names\n- [ ] Shared `renderComponent` helper used\n- [ ] Default props defined\n- [ ] Edge cases covered\n- [ ] Error scenarios tested\n- [ ] Mocks cleaned up between tests\n- [ ] Integration tests for API endpoints\n- [ ] E2E tests for critical flows\n- [ ] Coverage meets thresholds (80%+)\n\n<!-- File: .ai/skills/tsconfigs/SKILL.md (Tokens: ~1310 | Category: anthropic-skill) -->\n---\nname: tsconfigs\ndescription: >-\n  Locate and modify TypeScript configuration in RedisInsight. Use when adding\n  path aliases, introducing a new TS area, debugging webpack/ts-node/ESLint\n  path resolution, or the user asks about tsconfig, path mappings, or where\n  TypeScript is configured.\n---\n\n# TypeScript Configuration\n\nRedisInsight has no root `tsconfig.json`. Config is split per area, each owning its own paths, includes, and consumers. ESLint uses `parserOptions.project: true` to auto-discover the nearest tsconfig for each linted file.\n\n## Layout\n\n| File | Owns | Consumers |\n| - | - | - |\n| `redisinsight/ui/tsconfig.json` | UI source, `uiSrc/*`, `apiClient` paths | Vite (UI build), ESLint UI override, `npm run type-check:ui` |\n| `redisinsight/api/tsconfig.json` | API source, `src/*`, `tests/*` paths | NestJS build, ESLint API override |\n| `redisinsight/api/tsconfig.check.json` | Same as base + `strict: true` (with `strictPropertyInitialization` and `useUnknownInCatchVariables` off) and `noEmit: true` | `npm run type-check:api` only — kept separate so strict mode doesn't break `nest build`. See the `type-check-baselines` skill. |\n| `redisinsight/desktop/tsconfig.json` | Desktop source. Paths `desktopSrc/*`, `apiSrc/*`, `uiSrc/*`, `apiClient`, `apiClient/*` for TypeScript / IDE intellisense | ESLint for desktop files, TS language server |\n| `configs/tsconfig.json` | Compiler options (`module: CommonJS`, `esModuleInterop`) used by `ts-node` to load the `.ts` webpack configs | `ts-node` via `TS_NODE_PROJECT` set in `build:main` / `build:main:stage` / `build:stage` |\n| `.storybook/tsconfig.json` | Storybook framework files, extends UI tsconfig | Storybook + ESLint |\n| `stories/tsconfig.json` | Story files. Extends UI tsconfig; rewires `uiSrc/*` to resolve from `redisinsight/ui` | Storybook + ESLint |\n| `tests/e2e-playwright/tsconfig.json` | Playwright E2E suite | E2E runner (sub-project with own package.json, eslint-ignored at root) |\n\n## Webpack path resolution (`configs/webpack.config.base.ts`)\n\nWebpack uses explicit `resolve.alias` entries (not `tsconfig-paths-webpack-plugin`):\n\n```ts\nalias: {\n  desktopSrc: webpackPaths.desktopSrcPath,\n  apiSrc: resolve(webpackPaths.apiPath, 'src'),\n  uiSrc: webpackPaths.uiSrcPath,\n  apiClient: resolve(webpackPaths.riPath, 'api-client'),\n  // ...\n}\n```\n\nIf you add a new path alias used by desktop code, add it in **both** places: `redisinsight/desktop/tsconfig.json` (for TypeScript / ESLint / IDE) and `configs/webpack.config.base.ts` `resolve.alias` (for the electron bundle).\n\n## ts-node and webpack TS configs\n\nThe webpack configs in `configs/` are `.ts` files with type annotations. When `webpack --config ./configs/*.ts` runs, `ts-node` compiles them. Three package.json scripts set `TS_NODE_PROJECT=./configs/tsconfig.json` so ts-node uses the right compiler options:\n\n- `build:main`\n- `build:main:stage`\n- `build:stage`\n\n`configs/tsconfig.json` deliberately does not set `allowJs`. Any `.js` file inside `scripts/` or `configs/` must be CommonJS (`require` / `module.exports`) — Node 22's module syntax detection will treat ESM-syntax `.js` files as ESM and break references to `__dirname`. Two scripts converted for this reason: `scripts/prebuild.js`, `scripts/DeleteSourceMaps.js`.\n\n## ESLint\n\n`.eslintrc.js`:\n\n- Root `parserOptions.project: true` — auto-discovers the nearest tsconfig per file.\n- API override: `parserOptions.project: redisinsight/api/tsconfig.json` (explicit).\n- UI override: `parserOptions.project: redisinsight/ui/tsconfig.json` (explicit).\n- Files in `.storybook/`, `.github/`, etc. are not linted by `eslint .` because ESLint skips dot-prefixed paths during directory expansion.\n\nIf you add a top-level TS folder that ESLint will reach, drop a tsconfig in it or `parserOptions.project: true` will fail to find one.\n\n## Common tasks\n\n### Adding a path alias used by desktop / electron\n\n1. Add the alias to `redisinsight/desktop/tsconfig.json` `compilerOptions.paths`.\n2. If the alias is also used by UI code, add it to `redisinsight/ui/tsconfig.json`.\n3. If the alias is also used by API code, add it to `redisinsight/api/tsconfig.json`.\n4. For aliases used by the **electron bundle** (anything imported from `redisinsight/desktop/**`), also mirror the alias into `configs/webpack.config.base.ts` `resolve.alias`. Vite resolves UI aliases from `redisinsight/ui/tsconfig.json` automatically.\n\n### Adding a new top-level TS folder\n\nCreate a `tsconfig.json` in it. Bare minimum:\n\n```json\n{\n  \"extends\": \"../redisinsight/ui/tsconfig.json\",\n  \"include\": [\"**/*\"]\n}\n```\n\nWithout it, ESLint's `parserOptions.project: true` will error on those files.\n\n### Adding a new `.ts` webpack config under `configs/`\n\nNo action needed — `configs/tsconfig.json` includes `**/*`.\n\n### Adding a Node script that needs to run without ts-node\n\nPlace under `scripts/` as CommonJS (`require`/`module.exports`). Do not use ESM syntax — `configs/tsconfig.json` no longer has `allowJs` and Node 22 will load ESM-syntax `.js` files as ESM (no `__dirname`).\n\n<!-- File: .ai/skills/type-check-baselines/SKILL.md (Tokens: ~1445 | Category: anthropic-skill) -->\n---\nname: type-check-baselines\ndescription: >-\n  Run, refresh, or recover from RedisInsight's per-project TypeScript error\n  baselines (.tscheck.rec.json). Use when CI reports \"baseline is outdated\" or\n  \"more TS errors than previously recorded\", when the user mentions tscheck /\n  type-check / .tscheck.rec.json, when introducing or fixing TS errors, or when\n  reviewing PRs that touch baseline files.\n---\n\n# TS error baselines\n\nRedisInsight gates TypeScript errors per project via a one-way ratchet: current error counts are recorded in `.tscheck.rec.json` files, and CI fails if any (file × error-code) count increases. Counts can only go down.\n\n## Projects and commands\n\n| Project | tsconfig used | Baseline file | Per-project compare |\n| - | - | - | - |\n| UI | `redisinsight/ui/tsconfig.json` | `redisinsight/ui/.tscheck.rec.json` | `npm run type-check --prefix redisinsight/ui` |\n| API | `redisinsight/api/tsconfig.check.json` (strict, extends base) | `redisinsight/api/.tscheck.rec.json` | `npm run type-check --prefix redisinsight/api` |\n| Desktop | `redisinsight/desktop/tsconfig.json` | `redisinsight/desktop/.tscheck.rec.json` | `npm run type-check --prefix redisinsight/desktop` |\n| Configs | `configs/tsconfig.json` | — (must stay at 0 errors) | `npx tsc --project configs/tsconfig.json --noEmit` |\n\nRun all checks together from the repo root:\n\n- `npm run type-check` — compare against baselines (all four projects). E2E Playwright is type-checked by a separate workflow (`tests-e2e-playwright-lint.yml`) — not part of this.\n- `npm run tscheck` — refresh baselines for ui/api/desktop after fixing errors. Projects whose error count didn't change produce no diff.\n- `npm run tscheck:force` — force-overwrite baselines for ui/api/desktop. Emergencies only.\n\n**Always run refresh commands through the root `npm run tscheck` / `npm run tscheck:force` wrappers.** The per-workspace refresh scripts (`npm run tscheck --prefix redisinsight/<ws>`) shell out to `tsc`, `tsx`, and `tsc-output-parser`, which are installed only in the **root** `node_modules/.bin/` — this repo is not an npm workspace, so `npm run --prefix` only exposes the sub-dir's bin, not the root's. The root wrappers exist precisely to avoid that trap by running in the root context first. If you must invoke the per-package script directly, prepend the root bin dir manually: `PATH=\"$PWD/node_modules/.bin:$PATH\" npm run tscheck --prefix redisinsight/ui`.\n\n## API has a dedicated check tsconfig\n\n`redisinsight/api/tsconfig.check.json` extends the base `tsconfig.json` and adds:\n\n```json\n\"strict\": true,\n\"strictPropertyInitialization\": false,\n\"useUnknownInCatchVariables\": false,\n\"noEmit\": true\n```\n\nThe base `tsconfig.json` stays as-is so `nest build` is unaffected. **Never enable `strict` in the base** — it breaks the production build.\n\n## Workflows\n\n### CI says \"more TS errors than previously recorded\"\n\nYou introduced new errors. Fix them. Read the script output — it lists the file, error code (TSxxxx), and message for each new error. Do **not** run `tscheck:force` in any workspace to paper over them.\n\n### CI says \"baseline is outdated\"\n\nYou fixed errors (good). Refresh baselines from the repo root:\n\n```sh\nnpm run tscheck\n```\n\nThis runs the refresh for ui, api, and desktop; only the project whose count changed will produce a diff. Commit the updated `.tscheck.rec.json`.\n\n### Adding a brand-new file with TS errors\n\nSame rule: the file × error-code counts went from 0 to N — that's \"new errors\". Fix them before merging. Strict-mode escape hatches (`as any`, `// @ts-expect-error` with justification) are acceptable when fixing legitimately is out of scope, but prefer real fixes.\n\n### Bootstrapping a fresh baseline\n\nOnly needed once per project (already done for ui/api/desktop). The non-force `npm run tscheck --prefix redisinsight/<workspace>` calls `compare` first, which fails against an empty baseline. Use `npm run tscheck:force --prefix redisinsight/<workspace>` for the very first baseline only.\n\n### After `npm install` in `redisinsight/api/`\n\nThe api postinstall regenerates `redisinsight/api-client/`. That can shift UI and Desktop error counts (they both import from `apiClient`). If `npm run type-check:ui` or `npm run type-check:desktop` reports drift after an api install, refresh those baselines.\n\n### Local UI check disagrees with CI\n\nUI plugins under `redisinsight/ui/src/packages/{redisearch, redisgraph, redistimeseries-app, ri-explain, clients-list}` are sub-projects whose source gets type-checked via the UI tsconfig. Their deps live in nested `node_modules` populated by `npm run build:statics` (or by running `npm install --prefix redisinsight/ui/src/packages/<plugin>`). CI runs `npm run build:statics` before `npm run type-check:ui`, so the baseline reflects \"plugin deps installed.\"\n\nIf `npm run type-check:ui` shows TS7016 (\"Could not find a declaration file for module ...\") errors that CI doesn't, you're missing plugin deps. Run `npm run build:statics` once, then re-run the check. Don't refresh the baseline to your local state — CI runs with plugin deps installed.\n\n### Local Desktop check disagrees with CI\n\nDesktop type-check needs `redisinsight/api/dist/` populated with the **dev** nest build (`npm run build --prefix redisinsight/api`, not `build:prod` — prod skips `.d.ts` emission). CI does this automatically. Locally, build api once before generating or refreshing the desktop baseline.\n\n## Reviewing PRs\n\nReject PRs that:\n\n- Bump a `.tscheck.rec.json` file × code count **up** without a corresponding code fix.\n- Use `tscheck:force` in any workspace (look for the diff: a force overwrite typically touches many lines in the rec file with no related TS changes).\n- Enable `strict` in `redisinsight/api/tsconfig.json` (the base). Strict for api belongs only in `tsconfig.check.json`.\n\nApprove PRs that:\n\n- Leave counts unchanged.\n- Decrease counts (with the rec file updated and committed).\n\n<!-- ... +15 more modular skills available via /api/skills/redis/RedisInsight?path={path} -->\n</yakaai_skills>"}