style(vscode-ide-companion): form component style opt

This commit is contained in:
yiliang114
2025-12-01 00:15:18 +08:00
parent 1b37d729cb
commit ed0d5f67db
12 changed files with 223 additions and 117 deletions

View File

@@ -0,0 +1,93 @@
import * as vscode from 'vscode';
import type { DiffManager } from '../diff-manager.js';
import type { WebViewProvider } from '../webview/WebViewProvider.js';
type Logger = (message: string) => void;
export function registerNewCommands(
context: vscode.ExtensionContext,
log: Logger,
diffManager: DiffManager,
getWebViewProviders: () => WebViewProvider[],
createWebViewProvider: () => WebViewProvider,
): void {
const disposables: vscode.Disposable[] = [];
// qwenCode.showDiff
disposables.push(
vscode.commands.registerCommand(
'qwenCode.showDiff',
async (args: { path: string; oldText: string; newText: string }) => {
log(`[Command] showDiff called for: ${args.path}`);
try {
let absolutePath = args.path;
if (!args.path.startsWith('/') && !args.path.match(/^[a-zA-Z]:/)) {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (workspaceFolder) {
absolutePath = vscode.Uri.joinPath(
workspaceFolder.uri,
args.path,
).fsPath;
}
}
await diffManager.showDiff(absolutePath, args.oldText, args.newText);
} catch (error) {
log(`[Command] Error showing diff: ${error}`);
vscode.window.showErrorMessage(`Failed to show diff: ${error}`);
}
},
),
);
// qwenCode.openChat
disposables.push(
vscode.commands.registerCommand('qwenCode.openChat', () => {
const providers = getWebViewProviders();
if (providers.length > 0) {
providers[providers.length - 1].show();
} else {
const provider = createWebViewProvider();
provider.show();
}
}),
);
// qwenCode.openNewChatTab (not contributed in package.json; used programmatically)
disposables.push(
vscode.commands.registerCommand('qwenCode.openNewChatTab', () => {
const provider = createWebViewProvider();
provider.show();
}),
);
// qwenCode.clearAuthCache
disposables.push(
vscode.commands.registerCommand('qwenCode.clearAuthCache', async () => {
const providers = getWebViewProviders();
for (const provider of providers) {
await provider.clearAuthCache();
}
vscode.window.showInformationMessage(
'Qwen Code authentication cache cleared. You will need to login again on next connection.',
);
log('Auth cache cleared by user');
}),
);
// qwenCode.login
disposables.push(
vscode.commands.registerCommand('qwenCode.login', async () => {
const providers = getWebViewProviders();
if (providers.length > 0) {
await providers[providers.length - 1].forceReLogin();
} else {
vscode.window.showInformationMessage(
'Please open Qwen Code chat first before logging in.',
);
}
}),
);
context.subscriptions.push(...disposables);
}

View File

@@ -15,6 +15,7 @@ import {
type IdeInfo, type IdeInfo,
} from '@qwen-code/qwen-code-core/src/ide/detect-ide.js'; } from '@qwen-code/qwen-code-core/src/ide/detect-ide.js';
import { WebViewProvider } from './webview/WebViewProvider.js'; import { WebViewProvider } from './webview/WebViewProvider.js';
import { registerNewCommands } from './commands/index.js';
const CLI_IDE_COMPANION_IDENTIFIER = 'qwenlm.qwen-code-vscode-ide-companion'; const CLI_IDE_COMPANION_IDENTIFIER = 'qwenlm.qwen-code-vscode-ide-companion';
const INFO_MESSAGE_SHOWN_KEY = 'qwenCodeInfoMessageShown'; const INFO_MESSAGE_SHOWN_KEY = 'qwenCodeInfoMessageShown';
@@ -156,6 +157,15 @@ export async function activate(context: vscode.ExtensionContext) {
}), }),
); );
// Register newly added commands via commands module
registerNewCommands(
context,
log,
diffManager,
() => webViewProviders,
createWebViewProvider,
);
context.subscriptions.push( context.subscriptions.push(
vscode.workspace.onDidCloseTextDocument((doc) => { vscode.workspace.onDidCloseTextDocument((doc) => {
if (doc.uri.scheme === DIFF_SCHEME) { if (doc.uri.scheme === DIFF_SCHEME) {
@@ -178,70 +188,6 @@ export async function activate(context: vscode.ExtensionContext) {
diffManager.cancelDiff(docUri); diffManager.cancelDiff(docUri);
} }
}), }),
vscode.commands.registerCommand(
'qwenCode.showDiff',
async (args: { path: string; oldText: string; newText: string }) => {
log(`[Command] showDiff called for: ${args.path}`);
try {
// Convert relative path to absolute if needed
let absolutePath = args.path;
if (!args.path.startsWith('/') && !args.path.match(/^[a-zA-Z]:/)) {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (workspaceFolder) {
absolutePath = vscode.Uri.joinPath(
workspaceFolder.uri,
args.path,
).fsPath;
}
}
await diffManager.showDiff(absolutePath, args.oldText, args.newText);
} catch (error) {
log(`[Command] Error showing diff: ${error}`);
vscode.window.showErrorMessage(`Failed to show diff: ${error}`);
}
},
),
vscode.commands.registerCommand('qwenCode.openChat', () => {
// Open or reveal the most recent chat tab
if (webViewProviders.length > 0) {
const lastProvider = webViewProviders[webViewProviders.length - 1];
lastProvider.show();
} else {
// Create first chat tab
const provider = createWebViewProvider();
provider.show();
}
}),
vscode.commands.registerCommand('qwenCode.openNewChatTab', () => {
// Always create a new WebviewPanel (tab) in the same view column
// The PanelManager will find existing Qwen Code tabs and open in the same column
const provider = createWebViewProvider();
provider.show();
}),
vscode.commands.registerCommand('qwenCode.clearAuthCache', async () => {
// Clear auth state for all WebView providers
for (const provider of webViewProviders) {
await provider.clearAuthCache();
}
vscode.window.showInformationMessage(
'Qwen Code authentication cache cleared. You will need to login again on next connection.',
);
log('Auth cache cleared by user');
}),
vscode.commands.registerCommand('qwenCode.login', async () => {
// Get the current WebViewProvider instance - must already exist
if (webViewProviders.length > 0) {
const provider = webViewProviders[webViewProviders.length - 1];
await provider.forceReLogin();
} else {
// No WebViewProvider exists, show a message to user
vscode.window.showInformationMessage(
'Please open Qwen Code chat first before logging in.',
);
}
}),
); );
ideServer = new IDEServer(log, diffManager); ideServer = new IDEServer(log, diffManager);

View File

@@ -4,7 +4,13 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import React, { useState, useEffect, useRef, useCallback } from 'react'; import React, {
useState,
useEffect,
useRef,
useCallback,
useLayoutEffect,
} from 'react';
import { useVSCode } from './hooks/useVSCode.js'; import { useVSCode } from './hooks/useVSCode.js';
import { useSessionManagement } from './hooks/session/useSessionManagement.js'; import { useSessionManagement } from './hooks/session/useSessionManagement.js';
import { useFileContext } from './hooks/file/useFileContext.js'; import { useFileContext } from './hooks/file/useFileContext.js';
@@ -181,22 +187,40 @@ export const App: React.FC = () => {
// Auto-scroll handling: keep the view pinned to bottom when new content arrives, // Auto-scroll handling: keep the view pinned to bottom when new content arrives,
// but don't interrupt the user if they scrolled up. // but don't interrupt the user if they scrolled up.
// We track whether the user is currently "pinned" to the bottom (near the end).
const [pinnedToBottom, setPinnedToBottom] = useState(true);
const prevCountsRef = useRef({ msgLen: 0, inProgLen: 0, doneLen: 0 }); const prevCountsRef = useRef({ msgLen: 0, inProgLen: 0, doneLen: 0 });
// Observe scroll position to know if user has scrolled away from the bottom.
useEffect(() => { useEffect(() => {
const container = messagesContainerRef.current; const container = messagesContainerRef.current;
const endEl = messagesEndRef.current; if (!container) {
if (!container || !endEl) {
return; return;
} }
const nearBottom = () => { const onScroll = () => {
const threshold = 64; // px tolerance // Use a small threshold so slight deltas don't flip the state.
return ( // Note: there's extra bottom padding for the input area, so keep this a bit generous.
container.scrollTop + container.clientHeight >= const threshold = 80; // px tolerance
container.scrollHeight - threshold const distanceFromBottom =
); container.scrollHeight - (container.scrollTop + container.clientHeight);
setPinnedToBottom(distanceFromBottom <= threshold);
}; };
// Initialize once mounted so first render is correct
onScroll();
container.addEventListener('scroll', onScroll, { passive: true });
return () => container.removeEventListener('scroll', onScroll);
}, []);
// When content changes, if the user is pinned to bottom, keep it anchored there.
// Only smooth-scroll when new items are appended; do not smooth for streaming chunk updates.
useLayoutEffect(() => {
const container = messagesContainerRef.current;
if (!container) {
return;
}
// Detect whether new items were appended (vs. streaming chunk updates) // Detect whether new items were appended (vs. streaming chunk updates)
const prev = prevCountsRef.current; const prev = prevCountsRef.current;
const newMsg = messageHandling.messages.length > prev.msgLen; const newMsg = messageHandling.messages.length > prev.msgLen;
@@ -208,26 +232,22 @@ export const App: React.FC = () => {
doneLen: completedToolCalls.length, doneLen: completedToolCalls.length,
}; };
// If user is near bottom, or if we just appended a new item, scroll to bottom if (!pinnedToBottom) {
if (nearBottom() || newMsg || newInProg || newDone) { // Do nothing if user scrolled away; avoid stealing scroll.
// Try scrollIntoView first return;
const smooth = newMsg || newInProg || newDone; // avoid smooth on streaming chunks
endEl.scrollIntoView({
behavior: smooth ? 'smooth' : 'auto',
block: 'end',
});
// Fallback: directly set scrollTop if scrollIntoView doesn't work
setTimeout(() => {
if (container && endEl) {
const shouldScroll = nearBottom() || newMsg || newInProg || newDone;
if (shouldScroll) {
container.scrollTop = container.scrollHeight;
}
}
}, 50);
} }
const smooth = newMsg || newInProg || newDone; // avoid smooth on streaming chunks
// Anchor to the bottom on next frame to avoid layout thrash.
const raf = requestAnimationFrame(() => {
const top = container.scrollHeight - container.clientHeight;
// Use scrollTo to avoid cross-context issues with scrollIntoView.
container.scrollTo({ top, behavior: smooth ? 'smooth' : 'auto' });
});
return () => cancelAnimationFrame(raf);
}, [ }, [
pinnedToBottom,
messageHandling.messages, messageHandling.messages,
inProgressToolCalls, inProgressToolCalls,
completedToolCalls, completedToolCalls,
@@ -237,6 +257,45 @@ export const App: React.FC = () => {
planEntries, planEntries,
]); ]);
// When the last rendered item resizes (e.g., images/code blocks load/expand),
// if we're pinned to bottom, keep it anchored there.
useEffect(() => {
const container = messagesContainerRef.current;
const endEl = messagesEndRef.current;
if (!container || !endEl) {
return;
}
const lastItem = endEl.previousElementSibling as HTMLElement | null;
if (!lastItem) {
return;
}
let frame = 0;
const ro = new ResizeObserver(() => {
if (!pinnedToBottom) {
return;
}
// Defer to next frame to avoid thrash during rapid size changes
cancelAnimationFrame(frame);
frame = requestAnimationFrame(() => {
const top = container.scrollHeight - container.clientHeight;
container.scrollTo({ top });
});
});
ro.observe(lastItem);
return () => {
cancelAnimationFrame(frame);
ro.disconnect();
};
}, [
pinnedToBottom,
messageHandling.messages,
inProgressToolCalls,
completedToolCalls,
]);
// Handle permission response // Handle permission response
const handlePermissionResponse = useCallback( const handlePermissionResponse = useCallback(
(optionId: string) => { (optionId: string) => {
@@ -418,7 +477,7 @@ export const App: React.FC = () => {
<div <div
ref={messagesContainerRef} ref={messagesContainerRef}
className="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 [&>.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)]:mb-[10px] [&>.message-item]:px-0 [&>.message-item]:py-0 [&>*]: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

@@ -15,7 +15,7 @@ import {
LinkIcon, LinkIcon,
ArrowUpIcon, ArrowUpIcon,
} from './icons/index.js'; } from './icons/index.js';
import { ClaudeCompletionMenu } from './ui/ClaudeCompletionMenu.js'; import { CompletionMenu } from './ui/CompletionMenu.js';
import type { CompletionItem } from './CompletionTypes.js'; import type { CompletionItem } from './CompletionTypes.js';
type EditMode = 'ask' | 'auto' | 'plan'; type EditMode = 'ask' | 'auto' | 'plan';
@@ -124,11 +124,10 @@ export const InputForm: React.FC<InputFormProps> = ({
> >
<div className="block"> <div className="block">
<form <form
className="relative flex flex-col rounded-large border shadow-sm transition-all duration-200 focus-within:shadow-md" className="relative flex flex-col rounded-large border border-[var(--app-input-border)] shadow-sm transition-colors duration-200 focus-within:border-[var(--app-input-highlight)] focus-within:[box-shadow:0_1px_2px_color-mix(in_srgb,var(--app-input-highlight),transparent_80%)]"
style={{ style={{
backgroundColor: backgroundColor:
'var(--app-input-secondary-background, var(--app-input-background))', 'var(--app-input-secondary-background, var(--app-input-background))',
borderColor: 'var(--app-input-border)',
color: 'var(--app-input-foreground)', color: 'var(--app-input-foreground)',
}} }}
onSubmit={onSubmit} onSubmit={onSubmit}
@@ -151,7 +150,7 @@ export const InputForm: React.FC<InputFormProps> = ({
onCompletionSelect && onCompletionSelect &&
onCompletionClose && ( onCompletionClose && (
// Render dropdown above the input, matching Claude Code // Render dropdown above the input, matching Claude Code
<ClaudeCompletionMenu <CompletionMenu
items={completionItems} items={completionItems}
onSelect={onCompletionSelect} onSelect={onCompletionSelect}
onClose={onCompletionClose} onClose={onCompletionClose}
@@ -193,26 +192,30 @@ export const InputForm: React.FC<InputFormProps> = ({
{/* Edit mode button */} {/* Edit mode button */}
<button <button
type="button" type="button"
className="flex items-center gap-1.5 px-2.5 py-1.5 h-8 bg-transparent border border-transparent rounded-small cursor-pointer text-xs whitespace-nowrap transition-colors duration-150 hover:bg-[var(--app-ghost-button-hover-background)] [&>svg]:w-4 [&>svg]:h-4 [&>svg]:flex-shrink-0" className="flex items-center gap-1.5 px-2.5 py-1.5 h-8 bg-transparent border border-transparent rounded-small cursor-pointer text-xs transition-colors duration-150 hover:bg-[var(--app-ghost-button-hover-background)] min-w-0 flex-shrink overflow-hidden [&>svg]:w-4 [&>svg]:h-4 [&>svg]:flex-shrink-0"
style={{ color: 'var(--app-primary-foreground)' }} style={{ color: 'var(--app-primary-foreground)' }}
title={editModeInfo.title} title={editModeInfo.title}
onClick={onToggleEditMode} onClick={onToggleEditMode}
> >
{editModeInfo.icon} {editModeInfo.icon}
<span>{editModeInfo.text}</span> {/* Let the label truncate with ellipsis; hide on very small screens */}
<span className="min-w-0 truncate hidden sm:inline">
{editModeInfo.text}
</span>
</button> </button>
{/* Active file indicator */} {/* Active file indicator */}
{activeFileName && ( {activeFileName && (
<button <button
type="button" type="button"
className="flex items-center gap-1.5 px-2.5 py-1.5 h-8 bg-transparent border border-transparent rounded-small cursor-pointer text-xs whitespace-nowrap transition-colors duration-150 hover:bg-[var(--app-ghost-button-hover-background)] [&>svg]:w-4 [&>svg]:h-4 [&>svg]:flex-shrink-0 max-w-[200px] overflow-hidden text-ellipsis flex-shrink min-w-0" className="flex items-center gap-1.5 px-2.5 py-1.5 h-8 bg-transparent border border-transparent rounded-small cursor-pointer text-xs transition-colors duration-150 hover:bg-[var(--app-ghost-button-hover-background)] [&>svg]:w-4 [&>svg]:h-4 [&>svg]:flex-shrink-0 max-w-[200px] flex-shrink min-w-0 overflow-hidden"
style={{ color: 'var(--app-primary-foreground)' }} style={{ color: 'var(--app-primary-foreground)' }}
title={`Showing Qwen Code your current file selection: ${activeFileName}${activeSelection ? `#${activeSelection.startLine}-${activeSelection.endLine}` : ''}`} title={`Showing Qwen Code your current file selection: ${activeFileName}${activeSelection ? `#${activeSelection.startLine}-${activeSelection.endLine}` : ''}`}
onClick={onFocusActiveEditor} onClick={onFocusActiveEditor}
> >
<CodeBracketsIcon /> <CodeBracketsIcon />
<span> {/* Truncate file path/selection; hide label on very small screens */}
<span className="min-w-0 truncate hidden sm:inline">
{activeFileName} {activeFileName}
{activeSelection && {activeSelection &&
` #${activeSelection.startLine}${activeSelection.startLine !== activeSelection.endLine ? `-${activeSelection.endLine}` : ''}`} ` #${activeSelection.startLine}${activeSelection.startLine !== activeSelection.endLine ? `-${activeSelection.endLine}` : ''}`}

View File

@@ -4,12 +4,6 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
/**
* PlanDisplay.css -> Tailwind 化
* 说明:尽量用 @apply把原有类名保留便于调试
* 仅在必须的地方保留少量原生 CSS如关键帧
*/
/* 容器 */ /* 容器 */
.plan-display { .plan-display {
@apply bg-transparent border-0 py-2 px-4 my-2; @apply bg-transparent border-0 py-2 px-4 my-2;

View File

@@ -102,7 +102,8 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
onClick={openFirstDiff} onClick={openFirstDiff}
title="Open diff in VS Code" title="Open diff in VS Code"
> >
<span className="absolute left-2 top-[10px] text-[10px] text-[#74c991]"> {/* Center the bullet vertically like the shared container */}
<span className="absolute left-2 top-1/2 -translate-y-1/2 text-[10px] leading-none text-[#74c991]">
</span> </span>
{/* Keep content within overall width: pl-[30px] provides the bullet indent; */} {/* Keep content within overall width: pl-[30px] provides the bullet indent; */}
@@ -110,7 +111,8 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
<div className="toolcall-edit-content flex flex-col gap-1 pl-[30px] min-w-0 max-w-full"> <div className="toolcall-edit-content flex flex-col gap-1 pl-[30px] min-w-0 max-w-full">
<div className="flex items-center justify-between min-w-0"> <div className="flex items-center justify-between min-w-0">
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<span className="text-[13px] font-medium text-[var(--app-primary-foreground)]"> {/* Align the inline Edit label styling with shared toolcall label: larger + bold */}
<span className="text-[14px] leading-none font-bold text-[var(--app-primary-foreground)]">
Edit Edit
</span> </span>
{path && ( {path && (

View File

@@ -23,9 +23,6 @@ export const ReadToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
// Group content by type // Group content by type
const { errors } = groupContent(content); const { errors } = groupContent(content);
// Extract filename from path
// const getFileName = (path: string): string => path.split('/').pop() || path;
// Error case: show error // Error case: show error
if (errors.length > 0) { if (errors.length > 0) {
const path = locations?.[0]?.path || ''; const path = locations?.[0]?.path || '';

View File

@@ -68,7 +68,8 @@ export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
> >
</span> </span>
<span className="text-[13px] font-medium text-[var(--app-primary-foreground)]">
<span className="text-[14px] leading-none font-bold text-[var(--app-primary-foreground)]">
{label} {label}
</span> </span>
{/* {toolCallId && ( {/* {toolCallId && (

View File

@@ -8,7 +8,7 @@ import type React from 'react';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import type { CompletionItem } from '../CompletionTypes.js'; import type { CompletionItem } from '../CompletionTypes.js';
interface ClaudeCompletionMenuProps { interface CompletionMenuProps {
items: CompletionItem[]; items: CompletionItem[];
onSelect: (item: CompletionItem) => void; onSelect: (item: CompletionItem) => void;
onClose: () => void; onClose: () => void;
@@ -20,7 +20,7 @@ interface ClaudeCompletionMenuProps {
* Claude Code-like anchored dropdown rendered above the input field. * Claude Code-like anchored dropdown rendered above the input field.
* Keyboard: Up/Down to move, Enter to select, Esc to close. * Keyboard: Up/Down to move, Enter to select, Esc to close.
*/ */
export const ClaudeCompletionMenu: React.FC<ClaudeCompletionMenuProps> = ({ export const CompletionMenu: React.FC<CompletionMenuProps> = ({
items, items,
onSelect, onSelect,
onClose, onClose,

View File

@@ -115,7 +115,8 @@ export const FileLink: React.FC<FileLinkProps> = ({
// Keep a semantic handle for scoped overrides (e.g. DiffDisplay.css) // Keep a semantic handle for scoped overrides (e.g. DiffDisplay.css)
'file-link', 'file-link',
// Layout + interaction // Layout + interaction
'inline-flex items-baseline', // Use items-center + leading-none to vertically center within surrounding rows
'inline-flex items-center leading-none',
disableClick disableClick
? 'pointer-events-none cursor-[inherit] hover:no-underline' ? 'pointer-events-none cursor-[inherit] hover:no-underline'
: 'cursor-pointer', : 'cursor-pointer',
@@ -136,7 +137,7 @@ export const FileLink: React.FC<FileLinkProps> = ({
aria-label={`Open file: ${fullDisplayText}`} aria-label={`Open file: ${fullDisplayText}`}
// Inherit font family from context so it matches theme body text. // Inherit font family from context so it matches theme body text.
> >
<span className="file-link-path font-medium">{displayPath}</span> <span className="file-link-path">{displayPath}</span>
{line !== null && line !== undefined && ( {line !== null && line !== undefined && (
<span className="file-link-location opacity-70 text-[0.9em] font-normal dark:opacity-60"> <span className="file-link-location opacity-70 text-[0.9em] font-normal dark:opacity-60">
:{line} :{line}

View File

@@ -45,6 +45,8 @@
--app-input-active-border: var(--vscode-inputOption-activeBorder); --app-input-active-border: var(--vscode-inputOption-activeBorder);
--app-input-placeholder-foreground: var(--vscode-input-placeholderForeground); --app-input-placeholder-foreground: var(--vscode-input-placeholderForeground);
--app-input-secondary-background: var(--vscode-menu-background); --app-input-secondary-background: var(--vscode-menu-background);
/* Input Highlight (focus ring/border) */
--app-input-highlight: var(--app-qwen-orange);
/* Code Highlighting */ /* Code Highlighting */
--app-code-background: var(--vscode-textCodeBlock-background, rgba(0, 0, 0, 0.05)); --app-code-background: var(--vscode-textCodeBlock-background, rgba(0, 0, 0, 0.05));
@@ -93,6 +95,8 @@
/* Light Theme Overrides */ /* Light Theme Overrides */
.vscode-light { .vscode-light {
--app-transparent-inner-border: rgba(0, 0, 0, 0.07); --app-transparent-inner-border: rgba(0, 0, 0, 0.07);
/* Slightly different brand shade in light theme for better contrast */
--app-input-highlight: var(--app-qwen-clay-button-orange);
} }
/* Icon SVG styles */ /* Icon SVG styles */
@@ -144,6 +148,12 @@ button {
color: var(--app-primary-foreground); color: var(--app-primary-foreground);
} }
/* Message list container: prevent browser scroll anchoring from fighting our manual pin-to-bottom logic */
.chat-messages > * {
/* Disable overflow anchoring on individual items so the UA doesn't auto-adjust scroll */
overflow-anchor: none;
}
/* =========================== /* ===========================
Animations (used by message components) Animations (used by message components)
=========================== */ =========================== */

View File

@@ -24,7 +24,7 @@ export default {
theme: { theme: {
extend: { extend: {
keyframes: { keyframes: {
// ClaudeCompletionMenu mount animation: fade in + slight upward slide // CompletionMenu mount animation: fade in + slight upward slide
'completion-menu-enter': { 'completion-menu-enter': {
'0%': { opacity: '0', transform: 'translateY(4px)' }, '0%': { opacity: '0', transform: 'translateY(4px)' },
'100%': { opacity: '1', transform: 'translateY(0)' }, '100%': { opacity: '1', transform: 'translateY(0)' },