Skip to content

Commit b2d146c

Browse files
waleedlatif1claude
andauthored
improvement(mothership): message queueing for home chat (#3576)
* improvement(mothership): message queueing for home chat * fix(mothership): address PR review — move FileAttachmentForApi to types, defer onEditValueConsumed to effect, await sendMessage in sendNow * fix(mothership): replace updater side-effect with useEffect ref sync, move sendMessageRef to useLayoutEffect * fix(mothership): clear message queue on chat switch while sending * fix(mothership): remove stale isSending from handleKeyDown deps * fix(mothership): guard sendNow against double-click duplicate sends * fix(mothership): simplify queue callbacks — drop redundant deps and guard ref - Remove `setMessageQueue` from useCallback deps (stable setter, never changes) - Replace `sendNowProcessingRef` double-click guard with eager `messageQueueRef` update - Simplify `editQueuedMessage` with same eager-ref pattern for consistency Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(mothership): clear edit value on nav, stop queue drain on send failure - Reset editingInputValue when chatId changes so stale edit text doesn't leak into the next chat - Pass error flag to finalize so queue is cleared (not drained) when sendMessage fails — prevents cascading failures on auth expiry or rate limiting from silently consuming every queued message Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(mothership): eagerly update messageQueueRef in removeFromQueue Match the pattern used by sendNow and editQueuedMessage — update the ref synchronously so finalize's microtask cannot read a stale queue and drain a message the user just removed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(mothership): mark onSendNow as explicit fire-and-forget Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d06aa1d commit b2d146c

File tree

7 files changed

+437
-62
lines changed

7 files changed

+437
-62
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { MessageContent } from './message-content'
22
export { MothershipView } from './mothership-view'
3+
export { QueuedMessages } from './queued-messages'
34
export { TemplatePrompts } from './template-prompts'
45
export { UserInput } from './user-input'
56
export { UserMessageContent } from './user-message-content'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { QueuedMessages } from './queued-messages'
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { ArrowUp, ChevronDown, ChevronRight, Pencil, Trash2 } from 'lucide-react'
5+
import { Tooltip } from '@/components/emcn'
6+
import type { QueuedMessage } from '@/app/workspace/[workspaceId]/home/types'
7+
8+
interface QueuedMessagesProps {
9+
messageQueue: QueuedMessage[]
10+
onRemove: (id: string) => void
11+
onSendNow: (id: string) => Promise<void>
12+
onEdit: (id: string) => void
13+
}
14+
15+
export function QueuedMessages({ messageQueue, onRemove, onSendNow, onEdit }: QueuedMessagesProps) {
16+
const [isExpanded, setIsExpanded] = useState(true)
17+
18+
if (messageQueue.length === 0) return null
19+
20+
return (
21+
<div className='-mb-[12px] mx-[14px] overflow-hidden rounded-t-[16px] border border-[var(--border-1)] border-b-0 bg-[var(--surface-2)] pb-[12px] dark:bg-[var(--surface-3)]'>
22+
<button
23+
type='button'
24+
onClick={() => setIsExpanded(!isExpanded)}
25+
className='flex w-full items-center gap-[6px] px-[14px] py-[8px] transition-colors hover:bg-black/[0.03] dark:hover:bg-white/[0.03]'
26+
>
27+
{isExpanded ? (
28+
<ChevronDown className='h-[14px] w-[14px] text-[var(--text-tertiary)]' />
29+
) : (
30+
<ChevronRight className='h-[14px] w-[14px] text-[var(--text-tertiary)]' />
31+
)}
32+
<span className='font-medium text-[13px] text-[var(--text-secondary)]'>
33+
{messageQueue.length} Queued
34+
</span>
35+
</button>
36+
37+
{isExpanded && (
38+
<div>
39+
{messageQueue.map((msg) => (
40+
<div
41+
key={msg.id}
42+
className='flex items-center gap-[8px] px-[14px] py-[6px] transition-colors hover:bg-black/[0.03] dark:hover:bg-white/[0.03]'
43+
>
44+
<div className='flex h-[16px] w-[16px] shrink-0 items-center justify-center'>
45+
<div className='h-[10px] w-[10px] rounded-full border-[1.5px] border-[var(--text-tertiary)]/40' />
46+
</div>
47+
48+
<div className='min-w-0 flex-1'>
49+
<p className='truncate text-[13px] text-[var(--text-primary)]'>{msg.content}</p>
50+
</div>
51+
52+
<div className='flex shrink-0 items-center gap-[2px]'>
53+
<Tooltip.Root>
54+
<Tooltip.Trigger asChild>
55+
<button
56+
type='button'
57+
onClick={(e) => {
58+
e.stopPropagation()
59+
onEdit(msg.id)
60+
}}
61+
className='rounded-[6px] p-[5px] text-[var(--text-tertiary)] transition-colors hover:bg-black/[0.06] hover:text-[var(--text-primary)] dark:hover:bg-white/[0.06]'
62+
>
63+
<Pencil className='h-[13px] w-[13px]' />
64+
</button>
65+
</Tooltip.Trigger>
66+
<Tooltip.Content side='top' sideOffset={4}>
67+
Edit queued message
68+
</Tooltip.Content>
69+
</Tooltip.Root>
70+
71+
<Tooltip.Root>
72+
<Tooltip.Trigger asChild>
73+
<button
74+
type='button'
75+
onClick={(e) => {
76+
e.stopPropagation()
77+
void onSendNow(msg.id)
78+
}}
79+
className='rounded-[6px] p-[5px] text-[var(--text-tertiary)] transition-colors hover:bg-black/[0.06] hover:text-[var(--text-primary)] dark:hover:bg-white/[0.06]'
80+
>
81+
<ArrowUp className='h-[13px] w-[13px]' />
82+
</button>
83+
</Tooltip.Trigger>
84+
<Tooltip.Content side='top' sideOffset={4}>
85+
Send now
86+
</Tooltip.Content>
87+
</Tooltip.Root>
88+
89+
<Tooltip.Root>
90+
<Tooltip.Trigger asChild>
91+
<button
92+
type='button'
93+
onClick={(e) => {
94+
e.stopPropagation()
95+
onRemove(msg.id)
96+
}}
97+
className='rounded-[6px] p-[5px] text-[var(--text-tertiary)] transition-colors hover:bg-black/[0.06] hover:text-[var(--text-primary)] dark:hover:bg-white/[0.06]'
98+
>
99+
<Trash2 className='h-[13px] w-[13px]' />
100+
</button>
101+
</Tooltip.Trigger>
102+
<Tooltip.Content side='top' sideOffset={4}>
103+
Remove from queue
104+
</Tooltip.Content>
105+
</Tooltip.Root>
106+
</div>
107+
</div>
108+
))}
109+
</div>
110+
)}
111+
</div>
112+
)
113+
}

apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@ import { cn } from '@/lib/core/utils/cn'
6464
import { CHAT_ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
6565
import { useAvailableResources } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown'
6666
import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
67-
import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
67+
import type {
68+
FileAttachmentForApi,
69+
MothershipResource,
70+
} from '@/app/workspace/[workspaceId]/home/types'
6871
import {
6972
useContextManagement,
7073
useFileAttachments,
@@ -125,9 +128,17 @@ function autoResizeTextarea(e: React.FormEvent<HTMLTextAreaElement>, maxHeight:
125128
function mapResourceToContext(resource: MothershipResource): ChatContext {
126129
switch (resource.type) {
127130
case 'workflow':
128-
return { kind: 'workflow', workflowId: resource.id, label: resource.title }
131+
return {
132+
kind: 'workflow',
133+
workflowId: resource.id,
134+
label: resource.title,
135+
}
129136
case 'knowledgebase':
130-
return { kind: 'knowledge', knowledgeId: resource.id, label: resource.title }
137+
return {
138+
kind: 'knowledge',
139+
knowledgeId: resource.id,
140+
label: resource.title,
141+
}
131142
case 'table':
132143
return { kind: 'table', tableId: resource.id, label: resource.title }
133144
case 'file':
@@ -137,16 +148,12 @@ function mapResourceToContext(resource: MothershipResource): ChatContext {
137148
}
138149
}
139150

140-
export interface FileAttachmentForApi {
141-
id: string
142-
key: string
143-
filename: string
144-
media_type: string
145-
size: number
146-
}
151+
export type { FileAttachmentForApi } from '@/app/workspace/[workspaceId]/home/types'
147152

148153
interface UserInputProps {
149154
defaultValue?: string
155+
editValue?: string
156+
onEditValueConsumed?: () => void
150157
onSubmit: (
151158
text: string,
152159
fileAttachments?: FileAttachmentForApi[],
@@ -161,6 +168,8 @@ interface UserInputProps {
161168

162169
export function UserInput({
163170
defaultValue = '',
171+
editValue,
172+
onEditValueConsumed,
164173
onSubmit,
165174
isSending,
166175
onStopGeneration,
@@ -176,9 +185,27 @@ export function UserInput({
176185
const [plusMenuActiveIndex, setPlusMenuActiveIndex] = useState(0)
177186
const overlayRef = useRef<HTMLDivElement>(null)
178187

188+
const [prevDefaultValue, setPrevDefaultValue] = useState(defaultValue)
189+
if (defaultValue && defaultValue !== prevDefaultValue) {
190+
setPrevDefaultValue(defaultValue)
191+
setValue(defaultValue)
192+
} else if (!defaultValue && prevDefaultValue) {
193+
setPrevDefaultValue(defaultValue)
194+
}
195+
196+
const [prevEditValue, setPrevEditValue] = useState(editValue)
197+
if (editValue && editValue !== prevEditValue) {
198+
setPrevEditValue(editValue)
199+
setValue(editValue)
200+
} else if (!editValue && prevEditValue) {
201+
setPrevEditValue(editValue)
202+
}
203+
179204
useEffect(() => {
180-
if (defaultValue) setValue(defaultValue)
181-
}, [defaultValue])
205+
if (editValue) {
206+
onEditValueConsumed?.()
207+
}
208+
}, [editValue, onEditValueConsumed])
182209

183210
const animatedPlaceholder = useAnimatedPlaceholder(isInitialView)
184211
const placeholder = isInitialView ? animatedPlaceholder : 'Send message to Sim'
@@ -393,9 +420,7 @@ export function UserInput({
393420
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
394421
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
395422
e.preventDefault()
396-
if (!isSending) {
397-
handleSubmit()
398-
}
423+
handleSubmit()
399424
return
400425
}
401426

@@ -461,7 +486,7 @@ export function UserInput({
461486
}
462487
}
463488
},
464-
[handleSubmit, isSending, mentionTokensWithContext, value, textareaRef]
489+
[handleSubmit, mentionTokensWithContext, value, textareaRef]
465490
)
466491

467492
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
@@ -637,7 +662,9 @@ export function UserInput({
637662
<span
638663
key={`mention-${i}-${range.start}-${range.end}`}
639664
className='rounded-[5px] bg-[var(--surface-5)] py-[2px]'
640-
style={{ boxShadow: '-2px 0 0 var(--surface-5), 2px 0 0 var(--surface-5)' }}
665+
style={{
666+
boxShadow: '-2px 0 0 var(--surface-5), 2px 0 0 var(--surface-5)',
667+
}}
641668
>
642669
<span className='relative'>
643670
<span className='invisible'>{range.token.charAt(0)}</span>
@@ -662,7 +689,7 @@ export function UserInput({
662689
<div
663690
onClick={handleContainerClick}
664691
className={cn(
665-
'relative mx-auto w-full max-w-[42rem] cursor-text rounded-[20px] border border-[var(--border-1)] bg-[var(--white)] px-[10px] py-[8px] dark:bg-[var(--surface-4)]',
692+
'relative z-10 mx-auto w-full max-w-[42rem] cursor-text rounded-[20px] border border-[var(--border-1)] bg-[var(--white)] px-[10px] py-[8px] dark:bg-[var(--surface-4)]',
666693
isInitialView && 'shadow-sm'
667694
)}
668695
onDragEnter={files.handleDragEnter}
@@ -818,7 +845,11 @@ export function UserInput({
818845
)}
819846
onMouseEnter={() => setPlusMenuActiveIndex(index)}
820847
onClick={() => {
821-
handleResourceSelect({ type, id: item.id, title: item.name })
848+
handleResourceSelect({
849+
type,
850+
id: item.id,
851+
title: item.name,
852+
})
822853
setPlusMenuOpen(false)
823854
setPlusMenuSearch('')
824855
setPlusMenuActiveIndex(0)

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

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ import type { ChatContext } from '@/stores/panel'
1919
import {
2020
MessageContent,
2121
MothershipView,
22+
QueuedMessages,
2223
TemplatePrompts,
2324
UserInput,
2425
UserMessageContent,
2526
} from './components'
2627
import { PendingTagIndicator } from './components/message-content/components/special-tags'
27-
import type { FileAttachmentForApi } from './components/user-input/user-input'
2828
import { useAutoScroll, useChat } from './hooks'
29-
import type { MothershipResource, MothershipResourceType } from './types'
29+
import type { FileAttachmentForApi, MothershipResource, MothershipResourceType } from './types'
3030

3131
const logger = createLogger('Home')
3232

@@ -183,8 +183,29 @@ export function Home({ chatId }: HomeProps = {}) {
183183
addResource,
184184
removeResource,
185185
reorderResources,
186+
messageQueue,
187+
removeFromQueue,
188+
sendNow,
189+
editQueuedMessage,
186190
} = useChat(workspaceId, chatId, { onResourceEvent: handleResourceEvent })
187191

192+
const [editingInputValue, setEditingInputValue] = useState('')
193+
const clearEditingValue = useCallback(() => setEditingInputValue(''), [])
194+
195+
const handleEditQueuedMessage = useCallback(
196+
(id: string) => {
197+
const msg = editQueuedMessage(id)
198+
if (msg) {
199+
setEditingInputValue(msg.content)
200+
}
201+
},
202+
[editQueuedMessage]
203+
)
204+
205+
useEffect(() => {
206+
setEditingInputValue('')
207+
}, [chatId])
208+
188209
useEffect(() => {
189210
wasSendingRef.current = false
190211
if (resolvedChatId) markRead(resolvedChatId)
@@ -419,13 +440,21 @@ export function Home({ chatId }: HomeProps = {}) {
419440

420441
<div className='flex-shrink-0 px-[24px] pb-[16px]'>
421442
<div className='mx-auto max-w-[42rem]'>
443+
<QueuedMessages
444+
messageQueue={messageQueue}
445+
onRemove={removeFromQueue}
446+
onSendNow={sendNow}
447+
onEdit={handleEditQueuedMessage}
448+
/>
422449
<UserInput
423450
onSubmit={handleSubmit}
424451
isSending={isSending}
425452
onStopGeneration={stopGeneration}
426453
isInitialView={false}
427454
userId={session?.user?.id}
428455
onContextAdd={handleContextAdd}
456+
editValue={editingInputValue}
457+
onEditValueConsumed={clearEditingValue}
429458
/>
430459
</div>
431460
</div>

0 commit comments

Comments
 (0)