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

@nevescloud/pip

v3.9.2

Published

Floating assistant bubble + panel + chat runtime. ESM, no build.

Downloads

2,693

Readme

Pip

Floating assistant bubble + panel. ESM, no build, no dependencies. The module owns DOM, CSS, open/close, turn rendering. You provide the brains.

Use

import { createPip } from 'https://cdn.jsdelivr.net/npm/@nevescloud/pip@latest/pip-core.esm.js';

const pip = createPip({
  introText: 'Ask me anything.',
  placeholder: 'Type here…',
  async onSubmit(text, ctx) {
    return await yourAssistant(text);
  },
});

@latest (above) auto-tracks new releases on next jsdelivr/SW revalidation (~7d browser cache). Pin a specific version (@2.11.0) for production sites where a regression is costly — npm distributions are immutable per version, safe for forever-cached CDN delivery. @2 semver-locks the major (auto-updates minor/patch, catches breaking API changes) if you want a middle ground. See CONSUMERS.md for the trade-off.

Or via npm: npm install @nevescloud/pip.

Quickstart: bundle entries

Provider-named bundles re-export the three primitives most hosts compose, from a single import. Pick the brain you're wiring to:

// Anthropic — Claude on /v1/messages
import { createPip, createRuntime, anthropic } from 'https://cdn.jsdelivr.net/npm/@nevescloud/pip@latest/bundle/anthropic.esm.js';

const rt = createRuntime({ provider: anthropic({ model: 'claude-opus-4-7', apiKey: '…' }) });
const pip = createPip({ onSubmit: rt.onSubmit, onSlash: rt.onSlash, slashSource: rt.slashSource });
// OpenAI-compatible — OpenAI, GitHub Models, Together, Groq, OpenRouter, LM Studio, llama.cpp
import { createPip, createRuntime, openai } from 'https://cdn.jsdelivr.net/npm/@nevescloud/pip@latest/bundle/openai.esm.js';
// Local — in-browser inference via transformers.js + WebGPU
import { createPip, createRuntime, local } from 'https://cdn.jsdelivr.net/npm/@nevescloud/pip@latest/bundle/local.esm.js';

const rt = createRuntime({ provider: local({ model: 'LiquidAI/LFM2.5-350M-ONNX' }) });
const pip = createPip({ onSubmit: rt.onSubmit, onSlash: rt.onSlash, slashSource: rt.slashSource });
// `createTransformersRenderer` is also exported for hosts that drive
// a one-shot paint themselves (e.g. an offline fallback that bypasses
// the turn loop entirely).
// Chrome — on-device Gemini Nano via the Prompt API (zero download for users on Chrome 138+ that already has weights; ~2B-effective-param quality)
import { createRuntime }  from 'https://cdn.jsdelivr.net/npm/@nevescloud/pip@latest/runtime.esm.js';
import { createPip }      from 'https://cdn.jsdelivr.net/npm/@nevescloud/pip@latest/pip-core.esm.js';
import { chrome }         from 'https://cdn.jsdelivr.net/npm/@nevescloud/pip@latest/providers/chrome.esm.js';

const rt = createRuntime({ provider: chrome({ temperature: 0.1 }) });
const pip = createPip({ onSubmit: rt.onSubmit, onSlash: rt.onSlash, slashSource: rt.slashSource });
// No bundle — Chrome doesn't need its own re-export of createPip + createRuntime
// (`bundle/anthropic` already brings those, and chrome() composes alongside).

On jsdelivr the .esm.js suffix is required — jsdelivr serves files by raw path, not via package.json exports. npm-installed consumers can use the shorter @nevescloud/pip/bundle/anthropic (Node ESM resolver honors the exports map). pip/bundle.esm.js (or pip/bundle via npm) is an alias for bundle/anthropic — the default when you haven't picked a brain. Bundles are sugar over the layered files; hosts with a different brain shape (UI only, custom provider, in-browser model) import the granular files directly. See CONSUMERS.md for the full entry-point list.

Options

| Key | Default | Notes | |---|---|---| | onSubmit(text, ctx) | — | Required. Returns the reply string (or a promise of one). | | onSlash(text) | null | Legacy fallback intercept for /-prefixed input. Runs after registered commands and built-ins miss. New code should call pip.registerSlash() instead. | | slashSource() | built-in | Override for the autocomplete dropdown's source. By default, pip enumerates pip.registerSlash() registrations + built-in /clear. ↑/↓ to cycle, Enter to run, Tab to accept without submitting (use when adding args), Esc to close; complete(partial) per-keystroke after the space supplies arg suggestions. | | introText | "" | Muted message shown before first turn; auto-dismisses on submit. | | introDismissMs | 7000 | How long the intro stays before fading. 0 to keep until first message. | | placeholder | "Ask Pip…" | Input placeholder. | | autoOpen | false | Open the panel on mount. | | autoOpenDelayMs | 700 | Delay before auto-open. | | maxLength | 4000 | Input character cap. | | openHotkey | "/" | Global key that opens pip and prefills the input. "" or false to disable. Skipped while typing in another field. | | historyLimit | 10 | Rolling history window passed to onSubmit via ctx.history. | | fallbackReply | "Can't think right now — try again?" | Shown when onSubmit returns null / undefined. Override per host to point at recovery commands you actually expose, e.g. "Can't think right now — try \/model tiny`.". | | emptyReply|"I don't have a good answer for that — tell me more?"| Shown whenonSubmitreturns"". | | modelLabel|""| Small pill rendered in the header strip (active backend / model name). Update viapip.setModelLabel(label). | | slashHint|true| Render a/key-cap button that seeds the input + opens the slash autocomplete. Discoverable affordance; setfalseto hide. | |onAbort|null| Called when the user clicks the stop button while pip is responding. Wire to your "abort the in-flight LLM call" path. If omitted, the stop button still renders (visual consistency with the responding state) but click is a no-op. | |mic|false| See [Mic input](#mic-input) below.truemounts the Web Speech button with default behavior; pass{ onChunk, onFinal }for advanced hooks. | |container|document.body| Mount point. | |onOpen, onClose|null` | Lifecycle hooks. |

Slash commands

pip.registerSlash({ name, handler, description?, complete? }) adds a command. Registry-first dispatch: registered commands win over built-ins so a host can override /clear by registering with the same name.

pip.registerSlash({
  name: "model",
  description: "switch backend",
  complete: (partial) => ["claude", "gpt-4o"].filter(s => s.startsWith(partial)),
  handler: (args) => {
    setBackend(args.trim());
    return { reply: `Switched to ${args.trim()}.` };
  },
});

pip.unregisterSlash("model");

Handler returns { reply?, clearedUI?, openCompletions?, passThrough? } | null. null falls through to onSlash (if provided) and then to the LLM. The autocomplete dropdown is the help surface — every registered + built-in command appears with its description as the user types /. Built-in /clear wipes pip's local history + DOM.

  • reply — render as an assistant turn in chat history.
  • clearedUI — the handler already painted its own UI (via startTurn, askInChat, etc.); pip-core skips creating a turn.
  • openCompletions — re-open the autocomplete dropdown in arg-mode for this command. Use when the bare command (no arg) is meant to surface a sub-menu like a model picker — keeps the navigation inside the dropdown the user came from instead of logging a listing to history. Pip-core puts "/<cmd> " in the input, focuses it, and runs the registered complete('') to populate the dropdown.
  • passThrough — treat as if the handler didn't match; falls through to onSlash then the LLM.

Tool-using turns

For hosts running multi-step LLM loops (computer-use, tool dispatch), pip's default single-reply model collapses the turn into one block and hides the step-by-step reasoning users want to see. The interleave primitives let a turn render as [text 1] [tool pill 1] [text 2] [image] [pill 2] [final text] in arrival order — same shape Anthropic computer-use / hatch / Codex-style UIs converge on.

const pip = createPip({
  onSubmit: async (text, { turnEl }) => {
    let reply = null;
    for await (const ev of llmStream(text)) {
      if (ev.type === 'text_delta') {
        if (!reply) reply = pip.appendReplyBubble(turnEl);
        reply.setText(ev.fullText);
      } else if (ev.type === 'tool_start') {
        reply = null;  // next deltas land below the pill
        const pill = pip.appendToolPill(turnEl, ev.name, { label: `${ev.name} …` });
        ev.onFinish = ({ input, result, error, durationMs }) => {
          pill.finish({ label: summarize(ev.name, result), input, result, error, durationMs });
        };
      } else if (ev.type === 'image') {
        pip.appendTurnImage(turnEl, { src: ev.dataUrl, alt: ev.caption });
        reply = null;
      }
    }
    return '';  // we owned the rendering; let pip's default-reply stay hidden
  },
});

| Method | Returns | Notes | |---|---|---| | appendToolPill(turnEl, name, { label? }) | { el, finish({ label?, input?, result?, error?, durationMs? }) } | .running → completed/.error on finish. Disclosure button expands a pre with args + result (truncated at 240 chars per string). Hold the returned object across the await; pass it back to finish() without tracking the DOM separately. | | appendReplyBubble(turnEl) | { el, setText(md), setHtml(html) } | Multi-bubble per turn. setText renders markdown via pip's built-in renderMd. setHtml skips escaping — sanitize first. | | appendTurnImage(turnEl, { src, alt? }) | HTMLImageElement | Inline camera frame / screenshot. Capped at 220px tall. Lazy-loaded. | | scrollToBottom() | — | Exposed for hosts interleaving their own DOM between primitives. |

Mic input

createPip({ mic: true }) mounts a Web Speech-backed mic button next to the send button. Click toggles sticky-mode dictation: final transcripts flow through onSubmit exactly as if the user had typed and sent them. Escape cancels. No-speech retry with sticky-disarm after 2 consecutive flakes. No-op when Web Speech isn't supported in the current browser (no broken affordance).

For advanced hosts — safety-verb instant-fire that needs sub-second response, mid-turn injection that bypasses the input field — pass an object:

const pip = createPip({
  mic: {
    onChunk: async (text) => {
      // Fires on every Web Speech "final chunk" before the silence-commit
      // window. Return true to consume — pip stops the mic + clears the
      // input. Use for instant-fire safety verbs ("stop", "halt").
      if (isSafetyVerb(text)) { handleSafetyVerb(text); return true; }
      return false;
    },
    onFinal: async (text) => {
      // Fires on final + silence-commit. Return true if you handled the
      // transcript yourself (e.g. injected into an already-in-flight turn);
      // pip skips its default submit.
      if (isMidTurn()) { injectIntoActiveTurn(text); return true; }
      return false;
    },
  },
});

// Suspend/resume drive the muted visual state for events the gate can't
// see (your TTS playback, anything else).
pip.suspendMic();   // drops active session + lights mute icon
pip.resumeMic();    // re-arms sticky if it was on

// Programmatic toggle — useful for /voice slash:
pip.toggleMic();

// Render an inline cyan notice for mic-related status (permission, retry).
pip.surfaceMicNotice("Didn't catch that — try again.");

// Check support before showing affordances:
if (pip.micSupported) { /* render Voice button */ }

Styling

CSS is auto-injected. Hosts customize via CSS variables on :root or any ancestor:

Colors / surfaces

  • --pip-surface — panel background
  • --pip-ink, --pip-ink-muted — text colors
  • --pip-border — borders
  • --pip-accent — focus / AI-generated highlight color

Type scale (defaults shown)

  • --pip-t-input — input field. Default 14px on desktop (pointer: fine) and 16px on touch (mobile floor — going below 16 makes iOS Safari zoom on focus).
  • --pip-t-body: 14px — assistant replies (the primary content layer)
  • --pip-t-caption: 12px — intro, echoed user question, code blocks

Class overrides (.pip-panel, .pip-input, .pip-bubble, .pip-notify, …) work too.

Runtime + providers

For a turn loop, tool dispatch, history, and an Anthropic provider, pair pip-core with runtime.esm.js. See docs/RUNTIME.md.

Demo

docs/index.html is a standalone demo wiring three stub providers (echo, reverse, danger) through the runtime. Open it locally to play with the slash autocomplete, /model switching, and the chat shell without needing an API key. The danger stub fires a delete_thing tool_use that's gated by a preToolUse hook (Run/Cancel via askInChat) — a working example of the runtime's hook events.