diff --git a/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx b/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx index 77607befa9..5cdd0523a6 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx @@ -10,6 +10,7 @@ import { ModalContent, ModalFooter, ModalHeader, + TagIcon, Textarea, ThumbsDown, ThumbsUp, @@ -46,13 +47,16 @@ interface MessageActionsProps { content: string chatId?: string userQuery?: string + requestId?: string } -export function MessageActions({ content, chatId, userQuery }: MessageActionsProps) { +export function MessageActions({ content, chatId, userQuery, requestId }: MessageActionsProps) { const [copied, setCopied] = useState(false) + const [copiedRequestId, setCopiedRequestId] = useState(false) const [pendingFeedback, setPendingFeedback] = useState<'up' | 'down' | null>(null) const [feedbackText, setFeedbackText] = useState('') const resetTimeoutRef = useRef(null) + const requestIdTimeoutRef = useRef(null) const submitFeedback = useSubmitCopilotFeedback() useEffect(() => { @@ -60,6 +64,9 @@ export function MessageActions({ content, chatId, userQuery }: MessageActionsPro if (resetTimeoutRef.current !== null) { window.clearTimeout(resetTimeoutRef.current) } + if (requestIdTimeoutRef.current !== null) { + window.clearTimeout(requestIdTimeoutRef.current) + } } }, []) @@ -79,6 +86,20 @@ export function MessageActions({ content, chatId, userQuery }: MessageActionsPro } }, [content]) + const copyRequestId = useCallback(async () => { + if (!requestId) return + try { + await navigator.clipboard.writeText(requestId) + setCopiedRequestId(true) + if (requestIdTimeoutRef.current !== null) { + window.clearTimeout(requestIdTimeoutRef.current) + } + requestIdTimeoutRef.current = window.setTimeout(() => setCopiedRequestId(false), 1500) + } catch { + /* clipboard unavailable */ + } + }, [requestId]) + const handleFeedbackClick = useCallback( (type: 'up' | 'down') => { if (chatId && userQuery) { @@ -144,6 +165,21 @@ export function MessageActions({ content, chatId, userQuery }: MessageActionsPro > + {requestId && ( + + )} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx index fe8510c199..527b35ebb3 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx @@ -182,6 +182,7 @@ export function MothershipChat({ content={msg.content} chatId={chatId} userQuery={precedingUserMsg?.content} + requestId={msg.requestId} /> )} diff --git a/apps/sim/lib/copilot/request/go/stream.ts b/apps/sim/lib/copilot/request/go/stream.ts index 707bf192d6..7953c2b3f9 100644 --- a/apps/sim/lib/copilot/request/go/stream.ts +++ b/apps/sim/lib/copilot/request/go/stream.ts @@ -158,6 +158,10 @@ export interface StreamLoopOptions extends OrchestratorOptions { * Return true to skip the default handler for this event. */ onBeforeDispatch?: (event: StreamEvent, context: StreamingContext) => boolean | undefined + /** + * Called when the Go backend's trace ID (go_trace_id) is first received via SSE. + */ + onGoTraceId?: (goTraceId: string) => void } /** @@ -234,8 +238,12 @@ export async function runStreamLoop( const streamEvent = eventToStreamEvent(raw) if (raw.trace?.requestId) { + const prev = context.requestId context.requestId = raw.trace.requestId context.trace.setGoTraceId(raw.trace.requestId) + if (raw.trace.requestId !== prev) { + options.onGoTraceId?.(raw.trace.requestId) + } } if (shouldSkipToolCallEvent(streamEvent) || shouldSkipToolResultEvent(streamEvent)) { diff --git a/apps/sim/lib/copilot/request/lifecycle/run.ts b/apps/sim/lib/copilot/request/lifecycle/run.ts index 0da134c1d5..5958b21eb1 100644 --- a/apps/sim/lib/copilot/request/lifecycle/run.ts +++ b/apps/sim/lib/copilot/request/lifecycle/run.ts @@ -43,6 +43,7 @@ export interface CopilotLifecycleOptions extends OrchestratorOptions { goRoute?: string trace?: TraceCollector simRequestId?: string + onGoTraceId?: (goTraceId: string) => void } export async function runCopilotLifecycle( diff --git a/apps/sim/lib/copilot/request/lifecycle/start.ts b/apps/sim/lib/copilot/request/lifecycle/start.ts index 7654d03112..ed006a9898 100644 --- a/apps/sim/lib/copilot/request/lifecycle/start.ts +++ b/apps/sim/lib/copilot/request/lifecycle/start.ts @@ -143,6 +143,9 @@ export function createSSEStream(params: StreamingOrchestrationParams): ReadableS trace: collector, simRequestId: requestId, abortSignal: abortController.signal, + onGoTraceId: (goTraceId) => { + publisher.updateRequestId(goTraceId) + }, onEvent: async (event) => { await publisher.publish(event) }, diff --git a/apps/sim/lib/copilot/request/session/writer.ts b/apps/sim/lib/copilot/request/session/writer.ts index 5e330ccbf1..c23f813327 100644 --- a/apps/sim/lib/copilot/request/session/writer.ts +++ b/apps/sim/lib/copilot/request/session/writer.ts @@ -22,7 +22,7 @@ export interface StreamWriterOptions { export class StreamWriter { private readonly streamId: string private readonly chatId: string | undefined - private readonly requestId: string + private requestId: string private readonly keepaliveMs: number private readonly flushIntervalMs: number private readonly flushMaxBatch: number @@ -55,6 +55,10 @@ export class StreamWriter { return this._sawComplete } + updateRequestId(id: string): void { + this.requestId = id + } + attach(controller: ReadableStreamDefaultController): void { this.controller = controller }