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 type React from 'react';
|
||||||
|
import { FileLink } from '../../shared/FileLink.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props for ToolCallCard wrapper
|
* Props for ToolCallCard wrapper
|
||||||
@@ -20,11 +21,11 @@ interface ToolCallCardProps {
|
|||||||
* Main card wrapper with icon
|
* Main card wrapper with icon
|
||||||
*/
|
*/
|
||||||
export const ToolCallCard: React.FC<ToolCallCardProps> = ({
|
export const ToolCallCard: React.FC<ToolCallCardProps> = ({
|
||||||
icon,
|
icon: _icon,
|
||||||
children,
|
children,
|
||||||
}) => (
|
}) => (
|
||||||
<div className="tool-call-card">
|
<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 className="tool-call-grid">{children}</div>
|
||||||
</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 }) => (
|
export const LocationsList: React.FC<LocationsListProps> = ({ locations }) => (
|
||||||
<>
|
<div className="locations-list">
|
||||||
{locations.map((loc, idx) => (
|
{locations.map((loc, idx) => (
|
||||||
<div key={idx}>
|
<FileLink key={idx} path={loc.path} line={loc.line} showFullPath={true} />
|
||||||
{loc.path}
|
|
||||||
{loc.line !== null && loc.line !== undefined && `:${loc.line}`}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -76,6 +76,41 @@ export const getKindIcon = (kind: string): string => {
|
|||||||
export const shouldShowToolCall = (kind: string): boolean =>
|
export const shouldShowToolCall = (kind: string): boolean =>
|
||||||
!kind.includes('internal');
|
!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
|
* Group tool call content by type to avoid duplicate labels
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user