mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
style(vscode-ide-companion): form component style opt
This commit is contained in:
93
packages/vscode-ide-companion/src/commands/index.ts
Normal file
93
packages/vscode-ide-companion/src/commands/index.ts
Normal 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);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type IdeInfo,
|
||||
} from '@qwen-code/qwen-code-core/src/ide/detect-ide.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 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(
|
||||
vscode.workspace.onDidCloseTextDocument((doc) => {
|
||||
if (doc.uri.scheme === DIFF_SCHEME) {
|
||||
@@ -178,70 +188,6 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
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);
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
* 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 { useSessionManagement } from './hooks/session/useSessionManagement.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,
|
||||
// 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 });
|
||||
|
||||
// Observe scroll position to know if user has scrolled away from the bottom.
|
||||
useEffect(() => {
|
||||
const container = messagesContainerRef.current;
|
||||
const endEl = messagesEndRef.current;
|
||||
if (!container || !endEl) {
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nearBottom = () => {
|
||||
const threshold = 64; // px tolerance
|
||||
return (
|
||||
container.scrollTop + container.clientHeight >=
|
||||
container.scrollHeight - threshold
|
||||
);
|
||||
const onScroll = () => {
|
||||
// Use a small threshold so slight deltas don't flip the state.
|
||||
// Note: there's extra bottom padding for the input area, so keep this a bit generous.
|
||||
const threshold = 80; // px tolerance
|
||||
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)
|
||||
const prev = prevCountsRef.current;
|
||||
const newMsg = messageHandling.messages.length > prev.msgLen;
|
||||
@@ -208,26 +232,22 @@ export const App: React.FC = () => {
|
||||
doneLen: completedToolCalls.length,
|
||||
};
|
||||
|
||||
// If user is near bottom, or if we just appended a new item, scroll to bottom
|
||||
if (nearBottom() || newMsg || newInProg || newDone) {
|
||||
// Try scrollIntoView first
|
||||
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);
|
||||
if (!pinnedToBottom) {
|
||||
// Do nothing if user scrolled away; avoid stealing scroll.
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
inProgressToolCalls,
|
||||
completedToolCalls,
|
||||
@@ -237,6 +257,45 @@ export const App: React.FC = () => {
|
||||
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
|
||||
const handlePermissionResponse = useCallback(
|
||||
(optionId: string) => {
|
||||
@@ -418,7 +477,7 @@ export const App: React.FC = () => {
|
||||
|
||||
<div
|
||||
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)' }}
|
||||
>
|
||||
{!hasContent ? (
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
LinkIcon,
|
||||
ArrowUpIcon,
|
||||
} from './icons/index.js';
|
||||
import { ClaudeCompletionMenu } from './ui/ClaudeCompletionMenu.js';
|
||||
import { CompletionMenu } from './ui/CompletionMenu.js';
|
||||
import type { CompletionItem } from './CompletionTypes.js';
|
||||
|
||||
type EditMode = 'ask' | 'auto' | 'plan';
|
||||
@@ -124,11 +124,10 @@ export const InputForm: React.FC<InputFormProps> = ({
|
||||
>
|
||||
<div className="block">
|
||||
<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={{
|
||||
backgroundColor:
|
||||
'var(--app-input-secondary-background, var(--app-input-background))',
|
||||
borderColor: 'var(--app-input-border)',
|
||||
color: 'var(--app-input-foreground)',
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
@@ -151,7 +150,7 @@ export const InputForm: React.FC<InputFormProps> = ({
|
||||
onCompletionSelect &&
|
||||
onCompletionClose && (
|
||||
// Render dropdown above the input, matching Claude Code
|
||||
<ClaudeCompletionMenu
|
||||
<CompletionMenu
|
||||
items={completionItems}
|
||||
onSelect={onCompletionSelect}
|
||||
onClose={onCompletionClose}
|
||||
@@ -193,26 +192,30 @@ export const InputForm: React.FC<InputFormProps> = ({
|
||||
{/* Edit mode button */}
|
||||
<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)' }}
|
||||
title={editModeInfo.title}
|
||||
onClick={onToggleEditMode}
|
||||
>
|
||||
{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>
|
||||
|
||||
{/* Active file indicator */}
|
||||
{activeFileName && (
|
||||
<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)' }}
|
||||
title={`Showing Qwen Code your current file selection: ${activeFileName}${activeSelection ? `#${activeSelection.startLine}-${activeSelection.endLine}` : ''}`}
|
||||
onClick={onFocusActiveEditor}
|
||||
>
|
||||
<CodeBracketsIcon />
|
||||
<span>
|
||||
{/* Truncate file path/selection; hide label on very small screens */}
|
||||
<span className="min-w-0 truncate hidden sm:inline">
|
||||
{activeFileName}
|
||||
{activeSelection &&
|
||||
` #${activeSelection.startLine}${activeSelection.startLine !== activeSelection.endLine ? `-${activeSelection.endLine}` : ''}`}
|
||||
|
||||
@@ -4,12 +4,6 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* PlanDisplay.css -> Tailwind 化
|
||||
* 说明:尽量用 @apply,把原有类名保留,便于调试;
|
||||
* 仅在必须的地方保留少量原生 CSS(如关键帧)。
|
||||
*/
|
||||
|
||||
/* 容器 */
|
||||
.plan-display {
|
||||
@apply bg-transparent border-0 py-2 px-4 my-2;
|
||||
|
||||
@@ -102,7 +102,8 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
onClick={openFirstDiff}
|
||||
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>
|
||||
{/* 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="flex items-center justify-between 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
|
||||
</span>
|
||||
{path && (
|
||||
|
||||
@@ -23,9 +23,6 @@ export const ReadToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
// Group content by type
|
||||
const { errors } = groupContent(content);
|
||||
|
||||
// Extract filename from path
|
||||
// const getFileName = (path: string): string => path.split('/').pop() || path;
|
||||
|
||||
// Error case: show error
|
||||
if (errors.length > 0) {
|
||||
const path = locations?.[0]?.path || '';
|
||||
|
||||
@@ -68,7 +68,8 @@ export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
|
||||
>
|
||||
●
|
||||
</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}
|
||||
</span>
|
||||
{/* {toolCallId && (
|
||||
|
||||
@@ -8,7 +8,7 @@ import type React from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { CompletionItem } from '../CompletionTypes.js';
|
||||
|
||||
interface ClaudeCompletionMenuProps {
|
||||
interface CompletionMenuProps {
|
||||
items: CompletionItem[];
|
||||
onSelect: (item: CompletionItem) => void;
|
||||
onClose: () => void;
|
||||
@@ -20,7 +20,7 @@ interface ClaudeCompletionMenuProps {
|
||||
* Claude Code-like anchored dropdown rendered above the input field.
|
||||
* Keyboard: Up/Down to move, Enter to select, Esc to close.
|
||||
*/
|
||||
export const ClaudeCompletionMenu: React.FC<ClaudeCompletionMenuProps> = ({
|
||||
export const CompletionMenu: React.FC<CompletionMenuProps> = ({
|
||||
items,
|
||||
onSelect,
|
||||
onClose,
|
||||
@@ -115,7 +115,8 @@ export const FileLink: React.FC<FileLinkProps> = ({
|
||||
// Keep a semantic handle for scoped overrides (e.g. DiffDisplay.css)
|
||||
'file-link',
|
||||
// Layout + interaction
|
||||
'inline-flex items-baseline',
|
||||
// Use items-center + leading-none to vertically center within surrounding rows
|
||||
'inline-flex items-center leading-none',
|
||||
disableClick
|
||||
? 'pointer-events-none cursor-[inherit] hover:no-underline'
|
||||
: 'cursor-pointer',
|
||||
@@ -136,7 +137,7 @@ export const FileLink: React.FC<FileLinkProps> = ({
|
||||
aria-label={`Open file: ${fullDisplayText}`}
|
||||
// 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 && (
|
||||
<span className="file-link-location opacity-70 text-[0.9em] font-normal dark:opacity-60">
|
||||
:{line}
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
--app-input-active-border: var(--vscode-inputOption-activeBorder);
|
||||
--app-input-placeholder-foreground: var(--vscode-input-placeholderForeground);
|
||||
--app-input-secondary-background: var(--vscode-menu-background);
|
||||
/* Input Highlight (focus ring/border) */
|
||||
--app-input-highlight: var(--app-qwen-orange);
|
||||
|
||||
/* Code Highlighting */
|
||||
--app-code-background: var(--vscode-textCodeBlock-background, rgba(0, 0, 0, 0.05));
|
||||
@@ -93,6 +95,8 @@
|
||||
/* Light Theme Overrides */
|
||||
.vscode-light {
|
||||
--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 */
|
||||
@@ -144,6 +148,12 @@ button {
|
||||
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)
|
||||
=========================== */
|
||||
|
||||
@@ -24,7 +24,7 @@ export default {
|
||||
theme: {
|
||||
extend: {
|
||||
keyframes: {
|
||||
// ClaudeCompletionMenu mount animation: fade in + slight upward slide
|
||||
// CompletionMenu mount animation: fade in + slight upward slide
|
||||
'completion-menu-enter': {
|
||||
'0%': { opacity: '0', transform: 'translateY(4px)' },
|
||||
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||
|
||||
Reference in New Issue
Block a user