npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@atriarch-systems/tracery-visualizer

v0.3.2

Published

Consumer-configured React canvas for live nodes and directed activity edges.

Readme

@atriarch-systems/tracery-visualizer

A React 18/19 component for live activity graphs. One generic node card, configured by the consumer. There are no agent names, node-kind registries, network connections, or invocation reducers in the library.

This is an independent npm package inside Agent Kit. It is not part of the Python wheel and does not add JavaScript dependencies to Python consumers.

import { ActivityGraph, type ActivityNode, type ActivityEdge,
  type NodePresentation } from '@atriarch-systems/tracery-visualizer';

// This catalog belongs to the application, not Agent Kit.
const catalog = {
  input: { badge: 'SOURCE', icon: '↔' },
  worker: { badge: 'WORKER', icon: '✳', width: 152, height: 74, accent: '#adf17b' },
} satisfies Record<string, NodePresentation>;

const nodes: ActivityNode<{ operationId: string }>[] = [
  { id: 'source', label: 'Inbox', presentation: catalog.input,
    position: { x: -200, y: 0, anchored: true } },
  { id: 'worker', label: 'Research', detail: 'Reading sources',
    presentation: catalog.worker, status: 'running',
    activity: { highlighted: true }, data: { operationId: 'call-123' } },
];
const edges: ActivityEdge[] = [
  { id: 'source-worker', source: 'source', target: 'worker',
    label: 'invoke', count: 3, activity: { highlighted: true } },
];

<div style={{ height: 600 }}>
  <ActivityGraph nodes={nodes} edges={edges}
    onNodeSelect={node => console.log(node?.data)} />
</div>

Ownership

The consumer owns its node catalog, stable IDs, domain events, aggregation/counts, request selection, retained history, error meanings and inspector content. It supplies readonly node and edge arrays. The visualizer owns canvas drawing, force layout, anchoring, selection feedback, animation and camera controls. Force-engine positions and mutated edge endpoints stay inside private wrappers; consumer objects are never mutated.

ActivityNode<Data>, ActivityEdge<Data>, ActivityGroup, NodePresentation, Activity, ActivityGraphProps and ActivityGraphHandle are exported. ./types is a renderer-free import. VISUALIZER_CONTRACT_VERSION is 3; it versions the presentation contract, not any WebSocket protocol. The data parameter lets each application retain strongly typed inspector data.

All times are epoch milliseconds. highlighted lights an item; completedAt fades that highlight to gray; removedAt fades it out. The application removes expired items from its arrays. updatedAt animates a call traveling along an edge. Distinct edge IDs preserve incoming/outgoing and different relationship counts. Edge curvature, labels and accent are caller configuration. Different node types are presets of the same card, not renderer subclasses.

Executing nodes pulse with a breathing border and halo. active defaults to status === 'running'; set it explicitly to preserve an error state while another operation on the same node is still executing. Completed or unhighlighted nodes never pulse. Reduced motion keeps a static active border and indicator.

Give the host a nonzero height. Supply apiRef for fitView(), view for initial framing, and layoutKey to reset remembered positions between isolated workspaces. Controlled selection uses selectedNodeId and onNodeSelect. onNodeMove reports drag positions without mutating application state; onGroupMove reports a whole-group drag the same way — see "Draggable groups" below. onNodeActivate reports a double-click or an Enter keypress on the selected node — see "Groups and traces" below. Arrow keys select nodes, Enter activates the selected node, Escape clears, and F fits the view. Reduced motion follows system settings or an explicit prop. Drawing pauses after settling/fading and while hidden/offscreen.

The renderer is loaded only in a browser, so importing the package during SSR is safe. Fonts use system-ui and no global stylesheet is installed. Consumers can freely wrap the component with their own toolbar, inspector and overlays.

Development and consumption

npm install
npm run build
npm test
npm pack

Install the resulting tarball into a consumer with npm install ./vendor/atriarch-tracery-visualizer-0.3.0.tgz. The package includes compiled ESM and TypeScript declarations. Local consumers can use this artifact without sibling-repository source imports; registry publishing is a separate action. Virali's prototype is the first consumer; Saga can provide its own catalog and adapter using the same exported contract.

Tests cover frozen consumer input, stable layout across updates, engine array isolation, anchors, edge-first streams, separate directed relationships, removed nodes, duplicate IDs, lifecycle transitions, group hulls (small-group bounding rect vs. convex hull, dimmed alpha, empty groups), group hull hit-testing (hitTestGroup against the same padded-rect/convex-hull geometry drawGroupHull draws, including the concave-gap and boundary cases, and a regression test pinning both to the one shared groupHullShape function), dashed data edges vs. thicker spawn edges, placeBranches ignoring data edges as a parent, the pure double-click/activate window helper, and the GraphTheme contract (every semantic color slot, no-theme-vs-DEFAULT_GRAPH_THEME equivalence, and a node/group's own accent still beating the theme's fallback). Draggable-group interaction (capture-phase interception, live drag, click-vs-drag threshold, the individual-drag/group-drag coexistence) is covered end-to-end by apps/hub/web's Playwright suite, since it needs a live pointer/DOM, not this package's Node-based tests.

Performance notes (0.3.2)

  • The hit (pointer) canvas is refreshed straight after a drop, a settle and a pan or zoom, so a card dropped somewhere can be grabbed there at once (force-graph otherwise repaints it at most every 800 ms).
  • The canvas repaints every frame only while something pulses, fades or moves (autoPauseRedraw); a still graph costs no frames and its frame loop pauses once the layout engine has stopped. The loop is never paused mid-cooldown.
  • In layoutMode="guided" a drop pins the card and repaints; it does not restart the force simulation (cooldownTicks is 1, the collision force is off).
  • A data-only update (labels, status, activity) keeps the existing graph and repaints; only an added or removed node or edge hands the renderer a new graph. Existing cards never move.
  • Canvas callbacks keep stable identities, group hulls are cached per layout version, and the glow (shadowBlur) is not drawn when reducedMotion is set or the graph has more than 150 nodes.
  • fitView pads by half a card plus a margin (capped at a quarter of the canvas).

Canvas theming

Every canvas color -- node fills/borders/text, edge lines/arrows/labels, the traveling-call dot, and group hull fallback colors -- comes from a GraphTheme, passed as Partial<GraphTheme> on ActivityGraphProps.theme. Unset fields fall back to DEFAULT_GRAPH_THEME (the exact look you get by omitting theme entirely), so a partial override only touches what it names:

import { ActivityGraph, DEFAULT_GRAPH_THEME, type GraphTheme } from '@atriarch-systems/tracery-visualizer';

const oceanGraph: Partial<GraphTheme> = {
  ...DEFAULT_GRAPH_THEME,
  nodeAccentFallback: '#3fc6ff',
  nodeFillActive: '#123246',
  edgeAccentFallback: '#3fc6ff',
  travelingDot: '#c8f2ff',
};

<ActivityGraph nodes={nodes} edges={edges} theme={oceanGraph} />;

A node's own presentation.accent and a group's own accent always win over the theme's fallback accent fields (nodeAccentFallback/groupAccentFallback) -- the theme only supplies a color for the case where the consumer's data supplied none. resolveGraphTheme(partial?) (used internally, and available to consumers building layered themes) fills in every unset field explicitly.

Guided placement

placeBranches(nodes, edges, previousPositions) returns new node positions and a position map to pass to the next update. It places new cards beside their first observed parent and preserves the slots of existing cards. Consumer-owned layout hints can specify parentId, a vertical lane (90 units per lane), and a leaf/group for compact ordered siblings. Root anchors form a left column. The helper does not interpret agent kinds or treat repeated/return edges as new parents. Missing parents and cycles still produce finite positions.

Use the result with layoutMode="guided". Established cards retain their positions, including manually arranged cards; links do not exert force. The normal force layout remains the default. Reset the position map and change layoutKey when switching layout modes. The consumer decides whether to pan or fit a growing graph. The normal force layout remains available for applications that prefer free movement.

placeBranches picks a node's placement parent from node.layout?.parentId, else the first edge whose target is that node. Edges with kind: 'data' are skipped when looking for a parent (a data dependency is not a call chain); 'spawn' edges (and the default 'call') are valid parents.

Groups and traces

ActivityNode.group and ActivityGraphProps.groups draw a presentation-only cluster: a rounded hull behind a group's member cards, with the group's label at its top-left corner. Groups never feed into force layout or placeBranches; two nodes in the same group are laid out exactly as if group were absent, and the hull is drawn under them from the current node positions each frame.

<ActivityGraph nodes={nodes} edges={edges} groups={[
  { id: 'trace-child-7', label: 'Subagent: pricing lookup', accent: '#8bb971' },
  { id: 'trace-child-8', label: 'Subagent: inventory check', dimmed: true },
]} />

A one- or two-member group draws a padded, rounded bounding rectangle (a true hull is a visually thin sliver at that size); three or more members draw a rounded convex hull around the padded card boxes. dimmed: true renders that group's hull and every member node at 45% alpha — use it to recede ancestor flows behind the flow in focus. A group with no current members draws nothing.

Draggable groups

In layoutMode="guided" (the only mode where node positions are pinned at all), clicking and dragging inside a group's hull — anywhere in its padded background, away from every node — moves every member of that group together, preserving their positions relative to each other. A click that lands on a node instead is never hijacked: individual nodes keep dragging exactly as before, independently, whether or not they belong to a group, and a group drag never blocks a later individual drag or vice versa. In layoutMode="force" this does nothing extra; hulls stay non-interactive and panning is unaffected. While the pointer hovers a hull's background in guided mode the cursor shows grab, and grabbing once a drag actually starts, so the affordance is discoverable without reading this section. A plain click (no movement past a small threshold) on hull-covered background deselects the current node, matching a click on truly empty background.

<ActivityGraph nodes={nodes} edges={edges} layoutMode="guided" groups={groups}
  onNodeMove={(node, position) => savePosition(node.id, position)}
  onGroupMove={(group, positions) => {
    for (const p of positions) savePosition(p.id, p);
  }} />

onGroupMove(group, positions) fires once, at the end of a successful group drag, with every moved member's final { id, x, y }. Every one of those members also gets its own onNodeMove call in the same shape an individual drag already produces (an anchored member snaps back to its home position instead, exactly like an anchored node's own individual drag, and is excluded from both callbacks since it never actually moved) — onGroupMove is additional, not a replacement, so existing onNodeMove-based persistence needs no changes to pick up a whole group's move. It is optional; omit it and a group drag behaves like a bundle of individual drags with nothing extra to observe.

ActivityEdge.kind distinguishes three relationships, default 'call'. 'data' draws a dashed line and is excluded from placeBranches parent selection and from force-link pull. 'spawn' draws thicker in the edge's accent color with a small hollow circle at the source end — the convention for "this flow spawned that one" — and, like 'data', gets force-link strength 0 so spawned subgraphs don't get yanked back toward their spawner.

ActivityGraphProps.onNodeActivate fires with the node when a user double-clicks it (two onNodeClicks on the same node within 350ms; react-force-graph-2d has no native dblclick) or presses Enter while that node is selected. A trace explorer can use it to jump into a spawned child flow's group.

License

This activity visualizer package is licensed under the Apache License, Version 2.0. Copyright 2026 Atriarch Systems. This grant applies to this package's own code and documentation; other Agent Kit packages have their own terms.

Apache-2.0 permits use, modification, redistribution, and commercial use, provided the LICENSE and NOTICE files are retained in copies or substantial portions of the software, and it includes an express patent grant from contributors to users. The software is provided as is, without warranty, subject to the license's terms. No visible application badge is required.

The graph engine is React Force Graph by Vasco Asturiano (MIT). This package also uses D3 Force by Mike Bostock (ISC). Dependencies remain under their respective licenses; preserve applicable upstream notices when distributing them. These acknowledgments do not replace their licenses.

Support

☕ Tracery Graph is free and open source. If it saves you time, buy me a coffee.