fix(experimental): frame team and subagent wake badges with readable senders

This commit is contained in:
mektpoy committed 2026-10-26 13:25:51 +08:00
1 parent 878b93f58a
commit e5ff00cc3e
10 files changed
+97 -21

No files matched your search

@@ -1,6 +1,6 @@
- table "Performance":
- rowgroup:
- row "Turn / step Trigger Execution Context Output Cache hit First token Model time Output speed (tok/s) Tool time Total time":
- row "Turn / step Trigger Execution Context Output Cache hit First token Model time Output speed Tool time Total time":
- columnheader "Turn / step"
- columnheader "Trigger"
- columnheader "Execution"
@@ -9,7 +9,7 @@
- columnheader "Cache hit"
- columnheader "First token"
- columnheader "Model time"
- columnheader "Output speed (tok/s)"
- columnheader "Output speed"
- columnheader "Tool time"
- columnheader "Total time"
- rowgroup:
@@ -9,8 +9,8 @@
en: 092989f386525568
zh: 4a6276cfc268c851
/deepseek-ai-dsh-experimental-client-ui-perf/summary:
en: 284607c5791a44d9
zh: a231eac33a8186a1
en: 3f121ba393635463
zh: bfd775b20cf080c3
/deepseek-ai-dsh-experimental-client-ui-perf/table-of-contents:
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
Adds a Performance tab beside Chat and Trajectory. Each turn is one row; expanding it lists its steps with context tokens, output tokens, cache hit rate, first-token latency, model time, output speed, tool time, and a total-time bar split into first token, generation, tools, and the rest. A running step shows elapsed time instead of a blank. Scrolling to the top pulls earlier turns through the same Session paging the other views use, with a `Load earlier` fallback button, an explicit retry state for a page that failed or did not advance, and an end-of-history marker. When a step has no tool calls, its answer label is followed by the first line of the model reply, with the full text on hover. The Trigger column classifies what opened the turn: a human prompt shows its text, while a team/agent message, subagent settlement, goal round, scheduled/webhook/job run, or runtime/time context shows a localized trigger badge plus the triggering text; a team or agent message also names its sender in the badge. A prompt that attached images shows an image-count pill in the same style as the trigger badge. Hovering or keyboard-focusing the pill opens a small preview strip, and clicking a preview loads that image in the shared image viewer; an image-only prompt shows the pill instead of an empty cell. The panel reads Session events and the prompt's durable images; it registers no tool, prompt section, or service.
Adds a Performance tab beside Chat and Trajectory. Each turn is one row; expanding it lists its steps with context tokens, output tokens, cache hit rate, first-token latency, model time, output speed, tool time, and a total-time bar split into first token, generation, tools, and the rest. A running step shows elapsed time instead of a blank. Scrolling to the top pulls earlier turns through the same Session paging the other views use, with a `Load earlier` fallback button, an explicit retry state for a page that failed or did not advance, and an end-of-history marker. When a step has no tool calls, its answer label is followed by the first line of the model reply, with the full text on hover. The Trigger column classifies what opened the turn: a human prompt shows its text, while a team/agent message, subagent settlement, goal round, scheduled/webhook/job run, or runtime/time context shows a localized trigger badge plus the triggering text; a team or agent message names its member in the badge (`团队消息 · hfai-deploy`) and omits opaque session ids, while a subagent settlement shows its tag and the settled report with the transport framing stripped. A prompt that attached images shows an image-count pill in the same style as the trigger badge. Hovering or keyboard-focusing the pill opens a small preview strip, and clicking a preview loads that image in the shared image viewer; an image-only prompt shows the pill instead of an empty cell. The panel reads Session events and the prompt's durable images; it registers no tool, prompt section, or service.
## Table of Contents
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
在「对话」和「轨迹」旁边加一个「性能」页签。每一轮一行,展开后列出各步的上下文词数、输出词数、缓存命中率、首 token 延迟、模型用时、输出速度、工具调用用时,以及一条按首 token、生成、工具调用、其余四段拆开的总用时条。还在跑的步显示已用时间,而不是留空。「触发」列会说明这一轮由什么触发:人写的提问显示原文,团队/智能体消息、子代理完成、目标轮次、定时/Webhook/后台任务或上下文唤醒显示本地化触发标签加触发文本;团队或智能体消息还会在标签里写明发送方。带图的提问在「触发」列显示与触发标签同样式的张数标签;悬停或键盘聚焦标签会浮出小图预览,点击预览用共用看图器加载该图,只有图片的提问显示标签而不是空单元格。页签只读会话事件和提问里的持久图片,不注册工具、提示词段落或服务。
在「对话」和「轨迹」旁边加一个「性能」页签。每一轮一行,展开后列出各步的上下文词数、输出词数、缓存命中率、首 token 延迟、模型用时、输出速度、工具调用用时,以及一条按首 token、生成、工具调用、其余四段拆开的总用时条。还在跑的步显示已用时间,而不是留空。「触发」列会说明这一轮由什么触发:人写的提问显示原文,团队/智能体消息、子代理完成、目标轮次、定时/Webhook/后台任务或上下文唤醒显示本地化触发标签加触发文本;团队或智能体消息还会在标签里写明成员(如「团队消息 · hfai-deploy」),无法读懂的会话 UUID 不显示;子代理完成同样显示标签,正文去掉传输前缀后截断展示。带图的提问在「触发」列显示与触发标签同样式的张数标签;悬停或键盘聚焦标签会浮出小图预览,点击预览用共用看图器加载该图,只有图片的提问显示标签而不是空单元格。页签只读会话事件和提问里的持久图片,不注册工具、提示词段落或服务。
## 目录
@@ -1053,19 +1053,20 @@ function PromptCell({ turn, t, className, loadImage }: PromptCellProps): ReactEl
: <ImagePromptPill first={first} images={images} loadImage={loadImage} t={t} />
const source = turn.promptSource
if (source !== null && source !== 'user') {
// Automated turns explain what woke them; team messages also name the sender.
const base = triggerLabel(source, t)
// Automated turns show the wake tag, the named sender when known, and the
// body with its transport framing removed.
const raw = turn.prompt ?? ''
const base = triggerLabelOf(raw, source, t)
const sender = turn.promptSender
const label = sender === null ? base : `${base} · ${sender}`
const raw = turn.prompt ?? ''
const text = sender === null
? raw
: raw.replace(/^(?:Team message from [^:]+:|Agent \S+ sent a message:)\s*/u, '')
const text = triggerBodyOf(raw)
return (
<td className={className} title={raw === '' ? label : raw}>
<span className={css.triggerPill}>{label}</span>
{pill}
{text === '' ? null : <span className={css.promptText}>{text}</span>}
<span className={css.promptClip}>
<span className={css.triggerPill}>{label}</span>
{pill}
{text === '' ? null : <span className={css.promptText}>{text}</span>}
</span>
</td>
)
}
@@ -1086,6 +1087,26 @@ function PromptCell({ turn, t, className, loadImage }: PromptCellProps): ReactEl
)
}
const TEAM_FRAME = /^Team message from /u
const AGENT_FRAME = /^Agent \S+ sent a message:/u
const SUBAGENT_FRAME = /^Background subagent \S+ finished/u
/** Label of the concrete wake frame, falling back to the producer source kind. */
function triggerLabelOf(text: string, source: string, t: PerfTranslate): string {
if (TEAM_FRAME.test(text)) return t('trigger.team-message')
if (source === 'subagent-settled' && SUBAGENT_FRAME.test(text)) return t('trigger.subagent-settled')
if (source === 'agent-message' && AGENT_FRAME.test(text)) return t('trigger.agent-message')
return triggerLabel(source, t)
}
/** Remove the transport framing so only the message body is previewed. */
function triggerBodyOf(text: string): string {
return text
.replace(/^Team message from [^:]+:\s*/u, '')
.replace(/^Agent \S+ sent a message:\s*/u, '')
.replace(/^Background subagent \S+ finished\s*/u, '')
}
/** Localized label for one automated turn trigger. */
function triggerLabel(source: string, t: PerfTranslate): string {
switch (source) {
@@ -10,8 +10,8 @@ export const zh = {
'column.scope.title': '轮次行是该轮的合计;点一行展开或折叠它的每一步',
'column.prompt': '触发',
'column.prompt.title': '这一轮是由什么触发的;人写的提示词显示正文,自动触发显示来源标签',
'trigger.agent-message': '收到任务消息',
'trigger.team-message': '收到团队消息',
'trigger.agent-message': '智能体消息',
'trigger.team-message': '团队消息',
'trigger.schedule': '定时触发',
'trigger.webhook': 'Webhook',
'trigger.tool-jobs': '后台任务',
@@ -145,7 +145,7 @@ export const en: Record<PerfKey, string> = {
'column.scope.title': 'A turn row holds the turn total; click a row to expand or collapse its steps',
'column.prompt': 'Trigger',
'column.prompt.title': 'What opened this turn: a human prompt shows its text, automation shows a source badge',
'trigger.agent-message': 'Task message',
'trigger.agent-message': 'Agent message',
'trigger.team-message': 'Team message',
'trigger.schedule': 'Scheduled',
'trigger.webhook': 'Webhook',
@@ -267,8 +267,8 @@ export const ja: Record<PerfKey, string> = {
'column.scope.title': 'ターン行はそのターンの合計です。行をクリックすると各ステップを展開または折りたたみます',
'column.prompt': 'トリガー',
'column.prompt.title': 'このターンを開始したものを表示します。人間のプロンプトは本文、自動トリガーはタグを表示します',
'trigger.agent-message': 'タスクメッセージ受信',
'trigger.team-message': 'チームメッセージ受信',
'trigger.agent-message': 'エージェントメッセージ',
'trigger.team-message': 'チームメッセージ',
'trigger.schedule': 'スケジュール実行',
'trigger.webhook': 'Webhook',
'trigger.tool-jobs': 'バックグラウンドジョブ',
@@ -256,6 +256,7 @@ function isTokenDelta(chunk: StreamChunk): boolean {
const TEAM_MESSAGE_PREFIX = /^Team message from ([^:]+):/u
const AGENT_MESSAGE_PREFIX = /^Agent (\S+) sent a message:/u
const OPAQUE_SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu
/** Sender agent name of a teammate/agent notification, parsed from its body prefix. */
export function triggerSenderOf(message: UserMessage): string | null {
@@ -269,7 +270,9 @@ export function triggerSenderOf(message: UserMessage): string | null {
/* v8 ignore next -- a matched capture group is always present. */
if (team !== null) return team[1]?.trim() ?? null
const agent = AGENT_MESSAGE_PREFIX.exec(text)
return agent?.[1] ?? null
const id = agent?.[1] ?? null
// A raw session UUID is not a readable member name; keep the badge generic.
return id !== null && OPAQUE_SESSION_ID.test(id) ? null : id
}
/** Answer characters kept for the one-line Did preview. */
@@ -237,7 +237,24 @@
}
/* Did column: tool names, hints, and output sizes. */
.promptClip {
display: flex;
align-items: center;
gap: 4px;
min-width: 0;
}
.promptClip .promptText {
flex: 1;
min-width: 0;
max-width: none;
}
.triggerPill {
flex: none;
max-width: 70%;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 4px;
padding: 0 4px;
border: 0.5px solid var(--dsw-alias-border-l2);
@@ -734,6 +734,10 @@ describe('trigger sender helper', () => {
expect(triggerSenderOf(userMessage([textBlock('Agent abc-123 sent a message: hi')], {
kind: 'agent-message',
}))).toBe('abc-123')
// Raw session UUIDs are not member names.
expect(triggerSenderOf(userMessage([textBlock('Agent 52224ac7-b02b-4d25-8c6e-123456789abc sent a message: hi')], {
kind: 'agent-message',
}))).toBeNull()
})
it('returns nothing without a framed sender', () => {
@@ -597,6 +597,37 @@ describe('turn rows', () => {
expect(cell.getAttribute('title')).toBe('Team message from hfai-deploy: P1 已 rebase 完成')
})
it('shows the frame label and body without opaque session ids', () => {
mount({
turns: [
turn({
turn: 1,
prompt: 'Agent 52224ac7-b02b-4d25-8c6e-123456789abc sent a message: 请检查构建',
promptSource: 'agent-message',
}),
turn({
turn: 2,
prompt: 'Background subagent 10a0dbaa-08dd-4e3f-838d-d6f84b36babe finished and will do no further work.',
promptSource: 'subagent-settled',
}),
turn({ turn: 3, prompt: 'Team message from stack-rebase: P2 已合并', promptSource: 'team-message', promptSender: 'stack-rebase' }),
],
stepCount: 3,
})
const first = cellsOf(rowWithPrefix(say('turn.label', { turn: 1 })))[1] as HTMLTableCellElement
expect(first.textContent).toContain(say('trigger.agent-message'))
expect(first.textContent).not.toContain('52224ac7')
expect(first.textContent).toContain('请检查构建')
const second = cellsOf(rowWithPrefix(say('turn.label', { turn: 2 })))[1] as HTMLTableCellElement
expect(second.textContent).toContain(say('trigger.subagent-settled'))
expect(second.textContent).not.toContain('10a0dbaa')
expect(second.textContent).toContain('and will do no further work.')
const third = cellsOf(rowWithPrefix(say('turn.label', { turn: 3 })))[1] as HTMLTableCellElement
expect(third.textContent).toContain(`${say('trigger.team-message')} · stack-rebase`)
expect(third.textContent).toContain('P2 已合并')
expect(third.textContent).not.toContain('Team message from')
})
it('summarizes turn tools with pills, overflow, errors, and output size', () => {
mount({
turns: [turn({