From 0fb5a985d3b911e14ac988ff428bc1de4abf4773 Mon Sep 17 00:00:00 2001
From: erKam
Date: Sun, 11 Oct 2026 03:26:24 +0000
Subject: [PATCH 1/2] fix(signals): reset a stale kind filter and forget picks
of closed pages
A kind chosen on one graph stayed active after picking a component that
has no node of that kind. The chips only list kinds in the current
graph, so the list said "No signals match" with no pressed chip and no
visible cause. The kind filter is now a linkedSignal on the graph owner:
it keeps the choice when the new graph has that kind and goes back to
All otherwise. An active kind with no matches in the same graph still
renders as a pressed chip with a count of 0.
pickedByPage only grew. It now drops the entries of pages that leave
the signal graph state, so a long session does not keep picks for
closed tabs.
---
app/src/__tests__/signal-inspector.test.ts | 90 +++++++++++++++++++
app/src/pages/signal-inspector.ts | 49 +++++++---
apps/docs/src/content/inspectors/signals.md | 2 +-
...=> browser-agent-rpc-BXhoSh1z-vUsF80GK.js} | 2 +-
.../{index-CgvJVwtz.js => index-DhT-nDqh.js} | 40 ++++-----
extension/ui/index.html | 2 +-
6 files changed, 151 insertions(+), 34 deletions(-)
rename extension/ui/assets/{browser-agent-rpc-BXhoSh1z-CQUbrXfP.js => browser-agent-rpc-BXhoSh1z-vUsF80GK.js} (93%)
rename extension/ui/assets/{index-CgvJVwtz.js => index-DhT-nDqh.js} (91%)
diff --git a/app/src/__tests__/signal-inspector.test.ts b/app/src/__tests__/signal-inspector.test.ts
index aeec4fcb..b14815bb 100644
--- a/app/src/__tests__/signal-inspector.test.ts
+++ b/app/src/__tests__/signal-inspector.test.ts
@@ -221,6 +221,96 @@ describe('SignalInspector', () => {
expect(trigger(fixture, 'signals-page-label')).toBeNull();
expect(notices(fixture)).toContain('Tab A closed, so this shows another page.');
});
+
+ it('forgets the pick of a page that closed', async () => {
+ const { fixture, push } = await renderPages({ graph: b, pages: { A: a, B: b } });
+ await choose(fixture, 'signals-page-label', 'Tab A');
+ await choose(fixture, 'signals-component-label', 'AppA');
+ expect(fixture.componentInstance.pickerValue()).toBe('cA');
+
+ await push({ graph: b, pages: { B: b } });
+ await push({ graph: b, pages: { A: a, B: b } });
+ await choose(fixture, 'signals-page-label', 'Tab A');
+ expect(fixture.componentInstance.pickerValue()).toBe('follow');
+ });
+ });
+
+ describe('kind chips', () => {
+ const listeners = new Set<(value: unknown) => void>();
+ const owned = (id: string, kinds: string[]) => ({
+ ...graph,
+ nodes: kinds.map((kind, i) => ({ id: String(i + 1), kind, label: `${kind}${i}`, epoch: 1 })),
+ component: { ...graph.component, id },
+ });
+
+ async function renderLive(first: unknown) {
+ listeners.clear();
+ const rpc = {
+ call: () => Promise.resolve([]),
+ callEvent: () => Promise.resolve(),
+ sharedState: (name: string) =>
+ Promise.resolve({
+ value: () => (name === 'signal-graph' ? { graph: first } : { pages: {} }),
+ on: (_event: string, listener: (value: unknown) => void) => {
+ if (name === 'signal-graph') listeners.add(listener);
+ return () => listeners.delete(listener);
+ },
+ }),
+ };
+ const fixture = TestBed.createComponent(SignalInspector);
+ fixture.componentRef.setInput('rpc', {
+ connectionMeta: {},
+ scope: () => ({ rpc }),
+ } as unknown as DevframeRpcClient);
+ for (let i = 0; i < 3; i++) {
+ await new Promise((resolve) => setTimeout(resolve));
+ await fixture.whenStable();
+ }
+ const push = async (next: unknown) => {
+ for (const listener of listeners) listener({ graph: next });
+ await fixture.whenStable();
+ };
+ return { fixture, push };
+ }
+
+ function pressed(fixture: ComponentFixture) {
+ return Array.from(
+ (fixture.nativeElement as HTMLElement).querySelectorAll('.kind-chip[aria-pressed="true"]'),
+ (el) => el.textContent?.replace(/\s+/g, ''),
+ );
+ }
+
+ it('resets a kind the newly picked component does not have', async () => {
+ const { fixture, push } = await renderLive(owned('c1', ['signal', 'linkedSignal']));
+ fixture.componentInstance.kind.set('linkedSignal');
+ await fixture.whenStable();
+
+ await push(owned('c2', ['signal', 'computed']));
+ expect(fixture.componentInstance.kind()).toBeNull();
+ expect(pressed(fixture)).toEqual(['All2']);
+ expect((fixture.nativeElement as HTMLElement).textContent).not.toContain('No signals match.');
+ });
+
+ it('keeps a kind the newly picked component also has', async () => {
+ const { fixture, push } = await renderLive(owned('c1', ['signal', 'computed']));
+ fixture.componentInstance.kind.set('computed');
+ await fixture.whenStable();
+
+ await push(owned('c2', ['computed', 'effect']));
+ expect(fixture.componentInstance.kind()).toBe('computed');
+ expect(pressed(fixture)).toEqual(['computed1']);
+ });
+
+ it('shows an active kind with no matches as a pressed chip with count 0', async () => {
+ const { fixture, push } = await renderLive(owned('c1', ['signal', 'computed']));
+ fixture.componentInstance.kind.set('computed');
+ await fixture.whenStable();
+
+ await push(owned('c1', ['signal']));
+ expect(fixture.componentInstance.kind()).toBe('computed');
+ expect(pressed(fixture)).toEqual(['computed0']);
+ expect((fixture.nativeElement as HTMLElement).textContent).toContain('No signals match.');
+ });
});
it('offers Clear filters when a filter hides the only resource of a resource-only graph', async () => {
diff --git a/app/src/pages/signal-inspector.ts b/app/src/pages/signal-inspector.ts
index 1e301ebb..1c259933 100644
--- a/app/src/pages/signal-inspector.ts
+++ b/app/src/pages/signal-inspector.ts
@@ -10,6 +10,7 @@ import {
input,
linkedSignal,
signal,
+ untracked,
} from '@angular/core';
import { DatePipe, JsonPipe, NgTemplateOutlet } from '@angular/common';
import type { DevframeRpcClient } from 'devframe/client';
@@ -83,6 +84,15 @@ interface LiveNode {
}
const FOLLOW = 'follow';
+
+// Older servers send only the newest graph, without the per-page map.
+function graphPagesOf(state: {
+ graph?: SignalGraph | null;
+ pages: Record;
+}): Record {
+ if (Object.keys(state.pages).length) return state.pages;
+ return state.graph ? { [state.graph.pageId ?? '']: state.graph } : {};
+}
const ENV = 'env:';
const MAX_HISTORY = 50;
@@ -222,7 +232,7 @@ const KIND_COLORS: Record = {
left out.
}
- @if (kindCounts().length) {
+ @if (kindChips().length) {