npm package: @kapy.dev/ui
Description: React hooks and components for [Kapy](https://kapy.dev). Fetch plans, run executions, and watch results update in real time — with built-in caching and automatic polling, similar to TanStack Query but without the dependency.
Latest version: 1.8.0

# @kapy.dev/ui

React hooks and components for [Kapy](https://kapy.dev). Fetch plans, run executions, and watch results update in real time — with built-in caching and automatic polling, similar to TanStack Query but without the dependency.

## Requirements

- React ≥ 17
- A running bridge endpoint from [`@kapy.dev/sdk`](https://www.npmjs.com/package/@kapy.dev/sdk)

## Installation

```bash
npm install @kapy.dev/ui
# or
pnpm add @kapy.dev/ui
```

## Setup

Wrap your app (or the relevant subtree) with `KapyProvider`, pointing it at your backend bridge endpoint:

```tsx
import { KapyProvider } from "@kapy.dev/ui";

export default function App() {
  return (
    <KapyProvider endpoint="/api/krl">
      <YourApp />
    </KapyProvider>
  );
}
```

The `endpoint` is the URL of the bridge you set up with `@kapy.dev/sdk`. It handles authentication server-side so your API key is never exposed to the browser.

## Hooks

### `useKapyPlans()`

Fetches and caches the list of research plans. Automatically re-fetches after any `execute` or `planAndExecute` call.

```tsx
import { useKapyPlans } from "@kapy.dev/ui";

function PlansList() {
  const { plans, loading, error, refetch } = useKapyPlans();

  if (loading) return <p>Loading…</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <ul>
      {plans?.map((plan) => (
        <li key={plan.id}>{plan.input_question} — {plan.status}</li>
      ))}
    </ul>
  );
}
```

**Returns:**

| Field | Type | Description |
|-------|------|-------------|
| `plans` | `PlanSummary[] \| null` | Cached plan list |
| `loading` | `boolean` | True while fetching |
| `error` | `string \| null` | Last error message |
| `refetch` | `() => void` | Force a fresh fetch |

---

### `useKapyExecution(planId, executionId)`

Fetches an execution and polls every 2 seconds while its status is `pending`. Stops polling once the status changes to `success` or `error`, then caches the result indefinitely.

```tsx
import { useKapyExecution } from "@kapy.dev/ui";

function ExecutionView({ planId, executionId }: { planId: number; executionId: number }) {
  const { execution, loading, error } = useKapyExecution(planId, executionId);

  if (loading && !execution) return <p>Loading…</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <div>
      <p>Status: {execution?.status}</p>
      <pre>{JSON.stringify(execution?.output, null, 2)}</pre>
    </div>
  );
}
```

Pass `null` or `undefined` for either argument to skip fetching (useful while IDs are not yet available).

**Returns:**

| Field | Type | Description |
|-------|------|-------------|
| `execution` | `Execution \| null` | Cached execution result |
| `loading` | `boolean` | True while fetching |
| `error` | `string \| null` | Last error message |

---

### `useKapyActions()`

Returns async action functions. After each call, the plans cache is automatically invalidated so `useKapyPlans` refetches on the next render.

```tsx
import { useKapyActions } from "@kapy.dev/ui";

function Controls() {
  const { execute, planAndExecute } = useKapyActions();

  const handleExecute = async () => {
    const { planId, executionId } = await execute(42, { userId: "u1" });
    console.log("Started execution", executionId, "on plan", planId);
  };

  const handleNew = async () => {
    const { planId, executionId } = await planAndExecute(
      "What drives user retention?",
      { segment: "enterprise" }
    );
    console.log("New plan", planId, "execution", executionId);
  };

  return (
    <>
      <button onClick={handleExecute}>Run existing plan</button>
      <button onClick={handleNew}>Create & run</button>
    </>
  );
}
```

**`execute(planId, input?)`**

Runs an existing plan. Returns `{ planId, executionId }`.

**`planAndExecute(question, input?)`**

Creates a new plan and immediately reserves an execution slot. Uses `kind: "auto"` and empty context. Returns `{ planId, executionId }`.

---

## Caching behaviour

| Hook | Cache key | Invalidation |
|------|-----------|--------------|
| `useKapyPlans` | Global (one per provider) | After every `execute` or `planAndExecute` call |
| `useKapyExecution` | `"planId:executionId"` | Never (final results are immutable); polling stops once status is no longer `pending` |

---

## TypeScript

All types are exported:

```ts
import type {
  PlanSummary,
  PlanStatus,
  PlanKind,
  Execution,
  ExecutionStatus,
  ExecutionOutput,
  RunnerNode,
  RunnerEdge,
} from "@kapy.dev/ui";
```

---

## Full example

```tsx
import { KapyProvider, useKapyPlans, useKapyExecution, useKapyActions } from "@kapy.dev/ui";
import { useState } from "react";

function Dashboard() {
  const { plans } = useKapyPlans();
  const { planAndExecute } = useKapyActions();
  const [ids, setIds] = useState<{ planId: number; executionId: number } | null>(null);
  const { execution } = useKapyExecution(ids?.planId, ids?.executionId);

  const run = async () => {
    const result = await planAndExecute("Top retention drivers", {});
    setIds(result);
  };

  return (
    <div>
      <button onClick={run}>Run new analysis</button>
      <p>Plans: {plans?.length ?? "…"}</p>
      {execution && <p>Status: {execution.status}</p>}
    </div>
  );
}

export default function App() {
  return (
    <KapyProvider endpoint="/api/krl">
      <Dashboard />
    </KapyProvider>
  );
}
```

## License

MIT