@agent-native/core 0.36.0 → 0.37.0
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.
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +78 -0
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +8 -4
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +14 -10
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/dynamic-suggestions.d.ts +13 -7
- package/dist/client/dynamic-suggestions.d.ts.map +1 -1
- package/dist/client/dynamic-suggestions.js +23 -12
- package/dist/client/dynamic-suggestions.js.map +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/route-state.d.ts +116 -0
- package/dist/client/route-state.d.ts.map +1 -0
- package/dist/client/route-state.js +205 -0
- package/dist/client/route-state.js.map +1 -0
- package/dist/templates/default/AGENTS.md +7 -1
- package/dist/templates/default/app/hooks/use-navigation-state.ts +10 -76
- package/docs/content/context-awareness.md +90 -48
- package/docs/content/creating-templates.md +22 -1
- package/package.json +2 -1
- package/src/templates/default/AGENTS.md +7 -1
- package/src/templates/default/app/hooks/use-navigation-state.ts +10 -76
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { useLocation, useNavigate, } from "react-router";
|
|
4
|
+
import { deleteClientAppState, readClientAppState, setClientAppState, } from "./application-state.js";
|
|
5
|
+
const SAFE_BROWSER_TAB_ID_RE = /^[A-Za-z0-9_-]{1,96}$/;
|
|
6
|
+
function normalizeBrowserTabId(browserTabId) {
|
|
7
|
+
if (typeof browserTabId !== "string")
|
|
8
|
+
return undefined;
|
|
9
|
+
const trimmed = browserTabId.trim();
|
|
10
|
+
return SAFE_BROWSER_TAB_ID_RE.test(trimmed) ? trimmed : undefined;
|
|
11
|
+
}
|
|
12
|
+
function appStateKeyForBrowserTab(key, browserTabId) {
|
|
13
|
+
return browserTabId ? `${key}:${browserTabId}` : key;
|
|
14
|
+
}
|
|
15
|
+
function routeLocationFromReactRouter(location) {
|
|
16
|
+
return {
|
|
17
|
+
pathname: location.pathname,
|
|
18
|
+
search: location.search,
|
|
19
|
+
hash: location.hash,
|
|
20
|
+
searchParams: new URLSearchParams(location.search),
|
|
21
|
+
location,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function uniqueKeys(keys) {
|
|
25
|
+
return Array.from(new Set(keys));
|
|
26
|
+
}
|
|
27
|
+
function defaultCommandDedupKey(command) {
|
|
28
|
+
if (command && typeof command === "object" && "_writeId" in command) {
|
|
29
|
+
const writeId = command._writeId;
|
|
30
|
+
if (typeof writeId === "string" && writeId)
|
|
31
|
+
return writeId;
|
|
32
|
+
}
|
|
33
|
+
return JSON.stringify(command);
|
|
34
|
+
}
|
|
35
|
+
function currentRouterPath(location) {
|
|
36
|
+
return `${location.pathname}${location.search}${location.hash}`;
|
|
37
|
+
}
|
|
38
|
+
function stringifyForWriteDedup(value) {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.stringify(value);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Keeps semantic UI state agent-visible and consumes agent-authored one-shot
|
|
48
|
+
* commands. This is the framework primitive behind route/navigation sync; it
|
|
49
|
+
* intentionally knows nothing about app-specific route shapes.
|
|
50
|
+
*/
|
|
51
|
+
export function useSemanticNavigationState(options) {
|
|
52
|
+
const { requestSource, commandRefetchInterval = 2_000, enabled = true, keepalive = true, writeDebounceMs = 0, } = options;
|
|
53
|
+
const queryClient = useQueryClient();
|
|
54
|
+
const navigationKeys = useMemo(() => uniqueKeys(options.navigationKeys ?? ["navigation"]), [options.navigationKeys]);
|
|
55
|
+
const commandKeys = useMemo(() => uniqueKeys(options.commandKeys ?? ["navigate"]), [options.commandKeys]);
|
|
56
|
+
const commandQueryKey = useMemo(() => options.commandQueryKey ?? ["navigate-command"], [options.commandQueryKey]);
|
|
57
|
+
const navigationState = options.state ?? null;
|
|
58
|
+
const navigationWriteDedup = stringifyForWriteDedup({
|
|
59
|
+
keys: navigationKeys,
|
|
60
|
+
state: navigationState,
|
|
61
|
+
});
|
|
62
|
+
const getCommandDedupKeyRef = useRef(options.getCommandDedupKey);
|
|
63
|
+
const onCommandRef = useRef(options.onCommand);
|
|
64
|
+
const onErrorRef = useRef(options.onError);
|
|
65
|
+
getCommandDedupKeyRef.current = options.getCommandDedupKey;
|
|
66
|
+
onCommandRef.current = options.onCommand;
|
|
67
|
+
onErrorRef.current = options.onError;
|
|
68
|
+
const lastNavigationWriteRef = useRef(null);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!enabled)
|
|
71
|
+
return;
|
|
72
|
+
if (lastNavigationWriteRef.current === navigationWriteDedup)
|
|
73
|
+
return;
|
|
74
|
+
lastNavigationWriteRef.current = navigationWriteDedup;
|
|
75
|
+
const write = () => {
|
|
76
|
+
for (const key of navigationKeys) {
|
|
77
|
+
setClientAppState(key, navigationState, {
|
|
78
|
+
keepalive,
|
|
79
|
+
requestSource,
|
|
80
|
+
}).catch((error) => onErrorRef.current?.(error));
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
if (writeDebounceMs > 0) {
|
|
84
|
+
const timer = setTimeout(write, writeDebounceMs);
|
|
85
|
+
return () => clearTimeout(timer);
|
|
86
|
+
}
|
|
87
|
+
write();
|
|
88
|
+
}, [
|
|
89
|
+
enabled,
|
|
90
|
+
keepalive,
|
|
91
|
+
navigationKeys,
|
|
92
|
+
navigationState,
|
|
93
|
+
navigationWriteDedup,
|
|
94
|
+
requestSource,
|
|
95
|
+
writeDebounceMs,
|
|
96
|
+
]);
|
|
97
|
+
const commandQuery = useQuery({
|
|
98
|
+
queryKey: commandQueryKey,
|
|
99
|
+
enabled,
|
|
100
|
+
retry: false,
|
|
101
|
+
refetchInterval: commandRefetchInterval,
|
|
102
|
+
queryFn: async () => {
|
|
103
|
+
for (const key of commandKeys) {
|
|
104
|
+
const command = await readClientAppState(key);
|
|
105
|
+
if (command !== null && command !== undefined) {
|
|
106
|
+
return { key, command };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
const clearCommand = useCallback(async () => {
|
|
113
|
+
await Promise.all(commandKeys.map((key) => deleteClientAppState(key, { requestSource }).catch((error) => {
|
|
114
|
+
onErrorRef.current?.(error);
|
|
115
|
+
})));
|
|
116
|
+
queryClient.setQueryData(commandQueryKey, null);
|
|
117
|
+
}, [commandKeys, commandQueryKey, queryClient, requestSource]);
|
|
118
|
+
const lastProcessedDedupKeyRef = useRef(null);
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
const envelope = commandQuery.data;
|
|
121
|
+
if (!enabled || !envelope)
|
|
122
|
+
return;
|
|
123
|
+
const dedupKey = getCommandDedupKeyRef.current?.(envelope.command) ??
|
|
124
|
+
defaultCommandDedupKey(envelope.command);
|
|
125
|
+
const consume = () => {
|
|
126
|
+
deleteClientAppState(envelope.key, { requestSource }).catch((error) => onErrorRef.current?.(error));
|
|
127
|
+
queryClient.setQueryData(commandQueryKey, null);
|
|
128
|
+
};
|
|
129
|
+
if (lastProcessedDedupKeyRef.current === dedupKey) {
|
|
130
|
+
consume();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
lastProcessedDedupKeyRef.current = dedupKey;
|
|
134
|
+
consume();
|
|
135
|
+
Promise.resolve(onCommandRef.current(envelope.command)).catch((error) => onErrorRef.current?.(error));
|
|
136
|
+
}, [commandQuery.data, commandQueryKey, enabled, queryClient, requestSource]);
|
|
137
|
+
return {
|
|
138
|
+
navigationState,
|
|
139
|
+
command: commandQuery.data,
|
|
140
|
+
commandQueryKey,
|
|
141
|
+
clearCommand,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* React Router convenience wrapper around `useSemanticNavigationState`.
|
|
146
|
+
*
|
|
147
|
+
* Use URL query params as the source of truth for shareable filters. This hook
|
|
148
|
+
* writes semantic aliases and stable IDs to `navigation`; the framework's
|
|
149
|
+
* built-in URL sync separately exposes raw `pathname`, `search`, and
|
|
150
|
+
* `searchParams` through `<current-url>` and the `set-search-params` tool.
|
|
151
|
+
*/
|
|
152
|
+
export function useAgentRouteState(options) {
|
|
153
|
+
const { navigationKey = "navigation", commandKey = "navigate", writeGlobalNavigation = true, readGlobalCommandFallback = true, } = options;
|
|
154
|
+
const location = useLocation();
|
|
155
|
+
const navigate = useNavigate();
|
|
156
|
+
const browserTabId = useMemo(() => normalizeBrowserTabId(options.browserTabId), [options.browserTabId]);
|
|
157
|
+
const navigationKeys = useMemo(() => {
|
|
158
|
+
const scopedKey = appStateKeyForBrowserTab(navigationKey, browserTabId);
|
|
159
|
+
const keys = [scopedKey];
|
|
160
|
+
if (browserTabId && writeGlobalNavigation)
|
|
161
|
+
keys.push(navigationKey);
|
|
162
|
+
return uniqueKeys(keys);
|
|
163
|
+
}, [browserTabId, navigationKey, writeGlobalNavigation]);
|
|
164
|
+
const commandKeys = useMemo(() => {
|
|
165
|
+
const scopedKey = appStateKeyForBrowserTab(commandKey, browserTabId);
|
|
166
|
+
const keys = [scopedKey];
|
|
167
|
+
if (browserTabId && readGlobalCommandFallback)
|
|
168
|
+
keys.push(commandKey);
|
|
169
|
+
return uniqueKeys(keys);
|
|
170
|
+
}, [browserTabId, commandKey, readGlobalCommandFallback]);
|
|
171
|
+
const commandQueryKey = useMemo(() => options.commandQueryKey ?? [
|
|
172
|
+
"navigate-command",
|
|
173
|
+
commandKey,
|
|
174
|
+
browserTabId ?? "global",
|
|
175
|
+
], [browserTabId, commandKey, options.commandQueryKey]);
|
|
176
|
+
const routeLocation = useMemo(() => routeLocationFromReactRouter(location), [location]);
|
|
177
|
+
const navigationState = options.getNavigationState(routeLocation) ?? null;
|
|
178
|
+
return useSemanticNavigationState({
|
|
179
|
+
state: navigationState,
|
|
180
|
+
navigationKeys,
|
|
181
|
+
commandKeys,
|
|
182
|
+
commandQueryKey,
|
|
183
|
+
requestSource: options.requestSource,
|
|
184
|
+
commandRefetchInterval: options.refetchInterval,
|
|
185
|
+
enabled: options.enabled,
|
|
186
|
+
keepalive: options.keepalive,
|
|
187
|
+
writeDebounceMs: options.writeDebounceMs,
|
|
188
|
+
getCommandDedupKey: options.getCommandDedupKey,
|
|
189
|
+
onError: options.onError,
|
|
190
|
+
onCommand: (command) => {
|
|
191
|
+
const path = options.getCommandPath(command);
|
|
192
|
+
if (!path)
|
|
193
|
+
return;
|
|
194
|
+
options.onNavigate?.(command, path);
|
|
195
|
+
if (path === currentRouterPath(location))
|
|
196
|
+
return;
|
|
197
|
+
const navigateOptions = options.navigateOptions;
|
|
198
|
+
const resolvedOptions = typeof navigateOptions === "function"
|
|
199
|
+
? navigateOptions(command)
|
|
200
|
+
: navigateOptions;
|
|
201
|
+
navigate(path, resolvedOptions);
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=route-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-state.js","sourceRoot":"","sources":["../../src/client/route-state.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useRef } from \"react\";\nimport { useQuery, useQueryClient, type QueryKey } from \"@tanstack/react-query\";\nimport {\n useLocation,\n useNavigate,\n type Location,\n type NavigateOptions,\n} from \"react-router\";\nimport {\n deleteClientAppState,\n readClientAppState,\n setClientAppState,\n} from \"./application-state.js\";\n\nconst SAFE_BROWSER_TAB_ID_RE = /^[A-Za-z0-9_-]{1,96}$/;\n\nexport interface SemanticNavigationCommandEnvelope<NavigateCommand> {\n key: string;\n command: NavigateCommand;\n}\n\nexport interface UseSemanticNavigationStateOptions<\n NavigationState,\n NavigateCommand = NavigationState,\n> {\n /**\n * Compact, semantic screen state to expose to the agent: view names, record\n * IDs, active tabs, and useful aliases. Keep URL query params in the URL\n * unless the app needs a human-readable semantic alias.\n */\n state: NavigationState | null | undefined;\n /** Application-state keys the UI should write. Defaults to [`navigation`]. */\n navigationKeys?: readonly string[];\n /** Application-state keys to read for one-shot agent commands. Defaults to [`navigate`]. */\n commandKeys?: readonly string[];\n /** React Query key used for command polling/cache. Defaults to [`navigate-command`]. */\n commandQueryKey?: QueryKey;\n /** Request source tag for `useDbSync({ ignoreSource })` jitter prevention. */\n requestSource?: string;\n /** Poll interval for command reads. Defaults to 2000ms. Pass false to disable polling. */\n commandRefetchInterval?: number | false;\n /** Disable both navigation writes and command reads. */\n enabled?: boolean;\n /** Navigation writes use keepalive by default because they often fire during unload. */\n keepalive?: boolean;\n /** Debounce navigation writes. Defaults to 0ms. */\n writeDebounceMs?: number;\n /** Custom duplicate-command key. Defaults to `_writeId` or JSON content. */\n getCommandDedupKey?: (command: NavigateCommand) => string;\n /** Called once for each non-duplicate command after the command is consumed. */\n onCommand: (command: NavigateCommand) => void | Promise<void>;\n /** Optional sink for best-effort navigation write/read/delete/command errors. */\n onError?: (error: unknown) => void;\n}\n\nexport interface UseSemanticNavigationStateResult<\n NavigationState,\n NavigateCommand = NavigationState,\n> {\n navigationState: NavigationState | null;\n command:\n | SemanticNavigationCommandEnvelope<NavigateCommand>\n | null\n | undefined;\n commandQueryKey: QueryKey;\n clearCommand: () => Promise<void>;\n}\n\nexport interface AgentRouteLocation {\n pathname: string;\n search: string;\n hash: string;\n searchParams: URLSearchParams;\n location: Location;\n}\n\nexport interface UseAgentRouteStateOptions<\n NavigationState,\n NavigateCommand = NavigationState,\n> {\n /**\n * Derive compact, semantic screen state from the current React Router URL.\n * The framework separately exposes raw `pathname`, `search`, and parsed\n * `searchParams` through `<current-url>`.\n */\n getNavigationState: (\n location: AgentRouteLocation,\n ) => NavigationState | null | undefined;\n /**\n * Convert an agent-authored one-shot command into an app-local React Router\n * path. Return null to consume and ignore malformed or unsupported commands.\n */\n getCommandPath: (command: NavigateCommand) => string | null | undefined;\n /** Application-state key the UI writes. Defaults to `navigation`. */\n navigationKey?: string;\n /** Application-state key the agent writes for one-shot navigation. */\n commandKey?: string;\n /** Current browser tab id. Enables tab-scoped reads/writes. */\n browserTabId?: string;\n /** Request source tag for `useDbSync({ ignoreSource })` jitter prevention. */\n requestSource?: string;\n /**\n * Also write the unscoped navigation key when browserTabId is present.\n * Defaults to true so CLI/external agents still have a useful fallback.\n */\n writeGlobalNavigation?: boolean;\n /**\n * Fall back to the unscoped command key when no tab-scoped command exists.\n * Defaults to true for backwards compatibility with existing navigate tools.\n */\n readGlobalCommandFallback?: boolean;\n /** React Query key used for command polling/cache. */\n commandQueryKey?: QueryKey;\n /** Poll interval for command reads. Defaults to 2000ms. Pass false to disable polling. */\n refetchInterval?: number | false;\n /** Disable both navigation writes and command reads. */\n enabled?: boolean;\n /** Navigation writes use keepalive by default because they often fire during unload. */\n keepalive?: boolean;\n /** Debounce navigation writes. Defaults to 0ms. */\n writeDebounceMs?: number;\n /** Custom duplicate-command key. Defaults to `_writeId` or JSON content. */\n getCommandDedupKey?: (command: NavigateCommand) => string;\n /** React Router navigate options, or a function of the consumed command. */\n navigateOptions?:\n | NavigateOptions\n | ((command: NavigateCommand) => NavigateOptions | undefined);\n /** Called after a command is consumed and before React Router navigation. */\n onNavigate?: (command: NavigateCommand, path: string) => void;\n /** Optional sink for best-effort navigation write/read/delete errors. */\n onError?: (error: unknown) => void;\n}\n\nexport interface UseAgentRouteStateResult<\n NavigationState,\n NavigateCommand = NavigationState,\n> extends UseSemanticNavigationStateResult<NavigationState, NavigateCommand> {}\n\nfunction normalizeBrowserTabId(browserTabId?: string): string | undefined {\n if (typeof browserTabId !== \"string\") return undefined;\n const trimmed = browserTabId.trim();\n return SAFE_BROWSER_TAB_ID_RE.test(trimmed) ? trimmed : undefined;\n}\n\nfunction appStateKeyForBrowserTab(key: string, browserTabId?: string): string {\n return browserTabId ? `${key}:${browserTabId}` : key;\n}\n\nfunction routeLocationFromReactRouter(location: Location): AgentRouteLocation {\n return {\n pathname: location.pathname,\n search: location.search,\n hash: location.hash,\n searchParams: new URLSearchParams(location.search),\n location,\n };\n}\n\nfunction uniqueKeys(keys: readonly string[]): string[] {\n return Array.from(new Set(keys));\n}\n\nfunction defaultCommandDedupKey(command: unknown): string {\n if (command && typeof command === \"object\" && \"_writeId\" in command) {\n const writeId = (command as { _writeId?: unknown })._writeId;\n if (typeof writeId === \"string\" && writeId) return writeId;\n }\n return JSON.stringify(command);\n}\n\nfunction currentRouterPath(location: Location): string {\n return `${location.pathname}${location.search}${location.hash}`;\n}\n\nfunction stringifyForWriteDedup(value: unknown): string {\n try {\n return JSON.stringify(value);\n } catch {\n return \"\";\n }\n}\n\n/**\n * Keeps semantic UI state agent-visible and consumes agent-authored one-shot\n * commands. This is the framework primitive behind route/navigation sync; it\n * intentionally knows nothing about app-specific route shapes.\n */\nexport function useSemanticNavigationState<\n NavigationState,\n NavigateCommand = NavigationState,\n>(\n options: UseSemanticNavigationStateOptions<NavigationState, NavigateCommand>,\n): UseSemanticNavigationStateResult<NavigationState, NavigateCommand> {\n const {\n requestSource,\n commandRefetchInterval = 2_000,\n enabled = true,\n keepalive = true,\n writeDebounceMs = 0,\n } = options;\n\n const queryClient = useQueryClient();\n const navigationKeys = useMemo(\n () => uniqueKeys(options.navigationKeys ?? [\"navigation\"]),\n [options.navigationKeys],\n );\n const commandKeys = useMemo(\n () => uniqueKeys(options.commandKeys ?? [\"navigate\"]),\n [options.commandKeys],\n );\n const commandQueryKey = useMemo<QueryKey>(\n () => options.commandQueryKey ?? [\"navigate-command\"],\n [options.commandQueryKey],\n );\n const navigationState = options.state ?? null;\n const navigationWriteDedup = stringifyForWriteDedup({\n keys: navigationKeys,\n state: navigationState,\n });\n\n const getCommandDedupKeyRef = useRef(options.getCommandDedupKey);\n const onCommandRef = useRef(options.onCommand);\n const onErrorRef = useRef(options.onError);\n getCommandDedupKeyRef.current = options.getCommandDedupKey;\n onCommandRef.current = options.onCommand;\n onErrorRef.current = options.onError;\n\n const lastNavigationWriteRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!enabled) return;\n if (lastNavigationWriteRef.current === navigationWriteDedup) return;\n lastNavigationWriteRef.current = navigationWriteDedup;\n\n const write = () => {\n for (const key of navigationKeys) {\n setClientAppState(key, navigationState, {\n keepalive,\n requestSource,\n }).catch((error) => onErrorRef.current?.(error));\n }\n };\n\n if (writeDebounceMs > 0) {\n const timer = setTimeout(write, writeDebounceMs);\n return () => clearTimeout(timer);\n }\n write();\n }, [\n enabled,\n keepalive,\n navigationKeys,\n navigationState,\n navigationWriteDedup,\n requestSource,\n writeDebounceMs,\n ]);\n\n const commandQuery =\n useQuery<SemanticNavigationCommandEnvelope<NavigateCommand> | null>({\n queryKey: commandQueryKey,\n enabled,\n retry: false,\n refetchInterval: commandRefetchInterval,\n queryFn: async () => {\n for (const key of commandKeys) {\n const command = await readClientAppState<NavigateCommand>(key);\n if (command !== null && command !== undefined) {\n return { key, command };\n }\n }\n return null;\n },\n });\n\n const clearCommand = useCallback(async () => {\n await Promise.all(\n commandKeys.map((key) =>\n deleteClientAppState(key, { requestSource }).catch((error) => {\n onErrorRef.current?.(error);\n }),\n ),\n );\n queryClient.setQueryData(commandQueryKey, null);\n }, [commandKeys, commandQueryKey, queryClient, requestSource]);\n\n const lastProcessedDedupKeyRef = useRef<string | null>(null);\n\n useEffect(() => {\n const envelope = commandQuery.data;\n if (!enabled || !envelope) return;\n\n const dedupKey =\n getCommandDedupKeyRef.current?.(envelope.command) ??\n defaultCommandDedupKey(envelope.command);\n const consume = () => {\n deleteClientAppState(envelope.key, { requestSource }).catch((error) =>\n onErrorRef.current?.(error),\n );\n queryClient.setQueryData(commandQueryKey, null);\n };\n\n if (lastProcessedDedupKeyRef.current === dedupKey) {\n consume();\n return;\n }\n lastProcessedDedupKeyRef.current = dedupKey;\n consume();\n\n Promise.resolve(onCommandRef.current(envelope.command)).catch((error) =>\n onErrorRef.current?.(error),\n );\n }, [commandQuery.data, commandQueryKey, enabled, queryClient, requestSource]);\n\n return {\n navigationState,\n command: commandQuery.data,\n commandQueryKey,\n clearCommand,\n };\n}\n\n/**\n * React Router convenience wrapper around `useSemanticNavigationState`.\n *\n * Use URL query params as the source of truth for shareable filters. This hook\n * writes semantic aliases and stable IDs to `navigation`; the framework's\n * built-in URL sync separately exposes raw `pathname`, `search`, and\n * `searchParams` through `<current-url>` and the `set-search-params` tool.\n */\nexport function useAgentRouteState<\n NavigationState,\n NavigateCommand = NavigationState,\n>(\n options: UseAgentRouteStateOptions<NavigationState, NavigateCommand>,\n): UseAgentRouteStateResult<NavigationState, NavigateCommand> {\n const {\n navigationKey = \"navigation\",\n commandKey = \"navigate\",\n writeGlobalNavigation = true,\n readGlobalCommandFallback = true,\n } = options;\n\n const location = useLocation();\n const navigate = useNavigate();\n const browserTabId = useMemo(\n () => normalizeBrowserTabId(options.browserTabId),\n [options.browserTabId],\n );\n const navigationKeys = useMemo(() => {\n const scopedKey = appStateKeyForBrowserTab(navigationKey, browserTabId);\n const keys = [scopedKey];\n if (browserTabId && writeGlobalNavigation) keys.push(navigationKey);\n return uniqueKeys(keys);\n }, [browserTabId, navigationKey, writeGlobalNavigation]);\n const commandKeys = useMemo(() => {\n const scopedKey = appStateKeyForBrowserTab(commandKey, browserTabId);\n const keys = [scopedKey];\n if (browserTabId && readGlobalCommandFallback) keys.push(commandKey);\n return uniqueKeys(keys);\n }, [browserTabId, commandKey, readGlobalCommandFallback]);\n const commandQueryKey = useMemo<QueryKey>(\n () =>\n options.commandQueryKey ?? [\n \"navigate-command\",\n commandKey,\n browserTabId ?? \"global\",\n ],\n [browserTabId, commandKey, options.commandQueryKey],\n );\n\n const routeLocation = useMemo(\n () => routeLocationFromReactRouter(location),\n [location],\n );\n const navigationState = options.getNavigationState(routeLocation) ?? null;\n\n return useSemanticNavigationState<NavigationState, NavigateCommand>({\n state: navigationState,\n navigationKeys,\n commandKeys,\n commandQueryKey,\n requestSource: options.requestSource,\n commandRefetchInterval: options.refetchInterval,\n enabled: options.enabled,\n keepalive: options.keepalive,\n writeDebounceMs: options.writeDebounceMs,\n getCommandDedupKey: options.getCommandDedupKey,\n onError: options.onError,\n onCommand: (command) => {\n const path = options.getCommandPath(command);\n if (!path) return;\n options.onNavigate?.(command, path);\n if (path === currentRouterPath(location)) return;\n\n const navigateOptions = options.navigateOptions;\n const resolvedOptions =\n typeof navigateOptions === \"function\"\n ? navigateOptions(command)\n : navigateOptions;\n navigate(path, resolvedOptions);\n },\n });\n}\n"]}
|
|
@@ -57,6 +57,12 @@ Ephemeral UI state is stored in the SQL `application_state` table, accessed via
|
|
|
57
57
|
|
|
58
58
|
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
59
|
|
|
60
|
+
UI code should use `useAgentRouteState` / `useSemanticNavigationState` from
|
|
61
|
+
`@agent-native/core/client` for navigation sync instead of hand-written
|
|
62
|
+
`fetch("/_agent-native/application-state/...")` calls. Keep shareable filters
|
|
63
|
+
in URL query params; the framework exposes them as `<current-url>` and the
|
|
64
|
+
built-in agent can update them with `set-search-params`.
|
|
65
|
+
|
|
60
66
|
## Mounted Workspace Routing
|
|
61
67
|
|
|
62
68
|
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 +129,7 @@ Skills in `.agents/skills/` provide detailed guidance for each architectural rul
|
|
|
123
129
|
|
|
124
130
|
**Read the `adding-a-feature` skill first** — it has the full four-area checklist (UI / Action / Skills / App-State). Quick summary:
|
|
125
131
|
|
|
126
|
-
1. **Add navigation state entries** — extend `app/hooks/use-navigation-state.ts` to track new routes
|
|
132
|
+
1. **Add navigation state entries** — extend `app/hooks/use-navigation-state.ts` to track new routes with `useAgentRouteState`
|
|
127
133
|
2. **Enhance view-screen** — make the view-screen script return relevant context for the new view
|
|
128
134
|
3. **Create domain actions** — add actions in `actions/` for CRUD operations on new data models; do not create REST wrappers around those actions
|
|
129
135
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 {
|
|
@@ -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
|
-
|
|
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. **
|
|
18
|
-
3.
|
|
19
|
-
4.
|
|
20
|
-
5.
|
|
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 |
|
|
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:
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
-
```
|
|
263
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
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
|
-
```
|
|
333
|
+
```tsx
|
|
296
334
|
// app/hooks/use-navigation-state.ts
|
|
297
|
-
import {
|
|
298
|
-
import {
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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,
|
|
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";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
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",
|