From 1eedd36542a8f8a3f678e6d3a08b360decc8edd1 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Fri, 21 Nov 2025 01:51:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(vscode-ide-companion):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=85=B1=E4=BA=AB=20UI=20=E7=BB=84=E4=BB=B6=20FileLin?= =?UTF-8?q?k?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 FileLink 组件用于显示文件链接 - 更新 LayoutComponents 增加通用布局组件 - 新增 utils.ts 提供工具函数 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../webview/components/shared/FileLink.css | 128 +++++++++++++++++ .../webview/components/shared/FileLink.tsx | 129 ++++++++++++++++++ .../toolcalls/shared/LayoutComponents.tsx | 16 +-- .../components/toolcalls/shared/utils.ts | 35 +++++ 4 files changed, 299 insertions(+), 9 deletions(-) create mode 100644 packages/vscode-ide-companion/src/webview/components/shared/FileLink.css create mode 100644 packages/vscode-ide-companion/src/webview/components/shared/FileLink.tsx diff --git a/packages/vscode-ide-companion/src/webview/components/shared/FileLink.css b/packages/vscode-ide-companion/src/webview/components/shared/FileLink.css new file mode 100644 index 00000000..55cae240 --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/components/shared/FileLink.css @@ -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); +} diff --git a/packages/vscode-ide-companion/src/webview/components/shared/FileLink.tsx b/packages/vscode-ide-companion/src/webview/components/shared/FileLink.tsx new file mode 100644 index 00000000..08daf429 --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/components/shared/FileLink.tsx @@ -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 + * + * + * ``` + */ +export const FileLink: React.FC = ({ + path, + line, + column, + showFullPath = false, + className = '', + disableClick = false, +}) => { + const vscode = useVSCode(); + + /** + * 处理点击事件 - 发送消息到 VSCode 打开文件 + */ + const handleClick = (e: React.MouseEvent) => { + // 总是阻止默认行为(防止 标签的 # 跳转) + 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 ( + + {displayPath} + {line !== null && line !== undefined && ( + + :{line} + {column !== null && column !== undefined && <>:{column}} + + )} + + ); +}; 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 2b555141..5ba9170f 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 @@ -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 = ({ - icon, + icon: _icon, children, }) => (
-
{icon}
+ {/*
{icon}
*/}
{children}
); @@ -95,15 +96,12 @@ interface LocationsListProps { } /** - * List of file locations + * List of file locations with clickable links */ export const LocationsList: React.FC = ({ locations }) => ( - <> +
{locations.map((loc, idx) => ( -
- {loc.path} - {loc.line !== null && loc.line !== undefined && `:${loc.line}`} -
+ ))} - +
); diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/utils.ts b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/utils.ts index 5a93e234..0de41a72 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/utils.ts +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/utils.ts @@ -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 */