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,
|
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);
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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}` : ''}`}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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 || '';
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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)
|
||||||
=========================== */
|
=========================== */
|
||||||
|
|||||||
@@ -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)' },
|
||||||
|
|||||||
Reference in New Issue
Block a user