# AI App runtime SDK

A Wink AI App is a single React module that runs inside Wink's sandboxed iframe
on a dashboard. The host injects data and tool APIs. The app must not call
invented HTTP endpoints, embed tokens, or talk to MCP.

This document is the contract for agents that write that module. You write the
code; Wink hosts it.

Read this before writing `App.jsx`. Also read `wink-toolbox://docs/wsql` and
`wink-toolbox://docs/ai-tools`.

## What an AI App is

- One React module with `export default` of a function component
- One primary WSQL query, delivered to the app as `useHostData('rows', { version: '1.2' })`
- Optional on-demand WSQL via the allow-listed tool `gspread_reports_execute_query`
  (same action as MCP `run_wsql_query`)
- Optional writes via `executeHostTool` (same actions as MCP `run_ai_tool`)
- Tailwind CSS and an import map (React, `react-dom/client`, optional npm packages via
  `esm.sh`)

It is not a separately hosted website. It has no Node, no local filesystem, and no
cookies.

## Host-injected APIs

The host injects these globals. Import them as if they already exist; do not redefine
them and do not call `window.postMessage` yourself.

### `useHostData(resource, options)`

```js
const BRIDGE_VERSION = '1.2';
const { data, loading, refetch } = useHostData('rows', { version: BRIDGE_VERSION });
```

- `options.version` is **required** and must be the literal string `'1.2'`. Without it
  the hook logs a warning and never loads data.
- `resource` is `'rows'` for the primary query.
- Returns `{ data, loading, refetch }`. Call `refetch()` after a successful write if
  the table should refresh.

When `version` is `'1.2'`, `data` is an object (not a bare array):

```ts
{
  rows: Array<Record<string, unknown>>;
  columns: Record<string, string>; // column name → type name (Decimal, Date, ...)
  previousPeriodRows: Array<Record<string, unknown>>;
  dateComparisonMap: Array<{ currentDate: string; previousDate: string | null }>;
  compareDateKey: string | null;
}
```

Row keys are the WSQL column names (human-friendly Title Case, including spaces).

**Preview vs dashboard:** a preview has no dashboard filters and no compare period.
`previousPeriodRows` and `dateComparisonMap` are empty until the app is placed on a
dashboard that has those configured. Do not assume they are populated in preview.

Handle loading and empty states. Do not render as if `data` is always present.

### `executeHostTool(actionName, inputs, { version })`

```js
const result = await executeHostTool(
  'some_action_name',
  { Field: 'value' },
  { version: '1.2' },
);
```

- `version` is required (`'1.2'`).
- `actionName` must be in the app's `requested_tools` **and** on the caller's agent
  token. Discover names with MCP `list_available_ai_tools` / `get_ai_tool_details`.
  Never invent names.
- `inputs` is a plain object matching the tool schema.
- With version `'1.2'`, the promise resolves to the tool's `result` payload (not the
  full envelope). Use `executeHostToolDetailed` if you need `{ executionUuid, result,
  logs, actionCount }`.

This is the in-app equivalent of MCP `run_ai_tool`. Writes are real: preview and
dashboard both run against live organisation data with the token owner's permissions.

### `openUrl(url)`

Opens `http:` / `https:` URLs in a new tab. Do not use it for Wink admin URLs, MCP
endpoints, or `javascript:` links.

## Module rules

- Single file. `export default` a React function component. No nested component
  declarations; extract helpers as functions, not components.
- No local path imports (`./foo`, `../bar`). Bare package names only, listed in the
  import map.
- Default import map already includes `react`, `react/jsx-runtime`, `react-dom`,
  `react-dom/client`. Extra packages go through `esm.sh` (for example
  `https://esm.sh/lucide-react@0.460.0`). Tailwind is already loaded; do not import a
  CSS framework.
- Do not import the host APIs; they are injected. Do not use `window.useHostData` or
  raw `postMessage`.
- Do not `fetch()` Wink, MCP, database, or invented REST URLs. Data comes from
  `useHostData` or `executeHostTool`.
- Prefer Tailwind utility classes unless the user asks for something else. Tailwind is
  already loaded; do not import another CSS framework. Avoid CSS modules,
  styled-components, and `<style>` tags unless requested.
- Keep the widget visually quiet: modest type, padding, rounded corners, light borders.
  Optional `lucide-react` for headers or metrics (add it to the import map if used).
- Stable list keys. No widget title in the module (the dashboard chrome owns the title).
- Keep the module small. The host compiles it with Babel (`react` preset) in the iframe.

## Primary WSQL

The app's primary query is stored with the app (MCP `wsql` / `wsql_options` / `limit` on
create and update). The host runs it and feeds object rows into `useHostData('rows')`.

- Discover datasources with `list_data_sources` / `get_data_source_detail`.
- Validate **and execute** with `run_wsql_query` before you bind UI to a shape. You
  need to have seen real rows, not only a successful validate.
- Do not invent slugs or column names. Preserve exact human-friendly column casing.
- If the app has no WSQL, do not call `useHostData`.

On-demand WSQL at runtime uses `executeHostTool('gspread_reports_execute_query', …)`
when that tool is on the token and requested by the app. Compose the query first;
pass `parameters` as the schema requires; parse the tool result. This is a live query
with the token owner's access.

**Preview URLs are live-data capabilities.** Anyone with the URL can run the snapshot
query and the app's requested tools as the token owner until the session expires. If
`gspread_reports_execute_query` is requested, the URL can run additional WSQL for that
TTL. Treat preview links as secrets.

## Suggested on-disk shape (documentation only)

This MCP does not write files. A convenient local layout:

```text
App.jsx          # export default function App() { ... }
query.wsql       # primary WSQL
wink-app.json    # app_id, revision, title, requested_tools, import_map
```

- `create_ai_app` is not idempotent. After create, write `app_id` and `revision` into
  `wink-app.json`.
- `update_ai_app` requires `expected_revision`. A 409 means someone else (or another
  session) updated the remote; pull with `get_ai_app` rather than overwriting.
- `list_ai_apps` omits source code. Call `get_ai_app` when you need the module.
- If create times out before you see `app_id`, call `list_ai_apps`. Do not create a
  second app.

## Workflow for coding harnesses

1. `list_data_sources` → `get_data_source_detail` → write and `run_wsql_query`
   (`validate`, then `execute`) until you have seen real rows and columns.
2. `list_available_ai_tools` → `get_ai_tool_details` for any writes. Put those names in
   `requested_tools`. Names not on the token are rejected.
3. Write one `App.jsx` that uses `useHostData('rows', { version: '1.2' })` and
   `executeHostTool(..., { version: '1.2' })`.
4. `create_ai_app` or `update_ai_app` (with `expected_revision`).
5. **Always preview before publishing.** `create_preview_session`, then open the URL
   in a browser (use a browser tool if you have one). `get_preview_status`:
   `pending` means the tab has not been opened; `error` means fix the module and
   preview again. Compile and runtime errors are common; do not skip this step.
6. Publish only to a **dashboard** (`list_dashboards` / `create_dashboard`, then
   `publish_ai_app`). Tell the user `dashboard_url` and `item_url` from the result
   so they can open the dashboard and the widget.

## What this MCP will not do

- It will not generate the React module for you (no `generate_ai_app`).
- `validate_ai_app` is contract linting only. Compile and runtime errors show up in
  the browser preview (`WIDGET_ERROR` with `phase: 'compile'` after the URL is opened).
- It will not silently overwrite a remote app. Stale `expected_revision` is a conflict.
