Skip to content

Commit 8b170e5

Browse files
lxsmnsycclaude
andcommitted
feat: list the ancestry of the selected owner
The detail pane gains an ancestry section below the children. - Frames run nearest first, numbered like a stack, with the selected owner as frame zero. - Each frame shows its kind, name and source location, and clicking one selects that owner, so you can walk back up the tree. - The breadcrumb line above the props is gone, because the ancestry says the same thing with more detail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 76ff6fa commit 8b170e5

5 files changed

Lines changed: 125 additions & 17 deletions

File tree

.changeset/ownership-tree.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
Add an ownership tree panel to the dev toolbar.
66

77
The panel shows the app as a tree of owners. Component mode lists components only and folds the scopes between them into the component above, so a component shows the signals, memos and effects created inside it. Owner mode shows every owner.
8-
Selecting a row lists its prop names, the signals it holds with their values, the scopes folded into it and its children.
8+
Selecting a row lists its prop names, the signals it holds with their values, the scopes folded into it, its children, and the ancestry it was created under.
99
Components show where they are declared, and clicking the location opens the file in your editor.
1010
Rows flash when an owner is created, and the tree can be searched by component, scope or signal.

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ above, so a component shows every signal, memo and effect created inside it. Own
4646
shows every owner instead, including roots, memos and effects.
4747

4848
Selecting a row lists its prop names, the signals it holds with their values, the scopes
49-
folded into it and its children. Prop values are getters, so the panel lists their names
49+
folded into it, its children, and the ancestry it was created under. Every frame of the
50+
ancestry is clickable, so you can walk back up the tree. Prop values are getters, so the panel lists their names
5051
and never reads them.
5152

5253
A component also shows where it is declared. The location comes from the hot reload

src/dev-toolbar/ownership/index.tsx

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -151,14 +151,18 @@ export default function OwnershipViewer(props: OwnershipViewerProps): JSX.Elemen
151151
return id ? byId().get(id) : undefined;
152152
});
153153

154-
const selectedPath = createMemo(() => {
154+
const ancestry = createMemo<TreeNode[]>(() => {
155155
const id = selected();
156156
if (!id) return [];
157157
const nodes = byId();
158-
return ancestorsOf(nodes, id)
159-
.map((parent) => nodes.get(parent)?.name)
160-
.filter((name): name is string => !!name)
161-
.reverse();
158+
const current = nodes.get(id);
159+
if (!current) return [];
160+
const frames = [current];
161+
for (const parent of ancestorsOf(nodes, id)) {
162+
const owner = nodes.get(parent);
163+
if (owner) frames.push(owner);
164+
}
165+
return frames;
162166
});
163167

164168
return (
@@ -326,12 +330,6 @@ export default function OwnershipViewer(props: OwnershipViewerProps): JSX.Elemen
326330
)}
327331
</Show>
328332

329-
<Show when={selectedPath().length > 0}>
330-
<Text data-solid-ownership-path options={{ size: 'xs', font: 'mono' }}>
331-
{selectedPath().join(' › ')}
332-
</Text>
333-
</Show>
334-
335333
<Show when={node().hasValue}>
336334
<div data-solid-ownership-detail-block>
337335
<Text options={{ size: 'xs', weight: 'semibold' }}>Value</Text>
@@ -475,6 +473,46 @@ export default function OwnershipViewer(props: OwnershipViewerProps): JSX.Elemen
475473
</For>
476474
</div>
477475
</div>
476+
477+
<div data-solid-ownership-detail-block>
478+
<Text options={{ size: 'xs', weight: 'semibold' }}>
479+
{`Ancestry (${ancestry().length})`}
480+
</Text>
481+
<div data-solid-ownership-stack>
482+
<For each={ancestry()}>
483+
{(frame, index) => (
484+
<button
485+
type="button"
486+
data-solid-ownership-frame
487+
data-current={index() === 0 ? '' : undefined}
488+
onClick={() => setSelected(frame.id)}
489+
>
490+
<span data-solid-ownership-frame-index>{`${index()}`}</span>
491+
<span data-solid-ownership-kind={frame.kind} />
492+
<Text
493+
data-solid-ownership-frame-name
494+
options={{ size: 'xs', weight: 'semibold', font: 'mono' }}
495+
>
496+
{frame.name}
497+
</Text>
498+
<Show when={frame.location}>
499+
{(location) => (
500+
<Text
501+
data-solid-ownership-frame-location
502+
options={{ size: 'xs', font: 'mono', wrap: 'nowrap' }}
503+
>
504+
{location()}
505+
</Text>
506+
)}
507+
</Show>
508+
</button>
509+
)}
510+
</For>
511+
</div>
512+
<Text data-solid-ownership-note options={{ size: 'xs' }}>
513+
The owners this one was created under, nearest first.
514+
</Text>
515+
</div>
478516
</div>
479517
)}
480518
</Show>

src/dev-toolbar/ownership/styles.css

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -307,10 +307,6 @@
307307
background: var(--start-dt-surface-hover);
308308
}
309309

310-
[data-solid-ownership-path] {
311-
color: var(--start-dt-text-muted);
312-
}
313-
314310
[data-solid-ownership-detail-block] {
315311
display: flex;
316312
flex-direction: column;
@@ -377,6 +373,69 @@
377373
white-space: nowrap;
378374
}
379375

376+
[data-solid-ownership-stack] {
377+
display: flex;
378+
flex-direction: column;
379+
380+
gap: 0.125rem;
381+
}
382+
383+
[data-solid-ownership-frame] {
384+
display: flex;
385+
flex-direction: row;
386+
align-items: center;
387+
388+
gap: 0.375rem;
389+
390+
padding: 0.1875rem 0.375rem;
391+
392+
border: none;
393+
border-radius: 0.375rem;
394+
background: none;
395+
color: var(--start-dt-text);
396+
397+
text-align: left;
398+
cursor: pointer;
399+
400+
min-width: 0;
401+
}
402+
403+
[data-solid-ownership-frame]:hover {
404+
background: var(--start-dt-surface-hover);
405+
}
406+
407+
[data-solid-ownership-frame][data-current] {
408+
background: var(--start-dt-surface-active);
409+
}
410+
411+
[data-solid-ownership-frame-index] {
412+
width: 1rem;
413+
flex-shrink: 0;
414+
415+
color: var(--start-dt-text-muted);
416+
417+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
418+
font-size: 0.625rem;
419+
text-align: right;
420+
}
421+
422+
[data-solid-ownership-frame-name] {
423+
overflow: hidden;
424+
text-overflow: ellipsis;
425+
white-space: nowrap;
426+
}
427+
428+
[data-solid-ownership-frame-location] {
429+
margin-left: auto;
430+
431+
color: var(--start-dt-text-muted);
432+
433+
direction: rtl;
434+
overflow: hidden;
435+
text-overflow: ellipsis;
436+
max-width: 55%;
437+
}
438+
380439
[data-solid-ownership-chips] {
381440
display: flex;
382441
flex-wrap: wrap;

tests/e2e/devtools.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,16 @@ test('maps the ownership tree', async ({ page }) => {
9898
await expect(detail).toContainText('Props (1)');
9999
await expect(detail).toContainText('name');
100100

101+
// The ancestry section lists the owners above the selection, nearest first.
102+
const frames = detail.locator('[data-solid-ownership-frame]');
103+
await expect(frames).toHaveCount(2);
104+
await expect(frames.first()).toContainText('<Greeting>');
105+
await expect(frames.nth(1)).toContainText('<App>');
106+
107+
// Clicking a frame walks up the tree.
108+
await frames.nth(1).click();
109+
await expect(detail.locator('[data-solid-ownership-detail-head]')).toContainText('<App>');
110+
101111
// Owner mode adds the scopes that component mode folds away.
102112
await page.getByRole('button', { name: 'Owners', exact: true }).click();
103113
await expect(rows.filter({ hasText: 'doubled' })).toHaveCount(1);

0 commit comments

Comments
 (0)