Skip to content

Commit 168cd58

Browse files
Sg312icecrasher321
andauthored
feat(mothership): request ids (#3645)
* Include rid * Persist rid * fix ui * address comments * update types --------- Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
1 parent 5f89c71 commit 168cd58

File tree

17 files changed

+151
-5
lines changed

17 files changed

+151
-5
lines changed

apps/sim/app/api/mothership/chat/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ export async function POST(req: NextRequest) {
279279
role: 'assistant' as const,
280280
content: result.content,
281281
timestamp: new Date().toISOString(),
282+
...(result.requestId ? { requestId: result.requestId } : {}),
282283
}
283284
if (result.toolCalls.length > 0) {
284285
assistantMessage.toolCalls = result.toolCalls

apps/sim/app/workspace/[workspaceId]/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { ErrorState, type ErrorStateProps } from './error'
22
export { InlineRenameInput } from './inline-rename-input'
3+
export { MessageActions } from './message-actions'
34
export { ownerCell } from './resource/components/owner-cell/owner-cell'
45
export type {
56
BreadcrumbEditing,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { MessageActions } from './message-actions'
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
'use client'
2+
3+
import { useCallback, useEffect, useRef, useState } from 'react'
4+
import { Check, Copy, Ellipsis, Hash } from 'lucide-react'
5+
import {
6+
DropdownMenu,
7+
DropdownMenuContent,
8+
DropdownMenuItem,
9+
DropdownMenuTrigger,
10+
} from '@/components/emcn'
11+
12+
interface MessageActionsProps {
13+
content: string
14+
requestId?: string
15+
}
16+
17+
export function MessageActions({ content, requestId }: MessageActionsProps) {
18+
const [copied, setCopied] = useState<'message' | 'request' | null>(null)
19+
const resetTimeoutRef = useRef<number | null>(null)
20+
21+
useEffect(() => {
22+
return () => {
23+
if (resetTimeoutRef.current !== null) {
24+
window.clearTimeout(resetTimeoutRef.current)
25+
}
26+
}
27+
}, [])
28+
29+
const copyToClipboard = useCallback(async (text: string, type: 'message' | 'request') => {
30+
try {
31+
await navigator.clipboard.writeText(text)
32+
setCopied(type)
33+
if (resetTimeoutRef.current !== null) {
34+
window.clearTimeout(resetTimeoutRef.current)
35+
}
36+
resetTimeoutRef.current = window.setTimeout(() => setCopied(null), 1500)
37+
} catch {
38+
return
39+
}
40+
}, [])
41+
42+
if (!content && !requestId) {
43+
return null
44+
}
45+
46+
return (
47+
<DropdownMenu modal={false}>
48+
<DropdownMenuTrigger asChild>
49+
<button
50+
type='button'
51+
aria-label='More options'
52+
className='flex h-5 w-5 items-center justify-center rounded-sm text-[var(--text-icon)] opacity-0 transition-colors transition-opacity hover:bg-[var(--surface-3)] hover:text-[var(--text-primary)] focus-visible:opacity-100 focus-visible:outline-none group-hover/msg:opacity-100 data-[state=open]:opacity-100'
53+
onClick={(event) => event.stopPropagation()}
54+
>
55+
<Ellipsis className='h-3 w-3' strokeWidth={2} />
56+
</button>
57+
</DropdownMenuTrigger>
58+
<DropdownMenuContent align='end' side='top' sideOffset={4}>
59+
<DropdownMenuItem
60+
disabled={!content}
61+
onSelect={(event) => {
62+
event.stopPropagation()
63+
void copyToClipboard(content, 'message')
64+
}}
65+
>
66+
{copied === 'message' ? <Check /> : <Copy />}
67+
<span>Copy Message</span>
68+
</DropdownMenuItem>
69+
<DropdownMenuItem
70+
disabled={!requestId}
71+
onSelect={(event) => {
72+
event.stopPropagation()
73+
if (requestId) {
74+
void copyToClipboard(requestId, 'request')
75+
}
76+
}}
77+
>
78+
{copied === 'request' ? <Check /> : <Hash />}
79+
<span>Copy Request ID</span>
80+
</DropdownMenuItem>
81+
</DropdownMenuContent>
82+
</DropdownMenu>
83+
)
84+
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
LandingWorkflowSeedStorage,
1414
} from '@/lib/core/utils/browser-storage'
1515
import { persistImportedWorkflow } from '@/lib/workflows/operations/import-export'
16+
import { MessageActions } from '@/app/workspace/[workspaceId]/components'
1617
import { useChatHistory, useMarkTaskRead } from '@/hooks/queries/tasks'
1718
import type { ChatContext } from '@/stores/panel'
1819
import { useSidebarStore } from '@/stores/sidebar/store'
@@ -414,7 +415,12 @@ export function Home({ chatId }: HomeProps = {}) {
414415
const isLastMessage = index === messages.length - 1
415416

416417
return (
417-
<div key={msg.id} className='pb-4'>
418+
<div key={msg.id} className='group/msg relative pb-5'>
419+
{!isThisStreaming && (msg.content || msg.contentBlocks?.length) && (
420+
<div className='absolute right-0 bottom-0 z-10'>
421+
<MessageActions content={msg.content} requestId={msg.requestId} />
422+
</div>
423+
)}
418424
<MessageContent
419425
blocks={msg.contentBlocks || []}
420426
fallbackContent={msg.content}

apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ function mapStoredMessage(msg: TaskStoredMessage): ChatMessage {
142142
id: msg.id,
143143
role: msg.role,
144144
content: msg.content,
145+
...(msg.requestId ? { requestId: msg.requestId } : {}),
145146
}
146147

147148
const hasContentBlocks = Array.isArray(msg.contentBlocks) && msg.contentBlocks.length > 0
@@ -509,6 +510,7 @@ export function useChat(
509510
let activeSubagent: string | undefined
510511
let runningText = ''
511512
let lastContentSource: 'main' | 'subagent' | null = null
513+
let streamRequestId: string | undefined
512514

513515
streamingContentRef.current = ''
514516
streamingBlocksRef.current = []
@@ -526,14 +528,21 @@ export function useChat(
526528
const flush = () => {
527529
if (isStale()) return
528530
streamingBlocksRef.current = [...blocks]
529-
const snapshot = { content: runningText, contentBlocks: [...blocks] }
531+
const snapshot: Partial<ChatMessage> = {
532+
content: runningText,
533+
contentBlocks: [...blocks],
534+
}
535+
if (streamRequestId) snapshot.requestId = streamRequestId
530536
setMessages((prev) => {
531537
if (expectedGen !== undefined && streamGenRef.current !== expectedGen) return prev
532538
const idx = prev.findIndex((m) => m.id === assistantId)
533539
if (idx >= 0) {
534540
return prev.map((m) => (m.id === assistantId ? { ...m, ...snapshot } : m))
535541
}
536-
return [...prev, { id: assistantId, role: 'assistant' as const, ...snapshot }]
542+
return [
543+
...prev,
544+
{ id: assistantId, role: 'assistant' as const, content: '', ...snapshot },
545+
]
537546
})
538547
}
539548

@@ -597,6 +606,14 @@ export function useChat(
597606
}
598607
break
599608
}
609+
case 'request_id': {
610+
const rid = typeof parsed.data === 'string' ? parsed.data : undefined
611+
if (rid) {
612+
streamRequestId = rid
613+
flush()
614+
}
615+
break
616+
}
600617
case 'content': {
601618
const chunk = typeof parsed.data === 'string' ? parsed.data : (parsed.content ?? '')
602619
if (chunk) {

apps/sim/app/workspace/[workspaceId]/home/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface QueuedMessage {
3333
*/
3434
export type SSEEventType =
3535
| 'chat_id'
36+
| 'request_id'
3637
| 'title_updated'
3738
| 'content'
3839
| 'reasoning' // openai reasoning - render as thinking text
@@ -199,6 +200,7 @@ export interface ChatMessage {
199200
contentBlocks?: ContentBlock[]
200201
attachments?: ChatMessageAttachment[]
201202
contexts?: ChatMessageContext[]
203+
requestId?: string
202204
}
203205

204206
export const SUBAGENT_LABELS: Record<SubagentName, string> = {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { type FC, memo, useCallback, useMemo, useRef, useState } from 'react'
44
import { RotateCcw } from 'lucide-react'
55
import { Button } from '@/components/emcn'
6+
import { MessageActions } from '@/app/workspace/[workspaceId]/components'
67
import {
78
OptionsSelector,
89
parseSpecialTags,
@@ -409,10 +410,15 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
409410
if (isAssistant) {
410411
return (
411412
<div
412-
className={`w-full max-w-full flex-none overflow-hidden [max-width:var(--panel-max-width)] ${isDimmed ? 'opacity-40' : 'opacity-100'}`}
413+
className={`group/msg relative w-full max-w-full flex-none overflow-hidden [max-width:var(--panel-max-width)] ${isDimmed ? 'opacity-40' : 'opacity-100'}`}
413414
style={{ '--panel-max-width': `${panelWidth - 16}px` } as React.CSSProperties}
414415
>
415-
<div className='max-w-full space-y-[4px] px-[2px] pb-[4px]'>
416+
{!isStreaming && (message.content || message.contentBlocks?.length) && (
417+
<div className='absolute right-0 bottom-0 z-10'>
418+
<MessageActions content={message.content} requestId={message.requestId} />
419+
</div>
420+
)}
421+
<div className='max-w-full space-y-[4px] px-[2px] pb-5'>
416422
{/* Content blocks in chronological order */}
417423
{memoizedContentBlocks || (isStreaming && <div className='min-h-0' />)}
418424

apps/sim/hooks/queries/tasks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export interface TaskStoredMessage {
5454
id: string
5555
role: 'user' | 'assistant'
5656
content: string
57+
requestId?: string
5758
toolCalls?: TaskStoredToolCall[]
5859
contentBlocks?: TaskStoredContentBlock[]
5960
fileAttachments?: TaskStoredFileAttachment[]

apps/sim/lib/copilot/client-sse/handlers.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export function flushStreamingUpdates(set: StoreSet) {
9292
if (update) {
9393
return {
9494
...msg,
95+
requestId: update.requestId ?? msg.requestId,
9596
content: '',
9697
contentBlocks:
9798
update.contentBlocks.length > 0
@@ -129,6 +130,7 @@ export function updateStreamingMessage(set: StoreSet, context: ClientStreamingCo
129130
const newMessages = [...messages]
130131
newMessages[messages.length - 1] = {
131132
...lastMessage,
133+
requestId: lastMessageUpdate.requestId ?? lastMessage.requestId,
132134
content: '',
133135
contentBlocks:
134136
lastMessageUpdate.contentBlocks.length > 0
@@ -143,6 +145,7 @@ export function updateStreamingMessage(set: StoreSet, context: ClientStreamingCo
143145
if (update) {
144146
return {
145147
...msg,
148+
requestId: update.requestId ?? msg.requestId,
146149
content: '',
147150
contentBlocks:
148151
update.contentBlocks.length > 0
@@ -429,6 +432,12 @@ export const sseHandlers: Record<string, SSEHandler> = {
429432
writeActiveStreamToStorage(updatedStream)
430433
}
431434
},
435+
request_id: (data, context) => {
436+
const requestId = typeof data.data === 'string' ? data.data : undefined
437+
if (requestId) {
438+
context.requestId = requestId
439+
}
440+
},
432441
title_updated: (_data, _context, get, set) => {
433442
const title = _data.title
434443
if (!title) return

0 commit comments

Comments
 (0)