@nevescloud/pip 3.3.0 → 3.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nevescloud/pip",
3
- "version": "3.3.0",
3
+ "version": "3.4.1",
4
4
  "description": "Floating assistant bubble + panel + chat runtime. ESM, no build.",
5
5
  "type": "module",
6
6
  "main": "pip-core.esm.js",
@@ -8,6 +8,8 @@
8
8
  ".": "./pip-core.esm.js",
9
9
  "./pip-core.esm.js": "./pip-core.esm.js",
10
10
  "./runtime.esm.js": "./runtime.esm.js",
11
+ "./webmcp": "./webmcp.esm.js",
12
+ "./webmcp.esm.js": "./webmcp.esm.js",
11
13
  "./bundle": "./bundle/anthropic.esm.js",
12
14
  "./bundle/anthropic": "./bundle/anthropic.esm.js",
13
15
  "./bundle/anthropic.esm.js": "./bundle/anthropic.esm.js",
@@ -22,6 +24,7 @@
22
24
  "files": [
23
25
  "pip-core.esm.js",
24
26
  "runtime.esm.js",
27
+ "webmcp.esm.js",
25
28
  "bundle/",
26
29
  "providers/",
27
30
  "README.md",
package/pip-core.esm.js CHANGED
@@ -2039,10 +2039,11 @@ export function createPip(opts = {}) {
2039
2039
  slashList.innerHTML = "";
2040
2040
  if (!slashCurrent.length) { slashList.hidden = true; scroll.classList.remove("is-backdrop"); return; }
2041
2041
  scroll.classList.add("is-backdrop");
2042
+ let selectedLi = null;
2042
2043
  slashCurrent.forEach((item, i) => {
2043
2044
  const li = document.createElement("li");
2044
2045
  li.setAttribute("role", "option");
2045
- if (i === slashSelected) li.classList.add("selected");
2046
+ if (i === slashSelected) { li.classList.add("selected"); selectedLi = li; }
2046
2047
  const name = document.createElement("span");
2047
2048
  name.className = "name";
2048
2049
  name.textContent = item.isArg ? item.name : `/${item.name}`;
@@ -2054,10 +2055,12 @@ export function createPip(opts = {}) {
2054
2055
  li.appendChild(desc);
2055
2056
  }
2056
2057
  // Mouse hover promotes the row to the keyboard selection so Enter
2057
- // and arrow keys always agree with the visible highlight. Without
2058
- // this, hover and keyboard-cursor can land on different rows and
2059
- // the user can't predict which one Enter will pick.
2060
- li.addEventListener("mouseenter", () => {
2058
+ // and arrow keys agree with the visible highlight. Listen on
2059
+ // mousemove (not mouseenter) so a stationary cursor doesn't grab
2060
+ // selection when the keyboard scrolls a new row under it — the
2061
+ // re-render replaces the DOM, which would re-fire mouseenter on
2062
+ // the new element under the cursor and undo the arrow keypress.
2063
+ li.addEventListener("mousemove", () => {
2061
2064
  if (slashSelected === i) return;
2062
2065
  slashSelected = i;
2063
2066
  for (const sib of slashList.children) sib.classList.remove("selected");
@@ -2071,6 +2074,10 @@ export function createPip(opts = {}) {
2071
2074
  slashList.appendChild(li);
2072
2075
  });
2073
2076
  slashList.hidden = false;
2077
+ // Keep the keyboard-driven selection in view. block:"nearest" only
2078
+ // scrolls when the selected row is actually clipped — no jarring
2079
+ // re-center when it's already visible.
2080
+ selectedLi?.scrollIntoView({ block: "nearest" });
2074
2081
  }
2075
2082
 
2076
2083
  function closeSlashSuggest() {
package/webmcp.esm.js ADDED
@@ -0,0 +1,211 @@
1
+ // WebMCP adapter for pip-runtime.
2
+ //
3
+ // Discovers tools registered via the standard `navigator.modelContext`
4
+ // surface (W3C Community Group Report, 23 April 2026) and wires each one
5
+ // into pip's runtime tool registry. Tools registered on the host page
6
+ // become tools the runtime's provider can call.
7
+ //
8
+ // Usage:
9
+ // import { createRuntime } from '@nevescloud/pip/runtime.esm.js';
10
+ // import { attachWebmcp } from '@nevescloud/pip/webmcp.esm.js';
11
+ //
12
+ // const rt = createRuntime({ provider });
13
+ // const webmcp = attachWebmcp(rt);
14
+ //
15
+ // // Page code (somewhere) calls:
16
+ // navigator.modelContext.registerTool({
17
+ // name: 'get_greeting',
18
+ // description: 'Returns a fresh greeting from this tab.',
19
+ // inputSchema: { type: 'object', properties: {}, required: [] },
20
+ // async execute() { return 'Hello!'; },
21
+ // });
22
+ //
23
+ // // The runtime's provider now sees `get_greeting` as a callable tool.
24
+ //
25
+ // // Clean up (reverses the patch, unregisters mirrored tools):
26
+ // webmcp.stop();
27
+ //
28
+ // Discovery strategy:
29
+ //
30
+ // 1. **Snapshot at attach time.** If `navigator.modelContext` exposes an
31
+ // iterable of currently-registered tools (`mc.tools`, `mc.getTools()`,
32
+ // or similar), we read it and mirror each entry into runtime. This
33
+ // covers tools registered before the adapter started.
34
+ // 2. **Intercept future calls.** `registerTool` / `unregisterTool` on
35
+ // `navigator.modelContext` are patched so any later call from the
36
+ // same realm propagates to runtime automatically.
37
+ //
38
+ // Limitations:
39
+ //
40
+ // - Tools registered from a *different* JavaScript realm (e.g. a separate
41
+ // extension's content script with its own `navigator` reference) are
42
+ // invisible to the patch. Snapshot covers them only if `mc` exposes
43
+ // introspection.
44
+ // - The WebMCP draft is still in flux; the field names tools may use
45
+ // (`execute` vs `handler`, `inputSchema` vs `schema`, etc.) are
46
+ // normalized below — both shapes are accepted.
47
+
48
+ const DEFAULT_SCHEMA = { type: 'object', properties: {}, required: [] };
49
+
50
+ // Pull a callable executor off the WebMCP tool def under any of the names
51
+ // the spec/implementations currently use. Returning null lets the caller
52
+ // emit a clearer error than "undefined is not a function".
53
+ function pickExecutor(toolDef) {
54
+ return (
55
+ toolDef.execute ||
56
+ toolDef.handler ||
57
+ toolDef.run ||
58
+ null
59
+ );
60
+ }
61
+
62
+ // Some WebMCP impls hand the input through under `input`, others as a
63
+ // positional argument. The runtime always calls handler(input, ctx), so
64
+ // pass input through positionally and trust the executor's own shape.
65
+ function makeRuntimeHandler(toolDef) {
66
+ const exec = pickExecutor(toolDef);
67
+ if (!exec) {
68
+ return async () => ({
69
+ error: `[webmcp] tool "${toolDef.name}" has no execute()/handler()`,
70
+ });
71
+ }
72
+ return async (input, ctx) => {
73
+ // ctx.signal exists on runtime tools but WebMCP execute() may not
74
+ // expect a signal in its signature — pass through only if the
75
+ // executor declares ≥2 parameters. Conservative: forward signal as
76
+ // a sibling property on the input where possible.
77
+ try {
78
+ return await exec(input, ctx);
79
+ } catch (err) {
80
+ return { error: String(err?.message || err) };
81
+ }
82
+ };
83
+ }
84
+
85
+ function toRuntimeToolDef(toolDef) {
86
+ return {
87
+ name: toolDef.name,
88
+ description: toolDef.description || '',
89
+ schema: toolDef.inputSchema || toolDef.schema || DEFAULT_SCHEMA,
90
+ handler: makeRuntimeHandler(toolDef),
91
+ };
92
+ }
93
+
94
+ // Try to read the current WebMCP tool registry without committing to a
95
+ // single shape — different impls expose this differently.
96
+ function snapshotExisting(mc) {
97
+ if (Array.isArray(mc.tools)) return [...mc.tools];
98
+ if (typeof mc.getTools === 'function') {
99
+ try {
100
+ const t = mc.getTools();
101
+ if (Array.isArray(t)) return t;
102
+ } catch { /* swallow */ }
103
+ }
104
+ // No introspection available — best we can do is empty. Subsequent
105
+ // registerTool calls will be caught by the patch.
106
+ return [];
107
+ }
108
+
109
+ export function attachWebmcp(runtime, options = {}) {
110
+ if (!runtime || typeof runtime.registerTool !== 'function') {
111
+ throw new Error('[webmcp] attachWebmcp(runtime): runtime must expose registerTool/unregisterTool (createRuntime() handle).');
112
+ }
113
+ if (typeof navigator === 'undefined' || !navigator.modelContext) {
114
+ throw new Error('[webmcp] navigator.modelContext is not available. Requires a supporting browser (Chrome 146+ with the WebMCP flag, or a polyfill such as hatch).');
115
+ }
116
+
117
+ const {
118
+ autoRegisterExisting = true,
119
+ namePrefix = '',
120
+ onWire = null,
121
+ onUnwire = null,
122
+ } = options;
123
+
124
+ const mc = navigator.modelContext;
125
+ // wired: webmcp tool name (as seen by mc) → runtime tool name (post-prefix).
126
+ const wired = new Map();
127
+ const originalRegister = typeof mc.registerTool === 'function' ? mc.registerTool.bind(mc) : null;
128
+ const originalUnregister = typeof mc.unregisterTool === 'function' ? mc.unregisterTool.bind(mc) : null;
129
+ let patched = false;
130
+ let stopped = false;
131
+
132
+ function runtimeName(webmcpName) {
133
+ return namePrefix ? `${namePrefix}${webmcpName}` : webmcpName;
134
+ }
135
+
136
+ function wireOne(toolDef) {
137
+ if (!toolDef || !toolDef.name) return;
138
+ if (wired.has(toolDef.name)) return; // already mirrored
139
+ const rtDef = toRuntimeToolDef(toolDef);
140
+ rtDef.name = runtimeName(toolDef.name);
141
+ try {
142
+ runtime.registerTool(rtDef);
143
+ wired.set(toolDef.name, rtDef.name);
144
+ onWire?.(rtDef.name, toolDef);
145
+ } catch (err) {
146
+ // Surface but don't throw — one bad tool shouldn't break the rest.
147
+ console.warn(`[webmcp] could not mirror tool "${toolDef.name}":`, err?.message || err);
148
+ }
149
+ }
150
+
151
+ function unwireOne(webmcpName) {
152
+ const rtName = wired.get(webmcpName);
153
+ if (!rtName) return;
154
+ try { runtime.unregisterTool(rtName); } catch {}
155
+ wired.delete(webmcpName);
156
+ onUnwire?.(rtName, webmcpName);
157
+ }
158
+
159
+ function patchModelContext() {
160
+ if (patched || !originalRegister) return;
161
+ mc.registerTool = function patchedRegisterTool(toolDef) {
162
+ const result = originalRegister(toolDef);
163
+ try { wireOne(toolDef); } catch (err) {
164
+ console.warn('[webmcp] wireOne failed:', err?.message || err);
165
+ }
166
+ return result;
167
+ };
168
+ if (originalUnregister) {
169
+ mc.unregisterTool = function patchedUnregisterTool(name) {
170
+ const result = originalUnregister(name);
171
+ try { unwireOne(name); } catch {}
172
+ return result;
173
+ };
174
+ }
175
+ patched = true;
176
+ }
177
+
178
+ function unpatchModelContext() {
179
+ if (!patched) return;
180
+ if (originalRegister) mc.registerTool = originalRegister;
181
+ if (originalUnregister) mc.unregisterTool = originalUnregister;
182
+ patched = false;
183
+ }
184
+
185
+ if (autoRegisterExisting) {
186
+ for (const t of snapshotExisting(mc)) wireOne(t);
187
+ }
188
+ patchModelContext();
189
+
190
+ function stop() {
191
+ if (stopped) return;
192
+ stopped = true;
193
+ unpatchModelContext();
194
+ for (const webmcpName of [...wired.keys()]) unwireOne(webmcpName);
195
+ }
196
+
197
+ return {
198
+ stop,
199
+ /** Re-snapshot — useful if the host added tools through a different
200
+ * realm or before the polyfill installed itself. Idempotent: tools
201
+ * already mirrored are skipped. */
202
+ refresh() {
203
+ if (stopped) return;
204
+ for (const t of snapshotExisting(mc)) wireOne(t);
205
+ },
206
+ /** Names actually mirrored into the runtime (post-prefix). */
207
+ get wired() { return [...wired.values()]; },
208
+ /** Original WebMCP-side names (pre-prefix). */
209
+ get webmcpNames() { return [...wired.keys()]; },
210
+ };
211
+ }