@apollo/client-ai-apps 0.3.0 → 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 (61) hide show
  1. package/.github/workflows/compare-build-output.yml +28 -0
  2. package/config/compare-build-output-to.sh +90 -0
  3. package/dist/core/ApolloClient.d.ts +14 -0
  4. package/dist/index.d.ts +17 -11
  5. package/dist/index.js +96 -44
  6. package/dist/react/ApolloProvider.d.ts +9 -0
  7. package/dist/react/context/ToolUseContext.d.ts +15 -0
  8. package/dist/{hooks → react/hooks}/useOpenAiGlobal.d.ts +1 -1
  9. package/dist/react/hooks/useOpenExternal.d.ts +3 -0
  10. package/dist/{hooks → react/hooks}/useRequestDisplayMode.d.ts +1 -1
  11. package/dist/{hooks → react/hooks}/useToolEffect.d.ts +0 -4
  12. package/dist/react/hooks/useToolOutput.d.ts +1 -0
  13. package/dist/react/hooks/useToolResponseMetadata.d.ts +1 -0
  14. package/dist/react/hooks/useWidgetState.d.ts +4 -0
  15. package/dist/types/openai.d.ts +1 -2
  16. package/package.json +5 -1
  17. package/src/{apollo_client/client.ts → core/ApolloClient.ts} +21 -17
  18. package/src/{apollo_client/client.test.ts → core/__tests__/ApolloClient.test.ts} +8 -9
  19. package/src/index.ts +36 -11
  20. package/src/{apollo_client/provider.tsx → react/ApolloProvider.tsx} +12 -8
  21. package/src/{apollo_client/provider.test.tsx → react/__tests__/ApolloProvider.test.tsx} +9 -9
  22. package/src/react/context/ToolUseContext.tsx +30 -0
  23. package/src/{hooks → react/hooks/__tests__}/useCallTool.test.ts +1 -1
  24. package/src/{hooks → react/hooks/__tests__}/useOpenAiGlobal.test.ts +2 -2
  25. package/src/react/hooks/__tests__/useOpenExternal.test.tsx +24 -0
  26. package/src/{hooks → react/hooks/__tests__}/useRequestDisplayMode.test.ts +2 -2
  27. package/src/{hooks → react/hooks/__tests__}/useSendFollowUpMessage.test.ts +1 -1
  28. package/src/{hooks → react/hooks/__tests__}/useToolEffect.test.tsx +2 -1
  29. package/src/{hooks → react/hooks/__tests__}/useToolInput.test.ts +1 -1
  30. package/src/{hooks → react/hooks/__tests__}/useToolName.test.ts +1 -1
  31. package/src/react/hooks/__tests__/useToolOutput.test.tsx +49 -0
  32. package/src/react/hooks/__tests__/useToolResponseMetadata.test.tsx +49 -0
  33. package/src/react/hooks/__tests__/useWidgetState.test.tsx +158 -0
  34. package/src/{hooks → react/hooks}/useOpenAiGlobal.ts +4 -4
  35. package/src/react/hooks/useOpenExternal.ts +11 -0
  36. package/src/{hooks → react/hooks}/useRequestDisplayMode.ts +1 -1
  37. package/src/{hooks → react/hooks}/useToolEffect.tsx +3 -26
  38. package/src/{hooks → react/hooks}/useToolName.ts +1 -1
  39. package/src/react/hooks/useToolOutput.ts +5 -0
  40. package/src/react/hooks/useToolResponseMetadata.ts +5 -0
  41. package/src/react/hooks/useWidgetState.ts +48 -0
  42. package/src/testing/internal/index.ts +2 -0
  43. package/src/testing/internal/matchers/index.d.ts +9 -0
  44. package/src/testing/internal/matchers/index.ts +1 -0
  45. package/src/testing/internal/matchers/toRerender.ts +49 -0
  46. package/src/testing/internal/openai/dispatchStateChange.ts +9 -0
  47. package/src/testing/internal/openai/stubOpenAiGlobals.ts +13 -0
  48. package/src/types/openai.ts +1 -1
  49. package/src/vite/{absolute_asset_imports_plugin.test.ts → __tests__/absolute_asset_imports_plugin.test.ts} +1 -1
  50. package/src/vite/{application_manifest_plugin.test.ts → __tests__/application_manifest_plugin.test.ts} +7 -7
  51. package/vitest-setup.ts +1 -0
  52. package/dist/apollo_client/client.d.ts +0 -13
  53. package/dist/apollo_client/provider.d.ts +0 -5
  54. /package/dist/{apollo_client/link → link}/ToolCallLink.d.ts +0 -0
  55. /package/dist/{hooks → react/hooks}/useSendFollowUpMessage.d.ts +0 -0
  56. /package/dist/{hooks → react/hooks}/useToolInput.d.ts +0 -0
  57. /package/dist/{hooks → react/hooks}/useToolName.d.ts +0 -0
  58. /package/src/{apollo_client/link → link}/ToolCallLink.ts +0 -0
  59. /package/src/{hooks → react/hooks}/useCallTool.ts +0 -0
  60. /package/src/{hooks → react/hooks}/useSendFollowUpMessage.ts +0 -0
  61. /package/src/{hooks → react/hooks}/useToolInput.ts +0 -0
package/src/index.ts CHANGED
@@ -1,13 +1,38 @@
1
- export * from "./types/openai";
2
- export * from "./types/application-manifest";
3
- export * from "./hooks/useOpenAiGlobal";
4
- export * from "./hooks/useToolName";
5
- export * from "./hooks/useToolInput";
6
- export * from "./hooks/useSendFollowUpMessage";
7
- export * from "./hooks/useRequestDisplayMode";
8
- export * from "./hooks/useToolEffect";
1
+ export type {
2
+ API,
3
+ CallTool,
4
+ DeviceType,
5
+ DisplayMode,
6
+ OpenAiGlobals,
7
+ SafeArea,
8
+ SafeAreaInsets,
9
+ Theme,
10
+ UserAgent,
11
+ UnknownObject,
12
+ } from "./types/openai";
13
+ export { SET_GLOBALS_EVENT_TYPE, SetGlobalsEvent } from "./types/openai";
14
+
15
+ export type {
16
+ ApplicationManifest,
17
+ ManifestOperation,
18
+ ManifestTool,
19
+ ManifestExtraInput,
20
+ ManifestCsp,
21
+ } from "./types/application-manifest";
22
+
23
+ export { ToolUseProvider } from "./react/context/ToolUseContext";
24
+ export { useOpenAiGlobal } from "./react/hooks/useOpenAiGlobal";
25
+ export { useToolName } from "./react/hooks/useToolName";
26
+ export { useToolInput } from "./react/hooks/useToolInput";
27
+ export { useSendFollowUpMessage } from "./react/hooks/useSendFollowUpMessage";
28
+ export { useRequestDisplayMode } from "./react/hooks/useRequestDisplayMode";
29
+ export { useToolEffect } from "./react/hooks/useToolEffect";
30
+ export { useOpenExternal } from "./react/hooks/useOpenExternal";
31
+ export { useToolOutput } from "./react/hooks/useToolOutput";
32
+ export { useToolResponseMetadata } from "./react/hooks/useToolResponseMetadata";
33
+ export { useWidgetState } from "./react/hooks/useWidgetState";
9
34
 
10
35
  export * from "@apollo/client";
11
- export { ExtendedApolloClient as ApolloClient } from "./apollo_client/client";
12
- export { ExtendedApolloProvider as ApolloProvider } from "./apollo_client/provider";
13
- export { ToolCallLink } from "./apollo_client/link/ToolCallLink";
36
+ export { ApolloClient } from "./core/ApolloClient";
37
+ export { ApolloProvider } from "./react/ApolloProvider";
38
+ export { ToolCallLink } from "./link/ToolCallLink";
@@ -1,12 +1,16 @@
1
- import React, { useEffect, useState } from "react";
2
- import { ApolloProvider } from "@apollo/client/react";
3
- import { ExtendedApolloClient } from "./client";
1
+ import React, { ReactNode, useEffect, useState } from "react";
2
+ import { ApolloProvider as BaseApolloProvider } from "@apollo/client/react";
3
+ import { ApolloClient } from "../core/ApolloClient";
4
4
  import { SET_GLOBALS_EVENT_TYPE } from "../types/openai";
5
5
 
6
- export const ExtendedApolloProvider = ({
7
- children,
8
- client,
9
- }: React.PropsWithChildren<{ client: ExtendedApolloClient }>) => {
6
+ export declare namespace ApolloProvider {
7
+ export interface Props {
8
+ children?: ReactNode;
9
+ client: ApolloClient;
10
+ }
11
+ }
12
+
13
+ export const ApolloProvider = ({ children, client }: ApolloProvider.Props) => {
10
14
  const [hasPreloaded, setHasPreloaded] = useState(false);
11
15
 
12
16
  // This is to prevent against a race condition. We don't know if window.openai will be available when this loads or if it will become available shortly after.
@@ -33,6 +37,6 @@ export const ExtendedApolloProvider = ({
33
37
  }, []);
34
38
 
35
39
  return hasPreloaded ?
36
- <ApolloProvider client={client}>{children}</ApolloProvider>
40
+ <BaseApolloProvider client={client}>{children}</BaseApolloProvider>
37
41
  : null;
38
42
  };
@@ -1,8 +1,8 @@
1
1
  import { expect, test, vi } from "vitest";
2
- import { ExtendedApolloProvider } from "./provider";
2
+ import { ApolloProvider } from "../ApolloProvider";
3
3
  import { render } from "@testing-library/react";
4
- import { ExtendedApolloClient } from "./client";
5
- import { SET_GLOBALS_EVENT_TYPE } from "../types/openai";
4
+ import { ApolloClient } from "../../core/ApolloClient";
5
+ import { SET_GLOBALS_EVENT_TYPE } from "../../types/openai";
6
6
 
7
7
  test("Should call prefetch data when window.open is immediately available", () => {
8
8
  vi.stubGlobal("openai", {
@@ -11,9 +11,9 @@ test("Should call prefetch data when window.open is immediately available", () =
11
11
 
12
12
  const client = {
13
13
  prefetchData: vi.fn(async () => {}),
14
- } as unknown as ExtendedApolloClient;
14
+ } as unknown as ApolloClient;
15
15
 
16
- render(<ExtendedApolloProvider client={client} />);
16
+ render(<ApolloProvider client={client} />);
17
17
 
18
18
  expect(client.prefetchData).toBeCalled();
19
19
  });
@@ -21,9 +21,9 @@ test("Should call prefetch data when window.open is immediately available", () =
21
21
  test("Should NOT call prefetch data when window.open is not immediately available", () => {
22
22
  const client = {
23
23
  prefetchData: vi.fn(async () => {}),
24
- } as unknown as ExtendedApolloClient;
24
+ } as unknown as ApolloClient;
25
25
 
26
- render(<ExtendedApolloProvider client={client} />);
26
+ render(<ApolloProvider client={client} />);
27
27
 
28
28
  expect(client.prefetchData).not.toBeCalled();
29
29
  });
@@ -31,9 +31,9 @@ test("Should NOT call prefetch data when window.open is not immediately availabl
31
31
  test("Should call prefetch data when window.open is not immediately available and event is sent", () => {
32
32
  const client = {
33
33
  prefetchData: vi.fn(async () => {}),
34
- } as unknown as ExtendedApolloClient;
34
+ } as unknown as ApolloClient;
35
35
 
36
- render(<ExtendedApolloProvider client={client} />);
36
+ render(<ApolloProvider client={client} />);
37
37
 
38
38
  window.dispatchEvent(new CustomEvent(SET_GLOBALS_EVENT_TYPE));
39
39
 
@@ -0,0 +1,30 @@
1
+ import React, { createContext, ReactNode, useContext, useState } from "react";
2
+
3
+ interface ToolUseState {
4
+ appName: string;
5
+ hasNavigated: boolean;
6
+ setHasNavigated: (v: boolean) => void;
7
+ }
8
+
9
+ const ToolUseContext = createContext<ToolUseState | null>(null);
10
+
11
+ export declare namespace ToolUseProvider {
12
+ export interface Props {
13
+ children?: ReactNode;
14
+ appName: string;
15
+ }
16
+ }
17
+
18
+ export function ToolUseProvider({ children, appName }: ToolUseProvider.Props) {
19
+ const [hasNavigated, setHasNavigated] = useState(false);
20
+
21
+ return (
22
+ <ToolUseContext.Provider value={{ hasNavigated, setHasNavigated, appName }}>
23
+ {children}
24
+ </ToolUseContext.Provider>
25
+ );
26
+ }
27
+
28
+ export function useToolUseState() {
29
+ return useContext(ToolUseContext);
30
+ }
@@ -1,5 +1,5 @@
1
1
  import { expect, test, vi } from "vitest";
2
- import { useCallTool } from "./useCallTool";
2
+ import { useCallTool } from "../useCallTool";
3
3
 
4
4
  test("Should execute tool when returned function is called", async () => {
5
5
  vi.stubGlobal("openai", {
@@ -1,7 +1,7 @@
1
1
  import { expect, test, vi } from "vitest";
2
- import { useOpenAiGlobal } from "./useOpenAiGlobal";
2
+ import { useOpenAiGlobal } from "../useOpenAiGlobal";
3
3
  import { renderHook, act } from "@testing-library/react";
4
- import { SET_GLOBALS_EVENT_TYPE } from "../types/openai";
4
+ import { SET_GLOBALS_EVENT_TYPE } from "../../../types/openai";
5
5
 
6
6
  test("Should update value when globals are updated and event it triggered", async () => {
7
7
  vi.stubGlobal("openai", {
@@ -0,0 +1,24 @@
1
+ import { expect, test, vi } from "vitest";
2
+ import { renderHookToSnapshotStream } from "@testing-library/react-render-stream";
3
+ import { useOpenExternal } from "../useOpenExternal";
4
+ import { stubOpenAiGlobals } from "../../../testing/internal";
5
+
6
+ test("calls the global openExternal function", async () => {
7
+ const openExternalMock = vi.fn();
8
+
9
+ stubOpenAiGlobals({ openExternal: openExternalMock });
10
+
11
+ const { takeSnapshot } = await renderHookToSnapshotStream(() =>
12
+ useOpenExternal()
13
+ );
14
+
15
+ const openExternal = await takeSnapshot();
16
+ openExternal({ href: "https://example.com" });
17
+
18
+ expect(openExternalMock).toHaveBeenCalledTimes(1);
19
+ expect(openExternalMock).toHaveBeenCalledWith({
20
+ href: "https://example.com",
21
+ });
22
+
23
+ await expect(takeSnapshot).not.toRerender();
24
+ });
@@ -1,6 +1,6 @@
1
1
  import { expect, test, vi } from "vitest";
2
- import { useRequestDisplayMode } from "./useRequestDisplayMode";
3
- import { DisplayMode } from "../types/openai";
2
+ import { useRequestDisplayMode } from "../useRequestDisplayMode";
3
+ import { DisplayMode } from "../../../types/openai";
4
4
 
5
5
  test("Should set display mode when returned function is called", async () => {
6
6
  vi.stubGlobal("openai", {
@@ -1,5 +1,5 @@
1
1
  import { expect, test, vi } from "vitest";
2
- import { useSendFollowUpMessage } from "./useSendFollowUpMessage";
2
+ import { useSendFollowUpMessage } from "../useSendFollowUpMessage";
3
3
 
4
4
  test("Should set display mode when returned function is called", async () => {
5
5
  vi.stubGlobal("openai", {
@@ -1,6 +1,7 @@
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", {
@@ -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
+ });
@@ -1,15 +1,15 @@
1
- import { useSyncExternalStore } from "react";
1
+ import { useSyncExternalStore, useCallback } from "react";
2
2
  import {
3
3
  SET_GLOBALS_EVENT_TYPE,
4
4
  SetGlobalsEvent,
5
5
  OpenAiGlobals,
6
- } from "../types/openai";
6
+ } from "../../types/openai";
7
7
 
8
8
  export function useOpenAiGlobal<K extends keyof OpenAiGlobals>(
9
9
  key: K
10
10
  ): OpenAiGlobals[K] {
11
11
  return useSyncExternalStore(
12
- (onChange) => {
12
+ useCallback((onChange) => {
13
13
  const handleSetGlobal = (event: SetGlobalsEvent) => {
14
14
  const value = event.detail.globals[key];
15
15
  if (value === undefined) {
@@ -26,7 +26,7 @@ export function useOpenAiGlobal<K extends keyof OpenAiGlobals>(
26
26
  return () => {
27
27
  window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal);
28
28
  };
29
- },
29
+ }, []),
30
30
  () => window.openai[key]
31
31
  );
32
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 }) => {
@@ -1,37 +1,14 @@
1
- import React, { useEffect, useState } from "react";
1
+ import React, { useEffect } from "react";
2
2
  import { useToolName } from "./useToolName";
3
3
  import { useToolInput } from "./useToolInput";
4
-
5
- type ToolUseState = {
6
- appName: string;
7
- hasNavigated: boolean;
8
- setHasNavigated: (v: boolean) => void;
9
- };
10
-
11
- const ToolUseContext = React.createContext<ToolUseState | null>(null);
12
-
13
- export function ToolUseProvider({
14
- children,
15
- appName,
16
- }: {
17
- children: any;
18
- appName: string;
19
- }) {
20
- const [hasNavigated, setHasNavigated] = useState(false);
21
-
22
- return (
23
- <ToolUseContext.Provider value={{ hasNavigated, setHasNavigated, appName }}>
24
- {children}
25
- </ToolUseContext.Provider>
26
- );
27
- }
4
+ import { useToolUseState } from "../context/ToolUseContext";
28
5
 
29
6
  export const useToolEffect = (
30
7
  toolName: string | string[],
31
8
  effect: (toolInput: any) => void,
32
9
  deps: React.DependencyList = []
33
10
  ) => {
34
- const ctx = React.useContext(ToolUseContext);
11
+ const ctx = useToolUseState();
35
12
  const fullToolName = useToolName();
36
13
  const toolInput = useToolInput();
37
14
  if (!ctx)
@@ -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
+ }