mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
feat(vscode-ide-companion): 增加代码编辑功能和文件操作支持
- 实现了与 Claude Code 类似的代码编辑功能 - 添加了文件打开、保存等操作的支持 - 优化了消息显示,增加了代码高亮和文件路径点击功能 - 改进了用户界面,增加了编辑模式切换和思考模式功能
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* MessageContent styles
|
||||
*/
|
||||
|
||||
/* Code block styles */
|
||||
.message-code-block {
|
||||
background-color: var(--app-code-background, rgba(0, 0, 0, 0.05));
|
||||
border: 1px solid var(--app-primary-border-color);
|
||||
border-radius: var(--corner-radius-small, 4px);
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
overflow-x: auto;
|
||||
font-family: var(--app-monospace-font-family, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.message-code-block code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
font-family: inherit;
|
||||
color: var(--app-primary-foreground);
|
||||
}
|
||||
|
||||
/* Inline code styles */
|
||||
.message-inline-code {
|
||||
background-color: var(--app-code-background, rgba(0, 0, 0, 0.05));
|
||||
border: 1px solid var(--app-primary-border-color);
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
font-family: var(--app-monospace-font-family, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace);
|
||||
font-size: 0.9em;
|
||||
color: var(--app-primary-foreground);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* File path link styles */
|
||||
.message-file-path {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font-family: var(--app-monospace-font-family, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace);
|
||||
font-size: 0.95em;
|
||||
color: var(--app-link-foreground, #007ACC);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.message-file-path:hover {
|
||||
color: var(--app-link-active-foreground, #005A9E);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.message-file-path:active {
|
||||
color: var(--app-link-active-foreground, #005A9E);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* MessageContent component - renders message with code highlighting and clickable file paths
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import './MessageContent.css';
|
||||
|
||||
interface MessageContentProps {
|
||||
content: string;
|
||||
onFileClick?: (filePath: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Regular expressions for parsing content
|
||||
*/
|
||||
const FILE_PATH_REGEX =
|
||||
/([a-zA-Z]:)?([/\\][\w\-. ]+)+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|yaml|yml|toml|xml|html|vue|svelte)/gi;
|
||||
const CODE_BLOCK_REGEX = /```(\w+)?\n([\s\S]*?)```/g;
|
||||
const INLINE_CODE_REGEX = /`([^`]+)`/g;
|
||||
|
||||
/**
|
||||
* Parses message content and renders with syntax highlighting and clickable file paths
|
||||
*/
|
||||
export const MessageContent: React.FC<MessageContentProps> = ({
|
||||
content,
|
||||
onFileClick,
|
||||
}) => {
|
||||
/**
|
||||
* Parse and render content with special handling for code blocks, inline code, and file paths
|
||||
*/
|
||||
const renderContent = () => {
|
||||
const parts: React.ReactNode[] = [];
|
||||
let lastIndex = 0;
|
||||
let matchIndex = 0;
|
||||
|
||||
// First, handle code blocks
|
||||
const codeBlockMatches = Array.from(content.matchAll(CODE_BLOCK_REGEX));
|
||||
|
||||
codeBlockMatches.forEach((match) => {
|
||||
const [fullMatch, language, code] = match;
|
||||
const startIndex = match.index!;
|
||||
|
||||
// Add text before code block
|
||||
if (startIndex > lastIndex) {
|
||||
const textBefore = content.slice(lastIndex, startIndex);
|
||||
parts.push(...renderTextWithInlineCodeAndPaths(textBefore, matchIndex));
|
||||
matchIndex++;
|
||||
}
|
||||
|
||||
// Add code block
|
||||
parts.push(
|
||||
<pre key={`code-${matchIndex}`} className="message-code-block">
|
||||
<code className={`language-${language || 'plaintext'}`}>{code}</code>
|
||||
</pre>,
|
||||
);
|
||||
matchIndex++;
|
||||
|
||||
lastIndex = startIndex + fullMatch.length;
|
||||
});
|
||||
|
||||
// Add remaining text
|
||||
if (lastIndex < content.length) {
|
||||
const remainingText = content.slice(lastIndex);
|
||||
parts.push(
|
||||
...renderTextWithInlineCodeAndPaths(remainingText, matchIndex),
|
||||
);
|
||||
}
|
||||
|
||||
return parts.length > 0 ? parts : content;
|
||||
};
|
||||
|
||||
/**
|
||||
* Render text with inline code and file paths
|
||||
*/
|
||||
const renderTextWithInlineCodeAndPaths = (
|
||||
text: string,
|
||||
startIndex: number,
|
||||
) => {
|
||||
const parts: React.ReactNode[] = [];
|
||||
let lastIndex = 0;
|
||||
let matchIndex = startIndex;
|
||||
|
||||
// Split by inline code first
|
||||
const inlineCodeMatches = Array.from(text.matchAll(INLINE_CODE_REGEX));
|
||||
|
||||
if (inlineCodeMatches.length === 0) {
|
||||
// No inline code, just check for file paths
|
||||
return renderTextWithFilePaths(text, matchIndex);
|
||||
}
|
||||
|
||||
inlineCodeMatches.forEach((match) => {
|
||||
const [fullMatch, code] = match;
|
||||
const startIdx = match.index!;
|
||||
|
||||
// Add text before inline code (may contain file paths)
|
||||
if (startIdx > lastIndex) {
|
||||
parts.push(
|
||||
...renderTextWithFilePaths(
|
||||
text.slice(lastIndex, startIdx),
|
||||
matchIndex,
|
||||
),
|
||||
);
|
||||
matchIndex++;
|
||||
}
|
||||
|
||||
// Add inline code
|
||||
parts.push(
|
||||
<code key={`inline-${matchIndex}`} className="message-inline-code">
|
||||
{code}
|
||||
</code>,
|
||||
);
|
||||
matchIndex++;
|
||||
|
||||
lastIndex = startIdx + fullMatch.length;
|
||||
});
|
||||
|
||||
// Add remaining text
|
||||
if (lastIndex < text.length) {
|
||||
parts.push(...renderTextWithFilePaths(text.slice(lastIndex), matchIndex));
|
||||
}
|
||||
|
||||
return parts.length > 0 ? parts : [text];
|
||||
};
|
||||
|
||||
/**
|
||||
* Render text with file paths
|
||||
*/
|
||||
const renderTextWithFilePaths = (text: string, startIndex: number) => {
|
||||
const parts: React.ReactNode[] = [];
|
||||
let lastIndex = 0;
|
||||
let matchIndex = startIndex;
|
||||
|
||||
const filePathMatches = Array.from(text.matchAll(FILE_PATH_REGEX));
|
||||
|
||||
filePathMatches.forEach((match) => {
|
||||
const fullMatch = match[0];
|
||||
const startIdx = match.index!;
|
||||
|
||||
// Add text before file path
|
||||
if (startIdx > lastIndex) {
|
||||
parts.push(text.slice(lastIndex, startIdx));
|
||||
}
|
||||
|
||||
// Add file path link
|
||||
parts.push(
|
||||
<button
|
||||
key={`path-${matchIndex}`}
|
||||
className="message-file-path"
|
||||
onClick={() => onFileClick?.(fullMatch)}
|
||||
title={`Open ${fullMatch}`}
|
||||
>
|
||||
{fullMatch}
|
||||
</button>,
|
||||
);
|
||||
|
||||
matchIndex++;
|
||||
lastIndex = startIdx + fullMatch.length;
|
||||
});
|
||||
|
||||
// Add remaining text
|
||||
if (lastIndex < text.length) {
|
||||
parts.push(text.slice(lastIndex));
|
||||
}
|
||||
|
||||
return parts.length > 0 ? parts : [text];
|
||||
};
|
||||
|
||||
return <>{renderContent()}</>;
|
||||
};
|
||||
@@ -39,7 +39,9 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
||||
// Close drawer on Escape key
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!isOpen) return;
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Close on Escape
|
||||
if (e.key === 'Escape' && onClose) {
|
||||
|
||||
@@ -6,14 +6,15 @@
|
||||
|
||||
/**
|
||||
* PlanDisplay.css - Styles for the task plan component
|
||||
* Simple, clean timeline-style design
|
||||
*/
|
||||
|
||||
.plan-display {
|
||||
background-color: rgba(100, 150, 255, 0.05);
|
||||
border: 1px solid rgba(100, 150, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
background: var(--app-secondary-background);
|
||||
border: 1px solid var(--app-transparent-inner-border);
|
||||
border-radius: var(--corner-radius-medium);
|
||||
padding: 16px;
|
||||
margin: 8px 0;
|
||||
margin: 12px 0;
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@@ -21,92 +22,111 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-bottom: 16px;
|
||||
color: var(--app-primary-foreground);
|
||||
}
|
||||
|
||||
.plan-icon {
|
||||
font-size: 18px;
|
||||
.plan-header-icon {
|
||||
flex-shrink: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.plan-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(150, 180, 255, 1);
|
||||
color: var(--app-primary-foreground);
|
||||
}
|
||||
|
||||
.plan-entries {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.plan-entry {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Vertical line on the left */
|
||||
.plan-entry-line {
|
||||
position: absolute;
|
||||
left: 7px;
|
||||
top: 24px;
|
||||
bottom: -8px;
|
||||
width: 2px;
|
||||
background: var(--app-qwen-clay-button-orange);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.plan-entry:last-child .plan-entry-line {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Icon container */
|
||||
.plan-entry-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.plan-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.plan-entry-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
background-color: var(--vscode-input-background);
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
min-height: 24px;
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
.plan-entry[data-priority="high"] {
|
||||
border-left-color: #ff6b6b;
|
||||
.plan-entry-number {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--app-secondary-foreground);
|
||||
flex-shrink: 0;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.plan-entry[data-priority="medium"] {
|
||||
border-left-color: #ffd93d;
|
||||
.plan-entry-text {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--app-primary-foreground);
|
||||
}
|
||||
|
||||
.plan-entry[data-priority="low"] {
|
||||
border-left-color: #6bcf7f;
|
||||
}
|
||||
|
||||
.plan-entry.completed {
|
||||
/* Status-specific styles */
|
||||
.plan-entry.completed .plan-entry-text {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.plan-entry.completed .plan-entry-content {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.plan-entry.in_progress {
|
||||
background-color: rgba(100, 150, 255, 0.1);
|
||||
border-left-width: 4px;
|
||||
.plan-entry.in_progress .plan-entry-text {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.plan-entry-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.plan-entry-status,
|
||||
.plan-entry-priority {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.plan-entry-index {
|
||||
font-size: 12px;
|
||||
.plan-entry.in_progress .plan-entry-number {
|
||||
color: var(--app-qwen-orange);
|
||||
font-weight: 600;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.plan-entry-content {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
|
||||
@@ -21,55 +21,123 @@ interface PlanDisplayProps {
|
||||
* PlanDisplay component - displays AI's task plan/todo list
|
||||
*/
|
||||
export const PlanDisplay: React.FC<PlanDisplayProps> = ({ entries }) => {
|
||||
const getPriorityIcon = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high':
|
||||
return '🔴';
|
||||
case 'medium':
|
||||
return '🟡';
|
||||
case 'low':
|
||||
return '🟢';
|
||||
default:
|
||||
return '⚪';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
const getStatusIcon = (status: string, _index: number) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return '⏱️';
|
||||
case 'in_progress':
|
||||
return '⚙️';
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
className="plan-icon in-progress"
|
||||
>
|
||||
<rect
|
||||
x="2"
|
||||
y="2"
|
||||
width="12"
|
||||
height="12"
|
||||
rx="2"
|
||||
fill="var(--app-qwen-orange)"
|
||||
/>
|
||||
<path
|
||||
d="M7 4L7 12M10 8L4 8"
|
||||
stroke="var(--app-qwen-ivory)"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'completed':
|
||||
return '✅';
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
className="plan-icon completed"
|
||||
>
|
||||
<rect
|
||||
x="2"
|
||||
y="2"
|
||||
width="12"
|
||||
height="12"
|
||||
rx="2"
|
||||
fill="var(--app-qwen-green, #6BCF7F)"
|
||||
/>
|
||||
<path
|
||||
d="M5 8L7 10L11 6"
|
||||
stroke="var(--app-qwen-ivory)"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return '❓';
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
className="plan-icon pending"
|
||||
>
|
||||
<rect
|
||||
x="2.5"
|
||||
y="2.5"
|
||||
width="11"
|
||||
height="11"
|
||||
rx="2"
|
||||
stroke="var(--app-secondary-foreground)"
|
||||
strokeWidth="1"
|
||||
fill="transparent"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="plan-display">
|
||||
<div className="plan-header">
|
||||
<span className="plan-icon">📋</span>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
className="plan-header-icon"
|
||||
>
|
||||
<rect
|
||||
x="3"
|
||||
y="3"
|
||||
width="14"
|
||||
height="14"
|
||||
rx="2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M3 7H17M7 3V7M13 3V7"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<span className="plan-title">Task Plan</span>
|
||||
</div>
|
||||
<div className="plan-entries">
|
||||
{entries.map((entry, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`plan-entry ${entry.status}`}
|
||||
data-priority={entry.priority}
|
||||
>
|
||||
<div className="plan-entry-header">
|
||||
<span className="plan-entry-status">
|
||||
{getStatusIcon(entry.status)}
|
||||
</span>
|
||||
<span className="plan-entry-priority">
|
||||
{getPriorityIcon(entry.priority)}
|
||||
</span>
|
||||
<span className="plan-entry-index">{index + 1}.</span>
|
||||
<div key={index} className={`plan-entry ${entry.status}`}>
|
||||
<div className="plan-entry-line"></div>
|
||||
<div className="plan-entry-icon">
|
||||
{getStatusIcon(entry.status, index)}
|
||||
</div>
|
||||
<div className="plan-entry-content">
|
||||
<span className="plan-entry-number">{index + 1}.</span>
|
||||
<span className="plan-entry-text">{entry.content}</span>
|
||||
</div>
|
||||
<div className="plan-entry-content">{entry.content}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
StatusIndicator,
|
||||
CodeBlock,
|
||||
LocationsList,
|
||||
DiffDisplay,
|
||||
} from './shared/LayoutComponents.js';
|
||||
import { DiffDisplay } from './shared/DiffDisplay.js';
|
||||
import {
|
||||
formatValue,
|
||||
safeTitle,
|
||||
|
||||
@@ -14,9 +14,10 @@ import {
|
||||
StatusIndicator,
|
||||
CodeBlock,
|
||||
LocationsList,
|
||||
DiffDisplay,
|
||||
} from './shared/LayoutComponents.js';
|
||||
import { DiffDisplay } from './shared/DiffDisplay.js';
|
||||
import { formatValue, safeTitle, groupContent } from './shared/utils.js';
|
||||
import { useVSCode } from '../../hooks/useVSCode.js';
|
||||
|
||||
/**
|
||||
* Specialized component for Write/Edit tool calls
|
||||
@@ -26,10 +27,24 @@ export const WriteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { kind, title, status, rawInput, content, locations } = toolCall;
|
||||
const titleText = safeTitle(title);
|
||||
const isEdit = kind.toLowerCase() === 'edit';
|
||||
const vscode = useVSCode();
|
||||
|
||||
// Group content by type
|
||||
const { textOutputs, errors, diffs, otherData } = groupContent(content);
|
||||
|
||||
const handleOpenDiff = (
|
||||
path: string | undefined,
|
||||
oldText: string | null | undefined,
|
||||
newText: string | undefined,
|
||||
) => {
|
||||
if (path) {
|
||||
vscode.postMessage({
|
||||
type: 'openDiff',
|
||||
data: { path, oldText: oldText || '', newText: newText || '' },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolCallCard icon="✏️">
|
||||
{/* Title row */}
|
||||
@@ -59,6 +74,9 @@ export const WriteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
path={item.path}
|
||||
oldText={item.oldText}
|
||||
newText={item.newText}
|
||||
onOpenDiff={() =>
|
||||
handleOpenDiff(item.path, item.oldText, item.newText)
|
||||
}
|
||||
/>
|
||||
</ToolCallRow>
|
||||
),
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Diff display component for showing file changes
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
/**
|
||||
* Props for DiffDisplay
|
||||
*/
|
||||
interface DiffDisplayProps {
|
||||
path?: string;
|
||||
oldText?: string | null;
|
||||
newText?: string;
|
||||
onOpenDiff?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display diff with before/after sections and option to open in VSCode diff viewer
|
||||
*/
|
||||
export const DiffDisplay: React.FC<DiffDisplayProps> = ({
|
||||
path,
|
||||
oldText,
|
||||
newText,
|
||||
onOpenDiff,
|
||||
}) => (
|
||||
<div className="diff-display-container">
|
||||
<div className="diff-header">
|
||||
<div className="diff-file-path">
|
||||
<strong>{path || 'Unknown file'}</strong>
|
||||
</div>
|
||||
{onOpenDiff && (
|
||||
<button
|
||||
className="open-diff-button"
|
||||
onClick={onOpenDiff}
|
||||
title="Open in VS Code diff viewer"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path
|
||||
d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M5.25 8a.75.75 0 0 1 .75-.75h4a.75.75 0 0 1 0 1.5H6a.75.75 0 0 1-.75-.75z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Open Diff
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{oldText !== undefined && (
|
||||
<div className="diff-section">
|
||||
<div className="diff-label">Before:</div>
|
||||
<pre className="code-block">
|
||||
<div className="code-content">{oldText || '(empty)'}</div>
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{newText !== undefined && (
|
||||
<div className="diff-section">
|
||||
<div className="diff-label">After:</div>
|
||||
<pre className="code-block">
|
||||
<div className="code-content">{newText}</div>
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -107,55 +107,3 @@ export const LocationsList: React.FC<LocationsListProps> = ({ locations }) => (
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
/**
|
||||
* Props for DiffDisplay
|
||||
*/
|
||||
interface DiffDisplayProps {
|
||||
path?: string;
|
||||
oldText?: string | null;
|
||||
newText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display diff with before/after sections
|
||||
*/
|
||||
export const DiffDisplay: React.FC<DiffDisplayProps> = ({
|
||||
path,
|
||||
oldText,
|
||||
newText,
|
||||
}) => (
|
||||
<div>
|
||||
<div>
|
||||
<strong>{path || 'Unknown file'}</strong>
|
||||
</div>
|
||||
{oldText !== undefined && (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
opacity: 0.5,
|
||||
fontSize: '0.85em',
|
||||
marginTop: '4px',
|
||||
}}
|
||||
>
|
||||
Before:
|
||||
</div>
|
||||
<pre className="code-block">{oldText || '(empty)'}</pre>
|
||||
</div>
|
||||
)}
|
||||
{newText !== undefined && (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
opacity: 0.5,
|
||||
fontSize: '0.85em',
|
||||
marginTop: '4px',
|
||||
}}
|
||||
>
|
||||
After:
|
||||
</div>
|
||||
<pre className="code-block">{newText}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user