feat(session-manager): highlight search keywords in session titles and messages

This commit is contained in:
Jason
2026-04-10 08:30:37 +08:00
parent 308314baac
commit f9fb9085ac
4 changed files with 41 additions and 3 deletions
+6 -1
View File
@@ -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)}
</TooltipContent>
</Tooltip>
<span className="text-sm font-medium truncate flex-1">{title}</span>
<span className="text-sm font-medium truncate flex-1">
{searchQuery ? highlightText(title, searchQuery) : title}
</span>
<ChevronRight
className={cn(
"size-4 text-muted-foreground/50 shrink-0 transition-transform",
@@ -769,6 +769,7 @@ export function SessionManagerPage({ appId }: { appId: string }) {
session={session}
isSelected={isSelected}
selectionMode={selectionMode}
searchQuery={search}
isChecked={selectedSessionKeys.has(
getSessionKey(session),
)}
@@ -1005,6 +1006,7 @@ export function SessionManagerPage({ appId }: { appId: string }) {
message={message}
index={index}
isActive={activeMessageIndex === index}
searchQuery={search}
setRef={(el) => {
if (el) messageRefs.current.set(index, el);
}}
+11 -2
View File
@@ -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({
)}
</div>
<div className="whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-sm leading-relaxed min-w-0">
{message.content}
{searchQuery
? highlightText(message.content, searchQuery)
: message.content}
</div>
</div>
);
+22
View File
@@ -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,
);
};