Skip to content

Commit 0c307e2

Browse files
authored
feat: track and expose component error/warning counts (#35)
1 parent 1677e02 commit 0c307e2

File tree

11 files changed

+327
-10
lines changed

11 files changed

+327
-10
lines changed

.changeset/error-warning-counts.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"agent-react-devtools": minor
3+
---
4+
5+
Track and expose component error/warning counts
6+
7+
Components now track error and warning counts from the React DevTools protocol (`UPDATE_ERRORS_OR_WARNINGS` operations).
8+
9+
- New `errors` command lists components with non-zero error or warning counts
10+
- `get component` output includes error/warning counts when non-zero
11+
- Tree, search, and component output annotates affected components (e.g., `@c5 [fn] Form ⚠2 ✗1`)

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,28 @@ agent-react-devtools get tree [--depth N] # Component hierarchy
119119
agent-react-devtools get component <@c1 | id> # Props, state, hooks
120120
agent-react-devtools find <name> [--exact] # Search by display name
121121
agent-react-devtools count # Component count by type
122+
agent-react-devtools errors # Components with errors/warnings
122123
```
123124

124125
Components are labeled `@c1`, `@c2`, etc. You can use these labels or numeric IDs interchangeably.
125126

127+
Components with errors or warnings are annotated in tree and search output:
128+
129+
```
130+
@c5 [fn] Form ⚠2 ✗1
131+
```
132+
133+
Use the `errors` command to list only components with issues:
134+
135+
```sh
136+
agent-react-devtools errors
137+
```
138+
139+
```
140+
@c5 [fn] Form ⚠2 ✗1
141+
@c8 [fn] Input ✗3
142+
```
143+
126144
### Wait
127145

128146
Block until a condition is met. Useful in scripts or agent workflows where the daemon starts before the app:
@@ -260,6 +278,7 @@ This project uses agent-react-devtools to inspect the running React app.
260278
- `agent-react-devtools get tree` — see the component hierarchy
261279
- `agent-react-devtools get component @c1` — inspect a specific component
262280
- `agent-react-devtools find <Name>` — search for components
281+
- `agent-react-devtools errors` — list components with errors or warnings
263282
- `agent-react-devtools profile start` / `profile stop` / `profile slow` — diagnose render performance
264283
```
265284

packages/agent-react-devtools/skills/react-devtools/SKILL.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ agent-react-devtools get component @c5 # Props, state, hooks for a specific com
3636
agent-react-devtools find Button # Search by display name (fuzzy)
3737
agent-react-devtools find Button --exact # Exact match
3838
agent-react-devtools count # Count by type: fn, cls, host, memo, ...
39+
agent-react-devtools errors # List components with errors or warnings
3940
```
4041

4142
### Performance Profiling
@@ -70,6 +71,8 @@ Every component gets a stable label like `@c1`, `@c2`. Use these to reference co
7071

7172
Type abbreviations: `fn` = function, `cls` = class, `host` = DOM element, `memo` = React.memo, `fRef` = forwardRef, `susp` = Suspense, `ctx` = context.
7273

74+
Components with errors or warnings show annotations: `⚠2` = 2 warnings, `✗1` = 1 error. Use `agent-react-devtools errors` to list only affected components.
75+
7376
### Inspected Component
7477

7578
```

packages/agent-react-devtools/skills/react-devtools/references/commands.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ Returns a flat list of matching components with labels, types, and keys.
5454
### `agent-react-devtools count`
5555
Count components by type. Output: `42 components (fn:25 host:12 memo:3 cls:2)`.
5656

57+
### `agent-react-devtools errors`
58+
List all components that have non-zero error or warning counts. React tracks console errors and warnings per component; this command surfaces them.
59+
60+
Output example:
61+
```
62+
@c5 [fn] Form ⚠2 ✗1
63+
@c8 [fn] Input ✗3
64+
```
65+
66+
`⚠N` = N warnings, `✗N` = N errors. Returns "No components with errors or warnings" when everything is clean.
67+
68+
Error/warning annotations also appear in `get tree`, `get component`, and `find` output when counts are non-zero.
69+
5770
## Profiling
5871

5972
### `agent-react-devtools profile start [name]`

packages/agent-react-devtools/src/__tests__/component-tree.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,4 +226,116 @@ describe('ComponentTree', () => {
226226
expect(tree.getNode(6)!.type).toBe('profiler');
227227
expect(tree.getNode(7)!.type).toBe('suspense');
228228
});
229+
230+
describe('UPDATE_ERRORS_OR_WARNINGS', () => {
231+
it('should store error and warning counts on nodes', () => {
232+
const ops = buildOps(1, 100, ['App', 'Form'], (s) => [
233+
...addOp(1, 5, 0, s('App')),
234+
...addOp(2, 5, 1, s('Form')),
235+
]);
236+
tree.applyOperations(ops);
237+
238+
// Operation type 5 = UPDATE_ERRORS_OR_WARNINGS: opcode, id, numErrors, numWarnings
239+
const errorOps = [1, 100, 0, 5, 2, 1, 3];
240+
tree.applyOperations(errorOps);
241+
242+
const node = tree.getNode(2);
243+
expect(node).toBeDefined();
244+
expect(node!.errors).toBe(1);
245+
expect(node!.warnings).toBe(3);
246+
247+
// App should still have zero counts
248+
expect(tree.getNode(1)!.errors).toBe(0);
249+
expect(tree.getNode(1)!.warnings).toBe(0);
250+
});
251+
252+
it('should update counts when a second errors operation arrives', () => {
253+
const ops = buildOps(1, 100, ['App'], (s) => [
254+
...addOp(1, 5, 0, s('App')),
255+
]);
256+
tree.applyOperations(ops);
257+
258+
tree.applyOperations([1, 100, 0, 5, 1, 2, 0]);
259+
expect(tree.getNode(1)!.errors).toBe(2);
260+
expect(tree.getNode(1)!.warnings).toBe(0);
261+
262+
// Counts are replaced, not accumulated
263+
tree.applyOperations([1, 100, 0, 5, 1, 0, 5]);
264+
expect(tree.getNode(1)!.errors).toBe(0);
265+
expect(tree.getNode(1)!.warnings).toBe(5);
266+
});
267+
268+
it('should initialize errors and warnings to zero', () => {
269+
const ops = buildOps(1, 100, ['App'], (s) => [
270+
...addOp(1, 5, 0, s('App')),
271+
]);
272+
tree.applyOperations(ops);
273+
274+
expect(tree.getNode(1)!.errors).toBe(0);
275+
expect(tree.getNode(1)!.warnings).toBe(0);
276+
});
277+
});
278+
279+
describe('getComponentsWithErrorsOrWarnings', () => {
280+
it('should return only components with non-zero errors or warnings', () => {
281+
const ops = buildOps(1, 100, ['App', 'Form', 'Button'], (s) => [
282+
...addOp(1, 5, 0, s('App')),
283+
...addOp(2, 5, 1, s('Form')),
284+
...addOp(3, 5, 1, s('Button')),
285+
]);
286+
tree.applyOperations(ops);
287+
288+
// Give Form errors and Button warnings
289+
tree.applyOperations([1, 100, 0, 5, 2, 3, 0]); // Form: 3 errors
290+
tree.applyOperations([1, 100, 0, 5, 3, 0, 2]); // Button: 2 warnings
291+
292+
// Need to call getTree first to assign labels
293+
tree.getTree();
294+
295+
const results = tree.getComponentsWithErrorsOrWarnings();
296+
expect(results).toHaveLength(2);
297+
expect(results.map((r) => r.displayName).sort()).toEqual(['Button', 'Form']);
298+
299+
const form = results.find((r) => r.displayName === 'Form')!;
300+
expect(form.errors).toBe(3);
301+
expect(form.warnings).toBeUndefined();
302+
303+
const button = results.find((r) => r.displayName === 'Button')!;
304+
expect(button.errors).toBeUndefined();
305+
expect(button.warnings).toBe(2);
306+
});
307+
308+
it('should return empty array when no components have errors', () => {
309+
const ops = buildOps(1, 100, ['App'], (s) => [
310+
...addOp(1, 5, 0, s('App')),
311+
]);
312+
tree.applyOperations(ops);
313+
314+
tree.getTree();
315+
const results = tree.getComponentsWithErrorsOrWarnings();
316+
expect(results).toHaveLength(0);
317+
});
318+
});
319+
320+
describe('tree output includes error annotations', () => {
321+
it('should include errors/warnings on tree nodes when non-zero', () => {
322+
const ops = buildOps(1, 100, ['App', 'Form'], (s) => [
323+
...addOp(1, 5, 0, s('App')),
324+
...addOp(2, 5, 1, s('Form')),
325+
]);
326+
tree.applyOperations(ops);
327+
328+
tree.applyOperations([1, 100, 0, 5, 2, 1, 2]);
329+
330+
const treeNodes = tree.getTree();
331+
const formNode = treeNodes.find((n) => n.displayName === 'Form')!;
332+
expect(formNode.errors).toBe(1);
333+
expect(formNode.warnings).toBe(2);
334+
335+
// App has no errors/warnings, so they should be omitted
336+
const appNode = treeNodes.find((n) => n.displayName === 'App')!;
337+
expect(appNode.errors).toBeUndefined();
338+
expect(appNode.warnings).toBeUndefined();
339+
});
340+
});
229341
});

packages/agent-react-devtools/src/__tests__/formatters.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
formatComponent,
55
formatSearchResults,
66
formatCount,
7+
formatErrors,
78
formatStatus,
89
formatAgo,
910
formatProfileSummary,
@@ -428,6 +429,75 @@ describe('formatCommitDetail', () => {
428429
});
429430
});
430431

432+
describe('formatErrors', () => {
433+
it('should format empty results', () => {
434+
expect(formatErrors([])).toContain('No components with errors or warnings');
435+
});
436+
437+
it('should format components with errors and warnings', () => {
438+
const nodes: TreeNode[] = [
439+
{ id: 2, label: '@c2', displayName: 'Form', type: 'function', key: null, parentId: 1, children: [], depth: 1, errors: 3, warnings: 0 },
440+
{ id: 3, label: '@c3', displayName: 'Input', type: 'function', key: null, parentId: 2, children: [], depth: 2, warnings: 2 },
441+
];
442+
443+
const result = formatErrors(nodes);
444+
expect(result).toContain('@c2 [fn] Form');
445+
expect(result).toContain('✗3');
446+
expect(result).toContain('@c3 [fn] Input');
447+
expect(result).toContain('⚠2');
448+
});
449+
});
450+
451+
describe('error annotations in tree', () => {
452+
it('should annotate tree nodes with errors and warnings', () => {
453+
const nodes: TreeNode[] = [
454+
{ id: 1, label: '@c1', displayName: 'App', type: 'function', key: null, parentId: null, children: [2], depth: 0 },
455+
{ id: 2, label: '@c2', displayName: 'Form', type: 'function', key: null, parentId: 1, children: [], depth: 1, errors: 1, warnings: 2 },
456+
];
457+
458+
const result = formatTree(nodes);
459+
expect(result).toContain('@c2 [fn] Form ⚠2 ✗1');
460+
// App should not have annotations
461+
expect(result).toContain('@c1 [fn] App\n');
462+
});
463+
});
464+
465+
describe('error annotations in component', () => {
466+
it('should show error/warning annotations when non-zero', () => {
467+
const element: InspectedElement & { errors?: number; warnings?: number } = {
468+
id: 5,
469+
displayName: 'Form',
470+
type: 'function',
471+
key: null,
472+
props: {},
473+
state: null,
474+
hooks: null,
475+
renderedAt: null,
476+
errors: 2,
477+
warnings: 1,
478+
};
479+
480+
const result = formatComponent(element, '@c5');
481+
expect(result).toContain('@c5 [fn] Form ⚠1 ✗2');
482+
});
483+
484+
it('should not show annotations when counts are zero', () => {
485+
const element: InspectedElement = {
486+
id: 5,
487+
displayName: 'Form',
488+
type: 'function',
489+
key: null,
490+
props: {},
491+
state: null,
492+
hooks: null,
493+
renderedAt: null,
494+
};
495+
496+
const result = formatComponent(element, '@c5');
497+
expect(result).toBe('@c5 [fn] Form');
498+
});
499+
});
500+
431501
describe('formatChangedKeys', () => {
432502
it('should format all key categories', () => {
433503
const keys: ChangedKeys = { props: ['onClick', 'className'], state: ['count'], hooks: [0, 3] };

packages/agent-react-devtools/src/cli.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
formatComponent,
1111
formatSearchResults,
1212
formatCount,
13+
formatErrors,
1314
formatStatus,
1415
formatProfileSummary,
1516
formatProfileReport,
@@ -39,6 +40,7 @@ Components:
3940
get component <@c1 | id> Props, state, hooks
4041
find <name> [--exact] Search by display name
4142
count Component count by type
43+
errors Components with errors/warnings
4244
4345
Wait:
4446
wait --connected [--timeout S] Block until an app connects
@@ -260,6 +262,17 @@ async function main(): Promise<void> {
260262
return;
261263
}
262264

265+
if (cmd0 === 'errors') {
266+
const resp = await sendCommand({ type: 'errors' });
267+
if (resp.ok) {
268+
console.log(formatErrors(resp.data as any));
269+
} else {
270+
console.error(resp.error);
271+
process.exit(1);
272+
}
273+
return;
274+
}
275+
263276
// ── Wait ──
264277
if (cmd0 === 'wait') {
265278
const timeoutSec = parseNumericFlag(flags, 'timeout', 30)!;

0 commit comments

Comments
 (0)