@comergehq/studio 0.1.11 → 0.1.13
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/dist/index.d.mts +22 -2
- package/dist/index.d.ts +22 -2
- package/dist/index.js +352 -290
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +361 -296
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
- package/src/components/{floating-draggable-button/FloatingDraggableButton.tsx → bubble/Bubble.tsx} +3 -3
- package/src/components/{floating-draggable-button → bubble}/constants.ts +2 -2
- package/src/components/bubble/index.ts +4 -0
- package/src/components/{floating-draggable-button → bubble}/types.ts +3 -3
- package/src/components/index.ts +2 -2
- package/src/core/services/http/baseUrl.ts +1 -1
- package/src/core/services/http/public.ts +7 -7
- package/src/studio/ComergeStudio.tsx +23 -3
- package/src/studio/bootstrap/StudioBootstrap.tsx +2 -2
- package/src/studio/bootstrap/useStudioBootstrap.ts +7 -7
- package/src/studio/hooks/useBundleManager.ts +55 -2
- package/src/studio/ui/RuntimeRenderer.tsx +24 -1
- package/src/studio/ui/StudioOverlay.tsx +41 -12
- package/src/components/floating-draggable-button/index.ts +0 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@comergehq/studio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Comerge studio",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"axios": "^1.13.1",
|
|
45
|
+
"@comergehq/studio-control": "^0.1.5",
|
|
45
46
|
"react-native-markdown-display": "^7.0.2",
|
|
46
47
|
"react-native-logs": "^5.5.0"
|
|
47
48
|
},
|
|
@@ -56,6 +57,7 @@
|
|
|
56
57
|
"@gorhom/bottom-sheet": "*",
|
|
57
58
|
"@comergehq/runtime": "^0.1.1",
|
|
58
59
|
"expo": "*",
|
|
60
|
+
"expo-asset": "*",
|
|
59
61
|
"expo-file-system": "*",
|
|
60
62
|
"expo-haptics": "*",
|
|
61
63
|
"expo-linear-gradient": "*",
|
package/src/components/{floating-draggable-button/FloatingDraggableButton.tsx → bubble/Bubble.tsx}
RENAMED
|
@@ -23,7 +23,7 @@ import Animated, {
|
|
|
23
23
|
import { isLiquidGlassSupported } from '@callstack/liquid-glass';
|
|
24
24
|
|
|
25
25
|
import { DEFAULT_EDGE_PADDING, DEFAULT_OFFSET, DEFAULT_SIZE, ENTER_ROTATION_FROM_DEG, ENTER_SCALE_FROM, PULSE_DURATION_MS } from './constants';
|
|
26
|
-
import type {
|
|
26
|
+
import type { BubbleProps } from './types';
|
|
27
27
|
import { useTheme } from '../../theme';
|
|
28
28
|
import { ResettableLiquidGlassView } from '../utils/ResettableLiquidGlassView';
|
|
29
29
|
|
|
@@ -53,7 +53,7 @@ function getFinalTranslateY(height: number, size: number, bottomOffset: number)
|
|
|
53
53
|
return height - size - bottomOffset;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
export function
|
|
56
|
+
export function Bubble({
|
|
57
57
|
onPress,
|
|
58
58
|
size = DEFAULT_SIZE,
|
|
59
59
|
disabled = false,
|
|
@@ -69,7 +69,7 @@ export function FloatingDraggableButton({
|
|
|
69
69
|
testID,
|
|
70
70
|
edgePadding = DEFAULT_EDGE_PADDING,
|
|
71
71
|
backgroundColor,
|
|
72
|
-
}:
|
|
72
|
+
}: BubbleProps) {
|
|
73
73
|
const theme = useTheme();
|
|
74
74
|
const { width, height } = useWindowDimensions();
|
|
75
75
|
const isDanger = variant === 'danger';
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { BubbleOffset } from './types';
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_SIZE = 48;
|
|
4
4
|
export const DEFAULT_EDGE_PADDING = 10;
|
|
5
5
|
|
|
6
|
-
export const DEFAULT_OFFSET: Required<
|
|
6
|
+
export const DEFAULT_OFFSET: Required<BubbleOffset> = {
|
|
7
7
|
left: 20,
|
|
8
8
|
bottom: 60,
|
|
9
9
|
};
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type * as React from 'react';
|
|
2
2
|
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
3
|
|
|
4
|
-
export type
|
|
4
|
+
export type BubbleOffset = {
|
|
5
5
|
/** Distance from the left edge (in px). */
|
|
6
6
|
left?: number;
|
|
7
7
|
/** Distance from the bottom edge (in px). */
|
|
8
8
|
bottom?: number;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
export type
|
|
11
|
+
export type BubbleProps = {
|
|
12
12
|
/**
|
|
13
13
|
* Whether the button should be shown. When toggled, the button animates in/out.
|
|
14
14
|
* The component stays mounted to preserve its last drag position.
|
|
@@ -30,7 +30,7 @@ export type FloatingDraggableButtonProps = {
|
|
|
30
30
|
* Initial placement when it animates in.
|
|
31
31
|
* `left` is measured from the left edge; `bottom` from the bottom edge.
|
|
32
32
|
*/
|
|
33
|
-
offset?:
|
|
33
|
+
offset?: BubbleOffset;
|
|
34
34
|
|
|
35
35
|
/** Accessible label for screen readers (kept as `ariaLabel` for compatibility). */
|
|
36
36
|
ariaLabel?: string;
|
package/src/components/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type {
|
|
1
|
+
export { Bubble } from './bubble';
|
|
2
|
+
export type { BubbleProps, BubbleOffset } from './bubble';
|
|
3
3
|
|
|
4
4
|
export * from './icons/StudioIcons';
|
|
5
5
|
|
|
@@ -3,7 +3,7 @@ import axios from "axios";
|
|
|
3
3
|
import { BASE_URL } from "./baseUrl";
|
|
4
4
|
|
|
5
5
|
const CLIENT_KEY_HEADER = "x-comerge-api-key";
|
|
6
|
-
let
|
|
6
|
+
let clientKey: string | null = null;
|
|
7
7
|
|
|
8
8
|
export const publicApi = axios.create({
|
|
9
9
|
baseURL: BASE_URL,
|
|
@@ -14,19 +14,19 @@ export const publicApi = axios.create({
|
|
|
14
14
|
},
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
export function
|
|
18
|
-
const trimmed =
|
|
17
|
+
export function setClientKey(clientKeyInput: string) {
|
|
18
|
+
const trimmed = clientKeyInput?.trim?.() ?? "";
|
|
19
19
|
if (!trimmed) {
|
|
20
|
-
throw new Error("comerge-studio:
|
|
20
|
+
throw new Error("comerge-studio: clientKey is required");
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
clientKey = trimmed;
|
|
23
23
|
publicApi.defaults.headers.common[CLIENT_KEY_HEADER] = trimmed;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
publicApi.interceptors.request.use((config) => {
|
|
27
|
-
if (!
|
|
27
|
+
if (!clientKey) return config;
|
|
28
28
|
config.headers = config.headers ?? {};
|
|
29
|
-
(config.headers as any)[CLIENT_KEY_HEADER] =
|
|
29
|
+
(config.headers as any)[CLIENT_KEY_HEADER] = clientKey;
|
|
30
30
|
return config;
|
|
31
31
|
});
|
|
32
32
|
|
|
@@ -7,6 +7,7 @@ import { StudioBootstrap } from './bootstrap/StudioBootstrap';
|
|
|
7
7
|
import { useApp } from './hooks/useApp';
|
|
8
8
|
import { useThreadMessages } from './hooks/useThreadMessages';
|
|
9
9
|
import { useBundleManager } from './hooks/useBundleManager';
|
|
10
|
+
import type { EmbeddedBaseBundles } from './hooks/useBundleManager';
|
|
10
11
|
import { useMergeRequests } from './hooks/useMergeRequests';
|
|
11
12
|
import { useAttachmentUpload } from './hooks/useAttachmentUpload';
|
|
12
13
|
import { useStudioActions } from './hooks/useStudioActions';
|
|
@@ -17,18 +18,24 @@ import { LiquidGlassResetProvider } from '../components/utils/liquidGlassReset';
|
|
|
17
18
|
|
|
18
19
|
export type ComergeStudioProps = {
|
|
19
20
|
appId: string;
|
|
20
|
-
|
|
21
|
+
clientKey: string;
|
|
21
22
|
appKey?: string;
|
|
22
23
|
onNavigateHome?: () => void;
|
|
23
24
|
style?: ViewStyle;
|
|
25
|
+
showBubble?: boolean;
|
|
26
|
+
studioControlOptions?: import('@comergehq/studio-control').StudioControlOptions;
|
|
27
|
+
embeddedBaseBundles?: EmbeddedBaseBundles;
|
|
24
28
|
};
|
|
25
29
|
|
|
26
30
|
export function ComergeStudio({
|
|
27
31
|
appId,
|
|
28
|
-
|
|
32
|
+
clientKey,
|
|
29
33
|
appKey = 'MicroMain',
|
|
30
34
|
onNavigateHome,
|
|
31
35
|
style,
|
|
36
|
+
showBubble = true,
|
|
37
|
+
studioControlOptions,
|
|
38
|
+
embeddedBaseBundles,
|
|
32
39
|
}: ComergeStudioProps) {
|
|
33
40
|
const [activeAppId, setActiveAppId] = React.useState(appId);
|
|
34
41
|
const [runtimeAppId, setRuntimeAppId] = React.useState(appId);
|
|
@@ -44,7 +51,7 @@ export function ComergeStudio({
|
|
|
44
51
|
const captureTargetRef = React.useRef<View | null>(null);
|
|
45
52
|
|
|
46
53
|
return (
|
|
47
|
-
<StudioBootstrap
|
|
54
|
+
<StudioBootstrap clientKey={clientKey} fallback={<View style={{ flex: 1 }} />}>
|
|
48
55
|
{({ userId }) => (
|
|
49
56
|
<BottomSheetModalProvider>
|
|
50
57
|
<LiquidGlassResetProvider resetTriggers={[appId, activeAppId, runtimeAppId]}>
|
|
@@ -61,6 +68,9 @@ export function ComergeStudio({
|
|
|
61
68
|
onNavigateHome={onNavigateHome}
|
|
62
69
|
captureTargetRef={captureTargetRef}
|
|
63
70
|
style={style}
|
|
71
|
+
showBubble={showBubble}
|
|
72
|
+
studioControlOptions={studioControlOptions}
|
|
73
|
+
embeddedBaseBundles={embeddedBaseBundles}
|
|
64
74
|
/>
|
|
65
75
|
</LiquidGlassResetProvider>
|
|
66
76
|
</BottomSheetModalProvider>
|
|
@@ -82,6 +92,9 @@ type InnerProps = {
|
|
|
82
92
|
onNavigateHome?: () => void;
|
|
83
93
|
captureTargetRef: React.RefObject<View | null>;
|
|
84
94
|
style?: ViewStyle;
|
|
95
|
+
showBubble: boolean;
|
|
96
|
+
studioControlOptions?: import('@comergehq/studio-control').StudioControlOptions;
|
|
97
|
+
embeddedBaseBundles?: EmbeddedBaseBundles;
|
|
85
98
|
};
|
|
86
99
|
|
|
87
100
|
function ComergeStudioInner({
|
|
@@ -97,6 +110,9 @@ function ComergeStudioInner({
|
|
|
97
110
|
onNavigateHome,
|
|
98
111
|
captureTargetRef,
|
|
99
112
|
style,
|
|
113
|
+
showBubble,
|
|
114
|
+
studioControlOptions,
|
|
115
|
+
embeddedBaseBundles,
|
|
100
116
|
}: InnerProps) {
|
|
101
117
|
const { app, loading: appLoading } = useApp(activeAppId);
|
|
102
118
|
const { app: runtimeAppFromHook } = useApp(runtimeAppId, { enabled: runtimeAppId !== activeAppId });
|
|
@@ -126,6 +142,7 @@ function ComergeStudioInner({
|
|
|
126
142
|
base: { appId: runtimeAppId, commitId: runtimeApp?.headCommitId ?? undefined },
|
|
127
143
|
platform,
|
|
128
144
|
canRequestLatest: runtimeApp?.status === 'ready',
|
|
145
|
+
embeddedBaseBundles,
|
|
129
146
|
});
|
|
130
147
|
|
|
131
148
|
const sawEditingOnActiveAppRef = React.useRef(false);
|
|
@@ -209,6 +226,7 @@ function ComergeStudioInner({
|
|
|
209
226
|
bundlePath={bundle.bundlePath}
|
|
210
227
|
forcePreparing={showPostEditPreparing}
|
|
211
228
|
renderToken={bundle.renderToken}
|
|
229
|
+
allowInitialPreparing={!embeddedBaseBundles}
|
|
212
230
|
/>
|
|
213
231
|
|
|
214
232
|
<StudioOverlay
|
|
@@ -266,6 +284,8 @@ function ComergeStudioInner({
|
|
|
266
284
|
chatShowTypingIndicator={chatShowTypingIndicator}
|
|
267
285
|
onSendChat={(text, attachments) => actions.sendEdit({ prompt: text, attachments })}
|
|
268
286
|
onNavigateHome={onNavigateHome}
|
|
287
|
+
showBubble={showBubble}
|
|
288
|
+
studioControlOptions={studioControlOptions}
|
|
269
289
|
/>
|
|
270
290
|
</View>
|
|
271
291
|
</View>
|
|
@@ -16,8 +16,8 @@ export type StudioBootstrapProps = UseStudioBootstrapOptions & {
|
|
|
16
16
|
renderError?: (error: Error) => React.ReactNode;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
export function StudioBootstrap({ children, fallback, renderError,
|
|
20
|
-
const { ready, error, userId } = useStudioBootstrap({
|
|
19
|
+
export function StudioBootstrap({ children, fallback, renderError, clientKey }: StudioBootstrapProps) {
|
|
20
|
+
const { ready, error, userId } = useStudioBootstrap({ clientKey });
|
|
21
21
|
|
|
22
22
|
if (error) {
|
|
23
23
|
return (
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { setClientKey } from '../../core/services/http/public';
|
|
4
4
|
import { ensureAuthenticatedSession, ensureAnonymousSession } from '../../core/services/supabase/auth';
|
|
5
5
|
import { isSupabaseClientInjected, setSupabaseConfig } from '../../core/services/supabase/client';
|
|
6
|
-
|
|
6
|
+
const SUPABASE_URL = 'https://xtfxwbckjpfmqubnsusu.supabase.co';
|
|
7
|
+
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh0Znh3YmNranBmbXF1Ym5zdXN1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjA2MDEyMzAsImV4cCI6MjA3NjE3NzIzMH0.dzWGAWrK4CvrmHVHzf8w7JlUZohdap0ZPnLZnABMV8s';
|
|
7
8
|
|
|
8
9
|
export type UseStudioBootstrapOptions = {
|
|
9
|
-
|
|
10
|
+
clientKey: string;
|
|
10
11
|
};
|
|
11
12
|
|
|
12
13
|
export type StudioBootstrapState = {
|
|
@@ -27,11 +28,10 @@ export function useStudioBootstrap(options: UseStudioBootstrapOptions): StudioBo
|
|
|
27
28
|
|
|
28
29
|
(async () => {
|
|
29
30
|
try {
|
|
30
|
-
|
|
31
|
+
setClientKey(options.clientKey);
|
|
31
32
|
const requireAuth = isSupabaseClientInjected();
|
|
32
33
|
if (!requireAuth) {
|
|
33
|
-
|
|
34
|
-
setSupabaseConfig(cfg);
|
|
34
|
+
setSupabaseConfig({ url: SUPABASE_URL, anonKey: SUPABASE_ANON_KEY });
|
|
35
35
|
}
|
|
36
36
|
const { user } = requireAuth ? await ensureAuthenticatedSession() : await ensureAnonymousSession();
|
|
37
37
|
|
|
@@ -47,7 +47,7 @@ export function useStudioBootstrap(options: UseStudioBootstrapOptions): StudioBo
|
|
|
47
47
|
return () => {
|
|
48
48
|
cancelled = true;
|
|
49
49
|
};
|
|
50
|
-
}, [options.
|
|
50
|
+
}, [options.clientKey]);
|
|
51
51
|
|
|
52
52
|
return state;
|
|
53
53
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import * as FileSystem from 'expo-file-system/legacy';
|
|
3
|
+
import { Asset } from 'expo-asset';
|
|
3
4
|
|
|
4
5
|
import type { Platform as BundlePlatform, Bundle } from '../../data/apps/bundles/types';
|
|
5
6
|
import { bundlesRepository } from '../../data/apps/bundles/repository';
|
|
@@ -60,6 +61,7 @@ export type UseBundleManagerParams = {
|
|
|
60
61
|
* Test bundles (merge request previews) are NOT gated by this.
|
|
61
62
|
*/
|
|
62
63
|
canRequestLatest?: boolean;
|
|
64
|
+
embeddedBaseBundles?: EmbeddedBaseBundles;
|
|
63
65
|
};
|
|
64
66
|
|
|
65
67
|
export type BundleLoadState = {
|
|
@@ -85,6 +87,16 @@ export type UseBundleManagerResult = BundleLoadState & {
|
|
|
85
87
|
restoreBase: () => Promise<void>;
|
|
86
88
|
};
|
|
87
89
|
|
|
90
|
+
export type EmbeddedBaseBundle = {
|
|
91
|
+
module: number;
|
|
92
|
+
meta?: BaseBundleMeta | null;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type EmbeddedBaseBundles = {
|
|
96
|
+
ios?: EmbeddedBaseBundle;
|
|
97
|
+
android?: EmbeddedBaseBundle;
|
|
98
|
+
};
|
|
99
|
+
|
|
88
100
|
function safeName(s: string) {
|
|
89
101
|
return s.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
90
102
|
}
|
|
@@ -183,6 +195,31 @@ async function deleteFileIfExists(fileUri: string) {
|
|
|
183
195
|
}
|
|
184
196
|
}
|
|
185
197
|
|
|
198
|
+
async function hydrateBaseFromEmbeddedAsset(
|
|
199
|
+
appId: string,
|
|
200
|
+
platform: BundlePlatform,
|
|
201
|
+
embedded: EmbeddedBaseBundle | undefined
|
|
202
|
+
): Promise<{ bundlePath: string; meta?: BaseBundleMeta | null } | null> {
|
|
203
|
+
if (!embedded?.module) return null;
|
|
204
|
+
const key = baseBundleKey(appId, platform);
|
|
205
|
+
const targetUri = toBundleFileUri(key);
|
|
206
|
+
const existing = await getExistingNonEmptyFileUri(targetUri);
|
|
207
|
+
if (existing) return { bundlePath: existing, meta: embedded.meta ?? null };
|
|
208
|
+
|
|
209
|
+
const asset = Asset.fromModule(embedded.module);
|
|
210
|
+
await asset.downloadAsync();
|
|
211
|
+
const sourceUri = asset.localUri ?? asset.uri;
|
|
212
|
+
if (!sourceUri) return null;
|
|
213
|
+
const info = await FileSystem.getInfoAsync(sourceUri);
|
|
214
|
+
if (!info.exists) return null;
|
|
215
|
+
|
|
216
|
+
await deleteFileIfExists(targetUri);
|
|
217
|
+
await FileSystem.copyAsync({ from: sourceUri, to: targetUri });
|
|
218
|
+
const finalUri = await getExistingNonEmptyFileUri(targetUri);
|
|
219
|
+
if (!finalUri) return null;
|
|
220
|
+
return { bundlePath: finalUri, meta: embedded.meta ?? null };
|
|
221
|
+
}
|
|
222
|
+
|
|
186
223
|
async function safeReplaceFileFromUrl(url: string, targetUri: string, tmpKey: string): Promise<string> {
|
|
187
224
|
const tmpUri = toBundleFileUri(`tmp:${tmpKey}:${Date.now()}`);
|
|
188
225
|
try {
|
|
@@ -275,6 +312,7 @@ export function useBundleManager({
|
|
|
275
312
|
base,
|
|
276
313
|
platform,
|
|
277
314
|
canRequestLatest = true,
|
|
315
|
+
embeddedBaseBundles,
|
|
278
316
|
}: UseBundleManagerParams): UseBundleManagerResult {
|
|
279
317
|
const [bundlePath, setBundlePath] = React.useState<string | null>(null);
|
|
280
318
|
const [renderToken, setRenderToken] = React.useState(0);
|
|
@@ -287,6 +325,9 @@ export function useBundleManager({
|
|
|
287
325
|
const baseRef = React.useRef(base);
|
|
288
326
|
baseRef.current = base;
|
|
289
327
|
|
|
328
|
+
const embeddedBaseBundlesRef = React.useRef<EmbeddedBaseBundles | undefined>(embeddedBaseBundles);
|
|
329
|
+
embeddedBaseBundlesRef.current = embeddedBaseBundles;
|
|
330
|
+
|
|
290
331
|
// Monotonic operation ids to prevent stale async loads from overwriting newer ones.
|
|
291
332
|
const baseOpIdRef = React.useRef(0);
|
|
292
333
|
const testOpIdRef = React.useRef(0);
|
|
@@ -320,11 +361,23 @@ export function useBundleManager({
|
|
|
320
361
|
await ensureDir(dir);
|
|
321
362
|
const key = baseBundleKey(appId, platform);
|
|
322
363
|
const uri = toBundleFileUri(key);
|
|
323
|
-
|
|
364
|
+
let existing = await getExistingNonEmptyFileUri(uri);
|
|
365
|
+
let embeddedMeta: BaseBundleMeta | null = null;
|
|
366
|
+
if (!existing) {
|
|
367
|
+
const embedded = embeddedBaseBundlesRef.current?.[platform];
|
|
368
|
+
const hydrated = await hydrateBaseFromEmbeddedAsset(appId, platform, embedded);
|
|
369
|
+
if (hydrated?.bundlePath) {
|
|
370
|
+
existing = hydrated.bundlePath;
|
|
371
|
+
embeddedMeta = hydrated.meta ?? null;
|
|
372
|
+
if (embeddedMeta) {
|
|
373
|
+
await writeJsonFile(toBundleMetaFileUri(key), embeddedMeta);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
324
377
|
if (existing) {
|
|
325
378
|
lastBaseBundlePathRef.current = existing;
|
|
326
379
|
setBundlePath(existing);
|
|
327
|
-
const meta = await readJsonFile<BaseBundleMeta>(toBundleMetaFileUri(key));
|
|
380
|
+
const meta = embeddedMeta ?? (await readJsonFile<BaseBundleMeta>(toBundleMetaFileUri(key)));
|
|
328
381
|
if (meta?.fingerprint) {
|
|
329
382
|
lastBaseFingerprintRef.current = meta.fingerprint;
|
|
330
383
|
}
|
|
@@ -13,6 +13,10 @@ export type RuntimeRendererProps = {
|
|
|
13
13
|
* Used to avoid briefly rendering an outdated bundle during post-edit base refresh.
|
|
14
14
|
*/
|
|
15
15
|
forcePreparing?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* When false, suppress "Preparing app…" on the very first load.
|
|
18
|
+
*/
|
|
19
|
+
allowInitialPreparing?: boolean;
|
|
16
20
|
/**
|
|
17
21
|
* Used to force a runtime remount even when bundlePath stays constant
|
|
18
22
|
* (e.g. base bundle replaced in-place).
|
|
@@ -21,8 +25,27 @@ export type RuntimeRendererProps = {
|
|
|
21
25
|
style?: ViewStyle;
|
|
22
26
|
};
|
|
23
27
|
|
|
24
|
-
export function RuntimeRenderer({
|
|
28
|
+
export function RuntimeRenderer({
|
|
29
|
+
appKey,
|
|
30
|
+
bundlePath,
|
|
31
|
+
forcePreparing,
|
|
32
|
+
renderToken,
|
|
33
|
+
style,
|
|
34
|
+
allowInitialPreparing = true,
|
|
35
|
+
}: RuntimeRendererProps) {
|
|
36
|
+
const [hasRenderedOnce, setHasRenderedOnce] = React.useState(false);
|
|
37
|
+
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
if (bundlePath) {
|
|
40
|
+
setHasRenderedOnce(true);
|
|
41
|
+
}
|
|
42
|
+
}, [bundlePath]);
|
|
43
|
+
|
|
25
44
|
if (!bundlePath || forcePreparing) {
|
|
45
|
+
if (!hasRenderedOnce && !forcePreparing && !allowInitialPreparing) {
|
|
46
|
+
return <View style={[{ flex: 1 }, style]} />;
|
|
47
|
+
}
|
|
48
|
+
|
|
26
49
|
return (
|
|
27
50
|
<View style={[{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 }, style]}>
|
|
28
51
|
<Text variant="bodyMuted">Preparing app…</Text>
|
|
@@ -5,7 +5,7 @@ import type { App } from '../../data/apps/types';
|
|
|
5
5
|
import type { MergeRequest } from '../../data/merge-requests/types';
|
|
6
6
|
import { StudioBottomSheet } from '../../components/studio-sheet/StudioBottomSheet';
|
|
7
7
|
import { StudioSheetPager } from '../../components/studio-sheet/StudioSheetPager';
|
|
8
|
-
import {
|
|
8
|
+
import { Bubble } from '../../components/bubble/Bubble';
|
|
9
9
|
import { EdgeGlowFrame } from '../../components/overlays/EdgeGlowFrame';
|
|
10
10
|
import { DrawModeOverlay } from '../../components/draw/DrawModeOverlay';
|
|
11
11
|
import { AppCommentsSheet } from '../../components/comments/AppCommentsSheet';
|
|
@@ -15,6 +15,11 @@ import { ConfirmMergeFlow } from './ConfirmMergeFlow';
|
|
|
15
15
|
import type { MergeRequestSummary } from '../../components/models/types';
|
|
16
16
|
import { useTheme } from '../../theme';
|
|
17
17
|
import { useOptimisticChatMessages } from '../hooks/useOptimisticChatMessages';
|
|
18
|
+
import {
|
|
19
|
+
publishComergeStudioUIState,
|
|
20
|
+
startStudioControlPolling,
|
|
21
|
+
type StudioControlOptions,
|
|
22
|
+
} from '@comergehq/studio-control';
|
|
18
23
|
|
|
19
24
|
import { MergeIcon } from '../../components/icons/MergeIcon';
|
|
20
25
|
|
|
@@ -56,6 +61,8 @@ export type StudioOverlayProps = {
|
|
|
56
61
|
|
|
57
62
|
// Navigation callbacks
|
|
58
63
|
onNavigateHome?: () => void;
|
|
64
|
+
showBubble: boolean;
|
|
65
|
+
studioControlOptions?: StudioControlOptions;
|
|
59
66
|
};
|
|
60
67
|
|
|
61
68
|
type SheetPage = 'preview' | 'chat';
|
|
@@ -87,11 +94,14 @@ export function StudioOverlay({
|
|
|
87
94
|
chatShowTypingIndicator,
|
|
88
95
|
onSendChat,
|
|
89
96
|
onNavigateHome,
|
|
97
|
+
showBubble,
|
|
98
|
+
studioControlOptions,
|
|
90
99
|
}: StudioOverlayProps) {
|
|
91
100
|
const theme = useTheme();
|
|
92
101
|
const { width } = useWindowDimensions();
|
|
93
102
|
|
|
94
103
|
const [sheetOpen, setSheetOpen] = React.useState(false);
|
|
104
|
+
const sheetOpenRef = React.useRef(sheetOpen);
|
|
95
105
|
const [activePage, setActivePage] = React.useState<SheetPage>('preview');
|
|
96
106
|
|
|
97
107
|
const [drawing, setDrawing] = React.useState(false);
|
|
@@ -186,6 +196,23 @@ export function StudioOverlay({
|
|
|
186
196
|
[closeSheet, onTestMr]
|
|
187
197
|
);
|
|
188
198
|
|
|
199
|
+
React.useEffect(() => {
|
|
200
|
+
sheetOpenRef.current = sheetOpen;
|
|
201
|
+
}, [sheetOpen]);
|
|
202
|
+
|
|
203
|
+
React.useEffect(() => {
|
|
204
|
+
const poller = startStudioControlPolling((action) => {
|
|
205
|
+
if (action === 'show' && !sheetOpenRef.current) openSheet();
|
|
206
|
+
if (action === 'hide' && sheetOpenRef.current) closeSheet();
|
|
207
|
+
if (action === 'toggle') toggleSheet();
|
|
208
|
+
}, studioControlOptions);
|
|
209
|
+
return () => poller.stop();
|
|
210
|
+
}, [closeSheet, openSheet, studioControlOptions, toggleSheet]);
|
|
211
|
+
|
|
212
|
+
React.useEffect(() => {
|
|
213
|
+
void publishComergeStudioUIState(sheetOpen, studioControlOptions);
|
|
214
|
+
}, [sheetOpen, studioControlOptions]);
|
|
215
|
+
|
|
189
216
|
return (
|
|
190
217
|
<>
|
|
191
218
|
{/* Testing glow around runtime */}
|
|
@@ -243,17 +270,19 @@ export function StudioOverlay({
|
|
|
243
270
|
/>
|
|
244
271
|
</StudioBottomSheet>
|
|
245
272
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
273
|
+
{showBubble && (
|
|
274
|
+
<Bubble
|
|
275
|
+
visible={!sheetOpen && !drawing}
|
|
276
|
+
ariaLabel={sheetOpen ? 'Hide studio' : 'Show studio'}
|
|
277
|
+
badgeCount={incomingMergeRequests.length}
|
|
278
|
+
onPress={toggleSheet}
|
|
279
|
+
isLoading={app?.status === 'editing'}
|
|
280
|
+
>
|
|
281
|
+
<View style={{ width: 28, height: 28, alignItems: 'center', justifyContent: 'center' }}>
|
|
282
|
+
<MergeIcon width={24} height={24} color={theme.colors.floatingContent} />
|
|
283
|
+
</View>
|
|
284
|
+
</Bubble>
|
|
285
|
+
)}
|
|
257
286
|
|
|
258
287
|
<DrawModeOverlay
|
|
259
288
|
visible={drawing}
|