@agent-native/core 0.36.0 → 0.37.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 (50) hide show
  1. package/dist/agent/production-agent.d.ts.map +1 -1
  2. package/dist/agent/production-agent.js +72 -10
  3. package/dist/agent/production-agent.js.map +1 -1
  4. package/dist/cli/skills.d.ts.map +1 -1
  5. package/dist/cli/skills.js +78 -0
  6. package/dist/cli/skills.js.map +1 -1
  7. package/dist/client/AssistantChat.d.ts.map +1 -1
  8. package/dist/client/AssistantChat.js +8 -4
  9. package/dist/client/AssistantChat.js.map +1 -1
  10. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  11. package/dist/client/MultiTabAssistantChat.js +14 -10
  12. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  13. package/dist/client/composer/TiptapComposer.js +1 -1
  14. package/dist/client/composer/TiptapComposer.js.map +1 -1
  15. package/dist/client/composer/extensions/SkillReference.js +1 -1
  16. package/dist/client/composer/extensions/SkillReference.js.map +1 -1
  17. package/dist/client/dynamic-suggestions.d.ts +13 -7
  18. package/dist/client/dynamic-suggestions.d.ts.map +1 -1
  19. package/dist/client/dynamic-suggestions.js +23 -12
  20. package/dist/client/dynamic-suggestions.js.map +1 -1
  21. package/dist/client/index.d.ts +1 -0
  22. package/dist/client/index.d.ts.map +1 -1
  23. package/dist/client/index.js +1 -0
  24. package/dist/client/index.js.map +1 -1
  25. package/dist/client/resources/ResourcesPanel.js +2 -2
  26. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  27. package/dist/client/route-state.d.ts +116 -0
  28. package/dist/client/route-state.d.ts.map +1 -0
  29. package/dist/client/route-state.js +205 -0
  30. package/dist/client/route-state.js.map +1 -0
  31. package/dist/resources/store.js +4 -4
  32. package/dist/resources/store.js.map +1 -1
  33. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  34. package/dist/server/agent-chat-plugin.js +92 -56
  35. package/dist/server/agent-chat-plugin.js.map +1 -1
  36. package/dist/server/agents-bundle.d.ts +6 -4
  37. package/dist/server/agents-bundle.d.ts.map +1 -1
  38. package/dist/server/agents-bundle.js +20 -12
  39. package/dist/server/agents-bundle.js.map +1 -1
  40. package/dist/templates/default/AGENTS.md +16 -8
  41. package/dist/templates/default/app/hooks/use-navigation-state.ts +10 -76
  42. package/dist/vite/agents-bundle-plugin.d.ts.map +1 -1
  43. package/dist/vite/agents-bundle-plugin.js +16 -6
  44. package/dist/vite/agents-bundle-plugin.js.map +1 -1
  45. package/docs/content/context-awareness.md +90 -48
  46. package/docs/content/creating-templates.md +22 -1
  47. package/docs/content/workspace.md +3 -3
  48. package/package.json +2 -1
  49. package/src/templates/default/AGENTS.md +16 -8
  50. package/src/templates/default/app/hooks/use-navigation-state.ts +10 -76
@@ -11,13 +11,14 @@ How the agent knows what the user is looking at -- and how the agent can control
11
11
 
12
12
  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.
13
13
 
14
- Five patterns solve this:
14
+ Six patterns solve this:
15
15
 
16
16
  1. **Navigation state** -- the UI writes a `navigation` key to application-state on every route change
17
- 2. **Selection state** -- the UI writes a `selection` key when the user focuses, selects, or multi-selects something meaningful
18
- 3. **`view-screen`** -- an action that reads application state, fetches contextual data, and returns a snapshot of what the user sees
19
- 4. **Prompt handoff** -- UI controls call `sendToAgentChat()` when a click should become an agent turn
20
- 5. **`navigate`** -- a one-shot command from the agent that tells the UI where to go
17
+ 2. **Current URL** -- the framework writes `__url__` so query params are visible and editable by the agent
18
+ 3. **Selection state** -- the UI writes a `selection` key when the user focuses, selects, or multi-selects something meaningful
19
+ 4. **`view-screen`** -- an action that reads application state, fetches contextual data, and returns a snapshot of what the user sees
20
+ 5. **Prompt handoff** -- UI controls call `sendToAgentChat()` when a click should become an agent turn
21
+ 6. **`navigate`** -- a one-shot command from the agent that tells the UI where to go
21
22
 
22
23
  ## Context layers {#context-layers}
23
24
 
@@ -25,25 +26,26 @@ Use different context channels for different jobs:
25
26
 
26
27
  | Layer | Owner | Use it for |
27
28
  | ----------------------------------------- | ----------------- | -------------------------------------------------------------------------- |
28
- | `navigation` app-state key | UI | Current route, view, open record, filters, active tab |
29
+ | `navigation` app-state key | UI | Semantic route state: current view, open record, active tab, stable IDs |
30
+ | `__url__` app-state key | Framework UI | Current pathname, search string, hash, and parsed URL query params |
31
+ | `__set_url__` app-state key | Agent / framework | One-shot URL edits from `set-search-params` and `set-url-path` |
29
32
  | `selection` app-state key | UI | Durable semantic selection: rows, blocks, shapes, assets, messages |
30
33
  | `pending-selection-context` app-state key | UI / `AgentPanel` | One-shot selected text attached to the next chat turn, usually from Cmd+I |
31
34
  | `view-screen` action | Agent | Hydrating the app-state keys into real records and screen summaries |
32
35
  | `sendToAgentChat()` | UI | Turning a click, command, comment pin, or selected item into a chat prompt |
33
36
  | `navigate` app-state key | Agent | Asking the UI to move to another route or focus another object |
34
37
 
35
- The short version: app state tells the agent what the user is looking at, `view-screen` turns that state into useful data, and `sendToAgentChat()` turns UI intent into a chat message when the user clicks a command.
38
+ 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.
36
39
 
37
40
  ## Navigation state {#navigation-state}
38
41
 
39
- 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 filters shape the visible list.
42
+ 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.
40
43
 
41
44
  ```json
42
45
  {
43
46
  "view": "inbox",
44
47
  "threadId": "thread-123",
45
48
  "focusedEmailId": "msg-456",
46
- "search": "budget",
47
49
  "label": "important"
48
50
  }
49
51
  ```
@@ -52,10 +54,10 @@ What to include in navigation state:
52
54
 
53
55
  - `view` -- the current page/section, such as "inbox", "form-builder", or "dashboard"
54
56
  - Item IDs -- the selected/open item, such as `threadId` or `formId`
55
- - Filter state -- active search, label, or category filters
57
+ - Semantic aliases -- active tab, label name, or other stable app concepts that help the agent reason
56
58
  - Light focus state -- focused row, active tab, current panel
57
59
 
58
- Keep `navigation` small and URL-like. It should identify the current screen, not duplicate whole records. Fetch records in `view-screen` so the agent always gets fresh data.
60
+ 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.
59
61
 
60
62
  The agent reads this before acting:
61
63
 
@@ -66,6 +68,44 @@ const navigation = await readAppState("navigation");
66
68
  // { view: "inbox", threadId: "thread-123", label: "important" }
67
69
  ```
68
70
 
71
+ ## Current URL and filters {#current-url}
72
+
73
+ `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:
74
+
75
+ ```txt
76
+ <current-url>
77
+ pathname: /adhoc/revenue
78
+ search: ?f_region=west&q=renewal
79
+ searchParams:
80
+ f_region: west
81
+ q: renewal
82
+ </current-url>
83
+ ```
84
+
85
+ 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:
86
+
87
+ ```txt
88
+ set-search-params({ "params": { "f_region": "east", "q": null } })
89
+ ```
90
+
91
+ 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`.
92
+
93
+ When `view-screen` returns a richer snapshot, it can copy important URL filters into a friendly `activeFilters` object:
94
+
95
+ ```ts
96
+ const url = (await readAppState("__url__")) as {
97
+ searchParams?: Record<string, string>;
98
+ } | null;
99
+
100
+ if (url?.searchParams) {
101
+ screen.activeFilters = Object.fromEntries(
102
+ Object.entries(url.searchParams).filter(
103
+ ([key, value]) => key.startsWith("f_") && value,
104
+ ),
105
+ );
106
+ }
107
+ ```
108
+
69
109
  ## Selection state {#selection-state}
70
110
 
71
111
  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.
@@ -257,58 +297,60 @@ import { writeAppState } from "@agent-native/core/application-state";
257
297
  await writeAppState("navigate", { view: "inbox", threadId: "thread-123" });
258
298
  ```
259
299
 
260
- The UI polls for this command and navigates when it appears:
300
+ The UI should consume these commands through `useAgentRouteState`, which handles command polling, tab-scoped fallback keys, duplicate-command protection, and delete-after-read:
261
301
 
262
- ```ts
263
- // UI side -- poll for navigate commands
264
- import {
265
- deleteClientAppState,
266
- readClientAppState,
267
- } from "@agent-native/core/client";
268
-
269
- const { data: navCommand } = useQuery({
270
- queryKey: ["navigate-command"],
271
- queryFn: async () => {
272
- const data = await readClientAppState<NavigateCommand>("navigate");
273
- if (data) {
274
- await deleteClientAppState("navigate");
275
- return data;
276
- }
277
- return null;
278
- },
279
- staleTime: 2_000,
280
- });
302
+ ```tsx
303
+ import { useAgentRouteState } from "@agent-native/core/client";
304
+ import { TAB_ID } from "@/lib/tab-id";
281
305
 
282
- useEffect(() => {
283
- if (navCommand) {
284
- router.navigate(buildPath(navCommand));
285
- }
286
- }, [navCommand]);
306
+ interface NavigationState {
307
+ view: "inbox" | "thread";
308
+ threadId?: string;
309
+ }
310
+
311
+ export function useNavigationState() {
312
+ useAgentRouteState<NavigationState>({
313
+ browserTabId: TAB_ID,
314
+ requestSource: TAB_ID,
315
+ getNavigationState: ({ pathname }) => {
316
+ const match = pathname.match(/^\/thread\/([^/]+)/);
317
+ return match ? { view: "thread", threadId: match[1] } : { view: "inbox" };
318
+ },
319
+ getCommandPath: (command) =>
320
+ command.view === "thread" && command.threadId
321
+ ? `/thread/${command.threadId}`
322
+ : "/",
323
+ });
324
+ }
287
325
  ```
288
326
 
289
327
  The `navigation` key belongs to the UI -- the agent should never write to it directly. Instead, the agent writes to `navigate`, and the UI performs the actual navigation, which then updates `navigation`.
290
328
 
291
329
  ## useNavigationState hook {#use-navigation-state}
292
330
 
293
- The `use-navigation-state.ts` hook syncs routes to application-state on every navigation:
331
+ The `use-navigation-state.ts` hook is usually a thin wrapper around `useAgentRouteState`. Template code supplies the app-specific route mapping; core owns application-state writes, command reads/deletes, request-source headers, and duplicate-command prevention.
294
332
 
295
- ```ts
333
+ ```tsx
296
334
  // app/hooks/use-navigation-state.ts
297
- import { useEffect } from "react";
298
- import { useLocation } from "react-router";
299
- import { setClientAppState } from "@agent-native/core/client";
335
+ import { useAgentRouteState } from "@agent-native/core/client";
336
+ import { TAB_ID } from "@/lib/tab-id";
300
337
 
301
338
  export function useNavigationState() {
302
- const location = useLocation();
303
-
304
- useEffect(() => {
305
- const state = deriveNavigationState(location.pathname);
306
- setClientAppState("navigation", state).catch(() => {});
307
- }, [location.pathname]);
339
+ useAgentRouteState({
340
+ browserTabId: TAB_ID,
341
+ requestSource: TAB_ID,
342
+ getNavigationState: ({ pathname, searchParams }) => ({
343
+ view: pathname === "/" ? "home" : pathname.slice(1),
344
+ // Optional semantic alias. Raw query params are still visible in
345
+ // <current-url> and controllable with set-search-params.
346
+ label: searchParams.get("label"),
347
+ }),
348
+ getCommandPath: (command: any) => command.path ?? "/",
349
+ });
308
350
  }
309
351
  ```
310
352
 
311
- The `deriveNavigationState()` function is template-specific -- it parses the URL path and extracts the view, item IDs, and filters relevant to your app.
353
+ For non-router state channels, use the lower-level `useSemanticNavigationState`. It takes a ready-made `state`, an ordered list of `navigationKeys`/`commandKeys`, and an `onCommand` callback, but does not import or assume React Router.
312
354
 
313
355
  ## Jitter prevention {#jitter-prevention}
314
356
 
@@ -215,10 +215,31 @@ Common sources: `"action"` (every successful agent action — the reliable fallb
215
215
 
216
216
  Application state is how the agent knows what the user is seeing. At minimum, add:
217
217
 
218
- - A UI hook that writes `navigation` state when routes, selected records, filters, or editor selections change.
218
+ - A UI hook that writes semantic `navigation` state when routes, selected records, active tabs, or editor selections change.
219
219
  - A `view-screen` action that reads that state and returns the current screen snapshot.
220
220
  - A `navigate` action that writes a one-shot `navigate` command for the UI to consume.
221
221
 
222
+ Use `useAgentRouteState` for the UI hook so application-state writes, tab-scoped command reads, delete-after-read, and duplicate-command protection stay consistent:
223
+
224
+ ```tsx
225
+ import { useAgentRouteState } from "@agent-native/core/client";
226
+ import { TAB_ID } from "@/lib/tab-id";
227
+
228
+ export function useNavigationState() {
229
+ useAgentRouteState({
230
+ browserTabId: TAB_ID,
231
+ requestSource: TAB_ID,
232
+ getNavigationState: ({ pathname, searchParams }) => ({
233
+ view: pathname === "/" ? "home" : pathname.slice(1),
234
+ selectedId: searchParams.get("id"),
235
+ }),
236
+ getCommandPath: (command: any) => command.path ?? "/",
237
+ });
238
+ }
239
+ ```
240
+
241
+ Keep shareable filters in URL query params. The framework exposes them to the agent as `<current-url>` and the built-in agent can change them with `set-search-params`; `navigation` should hold semantic IDs and aliases, not a second copy of the full query string.
242
+
222
243
  ```ts
223
244
  // actions/navigate.ts
224
245
  import { defineAction } from "@agent-native/core";
@@ -139,7 +139,7 @@ Change how the agent behaves, in 60 seconds.
139
139
 
140
140
  ## How the Agent Uses Resources {#how-the-agent-uses-resources}
141
141
 
142
- The agent has built-in tools for managing resources: `resource-list`, `resource-read`, `resource-effective`, `resource-write`, and `resource-delete`. These are available in both Code mode and App mode.
142
+ The built-in app agent manages resources with the unified `resources` tool: use `action: "list"`, `"read"`, `"effective"`, `"write"`, `"promote"`, or `"delete"`. External CLI/code agents can use the equivalent `pnpm action resource-*` commands.
143
143
 
144
144
  At the start of every conversation, the agent automatically reads:
145
145
 
@@ -182,7 +182,7 @@ Both normal chat and integration-triggered agent runs load these instruction res
182
182
 
183
183
  ### Reference Resources {#reference-resources}
184
184
 
185
- Put reusable company context under `context/`: personas, positioning, messaging, product facts, customer proof points, brand guidelines, competitive notes, and similar material. The agent sees an index of workspace and shared reference resources and reads the relevant file with `resource-read` when a task may depend on it. Use `resource-effective --path "context/brand.md"` when you need to see whether a workspace default is overridden by an organization/app or personal resource.
185
+ Put reusable company context under `context/`: personas, positioning, messaging, product facts, customer proof points, brand guidelines, competitive notes, and similar material. The agent sees an index of workspace and shared reference resources and reads the relevant file with the `resources` tool (`action: "read"`) when a task may depend on it. Use `resources` with `action: "effective"` and `path: "context/brand.md"` when you need to see whether a workspace default is overridden by an organization/app or personal resource.
186
186
 
187
187
  Examples:
188
188
 
@@ -440,7 +440,7 @@ What shows up depends on the mode:
440
440
 
441
441
  ## / Slash Commands {#slash-commands}
442
442
 
443
- Type `/` at the start of a line to invoke a skill. A dropdown shows available skills with their names and descriptions. Selecting a skill adds it as an inline chip, and its content is included as context when the message is sent.
443
+ Type `/` at the start of a line to invoke a skill. A dropdown shows available skills with their names and descriptions. Selecting a skill adds it as an inline chip, and its content is included as context when the message is sent when the backend can resolve it; otherwise the agent receives the exact skill path and reads it with the appropriate resource or file tool.
444
444
 
445
445
  What shows up depends on the mode:
446
446
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.36.0",
3
+ "version": "0.37.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -43,6 +43,7 @@
43
43
  "./client/AgentPanel": "./dist/client/AgentPanel.js",
44
44
  "./client/application-state": "./dist/client/application-state.js",
45
45
  "./client/api-path": "./dist/client/api-path.js",
46
+ "./client/route-state": "./dist/client/route-state.js",
46
47
  "./client/observability": "./dist/client/observability/index.js",
47
48
  "./client/onboarding": "./dist/client/onboarding/index.js",
48
49
  "./client/settings/useBuilderStatus": "./dist/client/settings/useBuilderStatus.js",
@@ -37,14 +37,16 @@ Resources are SQL-backed persistent files for notes, learnings, and context.
37
37
  1. **`AGENTS.md`** -- inherited workspace defaults, app/team instructions, and user-specific context.
38
38
  2. **`LEARNINGS.md`** -- user preferences, corrections, and patterns. Read personal and shared scopes.
39
39
 
40
- **Update `LEARNINGS.md` when you learn something important.**
40
+ **Update `LEARNINGS.md` when you learn something important.** Built-in app
41
+ chat agents use the `resources` tool with the `action` argument. External CLI
42
+ agents can use the equivalent `pnpm action resource-*` commands.
41
43
 
42
- | Action | Args | Purpose |
43
- | ----------------- | ----------------------------------------------------------- | ----------------------- |
44
- | `resource-read` | `--path <path> [--scope personal\|shared]` | Read a resource |
45
- | `resource-write` | `--path <path> --content <text> [--scope personal\|shared]` | Write/update a resource |
46
- | `resource-list` | `[--prefix <path>] [--scope personal\|shared\|all]` | List resources |
47
- | `resource-delete` | `--path <path> [--scope personal\|shared]` | Delete a resource |
44
+ | Built-in agent tool call | CLI equivalent | Purpose |
45
+ | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------------- |
46
+ | `resources` with `action: "read"`, `path`, optional `scope` | `pnpm action resource-read --path <path> [--scope personal\|shared]` | Read a resource |
47
+ | `resources` with `action: "write"`, `path`, `content`, optional `scope` | `pnpm action resource-write --path <path> --content <text> [--scope personal\|shared]` | Write/update a resource |
48
+ | `resources` with `action: "list"`, optional `prefix`/`scope` | `pnpm action resource-list [--prefix <path>] [--scope personal\|shared\|all]` | List resources |
49
+ | `resources` with `action: "delete"`, `path`, optional `scope` | `pnpm action resource-delete --path <path> [--scope personal\|shared]` | Delete a resource |
48
50
 
49
51
  ## Application State
50
52
 
@@ -57,6 +59,12 @@ Ephemeral UI state is stored in the SQL `application_state` table, accessed via
57
59
 
58
60
  The `navigation` key is written by the UI whenever the route changes. The `navigate` key is a one-shot command: the agent writes it, the UI reads and executes the navigation, then deletes it.
59
61
 
62
+ UI code should use `useAgentRouteState` / `useSemanticNavigationState` from
63
+ `@agent-native/core/client` for navigation sync instead of hand-written
64
+ `fetch("/_agent-native/application-state/...")` calls. Keep shareable filters
65
+ in URL query params; the framework exposes them as `<current-url>` and the
66
+ built-in agent can update them with `set-search-params`.
67
+
60
68
  ## Mounted Workspace Routing
61
69
 
62
70
  This app may be mounted under `/<app-id>` in a workspace. Inside app source, React Router paths are app-local: use `<Link to="/review">` and `navigate("/review")`, not `/<app-id>/review`. The workspace gateway and `APP_BASE_PATH` add the mounted prefix in the browser; hardcoding it inside React Router links causes doubled URLs such as `/<app-id>/<app-id>/review`.
@@ -123,7 +131,7 @@ Skills in `.agents/skills/` provide detailed guidance for each architectural rul
123
131
 
124
132
  **Read the `adding-a-feature` skill first** — it has the full four-area checklist (UI / Action / Skills / App-State). Quick summary:
125
133
 
126
- 1. **Add navigation state entries** — extend `app/hooks/use-navigation-state.ts` to track new routes
134
+ 1. **Add navigation state entries** — extend `app/hooks/use-navigation-state.ts` to track new routes with `useAgentRouteState`
127
135
  2. **Enhance view-screen** — make the view-screen script return relevant context for the new view
128
136
  3. **Create domain actions** — add actions in `actions/` for CRUD operations on new data models; do not create REST wrappers around those actions
129
137
  4. **Wire UI for auto-refresh** — use `useActionQuery` / `useActionMutation` for normal CRUD. If a raw `useQuery` is unavoidable, fold `useChangeVersions([<source>, "action"])` into its key with `placeholderData`. When the agent mutates this data, the UI must reflect the change without a manual refresh. See `real-time-sync` skill.
@@ -1,10 +1,7 @@
1
- import { useEffect, useRef } from "react";
2
- import { useLocation, useNavigate } from "react-router";
3
- import { useQuery, useQueryClient } from "@tanstack/react-query";
4
1
  import {
5
- agentNativePath,
6
2
  appBasePath,
7
3
  appPath,
4
+ useAgentRouteState,
8
5
  } from "@agent-native/core/client";
9
6
  import { TAB_ID } from "../lib/tab-id";
10
7
 
@@ -18,79 +15,16 @@ export interface NavigationState {
18
15
  }
19
16
 
20
17
  export function useNavigationState() {
21
- const location = useLocation();
22
- const navigate = useNavigate();
23
- const qc = useQueryClient();
24
-
25
- useEffect(() => {
26
- const state: NavigationState = {
27
- view: viewFromPath(location.pathname),
28
- path: appPath(location.pathname),
29
- };
30
-
31
- fetch(agentNativePath("/_agent-native/application-state/navigation"), {
32
- method: "PUT",
33
- keepalive: true,
34
- headers: {
35
- "Content-Type": "application/json",
36
- "X-Request-Source": TAB_ID,
37
- },
38
- body: JSON.stringify(state),
39
- }).catch(() => {});
40
- }, [location.pathname]);
41
-
42
- // Default React Query structuralSharing reuses the previous reference when
43
- // the JSON is unchanged, so repeated invalidations driven by `useDbSync`
44
- // (which fire on every relevant app-state event) don't re-fire the
45
- // useEffect with a brand-new object containing the same command.
46
- const { data: navCommand } = useQuery<NavigationState | null>({
47
- queryKey: ["navigate-command"],
48
- queryFn: async () => {
49
- const res = await fetch(
50
- agentNativePath("/_agent-native/application-state/navigate"),
51
- );
52
- if (!res.ok) return null;
53
- const data = await res.json();
54
- return data ?? null;
55
- },
56
- refetchInterval: 2_000,
18
+ useAgentRouteState<NavigationState>({
19
+ browserTabId: TAB_ID,
20
+ requestSource: TAB_ID,
21
+ getNavigationState: ({ pathname, search, hash }) => ({
22
+ view: viewFromPath(pathname),
23
+ path: appPath(`${pathname}${search}${hash}`),
24
+ }),
25
+ getCommandPath: (command) =>
26
+ routerPath(command.path || pathFromView(command.view)),
57
27
  });
58
-
59
- const lastProcessedDedupKeyRef = useRef<string | null>(null);
60
-
61
- useEffect(() => {
62
- if (!navCommand) return;
63
- const cmd = navCommand;
64
- const dedupKey =
65
- cmd._writeId ?? JSON.stringify({ view: cmd.view, path: cmd.path });
66
- if (lastProcessedDedupKeyRef.current === dedupKey) {
67
- // Same command we already handled — the consume-DELETE races against
68
- // the next polling refetch, so when it loses the same command can show
69
- // up again. Re-fire DELETE and bail rather than navigate again.
70
- fetch(agentNativePath("/_agent-native/application-state/navigate"), {
71
- method: "DELETE",
72
- headers: {
73
- "X-Agent-Native-CSRF": "1",
74
- "X-Request-Source": TAB_ID,
75
- },
76
- }).catch(() => {});
77
- qc.setQueryData(["navigate-command"], null);
78
- return;
79
- }
80
- lastProcessedDedupKeyRef.current = dedupKey;
81
-
82
- fetch(agentNativePath("/_agent-native/application-state/navigate"), {
83
- method: "DELETE",
84
- headers: {
85
- "X-Agent-Native-CSRF": "1",
86
- "X-Request-Source": TAB_ID,
87
- },
88
- }).catch(() => {});
89
-
90
- const path = routerPath(cmd.path || pathFromView(cmd.view));
91
- navigate(path);
92
- qc.setQueryData(["navigate-command"], null);
93
- }, [navCommand, navigate, qc]);
94
28
  }
95
29
 
96
30
  function viewFromPath(pathname: string): string {