| 1 | # π€ Views β a bot shows you data, in place |
| 2 | |
| 3 | Status: **built** (branch `thread-views`). The evolution of the terse |
| 4 | service-op notice ("Done β issues.list on github returned 50 items"): when a |
| 5 | bot has list-shaped data worth *looking at* β GitHub issues, news headlines, |
| 6 | search results β it can now create a **view** in the chat where you asked. |
| 7 | |
| 8 | ## What a view is (and isn't) |
| 9 | |
| 10 | A view is **schema + data, never code**. It is: |
| 11 | |
| 12 | - **A surface the current chat carries.** Not an app, not a separate chat, no |
| 13 | sidebar entry. It opens from an **Open view** chip on the bot's reply as a |
| 14 | full-height right-hand column (the command panel's `side` placement β a |
| 15 | vertical sidebar suits list-shaped content; narrow screens fall back to the |
| 16 | in-flow band), and dismisses like any panel. |
| 17 | - **Thread state, like everything else.** A view rides the thread's own |
| 18 | encrypted log (`view.create` / `view.update` / `view.remove` events), folded |
| 19 | client-side by the `views` registry app and covered by the ordinary app-state |
| 20 | snapshots β so it syncs to every member and every device, and any bot or app |
| 21 | in the chat can read or replace it. The server stores ciphertext, as ever. |
| 22 | - **Rendered by the app schema.** The stored spec is expanded (a pure |
| 23 | function, `viewSchema`) into a real app-schema list that the shared |
| 24 | schema-engine renders β the same machinery that draws To-do and Reminders on |
| 25 | web and native. Views inherit search, sections, and rows for free, and the |
| 26 | native runtime can render them the day it adds the same expansion. |
| 27 | |
| 28 | Why schema+data instead of letting the model write a renderer: the engine is |
| 29 | reviewed platform code, so a model-authored view is *inert data* β it can't |
| 30 | fetch, can't script, can't touch the DOM. That's why views need no sandbox |
| 31 | (unlike member apps, which are code and get the opaque-origin iframe). |
| 32 | |
| 33 | ## The spec (what a bot sends) |
| 34 | |
| 35 | ```jsonc |
| 36 | { |
| 37 | "type": "view.create", // or view.update (replace by id), view.remove |
| 38 | "id": "vw-a1b2c3", |
| 39 | "title": "Top GitHub issues", // β€ 80 chars |
| 40 | "glyph": "β€", // optional, β€ 4 chars |
| 41 | "items": [ // 1..50 |
| 42 | { |
| 43 | "id": "1", // optional; assigned when absent |
| 44 | "title": "Crash on login", // required, β€ 300 |
| 45 | "subtitle": "#412 Β· opened by amy", // optional, β€ 300 |
| 46 | "note": "Prio: urgent", // optional, β€ 500 |
| 47 | "url": "https://github.com/β¦", // optional, https only, β€ 600 |
| 48 | "accent": true // optional; tints the subtitle |
| 49 | } |
| 50 | ] |
| 51 | } |
| 52 | ``` |
| 53 | |
| 54 | Bounded twice, fail-closed both times: the sidecar validates the model's |
| 55 | `create_view` tool input (`internal/bot/view.go`, modeled on `normalizeCard`) |
| 56 | before anything posts, and the client fold re-validates every event |
| 57 | independently (`web/static/apps/views.js`) β payloads aren't sender-signed, so |
| 58 | the fold trusts nothing: strings clamped, non-https URLs dropped, β€ 50 items, |
| 59 | whole spec β€ 32 KB, β€ 20 live views per thread (oldest evicted). A row's tap |
| 60 | opens its `url` in a new tab (`rel=noopener`); rows without one aren't |
| 61 | tappable. Everything renders via `textContent` β a view can display data and |
| 62 | link out, nothing else. |
| 63 | |
| 64 | ## How Eleven creates one |
| 65 | |
| 66 | `create_view` is a bot tool in two places: |
| 67 | |
| 68 | - **The research loop** (`internal/bot/research.go`) β the main path. "List |
| 69 | the 5 highest priority GitHub issues" β the member taps the research consent |
| 70 | chip β the loop reads `issues.list` under the short-lived grant, the model |
| 71 | sorts, calls `create_view`, and the sidecar posts the `view.create` event. |
| 72 | Validation errors return as the tool_result, so the model fixes its own spec. |
| 73 | - **The conversational brain** (`internal/bot/brain.go`) β an executor, like |
| 74 | `create_app`: when the data is already in context, the model can create a |
| 75 | view directly. |
| 76 | |
| 77 | Either way the sidecar β never the model β appends the **`open_view` chip** to |
| 78 | the reply, labeled with the view's real title, so the chip can't claim one |
| 79 | view and open another. `open_view` joins the client chip whitelist |
| 80 | (`ui.js CHIP_TYPES`) at the harmless-and-reversible bar: the tap only opens a |
| 81 | panel over state already folded on this device. |
| 82 | |
| 83 | ## Reuse: the news-bot shape |
| 84 | |
| 85 | A charter bot posts its morning summary, creates a view of the articles, and |
| 86 | the reply carries **Open view** β headlines listed, each tapping through to |
| 87 | the article. Same rail, no new machinery. `/views` (on the views app) reopens |
| 88 | any live view in the thread later, so the chip isn't the only door. |
| 89 | |
| 90 | ## Scope |
| 91 | |
| 92 | Web-first, deliberately: native renders through the webview surfaces today, |
| 93 | and the schema expansion is pure + twin-able (`docs/app-schema.md Β§ tap.url` |
| 94 | notes the one additive primitive native still ignores). Fold vectors live at |
| 95 | `test/event-vectors/views.json` so the native fold lands against the same |
| 96 | spec. Interactive views (rows that emit events back to the thread) are the |
| 97 | natural next rung β the schema engine already supports actions; the spec just |
| 98 | doesn't expose them yet. |
| 99 | |