# 🤖 Views — a bot shows you data, in place

Status: **built** (branch `thread-views`). The evolution of the terse
service-op notice ("Done — issues.list on github returned 50 items"): when a
bot has list-shaped data worth *looking at* — GitHub issues, news headlines,
search results — it can now create a **view** in the chat where you asked.

## What a view is (and isn't)

A view is **schema + data, never code**. It is:

- **A surface the current chat carries.** Not an app, not a separate chat, no
  sidebar entry. It opens from an **Open view** chip on the bot's reply as a
  full-height right-hand column (the command panel's `side` placement — a
  vertical sidebar suits list-shaped content; narrow screens fall back to the
  in-flow band), and dismisses like any panel.
- **Thread state, like everything else.** A view rides the thread's own
  encrypted log (`view.create` / `view.update` / `view.remove` events), folded
  client-side by the `views` registry app and covered by the ordinary app-state
  snapshots — so it syncs to every member and every device, and any bot or app
  in the chat can read or replace it. The server stores ciphertext, as ever.
- **Rendered by the app schema.** The stored spec is expanded (a pure
  function, `viewSchema`) into a real app-schema list that the shared
  schema-engine renders — the same machinery that draws To-do and Reminders on
  web and native. Views inherit search, sections, and rows for free, and the
  native runtime can render them the day it adds the same expansion.

Why schema+data instead of letting the model write a renderer: the engine is
reviewed platform code, so a model-authored view is *inert data* — it can't
fetch, can't script, can't touch the DOM. That's why views need no sandbox
(unlike member apps, which are code and get the opaque-origin iframe).

## The spec (what a bot sends)

```jsonc
{
  "type": "view.create",        // or view.update (replace by id), view.remove
  "id": "vw-a1b2c3",
  "title": "Top GitHub issues", // ≤ 80 chars
  "glyph": "▤",                 // optional, ≤ 4 chars
  "items": [                    // 1..50
    {
      "id": "1",                // optional; assigned when absent
      "title": "Crash on login",     // required, ≤ 300
      "subtitle": "#412 · opened by amy", // optional, ≤ 300
      "note": "Prio: urgent",         // optional, ≤ 500
      "url": "https://github.com/…",  // optional, https only, ≤ 600
      "accent": true                  // optional; tints the subtitle
    }
  ]
}
```

Bounded twice, fail-closed both times: the sidecar validates the model's
`create_view` tool input (`internal/bot/view.go`, modeled on `normalizeCard`)
before anything posts, and the client fold re-validates every event
independently (`web/static/apps/views.js`) — payloads aren't sender-signed, so
the fold trusts nothing: strings clamped, non-https URLs dropped, ≤ 50 items,
whole spec ≤ 32 KB, ≤ 20 live views per thread (oldest evicted). A row's tap
opens its `url` in a new tab (`rel=noopener`); rows without one aren't
tappable. Everything renders via `textContent` — a view can display data and
link out, nothing else.

## How Eleven creates one

`create_view` is a bot tool in two places:

- **The research loop** (`internal/bot/research.go`) — the main path. "List
  the 5 highest priority GitHub issues" → the member taps the research consent
  chip → the loop reads `issues.list` under the short-lived grant, the model
  sorts, calls `create_view`, and the sidecar posts the `view.create` event.
  Validation errors return as the tool_result, so the model fixes its own spec.
- **The conversational brain** (`internal/bot/brain.go`) — an executor, like
  `create_app`: when the data is already in context, the model can create a
  view directly.

Either way the sidecar — never the model — appends the **`open_view` chip** to
the reply, labeled with the view's real title, so the chip can't claim one
view and open another. `open_view` joins the client chip whitelist
(`ui.js CHIP_TYPES`) at the harmless-and-reversible bar: the tap only opens a
panel over state already folded on this device.

## Reuse: the news-bot shape

A charter bot posts its morning summary, creates a view of the articles, and
the reply carries **Open view** — headlines listed, each tapping through to
the article. Same rail, no new machinery. `/views` (on the views app) reopens
any live view in the thread later, so the chip isn't the only door.

## Scope

Web-first, deliberately: native renders through the webview surfaces today,
and the schema expansion is pure + twin-able (`docs/app-schema.md § tap.url`
notes the one additive primitive native still ignores). Fold vectors live at
`test/event-vectors/views.json` so the native fold lands against the same
spec. Interactive views (rows that emit events back to the thread) are the
natural next rung — the schema engine already supports actions; the spec just
doesn't expose them yet.
