How the agent knows what the user is looking at. Use when exposing UI state to the agent, implementing view-screen or navigate actions, wiring navigation state, or debugging agent context issues.
76%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./community-templates/account-expert/.agents/skills/context-awareness/SKILL.mdThe canonical home for this skill is context-awareness in BuilderIO/agent-native
Apps can sync semantic navigation state when the route changes by using the
shared route-state hook. When screen context is available, Core includes a
<current-screen> snapshot in each agent turn, so the agent can get basic
route context without first calling another tool. Apps can add concise
visible-record details to that snapshot and expose selection in tab-scoped
state.
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. Context awareness is what makes the agent feel like a collaborator rather than a disconnected chatbot.
navigation key)Use the shared route-state hook to write a tab-scoped navigation key as the
route changes. Include semantic screen state such as the view, open IDs, active
tab, and focused object.
UI side — use useAgentRouteState in the app's route-state hook. The app
can wrap it in an app-owned useNavigationState hook to map route paths to
semantic views and consume the agent's navigate command.
// app/hooks/use-navigation-state.ts
import { useAgentRouteState } from "@agent-native/core/client/navigation";
import { TAB_ID } from "@/lib/tab-id";
export function useNavigationState() {
useAgentRouteState({
browserTabId: TAB_ID,
requestSource: TAB_ID,
getNavigationState: ({ pathname, searchParams }) => ({
view:
pathname === "/"
? "home"
: pathname === "/sign-in"
? "sign-in"
: pathname.replace(/^\/+/, ""),
// Optional semantic alias. Raw query params are already exposed in
// <current-url> and controllable with set-search-params.
label: searchParams.get("label"),
}),
getCommandPath: (command: any) =>
command.path ?? `/${command.view === "home" ? "home" : command.view}`,
});
}The default scaffold uses /home for home and /sign-in for authentication;
/ redirects according to the configured home route. Map navigation to the
app's actual routes and pass an explicit path for destinations that do not
follow the /<view> convention.
TAB_ID comes from the framework's getBrowserTabId() (see the scaffolded app/lib/tab-id.ts; never redefine it). The server resolves tab-scoped application_state writes and page-local WebMCP calls (X-Agent-Native-Browser-Tab) against this same id, so a hand-rolled random id silently breaks selection sync for hidden and background tabs.
Agent side — read before acting:
import { readAppState } from "@agent-native/core/application-state";
const navigation = await readAppState("navigation");
// e.g. { view: "thread", threadId: "abc123", subject: "Re: Q3 Planning" }What to include in navigation state:
view — the current page/section (e.g., "inbox", "form-builder", "dashboard")threadId, formId, issueKey)Keep text-selection ranges and excerpts in the tab-scoped selection key, not
in navigation.
Raw URL query params are already synced by the framework to __url__ and shown to the built-in agent as <current-url>. Keep shareable filters in URL state, then use view-screen to summarize important query params as activeFilters when helpful.
Keep application_state values small. Do not store pasted files, base64 images,
recording chunks, screenshots, or other large blobs in navigation or app-state
keys; upload them and store only a URL or storage handle.
For app-owned per-tab selection, suffix the key with TAB_ID; requestSource
labels the write origin but does not scope the key. Keep the value to stable ids
and a short label or excerpt, never the full document:
import { writeClientAppState } from "@agent-native/core/client/hooks";
import { TAB_ID } from "@/lib/tab-id";
await writeClientAppState(
`selection:${TAB_ID}`,
{ kind: "text", id: "artifact-123", text: "short excerpt" },
{ requestSource: TAB_ID },
);A server action can read that tab's value with
readAppStateForCurrentTab("selection"). A view-screen result may include the
selection and a specific next action when that helps the agent target an edit.
When the user picks on-screen objects with the composer's Select button, each
one becomes an ordinary agent chat context chip keyed
agent-select:<kind>:<id>. Every turn's context then opens with a
## Selected objects block listing them. Treat that list as the target set:
apply the request to those objects and leave the rest unchanged unless the
message says otherwise. The tab-scoped agent-selection key mirrors the
list as { items: [{ kind, id, title }], updatedAt } for actions that need
it; read it with readAppStateForCurrentTab(AGENT_SELECTION_STATE_KEY) (the
constant is in @agent-native/core/shared).
UI side — offer objects to Select with the toolkit registry; never build a per-app pick mode or a second selection store:
import {
useAgentSelectable,
useAgentSelectableSource,
} from "@agent-native/toolkit/app/agent-select";
// One object per component: returns whether it is picked.
const picked = useAgentSelectable({
kind: "panel",
id: panel.id,
title: panel.title,
getContext: () => `Panel id: ${panel.id}`,
getElement: () => cardRef.current,
});
// Dynamic content (slide nodes, design layers): resolve the element under
// the pointer, including inside same-origin iframes.
useAgentSelectableSource((target) => resolveObject(target));The Select button appears in the composer only while something is registered.
selectAgentSelectable(target, { exclusive: true }) stages an object from a
plain click outside pick mode. A pick is released when its object unregisters
and does not remount, so leaving a screen or deleting an object needs no
cleanup. Give a source-resolved object a parent and a getElement that
finds its node again by a stable id: it is released with its parent, or when
the parent is on screen and the node is gone. Keep
getContext to ids, a short label, and the action to use; mark chrome pick
mode must skip with data-agent-select-ignore.
__url__ key)AgentPanel automatically writes __url__ with { pathname, search, hash, searchParams }. The built-in agent sees it as a <current-url> block in every turn.
Use this for URL-reachable filters and search state. The agent can update it with the built-in set-search-params and set-url-path tools; do not duplicate the whole query string into navigation.
settings-view key)The redesigned Settings shell writes tab-scoped settings-view = { page, sub, label } (for example { page: "integrations", sub: "builder", label: "Connections › Integrations › Builder.io" }) and deletes it when Settings closes. <current-url> shows it as a settingsPage: line, because a legacy or mounted pathname doesn't name the page the shell resolved. To send the user to a page, call the built-in open-settings-page tool with a page id (plus sub or anchor); it resolves old tab and section ids through the same redirect table as links. A template's own navigate action only needs a Settings branch for its app areas (/settings/app/<area>), built with buildSettingsRoute.
view-screen ActionUse a view-screen action for a concise snapshot of visible record data the
agent needs in addition to basic navigation. Read navigation and selection
state, then query the relevant summaries through existing data helpers or
Drizzle. Do not add REST wrappers just so view-screen can read app data.
// actions/view-screen.ts
import { defineAction } from "@agent-native/core/action";
import {
readAppState,
readAppStateForCurrentTab,
} from "@agent-native/core/application-state";
import { z } from "zod";
export default defineAction({
description: "Return a concise snapshot of the current screen.",
schema: z.object({}),
http: false,
readOnly: true,
run: async () => {
const navigation = await readAppState("navigation");
const screen: Record<string, unknown> = {};
if (navigation) screen.navigation = navigation;
const selection = await readAppStateForCurrentTab("selection");
if (selection) screen.selection = selection;
// Add concise visible-record summaries using the app's existing data helpers.
return screen;
},
});Core builds the <current-screen> block for each agent turn by running the
surfaced view-screen action. If no such action is available, it falls back to
the current tab's navigation state. The scaffold action returns navigation;
extend it with concise, relevant visible-record details when the app has them.
The agent can use the injected snapshot directly and call view-screen again
when it needs a fresh read after context has changed.
navigate ActionThe agent writes a one-shot navigate command to application-state. The UI reads it, performs the navigation, and deletes the entry.
Agent side:
import { writeAppStateForCurrentTab } from "@agent-native/core/application-state";
// Navigate the user to a specific thread
await writeAppStateForCurrentTab("navigate", {
view: "inbox",
path: "/inbox/abc123",
threadId: "abc123",
});UI side — use useAgentRouteState, shown above. It polls command keys,
dedupes _writeId, deletes consumed commands, and applies app-local routing.
When a destination has a real URL, let the navigate command carry that local
path (plus semantic fields when useful) and have the UI prefer path before
falling back to semantic routing. Keep app navigation single-channel: do not
also write __set_url__ for the same navigation. __set_url__ belongs to the
framework URL tools (set-url-path, set-search-params) and URL-only filter
changes. If a command can arrive while a chat stream is rendering, prefer
navigate(path, { replace: true, flushSync: true }) over a view-transition
wrapper so the URL and visible route commit together.
Keep path router-local. A segment matching the app mount prefix remains part
of the local route unless a matching view route or explicit publicPath
identifies the mounted URL. Carry the mounted form separately in publicPath;
never infer that path is mounted from its prefix alone.
Agent-side application-state writes are tagged with
requestSource: "agent". The UI uses useDbSync({ ignoreSource: TAB_ID }) so
it ignores its own writes while still picking up changes from agents, other
tabs, and scripts.
Import useAgentRouteState and useSemanticNavigationState from
@agent-native/core/client/navigation. Import
setClientAppState, writeClientAppState, readClientAppState, and
deleteClientAppState from @agent-native/core/client/hooks; avoid the
deprecated @agent-native/core/client barrel. 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.
// app/root.tsx
import { TAB_ID } from "@/lib/tab-id";
useDbSync({
queryClient,
ignoreSource: TAB_ID, // ignore events from this tab's own writes
});The UI sends its browser tab ID in X-Agent-Native-Browser-Tab; when a write
has a requestSource, the client sends it in X-Request-Source and the server
records it as the event origin. useDbSync ignores events whose origin matches
ignoreSource, preventing a tab from refetching after its own state writes.
The mail template demonstrates these patterns working together:
Navigation state shape:
{ "view": "inbox", "threadId": "thread-123", "focusedEmailId": "msg-456", "label": "important" }view-screen output:
__url__ if URL query filters matternavigate command:
{ "view": "starred" } — switch to starred view{ "view": "inbox", "threadId": "thread-123" } — open a specific threadset-search-params<current-screen> block for basic context — call view-screen only when you need richer datanavigation key (view, item IDs, active tab, focused row)<current-url> and set-search-params workview-screen for views where the agent needs them beyond the injected navigation contextuseAgentRouteState or useSemanticNavigationState for UI-side navigation sync and command consumptionnavigate command pattern for app navigation; include a same-origin path when the target URL is knownrequestSource: "agent" (the script helpers do this automatically)<current-screen> or read
navigation state when context is missing or stalenavigation key from the agent — it belongs to the UI. Use navigate instead.navigate and __set_url__ for one app navigation; competing consumers can make the browser URL change before React Router commits the page.<current-screen> block — it tells you where the user isnavigation when <current-url> already exposes themview-screen script fetches the actual data.useDbSync delivers app-state changes to the UIview-screen and navigate actionsBuilderIO/agent-native
46ed84c
Canonical home
since Jul 27, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.