Commit edfc2010 by luoqi

merge: 助手窗口调高 + 拿掉工具灰行与「执行过程」

· 默认高度 620 → 760(max-h 78vh → 88vh)——确认单一出正文就被挤到要滚
· 正文里的「出分配方案 `propose_assignment`」灰行删掉:后半是函数名,不是主管的词
· 末尾「执行过程 · N 步 …→ 摆选项按钮」删掉:讲的是装置内部怎么跑的
· 组件(StepRow / StepTrace)一并删, 不留成没人用的导出
️ 可核查性换了地方:等待文案(说动作不说工具名)+ agent_invocations.output.steps + 裸台
parents 066cc349 d49d42e5
Pipeline #3580 failed in 0 seconds
...@@ -32,7 +32,7 @@ import { useHasPermission } from '@/hooks/use-permission'; ...@@ -32,7 +32,7 @@ import { useHasPermission } from '@/hooks/use-permission';
import { useAssistantStore } from '@/stores/assistant-store'; import { useAssistantStore } from '@/stores/assistant-store';
import { AssignmentConfirmSheet } from './assignment-confirm-sheet'; import { AssignmentConfirmSheet } from './assignment-confirm-sheet';
import { SignalPrompt } from './signal-prompts'; import { SignalPrompt } from './signal-prompts';
import { ReasoningRow, StepRow, StepTrace, toolMeta } from './step-trace'; import { ReasoningRow, toolMeta } from './step-trace';
import { isDiscardedDraft, stripToolEcho } from './chat-blocks'; import { isDiscardedDraft, stripToolEcho } from './chat-blocks';
import { import {
useAssistantChat, useAssistantChat,
...@@ -46,7 +46,6 @@ import { ...@@ -46,7 +46,6 @@ import {
* ⛔ 不再单独出一行灰字 —— 否则同一件事画两遍,而且顺序还是反的 * ⛔ 不再单独出一行灰字 —— 否则同一件事画两遍,而且顺序还是反的
* (侧信道事件比 fullStream 的 tool-call 先到)。 * (侧信道事件比 fullStream 的 tool-call 先到)。
*/ */
const PLACE_TOOLS = new Set(['show_sheet', 'show_guidance']);
/** /**
* ⭐ **小图标按钮的唯一定义**(2026-08-15)。 * ⭐ **小图标按钮的唯一定义**(2026-08-15)。
...@@ -289,20 +288,15 @@ function BlockView({ ...@@ -289,20 +288,15 @@ function BlockView({
orphanKeys?: Set<string>; orphanKeys?: Set<string>;
}) { }) {
/** /**
* ⭐ 思考 / 工具调用 **按到达顺序就地渲染**(2026-08-14 改,此前只在末尾的执行过程里)。 * ⭐ 思考**按到达顺序就地渲染**(2026-08-14 改,此前只在末尾的执行过程里)——
* 位置本身就是信息:「想 → 调 → 写 → 再调」和「全调完再写」是两种行为,堆在末尾看不出区别。
* *
* 🔴 改的理由:位置本身就是信息 —— 「想 → 调 → 写 → 再调」和「全调完再写」 * 🔴 **工具那一类 2026-08-19 起整个不到这儿了**(在 `MessageView` 的 `rest` 里就滤掉了,
* 是两种完全不同的行为,而堆在末尾看不出区别。归因一次要靠这个顺序。 * 理由写在那儿)。⇒ 这里⛔ 不留 `block.kind === 'tool'` 分支:
* ⚠️ 当初钉在末尾的顾虑是「qwen 会把 tool-call 全提到文本之前,等于先给主管看一屏工具记录」—— * 它永远进不来,而一段永远不执行的代码读起来像"这个功能还在"。
* 现在两条都变了:默认模型是会交错的 DeepSeek;而每行都是**一行收起态**, * 要把它放回来,改 `rest` 的过滤那一处就够了。
* 就算全提到前面也只是几行灰字,⛔ 不再是"一屏记录"。
* ⛔ `show_*` 不出行:它们在正文里**已经有可见产物**(卡片 / 按钮),
* 而且侧信道事件比 fullStream 的 tool-call 先到 —— 两个都画会出现"产物排在自己的调用记录前面"。
*/ */
if (block.kind === 'reasoning') return <ReasoningRow text={block.text} />; if (block.kind === 'reasoning') return <ReasoningRow text={block.text} />;
if (block.kind === 'tool') {
return PLACE_TOOLS.has(block.step.tool) ? null : <StepRow step={block.step} />;
}
// 位置标记本身不渲染 —— MessageView 已经把卡片搬到它那儿了(理论上到不了这) // 位置标记本身不渲染 —— MessageView 已经把卡片搬到它那儿了(理论上到不了这)
if (block.kind === 'sheet_anchor') return null; if (block.kind === 'sheet_anchor') return null;
if (block.kind === 'artifact') return <ArtifactView artifact={block.artifact} />; if (block.kind === 'artifact') return <ArtifactView artifact={block.artifact} />;
...@@ -345,7 +339,13 @@ function BlockView({ ...@@ -345,7 +339,13 @@ function BlockView({
/> />
); );
} }
return <Markdown text={block.text} />; /**
* ⚠️ 兜底显式判 `text`,⛔ 别写成 `return <Markdown text={block.text} />` ——
* 那一版能编译过,是因为上面还有一条 `block.kind === 'tool'` 分支替它把类型收窄了;
* 工具分支删掉之后(见上),少了收窄这条尾巴就不成立了。
* ⇒ 判据写在自己这一行,⛔ 不靠别处的分支顺手兜。
*/
return block.kind === 'text' ? <Markdown text={block.text} /> : null;
} }
// ── Artifact:模型产出的 HTML 在隔离沙箱 iframe 渲染(即用即焚)────────────── // ── Artifact:模型产出的 HTML 在隔离沙箱 iframe 渲染(即用即焚)──────────────
...@@ -677,7 +677,18 @@ function MessageView({ ...@@ -677,7 +677,18 @@ function MessageView({
const rest = message.blocks.filter( const rest = message.blocks.filter(
(b) => (b) =>
b.kind !== 'assignment_sheet' && b.kind !== 'assignment_sheet' &&
(showTrace || (b.kind !== 'reasoning' && b.kind !== 'tool')), /**
* 🔴 **工具灰行整个不给主管看**(2026-08-19 产品定)——「出分配方案 `propose_assignment`」
* 这一行里,前半是产品说法、后半是**函数名**,而主管的词汇表就是他在界面上见过的那些
* (同 ② 诚实层那条)。他要判断的是"这一版分得对不对",⛔ 不是"它走了哪条代码路径"。
* ⚠️ 可核查性**没有丢**,只是挪了地方:
* · 「正在出分配方案…」那句等待文案还在(`hintLabel`,说的是动作不是工具名);
* · 每一步的形状落在 `agent_invocations.output.steps`(服务端留痕,可回查);
* · 裸台(`/assistant-lab`)仍旧原样展示全部步骤 —— 排查去那儿。
* ⇒ 开关(`showTrace`)现在只管**思考**那一类。
*/
b.kind !== 'tool' &&
(showTrace || b.kind !== 'reasoning'),
); );
/** /**
...@@ -805,19 +816,11 @@ function MessageView({ ...@@ -805,19 +816,11 @@ function MessageView({
))} ))}
{showHint && <GeneratingHint label={hintLabel} />} {showHint && <GeneratingHint label={hintLabel} />}
{/** {/**
* ⭐ 执行过程 —— 这一轮**到底调了哪些工具**,默认收起(见 step-trace 的整段理由)。 * ⛔ 「执行过程 · N 步 出分配方案 → 摆选项按钮 → …」**已删**(2026-08-19 产品定)。
* * 同上面工具灰行那条理由:主管要判断的是这一版分得对不对,
* ⚠️ 摆在最后、且**说完才出现**:流式期间它每来一步就长一行, * 而这行小结讲的是**装置内部怎么跑的** —— 它连"摆按钮"这种渲染动作都列出来了。
* 而主管这时正在读上面的文字 —— 底下抖动比看不见更烦。 * ⚠️ `StepTrace` 组件本身**留着**,裸台(`/assistant-lab`)还在用它排查。
* ⚠️ 一步都没有时组件自己返回 null,⛔ 这里不用再判一次。
*/} */}
{!streaming && (
<StepTrace
steps={message.blocks
.filter((b): b is Extract<Block, { kind: 'tool' }> => b.kind === 'tool')
.map((b) => b.step)}
/>
)}
</div> </div>
); );
} }
...@@ -1078,11 +1081,12 @@ export function AssistantChat({ ...@@ -1078,11 +1081,12 @@ export function AssistantChat({
const [voice, setVoice] = useState<'idle' | 'live'>('idle'); const [voice, setVoice] = useState<'idle' | 'live'>('idle');
const [voiceErr, setVoiceErr] = useState<string | null>(null); const [voiceErr, setVoiceErr] = useState<string | null>(null);
/** /**
* 思考与工具调用**展不展开**(2026-08-15 产品加)。 * 思考**展不展开**(2026-08-15 产品加;2026-08-19 起工具调用不在它管辖内了)。
* *
* ⚠️ 默认展开:它是「模型到底调没调工具」的可核查记录,这条生产线最贵的一类问题 * ⚠️ 默认展开:模型讲了什么、怎么想的,主管看得懂也用得上。
* 就靠它从"推理"变成"看一眼"。收起是主管当下不想看,⛔ 不是默认不给。
* ⚠️ 只活在这个组件里,刷新回到默认 —— 它是一次阅读时的取舍,⛔ 不是账号设置。 * ⚠️ 只活在这个组件里,刷新回到默认 —— 它是一次阅读时的取舍,⛔ 不是账号设置。
* ⚠️ 「模型到底调没调工具」这件事**不再靠界面看**(工具行已删)——
* 它落在 `agent_invocations.output.steps` 里,回查见那边注释。
*/ */
const [showTrace, setShowTrace] = useState(true); const [showTrace, setShowTrace] = useState(true);
const dictRef = useRef<{ const dictRef = useRef<{
...@@ -1368,14 +1372,14 @@ export function AssistantChat({ ...@@ -1368,14 +1372,14 @@ export function AssistantChat({
</button> </button>
)} )}
{/* {/*
思考与工具调用的开关 —— ⚠️ 收起时**图标变实**(不是变淡): 思考的开关 —— ⚠️ 收起时**图标变实**(不是变淡):
「有东西被我收起来了」需要一直看得见,收起之后底部那行小结是唯一的痕迹 「有东西被我收起来了」需要一直看得见。
⛔ 别用变淡表示"关":那读起来像"这个功能不可用"。 ⛔ 别用变淡表示"关":那读起来像"这个功能不可用"。
*/} */}
<button <button
type="button" type="button"
onClick={() => setShowTrace((v) => !v)} onClick={() => setShowTrace((v) => !v)}
title={showTrace ? '收起思考与工具调用' : '展开思考与工具调用'} title={showTrace ? '收起思考' : '展开思考'}
className={cn( className={cn(
'inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors', 'inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors',
showTrace showTrace
......
...@@ -43,7 +43,13 @@ export function AssistantWidget() { ...@@ -43,7 +43,13 @@ export function AssistantWidget() {
* ⛔ 别再往回收:那张卡是这个窗口的**主要内容**,窗口要迁就它。 * ⛔ 别再往回收:那张卡是这个窗口的**主要内容**,窗口要迁就它。
*/ */
const W = Math.min(480, window.innerWidth - 16); const W = Math.min(480, window.innerWidth - 16);
const H = Math.min(620, Math.round(window.innerHeight * 0.78)); /**
* ⚠️ **这个 760 / 0.88 与下面 className 里的 `h-[760px] max-h-[88vh]` 是一对,改一处要改两处**。
* 🔴 620 → 760(2026-08-19 产品:「默认高度改高点」):这个窗口的主要内容是**确认单**,
* 而它下面还要跟一段正文和几排引导按钮 —— 620px 里卡片一出,正文就被挤到要滚,
* 而正文正是"这一版为什么长这样"的那半句。⛔ 别再往回收。
*/
const H = Math.min(760, Math.round(window.innerHeight * 0.88));
// 水平:钮在屏幕右半 → 窗口右缘对齐钮右缘;左半 → 左缘对齐 // 水平:钮在屏幕右半 → 窗口右缘对齐钮右缘;左半 → 左缘对齐
let x = fab.left + fab.width / 2 > window.innerWidth / 2 ? fab.right - W : fab.left; let x = fab.left + fab.width / 2 > window.innerWidth / 2 ? fab.right - W : fab.left;
// 垂直:优先弹在钮上方,放不下弹下方 // 垂直:优先弹在钮上方,放不下弹下方
...@@ -106,8 +112,8 @@ export function AssistantWidget() { ...@@ -106,8 +112,8 @@ export function AssistantWidget() {
'transition-[opacity,transform] duration-150', 'transition-[opacity,transform] duration-150',
maximized maximized
? 'inset-3' // 最大化:填满窗口(留 12px 边距),不写死宽高,随窗口自适应 ? 'inset-3' // 最大化:填满窗口(留 12px 边距),不写死宽高,随窗口自适应
: // ⚠️ `w-[480px]` 与上面 openAt 里的 480 是一对,改一处要改两处 : // ⚠️ `w-[480px]` / `h-[760px]` 与上面 openAt 里的 480 / 760 是一对,改一处要改两处
cn(!winRect && 'bottom-4 right-4', 'h-[620px] max-h-[78vh] w-[480px] max-w-[calc(100vw-2rem)]'), cn(!winRect && 'bottom-4 right-4', 'h-[760px] max-h-[88vh] w-[480px] max-w-[calc(100vw-2rem)]'),
open ? 'translate-y-0 opacity-100' : 'pointer-events-none invisible translate-y-2 opacity-0', open ? 'translate-y-0 opacity-100' : 'pointer-events-none invisible translate-y-2 opacity-0',
)} )}
> >
......
...@@ -2,34 +2,29 @@ ...@@ -2,34 +2,29 @@
import { useState } from 'react'; import { useState } from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react'; import { ChevronDown, ChevronRight } from 'lucide-react';
import type { ToolStep } from './chat-blocks';
import { cn } from '@/lib/utils';
/** /**
* 执行过程 —— 这一轮**到底调了哪些工具**,可核查的原始记录 * 助手消息里的**轨迹行** —— 现在只剩「思考」一种,外加工具的中文说法表
* *
* ═══ 为什么要有 ═══════════════════════════════════════════════════ * ═══ 「执行过程」那一整套 2026-08-19 已删 ═══════════════════════════
* 🔴 「模型到底调没调工具」是这条生产线**最贵的一类问题**,而在此之前只能靠 * 产品定:「调用工具和执行过程去掉,用户不需要知道」。删掉的是
* "界面上有没有出现卡片/按钮"去反推。2026-08-13 实测吃过一次:主管点了 * · `StepRow` —— 正文里那行「出分配方案 `propose_assignment`」(前半产品说法、后半函数名)
* 「只圈电商平台那 251 人」,助手回了一整份确认单式的汇报 * · `StepTrace` —— 消息末尾那行「执行过程 · 4 步 出分配方案 → 摆选项按钮 → …」
* 主管要判断的是"这一版分得对不对",⛔ 不是"它走了哪条代码路径";而「摆选项按钮」
* 这种渲染动作被列出来,讲的是装置内部怎么跑的。
*
* ═══ ⚠️ 但下面这件事没有失效,只是换了地方 ═══════════════════════
* 🔴 「模型到底调没调工具」是这条生产线**最贵的一类问题**。2026-08-13 实测吃过一次:
* 主管点了「只圈电商平台那 251 人」,助手回了一整份确认单式的汇报
* (251 人 / 98 条落到 33 位客服 / 17 回专属 / 81 补空 / 153 待分配), * (251 人 / 98 条落到 33 位客服 / 17 回专属 / 81 补空 / 153 待分配),
* 数字彼此自洽、读起来毫无破绽 —— 但 `propose_assignment` 一次都没跑, * 数字彼此自洽、读起来毫无破绽 —— 但 `propose_assignment` 一次都没跑,
* 整段是照着提示词里那个三段模板**填出来的**。 * 整段是照着提示词里那个三段模板**填出来的**。
* 当时排查只能靠「卡片没出现 ⇒ 侧信道没推 ⇒ 工具没跑」这条推理链。 * ⇒ 现在这件事**在服务端查**:`agent_invocations.output.toolCalls / steps`
* ⇒ 把记录摆出来,这类问题从"推理"变成"看一眼"。 * (每一步「讲了多少字 + 调了哪些工具」,见 `assistant-invocation.ts` 的 `stepShapes`)。
* * 界面上的替代物只有一个:`toolMeta().running` 那句等待文案 —— 它说的是**动作**
* ═══ 三条边界 ═══════════════════════════════════════════════════ * (「正在挑人、排客服,出分配方案」),⛔ 不是工具名。
* ⛔ **一步都没有时整块不渲染**(产品定)——「执行过程 · 0 步」对主管是噪音, * ⚠️ 裸台 `/assistant-lab` 仍旧原样展示全部步骤,但它**自己写了一份**、⛔ 不引这里 ——
* 他不需要每条闲聊下面都挂一个空壳。⚠️ 但这也意味着"没调工具"本身**不会** * 改这个文件不会让裸台跟着变,反过来也一样。
* 在这里显形,那件事另有防线(见 assistant-prompts 第 2 段与服务端收尾标记)。
* ⛔ **默认收起**:这是脚注不是内容。主管要读的是结论,证据放在他伸手够得到的地方就行。
* ⚠️ **摆在整条消息的最后**(正文 → 卡片 → 这里)。Claude Code 是按发生顺序摆在正文之前,
* 我们不行:qwen 会把一轮的 tool-call 全提到文本之前,照发生顺序摆等于每次都先给主管
* 看一屏工具记录再给结论。位置固定在末尾,"这轮跑了几步"也就永远在同一个地方。
*
* ⚠️ 这里**允许出现英文工具名**,与「⛔ 别把工具名说给主管」不冲突:
* 那条约束管的是**助手嘴里说的话**;这里是一份可核查的记录,语境不同
* (同 Claude Code 的 "Ran a command")。中文动作名在前、原始名在后且更淡。
*/ */
/** /**
...@@ -78,14 +73,6 @@ const BOX = ...@@ -78,14 +73,6 @@ const BOX =
'max-h-56 overflow-auto rounded-md border border-slate-200/80 bg-slate-50/60 px-2.5 py-2'; 'max-h-56 overflow-auto rounded-md border border-slate-200/80 bg-slate-50/60 px-2.5 py-2';
const MONO = 'whitespace-pre-wrap break-all font-mono text-[10.5px] leading-[1.6] text-slate-600'; const MONO = 'whitespace-pre-wrap break-all font-mono text-[10.5px] leading-[1.6] text-slate-600';
/** 参数短就一行、长就换行铺开 —— ⛔ 别一律 pretty-print,`{"编号":3}` 占三行纯属浪费。 */
function fmt(v: unknown): string {
if (v == null) return '';
const one = typeof v === 'string' ? v : JSON.stringify(v);
if (one.length <= 80) return one;
return typeof v === 'string' ? v : JSON.stringify(v, null, 2);
}
/** /**
* ⭐ **一条可折叠的轨迹行** —— 思考和工具调用共用的唯一原语。 * ⭐ **一条可折叠的轨迹行** —— 思考和工具调用共用的唯一原语。
* *
...@@ -97,13 +84,11 @@ function fmt(v: unknown): string { ...@@ -97,13 +84,11 @@ function fmt(v: unknown): string {
function TraceRow({ function TraceRow({
label, label,
tag, tag,
status,
children, children,
}: { }: {
label: string; label: string;
/** 灰色小字:工具原始名 / 思考字数 —— 出问题时能拿去 grep 的那个词 */ /** 灰色小字:思考字数 —— 流式期间它在长,是唯一能看出"还在想"的信号 */
tag?: string; tag?: string;
status?: 'error' | 'running' | 'aborted';
children?: React.ReactNode; children?: React.ReactNode;
}) { }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
...@@ -131,16 +116,6 @@ function TraceRow({ ...@@ -131,16 +116,6 @@ function TraceRow({
) : ( ) : (
<ChevronRight className="size-3 shrink-0 text-slate-400" /> <ChevronRight className="size-3 shrink-0 text-slate-400" />
)} )}
{status === 'error' && (
<span className="ml-auto shrink-0 text-[10.5px] text-rose-600">失败</span>
)}
{status === 'running' && (
<span className="ml-auto shrink-0 text-[10.5px] text-slate-400">进行中…</span>
)}
{/* ⚠️ 中性色,⛔ 不是红:这一步**没有失败**,只是这一轮不听了(见 ToolStep 上那段) */}
{status === 'aborted' && (
<span className="ml-auto shrink-0 text-[10.5px] text-slate-400">已中断</span>
)}
</button> </button>
{/* ⚠️ 箭头挪到文字后面之后,展开内容与 label 左沿**天然对齐**(都落在 0)—— {/* ⚠️ 箭头挪到文字后面之后,展开内容与 label 左沿**天然对齐**(都落在 0)——
⛔ 别再加 `pl-*` 去"缩进一下":那正是刚拆掉的那种错位。 */} ⛔ 别再加 `pl-*` 去"缩进一下":那正是刚拆掉的那种错位。 */}
...@@ -149,34 +124,17 @@ function TraceRow({ ...@@ -149,34 +124,17 @@ function TraceRow({
); );
} }
/** 一次工具调用。⚠️ 参数/返回里会出现取值码,所以**默认收起**(同 StepTrace 顶部那段边界)。 */ /**
export function StepRow({ step }: { step: ToolStep }) { * ⛔ **`StepRow`(一次工具调用的灰行)与 `StepTrace`(执行过程 · N 步)已删**
const meta = toolMeta(step.tool); * (2026-08-19 产品定:「调用工具和执行过程去掉,用户不需要知道」)。
const args = fmt(step.args); *
const result = step.status === 'error' ? fmt(step.error) : fmt(step.result); * 删的是**渲染**,⛔ 不是可核查性 —— 上面那段"为什么要有"讲的事仍然成立,只是换了地方:
return ( * · 「正在出分配方案…」等待文案还在(`toolMeta().running`,说的是动作不是工具名);
<TraceRow * · 每一步的形状落在 `agent_invocations.output.steps`(服务端留痕,可回查);
label={meta.label} * · 裸台 `/assistant-lab` 有自己那一份执行过程(⚠️ 它**不引这里**,别改这边指望那边跟着变)。
tag={step.tool} * ⛔ 别把它们留成"没人用的导出" —— 一段永远不渲染的组件读起来像"这个功能还在"。
{...(step.status === 'done' ? {} : { status: step.status })} * 要放回来:改 `assistant-chat` 里 `rest` 那一处过滤,再把组件写回来。
> */
{args && (
<div className={BOX}>
<div className="mb-0.5 text-[10px] text-slate-400">参数</div>
<div className={MONO}>{args}</div>
</div>
)}
{result && (
<div className={BOX}>
<div className="mb-0.5 text-[10px] text-slate-400">
{step.status === 'error' ? '错因' : '返回'}
</div>
<div className={cn(MONO, step.status === 'error' && 'text-rose-700')}>{result}</div>
</div>
)}
</TraceRow>
);
}
/** /**
* 模型的思考。⚠️ 标签上带**字数**:流式期间它在长,字数是唯一能看出"还在想"的信号。 * 模型的思考。⚠️ 标签上带**字数**:流式期间它在长,字数是唯一能看出"还在想"的信号。
...@@ -191,32 +149,3 @@ export function ReasoningRow({ text }: { text: string }) { ...@@ -191,32 +149,3 @@ export function ReasoningRow({ text }: { text: string }) {
</TraceRow> </TraceRow>
); );
} }
/**
* 执行过程 —— **只剩一行小结**(2026-08-14 改)。
*
* 🔴 明细现在**按时序穿插在正文里**(`StepRow` / `ReasoningRow` 由 BlockView 就地渲染),
* 这里再展开一遍就是同一份东西的两个副本 —— 改一处漏一处。
* ⭐ 但这一行要留:**「这轮跑了几步」永远在同一个地方**,扫一眼就知道走了什么路,
* ⛔ 不用顺着正文一路数绿框和灰行。
* ⚠️ 计数**含**位置标记(`show_*`),而它们在正文里不出灰行(见 BlockView 那段),
* 所以这个数会比正文里的行数多 —— 差几个就是摆了几次。
*/
export function StepTrace({ steps }: { steps: ToolStep[] }) {
// ⛔ 一步都没有就整块不渲染(产品定)—— 空壳对主管是噪音
if (steps.length === 0) return null;
const failed = steps.filter((s) => s.status === 'error').length;
return (
<p className="mt-2 flex items-center gap-2 px-0.5 text-[11px] text-slate-400">
{/* 🔴 `shrink-0 whitespace-nowrap` **不许去掉**(2026-08-15 对抗审查实测):
同排那条工具链带 `truncate`(overflow:hidden ⇒ 最小尺寸为 0),但 flex 收缩是
**按比例分摊**的,不会优先榨可截断的那个 —— 首段跟着被压窄,压不下就逐字换行。
480px 浮窗实测:6 步(正好是出一版方案的正常步数)就折成两行、12 步三行,
而「这轮跑了几步」这个唯一固定锚点被劈开时,看起来像渲染坏了。 */}
<span className="shrink-0 whitespace-nowrap">执行过程 · {steps.length}</span>
<span className="truncate">{steps.map((s) => toolMeta(s.tool).label).join(' → ')}</span>
{/* ⭐ 失败数一直摆在外面 —— 不展开也得看得见有东西挂了 */}
{failed > 0 && <span className="ml-auto shrink-0 text-rose-600">{failed} 失败</span>}
</p>
);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment