@comergehq/studio 0.1.1 → 0.1.3

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 (172) hide show
  1. package/dist/index.js +255 -245
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +213 -203
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +9 -6
  6. package/src/components/chat/ChatComposer.tsx +277 -0
  7. package/src/components/chat/ChatHeader.tsx +31 -0
  8. package/src/components/chat/ChatMessageBubble.tsx +69 -0
  9. package/src/components/chat/ChatMessageList.tsx +137 -0
  10. package/src/components/chat/ChatPage.tsx +69 -0
  11. package/src/components/chat/ForkNoticeBanner.tsx +66 -0
  12. package/src/components/chat/MultilineTextInput.tsx +46 -0
  13. package/src/components/chat/ScrollToBottomButton.tsx +78 -0
  14. package/src/components/chat/TypingIndicator.tsx +54 -0
  15. package/src/components/chat/index.ts +28 -0
  16. package/src/components/comments/AppCommentsSheet.tsx +213 -0
  17. package/src/components/comments/CommentRow.tsx +63 -0
  18. package/src/components/comments/formatTimeAgo.ts +3 -0
  19. package/src/components/comments/index.ts +3 -0
  20. package/src/components/comments/useAppComments.ts +74 -0
  21. package/src/components/comments/useAppDetails.ts +35 -0
  22. package/src/components/comments/useIosKeyboardSnapFix.ts +24 -0
  23. package/src/components/dialogs/ConfirmMergeRequestDialog.tsx +156 -0
  24. package/src/components/dialogs/index.ts +4 -0
  25. package/src/components/draw/DrawColorPicker.tsx +77 -0
  26. package/src/components/draw/DrawModeOverlay.tsx +144 -0
  27. package/src/components/draw/DrawSurface.tsx +127 -0
  28. package/src/components/draw/DrawToolbar.tsx +253 -0
  29. package/src/components/draw/index.ts +15 -0
  30. package/src/components/draw/optionalHaptics.ts +15 -0
  31. package/src/components/draw/strokes.ts +21 -0
  32. package/src/components/draw/types.ts +9 -0
  33. package/src/components/floating-draggable-button/FloatingDraggableButton.tsx +323 -0
  34. package/src/components/floating-draggable-button/constants.ts +17 -0
  35. package/src/components/floating-draggable-button/index.ts +4 -0
  36. package/src/components/floating-draggable-button/types.ts +63 -0
  37. package/src/components/icons/MergeIcon.tsx +14 -0
  38. package/src/components/icons/StudioIcons.tsx +66 -0
  39. package/src/components/index.ts +17 -0
  40. package/src/components/merge-requests/MergeRequestStatusCard.tsx +179 -0
  41. package/src/components/merge-requests/ReviewMergeRequestActionButton.tsx +62 -0
  42. package/src/components/merge-requests/ReviewMergeRequestCard.tsx +192 -0
  43. package/src/components/merge-requests/ReviewMergeRequestCarousel.tsx +132 -0
  44. package/src/components/merge-requests/index.ts +7 -0
  45. package/src/components/merge-requests/mergeRequestStatusDisplay.ts +23 -0
  46. package/src/components/merge-requests/toIsoString.ts +9 -0
  47. package/src/components/merge-requests/useControlledExpansion.ts +16 -0
  48. package/src/components/models/index.ts +9 -0
  49. package/src/components/models/types.ts +43 -0
  50. package/src/components/overlays/EdgeGlowFrame.tsx +105 -0
  51. package/src/components/overlays/index.ts +4 -0
  52. package/src/components/preview/PreviewHeroCard.tsx +58 -0
  53. package/src/components/preview/PreviewImage.tsx +22 -0
  54. package/src/components/preview/PreviewMetaRow.tsx +70 -0
  55. package/src/components/preview/PreviewPage.tsx +36 -0
  56. package/src/components/preview/PreviewPlaceholder.tsx +72 -0
  57. package/src/components/preview/PreviewStatusBadge.tsx +63 -0
  58. package/src/components/preview/StatsBar.tsx +109 -0
  59. package/src/components/preview/index.ts +22 -0
  60. package/src/components/primitives/Avatar.tsx +68 -0
  61. package/src/components/primitives/Button.tsx +102 -0
  62. package/src/components/primitives/Card.tsx +30 -0
  63. package/src/components/primitives/Divider.tsx +17 -0
  64. package/src/components/primitives/Icon.tsx +40 -0
  65. package/src/components/primitives/MarkdownText.tsx +72 -0
  66. package/src/components/primitives/Modal.tsx +53 -0
  67. package/src/components/primitives/Surface.tsx +42 -0
  68. package/src/components/primitives/Text.tsx +83 -0
  69. package/src/components/primitives/index.ts +35 -0
  70. package/src/components/primitives/types.ts +30 -0
  71. package/src/components/studio-sheet/StudioBottomSheet.tsx +114 -0
  72. package/src/components/studio-sheet/StudioSheetBackground.tsx +63 -0
  73. package/src/components/studio-sheet/StudioSheetHeader.tsx +35 -0
  74. package/src/components/studio-sheet/StudioSheetHeaderIconButton.tsx +109 -0
  75. package/src/components/studio-sheet/StudioSheetPager.tsx +66 -0
  76. package/src/components/studio-sheet/index.ts +18 -0
  77. package/src/components/studio-sheet/types.ts +5 -0
  78. package/src/components/utils/color.ts +25 -0
  79. package/src/components/utils/formatTimeAgo.ts +19 -0
  80. package/src/core/logger.ts +42 -0
  81. package/src/core/services/http/baseUrl.ts +3 -0
  82. package/src/core/services/http/index.ts +128 -0
  83. package/src/core/services/http/public.ts +14 -0
  84. package/src/core/services/supabase/auth.ts +41 -0
  85. package/src/core/services/supabase/client.ts +43 -0
  86. package/src/core/services/supabase/index.ts +7 -0
  87. package/src/data/agent/remote.ts +30 -0
  88. package/src/data/agent/repository.ts +34 -0
  89. package/src/data/agent/types.ts +28 -0
  90. package/src/data/apps/bundles/remote.ts +47 -0
  91. package/src/data/apps/bundles/repository.ts +35 -0
  92. package/src/data/apps/bundles/types.ts +27 -0
  93. package/src/data/apps/images/remote.ts +61 -0
  94. package/src/data/apps/images/repository.ts +47 -0
  95. package/src/data/apps/remote.ts +97 -0
  96. package/src/data/apps/repository.ts +185 -0
  97. package/src/data/apps/types.ts +206 -0
  98. package/src/data/attachment/remote.ts +32 -0
  99. package/src/data/attachment/repository.ts +40 -0
  100. package/src/data/attachment/types.ts +42 -0
  101. package/src/data/base-remote.ts +3 -0
  102. package/src/data/base-repository.ts +11 -0
  103. package/src/data/comments/likes/remote.ts +87 -0
  104. package/src/data/comments/likes/repository.ts +61 -0
  105. package/src/data/comments/likes/types.ts +47 -0
  106. package/src/data/comments/remote.ts +71 -0
  107. package/src/data/comments/repository.ts +53 -0
  108. package/src/data/comments/types.ts +60 -0
  109. package/src/data/github/remote.ts +23 -0
  110. package/src/data/github/repository.ts +35 -0
  111. package/src/data/github/types.ts +23 -0
  112. package/src/data/home/remote.ts +24 -0
  113. package/src/data/home/repository.ts +28 -0
  114. package/src/data/home/types.ts +70 -0
  115. package/src/data/index.ts +3 -0
  116. package/src/data/likes/remote.ts +57 -0
  117. package/src/data/likes/repository.ts +47 -0
  118. package/src/data/likes/types.ts +46 -0
  119. package/src/data/me/remote.ts +28 -0
  120. package/src/data/me/repository.ts +30 -0
  121. package/src/data/me/types.ts +14 -0
  122. package/src/data/merge-requests/remote.ts +76 -0
  123. package/src/data/merge-requests/repository.ts +66 -0
  124. package/src/data/merge-requests/types.ts +33 -0
  125. package/src/data/messages/remote.ts +21 -0
  126. package/src/data/messages/repository.ts +104 -0
  127. package/src/data/messages/types.ts +20 -0
  128. package/src/data/public/studio-config/remote.ts +19 -0
  129. package/src/data/public/studio-config/repository.ts +23 -0
  130. package/src/data/public/studio-config/types.ts +6 -0
  131. package/src/data/ratings/remote.ts +76 -0
  132. package/src/data/ratings/repository.ts +63 -0
  133. package/src/data/ratings/types.ts +57 -0
  134. package/src/data/threads/remote.ts +40 -0
  135. package/src/data/threads/repository.ts +41 -0
  136. package/src/data/threads/types.ts +25 -0
  137. package/src/data/types.ts +8 -0
  138. package/src/data/users/remote.ts +31 -0
  139. package/src/data/users/repository.ts +45 -0
  140. package/src/data/users/types.ts +15 -0
  141. package/src/index.ts +6 -0
  142. package/src/studio/ComergeStudio.tsx +246 -0
  143. package/src/studio/bootstrap/StudioBootstrap.tsx +45 -0
  144. package/src/studio/bootstrap/useStudioBootstrap.ts +51 -0
  145. package/src/studio/hooks/useApp.ts +83 -0
  146. package/src/studio/hooks/useAppStats.ts +111 -0
  147. package/src/studio/hooks/useAttachmentUpload.ts +59 -0
  148. package/src/studio/hooks/useBundleManager.ts +389 -0
  149. package/src/studio/hooks/useMergeRequests.ts +173 -0
  150. package/src/studio/hooks/useStudioActions.ts +96 -0
  151. package/src/studio/hooks/useThreadMessages.ts +85 -0
  152. package/src/studio/lib/chat.ts +34 -0
  153. package/src/studio/ui/ChatPanel.tsx +154 -0
  154. package/src/studio/ui/ConfirmMergeFlow.tsx +55 -0
  155. package/src/studio/ui/PreviewPanel.tsx +131 -0
  156. package/src/studio/ui/RuntimeRenderer.tsx +40 -0
  157. package/src/studio/ui/StudioOverlay.tsx +257 -0
  158. package/src/studio/ui/preview-panel/PressableCardRow.tsx +49 -0
  159. package/src/studio/ui/preview-panel/PreviewCollaborateSection.tsx +174 -0
  160. package/src/studio/ui/preview-panel/PreviewCustomizeSection.tsx +160 -0
  161. package/src/studio/ui/preview-panel/PreviewHeroSection.tsx +56 -0
  162. package/src/studio/ui/preview-panel/PreviewMetaSection.tsx +67 -0
  163. package/src/studio/ui/preview-panel/PreviewPanelHeader.tsx +48 -0
  164. package/src/studio/ui/preview-panel/SectionTitle.tsx +31 -0
  165. package/src/studio/ui/preview-panel/usePreviewPanelData.ts +132 -0
  166. package/src/studio/ui/preview-panel/utils.ts +29 -0
  167. package/src/theme/index.ts +5 -0
  168. package/src/theme/tokens.ts +118 -0
  169. package/src/theme/types.ts +90 -0
  170. package/src/theme/useTheme.ts +11 -0
  171. package/dist/assets/images/merge.svg +0 -3
  172. package/dist/merge-72UG27QV.svg +0 -3
@@ -0,0 +1,96 @@
1
+ import * as React from 'react';
2
+
3
+ import type { App } from '../../data/apps/types';
4
+ import { appsRepository } from '../../data/apps/repository';
5
+ import { agentRepository } from '../../data/agent/repository';
6
+ import type { AttachmentMeta } from '../../data/attachment/types';
7
+
8
+ export type UseStudioActionsParams = {
9
+ userId: string | null;
10
+ /**
11
+ * Current app object for the active appId.
12
+ */
13
+ app: App | null;
14
+ /**
15
+ * Called when we fork and should switch to the new app.
16
+ */
17
+ onForkedApp?: (appId: string, opts?: { keepRenderingAppId?: string }) => void;
18
+ /**
19
+ * Upload function used to convert attachments.
20
+ */
21
+ uploadAttachments?: (params: { threadId: string; appId: string; dataUrls: string[] }) => Promise<AttachmentMeta[]>;
22
+ };
23
+
24
+ export type UseStudioActionsResult = {
25
+ isOwner: boolean;
26
+ shouldForkOnEdit: boolean;
27
+ forking: boolean;
28
+ sending: boolean;
29
+ error: Error | null;
30
+ sendEdit: (params: { prompt: string; attachments?: string[] }) => Promise<void>;
31
+ };
32
+
33
+ export function useStudioActions({
34
+ userId,
35
+ app,
36
+ onForkedApp,
37
+ uploadAttachments,
38
+ }: UseStudioActionsParams): UseStudioActionsResult {
39
+ const [forking, setForking] = React.useState(false);
40
+ const [sending, setSending] = React.useState(false);
41
+ const [error, setError] = React.useState<Error | null>(null);
42
+
43
+ const isOwner = Boolean(userId && app?.createdBy && userId === app.createdBy);
44
+ const shouldForkOnEdit = Boolean(userId && app && app.createdBy !== userId);
45
+
46
+ const sendEdit = React.useCallback(
47
+ async ({ prompt, attachments }: { prompt: string; attachments?: string[] }) => {
48
+ if (!userId || !app) return;
49
+ if (!prompt.trim()) return;
50
+ if (sending) return;
51
+
52
+ setSending(true);
53
+ setError(null);
54
+ try {
55
+ let targetApp = app;
56
+
57
+ if (shouldForkOnEdit) {
58
+ setForking(true);
59
+ const sourceAppId = app.id;
60
+ const forked = await appsRepository.fork(app.id, {});
61
+ targetApp = forked;
62
+ // For fork+edit, keep rendering the original app until the edit completes on the fork.
63
+ onForkedApp?.(forked.id, { keepRenderingAppId: sourceAppId });
64
+ }
65
+ setForking(false);
66
+
67
+ const threadId = targetApp.threadId;
68
+ if (!threadId) throw new Error('No thread available for this app.');
69
+
70
+ let attachmentMetas: AttachmentMeta[] | undefined;
71
+ if (attachments && attachments.length > 0 && uploadAttachments) {
72
+ attachmentMetas = await uploadAttachments({ threadId, appId: targetApp.id, dataUrls: attachments });
73
+ }
74
+
75
+ await agentRepository.editApp({
76
+ prompt,
77
+ thread_id: threadId,
78
+ app_id: targetApp.id,
79
+ attachments: attachmentMetas && attachmentMetas.length > 0 ? attachmentMetas : undefined,
80
+ });
81
+ } catch (e) {
82
+ const err = e instanceof Error ? e : new Error(String(e));
83
+ setError(err);
84
+ throw err;
85
+ } finally {
86
+ setForking(false);
87
+ setSending(false);
88
+ }
89
+ },
90
+ [app, onForkedApp, sending, shouldForkOnEdit, uploadAttachments, userId]
91
+ );
92
+
93
+ return { isOwner, shouldForkOnEdit, forking, sending, error, sendEdit };
94
+ }
95
+
96
+
@@ -0,0 +1,85 @@
1
+ import * as React from 'react';
2
+
3
+ import type { Message } from '../../data/messages/types';
4
+ import { messagesRepository } from '../../data/messages/repository';
5
+ import type { ChatMessage } from '../../components/models/types';
6
+
7
+ export type UseThreadMessagesResult = {
8
+ raw: Message[];
9
+ messages: ChatMessage[];
10
+ loading: boolean;
11
+ error: Error | null;
12
+ refetch: () => Promise<void>;
13
+ };
14
+
15
+ function extractMeta(payload: unknown): ChatMessage['meta'] {
16
+ const meta = (payload as any)?.meta;
17
+ if (!meta || typeof meta !== 'object') return null;
18
+ const obj = meta as Record<string, unknown>;
19
+ return {
20
+ kind: typeof obj.kind === 'string' ? obj.kind : undefined,
21
+ event: typeof obj.event === 'string' ? obj.event : undefined,
22
+ status: typeof obj.status === 'string' ? (obj.status as any) : undefined,
23
+ mergeRequestId: typeof obj.mergeRequestId === 'string' ? obj.mergeRequestId : undefined,
24
+ sourceAppId: typeof obj.sourceAppId === 'string' ? obj.sourceAppId : undefined,
25
+ targetAppId: typeof obj.targetAppId === 'string' ? obj.targetAppId : undefined,
26
+ appId: typeof obj.appId === 'string' ? obj.appId : undefined,
27
+ threadId: typeof obj.threadId === 'string' ? obj.threadId : undefined,
28
+ };
29
+ }
30
+
31
+ function mapMessageToChatMessage(m: Message): ChatMessage {
32
+ const kind = typeof (m.payload as any)?.type === 'string' ? String((m.payload as any).type) : null;
33
+ return {
34
+ id: m.id,
35
+ author: m.authorType === 'ai' ? 'assistant' : 'human',
36
+ content: typeof m.payload?.content === 'string' ? m.payload.content : '',
37
+ createdAt: m.createdAt,
38
+ kind,
39
+ meta: extractMeta(m.payload),
40
+ };
41
+ }
42
+
43
+ export function useThreadMessages(threadId: string): UseThreadMessagesResult {
44
+ const [raw, setRaw] = React.useState<Message[]>([]);
45
+ const [loading, setLoading] = React.useState(false);
46
+ const [error, setError] = React.useState<Error | null>(null);
47
+
48
+ const refetch = React.useCallback(async () => {
49
+ if (!threadId) {
50
+ setRaw([]);
51
+ return;
52
+ }
53
+ setLoading(true);
54
+ setError(null);
55
+ try {
56
+ const list = await messagesRepository.list(threadId);
57
+ setRaw(list);
58
+ } catch (e) {
59
+ setError(e instanceof Error ? e : new Error(String(e)));
60
+ setRaw([]);
61
+ } finally {
62
+ setLoading(false);
63
+ }
64
+ }, [threadId]);
65
+
66
+ React.useEffect(() => {
67
+ void refetch();
68
+ }, [refetch]);
69
+
70
+ React.useEffect(() => {
71
+ if (!threadId) return;
72
+ const unsubscribe = messagesRepository.subscribeThread(threadId, {
73
+ onInsert: (m) => setRaw((prev) => [...prev, m]),
74
+ onUpdate: (m) => setRaw((prev) => prev.map((x) => (x.id === m.id ? m : x))),
75
+ onDelete: (m) => setRaw((prev) => prev.filter((x) => x.id !== m.id)),
76
+ });
77
+ return unsubscribe;
78
+ }, [threadId]);
79
+
80
+ const messages = React.useMemo(() => raw.map(mapMessageToChatMessage), [raw]);
81
+
82
+ return { raw, messages, loading, error, refetch };
83
+ }
84
+
85
+
@@ -0,0 +1,34 @@
1
+ import type { Message } from '../../data/messages/types';
2
+
3
+ type MessagePayload = { type?: string; content?: unknown; text?: unknown; prompt?: unknown; message?: unknown };
4
+
5
+ export function getLastOutcomeIndex(messages: Message[]): number {
6
+ let idx = -1;
7
+ for (let i = 0; i < messages.length; i += 1) {
8
+ const payload = messages[i].payload as MessagePayload | undefined;
9
+ if (payload?.type === 'outcome') idx = i;
10
+ }
11
+ return idx;
12
+ }
13
+
14
+ export function hasNoOutcomeAfterLastHuman(messages: Message[]): boolean {
15
+ if (!messages || messages.length === 0) return false;
16
+
17
+ let lastHumanIndex = -1;
18
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
19
+ if (messages[i].authorType === 'human') {
20
+ lastHumanIndex = i;
21
+ break;
22
+ }
23
+ }
24
+ if (lastHumanIndex === -1) return false;
25
+
26
+ for (let i = lastHumanIndex + 1; i < messages.length; i += 1) {
27
+ const m = messages[i];
28
+ const payload = m.payload as MessagePayload | undefined;
29
+ if (m.authorType === 'ai' && payload?.type === 'outcome') return false;
30
+ }
31
+ return true;
32
+ }
33
+
34
+
@@ -0,0 +1,154 @@
1
+ import * as React from 'react';
2
+ import { ActivityIndicator, View } from 'react-native';
3
+
4
+ import type { ChatMessageListRef } from '../../components/chat/ChatMessageList';
5
+ import { ChatPage } from '../../components/chat/ChatPage';
6
+ import { ScrollToBottomButton } from '../../components/chat/ScrollToBottomButton';
7
+ import { ChatHeader } from '../../components/chat/ChatHeader';
8
+ import { ForkNoticeBanner } from '../../components/chat/ForkNoticeBanner';
9
+ import { StudioSheetHeaderIconButton } from '../../components/studio-sheet/StudioSheetHeaderIconButton';
10
+ import { IconArrowDown, IconBack, IconClose, IconDraw, IconHome } from '../../components/icons/StudioIcons';
11
+ import { Text } from '../../components/primitives/Text';
12
+ import type { ChatMessage } from '../../components/models/types';
13
+
14
+ export type ChatPanelProps = {
15
+ title?: string;
16
+ autoFocusComposer?: boolean;
17
+ messages: ChatMessage[];
18
+ showTypingIndicator?: boolean;
19
+ loading?: boolean;
20
+ sendDisabled?: boolean;
21
+ forking?: boolean;
22
+ sending?: boolean;
23
+ shouldForkOnEdit?: boolean;
24
+ attachments?: string[];
25
+ onRemoveAttachment?: (index: number) => void;
26
+ onClearAttachments?: () => void;
27
+ onBack: () => void;
28
+ onClose: () => void;
29
+ onNavigateHome?: () => void;
30
+ onStartDraw?: () => void;
31
+ onSend: (text: string, attachments?: string[]) => void | Promise<void>;
32
+ };
33
+
34
+ export function ChatPanel({
35
+ title = 'Chat',
36
+ autoFocusComposer = false,
37
+ messages,
38
+ showTypingIndicator,
39
+ loading,
40
+ sendDisabled,
41
+ forking = false,
42
+ sending,
43
+ shouldForkOnEdit,
44
+ attachments = [],
45
+ onRemoveAttachment,
46
+ onClearAttachments,
47
+ onBack,
48
+ onClose,
49
+ onNavigateHome,
50
+ onStartDraw,
51
+ onSend,
52
+ }: ChatPanelProps) {
53
+ const listRef = React.useRef<ChatMessageListRef | null>(null);
54
+ const [nearBottom, setNearBottom] = React.useState(true);
55
+
56
+ const handleSend = React.useCallback(
57
+ async (text: string, composerAttachments?: string[]) => {
58
+ const all = composerAttachments ?? attachments;
59
+ await onSend(text, all.length > 0 ? all : undefined);
60
+ onClearAttachments?.();
61
+ requestAnimationFrame(() => listRef.current?.scrollToBottom({ animated: true }));
62
+ },
63
+ [attachments, onClearAttachments, onSend]
64
+ );
65
+
66
+ const handleScrollToBottom = React.useCallback(() => {
67
+ listRef.current?.scrollToBottom({ animated: true });
68
+ }, []);
69
+
70
+ const header = (
71
+ <ChatHeader
72
+ left={
73
+ <View style={{ flexDirection: 'row', alignItems: 'center' }}>
74
+ <StudioSheetHeaderIconButton onPress={onBack} accessibilityLabel="Back" style={{ marginRight: 8 }}>
75
+ <IconBack size={20} colorToken="floatingContent" />
76
+ </StudioSheetHeaderIconButton>
77
+ {onNavigateHome ? (
78
+ <StudioSheetHeaderIconButton onPress={onNavigateHome} accessibilityLabel="Home">
79
+ <IconHome size={20} colorToken="floatingContent" />
80
+ </StudioSheetHeaderIconButton>
81
+ ) : null}
82
+ </View>
83
+ }
84
+ right={
85
+ <View style={{ flexDirection: 'row', alignItems: 'center' }}>
86
+ {onStartDraw ? (
87
+ <StudioSheetHeaderIconButton onPress={onStartDraw} accessibilityLabel="Draw" intent="danger" style={{ marginRight: 8 }}>
88
+ <IconDraw size={20} colorToken="onDanger" />
89
+ </StudioSheetHeaderIconButton>
90
+ ) : null}
91
+ <StudioSheetHeaderIconButton onPress={onClose} accessibilityLabel="Close">
92
+ <IconClose size={20} colorToken="floatingContent" />
93
+ </StudioSheetHeaderIconButton>
94
+ </View>
95
+ }
96
+ center={null}
97
+ />
98
+ );
99
+
100
+ const topBanner =
101
+ shouldForkOnEdit ? (
102
+ <ForkNoticeBanner
103
+ isOwner={!shouldForkOnEdit}
104
+ style={{ marginBottom: 12 }}
105
+ />
106
+ ) : null;
107
+
108
+ const showMessagesLoading = (Boolean(loading) && messages.length === 0) || forking;
109
+ if (showMessagesLoading) {
110
+ return (
111
+ <View style={{ flex: 1 }}>
112
+ <View>{header}</View>
113
+ {topBanner ? <View style={{ paddingHorizontal: 16, paddingTop: 8 }}>{topBanner}</View> : null}
114
+ <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 24, paddingVertical: 12 }}>
115
+ <ActivityIndicator />
116
+ <View style={{ height: 12 }} />
117
+ <Text variant="bodyMuted">{forking ? 'Creating your copy…' : 'Loading messages…'}</Text>
118
+ </View>
119
+ </View>
120
+ );
121
+ }
122
+
123
+ return (
124
+ <ChatPage
125
+ header={header}
126
+ messages={messages}
127
+ showTypingIndicator={showTypingIndicator}
128
+ topBanner={topBanner}
129
+ listRef={listRef}
130
+ onNearBottomChange={setNearBottom}
131
+ overlay={
132
+ <ScrollToBottomButton
133
+ visible={!nearBottom}
134
+ onPress={handleScrollToBottom}
135
+ style={{ bottom: 80 }}
136
+ >
137
+ <IconArrowDown size={20} colorToken="floatingContent" />
138
+ </ScrollToBottomButton>
139
+ }
140
+ composer={{
141
+ disabled: Boolean(loading) || Boolean(sendDisabled) || Boolean(forking),
142
+ sending: Boolean(sending),
143
+ autoFocus: autoFocusComposer,
144
+ onSend: handleSend,
145
+ attachments,
146
+ onRemoveAttachment: onRemoveAttachment,
147
+ onAddAttachment: onStartDraw,
148
+ useBottomSheetTextInput: true,
149
+ }}
150
+ />
151
+ );
152
+ }
153
+
154
+
@@ -0,0 +1,55 @@
1
+ import * as React from 'react';
2
+
3
+ import type { MergeRequest } from '../../data/merge-requests/types';
4
+ import { ConfirmMergeRequestDialog } from '../../components/dialogs/ConfirmMergeRequestDialog';
5
+ import type { MergeRequestSummary } from '../../components/models/types';
6
+
7
+ export type ConfirmMergeFlowProps = {
8
+ visible: boolean;
9
+ onOpenChange: (open: boolean) => void;
10
+ mergeRequest: MergeRequest | null;
11
+ toSummary: (mr: MergeRequest) => MergeRequestSummary;
12
+ /**
13
+ * Disable the primary "Approve Merge" action (e.g. while submitting).
14
+ */
15
+ approveDisabled?: boolean;
16
+ /**
17
+ * Whether the system is building/preparing a test bundle.
18
+ * Disables the "Test edits first" action and shows "Preparing…".
19
+ */
20
+ isBuilding?: boolean;
21
+ onConfirm: (mr: MergeRequest) => void | Promise<void>;
22
+ onTestFirst: (mr: MergeRequest) => void | Promise<void>;
23
+ };
24
+
25
+ export function ConfirmMergeFlow({
26
+ visible,
27
+ onOpenChange,
28
+ mergeRequest,
29
+ toSummary,
30
+ approveDisabled,
31
+ isBuilding,
32
+ onConfirm,
33
+ onTestFirst,
34
+ }: ConfirmMergeFlowProps) {
35
+ return (
36
+ <ConfirmMergeRequestDialog
37
+ visible={visible}
38
+ onOpenChange={onOpenChange}
39
+ mergeRequest={mergeRequest ? toSummary(mergeRequest) : null}
40
+ approveDisabled={approveDisabled}
41
+ isBuilding={isBuilding}
42
+ onConfirm={() => {
43
+ if (!mergeRequest) return;
44
+ return onConfirm(mergeRequest);
45
+ }}
46
+ onTestFirst={(mrSummary) => {
47
+ if (!mergeRequest) return;
48
+ void mrSummary;
49
+ return onTestFirst(mergeRequest);
50
+ }}
51
+ />
52
+ );
53
+ }
54
+
55
+
@@ -0,0 +1,131 @@
1
+ import * as React from 'react';
2
+ import { ActivityIndicator, View } from 'react-native';
3
+
4
+ import type { App } from '../../data/apps/types';
5
+ import type { MergeRequest } from '../../data/merge-requests/types';
6
+ import { PreviewPage } from '../../components/preview/PreviewPage';
7
+ import { Text } from '../../components/primitives/Text';
8
+ import { PreviewPanelHeader } from './preview-panel/PreviewPanelHeader';
9
+ import { PreviewHeroSection } from './preview-panel/PreviewHeroSection';
10
+ import { PreviewMetaSection } from './preview-panel/PreviewMetaSection';
11
+ import { PreviewCustomizeSection } from './preview-panel/PreviewCustomizeSection';
12
+ import { PreviewCollaborateSection } from './preview-panel/PreviewCollaborateSection';
13
+ import { usePreviewPanelData } from './preview-panel/usePreviewPanelData';
14
+
15
+ export type PreviewPanelProps = {
16
+ app: App | null;
17
+ loading?: boolean;
18
+ isOwner: boolean;
19
+ shouldForkOnEdit: boolean;
20
+ incomingMergeRequests: MergeRequest[];
21
+ outgoingMergeRequests: MergeRequest[];
22
+ creatorStatsById: Record<string, import('../../data/users/types').UserStats>;
23
+ processingMrId?: string | null;
24
+ isBuildingMrTest?: boolean;
25
+ testingMrId?: string | null;
26
+ toMergeRequestSummary: (mr: MergeRequest) => import('../../components/models/types').MergeRequestSummary;
27
+ onClose: () => void;
28
+ onNavigateHome?: () => void;
29
+ onGoToChat: () => void;
30
+ onStartDraw?: () => void;
31
+ onSubmitMergeRequest?: () => void | Promise<void>;
32
+ onRequestApprove?: (mr: MergeRequest) => void;
33
+ onReject?: (mr: MergeRequest) => void | Promise<void>;
34
+ onTestMr?: (mr: MergeRequest) => void | Promise<void>;
35
+ onOpenComments?: () => void;
36
+ commentCountOverride?: number;
37
+ };
38
+
39
+ export function PreviewPanel({
40
+ app,
41
+ loading,
42
+ isOwner,
43
+ shouldForkOnEdit,
44
+ incomingMergeRequests,
45
+ outgoingMergeRequests,
46
+ creatorStatsById,
47
+ processingMrId,
48
+ isBuildingMrTest,
49
+ testingMrId,
50
+ toMergeRequestSummary,
51
+ onClose,
52
+ onNavigateHome,
53
+ onGoToChat,
54
+ onStartDraw,
55
+ onSubmitMergeRequest,
56
+ onRequestApprove,
57
+ onReject,
58
+ onTestMr,
59
+ onOpenComments,
60
+ commentCountOverride,
61
+ }: PreviewPanelProps) {
62
+ const { imageUrl, imageLoaded, setImageLoaded, creator, insights, stats, showProcessing, canSubmitMergeRequest } = usePreviewPanelData({
63
+ app,
64
+ isOwner,
65
+ outgoingMergeRequests,
66
+ onOpenComments,
67
+ commentCountOverride,
68
+ });
69
+
70
+ const header = <PreviewPanelHeader isOwner={isOwner} onClose={onClose} onNavigateHome={onNavigateHome} onGoToChat={onGoToChat} />;
71
+
72
+ if (loading || !app) {
73
+ return (
74
+ <PreviewPage header={header}>
75
+ <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 }}>
76
+ <ActivityIndicator />
77
+ <View style={{ height: 12 }} />
78
+ <Text variant="bodyMuted">Loading app…</Text>
79
+ </View>
80
+ </PreviewPage>
81
+ );
82
+ }
83
+
84
+ return (
85
+ <PreviewPage header={header}>
86
+ <PreviewHeroSection
87
+ appStatus={app.status}
88
+ showProcessing={showProcessing}
89
+ imageUrl={imageUrl}
90
+ imageLoaded={imageLoaded}
91
+ onImageLoad={() => setImageLoaded(true)}
92
+ stats={{
93
+ likeCount: stats.likeCount,
94
+ commentCount: stats.commentCount,
95
+ forkCount: stats.forkCount,
96
+ isLiked: stats.isLiked,
97
+ handleLike: stats.handleLike,
98
+ handleOpenComments: stats.handleOpenComments,
99
+ }}
100
+ />
101
+
102
+ <PreviewMetaSection app={app} isOwner={isOwner} creator={creator} downloadsCount={insights.downloads} />
103
+
104
+ <PreviewCustomizeSection
105
+ app={app}
106
+ isOwner={isOwner}
107
+ shouldForkOnEdit={shouldForkOnEdit}
108
+ showProcessing={showProcessing}
109
+ onGoToChat={onGoToChat}
110
+ onStartDraw={onStartDraw}
111
+ />
112
+
113
+ <PreviewCollaborateSection
114
+ canSubmitMergeRequest={canSubmitMergeRequest}
115
+ incomingMergeRequests={incomingMergeRequests}
116
+ outgoingMergeRequests={outgoingMergeRequests}
117
+ creatorStatsById={creatorStatsById}
118
+ processingMrId={processingMrId}
119
+ isBuildingMrTest={isBuildingMrTest}
120
+ testingMrId={testingMrId}
121
+ toMergeRequestSummary={toMergeRequestSummary}
122
+ onSubmitMergeRequest={onSubmitMergeRequest}
123
+ onRequestApprove={onRequestApprove}
124
+ onReject={onReject}
125
+ onTestMr={onTestMr}
126
+ />
127
+ </PreviewPage>
128
+ );
129
+ }
130
+
131
+
@@ -0,0 +1,40 @@
1
+ import * as React from 'react';
2
+ import { View, type ViewStyle } from 'react-native';
3
+
4
+ import { ComergeRuntimeRenderer } from '@comergehq/runtime';
5
+
6
+ import { Text } from '../../components/primitives/Text';
7
+
8
+ export type RuntimeRendererProps = {
9
+ appKey: string;
10
+ bundlePath: string | null;
11
+ /**
12
+ * Used to force a runtime remount even when bundlePath stays constant
13
+ * (e.g. base bundle replaced in-place).
14
+ */
15
+ renderToken?: number;
16
+ style?: ViewStyle;
17
+ };
18
+
19
+ export function RuntimeRenderer({ appKey, bundlePath, renderToken, style }: RuntimeRendererProps) {
20
+ if (!bundlePath) {
21
+ return (
22
+ <View style={[{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 }, style]}>
23
+ <Text variant="bodyMuted">Preparing app…</Text>
24
+ </View>
25
+ );
26
+ }
27
+
28
+ return (
29
+ <View style={[{ flex: 1 }, style]}>
30
+ <ComergeRuntimeRenderer
31
+ key={`${appKey}:${bundlePath}:${renderToken ?? 0}`}
32
+ appKey={appKey}
33
+ bundlePath={bundlePath}
34
+ style={{ flex: 1 }}
35
+ />
36
+ </View>
37
+ );
38
+ }
39
+
40
+