From f9fb9085acb3d1f6df6213246aa3a43200609251 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 10 Apr 2026 08:30:37 +0800 Subject: [PATCH] feat(session-manager): highlight search keywords in session titles and messages --- src/components/sessions/SessionItem.tsx | 7 +++++- .../sessions/SessionManagerPage.tsx | 2 ++ .../sessions/SessionMessageItem.tsx | 13 +++++++++-- src/components/sessions/utils.ts | 22 +++++++++++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/components/sessions/SessionItem.tsx b/src/components/sessions/SessionItem.tsx index dc2841c33..1cacc1405 100644 --- a/src/components/sessions/SessionItem.tsx +++ b/src/components/sessions/SessionItem.tsx @@ -15,6 +15,7 @@ import { getProviderIconName, getProviderLabel, getSessionKey, + highlightText, } from "./utils"; interface SessionItemProps { @@ -23,6 +24,7 @@ interface SessionItemProps { selectionMode: boolean; isChecked: boolean; isCheckDisabled?: boolean; + searchQuery?: string; onSelect: (key: string) => void; onToggleChecked: (checked: boolean) => void; } @@ -33,6 +35,7 @@ export function SessionItem({ selectionMode, isChecked, isCheckDisabled = false, + searchQuery, onSelect, onToggleChecked, }: SessionItemProps) { @@ -82,7 +85,9 @@ export function SessionItem({ {getProviderLabel(session.providerId, t)} - {title} + + {searchQuery ? highlightText(title, searchQuery) : title} + { if (el) messageRefs.current.set(index, el); }} diff --git a/src/components/sessions/SessionMessageItem.tsx b/src/components/sessions/SessionMessageItem.tsx index 2293241bf..1dd4d2b62 100644 --- a/src/components/sessions/SessionMessageItem.tsx +++ b/src/components/sessions/SessionMessageItem.tsx @@ -9,12 +9,18 @@ import { } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import type { SessionMessage } from "@/types"; -import { formatTimestamp, getRoleLabel, getRoleTone } from "./utils"; +import { + formatTimestamp, + getRoleLabel, + getRoleTone, + highlightText, +} from "./utils"; interface SessionMessageItemProps { message: SessionMessage; index: number; isActive: boolean; + searchQuery?: string; setRef: (el: HTMLDivElement | null) => void; onCopy: (content: string) => void; } @@ -22,6 +28,7 @@ interface SessionMessageItemProps { export function SessionMessageItem({ message, isActive, + searchQuery, setRef, onCopy, }: SessionMessageItemProps) { @@ -68,7 +75,9 @@ export function SessionMessageItem({ )}
- {message.content} + {searchQuery + ? highlightText(message.content, searchQuery) + : message.content}
); diff --git a/src/components/sessions/utils.ts b/src/components/sessions/utils.ts index 633d7136d..1c2c677a9 100644 --- a/src/components/sessions/utils.ts +++ b/src/components/sessions/utils.ts @@ -1,3 +1,5 @@ +import type { ReactNode } from "react"; +import { createElement } from "react"; import { SessionMeta } from "@/types"; export const getSessionKey = (session: SessionMeta) => @@ -78,3 +80,23 @@ export const formatSessionTitle = (session: SessionMeta) => { session.sessionId.slice(0, 8) ); }; + +export const highlightText = (text: string, query: string): ReactNode => { + if (!query) return text; + const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const parts = text.split(new RegExp(`(${escaped})`, "gi")); + if (parts.length === 1) return text; + return parts.map((part, i) => + i % 2 === 1 + ? createElement( + "mark", + { + key: i, + className: + "bg-yellow-200/60 dark:bg-yellow-500/30 text-inherit rounded-sm px-0.5", + }, + part, + ) + : part, + ); +};