Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a80a38a
docs(docs): merge the setup guides into one page per setup
erkamyaman Nov 7, 2026
d59167a
docs(docs): add one troubleshooting page indexed by symptom
erkamyaman Nov 7, 2026
9706a96
docs(docs): add a supported setups matrix and builder list
erkamyaman Nov 7, 2026
976c8ca
docs(docs): add a platforms page with the planned targets
erkamyaman Nov 7, 2026
93d817b
feat(ui): link the setup views to the platforms page
erkamyaman Nov 7, 2026
c5ad745
docs(docs): add features, keyboard shortcuts and agent recipes pages
erkamyaman Nov 7, 2026
10e34cd
docs(docs): publish llms.txt and a generated agent tool index
erkamyaman Nov 7, 2026
6a8d504
docs(docs): give sidebar badges an expiry version
erkamyaman Nov 7, 2026
d3bfac8
build(extension): rebuild the panel with the platforms link
erkamyaman Nov 7, 2026
a26c1ff
docs(docs): address review of the site improvements
erkamyaman Nov 7, 2026
c31661a
docs(docs): tighten the form and provider recipes and the h3 install
erkamyaman Nov 7, 2026
eee4981
docs(docs): reproduce the form input and separate the fallback lookup
erkamyaman Nov 7, 2026
f518b84
docs(docs): compare form status and keep injection flags in the recipes
erkamyaman Nov 7, 2026
8d5c67f
docs(docs): generate an API reference from the package entry points
erkamyaman Nov 7, 2026
eaf6dd1
fix(docs): show inferred return types and re-exported symbols in the …
erkamyaman Nov 7, 2026
43a34b3
Merge remote-tracking branch 'origin/main' into docs/site-improvements
erkamyaman Nov 7, 2026
f64ec86
fix(docs): leave every internal entry point out of the API reference
erkamyaman Nov 7, 2026
5df161b
Merge remote-tracking branch 'origin/main' into docs/site-improvements
erkamyaman Nov 7, 2026
45b3ea1
Merge remote-tracking branch 'origin/main' into docs/site-improvements
erkamyaman Nov 7, 2026
10fa43d
Merge remote-tracking branch 'origin/main' into docs/site-improvements
erkamyaman Nov 7, 2026
d7682b7
build(extension): rebuild extension/ui after the main merges
erkamyaman Nov 7, 2026
fe7868b
docs: regenerate the agent tool index and API reference after the mai…
erkamyaman Nov 7, 2026
b004253
Merge remote-tracking branch 'origin/main' into docs/site-improvements
erkamyaman Nov 7, 2026
9b25c3d
build(extension): rebuild extension/ui after the main merges
erkamyaman Nov 7, 2026
561efa8
Merge main into docs/site-improvements
erkamyaman Nov 7, 2026
61cbd1c
Merge main into docs/site-improvements
erkamyaman Nov 7, 2026
b0b11cb
Merge main into docs/site-improvements
erkamyaman Nov 7, 2026
59368a0
Merge main into docs/site-improvements
erkamyaman Nov 7, 2026
266f054
Merge main into docs/site-improvements
erkamyaman Nov 7, 2026
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
2 changes: 1 addition & 1 deletion .claude/skills/devtools-docs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Reviewers reject changes that break these.
1. **No em dashes.** Use a period, comma or parentheses.
2. **No first person** ("we", "our").
3. **No future tense** ("will").
4. **No time-relative claims** ("new", "recently", "upcoming", "now supports"). Use a sidebar `status` badge in `ngmd.config.ts` and the changelog instead.
4. **No time-relative claims** ("new", "recently", "upcoming", "now supports"). Use a sidebar `status` badge in `ngmd.config.ts` and the changelog instead. A `new` or `updated` badge needs `until`: the minor after the release that ships the page (`until: '1.2.0'` for a page in 1.1), so it expires.
5. **No comparisons with other devtools products.** Describe this project on its own terms.
6. **No marketing words**: powerful, seamless, blazingly fast, simply, just, easy.
7. **No invented features.** Every name, label, option, default, tool and argument must exist in the code.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ npm install -D @pangular-inspector/devtools

An Angular SSR server imports the hub when it starts, so install the package as a dependency there (`npm install @pangular-inspector/devtools`).

For an Angular CLI app with SSR, the [quick start](./apps/docs/src/content/getting-started/quick-start.md) takes you from install to a live panel. For other setups, follow the [installation guide](./apps/docs/src/content/getting-started/installation.md): a client-only Angular CLI app, Vite and Analog, the standalone CLI, [Angular Native](./apps/docs/src/content/guides/angular-native.md), or [Capacitor](./apps/docs/src/content/guides/capacitor.md). For a coding agent, run `npx @pangular-inspector/devtools mcp`.
For an Angular CLI app with SSR, the [quick start](./apps/docs/src/content/getting-started/quick-start.md) takes you from install to a live panel. For other setups, follow the [installation guide](./apps/docs/src/content/getting-started/installation.md): a client-only Angular CLI app, Vite and Analog, the standalone CLI, [Angular Native](./apps/docs/src/content/getting-started/angular-native.md), or [Capacitor](./apps/docs/src/content/guides/capacitor.md). For a coding agent, run `npx @pangular-inspector/devtools mcp`.

## Documentation

Expand Down
4 changes: 3 additions & 1 deletion app/src/__tests__/angular-native-view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@ describe('Angular Native view', () => {
expect(text).toContain('No Angular Native app is connected');
const link = host(fixture).querySelector<HTMLAnchorElement>('a.cta');
expect(link?.textContent).toContain('Set up Angular Native');
expect(link?.href).toContain('guides/angular-native/');
expect(link?.href).toContain('getting-started/angular-native/');
const platforms = host(fixture).querySelector<HTMLAnchorElement>('.platforms a');
expect(platforms?.href).toBe('https://pangular-inspector.dev/getting-started/platforms/');
expect(tabNames(fixture)).toEqual([]);
});

Expand Down
4 changes: 4 additions & 0 deletions app/src/__tests__/capacitor-view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ describe('Capacitor view', () => {
expect(link?.textContent).toContain('Capacitor setup guide');
expect(link?.href).toBe('https://pangular-inspector.dev/guides/capacitor/');
expect(link?.target).toBe('_blank');
const platforms = el.querySelector<HTMLAnchorElement>('.platforms a');
expect(platforms?.textContent).toContain('Supported and planned platforms');
expect(platforms?.href).toBe('https://pangular-inspector.dev/getting-started/platforms/');
expect(platforms?.target).toBe('_blank');
expect(el.textContent).not.toContain('Coming Soon');
expect(tabNames(fixture)).toEqual([]);
});
Expand Down
4 changes: 2 additions & 2 deletions app/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ const NATIVESCRIPT_SETUP: ComingSoonInfo = {
],
link: {
label: 'NativeScript setup guide',
href: 'https://pangular-inspector.dev/guides/nativescript/',
href: 'https://pangular-inspector.dev/getting-started/nativescript/',
},
};

Expand Down Expand Up @@ -186,7 +186,7 @@ const NO_ANGULAR_NATIVE: ComingSoonInfo = {
],
link: {
label: 'Set up Angular Native',
href: 'https://pangular-inspector.dev/guides/angular-native/',
href: 'https://pangular-inspector.dev/getting-started/angular-native/',
},
};

Expand Down
30 changes: 30 additions & 0 deletions app/src/pages/coming-soon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export interface ComingSoonInfo {
link?: { label: string; href: string };
}

/** The docs page that lists the supported platforms and the planned ones, with their issues. */
export const PLATFORMS_URL = 'https://pangular-inspector.dev/getting-started/platforms/';

@Component({
selector: 'app-coming-soon',
host: { '[style.--brand]': 'info().color' },
Expand Down Expand Up @@ -111,6 +114,14 @@ export interface ComingSoonInfo {
<li [style.animation-delay.ms]="200 + i * 120">{{ plan }}</li>
}
</ul>

@if (info().id !== 'analog') {
<p class="platforms">
<a [href]="platformsUrl" target="_blank" rel="noopener noreferrer"
>Supported and planned platforms<span class="sr-only"> (opens in a new tab)</span></a
>
</p>
}
</section>
`,
styles: `
Expand Down Expand Up @@ -352,6 +363,24 @@ export interface ComingSoonInfo {
background: var(--brand);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 20%, transparent);
}
.platforms {
width: 100%;
max-width: 440px;
margin: 16px auto 0;
font-size: 13px;
text-align: left;
}
.platforms a {
color: var(--accent-text);
text-decoration: underline;
text-underline-offset: 2px;
}
.platforms a:hover {
color: var(--text-strong);
}
.platforms a:focus-visible {
@include m.focus-ring;
}
@media (max-height: 640px) {
.soon {
padding-top: 12px;
Expand Down Expand Up @@ -449,6 +478,7 @@ export interface ComingSoonInfo {
})
export class ComingSoon {
readonly info = input.required<ComingSoonInfo>();
protected readonly platformsUrl = PLATFORMS_URL;
readonly plansTitle = computed(() => {
const info = this.info();
return info.plansTitle ?? (info.link ? `In an ${info.name} app` : 'On the roadmap');
Expand Down
47 changes: 47 additions & 0 deletions apps/docs/agent-tool-index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {readFileSync} from 'node:fs';
import {join} from 'node:path';
import * as prettier from 'prettier';
import {TOOL_INDEX_PATH, findSection, renderToolIndex} from './agent-tool-index';

const docsRoot = import.meta.dirname;

describe('agent tool index', () => {
it('names every agent tool and resource in tools.md or resources.md', () => {
expect(renderToolIndex(docsRoot).problems).toEqual([]);
});

it('matches the page in the repository; run pnpm docs:agent-tools when it fails', async () => {
const file = join(docsRoot, TOOL_INDEX_PATH);
const options = (await prettier.resolveConfig(file)) ?? {};
const expected = await prettier.format(renderToolIndex(docsRoot).markdown, {
...options,
filepath: file,
});
expect(readFileSync(file, 'utf8')).toBe(expected);
}, 30_000);

it('links a tool to its own heading, then to the table that defines it', () => {
const markdown = [
'## Before you call a tool',
'Use `explain-field` and `explain-form-changes`.',
'## Forms',
'### Read',
'| Tool | What it answers |',
'| --- | --- |',
'| `explain-field` | One field |',
'',
'| Tool | Cap |',
'| --- | --- |',
'| `explain-form-changes` | 100 fields |',
'### Track',
'| Tool | What it answers |',
'| --- | --- |',
'| `explain-form-changes` | The net change |',
'### wait-for',
].join('\n');
expect(findSection(markdown, 'explain-field')?.slug).toBe('read');
expect(findSection(markdown, 'explain-form-changes')?.slug).toBe('track');
expect(findSection(markdown, 'wait-for')?.slug).toBe('wait-for');
expect(findSection(markdown, 'missing-tool')).toBeNull();
});
});
201 changes: 201 additions & 0 deletions apps/docs/agent-tool-index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import {readFileSync} from 'node:fs';
import {join} from 'node:path';
import {AGENT_ENTRIES, type AgentEntry} from '../../packages/devtools/src/config.ts';
import {createSlugger, headingText, slugify} from './src/app/utils/heading-slug.ts';

/**
* Builds `src/content/agents/tool-index.md` from `AGENT_ENTRIES` in
* `packages/devtools/src/config.ts`, the list the server registers its agent
* tools and resources from. `scripts/docs-agent-tools.mjs` writes the page,
* and `agent-tool-index.spec.ts` fails when the committed page is stale or a
* tool has no place in `tools.md` or `resources.md`.
*/

export const TOOL_INDEX_PATH = 'src/content/agents/tool-index.md';

const INSPECTOR_LABELS: Record<string, string> = {
components: 'Components',
signals: 'Signals',
injectors: 'Injectors',
ngrx: 'NgRx Store',
tanstackQuery: 'TanStack Query',
forms: 'Forms',
router: 'Router',
pipes: 'Pipes',
http: 'SSR & HTTP',
analog: 'Analog',
capacitor: 'Capacitor',
errors: 'Errors',
plugins: 'Plugins',
};

/** Sections that mention every tool in passing, so they are not where a tool is described. */
const PASSING_SECTIONS = new Set(['before-you-call-a-tool', 'renamed-tools', 'shared-state']);

interface Heading {
slug: string;
text: string;
section: string;
}

/** Each line's heading, with slugs computed the way the link guard and the TOC do. */
function headingsByLine(markdown: string): (Heading | null)[] {
const slug = createSlugger();
const out: (Heading | null)[] = [];
let current: Heading | null = null;
let section = '';
let fence: string | null = null;
for (const line of markdown.split(/\r?\n/)) {
const marker = /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1];
if (marker && (fence === null || marker.startsWith(fence))) {
fence = fence === null ? marker : null;
} else if (fence === null) {
const m = /^ {0,3}(#{1,6})\s+(.+?)\s*$/.exec(line);
if (m) {
const text = headingText(m[2]);
const id = m[1].length === 1 ? slugify(text) : slug(text);
if (m[1].length <= 2) section = id;
current = {slug: id, text, section};
}
}
out.push(current);
}
return out;
}

/** The heading a name is described under: its own heading, or the first section that names it. */
export function findSection(markdown: string, name: string): Heading | null {
const lines = markdown.split(/\r?\n/);
const headings = headingsByLine(markdown);
const own = headings.find((h) => h?.text === name);
if (own) return own;
const needle = '`' + name + '`';
// The header row of the table each line is in, so a row in a table of limits doesn't count.
const tableHeaders: string[] = [];
lines.forEach((line, i) => {
const inTable = line.startsWith('|');
tableHeaders.push(!inTable ? '' : lines[i - 1]?.startsWith('|') ? tableHeaders[i - 1] : line);
});
const isLimits = (i: number) => /^\|\s*\S+\s*\|\s*(Cap|Limit)\s*\|/.test(tableHeaders[i]);
// A table row that defines it, then a paragraph that starts with it, then any mention.
const tests = [
(line: string, i: number) => line.startsWith(`| ${needle} `) && !isLimits(i),
(line: string) => line.startsWith(needle),
(line: string) => line.includes(needle),
];
for (const test of tests) {
for (let i = 0; i < lines.length; i++) {
const heading = headings[i];
if (!heading || PASSING_SECTIONS.has(heading.section)) continue;
if (test(lines[i], i)) return heading;
}
}
return null;
}

function tier(entry: AgentEntry): string {
const base = entry.safety ?? 'read';
return entry.readOnlyVariant ? `${base} (read under \`agent.readOnly\`)` : base;
}

function inspectorOf(entry: AgentEntry): string {
const first = entry.inspectors?.[0];
if (!first) return 'Shared';
return entry.anyInspector ? `${INSPECTOR_LABELS[first]} and others` : INSPECTOR_LABELS[first];
}

function turnedOffBy(entry: AgentEntry): string {
if (!entry.inspectors?.length) return 'None';
const keys = entry.anyInspector ? entry.inspectors : entry.inspectors.slice(0, 1);
const parts = keys.map((key) => `\`inspectors.${key}\``);
const joined = entry.anyInspector ? `${parts.join(', ')} (all of them)` : parts[0];
return entry.actions?.length
? `${joined}, or blocking ${entry.actions.map((a) => `\`actions.${a}\``).join(' and ')}`
: joined;
}

export interface IndexProblem {
name: string;
problem: string;
}

/** The markdown of the page, and the entries that have no place in the docs. */
export function renderToolIndex(docsRoot: string): {markdown: string; problems: IndexProblem[]} {
const tools = readFileSync(join(docsRoot, 'src/content/agents/tools.md'), 'utf8');
const resources = readFileSync(join(docsRoot, 'src/content/agents/resources.md'), 'utf8');
const problems: IndexProblem[] = [];
const toolRows: string[] = [];
const resourceRows: string[] = [];

const order = ['Shared', ...Object.values(INSPECTOR_LABELS)];
const entries = Object.entries(AGENT_ENTRIES).sort(
([a, x], [b, y]) =>
order.indexOf(inspectorOf(x).replace(/ and others$/, '')) -
order.indexOf(inspectorOf(y).replace(/ and others$/, '')) || a.localeCompare(b),
);

for (const [name, entry] of entries) {
const isResource = entry.kind === 'resource';
const page = isResource ? './resources.md' : './tools.md';
const section = findSection(isResource ? resources : tools, name);
if (!section) {
problems.push({name, problem: `not named in ${isResource ? 'resources.md' : 'tools.md'}`});
continue;
}
const link = `[${section.text}](${page}#${section.slug})`;
const stdio = entry.page ? 'No' : 'Yes';
if (isResource) {
resourceRows.push(`| \`pangular:${name}\` | ${inspectorOf(entry)} | ${stdio} | ${link} |`);
} else {
toolRows.push(
`| \`${name}\` | ${inspectorOf(entry)} | ${tier(entry)} | ${stdio} | ${turnedOffBy(entry)} | ${link} |`,
);
}
}

const markdown = `---
title: Tool index
description: Every built-in agent tool and resource with its inspector, safety tier, transports and the option that turns it off, generated from the server's own list.
---

<ngmd-hero title="Tool index" gradient>
Every built-in agent tool and resource on one page, generated from the list the server registers them from.
</ngmd-hero>

# Tool index

This page is generated from \`AGENT_ENTRIES\` in \`packages/devtools/src/config.ts\`, the list the server registers its tools and resources from, so it can't drift from the code. [Tools](./tools.md) and [Resources](./resources.md) describe the arguments and answers of each one. Run \`pnpm docs:agent-tools\` after you add or change an entry.

Clients see each tool as \`pangular_<name>\`. See [Names](./tools.md#names). Plugin tools, \`pangular:plugin:<id>:<tool>\`, come from the plugins and are not listed here. See [A plugin's own tools](./tools.md#a-plugin-s-own-tools).

## Columns

| Column | Meaning |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Inspector | The inspector the entry belongs to. Shared entries belong to none. |
| Tier | \`read\`, \`action\` or \`destructive\`. See [Action tools](./tools.md#action-tools). |
| Over stdio | Whether the stdio server offers it. Entries that need a connected page, the app's dev server or the panel are HTTP only. |
| Turned off by | The [configuration](../getting-started/configuration.md#inspectors-and-agent-tools) that removes it. \`agent.tools.<inspector>: false\` also hides an inspector's entries, and \`agent.readOnly\` drops the \`action\` and \`destructive\` tools. |

## Tools

| Tool | Inspector | Tier | Over stdio | Turned off by | Described in |
| ---- | --------- | ---- | ---------- | ------------- | ------------ |
${toolRows.join('\n')}

## Resources

| Resource | Inspector | Over stdio | Described in |
| -------- | --------- | ---------- | ------------ |
${resourceRows.join('\n')}

## Where to next

<ngmd-pill-row>
<ngmd-pill href="/agents/tools" title="Tools"></ngmd-pill>
<ngmd-pill href="/agents/resources" title="Resources"></ngmd-pill>
<ngmd-pill href="/agents/recipes" title="Recipes for agents"></ngmd-pill>
</ngmd-pill-row>
`;
return {markdown, problems};
}
Loading
Loading