diff --git a/static/src/components/chat/MinimalBlocks.vue b/static/src/components/chat/MinimalBlocks.vue
index e02f922..e249589 100644
--- a/static/src/components/chat/MinimalBlocks.vue
+++ b/static/src/components/chat/MinimalBlocks.vue
@@ -34,7 +34,9 @@
- {{ getSummaryPreview(group.actions) }}
+ {{ getSummaryLineText(group.actions, group.id) }}
@@ -208,7 +210,6 @@ const personalizationStore = usePersonalizationStore();
const expandedGroups = ref(new Set());
const thinkingRefs = new Map();
const summaryLoaders = new Map();
-const SUMMARY_PREVIEW_MAX_CHARS = 25;
const TOOL_REEL_ITEM_HEIGHT = 26;
const TOOL_REEL_INTERVAL_MS = 1450;
const TOOL_REEL_ROLL_MS = 520;
@@ -230,14 +231,6 @@ const toolReelStates = reactive>({});
const toolReelIntervals = new Map();
const toolReelTimeouts = new Map();
-const truncateSummaryPreview = (raw: string) => {
- const text = typeof raw === 'string' ? raw : '';
- if (text.length > SUMMARY_PREVIEW_MAX_CHARS) {
- return `${text.slice(0, SUMMARY_PREVIEW_MAX_CHARS)}...`;
- }
- return text;
-};
-
const getAnimatedSummaryChars = (text: string) => Array.from(text || '');
const getFirstLine = (raw: string) => {
@@ -254,7 +247,7 @@ const getToolSummaryText = (action: Action) => {
const intentText = tool.intent_rendered || tool.intent_full || '';
if (intentEnabled && intentText) {
- return truncateSummaryPreview(getFirstLine(intentText));
+ return getFirstLine(intentText);
}
if (tool.status === 'preparing') {
@@ -426,10 +419,13 @@ const finishToolReel = (groupId: string, actions: Action[]) => {
}, TOOL_REEL_ROLL_MS);
pushToolReelTimeout(groupId, settleTimeout);
- const cleanupTimeout = window.setTimeout(() => {
- clearToolReelTimers(groupId);
- delete toolReelStates[groupId];
- }, TOOL_REEL_ROLL_MS + TOOL_REEL_SETTLE_MS + 260);
+ const cleanupTimeout = window.setTimeout(
+ () => {
+ clearToolReelTimers(groupId);
+ delete toolReelStates[groupId];
+ },
+ TOOL_REEL_ROLL_MS + TOOL_REEL_SETTLE_MS + 260
+ );
pushToolReelTimeout(groupId, cleanupTimeout);
});
};
@@ -438,7 +434,11 @@ const syncToolReels = () => {
const activeGroups = new Set();
blockGroups.value.forEach((group) => {
- if (group.type !== 'summary' || !group.actions || !shouldShowToolReel(group.actions, group.id)) {
+ if (
+ group.type !== 'summary' ||
+ !group.actions ||
+ !shouldShowToolReel(group.actions, group.id)
+ ) {
return;
}
@@ -584,22 +584,64 @@ const blockGroups = computed(() => {
return groups;
});
-// 获取摘要组的预览文本(始终显示最新的思考/工具)
-const getSummaryPreview = (actions: Action[]) => {
- // 优先返回正在 streaming 的最后一个
+type ToolCategory = 'read' | 'command' | 'edit' | 'search' | 'webpage' | 'other';
+
+const TOOL_CATEGORY_MAP: Record = {
+ // 读取文件
+ read_file: 'read',
+ read_skill: 'read',
+
+ // 运行指令
+ run_command: 'command',
+ terminal_session: 'command',
+ terminal_input: 'command',
+ terminal_snapshot: 'command',
+
+ // 编辑文件
+ create_file: 'edit',
+ write_file: 'edit',
+ edit_file: 'edit',
+ delete_file: 'edit',
+ rename_file: 'edit',
+ create_folder: 'edit',
+ save_webpage: 'edit',
+
+ // 搜索
+ web_search: 'search',
+
+ // 查看网页
+ extract_webpage: 'webpage'
+};
+
+const CATEGORY_LABELS: Record string> = {
+ read: (n) => `读取了 ${n} 个文件`,
+ command: (n) => `运行了 ${n} 个指令`,
+ edit: (n) => `编辑了 ${n} 次文件`,
+ search: (n) => `搜索了 ${n} 次`,
+ webpage: (n) => `查看了 ${n} 次网页`,
+ other: (n) => `执行了 ${n} 次其他操作`
+};
+
+const CATEGORY_ORDER: ToolCategory[] = ['read', 'command', 'edit', 'search', 'webpage', 'other'];
+
+const getToolCategory = (action: Action): ToolCategory => {
+ const name = action.tool?.name;
+ if (typeof name !== 'string') return 'other';
+ return TOOL_CATEGORY_MAP[name] || 'other';
+};
+
+// 运行中:显示当前最新步骤的意图或状态(单行,由 CSS 截断)
+const getRunningSummaryText = (actions: Action[]): string => {
const streamingActions = actions.filter((a) => a.streaming);
let currentStep;
if (streamingActions.length > 0) {
- // 如果有 streaming 的,返回最后一个 streaming 的
currentStep = streamingActions[streamingActions.length - 1];
} else {
- // 如果没有 streaming 的(全部完成了),优先返回最后一个工具
const toolActions = actions.filter((a) => a.type === 'tool');
if (toolActions.length > 0) {
currentStep = toolActions[toolActions.length - 1];
} else {
- // 如果没有工具,返回最后一个思考
currentStep = actions[actions.length - 1];
}
}
@@ -607,13 +649,11 @@ const getSummaryPreview = (actions: Action[]) => {
if (!currentStep) return '';
if (currentStep.type === 'thinking') {
- // 思考内容:最多显示到第一个换行,且不超过25字符(不含...)
const content = currentStep.content || '';
- const firstLineEnd = content.indexOf('\n');
- const textToShow = firstLineEnd > 0 ? content.substring(0, firstLineEnd) : content;
- return truncateSummaryPreview(textToShow);
- } else if (currentStep.type === 'tool') {
- // 工具:优先显示 intent_rendered 或 intent_full
+ return getFirstLine(content);
+ }
+
+ if (currentStep.type === 'tool') {
const tool = currentStep.tool;
if (!tool) return '';
@@ -621,13 +661,9 @@ const getSummaryPreview = (actions: Action[]) => {
const intentText = tool.intent_rendered || tool.intent_full || '';
if (intentEnabled && intentText) {
- // 开启intent模式且有intent时,只显示intent
- const firstLineEnd = intentText.indexOf('\n');
- const textToShow = firstLineEnd > 0 ? intentText.substring(0, firstLineEnd) : intentText;
- return truncateSummaryPreview(textToShow);
+ return getFirstLine(intentText);
}
- // 没有intent或未开启intent模式时显示状态
if (tool.status === 'preparing') {
return `准备调用 ${tool.name || '工具'}...`;
}
@@ -635,15 +671,64 @@ const getSummaryPreview = (actions: Action[]) => {
return `正在调用 ${tool.name || '工具'}...`;
}
if (tool.status === 'completed') {
- return truncateSummaryPreview(tool.display_name || tool.name || '工具执行完成');
+ return tool.display_name || tool.name || '工具执行完成';
}
- return currentStep.streaming
- ? '正在执行工具...'
- : truncateSummaryPreview(tool.display_name || tool.name || '工具执行完成');
+ return currentStep.streaming ? '正在执行工具...' : tool.display_name || tool.name || '执行工具';
}
+
return '';
};
+// 完成后:汇总本组所有工具的执行次数
+const getCompletedSummaryText = (actions: Action[]): string => {
+ const toolActions = actions.filter((a) => a.type === 'tool');
+
+ if (toolActions.length === 0) {
+ // 没有工具时回退到最后一条思考的单行内容
+ const lastThinking = actions.filter((a) => a.type === 'thinking').pop();
+ if (lastThinking) {
+ return getFirstLine(lastThinking.content || '');
+ }
+ return '';
+ }
+
+ // 只有一个工具时,直接显示该工具的 intent/名称
+ if (toolActions.length === 1) {
+ return getToolSummaryText(toolActions[0]);
+ }
+
+ const counts: Record = {
+ read: 0,
+ command: 0,
+ edit: 0,
+ search: 0,
+ webpage: 0,
+ other: 0
+ };
+
+ toolActions.forEach((action) => {
+ counts[getToolCategory(action)]++;
+ });
+
+ const parts: string[] = [];
+ CATEGORY_ORDER.forEach((category) => {
+ const count = counts[category];
+ if (count > 0) {
+ parts.push(CATEGORY_LABELS[category](count));
+ }
+ });
+
+ return parts.join(',');
+};
+
+// 摘要行统一入口:运行中显示实时进度,完成后显示统计总结
+const getSummaryLineText = (actions: Action[], groupId: string): string => {
+ if (isSummaryRunning(actions, groupId)) {
+ return getRunningSummaryText(actions);
+ }
+ return getCompletedSummaryText(actions);
+};
+
// 判断摘要组是否正在运行(显示加载动画还是对勾)
const isSummaryRunning = (actions: Action[], groupId: string) => {
if (!props.conversationRunning || !props.isLatestMessage) {
@@ -890,10 +975,11 @@ onBeforeUnmount(() => {
.summary-preview {
display: block;
- white-space: normal;
- overflow-wrap: anywhere;
- word-break: break-word;
+ white-space: nowrap;
+ overflow: hidden;
color: inherit;
+ mask-image: linear-gradient(to right, black 85%, transparent 100%);
+ -webkit-mask-image: linear-gradient(to right, black 85%, transparent 100%);
}
.summary-tool-reel-window {
@@ -1090,8 +1176,7 @@ onBeforeUnmount(() => {
margin-top: calc((1em - 1.7em) / 2);
/* 滚动条样式(参考 git 状态侧边栏文件列表,滚动槽透明贴合灰色底色) */
scrollbar-width: thin; /* Firefox */
- scrollbar-color: color-mix(in srgb, var(--claude-text-secondary) 55%, transparent)
- transparent;
+ scrollbar-color: color-mix(in srgb, var(--claude-text-secondary) 55%, transparent) transparent;
}
.thinking-content::-webkit-scrollbar {