From 177ab2140efa7c81764d15c138e1271e759aa8c7 Mon Sep 17 00:00:00 2001 From: mesalogo Date: Wed, 9 Sep 2026 01:51:27 +0800 Subject: [PATCH] fix: align chat messages and composer without horizontal overflow Move message avatars into outer gutters and align message content with the composer while preserving existing colors and styling. Keep scrollbar space in the outer gutter to prevent horizontal scrolling. Release note: Chat avatars now sit outside message content, with the composer aligned to message edges and no extra horizontal scrollbar. --- UI-DESIGN.md | 2 ++ src/renderer/src/ChatTimeline.test.tsx | 10 +++--- src/renderer/src/ChatTimeline.tsx | 14 ++++---- src/renderer/src/WorkspacePrimitives.test.tsx | 2 +- src/renderer/src/styles.css | 33 ++++++++++++++++--- 5 files changed, 44 insertions(+), 17 deletions(-) diff --git a/UI-DESIGN.md b/UI-DESIGN.md index 6bfe95be..90f62427 100644 --- a/UI-DESIGN.md +++ b/UI-DESIGN.md @@ -581,6 +581,8 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证 ### 13.1 聊天 - 使用 `reading` 壳层,消息流与输入区共享宽度。 +- 消息头像放在正文区域外侧:助手头像居左,用户头像居右。输入框左边缘与助手正文左边缘对齐, + 右边缘与用户气泡右边缘对齐;两侧为头像保留独立安全边距,滚动条出现或工作栏缩放不得改变对齐。 - 对话标题和当前项目范围位于 `PageHeader` 或对话上下文区,不在消息流中重复。 - 已结束且包含正文的助手回复在底部提供带可访问名称和工具提示的复制图标;复制 Markdown 原文,不包含推理过程、工具日志或引用元数据,结果通过应用通知反馈。流式回复完成前不显示 diff --git a/src/renderer/src/ChatTimeline.test.tsx b/src/renderer/src/ChatTimeline.test.tsx index 86700e74..e1ed4e2d 100644 --- a/src/renderer/src/ChatTimeline.test.tsx +++ b/src/renderer/src/ChatTimeline.test.tsx @@ -116,11 +116,11 @@ describe('ChatTimeline', () => { expect(rule).not.toContain('contain-intrinsic') }) - it('places message identity above the message body', () => { + it.each(['assistant', 'user'] as const)('places the %s avatar outside the header and body', (role) => { const messages: Message[] = [ { id: 'assistant-message', - role: 'assistant', + role, content: 'Answer', createdAt: 1_775_000_000_000, state: 'complete' @@ -145,9 +145,11 @@ describe('ChatTimeline', () => { const header = message?.querySelector('.message__header') const body = message?.querySelector('.message__body') - expect(header).toContainElement(message?.querySelector('.message__avatar') ?? null) + const avatar = message?.querySelector('.message__avatar') + + expect(avatar).toHaveAttribute('aria-hidden', 'true') expect(header).toContainElement(message?.querySelector('.message__meta') ?? null) - expect(Array.from(message?.children ?? [])).toEqual([header, body]) + expect(Array.from(message?.children ?? [])).toEqual([avatar, header, body]) }) it('enables HTML rendering only for completed Agent output', () => { diff --git a/src/renderer/src/ChatTimeline.tsx b/src/renderer/src/ChatTimeline.tsx index c65fcb6d..dfa3623d 100644 --- a/src/renderer/src/ChatTimeline.tsx +++ b/src/renderer/src/ChatTimeline.tsx @@ -553,14 +553,14 @@ function ChatMessageRowView({ ref={(element) => onArticleRef(message.id, element)} tabIndex={-1} > +
-
- {message.role === 'assistant' ? ( - - ) : ( - - )} -
{message.role === 'assistant' ? 'GoodBuddy' : t('chat.user')} diff --git a/src/renderer/src/WorkspacePrimitives.test.tsx b/src/renderer/src/WorkspacePrimitives.test.tsx index 54fe8e5e..b3df9909 100644 --- a/src/renderer/src/WorkspacePrimitives.test.tsx +++ b/src/renderer/src/WorkspacePrimitives.test.tsx @@ -248,7 +248,7 @@ describe('WorkspacePrimitives', () => { /@media \(max-width: 719px\)\s*\{(?:[^{}]|\{[^{}]*\})*?\.assistant-sidebar/u ) expect(stylesheet).toMatch( - /\.composer-wrap\s*\{[^}]*width:\s*100%;[^}]*max-width:\s*100%;[^}]*min-width:\s*0;[^}]*padding:\s*var\(--space-3\)\s*max\(var\(--page-gutter\),\s*calc\(\(100% - var\(--content-reading\)\) \/ 2\)\)\s*var\(--space-2\);/u + /\.composer-wrap\s*\{[^}]*width:\s*100%;[^}]*max-width:\s*100%;[^}]*min-width:\s*0;[^}]*padding:\s*var\(--space-3\)\s*var\(--chat-inline-padding\)\s*var\(--space-2\);/u ) expect(stylesheet).toMatch( /\.assistant-sidebar__tabs\s*\{[^}]*grid-template-columns:\s*repeat\(5,\s*minmax\(max-content,\s*1fr\)\);/u diff --git a/src/renderer/src/styles.css b/src/renderer/src/styles.css index d378d8d4..b4f05f61 100644 --- a/src/renderer/src/styles.css +++ b/src/renderer/src/styles.css @@ -5190,12 +5190,21 @@ button > svg { display: none; } +.chat, +.composer-wrap { + --message-avatar-size: 29px; + --chat-inline-padding: max( + calc(var(--space-4) + var(--message-avatar-size) + var(--space-2)), + calc((100cqw - var(--content-reading)) / 2) + ); +} + .chat { height: 100%; min-height: 0; padding: var(--page-gutter) - max(var(--page-gutter), calc((100% - var(--content-reading)) / 2)) + 0 10px; overflow-y: auto; background: var(--surface-raised); @@ -5333,6 +5342,13 @@ button > svg { line-height: 1.5; } +.chat > .welcome, +.message-list { + /* Keep the scrollbar inside the outer gutter without adding trailing scroll padding. */ + width: calc(100cqw - 2 * var(--chat-inline-padding)); + margin-left: var(--chat-inline-padding); +} + .message-list { display: flex; min-width: 0; @@ -5363,9 +5379,10 @@ button > svg { } .message { + position: relative; display: flex; min-width: 0; - padding: 17px 8px; + padding: 17px 0; flex-direction: column; gap: 8px; } @@ -5373,6 +5390,7 @@ button > svg { .message__header { display: flex; min-width: 0; + min-height: var(--message-avatar-size); align-items: center; gap: 12px; } @@ -5434,10 +5452,13 @@ button > svg { } .message__avatar { + position: absolute; + top: 17px; + left: calc(-1 * (var(--message-avatar-size) + var(--space-2))); display: grid; flex: 0 0 auto; - width: 29px; - height: 29px; + width: var(--message-avatar-size); + height: var(--message-avatar-size); place-items: center; border: 1px solid var(--border-default); border-radius: 9px; @@ -5446,6 +5467,8 @@ button > svg { } .message--user .message__avatar { + right: calc(-1 * (var(--message-avatar-size) + var(--space-2))); + left: auto; border-color: var(--accent-selected); background: var(--accent-subtle); color: var(--accent); @@ -6511,7 +6534,7 @@ button > svg { min-width: 0; padding: var(--space-3) - max(var(--page-gutter), calc((100% - var(--content-reading)) / 2)) + var(--chat-inline-padding) var(--space-2); background: var(--surface-raised); }