@miragon/mcp-toolkit-ui 2.7.0 → 2.8.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 +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 +14 -6
- package/dist/components/DrillButton.d.ts.map +1 -1
- package/dist/components/DrillButton.js +14 -7
- 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.test.tsx +48 -0
- package/src/components/DrillButton.tsx +26 -11
- 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
|
@@ -1,63 +1,186 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react"
|
|
2
|
+
import {
|
|
3
|
+
hashKey,
|
|
4
|
+
useMutation,
|
|
5
|
+
useQuery,
|
|
6
|
+
useQueryClient,
|
|
7
|
+
type MutationFunctionContext,
|
|
8
|
+
type UseMutationOptions,
|
|
9
|
+
type UseMutationResult,
|
|
10
|
+
type UseQueryResult,
|
|
11
|
+
} from "@tanstack/react-query"
|
|
12
|
+
import { useCallTool, useToolNameInQueryKey } from "../providers/query-provider.js"
|
|
3
13
|
import { parseToolResult } from "../lib/parse-tool-result.js"
|
|
14
|
+
import {
|
|
15
|
+
claimToolQueryKey,
|
|
16
|
+
invalidateToolQueries,
|
|
17
|
+
NoToolTransportError,
|
|
18
|
+
settleEachCall,
|
|
19
|
+
toolKeyCollisionMessage,
|
|
20
|
+
toolQueryKey,
|
|
21
|
+
toolQueryOptions,
|
|
22
|
+
type PerCallCallbacks,
|
|
23
|
+
type UseToolQueryOptions,
|
|
24
|
+
} from "../lib/tool-query.js"
|
|
4
25
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
26
|
+
export type { UseToolQueryOptions } from "../lib/tool-query.js"
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The exact cache key `useToolQuery`, `useViewToolQuery` and `useViewData`
|
|
30
|
+
* use for this tool and args under the nearest `AppQueryProvider`:
|
|
31
|
+
* `toolQueryKey` with the provider's `toolNameInQueryKey` setting. For
|
|
32
|
+
* `getQueryData`, `setQueryData` or `cancelQueries({ exact: true })`; prefix
|
|
33
|
+
* invalidation needs only the caller's `queryKey`. A new array per render.
|
|
34
|
+
*/
|
|
35
|
+
export function useToolQueryKey(
|
|
36
|
+
queryKey: readonly unknown[],
|
|
37
|
+
toolName: string,
|
|
38
|
+
args: object,
|
|
39
|
+
): unknown[] {
|
|
40
|
+
return toolQueryKey(queryKey, toolName, args, useToolNameInQueryKey())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Warns when two tools use one cache key (toolkit#176 K18): their answers
|
|
45
|
+
* would overwrite each other. Only the 2.x key can collide: with the tool
|
|
46
|
+
* name in the key (`toolNameInQueryKey`), two tools never share one.
|
|
47
|
+
*/
|
|
48
|
+
export function useToolKeyGuard(
|
|
49
|
+
queryKey: readonly unknown[],
|
|
50
|
+
toolName: string,
|
|
51
|
+
args: object,
|
|
52
|
+
): void {
|
|
53
|
+
const client = useQueryClient()
|
|
54
|
+
const keyId = hashKey(useToolQueryKey(queryKey, toolName, args))
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const previous = claimToolQueryKey(client, keyId, toolName)
|
|
57
|
+
if (previous !== null) console.warn(toolKeyCollisionMessage(keyId, toolName, previous))
|
|
58
|
+
}, [client, keyId, toolName])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fetches a query again once a tool transport arrives, when it failed for
|
|
63
|
+
* want of one ({@link NoToolTransportError}): a `callTool` that resolves
|
|
64
|
+
* after the first render. TanStack refetches on a new key or `enabled`,
|
|
65
|
+
* never on a new fetcher, so the query would keep the error until a remount.
|
|
66
|
+
*/
|
|
67
|
+
export function useRefetchOnTransport(
|
|
68
|
+
callTool: unknown,
|
|
69
|
+
query: Pick<UseQueryResult<unknown, Error>, "error" | "refetch">,
|
|
70
|
+
): void {
|
|
71
|
+
const { error, refetch } = query
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (callTool && error instanceof NoToolTransportError) void refetch()
|
|
74
|
+
}, [callTool, error, refetch])
|
|
8
75
|
}
|
|
9
76
|
|
|
10
77
|
/**
|
|
11
78
|
* Loads data from a widget tool via TanStack Query.
|
|
12
79
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* entries
|
|
16
|
-
*
|
|
17
|
-
*
|
|
80
|
+
* The effective cache key is `[...queryKey, args]` (`toolQueryKey`): two
|
|
81
|
+
* callers that share `queryKey` but pass different `args` get separate
|
|
82
|
+
* entries. Two tools must not share a key and args; the hook warns when they
|
|
83
|
+
* do. Under an `AppQueryProvider` with `toolNameInQueryKey` (opt-in in 2.x,
|
|
84
|
+
* the default from the next major) the key is `[...queryKey, toolName,
|
|
85
|
+
* args]` and two tools never collide; `useToolQueryKey` returns the key in
|
|
86
|
+
* force. `opts` passes TanStack's query options through (`retry`,
|
|
87
|
+
* `retryDelay`, `staleTime`, `placeholderData` / `keepPreviousData`,
|
|
88
|
+
* `initialData` + `initialDataUpdatedAt`, `enabled`, `select`, …), and the
|
|
89
|
+
* result exposes `refetch`, `failureCount` and `failureReason`. `retry`
|
|
90
|
+
* defaults to the client's; `retry: shouldRetryToolCall` stops retrying a
|
|
91
|
+
* tool's own `isError` answer.
|
|
92
|
+
*
|
|
93
|
+
* The tool goes through `AppQueryProvider`'s `callTool`, else the nearest
|
|
94
|
+
* `HostBridgeProvider`'s bridge; with neither, the query fails with a
|
|
95
|
+
* "no tool transport" error instead of idling, and fetches once one arrives.
|
|
18
96
|
*/
|
|
19
97
|
export function useToolQuery<TData = unknown, TSelected = TData>(
|
|
20
|
-
queryKey: unknown[],
|
|
98
|
+
queryKey: readonly unknown[],
|
|
21
99
|
toolName: string,
|
|
22
100
|
args: object,
|
|
23
101
|
opts?: UseToolQueryOptions<TData, TSelected>,
|
|
24
|
-
) {
|
|
102
|
+
): UseQueryResult<TSelected, Error> {
|
|
25
103
|
const callTool = useCallTool()
|
|
104
|
+
const toolNameInKey = useToolNameInQueryKey()
|
|
105
|
+
useToolKeyGuard(queryKey, toolName, args)
|
|
106
|
+
const query = useQuery<TData, Error, TSelected>(
|
|
107
|
+
toolQueryOptions<TData, TSelected>(queryKey, toolName, args, callTool, opts, {
|
|
108
|
+
toolNameInKey,
|
|
109
|
+
}),
|
|
110
|
+
)
|
|
111
|
+
useRefetchOnTransport(callTool, query)
|
|
112
|
+
return query
|
|
113
|
+
}
|
|
26
114
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
select: opts?.select,
|
|
35
|
-
enabled: !!callTool && (opts?.enabled ?? true),
|
|
36
|
-
})
|
|
115
|
+
/** Options of {@link useToolMutation}: TanStack's mutation options plus `invalidateKeys`. */
|
|
116
|
+
export type UseToolMutationOptions<TData = unknown> = Omit<
|
|
117
|
+
UseMutationOptions<TData, Error, Record<string, unknown>, unknown>,
|
|
118
|
+
"mutationFn" | "onMutate"
|
|
119
|
+
> & {
|
|
120
|
+
/** Query keys (prefixes) to invalidate after a successful call. */
|
|
121
|
+
invalidateKeys?: unknown[][]
|
|
37
122
|
}
|
|
38
123
|
|
|
39
124
|
/**
|
|
40
|
-
* Executes a mutation via a widget tool and invalidates
|
|
125
|
+
* Executes a mutation via a widget tool and invalidates `invalidateKeys`
|
|
126
|
+
* after each successful call (`invalidateQueries`: active queries refetch,
|
|
127
|
+
* inactive ones are marked stale and refetch when they are next used).
|
|
128
|
+
*
|
|
129
|
+
* `mutate(args, { onSuccess, onError, onSettled })` fires its per-call
|
|
130
|
+
* callbacks for EVERY call while the component is mounted, not only for the
|
|
131
|
+
* latest one (TanStack's own rule), so two quick calls each get their
|
|
132
|
+
* confirmation. The hook-level callbacks in `opts` run first; the
|
|
133
|
+
* invalidation runs before the hook-level `onSuccess`.
|
|
41
134
|
*/
|
|
42
135
|
export function useToolMutation<TData = unknown>(
|
|
43
136
|
toolName: string,
|
|
44
|
-
opts?:
|
|
45
|
-
) {
|
|
137
|
+
opts?: UseToolMutationOptions<TData>,
|
|
138
|
+
): UseMutationResult<TData, Error, Record<string, unknown>> {
|
|
46
139
|
const callTool = useCallTool()
|
|
47
140
|
const queryClient = useQueryClient()
|
|
141
|
+
const { invalidateKeys, onSuccess, ...options } = opts ?? {}
|
|
48
142
|
|
|
49
|
-
|
|
143
|
+
const mutation = useMutation<TData, Error, Record<string, unknown>>({
|
|
144
|
+
...options,
|
|
50
145
|
mutationFn: async (args: Record<string, unknown>) => {
|
|
51
|
-
if (!callTool) throw new
|
|
146
|
+
if (!callTool) throw new NoToolTransportError(toolName)
|
|
52
147
|
const result = await callTool(toolName, args)
|
|
53
148
|
return parseToolResult<TData>(result)
|
|
54
149
|
},
|
|
55
|
-
onSuccess: async () => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
opts.invalidateKeys.map((key) => queryClient.refetchQueries({ queryKey: key })),
|
|
59
|
-
)
|
|
60
|
-
}
|
|
150
|
+
onSuccess: async (data, variables, onMutateResult, context) => {
|
|
151
|
+
await invalidateToolQueries(queryClient, invalidateKeys)
|
|
152
|
+
await onSuccess?.(data, variables, onMutateResult, context)
|
|
61
153
|
},
|
|
62
154
|
})
|
|
155
|
+
|
|
156
|
+
// Per-call callbacks fire only while mounted (TanStack's rule, kept), with
|
|
157
|
+
// the latest meta/mutationKey. Both refs are written in effects and read
|
|
158
|
+
// only when a call settles, so `mutate` stays stable across renders.
|
|
159
|
+
const mounted = useRef(false)
|
|
160
|
+
const scope = useRef<Pick<MutationFunctionContext, "meta" | "mutationKey">>({
|
|
161
|
+
meta: undefined,
|
|
162
|
+
mutationKey: undefined,
|
|
163
|
+
})
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
scope.current = { meta: options.meta, mutationKey: options.mutationKey }
|
|
166
|
+
})
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
mounted.current = true
|
|
169
|
+
return () => {
|
|
170
|
+
mounted.current = false
|
|
171
|
+
}
|
|
172
|
+
}, [])
|
|
173
|
+
const { mutateAsync } = mutation
|
|
174
|
+
const mutate = useCallback(
|
|
175
|
+
(
|
|
176
|
+
variables: Record<string, unknown>,
|
|
177
|
+
callbacks?: PerCallCallbacks<TData, Record<string, unknown>>,
|
|
178
|
+
) => {
|
|
179
|
+
settleEachCall(mutateAsync(variables), variables, callbacks, () =>
|
|
180
|
+
mounted.current ? { client: queryClient, ...scope.current } : null,
|
|
181
|
+
)
|
|
182
|
+
},
|
|
183
|
+
[mutateAsync, queryClient],
|
|
184
|
+
)
|
|
185
|
+
return { ...mutation, mutate }
|
|
63
186
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest"
|
|
3
|
+
import type { ToolResultObserver } from "../lib/tool-result-observer.js"
|
|
4
|
+
import { createToolResultObserver } from "../lib/tool-result-observer.js"
|
|
5
|
+
import { advanceTimers, hookProbe, inAct, renderWithEffects } from "../render-effects.js"
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_ASSUME_STRIPPED_AFTER_MS,
|
|
8
|
+
useToolResultRecovery,
|
|
9
|
+
type ToolResultRecoveryOptions,
|
|
10
|
+
} from "./use-tool-result-recovery.js"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The hook's wiring with its effects running (toolkit#176 K13): the default
|
|
14
|
+
* observer is the document's (`installToolResultObserver`), the controller
|
|
15
|
+
* subscribes to it on mount, and a stripped result re-runs the tool from the
|
|
16
|
+
* observer's notification alone, while a slow tool on a compliant host is
|
|
17
|
+
* never re-run. The decision itself is pinned in the recovery model's tests.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const installed = vi.hoisted(() => ({ observer: null as ToolResultObserver | null }))
|
|
21
|
+
|
|
22
|
+
// The document's observer, as a framed view would install it; the tests drive
|
|
23
|
+
// a fake host channel instead of window.parent.
|
|
24
|
+
vi.mock(import("../lib/tool-result-observer.js"), async (importOriginal) => {
|
|
25
|
+
const actual = await importOriginal()
|
|
26
|
+
return { ...actual, installToolResultObserver: () => installed.observer }
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
/** A host channel the observer listens on, with the SEP-1865 messages a host sends. */
|
|
30
|
+
function fakeHost() {
|
|
31
|
+
const channel = new EventTarget()
|
|
32
|
+
const observer = createToolResultObserver(channel)
|
|
33
|
+
const send = (data: unknown) =>
|
|
34
|
+
inAct(() => channel.dispatchEvent(new MessageEvent("message", { data })))
|
|
35
|
+
const notify = (method: string, params: Record<string, unknown>) =>
|
|
36
|
+
send({ jsonrpc: "2.0", method, params })
|
|
37
|
+
return {
|
|
38
|
+
observer,
|
|
39
|
+
handshake: () =>
|
|
40
|
+
send({
|
|
41
|
+
jsonrpc: "2.0",
|
|
42
|
+
id: 1,
|
|
43
|
+
result: { protocolVersion: "2026-01-26", hostCapabilities: {} },
|
|
44
|
+
}),
|
|
45
|
+
toolInput: (args: Record<string, unknown>) =>
|
|
46
|
+
notify("ui/notifications/tool-input", { arguments: args }),
|
|
47
|
+
strippedResult: () =>
|
|
48
|
+
notify("ui/notifications/tool-result", { content: [{ type: "text", text: "Rendered." }] }),
|
|
49
|
+
structuredResult: (structuredContent: unknown) =>
|
|
50
|
+
notify("ui/notifications/tool-result", { content: [], structuredContent }),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type CallTool = ToolResultRecoveryOptions["callTool"]
|
|
55
|
+
|
|
56
|
+
const PAYLOAD = { ok: true, rows: 3 }
|
|
57
|
+
const isValid = (value: unknown) => (value as { ok?: unknown } | null)?.ok === true
|
|
58
|
+
|
|
59
|
+
let callTool: Mock<CallTool>
|
|
60
|
+
|
|
61
|
+
/** Mount the hook with the view's inputs; the probe keeps its latest state. */
|
|
62
|
+
async function mount(over: Partial<ToolResultRecoveryOptions> = {}) {
|
|
63
|
+
const options: ToolResultRecoveryOptions = {
|
|
64
|
+
resultReady: false,
|
|
65
|
+
props: undefined,
|
|
66
|
+
isValid,
|
|
67
|
+
// The view runtime's input: a partial stream may have merged into it.
|
|
68
|
+
toolInput: { id: 7 },
|
|
69
|
+
hostContext: { toolInfo: { tool: { name: "show_list" } } },
|
|
70
|
+
callTool,
|
|
71
|
+
...over,
|
|
72
|
+
}
|
|
73
|
+
const probe = hookProbe(() => useToolResultRecovery<typeof PAYLOAD>(options))
|
|
74
|
+
const view = await renderWithEffects(<probe.Probe />)
|
|
75
|
+
return { ...view, state: probe.current }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
vi.useFakeTimers()
|
|
80
|
+
callTool = vi.fn<CallTool>(() => Promise.resolve({ structuredContent: PAYLOAD }))
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
vi.useRealTimers()
|
|
85
|
+
installed.observer = null
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe("useToolResultRecovery with its effects (K13)", () => {
|
|
89
|
+
it("keep: a structured result the latch delivered is never re-run", async () => {
|
|
90
|
+
const host = fakeHost()
|
|
91
|
+
installed.observer = host.observer
|
|
92
|
+
await host.handshake()
|
|
93
|
+
await host.toolInput({ id: 7 })
|
|
94
|
+
await host.structuredResult(PAYLOAD)
|
|
95
|
+
const view = await mount({ resultReady: true, props: PAYLOAD })
|
|
96
|
+
await advanceTimers(vi.advanceTimersByTime, DEFAULT_ASSUME_STRIPPED_AFTER_MS * 4)
|
|
97
|
+
expect(callTool).not.toHaveBeenCalled()
|
|
98
|
+
expect(view.state().data).toBeNull()
|
|
99
|
+
await view.unmount()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it("slow: a compliant host still answering is never re-run, however long it takes", async () => {
|
|
103
|
+
const host = fakeHost()
|
|
104
|
+
installed.observer = host.observer
|
|
105
|
+
await host.handshake()
|
|
106
|
+
await host.toolInput({ id: 7 })
|
|
107
|
+
const view = await mount()
|
|
108
|
+
await advanceTimers(vi.advanceTimersByTime, DEFAULT_ASSUME_STRIPPED_AFTER_MS + 1)
|
|
109
|
+
await advanceTimers(vi.advanceTimersByTime, 10_000)
|
|
110
|
+
expect(callTool).not.toHaveBeenCalled()
|
|
111
|
+
expect(view.state()).toMatchObject({ data: null, failed: false, blocked: null })
|
|
112
|
+
await view.unmount()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("strip: the observer's notification alone re-runs the tool once, with the complete input", async () => {
|
|
116
|
+
const host = fakeHost()
|
|
117
|
+
installed.observer = host.observer
|
|
118
|
+
await host.handshake()
|
|
119
|
+
const view = await mount()
|
|
120
|
+
await host.toolInput({ id: 7, page: 2 })
|
|
121
|
+
expect(callTool).not.toHaveBeenCalled()
|
|
122
|
+
// Nothing re-renders the view here: only the subscription can react.
|
|
123
|
+
await host.strippedResult()
|
|
124
|
+
expect(callTool).toHaveBeenCalledTimes(1)
|
|
125
|
+
expect(callTool).toHaveBeenCalledWith("show_list", { id: 7, page: 2 })
|
|
126
|
+
expect(view.state()).toMatchObject({ data: PAYLOAD, failed: false, args: { id: 7, page: 2 } })
|
|
127
|
+
await advanceTimers(vi.advanceTimersByTime, 10_000)
|
|
128
|
+
expect(callTool, "a single flight").toHaveBeenCalledTimes(1)
|
|
129
|
+
await view.unmount()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it("without an observer (observer: null) the 2.x grace timer re-runs a silent call", async () => {
|
|
133
|
+
const view = await mount({ observer: null })
|
|
134
|
+
await advanceTimers(vi.advanceTimersByTime, DEFAULT_ASSUME_STRIPPED_AFTER_MS - 1)
|
|
135
|
+
expect(callTool).not.toHaveBeenCalled()
|
|
136
|
+
await advanceTimers(vi.advanceTimersByTime, 1)
|
|
137
|
+
expect(callTool).toHaveBeenCalledTimes(1)
|
|
138
|
+
expect(callTool).toHaveBeenCalledWith("show_list", { id: 7 })
|
|
139
|
+
await view.unmount()
|
|
140
|
+
})
|
|
141
|
+
})
|
|
@@ -5,10 +5,9 @@ import {
|
|
|
5
5
|
resolveRecoveryToolName,
|
|
6
6
|
} from "./use-tool-result-recovery.js"
|
|
7
7
|
|
|
8
|
-
// The hook's effect wiring (
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// pure decision logic.
|
|
8
|
+
// The hook's effect wiring (default observer, subscription, keep/strip/slow)
|
|
9
|
+
// runs in use-tool-result-recovery.effects.test.tsx and end-to-end in the
|
|
10
|
+
// consumer's Playwright host simulation; these tests pin the pure helpers.
|
|
12
11
|
|
|
13
12
|
describe("resolveRecoveryToolName", () => {
|
|
14
13
|
it("reads the SEP-1865 shape toolInfo.tool.name first", () => {
|
|
@@ -1,44 +1,55 @@
|
|
|
1
|
-
import { useEffect,
|
|
2
|
-
import {
|
|
1
|
+
import { useEffect, useState, useSyncExternalStore } from "react"
|
|
2
|
+
import { installToolResultObserver, type ToolResultObserver } from "../lib/tool-result-observer.js"
|
|
3
|
+
import {
|
|
4
|
+
createRecoveryController,
|
|
5
|
+
type RecoveryPolicy,
|
|
6
|
+
type ToolResultRecoveryState,
|
|
7
|
+
} from "./tool-result-recovery-model.js"
|
|
8
|
+
|
|
9
|
+
export { decodeRecoveredResult } from "./tool-result-recovery-model.js"
|
|
10
|
+
export type {
|
|
11
|
+
RecoveryBlockReason,
|
|
12
|
+
RecoveryPolicy,
|
|
13
|
+
ToolResultRecoveryState,
|
|
14
|
+
ToolSafety,
|
|
15
|
+
} from "./tool-result-recovery-model.js"
|
|
3
16
|
|
|
4
17
|
/**
|
|
5
18
|
* Recovery for hosts that deliver the `ui/notifications/tool-result`
|
|
6
19
|
* notification without `structuredContent` (observed on claude.ai and Claude
|
|
7
|
-
* Desktop, which forward only the model-facing `content` blocks).
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* forever and the shell would idle on its loading skeleton.
|
|
20
|
+
* Desktop, which forward only the model-facing `content` blocks). mcp-use
|
|
21
|
+
* 2.x's `useToolContext` latch ignores such a content-only result, so its
|
|
22
|
+
* status stays `"pending"` forever and the shell would idle on its skeleton.
|
|
11
23
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* result has latched within
|
|
16
|
-
* {@link ToolResultRecoveryOptions.assumeStrippedAfterMs}, the result is
|
|
17
|
-
* assumed delivered-but-stripped. Either way the hook re-executes the
|
|
18
|
-
* originating tool once via the host bridge's official `callTool` — tool-call
|
|
19
|
-
* RESPONSES carry `structuredContent` intact on those hosts — and exposes the
|
|
20
|
-
* decoded payload. Spec-conforming hosts (e.g. the mcp-use inspector) deliver
|
|
21
|
-
* a valid payload up front, so the recovery never fires there.
|
|
24
|
+
* The hook re-executes the originating tool once via the host bridge's
|
|
25
|
+
* `callTool` — tool-call RESPONSES carry `structuredContent` intact on those
|
|
26
|
+
* hosts — and exposes the decoded payload. When it does so (toolkit#176):
|
|
22
27
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* - **Only when the host stripped the result.** A passive observer of the
|
|
29
|
+
* host's lifecycle notifications (`installToolResultObserver`) sees the
|
|
30
|
+
* content-only result itself, so the recovery fires as it arrives, and a
|
|
31
|
+
* slow tool on a compliant host is never re-run (K13): no result has
|
|
32
|
+
* arrived because none was sent. Where the observer cannot vouch for that
|
|
33
|
+
* (no browser frame, or installed after the handshake), the 2.x grace timer
|
|
34
|
+
* ({@link ToolResultRecoveryOptions.assumeStrippedAfterMs}) still infers a
|
|
35
|
+
* stripped result, once the call's arguments arrived.
|
|
36
|
+
* - **With the call's complete arguments** (N21): the observer's complete
|
|
37
|
+
* `tool-input`, never a partial stream; an equal re-delivery is the same run.
|
|
38
|
+
* - **Never for a cancelled or failed call:** re-running repeats the failure
|
|
39
|
+
* or overrides the user's stop; the shell renders that state and offers an
|
|
40
|
+
* explicit retry.
|
|
41
|
+
* - **Only for a tool the policy allows** ({@link ToolResultRecoveryOptions.policy}):
|
|
42
|
+
* by default never one the host reports as writing (`readOnlyHint: false`
|
|
43
|
+
* or `destructiveHint: true` in `hostContext.toolInfo.tool.annotations`).
|
|
44
|
+
* A blocked recovery is reported (`blocked`) so the view can show its error.
|
|
30
45
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* repeat, and may rarely run concurrently with the original invocation when
|
|
34
|
-
* the grace timer beats a slow but conforming host.
|
|
46
|
+
* The decision and its timer live in `createRecoveryController`; this hook
|
|
47
|
+
* only binds it to React.
|
|
35
48
|
*/
|
|
36
49
|
|
|
37
50
|
/**
|
|
38
|
-
* Default grace period before a silent latch counts as a stripped result
|
|
39
|
-
*
|
|
40
|
-
* short enough that users on stripping hosts are not left staring at the
|
|
41
|
-
* skeleton noticeably.
|
|
51
|
+
* Default grace period before a silent latch counts as a stripped result —
|
|
52
|
+
* used only where the transport observer cannot vouch that nothing arrived.
|
|
42
53
|
*/
|
|
43
54
|
export const DEFAULT_ASSUME_STRIPPED_AFTER_MS = 2500
|
|
44
55
|
|
|
@@ -46,8 +57,7 @@ export interface ToolResultRecoveryOptions {
|
|
|
46
57
|
/**
|
|
47
58
|
* The latch fired (`useToolContext().status !== "pending"`) — a structured
|
|
48
59
|
* result, a tool error, or a host cancellation arrived. On hosts that strip
|
|
49
|
-
* `structuredContent` this never turns true
|
|
50
|
-
* that case.
|
|
60
|
+
* `structuredContent` this never turns true.
|
|
51
61
|
*/
|
|
52
62
|
resultReady: boolean
|
|
53
63
|
/**
|
|
@@ -66,10 +76,7 @@ export interface ToolResultRecoveryOptions {
|
|
|
66
76
|
errored?: boolean
|
|
67
77
|
/** The payload the host delivered (`useToolContext().toolOutput`). */
|
|
68
78
|
props: unknown
|
|
69
|
-
/**
|
|
70
|
-
* Validates a candidate payload. MUST be referentially stable (module-level
|
|
71
|
-
* function or memoised) — it is an effect dependency.
|
|
72
|
-
*/
|
|
79
|
+
/** Validates a candidate payload. */
|
|
73
80
|
isValid: (value: unknown) => boolean
|
|
74
81
|
/** The tool arguments from `ui/notifications/tool-input` (`useToolContext().toolInput`). */
|
|
75
82
|
toolInput: Record<string, unknown> | null | undefined
|
|
@@ -79,23 +86,21 @@ export interface ToolResultRecoveryOptions {
|
|
|
79
86
|
callTool: (name: string, args: Record<string, unknown>) => Promise<unknown>
|
|
80
87
|
/**
|
|
81
88
|
* Grace period in ms after which a silent latch counts as a stripped
|
|
82
|
-
* result.
|
|
83
|
-
*
|
|
84
|
-
* arguments mean the invocation may not have finished yet. Defaults to
|
|
89
|
+
* result. Applies only where the transport observer cannot vouch that no
|
|
90
|
+
* result arrived (see above); restarts for new arguments. Defaults to
|
|
85
91
|
* {@link DEFAULT_ASSUME_STRIPPED_AFTER_MS}.
|
|
86
92
|
*/
|
|
87
93
|
assumeStrippedAfterMs?: number
|
|
94
|
+
/** Which tools may be re-executed on their own. Default `"auto"`. */
|
|
95
|
+
policy?: RecoveryPolicy
|
|
96
|
+
/**
|
|
97
|
+
* The transport observer. Defaults to the document's
|
|
98
|
+
* (`installToolResultObserver()`); `null` disables it, leaving the grace
|
|
99
|
+
* timer to decide alone (the 2.x behaviour).
|
|
100
|
+
*/
|
|
101
|
+
observer?: ToolResultObserver | null
|
|
88
102
|
}
|
|
89
103
|
|
|
90
|
-
export interface ToolResultRecoveryState<T> {
|
|
91
|
-
/** The recovered payload, `null` until the re-execution resolved (or when it failed). */
|
|
92
|
-
data: T | null
|
|
93
|
-
/** `true` when the single recovery attempt failed — the shell keeps its skeleton, no retry loop. */
|
|
94
|
-
failed: boolean
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const IDLE: ToolResultRecoveryState<never> = { data: null, failed: false }
|
|
98
|
-
|
|
99
104
|
/**
|
|
100
105
|
* Resolves the originating tool name from the host context's `toolInfo`.
|
|
101
106
|
* Reads the SEP-1865 shape first (`toolInfo.tool.name`), then the flattened
|
|
@@ -119,26 +124,10 @@ export function resolveRecoveryToolName(hostContext: unknown): string | undefine
|
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
result: unknown,
|
|
127
|
-
isValid: (value: unknown) => boolean,
|
|
128
|
-
): T | null {
|
|
129
|
-
let decoded: unknown
|
|
130
|
-
try {
|
|
131
|
-
decoded = parseToolResult<unknown>(result)
|
|
132
|
-
} catch {
|
|
133
|
-
return null
|
|
134
|
-
}
|
|
135
|
-
return decoded !== null && isValid(decoded) ? (decoded as T) : null
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Whether the recovery may fire: the result latched (or is assumed stripped)
|
|
140
|
-
* and the call was neither cancelled nor failed. Exported for tests — this is
|
|
141
|
-
* the #176 rule "never recover on an error status".
|
|
127
|
+
* Whether a latched or assumed-stripped result may be recovered at all: the
|
|
128
|
+
* call was neither cancelled nor failed. Kept for existing imports; the full
|
|
129
|
+
* rule (observer, policy, arguments) is `decideRecovery`.
|
|
130
|
+
* @deprecated Use `decideRecovery` from the recovery model.
|
|
142
131
|
*/
|
|
143
132
|
export function isRecoveryReady(state: {
|
|
144
133
|
resultReady: boolean
|
|
@@ -163,81 +152,34 @@ export function useToolResultRecovery<T>(
|
|
|
163
152
|
hostContext,
|
|
164
153
|
callTool,
|
|
165
154
|
assumeStrippedAfterMs = DEFAULT_ASSUME_STRIPPED_AFTER_MS,
|
|
155
|
+
policy = "auto",
|
|
156
|
+
observer,
|
|
166
157
|
} = options
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const toolName = resolveRecoveryToolName(hostContext)
|
|
175
|
-
|
|
176
|
-
// A stripped result is unobservable (the 2.x latch ignores content-only
|
|
177
|
-
// results), so infer it: once a recovery could fire but the latch has been
|
|
178
|
-
// silent for the grace period, treat the result as delivered-but-stripped.
|
|
179
|
-
// Re-armed on every toolInput identity change — streaming arguments mean
|
|
180
|
-
// the invocation may still be running, so the grace period starts over.
|
|
181
|
-
// The re-arm is a render-phase reset (not an effect) so the cascading-
|
|
182
|
-
// render lint stays satisfied; the timer effect below restarts anyway
|
|
183
|
-
// because toolInput is one of its dependencies.
|
|
184
|
-
const [assumedReady, setAssumedReady] = useState(false)
|
|
185
|
-
const [prevTimerInput, setPrevTimerInput] = useState<unknown>(toolInput)
|
|
186
|
-
if (prevTimerInput !== toolInput) {
|
|
187
|
-
setPrevTimerInput(toolInput)
|
|
188
|
-
if (assumedReady) setAssumedReady(false)
|
|
189
|
-
}
|
|
190
|
-
const suppressed = cancelled || errored
|
|
158
|
+
// One controller per mount; it survives StrictMode's effect re-run, so an
|
|
159
|
+
// in-flight call is never fired twice.
|
|
160
|
+
const [controller] = useState(() =>
|
|
161
|
+
createRecoveryController<T>({
|
|
162
|
+
observer: observer === undefined ? installToolResultObserver() : observer,
|
|
163
|
+
}),
|
|
164
|
+
)
|
|
191
165
|
useEffect(() => {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}, [resultReady, suppressed, toolName, toolInput, assumeStrippedAfterMs])
|
|
196
|
-
|
|
197
|
-
// A cancelled or failed call never counts as ready, so the effect below
|
|
198
|
-
// invalidates any in-flight attempt the same way it does for a superseded run.
|
|
199
|
-
const ready = isRecoveryReady({ resultReady, assumedReady, cancelled, errored })
|
|
200
|
-
|
|
166
|
+
controller.start()
|
|
167
|
+
return () => controller.stop()
|
|
168
|
+
}, [controller])
|
|
201
169
|
useEffect(() => {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
flightRef.current = (async (): Promise<ToolResultRecoveryState<T>> => {
|
|
215
|
-
try {
|
|
216
|
-
const result = await callTool(toolName, toolInput ?? {})
|
|
217
|
-
const data = decodeRecoveredResult<T>(result, isValid)
|
|
218
|
-
if (data !== null) return { data, failed: false }
|
|
219
|
-
console.warn(
|
|
220
|
-
`[mcp-toolkit] tool-result recovery: re-executing "${toolName}" returned no usable payload`,
|
|
221
|
-
)
|
|
222
|
-
return { data: null, failed: true }
|
|
223
|
-
} catch (err) {
|
|
224
|
-
console.warn(`[mcp-toolkit] tool-result recovery for "${toolName}" failed:`, err)
|
|
225
|
-
return { data: null, failed: true }
|
|
226
|
-
}
|
|
227
|
-
})()
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const flight = flightRef.current
|
|
231
|
-
let stale = false
|
|
232
|
-
void flight.then((outcome) => {
|
|
233
|
-
// Drop the outcome when this subscription was superseded or the flight
|
|
234
|
-
// was invalidated by a newer tool run.
|
|
235
|
-
if (!stale && flightRef.current === flight) setState(outcome)
|
|
170
|
+
controller.update({
|
|
171
|
+
resultReady,
|
|
172
|
+
cancelled,
|
|
173
|
+
errored,
|
|
174
|
+
props,
|
|
175
|
+
isValid,
|
|
176
|
+
toolInput,
|
|
177
|
+
hostContext,
|
|
178
|
+
callTool,
|
|
179
|
+
policy,
|
|
180
|
+
graceMs: assumeStrippedAfterMs,
|
|
181
|
+
resolveToolName: resolveRecoveryToolName,
|
|
236
182
|
})
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
}, [ready, props, isValid, toolName, toolInput, callTool])
|
|
241
|
-
|
|
242
|
-
return state
|
|
183
|
+
})
|
|
184
|
+
return useSyncExternalStore(controller.subscribe, controller.getState, controller.getState)
|
|
243
185
|
}
|