You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

@rozenite/react-navigation-plugin

Package Overview
Dependencies
Maintainers
1
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@rozenite/react-navigation-plugin - npm Package Compare versions

Comparing version
1.4.0
to
1.5.0
+26
dist/src/react-native/useReactNavigationAgentTools.d.ts
import { NavigationAction, NavigationContainerRef, NavigationState } from '@react-navigation/core';
export type NavigationActionHistoryEntry = {
id: number;
timestamp: number;
action: NavigationAction;
state: NavigationState | undefined;
stack: string | undefined;
};
type NavigateInput = {
name: string;
params?: Record<string, unknown>;
path?: string;
merge?: boolean;
};
type UseReactNavigationAgentToolsConfig<TNavigationContainerRef extends NavigationContainerRef<any> = NavigationContainerRef<any>> = {
ref: React.RefObject<TNavigationContainerRef | null>;
getCurrentState: () => NavigationState | undefined;
getActionHistory: () => NavigationActionHistoryEntry[];
resetRoot: (state: NavigationState) => void;
openLink: (href: string) => Promise<void>;
navigate: (input: NavigateInput) => void;
goBack: (count: number) => number;
dispatchAction: (action: NavigationAction) => void;
};
export declare const useReactNavigationAgentTools: <TNavigationContainerRef extends NavigationContainerRef<any> = NavigationContainerRef<any>>({ ref, getCurrentState, getActionHistory, resetRoot, openLink, navigate, goBack, dispatchAction, }: UseReactNavigationAgentToolsConfig<TNavigationContainerRef>) => void;
export {};
import type {
NavigationAction,
NavigationContainerRef,
NavigationState,
Route,
} from '@react-navigation/core';
import { useRozenitePluginAgentTool, type AgentTool } from '@rozenite/agent-bridge';
export type NavigationActionHistoryEntry = {
id: number;
timestamp: number;
action: NavigationAction;
state: NavigationState | undefined;
stack: string | undefined;
};
type ListActionsInput = {
offset?: number;
limit?: number;
};
type ResetRootInput = {
state: NavigationState;
};
type OpenLinkInput = {
href: string;
};
type NavigateInput = {
name: string;
params?: Record<string, unknown>;
path?: string;
merge?: boolean;
};
type GoBackInput = {
count?: number;
};
type DispatchActionInput = {
action: NavigationAction;
};
type UseReactNavigationAgentToolsConfig<
TNavigationContainerRef extends NavigationContainerRef<any> = NavigationContainerRef<any>
> = {
ref: React.RefObject<TNavigationContainerRef | null>;
getCurrentState: () => NavigationState | undefined;
getActionHistory: () => NavigationActionHistoryEntry[];
resetRoot: (state: NavigationState) => void;
openLink: (href: string) => Promise<void>;
navigate: (input: NavigateInput) => void;
goBack: (count: number) => number;
dispatchAction: (action: NavigationAction) => void;
};
const pluginId = '@rozenite/react-navigation-plugin';
const getRootStateTool: AgentTool = {
name: 'get-root-state',
description: 'Get the current React Navigation root state.',
inputSchema: {
type: 'object',
properties: {},
},
};
const getFocusedRouteTool: AgentTool = {
name: 'get-focused-route',
description: 'Get the currently focused route and route path.',
inputSchema: {
type: 'object',
properties: {},
},
};
const listActionsTool: AgentTool = {
name: 'list-actions',
description: 'List recorded navigation actions with states using pagination.',
inputSchema: {
type: 'object',
properties: {
offset: {
type: 'number',
description: 'Pagination offset. Defaults to 0.',
},
limit: {
type: 'number',
description: 'Pagination size. Defaults to 100. Maximum 100.',
},
},
},
};
const resetRootTool: AgentTool = {
name: 'reset-root',
description: 'Reset navigation root state to provided state snapshot.',
inputSchema: {
type: 'object',
properties: {
state: {
type: 'object',
description: 'Navigation state to reset to.',
},
},
required: ['state'],
},
};
const openLinkTool: AgentTool = {
name: 'open-link',
description: 'Open a deep link URL using React Native Linking.',
inputSchema: {
type: 'object',
properties: {
href: {
type: 'string',
description: 'Deep link URL to open.',
},
},
required: ['href'],
},
};
const navigateTool: AgentTool = {
name: 'navigate',
description: 'Navigate to a route by name with optional params.',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Target route name.',
},
params: {
description: 'Optional route params.',
},
path: {
type: 'string',
description: 'Optional path for deep-link style navigation.',
},
merge: {
type: 'boolean',
description: 'Whether to merge params on existing route.',
},
},
required: ['name'],
},
};
const goBackTool: AgentTool = {
name: 'go-back',
description: 'Go back in navigation history.',
inputSchema: {
type: 'object',
properties: {
count: {
type: 'number',
description: 'How many steps to go back. Defaults to 1.',
},
},
},
};
const dispatchActionTool: AgentTool = {
name: 'dispatch-action',
description:
'Dispatch an arbitrary React Navigation action (e.g. NAVIGATE, JUMP_TO).',
inputSchema: {
type: 'object',
properties: {
action: {
type: 'object',
description: 'React Navigation action object to dispatch.',
},
},
required: ['action'],
},
};
const getCurrentRouteDetails = (state: NavigationState | undefined) => {
if (!state) {
return {
routePath: [] as string[],
focusedRoute: null as null | {
key: string;
name: string;
path?: string;
},
navigatorPath: [] as string[],
params: undefined as unknown,
};
}
const routePath: string[] = [];
const navigatorPath: string[] = [];
let currentState: NavigationState | undefined = state;
let currentRoute: Route<string> | undefined;
while (currentState) {
const index = Math.max(0, currentState.index);
const route = currentState.routes[index];
if (!route) {
break;
}
routePath.push(route.name);
navigatorPath.push(currentState.type);
currentRoute = route as Route<string>;
currentState = route.state as NavigationState | undefined;
}
return {
routePath,
focusedRoute: currentRoute
? {
key: currentRoute.key,
name: currentRoute.name,
path: currentRoute.path,
}
: null,
navigatorPath,
params: currentRoute?.params,
};
};
export const useReactNavigationAgentTools = <
TNavigationContainerRef extends NavigationContainerRef<any> = NavigationContainerRef<any>
>({
ref,
getCurrentState,
getActionHistory,
resetRoot,
openLink,
navigate,
goBack,
dispatchAction,
}: UseReactNavigationAgentToolsConfig<TNavigationContainerRef>) => {
useRozenitePluginAgentTool({
pluginId,
tool: getRootStateTool,
handler: () => {
const state = getCurrentState();
return {
state,
hasState: !!state,
};
},
});
useRozenitePluginAgentTool({
pluginId,
tool: getFocusedRouteTool,
handler: () => {
return getCurrentRouteDetails(getCurrentState());
},
});
useRozenitePluginAgentTool<ListActionsInput>({
pluginId,
tool: listActionsTool,
handler: ({ offset = 0, limit = 100 }) => {
const history = getActionHistory();
const safeOffset = Math.max(0, Math.floor(offset));
const safeLimit = Math.min(100, Math.max(1, Math.floor(limit)));
return {
total: history.length,
offset: safeOffset,
limit: safeLimit,
items: history.slice(safeOffset, safeOffset + safeLimit),
};
},
});
useRozenitePluginAgentTool<ResetRootInput>({
pluginId,
tool: resetRootTool,
handler: ({ state }) => {
if (!state || typeof state !== 'object') {
throw new Error('A valid navigation state is required.');
}
if (!ref.current) {
throw new Error('Navigation ref is not ready.');
}
resetRoot(state);
return {
applied: true,
};
},
});
useRozenitePluginAgentTool<OpenLinkInput>({
pluginId,
tool: openLinkTool,
handler: async ({ href }) => {
if (typeof href !== 'string' || href.trim().length === 0) {
throw new Error('A non-empty href string is required.');
}
await openLink(href);
return {
opened: true,
href,
};
},
});
useRozenitePluginAgentTool<NavigateInput>({
pluginId,
tool: navigateTool,
handler: ({ name, params, path, merge }) => {
if (typeof name !== 'string' || name.trim().length === 0) {
throw new Error('A non-empty route name is required.');
}
if (
params !== undefined &&
(typeof params !== 'object' || params === null || Array.isArray(params))
) {
throw new Error('params must be an object when provided.');
}
if (!ref.current) {
throw new Error('Navigation ref is not ready.');
}
navigate({
name,
params,
path,
merge,
});
return {
applied: true,
name,
};
},
});
useRozenitePluginAgentTool<GoBackInput>({
pluginId,
tool: goBackTool,
handler: ({ count = 1 }) => {
if (!Number.isFinite(count)) {
throw new Error('count must be a finite number.');
}
if (!ref.current) {
throw new Error('Navigation ref is not ready.');
}
const safeCount = Math.max(1, Math.floor(count));
const performed = goBack(safeCount);
return {
applied: performed > 0,
requested: safeCount,
performed,
};
},
});
useRozenitePluginAgentTool<DispatchActionInput>({
pluginId,
tool: dispatchActionTool,
handler: ({ action }) => {
if (!action || typeof action !== 'object') {
throw new Error('A valid navigation action object is required.');
}
if (typeof action.type !== 'string' || action.type.trim().length === 0) {
throw new Error('Navigation action must include a non-empty "type".');
}
if (!ref.current) {
throw new Error('Navigation ref is not ready.');
}
dispatchAction(action);
return {
applied: true,
type: action.type,
};
},
});
};
+12
-0
# @rozenite/react-navigation-plugin
## 1.5.0
### Minor Changes
- [#190](https://github.com/callstackincubator/rozenite/pull/190) [`5ae53a4`](https://github.com/callstackincubator/rozenite/commit/5ae53a4b509adbd8536ea24812f7ca523a95b625) Thanks [@V3RON](https://github.com/V3RON)! - Added Rozenite for Agents support to the Controls, MMKV, React Navigation, and Storage plugins.
### Patch Changes
- Updated dependencies [[`5ae53a4`](https://github.com/callstackincubator/rozenite/commit/5ae53a4b509adbd8536ea24812f7ca523a95b625)]:
- @rozenite/agent-bridge@1.5.0
- @rozenite/plugin-bridge@1.5.0
## 1.4.0

@@ -4,0 +16,0 @@

+394
-6
"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const core = require("@react-navigation/core");
const react = require("react");

@@ -7,2 +8,3 @@ const pluginBridge = require("@rozenite/plugin-bridge");

const reactNative = require("react-native");
const agentBridge = require("@rozenite/agent-bridge");
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };

@@ -100,5 +102,370 @@ const deepEqual__default = /* @__PURE__ */ _interopDefault(deepEqual);

}
const pluginId = "@rozenite/react-navigation-plugin";
const getRootStateTool = {
name: "get-root-state",
description: "Get the current React Navigation root state.",
inputSchema: {
type: "object",
properties: {}
}
};
const getFocusedRouteTool = {
name: "get-focused-route",
description: "Get the currently focused route and route path.",
inputSchema: {
type: "object",
properties: {}
}
};
const listActionsTool = {
name: "list-actions",
description: "List recorded navigation actions with states using pagination.",
inputSchema: {
type: "object",
properties: {
offset: {
type: "number",
description: "Pagination offset. Defaults to 0."
},
limit: {
type: "number",
description: "Pagination size. Defaults to 100. Maximum 100."
}
}
}
};
const resetRootTool = {
name: "reset-root",
description: "Reset navigation root state to provided state snapshot.",
inputSchema: {
type: "object",
properties: {
state: {
type: "object",
description: "Navigation state to reset to."
}
},
required: ["state"]
}
};
const openLinkTool = {
name: "open-link",
description: "Open a deep link URL using React Native Linking.",
inputSchema: {
type: "object",
properties: {
href: {
type: "string",
description: "Deep link URL to open."
}
},
required: ["href"]
}
};
const navigateTool = {
name: "navigate",
description: "Navigate to a route by name with optional params.",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Target route name."
},
params: {
description: "Optional route params."
},
path: {
type: "string",
description: "Optional path for deep-link style navigation."
},
merge: {
type: "boolean",
description: "Whether to merge params on existing route."
}
},
required: ["name"]
}
};
const goBackTool = {
name: "go-back",
description: "Go back in navigation history.",
inputSchema: {
type: "object",
properties: {
count: {
type: "number",
description: "How many steps to go back. Defaults to 1."
}
}
}
};
const dispatchActionTool = {
name: "dispatch-action",
description: "Dispatch an arbitrary React Navigation action (e.g. NAVIGATE, JUMP_TO).",
inputSchema: {
type: "object",
properties: {
action: {
type: "object",
description: "React Navigation action object to dispatch."
}
},
required: ["action"]
}
};
const getCurrentRouteDetails = (state) => {
if (!state) {
return {
routePath: [],
focusedRoute: null,
navigatorPath: [],
params: void 0
};
}
const routePath = [];
const navigatorPath = [];
let currentState = state;
let currentRoute;
while (currentState) {
const index = Math.max(0, currentState.index);
const route = currentState.routes[index];
if (!route) {
break;
}
routePath.push(route.name);
navigatorPath.push(currentState.type);
currentRoute = route;
currentState = route.state;
}
return {
routePath,
focusedRoute: currentRoute ? {
key: currentRoute.key,
name: currentRoute.name,
path: currentRoute.path
} : null,
navigatorPath,
params: currentRoute?.params
};
};
const useReactNavigationAgentTools = ({
ref,
getCurrentState,
getActionHistory,
resetRoot,
openLink,
navigate,
goBack,
dispatchAction
}) => {
agentBridge.useRozenitePluginAgentTool({
pluginId,
tool: getRootStateTool,
handler: () => {
const state = getCurrentState();
return {
state,
hasState: !!state
};
}
});
agentBridge.useRozenitePluginAgentTool({
pluginId,
tool: getFocusedRouteTool,
handler: () => {
return getCurrentRouteDetails(getCurrentState());
}
});
agentBridge.useRozenitePluginAgentTool({
pluginId,
tool: listActionsTool,
handler: ({ offset = 0, limit = 100 }) => {
const history = getActionHistory();
const safeOffset = Math.max(0, Math.floor(offset));
const safeLimit = Math.min(100, Math.max(1, Math.floor(limit)));
return {
total: history.length,
offset: safeOffset,
limit: safeLimit,
items: history.slice(safeOffset, safeOffset + safeLimit)
};
}
});
agentBridge.useRozenitePluginAgentTool({
pluginId,
tool: resetRootTool,
handler: ({ state }) => {
if (!state || typeof state !== "object") {
throw new Error("A valid navigation state is required.");
}
if (!ref.current) {
throw new Error("Navigation ref is not ready.");
}
resetRoot(state);
return {
applied: true
};
}
});
agentBridge.useRozenitePluginAgentTool({
pluginId,
tool: openLinkTool,
handler: async ({ href }) => {
if (typeof href !== "string" || href.trim().length === 0) {
throw new Error("A non-empty href string is required.");
}
await openLink(href);
return {
opened: true,
href
};
}
});
agentBridge.useRozenitePluginAgentTool({
pluginId,
tool: navigateTool,
handler: ({ name, params, path, merge }) => {
if (typeof name !== "string" || name.trim().length === 0) {
throw new Error("A non-empty route name is required.");
}
if (params !== void 0 && (typeof params !== "object" || params === null || Array.isArray(params))) {
throw new Error("params must be an object when provided.");
}
if (!ref.current) {
throw new Error("Navigation ref is not ready.");
}
navigate({
name,
params,
path,
merge
});
return {
applied: true,
name
};
}
});
agentBridge.useRozenitePluginAgentTool({
pluginId,
tool: goBackTool,
handler: ({ count = 1 }) => {
if (!Number.isFinite(count)) {
throw new Error("count must be a finite number.");
}
if (!ref.current) {
throw new Error("Navigation ref is not ready.");
}
const safeCount = Math.max(1, Math.floor(count));
const performed = goBack(safeCount);
return {
applied: performed > 0,
requested: safeCount,
performed
};
}
});
agentBridge.useRozenitePluginAgentTool({
pluginId,
tool: dispatchActionTool,
handler: ({ action }) => {
if (!action || typeof action !== "object") {
throw new Error("A valid navigation action object is required.");
}
if (typeof action.type !== "string" || action.type.trim().length === 0) {
throw new Error('Navigation action must include a non-empty "type".');
}
if (!ref.current) {
throw new Error("Navigation ref is not ready.");
}
dispatchAction(action);
return {
applied: true,
type: action.type
};
}
});
};
const useReactNavigationDevTools = ({
ref
}) => {
const actionHistoryRef = react.useRef([]);
const nextActionIdRef = react.useRef(1);
const currentStateRef = react.useRef(void 0);
const getCurrentState = react.useCallback(() => {
return ref.current?.getRootState() ?? currentStateRef.current;
}, [ref]);
const getActionHistory = react.useCallback(() => {
return actionHistoryRef.current;
}, []);
const resetRoot = react.useCallback(
(state) => {
if (!ref.current) {
throw new Error("Navigation ref is not ready.");
}
ref.current.resetRoot(state);
},
[ref]
);
const openLink = react.useCallback(async (href) => {
await reactNative.Linking.openURL(href);
}, []);
const navigate = react.useCallback(
({
name,
params,
path,
merge
}) => {
if (!ref.current) {
throw new Error("Navigation ref is not ready.");
}
ref.current.dispatch(
core.CommonActions.navigate({
name,
params,
path,
merge
})
);
},
[ref]
);
const goBack = react.useCallback(
(count) => {
if (!ref.current) {
throw new Error("Navigation ref is not ready.");
}
let performed = 0;
for (let i = 0; i < count; i += 1) {
if (!ref.current.canGoBack()) {
break;
}
ref.current.dispatch(core.CommonActions.goBack());
performed += 1;
}
return performed;
},
[ref]
);
const dispatchAction = react.useCallback(
(action) => {
if (!ref.current) {
throw new Error("Navigation ref is not ready.");
}
ref.current.dispatch(action);
},
[ref]
);
useReactNavigationAgentTools({
ref,
getCurrentState,
getActionHistory,
resetRoot,
openLink,
navigate,
goBack,
dispatchAction
});
const client = pluginBridge.useRozeniteDevToolsClient({

@@ -108,2 +475,17 @@ pluginId: "@rozenite/react-navigation-plugin"

useReactNavigationEvents(ref, (message) => {
if (message.type === "action") {
currentStateRef.current = message.state;
const entry = {
id: nextActionIdRef.current,
timestamp: Date.now(),
action: message.action,
state: message.state,
stack: message.stack
};
nextActionIdRef.current += 1;
actionHistoryRef.current = [entry, ...actionHistoryRef.current].slice(
0,
100
);
}
if (!client) {

@@ -121,15 +503,21 @@ return;

client.onMessage("init", () => {
const initialState = ref.current?.getRootState();
currentStateRef.current = initialState;
client.send("initial-state", {
type: "initial-state",
state: ref.current?.getRootState()
state: initialState
});
}),
client.onMessage("reset-root", (message) => {
ref.current?.resetRoot(message.state);
}),
client.onMessage("open-link", (message) => {
if (!message.state) {
return;
}
try {
reactNative.Linking.openURL(message.href);
resetRoot(message.state);
} catch {
}
}),
client.onMessage("open-link", (message) => {
void openLink(message.href).catch(() => {
});
})

@@ -140,4 +528,4 @@ );

};
}, [client]);
}, [client, openLink, ref, resetRoot]);
};
exports.useReactNavigationDevTools = useReactNavigationDevTools;

@@ -0,1 +1,2 @@

import { CommonActions } from "@react-navigation/core";
import { useRef, useEffect, useCallback } from "react";

@@ -5,2 +6,3 @@ import { useRozeniteDevToolsClient } from "@rozenite/plugin-bridge";

import { Linking } from "react-native";
import { useRozenitePluginAgentTool } from "@rozenite/agent-bridge";
function useReactNavigationEvents(ref, callback) {

@@ -96,5 +98,370 @@ const lastStateRef = useRef(void 0);

}
const pluginId = "@rozenite/react-navigation-plugin";
const getRootStateTool = {
name: "get-root-state",
description: "Get the current React Navigation root state.",
inputSchema: {
type: "object",
properties: {}
}
};
const getFocusedRouteTool = {
name: "get-focused-route",
description: "Get the currently focused route and route path.",
inputSchema: {
type: "object",
properties: {}
}
};
const listActionsTool = {
name: "list-actions",
description: "List recorded navigation actions with states using pagination.",
inputSchema: {
type: "object",
properties: {
offset: {
type: "number",
description: "Pagination offset. Defaults to 0."
},
limit: {
type: "number",
description: "Pagination size. Defaults to 100. Maximum 100."
}
}
}
};
const resetRootTool = {
name: "reset-root",
description: "Reset navigation root state to provided state snapshot.",
inputSchema: {
type: "object",
properties: {
state: {
type: "object",
description: "Navigation state to reset to."
}
},
required: ["state"]
}
};
const openLinkTool = {
name: "open-link",
description: "Open a deep link URL using React Native Linking.",
inputSchema: {
type: "object",
properties: {
href: {
type: "string",
description: "Deep link URL to open."
}
},
required: ["href"]
}
};
const navigateTool = {
name: "navigate",
description: "Navigate to a route by name with optional params.",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Target route name."
},
params: {
description: "Optional route params."
},
path: {
type: "string",
description: "Optional path for deep-link style navigation."
},
merge: {
type: "boolean",
description: "Whether to merge params on existing route."
}
},
required: ["name"]
}
};
const goBackTool = {
name: "go-back",
description: "Go back in navigation history.",
inputSchema: {
type: "object",
properties: {
count: {
type: "number",
description: "How many steps to go back. Defaults to 1."
}
}
}
};
const dispatchActionTool = {
name: "dispatch-action",
description: "Dispatch an arbitrary React Navigation action (e.g. NAVIGATE, JUMP_TO).",
inputSchema: {
type: "object",
properties: {
action: {
type: "object",
description: "React Navigation action object to dispatch."
}
},
required: ["action"]
}
};
const getCurrentRouteDetails = (state) => {
if (!state) {
return {
routePath: [],
focusedRoute: null,
navigatorPath: [],
params: void 0
};
}
const routePath = [];
const navigatorPath = [];
let currentState = state;
let currentRoute;
while (currentState) {
const index = Math.max(0, currentState.index);
const route = currentState.routes[index];
if (!route) {
break;
}
routePath.push(route.name);
navigatorPath.push(currentState.type);
currentRoute = route;
currentState = route.state;
}
return {
routePath,
focusedRoute: currentRoute ? {
key: currentRoute.key,
name: currentRoute.name,
path: currentRoute.path
} : null,
navigatorPath,
params: currentRoute?.params
};
};
const useReactNavigationAgentTools = ({
ref,
getCurrentState,
getActionHistory,
resetRoot,
openLink,
navigate,
goBack,
dispatchAction
}) => {
useRozenitePluginAgentTool({
pluginId,
tool: getRootStateTool,
handler: () => {
const state = getCurrentState();
return {
state,
hasState: !!state
};
}
});
useRozenitePluginAgentTool({
pluginId,
tool: getFocusedRouteTool,
handler: () => {
return getCurrentRouteDetails(getCurrentState());
}
});
useRozenitePluginAgentTool({
pluginId,
tool: listActionsTool,
handler: ({ offset = 0, limit = 100 }) => {
const history = getActionHistory();
const safeOffset = Math.max(0, Math.floor(offset));
const safeLimit = Math.min(100, Math.max(1, Math.floor(limit)));
return {
total: history.length,
offset: safeOffset,
limit: safeLimit,
items: history.slice(safeOffset, safeOffset + safeLimit)
};
}
});
useRozenitePluginAgentTool({
pluginId,
tool: resetRootTool,
handler: ({ state }) => {
if (!state || typeof state !== "object") {
throw new Error("A valid navigation state is required.");
}
if (!ref.current) {
throw new Error("Navigation ref is not ready.");
}
resetRoot(state);
return {
applied: true
};
}
});
useRozenitePluginAgentTool({
pluginId,
tool: openLinkTool,
handler: async ({ href }) => {
if (typeof href !== "string" || href.trim().length === 0) {
throw new Error("A non-empty href string is required.");
}
await openLink(href);
return {
opened: true,
href
};
}
});
useRozenitePluginAgentTool({
pluginId,
tool: navigateTool,
handler: ({ name, params, path, merge }) => {
if (typeof name !== "string" || name.trim().length === 0) {
throw new Error("A non-empty route name is required.");
}
if (params !== void 0 && (typeof params !== "object" || params === null || Array.isArray(params))) {
throw new Error("params must be an object when provided.");
}
if (!ref.current) {
throw new Error("Navigation ref is not ready.");
}
navigate({
name,
params,
path,
merge
});
return {
applied: true,
name
};
}
});
useRozenitePluginAgentTool({
pluginId,
tool: goBackTool,
handler: ({ count = 1 }) => {
if (!Number.isFinite(count)) {
throw new Error("count must be a finite number.");
}
if (!ref.current) {
throw new Error("Navigation ref is not ready.");
}
const safeCount = Math.max(1, Math.floor(count));
const performed = goBack(safeCount);
return {
applied: performed > 0,
requested: safeCount,
performed
};
}
});
useRozenitePluginAgentTool({
pluginId,
tool: dispatchActionTool,
handler: ({ action }) => {
if (!action || typeof action !== "object") {
throw new Error("A valid navigation action object is required.");
}
if (typeof action.type !== "string" || action.type.trim().length === 0) {
throw new Error('Navigation action must include a non-empty "type".');
}
if (!ref.current) {
throw new Error("Navigation ref is not ready.");
}
dispatchAction(action);
return {
applied: true,
type: action.type
};
}
});
};
const useReactNavigationDevTools = ({
ref
}) => {
const actionHistoryRef = useRef([]);
const nextActionIdRef = useRef(1);
const currentStateRef = useRef(void 0);
const getCurrentState = useCallback(() => {
return ref.current?.getRootState() ?? currentStateRef.current;
}, [ref]);
const getActionHistory = useCallback(() => {
return actionHistoryRef.current;
}, []);
const resetRoot = useCallback(
(state) => {
if (!ref.current) {
throw new Error("Navigation ref is not ready.");
}
ref.current.resetRoot(state);
},
[ref]
);
const openLink = useCallback(async (href) => {
await Linking.openURL(href);
}, []);
const navigate = useCallback(
({
name,
params,
path,
merge
}) => {
if (!ref.current) {
throw new Error("Navigation ref is not ready.");
}
ref.current.dispatch(
CommonActions.navigate({
name,
params,
path,
merge
})
);
},
[ref]
);
const goBack = useCallback(
(count) => {
if (!ref.current) {
throw new Error("Navigation ref is not ready.");
}
let performed = 0;
for (let i = 0; i < count; i += 1) {
if (!ref.current.canGoBack()) {
break;
}
ref.current.dispatch(CommonActions.goBack());
performed += 1;
}
return performed;
},
[ref]
);
const dispatchAction = useCallback(
(action) => {
if (!ref.current) {
throw new Error("Navigation ref is not ready.");
}
ref.current.dispatch(action);
},
[ref]
);
useReactNavigationAgentTools({
ref,
getCurrentState,
getActionHistory,
resetRoot,
openLink,
navigate,
goBack,
dispatchAction
});
const client = useRozeniteDevToolsClient({

@@ -104,2 +471,17 @@ pluginId: "@rozenite/react-navigation-plugin"

useReactNavigationEvents(ref, (message) => {
if (message.type === "action") {
currentStateRef.current = message.state;
const entry = {
id: nextActionIdRef.current,
timestamp: Date.now(),
action: message.action,
state: message.state,
stack: message.stack
};
nextActionIdRef.current += 1;
actionHistoryRef.current = [entry, ...actionHistoryRef.current].slice(
0,
100
);
}
if (!client) {

@@ -117,15 +499,21 @@ return;

client.onMessage("init", () => {
const initialState = ref.current?.getRootState();
currentStateRef.current = initialState;
client.send("initial-state", {
type: "initial-state",
state: ref.current?.getRootState()
state: initialState
});
}),
client.onMessage("reset-root", (message) => {
ref.current?.resetRoot(message.state);
}),
client.onMessage("open-link", (message) => {
if (!message.state) {
return;
}
try {
Linking.openURL(message.href);
resetRoot(message.state);
} catch {
}
}),
client.onMessage("open-link", (message) => {
void openLink(message.href).catch(() => {
});
})

@@ -136,3 +524,3 @@ );

};
}, [client]);
}, [client, openLink, ref, resetRoot]);
};

@@ -139,0 +527,0 @@ export {

+1
-1

@@ -1,1 +0,1 @@

{"name":"@rozenite/react-navigation-plugin","version":"1.4.0","description":"React Navigation for Rozenite.","panels":[{"name":"React Navigation","source":"/index.html"}]}
{"name":"@rozenite/react-navigation-plugin","version":"1.5.0","description":"React Navigation for Rozenite.","panels":[{"name":"React Navigation","source":"/index.html"}]}
{
"name": "@rozenite/react-navigation-plugin",
"version": "1.4.0",
"version": "1.5.0",
"description": "React Navigation for Rozenite.",

@@ -11,3 +11,4 @@ "type": "module",

"fast-deep-equal": "^3.1.3",
"@rozenite/plugin-bridge": "1.4.0"
"@rozenite/plugin-bridge": "1.5.0",
"@rozenite/agent-bridge": "1.5.0"
},

@@ -30,4 +31,4 @@ "devDependencies": {

"vite": "^7.3.1",
"rozenite": "1.4.0",
"@rozenite/vite-plugin": "1.4.0"
"@rozenite/vite-plugin": "1.5.0",
"rozenite": "1.5.0"
},

@@ -34,0 +35,0 @@ "peerDependencies": {

@@ -85,2 +85,19 @@ ![rozenite-banner](https://www.rozenite.dev/rozenite-banner.jpg)

## Agent Tools (LLM Integration)
When this plugin is active, it registers agent tools under the `@rozenite/react-navigation-plugin` domain. This lets LLMs inspect current navigation state and interact with navigation just like the DevTools panel.
Available tools:
- `navigate`: high-level route navigation by route name with optional params.
- `go-back`: high-level back navigation (`count` defaults to `1`).
- `get-root-state`: returns `{ state, hasState }`.
- `get-focused-route`: returns focused route details (`routePath`, `focusedRoute`, `navigatorPath`, `params`).
- `list-actions`: returns paginated action history (`offset`, `limit`, `total`, `items`).
- `reset-root`: resets navigation state to a provided snapshot.
- `open-link`: opens a deep link URL.
- `dispatch-action`: low-level arbitrary React Navigation action dispatch (for example `NAVIGATE` or `JUMP_TO`).
The action history used for agent tool reads is an in-memory rolling buffer (newest first) capped at 100 entries.
## Made with ❤️ at Callstack

@@ -87,0 +104,0 @@

@@ -1,3 +0,4 @@

import { useEffect } from 'react';
import { NavigationContainerRef } from '@react-navigation/core';
import type { NavigationAction, NavigationState } from '@react-navigation/core';
import { CommonActions, NavigationContainerRef } from '@react-navigation/core';
import { useCallback, useEffect, useRef } from 'react';
import {

@@ -10,2 +11,6 @@ useRozeniteDevToolsClient,

import { Linking } from 'react-native';
import {
NavigationActionHistoryEntry,
useReactNavigationAgentTools,
} from './useReactNavigationAgentTools';

@@ -21,2 +26,100 @@ export type ReactNavigationDevToolsConfig<

}: ReactNavigationDevToolsConfig): void => {
const actionHistoryRef = useRef<NavigationActionHistoryEntry[]>([]);
const nextActionIdRef = useRef(1);
const currentStateRef = useRef<NavigationState | undefined>(undefined);
const getCurrentState = useCallback(() => {
return ref.current?.getRootState() ?? currentStateRef.current;
}, [ref]);
const getActionHistory = useCallback(() => {
return actionHistoryRef.current;
}, []);
const resetRoot = useCallback(
(state: NavigationState) => {
if (!ref.current) {
throw new Error('Navigation ref is not ready.');
}
ref.current.resetRoot(state);
},
[ref]
);
const openLink = useCallback(async (href: string) => {
await Linking.openURL(href);
}, []);
const navigate = useCallback(
({
name,
params,
path,
merge,
}: {
name: string;
params?: Record<string, unknown>;
path?: string;
merge?: boolean;
}) => {
if (!ref.current) {
throw new Error('Navigation ref is not ready.');
}
ref.current.dispatch(
CommonActions.navigate({
name,
params,
path,
merge,
})
);
},
[ref]
);
const goBack = useCallback(
(count: number) => {
if (!ref.current) {
throw new Error('Navigation ref is not ready.');
}
let performed = 0;
for (let i = 0; i < count; i += 1) {
if (!ref.current.canGoBack()) {
break;
}
ref.current.dispatch(CommonActions.goBack());
performed += 1;
}
return performed;
},
[ref]
);
const dispatchAction = useCallback(
(action: NavigationAction) => {
if (!ref.current) {
throw new Error('Navigation ref is not ready.');
}
ref.current.dispatch(action);
},
[ref]
);
useReactNavigationAgentTools({
ref,
getCurrentState,
getActionHistory,
resetRoot,
openLink,
navigate,
goBack,
dispatchAction,
});
const client = useRozeniteDevToolsClient<ReactNavigationPluginEventMap>({

@@ -27,2 +130,18 @@ pluginId: '@rozenite/react-navigation-plugin',

useReactNavigationEvents(ref, (message) => {
if (message.type === 'action') {
currentStateRef.current = message.state;
const entry: NavigationActionHistoryEntry = {
id: nextActionIdRef.current,
timestamp: Date.now(),
action: message.action,
state: message.state,
stack: message.stack,
};
nextActionIdRef.current += 1;
actionHistoryRef.current = [entry, ...actionHistoryRef.current].slice(
0,
100
);
}
if (!client) {

@@ -44,16 +163,24 @@ return;

client.onMessage('init', () => {
const initialState = ref.current?.getRootState();
currentStateRef.current = initialState;
client.send('initial-state', {
type: 'initial-state',
state: ref.current?.getRootState(),
state: initialState,
});
}),
client.onMessage('reset-root', (message) => {
ref.current?.resetRoot(message.state);
}),
client.onMessage('open-link', (message) => {
if (!message.state) {
return;
}
try {
Linking.openURL(message.href);
resetRoot(message.state);
} catch {
// We don't care about errors here
}
}),
client.onMessage('open-link', (message) => {
void openLink(message.href).catch(() => {
// We don't care about errors here
});
})

@@ -65,3 +192,3 @@ );

};
}, [client]);
}, [client, openLink, ref, resetRoot]);
};

@@ -23,2 +23,5 @@ {

{
"path": "../agent-bridge"
},
{
"path": "../plugin-bridge"

@@ -25,0 +28,0 @@ },