## 1. Project Overview & Quickstart (freelensapp/DEVELOPMENT)

# DEVELOPMENT

Open-source repository freelensapp/DEVELOPMENT

### Repository Details
- **Repository:** [freelensapp/DEVELOPMENT](https://github.com/freelensapp/DEVELOPMENT)
- **Primary Language:** Code

*Note: High-volume repository documentation is actively indexed and synchronized by YakaAI.*

## 2. Official Technical Reference & Guides (freelensapp/docs)

## File: README.md

# Contribute to this doc

First of all install `mkdocs`:
```shell
pip install mkdocs-material
```

Edit files and test it locally with:
```shell
mkdocs serve
```

Then open the URL printed in the terminal (default `http://127.0.0.1:8000/`).
Edits to `docs/` or `mkdocs.yml` hot-reload automatically.

## Port already in use

If `http://127.0.0.1:8000/` does not load (for example on Windows where
`wslrelay` may hold port 8000), pick another port with `--dev-addr`:
```shell
mkdocs serve --dev-addr 127.0.0.1:8765
```

## Strict build check

Before pushing, verify the build is clean (errors on broken nav links or
missing pages):
```shell
mkdocs build --strict
```

Enjoy!

---

## File: docs/extensions/main/app-preferences.md

# App preferences

## What is appPreferences?
App preferences allow you to add new menu entries to the global settings page.
You can access this page by clicking the sandwich menu in the top-left corner and selecting `Preferences`.
Alternatively, you can press `Ctrl + ,`

<figure markdown="span">
  
</figure>

You will see the new component here
<figure markdown="span">
  
</figure>

# How to use it
## Interface

This is the appPreferences interface
```javascript
appPreferences: AppPreferenceRegistration[] = [];
```

AppPreferenceRegistration is defined as follows
```javascript linenums="1"
interface AppPreferenceRegistration {
  title: string;
  id?: string;
  showInPreferencesTab?: string;
  components: AppPreferenceComponents;
}
```

## Hands on
To add a new setting menu entry in cluster settings just add this code of block to your `renderer/index.tsx` file

```javascript linenums="1"
appPreferences = [
  {
    title: "My Settings",
    components: {
      Input: () => HELLO!,
      Hint: () => Span message,
    },
  },
];
```

!!! warning "After compiling and installing the plugin, remember to close Freelens from the tray icon and reopen it, or you won’t see the new menu entries"

---

## File: docs/extensions/main/catalog-api.md

# Catalog API

!!! info "Available in Freelens 1.7+"

The Catalog API provides extensions with access to cluster information across all registered Kubernetes clusters. This enables building multi-cluster extensions that can enumerate clusters, react to cluster changes, and access cluster metadata.

## Renderer API

```typescript
import { Catalog } from "@freelensapp/core/renderer";
```

### getAllClusters()

Returns all registered clusters.

```typescript
const clusters = Catalog.getAllClusters();

for (const cluster of clusters) {
  console.log(`${cluster.name}: ${cluster.status}`);
}
```

### getClusterById(id)

Returns a specific cluster by ID, or `undefined` if not found.

```typescript
const cluster = Catalog.getClusterById("my-cluster-id");

if (cluster) {
  console.log(`Found: ${cluster.name}`);
}
```

### getActiveCluster()

Returns the currently active (selected) cluster.

```typescript
const active = Catalog.getActiveCluster();
console.log(`Viewing: ${active?.name}`);
```

## Main API

```typescript
import { Catalog } from "@freelensapp/core/main";
```

The main process API provides:

- `Catalog.getAllClusters()` - Get all clusters
- `Catalog.getClusterById(id)` - Get cluster by ID

!!! note
    `getActiveCluster()` is only available in the renderer process since the "active" cluster is a UI concept.

## Types

### ClusterId

A type alias for cluster identifiers. Use this with the [K8s API](./k8s-api.md) to perform operations on clusters:

```typescript
import { Catalog, K8s, ClusterId } from "@freelensapp/core/renderer";

const cluster = Catalog.getActiveCluster();
const clusterId: ClusterId = cluster.id;

// Use with K8s API
const pods = await K8s.queryCluster(clusterId, { apiVersion: "v1", kind: "Pod" });
```

### ClusterInfo

```typescript
interface ClusterInfo {
  readonly id: ClusterId;  // Use with K8s API
  readonly name: string;
  readonly kubeConfigPath: string;
  readonly contextName: string;
  readonly status: ClusterConnectionStatus;
  readonly labels: Record<string, string>;
  readonly isActive: boolean;
  readonly metadata?: ClusterMetadata;
}
```

### ClusterConnectionStatus

```typescript
enum ClusterConnectionStatus {
  CONNECTING = "connecting",
  CONNECTED = "connected",
  DISCONNECTED = "disconnected",
  DISCONNECTING = "disconnecting",
}
```

### ClusterMetadata

Optional metadata when available:

```typescript
interface ClusterMetadata {
  readonly distribution?: string;       // "eks", "gke", "minikube", etc.
  readonly kubernetesVersion?: string;  // "1.28.0"
  readonly nodeCount?: number;
}
```

## Reactivity

The API is MobX-reactive. Use `reaction()` to respond to changes:

```typescript
import { reaction } from "mobx";
import { Catalog } from "@freelensapp/core/renderer";

// React when active cluster changes
const dispose = reaction(
  () => Catalog.getActiveCluster(),
  (cluster, prev) => {
    console.log(`Switched from ${prev?.name} to ${cluster?.name}`);
  }
);

// Clean up when done
dispose();
```

!!! tip
    Wrap React components with `observer()` from `mobx-react` to automatically re-render when cluster data changes.

!!! warning
    Always call the dispose function returned by `reaction()` when your extension deactivates to prevent memory leaks.

---

## File: docs/extensions/main/entity-settings.md

# Entity settings

## What is entitySettings?
entitySettings allow you to add new menu entries inside the cluster settings page.
You can access this page by right-clicking on your cluster and selecting `Settings`.

<figure markdown="span">
  
</figure>

You will see the new component here
<figure markdown="span">
  
</figure>

# How to use it
## Interface
This is the entitySettings interface
```javascript
entitySettings: EntitySettingRegistration[] = [];
```

EntitySettingRegistration is defined as follows
```javascript linenums="1"
interface EntitySettingRegistration {
  apiVersions: string[];
  kind: string;
  title: string;
  components: EntitySettingComponents;
  source?: string;
  id?: string;
  priority?: number;
  group?: string;
}
```

## Hands on
To add a new setting menu entry in cluster settings just add this code of block to your `renderer/index.tsx` file

```javascript linenums="1"
entitySettings = [
  {
    apiVersions: ["entity.k8slens.dev/v1alpha1"],
    kind: "KubernetesCluster",
    title: "My settings",
    components: {
      View: () => HELLO!,
    },
    id: "my-custom-cluster-settings",
    group: "Settings",
  },
];
```

!!! warning "After compiling and installing the plugin, remember to close Freelens from the tray icon and reopen it, or you won’t see the new menu entries"

---

## File: docs/extensions/main/k8s-api.md

# K8s API

!!! info "Available in Freelens 1.8+"

The K8s API provides extensions with the ability to perform Kubernetes operations on any cluster by ID, without switching the UI context. This enables building multi-cluster extensions that can query, create, update, and delete resources across clusters.

## Quick Start

```typescript
import { K8s, Catalog } from "@freelensapp/core/renderer";

// Get pods from a specific cluster
const cluster = Catalog.getActiveCluster();
const pods = await K8s.queryCluster(cluster.id, {
  apiVersion: "v1",
  kind: "Pod",
  namespace: "default",
});

console.log(`Found ${pods.length} pods`);
```

## Renderer API

```typescript
import { K8s } from "@freelensapp/core/renderer";
```

### queryCluster()

List resources from a specific cluster.

```typescript
const pods = await K8s.queryCluster(clusterId, {
  apiVersion: "v1",
  kind: "Pod",
  namespace: "default",
  labelSelector: "app=nginx", // optional
});
```

### queryClusters()

Query multiple clusters in parallel.

```typescript
const results = await K8s.queryClusters([clusterId1, clusterId2], {
  apiVersion: "v1",
  kind: "Pod",
  namespace: "default",
});

for (const [clusterId, data] of results) {
  if (data instanceof Error) {
    console.error(`Cluster ${clusterId} failed:`, data.message);
  } else {
    console.log(`Cluster ${clusterId}: ${data.length} pods`);
  }
}
```

### queryAllClusters()

Query all connected clusters.

```typescript
const results = await K8s.queryAllClusters<Node>({
  apiVersion: "v1",
  kind: "Node",
});

for (const [clusterId, data] of results) {
  if (!(data instanceof Error)) {
    console.log(`Cluster ${clusterId}: ${data.length} nodes`);
  }
}
```

### getResource()

Get a single resource by name.

```typescript
const pod = await K8s.getResource(clusterId, {
  apiVersion: "v1",
  kind: "Pod",
  namespace: "default",
  name: "my-pod",
});

if (pod) {
  console.log(`Pod status: ${pod.status?.phase}`);
}
```

### applyOnCluster()

Create or update a resource.

```typescript
await K8s.applyOnCluster(clusterId, {
  apiVersion: "v1",
  kind: "ConfigMap",
  metadata: { name: "my-config", namespace: "default" },
  data: { key: "value" },
});
```

### deleteOnCluster()

Delete a resource.

```typescript
await K8s.deleteOnCluster(clusterId, {
  apiVersion: "v1",
  kind: "Pod",
  namespace: "default",
  name: "my-pod",
});
```

### patchOnCluster()

Patch a resource using strategic merge, JSON merge, or JSON patch.

```typescript
// Strategic merge patch (default)
await K8s.patchOnCluster(
  clusterId,
  {
    apiVersion: "apps/v1",
    kind: "Deployment",
    namespace: "default",
    name: "my-app",
  },
  { spec: { replicas: 3 } },
  "strategic",
);

// JSON merge patch
await K8s.patchOnCluster(
  clusterId,
  {
    apiVersion: "v1",
    kind: "ConfigMap",
    namespace: "default",
    name: "my-config",
  },
  { data: { newKey: "newValue" } },
  "merge",
);

// JSON patch
await K8s.patchOnCluster(
  clusterId,
  { apiVersion: "v1", kind: "Pod", namespace: "default", name: "my-pod" },
  [{ op: "add", path: "/metadata/labels/patched", value: "true" }],
  "json",
);
```

## Main API

```typescript
import { K8s } from "@freelensapp/core/main";
```

The main process API provides all the same functions:

- `K8s.queryCluster()`
- `K8s.queryClusters()`
- `K8s.queryAllClusters()`
- `K8s.getResource()`
- `K8s.applyOnCluster()`
- `K8s.deleteOnCluster()`
- `K8s.patchOnCluster()`

## Types

### ClusterId

All functions accept a `ClusterId` type for the cluster identifier. Import it from `K8s` and get values from the Catalog API:

```typescript
import { K8s, Catalog } from "@freelensapp/core/renderer";
import type { ClusterId } from "@freelensapp/core/renderer";

const cluster = Catalog.getActiveCluster();
const clusterId: ClusterId = cluster.id; // ClusterInfo.id is typed as ClusterId
```

Or more simply, since `ClusterInfo.id` is already typed as `ClusterId`:

```typescript
import { K8s, Catalog } from "@freelensapp/core/renderer";

const cluster = Catalog.getActiveCluster();
const pods = await K8s.queryCluster(cluster.id, { ... }); // cluster.id is ClusterId
```

### ResourceQuery

```typescript
interface ResourceQuery {
  apiVersion: string; // e.g., "v1", "apps/v1"
  kind: string; // e.g., "Pod", "Deployment"
  namespace?: string; // required for namespaced resources
  name?: string; // for single-resource operations
  labelSelector?: string; // e.g., "app=nginx,env=prod"
  fieldSelector?: string; // e.g., "status.phase=Running"
}
```

### KubeApiPatchType

```typescript
type KubeApiPatchType = "strategic" | "merge" | "json";
```

## Error Handling

Single-cluster operations throw errors on failure:

```typescript
try {
  await K8s.queryCluster(clusterId, query);
} catch (error) {
  console.error("Query failed:", error.message);
}
```

Multi-cluster operations (`queryClusters`, `queryAllClusters`) return per-cluster results:

```typescript
const results = await K8s.queryClusters(clusterIds, query);

for (const [id, data] of results) {
  if (data instanceof Error) {
    console.error(`Cluster ${id}:`, data.message);
  } else {
    console.log(`Cluster ${id}: ${data.length} items`);
  }
}
```

!!! warning "Cluster must be connected"
    Operations will fail with an error if the target cluster is not connected. Check `cluster.status === ClusterConnectionStatus.CONNECTED` before making API calls.

!!! tip "Namespace is required for namespaced resources"
    Always include `namespace` in your query for namespaced resources like Pods, Deployments, and ConfigMaps. Cluster-scoped resources like Nodes and Namespaces don't require it.

---

## File: docs/extensions/main/kube-object-detail-items.md

# Kube object detail items

## What is kubeObjectDetailItems?

kubeObjectDetailItems allows you to add a custom component in the drawer panel of a resource like a `Pod` or an `Event`.
You can see your component by clicking on a pod (or another supported resource kind) and scrolling to the bottom of the drawer.

<figure markdown="span">
  
</figure>

# How to use it
## Interface

This is the kubeObjectDetailItems interface

```javascript
kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
```

KubeObjectDetailRegistration is defined as follows

```javascript linenums="1"
export interface KubeObjectDetailRegistration<T extends KubeObject = KubeObject<KubeObjectMetadata<KubeObjectScope>, any, any>,> {
  kind: string;
  apiVersions: string[];
  components: KubeObjectDetailComponents<T>;
  priority?: number;
  visible?: IComputedValue;
}
```

## Hands on
To add a new custom component in the drawer menu, let’s create a new custom component.

```react linenums="1"
import { Renderer } from "@freelensapp/extensions";

import styleInline from "./example-details.scss?inline";
const {
  Component: { DrawerItem },
} = Renderer;

type KubeObject = Renderer.K8sApi.KubeObject;
type KubeObjectDetailsProps<TKubeObject extends KubeObject> = Renderer.Component.KubeObjectDetailsProps<TKubeObject>;

export const ExampleDetails = ({ object }: KubeObjectDetailsProps<Renderer.K8sApi.KubeObject>) => {

  return (
    <>
      <style>{styleInline}</style>
      
        <DrawerItem name="kind">
          { object.kind }
        </DrawerItem>
        <DrawerItem name="name">
          { object.metadata.name }
        </DrawerItem>
      
    </>
  );
}
```

And then just add this block of code to your `renderer/index.tsx` file.

```javascript linenums="1"
kubeObjectDetailItems = [
  {
    kind: "Pod",
    apiVersions: ["v1"],
    priority: 1,
    components: {
      Details: (props: KubeObjectDetailsProps<Renderer.K8sApi.KubeObject>) => <ExampleDetails {...props} />,
    },
  },
  {
    kind: "Event",
    apiVersions: ["v1"],
    priority: 1,
    components: {
      Details: (props: KubeObjectDetailsProps<Renderer.K8sApi.KubeObject>) => <ExampleDetails {...props} />,
    },
  },
];
```

You can see all the properties of kubeObject [here](https://github.com/freelensapp/freelens/blob/main/packages/kube-object/src/kube-object.ts).

---

## File: docs/extensions/main/lens-renderer-extension-class.md

# LensRendererExtension class

The `LensRendererExtension` class extends the base `LensExtension` class and serves as a central place to register various UI components and features for a FreeLens extension.
Each property in this class is an array that holds registrations for different types of UI elements or functionality. These registrations are then integrated into specific parts of the FreeLens UI. For example:

```javascript linenums="1"
export class LensRendererExtension extends LensExtension {
  globalPages: PageRegistration[] = [];
  clusterPages: PageRegistration[] = [];
  clusterPageMenus: ClusterPageMenuRegistration[] = [];
  clusterFrameComponents: ClusterFrameChildComponent[] = [];
  kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
  appPreferences: AppPreferenceRegistration[] = [];
  appPreferenceTabs: AppPreferenceTabRegistration[] = [];
  entitySettings: EntitySettingRegistration[] = [];
  statusBarItems: StatusBarRegistration[] = [];
  kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
  kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
  kubeWorkloadsOverviewItems: WorkloadsOverviewDetailRegistration[] = [];
  commands: CommandRegistration[] = [];
  welcomeMenus: WelcomeMenuRegistration[] = [];
  catalogEntityDetailItems: CatalogEntityDetailRegistration<CatalogEntity>[] = [];
  topBarItems: TopBarRegistration[] = [];
  additionalCategoryColumns: AdditionalCategoryColumnRegistration[] = [];
  customCategoryViews: CustomCategoryViewRegistration[] = [];
  kubeObjectHandlers: KubeObjectHandlerRegistration[] = [];

  ...
}
```

Going further in the doc, you can find an example for each of these.

---

## File: docs/extensions/reference/01-getting-started.md

# 1. Getting started

An extension is a normal npm package. It is built into CJS bundles, packed into
a `.tgz`, and installed into Freelens (Preferences → Extensions, or by dropping
the tarball in the extensions folder). This page covers the project layout,
`package.json` manifest, and the build configuration that makes it all work.

## 1.1 Project layout

The example extension uses this layout (yours can differ, but this is the
idiomatic shape):

```
src/
  main/index.ts            # main-process entry (CJS)  → out/main/index.js
  renderer/index.tsx       # renderer-process entry (CJS) → out/renderer/index.js
  renderer/api/<crd>/*.ts  # KubeObject/Api/Store model classes, one per CRD version
  renderer/pages/*.tsx     # cluster page components
  renderer/details/*.tsx   # detail-panel components
  renderer/menus/*.tsx     # object-menu action components
  renderer/icons/*.tsx     # SVG icon wrappers
  renderer/preferences/*   # preference input/hint components
  common/store/*           # shared ExtensionStore subclass + model
  common/utils.ts          # shared helpers
```

Build output goes to `out/`. Only `out/**/*` is shipped (`files` field).

## 1.2 The manifest (`package.json`)

Your `package.json` **is** the extension manifest. The host parses it into a
`LensExtensionManifest` (`installed-extension.ts`).

```jsonc
{
  "name": "@myorg/my-extension",
  "version": "0.1.0",
  "description": "What it does",
  "main": "out/main/index.js",        // main-process entry (optional)
  "renderer": "out/renderer/index.js", // renderer-process entry (optional)
  "files": ["out/**/*"],
  "engines": {
    "node": ">= 22.0.0",
    "freelens": "^1.8.0"              // compatibility gate — MAJOR.MINOR only
  },
  "storeName": "my-extension",         // optional: persisted-data folder name
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" }
}
```

Manifest fields the host reads (`LensExtensionManifest` in
`installed-extension.ts`):

| Field                | Type                 | Meaning                                                              |
| -------------------- | -------------------- | ------------------------------------------------------------------- |
| `name`               | `string`             | Extension id/name.                                                  |
| `version`            | `string`             | Extension version.                                                  |
| `description`        | `string?`            | Shown in the extensions list.                                       |
| `main`               | `string?`            | Path to the compiled main-process bundle. Omit for no main code.    |
| `renderer`           | `string?`            | Path to the compiled renderer bundle. Omit for no renderer code.    |
| `engines.freelens`   | `string`             | Semver range; only MAJOR.MINOR is considered for `isCompatible`.    |
| `storeName`          | `string?`            | Folder name for persisted data; defaults to `name`. Set it so a rename doesn't orphan stored data. |
| `publishConfig`      | `Partial<Record<string,string>>?` | npm publish config; carried through into the parsed manifest. |

Each entry module must `export default` the appropriate extension subclass.

## 1.3 Host-provided globals (do not bundle these)

These packages are provided by the Freelens host at runtime as globals. You
must **externalize** them, never bundle them, or you get duplicate React/MobX
instances and broken hooks/observables:

| Package                  | Host global               | Available in |
| ------------------------ | ------------------------- | ------------ |
| `@freelensapp/extensions`| `global.LensExtensions`   | main + renderer |
| `mobx`                   | `global.Mobx`             | main + renderer |
| `react`                  | `global.React`            | renderer     |
| `react/jsx-runtime`      | `global.ReactJsxRuntime`  | renderer     |
| `react-dom`              | `global.ReactDom`         | renderer     |
| `mobx-react`             | `global.MobxReact`        | renderer     |
| `react-router-dom`       | `global.ReactRouterDom`   | renderer     |

Because they are never bundled, declare them under **`devDependencies`** (the
example extension has an empty `dependencies` — everything host-provided is
dev-only). Any other dependency you use **is** bundled into `out/`.

## 1.4 Build configuration (`electron-vite`)

The example builds with `electron-vite` (Vite + Rollup/Rolldown). Key points:

- **Output format is CommonJS** for Freelens 1.x hosts.
- Decorators must be enabled for MobX `@observable`: the config turns on both
  `oxc.decorator { legacy: true, emitDecoratorMetadata: true }` and the Babel
  `@babel/plugin-proposal-decorators` (`version: "2023-05"`).
- **`globalExternals` shim** — a small custom Rollup plugin rewrites imports of
  the host packages above into ESM shims that read from the corresponding
  `global.*`, emitting explicit named exports:

  ```js
  const __m = global.React;
  export default __m;
  export const forwardRef = __m.forwardRef;   // …and every other named export
  ```

  The plugin discovers export names at config time by `require`-ing the real
  module (falling back to source parsing for browser-only packages like
  `@freelensapp/extensions`). This replaces the older `module.exports =
  global.React` shim, which only exposed `default` and caused intermittent
  `[MISSING_EXPORT]` failures under Rolldown.
- `VITE_PRESERVE_MODULES=false` produces a single-file production bundle
  (`build:production`); the default dev build preserves modules.
- SCSS modules generate `*.module.d.scss.ts` typing files via
  `vite-plugin-sass-dts`. Clean them with `pnpm clean:dts` when SCSS changes.

## 1.5 Common commands (from the example extension)

```bash
pnpm type:check       # tsc --noEmit
pnpm biome:check      # TS/TSX/JS/JSON/CSS/SCSS/HTML
pnpm trunk:check      # Markdown/YAML/etc.
pnpm test:unit        # vitest
pnpm build            # type-check + electron-vite build
pnpm build:production # single-file bundle (no preserveModules)
pnpm pack:dev         # bump prerelease, build, produce .tgz for install
pnpm clean:all        # dts + node_modules + out + tgz
```

## 1.6 Minimal entries

**Main** (`src/main/index.ts`) — often just bootstraps a store:

```ts
import { Main } from "@freelensapp/extensions";
import { MyStore } from "../common/store";

export default class MyMain extends Main.LensExtension {
  async onActivate() {
    await MyStore.getInstanceOrCreate().loadExtension(this);
  }
}
```

**Renderer** (`src/renderer/index.tsx`) — registers UI:

```tsx
import { Renderer } from "@freelensapp/extensions";

export default class MyRenderer extends Renderer.LensExtension {
  clusterPages = [ /* … */ ];
  clusterPageMenus = [ /* … */ ];
  kubeObjectDetailItems = [ /* … */ ];
  async onActivate() { /* bootstrap */ }
}
```

## 1.7 How the host installs and discovers your extension

Understanding this explains *why* the build produces a `.tgz`, why declared
`dependencies` behave differently from bundled code, and where to look when an
extension "won't load."

- **Discovery folder.** The host watches `~/.freelens/extensions`
  (`extension-discovery.ts` → `localFolderPath`). Any subdirectory containing a
  `package.json` is treated as an extension. Installing via Preferences →
  Extensions (or dropping a tarball) lands here.
- **Dependency install.** When an extension appears, the host runs **pnpm** to
  install that extension's own npm `dependencies` into a shared `node_modules`
  (`installExtension` → `forkPnpm("install", "--prefer-offline", "--prod",
  "--force")`); uninstall runs `forkPnpm("uninstall", "--force", <name>)`. This
  is the key reason the split matters:
  - **Host-provided globals** (react, mobx, `@freelensapp/extensions`, …) →
    `devDependencies`, externalized, never installed or bundled.
  - **Your other runtime deps** → they are **bundled into `out/`** by the
    build, so in practice the example extension keeps `dependencies` empty.
    Anything you *do* leave in `dependencies` will be pnpm-installed at
    install time.
- **Folder vs `.tgz`.** In a **production** host, if a
  `<name>-<version>.tgz` sits next to the manifest it is loaded from the
  tarball (`absolutePath = npmPackage`); otherwise the folder is used
  (`loadExtensionFromFolder`). This is why `pnpm pack:dev` produces a `.tgz`.
- **Compatibility gate.** `isCompatibleExtension` coerces
  `engines.freelens` to `^MAJOR.MINOR` and compares against the host version.
  This is the **only** version negotiation — there is **no** inter-extension
  dependency or API-version mechanism. An extension outside the range is marked
  incompatible and not activated.

Next: [Extension classes and lifecycle](./02-extension-classes.md).

---

## File: docs/extensions/reference/02-extension-classes.md

# 2. Extension classes and lifecycle

All three namespaces expose a `LensExtension` class. You subclass one per
process:

- `Main.LensExtension` (= `LensMainExtension`) — main-process code.
- `Renderer.LensExtension` (= `LensRendererExtension`) — renderer/UI code.
- `Common.LensExtension` — **type only**; the shared base. You never subclass
  this directly.

Both concrete classes extend the shared base `LensExtension`
(`lens-extension.ts`).

## 2.1 Shared base: identity and lifecycle

Available on both `Main.LensExtension` and `Renderer.LensExtension`:

### Identity (read-only getters)

| Member                 | Type                | Meaning                                                    |
| ---------------------- | ------------------- | ---------------------------------------------------------- |
| `id`                   | `string`            | Unique extension id.                                       |
| `name`                 | `string`            | `manifest.name`.                                           |
| `version`              | `string`            | `manifest.version`.                                        |
| `description`          | `string?`           | `manifest.description`.                                    |
| `storeName`            | `string`            | `manifest.storeName ?? name`; the persisted-data folder.   |
| `manifest`             | `LensExtensionManifest` | Parsed `package.json`.                                  |
| `manifestPath`         | `string`            | Path to the (symlinked) manifest.                          |
| `sanitizedExtensionId` | `string`            | `name` with `@`/`/` stripped; used for tab/DOM ids.        |
| `isEnabled`            | `boolean` (computed)| Whether the extension is currently enabled.                |

> There is **no** `isBundled` on the instance. Bundled status is tracked by the
> loader, not the extension object.

### Lifecycle methods

| Method / hook          | Signature                              | Notes                                                                 |
| ---------------------- | -------------------------------------- | --------------------------------------------------------------------- |
| `onActivate()`         | `protected (): Promise<void> \| void`  | **Override this.** Called when the extension activates. Bootstrap stores, register catalog sources, etc. May be `async`. |
| `onDeactivate()`       | `protected (): Promise<void> \| void`  | **Override this.** Called on disable/teardown. Undo side effects. May be `async`. |
| `getExtensionFileFolder()` | `(): Promise<string>`             | A per-extension writable folder on disk (obfuscated path).            |
| `enable()` / `disable()` / `activate()` | host-driven           | Managed by the host; you generally don't call these.                  |

`disable()` runs a set of registered disposers automatically, so anything you
register through the built-in APIs (e.g. `Ipc.listen`, catalog sources via the
provided helpers) is cleaned up for you.

### Protocol handlers (`freelens://` URLs)

Both classes expose:

```ts
protocolHandlers: ProtocolHandlerRegistration[];

interface ProtocolHandlerRegistration {
  pathSchema: string;                       // e.g. "/settings/:section"
  handler: (params: {
    search: Record<string, string | undefined>;
    pathname: Record<string, string | undefined>;
    tail?: string;
  }) => void;
}
```

Push handlers here to respond to deep links like
`freelens://extension/<name>/…`.

## 2.2 `Main.LensExtension` (main process)

Registration fields (arrays or MobX computed arrays), and helper methods:

| Member                    | Type                                                             | Purpose |
| ------------------------- | ---------------------------------------------------------------- | ------- |
| `appMenus`                | `MenuRegistration[] \| IComputedValue<MenuRegistration[]>`       | Items in the application menu bar. |
| `trayMenus`               | `TrayMenuRegistration[] \| IComputedValue` | Items in the OS system-tray menu. |
| `terminalShellEnvModifier?` | `ShellEnvModifier`                                             | Mutate env vars for terminals opened by Freelens. |
| `navigate(pageId?, params?, frameId?)` | `async`                                              | Navigate a renderer frame to an extension page. |
| `addCatalogSource(id, source)` | `(string, IObservableArray<CatalogEntity> \| IComputedValue<CatalogEntity[]>)` | Publish catalog entities. |
| `removeCatalogSource(id)` | `(string)`                                                       | Remove a previously-added source. |

See [Main registrations](./04-main-registrations.md) for the type shapes.

## 2.3 `Renderer.LensExtension` (renderer process)

This is the big one — nearly all UI extension points live here. Every field is
an array you assign as a class field (defaults to `[]`):

| Field                        | Registration type                          | Adds… |
| ---------------------------- | ------------------------------------------ | ----- |
| `globalPages`                | `PageRegistration[]`                       | Full pages outside cluster context (root frame). |
| `clusterPages`               | `PageRegistration[]`                       | Full pages inside a cluster (with cluster sidebar). |
| `clusterPageMenus`           | `ClusterPageMenuRegistration[]`            | Sidebar entries (and sub-items) in a cluster. |
| `clusterFrameComponents`     | `ClusterFrameChildComponent[]`             | Always-mounted components in the cluster frame (overlays/dialogs). |
| `appPreferences`             | `AppPreferenceRegistration[]`              | Preference control blocks. |
| `appPreferenceTabs`          | `AppPreferenceTabRegistration[]`           | New tabs in the Preferences window. |
| `entitySettings`             | `EntitySettingRegistration[]`              | Settings panes for a catalog entity (e.g. per-cluster). |
| `statusBarItems`             | `StatusBarRegistration[]`                  | Items in the bottom status bar. |
| `kubeObjectDetailItems`      | `KubeObjectDetailRegistration[]`           | Panels in a k8s object's details drawer. |
| `kubeObjectMenuItems`        | `KubeObjectMenuRegistration[]`             | Actions in a k8s object's context/action menu. |
| `kubeObjectHandlers`         | `KubeObjectHandlerRegistration[]`          | Hooks fired when a k8s object context menu opens. |
| `kubeWorkloadsOverviewItems` | `WorkloadsOverviewDetailRegistration[]`    | Sections on the Workloads Overview page. |
| `commands`                   | `CommandRegistration[]`                    | Command-palette commands. |
| `welcomeMenus`               | `WelcomeMenuRegistration[]`                | Actions on the Welcome page. |
| `catalogEntityDetailItems`   | `CatalogEntityDetailRegistration[]`        | Detail panels in the catalog entity detail view. |
| `topBarItems`                | `TopBarRegistration[]`                     | Items in the application top bar. |
| `additionalCategoryColumns`  | `AdditionalCategoryColumnRegistration[]`   | Columns in a catalog category list table. |
| `customCategoryViews`        | `CustomCategoryViewRegistration[]`         | Replacement view for a whole catalog category. |

Plus helper methods on the renderer subclass:

```ts
navigate(pageId?: string, params?: object): Promise<void>;   // go to one of THIS extension's pages
navigateToPreferences(): void;                                // open this extension's preferences tab
addCatalogFilter(fn: EntityFilter): Disposer;                 // filter which catalog entities show
addCatalogCategoryFilter(fn: CategoryFilter): Disposer;       // filter which catalog categories show
isEnabledForCluster(cluster: KubernetesCluster): Promise | boolean;
```

- `navigate` resolves the target against this extension's own
  `globalPages`/`clusterPages` and is a no-op if the `pageId` doesn't match one
  of them. (Note: the renderer signature is `(pageId?, params?)` — it has no
  `frameId` argument, unlike `Main.navigate`.)
- `navigateToPreferences()` always opens **this** extension's preferences tab,
  never the generic or another extension's preferences.
- `addCatalogFilter` / `addCatalogCategoryFilter` register filter functions and
  return a `Disposer`; the filter is also auto-removed when the extension is
  disabled.

The last one, `isEnabledForCluster(cluster)`, returns `false` to disable this
extension's cluster UI for a given cluster.

> **Deprecated.** In the current source `isEnabledForCluster` is marked
> `@deprecated` — prefer the per-registration `enabled` / `visible` properties
> combined with the active cluster (`Renderer.Catalog.activeCluster`). It still
> works, but new code should gate on those instead.

See [Renderer registrations](./03-renderer-registrations.md) for every type
shape and where each renders.

## 2.4 Namespace → class mapping

| Author import                   | Concrete class          |
| ------------------------------- | ----------------------- |
| `Main.LensExtension`            | `LensMainExtension`     |
| `Renderer.LensExtension`        | `LensRendererExtension` |
| `Common.LensExtension` (type)   | `LensExtension` (base)  |

---

## File: docs/extensions/reference/03-renderer-registrations.md

# 3. Renderer registrations

Every UI extension point is a field on your `Renderer.LensExtension` subclass.
This page documents the shape of each registration type and where it renders.
All `components.*` fields take `React.ComponentType`.

Common conventions:

- `visible?: IComputedValue` — MobX-reactive show/hide.
- `priority?: number` / `orderNumber?: number` — ordering (default 50 where used).
- `apiVersions: string[]` + `kind: string` — match against a k8s object/entity.
- `id?: string` — usually optional; auto-derived when omitted.

---

## 3.1 Pages

```ts
interface PageRegistration {
  id?: string;                    // unique per extension; part of the URL
  params?: PageParams;            // typed page URL params
  components: { Page: React.ComponentType<any> };
  enabled?: IComputedValue;
}

interface PageTarget {
  extensionId?: string;
  pageId?: string;
  params?: PageParams;
}
```

- `globalPages` render in the **root frame** (no cluster context).
- `clusterPages` render **inside a cluster frame**, alongside the cluster sidebar.

A page is reached via a sidebar menu entry (`clusterPageMenus`) whose `target`
points at the page `id`, or programmatically via navigation.

## 3.2 Cluster sidebar menu

```ts
interface ClusterPageMenuRegistration {
  id?: string;
  parentId?: string;              // nest under another menu item to make a sub-item
  target?: PageTarget;            // page to navigate to on click
  title: React.ReactNode;         // source type is StrictReactNode, not plain string
  components: { Icon?: React.ComponentType | null };
  visible?: IComputedValue;
  orderNumber?: number;
}
```

Example (from the example extension):

```tsx
clusterPageMenus = [{
  id: "example",
  title: Example.crd.title,
  target: { pageId: "example" },
  components: { Icon: ExampleIcon },
}];
```

## 3.3 Always-mounted cluster components

```ts
interface ClusterFrameChildComponent {
  id: string;
  Component: React.ElementType;
  shouldRender: IComputedValue;
}
```

Use for dialogs/overlays that must exist regardless of the current page.

## 3.4 Preferences

```ts
interface AppPreferenceRegistration {
  title: string;
  id?: string;
  showInPreferencesTab?: string;   // target a tab by id
  components: {
    Hint: React.ComponentType<any>;
    Input: React.ComponentType<any>;
  };
}

interface AppPreferenceTabRegistration {
  title: string;
  id: string;
  orderNumber?: number;
  visible?: IComputedValue;
}
```

`appPreferences` adds a control block; `appPreferenceTabs` adds a whole tab to
the Preferences window. The `Input` component typically binds to an
[`ExtensionStore`](./07-persistence-and-ipc.md) observable.

## 3.5 Entity settings

```ts
interface EntitySettingRegistration {
  apiVersions: string[];
  kind: string;
  title: string;
  components: { View: React.ComponentType<{ entity: CatalogEntity }> };
  source?: string;
  id?: string;                     // default btoa(title)
  priority?: number;               // default 50
  group?: string;                  // default "Extensions"
}
```

Adds a settings pane for a catalog entity — e.g. a per-cluster settings page.

## 3.6 Status bar

```ts
interface StatusBarRegistration {
  components?: {
    Item: React.ComponentType<StatusBarItemProps>;   // StatusBarItemProps = {}
    position?: "left" | "right";                      // default "right"
  };
  visible?: IComputedValue;
  item?: React.ReactNode | (() => React.ReactNode);   // @deprecated, use components
}
```

## 3.7 Kube object detail panels

```ts
interface KubeObjectDetailRegistration<T extends KubeObject = KubeObject> {
  kind: string;
  apiVersions: string[];
  components: { Details: React.ComponentType<KubeObjectDetailsProps<T>> };
  priority?: number;
  visible?: IComputedValue;
}

interface KubeObjectDetailsProps<T extends KubeObject> {
  className?: string;
  object: T;
}
```

Adds a panel to the details drawer of any k8s object matching `kind` +
`apiVersions`. Your `Details` component receives the object as `props.object`.
Example:

```tsx
kubeObjectDetailItems = [{
  kind: Example.kind,
  apiVersions: Example.crd.apiVersions,
  priority: 10,
  components: {
    Details: (props: Renderer.Component.KubeObjectDetailsProps<any>) =>
      <ExampleDetails {...props} extension={this} />,
  },
}];
```

## 3.8 Kube object menu items

```ts
interface KubeObjectMenuRegistration {
  kind: string;
  apiVersions: string[];
  components: { MenuItem: React.ComponentType };
  visible?: IComputedValue;
}

interface KubeObjectMenuItemProps<Object extends KubeObject = KubeObject> {
  object: Object;
  toolbar?: boolean;
}
```

Adds action items to an object's context/action menu.

## 3.9 Kube object context-menu handlers

For dynamic menu items (computed when the menu opens):

```ts
type KubeObjectHandlerRegistration = { apiVersions: string[]; kind: string }
  & RequireAtLeastOne<{ onContextMenuOpen: KubeObjectOnContextMenuOpen }>;

// KubeObjectOnContextMenuOpen is the FUNCTION type; its argument is typed
// KubeObjectOnContextMenuOpenContext:
type KubeObjectOnContextMenuOpen = (ctx: KubeObjectOnContextMenuOpenContext) => void;

interface KubeObjectOnContextMenuOpenContext {
  readonly menuItems: IObservableArray<KubeObjectContextMenuItem>;
  navigate: (location: string) => void;
}

interface KubeObjectContextMenuItem {
  id?: string;
  icon: string | BaseIconProps;   // string = Material icon name shorthand
  title: string;
  onClick: (obj: KubeObject) => void;
}
```

Push items into `ctx.menuItems` when the menu opens.

## 3.10 Workloads Overview sections

```ts
interface WorkloadsOverviewDetailRegistration {
  components: { Details: React.ComponentType<{}> };
  priority?: number;
  visible?: IComputedValue;
}
```

## 3.11 Command palette

```ts
interface CommandRegistration {
  id: string;                                        // globally unique
  title: string | ((ctx: CommandContext) => string);
  action: (ctx: CommandActionContext) => void;
  isActive?: (ctx: CommandContext) => boolean;       // default () => true
  scope?: "global" | "entity";                       // @deprecated, use isActive
}

interface CommandContext { readonly entity: CatalogEntity }
interface CommandActionContext extends CommandContext {
  navigate: (url: string, opts?: { forceRootFrame?: boolean }) => void;
}
```

## 3.12 Welcome menu

```ts
interface WelcomeMenuRegistration {
  title: string | (() => string);
  icon: string;
  click: () => void | Promise<void>;
}
```

## 3.13 Catalog entity detail panels

```ts
interface CatalogEntityDetailRegistration<T extends CatalogEntity> {
  kind: string;
  apiVersions: string[];
  components: { Details: React.ComponentType<{ entity: T }> };
  priority?: number;
}
```

## 3.14 Top bar

```ts
interface TopBarRegistration {
  components: { Item: React.ComponentType };
}
```

## 3.15 Catalog category columns and views

```ts
interface CategoryColumnRegistration {
  priority?: number;                                    // default 50
  id: string;                                           // unique to your extension
  renderCell: (entity: CatalogEntity) => React.ReactNode;
  titleProps: { className?: string; title: React.ReactNode; "data-testid"?: string };
  sortCallback?: (entity: CatalogEntity) => string | number | (string | number)[];
  searchFilter?: (entity: CatalogEntity) => string | string[];
}

interface AdditionalCategoryColumnRegistration extends CategoryColumnRegistration {
  kind: string;    // e.g. "KubernetesCluster"
  group: string;   // e.g. "entity.k8slens.dev"
}

interface CustomCategoryViewRegistration {
  kind: string;
  group: string;
  priority?: number;                                    // default 50
  components: { View: React.ComponentType<{ category: CatalogCategory }> };
}
```

`additionalCategoryColumns` adds columns to an existing category list;
`customCategoryViews` replaces the whole view for a category.

---

## Note: two registrations that do NOT exist

Older Lens tutorials mention `kubeObjectStatusTexts` and
`kubeObjectListLayoutColumns`. **These are not present in this Freelens
version** — do not use them. To add columns to a resource list, render your own
`KubeObjectListLayout` inside a `clusterPage`
(see [Kubernetes API](./06-kubernetes-api.md)).