Skip to content

Commit 20ce273

Browse files
authored
refactor(formatters): consistent component reference formatting (#22)
1 parent 5704136 commit 20ce273

File tree

10 files changed

+249
-90
lines changed

10 files changed

+249
-90
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"agent-react-devtools": minor
3+
---
4+
5+
Standardize component reference format across all CLI output
6+
7+
All formatters now produce consistent `@cN [type] Name` references. Previously, tree and search commands used `@c1 [fn] "Name"` while profiling commands omitted labels, type tags, or both.
8+
9+
**Breaking changes to output format:**
10+
11+
- Component names are no longer quoted: `@c1 [fn] App` instead of `@c1 [fn] "App"`
12+
- Keys use `key=value` instead of `key="value"`
13+
- Profiling commands (`profile slow`, `profile rerenders`, `profile stop`, `profile commit`) now include `@cN` labels and `[type]` tags
14+
- `profile slow` and `profile rerenders` show all render causes instead of only the first
15+
- `profile report` now includes a `[type]` tag in the header
16+
- Column-aligned padding removed from profiling output in favor of consistent `formatRef` formatting

README.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,15 @@ agent-react-devtools get tree --depth 3
4444
```
4545

4646
```
47-
@c1 [fn] "App"
48-
├─ @c2 [fn] "Header"
49-
│ ├─ @c3 [fn] "Nav"
50-
│ └─ @c4 [fn] "SearchBar"
51-
├─ @c5 [fn] "TodoList"
52-
│ ├─ @c6 [fn] "TodoItem" key="1"
53-
│ ├─ @c7 [fn] "TodoItem" key="2"
54-
│ └─ @c8 [fn] "TodoItem" key="3"
55-
└─ @c9 [fn] "Footer"
47+
@c1 [fn] App
48+
├─ @c2 [fn] Header
49+
│ ├─ @c3 [fn] Nav
50+
│ └─ @c4 [fn] SearchBar
51+
├─ @c5 [fn] TodoList
52+
│ ├─ @c6 [fn] TodoItem key=1
53+
│ ├─ @c7 [fn] TodoItem key=2
54+
│ └─ @c8 [fn] TodoItem key=3
55+
└─ @c9 [fn] Footer
5656
```
5757

5858
Inspect a component's props, state, and hooks:
@@ -62,7 +62,7 @@ agent-react-devtools get component @c6
6262
```
6363

6464
```
65-
@c6 [fn] "TodoItem" key="1"
65+
@c6 [fn] TodoItem key=1
6666
props:
6767
id: 1
6868
text: "Buy groceries"
@@ -80,9 +80,9 @@ agent-react-devtools find TodoItem
8080
```
8181

8282
```
83-
@c6 [fn] "TodoItem" key="1"
84-
@c7 [fn] "TodoItem" key="2"
85-
@c8 [fn] "TodoItem" key="3"
83+
@c6 [fn] TodoItem key=1
84+
@c7 [fn] TodoItem key=2
85+
@c8 [fn] TodoItem key=3
8686
```
8787

8888
Profile rendering performance:
@@ -96,9 +96,9 @@ agent-react-devtools profile slow
9696

9797
```
9898
Slowest (by avg render time):
99-
TodoList avg:4.2ms max:8.1ms renders:6 cause:props
100-
SearchBar avg:2.1ms max:3.4ms renders:12 cause:hooks
101-
Header avg:0.8ms max:1.2ms renders:3 cause:parent
99+
@c5 [fn] TodoList avg:4.2ms max:8.1ms renders:6 causes:props-changed
100+
@c4 [fn] SearchBar avg:2.1ms max:3.4ms renders:12 causes:hooks-changed
101+
@c2 [fn] Header avg:0.8ms max:1.2ms renders:3 causes:parent-rendered
102102
```
103103

104104
## Commands

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,20 +56,20 @@ agent-react-devtools profile commit 3 # Detail for commit #3
5656
Every component gets a stable label like `@c1`, `@c2`. Use these to reference components in follow-up commands:
5757

5858
```
59-
@c1 [fn] "App"
60-
├─ @c2 [fn] "Header"
61-
├─ @c3 [fn] "TodoList"
62-
│ ├─ @c4 [fn] "TodoItem" key="1"
63-
│ └─ @c5 [fn] "TodoItem" key="2"
64-
└─ @c6 [host] "div"
59+
@c1 [fn] App
60+
├─ @c2 [fn] Header
61+
├─ @c3 [fn] TodoList
62+
│ ├─ @c4 [fn] TodoItem key=1
63+
│ └─ @c5 [fn] TodoItem key=2
64+
└─ @c6 [host] div
6565
```
6666

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

6969
### Inspected Component
7070

7171
```
72-
@c3 [fn] "TodoList"
72+
@c3 [fn] TodoList
7373
props:
7474
items: [{"id":1,"text":"Buy milk"},{"id":2,"text":"Walk dog"}]
7575
onDelete: ƒ
@@ -87,8 +87,8 @@ hooks:
8787

8888
```
8989
Slowest (by avg render time):
90-
ExpensiveList avg:12.3ms max:18.1ms renders:47 cause:props-changed
91-
TodoItem avg:2.1ms max:5.0ms renders:94 cause:parent-rendered
90+
@c3 [fn] ExpensiveList avg:12.3ms max:18.1ms renders:47 causes:props-changed
91+
@c4 [fn] TodoItem avg:2.1ms max:5.0ms renders:94 causes:parent-rendered, props-changed
9292
```
9393

9494
Render causes: `props-changed`, `state-changed`, `hooks-changed`, `parent-rendered`, `force-update`, `first-mount`.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,12 @@ Stop profiling and collect data from React. Shows a summary with duration, commi
5858
### `agent-react-devtools profile slow [--limit N]`
5959
Rank components by average render duration (slowest first). Default limit: 10.
6060

61-
Output columns: component name, avg duration, max duration, render count, primary cause.
61+
Output columns: label, type tag, component name, avg duration, max duration, render count, all causes.
6262

6363
### `agent-react-devtools profile rerenders [--limit N]`
6464
Rank components by render count (most re-renders first). Default limit: 10.
6565

66-
Output columns: component name, render count, primary cause.
66+
Output columns: label, type tag, component name, render count, all causes.
6767

6868
### `agent-react-devtools profile report <@cN | id>`
6969
Detailed render report for a single component: render count, avg/max/total duration, all render causes.

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

Lines changed: 139 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ import {
55
formatSearchResults,
66
formatCount,
77
formatStatus,
8+
formatProfileSummary,
89
formatProfileReport,
910
formatSlowest,
1011
formatRerenders,
1112
formatTimeline,
13+
formatCommitDetail,
1214
} from '../formatters.js';
1315
import type { TreeNode } from '../component-tree.js';
1416
import type { InspectedElement, StatusInfo, ComponentRenderReport } from '../types.js';
15-
import type { TimelineEntry } from '../profiler.js';
17+
import type { ProfileSummary, TimelineEntry, CommitDetail } from '../profiler.js';
1618

1719
describe('formatTree', () => {
1820
it('should format empty tree', () => {
@@ -27,9 +29,9 @@ describe('formatTree', () => {
2729
];
2830

2931
const result = formatTree(nodes);
30-
expect(result).toContain('@c1 [fn] "App"');
31-
expect(result).toContain('@c2 [memo] "Header"');
32-
expect(result).toContain('@c3 [host] "Footer"');
32+
expect(result).toContain('@c1 [fn] App');
33+
expect(result).toContain('@c2 [memo] Header');
34+
expect(result).toContain('@c3 [host] Footer');
3335
expect(result).toContain('├─');
3436
expect(result).toContain('└─');
3537
});
@@ -41,7 +43,7 @@ describe('formatTree', () => {
4143
];
4244

4345
const result = formatTree(nodes);
44-
expect(result).toContain('key="item-1"');
46+
expect(result).toContain('key=item-1');
4547
});
4648
});
4749

@@ -63,7 +65,7 @@ describe('formatComponent', () => {
6365
};
6466

6567
const result = formatComponent(element, '@c5');
66-
expect(result).toContain('@c5 [fn] "UserProfile"');
68+
expect(result).toContain('@c5 [fn] UserProfile');
6769
expect(result).toContain('props:');
6870
expect(result).toContain(' userId: 42');
6971
expect(result).toContain(' theme: "dark"');
@@ -72,6 +74,23 @@ describe('formatComponent', () => {
7274
expect(result).toContain('hooks:');
7375
expect(result).toContain(' useState: false');
7476
});
77+
78+
it('should show key without quotes', () => {
79+
const element: InspectedElement = {
80+
id: 5,
81+
displayName: 'Item',
82+
type: 'function',
83+
key: 'abc',
84+
props: {},
85+
state: null,
86+
hooks: null,
87+
renderedAt: null,
88+
};
89+
90+
const result = formatComponent(element, '@c5');
91+
expect(result).toContain('key=abc');
92+
expect(result).not.toContain('key="abc"');
93+
});
7594
});
7695

7796
describe('formatSearchResults', () => {
@@ -86,9 +105,9 @@ describe('formatSearchResults', () => {
86105
];
87106

88107
const result = formatSearchResults(results);
89-
expect(result).toContain('@c2 [fn] "UserProfile"');
90-
expect(result).toContain('@c3 [memo] "UserCard"');
91-
expect(result).toContain('key="bob"');
108+
expect(result).toContain('@c2 [fn] UserProfile');
109+
expect(result).toContain('@c3 [memo] UserCard');
110+
expect(result).toContain('key=bob');
92111
});
93112
});
94113

@@ -122,54 +141,113 @@ describe('formatStatus', () => {
122141
});
123142
});
124143

144+
describe('formatProfileSummary', () => {
145+
it('should format summary with labels and types', () => {
146+
const summary: ProfileSummary = {
147+
name: 'test-session',
148+
duration: 5000,
149+
commitCount: 3,
150+
componentRenderCounts: [
151+
{ id: 1, displayName: 'App', label: '@c1', type: 'function', count: 10 },
152+
{ id: 2, displayName: 'Header', label: '@c2', type: 'memo', count: 5 },
153+
],
154+
};
155+
156+
const result = formatProfileSummary(summary);
157+
expect(result).toContain('test-session');
158+
expect(result).toContain('5.0s');
159+
expect(result).toContain('3 commits');
160+
expect(result).toContain('@c1 [fn] App');
161+
expect(result).toContain('10 renders');
162+
expect(result).toContain('@c2 [memo] Header');
163+
expect(result).toContain('5 renders');
164+
});
165+
166+
it('should fallback for missing labels', () => {
167+
const summary: ProfileSummary = {
168+
name: 'sess',
169+
duration: 1000,
170+
commitCount: 1,
171+
componentRenderCounts: [
172+
{ id: 1, displayName: 'App', count: 3 },
173+
],
174+
};
175+
176+
const result = formatProfileSummary(summary);
177+
expect(result).toContain('? [?] App');
178+
});
179+
});
180+
125181
describe('formatProfileReport', () => {
126-
it('should format a render report', () => {
182+
it('should format a render report with type tag', () => {
127183
const report: ComponentRenderReport = {
128184
id: 5,
129185
displayName: 'UserProfile',
186+
label: '@c5',
187+
type: 'function',
130188
renderCount: 12,
131189
totalDuration: 540,
132190
avgDuration: 45,
133191
maxDuration: 120,
134192
causes: ['props-changed', 'state-changed'],
135193
};
136194

137-
const result = formatProfileReport(report, '@c5');
138-
expect(result).toContain('@c5 "UserProfile"');
195+
const result = formatProfileReport(report);
196+
expect(result).toContain('@c5 [fn] UserProfile');
139197
expect(result).toContain('renders:12');
140198
expect(result).toContain('avg:45.0ms');
141199
expect(result).toContain('max:120.0ms');
142200
expect(result).toContain('props-changed');
143201
});
202+
203+
it('should prefer explicit label param over report.label', () => {
204+
const report: ComponentRenderReport = {
205+
id: 5,
206+
displayName: 'UserProfile',
207+
label: '@c5',
208+
type: 'function',
209+
renderCount: 1,
210+
totalDuration: 10,
211+
avgDuration: 10,
212+
maxDuration: 10,
213+
causes: [],
214+
};
215+
216+
const result = formatProfileReport(report, '@c99');
217+
expect(result).toContain('@c99 [fn] UserProfile');
218+
});
144219
});
145220

146221
describe('formatSlowest', () => {
147222
it('should format empty data', () => {
148223
expect(formatSlowest([])).toContain('No profiling data');
149224
});
150225

151-
it('should format slowest components', () => {
226+
it('should format slowest components with labels and all causes', () => {
152227
const reports: ComponentRenderReport[] = [
153-
{ id: 1, displayName: 'SlowComp', renderCount: 5, totalDuration: 250, avgDuration: 50, maxDuration: 100, causes: ['props-changed'] },
154-
{ id: 2, displayName: 'FastComp', renderCount: 10, totalDuration: 100, avgDuration: 10, maxDuration: 20, causes: ['state-changed'] },
228+
{ id: 1, displayName: 'SlowComp', label: '@c1', type: 'function', renderCount: 5, totalDuration: 250, avgDuration: 50, maxDuration: 100, causes: ['props-changed', 'state-changed'] },
229+
{ id: 2, displayName: 'FastComp', label: '@c2', type: 'memo', renderCount: 10, totalDuration: 100, avgDuration: 10, maxDuration: 20, causes: ['state-changed'] },
155230
];
156231

157232
const result = formatSlowest(reports);
158233
expect(result).toContain('Slowest');
159-
expect(result).toContain('SlowComp');
160-
expect(result).toContain('FastComp');
234+
expect(result).toContain('@c1 [fn] SlowComp');
235+
expect(result).toContain('@c2 [memo] FastComp');
236+
expect(result).toContain('causes:props-changed, state-changed');
237+
expect(result).toContain('causes:state-changed');
161238
});
162239
});
163240

164241
describe('formatRerenders', () => {
165-
it('should format rerender data', () => {
242+
it('should format rerender data with labels and all causes', () => {
166243
const reports: ComponentRenderReport[] = [
167-
{ id: 1, displayName: 'Chatty', renderCount: 50, totalDuration: 100, avgDuration: 2, maxDuration: 5, causes: ['parent-rendered'] },
244+
{ id: 1, displayName: 'Chatty', label: '@c1', type: 'function', renderCount: 50, totalDuration: 100, avgDuration: 2, maxDuration: 5, causes: ['parent-rendered', 'props-changed'] },
168245
];
169246

170247
const result = formatRerenders(reports);
171248
expect(result).toContain('50 renders');
172-
expect(result).toContain('parent-rendered');
249+
expect(result).toContain('@c1 [fn] Chatty');
250+
expect(result).toContain('causes:parent-rendered, props-changed');
173251
});
174252
});
175253

@@ -187,3 +265,44 @@ describe('formatTimeline', () => {
187265
expect(result).toContain('8.3ms');
188266
});
189267
});
268+
269+
describe('formatCommitDetail', () => {
270+
it('should format commit detail with labels and types', () => {
271+
const detail: CommitDetail = {
272+
index: 0,
273+
timestamp: 1000,
274+
duration: 15.5,
275+
components: [
276+
{ id: 1, displayName: 'App', label: '@c1', type: 'function', actualDuration: 15.5, selfDuration: 5.2, causes: ['state-changed'] },
277+
{ id: 2, displayName: 'Header', label: '@c2', type: 'memo', actualDuration: 10.3, selfDuration: 10.3, causes: ['props-changed', 'hooks-changed'] },
278+
],
279+
totalComponents: 2,
280+
};
281+
282+
const result = formatCommitDetail(detail);
283+
expect(result).toContain('Commit #0');
284+
expect(result).toContain('15.5ms');
285+
expect(result).toContain('2 components');
286+
expect(result).toContain('@c1 [fn] App');
287+
expect(result).toContain('self:5.2ms');
288+
expect(result).toContain('total:15.5ms');
289+
expect(result).toContain('causes:state-changed');
290+
expect(result).toContain('@c2 [memo] Header');
291+
expect(result).toContain('causes:props-changed, hooks-changed');
292+
});
293+
294+
it('should show hidden count', () => {
295+
const detail: CommitDetail = {
296+
index: 1,
297+
timestamp: 2000,
298+
duration: 10,
299+
components: [
300+
{ id: 1, displayName: 'App', label: '@c1', type: 'function', actualDuration: 10, selfDuration: 10, causes: [] },
301+
],
302+
totalComponents: 5,
303+
};
304+
305+
const result = formatCommitDetail(detail);
306+
expect(result).toContain('... 4 more');
307+
});
308+
});

0 commit comments

Comments
 (0)