@nevescloud/pip 3.4.1 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,8 +38,14 @@ import { createPip, createRuntime, openai } from 'https://cdn.jsdelivr.net/npm/@
38
38
  ```
39
39
 
40
40
  ```js
41
- // Local — in-browser inference via transformers.js + WebGPU (different shape: no runtime, renderer is host-driven)
42
- import { createPip, createTransformersRenderer } from 'https://cdn.jsdelivr.net/npm/@nevescloud/pip@latest/bundle/local.esm.js';
41
+ // Local — in-browser inference via transformers.js + WebGPU
42
+ import { createPip, createRuntime, local } from 'https://cdn.jsdelivr.net/npm/@nevescloud/pip@latest/bundle/local.esm.js';
43
+
44
+ const rt = createRuntime({ provider: local({ model: 'LiquidAI/LFM2.5-350M-ONNX' }) });
45
+ const pip = createPip({ onSubmit: rt.onSubmit, onSlash: rt.onSlash, slashSource: rt.slashSource });
46
+ // `createTransformersRenderer` is also exported for hosts that drive
47
+ // a one-shot paint themselves (e.g. an offline fallback that bypasses
48
+ // the turn loop entirely).
43
49
  ```
44
50
 
45
51
  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](../../CONSUMERS.md) for the full entry-point list.
@@ -50,7 +56,7 @@ On jsdelivr the `.esm.js` suffix is required — jsdelivr serves files by raw pa
50
56
  |---|---|---|
51
57
  | `onSubmit(text, ctx)` | — | Required. Returns the reply string (or a promise of one). |
52
58
  | `onSlash(text)` | `null` | Legacy fallback intercept for `/`-prefixed input. Runs *after* registered commands and built-ins miss. New code should call `pip.registerSlash()` instead. |
53
- | `slashSource()` | built-in | Override for the autocomplete dropdown's source. By default, pip enumerates `pip.registerSlash()` registrations + built-in `/help` and `/clear`. ↑/↓ to cycle, Tab/Enter to accept, Esc to close; `complete(partial)` per-keystroke after the space supplies arg suggestions. |
59
+ | `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. |
54
60
  | `introText` | `""` | Muted message shown before first turn; auto-dismisses on submit. |
55
61
  | `introDismissMs` | `7000` | How long the intro stays before fading. `0` to keep until first message. |
56
62
  | `placeholder` | `"Ask Pip…"` | Input placeholder. |
@@ -70,7 +76,7 @@ On jsdelivr the `.esm.js` suffix is required — jsdelivr serves files by raw pa
70
76
 
71
77
  ## Slash commands
72
78
 
73
- `pip.registerSlash({ name, handler, description?, complete? })` adds a command. Registry-first dispatch: registered commands win over built-ins so a host can override `/help` or `/clear` by registering with the same name.
79
+ `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.
74
80
 
75
81
  ```js
76
82
  pip.registerSlash({
@@ -86,7 +92,7 @@ pip.registerSlash({
86
92
  pip.unregisterSlash("model");
87
93
  ```
88
94
 
89
- Handler returns `{ reply?, clearedUI?, openCompletions?, passThrough? } | null`. `null` falls through to `onSlash` (if provided) and then to the LLM. Built-ins ship `/help` (auto-lists registered + built-ins) and `/clear` (wipes pip's local history + DOM).
95
+ 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.
90
96
 
91
97
  - `reply` — render as an assistant turn in chat history.
92
98
  - `clearedUI` — the handler already painted its own UI (via `startTurn`, `askInChat`, etc.); pip-core skips creating a turn.
@@ -1,9 +1,18 @@
1
1
  // Quickstart entry — pip wired to an in-browser model in one import.
2
- // Re-exports the two primitives most local-renderer hosts compose.
3
- // Different shape from bundle/anthropic and bundle/openai: there's no
4
- // runtime here the renderer is host-driven (one-shot generate),
5
- // not part of the turn loop. Consumers shouldn't need to know which
6
- // underlying library powers the renderer; that's why this entry exists.
2
+ // Re-exports two shapes:
3
+ //
4
+ // * `createRuntime` + `local()` runtime-compatible provider so local
5
+ // participates in `/model` alongside anthropic and openai. Same wiring
6
+ // pattern as bundle/anthropic and bundle/openai.
7
+ //
8
+ // * `createTransformersRenderer` + `splitThinking` — direct renderer for
9
+ // hosts that paint a one-shot reply themselves (e.g. an offline
10
+ // fallback that bypasses the turn loop entirely).
11
+ //
12
+ // Consumers shouldn't need to know which underlying library powers either
13
+ // path — the file name signals the category (local), the export name on
14
+ // the renderer signals the library (transformers.js).
7
15
 
8
16
  export { createPip } from '../pip-core.esm.js';
9
- export { createTransformersRenderer, splitThinking } from '../providers/local.esm.js';
17
+ export { createRuntime } from '../runtime.esm.js';
18
+ export { local, createTransformersRenderer, splitThinking } from '../providers/local.esm.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nevescloud/pip",
3
- "version": "3.4.1",
3
+ "version": "3.5.0",
4
4
  "description": "Floating assistant bubble + panel + chat runtime. ESM, no build.",
5
5
  "type": "module",
6
6
  "main": "pip-core.esm.js",
package/pip-core.esm.js CHANGED
@@ -1559,7 +1559,7 @@ export function createPip(opts = {}) {
1559
1559
 
1560
1560
  // Built-in slash-command registry. Hosts call pip.registerSlash({name,
1561
1561
  // description, handler, complete?}) — dispatched in submit() before
1562
- // built-ins (/help, /clear) and the legacy onSlash callback. Keeps the
1562
+ // built-in /clear and the legacy onSlash callback. Keeps the
1563
1563
  // single mental model: one place to declare "what / commands does this
1564
1564
  // app expose?", and the autocomplete picks them up automatically.
1565
1565
  const _slashCommands = new Map();
@@ -1652,22 +1652,10 @@ export function createPip(opts = {}) {
1652
1652
  function defaultSlashSource() {
1653
1653
  const out = [];
1654
1654
  for (const s of _slashCommands.values()) out.push({ name: s.name, description: s.description, complete: s.complete });
1655
- if (!_slashCommands.has("help")) out.push({ name: "help", description: "list commands" });
1656
1655
  if (!_slashCommands.has("clear")) out.push({ name: "clear", description: "clear chat history" });
1657
1656
  return out;
1658
1657
  }
1659
1658
  function dispatchBuiltinSlash(cmd) {
1660
- if (cmd === "help" || cmd === "?") {
1661
- // The slash autocomplete dropdown already lists every command with
1662
- // its description — /help duplicating that as a chat turn was visual
1663
- // noise and history clutter. Reopen the dropdown in cmd-name mode
1664
- // instead, same surface a user gets by typing `/`.
1665
- input.value = "/";
1666
- input.setSelectionRange(1, 1);
1667
- input.focus();
1668
- updateSlashSuggest();
1669
- return { clearedUI: true };
1670
- }
1671
1659
  if (cmd === "clear") {
1672
1660
  history.length = 0;
1673
1661
  turns.innerHTML = "";
@@ -2091,7 +2079,7 @@ export function createPip(opts = {}) {
2091
2079
 
2092
2080
  function updateSlashSuggest() {
2093
2081
  // No early-return on missing slashSource — the built-in source ensures
2094
- // even hosts that didn't wire anything still see /help and /clear.
2082
+ // even hosts that didn't wire anything still see /clear.
2095
2083
  const value = input.value;
2096
2084
  if (!value.startsWith("/")) return closeSlashSuggest();
2097
2085
  const slice = value.slice(1);
@@ -2169,19 +2157,15 @@ export function createPip(opts = {}) {
2169
2157
  e.preventDefault();
2170
2158
  closeSlashSuggest();
2171
2159
  } else if (e.key === "Enter") {
2172
- // Cmd-name mode (no space yet): accept the highlighted command and
2173
- // stop user may still want to type or pick args.
2174
- // Arg mode (space typed): accept the highlighted arg into the input,
2175
- // then let the form's default Enter→submit fire so the just-mutated
2176
- // value lands in submit(). Without this, Enter would submit whatever
2177
- // partial the user had typed, losing the keyboard selection — click
2178
- // worked only because mousedown ran acceptSlashSuggest before submit.
2179
- // The early-return at the top of this handler guarantees a visible
2180
- // dropdown with at least one item, so accept always has a target.
2181
- const value = input.value;
2182
- const inCmdMode = value.startsWith("/") && !value.includes(" ");
2183
- if (inCmdMode) { e.preventDefault(); acceptSlashSuggest(); }
2184
- else acceptSlashSuggest();
2160
+ // Accept the highlighted entry and submit in one keystroke — CLI
2161
+ // command-palette convention (VS Code, shell tab+enter). Tab is the
2162
+ // path for "accept and keep editing" e.g. pick the command, then
2163
+ // type args before running. The early-return at the top of this
2164
+ // handler guarantees a visible dropdown with at least one item, so
2165
+ // accept always has a target.
2166
+ e.preventDefault();
2167
+ acceptSlashSuggest();
2168
+ form.requestSubmit();
2185
2169
  }
2186
2170
  });
2187
2171
 
@@ -1,30 +1,19 @@
1
- // In-browser model renderer using transformers.js. One-shot generate
2
- // (not part of pip-runtime's turn loop) — hands the host a tokenizer +
3
- // model loaded over WebGPU and a streaming generate() that paints into
4
- // a Pip turnEl. Includes the download-progress UI and a `<think>` pill
5
- // for reasoning models, so consumers don't reinvent either.
1
+ // In-browser model via transformers.js + WebGPU. Two shapes ship:
6
2
  //
7
- // Usage:
8
- // import { createTransformersRenderer } from
9
- // '@nevescloud/pip/providers/local.esm.js';
3
+ // 1. `local({ model, dtype, maxTokens, genParams })` — runtime-compatible
4
+ // provider, slots into `createRuntime({ models: [{ provider: local(...) }] })`
5
+ // next to anthropic() and openai(). Wraps the renderer below and adapts
6
+ // its setReplyText-callback paint into the runtime's async-generator
7
+ // event protocol. Use this when local should participate in `/model`.
10
8
  //
11
- // const renderer = createTransformersRenderer();
12
- // renderer.setModel({
13
- // id: 'LiquidAI/LFM2.5-350M-ONNX',
14
- // dtype: 'q4',
15
- // maxTokens: 256,
16
- // genParams: { temperature: 0.1, top_k: 50, repetition_penalty: 1.05 },
17
- // });
18
- // const text = await renderer.generate({
19
- // messages, // chat messages array
20
- // turnEl, // pip turnEl (download progress + <think> pill render here)
21
- // setReplyText, // pip's setReplyText
22
- // signal, // optional AbortSignal for mid-stream cancel
23
- // });
9
+ // 2. `createTransformersRenderer()` — direct renderer for hosts that build
10
+ // their own one-shot paint loop (e.g. neves.cloud's offline fallback,
11
+ // where local isn't a /model peer but a recovery path when the relay is
12
+ // unreachable). Exposes `setModel` + `generate({ messages, turnEl,
13
+ // setReplyText, signal })` returning a Promise<text>.
24
14
  //
25
- // Switching models: call setModel() again with a new config — the cached
26
- // model+tokenizer drop and reload on the next generate(). Within a single
27
- // model, repeat generate() calls reuse the loaded artifacts.
15
+ // Both share the same underlying model load (download-progress UI +
16
+ // <think> pill rendering); the provider just adapts the call shape.
28
17
 
29
18
  import { showLoading, hideLoading } from '../pip-core.esm.js';
30
19
 
@@ -270,4 +259,49 @@ export function createTransformersRenderer() {
270
259
  };
271
260
  }
272
261
 
262
+ // Runtime-compatible provider. The renderer streams reply text via the
263
+ // setReplyText callback (cumulative buffer per call); we proxy that
264
+ // callback, diff each call into a `text_delta` event, and yield events
265
+ // the runtime's turn loop already consumes. The <think> pill mounts onto
266
+ // turnEl directly inside the renderer — unaffected, still works.
267
+ //
268
+ // One pitfall handled: the renderer occasionally re-paints the same
269
+ // buffer (no new tokens emitted between calls), so the diff guards
270
+ // against zero-length deltas. AbortSignal flows through naturally —
271
+ // the underlying TextStreamer throws AbortError, which we surface.
272
+ export function local({ model, dtype = 'q4', maxTokens = 256, genParams } = {}) {
273
+ const renderer = createTransformersRenderer();
274
+ if (model) renderer.setModel({ id: model, dtype, maxTokens, genParams });
275
+
276
+ return ({ messages, signal, turnEl, setReplyText }) => (async function* () {
277
+ let lastFull = '';
278
+ const queue = [];
279
+ let wake = null;
280
+ let done = false;
281
+ let error = null;
282
+
283
+ const proxySetReplyText = (_el, fullText) => {
284
+ const delta = fullText.slice(lastFull.length);
285
+ lastFull = fullText;
286
+ if (delta) {
287
+ queue.push({ type: 'text_delta', text: delta });
288
+ wake?.();
289
+ }
290
+ };
291
+
292
+ renderer.generate({ messages, turnEl, setReplyText: proxySetReplyText, signal })
293
+ .then(() => { done = true; wake?.(); })
294
+ .catch((e) => { error = e; done = true; wake?.(); });
295
+
296
+ while (true) {
297
+ if (queue.length) { yield queue.shift(); continue; }
298
+ if (done) break;
299
+ await new Promise((r) => { wake = r; });
300
+ }
301
+
302
+ if (error) throw error;
303
+ yield { type: 'turn_end', stopReason: 'end_turn' };
304
+ })();
305
+ }
306
+
273
307
  export { splitThinking };
package/runtime.esm.js CHANGED
@@ -121,6 +121,12 @@ export function createRuntime({
121
121
  tools: buildToolList(),
122
122
  system: systemFn(),
123
123
  signal: abortCtrl.signal,
124
+ // turnEl + setReplyText let renderer-shaped providers (e.g. the
125
+ // local transformers wrapper) paint progress bars and <think>
126
+ // pills directly onto the active turn. Wire-shape providers
127
+ // (Anthropic, OpenAI) ignore unknown keys.
128
+ turnEl,
129
+ setReplyText,
124
130
  });
125
131
 
126
132
  const assistantContent = [];
@@ -210,8 +216,7 @@ export function createRuntime({
210
216
  }
211
217
 
212
218
  // Registry-first dispatch. Host-registered commands win over built-ins
213
- // so `/help` can be overridden if a host wants different formatting.
214
- // Built-in `/help` enumerates both layers so users see one combined list.
219
+ // so `/clear` (etc.) can be overridden if a host wants different behavior.
215
220
  function onSlash(text) {
216
221
  const slice = text.slice(1);
217
222
  const sp = slice.indexOf(' ');
@@ -255,17 +260,6 @@ export function createRuntime({
255
260
  const suffix = target.label ? ` (${target.label})` : '';
256
261
  return { reply: `Switched to \`${target.name}\`${suffix}.` };
257
262
  }
258
- if (cmd === 'help' || cmd === '?') {
259
- const lines = ['**Commands:**'];
260
- for (const s of registeredSlash.values()) {
261
- lines.push(`- \`/${s.name}\` — ${s.description || ''}`);
262
- }
263
- lines.push('- `/clear` — reset conversation');
264
- lines.push('- `/tools` — list registered tools');
265
- if (models.length) lines.push('- `/model` — list or switch active model');
266
- lines.push('- `/help` — this list');
267
- return { reply: lines.join('\n') };
268
- }
269
263
  return null;
270
264
  }
271
265
 
@@ -281,7 +275,7 @@ export function createRuntime({
281
275
  }
282
276
 
283
277
  // Slash commands. Same shape as registerTool: name + handler are required;
284
- // description and complete() are surfaced by autocomplete + /help.
278
+ // description and complete() are surfaced by the autocomplete dropdown.
285
279
  // handler(argsString) returns { reply?, clearedUI?, passThrough? } | null.
286
280
  function registerSlash(s) {
287
281
  if (!s || !s.name || typeof s.handler !== 'function') {
@@ -311,7 +305,6 @@ export function createRuntime({
311
305
  .filter((n) => n.toLowerCase().startsWith(partial.toLowerCase())),
312
306
  });
313
307
  }
314
- out.push({ name: 'help', description: 'list commands' });
315
308
  return out;
316
309
  }
317
310
  function slashSource() {