@comergehq/studio 0.1.15 → 0.1.17

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.
@@ -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);
@@ -66,18 +72,23 @@ export function useStudioActions({
66
72
 
67
73
  const threadId = targetApp.threadId;
68
74
  if (!threadId) throw new Error('No thread available for this app.');
75
+ onEditStart?.();
69
76
 
70
77
  let attachmentMetas: AttachmentMeta[] | undefined;
71
78
  if (attachments && attachments.length > 0 && uploadAttachments) {
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,9 @@ 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 next = prev.some((x) => x.id === m.id) ? prev.map((x) => (x.id === m.id ? m : x)) : [...prev, m];
53
- // Keep ordering stable for the UI (chat scrolling is very sensitive to reorders).
54
- next.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
86
+ const next = prev.filter((x) => x.id !== m.id);
87
+ next.push(m);
88
+ next.sort(compareMessages);
55
89
  return next;
56
90
  }, []);
57
91
 
@@ -66,8 +100,7 @@ export function useThreadMessages(threadId: string): UseThreadMessagesResult {
66
100
  try {
67
101
  const list = await messagesRepository.list(threadId);
68
102
  if (activeRequestIdRef.current !== requestId) return;
69
- // Ensure stable ordering for downstream scrolling behavior.
70
- setRaw([...list].sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt))));
103
+ setRaw([...list].sort(compareMessages));
71
104
  } catch (e) {
72
105
  if (activeRequestIdRef.current !== requestId) return;
73
106
  setError(e instanceof Error ? e : new Error(String(e)));
@@ -97,7 +130,11 @@ export function useThreadMessages(threadId: string): UseThreadMessagesResult {
97
130
  void refetch();
98
131
  }, [foregroundSignal, refetch, threadId]);
99
132
 
100
- const messages = React.useMemo(() => raw.map(mapMessageToChatMessage), [raw]);
133
+ const messages = React.useMemo(() => {
134
+ const visible = raw.filter((m) => !isQueuedHiddenMessage(m));
135
+ const resolved = visible.length > 0 ? visible : raw;
136
+ return resolved.map(mapMessageToChatMessage);
137
+ }, [raw]);
101
138
 
102
139
  return { raw, messages, loading, error, refetch };
103
140
  }
@@ -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,13 @@ export function StudioOverlay({
110
114
  const [commentsCount, setCommentsCount] = React.useState<number | null>(null);
111
115
 
112
116
  const threadId = app?.threadId ?? null;
117
+ const isForking = chatForking || app?.status === 'forking';
118
+ const queueItemsForChat = isForking ? [] : chatQueueItems;
119
+ const disableOptimistic = Boolean(queueItemsForChat && queueItemsForChat.length > 0) || app?.status === 'editing';
113
120
  const optimistic = useOptimisticChatMessages({
114
121
  threadId,
115
122
  shouldForkOnEdit,
123
+ disableOptimistic,
116
124
  chatMessages,
117
125
  onSendChat,
118
126
  });
@@ -265,6 +273,8 @@ export function StudioOverlay({
265
273
  onNavigateHome={onNavigateHome}
266
274
  onStartDraw={startDraw}
267
275
  onSend={optimistic.onSend}
276
+ queueItems={queueItemsForChat}
277
+ onRemoveQueueItem={onRemoveQueueItem}
268
278
  />
269
279
  }
270
280
  />