sunpeak 0.16.17 → 0.16.21
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 +2 -2
- package/bin/commands/build.mjs +1 -1
- package/bin/commands/dev.mjs +137 -19
- package/bin/commands/new.mjs +21 -2
- package/bin/commands/start.mjs +1 -1
- package/dist/chatgpt/chatgpt-conversation.d.ts +3 -1
- package/dist/chatgpt/globals.css +37 -8
- package/dist/chatgpt/index.cjs +3 -5
- package/dist/chatgpt/index.cjs.map +1 -1
- package/dist/chatgpt/index.d.ts +0 -1
- package/dist/chatgpt/index.js +3 -5
- package/dist/chatgpt/index.js.map +1 -1
- package/dist/claude/claude-conversation.d.ts +3 -1
- package/dist/claude/index.cjs +2 -2
- package/dist/claude/index.d.ts +1 -1
- package/dist/claude/index.js +2 -2
- package/dist/{discovery-DvIQWTez.js → discovery-BVqD-JsT.js} +4 -2
- package/dist/{discovery-DvIQWTez.js.map → discovery-BVqD-JsT.js.map} +1 -1
- package/dist/{discovery-SviNiBkF.cjs → discovery-D1gpaVz4.cjs} +4 -2
- package/dist/{discovery-SviNiBkF.cjs.map → discovery-D1gpaVz4.cjs.map} +1 -1
- package/dist/hooks/index.d.ts +10 -1
- package/dist/hooks/safe-area.d.ts +6 -2
- package/dist/hooks/use-device-capabilities.d.ts +3 -0
- package/dist/hooks/use-platform.d.ts +3 -0
- package/dist/hooks/use-styles.d.ts +2 -0
- package/dist/hooks/use-time-zone.d.ts +1 -0
- package/dist/hooks/use-tool-info.d.ts +3 -0
- package/dist/hooks/use-user-agent.d.ts +1 -0
- package/dist/hooks/use-viewport.d.ts +3 -0
- package/dist/{platform → host}/chatgpt/index.cjs +1 -1
- package/dist/host/chatgpt/index.cjs.map +1 -0
- package/dist/{platform → host}/chatgpt/index.d.ts +2 -2
- package/dist/{platform → host}/chatgpt/index.js +1 -1
- package/dist/host/chatgpt/index.js.map +1 -0
- package/dist/{platform → host}/chatgpt/use-create-file.d.ts +2 -2
- package/dist/{platform → host}/chatgpt/use-file-download.d.ts +2 -2
- package/dist/{platform → host}/chatgpt/use-open-modal.d.ts +2 -2
- package/dist/{platform → host}/chatgpt/use-request-checkout.d.ts +2 -2
- package/dist/{platform → host}/index.cjs +5 -3
- package/dist/host/index.cjs.map +1 -0
- package/dist/{platform → host}/index.d.ts +15 -11
- package/dist/{platform → host}/index.js +5 -3
- package/dist/host/index.js.map +1 -0
- package/dist/{index-CsYoMHyn.js → index-B4aC3vjH.js} +4 -4
- package/dist/index-B4aC3vjH.js.map +1 -0
- package/dist/{index-DHcaJ5PU.cjs → index-CKabCJyV.cjs} +4 -4
- package/dist/index-CKabCJyV.cjs.map +1 -0
- package/dist/index-CX6Z4bED.js +29 -0
- package/dist/index-CX6Z4bED.js.map +1 -0
- package/dist/index-bKBBCBK6.cjs +28 -0
- package/dist/index-bKBBCBK6.cjs.map +1 -0
- package/dist/index.cjs +233 -6297
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +228 -6292
- package/dist/index.js.map +1 -1
- package/dist/lib/discovery-cli.cjs +1 -1
- package/dist/lib/discovery-cli.js +1 -1
- package/dist/mcp/index.cjs +680 -6766
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.js +682 -6768
- package/dist/mcp/index.js.map +1 -1
- package/dist/{protocol-CfvM5B6z.cjs → protocol-DkDHRwOW.cjs} +50 -5
- package/dist/{protocol-CfvM5B6z.cjs.map → protocol-DkDHRwOW.cjs.map} +1 -1
- package/dist/{protocol-CF-P_kw5.js → protocol-uge7qFev.js} +102 -57
- package/dist/{protocol-CF-P_kw5.js.map → protocol-uge7qFev.js.map} +1 -1
- package/dist/simulator/hosts.d.ts +2 -0
- package/dist/simulator/index.cjs +3 -3
- package/dist/simulator/index.js +3 -3
- package/dist/simulator/simple-sidebar.d.ts +18 -4
- package/dist/simulator/simulator-url.d.ts +8 -0
- package/dist/simulator/simulator.d.ts +13 -1
- package/dist/simulator/use-simulator-state.d.ts +10 -6
- package/dist/simulator-D8t-r7HH.js +3222 -0
- package/dist/simulator-D8t-r7HH.js.map +1 -0
- package/dist/simulator-FFNttkqL.cjs +3237 -0
- package/dist/simulator-FFNttkqL.cjs.map +1 -0
- package/dist/{simulator-url-rgg_KYOg.cjs → simulator-url-DcSYRl-P.cjs} +7 -1
- package/dist/simulator-url-DcSYRl-P.cjs.map +1 -0
- package/dist/{simulator-url-CuLqtnSS.js → simulator-url-j_XV3EoP.js} +7 -1
- package/dist/simulator-url-j_XV3EoP.js.map +1 -0
- package/dist/style.css +37 -8
- package/dist/use-app-C9gpzIQO.js +349 -0
- package/dist/use-app-C9gpzIQO.js.map +1 -0
- package/dist/use-app-D09O2swh.cjs +348 -0
- package/dist/use-app-D09O2swh.cjs.map +1 -0
- package/package.json +26 -14
- package/template/.sunpeak/dev.tsx +28 -2
- package/template/node_modules/.bin/vite +2 -2
- package/template/node_modules/.bin/vitest +2 -2
- package/template/package.json +5 -5
- package/template/playwright.config.ts +6 -3
- package/template/src/resources/albums/albums.test.tsx +1 -0
- package/template/src/resources/albums/albums.tsx +5 -2
- package/template/src/resources/albums/components/albums.test.tsx +22 -18
- package/template/src/resources/albums/components/albums.tsx +63 -7
- package/template/src/resources/albums/components/fullscreen-viewer.test.tsx +3 -25
- package/template/src/resources/albums/components/fullscreen-viewer.tsx +2 -3
- package/template/src/resources/carousel/carousel.test.tsx +12 -16
- package/template/src/resources/carousel/carousel.tsx +47 -5
- package/template/src/resources/map/components/map.tsx +65 -9
- package/template/src/resources/map/map.test.tsx +0 -1
- package/template/src/resources/review/review.test.tsx +25 -27
- package/template/src/resources/review/review.tsx +85 -63
- package/template/src/tools/review-diff.test.ts +73 -0
- package/template/src/tools/review-diff.ts +29 -2
- package/template/src/tools/review-post.test.ts +100 -0
- package/template/src/tools/review-post.ts +30 -2
- package/template/src/tools/review-purchase.test.ts +111 -0
- package/template/src/tools/review-purchase.ts +35 -2
- package/template/src/tools/review.test.ts +40 -0
- package/template/src/tools/review.ts +4 -1
- package/template/src/tools/show-albums.test.ts +42 -0
- package/template/src/tools/show-albums.ts +22 -2
- package/template/src/tools/show-carousel.test.ts +45 -0
- package/template/src/tools/show-carousel.ts +19 -2
- package/template/src/tools/show-map.test.ts +74 -0
- package/template/src/tools/show-map.ts +21 -2
- package/template/tests/e2e/albums.spec.ts +75 -0
- package/template/tests/e2e/carousel.spec.ts +65 -0
- package/template/tests/e2e/global-setup.ts +25 -0
- package/template/tests/e2e/map.spec.ts +60 -0
- package/template/tests/e2e/review.spec.ts +72 -11
- package/dist/chatgpt/chatgpt-simulator.d.ts +0 -10
- package/dist/index-BFD3bAHd.cjs +0 -547
- package/dist/index-BFD3bAHd.cjs.map +0 -1
- package/dist/index-CsYoMHyn.js.map +0 -1
- package/dist/index-DHcaJ5PU.cjs.map +0 -1
- package/dist/index-wUvmyoCx.js +0 -532
- package/dist/index-wUvmyoCx.js.map +0 -1
- package/dist/platform/chatgpt/index.cjs.map +0 -1
- package/dist/platform/chatgpt/index.js.map +0 -1
- package/dist/platform/index.cjs.map +0 -1
- package/dist/platform/index.js.map +0 -1
- package/dist/simulator-BEFsuj9Z.cjs +0 -8872
- package/dist/simulator-BEFsuj9Z.cjs.map +0 -1
- package/dist/simulator-Da9iAupa.js +0 -8857
- package/dist/simulator-Da9iAupa.js.map +0 -1
- package/dist/simulator-url-CuLqtnSS.js.map +0 -1
- package/dist/simulator-url-rgg_KYOg.cjs.map +0 -1
- package/dist/use-app-CaTJmpgj.cjs +0 -6449
- package/dist/use-app-CaTJmpgj.cjs.map +0 -1
- package/dist/use-app-DTTzqi-0.js +0 -6450
- package/dist/use-app-DTTzqi-0.js.map +0 -1
- /package/dist/{platform → host}/chatgpt/openai-types.d.ts +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useToolData,
|
|
1
|
+
import { useToolData, useDeviceCapabilities, useDisplayMode, SafeArea } from 'sunpeak';
|
|
2
2
|
import type { ResourceConfig } from 'sunpeak';
|
|
3
3
|
import { Carousel, Card } from './components';
|
|
4
4
|
|
|
@@ -32,18 +32,60 @@ interface CarouselCard {
|
|
|
32
32
|
description: string;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
interface CarouselInput {
|
|
36
|
+
city?: string;
|
|
37
|
+
state?: string;
|
|
38
|
+
categories?: string[];
|
|
39
|
+
limit?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
35
42
|
interface CarouselData {
|
|
36
43
|
places: CarouselCard[];
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
export function CarouselResource() {
|
|
40
|
-
const { output
|
|
41
|
-
|
|
47
|
+
const { output, inputPartial, isLoading, isError, isCancelled, cancelReason } = useToolData<
|
|
48
|
+
CarouselInput,
|
|
49
|
+
CarouselData
|
|
50
|
+
>();
|
|
51
|
+
const { touch: hasTouch = false } = useDeviceCapabilities();
|
|
42
52
|
const displayMode = useDisplayMode();
|
|
43
|
-
|
|
44
|
-
const hasTouch = context?.deviceCapabilities?.touch ?? false;
|
|
45
53
|
const places = output?.places ?? [];
|
|
46
54
|
|
|
55
|
+
if (isLoading) {
|
|
56
|
+
const searchContext = inputPartial?.city;
|
|
57
|
+
return (
|
|
58
|
+
<SafeArea className="flex items-center justify-center gap-2 p-8 text-[var(--color-text-secondary)]">
|
|
59
|
+
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
|
60
|
+
<span>{searchContext ? `Finding places in ${searchContext}…` : 'Loading places…'}</span>
|
|
61
|
+
</SafeArea>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (isError) {
|
|
66
|
+
return (
|
|
67
|
+
<SafeArea className="flex items-center justify-center p-8 text-[var(--color-text-secondary)]">
|
|
68
|
+
Failed to load places
|
|
69
|
+
</SafeArea>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (isCancelled) {
|
|
74
|
+
return (
|
|
75
|
+
<SafeArea className="flex items-center justify-center p-8 text-[var(--color-text-secondary)]">
|
|
76
|
+
{cancelReason ?? 'Request was cancelled'}
|
|
77
|
+
</SafeArea>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (places.length === 0) {
|
|
82
|
+
return (
|
|
83
|
+
<SafeArea className="flex items-center justify-center p-8 text-[var(--color-text-secondary)]">
|
|
84
|
+
No places found
|
|
85
|
+
</SafeArea>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
47
89
|
return (
|
|
48
90
|
<SafeArea className="p-4">
|
|
49
91
|
<Carousel
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
useAppState,
|
|
4
|
+
useDisplayMode,
|
|
5
|
+
useRequestDisplayMode,
|
|
6
|
+
useToolData,
|
|
7
|
+
useViewport,
|
|
8
|
+
useUpdateModelContext,
|
|
9
|
+
} from 'sunpeak';
|
|
3
10
|
import { Button } from '@/components/button';
|
|
4
11
|
import { ExpandLg } from '@/components/icon';
|
|
5
12
|
import { cn } from '@/lib/index';
|
|
@@ -9,6 +16,14 @@ import { PlaceInspector } from './place-inspector';
|
|
|
9
16
|
import { MapView } from './map-view';
|
|
10
17
|
import type { Place, MapData } from './types';
|
|
11
18
|
|
|
19
|
+
interface MapInput {
|
|
20
|
+
query?: string;
|
|
21
|
+
location?: { lat: number; lng: number };
|
|
22
|
+
radius?: number;
|
|
23
|
+
minRating?: number;
|
|
24
|
+
priceRange?: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
interface MapState {
|
|
13
28
|
selectedPlaceId: string | null;
|
|
14
29
|
}
|
|
@@ -18,24 +33,32 @@ export type MapProps = {
|
|
|
18
33
|
};
|
|
19
34
|
|
|
20
35
|
export function Map({ className }: MapProps) {
|
|
21
|
-
const
|
|
22
|
-
|
|
36
|
+
const { output, inputPartial, isLoading, isError, isCancelled, cancelReason } = useToolData<
|
|
37
|
+
MapInput,
|
|
38
|
+
MapData
|
|
39
|
+
>();
|
|
23
40
|
const [state, setState] = useAppState<MapState>({
|
|
24
41
|
selectedPlaceId: null,
|
|
25
42
|
});
|
|
26
43
|
const displayMode = useDisplayMode();
|
|
44
|
+
const { requestDisplayMode, availableModes } = useRequestDisplayMode();
|
|
27
45
|
const viewport = useViewport();
|
|
46
|
+
const updateModelContext = useUpdateModelContext();
|
|
28
47
|
|
|
29
48
|
const maxHeight = viewport?.maxHeight ?? null;
|
|
30
49
|
const places = output?.places ?? [];
|
|
31
50
|
const selectedPlace = places.find((place: Place) => place.id === state.selectedPlaceId);
|
|
32
51
|
const isFullscreen = displayMode === 'fullscreen';
|
|
52
|
+
const canFullscreen = availableModes?.includes('fullscreen') ?? false;
|
|
33
53
|
|
|
34
54
|
const handleSelectPlace = React.useCallback(
|
|
35
55
|
(place: Place) => {
|
|
36
56
|
setState((prev) => ({ ...prev, selectedPlaceId: place.id }));
|
|
57
|
+
updateModelContext({
|
|
58
|
+
structuredContent: { selectedPlace: { id: place.id, name: place.name } },
|
|
59
|
+
});
|
|
37
60
|
},
|
|
38
|
-
[setState]
|
|
61
|
+
[setState, updateModelContext]
|
|
39
62
|
);
|
|
40
63
|
|
|
41
64
|
const handleCloseInspector = React.useCallback(() => {
|
|
@@ -47,8 +70,42 @@ export function Map({ className }: MapProps) {
|
|
|
47
70
|
if (state.selectedPlaceId) {
|
|
48
71
|
setState((prev) => ({ ...prev, selectedPlaceId: null }));
|
|
49
72
|
}
|
|
50
|
-
|
|
51
|
-
}, [
|
|
73
|
+
requestDisplayMode('fullscreen');
|
|
74
|
+
}, [requestDisplayMode, state.selectedPlaceId, setState]);
|
|
75
|
+
|
|
76
|
+
if (isLoading) {
|
|
77
|
+
const searchContext = inputPartial?.query;
|
|
78
|
+
return (
|
|
79
|
+
<div className="flex items-center justify-center gap-2 p-8 text-[var(--color-text-secondary)]">
|
|
80
|
+
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
|
81
|
+
<span>{searchContext ? `Searching for ${searchContext}…` : 'Loading map…'}</span>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (isError) {
|
|
87
|
+
return (
|
|
88
|
+
<div className="flex items-center justify-center p-8 text-[var(--color-text-secondary)]">
|
|
89
|
+
Failed to load map data
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (isCancelled) {
|
|
95
|
+
return (
|
|
96
|
+
<div className="flex items-center justify-center p-8 text-[var(--color-text-secondary)]">
|
|
97
|
+
{cancelReason ?? 'Request was cancelled'}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (places.length === 0) {
|
|
103
|
+
return (
|
|
104
|
+
<div className="flex items-center justify-center p-8 text-[var(--color-text-secondary)]">
|
|
105
|
+
No places found
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
52
109
|
|
|
53
110
|
const containerHeight = isFullscreen ? (maxHeight ?? 600) - 40 : 480;
|
|
54
111
|
|
|
@@ -58,7 +115,6 @@ export function Map({ className }: MapProps) {
|
|
|
58
115
|
style={{
|
|
59
116
|
height: containerHeight,
|
|
60
117
|
minHeight: 480,
|
|
61
|
-
maxHeight: maxHeight ?? undefined,
|
|
62
118
|
}}
|
|
63
119
|
>
|
|
64
120
|
<div
|
|
@@ -69,8 +125,8 @@ export function Map({ className }: MapProps) {
|
|
|
69
125
|
: 'border border-[var(--color-border-tertiary)] rounded-2xl sm:rounded-3xl'
|
|
70
126
|
)}
|
|
71
127
|
>
|
|
72
|
-
{/* Fullscreen button - only show
|
|
73
|
-
{!isFullscreen && (
|
|
128
|
+
{/* Fullscreen button - only show when fullscreen is available and not already fullscreen */}
|
|
129
|
+
{!isFullscreen && canFullscreen && (
|
|
74
130
|
<Button
|
|
75
131
|
variant="solid"
|
|
76
132
|
color="secondary"
|
|
@@ -4,7 +4,6 @@ import { MapResource } from './map';
|
|
|
4
4
|
|
|
5
5
|
// Mock sunpeak — SafeArea renders as a plain div
|
|
6
6
|
vi.mock('sunpeak', () => ({
|
|
7
|
-
useApp: () => null,
|
|
8
7
|
SafeArea: ({ children, ...props }: { children: React.ReactNode; [key: string]: unknown }) => (
|
|
9
8
|
<div data-testid="safe-area" {...props}>
|
|
10
9
|
{children}
|
|
@@ -15,21 +15,18 @@ let mockState: Record<string, unknown> = {
|
|
|
15
15
|
serverMessage: null,
|
|
16
16
|
serverError: false,
|
|
17
17
|
};
|
|
18
|
-
let
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
deviceCapabilities: { hover: true, touch: false },
|
|
18
|
+
let mockDeviceCapabilities: { hover?: boolean; touch?: boolean } = {
|
|
19
|
+
hover: true,
|
|
20
|
+
touch: false,
|
|
22
21
|
};
|
|
22
|
+
let mockAvailableModes: string[] = ['inline', 'fullscreen'];
|
|
23
23
|
let mockDisplayMode: 'inline' | 'fullscreen' = 'inline';
|
|
24
24
|
|
|
25
|
-
const
|
|
26
|
-
requestDisplayMode: mockRequestDisplayMode,
|
|
27
|
-
};
|
|
25
|
+
const mockUpdateModelContext = vi.fn();
|
|
28
26
|
|
|
29
27
|
vi.mock('sunpeak', () => ({
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
output: { ...defaultOutput, ...mockToolOutput },
|
|
28
|
+
useToolData: () => ({
|
|
29
|
+
output: mockToolOutput,
|
|
33
30
|
input: null,
|
|
34
31
|
inputPartial: null,
|
|
35
32
|
isError: false,
|
|
@@ -37,10 +34,18 @@ vi.mock('sunpeak', () => ({
|
|
|
37
34
|
isCancelled: false,
|
|
38
35
|
cancelReason: null,
|
|
39
36
|
}),
|
|
40
|
-
|
|
37
|
+
useDeviceCapabilities: () => mockDeviceCapabilities,
|
|
38
|
+
useHostInfo: () => ({ hostVersion: undefined, hostCapabilities: { serverTools: true } }),
|
|
41
39
|
useDisplayMode: () => mockDisplayMode,
|
|
40
|
+
useRequestDisplayMode: () => ({
|
|
41
|
+
requestDisplayMode: mockRequestDisplayMode,
|
|
42
|
+
availableModes: mockAvailableModes,
|
|
43
|
+
}),
|
|
42
44
|
useAppState: () => [mockState, mockSetState],
|
|
43
45
|
useCallServerTool: () => mockCallServerTool,
|
|
46
|
+
useUpdateModelContext: () => mockUpdateModelContext,
|
|
47
|
+
useTimeZone: () => 'America/New_York',
|
|
48
|
+
useLocale: () => 'en-US',
|
|
44
49
|
SafeArea: ({ children, ...props }: { children: React.ReactNode; [key: string]: unknown }) => (
|
|
45
50
|
<div data-testid="safe-area" {...props}>
|
|
46
51
|
{children}
|
|
@@ -100,7 +105,8 @@ describe('ReviewResource', () => {
|
|
|
100
105
|
serverMessage: null,
|
|
101
106
|
serverError: false,
|
|
102
107
|
};
|
|
103
|
-
|
|
108
|
+
mockDeviceCapabilities = { hover: true, touch: false };
|
|
109
|
+
mockAvailableModes = ['inline', 'fullscreen'];
|
|
104
110
|
mockDisplayMode = 'inline';
|
|
105
111
|
});
|
|
106
112
|
|
|
@@ -123,14 +129,6 @@ describe('ReviewResource', () => {
|
|
|
123
129
|
|
|
124
130
|
expect(screen.getByText('Please review the following items')).toBeInTheDocument();
|
|
125
131
|
});
|
|
126
|
-
|
|
127
|
-
it('renders loading when no sections', () => {
|
|
128
|
-
mockToolOutput = { title: 'Test', sections: [] };
|
|
129
|
-
|
|
130
|
-
render(<ReviewResource />);
|
|
131
|
-
|
|
132
|
-
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
133
|
-
});
|
|
134
132
|
});
|
|
135
133
|
|
|
136
134
|
describe('Action Buttons', () => {
|
|
@@ -610,8 +608,8 @@ describe('ReviewResource', () => {
|
|
|
610
608
|
expect(safeArea).toBeInTheDocument();
|
|
611
609
|
});
|
|
612
610
|
|
|
613
|
-
it('handles
|
|
614
|
-
|
|
611
|
+
it('handles empty device capabilities gracefully', () => {
|
|
612
|
+
mockDeviceCapabilities = {};
|
|
615
613
|
|
|
616
614
|
// Should render without errors
|
|
617
615
|
render(<ReviewResource />);
|
|
@@ -621,7 +619,7 @@ describe('ReviewResource', () => {
|
|
|
621
619
|
|
|
622
620
|
describe('Touch Device Support', () => {
|
|
623
621
|
it('renders larger buttons for touch devices', () => {
|
|
624
|
-
|
|
622
|
+
mockDeviceCapabilities = { hover: false, touch: true };
|
|
625
623
|
|
|
626
624
|
render(<ReviewResource />);
|
|
627
625
|
|
|
@@ -633,7 +631,7 @@ describe('ReviewResource', () => {
|
|
|
633
631
|
});
|
|
634
632
|
|
|
635
633
|
it('renders standard buttons for non-touch devices', () => {
|
|
636
|
-
|
|
634
|
+
mockDeviceCapabilities = { hover: true, touch: false };
|
|
637
635
|
|
|
638
636
|
render(<ReviewResource />);
|
|
639
637
|
|
|
@@ -644,8 +642,8 @@ describe('ReviewResource', () => {
|
|
|
644
642
|
expect(rejectButton).toHaveAttribute('data-size', 'md');
|
|
645
643
|
});
|
|
646
644
|
|
|
647
|
-
it('handles
|
|
648
|
-
|
|
645
|
+
it('handles empty device capabilities gracefully', () => {
|
|
646
|
+
mockDeviceCapabilities = {};
|
|
649
647
|
|
|
650
648
|
render(<ReviewResource />);
|
|
651
649
|
|
|
@@ -679,7 +677,7 @@ describe('ReviewResource', () => {
|
|
|
679
677
|
const expandButton = screen.getByLabelText('Enter fullscreen');
|
|
680
678
|
fireEvent.click(expandButton);
|
|
681
679
|
|
|
682
|
-
expect(mockRequestDisplayMode).toHaveBeenCalledWith(
|
|
680
|
+
expect(mockRequestDisplayMode).toHaveBeenCalledWith('fullscreen');
|
|
683
681
|
});
|
|
684
682
|
});
|
|
685
683
|
});
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
-
useApp,
|
|
3
2
|
useAppState,
|
|
4
3
|
useToolData,
|
|
5
|
-
|
|
4
|
+
useDeviceCapabilities,
|
|
5
|
+
useHostInfo,
|
|
6
6
|
useDisplayMode,
|
|
7
|
+
useRequestDisplayMode,
|
|
7
8
|
useCallServerTool,
|
|
9
|
+
useUpdateModelContext,
|
|
10
|
+
useTimeZone,
|
|
11
|
+
useLocale,
|
|
8
12
|
SafeArea,
|
|
9
13
|
} from 'sunpeak';
|
|
10
14
|
import type { ResourceConfig } from 'sunpeak';
|
|
@@ -494,12 +498,10 @@ function AlertBanner({ alert }: { alert: Alert }) {
|
|
|
494
498
|
// ============================================================================
|
|
495
499
|
|
|
496
500
|
export function ReviewResource() {
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
sections: [],
|
|
502
|
-
});
|
|
501
|
+
const { output, isLoading, isError, isCancelled, cancelReason } = useToolData<
|
|
502
|
+
unknown,
|
|
503
|
+
ReviewData
|
|
504
|
+
>();
|
|
503
505
|
|
|
504
506
|
const [state, setState] = useAppState<ReviewState>({
|
|
505
507
|
decision: null,
|
|
@@ -509,27 +511,37 @@ export function ReviewResource() {
|
|
|
509
511
|
serverError: false,
|
|
510
512
|
});
|
|
511
513
|
|
|
512
|
-
const
|
|
514
|
+
const { touch: hasTouch = false } = useDeviceCapabilities();
|
|
515
|
+
const { hostCapabilities } = useHostInfo();
|
|
513
516
|
const displayMode = useDisplayMode();
|
|
517
|
+
const { requestDisplayMode, availableModes } = useRequestDisplayMode();
|
|
518
|
+
const callServerTool = useCallServerTool();
|
|
519
|
+
const updateModelContext = useUpdateModelContext();
|
|
520
|
+
const timeZone = useTimeZone();
|
|
521
|
+
const locale = useLocale();
|
|
514
522
|
|
|
515
|
-
const
|
|
523
|
+
const canFullscreen = availableModes?.includes('fullscreen') ?? false;
|
|
524
|
+
const hasServerTools = !!hostCapabilities?.serverTools;
|
|
516
525
|
const decision = state.decision ?? null;
|
|
517
526
|
const isFullscreen = displayMode === 'fullscreen';
|
|
518
527
|
const data = output ?? { title: 'Review', sections: [] as Section[] };
|
|
519
528
|
|
|
520
529
|
const handleRequestFullscreen = () => {
|
|
521
|
-
|
|
530
|
+
requestDisplayMode('fullscreen');
|
|
522
531
|
};
|
|
523
532
|
|
|
524
|
-
const callServerTool = useCallServerTool();
|
|
525
|
-
|
|
526
533
|
const handleDecision = async (confirmed: boolean) => {
|
|
527
534
|
const decidedAt = new Date().toISOString();
|
|
528
535
|
const decision = confirmed ? 'accepted' : 'rejected';
|
|
529
536
|
|
|
537
|
+
// Inform the model about the user's decision
|
|
538
|
+
updateModelContext({
|
|
539
|
+
structuredContent: { decision, title: data.title, decidedAt },
|
|
540
|
+
});
|
|
541
|
+
|
|
530
542
|
const tool = data.reviewTool;
|
|
531
|
-
if (!tool) {
|
|
532
|
-
// No server tool — show result immediately
|
|
543
|
+
if (!tool || !hasServerTools) {
|
|
544
|
+
// No server tool or host doesn't support server tools — show result immediately
|
|
533
545
|
setState({ decision, decidedAt, pending: false, serverMessage: null, serverError: false });
|
|
534
546
|
return;
|
|
535
547
|
}
|
|
@@ -568,58 +580,71 @@ export function ReviewResource() {
|
|
|
568
580
|
const sections = data.sections ?? [];
|
|
569
581
|
const alerts = data.alerts ?? [];
|
|
570
582
|
|
|
583
|
+
if (isLoading) {
|
|
584
|
+
return (
|
|
585
|
+
<SafeArea className="flex items-center justify-center gap-2 p-8 text-[var(--color-text-secondary)]">
|
|
586
|
+
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
|
587
|
+
<span>Loading…</span>
|
|
588
|
+
</SafeArea>
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (isError) {
|
|
593
|
+
return (
|
|
594
|
+
<SafeArea className="flex items-center justify-center p-8 text-[var(--color-text-secondary)]">
|
|
595
|
+
Failed to load review data
|
|
596
|
+
</SafeArea>
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (isCancelled) {
|
|
601
|
+
return (
|
|
602
|
+
<SafeArea className="flex items-center justify-center p-8 text-[var(--color-text-secondary)]">
|
|
603
|
+
{cancelReason ?? 'Request was cancelled'}
|
|
604
|
+
</SafeArea>
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
571
608
|
return (
|
|
572
|
-
<SafeArea className="
|
|
609
|
+
<SafeArea className="px-4 py-4 space-y-4">
|
|
573
610
|
{/* Header */}
|
|
574
|
-
<div className="
|
|
575
|
-
<div className="flex
|
|
576
|
-
<
|
|
577
|
-
|
|
578
|
-
{data.description
|
|
579
|
-
<p className="mt-1 text-sm text-[var(--color-text-secondary)]">{data.description}</p>
|
|
580
|
-
)}
|
|
581
|
-
</div>
|
|
582
|
-
{!isFullscreen && (
|
|
583
|
-
<Button
|
|
584
|
-
variant="ghost"
|
|
585
|
-
color="secondary"
|
|
586
|
-
size="sm"
|
|
587
|
-
onClick={handleRequestFullscreen}
|
|
588
|
-
aria-label="Enter fullscreen"
|
|
589
|
-
className="flex-shrink-0"
|
|
590
|
-
>
|
|
591
|
-
<ExpandLg className="h-4 w-4" aria-hidden="true" />
|
|
592
|
-
</Button>
|
|
611
|
+
<div className="flex items-start justify-between gap-2">
|
|
612
|
+
<div className="flex-1 min-w-0">
|
|
613
|
+
<h1 className="text-xl font-semibold text-[var(--color-text-primary)]">{data.title}</h1>
|
|
614
|
+
{data.description && (
|
|
615
|
+
<p className="mt-1 text-sm text-[var(--color-text-secondary)]">{data.description}</p>
|
|
593
616
|
)}
|
|
594
617
|
</div>
|
|
618
|
+
{!isFullscreen && canFullscreen && (
|
|
619
|
+
<Button
|
|
620
|
+
variant="ghost"
|
|
621
|
+
color="secondary"
|
|
622
|
+
size="sm"
|
|
623
|
+
onClick={handleRequestFullscreen}
|
|
624
|
+
aria-label="Enter fullscreen"
|
|
625
|
+
className="flex-shrink-0"
|
|
626
|
+
>
|
|
627
|
+
<ExpandLg className="h-4 w-4" aria-hidden="true" />
|
|
628
|
+
</Button>
|
|
629
|
+
)}
|
|
595
630
|
</div>
|
|
596
631
|
|
|
597
|
-
{/*
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
</div>
|
|
606
|
-
)}
|
|
632
|
+
{/* Alerts */}
|
|
633
|
+
{alerts.length > 0 && (
|
|
634
|
+
<div className="space-y-2">
|
|
635
|
+
{alerts.map((alert, i) => (
|
|
636
|
+
<AlertBanner key={i} alert={alert} />
|
|
637
|
+
))}
|
|
638
|
+
</div>
|
|
639
|
+
)}
|
|
607
640
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
<div className="flex items-center justify-center gap-2 py-8 text-[var(--color-text-secondary)]">
|
|
613
|
-
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
|
614
|
-
<span>Loading...</span>
|
|
615
|
-
</div>
|
|
616
|
-
) : (
|
|
617
|
-
sections.map((section, i) => <SectionRenderer key={i} section={section} />)
|
|
618
|
-
)}
|
|
619
|
-
</div>
|
|
641
|
+
{/* Sections */}
|
|
642
|
+
{sections.map((section, i) => (
|
|
643
|
+
<SectionRenderer key={i} section={section} />
|
|
644
|
+
))}
|
|
620
645
|
|
|
621
|
-
{/*
|
|
622
|
-
<div className="
|
|
646
|
+
{/* Actions */}
|
|
647
|
+
<div className="pt-2">
|
|
623
648
|
{decision === null ? (
|
|
624
649
|
<div className="flex gap-3">
|
|
625
650
|
<Button
|
|
@@ -652,11 +677,9 @@ export function ReviewResource() {
|
|
|
652
677
|
<div className="flex flex-col items-center gap-1">
|
|
653
678
|
{state.serverMessage ? (
|
|
654
679
|
<>
|
|
655
|
-
{/* What the user clicked */}
|
|
656
680
|
<span className="text-xs text-[var(--color-text-secondary)]">
|
|
657
681
|
{decision === 'accepted' ? acceptedMessage : rejectedMessage}
|
|
658
682
|
</span>
|
|
659
|
-
{/* Server's result — color based on structuredContent.status */}
|
|
660
683
|
<div
|
|
661
684
|
className="flex items-center justify-center gap-2"
|
|
662
685
|
style={{
|
|
@@ -670,7 +693,6 @@ export function ReviewResource() {
|
|
|
670
693
|
</div>
|
|
671
694
|
</>
|
|
672
695
|
) : (
|
|
673
|
-
/* No server message (no reviewTool) — icon based on decision */
|
|
674
696
|
<div
|
|
675
697
|
className="flex items-center justify-center gap-2"
|
|
676
698
|
style={{
|
|
@@ -688,7 +710,7 @@ export function ReviewResource() {
|
|
|
688
710
|
)}
|
|
689
711
|
{state.decidedAt && (
|
|
690
712
|
<span className="text-xs text-[var(--color-text-secondary)]">
|
|
691
|
-
{new Date(state.decidedAt).toLocaleString()}
|
|
713
|
+
{new Date(state.decidedAt).toLocaleString(locale, { timeZone })}
|
|
692
714
|
</span>
|
|
693
715
|
)}
|
|
694
716
|
</div>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import handler, { tool, schema } from './review-diff';
|
|
3
|
+
|
|
4
|
+
const extra = {} as Parameters<typeof handler>[1];
|
|
5
|
+
|
|
6
|
+
describe('review-diff tool', () => {
|
|
7
|
+
it('exports correct tool config', () => {
|
|
8
|
+
expect(tool.resource).toBe('review');
|
|
9
|
+
expect(tool.title).toBe('Diff Review');
|
|
10
|
+
expect(tool.annotations?.readOnlyHint).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('has expected schema fields', () => {
|
|
14
|
+
expect(schema.changesetId).toBeDefined();
|
|
15
|
+
expect(schema.title).toBeDefined();
|
|
16
|
+
expect(schema.description).toBeDefined();
|
|
17
|
+
expect(schema.files).toBeDefined();
|
|
18
|
+
expect(schema.runMigrations).toBeDefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('returns structured content with default values', async () => {
|
|
22
|
+
const result = await handler(
|
|
23
|
+
{ changesetId: '', title: '', description: '', files: [], runMigrations: false },
|
|
24
|
+
extra
|
|
25
|
+
);
|
|
26
|
+
expect(result.structuredContent.title).toBe('Code Review');
|
|
27
|
+
expect(result.structuredContent.description).toBe('Review the proposed changes below');
|
|
28
|
+
expect(result.structuredContent.reviewTool).toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('creates changes from provided files', async () => {
|
|
32
|
+
const result = await handler(
|
|
33
|
+
{
|
|
34
|
+
changesetId: 'cs-42',
|
|
35
|
+
title: 'Fix Bug',
|
|
36
|
+
description: 'Fixes the login bug',
|
|
37
|
+
files: ['src/auth.ts', 'src/login.tsx'],
|
|
38
|
+
runMigrations: false,
|
|
39
|
+
},
|
|
40
|
+
extra
|
|
41
|
+
);
|
|
42
|
+
expect(result.structuredContent.title).toBe('Fix Bug');
|
|
43
|
+
const changes = result.structuredContent.sections[0].content;
|
|
44
|
+
expect(changes).toHaveLength(2);
|
|
45
|
+
expect(changes[0].path).toBe('src/auth.ts');
|
|
46
|
+
expect(changes[1].path).toBe('src/login.tsx');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('adds migration action when runMigrations is true', async () => {
|
|
50
|
+
const result = await handler(
|
|
51
|
+
{
|
|
52
|
+
changesetId: 'cs-1',
|
|
53
|
+
title: '',
|
|
54
|
+
description: '',
|
|
55
|
+
files: ['src/app.ts'],
|
|
56
|
+
runMigrations: true,
|
|
57
|
+
},
|
|
58
|
+
extra
|
|
59
|
+
);
|
|
60
|
+
const changes = result.structuredContent.sections[0].content;
|
|
61
|
+
expect(changes).toHaveLength(2);
|
|
62
|
+
expect(changes[1].type).toBe('action');
|
|
63
|
+
expect(changes[1].description).toContain('migrations');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('passes changesetId to reviewTool arguments', async () => {
|
|
67
|
+
const result = await handler(
|
|
68
|
+
{ changesetId: 'cs-99', title: '', description: '', files: [], runMigrations: false },
|
|
69
|
+
extra
|
|
70
|
+
);
|
|
71
|
+
expect(result.structuredContent.reviewTool.arguments.changesetId).toBe('cs-99');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -21,6 +21,33 @@ export const schema = {
|
|
|
21
21
|
|
|
22
22
|
type Args = z.infer<z.ZodObject<typeof schema>>;
|
|
23
23
|
|
|
24
|
-
export default async function (
|
|
25
|
-
|
|
24
|
+
export default async function (args: Args, _extra: ToolHandlerExtra) {
|
|
25
|
+
const files = args.files ?? ['src/app.tsx', 'src/utils/helpers.ts'];
|
|
26
|
+
const changes: Array<{ id: string; type: string; path?: string; description: string }> =
|
|
27
|
+
files.map((file, i) => ({
|
|
28
|
+
id: String(i + 1),
|
|
29
|
+
type: 'modify',
|
|
30
|
+
path: file,
|
|
31
|
+
description: `Changes to ${file.split('/').pop()}`,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
if (args.runMigrations) {
|
|
35
|
+
changes.push({
|
|
36
|
+
id: 'migration',
|
|
37
|
+
type: 'action',
|
|
38
|
+
description: 'Run database migrations',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
structuredContent: {
|
|
44
|
+
title: args.title || 'Code Review',
|
|
45
|
+
description: args.description || 'Review the proposed changes below',
|
|
46
|
+
sections: [{ type: 'changes', title: 'File Changes', content: changes }],
|
|
47
|
+
reviewTool: {
|
|
48
|
+
name: 'review',
|
|
49
|
+
arguments: { action: 'apply_changes', changesetId: args.changesetId || 'cs-1' },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
26
53
|
}
|