From a95590b1cf851465a7bf5adabcc16a7ee478e1c3 Mon Sep 17 00:00:00 2001 From: Chris <4436110+zqchris@users.noreply.github.com> Date: Thu, 5 Nov 2026 00:59:10 +0800 Subject: [PATCH 1/2] fix(bots): show partner avatars beside work processes on all clients Assisted-by: Cindy (https://cindy.app) Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com> --- .../teammate-process/process-fixture.tsx | 2 +- .../fixtures/teammate-process/ui-page.mjs | 7 +++++++ .../renderer/components/chat/MessageStream.tsx | 17 ++++++++--------- .../bots/__tests__/botChatSurface.test.ts | 10 +++++----- .../__tests__/pluginAttachmentBubble.test.tsx | 7 ++++--- apps/mobile/src/session/MessageRenderer.tsx | 14 +++++++++++--- docs/design-rules/DESIGN.md | 3 +++ 7 files changed, 39 insertions(+), 21 deletions(-) diff --git a/apps/desktop/scripts/fixtures/teammate-process/process-fixture.tsx b/apps/desktop/scripts/fixtures/teammate-process/process-fixture.tsx index 7301d347e65..c2a58b34ada 100644 --- a/apps/desktop/scripts/fixtures/teammate-process/process-fixture.tsx +++ b/apps/desktop/scripts/fixtures/teammate-process/process-fixture.tsx @@ -81,7 +81,7 @@ function Subject({ done, retry, artifactCase }: { done: boolean; retry: boolean; return
} simplifiedBotConversation isSessionStreaming={!finished} contentWidth={720} /> + assistantAvatar={} simplifiedBotConversation isSessionStreaming={!finished} contentWidth={720} />
; } void mountPage({ diff --git a/apps/desktop/scripts/fixtures/teammate-process/ui-page.mjs b/apps/desktop/scripts/fixtures/teammate-process/ui-page.mjs index 5db9bd0e260..3d494a5556e 100644 --- a/apps/desktop/scripts/fixtures/teammate-process/ui-page.mjs +++ b/apps/desktop/scripts/fixtures/teammate-process/ui-page.mjs @@ -97,6 +97,11 @@ export default { const journal = page.locator('[data-teammate-process]'); await journal.waitFor(); assert.equal(await journal.count(), 1, 'One disclosure owns all intermediate notes'); + // Visible UI regression: process and reply each own a portrait; expansion + // must not repeat it in tool details. Hidden spacers failed this check. + const processPortrait = journal.locator('xpath=../..').getByTestId('fixture-partner-avatar'); + assert.equal(await processPortrait.count(), 1); + assert.equal(await processPortrait.isVisible(), true, 'The process portrait is visible'); const heading = journal.locator(':scope > button'); if (definition.data.retry) { const action = journal.locator('[data-teammate-segment] > button'); @@ -138,6 +143,8 @@ export default { assert.equal(await heading.getAttribute('aria-expanded'), 'true', 'Keep inspected work open on completion'); } await ui.capture('records'); + assert.equal(await page.getByTestId('fixture-partner-avatar').count(), 2, 'Expanded process and final reply each have one portrait'); + assert.equal(await processPortrait.isVisible(), true); await heading.click(); assert.equal(await heading.getAttribute('aria-expanded'), 'false'); }, diff --git a/apps/desktop/src/renderer/components/chat/MessageStream.tsx b/apps/desktop/src/renderer/components/chat/MessageStream.tsx index 49d4b1fed95..4876bbdba06 100644 --- a/apps/desktop/src/renderer/components/chat/MessageStream.tsx +++ b/apps/desktop/src/renderer/components/chat/MessageStream.tsx @@ -2899,7 +2899,7 @@ function renderWorkGroupChild( } // 工作组里的中间过程文字不挂 assistantAvatar:折叠块里逐条画脸只会变噪音, - // 身份标记只属于对话流里真正的那句回复(见 MessageItem 的 assistantAvatar)。 + // 过程区外层与最终回复各显示一次头像,展开明细不重复。 return (
; context: WorkGroupRenderContext; compact: boolean; - /** 伙伴聊天:隐形头像占位,让工作过程与回复正文同列对齐。 */ - replyColumnSpacer?: ReactNode; + /** 伙伴聊天:过程区与最终回复各显示一个头像,正文保持同列。 */ + companionAvatar?: ReactNode; }) { // 完成态外层时间线可包含内层 work_group。递归只负责形状映射, // 具体折叠 / 直接详情逻辑全部复用 WorkGroupBlock。 @@ -2999,7 +2999,7 @@ export const WorkGroupRenderItemView = memo(function WorkGroupRenderItemView({ childItems={childItems} compact={compact} deferred={item.deferred} - fitHeader={Boolean(replyColumnSpacer)} + fitHeader={Boolean(companionAvatar)} journal={item.teammateJournal} journalTitle={item.isStreaming ? (elapsed) => ( {elapsed} @@ -3018,7 +3018,7 @@ export const WorkGroupRenderItemView = memo(function WorkGroupRenderItemView({ className="scroll-mt-20" data-message-client-ids={collectWorkGroupClientIds(item.children).join(' ')} > - {replyColumnSpacer ? withAssistantAvatar(replyColumnSpacer, block) : block} + {companionAvatar ? withAssistantAvatar(companionAvatar, block) : block}
); }); @@ -6352,8 +6352,7 @@ export function MessageStream({ previousLocalFileRefsRef.current = localFileRefs; }, [localFileRefs]); - // 伙伴聊天的工作过程、成果与回复正文同列:只占头像宽度,不重复画脸。memo 住以保持 - // WorkGroupRenderItemView 的 memo 比较成立。 + // 成果与回复正文同列,只占头像宽度;过程区单独显示伙伴头像。 const botReplyColumnSpacer = useMemo( () => simplifiedBotConversation && assistantAvatar ? ( @@ -6707,7 +6706,7 @@ export function MessageStream({ // 伙伴聊天与普通任务同一套工作过程:运行中露最近 5 条活动与计时, // 组头按内容宽;最终答复后保留折叠过程,按需展开。 compact={false} - replyColumnSpacer={botReplyColumnSpacer} + companionAvatar={simplifiedBotConversation ? assistantAvatar : undefined} /> ); } diff --git a/apps/desktop/src/renderer/features/bots/__tests__/botChatSurface.test.ts b/apps/desktop/src/renderer/features/bots/__tests__/botChatSurface.test.ts index 7ee1b514226..12d167d119f 100644 --- a/apps/desktop/src/renderer/features/bots/__tests__/botChatSurface.test.ts +++ b/apps/desktop/src/renderer/features/bots/__tests__/botChatSurface.test.ts @@ -53,15 +53,15 @@ describe('消息流的头像挂载', () => { expect(helper).toContain('if (!avatar) return bubble;'); }); - it('只有 assistant 分支挂头像', () => { + it('回复和过程区挂头像,卡片保留占位', () => { expect(messageStream).toContain('return withAssistantAvatar(\n assistantAvatar,'); - // Assistant text shows the avatar; task and teammate-message cards plus the - // work fold and generated artifacts reserve the same space invisibly. + // Assistant text and work folds show avatars; task and teammate-message + // cards and generated artifacts reserve the same space invisibly. // User messages bypass it; fixture geometry covers pending/empty artifacts. expect(messageStream.match(/withAssistantAvatar\(/g)?.length).toBe(5); expect(messageStream).toMatch(/simplifiedBotConversation\s*&&\s*\(message\.systemCardType === 'bot-session-task'\s*\|\|\s*message\.systemCardType === 'bot-session-task-result'\s*\|\|\s*message\.systemCardType === 'bot-session-task-message'\s*\|\|\s*message\.systemCardType === 'bot-direct-message'\)\s*\? withAssistantAvatar\(/); expect(messageStream).toMatch(/