Skip to content

Commit 811ecbd

Browse files
authored
fix(mothership): persist queued messages, edit-in-place preserves order (#4769)
* fix(mothership): persist queued messages, edit-in-place preserves order * fix(mothership): pause drain while head is in edit, restore handoff cleanup on edit, merge on migrate * improvement(mothership): strip editing on persist; tighten comments to codebase style * improvement(mothership): omit editing from partialize entirely * fix(mothership): honor user removal during dispatch in failure-restore path
1 parent 34c47f7 commit 811ecbd

9 files changed

Lines changed: 726 additions & 137 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,12 @@ interface MothershipChatProps {
4141
) => void
4242
onStopGeneration: () => void
4343
messageQueue: QueuedMessage[]
44+
editingQueuedId: string | null
45+
dispatchingHeadId: string | null
4446
onRemoveQueuedMessage: (id: string) => void
4547
onSendQueuedMessage: (id: string) => Promise<void>
4648
onEditQueuedMessage: (id: string) => QueuedMessage | undefined
49+
onCancelQueueEdit: () => void
4750
userId?: string
4851
chatId?: string
4952
onContextAdd?: (context: ChatContext) => void
@@ -183,9 +186,12 @@ export function MothershipChat({
183186
onSubmit,
184187
onStopGeneration,
185188
messageQueue,
189+
editingQueuedId,
190+
dispatchingHeadId,
186191
onRemoveQueuedMessage,
187192
onSendQueuedMessage,
188193
onEditQueuedMessage,
194+
onCancelQueueEdit,
189195
userId,
190196
chatId,
191197
onContextAdd,
@@ -313,9 +319,12 @@ export function MothershipChat({
313319
<div className={styles.footerInner}>
314320
<QueuedMessages
315321
messageQueue={messageQueue}
322+
editingQueuedId={editingQueuedId}
323+
dispatchingHeadId={dispatchingHeadId}
316324
onRemove={onRemoveQueuedMessage}
317325
onSendNow={onSendQueuedMessage}
318326
onEdit={handleEditQueued}
327+
onCancelEdit={onCancelQueueEdit}
319328
/>
320329
<UserInput
321330
ref={userInputRef}

apps/sim/app/workspace/[workspaceId]/home/components/queued-messages/queued-messages.tsx

Lines changed: 139 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,33 @@
11
'use client'
22

33
import { useCallback, useRef, useState } from 'react'
4-
import { ArrowUp, ChevronDown, ChevronRight, Paperclip, Pencil, Trash2 } from 'lucide-react'
4+
import { ArrowUp, ChevronDown, ChevronRight, Paperclip, Pencil, Trash2, X } from 'lucide-react'
55
import { Tooltip } from '@/components/emcn'
6+
import { cn } from '@/lib/core/utils/cn'
67
import { UserMessageContent } from '@/app/workspace/[workspaceId]/home/components/user-message-content'
78
import type { QueuedMessage } from '@/app/workspace/[workspaceId]/home/types'
89

910
const NARROW_WIDTH_PX = 320
1011

1112
interface QueuedMessagesProps {
1213
messageQueue: QueuedMessage[]
14+
editingQueuedId: string | null
15+
dispatchingHeadId: string | null
1316
onRemove: (id: string) => void
1417
onSendNow: (id: string) => Promise<void>
1518
onEdit: (id: string) => void
19+
onCancelEdit: () => void
1620
}
1721

18-
export function QueuedMessages({ messageQueue, onRemove, onSendNow, onEdit }: QueuedMessagesProps) {
22+
export function QueuedMessages({
23+
messageQueue,
24+
editingQueuedId,
25+
dispatchingHeadId,
26+
onRemove,
27+
onSendNow,
28+
onEdit,
29+
onCancelEdit,
30+
}: QueuedMessagesProps) {
1931
const [isExpanded, setIsExpanded] = useState(true)
2032
const [isNarrow, setIsNarrow] = useState(false)
2133
const roRef = useRef<ResizeObserver | null>(null)
@@ -57,101 +69,138 @@ export function QueuedMessages({ messageQueue, onRemove, onSendNow, onEdit }: Qu
5769

5870
{isExpanded && (
5971
<div>
60-
{messageQueue.map((msg) => (
61-
<div
62-
key={msg.id}
63-
className='flex items-center gap-2 py-1.5 pr-2 pl-3.5 transition-colors hover-hover:bg-[var(--surface-active)]'
64-
>
65-
<div className='flex size-[16px] shrink-0 items-center justify-center'>
66-
<div className='size-[10px] rounded-full border-[1.5px] border-[color-mix(in_srgb,var(--text-tertiary)_40%,transparent)]' />
67-
</div>
72+
{messageQueue.map((msg) => {
73+
const isEditing = msg.id === editingQueuedId
74+
const isDispatching = msg.id === dispatchingHeadId
75+
return (
76+
<div
77+
key={msg.id}
78+
className={cn(
79+
'flex items-center gap-2 py-1.5 pr-2 pl-3.5 transition-colors hover-hover:bg-[var(--surface-active)]',
80+
isEditing && 'bg-[var(--surface-active)]'
81+
)}
82+
>
83+
<div className='flex size-[16px] shrink-0 items-center justify-center'>
84+
<div
85+
className={cn(
86+
'size-[10px] rounded-full border-[1.5px] border-[color-mix(in_srgb,var(--text-tertiary)_40%,transparent)]',
87+
isEditing &&
88+
'border-[color-mix(in_srgb,var(--text-secondary)_60%,transparent)] border-dashed'
89+
)}
90+
/>
91+
</div>
6892

69-
<div className='min-w-0 flex-1 overflow-hidden'>
70-
<UserMessageContent
71-
content={msg.content}
72-
contexts={msg.contexts}
73-
plainMentions
74-
compact
75-
/>
76-
</div>
93+
<div className='min-w-0 flex-1 overflow-hidden'>
94+
<UserMessageContent
95+
content={msg.content}
96+
contexts={msg.contexts}
97+
plainMentions
98+
compact
99+
/>
100+
</div>
77101

78-
{msg.fileAttachments && msg.fileAttachments.length > 0 && (
79-
<span className='inline-flex min-w-0 max-w-[40%] shrink items-center gap-1 rounded-[5px] bg-[var(--surface-5)] px-[5px] py-0.5 text-[var(--text-primary)] text-small'>
80-
<Paperclip className='size-[12px] shrink-0 text-[var(--text-icon)]' />
81-
{isNarrow ? (
82-
<span className='shrink-0 text-[var(--text-secondary)]'>
83-
{msg.fileAttachments.length}
84-
</span>
102+
{msg.fileAttachments && msg.fileAttachments.length > 0 && (
103+
<span className='inline-flex min-w-0 max-w-[40%] shrink items-center gap-1 rounded-[5px] bg-[var(--surface-5)] px-[5px] py-0.5 text-[var(--text-primary)] text-small'>
104+
<Paperclip className='size-[12px] shrink-0 text-[var(--text-icon)]' />
105+
{isNarrow ? (
106+
<span className='shrink-0 text-[var(--text-secondary)]'>
107+
{msg.fileAttachments.length}
108+
</span>
109+
) : (
110+
<>
111+
<span className='truncate'>{msg.fileAttachments[0].filename}</span>
112+
{msg.fileAttachments.length > 1 && (
113+
<span className='shrink-0 text-[var(--text-secondary)]'>
114+
+{msg.fileAttachments.length - 1}
115+
</span>
116+
)}
117+
</>
118+
)}
119+
</span>
120+
)}
121+
122+
<div className='flex shrink-0 items-center gap-0.5'>
123+
{isEditing ? (
124+
<Tooltip.Root>
125+
<Tooltip.Trigger asChild>
126+
<button
127+
type='button'
128+
onClick={(e) => {
129+
e.stopPropagation()
130+
onCancelEdit()
131+
}}
132+
className='rounded-md p-[5px] text-[var(--text-icon)] transition-colors hover-hover:bg-[var(--surface-active)] hover-hover:text-[var(--text-primary)]'
133+
>
134+
<X className='size-[13px]' />
135+
</button>
136+
</Tooltip.Trigger>
137+
<Tooltip.Content side='top' sideOffset={4}>
138+
Cancel edit
139+
</Tooltip.Content>
140+
</Tooltip.Root>
85141
) : (
86142
<>
87-
<span className='truncate'>{msg.fileAttachments[0].filename}</span>
88-
{msg.fileAttachments.length > 1 && (
89-
<span className='shrink-0 text-[var(--text-secondary)]'>
90-
+{msg.fileAttachments.length - 1}
91-
</span>
92-
)}
93-
</>
94-
)}
95-
</span>
96-
)}
97-
98-
<div className='flex shrink-0 items-center gap-0.5'>
99-
<Tooltip.Root>
100-
<Tooltip.Trigger asChild>
101-
<button
102-
type='button'
103-
onClick={(e) => {
104-
e.stopPropagation()
105-
onEdit(msg.id)
106-
}}
107-
className='rounded-md p-[5px] text-[var(--text-icon)] transition-colors hover-hover:bg-[var(--surface-active)] hover-hover:text-[var(--text-primary)]'
108-
>
109-
<Pencil className='size-[13px]' />
110-
</button>
111-
</Tooltip.Trigger>
112-
<Tooltip.Content side='top' sideOffset={4}>
113-
Edit queued message
114-
</Tooltip.Content>
115-
</Tooltip.Root>
143+
<Tooltip.Root>
144+
<Tooltip.Trigger asChild>
145+
<button
146+
type='button'
147+
disabled={isDispatching}
148+
onClick={(e) => {
149+
e.stopPropagation()
150+
onEdit(msg.id)
151+
}}
152+
className='rounded-md p-[5px] text-[var(--text-icon)] transition-colors hover-hover:bg-[var(--surface-active)] hover-hover:text-[var(--text-primary)] disabled:cursor-not-allowed disabled:opacity-40 disabled:hover-hover:bg-transparent disabled:hover-hover:text-[var(--text-icon)]'
153+
>
154+
<Pencil className='size-[13px]' />
155+
</button>
156+
</Tooltip.Trigger>
157+
<Tooltip.Content side='top' sideOffset={4}>
158+
{isDispatching ? 'Sending now' : 'Edit queued message'}
159+
</Tooltip.Content>
160+
</Tooltip.Root>
116161

117-
<Tooltip.Root>
118-
<Tooltip.Trigger asChild>
119-
<button
120-
type='button'
121-
onClick={(e) => {
122-
e.stopPropagation()
123-
void onSendNow(msg.id)
124-
}}
125-
className='rounded-md p-[5px] text-[var(--text-icon)] transition-colors hover-hover:bg-[var(--surface-active)] hover-hover:text-[var(--text-primary)]'
126-
>
127-
<ArrowUp className='size-[13px]' />
128-
</button>
129-
</Tooltip.Trigger>
130-
<Tooltip.Content side='top' sideOffset={4}>
131-
Send now
132-
</Tooltip.Content>
133-
</Tooltip.Root>
162+
<Tooltip.Root>
163+
<Tooltip.Trigger asChild>
164+
<button
165+
type='button'
166+
disabled={isDispatching}
167+
onClick={(e) => {
168+
e.stopPropagation()
169+
void onSendNow(msg.id)
170+
}}
171+
className='rounded-md p-[5px] text-[var(--text-icon)] transition-colors hover-hover:bg-[var(--surface-active)] hover-hover:text-[var(--text-primary)] disabled:cursor-not-allowed disabled:opacity-40 disabled:hover-hover:bg-transparent disabled:hover-hover:text-[var(--text-icon)]'
172+
>
173+
<ArrowUp className='size-[13px]' />
174+
</button>
175+
</Tooltip.Trigger>
176+
<Tooltip.Content side='top' sideOffset={4}>
177+
Send now
178+
</Tooltip.Content>
179+
</Tooltip.Root>
134180

135-
<Tooltip.Root>
136-
<Tooltip.Trigger asChild>
137-
<button
138-
type='button'
139-
onClick={(e) => {
140-
e.stopPropagation()
141-
onRemove(msg.id)
142-
}}
143-
className='rounded-md p-[5px] text-[var(--text-icon)] transition-colors hover-hover:bg-[var(--surface-active)] hover-hover:text-[var(--text-primary)]'
144-
>
145-
<Trash2 className='size-[13px]' />
146-
</button>
147-
</Tooltip.Trigger>
148-
<Tooltip.Content side='top' sideOffset={4}>
149-
Remove from queue
150-
</Tooltip.Content>
151-
</Tooltip.Root>
181+
<Tooltip.Root>
182+
<Tooltip.Trigger asChild>
183+
<button
184+
type='button'
185+
onClick={(e) => {
186+
e.stopPropagation()
187+
onRemove(msg.id)
188+
}}
189+
className='rounded-md p-[5px] text-[var(--text-icon)] transition-colors hover-hover:bg-[var(--surface-active)] hover-hover:text-[var(--text-primary)]'
190+
>
191+
<Trash2 className='size-[13px]' />
192+
</button>
193+
</Tooltip.Trigger>
194+
<Tooltip.Content side='top' sideOffset={4}>
195+
Remove from queue
196+
</Tooltip.Content>
197+
</Tooltip.Root>
198+
</>
199+
)}
200+
</div>
152201
</div>
153-
</div>
154-
))}
202+
)
203+
})}
155204
</div>
156205
)}
157206
</div>

apps/sim/app/workspace/[workspaceId]/home/home.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ export function Home({ chatId }: HomeProps = {}) {
141141
removeFromQueue,
142142
sendNow,
143143
editQueuedMessage,
144+
cancelQueueEdit,
145+
editingQueuedId,
146+
dispatchingHeadId,
144147
previewSession,
145148
genericResourceData,
146149
getCurrentRequestId,
@@ -349,9 +352,12 @@ export function Home({ chatId }: HomeProps = {}) {
349352
onSubmit={handleSubmit}
350353
onStopGeneration={handleStopGeneration}
351354
messageQueue={messageQueue}
355+
editingQueuedId={editingQueuedId}
356+
dispatchingHeadId={dispatchingHeadId}
352357
onRemoveQueuedMessage={removeFromQueue}
353358
onSendQueuedMessage={sendNow}
354359
onEditQueuedMessage={editQueuedMessage}
360+
onCancelQueueEdit={cancelQueueEdit}
355361
userId={session?.user?.id}
356362
chatId={resolvedChatId}
357363
onContextAdd={handleContextAdd}

0 commit comments

Comments
 (0)