style(vscode-ide-companion): update UI components and styling

- Updated chat message container spacing and styling
- Adjusted AssistantMessage text color handling
- Enhanced QwenMessageTimeline CSS rules for better visual flow
- Moved InfoBanner to ui directory for better organization
- Updated InputForm styling
- Added new CSS classes and updated tailwind configuration
- Improved timeline connection lines and message item spacing

These changes enhance the visual appearance and user experience of the chat interface.
This commit is contained in:
yiliang114
2025-12-04 08:28:54 +08:00
parent 86cd06ef43
commit 3053e6c41f
10 changed files with 185 additions and 36 deletions

View File

@@ -32,7 +32,7 @@ import { EmptyState } from './components/ui/EmptyState.js';
import type { PlanEntry } from './components/PlanDisplay.js'; import type { PlanEntry } from './components/PlanDisplay.js';
import { type CompletionItem } from './types/CompletionTypes.js'; import { type CompletionItem } from './types/CompletionTypes.js';
import { useCompletionTrigger } from './hooks/useCompletionTrigger.js'; import { useCompletionTrigger } from './hooks/useCompletionTrigger.js';
import { InfoBanner } from './components/InfoBanner.js'; import { InfoBanner } from './components/ui/InfoBanner.js';
import { ChatHeader } from './components/ui/layouts/ChatHeader.js'; import { ChatHeader } from './components/ui/layouts/ChatHeader.js';
import { import {
UserMessage, UserMessage,
@@ -487,7 +487,7 @@ export const App: React.FC = () => {
<div <div
ref={messagesContainerRef} ref={messagesContainerRef}
className="chat-messages flex-1 overflow-y-auto overflow-x-hidden pt-5 pr-5 pl-5 pb-[120px] flex flex-col relative min-w-0 focus:outline-none [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-thumb]:rounded-sm [&::-webkit-scrollbar-thumb:hover]:bg-white/30 [&>*]:flex [&>*]:gap-0 [&>*]:items-start [&>*]:text-left [&>*]:py-2 [&>*:not(:last-child)]:pb-[10px] [&>.message-item]:px-0 [&>.message-item]:py-0 [&>*]:flex-col [&>*]:relative [&>*]:animate-[fadeIn_0.2s_ease-in]" className="chat-messages flex-1 overflow-y-auto overflow-x-hidden pt-5 pr-5 pl-5 pb-[120px] flex flex-col relative min-w-0 focus:outline-none [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-thumb]:rounded-sm [&::-webkit-scrollbar-thumb:hover]:bg-white/30 [&>*]:flex [&>*]:gap-0 [&>*]:items-start [&>*]:text-left [&>*]:py-2 [&>*:not(:last-child)]:pb-[8px] [&>*]:flex-col [&>*]:relative [&>*]:animate-[fadeIn_0.2s_ease-in]"
style={{ backgroundColor: 'var(--app-primary-background)' }} style={{ backgroundColor: 'var(--app-primary-background)' }}
> >
{!hasContent ? ( {!hasContent ? (

View File

@@ -201,6 +201,9 @@ export const InputForm: React.FC<InputFormProps> = ({
</button> </button>
)} )}
{/* Spacer */}
<div className="flex-1 min-w-0" />
{/* Thinking button */} {/* Thinking button */}
<button <button
type="button" type="button"

View File

@@ -66,11 +66,10 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({
paddingBottom: '8px', paddingBottom: '8px',
}} }}
> >
<span style={{ color: 'var(--app-secondary-foreground)', width: '100%' }}> <span style={{ width: '100%' }}>
<p <p
style={{ style={{
margin: 0, margin: 0,
color: 'var(--app-secondary-foreground)',
width: '100%', width: '100%',
wordWrap: 'break-word', wordWrap: 'break-word',
overflowWrap: 'break-word', overflowWrap: 'break-word',

View File

@@ -8,7 +8,7 @@
* 实现原理: * 实现原理:
* 1. 为所有AI消息项(qwen-message.message-item:not(.user-message-container))创建垂直连接线 * 1. 为所有AI消息项(qwen-message.message-item:not(.user-message-container))创建垂直连接线
* 2. 当用户消息(user-message-container)隔断AI消息序列时会自动重新开始一组新的时间线规则 * 2. 当用户消息(user-message-container)隔断AI消息序列时会自动重新开始一组新的时间线规则
* 3. 每组AI消息序列的开始元素(top设为15px),结束元素(bottom设为calc(100% - 15px)) * 3. 每组AI消息序列的开始元素(top设为15px),结束元素(底部预留15px)
*/ */
/* 默认的连接线样式 - 为所有AI消息项创建完整高度的连接线 */ /* 默认的连接线样式 - 为所有AI消息项创建完整高度的连接线 */
@@ -25,13 +25,37 @@
/* 处理每组AI消息序列的开始 - 包括整个消息列表的第一个AI消息和被用户消息隔断后的新AI消息 */ /* 处理每组AI消息序列的开始 - 包括整个消息列表的第一个AI消息和被用户消息隔断后的新AI消息 */
.qwen-message.message-item:not(.user-message-container):first-child::after, .qwen-message.message-item:not(.user-message-container):first-child::after,
.user-message-container + .qwen-message.message-item:not(.user-message-container)::after { .user-message-container + .qwen-message.message-item:not(.user-message-container)::after,
/* 如果前一个兄弟不是 .qwen-message.message-item例如等待提示、哨兵元素或卡片样式工具调用也作为一组新开始 */
.chat-messages > :not(.qwen-message.message-item)
+ .qwen-message.message-item:not(.user-message-container)::after {
top: 15px; top: 15px;
} }
/* 处理每组AI消息序列的结尾 */ /* 处理每组AI消息序列的结尾 */
.qwen-message.message-item:not(.user-message-container):last-child::after, /* 后一个兄弟是用户消息时 */
.qwen-message.message-item:not(.user-message-container):has(+ .user-message-container)::after { .qwen-message.message-item:not(.user-message-container):has(+ .user-message-container)::after,
bottom: calc(100% - 15px); /* 或者后一个兄弟不是 .qwen-message.message-item如等待提示、哨兵元素、卡片样式工具调用等时 */
.qwen-message.message-item:not(.user-message-container):has(+ :not(.qwen-message.message-item))::after,
/* 真正是父容器最后一个子元素时 */
.qwen-message.message-item:not(.user-message-container):last-child::after {
/* 注意:同时设置 top 和 bottom 时高度为 (容器高度 - top - bottom)。
* 这里期望“底部留 15px 间距”,因此 bottom 应为 15px而不是 calc(100% - 15px))。 */
top: 0; top: 0;
bottom: calc(100% - 15px);
}
.user-message-container:first-child {
margin-top: 0;
}
.message-item {
padding: 8px 0;
width: 100%;
align-items: flex-start;
padding-left: 30px;
user-select: text;
position: relative;
padding-top: 8px;
padding-bottom: 8px;
} }

View File

@@ -45,7 +45,7 @@ export const UserMessage: React.FC<UserMessageProps> = ({
return ( return (
<div <div
className="qwen-message message-item user-message-container flex gap-0 items-start text-left flex-col relative animate-[fadeIn_0.2s_ease-in]" className="qwen-message user-message-container flex gap-0 my-1 items-start text-left flex-col relative animate-[fadeIn_0.2s_ease-in]"
style={{ position: 'relative' }} style={{ position: 'relative' }}
> >
<div <div

View File

@@ -4,21 +4,19 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
* *
* Simplified timeline styles for tool calls and messages * Simplified timeline styles for tool calls and messages
* Merged version of both SimpleTimeline.css files * Only keeping actually used styles
*/ */
/* Basic timeline container */ /* ToolCallContainer timeline styles (from LayoutComponents.css) */
.simple-toolcall-container, .toolcall-container {
.simple-timeline-container {
position: relative; position: relative;
padding-left: 30px; padding-left: 30px;
padding-top: 8px; padding-top: 8px;
padding-bottom: 8px; padding-bottom: 8px;
} }
/* Timeline connector - simple version */ /* ToolCallContainer timeline connector */
.simple-toolcall-container::after, .toolcall-container::after {
.simple-timeline-container::after {
content: ''; content: '';
position: absolute; position: absolute;
left: 12px; left: 12px;
@@ -28,28 +26,45 @@
background-color: var(--app-primary-border-color); background-color: var(--app-primary-border-color);
} }
/* First item connector starts lower */ /* First item: connector starts from status point position */
.simple-toolcall-container:first-child::after, .toolcall-container:first-child::after {
.simple-timeline-container:first-child::after {
top: 24px; top: 24px;
} }
/* Last item connector ends higher */ /* Last item: connector shows only upper part */
.simple-toolcall-container:last-child::after, .toolcall-container:last-child::after {
.simple-timeline-container:last-child::after {
height: calc(100% - 24px); height: calc(100% - 24px);
top: 0; top: 0;
bottom: auto; bottom: auto;
} }
/* Bullet point */ /* AssistantMessage timeline styles */
.simple-toolcall-container::before, .assistant-message-container {
.simple-timeline-container::before { position: relative;
content: '\25cf'; padding-left: 30px;
position: absolute; padding-top: 8px;
left: 8px; padding-bottom: 8px;
padding-top: 2px; }
font-size: 10px;
color: var(--app-secondary-foreground); /* AssistantMessage timeline connector */
z-index: 2; .assistant-message-container::after {
content: '';
position: absolute;
left: 12px;
top: 0;
bottom: 0;
width: 1px;
background-color: var(--app-primary-border-color);
}
/* First item: connector starts from status point position */
.assistant-message-container:first-child::after {
top: 24px;
}
/* Last item: connector shows only upper part */
.assistant-message-container:last-child::after {
height: calc(100% - 24px);
top: 0;
bottom: auto;
} }

View File

@@ -0,0 +1,107 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { TerminalIcon, CloseIcon } from '../icons/index.js';
interface InfoBannerProps {
/**
* Whether the banner is visible
*/
visible: boolean;
/**
* Callback when the banner is dismissed
*/
onDismiss: () => void;
/**
* Optional: Custom message content (if not provided, uses default)
*/
message?: React.ReactNode;
/**
* Optional: Custom link text
*/
linkText?: string;
/**
* Optional: Callback when the link is clicked
*/
onLinkClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
}
export const InfoBanner: React.FC<InfoBannerProps> = ({
visible,
onDismiss,
message,
linkText = 'Switch back in Settings.',
onLinkClick,
}) => {
if (!visible) {
return null;
}
return (
<div
className="flex items-center justify-between"
style={{
gap: '12px',
padding: '8px 16px',
}}
>
<div className="flex items-center flex-1 min-w-0" style={{ gap: '8px' }}>
{/* Icon */}
<TerminalIcon className="flex-shrink-0 w-4 h-4" />
{/* Message */}
<label
className="m-0 leading-snug text-[11px]"
style={{ color: 'var(--app-primary-foreground)' }}
>
{message || (
<>
Prefer the Terminal experience?{' '}
{onLinkClick && (
<a
href="#"
className="no-underline hover:underline cursor-pointer outline-none"
style={{ color: 'var(--app-qwen-orange)' }}
onClick={onLinkClick}
>
{linkText}
</a>
)}
</>
)}
</label>
</div>
{/* Close button */}
<button
className="flex items-center justify-center cursor-pointer rounded"
style={{
background: 'none',
border: 'none',
padding: '6px',
color: 'var(--app-secondary-foreground)',
borderRadius: '4px',
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor =
'var(--app-ghost-button-hover-background)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
}}
aria-label="Close banner"
onClick={onDismiss}
>
<CloseIcon className="w-[10px] h-[10px]" />
</button>
</div>
);
};

View File

@@ -10,8 +10,9 @@
/* Import component styles */ /* Import component styles */
@import '../components/toolcalls/shared/DiffDisplay.css'; @import '../components/toolcalls/shared/DiffDisplay.css';
@import '../components/messages/AssistantMessage.css'; @import '../components/messages/AssistantMessage.css';
@import '../components/toolcalls/shared/MergedSimpleTimeline.css'; @import '../components/toolcalls/shared/SimpleTimeline.css';
@import '../components/messages/QwenMessageTimeline.css'; @import '../components/messages/QwenMessageTimeline.css';
@import '../components/MarkdownRenderer.css';
/* =========================== /* ===========================

View File

@@ -95,7 +95,8 @@
color: var(--app-secondary-foreground); color: var(--app-secondary-foreground);
} }
.btn-text-compact--primary { .btn-text-compact--primary {
color: var(--app-primary-foreground); color: var(--app-secondary-foreground);
/* color: var(--app-primary-foreground); */
} }
.btn-text-compact:hover { .btn-text-compact:hover {
background-color: var(--app-ghost-button-hover-background); background-color: var(--app-ghost-button-hover-background);

View File

@@ -15,7 +15,6 @@ export default {
'./src/webview/components/toolcalls/**/*.{js,jsx,ts,tsx}', './src/webview/components/toolcalls/**/*.{js,jsx,ts,tsx}',
'./src/webview/components/InProgressToolCall.tsx', './src/webview/components/InProgressToolCall.tsx',
'./src/webview/components/MessageContent.tsx', './src/webview/components/MessageContent.tsx',
'./src/webview/components/InfoBanner.tsx',
'./src/webview/components/InputForm.tsx', './src/webview/components/InputForm.tsx',
'./src/webview/components/PermissionDrawer.tsx', './src/webview/components/PermissionDrawer.tsx',
'./src/webview/components/PlanDisplay.tsx', './src/webview/components/PlanDisplay.tsx',