---
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

<Callout tone="info">

**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).

</Callout>

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.

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

<Diagram id="doc-block-1jsrbbk" title="How the agent sees what you see" summary={"The UI writes lightweight state keys; view-screen hydrates them into real records; the agent can write navigate back to move the UI."}>

```html
<div class="diagram-ctx">
  <div class="diagram-card col">
    <span class="diagram-pill">UI writes</span>
    <div class="diagram-node">
      navigation<br /><small class="diagram-muted">view, open ids</small>
    </div>
    <div class="diagram-node">
      __url__<br /><small class="diagram-muted">shareable filters</small>
    </div>
    <div class="diagram-node">
      selection<br /><small class="diagram-muted">rows, blocks, shapes</small>
    </div>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-panel center" data-rough>
    <span class="diagram-pill accent">view-screen</span
    ><small class="diagram-muted">reads state &middot; fetches records</small>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-box">
    Agent acts<br /><small class="diagram-muted">on the real object</small>
  </div>
  <div class="diagram-arrow diagram-accent" aria-hidden="true">&#8635;</div>
  <div class="diagram-box diagram-accent">
    navigate<br /><small class="diagram-muted">agent moves the UI</small>
  </div>
</div>
```

```css
.diagram-ctx {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.diagram-ctx .col {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 14px;
}
.diagram-ctx .center {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 14px;
}
.diagram-ctx .diagram-arrow {
  font-size: 22px;
  line-height: 1;
}
```

</Diagram>

## 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:

```text
<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:

```text
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/hooks";
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/hooks";
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.

<AnnotatedCode
  id="doc-block-1dy08gk"
  title={"view-screen — the agent's eyes"}
  filename="actions/view-screen.ts"
  language="ts"
  code={
    'import { defineAction } from "@agent-native/core/action";\nimport { readAppState } from "@agent-native/core/application-state";\nimport { eq, inArray } from "drizzle-orm";\nimport { z } from "zod";\nimport { getDb, schema } from "../server/db/index.js";\n\nexport default defineAction({\n  description:\n    "See what the user is currently looking at on screen.",\n  schema: z.object({}),\n  http: false,\n  run: async () => {\n    const navigation = (await readAppState("navigation")) as any;\n    const selection = (await readAppState("selection")) as any;\n    const screen: Record<string, unknown> = {};\n    if (navigation) screen.navigation = navigation;\n    if (selection) screen.selection = selection;\n\n    const db = getDb();\n\n    // Fetch data based on what the user is viewing\n    if (navigation?.view === "inbox") {\n      screen.emailList = await db\n        .select()\n        .from(schema.emails)\n        .where(eq(schema.emails.label, navigation.label));\n    }\n    if (navigation?.threadId) {\n      screen.thread = await db\n        .select()\n        .from(schema.threads)\n        .where(eq(schema.threads.id, navigation.threadId));\n    }\n    if (selection?.kind === "email.messages") {\n      screen.selectedMessages = await db\n        .select()\n        .from(schema.emails)\n        .where(inArray(schema.emails.id, selection.messageIds));\n    }\n\n    if (Object.keys(screen).length === 0) {\n      return "No application state found. Is the app running?";\n    }\n    return screen;\n  },\n});'
  }
  annotations={[
    {
      lines: "10-11",
      label: "Tool surface",
      note: "The agent reads this description to know it can call `view-screen` to see the current UI.",
    },
    {
      lines: "13",
      label: "http: false",
      note: "Internal action — not exposed over HTTP. The agent and `pnpm action` call it, not the browser.",
    },
    {
      lines: "15-16",
      label: "Read state",
      note: "Pulls the lightweight `navigation` and `selection` keys the UI wrote.",
    },
    {
      lines: "23-37",
      label: "Hydrate",
      note: "Turns those IDs into **fresh** records straight from SQL, so the agent verifies the live object before acting.",
    },
  ]}
/>

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.

<Callout id="doc-block-9kmk35" tone="info">

**Keep `navigation` and `selection` small.** Store IDs plus short labels, not whole records. `view-screen` fetches the source of truth on demand, so stale or bulky state never reaches the agent.

</Callout>

## 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/agent-chat";
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`.

When the destination has a real URL, include a same-origin `path` on the
`navigate` command and have the UI prefer that path before falling back to
semantic fields. Keep app navigation single-channel: do not write both
`navigate` and `__set_url__` for the same move. `__set_url__` is for the
framework URL tools (`set-url-path`, `set-search-params`) and URL-only filter
changes. For commands that can arrive while chat is streaming, commit the route
with `navigate(path, { replace: true, flushSync: true })` instead of wrapping it
in a view transition so the address bar and visible page stay together.

## 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/navigation";
import { TAB_ID } from "@/lib/tab-id";

interface NavigationState {
  view: "inbox" | "thread";
  threadId?: string;
  path?: 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.path ??
      (command.view === "thread" && command.threadId
        ? `/thread/${command.threadId}`
        : "/"),
    navigateOptions: { replace: true, flushSync: true },
  });
}
```

| 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

<Diagram id="doc-block-t8sq6j" title="Source tagging stops self-refetch jitter" summary="A tab ignores sync events stamped with its own TAB_ID, but still reacts to agent and other-tab writes.">

```html
<div class="diagram-jitter">
  <div class="diagram-node">
    This tab writes<br /><small class="diagram-muted"
      >X-Request-Source: TAB_ID</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-box" data-rough>
    Server stores source<br />on the event
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-card col">
    <div class="diagram-pill warn">source == TAB_ID &rarr; ignored</div>
    <small class="diagram-muted">no refetch, no flicker</small>
    <div class="diagram-pill ok">agent / other tab &rarr; applied</div>
    <small class="diagram-muted">UI updates live</small>
  </div>
</div>
```

```css
.diagram-jitter {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.diagram-jitter .col {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 14px;
}
.diagram-jitter .diagram-arrow {
  font-size: 22px;
  line-height: 1;
}
```

</Diagram>

## What's next {#whats-next}

- [**Using Your Agent**](/docs/using-your-agent) — how end users experience the context the agent sees
- [**Agent Mentions**](/docs/agent-mentions) — `@`-mention files and agents into a turn instead of relying on ambient context
- [**Drop-in Agent**](/docs/drop-in-agent) — mount the panel that reads and writes this state
- [**Real-Time Collaboration**](/docs/real-time-collaboration) — keep app state in sync when the agent and users edit together
- [**Client Methods**](/docs/client) — the full `application-state` and client helper reference
- [**Toolkit piece: Context & Knowledge**](/docs/toolkit-context-knowledge) — the Context X-Ray presentation built on this state
