From ea6e72fc7be278da564be090ff3d723f851e3196 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:46:10 +0200 Subject: [PATCH] fix: address remaining Codex findings after dev rebase Stamp agent ownership on streaming and final search snapshots. Compose the shared floating thought button. Preserve rebased group behavior, correct stale test selectors, and keep expansion callbacks outside state updaters. --- .../agents/__tests__/callbacks.spec.js | 30 +++++++++++++ api/server/controllers/agents/callbacks.js | 1 + api/server/services/Tools/search.js | 3 ++ api/server/services/Tools/search.spec.js | 31 +++++++++++++ .../Chat/Messages/Content/Parts/Reasoning.tsx | 8 +++- .../Chat/Messages/Content/Parts/Thinking.tsx | 13 +++--- .../Parts/__tests__/ThinkingButton.test.tsx | 45 ------------------- .../Chat/Messages/Content/ToolCall.tsx | 14 +++--- .../ContentParts.integration.test.tsx | 15 ++++--- .../Content/__tests__/ToolCallGroup.test.tsx | 7 +-- 10 files changed, 93 insertions(+), 74 deletions(-) delete mode 100644 client/src/components/Chat/Messages/Content/Parts/__tests__/ThinkingButton.test.tsx diff --git a/api/server/controllers/agents/__tests__/callbacks.spec.js b/api/server/controllers/agents/__tests__/callbacks.spec.js index 34bab04619..0a2a3a1bbf 100644 --- a/api/server/controllers/agents/__tests__/callbacks.spec.js +++ b/api/server/controllers/agents/__tests__/callbacks.spec.js @@ -468,6 +468,36 @@ describe('createToolEndCallback', () => { artifactPromises = []; }); + it('preserves separate owners for final search artifacts with repeated tool-call IDs', async () => { + const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); + for (const agentId of ['agent-a', 'agent-b']) { + await toolEndCallback( + { + output: { + tool_call_id: 'call_0', + artifact: { + [Tools.web_search]: { + turn: 0, + organic: [{ link: `https://example.com/${agentId}` }], + }, + }, + }, + }, + { run_id: 'run456', thread_id: 'thread789', agent_id: agentId }, + ); + } + const results = await Promise.all(artifactPromises); + expect( + results.map((attachment) => ({ + owner: attachment.agentId, + link: attachment[Tools.web_search].organic[0].link, + })), + ).toEqual([ + { owner: 'agent-a', link: 'https://example.com/agent-a' }, + { owner: 'agent-b', link: 'https://example.com/agent-b' }, + ]); + }); + describe('ui_resources artifact handling', () => { it('should process ui_resources artifact and return attachment when headers not sent', async () => { const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); diff --git a/api/server/controllers/agents/callbacks.js b/api/server/controllers/agents/callbacks.js index 7409f1c16b..505018fe71 100644 --- a/api/server/controllers/agents/callbacks.js +++ b/api/server/controllers/agents/callbacks.js @@ -1006,6 +1006,7 @@ function createToolEndCallback({ req, res, artifactPromises, streamId = null, jo type: Tools.web_search, messageId: metadata.run_id, toolCallId: output.tool_call_id, + agentId: metadata.agent_id, conversationId: metadata.thread_id, [Tools.web_search]: { ...output.artifact[Tools.web_search] }, }; diff --git a/api/server/services/Tools/search.js b/api/server/services/Tools/search.js index c6e031e9ee..12091db173 100644 --- a/api/server/services/Tools/search.js +++ b/api/server/services/Tools/search.js @@ -34,6 +34,7 @@ function createOnSearchResults(res, streamId = null, jobCreatedAt) { sourceMap: new Map(), searchResultData: undefined, toolCallId: undefined, + agentId: undefined, attachmentName: undefined, messageId: undefined, conversationId: undefined, @@ -83,6 +84,7 @@ function createOnSearchResults(res, streamId = null, jobCreatedAt) { } context.toolCallId = runnableConfig.toolCall.id; + context.agentId = runnableConfig.metadata.agent_id; context.messageId = runnableConfig.metadata.run_id; context.conversationId = runnableConfig.metadata.thread_id; context.attachmentName = `${runnableConfig.toolCall.name}_${context.toolCallId}_${nanoid()}`; @@ -132,6 +134,7 @@ function buildAttachment(context) { return { messageId: context.messageId, toolCallId: context.toolCallId, + agentId: context.agentId, conversationId: context.conversationId, name: context.attachmentName, type: Tools.web_search, diff --git a/api/server/services/Tools/search.spec.js b/api/server/services/Tools/search.spec.js index 8925d84062..beb6bfaa05 100644 --- a/api/server/services/Tools/search.spec.js +++ b/api/server/services/Tools/search.spec.js @@ -60,4 +60,35 @@ describe('createOnSearchResults', () => { ]); } }); + + it('keeps repeated search calls owned by their handoff agent through highlights', () => { + const snapshots = []; + const res = { + headersSent: true, + write: (event) => snapshots.push(JSON.parse(event.split('\ndata: ')[1])), + }; + for (const agentId of ['agent-a', 'agent-b']) { + const callbacks = createOnSearchResults(res); + callbacks.onSearchResults( + { + success: true, + data: { organic: [{ link: `https://example.com/${agentId}` }], topStories: [] }, + }, + { + metadata: { agent_id: agentId, run_id: 'response-1', thread_id: 'conversation-1' }, + toolCall: { id: 'call_0', name: 'web_search', turn: 0 }, + }, + ); + callbacks.onGetHighlights(`https://example.com/${agentId}`); + } + + expect(snapshots.map(({ agentId }) => agentId)).toEqual([ + 'agent-a', + 'agent-a', + 'agent-b', + 'agent-b', + ]); + expect(snapshots[1].web_search.organic[0].processed).toBe(true); + expect(snapshots[3].web_search.organic[0].processed).toBe(true); + }); }); diff --git a/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx b/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx index 9b46300939..a95572378e 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx @@ -5,7 +5,13 @@ import { Lightbulb, ChevronDown } from 'lucide-react'; import { ContentTypes } from 'librechat-data-provider'; import { Button, disclosureChevronVariants } from '@librechat/client'; import type { MouseEvent, FocusEvent } from 'react'; -import { ThinkingContent, ThinkingButton, ThinkingLabel, FloatingThinkingBar, useInViewport } from './Thinking'; +import { + ThinkingContent, + ThinkingButton, + ThinkingLabel, + FloatingThinkingBar, + useInViewport, +} from './Thinking'; import { useLocalize, useExpandCollapse, useLazyCollapseBody } from '~/hooks'; import useSmoothStreaming from '~/hooks/Messages/useSmoothStreaming'; import CopyButton from '~/components/Messages/Content/CopyButton'; diff --git a/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx b/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx index 68d061c3da..5b8bc23155 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx @@ -9,7 +9,7 @@ import { type MouseEvent, } from 'react'; import { useAtomValue } from 'jotai'; -import { Lightbulb, ChevronDown, ChevronUp } from 'lucide-react'; +import { Lightbulb, ChevronDown } from 'lucide-react'; import { Button, MorphIcon, TooltipAnchor } from '@librechat/client'; import { ChevronUp as ChevronUpNode, ChevronDown as ChevronDownNode } from 'lucide'; import type { FocusEvent, FC } from 'react'; @@ -255,24 +255,21 @@ export const FloatingThinkingBar = memo( - + } /> {content && ( diff --git a/client/src/components/Chat/Messages/Content/Parts/__tests__/ThinkingButton.test.tsx b/client/src/components/Chat/Messages/Content/Parts/__tests__/ThinkingButton.test.tsx deleted file mode 100644 index e352ba8a3f..0000000000 --- a/client/src/components/Chat/Messages/Content/Parts/__tests__/ThinkingButton.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { ThinkingButton } from '../Thinking'; - -jest.mock('jotai', () => ({ - ...jest.requireActual('jotai'), - useAtomValue: () => 'text-base', -})); - -jest.mock('~/hooks', () => ({ - useLocalize: () => (key: string) => key, -})); - -jest.mock('~/components/Messages/Content/CopyButton', () => ({ - __esModule: true, - default: ({ className }: { className?: string }) => ( -