perf: collapse long session messages to reduce text layout cost

Messages over 3000 characters are now truncated to 1500 characters by
default, with an expand/collapse toggle. This avoids expensive browser
text layout for large AI responses containing code or tool output.
This commit is contained in:
Jason
2026-04-14 16:16:57 +08:00
parent 7ae89e9106
commit 0383c13e66
4 changed files with 49 additions and 7 deletions
+40 -4
View File
@@ -1,5 +1,5 @@
import { memo } from "react";
import { Copy } from "lucide-react";
import { memo, useState } from "react";
import { ChevronDown, ChevronUp, Copy } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
@@ -17,6 +17,9 @@ import {
highlightText,
} from "./utils";
const COLLAPSE_THRESHOLD = 3000;
const COLLAPSED_LENGTH = 1500;
interface SessionMessageItemProps {
message: SessionMessage;
isActive: boolean;
@@ -31,6 +34,13 @@ export const SessionMessageItem = memo(function SessionMessageItem({
onCopy,
}: SessionMessageItemProps) {
const { t } = useTranslation();
const [expanded, setExpanded] = useState(false);
const isLong = message.content.length > COLLAPSE_THRESHOLD;
const displayContent =
isLong && !expanded
? message.content.slice(0, COLLAPSED_LENGTH) + "…"
: message.content;
return (
<div
@@ -73,9 +83,35 @@ export const SessionMessageItem = memo(function SessionMessageItem({
</div>
<div className="whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-sm leading-relaxed min-w-0">
{searchQuery
? highlightText(message.content, searchQuery)
: message.content}
? highlightText(displayContent, searchQuery)
: displayContent}
</div>
{isLong && (
<button
type="button"
onClick={() => setExpanded((v) => !v)}
className="flex items-center gap-1 mt-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
{expanded ? (
<>
<ChevronUp className="size-3" />
{t("sessionManager.collapseContent", {
defaultValue: "收起",
})}
</>
) : (
<>
<ChevronDown className="size-3" />
{t("sessionManager.expandContent", {
defaultValue: "展开完整内容",
})}
<span className="text-muted-foreground/60">
({Math.round(message.content.length / 1000)}k)
</span>
</>
)}
</button>
)}
</div>
);
});
+3 -1
View File
@@ -725,7 +725,9 @@
"copyCommand": "Copy Command",
"copyMessage": "Copy Message",
"messageCopied": "Message copied",
"conversationHistory": "Conversation History"
"conversationHistory": "Conversation History",
"expandContent": "Expand full content",
"collapseContent": "Collapse"
},
"console": {
"providerSwitchReceived": "Received provider switch event:",
+3 -1
View File
@@ -725,7 +725,9 @@
"copyCommand": "コマンドをコピー",
"copyMessage": "メッセージをコピー",
"messageCopied": "メッセージがコピーされました",
"conversationHistory": "会話履歴"
"conversationHistory": "会話履歴",
"expandContent": "全文を表示",
"collapseContent": "折りたたむ"
},
"console": {
"providerSwitchReceived": "プロバイダー切り替えイベントを受信:",
+3 -1
View File
@@ -725,7 +725,9 @@
"copyCommand": "复制命令",
"copyMessage": "复制消息",
"messageCopied": "已复制消息内容",
"conversationHistory": "对话记录"
"conversationHistory": "对话记录",
"expandContent": "展开完整内容",
"collapseContent": "收起"
},
"console": {
"providerSwitchReceived": "收到供应商切换事件:",