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

@mydatavalue/polter

v0.2.1

Published

Declarative React library for agent-driven UI control with visual guided execution

Readme

Your UI is the agent's interface. It opens the actual dropdown, clicks the actual button, with the user watching. No separate tools to build — single source of truth. After seeing it twice, users do it themselves.

Why

Every SaaS adding an AI agent faces two problems:

  1. Double the work. You build your UI, then build a whole separate set of agent tools — API endpoints, handlers, schemas — all duplicating what the UI already does.

  2. Users never learn. The agent does things behind the scenes or generates throwaway UI. Either way, users never see where buttons are or how the interface works. Permanent dependency.

Polter solves both. Your UI is the agent's interface — single source of truth. The agent scrolls to the real button, spotlights it, clicks it. Users watch and learn. After twice, they do it themselves. And you wrote zero agent-specific tools.

Install

npm install polter
# peer deps
npm install react react-dom zod

Quick Start

import { AgentActionProvider, AgentAction, useAgentActions } from 'polter';
import { z } from 'zod';

1. Wrap your app

<AgentActionProvider mode="guided" stepDelay={600}>
  <App />
</AgentActionProvider>

2. Register actions

Visual actions — wrap an element, the agent spotlights and clicks it:

<AgentAction name="export_csv" description="Export properties to CSV">
  <ExportButton />
</AgentAction>

Parameterized actions — spotlight the element, call your function:

<AgentAction
  name="sync_properties"
  description="Sync specific properties"
  parameters={z.object({
    property_ids: z.array(z.number()).optional().describe("IDs to sync")
  })}
  onExecute={(params) => triggerSync(params.property_ids)}
>
  <SyncButton />
</AgentAction>

Multi-step actions — sequential clicks (e.g. open dropdown, then select):

<AgentAction name="sync_data" description="Sync from API">
  <AgentStep label="Open sync menu">
    <DropdownTrigger />
  </AgentStep>
  <AgentStep label="Click sync">
    <SyncButton />
  </AgentStep>
</AgentAction>

Modal interactions — click a button to open a modal, type a value, then confirm:

// Parent — 3-step flow: open → type → confirm
<AgentAction name="apply_discount" parameters={z.object({ pct: z.number() })}
  onExecute={async () => { /* await async work started by the confirm click */ }}>
  <AgentStep label="Open settings">
    <SettingsButton />
  </AgentStep>
  <AgentStep label="Set value" fromTarget="discount-input" setParam="pct" />
  <AgentStep label="Confirm" fromTarget="confirm-btn" />
</AgentAction>

// Child (modal) — targets with prepareView to set up internal state
<AgentTarget name="discount-input" prepareView={() => setMode("custom")}>
  <Input value={value} onChange={...} />
</AgentTarget>
<AgentTarget name="confirm-btn">
  <ConfirmButton />
</AgentTarget>

3. Connect to your agent

const { schemas, execute, availableActions, isExecuting } = useAgentActions();

// Send schemas to your agent backend (auto-updates as components mount/unmount)
// Call execute("action_name", params) when the agent responds with a tool call

4. Integrate with existing handlers

import { useAgentCommandRouter } from 'polter';

// Wraps any existing command handler — registered actions get visual execution,
// unregistered ones fall through to your original handler.
const handleCommand = useAgentCommandRouter(existingHandler, (cmd) => cmd.action);

How it works

  1. <AgentAction> registers actions in a React context on mount, deregisters on unmount
  2. The registry always reflects exactly what's on screen — schemas auto-generate from Zod parameter definitions
  3. execute(name, params) looks up the action, finds the DOM element via refs, runs: scroll into view → dim surroundings → spotlight with pulsing ring → tooltip → pause → click/execute → cleanup
  4. <div style="display: contents"> wrapper provides DOM refs without affecting layout
  5. Components that mount = actions that exist. Navigate away = actions disappear. No manual sync.

Advanced: defineAction() + Registry

For multi-page apps, <AgentAction> schemas are only available when the component is mounted. If the user says "update the price on property 123" but that page isn't open, the agent can't see the action.

defineAction() solves this — schemas are available at import time, before any component mounts. Combined with the registry prop, the agent gets full knowledge of every action upfront (single LLM roundtrip).

1. Define actions (co-located with your feature)

// features/pricing/actions.ts
import { defineAction } from 'polter';
import { z } from 'zod';

export const updatePrice = defineAction({
  name: 'update_price',
  description: 'Update price markup on a property',
  parameters: z.object({
    property_id: z.string(),
    markup: z.number(),
  }),
  route: (p) => `/properties/${p.property_id}/pricing`,
});

2. Create a registry (barrel file)

// registry.ts
import { updatePrice } from './features/pricing/actions';
import { exportCsv } from './features/reports/actions';

export const agentRegistry = [updatePrice, exportCsv];

3. Pass to provider with your router

import { agentRegistry } from './registry';

<AgentActionProvider
  registry={agentRegistry}
  navigate={(path) => router.push(path)}
>
  <App />
</AgentActionProvider>

4. Components reference the definition

// features/pricing/PricingPage.tsx
import { updatePrice } from './actions';

<AgentAction action={updatePrice} onExecute={(p) => setMarkup(p.markup)}>
  <SaveButton />
</AgentAction>

How it works

  1. On mount, the provider registers all registry actions as schema-only entries — the agent sees them immediately
  2. When the agent calls execute('update_price', { property_id: '123', markup: 15 }):
    • Provider calculates the route: /properties/123/pricing
    • Calls your navigate() function
    • Waits for the <AgentAction> component to mount on the new page
    • Runs the visual execution (spotlight, click, etc.)
  3. When the component unmounts (user navigates away), the action reverts to schema-only — never disappears from the agent's view

If an action has no corresponding UI element anywhere in the app, you can provide onExecute directly on the definition as an escape hatch — it will execute without navigation or spotlight.

API

Execution modes

| Mode | Behavior | Use case | |------|----------|----------| | "guided" | Scroll → spotlight → pause → click | Teaching users, first-time flows | | "instant" | Execute immediately, no visual | Power users, repeat actions |

Provider props

| Prop | Type | Default | |------|------|---------| | mode | "guided" \| "instant" | "guided" | | stepDelay | number | 600 | | overlayOpacity | number | 0.5 | | spotlightPadding | number | 8 | | tooltipEnabled | boolean | true | | onExecutionStart | (name: string) => void | — | | onExecutionComplete | (result: ExecutionResult) => void | — | | registry | ActionDefinition[] | — | | navigate | (path: string) => void \| Promise<void> | — |

Disabled actions

<AgentAction
  name="push_changes"
  description="Push pending changes"
  disabled={!hasPendingChanges}
  disabledReason="No pending changes to push"
>
  <PushButton />
</AgentAction>

Disabled actions appear in availableActions but are excluded from schemas. Calling execute() on a disabled action returns { success: false, error: "No pending changes to push" }.

CSS customization

All overlay elements have class names:

.polter-spotlight { /* box-shadow overlay with cutout */ }
.polter-ring { /* pulsing border around target */ }
.polter-tooltip { /* label tooltip */ }

Best practices

See docs/best-practices.md for patterns around conditional rendering, per-row actions, Radix integration, dropdowns, and common pitfalls.

Zero dependencies

Peer deps only: React 18+ and Zod. No runtime dependencies.

License

MIT