@alexkroman1/aai 0.7.12 → 0.8.1

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 (81) hide show
  1. package/dist/aai.js +1 -1
  2. package/dist/cli.js +462 -352
  3. package/dist/sdk/_internal_types.d.ts +0 -1
  4. package/dist/sdk/_internal_types.d.ts.map +1 -1
  5. package/dist/sdk/_internal_types.js.map +1 -1
  6. package/dist/sdk/_render_check.d.ts +10 -0
  7. package/dist/sdk/_render_check.d.ts.map +1 -0
  8. package/dist/sdk/_render_check.js +42 -0
  9. package/dist/sdk/_render_check.js.map +1 -0
  10. package/dist/sdk/builtin_tools.d.ts.map +1 -1
  11. package/dist/sdk/builtin_tools.js +21 -0
  12. package/dist/sdk/builtin_tools.js.map +1 -1
  13. package/dist/sdk/define_agent.d.ts.map +1 -1
  14. package/dist/sdk/define_agent.js +0 -1
  15. package/dist/sdk/define_agent.js.map +1 -1
  16. package/dist/sdk/direct_executor.d.ts.map +1 -1
  17. package/dist/sdk/direct_executor.js +0 -1
  18. package/dist/sdk/direct_executor.js.map +1 -1
  19. package/dist/sdk/memory_tools.d.ts +38 -0
  20. package/dist/sdk/memory_tools.d.ts.map +1 -0
  21. package/dist/sdk/memory_tools.js +77 -0
  22. package/dist/sdk/memory_tools.js.map +1 -0
  23. package/dist/sdk/mod.d.ts +3 -1
  24. package/dist/sdk/mod.d.ts.map +1 -1
  25. package/dist/sdk/mod.js +2 -0
  26. package/dist/sdk/mod.js.map +1 -1
  27. package/dist/sdk/protocol.d.ts +3 -3
  28. package/dist/sdk/protocol.js +1 -1
  29. package/dist/sdk/s2s.d.ts.map +1 -1
  30. package/dist/sdk/s2s.js +3 -1
  31. package/dist/sdk/s2s.js.map +1 -1
  32. package/dist/sdk/session.d.ts.map +1 -1
  33. package/dist/sdk/session.js +2 -0
  34. package/dist/sdk/session.js.map +1 -1
  35. package/dist/sdk/types.d.ts +23 -14
  36. package/dist/sdk/types.d.ts.map +1 -1
  37. package/dist/sdk/types.js +29 -0
  38. package/dist/sdk/types.js.map +1 -1
  39. package/dist/ui/_components/app.d.ts.map +1 -1
  40. package/dist/ui/_components/app.js +6 -7
  41. package/dist/ui/_components/app.js.map +1 -1
  42. package/dist/ui/_components/sidebar_layout.d.ts +19 -0
  43. package/dist/ui/_components/sidebar_layout.d.ts.map +1 -0
  44. package/dist/ui/_components/sidebar_layout.js +21 -0
  45. package/dist/ui/_components/sidebar_layout.js.map +1 -0
  46. package/dist/ui/_components/start_screen.d.ts +24 -0
  47. package/dist/ui/_components/start_screen.d.ts.map +1 -0
  48. package/dist/ui/_components/start_screen.js +25 -0
  49. package/dist/ui/_components/start_screen.js.map +1 -0
  50. package/dist/ui/_hooks.d.ts +21 -0
  51. package/dist/ui/_hooks.d.ts.map +1 -0
  52. package/dist/ui/_hooks.js +35 -0
  53. package/dist/ui/_hooks.js.map +1 -0
  54. package/dist/ui/components.d.ts +20 -0
  55. package/dist/ui/components.d.ts.map +1 -1
  56. package/dist/ui/components.js +12 -0
  57. package/dist/ui/components.js.map +1 -1
  58. package/dist/ui/components_mod.d.ts +2 -1
  59. package/dist/ui/components_mod.d.ts.map +1 -1
  60. package/dist/ui/components_mod.js +2 -1
  61. package/dist/ui/components_mod.js.map +1 -1
  62. package/dist/ui/mod.d.ts +2 -1
  63. package/dist/ui/mod.d.ts.map +1 -1
  64. package/dist/ui/mod.js +2 -1
  65. package/dist/ui/mod.js.map +1 -1
  66. package/dist/ui/signals.d.ts.map +1 -1
  67. package/dist/ui/signals.js +5 -2
  68. package/dist/ui/signals.js.map +1 -1
  69. package/package.json +17 -1
  70. package/templates/_shared/CLAUDE.md +241 -121
  71. package/templates/_shared/package.json +4 -1
  72. package/templates/dispatch-center/agent.ts +43 -72
  73. package/templates/embedded-assets/agent.ts +4 -5
  74. package/templates/health-assistant/agent.ts +7 -7
  75. package/templates/infocom-adventure/agent.ts +20 -20
  76. package/templates/memory-agent/agent.ts +1 -55
  77. package/templates/night-owl/agent.ts +4 -4
  78. package/templates/night-owl/client.tsx +6 -23
  79. package/templates/pizza-ordering/agent.ts +14 -15
  80. package/templates/pizza-ordering/client.tsx +41 -101
  81. package/templates/smart-research/agent.ts +10 -10
@@ -31,6 +31,7 @@ You are helping a user build a voice agent using the **aai** framework.
31
31
  aai init # Scaffold a new agent (uses simple template)
32
32
  aai init -t <template> # Scaffold from a specific template
33
33
  aai dev # Start local dev server
34
+ aai build # Bundle and validate (no server or deploy)
34
35
  aai deploy # Bundle and deploy to production
35
36
  aai deploy -y # Deploy without prompts
36
37
  aai deploy --dry-run # Validate and bundle without deploying
@@ -75,15 +76,14 @@ export default defineAgent({
75
76
  name: "My Agent",
76
77
  instructions: "You are a helpful assistant that...",
77
78
  greeting: "Hey there. What can I help you with?",
78
- voice: "694f9389-aac1-45b6-b726-9d9369183238", // Sarah
79
79
  });
80
80
  ```
81
81
 
82
82
  ### Imports
83
83
 
84
84
  ```ts
85
- import { defineAgent } from "aai"; // Always needed
86
- import type { BeforeStepResult, HookContext, ToolContext } from "aai"; // Type annotations
85
+ import { defineAgent, memoryTools, tool } from "aai"; // defineAgent + helpers
86
+ import type { BeforeStepResult, BuiltinTool, HookContext, StepInfo, ToolContext } from "aai";
87
87
  import { z } from "zod"; // Tools with typed params (included in package.json)
88
88
  ```
89
89
 
@@ -95,8 +95,6 @@ defineAgent({
95
95
  name: string; // Required: display name
96
96
  instructions?: string; // System prompt (default: general voice assistant)
97
97
  greeting?: string; // Spoken on connect (default: "Hey, how can I help you?")
98
- voice?: Voice; // Cartesia voice UUID (default: Sarah 694f9389...)
99
-
100
98
  // Speech
101
99
  sttPrompt?: string; // STT guidance for jargon, names, acronyms
102
100
 
@@ -121,42 +119,11 @@ defineAgent({
121
119
  });
122
120
  ```
123
121
 
124
- ### Voices
125
-
126
- Voices use Cartesia voice UUIDs. Browse all voices at
127
- [play.cartesia.ai](https://play.cartesia.ai).
128
-
129
- Common voices:
130
-
131
- | Name | Voice ID |
132
- | --------------------- | -------------------------------------- |
133
- | Sarah (default) | `694f9389-aac1-45b6-b726-9d9369183238` |
134
- | Customer Support Man | `a167e0f3-df7e-4d52-a9c3-f949145efdab` |
135
- | Customer Support Lady | `829ccd10-f8b3-43cd-b8a0-4aeaa81f3b30` |
136
- | Helpful Woman | `156fb8d2-335b-4950-9cb3-a2d33befec77` |
137
- | Professional Woman | `248be419-c632-4f23-adf1-5324ed7dbf1d` |
138
- | Sweet Lady | `e3827ec5-697a-4b7c-9704-1a23041bbc51` |
139
- | British Lady | `79a125e8-cd45-4c13-8a67-188112f4dd22` |
140
- | Calm Lady | `00a77add-48d5-4ef6-8157-71e5437b282d` |
141
- | Laidback Woman | `21b81c14-f85b-436d-aff5-43f2e788ecf8` |
142
- | Storyteller Lady | `996a8b96-4804-46f0-8e05-3fd4ef1a87cd` |
143
- | Newslady | `bf991597-6c13-47e4-8411-91ec2de5c466` |
144
- | Friendly Reading Man | `69267136-1bdc-412f-ad78-0caad210fb40` |
145
- | Confident British Man | `63ff761f-c1e8-414b-b969-d1833d1c870c` |
146
- | New York Man | `34575e71-908f-4ab6-ab54-b08c95d6597d` |
147
- | California Girl | `b7d50908-b17c-442d-ad8d-810c63997ed9` |
148
- | Newsman | `d46abd1d-2d02-43e8-819f-51fb652c1c61` |
149
- | Salesman | `820a3788-2b37-4d21-847a-b65d8a68c99a` |
150
- | Wise Man | `b043dea0-a007-4bbe-a708-769dc0d0c569` |
151
- | Child | `2ee87190-8f84-4925-97da-e52547f9462c` |
152
-
153
- Any Cartesia voice UUID works — the list above is just a starting point.
154
-
155
122
  Use `sttPrompt` for domain-specific vocabulary:
156
123
 
157
124
  ```ts
158
125
  export default defineAgent({
159
- voice: "a167e0f3-df7e-4d52-a9c3-f949145efdab", // Customer Support Man
126
+ name: "Tech Support",
160
127
  sttPrompt: "Transcribe technical terms: Kubernetes, gRPC, PostgreSQL",
161
128
  });
162
129
  ```
@@ -196,17 +163,6 @@ aai env pull
196
163
  aai env rm MY_API_KEY
197
164
  ```
198
165
 
199
- Declare required env vars in the agent config so the CLI validates them at
200
- deploy time:
201
-
202
- ```ts
203
- export default defineAgent({
204
- name: "API Agent",
205
- env: ["ASSEMBLYAI_API_KEY", "MY_API_KEY"],
206
- // ...
207
- });
208
- ```
209
-
210
166
  Access secrets in tool code via `ctx.env`:
211
167
 
212
168
  ```ts
@@ -215,7 +171,6 @@ import { z } from "zod";
215
171
 
216
172
  export default defineAgent({
217
173
  name: "API Agent",
218
- env: ["ASSEMBLYAI_API_KEY", "MY_API_KEY"],
219
174
  tools: {
220
175
  call_api: {
221
176
  description: "Call an external API",
@@ -239,26 +194,27 @@ Define tools as plain objects in the `tools` record. The `parameters` field
239
194
  takes a Zod schema for type-safe argument inference:
240
195
 
241
196
  ```ts
242
- import { defineAgent } from "aai";
197
+ import { defineAgent, tool } from "aai";
243
198
  import { z } from "zod";
244
199
 
245
200
  export default defineAgent({
246
201
  name: "Weather Agent",
247
202
  tools: {
248
- get_weather: {
203
+ get_weather: tool({
249
204
  description: "Get current weather for a city",
250
205
  parameters: z.object({
251
206
  city: z.string().describe("City name"),
252
207
  }),
253
- execute: async (args, ctx) => {
208
+ execute: async ({ city }, ctx) => {
209
+ // city is typed as string (inferred from Zod schema)
254
210
  const data = await fetch(
255
- `https://api.example.com/weather?q=${args.city}`,
211
+ `https://api.example.com/weather?q=${city}`,
256
212
  );
257
213
  return data.json();
258
214
  },
259
- },
215
+ }),
260
216
 
261
- // No-parameter tools — omit `parameters`
217
+ // No-parameter tools — omit `parameters` and `tool()` wrapper
262
218
  list_items: {
263
219
  description: "List all items",
264
220
  execute: () => items,
@@ -267,6 +223,9 @@ export default defineAgent({
267
223
  });
268
224
  ```
269
225
 
226
+ **Important:** Wrap tool definitions in `tool()` to get typed `args` inferred
227
+ from the Zod `parameters` schema. Without `tool()`, args are untyped.
228
+
270
229
  Zod schema patterns:
271
230
 
272
231
  ```ts
@@ -289,6 +248,7 @@ Enable via `builtinTools`.
289
248
  | `fetch_json` | HTTP GET a JSON API | `url`, `headers?` |
290
249
  | `run_code` | Execute JS in sandbox (no net/fs, 30s timeout) | `code` |
291
250
  | `vector_search` | Search the agent's RAG knowledge base | `query`, `topK?` (default 5) |
251
+ | `memory` | Persistent KV memory (4 tools, see below) | — |
292
252
 
293
253
  The agentic loop runs up to `maxSteps` iterations (default 5) and stops when the
294
254
  LLM produces a text response.
@@ -303,10 +263,17 @@ ctx.env; // Record<string, string> — secrets from `aai env add`
303
263
  ctx.abortSignal; // AbortSignal — cancelled on interruption (tools only)
304
264
  ctx.state; // per-session state
305
265
  ctx.kv; // persistent KV store
266
+ ctx.vector; // VectorStore — vector store for RAG (tools only)
306
267
  ctx.messages; // readonly Message[] — conversation history (tools only)
307
268
  ```
308
269
 
309
- Hooks get `HookContext` (same but without `abortSignal` and `messages`).
270
+ Hooks get `HookContext` (same but without `abortSignal`, `vector`, and
271
+ `messages`).
272
+
273
+ **Timeouts:** Tool execution times out after **30 seconds** (`abortSignal`
274
+ fires). Lifecycle hooks (`onConnect`, `onTurn`, etc.) time out after **5
275
+ seconds**. Long-running tools should pass `ctx.abortSignal` to `fetch` and
276
+ check `ctx.abortSignal.aborted` in loops.
310
277
 
311
278
  ### Fetching external APIs
312
279
 
@@ -369,6 +336,37 @@ Keys are strings; use colon-separated prefixes (`"user:123"`). Max value: 64 KB.
369
336
  `kv.list()` returns `KvEntry[]` where each entry has
370
337
  `{ key: string, value: T }`.
371
338
 
339
+ ### Memory tools (pre-built KV tools)
340
+
341
+ Add `"memory"` to `builtinTools` to give the agent four persistent KV tools:
342
+ `save_memory`, `recall_memory`, `list_memories`, and `forget_memory`.
343
+
344
+ ```ts
345
+ import { defineAgent } from "aai";
346
+
347
+ export default defineAgent({
348
+ name: "My Agent",
349
+ builtinTools: ["memory"],
350
+ });
351
+ ```
352
+
353
+ Keys use colon-separated prefixes (`"user:name"`, `"preference:color"`).
354
+
355
+ You can also spread `memoryTools()` into `tools` if you want to combine them
356
+ with custom tools or override individual tools:
357
+
358
+ ```ts
359
+ import { defineAgent, memoryTools } from "aai";
360
+
361
+ export default defineAgent({
362
+ name: "My Agent",
363
+ tools: {
364
+ ...memoryTools(),
365
+ // your other tools...
366
+ },
367
+ });
368
+ ```
369
+
372
370
  ## Advanced patterns
373
371
 
374
372
  ### Step hooks
@@ -552,18 +550,41 @@ mount(App, {
552
550
 
553
551
  Import from `aai/ui`:
554
552
 
555
- | Component | Description |
556
- | ------------------- | -------------------------------------------------- |
557
- | `App` | Default full UI (start screen + ChatView) |
558
- | `ChatView` | Chat interface with header, messages, and controls |
559
- | `MessageBubble` | Single message (user right-aligned, agent left) |
560
- | `Transcript` | Live STT text display |
561
- | `StateIndicator` | Colored dot + agent state label |
562
- | `ErrorBanner` | Red error box with message |
563
- | `ThinkingIndicator` | Animated dots during processing |
564
- | `ToolCallBlock` | Collapsible tool call display (name, args, result) |
553
+ **Layout components:**
565
554
 
566
- Use `useMountConfig()` to access the `title` and `theme` passed to `mount()`.
555
+ | Component | Description |
556
+ | --------------- | ---------------------------------------------------- |
557
+ | `App` | Default full UI (StartScreen + ChatView) |
558
+ | `StartScreen` | Centered start card; renders children after start |
559
+ | `ChatView` | Chat interface (header + messages + controls) |
560
+ | `SidebarLayout` | Two-column layout with sidebar + main area |
561
+ | `Controls` | Stop/Resume + New Conversation buttons |
562
+ | `MessageList` | Messages with auto-scroll, tool calls, transcript |
563
+
564
+ `StartScreen` props: `{ children, icon?, title?, subtitle?, buttonText? }`
565
+ `SidebarLayout` props: `{ sidebar, children, width?, side? }`
566
+
567
+ **Atomic components:**
568
+
569
+ | Component | Props | Description |
570
+ | ------------------- | --------------------------------------- | ------------------------------- |
571
+ | `MessageBubble` | `{ message: Message }` | Single message bubble |
572
+ | `Transcript` | `{ userUtterance: Signal<str\|null> }` | Live STT text display |
573
+ | `StateIndicator` | `{ state: Signal<AgentState> }` | Colored dot + state label |
574
+ | `ErrorBanner` | `{ error: Signal<SessionError\|null> }` | Red error box with message |
575
+ | `ThinkingIndicator` | none | Animated dots during processing |
576
+ | `ToolCallBlock` | `{ toolCall: ToolCallInfo }` | Collapsible tool call display |
577
+
578
+ **Hooks:**
579
+
580
+ - `useAutoScroll()` — returns a `RefObject<HTMLDivElement>` to attach to a
581
+ sentinel div. Auto-scrolls when messages or utterances change.
582
+ - `useMountConfig()` — returns the `title` and `theme` passed to `mount()`.
583
+
584
+ **Important:** Components that accept `Signal<T>` props (like `StateIndicator`,
585
+ `Transcript`, `ErrorBanner`) expect the Signal object itself, NOT `.value`. Pass
586
+ `session.state`, not `session.state.value`. Passing `.value` compiles but breaks
587
+ reactivity silently.
567
588
 
568
589
  ### Session signals (`useSession()`)
569
590
 
@@ -571,16 +592,17 @@ Use `useMountConfig()` to access the `title` and `theme` passed to `mount()`.
571
592
  `{ session, started, running, start, toggle, reset, dispose }`. Reactive agent
572
593
  data lives on `session` (a `VoiceSession`); UI-only controls are top-level.
573
594
 
574
- | Signal / field | Type | Description |
575
- | ----------------------------- | ---------------------- | ---------------------------------------------------------------------------------------- |
576
- | `session.state.value` | `AgentState` | "disconnected", "connecting", "ready", "listening", "thinking", "speaking", "error" |
577
- | `session.messages.value` | `Message[]` | `{ role, text }` objects |
578
- | `session.toolCalls.value` | `ToolCallInfo[]` | `{ toolCallId, toolName, args, status, result?, afterMessageIndex }` — active tool calls |
579
- | `session.userUtterance.value` | `string \| null` | `null` = not speaking, `""` = speech detected, string = transcript |
580
- | `session.error.value` | `SessionError \| null` | `{ code, message }` |
581
- | `session.disconnected.value` | `object \| null` | `{ intentional: boolean }` when disconnected, `null` when connected |
582
- | `started.value` | `boolean` | Whether session has started |
583
- | `running.value` | `boolean` | Whether session is active |
595
+ | Signal / field | Type | Description |
596
+ | ------------------------------ | ---------------------- | --------------------------------------------------------------- |
597
+ | `session.state.value` | `AgentState` | "disconnected", "connecting", "ready", "listening", etc. |
598
+ | `session.messages.value` | `Message[]` | `{ role, text }` objects |
599
+ | `session.toolCalls.value` | `ToolCallInfo[]` | `{ toolCallId, toolName, args, status, result? }` — tool calls |
600
+ | `session.userUtterance.value` | `string \| null` | `null` = not speaking, `""` = speech detected, string = text |
601
+ | `session.agentUtterance.value` | `string \| null` | `null` = not speaking, string = streaming agent response text |
602
+ | `session.error.value` | `SessionError \| null` | `{ code, message }` |
603
+ | `session.disconnected.value` | `object \| null` | `{ intentional: boolean }` when disconnected, `null` otherwise |
604
+ | `started.value` | `boolean` | Whether session has been started |
605
+ | `running.value` | `boolean` | Whether session is active |
584
606
 
585
607
  **Methods:** `start()`, `toggle()`, `reset()`, `dispose()`
586
608
 
@@ -590,6 +612,20 @@ data lives on `session` (a `VoiceSession`); UI-only controls are top-level.
590
612
  | --------------------------------------------- | -------------------------------------------------------------------------------------------- |
591
613
  | `useToolResult((toolName, result, tc) => {})` | Fires once per completed tool call with parsed JSON result. Use for carts, scoreboards, etc. |
592
614
 
615
+ **Signal semantics for utterances:**
616
+
617
+ - `userUtterance`: `null` = user is not speaking, `""` = speech detected but
618
+ no text yet (show "..."), non-empty string = partial/final transcript
619
+ - `agentUtterance`: `null` = agent is not speaking, non-empty string =
620
+ streaming response text (cleared when final `chat` message arrives)
621
+ - `disconnected`: `null` = connected, `{ intentional: true }` = user
622
+ disconnected, `{ intentional: false }` = unexpected disconnect (show
623
+ reconnect UI)
624
+
625
+ **UI Message type:** `{ role: "user" | "assistant"; text: string }`. Note: UI
626
+ messages use `text` (not `content`). The SDK `Message` type uses `content` — do
627
+ not mix them up in custom UIs.
628
+
593
629
  ### Showing tool calls in custom UI
594
630
 
595
631
  ```tsx
@@ -629,12 +665,11 @@ the parsed JSON result, handling deduplication internally.
629
665
  ```tsx
630
666
  import "aai/ui/styles.css";
631
667
  import { useState } from "preact/hooks";
632
- import { ChatView, mount, useSession, useToolResult } from "aai/ui";
668
+ import { ChatView, SidebarLayout, StartScreen, mount, useToolResult } from "aai/ui";
633
669
 
634
670
  interface CartItem { id: number; name: string; price: number }
635
671
 
636
672
  function ShopAgent() {
637
- const { started, start } = useSession();
638
673
  const [cart, setCart] = useState<CartItem[]>([]);
639
674
 
640
675
  useToolResult((toolName, result: any) => {
@@ -651,16 +686,19 @@ function ShopAgent() {
651
686
  }
652
687
  });
653
688
 
654
- if (!started.value) return <button onClick={start}>Start</button>;
689
+ const sidebar = (
690
+ <div class="p-4">
691
+ <h3 class="text-aai-text font-bold">Cart ({cart.length})</h3>
692
+ {cart.map((i) => <p key={i.id} class="text-aai-text text-sm">{i.name} — ${i.price}</p>)}
693
+ </div>
694
+ );
655
695
 
656
696
  return (
657
- <div class="flex h-screen">
658
- <div class="w-64 p-4 border-r border-aai-border">
659
- <h3>Cart ({cart.length})</h3>
660
- {cart.map((i) => <p key={i.id}>{i.name} — ${i.price}</p>)}
661
- </div>
662
- <div class="flex-1"><ChatView /></div>
663
- </div>
697
+ <StartScreen title="Shop" buttonText="Start Shopping">
698
+ <SidebarLayout sidebar={sidebar}>
699
+ <ChatView />
700
+ </SidebarLayout>
701
+ </StartScreen>
664
702
  );
665
703
  }
666
704
 
@@ -702,11 +740,15 @@ mount(App);
702
740
 
703
741
  ### Styling custom UIs
704
742
 
705
- The framework uses **Tailwind CSS v4** (compiled at bundle time). Three
706
- approaches:
743
+ The framework uses **Tailwind CSS v4** (compiled at bundle time). Prefer
744
+ Tailwind classes over inline styles — all design tokens work as classes:
745
+ `bg-aai-surface` not `style={{ background: "var(--color-aai-surface)" }}`,
746
+ `border-t border-aai-border` not `style={{ borderTop: "1px solid var(--)" }}`.
707
747
 
708
- 1. **Tailwind classes** — `class="flex items-center gap-2 bg-gray-900"`
709
- 2. **Inline styles** — `style={{ color: "red", padding: "1rem" }}`
748
+ Three approaches:
749
+
750
+ 1. **Tailwind classes** — `class="flex items-center gap-2 bg-aai-surface"`
751
+ 2. **Inline styles** — only for dynamic values (`style={{ width: pixels }}`)
710
752
  3. **Injected `<style>` tags** — for keyframes, selectors, media queries:
711
753
 
712
754
  ```tsx
@@ -726,11 +768,65 @@ function App() {
726
768
  }
727
769
  ```
728
770
 
729
- **CSS custom properties** available from the theme:
771
+ **Design tokens** — available as CSS custom properties and Tailwind classes
772
+ (e.g. `bg-aai-bg`, `text-aai-text-muted`, `rounded-aai`, `font-aai`):
773
+
774
+ | Token | Tailwind class | Default |
775
+ | ---------------------------- | ------------------------- | ------------------------- |
776
+ | `--color-aai-bg` | `bg-aai-bg` | `#101010` |
777
+ | `--color-aai-surface` | `bg-aai-surface` | `#151515` |
778
+ | `--color-aai-surface-faint` | `bg-aai-surface-faint` | `rgba(255,255,255,0.031)` |
779
+ | `--color-aai-surface-hover` | `bg-aai-surface-hover` | `rgba(255,255,255,0.059)` |
780
+ | `--color-aai-border` | `border-aai-border` | `#282828` |
781
+ | `--color-aai-primary` | `text-aai-primary` | `#fab283` |
782
+ | `--color-aai-text` | `text-aai-text` | `rgba(255,255,255,0.936)` |
783
+ | `--color-aai-text-secondary` | `text-aai-text-secondary` | `rgba(255,255,255,0.618)` |
784
+ | `--color-aai-text-muted` | `text-aai-text-muted` | `rgba(255,255,255,0.284)` |
785
+ | `--color-aai-text-dim` | `text-aai-text-dim` | `rgba(255,255,255,0.422)` |
786
+ | `--color-aai-error` | `text-aai-error` | `#e06c75` |
787
+ | `--color-aai-ring` | `ring-aai-ring` | `#56b6c2` |
788
+ | `--color-aai-state-{state}` | `text-aai-state-{state}` | per-state colors |
789
+ | `--radius-aai` | `rounded-aai` | `6px` |
790
+ | `--font-aai` | `font-aai` | Inter, sans-serif |
791
+ | `--font-aai-mono` | `font-aai-mono` | IBM Plex Mono, mono |
792
+
793
+ The 5 core colors (`bg`, `primary`, `text`, `surface`, `border`) can be
794
+ overridden via `mount()` theme options. All other tokens use fixed defaults.
795
+
796
+ ### Common UI patterns
797
+
798
+ **Auto-scrolling messages** — use `useAutoScroll` for custom message lists:
799
+
800
+ ```tsx
801
+ import { useAutoScroll, useSession } from "aai/ui";
802
+
803
+ function MyChat() {
804
+ const { session } = useSession();
805
+ const bottomRef = useAutoScroll();
730
806
 
731
- - `--color-aai-bg`, `--color-aai-primary`, `--color-aai-text`
732
- - `--color-aai-surface`, `--color-aai-border`
733
- - `--color-aai-state-{state}` color for each `AgentState` value
807
+ return (
808
+ <div class="overflow-y-auto">
809
+ {session.messages.value.map((m, i) => <p key={i}>{m.text}</p>)}
810
+ <div ref={bottomRef} />
811
+ </div>
812
+ );
813
+ }
814
+ ```
815
+
816
+ Note: `MessageList` and `ChatView` already include auto-scroll. Only use
817
+ `useAutoScroll` when building a fully custom message list.
818
+
819
+ **Reading signal values in render:** Extract `.value` once at the top of the
820
+ component to avoid redundant signal subscriptions:
821
+
822
+ ```tsx
823
+ function MyComponent() {
824
+ const { session } = useSession();
825
+ const state = session.state.value;
826
+ const msgs = session.messages.value;
827
+ // Use `state` and `msgs` as plain values throughout the render
828
+ }
829
+ ```
734
830
 
735
831
  ## Self-hosting with `createServer()`
736
832
 
@@ -782,41 +878,65 @@ Drug interactions (RxNorm):
782
878
 
783
879
  Use `fetch_json` builtin tool or `fetch` in custom tools to call these.
784
880
 
785
- ## Partial custom UI with `ChatView`
881
+ ## Custom start screen with `StartScreen`
786
882
 
787
- For a custom start screen that transitions to the default chat interface:
883
+ Use `StartScreen` for a branded start card that transitions to `ChatView`:
788
884
 
789
885
  ```tsx
790
886
  import "aai/ui/styles.css";
791
- import { ChatView, mount, useSession } from "aai/ui";
887
+ import { ChatView, StartScreen, mount } from "aai/ui";
792
888
 
793
889
  function MyAgent() {
794
- const { started, start } = useSession();
795
-
796
- if (!started.value) {
797
- return (
798
- <div class="flex items-center justify-center h-screen bg-aai-bg">
799
- <div class="flex flex-col items-center gap-6">
800
- <h1 class="text-xl text-aai-text">My Agent</h1>
801
- <button
802
- class="px-8 py-3 rounded-aai bg-aai-primary text-white border-none cursor-pointer"
803
- onClick={start}
804
- >
805
- Start
806
- </button>
807
- </div>
808
- </div>
809
- );
810
- }
811
-
812
- return <ChatView />;
890
+ return (
891
+ <StartScreen icon={<span>&#x1F3A4;</span>} title="My Agent" subtitle="Ask me anything">
892
+ <ChatView />
893
+ </StartScreen>
894
+ );
813
895
  }
814
896
 
815
897
  mount(MyAgent);
816
898
  ```
817
899
 
818
- This gives you full control over the start screen while reusing the built-in
819
- chat UI once the session begins.
900
+ `StartScreen` handles the started/not-started transition automatically. Pass
901
+ `icon`, `title`, `subtitle`, and `buttonText` to customize the card.
902
+
903
+ ## Sidebar layout with `SidebarLayout`
904
+
905
+ Use `SidebarLayout` for apps with a persistent side panel (cart, dashboard):
906
+
907
+ ```tsx
908
+ import "aai/ui/styles.css";
909
+ import { useState } from "preact/hooks";
910
+ import { ChatView, SidebarLayout, StartScreen, mount, useToolResult } from "aai/ui";
911
+
912
+ function ShopAgent() {
913
+ const [cart, setCart] = useState<{ id: number; name: string }[]>([]);
914
+
915
+ useToolResult((toolName, result: any) => {
916
+ if (toolName === "add_item") setCart((prev) => [...prev, result.item]);
917
+ });
918
+
919
+ const sidebar = (
920
+ <div class="p-4">
921
+ <h3 class="text-aai-text font-bold">Cart ({cart.length})</h3>
922
+ {cart.map((i) => <p key={i.id} class="text-aai-text text-sm">{i.name}</p>)}
923
+ </div>
924
+ );
925
+
926
+ return (
927
+ <StartScreen title="Shop" buttonText="Start Shopping">
928
+ <SidebarLayout sidebar={sidebar}>
929
+ <ChatView />
930
+ </SidebarLayout>
931
+ </StartScreen>
932
+ );
933
+ }
934
+
935
+ mount(ShopAgent);
936
+ ```
937
+
938
+ `SidebarLayout` accepts `width` (default `"20rem"`) and `side` (`"left"` or
939
+ `"right"`).
820
940
 
821
941
  ## Project structure
822
942
 
@@ -912,8 +1032,8 @@ Use directional words naturally: "To the north you see..." not "N: forest"
912
1032
  - **Telling the agent to be verbose** — Voice responses should be 1-3 sentences.
913
1033
  If your `instructions` say "provide detailed explanations", the agent will
914
1034
  monologue. Instruct it to be brief and let the user ask follow-ups.
915
- - **Not declaring `env`** — If your agent needs custom env vars, list them in
916
- the `env` array so the CLI validates they're set before deploying.
1035
+ - **Not setting env vars before deploying** — If your agent needs custom env
1036
+ vars, set them with `aai env add MY_KEY` before deploying.
917
1037
  - **Forgetting SSRF restrictions on `fetch`** — The host validates all proxied
918
1038
  fetch URLs. Requests to private/internal IP addresses (localhost, 10.x,
919
1039
  192.168.x, etc.) are blocked.
@@ -8,7 +8,10 @@
8
8
  "lint:fix": "biome check --write ."
9
9
  },
10
10
  "dependencies": {
11
- "@alexkroman1/aai": "*"
11
+ "@alexkroman1/aai": "*",
12
+ "@preact/signals": "^2.8.2",
13
+ "preact": "^10.29.0",
14
+ "tailwindcss": "^4.2.1"
12
15
  },
13
16
  "devDependencies": {
14
17
  "@biomejs/biome": "^2",