@agent-native/core 0.135.1 → 0.135.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/docs/content/getting-started-actions.mdx +235 -0
  4. package/corpus/core/docs/content/getting-started-database.mdx +253 -0
  5. package/corpus/core/docs/content/getting-started-pages.mdx +190 -0
  6. package/corpus/core/docs/content/getting-started.mdx +57 -613
  7. package/corpus/core/docs/content/what-is-agent-native.mdx +155 -298
  8. package/corpus/core/package.json +1 -1
  9. package/corpus/templates/clips/actions/add-comment.ts +6 -2
  10. package/corpus/templates/clips/app/components/player/comments-panel.tsx +9 -2
  11. package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +44 -27
  12. package/corpus/templates/clips/app/components/player/scrubber.tsx +10 -2
  13. package/corpus/templates/clips/app/components/player/video-player.tsx +4 -0
  14. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +22 -15
  15. package/corpus/templates/clips/app/routes/share.$shareId.tsx +1 -0
  16. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +23 -10
  17. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +20 -9
  18. package/corpus/templates/content/app/components/sidebar/document-sidebar-actions.ts +31 -0
  19. package/corpus/templates/content/changelog/2026-07-26-viewers-can-favorite-shared-pages.md +6 -0
  20. package/dist/collab/routes.d.ts +1 -1
  21. package/dist/collab/struct-routes.d.ts +1 -1
  22. package/dist/notifications/routes.d.ts +2 -2
  23. package/dist/progress/routes.d.ts +1 -1
  24. package/dist/resources/handlers.d.ts +1 -1
  25. package/dist/secrets/routes.d.ts +9 -9
  26. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  27. package/dist/server/realtime-token.d.ts +1 -1
  28. package/docs/content/getting-started-actions.mdx +235 -0
  29. package/docs/content/getting-started-database.mdx +253 -0
  30. package/docs/content/getting-started-pages.mdx +190 -0
  31. package/docs/content/getting-started.mdx +57 -613
  32. package/docs/content/what-is-agent-native.mdx +155 -298
  33. package/package.json +1 -1
@@ -0,0 +1,235 @@
1
+ ---
2
+ title: "Add an Action"
3
+ description: "Define your first action and render its result as a UI component directly inside the chat transcript."
4
+ ---
5
+
6
+ # Add an Action
7
+
8
+ This is part two of the Getting Started series. In [Getting Started](/docs/getting-started) you created a Chat app and connected an AI engine. Here you'll define your first action and render its result inline in chat.
9
+
10
+ ## Add an action {#add-an-action}
11
+
12
+ An action is a typed operation that both your agent and your UI can call. It's
13
+ how the agent does things in your app. Actions live in the `actions/` directory
14
+ and can be triggered from chat, from React components, from the CLI, or on a
15
+ schedule. You define them once and call them from anywhere.
16
+
17
+ ### Try the starter action
18
+
19
+ The Chat template includes a `hello` action at `actions/hello.ts`:
20
+
21
+ ```ts filename="actions/hello.ts"
22
+ import { defineAction } from "@agent-native/core/action";
23
+ import { z } from "zod";
24
+
25
+ export default defineAction({
26
+ description: "Return a friendly greeting.",
27
+ schema: z.object({
28
+ name: z.string().default("world").describe("Name to greet"),
29
+ }),
30
+ http: { method: "GET" },
31
+ run: async ({ name }) => {
32
+ return { message: `Hello, ${name}!` };
33
+ },
34
+ });
35
+ ```
36
+
37
+ Run it from the terminal (inside your `my-app/` directory):
38
+
39
+ ```bash
40
+ pnpm action hello --name Alice
41
+ ```
42
+
43
+ Or open your app at `http://localhost:8080` and ask the agent in the chat there:
44
+
45
+ > Use the hello action with the name Alice.
46
+
47
+ ### Add your own action
48
+
49
+ Replace the starter action with the first real operation in your domain. This example
50
+ counts words, sentences, and paragraphs in any text you pass it. It computes
51
+ everything locally, so there's nothing to configure and no external service to connect.
52
+
53
+ Create a new file called `analyze-text.ts` in your `actions/` directory:
54
+
55
+ ```ts filename="actions/analyze-text.ts"
56
+ import { defineAction } from "@agent-native/core/action";
57
+ import { z } from "zod";
58
+
59
+ const textStatsSchema = z.object({
60
+ title: z.string(),
61
+ points: z.array(z.object({ label: z.string(), value: z.number() })),
62
+ });
63
+
64
+ export default defineAction({
65
+ description: "Count words, sentences, and paragraphs in a block of text.",
66
+ schema: z.object({
67
+ text: z
68
+ .string()
69
+ .default(
70
+ "The quick brown fox jumps over the lazy dog. Pack my box with five dozen liquor jugs.",
71
+ ),
72
+ }),
73
+ outputSchema: textStatsSchema,
74
+ chatUI: {
75
+ renderer: "text.stats-chart",
76
+ title: "Text stats",
77
+ },
78
+ readOnly: true,
79
+ run: async ({ text }) => ({
80
+ title: "Text statistics",
81
+ points: [
82
+ { label: "Characters", value: text.length },
83
+ { label: "Words", value: text.split(/\s+/).filter(Boolean).length },
84
+ {
85
+ label: "Sentences",
86
+ value: text.split(/[.!?]+/).filter(Boolean).length,
87
+ },
88
+ {
89
+ label: "Paragraphs",
90
+ value: text.split(/\n\n+/).filter(Boolean).length,
91
+ },
92
+ ],
93
+ }),
94
+ });
95
+ ```
96
+
97
+ Try it from the terminal:
98
+
99
+ ```bash
100
+ pnpm action analyze-text --text "Hello world. How are you today?"
101
+ ```
102
+
103
+ Or open your app at `http://localhost:8080` and ask the agent in the chat there:
104
+
105
+ > Run the analyze-text action on "Hello world. How are you today?"
106
+
107
+ #### Define once, call from anywhere
108
+
109
+ This action is now reachable from chat, React hooks, CLI, HTTP, MCP, A2A,
110
+ scheduled jobs, and webhooks.
111
+
112
+ TIP: Any time you want the agent to call a specific action without ambiguity, phrasing it as "Run the `<action-name>` action" is most reliable. Natural-language prompts work well once the agent has enough context about your app's domain. For a brand-new app with no data or context yet, explicit is safer.
113
+
114
+ ## Render the result inline {#render-inline}
115
+
116
+ When the agent runs `analyze-text`, it returns structured data: a title and an
117
+ array of counts. By default the agent will describe that data in prose: "The
118
+ text has 9 words, 2 sentences..." and so on. That works, but you can
119
+ also render the result as a real UI component (a bar chart, a table, a card)
120
+ directly inside the chat transcript, right where the agent responded.
121
+
122
+ This is what `chatUI.renderer` in the action does. It's a label that says "when
123
+ this action's result appears in chat, hand it to this React component instead of
124
+ summarizing it in text." The component receives the validated action output as
125
+ props and renders whatever you want.
126
+
127
+ In the next step, you'll create `app/chat-renderers.tsx`, but first, add one import line
128
+ to `app/root.tsx` so it runs on startup:
129
+
130
+ ```ts filename="app/root.tsx"
131
+ import "./chat-renderers";
132
+ ```
133
+
134
+ Add it alongside your other imports at the top of the file. That's the only
135
+ change to `root.tsx`. The import just ensures the file runs and registers the
136
+ renderer. Now create the renderer file:
137
+
138
+ ```tsx filename="app/chat-renderers.tsx"
139
+ import {
140
+ registerActionChatRenderer,
141
+ type ToolRendererProps,
142
+ } from "@agent-native/core/client/chat";
143
+
144
+ type TextStatsResult = {
145
+ title: string;
146
+ points: Array<{ label: string; value: number }>;
147
+ };
148
+
149
+ const MAX_BAR_PX = 80;
150
+
151
+ function TextStatsChart({ context }: ToolRendererProps) {
152
+ const result = context.resultJson as TextStatsResult;
153
+ const max = Math.max(...result.points.map((point) => point.value), 1);
154
+ return (
155
+ <section className="rounded-lg border bg-card p-4">
156
+ <h3 className="text-sm font-medium">{result.title}</h3>
157
+ <div className="mt-4 flex items-end gap-2">
158
+ {result.points.map((point) => (
159
+ <div
160
+ key={point.label}
161
+ className="flex flex-1 flex-col items-center gap-2"
162
+ >
163
+ <div
164
+ className="w-full rounded-t bg-blue-500"
165
+ style={{
166
+ height: `${Math.max(Math.round((point.value / max) * MAX_BAR_PX), 2)}px`,
167
+ }}
168
+ />
169
+ <span className="text-xs text-muted-foreground">{point.label}</span>
170
+ </div>
171
+ ))}
172
+ </div>
173
+ </section>
174
+ );
175
+ }
176
+
177
+ registerActionChatRenderer({
178
+ id: "text.stats-chart",
179
+ renderer: "text.stats-chart",
180
+ Component: TextStatsChart,
181
+ });
182
+ ```
183
+
184
+ Once the renderer is registered, the agent's response looks like this. Instead
185
+ of a paragraph of text, your React component renders directly inside the chat
186
+ transcript:
187
+
188
+ <WireframeBlock id="doc-block-inline-result-wireframe">
189
+ <Screen
190
+ surface="desktop"
191
+ html={
192
+ "<div style='min-height:340px;box-sizing:border-box;padding:24px;display:flex;justify-content:center;align-items:center;background:var(--wf-bg)'><div style='width:min(640px,100%);display:flex;flex-direction:column;gap:14px'><div class='wf-card' data-rough style='align-self:flex-end;max-width:70%;padding:12px 14px'><strong>User</strong><p style='margin:6px 0 0'>Run the analyze-text action on \"Hello world. How are you today?\"</p></div><div class='wf-card' data-rough style='align-self:flex-start;width:min(480px,100%);padding:14px'><strong>Agent</strong><p class='wf-muted' style='margin:6px 0 12px'>Rendered with text.stats-chart.</p><section class='wf-card' data-rough style='padding:14px'><h3 style='margin:0 0 12px;font-size:14px'>Text statistics</h3><div data-rough='line:bottom' style='height:104px;display:flex;align-items:end;gap:8px;border-bottom:1.4px solid var(--wf-line);padding-bottom:4px'><div style='flex:1;display:flex;flex-direction:column;align-items:center;gap:6px'><div data-rough style='height:80px;width:100%;background:color-mix(in srgb, var(--wf-accent) 36%, transparent);border:1.4px solid var(--wf-accent);border-radius:8px 8px 3px 3px'></div><span class='wf-muted'>Characters</span></div><div style='flex:1;display:flex;flex-direction:column;align-items:center;gap:6px'><div data-rough style='height:18px;width:100%;background:color-mix(in srgb, var(--wf-accent) 30%, transparent);border:1.4px solid var(--wf-accent);border-radius:8px 8px 3px 3px'></div><span class='wf-muted'>Words</span></div><div style='flex:1;display:flex;flex-direction:column;align-items:center;gap:6px'><div data-rough style='height:2px;width:100%;background:color-mix(in srgb, var(--wf-accent) 24%, transparent);border:1.4px solid var(--wf-accent);border-radius:8px 8px 3px 3px'></div><span class='wf-muted'>Sentences</span></div><div style='flex:1;display:flex;flex-direction:column;align-items:center;gap:6px'><div data-rough style='height:2px;width:100%;background:color-mix(in srgb, var(--wf-accent) 24%, transparent);border:1.4px solid var(--wf-accent);border-radius:8px 8px 3px 3px'></div><span class='wf-muted'>Paragraphs</span></div></div></section></div></div></div>"
193
+ }
194
+ />
195
+ </WireframeBlock>
196
+
197
+ Use this step when the result belongs where the agent is speaking:
198
+
199
+ - setup summaries
200
+ - short reports
201
+ - approvals
202
+ - tables or charts small enough to inspect inline
203
+ - links into durable app views
204
+
205
+ For reusable generic outputs, the framework also ships built-in
206
+ `data-chart` and `data-table` renderers, plus `data-insights` for combined
207
+ summary/chart/table cards. See [Native Chat UI](/docs/native-chat-ui). For
208
+ temporary controls the agent creates at runtime, see
209
+ [Generative UI](/docs/generative-ui).
210
+
211
+ ## What's next {#next}
212
+
213
+ <Cards>
214
+
215
+ ### [Persist Data in SQL](/docs/getting-started-database)
216
+
217
+ Save action results to a database so the agent can reference them across
218
+ conversations. Next in the series.
219
+
220
+ ### [Actions](/docs/actions)
221
+
222
+ Schemas, auth, approvals, hooks, and transport — the full reference for what
223
+ actions can do.
224
+
225
+ ### [Native Chat UI](/docs/native-chat-ui)
226
+
227
+ Built-in renderers for tables, charts, and typed cards — beyond the custom
228
+ renderer you just built.
229
+
230
+ ### [Key Concepts](/docs/key-concepts)
231
+
232
+ The architecture underneath this tutorial: SQL, actions, live sync, and context
233
+ awareness.
234
+
235
+ </Cards>
@@ -0,0 +1,253 @@
1
+ ---
2
+ title: "Persist Data in SQL"
3
+ description: "Save action results to a SQL database so the agent can reference them across conversations."
4
+ ---
5
+
6
+ # Persist Data in SQL
7
+
8
+ This is part three of the Getting Started series. In [Add an Action](/docs/getting-started-actions) you defined the `analyze-text` action and rendered its result inline in chat. Here you'll persist those results to a SQL database.
9
+
10
+ ## Persist data in SQL {#persist-data}
11
+
12
+ Right now, every time the agent runs `analyze-text` the result appears in chat
13
+ and then disappears. There's nothing to look back at, nothing the agent can
14
+ reference later, and no way to build a page around the data. Persisting to SQL
15
+ fixes that: the agent writes results to a table, and both the agent and your UI
16
+ can read them back at any time.
17
+
18
+ Agent-Native apps have a SQL database available by default: SQLite locally,
19
+ and your configured provider (Postgres, Turso/libSQL, Cloudflare D1) in
20
+ production.
21
+
22
+ ### Wire up the database plugin
23
+
24
+ The Chat template doesn't include a database plugin by default. Create
25
+ `server/plugins/db.ts` to initialize it. This is what runs migrations and
26
+ makes the database available to your actions:
27
+
28
+ ```ts filename="server/plugins/db.ts"
29
+ import { runMigrations } from "@agent-native/core/db";
30
+
31
+ export default runMigrations(
32
+ [
33
+ {
34
+ version: 1,
35
+ sql: `CREATE TABLE IF NOT EXISTS text_analyses (
36
+ id TEXT PRIMARY KEY,
37
+ input TEXT NOT NULL,
38
+ char_count INTEGER NOT NULL,
39
+ word_count INTEGER NOT NULL,
40
+ sentence_count INTEGER NOT NULL,
41
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
42
+ )`,
43
+ },
44
+ ],
45
+ { table: "text_analyses_migrations" },
46
+ );
47
+ ```
48
+
49
+ Each entry in the array is an additive migration. When you add new columns or
50
+ tables later, append a new version object. Never edit existing ones.
51
+
52
+ ### Define the schema
53
+
54
+ Create `server/db/schema.ts`. The `server/db/` directory may not exist yet,
55
+ so create it if needed. This file describes your tables using typed helpers so
56
+ your actions get full TypeScript autocomplete:
57
+
58
+ ```ts filename="server/db/schema.ts"
59
+ import { integer, now, table, text } from "@agent-native/core/db/schema";
60
+
61
+ export const textAnalyses = table("text_analyses", {
62
+ id: text("id").primaryKey(),
63
+ input: text("input").notNull(),
64
+ charCount: integer("char_count").notNull(),
65
+ wordCount: integer("word_count").notNull(),
66
+ sentenceCount: integer("sentence_count").notNull(),
67
+ createdAt: text("created_at").notNull().default(now()),
68
+ });
69
+ ```
70
+
71
+ Use the framework schema helpers (`table`, `text`, `integer`, `now`) rather than
72
+ `sqliteTable`, `pgTable`, or dialect-specific imports. They pick the configured
73
+ SQL backend automatically, so the same schema runs locally on SQLite and in
74
+ production on any supported provider.
75
+
76
+ After adding both files, restart the dev server so the migration runs:
77
+
78
+ ```bash
79
+ pnpm dev
80
+ ```
81
+
82
+ Look for these two lines in the terminal output. They confirm the table was created:
83
+
84
+ ```
85
+ [db] Applying 1 migration(s) on SQLite/libsql…
86
+ [db] Applied migration v1 (1 statement)
87
+ ```
88
+
89
+ The `NitroViteError` lines, `BETTER_AUTH_SECRET` warning, and
90
+ `SECRETS_ENCRYPTION_KEY` warning that also appear are normal for local dev and
91
+ can be ignored.
92
+
93
+ ### Add actions for the table
94
+
95
+ Now create the action files that read and write the table. These go in your
96
+ `actions/` directory, the same place as `hello.ts` and `analyze-text.ts`. You
97
+ create them yourself, one file per operation. The agent and your UI will call
98
+ them the same way they call any other action.
99
+
100
+ **`actions/save-text-analysis.ts`** writes a result row to the database.
101
+ Call this after running `analyze-text` to make the result durable:
102
+
103
+ ```ts filename="actions/save-text-analysis.ts"
104
+ import { defineAction } from "@agent-native/core/action";
105
+ import { getDbExec } from "@agent-native/core/db";
106
+ import { z } from "zod";
107
+
108
+ export default defineAction({
109
+ description: "Save a text analysis result to the database.",
110
+ schema: z.object({
111
+ input: z.string(),
112
+ charCount: z.number(),
113
+ wordCount: z.number(),
114
+ sentenceCount: z.number(),
115
+ }),
116
+ run: async ({ input, charCount, wordCount, sentenceCount }) => {
117
+ const id = crypto.randomUUID();
118
+ await getDbExec().execute({
119
+ sql: `INSERT INTO text_analyses (id, input, char_count, word_count, sentence_count)
120
+ VALUES (?, ?, ?, ?, ?)`,
121
+ args: [id, input, charCount, wordCount, sentenceCount],
122
+ });
123
+ return { id };
124
+ },
125
+ });
126
+ ```
127
+
128
+ **`actions/list-text-analyses.ts`** reads all saved results. The agent can
129
+ call this to summarize past analyses, and your UI can use it to populate a page:
130
+
131
+ ```ts filename="actions/list-text-analyses.ts"
132
+ import { defineAction } from "@agent-native/core/action";
133
+ import { getDbExec } from "@agent-native/core/db";
134
+ import { z } from "zod";
135
+
136
+ export default defineAction({
137
+ description: "List all saved text analyses, newest first.",
138
+ schema: z.object({}),
139
+ run: async () => {
140
+ const result = await getDbExec().execute(
141
+ `SELECT id, input, char_count, word_count, sentence_count, created_at
142
+ FROM text_analyses
143
+ ORDER BY created_at DESC`,
144
+ );
145
+ return result.rows;
146
+ },
147
+ });
148
+ ```
149
+
150
+ **`actions/delete-text-analysis.ts`** removes a row by id:
151
+
152
+ ```ts filename="actions/delete-text-analysis.ts"
153
+ import { defineAction } from "@agent-native/core/action";
154
+ import { getDbExec } from "@agent-native/core/db";
155
+ import { z } from "zod";
156
+
157
+ export default defineAction({
158
+ description: "Delete a saved text analysis by id.",
159
+ schema: z.object({ id: z.string() }),
160
+ run: async ({ id }) => {
161
+ await getDbExec().execute({
162
+ sql: `DELETE FROM text_analyses WHERE id = ?`,
163
+ args: [id],
164
+ });
165
+ return { deleted: id };
166
+ },
167
+ });
168
+ ```
169
+
170
+ Once these files are saved the dev server picks them up automatically. No
171
+ restart needed. Try listing analyses from the terminal:
172
+
173
+ ```bash
174
+ pnpm action list-text-analyses
175
+ ```
176
+
177
+ You should see an empty array. The table exists and the action works; there's
178
+ just nothing saved yet:
179
+
180
+ ```
181
+ []
182
+ ```
183
+
184
+ Data is saved to `data/app.db`, a SQLite file in your project directory that
185
+ gets created automatically on first run. In production you'd point
186
+ `DATABASE_URL` at a hosted database instead, but locally this file is all you
187
+ need.
188
+
189
+ To save something, first run `analyze-text` to get the counts:
190
+
191
+ ```bash
192
+ pnpm action analyze-text --text "Hello world"
193
+ ```
194
+
195
+ You'll see output like:
196
+
197
+ ```
198
+ {
199
+ title: 'Text statistics',
200
+ points: [
201
+ { label: 'Characters', value: 11 },
202
+ { label: 'Words', value: 2 },
203
+ { label: 'Sentences', value: 1 },
204
+ { label: 'Paragraphs', value: 1 }
205
+ ]
206
+ }
207
+ ```
208
+
209
+ Then pass those values to `save-text-analysis`:
210
+
211
+ ```bash
212
+ pnpm action save-text-analysis \
213
+ --input "Hello world" \
214
+ --charCount 11 \
215
+ --wordCount 2 \
216
+ --sentenceCount 1
217
+ ```
218
+
219
+ Now run `list-text-analyses` again and you'll see the saved row:
220
+
221
+ ```bash
222
+ pnpm action list-text-analyses
223
+ ```
224
+
225
+ Or ask the agent in the chat at `http://localhost:8080` to do both steps at once:
226
+
227
+ > Run analyze-text on "Hello world", then save the result.
228
+
229
+ ## What's next {#next}
230
+
231
+ <Cards>
232
+
233
+ ### [Add a Page](/docs/getting-started-pages)
234
+
235
+ Build a React route that displays your saved analyses and wire it into the
236
+ sidebar. Next in the series.
237
+
238
+ ### [Database](/docs/database)
239
+
240
+ Migrations, schema helpers, and production database setup — the full reference
241
+ for SQL in Agent Native.
242
+
243
+ ### [Actions](/docs/actions)
244
+
245
+ Schemas, auth, approvals, hooks, and transport — the full reference for what
246
+ actions can do.
247
+
248
+ ### [Key Concepts](/docs/key-concepts)
249
+
250
+ The architecture underneath this tutorial: SQL, actions, live sync, and context
251
+ awareness.
252
+
253
+ </Cards>
@@ -0,0 +1,190 @@
1
+ ---
2
+ title: "Add a Page"
3
+ description: "Build a React route that displays your saved data and wire it into the sidebar so the agent can navigate to it."
4
+ ---
5
+
6
+ # Add a Page
7
+
8
+ This is part four of the Getting Started series. In [Persist Data in SQL](/docs/getting-started-database) you saved action results to a database. Here you'll build a page that displays that data and connect it to the sidebar.
9
+
10
+ ## Add a page the agent can open {#add-a-page}
11
+
12
+ Chat is great for conversational interaction, but some data is better inspected
13
+ in a dedicated UI: a table you can scan, sort, or delete rows from. This step
14
+ adds a React route that displays everything saved in `text_analyses`, using the
15
+ same `list-text-analyses` and `delete-text-analysis` actions you already wrote.
16
+ There's no second data layer. The page is just a view over the same SQL state
17
+ the agent reads and writes.
18
+
19
+ Create the route file at `app/routes/text-analyses.tsx`. Route files in
20
+ `app/routes/` are automatically picked up by the framework. The filename
21
+ becomes the URL path, so this page will be available at
22
+ `http://localhost:8080/text-analyses`.
23
+
24
+ ```tsx filename="app/routes/text-analyses.tsx"
25
+ import {
26
+ useActionMutation,
27
+ useActionQuery,
28
+ } from "@agent-native/core/client/hooks";
29
+
30
+ export default function TextAnalysesRoute() {
31
+ const analyses = useActionQuery("list-text-analyses", {});
32
+ const deleteAnalysis = useActionMutation("delete-text-analysis");
33
+
34
+ return (
35
+ <main className="mx-auto flex max-w-3xl flex-col gap-6 p-6">
36
+ <header>
37
+ <h1 className="text-2xl font-semibold">Text analyses</h1>
38
+ <p className="text-muted-foreground">
39
+ Results saved by the agent or triggered manually.
40
+ </p>
41
+ </header>
42
+ <section className="flex flex-col gap-3">
43
+ {analyses.data?.length === 0 && (
44
+ <p className="text-muted-foreground">No analyses saved yet.</p>
45
+ )}
46
+ {analyses.data?.map((row: any) => (
47
+ <article
48
+ key={row.id}
49
+ className="flex items-start justify-between rounded-lg border p-4"
50
+ >
51
+ <div className="flex flex-col gap-1">
52
+ <p className="text-sm font-medium">{row.input}</p>
53
+ <p className="text-xs text-muted-foreground">
54
+ {row.word_count} words · {row.char_count} characters ·{" "}
55
+ {row.sentence_count} sentences
56
+ </p>
57
+ </div>
58
+ <button
59
+ className="text-xs text-destructive hover:underline"
60
+ onClick={() => deleteAnalysis.mutate({ id: row.id })}
61
+ >
62
+ Delete
63
+ </button>
64
+ </article>
65
+ ))}
66
+ </section>
67
+ </main>
68
+ );
69
+ }
70
+ ```
71
+
72
+ `useActionQuery` calls `list-text-analyses` and keeps the result live. If the
73
+ agent saves a new row while the page is open, it appears automatically.
74
+ `useActionMutation` calls `delete-text-analysis` when the user clicks Delete,
75
+ then invalidates the query so the list refreshes.
76
+
77
+ Open `http://localhost:8080/text-analyses` in your browser. If you saved an
78
+ analysis in the previous step you'll see it listed. Then ask the agent in chat:
79
+
80
+ > Open the text analyses page.
81
+
82
+ If you get a 404, try restarting your dev server.
83
+
84
+ The agent calls the `navigate` action (already included in the Chat
85
+ template) to send the browser to `/text-analyses`. This is what it looks like
86
+ with a few saved rows:
87
+
88
+ <WireframeBlock id="doc-block-response-insights-page-wireframe">
89
+ <Screen
90
+ surface="desktop"
91
+ html={
92
+ "<main style='min-height:400px;box-sizing:border-box;padding:28px;background:var(--wf-bg)'><div style='max-width:720px;margin:0 auto;display:flex;flex-direction:column;gap:20px'><header><h2 style='margin:0 0 4px;font-size:24px;font-weight:600'>Text analyses</h2><p class='wf-muted' style='margin:0;font-size:14px'>Results saved by the agent or triggered manually.</p></header><section style='display:flex;flex-direction:column;gap:10px'><article class='wf-card' data-rough style='display:flex;align-items:center;justify-content:space-between;padding:14px 16px'><div><p style='margin:0 0 4px;font-size:14px;font-weight:500'>Hello world</p><p class='wf-muted' style='margin:0;font-size:12px'>2 words · 11 characters · 1 sentence</p></div><span class='wf-muted' style='font-size:12px'>Delete</span></article><article class='wf-card' data-rough style='display:flex;align-items:center;justify-content:space-between;padding:14px 16px'><div><p style='margin:0 0 4px;font-size:14px;font-weight:500'>The quick brown fox jumps over the lazy dog.</p><p class='wf-muted' style='margin:0;font-size:12px'>9 words · 44 characters · 1 sentence</p></div><span class='wf-muted' style='font-size:12px'>Delete</span></article><article class='wf-card' data-rough style='display:flex;align-items:center;justify-content:space-between;padding:14px 16px'><div><p style='margin:0 0 4px;font-size:14px;font-weight:500'>Pack my box with five dozen liquor jugs.</p><p class='wf-muted' style='margin:0;font-size:12px'>8 words · 40 characters · 1 sentence</p></div><span class='wf-muted' style='font-size:12px'>Delete</span></article></section></div></main>"
93
+ }
94
+ />
95
+ </WireframeBlock>
96
+
97
+ ## Extend the navigation {#extend-navigation}
98
+
99
+ The sidebar's links are a plain array in `app/components/layout/Sidebar.tsx`,
100
+ not a separate config file. Open it and add an entry for the Text analyses
101
+ page next to the existing Chat entry:
102
+
103
+ ```tsx filename="app/components/layout/Sidebar.tsx"
104
+ import { IconList, IconMessageCircle } from "@tabler/icons-react";
105
+
106
+ const navItems = [
107
+ {
108
+ icon: IconMessageCircle,
109
+ labelKey: "navigation.chat",
110
+ href: "/",
111
+ view: "chat",
112
+ },
113
+ {
114
+ icon: IconList,
115
+ labelKey: "navigation.textAnalyses",
116
+ href: "/text-analyses",
117
+ view: "text-analyses",
118
+ },
119
+ ];
120
+ ```
121
+
122
+ `icon` takes an imported Tabler icon component, not a string name. `labelKey`
123
+ looks up a string in the i18n catalog (`app/i18n/en-US.ts` and the other
124
+ locale files); an unregistered key still renders — it falls back to a
125
+ humanized version of the key (`navigation.textAnalyses` becomes "Text
126
+ analyses") — but add it to the catalogs if you want the label translated. See
127
+ [Internationalization](/docs/internationalization).
128
+
129
+ Save the file. The dev server picks up the change automatically and the sidebar
130
+ updates without a restart.
131
+
132
+ ### Agent navigation
133
+
134
+ The sidebar link lets users navigate manually. The agent can also open pages on
135
+ its own using two built-in actions that ship with the Chat template:
136
+
137
+ - **`view-screen`** reads the current route and returns a compact summary of
138
+ what the user is looking at.
139
+ - **`navigate`** writes a same-origin path to the browser's history.
140
+
141
+ As you add more pages, keep `navigate` updated so the agent knows what
142
+ destinations exist. Document available paths in `AGENTS.md` so the model can
143
+ reason about them.
144
+
145
+ When the app has both a full-page chat route and an app page, use the shared chat
146
+ handoff helpers described in [Agent Surfaces](/docs/agent-surfaces#rich-chat):
147
+ `AgentChatSurface`, `AgentSidebar`, `useAgentChatHomeHandoff`,
148
+ `useAgentChatHomeHandoffLinks`, and `chatViewTransition`. That lets the full
149
+ chat slide into the side panel as the page opens, keeping the same thread while
150
+ the user inspects durable data.
151
+
152
+ ## Project structure {#project-structure}
153
+
154
+ ```text
155
+ my-app/
156
+ actions/ # Agent-callable and UI-callable operations
157
+ app/ # React routes, pages, and chat surfaces
158
+ server/ # Nitro server and SQL schema
159
+ AGENTS.md # Always-on instructions for the app agent
160
+ .agents/ # Skills the agent loads when relevant
161
+ data/app.db # Local SQLite state when DATABASE_URL is unset
162
+ ```
163
+
164
+ ## What's next {#next}
165
+
166
+ You've built a complete agentic app: a working chat interface, an action, inline
167
+ rendering, a database, and a page the agent can navigate to. From here:
168
+
169
+ <Cards>
170
+
171
+ ### [Key Concepts](/docs/key-concepts)
172
+
173
+ The architecture underneath this tutorial: SQL, actions, live sync, and context
174
+ awareness.
175
+
176
+ ### [Agent Surfaces](/docs/agent-surfaces)
177
+
178
+ Chat, inline UI, app pages, embedded sidecars, automation, and external agents —
179
+ all the ways your app can surface the agent.
180
+
181
+ ### [Context Awareness](/docs/context-awareness)
182
+
183
+ `view-screen`, `navigate`, route state, and selected objects — how the agent
184
+ knows what the user is looking at.
185
+
186
+ ### [Deployment](/docs/deployment)
187
+
188
+ Put your app on your own domain.
189
+
190
+ </Cards>