Repository navigation
feat(devtools,store): add the DevTools panel - #263
Merged
Merged
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #263 +/- ##
==========================================
+ Coverage 83.56% 85.74% +2.17%
==========================================
Files 99 166 +67
Lines 2634 3766 +1132
Branches 592 871 +279
==========================================
+ Hits 2201 3229 +1028
- Misses 296 359 +63
- Partials 137 178 +41 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
dangreen
force-pushed
the
feat/devtools-uikit
branch
3 times, most recently
from
September 23, 2026 10:45
d680592 to
3da9c2a
Compare
dangreen
force-pushed
the
feat/devtools-uikit
branch
from
September 24, 2026 12:12
40f0146 to
b0c9ad4
Compare
devtools() entry
dangreen
force-pushed
the
feat/devtools-uikit
branch
from
September 25, 2026 18:15
d069927 to
faab057
Compare
…s and the registry of the panel `@nano_kit/devtools` is the in-page DevTools panel for nano_kit, a private package until the panel is real. This is the first cut: the package on Nanoviews with Storybook, the icon sprite, and the UI kit of the panel, the `Theme` tokens with a light variant, the typography and mixins, and the presentational components with a story each: `Icon`, `Button`, `IconButton`, `LinkButton`, `Input`, `Tabs`, `Table` as a tree grid, `Disclosure`, `Status`, `Tag`, `Box`, `Properties`, `Notice`, `Beacon`, `Pill`, `Window` and `ValueTree`. Below the components, the services and the registry: `InspectorService$` listens to `onSignal` and `inspect` of the core and hands the events of a task over in one batch, a microtask later; `NamingService$` names a node after the call stack of its creation; the registry module tells what a node is and what state it is in; the values module previews a value in one line and opens it an entry at a time; `RegistryStore$` keeps a record per node in an `IndexedSignalsMap`, written from the events while somebody reads it; `PanelStore$` holds the tab and the filter. The workspace overrides `agera` and `kida` to the workspace sources, so Nanoviews, the panel and an inspected application share one core, and the agent skills the package is written with are locked.
The Signals tab of the panel: `SignalsStore$` builds the table over the records of the registry and holds the selection, and two blocks read it. `SignalsTable` groups the signals, computeds and selectors by the function or the file they were created in, child signals under their parent, and shows the kind, a preview of the value, the state and the counts of dependencies and subscribers; effects and scopes have no rows and count among the subscribers of what they read, and the filter matches the name, the site, the owner and the preview. `Inspector` shows the selected node in three boxes: `Node` with its name, kind, owner, site and state, `Value` with the value tree and `Evaluate now` for a computed that is out of date or not evaluated yet, and `Links` with the dependencies and the subscribers as links to their rows, an effect by its name and site. The stories and the specs of the blocks run over `blocks/shared/app.mock.ts`, a small application with a node in every state a row can show. On the way `InspectorService$` meets an effect created inside a scope on the stack of the body, a level of `ValueTree` where nothing opens has no spacer, `Notice` gets rounded corners and room for its actions, and the package moves to the Nanoviews prerelease of 2026-09-23, which keeps the uninspected mark on the rows of a read-only `for_`; the UI kit takes `inject` and `provide` from `nanoviews/store`.
…n icon of their own The rows of `Table` share one tab stop, the way a tree grid does: the table holds it until a row is focused and hands the focus to the selected row or the first one; up and down move between the rows, Home and End go to the ends, right opens a row or goes down to its first child, left closes it or goes up to its parent, Enter and Space act as a click, and the chevron stays for the mouse, out of the tab order. The focus ring is drawn by the cells, rounded at the ends like the row. The tree column lines up by drawings, not by boxes: the chevron sits in a slot as wide as an icon, 4 px from the icon, so the chevron of a nested row stands under the icon of its parent and the icon of a nested row under the text of its parent; the last head cell keeps the inset of the last body cell. Selectors in the Signals table get an icon of their own, the diamond of a computed with a dot inside.
…t lines in the Inspector The Log tab of the panel: `groupEvents` of the log service puts the events of a task into transactions, a group per flush and one for what came after the last flush, and `LogStore$` keeps 200 of them, newest first, with `pause`, `resume` and `clear`, listening while somebody reads them. A line tells of a write with its values before and after, a run of a computed or an effect, a computed an update left out of date and nobody read, a lifecycle transition or a stop; a run is timed on its own and with the runs nested in it, from `RunEvent` to `RunEndEvent`, and a first evaluation is marked as such. The `Log` block shows a row per group with the counts, the slowest run, the duration and the time of the day, folded until it is opened into its lines, a run indented under the run it happened in, and a click on a line selects its node. The Inspector gets `Recent`, the last lines of the selected node from `$selectedRecent` of `SignalsStore$`, and the last run in `Node`. `Duration` joins the UI kit, a time in milliseconds set apart by the tone a block picks, orange from a millisecond and red from a frame, and `LineDetail` is the block both show a line with. On the way `InspectorService$` takes several listeners, stamps every event with its time and passes a flush on only after events of the application, and `RegistryStore$` keeps the record of a node that stopped, out of every link, until the node is collected.
… hotkey, and show the source of effects and computeds
… shadow root with its styles and icons built in
…lue of a selector key and snap the pill to the middle
…r its owner, and show the value last written to a signal
…ap for the devtools in development
…ter the owner of the node it listens to
dangreen
force-pushed
the
feat/devtools-uikit
branch
from
September 26, 2026 10:28
36be209 to
18fd33a
Compare
devtools() entrydevtools() entry, and signalNodeAssign to mark nodes for it
devtools() entry, and signalNodeAssign to mark nodes for it
dangreen
marked this pull request as ready for review
September 26, 2026 12:49
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The DevTools panel of nano_kit lives on the page and is written on Nanoviews. This pull request brings the package up to the call an application makes to get it: the workspace wiring for one shared core, the Storybook setup, the UI kit, the services and the registry, the Signals and Log tabs with the inspector, the shell the panel takes on the page, the
devtools()entry and a README. The panel was tried on the weather example, and what the trial found is fixed here: the owners of the nodes libraries create, the value of a signal nobody has read since a write, and the signals maps, which@nano_kit/storenow marks for the panel in development.What
packages/devtools, published as@nano_kit/devtools:nanoviews,@nano_kit/storeand@nano_kit/platform-webas dependencies, Storybook on@nanoviews/storybook-vitewith a theme toggle in the toolbar andnanoviewskept out of the pre-bundling so a story runs on one core, Vitest with@nanoviews/testing-library, the oxlint config of the package, a library build on Vite, and atsconfig.build.jsonthat leaves the specs, the stories and the mocks out.exportspoints atdistin the workspace as well, so an example of the repository runs what an application gets. A README tells what the panel does and how to call it, with screenshots of both tabs in the light and the dark theme inwebsite/src/assets.src/assets/icons, compiled byscripts/svg-sprite.jsintoassets/sprite.svgwith no XML declaration, and put into the tree as markup:Iconrefers to a symbol of it with<use href="#name">, the entry puts the sprite into the shadow root of the panel and the preview of Storybook into the page.Themewith the design tokens as custom properties and alightvariant,typographyandmixins,Icon,Button,IconButton,LinkButton,Input,TabswithTabover a native radio group,Tableover a real<table>with roletreegrid, walked from the keyboard the way a tree grid is, its rows sharing one tab stop, a group row opening on a click anywhere on it, and aTableTreeCellfor the chevron,Disclosure,Status,Tag,Duration, a time in milliseconds set apart by a tone,Boxwith its header, subheader and body,Properties,Notice,Beacon,Pill,Windowwith its bar and body,WindowEdge, an invisible handle on a side, the bottom edge or a lower corner of a window, half out of it so a scrollbar along it keeps most of its width, which starts no selection of text, andValueTreewithValueNode,ValueChildrenandValueMore, the presentational half of the value viewer....restPropsspread neither drops nor replaces it:DisclosureforonClick,TabsforonChange,ValueNodeforonKeyDown.src/services, pure logic and the one adapter to the core, all on@nano_kit/store:InspectorService$listens toonSignalandinspect, tells a node met for the first time from a known one and takes its call stack right there, and hands the events of a task over, each stamped with its time, to every listener in turn, in one batch insideuninspected, a microtask later, and passes a flush on only after events of the application;NamingService$turns the stack of a creation into a name in the Docker style, a creation site and an owner, with theLibraryDetector$token for the files to skip on the way to the application, and takesObject.fnandObject.compute, the names V8 gives a body the core runs through its node, for no name, so an effect is owned by its file in Chrome and Edge as in Safari and Firefox; a node libraries alone created inside the body of a computed or an effect, or inside a lifecycle listener, belongs to the owner of the node the core was running, since the search along the stack stops at the frame ofcallInspectedand the registry knows which node runs;before DevToolsis said only of a node whose creation was not seen,by librariesof one whose stack holds libraries alone; the registry module tells what a node is, its state from the flags and links, its value read from the node and never evaluated, the value written last for a signal, which is never out of date, a signals map told by the marks of the store package, its version standing for it, the source of the function an effect or a computed runs, and what an update left out of date; the values module previews a value in one bounded line and opens a collection a page at a time, with no serialization in depth; the log module puts the events of a task into transactions, a group per flush, times every run fromRunEventtoRunEndEvent, on its own and with the runs nested in it, and cuts a group down to the lines a filter finds.src/stores:RegistryStore$keeps an immutable record per node in anIndexedSignalsMapof the store package, applies the events of the inspector to it in one batch, follows the nodes whose bodies or lifecycle listeners run, reads the parent of a new node when it applies the batch rather than when the node is met, since a map marks an entry after its signal is made, counts a new value of an entry as a value of its map and lets an entry go with its key, listens while somebody reads the records, and keeps the record of a node that stopped, out of every link, until the node is collected;LogStore$keeps the last 200 transactions, newest first, with pause, resume and clear, and the groups the filter leaves;PanelStore$keeps whether the window is open, the tab, the theme, the frame of the window and the place of the pill inlocalStoragethroughlocalStoredof@nano_kit/platform-web, and the filter in memory.SignalsStore$builds the table over the records of the registry and holds the selection.SignalsTablegroups the signals, computeds, selectors and signals maps by the function or the file they were created in, child signals under their parent and the entries and the index of a map under the map, the nodes of no owner apart by whether they are older than the panel or created by libraries alone, and shows the kind, a preview of the value, the state and the counts of dependencies and subscribers; effects and scopes have no rows and count among the subscribers of what they read, and the filter matches the name, the site, the owner and the preview.Inspectorshows the selected node in four boxes: Node with its last run; Value with the value, the body of an effect in its place and the compute of a computed under its value, andEvaluate nowfor a computed that is out of date or not evaluated yet; Links; and Recent, the last lines of the log about the node. The stories and the specs of the blocks run overblocks/shared/app.mock.ts, a small application with a node in every state a row can show.Logshows a row per transaction with the counts of its lines, the slowest run once it takes a millisecond, the duration and the time of the day, striped by the number of the transaction so a new one on top flips nothing, folded until it is opened; a line tells of a write with its values, a run of a computed or an effect with its own time and the total, a computed left out of date that nobody read, an effect left out of date that did not run, a change of the keys of a map, a lifecycle transition or a stop, a run indented under the run it happened in, and a click on a line selects its node. The filter finds lines by the name, the site, the owner or a value.LineDetailis the block the Log and the Recent box show a line with.blocks/Panel:Panelshows a pill on the bottom edge, dragged along it, or the window it opens, moved by its bar, resized by its sides, its bottom edge and its lower corners, and kept in the viewport by CSS. Alt+Shift+D, Option+Shift+D on a Mac, opens and collapses it from anywhere on the page, and Escape collapses it while the focus is inside. The bar has the tabs, the filter, the pause, play and clear buttons of the log, the theme switch and the close button, and a tab left for the other is set aside with what was opened in it rather than built again.src/devtools.ts:devtools(options?), the one export of the package, runs insideuninspected, starts the registry and the log so they listen from the call on, appends adivtobody, attaches an open shadow root to it, adopts the styles and mountsPanelthere. The styles are one minified string in the bundle:inlineStyles()of the Vite config givesvirtual:stylesa stand-in and puts the CSS that library mode emits in its place, so the bundler of an application gets no CSS to process, andadoptedStyleSheetsapplies it on a page whose policy forbids inline styles, where a<style>would be blocked.:host { all: initial !important }keeps the styles of the page out.options.libraryadds a rule for the files of libraries to the one ofnode_modules, for a workspace that links the kit as sources. Segoe UI sets its baseline a pixel lower in a line than SF, so the entry adds its faces to the fonts of the document asnano_kit Segoe UIwith the metrics that set it where SF does; the monospace token takes Cascadia Mono Light, which Chromium finds on Windows 11, before Consolas.@nano_kit/store:signalNodeAssign($signal, fields)assigns fields to the node of a signal, the way a store marks its nodes for the devtools; it throws outside of development, so a call sits insideif (import.meta.env.DEV), which the production build folds away.SignalsMapmarks its version with the map and an entry with the map and its key, andIndexedSignalsMapmarks its index with the map alone, so no key of the map is taken for it, and creates its anchor insideuninspected, which keeps the anchor and its links out of the panel. The store skill gets a paragraph about the devtools.ageraandkidaare overridden to the workspace sources inpnpm-workspace.yaml, so Nanoviews, which pins published kida snapshots, the panel and an inspected application share one core instance.@nano_kit/devtoolsis overridden to the workspace too, for the examples. The weather example on nano_kit DI mounts the panel from a module imported first, withnanoviewskept out of the pre-bundling of Vite, which would give it a copy of the linked core of its own. The integration job of CI builds the devtools before it tests the examples, which take the kit from its sources and the devtools from their build.storybook-staticis ignored.skills-lock.jsonlocks the agent skills the package is written with.Size
An application that calls the panel behind the development flag ships none of it. The store gains
signalNodeAssign, an export, so theimport *bundles carry it:All publicsof store from 6667 to 6716 B gzip and from 6135 to 6148 B brotli againstmain, with the limits re-pinned to 6.75 and 6.2 kB in a commit of their own; a named import pays nothing, and the marks of the signals maps fold away withimport.meta.env.DEV.Notes
main:RunEndEventand a first evaluation told by the missingoldValuefrom feat(agera): report the end of a run withRunEndEvent, and leaveoldValueout of a first evaluation #265, and the fire of lifecycle listeners from feat(agera): enclose the lifecycle listeners of a node betweenFireEventandFireEndEvent#267. fix(agera): drop the development warning about a signal written inside a computed #266 dropped the development warning about a write inside a computed, which the cache of a query set off on every start.devtools({ library: file => file.includes('/packages/') }).tsc --noEmiton both configs, the 249 specs of the panel with coverage, the specs and size-limit of the store, the stories in Storybook, the panel on the weather example in Chrome, and the built package on a page understyle-src 'self'with styles of its own onbody, in Safari 18.4, Chrome 153 and Firefox 156 on macOS Sequoia and Edge 153, Chrome 153 and Firefox 156 on Windows 11 on BrowserStack, and in headless Chrome 153 on Linux. The layout is the same in all of them; Firefox on macOS sets text half a pixel lower than Safari and Chrome, which a page cannot change for SF. The pre-push hook was skipped on purpose: it runs the whole monorepo suite in parallel, which the dev machine does not survive. CI runs the rest.