agent-Specialization/static/src/components/chat/MinimalBlocks.vue
JOJO b4030c386e refactor(frontend): 颜色 token 系统两层重构 + 三主题填色 + 去半透明
将前端配色从散落的硬编码/双命名体系(--claude-* / --theme-*)重构为
两层 token 结构(原始层 + 中性语义层),并按"经典抄 Claude、浅色抄
ChatGPT、深色自研"的定位完成三主题填色,同时建立 stylelint 防回退栏杆。
代码大量由 AI 生成的历史遗留配色问题(同语义多值、跨主题塌缩、强调色
撞文字色、fallback 第二真相源等)在此一并收口。

== 一、Token 结构重构(_tokens.scss)==
- 重写为两层:原始层 + 语义层;组件只引用中性语义名
  (--surface-* / --text-* / --border-* / --accent* / --state-* 等)。
- --claude-* / --theme-* 降级为兼容别名(41 个转发),迁移完成后删除。
- 三主题(classic / light / dark)+ 首屏回退(:root:not([data-theme]),
  与经典同值)全部补齐,结构对齐、不再缺项。

== 二、三主题填色 ==
- 经典 = Claude 官网亮色盘:暖奶油表面阶梯
  canvas #faf9f5 < soft #f5f0e8 < card #efe9de < cream-strong #e8e0d2
  < 纯白浮起;文字 #141413;hairline #e6dfd8;暖橙 primary #cc785c。
- 浅色 = ChatGPT 亮色盘:冷白 #ffffff / 侧栏 #f9f9f9 / 嵌套 #f3f3f3,
  中性灰文字,近黑 primary #181818。
- 深色 = 自研中性灰阶,本轮完全不动(原样保留)。
- 修复塌缩 bug:强调色在 light/dark 原被误填成灰(CTA 失去品牌色),
  现统一回品牌 primary;light 表面原全塌成 #ffffff(过于平面),
  现按灰阶拉开层次。
- 强调色克制(CTA-only voltage):primary 仅用于发送按钮/logo 等极少
  数处;hover / 选中 / 运行态一律走中性灰(运行态指示器从 amber 改灰)。
- 对话区背景从纯白改为暖奶油 canvas;个人空间下拉菜单、git 状态条、
  二级菜单等收口到统一语义色。

== 三、全面去半透明 ==
- 所有实体面板/二级菜单背景改为不透明(surface token 由半透明白改实色,
  --mobile-menu 三亮色主题改实色)。
- 移除已不透明面板上失效的 backdrop-filter 磨砂(移动端菜单、右键菜单、
  模型模式下拉等)。
- 遮罩 scrim(--overlay-scrim)与刻意玻璃质感装饰保留半透明。

== 四、块展开底色统一 ==
- 思考/工具块展开区底色统一为对话背景色:.collapsible-block 及其 header
  背景由 --surface-soft 改 transparent,与堆叠块(stacked-block)一致,
  消除"单块奶油底 vs 多块透明"的不一致。

== 五、布局修复 ==
- 工作区隐藏后残留空白:.workspace-panel--collapsed 补 flex-basis:0 +
  width/min-width:0,修正 flex-basis:auto 被子元素内容宽度撑开导致内联
  width:0 失效、collapsed 后仍占 ~300px 的问题。

== 六、防回退栏杆(新增)==
- 新增 .stylelintrc.cjs:三条规则拦截裸 hex / rgb()|hsl() 字面色 /
  var(--x, fallback) 兜底 / prefers-color-scheme;存量未清理文件用
  BASELINE_EXEMPT 临时豁免(现仅剩 5 个 admin 页面,本轮按用户要求不处理),
  清理一个移除一个、不再回退。
- package.json:build 脚本接入 stylelint(tsc → stylelint → vite),
  新增 lint:css 脚本及 stylelint/postcss-scss/postcss-html 依赖。
- 修复 25 处 \!important 笔误(zsh 历史扩展污染导致的无效 CSS)。

== 七、辅助脚本(新增 scripts/)==
- lint_check.py:Node 环境不可用时的 stylelint 替身(复刻三规则 + 括号
  校验,支持多行声明检测)。
- color_map.py:硬编码色 → token 反查,判定可映射/野色。
- strip_fallback.py:安全剥离已定义 token 的冗余 fallback。

== 八、文档 ==
- CLAUDE.md / AGENTS.md 前端设计规范更新:颜色规则扩写为三主题定位 +
  两层 token + 表面层次 + 强调色克制 + color-mix 派生 + stylelint 栏杆;
  新增"实体面板禁止半透明"规则;"禁止边缘光晕"补充"仅在用户明确允许
  或要求时才可使用"。

注:前端构建验证(npm run build / lint:css)由用户本机完成;AI 工具
上下文 Node v24 段错误无法运行 node/npm/vite/stylelint。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:20:58 +08:00

838 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="minimal-blocks-container">
<!-- 按真实顺序显示分组 -->
<template v-for="group in blockGroups" :key="group.id">
<!-- 摘要组thinking/tool -->
<div v-if="group.type === 'summary' && group.actions" class="summary-group">
<!-- 摘要行(只有文字,可点击展开) -->
<div
class="summary-line-text"
:class="{ running: isSummaryRunning(group.actions, group.id) }"
@click="toggleExpand(group.id)"
>
<div class="summary-content-wrapper">
<span class="summary-preview">
<template v-if="isSummaryRunning(group.actions, group.id)">
<span
v-for="(char, idx) in getAnimatedSummaryChars(getSummaryPreview(group.actions))"
:key="`${group.id}-${idx}`"
class="summary-char"
:style="{ animationDelay: `${(idx + 1) * 0.12}s` }"
>
{{ char === ' ' ? '\u00A0' : char }}
</span>
</template>
<template v-else>
{{ getSummaryPreview(group.actions) }}
</template>
</span>
</div>
<!-- 加载动画或完成图标 -->
<div class="summary-status-icon">
<component
v-if="isSummaryRunning(group.actions, group.id)"
:is="getSummaryLoader(group.id)"
/>
<svg
v-else
class="check-icon"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" />
<polyline points="7.5 12 10.5 15 16.5 9" />
</svg>
</div>
</div>
<!-- 步骤容器(展开时显示所有步骤) -->
<div class="steps-container" :class="{ show: expandedGroups.has(group.id) }">
<div class="steps-wrapper">
<div v-for="step in getSummarySteps(group.actions)" :key="step.id" class="step-item">
<div class="step-timeline">
<span
class="step-icon icon icon-sm"
:style="getStepIconStyle(step)"
aria-hidden="true"
></span>
<div class="step-line"></div>
</div>
<div class="step-content">
<div v-if="step.type === 'thinking'" class="step-body">
<div
class="thinking-content"
:ref="(el) => registerThinking(step.id, el)"
@scroll="handleScroll(step.id, $event)"
v-html="renderContent(step.content)"
></div>
</div>
<div v-else-if="step.type === 'tool'" class="step-body">
<div class="step-header">{{ getToolName(step.action) }}</div>
<div class="tool-intent" v-if="getToolIntent(step.action)">
{{ getToolIntent(step.action) }}
</div>
<div class="tool-result" v-if="step.action.tool?.result">
<div v-html="renderToolResult(step.action)"></div>
</div>
<div v-else class="tool-result">
<div class="result-item">执行中...</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 文本组 -->
<div
v-else-if="group.type === 'text'"
class="text-output"
:class="{ 'streaming-text': group.streaming }"
>
<div class="text-content" :class="{ 'streaming-text': group.streaming }">
<div v-if="group.streaming" v-html="renderMarkdown(group.content || '', true)"></div>
<div v-else v-html="renderMarkdown(group.content || '', false)"></div>
</div>
</div>
<div v-else-if="group.type === 'system'" class="summary-group sub-agent-system-group">
<div class="summary-line-text sub-agent-system-summary-line">
<div class="summary-content-wrapper">
<span class="summary-preview">{{ group.content || '' }}</span>
</div>
<div class="summary-status-icon">
<svg
class="check-icon"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" />
<polyline points="7.5 12 10.5 15 16.5 9" />
</svg>
</div>
</div>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick, Component } from 'vue';
import { usePersonalizationStore } from '@/stores/personalization';
import { renderEnhancedToolResult } from './actions/toolRenderers';
import { getRandomLoader } from './loaders/index';
interface Action {
id?: string;
type: 'thinking' | 'tool' | 'text' | 'system';
content: string;
streaming?: boolean;
blockId?: string;
tool?: {
name?: string;
intent?: string;
arguments?: any;
result?: any;
};
}
interface Step {
id: string;
type: 'thinking' | 'tool';
content: string;
streaming: boolean;
action: Action;
}
interface BlockGroup {
type: 'summary' | 'text' | 'system';
id: string;
actions?: Action[];
content?: string;
streaming?: boolean;
}
const props = defineProps<{
actions: Action[];
conversationRunning?: boolean;
isLatestMessage?: boolean;
iconStyle?: (name: string) => any;
getToolIcon?: (tool: any) => string;
getToolStatusText?: (tool: any) => string;
getToolDescription?: (tool: any) => string;
formatSearchTopic?: (filters: Record<string, any>) => string;
formatSearchTime?: (filters: Record<string, any>) => string;
formatSearchDomains?: (filters: Record<string, any>) => string;
renderMarkdown?: (content: string, isStreaming: boolean) => string;
registerThinkingRef?: (key: string, el: Element | null) => void;
handleThinkingScroll?: (blockId: string, event: Event) => void;
}>();
const personalizationStore = usePersonalizationStore();
const expandedGroups = ref(new Set<string>());
const thinkingRefs = new Map<string, HTMLElement>();
const summaryLoaders = new Map<string, Component>();
const SUMMARY_PREVIEW_MAX_CHARS = 25;
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 || '');
// 获取摘要组的加载动画组件每次action类型切换时随机一次
const getSummaryLoader = (groupId: string) => {
const group = blockGroups.value.find((g) => g.id === groupId);
if (!group || !group.actions) return getRandomLoader();
// 找到当前正在streaming的action或最后一个action
const streamingAction = group.actions.find((a) => a.streaming);
const currentAction = streamingAction || group.actions[group.actions.length - 1];
if (!currentAction) return getRandomLoader();
// 找到当前action之前最近的一个不同类型的action
// 用于判断是否发生了类型切换thinking <-> tool
const currentIndex = group.actions.indexOf(currentAction);
let lastDifferentTypeIndex = -1;
for (let i = currentIndex - 1; i >= 0; i--) {
if (group.actions[i].type !== currentAction.type) {
lastDifferentTypeIndex = i;
break;
}
}
// 使用"类型切换点"作为key
// 如果是第一个action或者类型发生了切换就会生成新的key
// 如果是相同类型的连续action如多个工具并行使用相同的key
const typeSegmentKey =
lastDifferentTypeIndex >= 0
? `${group.actions[lastDifferentTypeIndex].id}-${currentAction.type}`
: `start-${currentAction.type}`;
const cacheKey = `${groupId}-${typeSegmentKey}`;
if (!summaryLoaders.has(cacheKey)) {
summaryLoaders.set(cacheKey, getRandomLoader());
}
return summaryLoaders.get(cacheKey);
};
// 将 actions 分组:连续的 thinking/tool 为一组text 单独为一组
const blockGroups = computed(() => {
const groups: BlockGroup[] = [];
let currentGroup: Action[] = [];
let groupIndex = 0;
props.actions.forEach((action, idx) => {
if (action.type === 'thinking' || action.type === 'tool') {
currentGroup.push(action);
} else if (action.type === 'text' || action.type === 'system') {
// 先保存当前的 thinking/tool 组
if (currentGroup.length > 0) {
// 使用第一个 action 的 id 作为 group id保证稳定性
const firstActionId =
currentGroup[0].id || currentGroup[0].blockId || `summary-${groupIndex}`;
groups.push({
type: 'summary',
id: `summary-${firstActionId}`,
actions: currentGroup
});
groupIndex++;
currentGroup = [];
}
if (action.type === 'text') {
const text = typeof action.content === 'string' ? action.content : '';
if (!text.trim()) {
return;
}
// 添加 text 组
groups.push({
type: 'text',
id: `text-${idx}`,
content: text,
streaming: action.streaming
});
} else {
// 添加 system 组(用于子智能体完成通知,保持顺序)
groups.push({
type: 'system',
id: `system-${idx}`,
content: action.content
});
}
}
});
// 处理最后剩余的 thinking/tool 组
if (currentGroup.length > 0) {
// 使用第一个 action 的 id 作为 group id保证稳定性
const firstActionId = currentGroup[0].id || currentGroup[0].blockId || `summary-${groupIndex}`;
groups.push({
type: 'summary',
id: `summary-${firstActionId}`,
actions: currentGroup
});
}
return groups;
});
// 获取摘要组的预览文本(始终显示最新的思考/工具)
const getSummaryPreview = (actions: Action[]) => {
// 优先返回正在 streaming 的最后一个
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];
}
}
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
const tool = currentStep.tool;
if (!tool) return '';
const intentEnabled = personalizationStore.form.tool_intent_enabled;
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);
}
// 没有intent或未开启intent模式时显示状态
if (tool.status === 'preparing') {
return `准备调用 ${tool.name || '工具'}...`;
}
if (tool.status === 'running') {
return `正在调用 ${tool.name || '工具'}...`;
}
if (tool.status === 'completed') {
return '工具执行完成';
}
return currentStep.streaming ? '正在执行工具...' : '工具执行完成';
}
return '';
};
// 判断摘要组是否正在运行(显示加载动画还是对勾)
const isSummaryRunning = (actions: Action[], groupId: string) => {
if (!props.conversationRunning || !props.isLatestMessage) {
return false;
}
// 找到当前 group 在 blockGroups 中的索引
const currentIndex = blockGroups.value.findIndex((g) => g.id === groupId);
if (currentIndex === -1) return false;
// 检查后面是否有文本输出
const hasTextAfter = blockGroups.value.slice(currentIndex + 1).some((g) => g.type === 'text');
// 只有当后面有文本输出时,才显示对勾(说明这个摘要组已经完成并开始输出了)
// 否则一直显示加载动画(即使工具执行完了,也要等下一个步骤或文本输出)
return !hasTextAfter;
};
// 获取摘要组的所有步骤(用于展开显示)
const getSummarySteps = (actions: Action[]) => {
return actions.map((action, idx) => ({
id: action.id || action.blockId || `step-${idx}`,
type: action.type as 'thinking' | 'tool',
content: action.content || '',
streaming: action.streaming || false,
action
}));
};
const toggleExpand = (groupId: string) => {
if (expandedGroups.value.has(groupId)) {
expandedGroups.value.delete(groupId);
// 折叠后,取消注册该组中的思考内容 ref
nextTick(() => {
const group = blockGroups.value.find((g) => g.id === groupId);
if (group && group.actions) {
group.actions.forEach((action) => {
if (action.type === 'thinking') {
const blockId = action.id || action.blockId;
if (blockId && typeof props.registerThinkingRef === 'function') {
props.registerThinkingRef(blockId, null);
}
}
});
}
});
} else {
expandedGroups.value.add(groupId);
// 展开后,注册该组中的思考内容 ref
nextTick(() => {
const group = blockGroups.value.find((g) => g.id === groupId);
if (group && group.actions) {
group.actions.forEach((action) => {
if (action.type === 'thinking') {
const blockId = action.id || action.blockId;
if (blockId) {
const el = thinkingRefs.get(blockId);
if (el && typeof props.registerThinkingRef === 'function') {
props.registerThinkingRef(blockId, el);
// 如果正在 streaming滚动到底部
if (action.streaming) {
el.scrollTop = el.scrollHeight;
}
}
}
}
});
}
});
}
};
const getStepIconStyle = (step: Step) => {
if (step.type === 'thinking') {
return props.iconStyle ? props.iconStyle('brain') : {};
} else if (step.type === 'tool' && step.action.tool) {
const iconKey = props.getToolIcon ? props.getToolIcon(step.action.tool) : 'terminal';
return props.iconStyle ? props.iconStyle(iconKey) : {};
}
return props.iconStyle ? props.iconStyle('terminal') : {};
};
const getToolName = (action: Action) => {
const tool = action.tool;
if (!tool) return '执行工具';
const intentEnabled = personalizationStore.form.tool_intent_enabled;
const intentText = tool.intent_rendered || tool.intent_full || '';
if (intentEnabled && intentText) {
// 开启intent模式且有intent时只显示intent
return intentText;
}
// 没有intent或未开启intent模式时显示状态
if (tool.status === 'preparing') {
return `准备调用 ${tool.name || '工具'}...`;
}
if (tool.status === 'running') {
return `正在调用 ${tool.name || '工具'}...`;
}
if (tool.status === 'completed') {
return '工具执行完成';
}
return tool.name || '执行工具';
};
const getToolIntent = (action: Action) => {
return action.tool?.intent || '';
};
const renderToolResult = (action: Action) => {
if (personalizationStore.form.enhanced_tool_display) {
const rendered = renderEnhancedToolResult(
action,
props.formatSearchTopic || (() => ''),
props.formatSearchTime || (() => ''),
props.formatSearchDomains || (() => '')
);
if (rendered) return rendered;
}
const result = action.tool?.result;
if (!result) return '<div class="result-item">执行中...</div>';
if (typeof result === 'string') {
return `<div class="result-item"><pre>${escapeHtml(result)}</pre></div>`;
}
return `<div class="result-item"><pre>${escapeHtml(JSON.stringify(result, null, 2))}</pre></div>`;
};
const renderContent = (content: string) => {
if (!content) return '';
return escapeHtml(content).replace(/\n/g, '<br>');
};
const renderMarkdown = (content: string, isStreaming: boolean) => {
if (props.renderMarkdown) {
return props.renderMarkdown(content, isStreaming);
}
// 降级如果没有提供renderMarkdown使用简单的HTML转义
return renderContent(content);
};
const escapeHtml = (text: string) => {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
};
const registerThinking = (key: string, el: Element | null) => {
// 本地保存一份,用于展开/折叠时管理
if (el instanceof HTMLElement) {
thinkingRefs.set(key, el);
} else {
thinkingRefs.delete(key);
}
// 检查当前思考内容所在的摘要是否展开
const group = blockGroups.value.find(
(g) =>
g.type === 'summary' && g.actions && g.actions.some((a) => a.id === key || a.blockId === key)
);
// 只有展开的摘要中的思考内容才注册到父组件
if (group && expandedGroups.value.has(group.id)) {
if (typeof props.registerThinkingRef === 'function') {
props.registerThinkingRef(key, el);
}
}
};
const handleScroll = (blockId: string, event: Event) => {
if (typeof props.handleThinkingScroll === 'function') {
props.handleThinkingScroll(blockId, event);
}
};
watch(
() => props.actions,
() => {
// 简化:不需要动态更新高度
},
{ deep: true }
);
</script>
<style scoped>
.minimal-blocks-container {
margin: 16px 0;
}
/* 摘要组 */
.summary-group {
margin: 16px 0;
}
.sub-agent-system-group {
margin: 0;
}
.sub-agent-system-group .sub-agent-system-summary-line {
cursor: default;
}
.sub-agent-system-group .sub-agent-system-summary-line:hover {
background-color: transparent;
}
/* 摘要行(只有文字) */
.summary-line-text {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
column-gap: 8px;
padding: 0 6px 0 6px;
cursor: pointer;
transition: background-color 0.2s;
}
.summary-line-text:hover {
background-color: var(--claude-panel);
}
.summary-content-wrapper {
min-width: 0;
font-size: 15px;
color: var(--claude-text-secondary);
line-height: 1.7;
padding: 0;
}
.summary-preview {
display: block;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
color: inherit;
}
.summary-status-icon {
flex-shrink: 0;
margin-left: 0;
align-self: start;
margin-top: calc((1.7em - 18px) / 2);
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
}
.check-icon {
color: var(--claude-text-tertiary);
width: 18px;
height: 18px;
}
/* 运行中文本:按字符依次闪烁 */
.summary-line-text.running .summary-char {
display: inline-block;
color: var(--claude-text-secondary);
animation: summaryPass 2s ease-in-out infinite;
}
@keyframes summaryPass {
0%,
100% {
color: var(--claude-text-secondary);
}
50% {
color: var(--claude-accent);
}
}
/* 步骤容器 */
.steps-container {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1);
margin-top: 8px;
padding: 0 6px 0 6px;
}
/* 桌面端与正式文本输出左边距保持一致text-content: 0 20px 0 15px */
@media (min-width: 769px) {
.summary-line-text,
.steps-container {
padding: 0 20px 0 15px;
}
}
.steps-wrapper {
overflow: hidden;
min-height: 0;
}
.steps-container.show {
grid-template-rows: 1fr;
}
/* 步骤项 */
.step-item {
display: flex;
gap: 12px;
}
.step-item:not(:last-child) {
margin-bottom: 16px;
}
.step-timeline {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
padding-top: 2px;
align-self: stretch; /* 填充整个 step-item 的高度 */
}
.step-icon {
flex-shrink: 0;
color: var(--claude-text-tertiary); /* 和竖线、字体一样的灰色 */
}
.step-line {
width: 2px;
flex: 1; /* 自动填充剩余空间 */
background: var(--claude-border);
margin-top: 4px;
margin-bottom: -14px; /* 延伸到下一个 step-item保持上下距离 SVG 相等 */
flex-shrink: 0;
}
/* 最后一个步骤隐藏竖线 */
.step-item:last-child .step-line {
display: none;
}
.step-content {
flex: 1;
min-width: 0;
padding-top: 2px;
}
.step-header {
font-size: 12px;
font-weight: 600;
color: var(--claude-text-tertiary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.step-body {
font-size: 14px;
color: var(--claude-text-secondary);
line-height: 1.7;
}
/* 思考内容 */
.thinking-content {
color: var(--claude-text-secondary);
font-size: 14px;
line-height: 1.7;
white-space: pre-wrap;
word-wrap: break-word;
max-height: calc(1.7em * 6); /* 6行 */
overflow-y: auto;
margin: 0;
padding: 0;
display: block;
/* 让第一行文字中心和 SVG 中心对齐:向上移动半个行高减去半个字高 */
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;
}
.thinking-content::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.thinking-content::-webkit-scrollbar-track {
background: transparent;
border-radius: 999px;
margin: 8px;
}
.thinking-content::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--claude-text-secondary) 55%, transparent);
border-radius: 999px;
border: 2px solid transparent;
background-clip: padding-box;
}
/* 工具内容 */
.step-header {
font-size: 12px;
font-weight: 600;
color: var(--claude-text-tertiary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 0 0 8px 0;
padding: 0;
line-height: 1;
}
.tool-intent {
font-weight: 500;
color: var(--claude-text);
margin-bottom: 12px;
font-size: 14px;
line-height: 1.7;
/* 让第一行文字紧贴顶部 */
margin-top: calc((1.7em - 1em) / -2);
}
.tool-result {
background: var(--claude-panel);
border: 1px solid var(--claude-border);
border-radius: 8px;
padding: 12px;
font-size: 13px;
max-height: 200px; /* 约为原来卡片的三分之一 */
overflow-y: auto;
/* 隐藏滚动条 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
.tool-result::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
.tool-result * {
/* 隐藏所有子元素的滚动条 */
scrollbar-width: none;
-ms-overflow-style: none;
}
.tool-result *::-webkit-scrollbar {
display: none;
}
.tool-result pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
font-size: 12px;
color: var(--claude-text-secondary);
}
.result-item {
padding: 8px 0;
border-bottom: 1px solid var(--claude-border);
}
.result-item:last-child {
border-bottom: none;
}
/* 文本输出 */
.text-output {
margin: 16px 0;
padding: 0;
}
@media (max-width: 768px) {
.text-output {
margin: 16px -9px;
}
}
.text-content {
font-size: 15px;
line-height: 1.7;
color: var(--claude-text);
word-wrap: break-word;
}
</style>