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' ? (
-
- ) : (
-
- )}
-
{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);
}