From 2a45262fa2973d6a3413f14d244f99a840275a99 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 21 May 2026 20:42:32 +0700 Subject: [PATCH] fix: truncate search results (#25566) 1. truncates search results 2. display a loading spinner when retrieving new search results when existing results are displayed 3. Improve dialog resizing behavior --- .../dialogs/ChatSearchDialog.stories.tsx | 96 ++++++++++++++++++- .../ChatsSidebar/dialogs/ChatSearchDialog.tsx | 20 +++- .../ChatsSidebar/dialogs/ChatSearchInput.tsx | 2 +- .../dialogs/ChatSearchResults.tsx | 36 +++++-- 4 files changed, 140 insertions(+), 14 deletions(-) diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchDialog.stories.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchDialog.stories.tsx index 02b569f52a..523c0f6266 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchDialog.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchDialog.stories.tsx @@ -58,6 +58,22 @@ const mockChats: Chat[] = [ }, }, ]; +const overflowMockChats: Chat[] = [ + { + ...mockChat, + id: "chat-long-1", + title: + "Review this PR and respond to every inline comment with detailed notes about selected row behavior in Table.tsx", + last_turn_summary: + "Posted review on PR #25069 with 10 inline comments covering 1 P2 issue, 4 P3s, and 2 observations.", + updated_at: "2026-05-20T09:30:00.000Z", + has_unread: false, + diff_status: { + ...mockDiffStatus, + chat_id: "chat-long-1", + }, + }, +]; const cappedMockChats: Chat[] = Array.from( { length: CHAT_SEARCH_LIMIT }, (_, index) => ({ @@ -146,6 +162,81 @@ export const Results: Story = { }, }; +export const RefreshingResults: Story = { + beforeEach: () => { + let requestCount = 0; + spyOn(API.experimental, "getChats").mockImplementation(() => { + requestCount += 1; + if (requestCount === 1) { + return Promise.resolve(mockChats); + } + return new Promise((_resolve) => { + // Keep request pending to show the refresh indicator with stale results. + }); + }); + }, + play: async () => { + const body = within(document.body); + const searchInput = body.getByRole("combobox", { name: "Search chats" }); + + await userEvent.type(searchInput, "Fix"); + await expect( + await body.findByText("Fix race condition in auth middleware"), + ).toBeInTheDocument(); + // While the first query is in its steady state the inline refresh spinner + // must be absent. Without this assertion, the test would still pass if the + // spinner were always visible. + expect(body.queryByLabelText("Searching chats")).not.toBeInTheDocument(); + + await userEvent.clear(searchInput); + await userEvent.type(searchInput, "review"); + + await waitFor(() => { + expect(API.experimental.getChats).toHaveBeenCalledTimes(2); + }); + await expect(body.getByLabelText("Searching chats")).toBeInTheDocument(); + await expect( + body.getByText("Fix race condition in auth middleware"), + ).toBeInTheDocument(); + }, +}; + +export const OverflowResults: Story = { + beforeEach: () => { + spyOn(API.experimental, "getChats").mockResolvedValue(overflowMockChats); + }, + play: async () => { + const body = within(document.body); + await userEvent.type( + body.getByRole("combobox", { name: "Search chats" }), + "review", + ); + await waitFor(() => { + expect(API.experimental.getChats).toHaveBeenCalledWith({ + limit: CHAT_SEARCH_LIMIT, + q: 'title:"review"', + }); + }); + + const result = await body.findByRole("option", { + name: /Review this PR and respond/i, + }); + const summary = await body.findByText(/Posted review on PR #25069/i); + const dialog = result.closest('[role="dialog"]'); + if (!dialog) { + throw new Error("Expected search result to render in a dialog"); + } + + const dialogRight = Math.ceil(dialog.getBoundingClientRect().right); + expect(Math.ceil(result.getBoundingClientRect().right)).toBeLessThanOrEqual( + dialogRight, + ); + expect( + Math.ceil(summary.getBoundingClientRect().right), + ).toBeLessThanOrEqual(dialogRight); + }, +}; + export const CappedResults: Story = { beforeEach: () => { spyOn(API.experimental, "getChats").mockResolvedValue(cappedMockChats); @@ -165,8 +256,9 @@ export const CappedResults: Story = { await expect( await body.findByText( (_content, element) => - element?.textContent?.replace(/\s+/g, " ").trim() === - `Showing first ${CHAT_SEARCH_LIMIT} results.`, + element?.tagName === "P" && + element.textContent?.replace(/\s+/g, " ").trim() === + `Showing first ${CHAT_SEARCH_LIMIT} results.`, ), ).toBeInTheDocument(); }, diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchDialog.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchDialog.tsx index e7cb5c3bbe..912fe40271 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchDialog.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchDialog.tsx @@ -27,7 +27,19 @@ export const ChatSearchDialog: FC = ({ return ( { event.preventDefault(); @@ -92,6 +104,11 @@ const ChatSearchDialogContent: FC = ({ hasQuery && (searchQuery.isLoading || (searchQuery.isFetching && (searchQuery.data?.length ?? 0) === 0)); + const isRefreshing = + hasQuery && + searchQuery.isFetching && + searchQuery.isPlaceholderData && + !showResultsLoading; const handleInputKeyDown: KeyboardEventHandler = ( event, ) => { @@ -149,6 +166,7 @@ const ChatSearchDialogContent: FC = ({ listboxId={listboxId} selectedChatIndex={safeSelectedChatIndex} showLoading={showResultsLoading} + isRefreshing={isRefreshing} onSelectChat={closeDialog} /> diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchInput.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchInput.tsx index 265d1ac7db..9b07f591d9 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchInput.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchInput.tsx @@ -27,7 +27,7 @@ export const ChatSearchInput: FC = ({ onKeyDown, }) => { return ( -
+
void; }; @@ -28,6 +30,7 @@ export const ChatSearchResults: FC = ({ listboxId, selectedChatIndex, showLoading, + isRefreshing, onSelectChat, }) => { if (error) { @@ -68,9 +71,22 @@ export const ChatSearchResults: FC = ({ return (
-

{resultSummary}

+

+ {resultSummary} + {isRefreshing && ( + + )} +

= ({ to={{ pathname: `/agents/${chat.id}`, search: location.search }} onClick={onSelect} className={cn( - "flex items-start gap-2 rounded-md px-1.5 py-1 text-content-secondary no-underline hover:bg-surface-tertiary/40 hover:text-content-primary", + "grid w-full min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-2 rounded-md px-1.5 py-1 text-content-secondary no-underline hover:bg-surface-tertiary/40 hover:text-content-primary", isSelected && "bg-surface-tertiary/40 text-content-primary", )} > -
-
- - {chat.title} - +
+
+ {chat.title}
-
+
{hasLineStats && ( +{additions} @@ -206,7 +220,9 @@ const ChatSearchResultRow: FC = ({ )} - {subtitle} + + {subtitle} +