| title | Popup and hub |
|---|---|
| description | The floating button, the panel and its dock modes, the splitter, the pop-out window, the hub rail and deep links. |
When the overlay loads, a floating button appears in the bottom-right corner of your page. Click it to open the devtools in a panel on top of your app. You don't need a browser extension.
Importing the overlay adds the button. The first time the panel opens, it looks for the devtools server:
- If the overlay connected, it uses that path. When the path is
<base>pangular/and a hub answers at<base>, it opens the hub. - Otherwise it checks for the hub at
/__devframes/, then for the devtools alone in the places the overlay looks:/__devframes/pangular/,/__pangular/, next to the page and/__devframe/.
A path counts only when it answers with JSON. If nothing answers, the panel says No devtools server found with a Try again button, and looks again the next time it opens.
Most apps never call the popup API. To add the button without the overlay, call createDevtoolsPopup():
// src/main.ts
import {createDevtoolsPopup} from '@pangular-inspector/devtools/popup';
createDevtoolsPopup();It adds the button and returns a handle with toggle() and destroy(). The panel finds the devtools server as described above. Importing the popup module in the browser also adds the button on its own, so calling it again returns the same handle.
To open a fixed page instead, pass src:
// src/main.ts
import {createDevtoolsPopup} from '@pangular-inspector/devtools/popup';
createDevtoolsPopup({src: '/__my-devtools/'});A src given to a later call replaces the one before. showDevtools() adds the button when it is missing, and hideDevtools() removes it.
The button reads CSS variables from your page. Set them on :root to match your app:
/* src/styles.css */
:root {
--pangular-accent: #f5a524; /* button background */
--pangular-accent-ink: #1c1300; /* button icon */
--pangular-title: #f5a524; /* panel title and active dock mode */
}The values above are the defaults.
A free panel on top of your app. Drag it by the toolbar with a mouse, finger or pen, and resize it from the corner. Full width, 40% of the viewport height until you resize it. Drag the splitter on its top edge. 40% of the viewport width until you resize it, full height. Drag the splitter on its left edge.You can drag only the floating panel. Switch modes from the buttons in the panel toolbar, or from the command palette. The floating panel always stays inside the window: when the window shrinks, the panel moves and shrinks to fit, and grows back to its saved size when there is room again.
A docked panel has a splitter on the edge that faces your app. Drag it with a mouse, finger or pen, or focus it with Tab and use the keyboard:
| Key | Bottom dock | Right dock |
|---|---|---|
| ↑ and ↓ | Taller and shorter by 16 pixels | Nothing |
| ← and → | Nothing | Wider and narrower by 16 pixels |
| Shift with an arrow key | 64 pixels at a time | 64 pixels at a time |
| Home and End | The smallest and the largest size | The smallest and the largest size |
A bottom dock is at least 120 pixels high and a right dock at least 280 pixels wide. Both leave 48 pixels of the window for your app. Double-click the splitter, or run Popup: reset the docked size from the command palette, to go back to 40%. The bottom and right docks each keep their own size, and a saved size shrinks to fit a smaller window.
Screen readers announce the splitter as a separator named Resize the panel, with its current, smallest and largest size in pixels.
The Open in its own window button in the panel toolbar, or Popup: open in its own window in the command palette, opens the panel in a separate browser window and closes it on the page. Use it to keep the devtools on a second screen.
The window shows the full panel without the hub rail, and its address carries the page's id (?pageId=), so it keeps showing this browser tab after a reload of the app. If you passed src to createDevtoolsPopup, the window opens that address instead. Each tab gets its own window, and popping out again reuses it.
To bring the panel back into the page, click the floating button (its name is Bring Pangular Inspector back into the page while the window is open), press the popup shortcut, or run Popup: back into the page from the command palette in the window. Each closes the window and opens the panel on the page. Closing the window yourself also gives the button its usual job back.
If the browser blocks the window, the panel stays on the page and says so. Allow pop-ups for your app's address and try again.
The button and the panel sit in the browser top layer, like a <dialog>, a popover or a CDK overlay (MatDialog, MatMenu, MatSelect). When one of those opens, the button and the panel move back above it. A modal <dialog> makes the rest of the page inert, so they stay under it until it closes.
| Action | How |
|---|---|
| Open the panel | Click the button, or focus it and press Enter or Space; from the keyboard, focus moves into the panel |
| Open or close the panel | Press Alt+Shift+P anywhere on the page, or the shortcut in popup.hotkey. Closing gives focus back to where it was |
| Run a command | Cmd+K or Ctrl+K in the panel opens the command palette |
| Move between tabs | Arrow keys, Home and End in the tab bar, then Enter to open a tab |
| Move through a long list | ↑, ↓, Home and End in the SSR & HTTP timeline, the Store changes and events, and the Signals list. Each list is one Tab stop |
| Close the panel | Escape, outside a search box with text |
| Move the floating panel | Drag its toolbar |
| Resize a docked panel | Drag the splitter, or focus it and use the arrow keys, Home and End. See Resize a docked panel |
| Reset the panel position | Double-click its toolbar |
| Move the button | Drag it, or focus it and use the arrow keys |
| Move the button further | Hold Shift with the arrow keys |
| Reset the button position | Double-click it |
In a search box that has text, Escape clears the box and leaves the panel open. Press it again to close the panel.
The panel saves its position, size, dock mode, the size of each docked side and passive mode in localStorage under pangular-popup, so it keeps its layout across reloads. Clear that key to reset it.
In passive mode the floating button stays hidden until you press the popup shortcut. Turn it on in Settings.
The panel follows the color scheme of the hub, which follows your system unless you pick light or dark in the hub. Without the hub, the panel follows your system. To keep the panel light or dark whatever the hub does, pick it in Settings. The popup frame around the panel switches with it and saves the last theme with the layout. It takes the theme only from the panel's own frame, so other frames on your page cannot change it.
When the page's server mounts the hub (/__devframes/), the button opens the whole hub. The side rail holds every Pangular dock in one Pangular group. Pick the group to see its docks: All tools first, then one dock per tool. The group opens All tools the first time, then the dock you picked last.
| Dock | Shows |
|---|---|
| All tools | The full panel, with every tab in one tab bar (/__devframes/pangular/) |
| Angular | Dashboard, Timeline, Audit, Components, Routes, Signals, Injectors, Forms, Pipes, SSR & HTTP, and Errors |
| NgRx | The Store tab |
| Analog | The Analog tab, or a notice in apps that do not use Analog |
| Angular Native | Components, Signals, Injectors, Store, Queries, Pipes and Errors for a connected Angular Native app, or a setup notice when none is connected |
| NativeScript | Setup steps for NativeScript apps |
| Capacitor | Plugin calls of a connected Capacitor app, or setup steps when none is connected |
All tools opens the whole panel, the same as /__devframes/pangular/: the Dashboard and every tab in one tab bar, including the Store, Analog and Capacitor tabs. The rail keeps All tools when you turn inspectors off.
A single dock has an Open full panel link in its header. It opens the full panel in the same frame and selects All tools in the rail. From the NgRx, Analog and Capacitor docks it lands on the matching tab (#tab=store, #tab=analog, #tab=capacitor). From the Angular and Angular Native docks it lands on the tab you had open, and from a dock that shows setup steps it lands on the Dashboard. The link shows only on a single view (a URL with ?view=), so the full panel, the Chrome extension and the popup without the hub don't have it. Outside the hub, it loads the full panel in place of the view.
The Analog, Angular Native and Capacitor docks open setup steps when their tool has nothing to show. Their rail entry then says why in its tooltip:
| Dock | Tooltip | Back to the plain name when |
|---|---|---|
| Analog | Analog: not an Analog app |
The server restarts in an Analog app |
| Angular Native | Angular Native: no app connected |
An Angular Native app reports |
| Capacitor | Capacitor: no app connected |
A Capacitor app reports |
The setup steps of the Angular Native, NativeScript and Capacitor docks end with a Supported and planned platforms link to Platforms.
The tooltip comes back when the app stops reporting and its data expires. The Angular and NgRx docks always have data. The NativeScript dock always shows setup steps, because a NativeScript app's data shows in the Angular dock, so its tooltip stays NativeScript.
The hub keeps a list of messages, which the Messages dock lists and filters. Pangular Inspector posts to it when something happens in the app:
| Message | Level | Category | Posted when |
|---|---|---|---|
Agent ran <tool> |
info | agent |
An action tool changes the app or the server, such as set-http-rule |
The NG error, such as NG01203: ... |
error | runtime |
A page reports a forms setup error (NG01xxx) |
Navigation failed (NG04002): ... |
error | runtime |
A page reports a failed navigation |
Possible open redirect: target taken from query param <name> |
warn | runtime |
A page reports a navigation or redirect whose target is the value of a query param (details) |
Hydration mismatch at <app-cart> |
error | hydration |
A page reports a node that failed to hydrate, with the expected and actual node |
The hydration message, such as NG0505: ... |
warn | hydration |
A page logs an NG05xx hydration message |
Each message names the pages it came from (also as page:<pageId> labels) and has an Open in Pangular action that opens the dock that shows it. An error opens a toast the first time it shows up. When the same item comes back, from the same page after a reload or from another tab, the first message counts it ((3 times)) instead of posting it again. A call that only reads, like change-detection without record, posts nothing, and neither does a call that changed nothing: no page to act on, a refusal from the page or the config, or an argument to fix, such as navigate answering No router state has been reported. Text in messages is masked like the rest of the panel. An agent message shows the first line of the tool's answer that says what happened, past the untrusted-data notice, and the page argument, not the other arguments.
A dock shows how many items it has that you have not seen yet: NgRx writes on NgRx, Analog writes on Analog, and everything else on Angular. The Pangular group shows the total. The badge is red while one of them is an error or a warning. Opening a dock clears its count, and opening All tools clears every count. The tools sidebar of the open group shows the same counts, in the same colours. The count is also part of the dock name that screen readers and the tooltip use, for example Angular (2 new, with errors). When the unseen items hold warnings, such as an NG05xx hydration message, but no error, the name ends in with warnings instead.
Messages follow the config. An inspector that is off posts nothing (forms errors need inspectors.forms, failed navigations and open redirect warnings inspectors.router, hydration messages inspectors.http), and an agent tool turned off with agent.tools, agent.readOnly or actions posts nothing either. See Configuration.
With terminals: true, the hub also has a Terminals dock that runs your project's build and test commands and shows their output. See Terminals.
The full-page viewer is at /__devframes/ on the same server. The hub is built on @devframes/hub, so other devframe tools can join the same rail.
Without the hub (for example the standalone CLI, or a panel mounted with initDevframe()), every tab sits in one tab bar. The Store tab is a regular tab there.
The URL hash selects a tab. Open /__devframes/pangular/#tab=signals to land on the Signals tab. Switching tabs updates the hash, so you can copy the URL at any time. The panel also follows a hash that changes after it loads, such as a link inside the panel or location.hash = '#tab=forms'.
| Tab | Hash |
|---|---|
| Dashboard | #tab=dashboard |
| Timeline | #tab=timeline |
| Audit | #tab=audit |
| Components | #tab=components |
| Routes | #tab=routes |
| Signals | #tab=signals |
| Injectors | #tab=injectors |
| Store | #tab=store |
| Forms | #tab=forms |
| Pipes | #tab=pipes |
| SSR & HTTP | #tab=network |
| Errors | #tab=errors |
| Analog | #tab=analog |
| Capacitor | #tab=capacitor |
| Settings | #tab=settings |
A link can also select an item on its tab, on one page: #tab=<tab>&id=<id>&page=<pageId>. Open /__devframes/pangular/#tab=components&id=12&page=k3x9 to land on Components with that component instance selected.
| Tab | id selects |
|---|---|
| Components | A component instance, by its id in the component tree |
| Injectors | The element injector of a component, by the component's id |
| Signals | The signal graph of a component, by the component's id |
| Forms | A form, by its id |
On other tabs, id is ignored and the link opens the tab. page is the page id from list-pages. Without it, the tab looks on the page it shows. If the item is not on the page, the tab says so. The open-in-panel agent tool builds these links and switches the panels that are open to them.
Without a hash, the panel opens the tab you used last in this browser tab. The Chrome extension and the popup reload the panel on every navigation, and the panel keeps its tab across those reloads. It saves the tab in sessionStorage, so another browser tab starts on the Dashboard. A hash wins over the saved tab, and a tab that is turned off falls back to the Dashboard.
Each tab keeps its scroll position. The Timeline, SSR & HTTP, NgRx Store, TanStack Query, Forms and Capacitor tabs also keep their selected item. Switching tabs or reloading the panel restores both. The panel saves them in sessionStorage per view and inspected page. If storage is blocked, the panel keeps them in memory until it reloads.
A hash or a saved tab only works for a tab that exists when the panel opens. The Analog tab appears after the server confirms the app is an Analog app, so #tab=analog does not select it on load. Inside the Angular dock, the Store and Analog tabs live in their own docks.
The panel header shows the state of the connection:
| Label | Meaning |
|---|---|
| Live | The panel is connected to the server. |
| Connecting… | The panel is trying to reach the server. |
| Disconnected | The server is gone. |
If the panel cannot reach the server when it opens, it says so and tries again after 1, 2, 5 and then every 10 seconds, so a dev server that starts slowly or restarts connects without a reload. Click Retry now to try at once.
A browser tab in the background runs its timers about once a minute. The app page stops reading itself while its tab is hidden and tells the server, and the server keeps its last data instead of dropping it. The panel then says Page in background, showing the last data. When you switch back, the page sends fresh data. If a hidden tab stops answering for 5 minutes, the server drops it. The server drops a visible tab that crashes or is killed without closing, with the data of every inspector, 30 seconds after its last report or heartbeat.
The troubleshooting page lists the problems every setup shares, by symptom.
Drag it somewhere else, or focus it and use the arrow keys. Double-click it to reset its position. To remove it, stop the overlay withdisposeOverlay.
Remove the pangular-popup key from localStorage and reload.
The overlay did not find the hub at /__devframes/. Check that your server mounts initPangularHub() or the Vite plugin on the default base. On a custom base, pass <base>pangular/ to initOverlay.
No devtools path answered with JSON. Mount the hub or the Vite plugin, before the SSR handler. See No devtools server found.