---
title: "Context Awareness"
description: "How the agent knows what the user is looking at: navigation state, selection context, view-screen, sendToAgentChat handoffs, navigate commands, and jitter prevention."
---

# Context Awareness

> **Developer page.** This page is for developers wiring the app's context layer. For the end-user experience — how the agent uses that context in conversation — see [Using Your Agent](/docs/using-your-agent).

How the agent knows what the user is looking at -- and how the agent can control what the user sees.

## Overview {#overview}

Without context awareness, the agent is blind. It asks "which email?" when the user is staring at one. It cannot act on the current selection, cannot provide relevant suggestions, and cannot modify what the user sees. With context awareness, the user can click a row, highlight a paragraph, select a slide element, or press Cmd+I, then say "summarize this" and the agent already knows what "this" means.

Six patterns solve this. To understand what to put in which surface (AGENTS.md vs. skills vs. application_state), see [Writing Agent Instructions — The four surfaces the agent sees](/docs/writing-agent-instructions#four-surfaces).

Six patterns solve this:

1. **Navigation state** -- the UI writes a `navigation` key to application-state on every route change
2. **Current URL** -- the framework writes `__url__` so query params are visible and editable by the agent
3. **Selection state** -- the UI writes a `selection` key when the user focuses, selects, or multi-selects something meaningful
4. **`view-screen`** -- an action that reads application state, fetches contextual data, and returns a snapshot of what the user sees
5. **Prompt handoff** -- UI controls call `sendToAgentChat()` when a click should become an agent turn
6. **`navigate`** -- a one-shot command from the agent that tells the UI where to go

## Context layers {#context-layers}

Use different context channels for different jobs:

| Layer                                     | Owner             | Use it for                                                                 |
| ----------------------------------------- | ----------------- | -------------------------------------------------------------------------- |
| `navigation` app-state key                | UI                | Semantic route state: current view, open record, active tab, stable IDs    |
| `__url__` app-state key                   | Framework UI      | Current pathname, search string, hash, and parsed URL query params         |
| `__set_url__` app-state key               | Agent / framework | One-shot URL edits from `set-search-params` and `set-url-path`             |
| `selection` app-state key                 | UI                | Durable semantic selection: rows, blocks, shapes, assets, messages         |
| `pending-selection-context` app-state key | UI / `AgentPanel` | One-shot selected text attached to the next chat turn, usually from Cmd+I  |
| `view-screen` action                      | Agent             | Hydrating the app-state keys into real records and screen summaries        |
| `sendToAgentChat()`                       | UI                | Turning a click, command, comment pin, or selected item into a chat prompt |
| `navigate` app-state key                  | Agent             | Asking the UI to move to another route or focus another object             |

The short version: URL query params are the source of truth for shareable filters, `navigation` stores semantic IDs and view names, `view-screen` turns those state layers into useful data, and `sendToAgentChat()` turns UI intent into a chat message when the user clicks a command.

## Navigation state {#navigation-state}

The UI writes a `navigation` key to application-state on every route change. This tells the agent what view the user is on, what item is open, and which semantic UI state matters.

```json
{
  "view": "inbox",
  "threadId": "thread-123",
  "focusedEmailId": "msg-456",
  "label": "important"
}
```

What to include in navigation state:

- `view` -- the current page/section, such as "inbox", "form-builder", or "dashboard"
- Item IDs -- the selected/open item, such as `threadId` or `formId`
- Semantic aliases -- active tab, label name, or other stable app concepts that help the agent reason
- Light focus state -- focused row, active tab, current panel

Keep `navigation` small and semantic. It should identify the current screen, not duplicate whole records or mirror every query param. Fetch records in `view-screen` so the agent always gets fresh data.

The agent reads this before acting:

```ts
import { readAppState } from "@agent-native/core/application-state";

const navigation = await readAppState("navigation");
// { view: "inbox", threadId: "thread-123", label: "important" }
```

## Current URL and filters {#current-url}

`AgentPanel` automatically syncs the current React Router URL into the `__url__` application-state key. The built-in agent includes it in every turn as a `<current-url>` block:

```txt
<current-url>
pathname: /adhoc/revenue
search: ?f_region=west&q=renewal
searchParams:
  f_region: west
  q: renewal
</current-url>
```

This is the canonical layer for shareable filter state. If the user can copy a URL and come back to the same filtered list, the filter belongs in the query string. The agent can change those filters with the built-in `set-search-params` tool:

```txt
set-search-params({ "params": { "f_region": "east", "q": null } })
```

Use `navigation` only for semantic aliases that help `view-screen` fetch or summarize the right data. A dashboard might keep `navigation.dashboardId` while `__url__.searchParams` owns `f_region`, `f_dateStart`, and `q`.

When `view-screen` returns a richer snapshot, it can copy important URL filters into a friendly `activeFilters` object:

```ts
const url = (await readAppState("__url__")) as {
  searchParams?: Record<string, string>;
} | null;

if (url?.searchParams) {
  screen.activeFilters = Object.fromEntries(
    Object.entries(url.searchParams).filter(
      ([key, value]) => key.startsWith("f_") && value,
    ),
  );
}
```

## Selection state {#selection-state}

Selection is semantic UI state. It is how "the chart I clicked", "these three rows", "this slide title", or "the current email draft range" becomes model-visible context.

Use the `selection` app-state key for durable selection that should survive a moment of navigation, empty-chat suggestions, or a later `view-screen` call:

```json
{
  "kind": "slide.elements",
  "deckId": "deck-123",
  "slideId": "slide-4",
  "items": [
    {
      "id": "hero-title",
      "selector": "[data-block-id='hero-title']",
      "label": "Hero title",
      "text": "Q3 launch plan"
    }
  ],
  "capturedAt": 1780332977027
}
```

Write it from the UI when the user selects, focuses, or multi-selects meaningful objects:

```tsx
import { setClientAppState } from "@agent-native/core/client";

async function syncSelection(selection: unknown | null) {
  await setClientAppState("selection", selection, { keepalive: true });
}
```

Good selection state includes:

- Stable IDs the agent can use in actions, such as `threadId`, `slideId`, or `assetId`
- A short human label so prompts and suggestions are readable
- Enough text or metadata to disambiguate the object
- Optional UI locators such as selectors or coordinates when the agent needs to refer back to a visual element
- `capturedAt` when stale selection would be harmful

Avoid storing secrets, full documents, large binary payloads, or whole API responses in `selection`. Store IDs plus short excerpts, then let `view-screen` fetch the current source of truth.

### One-shot selected text {#pending-selection-context}

`AgentPanel` already handles the common text-selection flow. When the user presses Cmd+I (or Ctrl+I) with text selected on the page, it:

1. Reads `window.getSelection()`
2. Writes `{ text, capturedAt }` to `pending-selection-context`
3. Focuses the agent chat

The production agent injects that key into the next turn as immediate selection context and ignores it once it is stale. This is the path that makes "select text, press Cmd+I, ask 'make this punchier'" work without the user copying the selection into the prompt.

Custom editors can write the same key when their selection is not represented by native browser selection:

```tsx
import { setClientAppState } from "@agent-native/core/client";

await setClientAppState(
  "pending-selection-context",
  {
    text: selectedMarkdown,
    capturedAt: Date.now(),
  },
  { keepalive: true },
);
```

Use `pending-selection-context` for one-shot "act on this exact highlighted text" flows. Use `selection` for durable object selection that `view-screen` and dynamic suggestions should keep seeing.

## The view-screen action {#view-screen-action}

Every template should have a `view-screen` action. It reads navigation and selection state, fetches the relevant data, and returns a snapshot of what the user sees. This is the agent's eyes.

```ts
// actions/view-screen.ts
import { defineAction } from "@agent-native/core";
import { readAppState } from "@agent-native/core/application-state";
import { eq, inArray } from "drizzle-orm";
import { z } from "zod";
import { getDb, schema } from "../server/db/index.js";

export default defineAction({
  description:
    "See what the user is currently looking at on screen. Reads navigation and selection state and fetches matching data.",
  schema: z.object({}),
  http: false,
  run: async () => {
    const navigation = (await readAppState("navigation")) as any;
    const selection = (await readAppState("selection")) as any;
    const screen: Record<string, unknown> = {};
    if (navigation) screen.navigation = navigation;
    if (selection) screen.selection = selection;

    const db = getDb();

    // Fetch data based on what the user is viewing
    if (navigation?.view === "inbox") {
      screen.emailList = await db
        .select()
        .from(schema.emails)
        .where(eq(schema.emails.label, navigation.label));
    }
    if (navigation?.threadId) {
      screen.thread = await db
        .select()
        .from(schema.threads)
        .where(eq(schema.threads.id, navigation.threadId));
    }
    if (selection?.kind === "email.messages") {
      screen.selectedMessages = await db
        .select()
        .from(schema.emails)
        .where(inArray(schema.emails.id, selection.messageIds));
    }

    if (Object.keys(screen).length === 0) {
      return "No application state found. Is the app running?";
    }
    return screen;
  },
});
```

The agent should call `pnpm action view-screen` before acting on the current UI. This is a hard convention across all templates. When adding new features, update `view-screen` to return data for the new view and any new selection shape.

## Prompt handoff with `sendToAgentChat()` {#send-to-agent-chat}

Sometimes context should not just sit in app state. A user clicks a button, drops a comment pin, selects an item and chooses "Ask agent", or presses an AI command in a toolbar. That click is an instruction. In browser UI, hand it to the agent with `sendToAgentChat()`.

```tsx
import { sendToAgentChat } from "@agent-native/core/client";

function askAgentAboutSelection(selection: {
  documentId: string;
  blockId: string;
  label: string;
  text: string;
}) {
  sendToAgentChat({
    message: `Improve the selected block: ${selection.label}`,
    context: [
      `Document id: ${selection.documentId}`,
      `Block id: ${selection.blockId}`,
      "Current selected text:",
      selection.text,
    ].join("\n"),
    submit: false,
    openSidebar: true,
  });
}
```

Use the fields deliberately:

| Field               | Meaning                                                                          |
| ------------------- | -------------------------------------------------------------------------------- |
| `message`           | Visible prompt text shown in chat                                                |
| `context`           | Hidden model-visible context, not shown as user-facing chat text                 |
| `submit: true`      | Send immediately; good for explicit command buttons such as "Fix layout"         |
| `submit: false`     | Prefill for user review; good for "Ask agent about this" or ambiguous selections |
| `openSidebar: true` | Make the agent response visible even if the panel was collapsed                  |
| `newTab: true`      | Start a separate chat thread for a larger creation task                          |
| `type: "code"`      | Route to the code-editing frame when the request is about changing app source    |

`sendToAgentChat()` is the supported browser wrapper for the submitted-chat path sometimes seen internally as `agentNative.submitChat`. App UI should call the wrapper instead of posting `agentNative.submitChat` directly because the wrapper handles local sidebars, Builder/Frame routing, MCP App host routing, tab IDs, and code-request routing.

Use `agentChat.submit()` or `agentChat.prefill()` for Node/script contexts where there is no browser sidebar. Server actions generally should not call browser-only `sendToAgentChat()`; if an action needs the open UI to ask the agent something, write a small request into `application_state` and let a UI bridge send it from the browser.

### Clicked items in the prompt {#clicked-items-in-prompt}

For the "click items in the UI and they become part of the prompt" experience, combine selection state with prompt handoff:

1. On click or multi-select, write semantic `selection` state so `view-screen`, dynamic suggestions, and future turns can see it.
2. If the click is also a command, call `sendToAgentChat()` with a concise visible `message` and richer hidden `context`.
3. In `view-screen`, hydrate the selected IDs into current records so the agent can verify the object before mutating it.
4. Clear `selection` when the object is no longer selected, deleted, or no longer relevant.

That gives the user the magic "this is what I meant" behavior without stuffing every prompt with bulky visible context.

## The navigate action {#navigate-action}

`navigate` is the mirror image of `navigation`. Where `navigation` is the UI telling the agent where the user is, `navigate` is the agent telling the UI where to go. The agent writes a one-shot `navigate` command to application-state; the UI reads it, performs the navigation, then deletes the entry.

```ts
// Agent side -- write a navigate command
import { writeAppState } from "@agent-native/core/application-state";

await writeAppState("navigate", { view: "inbox", threadId: "thread-123" });
```

On the UI side you never poll or delete this key by hand. Both directions -- writing `navigation` on every route change and consuming the agent's `navigate` command -- are handled by a single hook, [`useNavigationState`](#use-navigation-state), covered in the next section.

The `navigation` key belongs to the UI; the agent must never write to it directly. The agent writes `navigate`, the UI performs the move, and that move is what updates `navigation`.

## The useNavigationState hook {#use-navigation-state}

`useNavigationState` is **your app's hook, not a framework import.** Every template ships one at `app/hooks/use-navigation-state.ts` and calls it once from the app shell (`root.tsx`). It is the single place that wires navigation in both directions:

- **Outbound (UI → agent):** writes the `navigation` key whenever the route changes, so the agent always knows the current view.
- **Inbound (agent → UI):** polls the `navigate` command, runs the navigation, and deletes the command.

It stays short because it is a thin wrapper around the real framework primitive, `useAgentRouteState` (exported from `@agent-native/core/client`). You supply two app-specific functions and the framework does the rest:

```tsx
// app/hooks/use-navigation-state.ts -- this file lives in YOUR app
import { useAgentRouteState } from "@agent-native/core/client";
import { TAB_ID } from "@/lib/tab-id";

interface NavigationState {
  view: "inbox" | "thread";
  threadId?: string;
}

export function useNavigationState() {
  useAgentRouteState<NavigationState>({
    browserTabId: TAB_ID,
    requestSource: TAB_ID,

    // UI → agent: derive semantic state from the current URL.
    getNavigationState: ({ pathname }) => {
      const match = pathname.match(/^\/thread\/([^/]+)/);
      return match ? { view: "thread", threadId: match[1] } : { view: "inbox" };
    },

    // agent → UI: turn a `navigate` command into a route to push.
    getCommandPath: (command) =>
      command.view === "thread" && command.threadId
        ? `/thread/${command.threadId}`
        : "/",
  });
}
```

| You write                                              | The framework handles                                                                    |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
| `getNavigationState` — map the URL to semantic state   | `navigation` writes, tab-scoped plus a global fallback key                               |
| `getCommandPath` — map a `navigate` command to a route | command polling, delete-after-read, duplicate-command protection, request-source tagging |

`useAgentRouteState` assumes React Router. When navigation does not live in the URL -- a wizard step, a canvas selection, a non-router shell -- drop down to the lower-level `useSemanticNavigationState` instead: you hand it a ready-made `state` value plus `navigationKeys`/`commandKeys` and an `onCommand` callback, and it stays completely agnostic about React Router.

## Jitter prevention {#jitter-prevention}

When the agent writes to application-state, the sync system might cause the UI to refetch data it just wrote. This creates jitter. The solution is source tagging:

Use `setClientAppState`, `writeClientAppState`, `readClientAppState`, and `deleteClientAppState` from `@agent-native/core/client` for browser-side application-state access. Pass `{ requestSource: TAB_ID }` on UI writes when pairing with `useDbSync({ ignoreSource: TAB_ID })`; pass `{ keepalive: true }` for short-lived writes such as selection cleanup during unload.

```ts
// app/root.tsx
import { TAB_ID } from "@/lib/tab-id";

useDbSync({
  queryClient,
  ignoreSource: TAB_ID, // ignore events from this tab's own writes
});
```

How it works:

- Agent writes are tagged with `requestSource: "agent"` (the action helpers do this automatically)
- UI writes include the tab's unique ID via `X-Request-Source` header
- The server stores the source on each event
- When processing sync events, the UI filters out events matching its own `ignoreSource` value -- so it doesn't refetch data it just wrote
- Events from agents, other tabs, and actions still come through normally
