nitrostack 1.0.74 → 1.0.76

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 (52) hide show
  1. package/dist/auth/__tests__/pkce.test.js +53 -4
  2. package/dist/auth/__tests__/pkce.test.js.map +1 -1
  3. package/dist/auth/__tests__/simple-jwt.test.js +153 -2
  4. package/dist/auth/__tests__/simple-jwt.test.js.map +1 -1
  5. package/dist/cli/commands/install.d.ts +10 -0
  6. package/dist/cli/commands/install.d.ts.map +1 -0
  7. package/dist/cli/commands/install.js +79 -0
  8. package/dist/cli/commands/install.js.map +1 -0
  9. package/dist/cli/index.d.ts.map +1 -1
  10. package/dist/cli/index.js +8 -0
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/core/__tests__/errors.test.d.ts +2 -0
  13. package/dist/core/__tests__/errors.test.d.ts.map +1 -0
  14. package/dist/core/__tests__/errors.test.js +109 -0
  15. package/dist/core/__tests__/errors.test.js.map +1 -0
  16. package/dist/core/__tests__/logger.test.d.ts +2 -0
  17. package/dist/core/__tests__/logger.test.d.ts.map +1 -0
  18. package/dist/core/__tests__/logger.test.js +83 -0
  19. package/dist/core/__tests__/logger.test.js.map +1 -0
  20. package/dist/core/__tests__/prompt.test.d.ts +2 -0
  21. package/dist/core/__tests__/prompt.test.d.ts.map +1 -0
  22. package/dist/core/__tests__/prompt.test.js +126 -0
  23. package/dist/core/__tests__/prompt.test.js.map +1 -0
  24. package/dist/core/__tests__/resource.test.d.ts +2 -0
  25. package/dist/core/__tests__/resource.test.d.ts.map +1 -0
  26. package/dist/core/__tests__/resource.test.js +173 -0
  27. package/dist/core/__tests__/resource.test.js.map +1 -0
  28. package/dist/core/pipes/__tests__/pipes.test.js +113 -1
  29. package/dist/core/pipes/__tests__/pipes.test.js.map +1 -1
  30. package/dist/widgets/hooks/__tests__/hooks.test.d.ts +2 -0
  31. package/dist/widgets/hooks/__tests__/hooks.test.d.ts.map +1 -0
  32. package/dist/widgets/hooks/__tests__/hooks.test.js +129 -0
  33. package/dist/widgets/hooks/__tests__/hooks.test.js.map +1 -0
  34. package/dist/widgets/hooks/__tests__/use-widget-state.test.d.ts +2 -0
  35. package/dist/widgets/hooks/__tests__/use-widget-state.test.d.ts.map +1 -0
  36. package/dist/widgets/hooks/__tests__/use-widget-state.test.js +66 -0
  37. package/dist/widgets/hooks/__tests__/use-widget-state.test.js.map +1 -0
  38. package/dist/widgets/runtime/__tests__/widget-polyfill.test.d.ts +2 -0
  39. package/dist/widgets/runtime/__tests__/widget-polyfill.test.d.ts.map +1 -0
  40. package/dist/widgets/runtime/__tests__/widget-polyfill.test.js +66 -0
  41. package/dist/widgets/runtime/__tests__/widget-polyfill.test.js.map +1 -0
  42. package/package.json +6 -2
  43. package/src/studio/app/chat/page.tsx +21 -0
  44. package/src/studio/lib/llm-service.ts +77 -10
  45. package/templates/typescript-oauth/OAUTH_SETUP.md +592 -0
  46. package/templates/typescript-oauth/README.md +184 -235
  47. package/templates/typescript-oauth/package.json +4 -3
  48. package/templates/typescript-pizzaz/README.md +76 -57
  49. package/templates/typescript-pizzaz/package.json +31 -30
  50. package/templates/typescript-starter/README.md +20 -14
  51. package/templates/typescript-starter/package.json +3 -1
  52. package/templates/typescript-starter/package-lock.json +0 -4112
@@ -0,0 +1,129 @@
1
+ import { jest, describe, it, expect, beforeEach } from '@jest/globals';
2
+ // Mock React with any type to avoid strict typing issues
3
+ const mockUseSyncExternalStore = jest.fn();
4
+ const mockUseState = jest.fn();
5
+ const mockUseEffect = jest.fn();
6
+ jest.unstable_mockModule('react', () => ({
7
+ useSyncExternalStore: mockUseSyncExternalStore,
8
+ useState: mockUseState,
9
+ useEffect: mockUseEffect,
10
+ }));
11
+ // Mock window.openai
12
+ const mockOpenai = {
13
+ theme: 'dark',
14
+ displayMode: 'inline',
15
+ maxHeight: 500,
16
+ locale: 'en-US',
17
+ userAgent: 'test-agent',
18
+ };
19
+ // Set up window with openai
20
+ global.window = {
21
+ openai: mockOpenai,
22
+ addEventListener: jest.fn(),
23
+ removeEventListener: jest.fn(),
24
+ };
25
+ describe('Widget Hooks', () => {
26
+ beforeEach(() => {
27
+ jest.clearAllMocks();
28
+ // Reset useSyncExternalStore to call getSnapshot
29
+ mockUseSyncExternalStore.mockImplementation((_sub, getSnapshot) => {
30
+ return getSnapshot();
31
+ });
32
+ });
33
+ describe('useOpenAiGlobal', () => {
34
+ it('should return theme from window.openai', async () => {
35
+ const { useOpenAiGlobal } = await import('../use-openai-global.js');
36
+ const result = useOpenAiGlobal('theme');
37
+ expect(result).toBe('dark');
38
+ });
39
+ it('should return displayMode from window.openai', async () => {
40
+ const { useOpenAiGlobal } = await import('../use-openai-global.js');
41
+ const result = useOpenAiGlobal('displayMode');
42
+ expect(result).toBe('inline');
43
+ });
44
+ it('should return maxHeight from window.openai', async () => {
45
+ const { useOpenAiGlobal } = await import('../use-openai-global.js');
46
+ const result = useOpenAiGlobal('maxHeight');
47
+ expect(result).toBe(500);
48
+ });
49
+ it('should return locale from window.openai', async () => {
50
+ const { useOpenAiGlobal } = await import('../use-openai-global.js');
51
+ const result = useOpenAiGlobal('locale');
52
+ expect(result).toBe('en-US');
53
+ });
54
+ });
55
+ describe('useTheme', () => {
56
+ it('should return theme value', async () => {
57
+ const { useTheme } = await import('../use-theme.js');
58
+ const result = useTheme();
59
+ expect(result).toBe('dark');
60
+ });
61
+ });
62
+ describe('useDisplayMode', () => {
63
+ it('should return displayMode value', async () => {
64
+ const { useDisplayMode } = await import('../use-display-mode.js');
65
+ const result = useDisplayMode();
66
+ expect(result).toBe('inline');
67
+ });
68
+ });
69
+ describe('useMaxHeight', () => {
70
+ it('should return maxHeight value', async () => {
71
+ const { useMaxHeight } = await import('../use-max-height.js');
72
+ const result = useMaxHeight();
73
+ expect(result).toBe(500);
74
+ });
75
+ });
76
+ describe('useWidgetSDK', () => {
77
+ beforeEach(() => {
78
+ // Mock useState to return initialized values
79
+ let sdkInstance = null;
80
+ mockUseState.mockImplementation((initializer) => {
81
+ if (typeof initializer === 'function') {
82
+ if (!sdkInstance) {
83
+ sdkInstance = initializer();
84
+ }
85
+ return [sdkInstance, jest.fn()];
86
+ }
87
+ return [initializer, jest.fn()];
88
+ });
89
+ // Mock useEffect to run synchronously
90
+ mockUseEffect.mockImplementation((effect) => {
91
+ effect();
92
+ });
93
+ });
94
+ it('should return SDK methods', async () => {
95
+ const { useWidgetSDK } = await import('../useWidgetSDK.js');
96
+ const result = useWidgetSDK();
97
+ expect(result.sdk).toBeDefined();
98
+ expect(typeof result.isReady).toBe('boolean');
99
+ expect(typeof result.setState).toBe('function');
100
+ expect(typeof result.getState).toBe('function');
101
+ expect(typeof result.callTool).toBe('function');
102
+ expect(typeof result.requestFullscreen).toBe('function');
103
+ expect(typeof result.requestInline).toBe('function');
104
+ expect(typeof result.requestPip).toBe('function');
105
+ expect(typeof result.requestDisplayMode).toBe('function');
106
+ expect(typeof result.requestClose).toBe('function');
107
+ });
108
+ it('should return data access methods', async () => {
109
+ const { useWidgetSDK } = await import('../useWidgetSDK.js');
110
+ const result = useWidgetSDK();
111
+ expect(typeof result.getToolInput).toBe('function');
112
+ expect(typeof result.getToolOutput).toBe('function');
113
+ expect(typeof result.getToolResponseMetadata).toBe('function');
114
+ expect(typeof result.getTheme).toBe('function');
115
+ expect(typeof result.getMaxHeight).toBe('function');
116
+ expect(typeof result.getDisplayMode).toBe('function');
117
+ expect(typeof result.getUserAgent).toBe('function');
118
+ expect(typeof result.getLocale).toBe('function');
119
+ expect(typeof result.getSafeArea).toBe('function');
120
+ });
121
+ it('should return navigation methods', async () => {
122
+ const { useWidgetSDK } = await import('../useWidgetSDK.js');
123
+ const result = useWidgetSDK();
124
+ expect(typeof result.openExternal).toBe('function');
125
+ expect(typeof result.sendFollowUpMessage).toBe('function');
126
+ });
127
+ });
128
+ });
129
+ //# sourceMappingURL=hooks.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.test.js","sourceRoot":"","sources":["../../../../src/widgets/hooks/__tests__/hooks.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEvE,yDAAyD;AACzD,MAAM,wBAAwB,GAAG,IAAI,CAAC,EAAE,EAAS,CAAC;AAClD,MAAM,YAAY,GAAG,IAAI,CAAC,EAAE,EAAS,CAAC;AACtC,MAAM,aAAa,GAAG,IAAI,CAAC,EAAE,EAAS,CAAC;AAEvC,IAAI,CAAC,mBAAmB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;IACrC,oBAAoB,EAAE,wBAAwB;IAC9C,QAAQ,EAAE,YAAY;IACtB,SAAS,EAAE,aAAa;CAC3B,CAAC,CAAC,CAAC;AAEJ,qBAAqB;AACrB,MAAM,UAAU,GAAG;IACf,KAAK,EAAE,MAAe;IACtB,WAAW,EAAE,QAAiB;IAC9B,SAAS,EAAE,GAAG;IACd,MAAM,EAAE,OAAO;IACf,SAAS,EAAE,YAAY;CAC1B,CAAC;AAEF,4BAA4B;AAC3B,MAAc,CAAC,MAAM,GAAG;IACrB,MAAM,EAAE,UAAU;IAClB,gBAAgB,EAAE,IAAI,CAAC,EAAE,EAAE;IAC3B,mBAAmB,EAAE,IAAI,CAAC,EAAE,EAAE;CACjC,CAAC;AAEF,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;IAC1B,UAAU,CAAC,GAAG,EAAE;QACZ,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,iDAAiD;QACjD,wBAAwB,CAAC,kBAAkB,CAAC,CAAC,IAAS,EAAE,WAAgB,EAAE,EAAE;YACxE,OAAO,WAAW,EAAE,CAAC;QACzB,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;QAC7B,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;YACpD,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC;YAEpE,MAAM,MAAM,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;YACxC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAChC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;YAC1D,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC;YAEpE,MAAM,MAAM,GAAG,eAAe,CAAC,aAAa,CAAC,CAAC;YAC9C,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAClC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;YACxD,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC;YAEpE,MAAM,MAAM,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;YAC5C,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;YACrD,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC;YAEpE,MAAM,MAAM,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;YACzC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;QACtB,EAAE,CAAC,2BAA2B,EAAE,KAAK,IAAI,EAAE;YACvC,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC;YAErD,MAAM,MAAM,GAAG,QAAQ,EAAE,CAAC;YAC1B,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAChC,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;QAC5B,EAAE,CAAC,iCAAiC,EAAE,KAAK,IAAI,EAAE;YAC7C,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC,wBAAwB,CAAC,CAAC;YAElE,MAAM,MAAM,GAAG,cAAc,EAAE,CAAC;YAChC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAClC,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;QAC1B,EAAE,CAAC,+BAA+B,EAAE,KAAK,IAAI,EAAE;YAC3C,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,sBAAsB,CAAC,CAAC;YAE9D,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;YAC9B,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;QAC1B,UAAU,CAAC,GAAG,EAAE;YACZ,6CAA6C;YAC7C,IAAI,WAAW,GAAQ,IAAI,CAAC;YAC5B,YAAY,CAAC,kBAAkB,CAAC,CAAC,WAAgB,EAAE,EAAE;gBACjD,IAAI,OAAO,WAAW,KAAK,UAAU,EAAE,CAAC;oBACpC,IAAI,CAAC,WAAW,EAAE,CAAC;wBACf,WAAW,GAAG,WAAW,EAAE,CAAC;oBAChC,CAAC;oBACD,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;gBACpC,CAAC;gBACD,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;YACpC,CAAC,CAAC,CAAC;YAEH,sCAAsC;YACtC,aAAa,CAAC,kBAAkB,CAAC,CAAC,MAAW,EAAE,EAAE;gBAC7C,MAAM,EAAE,CAAC;YACb,CAAC,CAAC,CAAC;QACP,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,2BAA2B,EAAE,KAAK,IAAI,EAAE;YACvC,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;YAE5D,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;YAE9B,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;YACjC,MAAM,CAAC,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC9C,MAAM,CAAC,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAChD,MAAM,CAAC,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAChD,MAAM,CAAC,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAChD,MAAM,CAAC,OAAO,MAAM,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACzD,MAAM,CAAC,OAAO,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACrD,MAAM,CAAC,OAAO,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAClD,MAAM,CAAC,OAAO,MAAM,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC1D,MAAM,CAAC,OAAO,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACxD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mCAAmC,EAAE,KAAK,IAAI,EAAE;YAC/C,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;YAE5D,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;YAE9B,MAAM,CAAC,OAAO,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACpD,MAAM,CAAC,OAAO,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACrD,MAAM,CAAC,OAAO,MAAM,CAAC,uBAAuB,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC/D,MAAM,CAAC,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAChD,MAAM,CAAC,OAAO,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACpD,MAAM,CAAC,OAAO,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACtD,MAAM,CAAC,OAAO,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACpD,MAAM,CAAC,OAAO,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACjD,MAAM,CAAC,OAAO,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACvD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,kCAAkC,EAAE,KAAK,IAAI,EAAE;YAC9C,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;YAE5D,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;YAE9B,MAAM,CAAC,OAAO,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACpD,MAAM,CAAC,OAAO,MAAM,CAAC,mBAAmB,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC/D,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=use-widget-state.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-widget-state.test.d.ts","sourceRoot":"","sources":["../../../../src/widgets/hooks/__tests__/use-widget-state.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,66 @@
1
+ import { jest, describe, it, expect, beforeEach } from '@jest/globals';
2
+ // Mock React hooks
3
+ const mockUseSyncExternalStore = jest.fn();
4
+ const mockUseState = jest.fn();
5
+ const mockUseEffect = jest.fn();
6
+ const mockUseCallback = jest.fn();
7
+ jest.unstable_mockModule('react', () => ({
8
+ useSyncExternalStore: mockUseSyncExternalStore,
9
+ useState: mockUseState,
10
+ useEffect: mockUseEffect,
11
+ useCallback: mockUseCallback,
12
+ }));
13
+ // Mock window.openai
14
+ global.window = {
15
+ openai: {
16
+ widgetState: undefined,
17
+ setWidgetState: jest.fn(),
18
+ },
19
+ addEventListener: jest.fn(),
20
+ removeEventListener: jest.fn(),
21
+ };
22
+ describe('useWidgetState', () => {
23
+ beforeEach(() => {
24
+ jest.clearAllMocks();
25
+ // Mock useSyncExternalStore to return null (no widget state from window)
26
+ mockUseSyncExternalStore.mockImplementation(() => null);
27
+ // Mock useState with proper state management
28
+ mockUseState.mockImplementation((initializer) => {
29
+ const value = typeof initializer === 'function' ? initializer() : initializer;
30
+ return [value, jest.fn()];
31
+ });
32
+ // Mock useEffect to run synchronously
33
+ mockUseEffect.mockImplementation((effect) => {
34
+ effect();
35
+ });
36
+ // Mock useCallback to return the function directly
37
+ mockUseCallback.mockImplementation((fn) => fn);
38
+ });
39
+ it('should return default state', async () => {
40
+ const { useWidgetState } = await import('../use-widget-state.js');
41
+ const [state] = useWidgetState({ count: 0 });
42
+ expect(state).toEqual({ count: 0 });
43
+ });
44
+ it('should return state from function initializer', async () => {
45
+ const { useWidgetState } = await import('../use-widget-state.js');
46
+ const [state] = useWidgetState(() => ({ items: [] }));
47
+ expect(state).toEqual({ items: [] });
48
+ });
49
+ it('should return null when no default provided', async () => {
50
+ const { useWidgetState } = await import('../use-widget-state.js');
51
+ const [state] = useWidgetState(null);
52
+ expect(state).toBeNull();
53
+ });
54
+ it('should return widget state from window if available', async () => {
55
+ mockUseSyncExternalStore.mockImplementation(() => ({ savedData: 'value' }));
56
+ const { useWidgetState } = await import('../use-widget-state.js');
57
+ const [state] = useWidgetState({ default: 'value' });
58
+ expect(state).toEqual({ savedData: 'value' });
59
+ });
60
+ it('should return setState function', async () => {
61
+ const { useWidgetState } = await import('../use-widget-state.js');
62
+ const [, setState] = useWidgetState({ count: 0 });
63
+ expect(typeof setState).toBe('function');
64
+ });
65
+ });
66
+ //# sourceMappingURL=use-widget-state.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-widget-state.test.js","sourceRoot":"","sources":["../../../../src/widgets/hooks/__tests__/use-widget-state.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=widget-polyfill.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widget-polyfill.test.d.ts","sourceRoot":"","sources":["../../../../src/widgets/runtime/__tests__/widget-polyfill.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,66 @@
1
+ import { jest, describe, it, expect, beforeEach } from '@jest/globals';
2
+ describe('Widget Polyfill', () => {
3
+ let messageHandler = null;
4
+ beforeEach(() => {
5
+ // Reset
6
+ messageHandler = null;
7
+ global.window = {
8
+ addEventListener: jest.fn((event, handler) => {
9
+ if (event === 'message') {
10
+ messageHandler = handler;
11
+ }
12
+ }),
13
+ dispatchEvent: jest.fn(),
14
+ };
15
+ global.CustomEvent = jest.fn().mockImplementation((name) => ({ type: name }));
16
+ });
17
+ it('should be a module', async () => {
18
+ // Widget polyfill is an IIFE, we just need to check it can be loaded
19
+ // The actual functionality sets up window listeners
20
+ expect(true).toBe(true);
21
+ });
22
+ it('should handle NITRO_INJECT_OPENAI message', async () => {
23
+ // Manually simulate what the polyfill does
24
+ const mockOpenai = { theme: 'dark' };
25
+ const event = {
26
+ data: {
27
+ type: 'NITRO_INJECT_OPENAI',
28
+ openai: mockOpenai
29
+ }
30
+ };
31
+ // Set up window
32
+ global.window = {
33
+ addEventListener: jest.fn(),
34
+ dispatchEvent: jest.fn(),
35
+ openai: undefined,
36
+ };
37
+ // Simulate the message handler behavior
38
+ if (event.data?.type === 'NITRO_INJECT_OPENAI') {
39
+ window.openai = event.data.openai;
40
+ }
41
+ expect(window.openai).toEqual(mockOpenai);
42
+ });
43
+ it('should handle TOOL_OUTPUT message', async () => {
44
+ const mockData = { key: 'value' };
45
+ const event = {
46
+ data: {
47
+ type: 'TOOL_OUTPUT',
48
+ data: mockData
49
+ }
50
+ };
51
+ // Set up window with openai already set
52
+ global.window = {
53
+ addEventListener: jest.fn(),
54
+ dispatchEvent: jest.fn(),
55
+ openai: { toolOutput: undefined },
56
+ };
57
+ // Simulate the message handler behavior
58
+ if (event.data?.type === 'TOOL_OUTPUT' && event.data?.data) {
59
+ if (window.openai) {
60
+ window.openai.toolOutput = event.data.data;
61
+ }
62
+ }
63
+ expect(window.openai.toolOutput).toEqual(mockData);
64
+ });
65
+ });
66
+ //# sourceMappingURL=widget-polyfill.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widget-polyfill.test.js","sourceRoot":"","sources":["../../../../src/widgets/runtime/__tests__/widget-polyfill.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEvE,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC7B,IAAI,cAAc,GAAkC,IAAI,CAAC;IAEzD,UAAU,CAAC,GAAG,EAAE;QACZ,QAAQ;QACR,cAAc,GAAG,IAAI,CAAC;QACrB,MAAc,CAAC,MAAM,GAAG;YACrB,gBAAgB,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,KAAa,EAAE,OAAY,EAAE,EAAE;gBACtD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;oBACtB,cAAc,GAAG,OAAO,CAAC;gBAC7B,CAAC;YACL,CAAC,CAAC;YACF,aAAa,EAAE,IAAI,CAAC,EAAE,EAAE;SAC3B,CAAC;QACD,MAAc,CAAC,WAAW,GAAG,IAAI,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,CAAC,IAAS,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAChG,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oBAAoB,EAAE,KAAK,IAAI,EAAE;QAChC,qEAAqE;QACrE,oDAAoD;QACpD,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,KAAK,IAAI,EAAE;QACvD,2CAA2C;QAC3C,MAAM,UAAU,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QACrC,MAAM,KAAK,GAAG;YACV,IAAI,EAAE;gBACF,IAAI,EAAE,qBAAqB;gBAC3B,MAAM,EAAE,UAAU;aACrB;SACJ,CAAC;QAEF,gBAAgB;QACf,MAAc,CAAC,MAAM,GAAG;YACrB,gBAAgB,EAAE,IAAI,CAAC,EAAE,EAAE;YAC3B,aAAa,EAAE,IAAI,CAAC,EAAE,EAAE;YACxB,MAAM,EAAE,SAAS;SACpB,CAAC;QAEF,wCAAwC;QACxC,IAAI,KAAK,CAAC,IAAI,EAAE,IAAI,KAAK,qBAAqB,EAAE,CAAC;YAC5C,MAAc,CAAC,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC;QAC/C,CAAC;QAED,MAAM,CAAE,MAAc,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,KAAK,IAAI,EAAE;QAC/C,MAAM,QAAQ,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG;YACV,IAAI,EAAE;gBACF,IAAI,EAAE,aAAa;gBACnB,IAAI,EAAE,QAAQ;aACjB;SACJ,CAAC;QAEF,wCAAwC;QACvC,MAAc,CAAC,MAAM,GAAG;YACrB,gBAAgB,EAAE,IAAI,CAAC,EAAE,EAAE;YAC3B,aAAa,EAAE,IAAI,CAAC,EAAE,EAAE;YACxB,MAAM,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE;SACpC,CAAC;QAEF,wCAAwC;QACxC,IAAI,KAAK,CAAC,IAAI,EAAE,IAAI,KAAK,aAAa,IAAI,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;YACzD,IAAK,MAAc,CAAC,MAAM,EAAE,CAAC;gBACxB,MAAc,CAAC,MAAM,CAAC,UAAU,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;YACxD,CAAC;QACL,CAAC;QAED,MAAM,CAAE,MAAc,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitrostack",
3
- "version": "1.0.74",
3
+ "version": "1.0.76",
4
4
  "description": "NitroStack - Build powerful MCP servers with TypeScript",
5
5
  "type": "module",
6
6
  "main": "dist/core/index.js",
@@ -87,6 +87,8 @@
87
87
  "zod-to-json-schema": "^3.24.6"
88
88
  },
89
89
  "devDependencies": {
90
+ "@testing-library/jest-dom": "^6.9.1",
91
+ "@testing-library/react": "^16.3.1",
90
92
  "@types/bcryptjs": "^2.4.6",
91
93
  "@types/better-sqlite3": "^7.6.12",
92
94
  "@types/cors": "^2.8.19",
@@ -100,6 +102,8 @@
100
102
  "@types/uuid": "^10.0.0",
101
103
  "@types/ws": "^8.18.1",
102
104
  "jest": "^29.7.0",
105
+ "react": "^19.2.3",
106
+ "react-dom": "^19.2.3",
103
107
  "ts-jest": "^29.2.5",
104
108
  "typescript": "^5.7.2"
105
109
  },
@@ -115,4 +119,4 @@
115
119
  "url": "https://github.com/abhishekpanditofficial/nitrostack/issues"
116
120
  },
117
121
  "homepage": "https://nitrostack.vercel.app"
118
- }
122
+ }
@@ -2181,6 +2181,27 @@ function ChatMessageComponent({ message, tools }: { message: ChatMessage; tools:
2181
2181
  if (message.role === 'tool') return null;
2182
2182
  const isUser = message.role === 'user';
2183
2183
 
2184
+ // Check if this message has any visible content to show
2185
+ const hasContent = !!message.content;
2186
+ const hasFile = !!message.file;
2187
+ const hasToolCalls = message.toolCalls && message.toolCalls.length > 0;
2188
+
2189
+ // Check if any tool call has a widget to display
2190
+ const hasVisibleWidgets = hasToolCalls && message.toolCalls?.some((tc: ToolCall) => {
2191
+ const tool = tools.find((t) => t.name === tc.name);
2192
+ const componentUri = tool?.widget?.route ||
2193
+ tool?.outputTemplate ||
2194
+ tool?._meta?.['openai/outputTemplate'] ||
2195
+ tool?._meta?.['ui/template'];
2196
+ return !!componentUri && !!(tc.result || tc.arguments);
2197
+ });
2198
+
2199
+ // For assistant messages with tool calls but no widgets and no content, skip rendering
2200
+ // This prevents the empty bubble from showing
2201
+ if (!isUser && !hasContent && !hasFile && hasToolCalls && !hasVisibleWidgets) {
2202
+ return null;
2203
+ }
2204
+
2184
2205
  return (
2185
2206
  <div className="flex gap-4 items-start animate-fade-in group">
2186
2207
  {!isUser && (
@@ -466,10 +466,7 @@ User: "list all resources"
466
466
  } else {
467
467
  // Regular user or assistant message
468
468
  const parts: GeminiFunctionPart[] = [];
469
-
470
- if (msg.content) {
471
- parts.push({ text: msg.content });
472
- }
469
+ let hasImageData = false;
473
470
 
474
471
  if (msg.role === 'user' && msg.file) {
475
472
  // Extract base64 and mime type
@@ -486,6 +483,7 @@ User: "list all resources"
486
483
  mimeType.startsWith('audio/');
487
484
 
488
485
  if (isSupported) {
486
+ hasImageData = true;
489
487
  parts.push({
490
488
  inlineData: {
491
489
  mimeType: mimeType,
@@ -516,10 +514,22 @@ User: "list all resources"
516
514
  }
517
515
  }
518
516
 
519
- contents.push({
520
- role: msg.role === 'assistant' ? 'model' : 'user',
521
- parts,
522
- });
517
+ // Add text content - IMPORTANT: Gemini REQUIRES a text prompt when processing images
518
+ if (msg.content) {
519
+ parts.push({ text: msg.content });
520
+ } else if (hasImageData) {
521
+ // If we have image data but no text, add a default prompt
522
+ // Gemini requires at least some text instruction with images
523
+ parts.push({ text: `Please analyze this image (${msg.file?.name || 'uploaded file'}) and process it with the appropriate tool if needed.` });
524
+ }
525
+
526
+ // Only add content if we have parts
527
+ if (parts.length > 0) {
528
+ contents.push({
529
+ role: msg.role === 'assistant' ? 'model' : 'user',
530
+ parts,
531
+ });
532
+ }
523
533
  i++;
524
534
  }
525
535
  }
@@ -582,8 +592,36 @@ User: "list all resources"
582
592
  // console.log('Sending tools to Gemini:', JSON.stringify(requestBody.tools, null, 2));
583
593
  }
584
594
 
595
+ // Add generation config to ensure we get a response
596
+ requestBody.generationConfig = {
597
+ temperature: 0.7,
598
+ topP: 0.95,
599
+ topK: 40,
600
+ maxOutputTokens: 8192,
601
+ };
602
+
603
+ // Add safety settings to be more permissive (for image processing)
604
+ requestBody.safetySettings = [
605
+ { category: 'HARM_CATEGORY_HARASSMENT', threshold: 'BLOCK_ONLY_HIGH' },
606
+ { category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_ONLY_HIGH' },
607
+ { category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', threshold: 'BLOCK_ONLY_HIGH' },
608
+ { category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_ONLY_HIGH' },
609
+ ];
610
+
611
+ // Log request for debugging (truncate large data)
612
+ const debugRequest = JSON.parse(JSON.stringify(requestBody));
613
+ if (debugRequest.contents) {
614
+ debugRequest.contents = debugRequest.contents.map((c: { parts?: Array<{ inlineData?: { data?: string } }> }) => ({
615
+ ...c,
616
+ parts: c.parts?.map((p: { inlineData?: { data?: string } }) =>
617
+ p.inlineData ? { ...p, inlineData: { ...p.inlineData, data: '[BASE64_TRUNCATED]' } } : p
618
+ )
619
+ }));
620
+ }
621
+ console.log('Gemini request body (truncated):', JSON.stringify(debugRequest, null, 2));
622
+
585
623
  // Use Gemini 2.0 Flash Experimental (latest model with function calling)
586
- // The v1beta API uses 'gemini-2.0-flash-exp' for the newest features
624
+ // Fallback to gemini-1.5-flash if issues persist
587
625
  const model = 'gemini-2.0-flash-exp';
588
626
  const response = await fetch(
589
627
  `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`,
@@ -605,7 +643,36 @@ User: "list all resources"
605
643
 
606
644
  const candidate = data.candidates?.[0];
607
645
  if (!candidate) {
608
- throw new Error('No response from Gemini');
646
+ // Check for prompt feedback (content filtering)
647
+ if (data.promptFeedback) {
648
+ const feedback = data.promptFeedback;
649
+ if (feedback.blockReason) {
650
+ throw new Error(`Gemini blocked the request: ${feedback.blockReason}. ${feedback.blockReasonMessage || ''}`);
651
+ }
652
+ if (feedback.safetyRatings) {
653
+ const blockedRatings = feedback.safetyRatings.filter(
654
+ (r: { probability: string }) => r.probability === 'HIGH' || r.probability === 'MEDIUM'
655
+ );
656
+ if (blockedRatings.length > 0) {
657
+ throw new Error(`Gemini safety filter triggered: ${JSON.stringify(blockedRatings)}`);
658
+ }
659
+ }
660
+ }
661
+
662
+ // Check if there's a candidates array but it's empty with finish reason
663
+ if (data.candidates && data.candidates.length === 0) {
664
+ throw new Error('Gemini returned empty candidates - content may have been filtered');
665
+ }
666
+
667
+ // Generic error with full response for debugging
668
+ console.error('Full Gemini response without candidates:', JSON.stringify(data, null, 2));
669
+ throw new Error(`No response from Gemini. Response metadata: ${JSON.stringify(data.usageMetadata || {})}`);
670
+ }
671
+
672
+ // Check if candidate was blocked
673
+ if (candidate.finishReason === 'SAFETY' || candidate.finishReason === 'BLOCKED') {
674
+ const safetyRatings = candidate.safetyRatings || [];
675
+ throw new Error(`Gemini blocked response due to safety: ${JSON.stringify(safetyRatings)}`);
609
676
  }
610
677
 
611
678
  const parts = candidate.content?.parts || [];