@nimblebrain/synapse 0.12.0 → 0.12.2
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 +13 -7
- package/dist/ui/index.cjs +88 -29
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +4 -4
- package/dist/ui/index.d.ts +4 -4
- package/dist/ui/index.js +88 -29
- package/dist/ui/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,10 @@ npm install @nimblebrain/synapse
|
|
|
37
37
|
|
|
38
38
|
**Peer dependency:** `@modelcontextprotocol/ext-apps@^1.3.1`
|
|
39
39
|
|
|
40
|
+
**Building a Python MCP server?** The server half — one self-contained `ui://`
|
|
41
|
+
component rendered across ChatGPT, Claude, and NimbleBrain — ships as the
|
|
42
|
+
[`nimblebrain-synapse`](python/README.md) PyPI package (`pip install nimblebrain-synapse`).
|
|
43
|
+
|
|
40
44
|
## Package Exports
|
|
41
45
|
|
|
42
46
|
| Entry Point | Description |
|
|
@@ -78,8 +82,10 @@ These are the durable decisions behind the library; they rarely change.
|
|
|
78
82
|
- **Responsive to the pane, not the device.** Layouts observe their own width
|
|
79
83
|
(`ResizeObserver` via `useBreakpoint`), because an app's iframe may be
|
|
80
84
|
fullscreen, split, or a narrow rail regardless of screen size.
|
|
81
|
-
- **
|
|
82
|
-
|
|
85
|
+
- **Sandbox-safe overlays.** `Drawer` is a plain `<div>` overlay, not a native
|
|
86
|
+
`<dialog>`: the app iframe withholds `allow-modals`, so `<dialog>.showModal()`
|
|
87
|
+
throws there. The scrim, Tab focus trap, focus-in/restore, scroll-lock, and
|
|
88
|
+
Escape are hand-rolled.
|
|
83
89
|
|
|
84
90
|
## Quick Start
|
|
85
91
|
|
|
@@ -113,7 +119,7 @@ app.updateModelContext(
|
|
|
113
119
|
### React
|
|
114
120
|
|
|
115
121
|
```tsx
|
|
116
|
-
import { AppProvider, useToolResult,
|
|
122
|
+
import { AppProvider, useToolResult, useResize } from "@nimblebrain/synapse/react";
|
|
117
123
|
|
|
118
124
|
function App() {
|
|
119
125
|
return (
|
|
@@ -125,7 +131,6 @@ function App() {
|
|
|
125
131
|
|
|
126
132
|
function ItemList() {
|
|
127
133
|
const result = useToolResult();
|
|
128
|
-
const { call, data, isPending } = useCallTool("list_items");
|
|
129
134
|
const resize = useResize();
|
|
130
135
|
|
|
131
136
|
useEffect(() => { if (result) resize(); }, [result, resize]);
|
|
@@ -336,7 +341,7 @@ await synapse.ready;
|
|
|
336
341
|
| `ready` | Promise that resolves after the ext-apps handshake |
|
|
337
342
|
| `isNimbleBrainHost` | Whether the host is a NimbleBrain platform |
|
|
338
343
|
| `callTool(name, args?)` | Call an MCP tool and get typed result |
|
|
339
|
-
| `callToolAsTask(name, args?, opts?)` | Call a long-running tool task-augmented; returns a `TaskHandle
|
|
344
|
+
| `callToolAsTask(name, args?, opts?)` | Call a long-running tool task-augmented; returns a `Promise<TaskHandle>`. See [Long-running tools](#long-running-tools-tasks) below. |
|
|
340
345
|
| `onDataChanged(cb)` | Subscribe to data change events |
|
|
341
346
|
| `onAction(cb)` | Subscribe to agent actions (typed, declarative) |
|
|
342
347
|
| `getTheme()` | Get current theme |
|
|
@@ -359,7 +364,7 @@ await synapse.ready;
|
|
|
359
364
|
Wrap your app with `<AppProvider>` and use these hooks. Each is a thin wrapper over `connect()`.
|
|
360
365
|
|
|
361
366
|
```tsx
|
|
362
|
-
import { AppProvider, useApp, useToolResult, useToolInput, useResize
|
|
367
|
+
import { AppProvider, useApp, useToolResult, useToolInput, useResize } from "@nimblebrain/synapse/react";
|
|
363
368
|
```
|
|
364
369
|
|
|
365
370
|
| Hook | Returns | Description |
|
|
@@ -369,7 +374,8 @@ import { AppProvider, useApp, useToolResult, useToolInput, useResize, useCallToo
|
|
|
369
374
|
| `useToolInput()` | `Record<string, unknown> \| null` | Re-renders on every `tool-input` event |
|
|
370
375
|
| `useConnectTheme()` | `Theme` | Reactive theme from `connect()` |
|
|
371
376
|
| `useResize()` | `(w?, h?) => void` | Resize helper — auto-measures body if no args |
|
|
372
|
-
|
|
377
|
+
|
|
378
|
+
To call a tool on this path, use the `App` instance: `useApp()`, then `app.callTool(name, args)`. The `useCallTool` hook (with built-in loading state) belongs to the `SynapseProvider` set below.
|
|
373
379
|
|
|
374
380
|
### `SynapseProvider`-based (Legacy)
|
|
375
381
|
|
package/dist/ui/index.cjs
CHANGED
|
@@ -307,21 +307,23 @@ function Card({
|
|
|
307
307
|
}
|
|
308
308
|
var STYLE_ID3 = "nb-synapse-drawer";
|
|
309
309
|
var RULES3 = `
|
|
310
|
+
.nb-drawer-scrim {
|
|
311
|
+
position: fixed; inset: 0; z-index: 1000;
|
|
312
|
+
display: flex; background: rgba(0, 0, 0, 0.32);
|
|
313
|
+
animation: nb-drawer-fade 200ms ease;
|
|
314
|
+
}
|
|
315
|
+
.nb-drawer-scrim--right { justify-content: flex-end; }
|
|
316
|
+
.nb-drawer-scrim--left { justify-content: flex-start; }
|
|
317
|
+
.nb-drawer-scrim--bottom { align-items: flex-end; }
|
|
310
318
|
.nb-drawer {
|
|
311
319
|
margin: 0; padding: 0; border: none; max-width: 92%;
|
|
312
320
|
height: 100%; max-height: 100%; display: flex; flex-direction: column;
|
|
321
|
+
outline: none;
|
|
313
322
|
}
|
|
314
|
-
.nb-drawer--
|
|
315
|
-
.nb-drawer--
|
|
316
|
-
.nb-drawer--
|
|
317
|
-
|
|
318
|
-
height: auto; max-height: 92%;
|
|
319
|
-
}
|
|
320
|
-
.nb-drawer--right[open] { animation: nb-drawer-in-right 240ms cubic-bezier(0.2, 0, 0, 1); }
|
|
321
|
-
.nb-drawer--left[open] { animation: nb-drawer-in-left 240ms cubic-bezier(0.2, 0, 0, 1); }
|
|
322
|
-
.nb-drawer--bottom[open] { animation: nb-drawer-in-bottom 240ms cubic-bezier(0.2, 0, 0, 1); }
|
|
323
|
-
.nb-drawer::backdrop { background: rgba(0, 0, 0, 0.32); }
|
|
324
|
-
.nb-drawer[open]::backdrop { animation: nb-drawer-fade 200ms ease; }
|
|
323
|
+
.nb-drawer--bottom { width: 100%; max-width: 100%; height: auto; max-height: 92%; }
|
|
324
|
+
.nb-drawer--right { animation: nb-drawer-in-right 240ms cubic-bezier(0.2, 0, 0, 1); }
|
|
325
|
+
.nb-drawer--left { animation: nb-drawer-in-left 240ms cubic-bezier(0.2, 0, 0, 1); }
|
|
326
|
+
.nb-drawer--bottom { animation: nb-drawer-in-bottom 240ms cubic-bezier(0.2, 0, 0, 1); }
|
|
325
327
|
@keyframes nb-drawer-in-right { from { transform: translateX(100%); } }
|
|
326
328
|
@keyframes nb-drawer-in-left { from { transform: translateX(-100%); } }
|
|
327
329
|
@keyframes nb-drawer-in-bottom { from { transform: translateY(100%); } }
|
|
@@ -331,6 +333,18 @@ var RULES3 = `
|
|
|
331
333
|
.nb-drawer-iconbtn { min-width: 44px; min-height: 44px; }
|
|
332
334
|
}
|
|
333
335
|
`;
|
|
336
|
+
function inClosedDetails(el) {
|
|
337
|
+
const details = el.closest("details:not([open])");
|
|
338
|
+
if (!details) return false;
|
|
339
|
+
const summary = details.querySelector("summary");
|
|
340
|
+
return !summary || !summary.contains(el);
|
|
341
|
+
}
|
|
342
|
+
var CANDIDATES = "a[href], button, input, select, textarea, summary, [tabindex]";
|
|
343
|
+
function tabbables(panel) {
|
|
344
|
+
return Array.from(panel.querySelectorAll(CANDIDATES)).filter(
|
|
345
|
+
(el) => el.tabIndex >= 0 && !el.matches(":disabled") && !el.closest("[hidden]") && !inClosedDetails(el) && el.type !== "hidden"
|
|
346
|
+
);
|
|
347
|
+
}
|
|
334
348
|
var DrawerContext = react.createContext(null);
|
|
335
349
|
function DrawerRoot({
|
|
336
350
|
open,
|
|
@@ -343,41 +357,86 @@ function DrawerRoot({
|
|
|
343
357
|
...rest
|
|
344
358
|
}) {
|
|
345
359
|
chunkRX6XCLFF_cjs.ensureStyle(STYLE_ID3, RULES3);
|
|
346
|
-
const
|
|
360
|
+
const panelRef = react.useRef(null);
|
|
347
361
|
const labelId = react.useId();
|
|
348
362
|
const [hasTitle, setHasTitle] = react.useState(false);
|
|
349
363
|
react.useEffect(() => {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
364
|
+
if (!open) return;
|
|
365
|
+
const onKey = (e) => {
|
|
366
|
+
if (e.key === "Escape") {
|
|
367
|
+
e.preventDefault();
|
|
368
|
+
(onEscape ?? onClose)();
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
window.addEventListener("keydown", onKey);
|
|
372
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
373
|
+
}, [open, onEscape, onClose]);
|
|
374
|
+
react.useEffect(() => {
|
|
375
|
+
if (!open) return;
|
|
376
|
+
const panel = panelRef.current;
|
|
377
|
+
const previouslyFocused = document.activeElement;
|
|
378
|
+
panel?.focus();
|
|
379
|
+
const { body } = document;
|
|
380
|
+
const prevOverflow = body.style.overflow;
|
|
381
|
+
body.style.overflow = "hidden";
|
|
382
|
+
const onKeyDown = (e) => {
|
|
383
|
+
if (e.key !== "Tab" || !panel) return;
|
|
384
|
+
const focusables = tabbables(panel);
|
|
385
|
+
if (focusables.length === 0) {
|
|
386
|
+
e.preventDefault();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const first = focusables[0];
|
|
390
|
+
const last = focusables[focusables.length - 1];
|
|
391
|
+
const active = document.activeElement;
|
|
392
|
+
if (e.shiftKey && (active === first || active === panel)) {
|
|
393
|
+
e.preventDefault();
|
|
394
|
+
last.focus();
|
|
395
|
+
} else if (!e.shiftKey && active === last) {
|
|
396
|
+
e.preventDefault();
|
|
397
|
+
first.focus();
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
panel?.addEventListener("keydown", onKeyDown);
|
|
401
|
+
return () => {
|
|
402
|
+
panel?.removeEventListener("keydown", onKeyDown);
|
|
403
|
+
body.style.overflow = prevOverflow;
|
|
404
|
+
previouslyFocused?.focus?.();
|
|
405
|
+
};
|
|
354
406
|
}, [open]);
|
|
407
|
+
const ctxValue = react.useMemo(() => ({ labelId, setHasTitle }), [labelId]);
|
|
408
|
+
if (!open) return null;
|
|
355
409
|
const isBottom = side === "bottom";
|
|
356
410
|
const panelStyle = {
|
|
357
411
|
...isBottom ? { width: "100%", maxWidth: "100%" } : { width },
|
|
358
412
|
background: tokens.bg,
|
|
359
413
|
color: tokens.fg,
|
|
360
414
|
boxShadow: tokens.shadowLg,
|
|
415
|
+
outline: "none",
|
|
361
416
|
...side === "right" ? { borderLeft: `${tokens.borderWidth} solid ${tokens.border}` } : side === "left" ? { borderRight: `${tokens.borderWidth} solid ${tokens.border}` } : { borderTop: `${tokens.borderWidth} solid ${tokens.border}` },
|
|
362
417
|
...style
|
|
363
418
|
};
|
|
364
|
-
const ctxValue = react.useMemo(() => ({ labelId, setHasTitle }), [labelId]);
|
|
365
419
|
return /* @__PURE__ */ jsxRuntime.jsx(DrawerContext.Provider, { value: ctxValue, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
366
|
-
"
|
|
420
|
+
"div",
|
|
367
421
|
{
|
|
368
|
-
|
|
369
|
-
className: `nb-drawer nb-drawer--${side}`,
|
|
370
|
-
style: panelStyle,
|
|
371
|
-
"aria-labelledby": hasTitle ? labelId : void 0,
|
|
372
|
-
onCancel: (e) => {
|
|
373
|
-
e.preventDefault();
|
|
374
|
-
(onEscape ?? onClose)();
|
|
375
|
-
},
|
|
422
|
+
className: `nb-drawer-scrim nb-drawer-scrim--${side}`,
|
|
376
423
|
onClick: (e) => {
|
|
377
|
-
if (e.target ===
|
|
424
|
+
if (e.target === e.currentTarget) onClose();
|
|
378
425
|
},
|
|
379
|
-
|
|
380
|
-
|
|
426
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
427
|
+
"div",
|
|
428
|
+
{
|
|
429
|
+
ref: panelRef,
|
|
430
|
+
role: "dialog",
|
|
431
|
+
"aria-modal": "true",
|
|
432
|
+
"aria-labelledby": hasTitle ? labelId : void 0,
|
|
433
|
+
tabIndex: -1,
|
|
434
|
+
className: `nb-drawer nb-drawer--${side}`,
|
|
435
|
+
style: panelStyle,
|
|
436
|
+
...rest,
|
|
437
|
+
children
|
|
438
|
+
}
|
|
439
|
+
)
|
|
381
440
|
}
|
|
382
441
|
) });
|
|
383
442
|
}
|