skybridge 0.0.0-dev.97c9ddf → 0.0.0-dev.97e4bf7
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 +15 -14
- package/dist/cli/build-helpers.d.ts +1 -1
- package/dist/cli/build-helpers.js +4 -4
- package/dist/cli/build-helpers.js.map +1 -1
- package/dist/cli/build-helpers.test.js +2 -2
- package/dist/cli/build-helpers.test.js.map +1 -1
- package/dist/cli/build-steps.js +1 -1
- package/dist/cli/build-steps.js.map +1 -1
- package/dist/cli/build-steps.test.js +3 -3
- package/dist/cli/build-steps.test.js.map +1 -1
- package/dist/cli/detect-port.js +0 -1
- package/dist/cli/detect-port.js.map +1 -1
- package/dist/cli/run-plain.js +2 -1
- package/dist/cli/run-plain.js.map +1 -1
- package/dist/cli/telemetry.js +2 -0
- package/dist/cli/telemetry.js.map +1 -1
- package/dist/cli/use-execute-steps.d.ts +1 -1
- package/dist/cli/use-nodemon.d.ts +11 -1
- package/dist/cli/use-nodemon.js +30 -8
- package/dist/cli/use-nodemon.js.map +1 -1
- package/dist/cli/use-typescript-check.js +1 -1
- package/dist/cli/use-typescript-check.js.map +1 -1
- package/dist/commands/dev.js +4 -4
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/start.js +2 -2
- package/dist/commands/start.js.map +1 -1
- package/dist/server/app.d.ts +194 -0
- package/dist/server/app.js +266 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/app.test.js +97 -0
- package/dist/server/app.test.js.map +1 -0
- package/dist/server/auth/discovery.d.ts +2 -1
- package/dist/server/auth/discovery.js +2 -2
- package/dist/server/auth/discovery.js.map +1 -1
- package/dist/server/auth/index.d.ts +21 -17
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/auth/providers/auth0.d.ts +2 -2
- package/dist/server/auth/providers/auth0.js +13 -8
- package/dist/server/auth/providers/auth0.js.map +1 -1
- package/dist/server/auth/providers/auth0.test.js +1 -1
- package/dist/server/auth/providers/auth0.test.js.map +1 -1
- package/dist/server/auth/providers/authplane.d.ts +2 -2
- package/dist/server/auth/providers/authplane.js.map +1 -1
- package/dist/server/auth/providers/authplane.test.js +10 -7
- package/dist/server/auth/providers/authplane.test.js.map +1 -1
- package/dist/server/auth/providers/clerk.d.ts +2 -2
- package/dist/server/auth/providers/clerk.js.map +1 -1
- package/dist/server/auth/providers/clerk.test.js +1 -1
- package/dist/server/auth/providers/clerk.test.js.map +1 -1
- package/dist/server/auth/providers/custom.d.ts +8 -6
- package/dist/server/auth/providers/custom.js +5 -2
- package/dist/server/auth/providers/custom.js.map +1 -1
- package/dist/server/auth/providers/custom.test.js +13 -10
- package/dist/server/auth/providers/custom.test.js.map +1 -1
- package/dist/server/auth/providers/descope.d.ts +2 -2
- package/dist/server/auth/providers/descope.js.map +1 -1
- package/dist/server/auth/providers/descope.test.js +3 -3
- package/dist/server/auth/providers/descope.test.js.map +1 -1
- package/dist/server/auth/providers/stytch.d.ts +2 -2
- package/dist/server/auth/providers/stytch.js.map +1 -1
- package/dist/server/auth/providers/workos.d.ts +5 -4
- package/dist/server/auth/providers/workos.js +2 -1
- package/dist/server/auth/providers/workos.js.map +1 -1
- package/dist/server/auth/setup.d.ts +1 -1
- package/dist/server/auth/setup.js +3 -4
- package/dist/server/auth/setup.js.map +1 -1
- package/dist/server/auth/setup.test.js +83 -77
- package/dist/server/auth/setup.test.js.map +1 -1
- package/dist/server/auth/verify.d.ts +5 -3
- package/dist/server/auth/verify.js +2 -2
- package/dist/server/auth/verify.js.map +1 -1
- package/dist/server/auth/verify.test.js +16 -0
- package/dist/server/auth/verify.test.js.map +1 -1
- package/dist/server/auth-extra.test-d.js +97 -49
- package/dist/server/auth-extra.test-d.js.map +1 -1
- package/dist/server/auth.d.ts +7 -8
- package/dist/server/auth.js +4 -5
- package/dist/server/auth.js.map +1 -1
- package/dist/server/build-manifest.test.js +7 -6
- package/dist/server/build-manifest.test.js.map +1 -1
- package/dist/server/content-helpers.d.ts +1 -1
- package/dist/server/content-helpers.js.map +1 -1
- package/dist/server/events.d.ts +113 -0
- package/dist/server/events.js +318 -0
- package/dist/server/events.js.map +1 -0
- package/dist/server/events.test.js +172 -0
- package/dist/server/events.test.js.map +1 -0
- package/dist/server/express.d.ts +23 -3
- package/dist/server/express.js +43 -32
- package/dist/server/express.js.map +1 -1
- package/dist/server/express.test.js +221 -103
- package/dist/server/express.test.js.map +1 -1
- package/dist/server/file-ref.d.ts +1 -1
- package/dist/server/file-ref.js +1 -1
- package/dist/server/file-ref.js.map +1 -1
- package/dist/server/form-input.d.ts +120 -0
- package/dist/server/form-input.js +89 -0
- package/dist/server/form-input.js.map +1 -0
- package/dist/server/index.d.ts +8 -3
- package/dist/server/index.js +5 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp-errors.test.js +41 -0
- package/dist/server/mcp-errors.test.js.map +1 -0
- package/dist/server/middleware.d.ts +6 -6
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/middleware.test-d.js +1 -1
- package/dist/server/middleware.test-d.js.map +1 -1
- package/dist/server/middleware.test.js +222 -150
- package/dist/server/middleware.test.js.map +1 -1
- package/dist/server/register-tool.test.js +230 -0
- package/dist/server/register-tool.test.js.map +1 -0
- package/dist/server/server.d.ts +327 -128
- package/dist/server/server.js +362 -305
- package/dist/server/server.js.map +1 -1
- package/dist/server/skills-integration.test.js +19 -12
- package/dist/server/skills-integration.test.js.map +1 -1
- package/dist/server/skills.d.ts +3 -5
- package/dist/server/skills.js +17 -24
- package/dist/server/skills.js.map +1 -1
- package/dist/server/skills.test.js +8 -12
- package/dist/server/skills.test.js.map +1 -1
- package/dist/server/templateHelper.d.ts +0 -3
- package/dist/server/templateHelper.js.map +1 -1
- package/dist/server/templates.generated.js +4 -8
- package/dist/server/templates.generated.js.map +1 -1
- package/dist/server/tool-extra.test.js +167 -0
- package/dist/server/tool-extra.test.js.map +1 -0
- package/dist/server/view-resource-resolution.test.js +18 -13
- package/dist/server/view-resource-resolution.test.js.map +1 -1
- package/dist/test/utils.d.ts +9 -11
- package/dist/test/utils.js +220 -201
- package/dist/test/utils.js.map +1 -1
- package/dist/test/view.test.js +25 -37
- package/dist/test/view.test.js.map +1 -1
- package/dist/views/index.d.ts +10 -0
- package/dist/views/index.js +3 -0
- package/dist/views/index.js.map +1 -0
- package/dist/{web/plugin → views}/scan-views.js +1 -1
- package/dist/views/scan-views.js.map +1 -0
- package/dist/views/scan-views.test.js.map +1 -0
- package/dist/views/validate-view.d.ts +1 -0
- package/dist/{web/plugin → views}/validate-view.js +2 -2
- package/dist/views/validate-view.js.map +1 -0
- package/dist/views/validate-view.test.js.map +1 -0
- package/dist/web/bridges/adaptor.d.ts +14 -3
- package/dist/web/bridges/adaptor.js +157 -13
- package/dist/web/bridges/adaptor.js.map +1 -1
- package/dist/web/bridges/adaptor.test.js +46 -7
- package/dist/web/bridges/adaptor.test.js.map +1 -1
- package/dist/web/bridges/apps-sdk/bridge.js +1 -1
- package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.d.ts +4 -4
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js +4 -4
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -1
- package/dist/web/bridges/get-adaptor.test.js +2 -2
- package/dist/web/bridges/get-adaptor.test.js.map +1 -1
- package/dist/web/bridges/mcp-app/bridge.d.ts +1 -1
- package/dist/web/bridges/mcp-app/bridge.js +10 -3
- package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
- package/dist/web/bridges/mcp-app/bridge.test.js +2 -2
- package/dist/web/bridges/mcp-app/bridge.test.js.map +1 -1
- package/dist/web/bridges/mcp-app/types.d.ts +18 -3
- package/dist/web/bridges/mcp-app/types.js.map +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +2 -2
- package/dist/web/bridges/mcp-app/use-mcp-app-context.js +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js.map +1 -1
- package/dist/web/bridges/mcp-app/view-tools.test.js +1 -1
- package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -1
- package/dist/web/bridges/types.d.ts +74 -16
- package/dist/web/bridges/types.js +1 -1
- package/dist/web/bridges/types.js.map +1 -1
- package/dist/web/create-store.test.js +2 -2
- package/dist/web/create-store.test.js.map +1 -1
- package/dist/web/data-llm.js +3 -2
- package/dist/web/data-llm.js.map +1 -1
- package/dist/web/data-llm.test.js +8 -2
- package/dist/web/data-llm.test.js.map +1 -1
- package/dist/web/generate-helpers.d.ts +23 -19
- package/dist/web/generate-helpers.js +21 -17
- package/dist/web/generate-helpers.js.map +1 -1
- package/dist/web/generate-helpers.test-d.js +3 -3
- package/dist/web/generate-helpers.test-d.js.map +1 -1
- package/dist/web/generate-helpers.test.js +2 -2
- package/dist/web/generate-helpers.test.js.map +1 -1
- package/dist/web/hooks/index.d.ts +6 -2
- package/dist/web/hooks/index.js +6 -2
- package/dist/web/hooks/index.js.map +1 -1
- package/dist/web/hooks/openai-extensions.test.js +107 -0
- package/dist/web/hooks/openai-extensions.test.js.map +1 -0
- package/dist/web/hooks/use-call-tool.d.ts +2 -2
- package/dist/web/hooks/use-call-tool.js +15 -10
- package/dist/web/hooks/use-call-tool.js.map +1 -1
- package/dist/web/hooks/use-call-tool.test.js +15 -4
- package/dist/web/hooks/use-call-tool.test.js.map +1 -1
- package/dist/web/hooks/use-deep-link.d.ts +22 -0
- package/dist/web/hooks/use-deep-link.js +34 -0
- package/dist/web/hooks/use-deep-link.js.map +1 -0
- package/dist/web/hooks/use-display-mode.d.ts +1 -1
- package/dist/web/hooks/use-display-mode.js +1 -1
- package/dist/web/hooks/use-display-mode.js.map +1 -1
- package/dist/web/hooks/use-display-mode.test.js +1 -1
- package/dist/web/hooks/use-display-mode.test.js.map +1 -1
- package/dist/web/hooks/use-download.d.ts +1 -3
- package/dist/web/hooks/use-download.js +1 -2
- package/dist/web/hooks/use-download.js.map +1 -1
- package/dist/web/hooks/use-download.test.js +7 -7
- package/dist/web/hooks/use-download.test.js.map +1 -1
- package/dist/web/hooks/use-file-resource.d.ts +37 -0
- package/dist/web/hooks/use-file-resource.js +98 -0
- package/dist/web/hooks/use-file-resource.js.map +1 -0
- package/dist/web/hooks/use-file-resource.test.d.ts +1 -0
- package/dist/web/hooks/use-file-resource.test.js +65 -0
- package/dist/web/hooks/use-file-resource.test.js.map +1 -0
- package/dist/web/hooks/use-files.test.js +1 -1
- package/dist/web/hooks/use-files.test.js.map +1 -1
- package/dist/web/hooks/use-host.d.ts +44 -0
- package/dist/web/hooks/use-host.js +45 -0
- package/dist/web/hooks/use-host.js.map +1 -0
- package/dist/web/hooks/use-host.test.d.ts +1 -0
- package/dist/web/hooks/{use-host-info.test.js → use-host.test.js} +7 -7
- package/dist/web/hooks/use-host.test.js.map +1 -0
- package/dist/web/hooks/use-model-context.d.ts +44 -0
- package/dist/web/hooks/use-model-context.js +63 -0
- package/dist/web/hooks/use-model-context.js.map +1 -0
- package/dist/web/hooks/use-open-external.test.js +2 -2
- package/dist/web/hooks/use-open-external.test.js.map +1 -1
- package/dist/web/hooks/use-open-file.d.ts +20 -0
- package/dist/web/hooks/use-open-file.js +25 -0
- package/dist/web/hooks/use-open-file.js.map +1 -0
- package/dist/web/hooks/use-register-view-tool.d.ts +3 -4
- package/dist/web/hooks/use-register-view-tool.js +1 -1
- package/dist/web/hooks/use-register-view-tool.js.map +1 -1
- package/dist/web/hooks/use-request-close.test.js +1 -1
- package/dist/web/hooks/use-request-close.test.js.map +1 -1
- package/dist/web/hooks/use-request-modal.test.js +1 -1
- package/dist/web/hooks/use-request-modal.test.js.map +1 -1
- package/dist/web/hooks/use-request-size.d.ts +1 -1
- package/dist/web/hooks/use-request-size.js +1 -1
- package/dist/web/hooks/use-request-size.js.map +1 -1
- package/dist/web/hooks/use-request-size.test.js +1 -1
- package/dist/web/hooks/use-request-size.test.js.map +1 -1
- package/dist/web/hooks/use-send-follow-up-message.d.ts +8 -2
- package/dist/web/hooks/use-send-follow-up-message.js +6 -0
- package/dist/web/hooks/use-send-follow-up-message.js.map +1 -1
- package/dist/web/hooks/use-set-open-in-app-url.test.js +2 -8
- package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
- package/dist/web/hooks/use-tool-info.d.ts +1 -20
- package/dist/web/hooks/use-tool-info.js +0 -1
- package/dist/web/hooks/use-tool-info.js.map +1 -1
- package/dist/web/hooks/use-tool-info.test-d.js +0 -5
- package/dist/web/hooks/use-tool-info.test-d.js.map +1 -1
- package/dist/web/hooks/use-tool-info.test.js +2 -7
- package/dist/web/hooks/use-tool-info.test.js.map +1 -1
- package/dist/web/hooks/use-user.d.ts +3 -2
- package/dist/web/hooks/use-user.js +4 -3
- package/dist/web/hooks/use-user.js.map +1 -1
- package/dist/web/hooks/use-user.test.js +5 -3
- package/dist/web/hooks/use-user.test.js.map +1 -1
- package/dist/web/hooks/use-view-state.test.js +2 -2
- package/dist/web/hooks/use-view-state.test.js.map +1 -1
- package/dist/web/hooks/use-viewport.d.ts +20 -0
- package/dist/web/hooks/use-viewport.js +21 -0
- package/dist/web/hooks/use-viewport.js.map +1 -0
- package/dist/web/hooks/use-viewport.test.d.ts +1 -0
- package/dist/web/hooks/{use-layout.test.js → use-viewport.test.js} +12 -32
- package/dist/web/hooks/use-viewport.test.js.map +1 -0
- package/dist/web/mount-view.d.ts +3 -2
- package/dist/web/mount-view.js +5 -8
- package/dist/web/mount-view.js.map +1 -1
- package/package.json +17 -13
- package/dist/web/hooks/use-host-info.d.ts +0 -22
- package/dist/web/hooks/use-host-info.js +0 -30
- package/dist/web/hooks/use-host-info.js.map +0 -1
- package/dist/web/hooks/use-host-info.test.js.map +0 -1
- package/dist/web/hooks/use-layout.d.ts +0 -24
- package/dist/web/hooks/use-layout.js +0 -25
- package/dist/web/hooks/use-layout.js.map +0 -1
- package/dist/web/hooks/use-layout.test.js.map +0 -1
- package/dist/web/plugin/data-llm.test.js +0 -81
- package/dist/web/plugin/data-llm.test.js.map +0 -1
- package/dist/web/plugin/plugin-build.test.js +0 -54
- package/dist/web/plugin/plugin-build.test.js.map +0 -1
- package/dist/web/plugin/plugin.d.ts +0 -39
- package/dist/web/plugin/plugin.js +0 -200
- package/dist/web/plugin/plugin.js.map +0 -1
- package/dist/web/plugin/plugin.test.js +0 -61
- package/dist/web/plugin/plugin.test.js.map +0 -1
- package/dist/web/plugin/scan-views.js.map +0 -1
- package/dist/web/plugin/scan-views.test.js.map +0 -1
- package/dist/web/plugin/transform-data-llm.d.ts +0 -12
- package/dist/web/plugin/transform-data-llm.js +0 -96
- package/dist/web/plugin/transform-data-llm.js.map +0 -1
- package/dist/web/plugin/transform-data-llm.test.js +0 -81
- package/dist/web/plugin/transform-data-llm.test.js.map +0 -1
- package/dist/web/plugin/validate-view.d.ts +0 -1
- package/dist/web/plugin/validate-view.js.map +0 -1
- package/dist/web/plugin/validate-view.test.js.map +0 -1
- /package/dist/{web/hooks/use-host-info.test.d.ts → server/app.test.d.ts} +0 -0
- /package/dist/{web/hooks/use-layout.test.d.ts → server/events.test.d.ts} +0 -0
- /package/dist/{web/plugin/data-llm.test.d.ts → server/mcp-errors.test.d.ts} +0 -0
- /package/dist/{web/plugin/plugin-build.test.d.ts → server/register-tool.test.d.ts} +0 -0
- /package/dist/{web/plugin/plugin.test.d.ts → server/tool-extra.test.d.ts} +0 -0
- /package/dist/{web/plugin → views}/scan-views.d.ts +0 -0
- /package/dist/{web/plugin → views}/scan-views.test.d.ts +0 -0
- /package/dist/{web/plugin → views}/scan-views.test.js +0 -0
- /package/dist/{web/plugin → views}/validate-view.test.d.ts +0 -0
- /package/dist/{web/plugin → views}/validate-view.test.js +0 -0
- /package/dist/web/{plugin/transform-data-llm.test.d.ts → hooks/openai-extensions.test.d.ts} +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-relative URL (path and query, e.g. `"/parts?tag=bolt"`) that ChatGPT
|
|
3
|
+
* opened the view on through a deep link. Updates when the user follows
|
|
4
|
+
* another deep link while the view is open. `undefined` when the view was not
|
|
5
|
+
* opened through a deep link, on hosts other than ChatGPT, or when the host
|
|
6
|
+
* sends a URL that isn't app-relative (it must start with a single `/`) or
|
|
7
|
+
* that contains a fragment.
|
|
8
|
+
*
|
|
9
|
+
* Deep links target global entrypoints: set `openai.entrypoints` to include
|
|
10
|
+
* `{ type: "global" }` on the tool.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* const deepLink = useDeepLink();
|
|
15
|
+
* useEffect(() => {
|
|
16
|
+
* if (deepLink) navigate(deepLink);
|
|
17
|
+
* }, [deepLink, navigate]);
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @see https://docs.skybridge.tech/api-reference/use-deep-link
|
|
21
|
+
*/
|
|
22
|
+
export declare function useDeepLink(): string | undefined;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useMcpAppContext } from "../bridges/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* App-relative URL (path and query, e.g. `"/parts?tag=bolt"`) that ChatGPT
|
|
4
|
+
* opened the view on through a deep link. Updates when the user follows
|
|
5
|
+
* another deep link while the view is open. `undefined` when the view was not
|
|
6
|
+
* opened through a deep link, on hosts other than ChatGPT, or when the host
|
|
7
|
+
* sends a URL that isn't app-relative (it must start with a single `/`) or
|
|
8
|
+
* that contains a fragment.
|
|
9
|
+
*
|
|
10
|
+
* Deep links target global entrypoints: set `openai.entrypoints` to include
|
|
11
|
+
* `{ type: "global" }` on the tool.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* const deepLink = useDeepLink();
|
|
16
|
+
* useEffect(() => {
|
|
17
|
+
* if (deepLink) navigate(deepLink);
|
|
18
|
+
* }, [deepLink, navigate]);
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @see https://docs.skybridge.tech/api-reference/use-deep-link
|
|
22
|
+
*/
|
|
23
|
+
export function useDeepLink() {
|
|
24
|
+
const url = useMcpAppContext("openai/deepLink")?.url;
|
|
25
|
+
if (typeof url !== "string" ||
|
|
26
|
+
!url.startsWith("/") ||
|
|
27
|
+
url.startsWith("//") ||
|
|
28
|
+
url.startsWith("/\\") ||
|
|
29
|
+
url.includes("#")) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return url;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=use-deep-link.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-deep-link.js","sourceRoot":"","sources":["../../../src/web/hooks/use-deep-link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,WAAW;IACzB,MAAM,GAAG,GAAG,gBAAgB,CAAC,iBAAiB,CAAC,EAAE,GAAG,CAAC;IACrD,IACE,OAAO,GAAG,KAAK,QAAQ;QACvB,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC;QACpB,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC;QACpB,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC;QACrB,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,EACjB,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["import { useMcpAppContext } from \"../bridges/index.js\";\n\n/**\n * App-relative URL (path and query, e.g. `\"/parts?tag=bolt\"`) that ChatGPT\n * opened the view on through a deep link. Updates when the user follows\n * another deep link while the view is open. `undefined` when the view was not\n * opened through a deep link, on hosts other than ChatGPT, or when the host\n * sends a URL that isn't app-relative (it must start with a single `/`) or\n * that contains a fragment.\n *\n * Deep links target global entrypoints: set `openai.entrypoints` to include\n * `{ type: \"global\" }` on the tool.\n *\n * @example\n * ```tsx\n * const deepLink = useDeepLink();\n * useEffect(() => {\n * if (deepLink) navigate(deepLink);\n * }, [deepLink, navigate]);\n * ```\n *\n * @see https://docs.skybridge.tech/api-reference/use-deep-link\n */\nexport function useDeepLink(): string | undefined {\n const url = useMcpAppContext(\"openai/deepLink\")?.url;\n if (\n typeof url !== \"string\" ||\n !url.startsWith(\"/\") ||\n url.startsWith(\"//\") ||\n url.startsWith(\"/\\\\\") ||\n url.includes(\"#\")\n ) {\n return undefined;\n }\n return url;\n}\n"]}
|
|
@@ -9,7 +9,7 @@ import type { RequestDisplayMode } from "../bridges/types.js";
|
|
|
9
9
|
*
|
|
10
10
|
* `"modal"` is reachable via {@link useRequestModal}, not this hook. To react
|
|
11
11
|
* to layout changes that come with display-mode switches (e.g. `maxHeight`),
|
|
12
|
-
* pair with {@link
|
|
12
|
+
* pair with {@link useViewport}.
|
|
13
13
|
*
|
|
14
14
|
* @example
|
|
15
15
|
* ```tsx
|
|
@@ -10,7 +10,7 @@ import { getAdaptor, useHostContext } from "../bridges/index.js";
|
|
|
10
10
|
*
|
|
11
11
|
* `"modal"` is reachable via {@link useRequestModal}, not this hook. To react
|
|
12
12
|
* to layout changes that come with display-mode switches (e.g. `maxHeight`),
|
|
13
|
-
* pair with {@link
|
|
13
|
+
* pair with {@link useViewport}.
|
|
14
14
|
*
|
|
15
15
|
* @example
|
|
16
16
|
* ```tsx
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-display-mode.js","sourceRoot":"","sources":["../../../src/web/hooks/use-display-mode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGjE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,cAAc;IAC5B,MAAM,WAAW,GAAG,cAAc,CAAC,aAAa,CAAC,CAAC;IAClD,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,MAAM,cAAc,GAAG,WAAW,CAChC,CAAC,IAAwB,EAAE,EAAE,CAAC,OAAO,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAC9D,CAAC,OAAO,CAAC,CACV,CAAC;IAEF,OAAO,CAAC,WAAW,EAAE,cAAc,CAAU,CAAC;AAChD,CAAC","sourcesContent":["import { useCallback } from \"react\";\nimport { getAdaptor, useHostContext } from \"../bridges/index.js\";\nimport type { RequestDisplayMode } from \"../bridges/types.js\";\n\n/**\n * Read and change the view's display mode (`\"inline\"`, `\"pip\"`, `\"fullscreen\"`).\n *\n * Returns a tuple `[displayMode, setDisplayMode]`. `setDisplayMode` asks the\n * host to switch modes — the host returns the mode it actually applied, which\n * may differ from the request. The reported value also updates when the host\n * changes the mode on its own (e.g. user expands the widget).\n *\n * `\"modal\"` is reachable via {@link useRequestModal}, not this hook. To react\n * to layout changes that come with display-mode switches (e.g. `maxHeight`),\n * pair with {@link
|
|
1
|
+
{"version":3,"file":"use-display-mode.js","sourceRoot":"","sources":["../../../src/web/hooks/use-display-mode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGjE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,cAAc;IAC5B,MAAM,WAAW,GAAG,cAAc,CAAC,aAAa,CAAC,CAAC;IAClD,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,MAAM,cAAc,GAAG,WAAW,CAChC,CAAC,IAAwB,EAAE,EAAE,CAAC,OAAO,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAC9D,CAAC,OAAO,CAAC,CACV,CAAC;IAEF,OAAO,CAAC,WAAW,EAAE,cAAc,CAAU,CAAC;AAChD,CAAC","sourcesContent":["import { useCallback } from \"react\";\nimport { getAdaptor, useHostContext } from \"../bridges/index.js\";\nimport type { RequestDisplayMode } from \"../bridges/types.js\";\n\n/**\n * Read and change the view's display mode (`\"inline\"`, `\"pip\"`, `\"fullscreen\"`).\n *\n * Returns a tuple `[displayMode, setDisplayMode]`. `setDisplayMode` asks the\n * host to switch modes — the host returns the mode it actually applied, which\n * may differ from the request. The reported value also updates when the host\n * changes the mode on its own (e.g. user expands the widget).\n *\n * `\"modal\"` is reachable via {@link useRequestModal}, not this hook. To react\n * to layout changes that come with display-mode switches (e.g. `maxHeight`),\n * pair with {@link useViewport}.\n *\n * @example\n * ```tsx\n * const [mode, setMode] = useDisplayMode();\n * <button onClick={() => setMode(\"fullscreen\")}>Expand</button>\n * ```\n *\n * @see https://docs.skybridge.tech/api-reference/use-display-mode\n */\nexport function useDisplayMode() {\n const displayMode = useHostContext(\"displayMode\");\n const adaptor = getAdaptor();\n const setDisplayMode = useCallback(\n (mode: RequestDisplayMode) => adaptor.requestDisplayMode(mode),\n [adaptor],\n );\n\n return [displayMode, setDisplayMode] as const;\n}\n"]}
|
|
@@ -11,7 +11,7 @@ describe("useDisplayMode", () => {
|
|
|
11
11
|
HostAdaptor.resetInstance();
|
|
12
12
|
McpAppBridge.resetInstance();
|
|
13
13
|
AppsSdkBridge.resetInstance();
|
|
14
|
-
vi.stubGlobal("skybridge", {
|
|
14
|
+
vi.stubGlobal("skybridge", {});
|
|
15
15
|
vi.stubGlobal("openai", { view: { mode: "inline" } });
|
|
16
16
|
vi.stubGlobal("ResizeObserver", MockResizeObserver);
|
|
17
17
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-display-mode.test.js","sourceRoot":"","sources":["../../../src/web/hooks/use-display-mode.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,EACL,4BAA4B,EAC5B,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,UAAU,CAAC,GAAG,EAAE;QACd,WAAW,CAAC,aAAa,EAAE,CAAC;QAC5B,YAAY,CAAC,aAAa,EAAE,CAAC;QAC7B,aAAa,CAAC,aAAa,EAAE,CAAC;QAC9B,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,EAAE,
|
|
1
|
+
{"version":3,"file":"use-display-mode.test.js","sourceRoot":"","sources":["../../../src/web/hooks/use-display-mode.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,EACL,4BAA4B,EAC5B,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,UAAU,CAAC,GAAG,EAAE;QACd,WAAW,CAAC,aAAa,EAAE,CAAC;QAC5B,YAAY,CAAC,aAAa,EAAE,CAAC;QAC7B,aAAa,CAAC,aAAa,EAAE,CAAC;QAC9B,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QAC/B,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;QACtD,EAAE,CAAC,UAAU,CAAC,gBAAgB,EAAE,kBAAkB,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,EAAE,CAAC,gBAAgB,EAAE,CAAC;QACtB,EAAE,CAAC,aAAa,EAAE,CAAC;QACnB,WAAW,CAAC,aAAa,EAAE,CAAC;QAC5B,YAAY,CAAC,aAAa,EAAE,CAAC;QAC7B,aAAa,CAAC,aAAa,EAAE,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8DAA8D,EAAE,KAAK,IAAI,EAAE;QAC5E,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE;YACtB,WAAW,EAAE,4BAA4B,CAAC,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC;SACrE,CAAC,CAAC;QAEH,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC;QAEtD,MAAM,OAAO,CAAC,GAAG,EAAE;YACjB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC3C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,KAAK,IAAI,EAAE;QAC/E,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE;YACtB,WAAW,EAAE,4BAA4B,CAAC,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC;SACrE,CAAC,CAAC;QAEH,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC;QAEtD,MAAM,OAAO,CAAC,GAAG,EAAE;YACjB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC3C,CAAC,CAAC,CAAC;QAEH,GAAG,CAAC,GAAG,EAAE;YACP,SAAS,CACP,MAAM,EACN,IAAI,YAAY,CAAC,SAAS,EAAE;gBAC1B,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,IAAI,EAAE;oBACJ,OAAO,EAAE,KAAK;oBACd,MAAM,EAAE,uCAAuC;oBAC/C,MAAM,EAAE,EAAE,WAAW,EAAE,YAAY,EAAE;iBACtC;aACF,CAAC,CACH,CAAC;QACJ,CAAC,CAAC,CAAC;QAEH,MAAM,OAAO,CAAC,GAAG,EAAE;YACjB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC/C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6EAA6E,EAAE,KAAK,IAAI,EAAE;QAC3F,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE;YACtB,WAAW,EAAE,4BAA4B,CAAC,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC;SACrE,CAAC,CAAC;QAEH,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC;QAEtD,MAAM,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;QAE9D,kDAAkD;QAClD,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG;YACd,kBAAkB,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;SACtE,CAAC;QACF,wDAAwD;QACvD,OAAe,CAAC,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;QAEjE,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE;YACnB,MAAM,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;QACxC,CAAC,CAAC,CAAC;QAEH,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC,oBAAoB,CAAC;YACtD,IAAI,EAAE,YAAY;SACnB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { act, fireEvent, renderHook, waitFor } from \"@testing-library/react\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\nimport { HostAdaptor } from \"../bridges/adaptor.js\";\nimport { AppsSdkBridge } from \"../bridges/apps-sdk/bridge.js\";\nimport { getAdaptor } from \"../bridges/get-adaptor.js\";\nimport { McpAppBridge } from \"../bridges/mcp-app/bridge.js\";\nimport {\n getMcpAppHostPostMessageMock,\n MockResizeObserver,\n} from \"./test/utils.js\";\nimport { useDisplayMode } from \"./use-display-mode.js\";\n\ndescribe(\"useDisplayMode\", () => {\n beforeEach(() => {\n HostAdaptor.resetInstance();\n McpAppBridge.resetInstance();\n AppsSdkBridge.resetInstance();\n vi.stubGlobal(\"skybridge\", {});\n vi.stubGlobal(\"openai\", { view: { mode: \"inline\" } });\n vi.stubGlobal(\"ResizeObserver\", MockResizeObserver);\n });\n\n afterEach(() => {\n vi.unstubAllGlobals();\n vi.resetAllMocks();\n HostAdaptor.resetInstance();\n McpAppBridge.resetInstance();\n AppsSdkBridge.resetInstance();\n });\n\n it(\"should return the current display mode from MCP host context\", async () => {\n vi.stubGlobal(\"parent\", {\n postMessage: getMcpAppHostPostMessageMock({ displayMode: \"inline\" }),\n });\n\n const { result } = renderHook(() => useDisplayMode());\n\n await waitFor(() => {\n expect(result.current[0]).toBe(\"inline\");\n });\n });\n\n it(\"should update display mode on host-context-changed notification\", async () => {\n vi.stubGlobal(\"parent\", {\n postMessage: getMcpAppHostPostMessageMock({ displayMode: \"inline\" }),\n });\n\n const { result } = renderHook(() => useDisplayMode());\n\n await waitFor(() => {\n expect(result.current[0]).toBe(\"inline\");\n });\n\n act(() => {\n fireEvent(\n window,\n new MessageEvent(\"message\", {\n source: window.parent,\n data: {\n jsonrpc: \"2.0\",\n method: \"ui/notifications/host-context-changed\",\n params: { displayMode: \"fullscreen\" },\n },\n }),\n );\n });\n\n await waitFor(() => {\n expect(result.current[0]).toBe(\"fullscreen\");\n });\n });\n\n it(\"should call requestDisplayMode on the adaptor when setDisplayMode is called\", async () => {\n vi.stubGlobal(\"parent\", {\n postMessage: getMcpAppHostPostMessageMock({ displayMode: \"inline\" }),\n });\n\n const { result } = renderHook(() => useDisplayMode());\n\n await waitFor(() => expect(result.current[0]).toBe(\"inline\"));\n\n // Patch mcp.getApp to avoid real protocol request\n const adaptor = getAdaptor();\n const fakeApp = {\n requestDisplayMode: vi.fn().mockResolvedValue({ mode: \"fullscreen\" }),\n };\n // biome-ignore lint/suspicious/noExplicitAny: test seam\n (adaptor as any).mcp.getApp = vi.fn().mockResolvedValue(fakeApp);\n\n await act(async () => {\n await result.current[1](\"fullscreen\");\n });\n\n expect(fakeApp.requestDisplayMode).toHaveBeenCalledWith({\n mode: \"fullscreen\",\n });\n });\n});\n"]}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
import type { DownloadParams, DownloadResult } from "../bridges/types.js";
|
|
2
2
|
export type DownloadFn = (params: DownloadParams) => Promise<DownloadResult>;
|
|
3
|
-
export declare function useDownload():
|
|
4
|
-
download: DownloadFn;
|
|
5
|
-
};
|
|
3
|
+
export declare function useDownload(): DownloadFn;
|
|
@@ -2,7 +2,6 @@ import { useCallback } from "react";
|
|
|
2
2
|
import { getAdaptor } from "../bridges/index.js";
|
|
3
3
|
export function useDownload() {
|
|
4
4
|
const adaptor = getAdaptor();
|
|
5
|
-
|
|
6
|
-
return { download };
|
|
5
|
+
return useCallback((params) => adaptor.download(params), [adaptor]);
|
|
7
6
|
}
|
|
8
7
|
//# sourceMappingURL=use-download.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-download.js","sourceRoot":"","sources":["../../../src/web/hooks/use-download.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAKjD,MAAM,UAAU,WAAW;IACzB,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,
|
|
1
|
+
{"version":3,"file":"use-download.js","sourceRoot":"","sources":["../../../src/web/hooks/use-download.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAKjD,MAAM,UAAU,WAAW;IACzB,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,OAAO,WAAW,CAChB,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,EACpC,CAAC,OAAO,CAAC,CACV,CAAC;AACJ,CAAC","sourcesContent":["import { useCallback } from \"react\";\nimport { getAdaptor } from \"../bridges/index.js\";\nimport type { DownloadParams, DownloadResult } from \"../bridges/types.js\";\n\nexport type DownloadFn = (params: DownloadParams) => Promise<DownloadResult>;\n\nexport function useDownload(): DownloadFn {\n const adaptor = getAdaptor();\n return useCallback<DownloadFn>(\n (params) => adaptor.download(params),\n [adaptor],\n );\n}\n"]}
|
|
@@ -20,7 +20,7 @@ describe("useDownload", () => {
|
|
|
20
20
|
describe("apps-sdk host", () => {
|
|
21
21
|
beforeEach(() => {
|
|
22
22
|
vi.stubGlobal("openai", {});
|
|
23
|
-
vi.stubGlobal("skybridge", {
|
|
23
|
+
vi.stubGlobal("skybridge", {});
|
|
24
24
|
vi.stubGlobal("ResizeObserver", MockResizeObserver);
|
|
25
25
|
vi.stubGlobal("parent", { postMessage: getMcpAppHostPostMessageMock() });
|
|
26
26
|
});
|
|
@@ -33,7 +33,7 @@ describe("useDownload", () => {
|
|
|
33
33
|
it("returns { isError: true } and logs an error", async () => {
|
|
34
34
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
35
35
|
const { result } = renderHook(() => useDownload());
|
|
36
|
-
const res = await result.current
|
|
36
|
+
const res = await result.current(params);
|
|
37
37
|
expect(res).toEqual({ isError: true });
|
|
38
38
|
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("does not support ui/download-file"));
|
|
39
39
|
});
|
|
@@ -41,7 +41,7 @@ describe("useDownload", () => {
|
|
|
41
41
|
describe("mcp-app host without downloadFile capability", () => {
|
|
42
42
|
beforeEach(() => {
|
|
43
43
|
vi.stubGlobal("openai", undefined);
|
|
44
|
-
vi.stubGlobal("skybridge", {
|
|
44
|
+
vi.stubGlobal("skybridge", {});
|
|
45
45
|
vi.stubGlobal("ResizeObserver", MockResizeObserver);
|
|
46
46
|
vi.stubGlobal("parent", { postMessage: getMcpAppHostPostMessageMock() });
|
|
47
47
|
});
|
|
@@ -54,7 +54,7 @@ describe("useDownload", () => {
|
|
|
54
54
|
it("returns { isError: true } and logs an error", async () => {
|
|
55
55
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
56
56
|
const { result } = renderHook(() => useDownload());
|
|
57
|
-
const res = await result.current
|
|
57
|
+
const res = await result.current(params);
|
|
58
58
|
expect(res).toEqual({ isError: true });
|
|
59
59
|
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("does not support ui/download-file"));
|
|
60
60
|
});
|
|
@@ -63,7 +63,7 @@ describe("useDownload", () => {
|
|
|
63
63
|
let postMessageMock;
|
|
64
64
|
beforeEach(() => {
|
|
65
65
|
vi.stubGlobal("openai", undefined);
|
|
66
|
-
vi.stubGlobal("skybridge", {
|
|
66
|
+
vi.stubGlobal("skybridge", {});
|
|
67
67
|
vi.stubGlobal("ResizeObserver", MockResizeObserver);
|
|
68
68
|
postMessageMock = getMcpAppHostPostMessageMock({}, { hostCapabilities: { downloadFile: {} } });
|
|
69
69
|
vi.stubGlobal("parent", { postMessage: postMessageMock });
|
|
@@ -76,7 +76,7 @@ describe("useDownload", () => {
|
|
|
76
76
|
});
|
|
77
77
|
it("sends ui/download-file with the provided contents", async () => {
|
|
78
78
|
const { result } = renderHook(() => useDownload());
|
|
79
|
-
const res = await result.current
|
|
79
|
+
const res = await result.current(params);
|
|
80
80
|
expect(res).toEqual({});
|
|
81
81
|
await waitFor(() => {
|
|
82
82
|
expect(postMessageMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
@@ -95,7 +95,7 @@ describe("useDownload", () => {
|
|
|
95
95
|
});
|
|
96
96
|
vi.stubGlobal("parent", { postMessage: postMessageMock });
|
|
97
97
|
const { result } = renderHook(() => useDownload());
|
|
98
|
-
const res = await result.current
|
|
98
|
+
const res = await result.current(params);
|
|
99
99
|
expect(res).toEqual({ isError: true });
|
|
100
100
|
});
|
|
101
101
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-download.test.js","sourceRoot":"","sources":["../../../src/web/hooks/use-download.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAE5D,OAAO,EACL,4BAA4B,EAC5B,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,MAAM,MAAM,GAAmB;IAC7B,QAAQ,EAAE;QACR;YACE,IAAI,EAAE,UAAU;YAChB,QAAQ,EAAE;gBACR,GAAG,EAAE,qBAAqB;gBAC1B,QAAQ,EAAE,kBAAkB;gBAC5B,IAAI,EAAE,mBAAmB;aAC1B;SACF;KACF;CACF,CAAC;AAEF,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;QAC7B,UAAU,CAAC,GAAG,EAAE;YACd,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;YAC5B,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,EAAE,
|
|
1
|
+
{"version":3,"file":"use-download.test.js","sourceRoot":"","sources":["../../../src/web/hooks/use-download.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAE5D,OAAO,EACL,4BAA4B,EAC5B,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,MAAM,MAAM,GAAmB;IAC7B,QAAQ,EAAE;QACR;YACE,IAAI,EAAE,UAAU;YAChB,QAAQ,EAAE;gBACR,GAAG,EAAE,qBAAqB;gBAC1B,QAAQ,EAAE,kBAAkB;gBAC5B,IAAI,EAAE,mBAAmB;aAC1B;SACF;KACF;CACF,CAAC;AAEF,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;QAC7B,UAAU,CAAC,GAAG,EAAE;YACd,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;YAC5B,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;YAC/B,EAAE,CAAC,UAAU,CAAC,gBAAgB,EAAE,kBAAkB,CAAC,CAAC;YACpD,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,WAAW,EAAE,4BAA4B,EAAE,EAAE,CAAC,CAAC;QAC3E,CAAC,CAAC,CAAC;QAEH,SAAS,CAAC,GAAG,EAAE;YACb,EAAE,CAAC,gBAAgB,EAAE,CAAC;YACtB,EAAE,CAAC,aAAa,EAAE,CAAC;YACnB,WAAW,CAAC,aAAa,EAAE,CAAC;YAC5B,YAAY,CAAC,aAAa,EAAE,CAAC;QAC/B,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;YAC3D,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,kBAAkB,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YACzE,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;YAEnD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YAEzC,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACvC,MAAM,CAAC,QAAQ,CAAC,CAAC,oBAAoB,CACnC,MAAM,CAAC,gBAAgB,CAAC,mCAAmC,CAAC,CAC7D,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,8CAA8C,EAAE,GAAG,EAAE;QAC5D,UAAU,CAAC,GAAG,EAAE;YACd,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;YACnC,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;YAC/B,EAAE,CAAC,UAAU,CAAC,gBAAgB,EAAE,kBAAkB,CAAC,CAAC;YACpD,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,WAAW,EAAE,4BAA4B,EAAE,EAAE,CAAC,CAAC;QAC3E,CAAC,CAAC,CAAC;QAEH,SAAS,CAAC,KAAK,IAAI,EAAE;YACnB,EAAE,CAAC,gBAAgB,EAAE,CAAC;YACtB,EAAE,CAAC,aAAa,EAAE,CAAC;YACnB,WAAW,CAAC,aAAa,EAAE,CAAC;YAC5B,YAAY,CAAC,aAAa,EAAE,CAAC;QAC/B,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;YAC3D,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,kBAAkB,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YACzE,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;YAEnD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YAEzC,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACvC,MAAM,CAAC,QAAQ,CAAC,CAAC,oBAAoB,CACnC,MAAM,CAAC,gBAAgB,CAAC,mCAAmC,CAAC,CAC7D,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACzD,IAAI,eAAgE,CAAC;QAErE,UAAU,CAAC,GAAG,EAAE;YACd,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;YACnC,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;YAC/B,EAAE,CAAC,UAAU,CAAC,gBAAgB,EAAE,kBAAkB,CAAC,CAAC;YACpD,eAAe,GAAG,4BAA4B,CAC5C,EAAE,EACF,EAAE,gBAAgB,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,EAAE,CAC3C,CAAC;YACF,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,WAAW,EAAE,eAAe,EAAE,CAAC,CAAC;QAC5D,CAAC,CAAC,CAAC;QAEH,SAAS,CAAC,KAAK,IAAI,EAAE;YACnB,EAAE,CAAC,gBAAgB,EAAE,CAAC;YACtB,EAAE,CAAC,aAAa,EAAE,CAAC;YACnB,WAAW,CAAC,aAAa,EAAE,CAAC;YAC5B,YAAY,CAAC,aAAa,EAAE,CAAC;QAC/B,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;YACjE,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;YAEnD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YAEzC,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACxB,MAAM,OAAO,CAAC,GAAG,EAAE;gBACjB,MAAM,CAAC,eAAe,CAAC,CAAC,oBAAoB,CAC1C,MAAM,CAAC,gBAAgB,CAAC;oBACtB,OAAO,EAAE,KAAK;oBACd,MAAM,EAAE,kBAAkB;oBAC1B,MAAM;iBACP,CAAC,EACF,GAAG,CACJ,CAAC;YACJ,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,4DAA4D,EAAE,KAAK,IAAI,EAAE;YAC1E,WAAW,CAAC,aAAa,EAAE,CAAC;YAC5B,YAAY,CAAC,aAAa,EAAE,CAAC;YAC7B,eAAe,GAAG,4BAA4B,CAC5C,EAAE,EACF;gBACE,gBAAgB,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE;gBACtC,kBAAkB,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;aACtC,CACF,CAAC;YACF,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,WAAW,EAAE,eAAe,EAAE,CAAC,CAAC;YAE1D,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;YAEnD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YAEzC,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { renderHook, waitFor } from \"@testing-library/react\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\nimport { HostAdaptor } from \"../bridges/adaptor.js\";\nimport { McpAppBridge } from \"../bridges/mcp-app/bridge.js\";\nimport type { DownloadParams } from \"../bridges/types.js\";\nimport {\n getMcpAppHostPostMessageMock,\n MockResizeObserver,\n} from \"./test/utils.js\";\nimport { useDownload } from \"./use-download.js\";\n\nconst params: DownloadParams = {\n contents: [\n {\n type: \"resource\",\n resource: {\n uri: \"file:///export.json\",\n mimeType: \"application/json\",\n text: '{\"hello\":\"world\"}',\n },\n },\n ],\n};\n\ndescribe(\"useDownload\", () => {\n describe(\"apps-sdk host\", () => {\n beforeEach(() => {\n vi.stubGlobal(\"openai\", {});\n vi.stubGlobal(\"skybridge\", {});\n vi.stubGlobal(\"ResizeObserver\", MockResizeObserver);\n vi.stubGlobal(\"parent\", { postMessage: getMcpAppHostPostMessageMock() });\n });\n\n afterEach(() => {\n vi.unstubAllGlobals();\n vi.resetAllMocks();\n HostAdaptor.resetInstance();\n McpAppBridge.resetInstance();\n });\n\n it(\"returns { isError: true } and logs an error\", async () => {\n const errorSpy = vi.spyOn(console, \"error\").mockImplementation(() => {});\n const { result } = renderHook(() => useDownload());\n\n const res = await result.current(params);\n\n expect(res).toEqual({ isError: true });\n expect(errorSpy).toHaveBeenCalledWith(\n expect.stringContaining(\"does not support ui/download-file\"),\n );\n });\n });\n\n describe(\"mcp-app host without downloadFile capability\", () => {\n beforeEach(() => {\n vi.stubGlobal(\"openai\", undefined);\n vi.stubGlobal(\"skybridge\", {});\n vi.stubGlobal(\"ResizeObserver\", MockResizeObserver);\n vi.stubGlobal(\"parent\", { postMessage: getMcpAppHostPostMessageMock() });\n });\n\n afterEach(async () => {\n vi.unstubAllGlobals();\n vi.resetAllMocks();\n HostAdaptor.resetInstance();\n McpAppBridge.resetInstance();\n });\n\n it(\"returns { isError: true } and logs an error\", async () => {\n const errorSpy = vi.spyOn(console, \"error\").mockImplementation(() => {});\n const { result } = renderHook(() => useDownload());\n\n const res = await result.current(params);\n\n expect(res).toEqual({ isError: true });\n expect(errorSpy).toHaveBeenCalledWith(\n expect.stringContaining(\"does not support ui/download-file\"),\n );\n });\n });\n\n describe(\"mcp-app host with downloadFile capability\", () => {\n let postMessageMock: ReturnType<typeof getMcpAppHostPostMessageMock>;\n\n beforeEach(() => {\n vi.stubGlobal(\"openai\", undefined);\n vi.stubGlobal(\"skybridge\", {});\n vi.stubGlobal(\"ResizeObserver\", MockResizeObserver);\n postMessageMock = getMcpAppHostPostMessageMock(\n {},\n { hostCapabilities: { downloadFile: {} } },\n );\n vi.stubGlobal(\"parent\", { postMessage: postMessageMock });\n });\n\n afterEach(async () => {\n vi.unstubAllGlobals();\n vi.resetAllMocks();\n HostAdaptor.resetInstance();\n McpAppBridge.resetInstance();\n });\n\n it(\"sends ui/download-file with the provided contents\", async () => {\n const { result } = renderHook(() => useDownload());\n\n const res = await result.current(params);\n\n expect(res).toEqual({});\n await waitFor(() => {\n expect(postMessageMock).toHaveBeenCalledWith(\n expect.objectContaining({\n jsonrpc: \"2.0\",\n method: \"ui/download-file\",\n params,\n }),\n \"*\",\n );\n });\n });\n\n it(\"returns { isError: true } when the host denies the request\", async () => {\n HostAdaptor.resetInstance();\n McpAppBridge.resetInstance();\n postMessageMock = getMcpAppHostPostMessageMock(\n {},\n {\n hostCapabilities: { downloadFile: {} },\n downloadFileResult: { isError: true },\n },\n );\n vi.stubGlobal(\"parent\", { postMessage: postMessageMock });\n\n const { result } = renderHook(() => useDownload());\n\n const res = await result.current(params);\n\n expect(res).toEqual({ isError: true });\n });\n });\n});\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type FileResource, type FileResourceContent, type FileResourceWriteResult, type ResourceRepresentation } from "../bridges/index.js";
|
|
2
|
+
/** State and actions returned by {@link useFileResource}. */
|
|
3
|
+
export type FileResourceState = {
|
|
4
|
+
data: FileResource | undefined;
|
|
5
|
+
isLoading: boolean;
|
|
6
|
+
error: unknown;
|
|
7
|
+
write: (content: FileResourceContent) => Promise<FileResourceWriteResult>;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Read a resource by URI, re-read it whenever the host reports a change, and
|
|
11
|
+
* write it back. Built for the file a ChatGPT file entrypoint opened, whose
|
|
12
|
+
* `resourceUri` arrives in the tool input as `file.resourceUri`.
|
|
13
|
+
*
|
|
14
|
+
* Reading uses the standard `resources/read`. `representation` asks ChatGPT
|
|
15
|
+
* for text or a base64 blob. `write` uses the OpenAI MCP extensions
|
|
16
|
+
* (`openai/resources/write`) and sends the current `etag` as `ifMatch`, so it
|
|
17
|
+
* resolves with `conflict` instead of overwriting a newer version, and then
|
|
18
|
+
* re-reads the latest one. When ChatGPT returned no `etag`, the write has no
|
|
19
|
+
* version check. It rejects with `NotSupportedError` when the host doesn't
|
|
20
|
+
* advertise `openai/resource`, or the resource isn't loaded or writable.
|
|
21
|
+
*
|
|
22
|
+
* Pass `undefined` or an empty string as `uri` to do nothing, for example outside a file
|
|
23
|
+
* entrypoint.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* const { input } = useToolInfo<"open-part">();
|
|
28
|
+
* const { data, write } = useFileResource(input.file?.resourceUri, {
|
|
29
|
+
* representation: "text",
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @see https://docs.skybridge.tech/api-reference/use-file-resource
|
|
34
|
+
*/
|
|
35
|
+
export declare function useFileResource(uri: string | undefined, options?: {
|
|
36
|
+
representation?: ResourceRepresentation;
|
|
37
|
+
}): FileResourceState;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { getAdaptor, NotSupportedError, } from "../bridges/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Read a resource by URI, re-read it whenever the host reports a change, and
|
|
5
|
+
* write it back. Built for the file a ChatGPT file entrypoint opened, whose
|
|
6
|
+
* `resourceUri` arrives in the tool input as `file.resourceUri`.
|
|
7
|
+
*
|
|
8
|
+
* Reading uses the standard `resources/read`. `representation` asks ChatGPT
|
|
9
|
+
* for text or a base64 blob. `write` uses the OpenAI MCP extensions
|
|
10
|
+
* (`openai/resources/write`) and sends the current `etag` as `ifMatch`, so it
|
|
11
|
+
* resolves with `conflict` instead of overwriting a newer version, and then
|
|
12
|
+
* re-reads the latest one. When ChatGPT returned no `etag`, the write has no
|
|
13
|
+
* version check. It rejects with `NotSupportedError` when the host doesn't
|
|
14
|
+
* advertise `openai/resource`, or the resource isn't loaded or writable.
|
|
15
|
+
*
|
|
16
|
+
* Pass `undefined` or an empty string as `uri` to do nothing, for example outside a file
|
|
17
|
+
* entrypoint.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* const { input } = useToolInfo<"open-part">();
|
|
22
|
+
* const { data, write } = useFileResource(input.file?.resourceUri, {
|
|
23
|
+
* representation: "text",
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @see https://docs.skybridge.tech/api-reference/use-file-resource
|
|
28
|
+
*/
|
|
29
|
+
export function useFileResource(uri, options) {
|
|
30
|
+
const representation = options?.representation;
|
|
31
|
+
const [state, setState] = useState();
|
|
32
|
+
const sequence = useRef(0);
|
|
33
|
+
const current = uri && state?.uri === uri ? state : undefined;
|
|
34
|
+
const currentRef = useRef(current);
|
|
35
|
+
currentRef.current = current;
|
|
36
|
+
const read = useCallback(() => {
|
|
37
|
+
if (!uri) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const id = ++sequence.current;
|
|
41
|
+
getAdaptor()
|
|
42
|
+
.readResource(uri, representation)
|
|
43
|
+
.then((resource) => ({ resource, error: undefined }), (error) => ({ error }))
|
|
44
|
+
.then((next) => {
|
|
45
|
+
if (id === sequence.current) {
|
|
46
|
+
setState((previous) => ({
|
|
47
|
+
...(previous?.uri === uri && previous),
|
|
48
|
+
uri,
|
|
49
|
+
...next,
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}, [uri, representation]);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!uri) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
read();
|
|
59
|
+
const stopWatching = getAdaptor().watchResource(uri, read);
|
|
60
|
+
return () => {
|
|
61
|
+
sequence.current++;
|
|
62
|
+
stopWatching();
|
|
63
|
+
};
|
|
64
|
+
}, [uri, read]);
|
|
65
|
+
const write = useCallback(async (content) => {
|
|
66
|
+
const resource = currentRef.current?.resource;
|
|
67
|
+
if (!uri || currentRef.current?.uri !== uri || !resource?.writable) {
|
|
68
|
+
throw new NotSupportedError("writeResource", "the resource is not loaded or not writable");
|
|
69
|
+
}
|
|
70
|
+
const result = await getAdaptor().writeResource(uri, content, resource.etag);
|
|
71
|
+
if (currentRef.current?.uri !== uri) {
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
if (result.outcome === "saved") {
|
|
75
|
+
sequence.current++;
|
|
76
|
+
setState({
|
|
77
|
+
uri,
|
|
78
|
+
resource: {
|
|
79
|
+
...content,
|
|
80
|
+
mimeType: resource.mimeType,
|
|
81
|
+
writable: true,
|
|
82
|
+
etag: result.etag,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
else if (result.outcome === "conflict") {
|
|
87
|
+
read();
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}, [uri, read]);
|
|
91
|
+
return {
|
|
92
|
+
data: current?.resource,
|
|
93
|
+
isLoading: Boolean(uri) && current === undefined,
|
|
94
|
+
error: current?.error,
|
|
95
|
+
write,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=use-file-resource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-file-resource.js","sourceRoot":"","sources":["../../../src/web/hooks/use-file-resource.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n type FileResource,\n type FileResourceContent,\n type FileResourceWriteResult,\n getAdaptor,\n NotSupportedError,\n type ResourceRepresentation,\n} from \"../bridges/index.js\";\n\n/** State and actions returned by {@link useFileResource}. */\nexport type FileResourceState = {\n data: FileResource | undefined;\n isLoading: boolean;\n error: unknown;\n write: (content: FileResourceContent) => Promise<FileResourceWriteResult>;\n};\n\n/**\n * Read a resource by URI, re-read it whenever the host reports a change, and\n * write it back. Built for the file a ChatGPT file entrypoint opened, whose\n * `resourceUri` arrives in the tool input as `file.resourceUri`.\n *\n * Reading uses the standard `resources/read`. `representation` asks ChatGPT\n * for text or a base64 blob. `write` uses the OpenAI MCP extensions\n * (`openai/resources/write`) and sends the current `etag` as `ifMatch`, so it\n * resolves with `conflict` instead of overwriting a newer version, and then\n * re-reads the latest one. When ChatGPT returned no `etag`, the write has no\n * version check. It rejects with `NotSupportedError` when the host doesn't\n * advertise `openai/resource`, or the resource isn't loaded or writable.\n *\n * Pass `undefined` or an empty string as `uri` to do nothing, for example outside a file\n * entrypoint.\n *\n * @example\n * ```tsx\n * const { input } = useToolInfo<\"open-part\">();\n * const { data, write } = useFileResource(input.file?.resourceUri, {\n * representation: \"text\",\n * });\n * ```\n *\n * @see https://docs.skybridge.tech/api-reference/use-file-resource\n */\nexport function useFileResource(\n uri: string | undefined,\n options?: { representation?: ResourceRepresentation },\n): FileResourceState {\n const representation = options?.representation;\n const [state, setState] = useState<{\n uri: string;\n resource?: FileResource;\n error?: unknown;\n }>();\n const sequence = useRef(0);\n const current = uri && state?.uri === uri ? state : undefined;\n const currentRef = useRef(current);\n currentRef.current = current;\n\n const read = useCallback(() => {\n if (!uri) {\n return;\n }\n const id = ++sequence.current;\n getAdaptor()\n .readResource(uri, representation)\n .then(\n (resource) => ({ resource, error: undefined }),\n (error: unknown) => ({ error }),\n )\n .then((next) => {\n if (id === sequence.current) {\n setState((previous) => ({\n ...(previous?.uri === uri && previous),\n uri,\n ...next,\n }));\n }\n });\n }, [uri, representation]);\n\n useEffect(() => {\n if (!uri) {\n return;\n }\n read();\n const stopWatching = getAdaptor().watchResource(uri, read);\n return () => {\n sequence.current++;\n stopWatching();\n };\n }, [uri, read]);\n\n const write = useCallback(\n async (content: FileResourceContent) => {\n const resource = currentRef.current?.resource;\n if (!uri || currentRef.current?.uri !== uri || !resource?.writable) {\n throw new NotSupportedError(\n \"writeResource\",\n \"the resource is not loaded or not writable\",\n );\n }\n const result = await getAdaptor().writeResource(\n uri,\n content,\n resource.etag,\n );\n if (currentRef.current?.uri !== uri) {\n return result;\n }\n if (result.outcome === \"saved\") {\n sequence.current++;\n setState({\n uri,\n resource: {\n ...content,\n mimeType: resource.mimeType,\n writable: true,\n etag: result.etag,\n },\n });\n } else if (result.outcome === \"conflict\") {\n read();\n }\n return result;\n },\n [uri, read],\n );\n\n return {\n data: current?.resource,\n isLoading: Boolean(uri) && current === undefined,\n error: current?.error,\n write,\n };\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { HostAdaptor } from "../bridges/adaptor.js";
|
|
4
|
+
import { getAdaptor } from "../bridges/get-adaptor.js";
|
|
5
|
+
import { McpAppBridge } from "../bridges/mcp-app/bridge.js";
|
|
6
|
+
import { useFileResource } from "./use-file-resource.js";
|
|
7
|
+
describe("useFileResource", () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.stubGlobal("parent", { postMessage: vi.fn() });
|
|
10
|
+
vi.stubGlobal("skybridge", {});
|
|
11
|
+
vi.stubGlobal("openai", undefined);
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
vi.unstubAllGlobals();
|
|
15
|
+
HostAdaptor.resetInstance();
|
|
16
|
+
McpAppBridge.resetInstance();
|
|
17
|
+
});
|
|
18
|
+
it("reads the resource, re-reads it on update and writes with the current etag", async () => {
|
|
19
|
+
let version = 1;
|
|
20
|
+
let onUpdated = () => { };
|
|
21
|
+
const request = vi.fn(async ({ method }) => method === "openai/resources/write"
|
|
22
|
+
? { outcome: "saved", etag: "v3" }
|
|
23
|
+
: {});
|
|
24
|
+
const app = {
|
|
25
|
+
getHostCapabilities: () => ({ experimental: { "openai/resource": {} } }),
|
|
26
|
+
readServerResource: vi.fn(async ({ uri }) => ({
|
|
27
|
+
contents: [
|
|
28
|
+
{
|
|
29
|
+
uri,
|
|
30
|
+
text: `version ${version}`,
|
|
31
|
+
_meta: {
|
|
32
|
+
"openai/resource": { writable: true, etag: `v${version}` },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
})),
|
|
37
|
+
setNotificationHandler: (_method, handler) => {
|
|
38
|
+
onUpdated = handler;
|
|
39
|
+
},
|
|
40
|
+
request,
|
|
41
|
+
};
|
|
42
|
+
getAdaptor().mcp.getApp = () => Promise.resolve(app);
|
|
43
|
+
const { result } = renderHook(() => useFileResource("host-resource://part", { representation: "text" }));
|
|
44
|
+
await waitFor(() => expect(result.current.data).toMatchObject({
|
|
45
|
+
text: "version 1",
|
|
46
|
+
etag: "v1",
|
|
47
|
+
}));
|
|
48
|
+
await waitFor(() => expect(request).toHaveBeenCalledWith({
|
|
49
|
+
method: "resources/subscribe",
|
|
50
|
+
params: { uri: "host-resource://part" },
|
|
51
|
+
}, expect.anything()));
|
|
52
|
+
version = 2;
|
|
53
|
+
act(() => onUpdated({ params: { uri: "host-resource://part" } }));
|
|
54
|
+
await waitFor(() => expect(result.current.data).toMatchObject({ etag: "v2" }));
|
|
55
|
+
await act(async () => {
|
|
56
|
+
await result.current.write({ text: "edited" });
|
|
57
|
+
});
|
|
58
|
+
expect(request).toHaveBeenCalledWith({
|
|
59
|
+
method: "openai/resources/write",
|
|
60
|
+
params: { uri: "host-resource://part", text: "edited", ifMatch: "v2" },
|
|
61
|
+
}, expect.anything());
|
|
62
|
+
expect(result.current.data).toMatchObject({ text: "edited", etag: "v3" });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
//# sourceMappingURL=use-file-resource.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-file-resource.test.js","sourceRoot":"","sources":["../../../src/web/hooks/use-file-resource.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { act, renderHook, waitFor } from \"@testing-library/react\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\nimport { HostAdaptor } from \"../bridges/adaptor.js\";\nimport { getAdaptor } from \"../bridges/get-adaptor.js\";\nimport { McpAppBridge } from \"../bridges/mcp-app/bridge.js\";\nimport { useFileResource } from \"./use-file-resource.js\";\n\ndescribe(\"useFileResource\", () => {\n beforeEach(() => {\n vi.stubGlobal(\"parent\", { postMessage: vi.fn() });\n vi.stubGlobal(\"skybridge\", {});\n vi.stubGlobal(\"openai\", undefined);\n });\n\n afterEach(() => {\n vi.unstubAllGlobals();\n HostAdaptor.resetInstance();\n McpAppBridge.resetInstance();\n });\n\n it(\"reads the resource, re-reads it on update and writes with the current etag\", async () => {\n let version = 1;\n let onUpdated: (notification: { params: { uri: string } }) => void =\n () => {};\n const request = vi.fn(async ({ method }: { method: string }) =>\n method === \"openai/resources/write\"\n ? { outcome: \"saved\", etag: \"v3\" }\n : {},\n );\n const app = {\n getHostCapabilities: () => ({ experimental: { \"openai/resource\": {} } }),\n readServerResource: vi.fn(async ({ uri }: { uri: string }) => ({\n contents: [\n {\n uri,\n text: `version ${version}`,\n _meta: {\n \"openai/resource\": { writable: true, etag: `v${version}` },\n },\n },\n ],\n })),\n setNotificationHandler: (_method: string, handler: typeof onUpdated) => {\n onUpdated = handler;\n },\n request,\n };\n (\n getAdaptor() as unknown as { mcp: { getApp: () => Promise<object> } }\n ).mcp.getApp = () => Promise.resolve(app);\n\n const { result } = renderHook(() =>\n useFileResource(\"host-resource://part\", { representation: \"text\" }),\n );\n await waitFor(() =>\n expect(result.current.data).toMatchObject({\n text: \"version 1\",\n etag: \"v1\",\n }),\n );\n await waitFor(() =>\n expect(request).toHaveBeenCalledWith(\n {\n method: \"resources/subscribe\",\n params: { uri: \"host-resource://part\" },\n },\n expect.anything(),\n ),\n );\n\n version = 2;\n act(() => onUpdated({ params: { uri: \"host-resource://part\" } }));\n await waitFor(() =>\n expect(result.current.data).toMatchObject({ etag: \"v2\" }),\n );\n\n await act(async () => {\n await result.current.write({ text: \"edited\" });\n });\n expect(request).toHaveBeenCalledWith(\n {\n method: \"openai/resources/write\",\n params: { uri: \"host-resource://part\", text: \"edited\", ifMatch: \"v2\" },\n },\n expect.anything(),\n );\n expect(result.current.data).toMatchObject({ text: \"edited\", etag: \"v3\" });\n });\n});\n"]}
|
|
@@ -17,7 +17,7 @@ describe("useFiles", () => {
|
|
|
17
17
|
HostAdaptor.resetInstance();
|
|
18
18
|
McpAppBridge.resetInstance();
|
|
19
19
|
AppsSdkBridge.resetInstance();
|
|
20
|
-
vi.stubGlobal("skybridge", {
|
|
20
|
+
vi.stubGlobal("skybridge", {});
|
|
21
21
|
vi.stubGlobal("openai", OpenaiMock);
|
|
22
22
|
vi.stubGlobal("parent", { postMessage: vi.fn() });
|
|
23
23
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-files.test.js","sourceRoot":"","sources":["../../../src/web/hooks/use-files.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;IACxB,MAAM,UAAU,GAA4B;QAC1C,UAAU,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC;YACpC,MAAM,EAAE,wBAAwB;SACjC,CAAC;QACF,kBAAkB,EAAE,EAAE,CAAC,EAAE,EAAE;QAC3B,WAAW,EAAE,IAAI;QACjB,cAAc,EAAE,EAAE,CAAC,EAAE,EAAE;KACxB,CAAC;IAEF,UAAU,CAAC,GAAG,EAAE;QACd,WAAW,CAAC,aAAa,EAAE,CAAC;QAC5B,YAAY,CAAC,aAAa,EAAE,CAAC;QAC7B,aAAa,CAAC,aAAa,EAAE,CAAC;QAC9B,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,EAAE,
|
|
1
|
+
{"version":3,"file":"use-files.test.js","sourceRoot":"","sources":["../../../src/web/hooks/use-files.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { renderHook } from \"@testing-library/react\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\nimport { HostAdaptor } from \"../bridges/adaptor.js\";\nimport { AppsSdkBridge } from \"../bridges/apps-sdk/bridge.js\";\nimport { McpAppBridge } from \"../bridges/mcp-app/bridge.js\";\nimport { useFiles } from \"./use-files.js\";\n\ndescribe(\"useFiles\", () => {\n const OpenaiMock: Record<string, unknown> = {\n uploadFile: vi.fn().mockResolvedValue({\n fileId: `sediment://file_abc123`,\n }),\n getFileDownloadUrl: vi.fn(),\n widgetState: null,\n setWidgetState: vi.fn(),\n };\n\n beforeEach(() => {\n HostAdaptor.resetInstance();\n McpAppBridge.resetInstance();\n AppsSdkBridge.resetInstance();\n vi.stubGlobal(\"skybridge\", {});\n vi.stubGlobal(\"openai\", OpenaiMock);\n vi.stubGlobal(\"parent\", { postMessage: vi.fn() });\n });\n\n afterEach(() => {\n vi.unstubAllGlobals();\n vi.clearAllMocks();\n HostAdaptor.resetInstance();\n McpAppBridge.resetInstance();\n AppsSdkBridge.resetInstance();\n });\n\n const dummyFile = new File([], \"test.txt\");\n\n it(\"should upload a file to ChatGPT\", () => {\n const { result } = renderHook(() => useFiles());\n\n result.current.upload(dummyFile);\n expect(OpenaiMock.uploadFile).toHaveBeenCalledWith(dummyFile, undefined);\n });\n\n it(\"should upload a file with library option\", () => {\n const { result } = renderHook(() => useFiles());\n\n result.current.upload(dummyFile, { library: true });\n expect(OpenaiMock.uploadFile).toHaveBeenCalledWith(dummyFile, {\n library: true,\n });\n });\n\n it(\"should select files from ChatGPT\", async () => {\n const selectedFiles = [\n { fileId: \"file_1\", fileName: \"doc.pdf\", mimeType: \"application/pdf\" },\n ];\n OpenaiMock.selectFiles = vi.fn().mockResolvedValue(selectedFiles);\n\n const { result } = renderHook(() => useFiles());\n\n const files = await result.current.selectFiles();\n expect(OpenaiMock.selectFiles).toHaveBeenCalled();\n expect(files).toEqual(selectedFiles);\n\n delete OpenaiMock.selectFiles;\n });\n\n it(\"should download a file from ChatGPT\", () => {\n const fileId = \"123\";\n const { result } = renderHook(() => useFiles());\n\n result.current.getDownloadUrl({ fileId });\n expect(OpenaiMock.getFileDownloadUrl).toHaveBeenCalledWith({ fileId });\n });\n});\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { McpUiHostCapabilities } from "@modelcontextprotocol/ext-apps";
|
|
2
|
+
/**
|
|
3
|
+
* Known host applications, as normalized slugs. Unrecognized hosts surface
|
|
4
|
+
* their raw `hostInfo.name` string instead.
|
|
5
|
+
*/
|
|
6
|
+
export type Host = "chatgpt" | "claude" | "cursor" | "goose" | "mistral-vibe" | "alpic";
|
|
7
|
+
/**
|
|
8
|
+
* OpenAI MCP extensions the host advertises. Each flag is `true` only when the
|
|
9
|
+
* host sends a truthy `hostCapabilities.experimental["openai/<name>"]`, so every flag is
|
|
10
|
+
* `false` before the handshake and on hosts other than ChatGPT.
|
|
11
|
+
*/
|
|
12
|
+
export type OpenAIHostCapabilities = {
|
|
13
|
+
/** Read, subscribe to and write the file opened through a file entrypoint. */
|
|
14
|
+
resource: boolean;
|
|
15
|
+
/** Titles, thumbnails and background content in `ui/update-model-context`. */
|
|
16
|
+
modelContext: boolean;
|
|
17
|
+
/** `ui/message` targets and titled items. */
|
|
18
|
+
message: boolean;
|
|
19
|
+
/** Open a local file with `openai/files/open`. */
|
|
20
|
+
files: boolean;
|
|
21
|
+
};
|
|
22
|
+
export type HostInfo = {
|
|
23
|
+
name: Host | (string & {}) | undefined;
|
|
24
|
+
version: string | undefined;
|
|
25
|
+
/** The host's MCP Apps capabilities, `undefined` until the handshake. */
|
|
26
|
+
capabilities: McpUiHostCapabilities | undefined;
|
|
27
|
+
/** OpenAI MCP extensions the host supports. */
|
|
28
|
+
openaiCapabilities: OpenAIHostCapabilities;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Identity and capabilities of the host application rendering the view, from
|
|
32
|
+
* the MCP Apps `ui/initialize` handshake. `name` is normalized to a
|
|
33
|
+
* {@link Host} slug when recognized, otherwise the raw string; `name`,
|
|
34
|
+
* `version` and `capabilities` are `undefined` until the handshake resolves
|
|
35
|
+
* (the view renders first and re-renders once it lands).
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* const { name, openaiCapabilities } = useHost();
|
|
40
|
+
* if (name === "claude") return <ClaudeLayout />;
|
|
41
|
+
* if (openaiCapabilities.files) return <OpenFileButton />;
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function useHost(): HostInfo;
|