@miragon/mcp-toolkit-ui 2.7.0 → 2.8.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 +7 -4
- package/dist/app/adapt-data-widget.d.ts +1 -22
- package/dist/app/adapt-data-widget.d.ts.map +1 -1
- package/dist/app/adapt-data-widget.js +1 -35
- package/dist/app/adapt-data-widget.js.map +1 -1
- package/dist/app/builder/BuilderBanners.js +1 -1
- package/dist/app/builder/BuilderBanners.js.map +1 -1
- package/dist/app/builder/CanvasRow.d.ts +4 -2
- package/dist/app/builder/CanvasRow.d.ts.map +1 -1
- package/dist/app/builder/CanvasRow.js +21 -7
- package/dist/app/builder/CanvasRow.js.map +1 -1
- package/dist/app/builder/CatalogueSheet.d.ts.map +1 -1
- package/dist/app/builder/CatalogueSheet.js +11 -4
- package/dist/app/builder/CatalogueSheet.js.map +1 -1
- package/dist/app/builder/CellPropsSheet.d.ts +7 -3
- package/dist/app/builder/CellPropsSheet.d.ts.map +1 -1
- package/dist/app/builder/CellPropsSheet.js +10 -3
- package/dist/app/builder/CellPropsSheet.js.map +1 -1
- package/dist/app/builder/DiscardDialog.d.ts +18 -0
- package/dist/app/builder/DiscardDialog.d.ts.map +1 -0
- package/dist/app/builder/DiscardDialog.js +18 -0
- package/dist/app/builder/DiscardDialog.js.map +1 -0
- package/dist/app/builder/LayoutSurface.d.ts +33 -9
- package/dist/app/builder/LayoutSurface.d.ts.map +1 -1
- package/dist/app/builder/LayoutSurface.js +74 -23
- package/dist/app/builder/LayoutSurface.js.map +1 -1
- package/dist/app/builder/PipelineStrip.d.ts +2 -1
- package/dist/app/builder/PipelineStrip.d.ts.map +1 -1
- package/dist/app/builder/PipelineStrip.js +26 -13
- package/dist/app/builder/PipelineStrip.js.map +1 -1
- package/dist/app/builder/SaveDialog.d.ts +9 -1
- package/dist/app/builder/SaveDialog.d.ts.map +1 -1
- package/dist/app/builder/SaveDialog.js +8 -2
- package/dist/app/builder/SaveDialog.js.map +1 -1
- package/dist/app/builder/Toolbar.d.ts +12 -4
- package/dist/app/builder/Toolbar.d.ts.map +1 -1
- package/dist/app/builder/Toolbar.js +18 -7
- package/dist/app/builder/Toolbar.js.map +1 -1
- package/dist/app/builder/Workspace.d.ts +23 -3
- package/dist/app/builder/Workspace.d.ts.map +1 -1
- package/dist/app/builder/Workspace.js +58 -8
- package/dist/app/builder/Workspace.js.map +1 -1
- package/dist/app/builder/builder-model.d.ts +143 -1
- package/dist/app/builder/builder-model.d.ts.map +1 -1
- package/dist/app/builder/builder-model.js +191 -1
- package/dist/app/builder/builder-model.js.map +1 -1
- package/dist/app/builder/reveal.d.ts +10 -0
- package/dist/app/builder/reveal.d.ts.map +1 -0
- package/dist/app/builder/reveal.js +10 -0
- package/dist/app/builder/reveal.js.map +1 -0
- package/dist/app/builder/use-builder-catalogue.d.ts +4 -1
- package/dist/app/builder/use-builder-catalogue.d.ts.map +1 -1
- package/dist/app/builder/use-builder-catalogue.js +44 -17
- package/dist/app/builder/use-builder-catalogue.js.map +1 -1
- package/dist/app/builder/use-builder-session.d.ts +30 -0
- package/dist/app/builder/use-builder-session.d.ts.map +1 -0
- package/dist/app/builder/use-builder-session.js +58 -0
- package/dist/app/builder/use-builder-session.js.map +1 -0
- package/dist/app/builder/use-display-resolver.d.ts +17 -0
- package/dist/app/builder/use-display-resolver.d.ts.map +1 -0
- package/dist/app/builder/use-display-resolver.js +16 -0
- package/dist/app/builder/use-display-resolver.js.map +1 -0
- package/dist/app/index.d.ts +8 -3
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +8 -2
- package/dist/app/index.js.map +1 -1
- package/dist/app/layout-builder.d.ts +13 -25
- package/dist/app/layout-builder.d.ts.map +1 -1
- package/dist/app/layout-builder.js +75 -81
- package/dist/app/layout-builder.js.map +1 -1
- package/dist/app/mcp-app-view-chrome.d.ts +12 -3
- package/dist/app/mcp-app-view-chrome.d.ts.map +1 -1
- package/dist/app/mcp-app-view-chrome.js +21 -5
- package/dist/app/mcp-app-view-chrome.js.map +1 -1
- package/dist/app/mcp-app-view-states.d.ts +11 -3
- package/dist/app/mcp-app-view-states.d.ts.map +1 -1
- package/dist/app/mcp-app-view-states.js +29 -8
- package/dist/app/mcp-app-view-states.js.map +1 -1
- package/dist/app/mcp-app-view.d.ts +33 -1
- package/dist/app/mcp-app-view.d.ts.map +1 -1
- package/dist/app/mcp-app-view.js +181 -127
- package/dist/app/mcp-app-view.js.map +1 -1
- package/dist/app/mcp-toolkit-app.d.ts +6 -1
- package/dist/app/mcp-toolkit-app.d.ts.map +1 -1
- package/dist/app/mcp-toolkit-app.js +8 -1
- package/dist/app/mcp-toolkit-app.js.map +1 -1
- package/dist/app/mcp-use-host-bridge.d.ts +15 -6
- package/dist/app/mcp-use-host-bridge.d.ts.map +1 -1
- package/dist/app/mcp-use-host-bridge.js +51 -22
- package/dist/app/mcp-use-host-bridge.js.map +1 -1
- package/dist/app/model-context.d.ts +71 -0
- package/dist/app/model-context.d.ts.map +1 -0
- package/dist/app/model-context.js +101 -0
- package/dist/app/model-context.js.map +1 -0
- package/dist/app/use-build-mode-focus.d.ts +33 -0
- package/dist/app/use-build-mode-focus.d.ts.map +1 -0
- package/dist/app/use-build-mode-focus.js +59 -0
- package/dist/app/use-build-mode-focus.js.map +1 -0
- package/dist/app/use-view-actions.d.ts +31 -0
- package/dist/app/use-view-actions.d.ts.map +1 -0
- package/dist/app/use-view-actions.js +81 -0
- package/dist/app/use-view-actions.js.map +1 -0
- package/dist/app/use-view-lifecycle.d.ts +4 -0
- package/dist/app/use-view-lifecycle.d.ts.map +1 -1
- package/dist/app/use-view-lifecycle.js +2 -0
- package/dist/app/use-view-lifecycle.js.map +1 -1
- package/dist/app/view-lifecycle.d.ts +80 -4
- package/dist/app/view-lifecycle.d.ts.map +1 -1
- package/dist/app/view-lifecycle.js +90 -3
- package/dist/app/view-lifecycle.js.map +1 -1
- package/dist/app/widget-fixture.d.ts +10 -3
- package/dist/app/widget-fixture.d.ts.map +1 -1
- package/dist/app/widget-fixture.js +21 -8
- package/dist/app/widget-fixture.js.map +1 -1
- package/dist/app/widget-props-sheet.d.ts +3 -1
- package/dist/app/widget-props-sheet.d.ts.map +1 -1
- package/dist/app/widget-props-sheet.js +22 -10
- package/dist/app/widget-props-sheet.js.map +1 -1
- package/dist/app/widget-renderer.js +1 -1
- package/dist/app/widget-renderer.js.map +1 -1
- package/dist/components/DrillButton.d.ts +9 -6
- package/dist/components/DrillButton.d.ts.map +1 -1
- package/dist/components/DrillButton.js +8 -6
- package/dist/components/DrillButton.js.map +1 -1
- package/dist/components/EmptyState.d.ts.map +1 -1
- package/dist/components/EmptyState.js +4 -2
- package/dist/components/EmptyState.js.map +1 -1
- package/dist/components/ErrorState.d.ts +5 -2
- package/dist/components/ErrorState.d.ts.map +1 -1
- package/dist/components/ErrorState.js +25 -8
- package/dist/components/ErrorState.js.map +1 -1
- package/dist/components/FilterBar.d.ts +15 -4
- package/dist/components/FilterBar.d.ts.map +1 -1
- package/dist/components/FilterBar.js +11 -6
- package/dist/components/FilterBar.js.map +1 -1
- package/dist/components/KpiGrid.d.ts +66 -21
- package/dist/components/KpiGrid.d.ts.map +1 -1
- package/dist/components/KpiGrid.js +101 -34
- package/dist/components/KpiGrid.js.map +1 -1
- package/dist/components/ListFooter.d.ts +31 -4
- package/dist/components/ListFooter.d.ts.map +1 -1
- package/dist/components/ListFooter.js +30 -11
- package/dist/components/ListFooter.js.map +1 -1
- package/dist/components/PagedListFooter.d.ts +57 -0
- package/dist/components/PagedListFooter.d.ts.map +1 -0
- package/dist/components/PagedListFooter.js +27 -0
- package/dist/components/PagedListFooter.js.map +1 -0
- package/dist/components/Pills.d.ts +27 -7
- package/dist/components/Pills.d.ts.map +1 -1
- package/dist/components/Pills.js +21 -6
- package/dist/components/Pills.js.map +1 -1
- package/dist/components/SectionHeading.d.ts +3 -1
- package/dist/components/SectionHeading.d.ts.map +1 -1
- package/dist/components/SectionHeading.js +6 -2
- package/dist/components/SectionHeading.js.map +1 -1
- package/dist/components/Toast.d.ts +9 -3
- package/dist/components/Toast.d.ts.map +1 -1
- package/dist/components/Toast.js +31 -16
- package/dist/components/Toast.js.map +1 -1
- package/dist/components/WidgetHeader.d.ts +34 -10
- package/dist/components/WidgetHeader.d.ts.map +1 -1
- package/dist/components/WidgetHeader.js +11 -6
- package/dist/components/WidgetHeader.js.map +1 -1
- package/dist/hooks/index.d.ts +4 -4
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/tool-result-recovery-model.d.ts +155 -0
- package/dist/hooks/tool-result-recovery-model.d.ts.map +1 -0
- package/dist/hooks/tool-result-recovery-model.js +273 -0
- package/dist/hooks/tool-result-recovery-model.js.map +1 -0
- package/dist/hooks/use-tool-query.d.ts +56 -14
- package/dist/hooks/use-tool-query.d.ts.map +1 -1
- package/dist/hooks/use-tool-query.js +102 -25
- package/dist/hooks/use-tool-query.js.map +1 -1
- package/dist/hooks/use-tool-result-recovery.d.ts +47 -52
- package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -1
- package/dist/hooks/use-tool-result-recovery.js +58 -120
- package/dist/hooks/use-tool-result-recovery.js.map +1 -1
- package/dist/hooks/use-view-data.d.ts +52 -10
- package/dist/hooks/use-view-data.d.ts.map +1 -1
- package/dist/hooks/use-view-data.js +81 -16
- package/dist/hooks/use-view-data.js.map +1 -1
- package/dist/hooks/use-view-tool-query.d.ts +12 -7
- package/dist/hooks/use-view-tool-query.d.ts.map +1 -1
- package/dist/hooks/use-view-tool-query.js +15 -14
- package/dist/hooks/use-view-tool-query.js.map +1 -1
- package/dist/index.d.ts +17 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -6
- package/dist/index.js.map +1 -1
- package/dist/lib/focus-return.d.ts +61 -0
- package/dist/lib/focus-return.d.ts.map +1 -0
- package/dist/lib/focus-return.js +46 -0
- package/dist/lib/focus-return.js.map +1 -0
- package/dist/lib/parse-tool-result.d.ts +23 -1
- package/dist/lib/parse-tool-result.d.ts.map +1 -1
- package/dist/lib/parse-tool-result.js +38 -2
- package/dist/lib/parse-tool-result.js.map +1 -1
- package/dist/lib/toast-model.d.ts +41 -1
- package/dist/lib/toast-model.d.ts.map +1 -1
- package/dist/lib/toast-model.js +67 -1
- package/dist/lib/toast-model.js.map +1 -1
- package/dist/lib/tone-utils.d.ts +58 -6
- package/dist/lib/tone-utils.d.ts.map +1 -1
- package/dist/lib/tone-utils.js +66 -4
- package/dist/lib/tone-utils.js.map +1 -1
- package/dist/lib/tool-query.d.ts +107 -0
- package/dist/lib/tool-query.d.ts.map +1 -0
- package/dist/lib/tool-query.js +182 -0
- package/dist/lib/tool-query.js.map +1 -0
- package/dist/lib/tool-result-observer.d.ts +108 -0
- package/dist/lib/tool-result-observer.d.ts.map +1 -0
- package/dist/lib/tool-result-observer.js +158 -0
- package/dist/lib/tool-result-observer.js.map +1 -0
- package/dist/lib/typography.d.ts +37 -0
- package/dist/lib/typography.d.ts.map +1 -0
- package/dist/lib/typography.js +41 -0
- package/dist/lib/typography.js.map +1 -0
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +11 -1
- package/dist/lib/utils.js.map +1 -1
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.d.ts.map +1 -1
- package/dist/locale/message-types.d.ts +117 -4
- package/dist/locale/message-types.d.ts.map +1 -1
- package/dist/locale/messages-de.d.ts.map +1 -1
- package/dist/locale/messages-de.js +44 -10
- package/dist/locale/messages-de.js.map +1 -1
- package/dist/locale/messages-en.d.ts.map +1 -1
- package/dist/locale/messages-en.js +44 -10
- package/dist/locale/messages-en.js.map +1 -1
- package/dist/locale/toolkit-messages.d.ts +19 -1
- package/dist/locale/toolkit-messages.d.ts.map +1 -1
- package/dist/locale/toolkit-messages.js +54 -1
- package/dist/locale/toolkit-messages.js.map +1 -1
- package/dist/primitives/alert.d.ts +1 -1
- package/dist/primitives/alert.d.ts.map +1 -1
- package/dist/primitives/alert.js +2 -0
- package/dist/primitives/alert.js.map +1 -1
- package/dist/primitives/badge.d.ts +1 -1
- package/dist/primitives/badge.d.ts.map +1 -1
- package/dist/primitives/badge.js +9 -4
- package/dist/primitives/badge.js.map +1 -1
- package/dist/primitives/button.d.ts +1 -1
- package/dist/primitives/button.js +1 -1
- package/dist/primitives/button.js.map +1 -1
- package/dist/primitives/dialog.d.ts +1 -1
- package/dist/primitives/dialog.d.ts.map +1 -1
- package/dist/primitives/dialog.js +10 -3
- package/dist/primitives/dialog.js.map +1 -1
- package/dist/primitives/select.js +1 -1
- package/dist/primitives/select.js.map +1 -1
- package/dist/primitives/sheet.d.ts +1 -1
- package/dist/primitives/sheet.d.ts.map +1 -1
- package/dist/primitives/sheet.js +6 -2
- package/dist/primitives/sheet.js.map +1 -1
- package/dist/providers/query-provider.d.ts +32 -1
- package/dist/providers/query-provider.d.ts.map +1 -1
- package/dist/providers/query-provider.js +53 -4
- package/dist/providers/query-provider.js.map +1 -1
- package/dist/render-cases-primitives.js +1 -1
- package/dist/render-cases-primitives.js.map +1 -1
- package/dist/render-cases.d.ts.map +1 -1
- package/dist/render-cases.js +30 -6
- package/dist/render-cases.js.map +1 -1
- package/dist/render-effects.d.ts +42 -0
- package/dist/render-effects.d.ts.map +1 -0
- package/dist/render-effects.js +81 -0
- package/dist/render-effects.js.map +1 -0
- package/dist/theme/create-theme.d.ts +19 -1
- package/dist/theme/create-theme.d.ts.map +1 -1
- package/dist/theme/create-theme.js +15 -0
- package/dist/theme/create-theme.js.map +1 -1
- package/package.json +5 -4
- package/src/app/adapt-data-widget.tsx +2 -42
- package/src/app/builder/BuilderBanners.tsx +1 -1
- package/src/app/builder/CanvasRow.tsx +63 -17
- package/src/app/builder/CatalogueSheet.tsx +50 -29
- package/src/app/builder/CellPropsSheet.tsx +17 -2
- package/src/app/builder/DiscardDialog.tsx +59 -0
- package/src/app/builder/LayoutSurface.tsx +206 -66
- package/src/app/builder/PipelineStrip.tsx +253 -153
- package/src/app/builder/SaveDialog.tsx +14 -2
- package/src/app/builder/Toolbar.tsx +144 -59
- package/src/app/builder/Workspace.tsx +226 -58
- package/src/app/builder/builder-model.ts +341 -2
- package/src/app/builder/builder-model.ux.test.ts +456 -0
- package/src/app/builder/builder-ux.test.tsx +844 -0
- package/src/app/builder/reveal.ts +10 -0
- package/src/app/builder/use-builder-catalogue.ts +56 -19
- package/src/app/builder/use-builder-session.ts +68 -0
- package/src/app/builder/use-display-resolver.ts +24 -0
- package/src/app/index.ts +12 -2
- package/src/app/layout-builder.tsx +178 -191
- package/src/app/mcp-app-view-chrome.test.tsx +141 -1
- package/src/app/mcp-app-view-chrome.tsx +56 -6
- package/src/app/mcp-app-view-states.tsx +51 -11
- package/src/app/mcp-app-view.effects.test.tsx +292 -0
- package/src/app/mcp-app-view.test.tsx +222 -2
- package/src/app/mcp-app-view.tsx +267 -117
- package/src/app/mcp-toolkit-app.test.tsx +40 -0
- package/src/app/mcp-toolkit-app.tsx +8 -1
- package/src/app/mcp-use-host-bridge.test.tsx +180 -0
- package/src/app/mcp-use-host-bridge.tsx +67 -24
- package/src/app/model-context.test.tsx +116 -0
- package/src/app/model-context.tsx +125 -0
- package/src/app/use-build-mode-focus.ts +62 -0
- package/src/app/use-view-actions.ts +89 -0
- package/src/app/use-view-lifecycle.ts +6 -0
- package/src/app/view-lifecycle.test.ts +216 -0
- package/src/app/view-lifecycle.ts +165 -5
- package/src/app/widget-fixture.effects.test.tsx +51 -0
- package/src/app/widget-fixture.render.test.tsx +81 -0
- package/src/app/widget-fixture.tsx +34 -11
- package/src/app/widget-props-sheet.tsx +57 -16
- package/src/app/widget-renderer.tsx +1 -1
- package/src/components/DrillButton.tsx +23 -10
- package/src/components/EmptyState.tsx +6 -4
- package/src/components/ErrorState.tsx +31 -9
- package/src/components/FilterBar.test.tsx +82 -0
- package/src/components/FilterBar.tsx +52 -26
- package/src/components/KpiGrid.test.tsx +279 -0
- package/src/components/KpiGrid.tsx +297 -90
- package/src/components/ListFooter.test.tsx +116 -0
- package/src/components/ListFooter.tsx +102 -15
- package/src/components/PagedListFooter.test.tsx +140 -0
- package/src/components/PagedListFooter.tsx +107 -0
- package/src/components/Pills.test.tsx +77 -0
- package/src/components/Pills.tsx +63 -10
- package/src/components/SectionHeading.tsx +9 -3
- package/src/components/Toast.tsx +37 -19
- package/src/components/WidgetHeader.test.tsx +78 -0
- package/src/components/WidgetHeader.tsx +63 -16
- package/src/components/feedback-states.test.tsx +98 -9
- package/src/components/tone-figures.test.tsx +8 -4
- package/src/hooks/index.ts +6 -3
- package/src/hooks/tool-result-recovery-model.test.ts +744 -0
- package/src/hooks/tool-result-recovery-model.ts +418 -0
- package/src/hooks/use-tool-query.effects.test.tsx +205 -0
- package/src/hooks/use-tool-query.ts +156 -33
- package/src/hooks/use-tool-result-recovery.effects.test.tsx +141 -0
- package/src/hooks/use-tool-result-recovery.test.ts +3 -4
- package/src/hooks/use-tool-result-recovery.ts +84 -142
- package/src/hooks/use-view-data.test.tsx +118 -9
- package/src/hooks/use-view-data.ts +128 -18
- package/src/hooks/use-view-tool-query.ts +25 -19
- package/src/i18n-coverage.test.tsx +104 -2
- package/src/index.ts +48 -11
- package/src/lib/focus-return.test.ts +288 -0
- package/src/lib/focus-return.ts +96 -0
- package/src/lib/parse-tool-result.test.ts +46 -1
- package/src/lib/parse-tool-result.ts +40 -2
- package/src/lib/toast-model.test.ts +166 -1
- package/src/lib/toast-model.ts +104 -2
- package/src/lib/tone-utils.test.ts +86 -10
- package/src/lib/tone-utils.ts +89 -8
- package/src/lib/tool-query.test.ts +569 -0
- package/src/lib/tool-query.ts +253 -0
- package/src/lib/tool-result-observer.test.ts +296 -0
- package/src/lib/tool-result-observer.ts +211 -0
- package/src/lib/typography.test.ts +45 -0
- package/src/lib/typography.ts +47 -0
- package/src/lib/utils.ts +12 -1
- package/src/locale/index.ts +1 -0
- package/src/locale/message-types.ts +118 -2
- package/src/locale/messages-de.ts +49 -10
- package/src/locale/messages-en.ts +49 -10
- package/src/locale/toolkit-messages.test.ts +133 -0
- package/src/locale/toolkit-messages.ts +63 -2
- package/src/primitives/alert.tsx +3 -0
- package/src/primitives/badge.tsx +9 -4
- package/src/primitives/button.tsx +1 -1
- package/src/primitives/dialog.tsx +10 -1
- package/src/primitives/select.tsx +1 -1
- package/src/primitives/sheet.tsx +7 -0
- package/src/providers/query-provider.test.tsx +154 -0
- package/src/providers/query-provider.tsx +66 -5
- package/src/render-cases-primitives.tsx +1 -1
- package/src/render-cases.tsx +38 -6
- package/src/render-effects.tsx +96 -0
- package/src/theme/cd-tokens.test.ts +3 -1
- package/src/theme/create-theme.test.ts +10 -0
- package/src/theme/create-theme.ts +28 -0
- package/src/theme/theme-css.test.ts +51 -0
- package/src/theme.css +31 -1
- package/ui-catalog.json +448 -41
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plain logic behind `useToolQuery`, `useViewToolQuery`, `useViewData`
|
|
3
|
+
* and `useToolMutation` (toolkit#176 K18): the effective cache key and its
|
|
4
|
+
* collision guard, the opt-in retry policy for tool errors, the TanStack
|
|
5
|
+
* options a tool query runs with, and per-call mutation callbacks that fire
|
|
6
|
+
* for EVERY call. No React, no `mcp-use/react`, so it is unit-tested without
|
|
7
|
+
* a renderer.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
hashKey,
|
|
11
|
+
type MutateOptions,
|
|
12
|
+
type MutationFunctionContext,
|
|
13
|
+
type QueryClient,
|
|
14
|
+
type UseQueryOptions,
|
|
15
|
+
} from "@tanstack/react-query"
|
|
16
|
+
import { isToolError, parseToolResult } from "./parse-tool-result.js"
|
|
17
|
+
|
|
18
|
+
/** A `(name, args)` tool caller — the shape of `useCallTool()` and `HostBridge.callTool`. */
|
|
19
|
+
export type ToolCaller = (name: string, args: object) => Promise<unknown>
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* TanStack query options a tool query passes through: everything except the
|
|
23
|
+
* key and the fetcher, which the hooks build from the tool and its args
|
|
24
|
+
* (`retry`, `retryDelay`, `staleTime`, `placeholderData` / `keepPreviousData`,
|
|
25
|
+
* `initialData` + `initialDataUpdatedAt`, `enabled`, `select`, …).
|
|
26
|
+
*/
|
|
27
|
+
export type UseToolQueryOptions<TData = unknown, TSelected = TData> = Omit<
|
|
28
|
+
UseQueryOptions<TData, Error, TSelected>,
|
|
29
|
+
"queryKey" | "queryFn"
|
|
30
|
+
>
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Neither an `AppQueryProvider` `callTool` nor a `HostBridgeProvider` is in
|
|
34
|
+
* reach, so the query cannot call any tool. Never retried: nothing changes
|
|
35
|
+
* until the mount does.
|
|
36
|
+
*/
|
|
37
|
+
export class NoToolTransportError extends Error {
|
|
38
|
+
constructor(toolName: string) {
|
|
39
|
+
super(
|
|
40
|
+
`[mcp-toolkit] cannot call "${toolName}": no tool transport. Mount the widget under ` +
|
|
41
|
+
"AppQueryProvider with callTool, or a HostBridgeProvider.",
|
|
42
|
+
)
|
|
43
|
+
this.name = "NoToolTransportError"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Retries {@link shouldRetryToolCall} grants a failed transport (TanStack's own default count). */
|
|
48
|
+
export const TOOL_QUERY_RETRIES = 3
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A retry policy for tool queries: up to {@link TOOL_QUERY_RETRIES} retries
|
|
52
|
+
* for a failed transport, none for a tool's own `isError` answer
|
|
53
|
+
* (`isToolError`) or a missing transport — repeating a deterministic answer
|
|
54
|
+
* only repeats it and holds the error back about 7 s.
|
|
55
|
+
*
|
|
56
|
+
* Opt-in in 2.x (the hooks keep the client's `retry`, TanStack's 3 by
|
|
57
|
+
* default, because a tool may also answer a transient failure with
|
|
58
|
+
* `isError`): pass `retry: shouldRetryToolCall` to a hook, or set it as the
|
|
59
|
+
* client's default (`queryClient.setDefaultOptions`, or an `AppQueryProvider`
|
|
60
|
+
* `client` of your own).
|
|
61
|
+
*/
|
|
62
|
+
export function shouldRetryToolCall(failureCount: number, error: unknown): boolean {
|
|
63
|
+
if (isToolError(error) || error instanceof NoToolTransportError) return false
|
|
64
|
+
return failureCount < TOOL_QUERY_RETRIES
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The effective cache key of a tool query (toolkit#176 K18).
|
|
69
|
+
*
|
|
70
|
+
* - 2.x default: the caller's key, then the args (`[...queryKey, args]`), so
|
|
71
|
+
* two callers sharing a key with different args get separate entries.
|
|
72
|
+
* - `toolNameInKey`: the caller's key, the tool name, then the args
|
|
73
|
+
* (`[...queryKey, toolName, args]`), so two tools never share an entry,
|
|
74
|
+
* whatever key their callers pass. Opt-in in 2.x (`AppQueryProvider` /
|
|
75
|
+
* `McpAppView` `toolNameInQueryKey`), the default from the next major.
|
|
76
|
+
*
|
|
77
|
+
* Build exact keys (`getQueryData`, `setQueryData`, `cancelQueries` with
|
|
78
|
+
* `exact: true`) with this helper, or with `useToolQueryKey`, which reads the
|
|
79
|
+
* provider's setting, rather than by hand: code that goes through them keeps
|
|
80
|
+
* working when the default changes. Prefix invalidation by `queryKey`
|
|
81
|
+
* matches either way.
|
|
82
|
+
*/
|
|
83
|
+
export function toolQueryKey(
|
|
84
|
+
queryKey: readonly unknown[],
|
|
85
|
+
toolName: string,
|
|
86
|
+
args: object,
|
|
87
|
+
toolNameInKey = false,
|
|
88
|
+
): unknown[] {
|
|
89
|
+
return toolNameInKey ? [...queryKey, toolName, args] : [...queryKey, args]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The tool that last claimed each cache key, per client — the guard against
|
|
94
|
+
* two tools sharing one entry (their answers would overwrite each other).
|
|
95
|
+
*/
|
|
96
|
+
const keyOwners = new WeakMap<QueryClient, Map<string, string>>()
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Claim the cache key `keyId` (`hashKey` of a {@link toolQueryKey}) for
|
|
100
|
+
* `toolName` on `client`. Returns the tool that held it
|
|
101
|
+
* before when that is ANOTHER tool — a collision the hooks report — else
|
|
102
|
+
* `null`. The latest claim wins, so each collision is reported once per
|
|
103
|
+
* switch.
|
|
104
|
+
*/
|
|
105
|
+
export function claimToolQueryKey(
|
|
106
|
+
client: QueryClient,
|
|
107
|
+
keyId: string,
|
|
108
|
+
toolName: string,
|
|
109
|
+
): string | null {
|
|
110
|
+
const owners = keyOwners.get(client) ?? new Map<string, string>()
|
|
111
|
+
keyOwners.set(client, owners)
|
|
112
|
+
const previous = owners.get(keyId)
|
|
113
|
+
owners.set(keyId, toolName)
|
|
114
|
+
return previous !== undefined && previous !== toolName ? previous : null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The console line for a {@link claimToolQueryKey} collision. */
|
|
118
|
+
export function toolKeyCollisionMessage(keyId: string, toolName: string, previous: string): string {
|
|
119
|
+
return (
|
|
120
|
+
`[mcp-toolkit] the query key ${keyId} is used by "${previous}" and "${toolName}": ` +
|
|
121
|
+
"both share one cache entry and overwrite each other's answer. Give each tool its own queryKey."
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** How {@link toolQueryOptions} decodes a result and builds the key. */
|
|
126
|
+
export interface ToolQueryBuild<TData> {
|
|
127
|
+
/** Decodes the tool result. Default: `parseToolResult` (structured-first). */
|
|
128
|
+
decode?: (result: unknown) => TData
|
|
129
|
+
/** Put the tool name into the key (see {@link toolQueryKey}). Default `false`. */
|
|
130
|
+
toolNameInKey?: boolean
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The TanStack options of a tool query: the caller's options, the
|
|
135
|
+
* {@link toolQueryKey} and a fetcher that calls `toolName` and decodes the
|
|
136
|
+
* result. `retry` stays the caller's, else the client's default; only a
|
|
137
|
+
* missing transport is never retried. Without a transport the fetcher fails
|
|
138
|
+
* with {@link NoToolTransportError} instead of leaving the query idle.
|
|
139
|
+
*/
|
|
140
|
+
export function toolQueryOptions<TData, TSelected = TData>(
|
|
141
|
+
queryKey: readonly unknown[],
|
|
142
|
+
toolName: string,
|
|
143
|
+
args: object,
|
|
144
|
+
callTool: ToolCaller | undefined,
|
|
145
|
+
opts?: UseToolQueryOptions<TData, TSelected>,
|
|
146
|
+
build: ToolQueryBuild<TData> = {},
|
|
147
|
+
): UseQueryOptions<TData, Error, TSelected> {
|
|
148
|
+
const decode = build.decode ?? ((result: unknown) => parseToolResult<TData>(result))
|
|
149
|
+
return {
|
|
150
|
+
...opts,
|
|
151
|
+
// An omitted `retry` must stay omitted: an explicit `undefined` would
|
|
152
|
+
// shadow the client's default.
|
|
153
|
+
...(callTool ? null : { retry: false }),
|
|
154
|
+
queryKey: toolQueryKey(queryKey, toolName, args, build.toolNameInKey),
|
|
155
|
+
queryFn: async () => {
|
|
156
|
+
if (!callTool) throw new NoToolTransportError(toolName)
|
|
157
|
+
return decode(await callTool(toolName, args))
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The content of the payload last seeded per query key, per client: a
|
|
164
|
+
* remounted or re-rendered OLD payload must not roll a refetched answer
|
|
165
|
+
* back, while a payload not seeded before is a new delivery and wins.
|
|
166
|
+
*/
|
|
167
|
+
const lastSeeded = new WeakMap<QueryClient, Map<string, string>>()
|
|
168
|
+
|
|
169
|
+
function contentOf(value: unknown): string | undefined {
|
|
170
|
+
try {
|
|
171
|
+
return JSON.stringify(value)
|
|
172
|
+
} catch {
|
|
173
|
+
return undefined
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Seed `queryKey` with host-delivered `seed`: written when the entry is
|
|
179
|
+
* empty, or when the seed is a payload this client has not seeded for the
|
|
180
|
+
* key before (a new delivery). Returns whether the cache was written.
|
|
181
|
+
*/
|
|
182
|
+
export function seedToolQuery(
|
|
183
|
+
client: QueryClient,
|
|
184
|
+
queryKey: readonly unknown[],
|
|
185
|
+
seed: unknown,
|
|
186
|
+
updatedAt?: number,
|
|
187
|
+
): boolean {
|
|
188
|
+
const keyId = hashKey(queryKey)
|
|
189
|
+
const seeded = lastSeeded.get(client) ?? new Map<string, string>()
|
|
190
|
+
lastSeeded.set(client, seeded)
|
|
191
|
+
const content = contentOf(seed)
|
|
192
|
+
const delivered = content === undefined || seeded.get(keyId) !== content
|
|
193
|
+
if (content !== undefined) seeded.set(keyId, content)
|
|
194
|
+
if (!delivered && client.getQueryData(queryKey) !== undefined) return false
|
|
195
|
+
client.setQueryData(queryKey, seed, updatedAt === undefined ? undefined : { updatedAt })
|
|
196
|
+
return true
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Callbacks of one `mutate` call, typed as TanStack's per-call options. */
|
|
200
|
+
export type PerCallCallbacks<TData, TVariables> = MutateOptions<TData, Error, TVariables, unknown>
|
|
201
|
+
|
|
202
|
+
function settle(run: () => void): void {
|
|
203
|
+
try {
|
|
204
|
+
run()
|
|
205
|
+
} catch (err) {
|
|
206
|
+
// Surface a throwing callback as an unhandled rejection, as TanStack
|
|
207
|
+
// does, without skipping the callbacks after it.
|
|
208
|
+
void Promise.reject(err instanceof Error ? err : new Error(String(err)))
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Fire one call's per-call `onSuccess` / `onError` / `onSettled` once `call`
|
|
214
|
+
* settles — for EVERY call. TanStack's own `mutate` keeps only the latest
|
|
215
|
+
* call's callbacks, so two quick calls (retrying two incidents in a row)
|
|
216
|
+
* would confirm only the second. `context()` is read when the call settles:
|
|
217
|
+
* `null` (the component unmounted) skips the callbacks, as TanStack does.
|
|
218
|
+
* A rejected `call` is handled here, never left unhandled.
|
|
219
|
+
*/
|
|
220
|
+
export function settleEachCall<TData, TVariables>(
|
|
221
|
+
call: Promise<TData>,
|
|
222
|
+
variables: TVariables,
|
|
223
|
+
callbacks: PerCallCallbacks<TData, TVariables> | undefined,
|
|
224
|
+
context: () => MutationFunctionContext | null,
|
|
225
|
+
): void {
|
|
226
|
+
call.then(
|
|
227
|
+
(data) => {
|
|
228
|
+
const ctx = callbacks ? context() : null
|
|
229
|
+
if (!callbacks || !ctx) return
|
|
230
|
+
settle(() => callbacks.onSuccess?.(data, variables, undefined, ctx))
|
|
231
|
+
settle(() => callbacks.onSettled?.(data, null, variables, undefined, ctx))
|
|
232
|
+
},
|
|
233
|
+
(error: Error) => {
|
|
234
|
+
const ctx = callbacks ? context() : null
|
|
235
|
+
if (!callbacks || !ctx) return
|
|
236
|
+
settle(() => callbacks.onError?.(error, variables, undefined, ctx))
|
|
237
|
+
settle(() => callbacks.onSettled?.(undefined, error, variables, undefined, ctx))
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Invalidate the query keys (prefixes) a mutation names: matching ACTIVE
|
|
244
|
+
* queries refetch now, inactive ones are only marked stale and refetch when
|
|
245
|
+
* they are next used — never a refetch of every inactive cached page.
|
|
246
|
+
*/
|
|
247
|
+
export async function invalidateToolQueries(
|
|
248
|
+
client: QueryClient,
|
|
249
|
+
keys: readonly (readonly unknown[])[] | undefined,
|
|
250
|
+
): Promise<void> {
|
|
251
|
+
if (!keys) return
|
|
252
|
+
await Promise.all(keys.map((queryKey) => client.invalidateQueries({ queryKey })))
|
|
253
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
classifyHostMessage,
|
|
4
|
+
classifyToolResult,
|
|
5
|
+
createToolResultObserver,
|
|
6
|
+
EMPTY_TOOL_OBSERVATION,
|
|
7
|
+
installToolResultObserver,
|
|
8
|
+
reduceToolObservation,
|
|
9
|
+
type HostLifecycleMessage,
|
|
10
|
+
type ToolObservation,
|
|
11
|
+
} from "./tool-result-observer.js"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The passive observer behind the stripped-result recovery (toolkit#176 K13,
|
|
15
|
+
* N21): it must tell a stripped result from a structured one and from "not
|
|
16
|
+
* yet", read only the COMPLETE tool input, and never act on foreign traffic.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const rpc = (method: string, params?: Record<string, unknown>) => ({
|
|
20
|
+
jsonrpc: "2.0",
|
|
21
|
+
method,
|
|
22
|
+
...(params ? { params } : {}),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
/** The host's answer to `ui/initialize`, as ext-apps sends it. */
|
|
26
|
+
const HANDSHAKE = {
|
|
27
|
+
jsonrpc: "2.0",
|
|
28
|
+
id: 1,
|
|
29
|
+
result: {
|
|
30
|
+
protocolVersion: "2026-01-26",
|
|
31
|
+
hostInfo: { name: "host", version: "1" },
|
|
32
|
+
hostCapabilities: { serverTools: {} },
|
|
33
|
+
hostContext: { toolInfo: { tool: { name: "render-view" } } },
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("classifyToolResult (mirrors the mcp-use 2.x latch)", () => {
|
|
38
|
+
it("an isError result is an error, whatever else it carries", () => {
|
|
39
|
+
expect(classifyToolResult({ isError: true })).toBe("error")
|
|
40
|
+
expect(classifyToolResult({ isError: true, structuredContent: { a: 1 } })).toBe("error")
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it("a result with structuredContent — even null — is structured", () => {
|
|
44
|
+
expect(classifyToolResult({ structuredContent: { a: 1 } })).toBe("structured")
|
|
45
|
+
expect(classifyToolResult({ structuredContent: null })).toBe("structured")
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("a content-only result is the stripped one claude.ai sends", () => {
|
|
49
|
+
expect(classifyToolResult({ content: [{ type: "text", text: "Rendered." }] })).toBe("stripped")
|
|
50
|
+
expect(classifyToolResult({})).toBe("stripped")
|
|
51
|
+
expect(classifyToolResult({ isError: false })).toBe("stripped")
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe("classifyHostMessage", () => {
|
|
56
|
+
it("recognises the host's ui/initialize answer by its capabilities or protocol version", () => {
|
|
57
|
+
expect(classifyHostMessage(HANDSHAKE)).toEqual({ kind: "handshake" })
|
|
58
|
+
expect(
|
|
59
|
+
classifyHostMessage({ jsonrpc: "2.0", id: 7, result: { protocolVersion: "x" } }),
|
|
60
|
+
).toEqual({ kind: "handshake" })
|
|
61
|
+
expect(
|
|
62
|
+
classifyHostMessage({ jsonrpc: "2.0", id: 7, result: { hostCapabilities: {} } }),
|
|
63
|
+
).toEqual({ kind: "handshake" })
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it("ignores every other response (a tools/call answer, an error, a bare result)", () => {
|
|
67
|
+
expect(
|
|
68
|
+
classifyHostMessage({ jsonrpc: "2.0", id: 2, result: { structuredContent: {} } }),
|
|
69
|
+
).toBeNull()
|
|
70
|
+
expect(classifyHostMessage({ jsonrpc: "2.0", id: 2, error: { code: 1 } })).toBeNull()
|
|
71
|
+
expect(classifyHostMessage({ jsonrpc: "2.0", id: 2, result: "x" })).toBeNull()
|
|
72
|
+
expect(
|
|
73
|
+
classifyHostMessage({ jsonrpc: "2.0", result: { protocolVersion: "x" } }),
|
|
74
|
+
"a response needs an id",
|
|
75
|
+
).toBeNull()
|
|
76
|
+
expect(
|
|
77
|
+
classifyHostMessage({ jsonrpc: "2.0", id: 3, result: { protocolVersion: 1 } }),
|
|
78
|
+
).toBeNull()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("reads the complete tool input, never the partial stream", () => {
|
|
82
|
+
expect(
|
|
83
|
+
classifyHostMessage(rpc("ui/notifications/tool-input", { arguments: { id: 1 } })),
|
|
84
|
+
).toEqual({ kind: "tool-input", arguments: { id: 1 } })
|
|
85
|
+
expect(
|
|
86
|
+
classifyHostMessage(rpc("ui/notifications/tool-input-partial", { arguments: { id: 1 } })),
|
|
87
|
+
).toBeNull()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("a tool input without usable arguments is an empty argument set", () => {
|
|
91
|
+
expect(classifyHostMessage(rpc("ui/notifications/tool-input"))).toEqual({
|
|
92
|
+
kind: "tool-input",
|
|
93
|
+
arguments: {},
|
|
94
|
+
})
|
|
95
|
+
expect(
|
|
96
|
+
classifyHostMessage(rpc("ui/notifications/tool-input", { arguments: ["not", "a", "map"] })),
|
|
97
|
+
).toEqual({ kind: "tool-input", arguments: {} })
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it("classifies the tool result and the cancellation", () => {
|
|
101
|
+
expect(classifyHostMessage(rpc("ui/notifications/tool-result", { content: [] }))).toEqual({
|
|
102
|
+
kind: "tool-result",
|
|
103
|
+
result: "stripped",
|
|
104
|
+
})
|
|
105
|
+
expect(
|
|
106
|
+
classifyHostMessage(rpc("ui/notifications/tool-result", { structuredContent: {} })),
|
|
107
|
+
).toEqual({ kind: "tool-result", result: "structured" })
|
|
108
|
+
expect(classifyHostMessage(rpc("ui/notifications/tool-result"))).toEqual({
|
|
109
|
+
kind: "tool-result",
|
|
110
|
+
result: "stripped",
|
|
111
|
+
})
|
|
112
|
+
expect(classifyHostMessage(rpc("ui/notifications/tool-cancelled"))).toEqual({
|
|
113
|
+
kind: "tool-cancelled",
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it("ignores foreign traffic and other methods", () => {
|
|
118
|
+
expect(classifyHostMessage(null)).toBeNull()
|
|
119
|
+
expect(classifyHostMessage("ui/notifications/tool-result")).toBeNull()
|
|
120
|
+
expect(classifyHostMessage([HANDSHAKE])).toBeNull()
|
|
121
|
+
expect(classifyHostMessage({ method: "ui/notifications/tool-result" })).toBeNull()
|
|
122
|
+
expect(classifyHostMessage({ ...HANDSHAKE, jsonrpc: "1.0" })).toBeNull()
|
|
123
|
+
expect(classifyHostMessage(rpc("ui/notifications/host-context-changed", {}))).toBeNull()
|
|
124
|
+
expect(classifyHostMessage(rpc("ui/resource-teardown", {}))).toBeNull()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe("reduceToolObservation", () => {
|
|
129
|
+
const fold = (...messages: HostLifecycleMessage[]): ToolObservation =>
|
|
130
|
+
messages.reduce(reduceToolObservation, EMPTY_TOOL_OBSERVATION)
|
|
131
|
+
|
|
132
|
+
it("starts empty", () => {
|
|
133
|
+
expect(EMPTY_TOOL_OBSERVATION).toEqual({
|
|
134
|
+
sawHandshake: false,
|
|
135
|
+
completeInput: undefined,
|
|
136
|
+
result: "none",
|
|
137
|
+
cancelled: false,
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it("records the handshake, the latest complete input and the result", () => {
|
|
142
|
+
expect(
|
|
143
|
+
fold(
|
|
144
|
+
{ kind: "handshake" },
|
|
145
|
+
{ kind: "tool-input", arguments: { a: 1 } },
|
|
146
|
+
{ kind: "tool-input", arguments: { a: 2 } },
|
|
147
|
+
{ kind: "tool-result", result: "stripped" },
|
|
148
|
+
),
|
|
149
|
+
).toEqual({ sawHandshake: true, completeInput: { a: 2 }, result: "stripped", cancelled: false })
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it("settles once, like the mcp-use latch: nothing changes a delivered call", () => {
|
|
153
|
+
const settled = fold(
|
|
154
|
+
{ kind: "tool-input", arguments: { a: 1 } },
|
|
155
|
+
{ kind: "tool-result", result: "structured" },
|
|
156
|
+
)
|
|
157
|
+
expect(reduceToolObservation(settled, { kind: "tool-input", arguments: { a: 9 } })).toBe(
|
|
158
|
+
settled,
|
|
159
|
+
)
|
|
160
|
+
expect(reduceToolObservation(settled, { kind: "tool-result", result: "stripped" })).toBe(
|
|
161
|
+
settled,
|
|
162
|
+
)
|
|
163
|
+
expect(reduceToolObservation(settled, { kind: "tool-cancelled" })).toBe(settled)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it("a cancellation settles the call too", () => {
|
|
167
|
+
const cancelled = fold({ kind: "tool-cancelled" })
|
|
168
|
+
expect(cancelled.cancelled).toBe(true)
|
|
169
|
+
expect(reduceToolObservation(cancelled, { kind: "tool-result", result: "stripped" })).toBe(
|
|
170
|
+
cancelled,
|
|
171
|
+
)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it("a repeated handshake is the same state; a late one still counts after a result", () => {
|
|
175
|
+
const shaken = fold({ kind: "handshake" })
|
|
176
|
+
expect(reduceToolObservation(shaken, { kind: "handshake" })).toBe(shaken)
|
|
177
|
+
const late = fold({ kind: "tool-result", result: "stripped" }, { kind: "handshake" })
|
|
178
|
+
expect(late).toMatchObject({ sawHandshake: true, result: "stripped" })
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
describe("createToolResultObserver", () => {
|
|
183
|
+
const post = (target: EventTarget, data: unknown, source: unknown = null) => {
|
|
184
|
+
const event = new MessageEvent("message", { data })
|
|
185
|
+
Object.defineProperty(event, "source", { value: source })
|
|
186
|
+
target.dispatchEvent(event)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
it("folds the messages it accepts and notifies only on a change", () => {
|
|
190
|
+
const target = new EventTarget()
|
|
191
|
+
const observer = createToolResultObserver(target)
|
|
192
|
+
const listener = vi.fn()
|
|
193
|
+
observer.subscribe(listener)
|
|
194
|
+
post(target, HANDSHAKE)
|
|
195
|
+
post(target, rpc("ui/notifications/tool-input", { arguments: { id: 7 } }))
|
|
196
|
+
post(target, rpc("ui/notifications/tool-input-partial", { arguments: { id: 8 } }))
|
|
197
|
+
post(target, { unrelated: true })
|
|
198
|
+
post(target, rpc("ui/notifications/tool-result", { content: [] }))
|
|
199
|
+
post(target, rpc("ui/notifications/tool-result", { structuredContent: {} }))
|
|
200
|
+
expect(observer.getSnapshot()).toEqual({
|
|
201
|
+
sawHandshake: true,
|
|
202
|
+
completeInput: { id: 7 },
|
|
203
|
+
result: "stripped",
|
|
204
|
+
cancelled: false,
|
|
205
|
+
})
|
|
206
|
+
expect(listener).toHaveBeenCalledTimes(3)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it("drops what the filter refuses (another frame's traffic)", () => {
|
|
210
|
+
const target = new EventTarget()
|
|
211
|
+
const host = { name: "host" }
|
|
212
|
+
const observer = createToolResultObserver(target, (event) => event.source === host)
|
|
213
|
+
post(target, HANDSHAKE, { name: "intruder" })
|
|
214
|
+
post(target, rpc("ui/notifications/tool-result", { content: [] }), null)
|
|
215
|
+
expect(observer.getSnapshot()).toBe(EMPTY_TOOL_OBSERVATION)
|
|
216
|
+
post(target, HANDSHAKE, host)
|
|
217
|
+
expect(observer.getSnapshot().sawHandshake).toBe(true)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it("unsubscribes a listener, and dispose stops listening but keeps the last snapshot", () => {
|
|
221
|
+
const target = new EventTarget()
|
|
222
|
+
const observer = createToolResultObserver(target)
|
|
223
|
+
const kept = vi.fn()
|
|
224
|
+
const dropped = vi.fn()
|
|
225
|
+
observer.subscribe(kept)
|
|
226
|
+
const unsubscribe = observer.subscribe(dropped)
|
|
227
|
+
unsubscribe()
|
|
228
|
+
post(target, HANDSHAKE)
|
|
229
|
+
expect(kept).toHaveBeenCalledTimes(1)
|
|
230
|
+
expect(dropped).not.toHaveBeenCalled()
|
|
231
|
+
observer.dispose()
|
|
232
|
+
post(target, rpc("ui/notifications/tool-result", { content: [] }))
|
|
233
|
+
expect(observer.getSnapshot()).toMatchObject({ sawHandshake: true, result: "none" })
|
|
234
|
+
expect(kept).toHaveBeenCalledTimes(1)
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
describe("installToolResultObserver", () => {
|
|
239
|
+
it("is null outside a framed browser document (no host to observe)", () => {
|
|
240
|
+
expect(installToolResultObserver()).toBeNull()
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it("is null in a top-level window (no host frames the view)", async () => {
|
|
244
|
+
vi.resetModules()
|
|
245
|
+
const target = new EventTarget()
|
|
246
|
+
vi.stubGlobal("window", Object.assign(target, { parent: target }))
|
|
247
|
+
try {
|
|
248
|
+
const fresh = await import("./tool-result-observer.js")
|
|
249
|
+
expect(fresh.installToolResultObserver()).toBeNull()
|
|
250
|
+
} finally {
|
|
251
|
+
vi.unstubAllGlobals()
|
|
252
|
+
vi.resetModules()
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it("listens from the moment the bundle evaluates, before any explicit install", async () => {
|
|
257
|
+
vi.resetModules()
|
|
258
|
+
const target = new EventTarget()
|
|
259
|
+
const parent = { name: "host" }
|
|
260
|
+
vi.stubGlobal("window", Object.assign(target, { parent }))
|
|
261
|
+
try {
|
|
262
|
+
const fresh = await import("./tool-result-observer.js")
|
|
263
|
+
post(target, HANDSHAKE, parent)
|
|
264
|
+
expect(fresh.installToolResultObserver()?.getSnapshot().sawHandshake).toBe(true)
|
|
265
|
+
} finally {
|
|
266
|
+
vi.unstubAllGlobals()
|
|
267
|
+
vi.resetModules()
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it("installs once in a framed document and listens to the parent only", async () => {
|
|
272
|
+
vi.resetModules()
|
|
273
|
+
const target = new EventTarget()
|
|
274
|
+
const parent = { name: "host" }
|
|
275
|
+
vi.stubGlobal("window", Object.assign(target, { parent }))
|
|
276
|
+
try {
|
|
277
|
+
const fresh = await import("./tool-result-observer.js")
|
|
278
|
+
const observer = fresh.installToolResultObserver()
|
|
279
|
+
expect(observer).not.toBeNull()
|
|
280
|
+
expect(fresh.installToolResultObserver(), "idempotent").toBe(observer)
|
|
281
|
+
post(target, HANDSHAKE, { name: "other" })
|
|
282
|
+
expect(observer?.getSnapshot().sawHandshake).toBe(false)
|
|
283
|
+
post(target, HANDSHAKE, parent)
|
|
284
|
+
expect(observer?.getSnapshot().sawHandshake).toBe(true)
|
|
285
|
+
} finally {
|
|
286
|
+
vi.unstubAllGlobals()
|
|
287
|
+
vi.resetModules()
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
function post(target: EventTarget, data: unknown, source: unknown) {
|
|
292
|
+
const event = new MessageEvent("message", { data })
|
|
293
|
+
Object.defineProperty(event, "source", { value: source })
|
|
294
|
+
target.dispatchEvent(event)
|
|
295
|
+
}
|
|
296
|
+
})
|