The inspector shows a developer what a mounted screen is doing: its nodes as mounted, with their evaluated
properties; its queries and mutations; the command lists it ran; and the reactive runtime's counters. It implements
spec 6 §21's second tool and §12's "Inspector" test (docs/superpowers/specs/2026-10-08-app-hosting-design.md).
| Header | What it holds |
|---|---|
include/morph/ui/inspector.hpp |
Inspector, InspectedNode, InspectedQuery |
include/morph/ui/interpreter.hpp |
CommandLogEntry, LoggedCommand, and what the screen records for the inspector |
include/morph/reactive/runtime.hpp |
RuntimeStats, Runtime::stats() |
include/morph/qt_quick/inspector.hpp |
qt_quick::InspectorWindow, qt_quick::InspectFocused |
- Off unless development mode
- Read-only
- What it lists
- Reaching the focused screen
- The Qt Quick window
- Design decisions
- Not yet
The inspector is in every build and on only in development mode. Development mode is a session option,
SessionOptions::developmentTools (DevelopmentTools::Off, the default, or On, from
include/morph/ui/dev_tools.hpp); the generic client sets it from --dev or MORPH_UI_DEV=1.
ScreenCorecopies the option when it is made, since a session's options do not change after it is made.- With
On, a screen records each node it lowers, each live scope and its latestkCommandLogSizecommand lists. - With
Off, it records none of them: each record site is one comparison, and the prune of the node list does not run. Inspector::over(screen)is the one way to make an inspector. It gives nothing for a screen of anOffsession, soDocumentScreenHost::inspector(handle)gives nothing for a closed handle, a foreign one, or a screen whose session isOff, and a shell'sinspectFocusedthen calls nothing.- The Qt Quick window and its shortcut exist only when
FrontendConfig::devToolsisOn. A client sets that and the session option together; a window over anOffsession shows "no screen is shown".
Check it at run time: with Off, Inspector::over gives nothing for a mounted screen and Ctrl+Shift+I opens no
window; with On (the control), the same screen lists its nodes and logs a command list it ran, and the shortcut opens
the window.
The reactive counters (Runtime::stats()) are kept in every build. They are a few increments per flush, they belong to
the runtime rather than to the inspector, and a release binary can read them without the inspector.
The inspector reads the interpreter's state and never writes it, except through a command a user could run.
- Every read (
nodes(),queries(),commandLog(),counters()) returns values: copies of strings,Values and numbers. No pointer, reference or handle into a scope, a signal or a cache entry leaves it. - Its reads are untracked, so calling one inside an Effect subscribes the Effect to nothing.
- Evaluating a node's properties can meet an expression error. While the inspector evaluates, the screen's
reportis muted, so the inspector adds no diagnostic. Aletor a state init evaluated for the first time is computed and kept, exactly as a binding's first read would. refetch(query)is the one action. It runs the document command{"refetch": "<query>"}in the scope that declares the query, throughScreenCore::runListinsideSession::asUserand one batch: the path a button'sonClicktakes. It is logged like any other command list, with the event/inspector, and it refetches nothing a user'srefetchwould not.- The host hands out an
Inspector, never theScreen:DocumentScreenHost::inspector(handle).
Nodes. Each node is recorded as it is lowered (ScreenCore::lower), with the scope and the bindings it was
lowered with, both held weakly. nodes() lists, in lowering order (a parent before its children), every recorded
node whose scope and bindings are still alive, with its kind, id, testId, JSON Pointer and every expression
property evaluated now in that scope.
- A dialog's content, a switch case and a row are lowered when they mount and listed while they are.
- A row's nodes are listed once per row.
- A switch's
defaultand a custom node's fallback are lowered with their enclosing scope and listed whether or not they are shown. - Entries whose scope is gone are dropped every
kPruneLoweredAt(256) recordings, so a dialog opened and closed many times does not grow the list without bound.
Queries and mutations. Every query and mutation of the screen's live scopes (each RtScope adds itself to its
screen on construction and removes itself on destruction), the screen scope's first, then the app document's.
| Field | Query | Mutation |
|---|---|---|
scope |
screen, nested or app |
the same |
key |
the cache entry's key: model type, instance, action and canonical body; empty while idle | empty |
state |
idle with no entry, or an entry with neither a value nor a request; pending while a request is in flight; error when the last one failed; stale when the entry was invalidated or its value is at least staleTime old; fresh otherwise |
pending while a run is in flight or queued; error after a failed run; fresh after a successful one; idle before any |
age |
the session clock's now minus the entry's fetchedAt |
none |
lastError |
the entry's error text | the last failure's text |
The command log. runList records each command list it runs: the list's JSON Pointer (the event, lifecycle key or
step that holds it), each command's key, target and path, and the number of the reactive batch it ran in
(RuntimeCore::currentBatch, 0 outside every batch). The log keeps the latest kCommandLogSize (256) lists.
The counters. counters() is Runtime::stats() for the screen's runtime:
- flushes that ran a queued Effect;
- Effects processed, in all, in the latest flush, and the most in one flush;
- outermost batches opened;
- misuses reported, with the sites of the latest 16.
Misuses are counted with atomics, because a misuse off the owner is reported from the thread that made it.
A frontend does not know the shell's screens. MountedShell::inspectFocused(visit) hands the focused screen's
inspector to visit before it returns, or calls nothing. The default calls nothing. The generic client's shell passes
its shown screen's inspector (DocumentScreenHost::inspector). The inspector is a temporary of the call, so nothing
keeps it past a screen that closes or is remounted.
qt_quick::InspectorWindow(focused) is a separate QQuickWindow with its own QQmlEngine. Its QML is compiled from
a string in src/qt_quick/inspector.cpp, so it is not part of the MorphUi module.
attachTo(window)installs an event filter on the application's window. Ctrl+Shift+I toggles the inspector before any item of that window sees the key, wherever focus is.- While shown, it reads the focused screen through
focusedtwice a second (refresh()). It shows the counters, a line per query or mutation with a Refetch button for each query, a line per mounted node with its evaluated properties, and a line per logged command list. - Refetch calls
Inspector::refetch. text()is what it shows, section by section, as plain text, for a test.qt_quick::Frontendbuilds one whenFrontendConfig::devToolsis set: the generic client sets it from--devorMORPH_UI_DEV=1. It reads the shell throughinspectFocused. When the loop ends it hides the window and stops reading, beforerunAppdestroys the shell.
| Decision | Choice | Why |
|---|---|---|
| Where the switch lives | A session option, read once by each screen | A screen records through its session, and one session is one client's run, which is what development mode describes. Being in every build keeps the tools at hand in a release client, and gives every translation unit one ScreenCore layout. |
| Where the Off check lives | Inspector::over |
The rule is part of the inspector's contract, so the type that is handed out decides when it can be. |
| What the inspector returns | Copies | A reference into a scope or a signal would let a caller write state. |
| How it refetches | The document's own refetch command, as a user event |
The spec allows writes only through commands a user could run. A direct QueryRuntime::refetch would skip the log and the user-event path. |
| Where nodes come from | Recorded at lowering, held weakly | The view tree has no document kinds, ids or expressions. The document alone does not know which scopes are mounted, so a node is listed while the scope it was lowered in lives. |
| How the window finds the screen | A visitor on MountedShell |
The frontend owns the window and the shell owns the screens. A visitor keeps the inspector a temporary, so nothing outlives a screen. |
| The counters | In every build | They cost a few increments, and a counter that exists only in development builds could not be compared with a release run. |
- The node under the pointer is not highlighted in the screen (spec 6 §21). The window lists nodes; it does not map the pointer to one.
- The TUI frontend has no inspector window.