diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index cfb8c34e..49649e95 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -24,7 +24,7 @@ import { hasToolCallOutput } from './components/toolcalls/shared/utils.js'; import { InProgressToolCall } from './components/InProgressToolCall.js'; import { EmptyState } from './components/EmptyState.js'; import type { PlanEntry } from './components/PlanDisplay.js'; -import { type CompletionItem } from './components/CompletionMenu.js'; +import { type CompletionItem } from './components/CompletionTypes.js'; import { useCompletionTrigger } from './hooks/useCompletionTrigger.js'; import { InfoBanner } from './components/InfoBanner.js'; import { ChatHeader } from './components/layouts/ChatHeader.js'; @@ -210,11 +210,22 @@ export const App: React.FC = () => { // 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); } }, [ messageHandling.messages, @@ -222,6 +233,8 @@ export const App: React.FC = () => { completedToolCalls, messageHandling.isWaitingForResponse, messageHandling.loadingMessage, + messageHandling.isStreaming, + planEntries, ]); // Handle permission response diff --git a/packages/vscode-ide-companion/src/webview/ContextAttachmentManager.ts b/packages/vscode-ide-companion/src/webview/ContextAttachmentManager.ts deleted file mode 100644 index 41d5e6f3..00000000 --- a/packages/vscode-ide-companion/src/webview/ContextAttachmentManager.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * Context attachment types - * Based on vscode-copilot-chat implementation - */ -export interface ContextAttachment { - id: string; - type: 'file' | 'symbol' | 'selection' | 'variable'; - name: string; - value: string | { uri: string; range?: { start: number; end: number } }; - icon?: string; -} - -/** - * Manages context attachments for the chat - * Similar to ChatContextAttachments in vscode-copilot-chat - */ -export class ContextAttachmentManager { - private attachments: Map = new Map(); - private listeners: Array<(attachments: ContextAttachment[]) => void> = []; - - /** - * Add a context attachment - */ - addAttachment(attachment: ContextAttachment): void { - this.attachments.set(attachment.id, attachment); - this.notifyListeners(); - } - - /** - * Remove a context attachment - */ - removeAttachment(id: string): void { - this.attachments.delete(id); - this.notifyListeners(); - } - - /** - * Get all attachments - */ - getAttachments(): ContextAttachment[] { - return Array.from(this.attachments.values()); - } - - /** - * Check if an attachment exists - */ - hasAttachment(id: string): boolean { - return this.attachments.has(id); - } - - /** - * Clear all attachments - */ - clearAttachments(): void { - this.attachments.clear(); - this.notifyListeners(); - } - - /** - * Subscribe to attachment changes - */ - subscribe(listener: (attachments: ContextAttachment[]) => void): () => void { - this.listeners.push(listener); - return () => { - const index = this.listeners.indexOf(listener); - if (index > -1) { - this.listeners.splice(index, 1); - } - }; - } - - /** - * Notify all listeners of changes - */ - private notifyListeners(): void { - const attachments = this.getAttachments(); - this.listeners.forEach((listener) => listener(attachments)); - } - - /** - * Get context for message sending - */ - getContextForMessage(): Array> { - return this.getAttachments().map((att) => ({ - id: att.id, - type: att.type, - name: att.name, - value: att.value, - })); - } -} diff --git a/packages/vscode-ide-companion/src/webview/components/ClaudeCompletionMenu.css b/packages/vscode-ide-companion/src/webview/components/ClaudeCompletionMenu.css deleted file mode 100644 index a8e49bd7..00000000 --- a/packages/vscode-ide-companion/src/webview/components/ClaudeCompletionMenu.css +++ /dev/null @@ -1,115 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -/* Claude Code-like dropdown anchored to input container */ -.hi { - display: flex; - flex-direction: column; - position: absolute; - bottom: 100%; - left: 0; - right: 0; - margin-bottom: 8px; - background: var(--app-menu-background); - border: 1px solid var(--app-input-border); - border-radius: var(--corner-radius-large); - overflow: hidden; - animation: So .15s ease-out; - max-height: 50vh; - z-index: 1000; -} - -/* Optional top spacer to create visual separation from input */ -.hi > .spacer-4px { height: 4px; } - -.xi { - max-height: 300px; - display: flex; - flex-direction: column; - overflow-y: auto; - padding: var(--app-list-padding); - gap: var(--app-list-gap); - padding-bottom: 8px; -} - -.fi { /* divider */ - height: 1px; - background: var(--app-input-border); - margin: 4px 0; -} - -.vi { /* section label */ - padding: 4px 12px; - color: var(--app-primary-foreground); - opacity: .5; - font-size: .9em; -} - -.wi { /* item */ - padding: var(--app-list-item-padding); - margin: 0 4px; - cursor: pointer; - border-radius: var(--app-list-border-radius); -} - -.ki { /* item content */ - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; -} - -.Ii { /* leading icon */ - width: 16px; - height: 16px; - display: inline-flex; - align-items: center; - justify-content: center; - color: var(--vscode-symbolIcon-fileForeground, #cccccc); -} - -.Lo { /* primary text */ - color: var(--app-primary-foreground); - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.Mo { /* secondary text (path/description) */ - color: var(--app-secondary-foreground); - opacity: .7; - font-size: .9em; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 50%; -} - -.jo { /* active/selected */ - background: var(--app-list-active-background); - color: var(--app-list-active-foreground); -} - -.jo .Lo { color: var(--app-list-active-foreground); } - -.yi { /* trailing icon placeholder */ - width: 16px; - height: 16px; - opacity: .5; - margin-left: auto; -} - -@keyframes So { - from { opacity: 0; transform: translateY(4px); } - to { opacity: 1; transform: translateY(0); } -} - -/* Container around the input to anchor the dropdown */ -.Bo { - position: relative; - display: flex; -} diff --git a/packages/vscode-ide-companion/src/webview/components/ClaudeCompletionMenu.tsx b/packages/vscode-ide-companion/src/webview/components/ClaudeCompletionMenu.tsx deleted file mode 100644 index 6c190e3f..00000000 --- a/packages/vscode-ide-companion/src/webview/components/ClaudeCompletionMenu.tsx +++ /dev/null @@ -1,123 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import type React from 'react'; -import { useEffect, useRef, useState } from 'react'; -import './ClaudeCompletionMenu.css'; -import type { CompletionItem } from './CompletionMenu.js'; - -interface ClaudeCompletionMenuProps { - items: CompletionItem[]; - onSelect: (item: CompletionItem) => void; - onClose: () => void; - title?: string; - selectedIndex?: number; -} - -/** - * 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 = ({ - items, - onSelect, - onClose, - title, - selectedIndex = 0, -}) => { - const containerRef = useRef(null); - const [selected, setSelected] = useState(selectedIndex); - - useEffect(() => setSelected(selectedIndex), [selectedIndex]); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - containerRef.current && - !containerRef.current.contains(event.target as Node) - ) { - onClose(); - } - }; - - const handleKeyDown = (event: KeyboardEvent) => { - switch (event.key) { - case 'ArrowDown': - event.preventDefault(); - setSelected((prev) => Math.min(prev + 1, items.length - 1)); - break; - case 'ArrowUp': - event.preventDefault(); - setSelected((prev) => Math.max(prev - 1, 0)); - break; - case 'Enter': - event.preventDefault(); - if (items[selected]) { - onSelect(items[selected]); - } - break; - case 'Escape': - event.preventDefault(); - onClose(); - break; - default: - break; - } - }; - - document.addEventListener('mousedown', handleClickOutside); - document.addEventListener('keydown', handleKeyDown); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - document.removeEventListener('keydown', handleKeyDown); - }; - }, [items, selected, onSelect, onClose]); - - useEffect(() => { - const selectedEl = containerRef.current?.querySelector( - `[data-index="${selected}"]`, - ); - if (selectedEl) { - selectedEl.scrollIntoView({ block: 'nearest' }); - } - }, [selected]); - - if (!items.length) { - return null; - } - - return ( -
-
-
- {title &&
{title}
} - {items.map((item, index) => { - const selectedCls = index === selected ? 'jo' : ''; - return ( -
onSelect(item)} - onMouseEnter={() => setSelected(index)} - role="menuitem" - > -
- {item.icon && {item.icon}} - {item.label} - {item.description && ( - - {item.description} - - )} -
-
- ); - })} -
-
- ); -}; diff --git a/packages/vscode-ide-companion/src/webview/components/CompletionMenu.css b/packages/vscode-ide-companion/src/webview/components/CompletionMenu.css deleted file mode 100644 index e51dc441..00000000 --- a/packages/vscode-ide-companion/src/webview/components/CompletionMenu.css +++ /dev/null @@ -1,107 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -.completion-menu { - position: fixed; - background: var(--vscode-quickInput-background, #252526); - border: 1px solid var(--vscode-quickInput-border, #454545); - border-radius: 6px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - z-index: 1000; - min-width: 300px; - max-width: 500px; - max-height: 400px; - overflow: hidden; - display: flex; - flex-direction: column; - transform: translateY(-100%); - margin-top: -8px; -} - -.completion-menu-items { - overflow-y: auto; - overflow-x: hidden; -} - -.completion-menu-item { - display: flex; - align-items: center; - padding: 8px 12px; - cursor: pointer; - transition: background-color 0.1s ease; - border-bottom: 1px solid var(--vscode-quickInput-separator, transparent); -} - -.completion-menu-item:last-child { - border-bottom: none; -} - -.completion-menu-item:hover, -.completion-menu-item.selected { - background-color: var(--vscode-list-hoverBackground, #2a2d2e); -} - -.completion-item-icon { - flex-shrink: 0; - margin-right: 10px; - width: 16px; - height: 16px; - display: flex; - align-items: center; - justify-content: center; - color: var(--vscode-symbolIcon-fileForeground, #cccccc); -} - -.completion-item-icon svg { - width: 16px; - height: 16px; -} - -.completion-item-content { - flex: 1; - min-width: 0; - display: flex; - flex-direction: row; - align-items: baseline; - gap: 8px; -} - -.completion-item-label { - font-size: 13px; - color: var(--vscode-quickInput-foreground, #cccccc); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex-shrink: 0; -} - -.completion-item-description { - font-size: 11px; - color: var(--vscode-descriptionForeground, #8c8c8c); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; - min-width: 0; -} - -/* Scrollbar styling */ -.completion-menu-items::-webkit-scrollbar { - width: 10px; -} - -.completion-menu-items::-webkit-scrollbar-track { - background: transparent; -} - -.completion-menu-items::-webkit-scrollbar-thumb { - background: var(--vscode-scrollbarSlider-background, #424242); - border-radius: 5px; -} - -.completion-menu-items::-webkit-scrollbar-thumb:hover { - background: var(--vscode-scrollbarSlider-hoverBackground, #4f4f4f); -} diff --git a/packages/vscode-ide-companion/src/webview/components/CompletionMenu.tsx b/packages/vscode-ide-companion/src/webview/components/CompletionMenu.tsx deleted file mode 100644 index 6a12ef2d..00000000 --- a/packages/vscode-ide-companion/src/webview/components/CompletionMenu.tsx +++ /dev/null @@ -1,140 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import type React from 'react'; -import { useEffect, useRef, useState } from 'react'; -import './CompletionMenu.css'; - -export interface CompletionItem { - id: string; - label: string; - description?: string; - icon?: React.ReactNode; - type: 'file' | 'folder' | 'symbol' | 'command' | 'variable' | 'info'; - // Value inserted into the input when selected (e.g., filename or command) - value?: string; - // Optional full path for files (used to build @filename -> full path mapping) - path?: string; -} - -interface CompletionMenuProps { - items: CompletionItem[]; - position: { top: number; left: number }; - onSelect: (item: CompletionItem) => void; - onClose: () => void; - selectedIndex?: number; -} - -/** - * Completion menu for @ and / triggers - * Based on vscode-copilot-chat's AttachContextAction - */ -export const CompletionMenu: React.FC = ({ - items, - position, - onSelect, - onClose, - selectedIndex = 0, -}) => { - const menuRef = useRef(null); - const [selected, setSelected] = useState(selectedIndex); - - useEffect(() => { - setSelected(selectedIndex); - }, [selectedIndex]); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (menuRef.current && !menuRef.current.contains(event.target as Node)) { - onClose(); - } - }; - - const handleKeyDown = (event: KeyboardEvent) => { - switch (event.key) { - case 'ArrowDown': - event.preventDefault(); - setSelected((prev) => Math.min(prev + 1, items.length - 1)); - break; - case 'ArrowUp': - event.preventDefault(); - setSelected((prev) => Math.max(prev - 1, 0)); - break; - case 'Enter': - event.preventDefault(); - if (items[selected]) { - onSelect(items[selected]); - } - break; - case 'Escape': - event.preventDefault(); - onClose(); - break; - default: - break; - } - }; - - document.addEventListener('mousedown', handleClickOutside); - document.addEventListener('keydown', handleKeyDown); - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - document.removeEventListener('keydown', handleKeyDown); - }; - }, [items, selected, onSelect, onClose]); - - // Scroll selected item into view - useEffect(() => { - const selectedElement = menuRef.current?.querySelector( - `[data-index="${selected}"]`, - ); - if (selectedElement) { - selectedElement.scrollIntoView({ block: 'nearest' }); - } - }, [selected]); - - const isEmpty = items.length === 0; - - return ( -
-
- {isEmpty ? ( -
Type to search files…
- ) : ( - items.map((item, index) => ( -
onSelect(item)} - onMouseEnter={() => setSelected(index)} - > - {item.icon && ( -
{item.icon}
- )} -
-
{item.label}
- {item.description && ( -
- {item.description} -
- )} -
-
- )) - )} -
-
- ); -}; diff --git a/packages/vscode-ide-companion/src/webview/components/CompletionTypes.ts b/packages/vscode-ide-companion/src/webview/components/CompletionTypes.ts new file mode 100644 index 00000000..463d4f50 --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/components/CompletionTypes.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import type React from 'react'; + +// Shared type for completion items used by the input completion system +export interface CompletionItem { + id: string; + label: string; + description?: string; + icon?: React.ReactNode; + type: 'file' | 'folder' | 'symbol' | 'command' | 'variable' | 'info'; + // Value inserted into the input when selected (e.g., filename or command) + value?: string; + // Optional full path for files (used to build @filename -> full path mapping) + path?: string; +} diff --git a/packages/vscode-ide-companion/src/webview/components/ContextPills.css b/packages/vscode-ide-companion/src/webview/components/ContextPills.css deleted file mode 100644 index 785b7bcc..00000000 --- a/packages/vscode-ide-companion/src/webview/components/ContextPills.css +++ /dev/null @@ -1,79 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -.context-pills-container { - display: flex; - flex-wrap: wrap; - gap: 6px; - padding: 8px 12px; - background: var(--vscode-input-background, #3c3c3c); - border-bottom: 1px solid var(--vscode-input-border, #454545); -} - -.context-pill { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 4px 8px 4px 6px; - background: var(--vscode-badge-background, #4d4d4d); - color: var(--vscode-badge-foreground, #ffffff); - border-radius: 12px; - font-size: 12px; - max-width: 200px; - transition: background-color 0.1s ease; -} - -.context-pill:hover { - background: var(--vscode-badge-background, #5a5a5a); -} - -.context-pill-icon { - display: flex; - align-items: center; - justify-content: center; - width: 14px; - height: 14px; - flex-shrink: 0; -} - -.context-pill-icon svg { - width: 14px; - height: 14px; -} - -.context-pill-label { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - line-height: 1.2; -} - -.context-pill-remove { - display: flex; - align-items: center; - justify-content: center; - width: 16px; - height: 16px; - padding: 0; - background: transparent; - border: none; - color: inherit; - cursor: pointer; - opacity: 0.7; - transition: opacity 0.1s ease; - flex-shrink: 0; -} - -.context-pill-remove:hover { - opacity: 1; -} - -.context-pill-remove svg { - width: 12px; - height: 12px; -} - diff --git a/packages/vscode-ide-companion/src/webview/components/ContextPills.tsx b/packages/vscode-ide-companion/src/webview/components/ContextPills.tsx deleted file mode 100644 index 8150cf79..00000000 --- a/packages/vscode-ide-companion/src/webview/components/ContextPills.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import type React from 'react'; -import type { ContextAttachment } from '../ContextAttachmentManager.js'; -import { - FileListIcon, - PlusSmallIcon, - SymbolIcon, - SelectionIcon, - CloseSmallIcon, -} from './icons/index.js'; -import './ContextPills.css'; - -interface ContextPillsProps { - attachments: ContextAttachment[]; - onRemove: (id: string) => void; -} - -/** - * Display attached context as pills/chips - * Similar to ChatContextAttachments UI in vscode-copilot-chat - */ -export const ContextPills: React.FC = ({ - attachments, - onRemove, -}) => { - if (attachments.length === 0) { - return null; - } - - const getIcon = (type: string) => { - switch (type) { - case 'file': - return ; - case 'symbol': - return ; - case 'selection': - return ; - default: - return ; - } - }; - - return ( -
- {attachments.map((attachment) => ( -
-
{getIcon(attachment.type)}
-
{attachment.name}
- -
- ))} -
- ); -}; diff --git a/packages/vscode-ide-companion/src/webview/components/InProgressToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/InProgressToolCall.tsx index 27ac4768..d300c099 100644 --- a/packages/vscode-ide-companion/src/webview/components/InProgressToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/InProgressToolCall.tsx @@ -8,7 +8,7 @@ import React from 'react'; import type { ToolCallData } from './toolcalls/shared/types.js'; -import { FileLink } from './shared/FileLink.js'; +import { FileLink } from './ui/FileLink.js'; import { useVSCode } from '../hooks/useVSCode.js'; interface InProgressToolCallProps { diff --git a/packages/vscode-ide-companion/src/webview/components/InputForm.tsx b/packages/vscode-ide-companion/src/webview/components/InputForm.tsx index 3b6be503..ca1bf8f3 100644 --- a/packages/vscode-ide-companion/src/webview/components/InputForm.tsx +++ b/packages/vscode-ide-companion/src/webview/components/InputForm.tsx @@ -15,8 +15,8 @@ import { LinkIcon, ArrowUpIcon, } from './icons/index.js'; -import { ClaudeCompletionMenu } from './ClaudeCompletionMenu.js'; -import type { CompletionItem } from './CompletionMenu.js'; +import { ClaudeCompletionMenu } from './ui/ClaudeCompletionMenu.js'; +import type { CompletionItem } from './CompletionTypes.js'; type EditMode = 'ask' | 'auto' | 'plan'; @@ -143,7 +143,7 @@ export const InputForm: React.FC = ({
{/* Input wrapper (Claude-style anchor container) */} -
+
{/* Claude-style anchored dropdown */} {completionIsOpen && completionItems && diff --git a/packages/vscode-ide-companion/src/webview/components/NotLoggedInMessage.tsx b/packages/vscode-ide-companion/src/webview/components/NotLoggedInMessage.tsx deleted file mode 100644 index 51ef9652..00000000 --- a/packages/vscode-ide-companion/src/webview/components/NotLoggedInMessage.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import type React from 'react'; -import { WarningTriangleIcon, CloseIcon } from './icons/index.js'; - -interface NotLoggedInMessageProps { - /** - * The message to display - */ - message: string; - - /** - * Callback when the login button is clicked - */ - onLoginClick: () => void; - - /** - * Callback when the message is dismissed (optional) - */ - onDismiss?: () => void; -} - -export const NotLoggedInMessage: React.FC = ({ - message, - onLoginClick, - onDismiss, -}) => ( -
- {/* Warning Icon */} - - - {/* Content */} -
-

- {message} -

- - {/* Login Button */} - -
- - {/* Optional Close Button */} - {onDismiss && ( - - )} -
-); diff --git a/packages/vscode-ide-companion/src/webview/components/Timeline.css b/packages/vscode-ide-companion/src/webview/components/Timeline.css index dabac37a..44fea9ee 100644 --- a/packages/vscode-ide-companion/src/webview/components/Timeline.css +++ b/packages/vscode-ide-companion/src/webview/components/Timeline.css @@ -76,7 +76,7 @@ } .timeline-dot.green .dot-inner { - background-color: var(--app-qwen-green, #6BCF7F); + background-color: var(--app-qwen-green, #6bcf7f); } .timeline-dot.purple .dot-inner { @@ -147,7 +147,12 @@ border: 1px solid var(--app-transparent-inner-border); border-radius: 6px; padding: 12px; - font-family: var(--vscode-editor-font-family, 'Monaco', 'Courier New', monospace); + font-family: var( + --vscode-editor-font-family, + 'Monaco', + 'Courier New', + monospace + ); font-size: 12px; color: var(--app-secondary-foreground); max-height: 300px; @@ -170,11 +175,6 @@ padding: 10px 14px; } -/* 助手消息样式 */ -.timeline-item.assistant-message .timeline-body { - /* 保持默认样式,不加背景 */ -} - /* 思考消息样式 */ .timeline-item.thinking .timeline-body { color: var(--app-secondary-foreground); diff --git a/packages/vscode-ide-companion/src/webview/components/icons/FileIcons.tsx b/packages/vscode-ide-companion/src/webview/components/icons/FileIcons.tsx index bca7da0d..38bf27f7 100644 --- a/packages/vscode-ide-companion/src/webview/components/icons/FileIcons.tsx +++ b/packages/vscode-ide-companion/src/webview/components/icons/FileIcons.tsx @@ -32,10 +32,6 @@ export const FileIcon: React.FC = ({ ); -/** - * File list icon (16x16) - * Used for file type indicator in context pills - */ export const FileListIcon: React.FC = ({ size = 16, className, diff --git a/packages/vscode-ide-companion/src/webview/components/icons/NavigationIcons.tsx b/packages/vscode-ide-companion/src/webview/components/icons/NavigationIcons.tsx index e8bd9512..9a4e52fb 100644 --- a/packages/vscode-ide-companion/src/webview/components/icons/NavigationIcons.tsx +++ b/packages/vscode-ide-companion/src/webview/components/icons/NavigationIcons.tsx @@ -137,10 +137,6 @@ export const CloseIcon: React.FC = ({ ); -/** - * Close X icon for context pills (16x16) - * Used to remove attachments - */ export const CloseSmallIcon: React.FC = ({ size = 16, className, diff --git a/packages/vscode-ide-companion/src/webview/components/icons/StatusIcons.tsx b/packages/vscode-ide-companion/src/webview/components/icons/StatusIcons.tsx index 375bba94..fdaa2943 100644 --- a/packages/vscode-ide-companion/src/webview/components/icons/StatusIcons.tsx +++ b/packages/vscode-ide-companion/src/webview/components/icons/StatusIcons.tsx @@ -149,10 +149,6 @@ export const UserIcon: React.FC = ({ ); -/** - * Symbol arrow icon (16x16) - * Used for symbol type in context pills - */ export const SymbolIcon: React.FC = ({ size = 16, className, @@ -172,10 +168,6 @@ export const SymbolIcon: React.FC = ({ ); -/** - * Selection/text lines icon (16x16) - * Used for selection type in context pills - */ export const SelectionIcon: React.FC = ({ size = 16, className, diff --git a/packages/vscode-ide-companion/src/webview/components/shared/FileLink.css b/packages/vscode-ide-companion/src/webview/components/shared/FileLink.css deleted file mode 100644 index 8f918214..00000000 --- a/packages/vscode-ide-companion/src/webview/components/shared/FileLink.css +++ /dev/null @@ -1,120 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - * - * FileLink 组件样式 - */ - -/** - * 文件链接基础样式 - */ -.file-link { - /* 使用 VSCode 主题的链接颜色 */ - color: var(--vscode-textLink-foreground); - cursor: pointer; - text-decoration: none; - - /* 使用编辑器字体保持一致性 */ - font-family: var(--vscode-editor-font-family, 'Menlo', 'Monaco', 'Courier New', monospace); - font-size: inherit; - - /* 行内显示 */ - display: inline-flex; - align-items: baseline; - gap: 0; - - /* 平滑过渡效果 */ - transition: color 0.1s ease-in-out, text-decoration 0.1s ease-in-out; -} - -/** - * 悬停状态 - */ -.file-link:hover { - /* 悬停时显示下划线 */ - text-decoration: underline; - - /* 使用激活状态的链接颜色 */ - color: var(--vscode-textLink-activeForeground); -} - -/** - * 聚焦状态(键盘导航) - */ -.file-link:focus { - outline: 1px solid var(--vscode-focusBorder); - outline-offset: 2px; - border-radius: 2px; -} - -/** - * 激活状态(点击时) - */ -.file-link:active { - opacity: 0.8; -} - -/** - * 文件路径部分 - */ -.file-link-path { - font-weight: 500; - /* 继承父元素的颜色 */ - color: inherit; -} - -/** - * 位置信息部分(行号和列号) - */ -.file-link-location { - opacity: 0.7; - font-size: 0.9em; - /* 继承父元素的颜色 */ - color: inherit; - font-weight: normal; -} - -/** - * 在深色主题下增强可读性 - */ -@media (prefers-color-scheme: dark) { - .file-link-location { - opacity: 0.6; - } -} - -/** - * 高对比度模式支持 - */ -@media (prefers-contrast: high) { - .file-link { - text-decoration: underline; - font-weight: 600; - } - - .file-link-location { - opacity: 1; - } -} - -/** - * 禁用点击时的样式(当父元素处理点击时) - */ -.file-link-disabled { - cursor: inherit; - pointer-events: none; -} - -.file-link-disabled:hover { - text-decoration: none; - color: inherit; -} - -/** - * 在代码块中的样式调整 - */ -.code-block .file-link { - /* 在代码块中保持等宽字体 */ - font-family: var(--vscode-editor-font-family, 'Menlo', 'Monaco', 'Courier New', monospace); -} diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/EditToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/EditToolCall.tsx index 9c9b370c..39899917 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/EditToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/EditToolCall.tsx @@ -12,7 +12,7 @@ import { ToolCallContainer } from './shared/LayoutComponents.js'; import { DiffDisplay } from './shared/DiffDisplay.js'; import { groupContent } from './shared/utils.js'; import { useVSCode } from '../../hooks/useVSCode.js'; -import { FileLink } from '../shared/FileLink.js'; +import { FileLink } from '../ui/FileLink.js'; /** * Calculate diff summary (added/removed lines) diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/ReadToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/ReadToolCall.tsx index 8a54ff9e..f3d9b573 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/ReadToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/ReadToolCall.tsx @@ -10,7 +10,7 @@ import type React from 'react'; import type { BaseToolCallProps } from './shared/types.js'; import { ToolCallContainer } from './shared/LayoutComponents.js'; import { groupContent } from './shared/utils.js'; -import { FileLink } from '../shared/FileLink.js'; +import { FileLink } from '../ui/FileLink.js'; /** * Specialized component for Read tool calls diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/WriteToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/WriteToolCall.tsx index 61a32fd7..c5d4a534 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/WriteToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/WriteToolCall.tsx @@ -10,7 +10,7 @@ import type React from 'react'; import type { BaseToolCallProps } from './shared/types.js'; import { ToolCallContainer } from './shared/LayoutComponents.js'; import { groupContent } from './shared/utils.js'; -import { FileLink } from '../shared/FileLink.js'; +import { FileLink } from '../ui/FileLink.js'; /** * Specialized component for Write tool calls diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/DiffDisplay.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/DiffDisplay.tsx index bea1ddc0..78af74f5 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/DiffDisplay.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/DiffDisplay.tsx @@ -8,7 +8,7 @@ import type React from 'react'; import { useMemo } from 'react'; -import { FileLink } from '../../shared/FileLink.js'; +import { FileLink } from '../../ui/FileLink.js'; import { calculateDiffStats, formatDiffStatsDetailed, diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/LayoutComponents.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/LayoutComponents.tsx index 46734982..eb949430 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/LayoutComponents.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/LayoutComponents.tsx @@ -8,7 +8,7 @@ */ import type React from 'react'; -import { FileLink } from '../../shared/FileLink.js'; +import { FileLink } from '../../ui/FileLink.js'; /** * Props for ToolCallContainer - Claude Code style layout diff --git a/packages/vscode-ide-companion/src/webview/components/ui/ClaudeCompletionMenu.tsx b/packages/vscode-ide-companion/src/webview/components/ui/ClaudeCompletionMenu.tsx new file mode 100644 index 00000000..f9c65747 --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/components/ui/ClaudeCompletionMenu.tsx @@ -0,0 +1,176 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import type React from 'react'; +import { useEffect, useRef, useState } from 'react'; +import type { CompletionItem } from '../CompletionTypes.js'; + +interface ClaudeCompletionMenuProps { + items: CompletionItem[]; + onSelect: (item: CompletionItem) => void; + onClose: () => void; + title?: string; + selectedIndex?: number; +} + +/** + * 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 = ({ + items, + onSelect, + onClose, + title, + selectedIndex = 0, +}) => { + const containerRef = useRef(null); + const [selected, setSelected] = useState(selectedIndex); + // Mount state to drive a simple Tailwind transition (replaces CSS keyframes) + const [mounted, setMounted] = useState(false); + + useEffect(() => setSelected(selectedIndex), [selectedIndex]); + useEffect(() => setMounted(true), []); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(event.target as Node) + ) { + onClose(); + } + }; + + const handleKeyDown = (event: KeyboardEvent) => { + switch (event.key) { + case 'ArrowDown': + event.preventDefault(); + setSelected((prev) => Math.min(prev + 1, items.length - 1)); + break; + case 'ArrowUp': + event.preventDefault(); + setSelected((prev) => Math.max(prev - 1, 0)); + break; + case 'Enter': + event.preventDefault(); + if (items[selected]) { + onSelect(items[selected]); + } + break; + case 'Escape': + event.preventDefault(); + onClose(); + break; + default: + break; + } + }; + + document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('keydown', handleKeyDown); + }; + }, [items, selected, onSelect, onClose]); + + useEffect(() => { + const selectedEl = containerRef.current?.querySelector( + `[data-index="${selected}"]`, + ); + if (selectedEl) { + selectedEl.scrollIntoView({ block: 'nearest' }); + } + }, [selected]); + + if (!items.length) { + return null; + } + + return ( +
+ {/* Optional top spacer for visual separation from the input */} +
+
+ {title && ( +
+ {title} +
+ )} + {items.map((item, index) => { + const isActive = index === selected; + return ( +
onSelect(item)} + onMouseEnter={() => setSelected(index)} + className={[ + // Semantic + 'completion-menu-item', + // Hit area + 'mx-1 cursor-pointer rounded-[var(--app-list-border-radius)]', + 'p-[var(--app-list-item-padding)]', + // Active background + isActive ? 'bg-[var(--app-list-active-background)]' : '', + ].join(' ')} + > +
+ {item.icon && ( + + {item.icon} + + )} + + {item.label} + + {item.description && ( + + {item.description} + + )} +
+
+ ); + })} +
+
+ ); +}; diff --git a/packages/vscode-ide-companion/src/webview/components/shared/FileLink.tsx b/packages/vscode-ide-companion/src/webview/components/ui/FileLink.tsx similarity index 73% rename from packages/vscode-ide-companion/src/webview/components/shared/FileLink.tsx rename to packages/vscode-ide-companion/src/webview/components/ui/FileLink.tsx index 33bf77ad..8e42c0e2 100644 --- a/packages/vscode-ide-companion/src/webview/components/shared/FileLink.tsx +++ b/packages/vscode-ide-companion/src/webview/components/ui/FileLink.tsx @@ -9,7 +9,7 @@ import type React from 'react'; import { useVSCode } from '../../hooks/useVSCode.js'; -import './FileLink.css'; +// Tailwind rewrite: styles from FileLink.css are now expressed as utility classes /** * Props for FileLink @@ -111,15 +111,34 @@ export const FileLink: React.FC = ({ return ( - {displayPath} + {displayPath} {line !== null && line !== undefined && ( - + :{line} {column !== null && column !== undefined && <>:{column}} diff --git a/packages/vscode-ide-companion/src/webview/hooks/useCompletionTrigger.ts b/packages/vscode-ide-companion/src/webview/hooks/useCompletionTrigger.ts index 9f186f8f..521716c6 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useCompletionTrigger.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useCompletionTrigger.ts @@ -6,7 +6,7 @@ import type { RefObject } from 'react'; import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; -import type { CompletionItem } from '../components/CompletionMenu.js'; +import type { CompletionItem } from '../components/CompletionTypes.js'; interface CompletionTriggerState { isOpen: boolean; diff --git a/packages/vscode-ide-companion/src/webview/styles/ClaudeCodeStyles.css b/packages/vscode-ide-companion/src/webview/styles/ClaudeCodeStyles.css index fd1ebc73..1887b156 100644 --- a/packages/vscode-ide-companion/src/webview/styles/ClaudeCodeStyles.css +++ b/packages/vscode-ide-companion/src/webview/styles/ClaudeCodeStyles.css @@ -9,11 +9,8 @@ /* Import component styles */ @import '../components/EmptyState.css'; -@import '../components/CompletionMenu.css'; -@import '../components/ContextPills.css'; @import '../components/PlanDisplay.css'; @import '../components/Timeline.css'; -@import '../components/shared/FileLink.css'; @import '../components/toolcalls/shared/DiffDisplay.css'; @import '../components/messages/AssistantMessage.css'; @@ -63,4 +60,3 @@ --app-warning-border: var(--vscode-editorWarning-foreground, #ffcc00); --app-warning-foreground: var(--vscode-editorWarning-foreground, #ffcc00); } - diff --git a/packages/vscode-ide-companion/tailwind.config.js b/packages/vscode-ide-companion/tailwind.config.js index 67f9eeea..6c7c4213 100644 --- a/packages/vscode-ide-companion/tailwind.config.js +++ b/packages/vscode-ide-companion/tailwind.config.js @@ -23,6 +23,16 @@ export default { ], theme: { extend: { + keyframes: { + // ClaudeCompletionMenu mount animation: fade in + slight upward slide + 'completion-menu-enter': { + '0%': { opacity: '0', transform: 'translateY(4px)' }, + '100%': { opacity: '1', transform: 'translateY(0)' }, + }, + }, + animation: { + 'completion-menu-enter': 'completion-menu-enter 150ms ease-out both', + }, colors: { qwen: { orange: '#615fff',