mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat(vscode-ide-companion): 新增共享 UI 组件 FileLink
- 新增 FileLink 组件用于显示文件链接 - 更新 LayoutComponents 增加通用布局组件 - 新增 utils.ts 提供工具函数 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在工具调用卡片中的样式调整
|
||||
*/
|
||||
.tool-call-card .file-link {
|
||||
/* 在工具调用中略微缩小字体 */
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在代码块中的样式调整
|
||||
*/
|
||||
.code-block .file-link {
|
||||
/* 在代码块中保持等宽字体 */
|
||||
font-family: var(--vscode-editor-font-family, 'Menlo', 'Monaco', 'Courier New', monospace);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* FileLink 组件 - 可点击的文件路径链接
|
||||
* 支持点击打开文件并跳转到指定行号和列号
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useVSCode } from '../../hooks/useVSCode.js';
|
||||
import './FileLink.css';
|
||||
|
||||
/**
|
||||
* Props for FileLink
|
||||
*/
|
||||
interface FileLinkProps {
|
||||
/** 文件路径 */
|
||||
path: string;
|
||||
/** 可选的行号(从 1 开始) */
|
||||
line?: number | null;
|
||||
/** 可选的列号(从 1 开始) */
|
||||
column?: number | null;
|
||||
/** 是否显示完整路径,默认 false(只显示文件名) */
|
||||
showFullPath?: boolean;
|
||||
/** 可选的自定义类名 */
|
||||
className?: string;
|
||||
/** 是否禁用点击行为(当父元素已经处理点击时使用) */
|
||||
disableClick?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从完整路径中提取文件名
|
||||
* @param path 文件路径
|
||||
* @returns 文件名
|
||||
*/
|
||||
function getFileName(path: string): string {
|
||||
const segments = path.split(/[/\\]/);
|
||||
return segments[segments.length - 1] || path;
|
||||
}
|
||||
|
||||
/**
|
||||
* FileLink 组件 - 可点击的文件链接
|
||||
*
|
||||
* 功能:
|
||||
* - 点击打开文件
|
||||
* - 支持行号和列号跳转
|
||||
* - 悬停显示完整路径
|
||||
* - 可选显示模式(完整路径 vs 仅文件名)
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <FileLink path="/src/App.tsx" line={42} />
|
||||
* <FileLink path="/src/components/Button.tsx" line={10} column={5} showFullPath={true} />
|
||||
* ```
|
||||
*/
|
||||
export const FileLink: React.FC<FileLinkProps> = ({
|
||||
path,
|
||||
line,
|
||||
column,
|
||||
showFullPath = false,
|
||||
className = '',
|
||||
disableClick = false,
|
||||
}) => {
|
||||
const vscode = useVSCode();
|
||||
|
||||
/**
|
||||
* 处理点击事件 - 发送消息到 VSCode 打开文件
|
||||
*/
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
// 总是阻止默认行为(防止 <a> 标签的 # 跳转)
|
||||
e.preventDefault();
|
||||
|
||||
if (disableClick) {
|
||||
// 如果禁用点击,直接返回,不阻止冒泡
|
||||
// 这样父元素可以处理点击事件
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果启用点击,阻止事件冒泡
|
||||
e.stopPropagation();
|
||||
|
||||
// 构建包含行号和列号的完整路径
|
||||
let fullPath = path;
|
||||
if (line !== null && line !== undefined) {
|
||||
fullPath += `:${line}`;
|
||||
if (column !== null && column !== undefined) {
|
||||
fullPath += `:${column}`;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[FileLink] Opening file:', fullPath);
|
||||
|
||||
vscode.postMessage({
|
||||
type: 'openFile',
|
||||
data: { path: fullPath },
|
||||
});
|
||||
};
|
||||
|
||||
// 构建显示文本
|
||||
const displayPath = showFullPath ? path : getFileName(path);
|
||||
|
||||
// 构建悬停提示(始终显示完整路径)
|
||||
const fullDisplayText =
|
||||
line !== null && line !== undefined
|
||||
? column !== null && column !== undefined
|
||||
? `${path}:${line}:${column}`
|
||||
: `${path}:${line}`
|
||||
: path;
|
||||
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
className={`file-link ${disableClick ? 'file-link-disabled' : ''} ${className}`}
|
||||
onClick={handleClick}
|
||||
title={fullDisplayText}
|
||||
role="button"
|
||||
aria-label={`Open file: ${fullDisplayText}`}
|
||||
>
|
||||
<span className="file-link-path">{displayPath}</span>
|
||||
{line !== null && line !== undefined && (
|
||||
<span className="file-link-location">
|
||||
:{line}
|
||||
{column !== null && column !== undefined && <>:{column}</>}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { FileLink } from '../../shared/FileLink.js';
|
||||
|
||||
/**
|
||||
* Props for ToolCallCard wrapper
|
||||
@@ -20,11 +21,11 @@ interface ToolCallCardProps {
|
||||
* Main card wrapper with icon
|
||||
*/
|
||||
export const ToolCallCard: React.FC<ToolCallCardProps> = ({
|
||||
icon,
|
||||
icon: _icon,
|
||||
children,
|
||||
}) => (
|
||||
<div className="tool-call-card">
|
||||
<div className="tool-call-icon">{icon}</div>
|
||||
{/* <div className="tool-call-icon">{icon}</div> */}
|
||||
<div className="tool-call-grid">{children}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -95,15 +96,12 @@ interface LocationsListProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* List of file locations
|
||||
* List of file locations with clickable links
|
||||
*/
|
||||
export const LocationsList: React.FC<LocationsListProps> = ({ locations }) => (
|
||||
<>
|
||||
<div className="locations-list">
|
||||
{locations.map((loc, idx) => (
|
||||
<div key={idx}>
|
||||
{loc.path}
|
||||
{loc.line !== null && loc.line !== undefined && `:${loc.line}`}
|
||||
</div>
|
||||
<FileLink key={idx} path={loc.path} line={loc.line} showFullPath={true} />
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -76,6 +76,41 @@ export const getKindIcon = (kind: string): string => {
|
||||
export const shouldShowToolCall = (kind: string): boolean =>
|
||||
!kind.includes('internal');
|
||||
|
||||
/**
|
||||
* Check if a tool call has actual output to display
|
||||
* Returns false for tool calls that completed successfully but have no visible output
|
||||
*/
|
||||
export const hasToolCallOutput = (
|
||||
toolCall: import('./types.js').ToolCallData,
|
||||
): boolean => {
|
||||
// Always show failed tool calls (even without content)
|
||||
if (toolCall.status === 'failed') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Show if there are locations (file paths)
|
||||
if (toolCall.locations && toolCall.locations.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Show if there is content
|
||||
if (toolCall.content && toolCall.content.length > 0) {
|
||||
const grouped = groupContent(toolCall.content);
|
||||
// Has any meaningful content?
|
||||
if (
|
||||
grouped.textOutputs.length > 0 ||
|
||||
grouped.errors.length > 0 ||
|
||||
grouped.diffs.length > 0 ||
|
||||
grouped.otherData.length > 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// No output, don't show
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Group tool call content by type to avoid duplicate labels
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user