Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-walls-trace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rozenite/redux-devtools-plugin": minor
---

Add symbolicated Redux action traces in the Redux DevTools panel.
42 changes: 42 additions & 0 deletions apps/playground/src/app/screens/NetworkTestScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import {
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import EventSource from 'react-native-sse';
import { NavigationProp, useNavigation } from '@react-navigation/native';
import {
createSection,
useRozeniteControlsPlugin,
} from '@rozenite/controls-plugin';
import {
NitroWebSocket,
type WebSocketCloseEvent as NitroWebSocketCloseEvent,
Expand Down Expand Up @@ -1782,6 +1786,44 @@ export const NetworkTestScreen: React.FC = () => {
| 'request-body'
>('http');

const networkControlsSections = React.useMemo(
() => [
createSection({
id: 'network-playground',
title: 'Network Playground',
description:
'Local controls registered from the Network screen so we can verify Rozenite Controls can be mounted more than once.',
items: [
{
id: 'active-test',
type: 'text' as const,
title: 'Active Test',
value: activeTest,
},
{
id: 'reset-http',
type: 'button' as const,
title: 'Reset to HTTP',
actionLabel: 'Reset',
onPress: () => setActiveTest('http'),
},
{
id: 'request-body-test',
type: 'button' as const,
title: 'Open Request Body Test',
actionLabel: 'Open',
onPress: () => navigation.navigate('RequestBodyTest'),
},
],
}),
],
[activeTest, navigation],
);

useRozeniteControlsPlugin({
sections: networkControlsSections,
});

const renderHeader = () => (
<View style={styles.header}>
<Text style={styles.title}>Network Test</Text>
Expand Down
1 change: 1 addition & 0 deletions apps/playground/src/app/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const createCounterStore = (name: string) =>
rozeniteDevToolsEnhancer({
name,
maxAge: 150,
trace: true,
}),
),
});
Expand Down
141 changes: 141 additions & 0 deletions docs/react-agent-features/01-get-tree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Get Tree

## Summary

Add a `getTree` tool to the built-in `react` agent domain. It should return a compact component hierarchy for the current React tree, optionally scoped by root/subtree and limited by depth.

This ports the most useful part of `agent-react-devtools get tree`: one command that lets an agent understand the app structure before choosing a component to inspect.

## PR Recommendation

Implement in the same PR as:

- Component labels
- Get component
- Host filtering

Reason: `getTree` should expose stable labels, accept labels as roots, and share traversal/filtering logic with `getComponent`.

## Current Rozenite Behavior

Rozenite currently exposes:

- `searchNodes`
- `getNode`
- `getChildren`

These are accurate but require several calls to reconstruct the tree. The relevant implementation is in:

- `packages/middleware/src/agent/runtime/react/store.ts`
- `packages/middleware/src/agent/runtime/react/types.ts`
- `packages/middleware/src/agent/local-domains.ts`

## Proposed Tool

Tool name: `getTree`

Input:

```ts
type ReactGetTreeRequest = {
root?: number | string;
depth?: number;
noHost?: boolean;
limit?: number;
cursor?: string;
};
```

Output:

```ts
type ReactTreeNode = {
nodeId: number;
label: string;
displayName: string;
elementType: string;
key?: string;
parentId?: number;
parentLabel?: string;
childIds: number[];
childLabels: string[];
childCount: number;
depth: number;
errors?: number;
warnings?: number;
};

type ReactGetTreeResult = {
roots: Array<{ nodeId: number; label: string }>;
items: ReactTreeNode[];
totalCount: number;
page: {
limit: number;
hasMore: boolean;
nextCursor?: string;
};
};
```

Use `items` instead of `nodes` to match existing paginated Rozenite tool style.

## Behavior

- If `root` is omitted, traverse all current React roots.
- If `root` is provided, accept a numeric node ID or a label such as `@c12`.
- `depth` limits descendants relative to the selected traversal root. `0` means only the root node(s).
- `noHost` filters out host nodes that are not significant. See `06-host-filtering.md`.
- Results should be breadth-first or pre-order depth-first, but must be deterministic. Prefer pre-order because it reads like a component tree.
- Include `totalCount` before pagination so agents know whether the response is partial.
- Preserve existing cursor conventions using base64url cursor payloads with `tool: "getTree"` and a filters hash.

## Implementation Steps

1. Add request/result types in `packages/middleware/src/agent/runtime/react/types.ts`.
2. Add constants in `store.ts`:
- `GET_TREE_TOOL_NAME = 'getTree'`
- default/max limits can reuse existing search limits unless a tree-specific default is desired.
3. Add an ID resolver helper shared by future label support:
- `resolveNodeId(state, value, fieldName)`
- Accepts `number` and label `string`.
- Initially numeric only if labels are not in the same patch, but the preferred PR includes labels.
4. Add traversal helper:
- Starts from resolved root or `state.rootIds`.
- Computes `depth`.
- Avoids cycles via a visited set.
- Skips missing child IDs defensively.
5. Convert internal `ReactNodeRecord` to `ReactTreeNode`.
6. Add pagination:
- Include `root`, `depth`, and `noHost` in `filtersHash`.
- Reject mismatched cursors with the same error style as existing tools.
7. Expose `getTree` from `createReactTreeStore`.
8. Register the tool in `createReactDomainService` in `packages/middleware/src/agent/local-domains.ts`.
9. Add `getTree` to `STATIC_DOMAIN_TOOL_NAMES.react` in `packages/agent-sdk/src/constants.ts`.
10. Update `packages/cli/skills/rozenite-agent/domains/react.md`.
11. If required by the repository structure, mirror runtime changes under `packages/cli/src/commands/agent/runtime/react/*`.

## Test Plan

Add focused tests for the store-level behavior. If there is no existing test file for React store, create one under:

`packages/middleware/src/agent/runtime/react/__tests__/store.test.ts`

Cover:

- Returns all roots and descendants in deterministic order.
- `depth: 0`, `depth: 1`, and omitted depth.
- Scoped root by numeric ID.
- Scoped root by label after label support lands.
- Pagination returns `nextCursor` and rejects mismatched cursor context.
- Missing/stale root throws a clear error.
- `noHost` behavior if implemented in the same PR.

Also add a domain registration test if local-domain tests are already asserting built-in tools.

## Acceptance Criteria

- `rozenite agent react call --tool getTree --args '{}' --session <id>` returns a compact tree.
- Existing React tools keep their current schemas and behavior.
- New tool appears in `rozenite agent react tools`.
- SDK domain resolution recognizes `getTree`.

157 changes: 157 additions & 0 deletions docs/react-agent-features/02-component-labels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Component Labels

## Summary

Add agent-friendly labels such as `@c1`, `@c2`, and `@c3` to React node summaries, and allow those labels anywhere a React tool currently accepts `nodeId`.

This ports one of the best usability features from `agent-react-devtools`: agents can inspect by label after reading a tree, without copying long raw numeric React IDs.

## PR Recommendation

Implement in the same PR as:

- Get tree
- Get component
- Host filtering

Reason: labels are most valuable when `getTree` emits them and `getComponent` accepts them. Adding labels alone would not improve the workflow much.

## Current Rozenite Behavior

Rozenite exposes raw numeric React DevTools node IDs:

- `ReactNodeSummary.nodeId`
- `getNode({ nodeId })`
- `getChildren({ nodeId })`
- `getProps({ nodeId })`
- `getState({ nodeId })`
- `getHooks({ nodeId })`
- `searchNodes({ query })`

The relevant files are:

- `packages/middleware/src/agent/runtime/react/types.ts`
- `packages/middleware/src/agent/runtime/react/store.ts`
- `packages/middleware/src/agent/local-domains.ts`

## Proposed Data Model

Add `label` to node summaries:

```ts
type ReactNodeSummary = {
nodeId: number;
label: string;
displayName: string;
elementType: string;
key?: string;
childCount: number;
parentId?: number;
parentLabel?: string;
};
```

Add label maps to `DeviceReactTreeState`:

```ts
type DeviceReactTreeState = {
labelByNodeId: Map<number, string>;
nodeIdByLabel: Map<string, number>;
};
```

Labels should be regenerated whenever a tree sync replaces topology.

## Label Stability

Use deterministic traversal order:

1. Sort roots by numeric ID.
2. Traverse each root in pre-order.
3. Preserve each node's `childIds` order from React DevTools operations.
4. Assign labels incrementally: `@c1`, `@c2`, etc.

Labels are session-local and tree-snapshot-local. They may change after reloads or large tree changes. That is acceptable as long as every response includes the current label.

## Label Resolution

Add helper:

```ts
const resolveNodeId = (
state: DeviceReactTreeState,
value: unknown,
fieldName: string,
): number => { ... };
```

Rules:

- If `value` is an integer, use it as a node ID.
- If `value` is a string matching `/^@c\d+$/`, resolve from `nodeIdByLabel`.
- If `value` is a numeric string, optionally accept it for convenience.
- Throw clear errors:
- `"nodeId" must be an integer or component label like "@c12"`
- `Component label "@c12" no longer exists in the current React tree.`
- `Node "123" no longer exists in the current React tree.`

## Tool Schema Changes

Existing tools can remain backward compatible by adding an optional `id` field while keeping `nodeId`.

Recommended pattern:

```ts
type ReactNodeIdentifier = number | string;

type ReactGetNodeRequest = {
nodeId?: number;
id?: ReactNodeIdentifier;
};
```

Resolution priority:

1. `id`
2. `nodeId`

For minimal churn, existing tool descriptions can say `nodeId` accepts labels after broadening schema to `oneOf: [{ type: 'integer' }, { type: 'string' }]`.

## Implementation Steps

1. Extend React node types with `label` and optional `parentLabel`.
2. Add label maps to device state.
3. In `syncTree`, after `rootIds` and `nodesById` are replaced, rebuild labels.
4. Update `ensureNodeSummary` to include `label` and `parentLabel`.
5. Replace `getNodeId` usage with `resolveNodeId` in:
- `getNode`
- `getChildren`
- `getProps`
- `getState`
- `getHooks`
- `getRenderData` if component/fiber IDs become accepted there later
- new `getTree`
- new `getComponent`
6. Update tool schemas in `createReactDomainService`.
7. Update CLI skill docs with examples using labels.
8. Mirror equivalent CLI runtime files if needed.

## Test Plan

Cover:

- Labels are assigned after tree sync.
- Labels are deterministic for the same tree.
- `searchNodes` returns labels.
- `getNode({ id: "@c2" })` returns the expected node.
- Existing `getNode({ nodeId: 123 })` still works.
- Stale labels produce a helpful error.
- Labels rebuild after a new tree sync.

## Acceptance Criteria

- Every React node summary includes `label`.
- Existing numeric `nodeId` workflows remain valid.
- Agents can use `@cN` labels in all inspection/tree tools.
- Tool descriptions explain that labels are session-local.

Loading
Loading