mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
style(vscode-ide-companion): use tailwind to refactor some ui components
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<string, ContextAttachment> = 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<Record<string, unknown>> {
|
||||
return this.getAttachments().map((att) => ({
|
||||
id: att.id,
|
||||
type: att.type,
|
||||
name: att.name,
|
||||
value: att.value,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<ClaudeCompletionMenuProps> = ({
|
||||
items,
|
||||
onSelect,
|
||||
onClose,
|
||||
title,
|
||||
selectedIndex = 0,
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(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 (
|
||||
<div ref={containerRef} className="hi" role="menu">
|
||||
<div className="spacer-4px" />
|
||||
<div className="xi">
|
||||
{title && <div className="vi">{title}</div>}
|
||||
{items.map((item, index) => {
|
||||
const selectedCls = index === selected ? 'jo' : '';
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
data-index={index}
|
||||
className={`wi ${selectedCls}`}
|
||||
onClick={() => onSelect(item)}
|
||||
onMouseEnter={() => setSelected(index)}
|
||||
role="menuitem"
|
||||
>
|
||||
<div className="ki">
|
||||
{item.icon && <span className="Ii">{item.icon}</span>}
|
||||
<span className="Lo">{item.label}</span>
|
||||
{item.description && (
|
||||
<span className="Mo" title={item.description}>
|
||||
{item.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<CompletionMenuProps> = ({
|
||||
items,
|
||||
position,
|
||||
onSelect,
|
||||
onClose,
|
||||
selectedIndex = 0,
|
||||
}) => {
|
||||
const menuRef = useRef<HTMLDivElement>(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 (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="completion-menu"
|
||||
style={{
|
||||
top: `${position.top}px`,
|
||||
left: `${position.left}px`,
|
||||
}}
|
||||
>
|
||||
<div className="completion-menu-items">
|
||||
{isEmpty ? (
|
||||
<div className="completion-menu-empty">Type to search files…</div>
|
||||
) : (
|
||||
items.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
data-index={index}
|
||||
className={`completion-menu-item ${index === selected ? 'selected' : ''}`}
|
||||
onClick={() => onSelect(item)}
|
||||
onMouseEnter={() => setSelected(index)}
|
||||
>
|
||||
{item.icon && (
|
||||
<div className="completion-item-icon">{item.icon}</div>
|
||||
)}
|
||||
<div className="completion-item-content">
|
||||
<div className="completion-item-label">{item.label}</div>
|
||||
{item.description && (
|
||||
<div className="completion-item-description">
|
||||
{item.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<ContextPillsProps> = ({
|
||||
attachments,
|
||||
onRemove,
|
||||
}) => {
|
||||
if (attachments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'file':
|
||||
return <FileListIcon />;
|
||||
case 'symbol':
|
||||
return <SymbolIcon />;
|
||||
case 'selection':
|
||||
return <SelectionIcon />;
|
||||
default:
|
||||
return <PlusSmallIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="context-pills-container">
|
||||
{attachments.map((attachment) => (
|
||||
<div key={attachment.id} className="context-pill">
|
||||
<div className="context-pill-icon">{getIcon(attachment.type)}</div>
|
||||
<div className="context-pill-label">{attachment.name}</div>
|
||||
<button
|
||||
className="context-pill-remove"
|
||||
onClick={() => onRemove(attachment.id)}
|
||||
aria-label="Remove attachment"
|
||||
>
|
||||
<CloseSmallIcon />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<InputFormProps> = ({
|
||||
<div className="input-banner" />
|
||||
|
||||
{/* Input wrapper (Claude-style anchor container) */}
|
||||
<div className="relative flex z-[1] Bo">
|
||||
<div className="relative flex z-[1]">
|
||||
{/* Claude-style anchored dropdown */}
|
||||
{completionIsOpen &&
|
||||
completionItems &&
|
||||
|
||||
@@ -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<NotLoggedInMessageProps> = ({
|
||||
message,
|
||||
onLoginClick,
|
||||
onDismiss,
|
||||
}) => (
|
||||
<div
|
||||
className="flex items-start gap-3 p-4 my-4 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: 'var(--app-warning-background)',
|
||||
borderLeft: '3px solid var(--app-warning-border)',
|
||||
}}
|
||||
>
|
||||
{/* Warning Icon */}
|
||||
<WarningTriangleIcon
|
||||
className="flex-shrink-0 w-5 h-5 mt-0.5"
|
||||
style={{ color: 'var(--app-warning-foreground)' }}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p
|
||||
className="m-0 mb-3 text-sm leading-relaxed"
|
||||
style={{ color: 'var(--app-primary-foreground)' }}
|
||||
>
|
||||
{message}
|
||||
</p>
|
||||
|
||||
{/* Login Button */}
|
||||
<button
|
||||
className="px-4 py-2 text-sm font-medium rounded transition-colors duration-200"
|
||||
style={{
|
||||
backgroundColor: 'var(--app-qwen-orange)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.opacity = '0.9';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.opacity = '1';
|
||||
}}
|
||||
onClick={() => {
|
||||
if (onDismiss) {
|
||||
onDismiss();
|
||||
}
|
||||
onLoginClick();
|
||||
}}
|
||||
>
|
||||
Login Now
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Optional Close Button */}
|
||||
{onDismiss && (
|
||||
<button
|
||||
className="flex-shrink-0 flex items-center justify-center cursor-pointer rounded"
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
padding: '6px',
|
||||
color: 'var(--app-secondary-foreground)',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor =
|
||||
'var(--app-ghost-button-hover-background)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
}}
|
||||
aria-label="Dismiss"
|
||||
onClick={onDismiss}
|
||||
>
|
||||
<CloseIcon className="w-[10px] h-[10px]" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -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);
|
||||
|
||||
@@ -32,10 +32,6 @@ export const FileIcon: React.FC<IconProps> = ({
|
||||
</svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* File list icon (16x16)
|
||||
* Used for file type indicator in context pills
|
||||
*/
|
||||
export const FileListIcon: React.FC<IconProps> = ({
|
||||
size = 16,
|
||||
className,
|
||||
|
||||
@@ -137,10 +137,6 @@ export const CloseIcon: React.FC<IconProps> = ({
|
||||
</svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Close X icon for context pills (16x16)
|
||||
* Used to remove attachments
|
||||
*/
|
||||
export const CloseSmallIcon: React.FC<IconProps> = ({
|
||||
size = 16,
|
||||
className,
|
||||
|
||||
@@ -149,10 +149,6 @@ export const UserIcon: React.FC<IconProps> = ({
|
||||
</svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Symbol arrow icon (16x16)
|
||||
* Used for symbol type in context pills
|
||||
*/
|
||||
export const SymbolIcon: React.FC<IconProps> = ({
|
||||
size = 16,
|
||||
className,
|
||||
@@ -172,10 +168,6 @@ export const SymbolIcon: React.FC<IconProps> = ({
|
||||
</svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Selection/text lines icon (16x16)
|
||||
* Used for selection type in context pills
|
||||
*/
|
||||
export const SelectionIcon: React.FC<IconProps> = ({
|
||||
size = 16,
|
||||
className,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<ClaudeCompletionMenuProps> = ({
|
||||
items,
|
||||
onSelect,
|
||||
onClose,
|
||||
title,
|
||||
selectedIndex = 0,
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(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 (
|
||||
<div
|
||||
ref={containerRef}
|
||||
role="menu"
|
||||
className={[
|
||||
// Semantic class name for readability (no CSS attached)
|
||||
'completion-menu',
|
||||
// Positioning and container styling (Tailwind)
|
||||
'absolute bottom-full left-0 right-0 mb-2 flex flex-col overflow-hidden',
|
||||
'rounded-large border bg-[var(--app-menu-background)]',
|
||||
'border-[var(--app-input-border)] max-h-[50vh] z-[1000]',
|
||||
// Mount animation (fade + slight slide up) via keyframes
|
||||
mounted ? 'animate-completion-menu-enter' : '',
|
||||
].join(' ')}
|
||||
>
|
||||
{/* Optional top spacer for visual separation from the input */}
|
||||
<div className="h-1" />
|
||||
<div
|
||||
className={[
|
||||
// Semantic
|
||||
'completion-menu-list',
|
||||
// Scroll area
|
||||
'flex max-h-[300px] flex-col overflow-y-auto',
|
||||
// Spacing driven by theme vars
|
||||
'p-[var(--app-list-padding)] pb-2 gap-[var(--app-list-gap)]',
|
||||
].join(' ')}
|
||||
>
|
||||
{title && (
|
||||
<div className="completion-menu-section-label px-3 py-1 text-[var(--app-primary-foreground)] opacity-50 text-[0.9em]">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
{items.map((item, index) => {
|
||||
const isActive = index === selected;
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
data-index={index}
|
||||
role="menuitem"
|
||||
onClick={() => 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(' ')}
|
||||
>
|
||||
<div className="completion-menu-item-row flex items-center justify-between gap-2">
|
||||
{item.icon && (
|
||||
<span className="completion-menu-item-icon inline-flex h-4 w-4 items-center justify-center text-[var(--vscode-symbolIcon-fileForeground,#cccccc)]">
|
||||
{item.icon}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={[
|
||||
'completion-menu-item-label flex-1 truncate',
|
||||
isActive
|
||||
? 'text-[var(--app-list-active-foreground)]'
|
||||
: 'text-[var(--app-primary-foreground)]',
|
||||
].join(' ')}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
{item.description && (
|
||||
<span
|
||||
className="completion-menu-item-desc max-w-[50%] truncate text-[0.9em] text-[var(--app-secondary-foreground)] opacity-70"
|
||||
title={item.description}
|
||||
>
|
||||
{item.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<FileLinkProps> = ({
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
className={`file-link ${disableClick ? 'file-link-disabled' : ''} ${className}`}
|
||||
className={[
|
||||
// Keep a semantic handle for scoped overrides (e.g. DiffDisplay.css)
|
||||
'file-link',
|
||||
// Layout + interaction
|
||||
'inline-flex items-baseline',
|
||||
disableClick
|
||||
? 'pointer-events-none cursor-[inherit] hover:no-underline'
|
||||
: 'cursor-pointer',
|
||||
// Typography + color: match theme body text and fixed size
|
||||
'text-[11px] no-underline hover:underline',
|
||||
'text-[var(--app-primary-foreground)]',
|
||||
// Transitions
|
||||
'transition-colors duration-100 ease-in-out',
|
||||
// Focus ring (keyboard nav)
|
||||
'focus:outline focus:outline-1 focus:outline-[var(--vscode-focusBorder)] focus:outline-offset-2 focus:rounded-[2px]',
|
||||
// Active state
|
||||
'active:opacity-80',
|
||||
className,
|
||||
].join(' ')}
|
||||
onClick={handleClick}
|
||||
title={fullDisplayText}
|
||||
role="button"
|
||||
aria-label={`Open file: ${fullDisplayText}`}
|
||||
// Inherit font family from context so it matches theme body text.
|
||||
>
|
||||
<span className="file-link-path">{displayPath}</span>
|
||||
<span className="file-link-path font-medium">{displayPath}</span>
|
||||
{line !== null && line !== undefined && (
|
||||
<span className="file-link-location">
|
||||
<span className="file-link-location opacity-70 text-[0.9em] font-normal dark:opacity-60">
|
||||
:{line}
|
||||
{column !== null && column !== undefined && <>:{column}</>}
|
||||
</span>
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user