diff --git a/packages/shared/src/types/search.ts b/packages/shared/src/types/search.ts index 6b9c408cede..145c5ba3151 100644 --- a/packages/shared/src/types/search.ts +++ b/packages/shared/src/types/search.ts @@ -41,6 +41,7 @@ export interface CompanySearchResult { snippets: CompanySearchSnippet[]; issue?: CompanySearchIssueSummary; updatedAt: string | null; + previewImageUrl: string | null; } export interface CompanySearchResponse { diff --git a/server/src/services/company-search.ts b/server/src/services/company-search.ts index ecb1eae0ee2..d7297786afd 100644 --- a/server/src/services/company-search.ts +++ b/server/src/services/company-search.ts @@ -109,6 +109,14 @@ function plainText(value: string | null | undefined) { .trim(); } +const MARKDOWN_IMAGE_PATTERN = /!\[[^\]]*\]\(\s*([^)\s]+)(?:\s+"[^"]*")?\s*\)/; + +function extractFirstImageUrl(value: string | null | undefined): string | null { + if (!value) return null; + const match = MARKDOWN_IMAGE_PATTERN.exec(value); + return match ? match[1] : null; +} + function findFirstMatchIndex(value: string, terms: string[]) { const lower = value.toLowerCase(); let best = -1; @@ -247,6 +255,10 @@ function issueResult(row: IssueSearchRow, prefix: string, normalizedQuery: strin projectId: row.projectId, updatedAt: iso(row.updatedAt)!, }; + const previewImageUrl = + extractFirstImageUrl(row.description) ?? + extractFirstImageUrl(row.commentSnippet) ?? + extractFirstImageUrl(row.documentSnippet); return { id: row.id, type: "issue", @@ -259,6 +271,7 @@ function issueResult(row: IssueSearchRow, prefix: string, normalizedQuery: strin snippets, issue, updatedAt: issue.updatedAt, + previewImageUrl, }; } @@ -643,6 +656,7 @@ export function companySearchService(db: Db) { snippet: snippet?.text ?? null, snippets: snippet ? [snippet] : [], updatedAt: iso(row.updatedAt), + previewImageUrl: null, }; }), ...(projectRows as SimpleSearchRow[]).map((row) => { @@ -659,6 +673,7 @@ export function companySearchService(db: Db) { snippet: snippet?.text ?? null, snippets: snippet ? [snippet] : [], updatedAt: iso(row.updatedAt), + previewImageUrl: null, }; }), ].sort((left, right) => { diff --git a/ui/src/components/search/SearchResultRow.tsx b/ui/src/components/search/SearchResultRow.tsx index a108aff30b8..858819444f1 100644 --- a/ui/src/components/search/SearchResultRow.tsx +++ b/ui/src/components/search/SearchResultRow.tsx @@ -51,7 +51,7 @@ export interface SearchResultRowProps { } const ROW_BASE = - "group flex items-start gap-3 px-4 transition-colors no-underline text-inherit hover:bg-muted/40"; + "group flex items-start gap-3 rounded-md px-3 transition-colors no-underline text-inherit hover:bg-muted/40"; function SearchResultRowImpl({ result, @@ -63,7 +63,7 @@ function SearchResultRowImpl({ return ( @@ -90,7 +90,7 @@ function SearchResultRowImpl({ return ( @@ -117,12 +117,13 @@ function SearchResultRowImpl({ const updated = formatRelativeTime(result.updatedAt ?? issue.updatedAt); const titleHighlights = result.snippets.find((snippet) => snippet.field === "title")?.highlights; const bodySnippets = result.snippets.filter((snippet) => snippet.field !== "title").slice(0, 2); + const previewImageUrl = result.previewImageUrl; return (
@@ -160,6 +161,15 @@ function SearchResultRowImpl({ {updated ? {updated} : null}
+ {previewImageUrl ? ( + + ) : null} ); } diff --git a/ui/src/pages/Search.tsx b/ui/src/pages/Search.tsx index 149f2bbbc58..858d9c806e8 100644 --- a/ui/src/pages/Search.tsx +++ b/ui/src/pages/Search.tsx @@ -577,20 +577,20 @@ function SearchTabContent({ if (!hasResults) return null; return ( -
-
+
+
{totalResults === 1 ? "1 result" : `${totalResults} results`} · sorted by relevance {isFetching ? Updating… : null}
-
+
{scope === "all" ? ( subgroups.map((group, groupIndex) => (
0 && "mt-3 border-t border-border")} + className={cn("flex flex-col", groupIndex > 0 && "mt-6")} > } - className="px-4 pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground" + className="pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground" /> -
+
{group.results.map((result) => ( )) ) : ( -
+
{subgroups .flatMap((group) => group.results) .map((result) => ( diff --git a/ui/storybook/stories/search.stories.tsx b/ui/storybook/stories/search.stories.tsx index f3abd9642ab..0054fba2841 100644 --- a/ui/storybook/stories/search.stories.tsx +++ b/ui/storybook/stories/search.stories.tsx @@ -62,6 +62,7 @@ function buildIssueResult(overrides: IssueResultOverrides): CompanySearchResult snippets: overrides.snippets ?? [], issue: baseIssue, updatedAt: baseIssue.updatedAt, + previewImageUrl: overrides.previewImageUrl ?? null, }; } @@ -112,6 +113,8 @@ const fixtureResults: CompanySearchResult[] = [ highlights: [{ start: 12, end: 16 }, { start: 26, end: 30 }], }, ], + previewImageUrl: + "data:image/svg+xml;utf8,chart", }), buildIssueResult({ id: "issue-3", @@ -154,6 +157,7 @@ const fixtureAgents: CompanySearchResult[] = storybookAgents.slice(0, 1).map((ag ] : [], updatedAt: new Date().toISOString(), + previewImageUrl: null, })); const fixtureProjects: CompanySearchResult[] = storybookProjects.slice(0, 1).map((project) => ({ @@ -176,6 +180,7 @@ const fixtureProjects: CompanySearchResult[] = storybookProjects.slice(0, 1).map ] : [], updatedAt: new Date().toISOString(), + previewImageUrl: null, })); const fixtureResponse: CompanySearchResponse = { @@ -261,8 +266,8 @@ function SearchPagePreview({
{state === "results" ? ( -
-
+
+
{response.results.length} results · sorted by relevance
@@ -273,9 +278,9 @@ function SearchPagePreview({ {fixtureResults.length} } - className="px-4 pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground" + className="pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground" /> -
+
{fixtureResults.map((result) => (
-
+
} - className="px-4 pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground" + className="pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground" /> -
+
{fixtureAgents.map((result) => ( ))}
-
+
} - className="px-4 pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground" + className="pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground" /> -
+
{fixtureProjects.map((result) => ( ))} @@ -572,7 +577,7 @@ function SearchStories() {
Search result row

Issue, agent, project rows

-
+
{fixtureResults.map((result) => (