@nimblebrain/synapse 0.12.0-rc.0 → 0.12.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/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 |
@@ -113,7 +117,7 @@ app.updateModelContext(
113
117
  ### React
114
118
 
115
119
  ```tsx
116
- import { AppProvider, useToolResult, useCallTool, useResize } from "@nimblebrain/synapse/react";
120
+ import { AppProvider, useToolResult, useResize } from "@nimblebrain/synapse/react";
117
121
 
118
122
  function App() {
119
123
  return (
@@ -125,7 +129,6 @@ function App() {
125
129
 
126
130
  function ItemList() {
127
131
  const result = useToolResult();
128
- const { call, data, isPending } = useCallTool("list_items");
129
132
  const resize = useResize();
130
133
 
131
134
  useEffect(() => { if (result) resize(); }, [result, resize]);
@@ -336,7 +339,7 @@ await synapse.ready;
336
339
  | `ready` | Promise that resolves after the ext-apps handshake |
337
340
  | `isNimbleBrainHost` | Whether the host is a NimbleBrain platform |
338
341
  | `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` immediately. See [Long-running tools](#long-running-tools-tasks) below. |
342
+ | `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
343
  | `onDataChanged(cb)` | Subscribe to data change events |
341
344
  | `onAction(cb)` | Subscribe to agent actions (typed, declarative) |
342
345
  | `getTheme()` | Get current theme |
@@ -359,7 +362,7 @@ await synapse.ready;
359
362
  Wrap your app with `<AppProvider>` and use these hooks. Each is a thin wrapper over `connect()`.
360
363
 
361
364
  ```tsx
362
- import { AppProvider, useApp, useToolResult, useToolInput, useResize, useCallTool } from "@nimblebrain/synapse/react";
365
+ import { AppProvider, useApp, useToolResult, useToolInput, useResize } from "@nimblebrain/synapse/react";
363
366
  ```
364
367
 
365
368
  | Hook | Returns | Description |
@@ -369,7 +372,8 @@ import { AppProvider, useApp, useToolResult, useToolInput, useResize, useCallToo
369
372
  | `useToolInput()` | `Record<string, unknown> \| null` | Re-renders on every `tool-input` event |
370
373
  | `useConnectTheme()` | `Theme` | Reactive theme from `connect()` |
371
374
  | `useResize()` | `(w?, h?) => void` | Resize helper — auto-measures body if no args |
372
- | `useCallTool(name)` | `{ call, data, isPending, error }` | Call a tool with loading/error state |
375
+
376
+ 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
377
 
374
378
  ### `SynapseProvider`-based (Legacy)
375
379
 
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--right { margin-left: auto; }
315
- .nb-drawer--left { margin-right: auto; }
316
- .nb-drawer--bottom {
317
- margin-top: auto; width: 100%; max-width: 100%;
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%); } }
@@ -343,41 +345,65 @@ function DrawerRoot({
343
345
  ...rest
344
346
  }) {
345
347
  chunkRX6XCLFF_cjs.ensureStyle(STYLE_ID3, RULES3);
346
- const dialogRef = react.useRef(null);
348
+ const panelRef = react.useRef(null);
347
349
  const labelId = react.useId();
348
350
  const [hasTitle, setHasTitle] = react.useState(false);
349
351
  react.useEffect(() => {
350
- const dialog = dialogRef.current;
351
- if (!dialog || typeof dialog.showModal !== "function") return;
352
- if (open && !dialog.open) dialog.showModal();
353
- else if (!open && dialog.open) dialog.close();
352
+ if (!open) return;
353
+ const onKey = (e) => {
354
+ if (e.key === "Escape") {
355
+ e.preventDefault();
356
+ (onEscape ?? onClose)();
357
+ }
358
+ };
359
+ window.addEventListener("keydown", onKey);
360
+ return () => window.removeEventListener("keydown", onKey);
361
+ }, [open, onEscape, onClose]);
362
+ react.useEffect(() => {
363
+ if (!open) return;
364
+ const previouslyFocused = document.activeElement;
365
+ panelRef.current?.focus();
366
+ const { body } = document;
367
+ const prevOverflow = body.style.overflow;
368
+ body.style.overflow = "hidden";
369
+ return () => {
370
+ body.style.overflow = prevOverflow;
371
+ previouslyFocused?.focus?.();
372
+ };
354
373
  }, [open]);
374
+ const ctxValue = react.useMemo(() => ({ labelId, setHasTitle }), [labelId]);
375
+ if (!open) return null;
355
376
  const isBottom = side === "bottom";
356
377
  const panelStyle = {
357
378
  ...isBottom ? { width: "100%", maxWidth: "100%" } : { width },
358
379
  background: tokens.bg,
359
380
  color: tokens.fg,
360
381
  boxShadow: tokens.shadowLg,
382
+ outline: "none",
361
383
  ...side === "right" ? { borderLeft: `${tokens.borderWidth} solid ${tokens.border}` } : side === "left" ? { borderRight: `${tokens.borderWidth} solid ${tokens.border}` } : { borderTop: `${tokens.borderWidth} solid ${tokens.border}` },
362
384
  ...style
363
385
  };
364
- const ctxValue = react.useMemo(() => ({ labelId, setHasTitle }), [labelId]);
365
386
  return /* @__PURE__ */ jsxRuntime.jsx(DrawerContext.Provider, { value: ctxValue, children: /* @__PURE__ */ jsxRuntime.jsx(
366
- "dialog",
387
+ "div",
367
388
  {
368
- ref: dialogRef,
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
- },
389
+ className: `nb-drawer-scrim nb-drawer-scrim--${side}`,
376
390
  onClick: (e) => {
377
- if (e.target === dialogRef.current) onClose();
391
+ if (e.target === e.currentTarget) onClose();
378
392
  },
379
- ...rest,
380
- children
393
+ children: /* @__PURE__ */ jsxRuntime.jsx(
394
+ "div",
395
+ {
396
+ ref: panelRef,
397
+ role: "dialog",
398
+ "aria-modal": "true",
399
+ "aria-labelledby": hasTitle ? labelId : void 0,
400
+ tabIndex: -1,
401
+ className: `nb-drawer nb-drawer--${side}`,
402
+ style: panelStyle,
403
+ ...rest,
404
+ children
405
+ }
406
+ )
381
407
  }
382
408
  ) });
383
409
  }