eleven / ocho Public
π€ 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
sideplacement β 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.removeevents), folded client-side by theviewsregistry 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)
{
"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 readsissues.listunder the short-lived grant, the model sorts, callscreate_view, and the sidecar posts theview.createevent. Validation errors return as the tool_result, so the model fixes its own spec. - The conversational brain (
internal/bot/brain.go) β an executor, likecreate_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.