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
49 changes: 32 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Inspect Angular component trees, signals, dependency injection, and routes — a
- **DI inspector** — browse the injector hierarchy (element and environment) with providers at each level (Angular 17+)
- **Route inspector** — list registered routes from source
- **NgRx Store inspector** — detect `@ngrx/store` (actions, reducers, effects, selectors) and `@ngrx/signals` (`signalStore`, `signalState`, `signalMethod`) patterns from source; live state & action log via Redux DevTools protocol
- **Forms inspector** — every form on the page (Signal Forms, reactive and template-driven) with each field's value, status, touched/dirty state and readable errors, plus a timeline of recent changes; hover a field to highlight its input
- **Build metadata** — Angular version, TypeScript version, SSR status
- **In-page popup** — floating devtools panel with dock modes (float, bottom, right), drag, resize, and localStorage persistence
- **Agent-native** — all inspectors exposed as MCP tools and resources
Expand Down Expand Up @@ -87,26 +88,40 @@ When embedded in Express, the MCP endpoint is also available over HTTP at `/__ng

MCP clients see these with an underscore, as `ng-devtools_get-routes`.

| Tool | Description |
| ------------------------------- | ----------------------------------------------------------- |
| `ng-devtools:get-routes` | List Angular routes from source |
| `ng-devtools:get-components` | Discover components and directives, with inputs and outputs |
| `ng-devtools:get-signals` | Signal declarations from source |
| `ng-devtools:get-providers` | DI providers from source |
| `ng-devtools:build-meta` | Angular/TS versions, SSR status |
| `ng-devtools:highlight` | Highlight a component in the page |
| `ng-devtools:inspect-signals` | Signal graph a connected page reported |
| `ng-devtools:inspect-providers` | Injector tree a connected page reported |
| `ng-devtools:get-ngrx-store` | Scan source for NgRx store patterns |
| Tool | Description |
| ---------------------------------- | ----------------------------------------------------------- |
| `ng-devtools:get-routes` | List Angular routes from source |
| `ng-devtools:get-components` | Discover components and directives, with inputs and outputs |
| `ng-devtools:get-signals` | Signal declarations from source |
| `ng-devtools:get-providers` | DI providers from source |
| `ng-devtools:build-meta` | Angular/TS versions, SSR status |
| `ng-devtools:highlight` | Highlight a component in the page |
| `ng-devtools:inspect-signals` | Signal graph a connected page reported |
| `ng-devtools:inspect-providers` | Injector tree a connected page reported |
| `ng-devtools:get-ngrx-store` | Scan source for NgRx store patterns |
| `ng-devtools:inspect-forms` | Forms on the page with every field's state and errors |
| `ng-devtools:explain-form-invalid` | Which fields make a form invalid, and why |

#### Forms

The Forms tab and the forms tools read Signal Forms, reactive forms and template-driven forms from the running page, in development builds only. Signal Forms need Angular 21 or later. The live change timeline for reactive and template-driven forms uses `control.events` (Angular 18+); on Angular 17 changes are picked up every few seconds instead, without submit and reset events.

- Each field shows its value, status, touched/dirty state and errors, plus: Signal Forms constraints (`min`, `max`, `minLength`, `maxLength`, `pattern`), a pending `debounce`, `submitting`, and disabled reasons; for reactive and template-driven forms, whether validators and async validators are attached, the value `reset()` goes back to, `updateOn`, and the bound `ControlValueAccessor`.
- `ng-devtools:explain-form-invalid` is the tool to reach for first: without arguments it lists every form that is invalid or waiting on async validation, with each failing field's current value, the validator that failed, its message and whether it was touched. Pass `form` (an id like `form-1`, or part of a label like `SignupComponent`) to explain one form.
- `ng-devtools:inspect-forms` lists the forms with their status and error counts. Pass `form` for a field tree, plus `path` (e.g. `address.city`), `onlyInvalid` or `includeValues: false` to narrow it down.
- Both tools note when the page last reported, so an agent can tell when the data is stale.

Form values leave the page: they are sent to the devtools server, shown in the Forms tab and returned to agents. Values of password fields, fields with a password, one-time-code or credit-card `autocomplete`, and fields whose name looks secret (password, token, card, cvv and similar) are replaced with `[redacted]`. Other values are sent as they are, so keep real credentials out of forms you inspect, and don't expose the dev server beyond localhost.

#### Agent Resources

| Resource | Content |
| ---------------------------- | ---------------------------- |
| `ng-devtools:component-tree` | Live component hierarchy |
| `ng-devtools:signal-graph` | Signal dependency graph |
| `ng-devtools:injector-tree` | DI injector hierarchy |
| `ng-devtools:ngrx-store` | Live NgRx state & action log |
| Resource | Content |
| ---------------------------- | ----------------------------- |
| `ng-devtools:component-tree` | Live component hierarchy |
| `ng-devtools:signal-graph` | Signal dependency graph |
| `ng-devtools:injector-tree` | DI injector hierarchy |
| `ng-devtools:ngrx-store` | Live NgRx state & action log |
| `ng-devtools:forms` | Live forms and recent changes |

### Vite DevTools Dock

Expand Down
33 changes: 29 additions & 4 deletions app/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,24 @@ import { RouteInspector } from './pages/route-inspector';
import { SignalInspector } from './pages/signal-inspector';
import { DiInspector } from './pages/di-inspector';
import { StoreInspector } from './pages/store-inspector';
import { FormsInspector } from './pages/forms-inspector';

type Tab = 'dashboard' | 'components' | 'routes' | 'signals' | 'injectors' | 'store';
type Tab = 'dashboard' | 'components' | 'routes' | 'signals' | 'injectors' | 'store' | 'forms';

@Component({
selector: 'app-root',
imports: [Dashboard, ComponentTree, RouteInspector, SignalInspector, DiInspector, StoreInspector],
imports: [
Dashboard,
ComponentTree,
RouteInspector,
SignalInspector,
DiInspector,
StoreInspector,
FormsInspector,
],
template: `
<header>
<div class="brand">
<h1 class="brand">
<!-- The Angular shield, from the wordmark on angular.dev. -->
<svg width="20" height="22" viewBox="0 0 223 236" fill="url(#ng-logo)" aria-hidden="true">
<defs>
Expand All @@ -39,7 +48,7 @@ type Tab = 'dashboard' | 'components' | 'routes' | 'signals' | 'injectors' | 'st
/>
</svg>
<span>Angular DevTools</span>
</div>
</h1>
<nav>
@for (t of tabs; track t.id) {
<button [class.active]="tab() === t.id" (click)="switchTab(t.id)">{{ t.label }}</button>
Expand Down Expand Up @@ -69,6 +78,9 @@ type Tab = 'dashboard' | 'components' | 'routes' | 'signals' | 'injectors' | 'st
@case ('store') {
<app-store-inspector [rpc]="rpc()" />
}
@case ('forms') {
<app-forms-inspector [rpc]="rpc()" />
}
}
</main>
`,
Expand All @@ -80,13 +92,16 @@ type Tab = 'dashboard' | 'components' | 'routes' | 'signals' | 'injectors' | 'st
}
header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px;
padding: 8px 16px;
background: #18181b;
border-bottom: 1px solid #27272a;
}
.brand {
margin: 0;
font-size: inherit;
display: flex;
align-items: center;
gap: 8px;
Expand All @@ -99,8 +114,16 @@ type Tab = 'dashboard' | 'components' | 'routes' | 'signals' | 'injectors' | 'st
}
nav {
display: flex;
flex-wrap: wrap;
gap: 4px;
flex: 1;
min-width: 0;
}
@media (max-width: 640px) {
nav {
order: 3;
flex-basis: 100%;
}
}
nav button {
padding: 6px 14px;
Expand All @@ -121,6 +144,7 @@ type Tab = 'dashboard' | 'components' | 'routes' | 'signals' | 'injectors' | 'st
color: #fff;
}
.status {
margin-left: auto;
font-size: 12px;
padding: 3px 10px;
border-radius: 99px;
Expand All @@ -146,6 +170,7 @@ export class App implements OnInit, OnDestroy {
{ id: 'signals' as Tab, label: 'Signals' },
{ id: 'injectors' as Tab, label: 'Injectors' },
{ id: 'store' as Tab, label: 'Store' },
{ id: 'forms' as Tab, label: 'Forms' },
];

tab = signal<Tab>('dashboard');
Expand Down
14 changes: 7 additions & 7 deletions app/src/pages/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { DevframeRpcClient } from 'devframe/client';
template: `
<div class="grid">
<div class="card">
<h3>Project</h3>
<h2>Project</h2>
<dl>
<dt>Name</dt>
<dd>{{ meta()?.projectName ?? '…' }}</dd>
Expand All @@ -19,27 +19,27 @@ import type { DevframeRpcClient } from 'devframe/client';
</dl>
</div>
<div class="card clickable" (click)="navigate.emit('components')">
<h3>Components</h3>
<h2>Components</h2>
<p class="big">{{ componentCount() }}</p>
<p class="sub">discovered in source</p>
</div>
<div class="card clickable" (click)="navigate.emit('routes')">
<h3>Routes</h3>
<h2>Routes</h2>
<p class="big">{{ routeCount() }}</p>
<p class="sub">registered paths</p>
</div>
<div class="card clickable" (click)="navigate.emit('signals')">
<h3>Signals</h3>
<h2>Signals</h2>
<p class="big">{{ signalCount() }}</p>
<p class="sub">reactive primitives</p>
</div>
<div class="card clickable" (click)="navigate.emit('injectors')">
<h3>Injectors</h3>
<h2>Injectors</h2>
<p class="big">{{ providerCount() }}</p>
<p class="sub">DI providers</p>
</div>
<div class="card clickable" (click)="navigate.emit('store')">
<h3>NgRx Store</h3>
<h2>NgRx Store</h2>
<p class="big">{{ storeCount() }}</p>
<p class="sub">store entries</p>
</div>
Expand All @@ -64,7 +64,7 @@ import type { DevframeRpcClient } from 'devframe/client';
.card.clickable:hover {
border-color: var(--accent);
}
h3 {
h2 {
font-size: 13px;
text-transform: uppercase;
color: #71717a;
Expand Down
Loading
Loading