To-do List
The user's own to-do list, from their tasks app or drafted by the agent, fully editable in the chat. Edits go back as one change set when the user saves.
≈ 1,603 tokens per use (tool 958 · guidance 316 · typical payload 329)How token costs are estimated
Example: To-do List
What's on my to-do list today?
Today
5 to do · 2 done · 1 overdue
Press Space or Enter to pick up this task. Then use the up and down arrow keys to move it within its group, Space or Enter to drop it, or Escape to cancel.
When to use
- The user asks about their own to-do list: "what's on my list today?", "what's overdue?".
- The user asks to add, reschedule or tick off tasks in their tasks app.
- The user wants a list they'll keep and edit, such as a packing or shopping list.
When not to use
- The steps are the agent's instructions for one task, like a recipe or a setup guide. Use a Checklist.
- The items are things the agent will do itself. Show a plan instead.
- Work items move between stages on a team board. Use a Kanban Board.
- The agent can't read the user's list. Fetch it first; never invent their tasks.
How the agent should use it
- Pass
itemsexactly as the tasks app returns them, in order. Never change their text, dates or done state yourself. - To add a task the user asked for, include it with
isNew: trueand a new id. It shows as "Added" and is saved only when the user saves. - Put your own ideas in
suggestions, each with a shortreason. They show under "Suggested" with Add and a dismiss button; the reason is a tooltip and is read to screen readers. Never mix them intoitems. - Pick
groupByfor the question:duefor "today" or "overdue",tagfor categorized lists like packing,noneorprioritywhen the user's own order matters (only those two can be reordered by hand). - Write nothing until the result arrives, then apply exactly the returned changes. On
discard, write nothing. - Lifecycle: the list is the user's own and they keep working in it, so don't set
expiresAt. SetfetchedAtandstaleAfterMs(e.g. an hour) when the list was read from a tasks app that other devices also edit, so the Renderer offers a refresh. When the agent shows the same list again with fresh data, supersede the old call.
Editing
Every edit is collected locally and counted in the footer ("3 changes"). Nothing is written until the user presses Save to Tasks app (the label uses source.label, or just "Save"). Discard goes back to the last saved state. The header shows only the title: the counts are read to screen readers, and the Save button names the source.
| Action | How |
|---|---|
| Add | Type in "Add a task" and press Enter. "tomorrow", "friday" (the next one, so a week out on a Friday), "next week" (next Monday), "in 3 days", and !high, !low or !! are read into a due date and priority. They show as chips before adding, each with a button to keep the words as plain text instead |
| Tick off | The item is struck through and fades, then moves to a collapsed "Done" group after a second (at once with reduced motion). Screen readers hear "Marked … done" |
| Edit text | Click the text or the pencil button (on devices with hover, the row's buttons appear on hover or focus). Enter or leaving the field saves; Escape cancels |
| Due date, priority, delete | The "More options" button opens a panel with a date field, priority buttons and Delete. A deleted item leaves a "Deleted … · Undo" row in place |
| Reorder | With groupBy none or priority, drag the handle, or focus it and press Space, the arrow keys, then Space again (Escape cancels). Items move within their group; each step is announced |
| Subtasks and notes | Shown under the item with a "2/5" progress toggle; subtasks can be ticked |
Due groups are computed in timeZone (or the viewer's): Overdue, Today, Tomorrow, This week (2 to 6 days out), Later and No date. A date-time that has passed today counts as overdue. Pass now to fix the moment they're computed from; it defaults to the viewer's clock after mount. Setting a date from the options panel stores a plain date.
Done items are the one place the list starts collapsed, so the open tasks stay in view. Set hideDone to leave them out.
Installation
npx shadcn@latest add https://your-site.com/r/todo-list.json
Usage
import { tool } from "ai"
import { todoListTool } from "@/components/genui/todo-list.schema"
const tools = {
// No execute(): the call is completed by the user's save or discard.
[todoListTool.name]: tool({
description: todoListTool.description,
inputSchema: todoListTool.inputSchema,
}),
}<TodoList
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
progress={savedProgress}
onProgress={(p) => saveProgress(part.toolCallId, p)}
result={part.state === "output-available" ? part.output : undefined}
onResult={(result) =>
part.state === "output-available"
? saveTasks(result) // later saves: write directly
: addToolResult({ tool: "show_todos", toolCallId: part.toolCallId, output: result })
}
/>The agent receives one of:
{
action: "save",
changes: {
added: [{ id: "new-1", text: "Call the bank", done: false, due: "2026-10-09" }],
updated: [{ id: "t-112", fields: { due: "2026-10-12", priority: null } }],
completed: ["t-110"],
reopened: [],
deleted: ["t-113"],
reordered: ["t-111"],
order: ["t-111", "t-110", "t-112", "new-1"],
},
}
{ action: "discard" }changes is the smallest set that turns the list as loaded into the user's. A cleared field is null. reordered lists the fewest items whose move explains the new order (moving one item and back again isn't a change), and order gives every id in the final order whenever anything moved. Ids in added are the component's own (new-1, …); the tasks app assigns real ones.
A to-do list is ongoing, so the list stays usable after a save: it shows "Saved · 3 changes" and counts later edits against the saved state. Each later save calls onResult again with only the changes since the previous one. Send the first as the tool result and write later ones straight to the tasks app.
With LangGraph
Push it from a graph node with ui.push({ name: "show_todos", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.
Restoring from history
There are two ways to restore the list:
| Prop | Behavior | |
|---|---|---|
| Working copy | onProgress → progress | Called on every edit with { items, baseline, handledSuggestions }. Pass it back and the list picks up where the user left off, still editable |
| A decision | result | Present when the list mounts (and without progress): restores the saved list read-only with "Saved · 3 changes", or the original list with "Changes discarded". Never calls onResult |
A result that arrives after mount (the user just saved) doesn't lock the list.
Schema
| Prop | Type | Notes |
|---|---|---|
title | string | e.g. "Today" or "Packing list" |
source | { label }? | Data-bound. Where the list lives, e.g. "Tasks app". Omit for a drafted list |
items | TodoItem[] | Data-bound. Up to 100, in the tasks app's order |
groupBy | "none" | "due" | "priority" | "tag" | Default none |
timeZone | string? | IANA zone for "today" and "overdue". Defaults to the viewer's |
allowAdd | boolean | Show "Add a task" and suggestions. Default true |
allowEdit | boolean | Edit, reorder, reschedule and delete. Ticking is always allowed. Default true |
hideDone | boolean | Hide done items instead of collapsing them. Default false |
suggestions | { text, due?, tags?, reason? }[] | Up to 5 tasks the agent suggests adding |
TodoItem
| Field | Type | Notes |
|---|---|---|
id | string | Data-bound. Returned in changes |
text | string | |
done | boolean | |
due | string? | ISO date (2026-10-09) or date-time (2026-10-09T15:00, read in timeZone, or with an offset) |
priority | "high" | "medium" | "low" | An icon, with the level as text for screen readers |
notes | string? | |
tags | string[]? | Up to 3. groupBy: "tag" uses the first |
subtasks | { id, text, done }[]? | Up to 10 |
assignee | { name, initials }? | Shared lists. Shown as initials |
isNew | boolean? | Added by the agent at the user's request; not in the tasks app yet |
Host props: status (streaming, ready, saving, saved, discarded), onResult, onProgress, progress, result, now and className.