@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.
@@ -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 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 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
- // Ensure stable ordering for downstream scrolling behavior.
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
  />