@comergehq/studio 0.1.15 → 0.1.16
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.js +609 -176
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +603 -170
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/ChatPage.tsx +19 -2
- package/src/components/chat/ChatQueue.tsx +163 -0
- package/src/data/agent/types.ts +2 -1
- package/src/data/apps/edit-queue/remote.ts +45 -0
- package/src/data/apps/edit-queue/repository.ts +136 -0
- package/src/data/apps/edit-queue/types.ts +31 -0
- package/src/studio/ComergeStudio.tsx +70 -2
- package/src/studio/hooks/useEditQueue.ts +71 -0
- package/src/studio/hooks/useEditQueueActions.ts +29 -0
- package/src/studio/hooks/useOptimisticChatMessages.ts +4 -2
- package/src/studio/hooks/useStudioActions.ts +14 -2
- package/src/studio/hooks/useThreadMessages.ts +39 -5
- package/src/studio/ui/ChatPanel.tsx +11 -0
- package/src/studio/ui/StudioOverlay.tsx +8 -0
|
@@ -5,6 +5,7 @@ import type { ChatMessage } from '../../components/models/types';
|
|
|
5
5
|
export type UseOptimisticChatMessagesParams = {
|
|
6
6
|
threadId: string | null;
|
|
7
7
|
shouldForkOnEdit: boolean;
|
|
8
|
+
disableOptimistic?: boolean;
|
|
8
9
|
chatMessages: ChatMessage[];
|
|
9
10
|
onSendChat: (text: string, attachments?: string[]) => void | Promise<void>;
|
|
10
11
|
};
|
|
@@ -63,6 +64,7 @@ function isOptimisticResolvedByServer(chatMessages: ChatMessage[], o: Optimistic
|
|
|
63
64
|
export function useOptimisticChatMessages({
|
|
64
65
|
threadId,
|
|
65
66
|
shouldForkOnEdit,
|
|
67
|
+
disableOptimistic = false,
|
|
66
68
|
chatMessages,
|
|
67
69
|
onSendChat,
|
|
68
70
|
}: UseOptimisticChatMessagesParams): UseOptimisticChatMessagesResult {
|
|
@@ -105,7 +107,7 @@ export function useOptimisticChatMessages({
|
|
|
105
107
|
|
|
106
108
|
const onSend = React.useCallback(
|
|
107
109
|
async (text: string, attachments?: string[]) => {
|
|
108
|
-
if (shouldForkOnEdit) {
|
|
110
|
+
if (shouldForkOnEdit || disableOptimistic) {
|
|
109
111
|
await onSendChat(text, attachments);
|
|
110
112
|
return;
|
|
111
113
|
}
|
|
@@ -120,7 +122,7 @@ export function useOptimisticChatMessages({
|
|
|
120
122
|
setOptimisticChat((prev) => prev.map((m) => (m.id === id ? { ...m, failed: true } : m)));
|
|
121
123
|
});
|
|
122
124
|
},
|
|
123
|
-
[chatMessages, onSendChat, shouldForkOnEdit]
|
|
125
|
+
[chatMessages, disableOptimistic, onSendChat, shouldForkOnEdit]
|
|
124
126
|
);
|
|
125
127
|
|
|
126
128
|
return { messages, onSend };
|
|
@@ -15,6 +15,9 @@ export type UseStudioActionsParams = {
|
|
|
15
15
|
* Called when we fork and should switch to the new app.
|
|
16
16
|
*/
|
|
17
17
|
onForkedApp?: (appId: string, opts?: { keepRenderingAppId?: string }) => void;
|
|
18
|
+
onEditStart?: () => void;
|
|
19
|
+
onEditQueued?: (info: { queueItemId?: string | null; queuePosition?: number | null }) => void;
|
|
20
|
+
onEditFinished?: () => void;
|
|
18
21
|
/**
|
|
19
22
|
* Upload function used to convert attachments.
|
|
20
23
|
*/
|
|
@@ -34,6 +37,9 @@ export function useStudioActions({
|
|
|
34
37
|
userId,
|
|
35
38
|
app,
|
|
36
39
|
onForkedApp,
|
|
40
|
+
onEditStart,
|
|
41
|
+
onEditQueued,
|
|
42
|
+
onEditFinished,
|
|
37
43
|
uploadAttachments,
|
|
38
44
|
}: UseStudioActionsParams): UseStudioActionsResult {
|
|
39
45
|
const [forking, setForking] = React.useState(false);
|
|
@@ -52,6 +58,7 @@ export function useStudioActions({
|
|
|
52
58
|
setSending(true);
|
|
53
59
|
setError(null);
|
|
54
60
|
try {
|
|
61
|
+
onEditStart?.();
|
|
55
62
|
let targetApp = app;
|
|
56
63
|
|
|
57
64
|
if (shouldForkOnEdit) {
|
|
@@ -72,12 +79,16 @@ export function useStudioActions({
|
|
|
72
79
|
attachmentMetas = await uploadAttachments({ threadId, appId: targetApp.id, dataUrls: attachments });
|
|
73
80
|
}
|
|
74
81
|
|
|
75
|
-
await agentRepository.editApp({
|
|
82
|
+
const editResult = await agentRepository.editApp({
|
|
76
83
|
prompt,
|
|
77
84
|
thread_id: threadId,
|
|
78
85
|
app_id: targetApp.id,
|
|
79
86
|
attachments: attachmentMetas && attachmentMetas.length > 0 ? attachmentMetas : undefined,
|
|
80
87
|
});
|
|
88
|
+
onEditQueued?.({
|
|
89
|
+
queueItemId: editResult.queueItemId ?? null,
|
|
90
|
+
queuePosition: editResult.queuePosition ?? null,
|
|
91
|
+
});
|
|
81
92
|
} catch (e) {
|
|
82
93
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
83
94
|
setError(err);
|
|
@@ -85,9 +96,10 @@ export function useStudioActions({
|
|
|
85
96
|
} finally {
|
|
86
97
|
setForking(false);
|
|
87
98
|
setSending(false);
|
|
99
|
+
onEditFinished?.();
|
|
88
100
|
}
|
|
89
101
|
},
|
|
90
|
-
[app, onForkedApp, sending, shouldForkOnEdit, uploadAttachments, userId]
|
|
102
|
+
[app, onEditFinished, onEditQueued, onEditStart, onForkedApp, sending, shouldForkOnEdit, uploadAttachments, userId]
|
|
91
103
|
);
|
|
92
104
|
|
|
93
105
|
return { isOwner, shouldForkOnEdit, forking, sending, error, sendEdit };
|
|
@@ -29,6 +29,40 @@ function extractMeta(payload: unknown): ChatMessage['meta'] {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function getPayloadMeta(payload: Message['payload']): Record<string, unknown> | null {
|
|
33
|
+
const meta = (payload as any)?.meta;
|
|
34
|
+
if (!meta || typeof meta !== 'object') return null;
|
|
35
|
+
return meta as Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isQueuedHiddenMessage(m: Message): boolean {
|
|
39
|
+
if (m.authorType !== 'human') return false;
|
|
40
|
+
const meta = getPayloadMeta(m.payload);
|
|
41
|
+
return meta?.visibility === 'queued';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function toEpochMs(value: unknown): number {
|
|
45
|
+
if (value == null) return 0;
|
|
46
|
+
if (typeof value === 'number') return value;
|
|
47
|
+
if (value instanceof Date) return value.getTime();
|
|
48
|
+
const parsed = Date.parse(String(value));
|
|
49
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getEffectiveSortMs(m: Message): number {
|
|
53
|
+
const meta = getPayloadMeta(m.payload);
|
|
54
|
+
const runStartedAt = meta?.runStartedAt;
|
|
55
|
+
const runMs = toEpochMs(runStartedAt);
|
|
56
|
+
return runMs > 0 ? runMs : toEpochMs(m.createdAt);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function compareMessages(a: Message, b: Message): number {
|
|
60
|
+
const aMs = getEffectiveSortMs(a);
|
|
61
|
+
const bMs = getEffectiveSortMs(b);
|
|
62
|
+
if (aMs !== bMs) return aMs - bMs;
|
|
63
|
+
return String(a.createdAt).localeCompare(String(b.createdAt));
|
|
64
|
+
}
|
|
65
|
+
|
|
32
66
|
function mapMessageToChatMessage(m: Message): ChatMessage {
|
|
33
67
|
const kind = typeof (m.payload as any)?.type === 'string' ? String((m.payload as any).type) : null;
|
|
34
68
|
return {
|
|
@@ -49,9 +83,10 @@ export function useThreadMessages(threadId: string): UseThreadMessagesResult {
|
|
|
49
83
|
const foregroundSignal = useForegroundSignal(Boolean(threadId));
|
|
50
84
|
|
|
51
85
|
const upsertSorted = React.useCallback((prev: Message[], m: Message) => {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
86
|
+
const include = !isQueuedHiddenMessage(m);
|
|
87
|
+
const next = prev.filter((x) => x.id !== m.id);
|
|
88
|
+
if (include) next.push(m);
|
|
89
|
+
next.sort(compareMessages);
|
|
55
90
|
return next;
|
|
56
91
|
}, []);
|
|
57
92
|
|
|
@@ -66,8 +101,7 @@ export function useThreadMessages(threadId: string): UseThreadMessagesResult {
|
|
|
66
101
|
try {
|
|
67
102
|
const list = await messagesRepository.list(threadId);
|
|
68
103
|
if (activeRequestIdRef.current !== requestId) return;
|
|
69
|
-
|
|
70
|
-
setRaw([...list].sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt))));
|
|
104
|
+
setRaw([...list].filter((m) => !isQueuedHiddenMessage(m)).sort(compareMessages));
|
|
71
105
|
} catch (e) {
|
|
72
106
|
if (activeRequestIdRef.current !== requestId) return;
|
|
73
107
|
setError(e instanceof Error ? e : new Error(String(e)));
|
|
@@ -10,6 +10,8 @@ import { StudioSheetHeaderIconButton } from '../../components/studio-sheet/Studi
|
|
|
10
10
|
import { IconArrowDown, IconBack, IconClose, IconDraw, IconHome } from '../../components/icons/StudioIcons';
|
|
11
11
|
import { Text } from '../../components/primitives/Text';
|
|
12
12
|
import type { ChatMessage } from '../../components/models/types';
|
|
13
|
+
import type { EditQueueItem } from '../../data/apps/edit-queue/types';
|
|
14
|
+
import { ChatQueue } from '../../components/chat/ChatQueue';
|
|
13
15
|
|
|
14
16
|
export type ChatPanelProps = {
|
|
15
17
|
title?: string;
|
|
@@ -29,6 +31,8 @@ export type ChatPanelProps = {
|
|
|
29
31
|
onNavigateHome?: () => void;
|
|
30
32
|
onStartDraw?: () => void;
|
|
31
33
|
onSend: (text: string, attachments?: string[]) => void | Promise<void>;
|
|
34
|
+
queueItems?: EditQueueItem[];
|
|
35
|
+
onRemoveQueueItem?: (id: string) => void;
|
|
32
36
|
};
|
|
33
37
|
|
|
34
38
|
export function ChatPanel({
|
|
@@ -49,6 +53,8 @@ export function ChatPanel({
|
|
|
49
53
|
onNavigateHome,
|
|
50
54
|
onStartDraw,
|
|
51
55
|
onSend,
|
|
56
|
+
queueItems = [],
|
|
57
|
+
onRemoveQueueItem,
|
|
52
58
|
}: ChatPanelProps) {
|
|
53
59
|
const listRef = React.useRef<ChatMessageListRef | null>(null);
|
|
54
60
|
const [nearBottom, setNearBottom] = React.useState(true);
|
|
@@ -123,12 +129,17 @@ export function ChatPanel({
|
|
|
123
129
|
);
|
|
124
130
|
}
|
|
125
131
|
|
|
132
|
+
const queueTop = queueItems.length > 0 ? (
|
|
133
|
+
<ChatQueue items={queueItems} onRemove={onRemoveQueueItem} />
|
|
134
|
+
) : null;
|
|
135
|
+
|
|
126
136
|
return (
|
|
127
137
|
<ChatPage
|
|
128
138
|
header={header}
|
|
129
139
|
messages={messages}
|
|
130
140
|
showTypingIndicator={showTypingIndicator}
|
|
131
141
|
topBanner={topBanner}
|
|
142
|
+
composerTop={queueTop}
|
|
132
143
|
composerHorizontalPadding={0}
|
|
133
144
|
listRef={listRef}
|
|
134
145
|
onNearBottomChange={setNearBottom}
|
|
@@ -58,6 +58,8 @@ export type StudioOverlayProps = {
|
|
|
58
58
|
chatSending?: boolean;
|
|
59
59
|
chatShowTypingIndicator?: boolean;
|
|
60
60
|
onSendChat: (text: string, attachments?: string[]) => void | Promise<void>;
|
|
61
|
+
chatQueueItems?: import('../../data/apps/edit-queue/types').EditQueueItem[];
|
|
62
|
+
onRemoveQueueItem?: (id: string) => void;
|
|
61
63
|
|
|
62
64
|
// Navigation callbacks
|
|
63
65
|
onNavigateHome?: () => void;
|
|
@@ -93,6 +95,8 @@ export function StudioOverlay({
|
|
|
93
95
|
chatSending,
|
|
94
96
|
chatShowTypingIndicator,
|
|
95
97
|
onSendChat,
|
|
98
|
+
chatQueueItems,
|
|
99
|
+
onRemoveQueueItem,
|
|
96
100
|
onNavigateHome,
|
|
97
101
|
showBubble,
|
|
98
102
|
studioControlOptions,
|
|
@@ -110,9 +114,11 @@ export function StudioOverlay({
|
|
|
110
114
|
const [commentsCount, setCommentsCount] = React.useState<number | null>(null);
|
|
111
115
|
|
|
112
116
|
const threadId = app?.threadId ?? null;
|
|
117
|
+
const disableOptimistic = Boolean(chatQueueItems && chatQueueItems.length > 0) || app?.status === 'editing';
|
|
113
118
|
const optimistic = useOptimisticChatMessages({
|
|
114
119
|
threadId,
|
|
115
120
|
shouldForkOnEdit,
|
|
121
|
+
disableOptimistic,
|
|
116
122
|
chatMessages,
|
|
117
123
|
onSendChat,
|
|
118
124
|
});
|
|
@@ -265,6 +271,8 @@ export function StudioOverlay({
|
|
|
265
271
|
onNavigateHome={onNavigateHome}
|
|
266
272
|
onStartDraw={startDraw}
|
|
267
273
|
onSend={optimistic.onSend}
|
|
274
|
+
queueItems={chatQueueItems}
|
|
275
|
+
onRemoveQueueItem={onRemoveQueueItem}
|
|
268
276
|
/>
|
|
269
277
|
}
|
|
270
278
|
/>
|