@apollo/client-ai-apps 0.2.4 → 0.3.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.
Files changed (72) hide show
  1. package/.git-blame-ignore-revs +2 -0
  2. package/.github/workflows/compare-build-output.yml +28 -0
  3. package/.github/workflows/pr.yaml +23 -15
  4. package/.github/workflows/release.yaml +46 -46
  5. package/.prettierrc +9 -0
  6. package/config/compare-build-output-to.sh +90 -0
  7. package/dist/core/ApolloClient.d.ts +14 -0
  8. package/dist/index.d.ts +17 -10
  9. package/dist/index.js +164 -62
  10. package/dist/link/ToolCallLink.d.ts +26 -0
  11. package/dist/react/ApolloProvider.d.ts +9 -0
  12. package/dist/react/context/ToolUseContext.d.ts +15 -0
  13. package/dist/{hooks → react/hooks}/useOpenAiGlobal.d.ts +1 -1
  14. package/dist/react/hooks/useOpenExternal.d.ts +3 -0
  15. package/dist/{hooks → react/hooks}/useRequestDisplayMode.d.ts +1 -1
  16. package/dist/{hooks → react/hooks}/useToolEffect.d.ts +0 -4
  17. package/dist/react/hooks/useToolOutput.d.ts +1 -0
  18. package/dist/react/hooks/useToolResponseMetadata.d.ts +1 -0
  19. package/dist/react/hooks/useWidgetState.d.ts +4 -0
  20. package/dist/types/openai.d.ts +1 -2
  21. package/dist/vite/index.js +74 -21
  22. package/package.json +9 -2
  23. package/scripts/dev.mjs +3 -1
  24. package/src/core/ApolloClient.ts +108 -0
  25. package/src/{apollo_client/client.test.ts → core/__tests__/ApolloClient.test.ts} +232 -20
  26. package/src/index.ts +36 -10
  27. package/src/link/ToolCallLink.ts +49 -0
  28. package/src/{apollo_client/provider.tsx → react/ApolloProvider.tsx} +19 -9
  29. package/src/{apollo_client/provider.test.tsx → react/__tests__/ApolloProvider.test.tsx} +9 -9
  30. package/src/react/context/ToolUseContext.tsx +30 -0
  31. package/src/{hooks → react/hooks/__tests__}/useCallTool.test.ts +1 -1
  32. package/src/{hooks → react/hooks/__tests__}/useOpenAiGlobal.test.ts +5 -3
  33. package/src/react/hooks/__tests__/useOpenExternal.test.tsx +24 -0
  34. package/src/{hooks → react/hooks/__tests__}/useRequestDisplayMode.test.ts +2 -2
  35. package/src/{hooks → react/hooks/__tests__}/useSendFollowUpMessage.test.ts +4 -2
  36. package/src/{hooks → react/hooks/__tests__}/useToolEffect.test.tsx +27 -10
  37. package/src/{hooks → react/hooks/__tests__}/useToolInput.test.ts +1 -1
  38. package/src/{hooks → react/hooks/__tests__}/useToolName.test.ts +1 -1
  39. package/src/react/hooks/__tests__/useToolOutput.test.tsx +49 -0
  40. package/src/react/hooks/__tests__/useToolResponseMetadata.test.tsx +49 -0
  41. package/src/react/hooks/__tests__/useWidgetState.test.tsx +158 -0
  42. package/src/react/hooks/useCallTool.ts +13 -0
  43. package/src/{hooks → react/hooks}/useOpenAiGlobal.ts +11 -5
  44. package/src/react/hooks/useOpenExternal.ts +11 -0
  45. package/src/{hooks → react/hooks}/useRequestDisplayMode.ts +1 -1
  46. package/src/react/hooks/useToolEffect.tsx +37 -0
  47. package/src/{hooks → react/hooks}/useToolName.ts +1 -1
  48. package/src/react/hooks/useToolOutput.ts +5 -0
  49. package/src/react/hooks/useToolResponseMetadata.ts +5 -0
  50. package/src/react/hooks/useWidgetState.ts +48 -0
  51. package/src/testing/internal/index.ts +2 -0
  52. package/src/testing/internal/matchers/index.d.ts +9 -0
  53. package/src/testing/internal/matchers/index.ts +1 -0
  54. package/src/testing/internal/matchers/toRerender.ts +49 -0
  55. package/src/testing/internal/openai/dispatchStateChange.ts +9 -0
  56. package/src/testing/internal/openai/stubOpenAiGlobals.ts +13 -0
  57. package/src/types/openai.ts +6 -3
  58. package/src/vite/{absolute_asset_imports_plugin.test.ts → __tests__/absolute_asset_imports_plugin.test.ts} +4 -2
  59. package/src/vite/{application_manifest_plugin.test.ts → __tests__/application_manifest_plugin.test.ts} +176 -53
  60. package/src/vite/absolute_asset_imports_plugin.ts +3 -1
  61. package/src/vite/application_manifest_plugin.ts +84 -24
  62. package/vitest-setup.ts +1 -0
  63. package/dist/apollo_client/client.d.ts +0 -14
  64. package/dist/apollo_client/provider.d.ts +0 -5
  65. package/src/apollo_client/client.ts +0 -90
  66. package/src/hooks/useCallTool.ts +0 -8
  67. package/src/hooks/useToolEffect.tsx +0 -41
  68. /package/dist/{hooks → react/hooks}/useSendFollowUpMessage.d.ts +0 -0
  69. /package/dist/{hooks → react/hooks}/useToolInput.d.ts +0 -0
  70. /package/dist/{hooks → react/hooks}/useToolName.d.ts +0 -0
  71. /package/src/{hooks → react/hooks}/useSendFollowUpMessage.ts +0 -0
  72. /package/src/{hooks → react/hooks}/useToolInput.ts +0 -0
@@ -1,15 +1,20 @@
1
1
  import { expect, test, vi } from "vitest";
2
- import { useToolEffect, ToolUseProvider } from "./useToolEffect";
2
+ import { useToolEffect } from "../useToolEffect";
3
3
  import { renderHook } from "@testing-library/react";
4
+ import { ToolUseProvider } from "../../context/ToolUseContext";
4
5
 
5
6
  test("Should trigger effect when tool name matches toolResponseMetadata", async () => {
6
7
  vi.stubGlobal("openai", {
7
8
  toolResponseMetadata: { toolName: "my-app--my-tool" },
8
9
  });
9
10
  const navigate = vi.fn();
10
- const wrapper = ({ children }: { children: any }) => <ToolUseProvider appName="my-app">{children}</ToolUseProvider>;
11
+ const wrapper = ({ children }: { children: any }) => (
12
+ <ToolUseProvider appName="my-app">{children}</ToolUseProvider>
13
+ );
11
14
 
12
- renderHook(() => useToolEffect("my-tool", () => navigate(), [navigate]), { wrapper });
15
+ renderHook(() => useToolEffect("my-tool", () => navigate(), [navigate]), {
16
+ wrapper,
17
+ });
13
18
 
14
19
  expect(navigate).toBeCalled();
15
20
  });
@@ -19,9 +24,17 @@ test("Should trigger effect when one of multiple tool name matches toolResponseM
19
24
  toolResponseMetadata: { toolName: "my-app--my-tool" },
20
25
  });
21
26
  const navigate = vi.fn();
22
- const wrapper = ({ children }: { children: any }) => <ToolUseProvider appName="my-app">{children}</ToolUseProvider>;
27
+ const wrapper = ({ children }: { children: any }) => (
28
+ <ToolUseProvider appName="my-app">{children}</ToolUseProvider>
29
+ );
23
30
 
24
- renderHook(() => useToolEffect(["my-tool", "my-similar-tool"], () => navigate(), [navigate]), { wrapper });
31
+ renderHook(
32
+ () =>
33
+ useToolEffect(["my-tool", "my-similar-tool"], () => navigate(), [
34
+ navigate,
35
+ ]),
36
+ { wrapper }
37
+ );
25
38
 
26
39
  expect(navigate).toBeCalled();
27
40
  });
@@ -31,9 +44,13 @@ test("Should not trigger effect when tool name does not match toolResponseMetada
31
44
  toolResponseMetadata: { toolName: "my-app--my-other-tool" },
32
45
  });
33
46
  const navigate = vi.fn();
34
- const wrapper = ({ children }: { children: any }) => <ToolUseProvider appName="my-app">{children}</ToolUseProvider>;
47
+ const wrapper = ({ children }: { children: any }) => (
48
+ <ToolUseProvider appName="my-app">{children}</ToolUseProvider>
49
+ );
35
50
 
36
- renderHook(() => useToolEffect("my-tool", () => navigate(), [navigate]), { wrapper });
51
+ renderHook(() => useToolEffect("my-tool", () => navigate(), [navigate]), {
52
+ wrapper,
53
+ });
37
54
 
38
55
  expect(navigate).not.toBeCalled();
39
56
  });
@@ -44,7 +61,7 @@ test("Should throw an error when used outside of a ToolUseProvider", async () =>
44
61
  });
45
62
  const navigate = vi.fn();
46
63
 
47
- expect(() => renderHook(() => useToolEffect("my-tool", () => navigate(), [navigate]))).toThrowError(
48
- "useToolEffect must be used within ToolUseProvider"
49
- );
64
+ expect(() =>
65
+ renderHook(() => useToolEffect("my-tool", () => navigate(), [navigate]))
66
+ ).toThrowError("useToolEffect must be used within ToolUseProvider");
50
67
  });
@@ -1,5 +1,5 @@
1
1
  import { expect, test, vi } from "vitest";
2
- import { useToolInput } from "./useToolInput";
2
+ import { useToolInput } from "../useToolInput";
3
3
  import { renderHook } from "@testing-library/react";
4
4
 
5
5
  test("Should return tool input when called", async () => {
@@ -1,5 +1,5 @@
1
1
  import { expect, test, vi } from "vitest";
2
- import { useToolName } from "./useToolName";
2
+ import { useToolName } from "../useToolName";
3
3
  import { renderHook } from "@testing-library/react";
4
4
 
5
5
  test("Should return tool input when called", async () => {
@@ -0,0 +1,49 @@
1
+ import { afterEach, expect, test, vi } from "vitest";
2
+ import {
3
+ dispatchStateChange,
4
+ stubOpenAiGlobals,
5
+ } from "../../../testing/internal";
6
+ import { renderHookToSnapshotStream } from "@testing-library/react-render-stream";
7
+ import { useToolOutput } from "../useToolOutput";
8
+
9
+ afterEach(() => {
10
+ vi.unstubAllGlobals();
11
+ });
12
+
13
+ test("returns the tool output set in window", async () => {
14
+ stubOpenAiGlobals({ toolOutput: { test: true } });
15
+
16
+ const { takeSnapshot } = await renderHookToSnapshotStream(() =>
17
+ useToolOutput()
18
+ );
19
+
20
+ await expect(takeSnapshot()).resolves.toEqual({ test: true });
21
+ await expect(takeSnapshot).not.toRerender();
22
+ });
23
+
24
+ test("returns null when not set", async () => {
25
+ stubOpenAiGlobals();
26
+
27
+ const { takeSnapshot } = await renderHookToSnapshotStream(() =>
28
+ useToolOutput()
29
+ );
30
+
31
+ await expect(takeSnapshot()).resolves.toBeNull();
32
+ await expect(takeSnapshot).not.toRerender();
33
+ });
34
+
35
+ test("reacts to changes in globals", async () => {
36
+ stubOpenAiGlobals({ toolOutput: { initial: true } });
37
+
38
+ const { takeSnapshot } = await renderHookToSnapshotStream(() =>
39
+ useToolOutput()
40
+ );
41
+
42
+ await expect(takeSnapshot()).resolves.toEqual({ initial: true });
43
+
44
+ window.openai.toolOutput = { updated: true };
45
+ dispatchStateChange();
46
+
47
+ await expect(takeSnapshot()).resolves.toEqual({ updated: true });
48
+ await expect(takeSnapshot).not.toRerender();
49
+ });
@@ -0,0 +1,49 @@
1
+ import { afterEach, expect, test, vi } from "vitest";
2
+ import {
3
+ dispatchStateChange,
4
+ stubOpenAiGlobals,
5
+ } from "../../../testing/internal";
6
+ import { renderHookToSnapshotStream } from "@testing-library/react-render-stream";
7
+ import { useToolResponseMetadata } from "../useToolResponseMetadata";
8
+
9
+ afterEach(() => {
10
+ vi.unstubAllGlobals();
11
+ });
12
+
13
+ test("returns the tool output set in window", async () => {
14
+ stubOpenAiGlobals({ toolResponseMetadata: { test: true } });
15
+
16
+ const { takeSnapshot } = await renderHookToSnapshotStream(() =>
17
+ useToolResponseMetadata()
18
+ );
19
+
20
+ await expect(takeSnapshot()).resolves.toEqual({ test: true });
21
+ await expect(takeSnapshot).not.toRerender();
22
+ });
23
+
24
+ test("returns null when not set", async () => {
25
+ stubOpenAiGlobals();
26
+
27
+ const { takeSnapshot } = await renderHookToSnapshotStream(() =>
28
+ useToolResponseMetadata()
29
+ );
30
+
31
+ await expect(takeSnapshot()).resolves.toBeNull();
32
+ await expect(takeSnapshot).not.toRerender();
33
+ });
34
+
35
+ test("reacts to changes in globals", async () => {
36
+ stubOpenAiGlobals({ toolResponseMetadata: { initial: true } });
37
+
38
+ const { takeSnapshot } = await renderHookToSnapshotStream(() =>
39
+ useToolResponseMetadata()
40
+ );
41
+
42
+ await expect(takeSnapshot()).resolves.toEqual({ initial: true });
43
+
44
+ window.openai.toolResponseMetadata = { updated: true };
45
+ dispatchStateChange();
46
+
47
+ await expect(takeSnapshot()).resolves.toEqual({ updated: true });
48
+ await expect(takeSnapshot).not.toRerender();
49
+ });
@@ -0,0 +1,158 @@
1
+ import { afterEach, expect, test, vi } from "vitest";
2
+ import {
3
+ disableActEnvironment,
4
+ renderHookToSnapshotStream,
5
+ } from "@testing-library/react-render-stream";
6
+ import { useWidgetState } from "../useWidgetState";
7
+ import { stubOpenAiGlobals } from "../../../testing/internal";
8
+
9
+ afterEach(() => {
10
+ vi.unstubAllGlobals();
11
+ });
12
+
13
+ test("returns state from global", async () => {
14
+ stubOpenAiGlobals({ widgetState: { test: true } });
15
+
16
+ using _disabledAct = disableActEnvironment();
17
+ const { takeSnapshot } = await renderHookToSnapshotStream(() =>
18
+ useWidgetState()
19
+ );
20
+
21
+ const [widgetState] = await takeSnapshot();
22
+
23
+ expect(widgetState).toEqual({ test: true });
24
+ await expect(takeSnapshot).not.toRerender();
25
+ });
26
+
27
+ test("returns null when global does not exist", async () => {
28
+ stubOpenAiGlobals();
29
+
30
+ using _disabledAct = disableActEnvironment();
31
+ const { takeSnapshot } = await renderHookToSnapshotStream(() =>
32
+ useWidgetState()
33
+ );
34
+
35
+ const [widgetState] = await takeSnapshot();
36
+
37
+ expect(widgetState).toBeNull();
38
+ await expect(takeSnapshot).not.toRerender();
39
+ });
40
+
41
+ test("returns provided default state when global does not exist", async () => {
42
+ stubOpenAiGlobals();
43
+
44
+ using _disabledAct = disableActEnvironment();
45
+ const { takeSnapshot } = await renderHookToSnapshotStream(() =>
46
+ useWidgetState({ defaultValue: true })
47
+ );
48
+
49
+ const [widgetState] = await takeSnapshot();
50
+
51
+ expect(widgetState).toEqual({ defaultValue: true });
52
+ await expect(takeSnapshot).not.toRerender();
53
+ });
54
+
55
+ test("returns provided default state returned from init function when global does not exist", async () => {
56
+ stubOpenAiGlobals();
57
+
58
+ using _disabledAct = disableActEnvironment();
59
+ const { takeSnapshot } = await renderHookToSnapshotStream(() =>
60
+ useWidgetState(() => ({ defaultValueFromFunction: true }))
61
+ );
62
+
63
+ const [widgetState] = await takeSnapshot();
64
+
65
+ expect(widgetState).toEqual({ defaultValueFromFunction: true });
66
+ await expect(takeSnapshot).not.toRerender();
67
+ });
68
+
69
+ test("prefers global value over default value", async () => {
70
+ stubOpenAiGlobals({ widgetState: { globalWidgetState: true } });
71
+
72
+ using _disabledAct = disableActEnvironment();
73
+ const { takeSnapshot } = await renderHookToSnapshotStream(() =>
74
+ useWidgetState({ defaultValue: true })
75
+ );
76
+
77
+ const [widgetState] = await takeSnapshot();
78
+
79
+ expect(widgetState).toEqual({ globalWidgetState: true });
80
+ await expect(takeSnapshot).not.toRerender();
81
+ });
82
+
83
+ test("rerenders with new value after setting new value", async () => {
84
+ stubOpenAiGlobals({ widgetState: { globalWidgetState: true } });
85
+
86
+ using _disabledAct = disableActEnvironment();
87
+ const { takeSnapshot, getCurrentSnapshot } = await renderHookToSnapshotStream(
88
+ () => useWidgetState()
89
+ );
90
+
91
+ {
92
+ const [widgetState] = await takeSnapshot();
93
+
94
+ expect(widgetState).toEqual({ globalWidgetState: true });
95
+ }
96
+
97
+ const [, setWidgetState] = getCurrentSnapshot();
98
+ setWidgetState({ rerendered: true });
99
+
100
+ {
101
+ const [widgetState] = await takeSnapshot();
102
+
103
+ expect(widgetState).toEqual({ rerendered: true });
104
+ }
105
+
106
+ await expect(takeSnapshot).not.toRerender();
107
+ });
108
+
109
+ test("allows state setter function with previous value", async () => {
110
+ stubOpenAiGlobals({ widgetState: { globalWidgetState: true } });
111
+
112
+ using _disabledAct = disableActEnvironment();
113
+ const { takeSnapshot, getCurrentSnapshot } = await renderHookToSnapshotStream(
114
+ () => useWidgetState()
115
+ );
116
+
117
+ {
118
+ const [widgetState] = await takeSnapshot();
119
+
120
+ expect(widgetState).toEqual({ globalWidgetState: true });
121
+ }
122
+
123
+ const [, setWidgetState] = getCurrentSnapshot();
124
+ setWidgetState((prev) => ({ ...prev, rerendered: true }));
125
+
126
+ {
127
+ const [widgetState] = await takeSnapshot();
128
+
129
+ expect(widgetState).toEqual({ globalWidgetState: true, rerendered: true });
130
+ }
131
+
132
+ await expect(takeSnapshot).not.toRerender();
133
+ });
134
+
135
+ test("updates value from window when changed globally", async () => {
136
+ stubOpenAiGlobals({ widgetState: { globalWidgetState: true } });
137
+
138
+ using _disabledAct = disableActEnvironment();
139
+ const { takeSnapshot } = await renderHookToSnapshotStream(() =>
140
+ useWidgetState()
141
+ );
142
+
143
+ {
144
+ const [widgetState] = await takeSnapshot();
145
+
146
+ expect(widgetState).toEqual({ globalWidgetState: true });
147
+ }
148
+
149
+ window.openai.setWidgetState({ fromEvent: true });
150
+
151
+ {
152
+ const [widgetState] = await takeSnapshot();
153
+
154
+ expect(widgetState).toEqual({ fromEvent: true });
155
+ }
156
+
157
+ await expect(takeSnapshot).not.toRerender();
158
+ });
@@ -0,0 +1,13 @@
1
+ type UseCallToolResult = <K>(
2
+ toolId: string,
3
+ variables?: Record<string, unknown> | undefined
4
+ ) => Promise<K>;
5
+
6
+ export const useCallTool = (): UseCallToolResult => {
7
+ const callTool = async (
8
+ toolId: string,
9
+ variables: Record<string, unknown> | undefined = {}
10
+ ) => await window.openai?.callTool(toolId, variables);
11
+
12
+ return callTool;
13
+ };
@@ -1,9 +1,15 @@
1
- import { useSyncExternalStore } from "react";
2
- import { SET_GLOBALS_EVENT_TYPE, SetGlobalsEvent, OpenAiGlobals } from "../types/openai";
1
+ import { useSyncExternalStore, useCallback } from "react";
2
+ import {
3
+ SET_GLOBALS_EVENT_TYPE,
4
+ SetGlobalsEvent,
5
+ OpenAiGlobals,
6
+ } from "../../types/openai";
3
7
 
4
- export function useOpenAiGlobal<K extends keyof OpenAiGlobals>(key: K): OpenAiGlobals[K] {
8
+ export function useOpenAiGlobal<K extends keyof OpenAiGlobals>(
9
+ key: K
10
+ ): OpenAiGlobals[K] {
5
11
  return useSyncExternalStore(
6
- (onChange) => {
12
+ useCallback((onChange) => {
7
13
  const handleSetGlobal = (event: SetGlobalsEvent) => {
8
14
  const value = event.detail.globals[key];
9
15
  if (value === undefined) {
@@ -20,7 +26,7 @@ export function useOpenAiGlobal<K extends keyof OpenAiGlobals>(key: K): OpenAiGl
20
26
  return () => {
21
27
  window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal);
22
28
  };
23
- },
29
+ }, []),
24
30
  () => window.openai[key]
25
31
  );
26
32
  }
@@ -0,0 +1,11 @@
1
+ import { useCallback } from "react";
2
+ import { API } from "../../types/openai";
3
+
4
+ type OpenExternalFn = API<any>["openExternal"];
5
+
6
+ export function useOpenExternal() {
7
+ return useCallback<OpenExternalFn>(
8
+ (...args) => window.openai.openExternal(...args),
9
+ []
10
+ );
11
+ }
@@ -1,4 +1,4 @@
1
- import { DisplayMode } from "../types/openai";
1
+ import { DisplayMode } from "../../types/openai";
2
2
 
3
3
  export const useRequestDisplayMode = () => {
4
4
  return async (args: { mode: DisplayMode }) => {
@@ -0,0 +1,37 @@
1
+ import React, { useEffect } from "react";
2
+ import { useToolName } from "./useToolName";
3
+ import { useToolInput } from "./useToolInput";
4
+ import { useToolUseState } from "../context/ToolUseContext";
5
+
6
+ export const useToolEffect = (
7
+ toolName: string | string[],
8
+ effect: (toolInput: any) => void,
9
+ deps: React.DependencyList = []
10
+ ) => {
11
+ const ctx = useToolUseState();
12
+ const fullToolName = useToolName();
13
+ const toolInput = useToolInput();
14
+ if (!ctx)
15
+ throw new Error("useToolEffect must be used within ToolUseProvider");
16
+
17
+ const toolNames = Array.isArray(toolName) ? toolName : [toolName];
18
+
19
+ useEffect(() => {
20
+ const matches = toolNames.some(
21
+ (name) => fullToolName === `${ctx.appName}--${name}`
22
+ );
23
+
24
+ if (!ctx.hasNavigated && matches) {
25
+ effect(toolInput);
26
+ ctx.setHasNavigated(true);
27
+ }
28
+ }, [
29
+ ctx.hasNavigated,
30
+ ctx.setHasNavigated,
31
+ ctx.appName,
32
+ toolNames,
33
+ fullToolName,
34
+ toolInput,
35
+ ...deps,
36
+ ]);
37
+ };
@@ -3,5 +3,5 @@ import { useOpenAiGlobal } from "./useOpenAiGlobal";
3
3
  export const useToolName = (): string | undefined => {
4
4
  const toolResponseMetadata = useOpenAiGlobal("toolResponseMetadata");
5
5
 
6
- return toolResponseMetadata?.toolName;
6
+ return toolResponseMetadata?.toolName as string;
7
7
  };
@@ -0,0 +1,5 @@
1
+ import { useOpenAiGlobal } from "./useOpenAiGlobal";
2
+
3
+ export function useToolOutput() {
4
+ return useOpenAiGlobal("toolOutput") ?? null;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { useOpenAiGlobal } from "./useOpenAiGlobal";
2
+
3
+ export function useToolResponseMetadata() {
4
+ return useOpenAiGlobal("toolResponseMetadata") ?? null;
5
+ }
@@ -0,0 +1,48 @@
1
+ import { SetStateAction, useCallback, useState } from "react";
2
+ import { UnknownObject } from "../../types/openai";
3
+ import { useOpenAiGlobal } from "./useOpenAiGlobal";
4
+
5
+ export function useWidgetState<T extends UnknownObject>(
6
+ defaultState: T | (() => T)
7
+ ): readonly [T, (state: SetStateAction<T>) => void];
8
+
9
+ export function useWidgetState<T extends UnknownObject>(
10
+ defaultState?: T | (() => T | null) | null
11
+ ): readonly [T | null, (state: SetStateAction<T | null>) => void];
12
+
13
+ export function useWidgetState<T extends UnknownObject>(
14
+ defaultState?: T | (() => T | null) | null
15
+ ): readonly [T | null, (state: SetStateAction<T | null>) => void] {
16
+ const widgetStateFromWindow = useOpenAiGlobal("widgetState") as T;
17
+ const [previousWidgetStateFromWindow, setPreviousWidgetStateFromWindow] =
18
+ useState(widgetStateFromWindow);
19
+
20
+ let [widgetState, _setWidgetState] = useState<T | null>(() => {
21
+ if (widgetStateFromWindow != null) {
22
+ return widgetStateFromWindow;
23
+ }
24
+
25
+ return typeof defaultState === "function" ? defaultState() : (
26
+ (defaultState ?? null)
27
+ );
28
+ });
29
+
30
+ if (previousWidgetStateFromWindow !== widgetStateFromWindow) {
31
+ _setWidgetState((widgetState = widgetStateFromWindow));
32
+ setPreviousWidgetStateFromWindow(widgetStateFromWindow);
33
+ }
34
+
35
+ const setWidgetState = useCallback((state: SetStateAction<T | null>) => {
36
+ _setWidgetState((prevState) => {
37
+ const newState = typeof state === "function" ? state(prevState) : state;
38
+
39
+ if (newState != null && typeof window !== "undefined") {
40
+ void window.openai?.setWidgetState?.(newState);
41
+ }
42
+
43
+ return newState;
44
+ });
45
+ }, []);
46
+
47
+ return [widgetState, setWidgetState];
48
+ }
@@ -0,0 +1,2 @@
1
+ export { dispatchStateChange } from "./openai/dispatchStateChange";
2
+ export { stubOpenAiGlobals } from "./openai/stubOpenAiGlobals";
@@ -0,0 +1,9 @@
1
+ import { NextRenderOptions } from "@testing-library/react-render-stream";
2
+
3
+ interface CustomMatchers<R = unknown> {
4
+ toRerender: (options?: NextRenderOptions) => Promise<R>;
5
+ }
6
+
7
+ declare module "vitest" {
8
+ interface Assertion<T = any> extends CustomMatchers<T> {}
9
+ }
@@ -0,0 +1 @@
1
+ import "./toRerender";
@@ -0,0 +1,49 @@
1
+ // Vitest port of toRerender from
2
+ // https://github.com/testing-library/react-render-stream-testing-library/blob/main/src/expect/renderStreamMatchers.ts
3
+ import {
4
+ Assertable,
5
+ NextRenderOptions,
6
+ RenderStream,
7
+ WaitForRenderTimeoutError,
8
+ } from "@testing-library/react-render-stream";
9
+
10
+ import { expect } from "vitest";
11
+
12
+ const assertableSymbol = Symbol.for(
13
+ "@testing-library/react-render-stream:assertable"
14
+ );
15
+
16
+ expect.extend({
17
+ async toRerender(actual, options: NextRenderOptions) {
18
+ const _stream = actual as RenderStream<any> | Assertable;
19
+ const stream = (
20
+ assertableSymbol in _stream ?
21
+ _stream[assertableSymbol]
22
+ : _stream) as RenderStream<any>;
23
+ const hint = this.utils.matcherHint("toRerender", undefined, undefined, {
24
+ isNot: this.isNot,
25
+ });
26
+
27
+ let pass = true;
28
+
29
+ try {
30
+ await stream.peekRender({ timeout: 100, ...options });
31
+ } catch (e) {
32
+ if (e instanceof WaitForRenderTimeoutError) {
33
+ pass = false;
34
+ } else {
35
+ throw e;
36
+ }
37
+ }
38
+
39
+ return {
40
+ pass,
41
+ message() {
42
+ return (
43
+ `${hint}\n\nExpected component to${pass ? " not" : ""} rerender, ` +
44
+ `but it did${pass ? "" : " not"}.`
45
+ );
46
+ },
47
+ };
48
+ },
49
+ });
@@ -0,0 +1,9 @@
1
+ import { SET_GLOBALS_EVENT_TYPE } from "../../../types/openai";
2
+
3
+ export function dispatchStateChange() {
4
+ window.dispatchEvent(
5
+ new CustomEvent(SET_GLOBALS_EVENT_TYPE, {
6
+ detail: { globals: window.openai },
7
+ })
8
+ );
9
+ }
@@ -0,0 +1,13 @@
1
+ import { vi } from "vitest";
2
+ import { API, OpenAiGlobals, UnknownObject } from "../../../types/openai";
3
+ import { dispatchStateChange } from "./dispatchStateChange";
4
+
5
+ export function stubOpenAiGlobals(globals?: Partial<API<any> & OpenAiGlobals>) {
6
+ vi.stubGlobal("openai", {
7
+ setWidgetState: (state: UnknownObject) => {
8
+ window.openai.widgetState = state;
9
+ dispatchStateChange();
10
+ },
11
+ ...globals,
12
+ });
13
+ }
@@ -1,4 +1,4 @@
1
- type UnknownObject = any;
1
+ export type UnknownObject = Record<string, unknown>;
2
2
 
3
3
  declare global {
4
4
  interface Window {
@@ -14,7 +14,7 @@ export type OpenAiGlobals<
14
14
  ToolInput extends UnknownObject = UnknownObject,
15
15
  ToolOutput extends UnknownObject = UnknownObject,
16
16
  ToolResponseMetadata extends UnknownObject = UnknownObject,
17
- WidgetState extends UnknownObject = UnknownObject
17
+ WidgetState extends UnknownObject = UnknownObject,
18
18
  > = {
19
19
  theme: Theme;
20
20
  userAgent: UserAgent;
@@ -62,7 +62,10 @@ export class SetGlobalsEvent extends CustomEvent<{
62
62
  readonly type = SET_GLOBALS_EVENT_TYPE;
63
63
  }
64
64
 
65
- export type CallTool = (name: string, args: Record<string, unknown>) => Promise<any>;
65
+ export type CallTool = (
66
+ name: string,
67
+ args: Record<string, unknown>
68
+ ) => Promise<any>;
66
69
 
67
70
  export type DisplayMode = "pip" | "inline" | "fullscreen";
68
71
 
@@ -1,5 +1,5 @@
1
1
  import { expect, test, vi, describe, beforeEach, Mock } from "vitest";
2
- import { AbsoluteAssetImportsPlugin } from "./absolute_asset_imports_plugin";
2
+ import { AbsoluteAssetImportsPlugin } from "../absolute_asset_imports_plugin";
3
3
 
4
4
  test("Should replace root relative scripts with full url when origin is provided", () => {
5
5
  const ctx = {
@@ -96,5 +96,7 @@ test("Should not modify html when not running a local server", () => {
96
96
 
97
97
  let result = plugin.transformIndexHtml(html, ctx);
98
98
 
99
- expect(result).toMatchInlineSnapshot(`"<html><head><script type="module" src="/@vite/client"></script></head><body><script module src="/assets/main.ts?t=12345"></script></body></html>"`);
99
+ expect(result).toMatchInlineSnapshot(
100
+ `"<html><head><script type="module" src="/@vite/client"></script></head><body><script module src="/assets/main.ts?t=12345"></script></body></html>"`
101
+ );
100
102
  });