Live preview
Add a few tasks, then refresh the page — they survive. The filter
buttons mutate $filter declaratively; the list updates
because list_todos re-runs whenever its args change.
localStorage
The UI is plain Streaming UI Script — input, filter buttons, list,
bulk actions, footer. Reads go through a Query tool,
writes through Mutations. A single Script
listens for keyboard shortcuts (⌘+Enter to add) and exposes a
keyboard hint badge.
Add a few tasks, then refresh the page — they survive. The filter
buttons mutate $filter declaratively; the list updates
because list_todos re-runs whenever its args change.
Note how the script only takes care of the imperative pieces (focus, keyboard shortcut, status pill). State and data flow stay declarative.
$filter = "all"
$draft = ""
$ver = 0
$shortcut = "⌘ Enter to add"
data = Query("list_todos", {filter: $filter, ver: $ver}, {rows: [], total: 0, active: 0, done: 0})
add = Mutation("add_todo", {title: $draft})
toggle = Mutation("toggle_todo", {id: $toggleId})
remove = Mutation("delete_todo", {id: $deleteId})
clearDone = Mutation("clear_completed", {})
$toggleId = ""
$deleteId = ""
inputField = FormControl("New task", Input("draft", "What needs doing?", "text", null, $draft))
addBtn = Button("Add", Action([@Run(add), @Run(data), @Reset($draft)]), "primary")
form = Stack([inputField, addBtn], "row", "m", "end")
filterButtons = Buttons([
Button("All " + "(" + data.total + ")", Action([@Set($filter, "all")]), $filter == "all" ? "primary" : "ghost", "normal", "small"),
Button("Active " + "(" + data.active + ")", Action([@Set($filter, "active")]), $filter == "active" ? "primary" : "ghost", "normal", "small"),
Button("Completed " + "(" + data.done + ")", Action([@Set($filter, "done")]), $filter == "done" ? "primary" : "ghost", "normal", "small")
])
statusTag = Tag($shortcut, null, "sm", "neutral")
toggleBtn = @Each(data.rows, "t", Button(t.done ? "Done" : "Open", Action([@Set($toggleId, t.id), @Run(toggle), @Run(data)]), t.done ? "secondary" : "ghost", "normal", "small"))
deleteBtn = @Each(data.rows, "t", Button("Delete", Action([@Set($deleteId, t.id), @Run(remove), @Run(data)]), "ghost", "normal", "small"))
titleCell = @Each(data.rows, "t", TextContent(t.title, t.done ? "small" : "body", t.done ? "muted" : null))
createdCell= @Each(data.rows, "t", TextContent(t.created, "small", "muted"))
tbl = Table([
Col("", toggleBtn),
Col("Title", titleCell),
Col("Added", createdCell),
Col("", deleteBtn)
])
empty = TextContent($filter == "done" ? "Nothing completed yet." : ($filter == "active" ? "All clear!" : "No tasks. Add your first one above."), "small", "muted")
list = @Count(data.rows) > 0 ? tbl : empty
footer = Stack([
TextContent("" + data.active + " active · " + data.done + " done", "small", "muted"),
Button("Clear completed", Action([@Run(clearDone), @Run(data)]), "ghost", "normal", "small")
], "row", "m", "center", "between")
shortcutScript = Script("shortcut", "const onKey = async (e) => { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); const draft = (ctx.state.get('draft') ?? '').trim(); if (!draft) return; await ctx.tools.add_todo({ title: draft }); if (ctx.signal.aborted) return; ctx.state.set('draft', ''); ctx.state.set('ver', (ctx.state.get('ver') ?? 0) + 1); } }; window.addEventListener('keydown', onKey); ctx.cleanup(() => window.removeEventListener('keydown', onKey));")
root = Stack([
Card([CardHeader("Things to do", "Persisted to localStorage")]),
Card([form]),
Card([Stack([filterButtons, statusTag], "row", "m", "center", "between")]),
Card([list]),
Card([footer]),
shortcutScript
])
Tools are plain functions — the LLM (or you, in this case) decides
when to call them. Add localStorage persistence in one place and
every Query / Mutation stays in sync.
const STORAGE_KEY = "rui-todo-app";
const load = () => JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "[]");
const save = (todos) => localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
el.setTools({
list_todos: ({ filter }) => {
const todos = load();
const visible = todos.filter((t) => (
filter === "all" || (filter === "done" ? t.done : !t.done)
));
return {
rows: visible.map((t) => ({ ...t, created: relative(t.createdAt) })),
total: todos.length,
active: todos.filter((t) => !t.done).length,
done: todos.filter((t) => t.done).length,
};
},
add_todo: ({ title }) => { ... },
toggle_todo: ({ id }) => { ... },
delete_todo: ({ id }) => { ... },
clear_completed: () => { ... },
});