issue: add copy functionality to file manager and tool-call side panel

This commit is contained in:
Chaitanya045 2025-08-01 08:20:20 +05:30
parent d5159f5fba
commit a004a1b2ee
4 changed files with 243 additions and 7 deletions

View File

@ -21,6 +21,8 @@ import {
FileText,
ChevronDown,
Archive,
Copy,
Check,
} from 'lucide-react';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
@ -160,6 +162,10 @@ export function FileViewerModal({
currentFile: string;
} | null>(null);
// Add state for copy functionality
const [isCopyingPath, setIsCopyingPath] = useState(false);
const [isCopyingContent, setIsCopyingContent] = useState(false);
// Setup project with sandbox URL if not provided directly
useEffect(() => {
if (project) {
@ -849,6 +855,43 @@ export function FileViewerModal({
return filePath ? filePath.toLowerCase().endsWith('.md') : false;
}, []);
// Copy functions
const copyToClipboard = useCallback(async (text: string) => {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.error('Failed to copy text: ', err);
return false;
}
}, []);
const handleCopyPath = useCallback(async () => {
if (!textContentForRenderer) return;
setIsCopyingPath(true);
const success = await copyToClipboard(textContentForRenderer);
if (success) {
toast.success('File content copied to clipboard');
} else {
toast.error('Failed to copy file content');
}
setTimeout(() => setIsCopyingPath(false), 500);
}, [textContentForRenderer, copyToClipboard]);
const handleCopyContent = useCallback(async () => {
if (!textContentForRenderer) return;
setIsCopyingContent(true);
const success = await copyToClipboard(textContentForRenderer);
if (success) {
toast.success('File content copied to clipboard');
} else {
toast.error('Failed to copy file content');
}
setTimeout(() => setIsCopyingContent(false), 500);
}, [textContentForRenderer, copyToClipboard]);
// Handle PDF export for markdown files
const handleExportPdf = useCallback(
async (orientation: 'portrait' | 'landscape' = 'portrait') => {
@ -1313,6 +1356,24 @@ export function FileViewerModal({
<div className="flex items-center gap-2 flex-shrink-0">
{selectedFilePath && (
<>
{/* Copy content button - only show for text files */}
{textContentForRenderer && (
<Button
variant="outline"
size="sm"
onClick={handleCopyContent}
disabled={isCopyingContent || isCachedFileLoading}
className="h-8 gap-1"
>
{isCopyingContent ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
<span className="hidden sm:inline">Copy</span>
</Button>
)}
<Button
variant="outline"
size="sm"

View File

@ -6,7 +6,7 @@ import React from 'react';
import { Slider } from '@/components/ui/slider';
import { Skeleton } from '@/components/ui/skeleton';
import { ApiMessageType } from '@/components/thread/types';
import { CircleDashed, X, ChevronLeft, ChevronRight, Computer, Radio, Maximize2, Minimize2 } from 'lucide-react';
import { CircleDashed, X, ChevronLeft, ChevronRight, Computer, Radio, Maximize2, Minimize2, Copy, Check } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useIsMobile } from '@/hooks/use-mobile';
import { Button } from '@/components/ui/button';
@ -82,6 +82,9 @@ export function ToolCallSidePanel({
const [toolCallSnapshots, setToolCallSnapshots] = React.useState<ToolCallSnapshot[]>([]);
const [isInitialized, setIsInitialized] = React.useState(false);
// Add copy functionality state
const [isCopyingContent, setIsCopyingContent] = React.useState(false);
const isMobile = useIsMobile();
const handleClose = React.useCallback(() => {
@ -215,6 +218,55 @@ export function ToolCallSidePanel({
const isSuccess = isStreaming ? true : getActualSuccess(displayToolCall);
// Copy functions
const copyToClipboard = React.useCallback(async (text: string) => {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.error('Failed to copy text: ', err);
return false;
}
}, []);
const handleCopyContent = React.useCallback(async () => {
const toolContent = displayToolCall?.toolResult?.content;
if (!toolContent || toolContent === 'STREAMING') return;
// Try to extract file content from tool result
let fileContent = '';
// If the tool result is JSON, try to extract file content
try {
const parsed = JSON.parse(toolContent);
if (parsed.content && typeof parsed.content === 'string') {
fileContent = parsed.content;
} else if (parsed.file_content && typeof parsed.file_content === 'string') {
fileContent = parsed.file_content;
} else if (parsed.result && typeof parsed.result === 'string') {
fileContent = parsed.result;
} else if (parsed.toolOutput && typeof parsed.toolOutput === 'string') {
fileContent = parsed.toolOutput;
} else {
// If no string content found, stringify the object
fileContent = JSON.stringify(parsed, null, 2);
}
} catch (e) {
// If it's not JSON, use the content as is
fileContent = typeof toolContent === 'string' ? toolContent : JSON.stringify(toolContent, null, 2);
}
setIsCopyingContent(true);
const success = await copyToClipboard(fileContent);
if (success) {
// Use toast if available, otherwise just log
console.log('File content copied to clipboard');
} else {
console.error('Failed to copy file content');
}
setTimeout(() => setIsCopyingContent(false), 500);
}, [displayToolCall?.toolResult?.content, copyToClipboard]);
const internalNavigate = React.useCallback((newIndex: number, source: string = 'internal') => {
if (newIndex < 0 || newIndex >= totalCalls) return;

View File

@ -6,12 +6,15 @@ import {
AlertTriangle,
Clock,
Wrench,
Copy,
Check,
} from 'lucide-react';
import { ToolViewProps } from './types';
import { formatTimestamp, getToolTitle, extractToolData } from './utils';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from '@/components/ui/button';
import { LoadingState } from './shared/LoadingState';
export function GenericToolView({
@ -119,6 +122,47 @@ export function GenericToolView({
[toolContent],
);
// Add copy functionality state
const [isCopyingInput, setIsCopyingInput] = React.useState(false);
const [isCopyingOutput, setIsCopyingOutput] = React.useState(false);
// Copy functions
const copyToClipboard = React.useCallback(async (text: string) => {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.error('Failed to copy text: ', err);
return false;
}
}, []);
const handleCopyInput = React.useCallback(async () => {
if (!formattedAssistantContent) return;
setIsCopyingInput(true);
const success = await copyToClipboard(formattedAssistantContent);
if (success) {
console.log('Tool input copied to clipboard');
} else {
console.error('Failed to copy tool input');
}
setTimeout(() => setIsCopyingInput(false), 500);
}, [formattedAssistantContent, copyToClipboard]);
const handleCopyOutput = React.useCallback(async () => {
if (!formattedToolContent) return;
setIsCopyingOutput(true);
const success = await copyToClipboard(formattedToolContent);
if (success) {
console.log('Tool output copied to clipboard');
} else {
console.error('Failed to copy tool output');
}
setTimeout(() => setIsCopyingOutput(false), 500);
}, [formattedToolContent, copyToClipboard]);
return (
<Card className="gap-0 flex border shadow-none border-t border-b-0 border-x-0 p-0 rounded-none flex-col h-full overflow-hidden bg-card">
<CardHeader className="h-14 bg-zinc-50/80 dark:bg-zinc-900/80 backdrop-blur-sm border-b p-2 px-4 space-y-2">
@ -169,9 +213,25 @@ export function GenericToolView({
<div className="p-4 space-y-4">
{formattedAssistantContent && (
<div className="space-y-2">
<div className="text-sm font-medium text-zinc-700 dark:text-zinc-300 flex items-center">
<Wrench className="h-4 w-4 mr-2 text-zinc-500 dark:text-zinc-400" />
Input
<div className="text-sm font-medium text-zinc-700 dark:text-zinc-300 flex items-center justify-between">
<div className="flex items-center">
<Wrench className="h-4 w-4 mr-2 text-zinc-500 dark:text-zinc-400" />
Input
</div>
<Button
variant="ghost"
size="sm"
onClick={handleCopyInput}
disabled={isCopyingInput}
className="h-6 w-6 p-0"
title="Copy input"
>
{isCopyingInput ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
<div className="border-muted bg-muted/20 rounded-lg overflow-hidden border">
<div className="p-4">
@ -185,9 +245,25 @@ export function GenericToolView({
{formattedToolContent && (
<div className="space-y-2">
<div className="text-sm font-medium text-zinc-700 dark:text-zinc-300 flex items-center">
<Wrench className="h-4 w-4 mr-2 text-zinc-500 dark:text-zinc-400" />
Output
<div className="text-sm font-medium text-zinc-700 dark:text-zinc-300 flex items-center justify-between">
<div className="flex items-center">
<Wrench className="h-4 w-4 mr-2 text-zinc-500 dark:text-zinc-400" />
Output
</div>
<Button
variant="ghost"
size="sm"
onClick={handleCopyOutput}
disabled={isCopyingOutput}
className="h-6 w-6 p-0"
title="Copy output"
>
{isCopyingOutput ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
<div className="border-muted bg-muted/20 rounded-lg overflow-hidden border">
<div className="p-4">

View File

@ -7,6 +7,8 @@ import {
Code,
Eye,
File,
Copy,
Check,
} from 'lucide-react';
import {
extractFilePath,
@ -68,6 +70,33 @@ export function FileOperationToolView({
const { resolvedTheme } = useTheme();
const isDarkTheme = resolvedTheme === 'dark';
// Add copy functionality state
const [isCopyingContent, setIsCopyingContent] = useState(false);
// Copy functions
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.error('Failed to copy text: ', err);
return false;
}
};
const handleCopyContent = async () => {
if (!fileContent) return;
setIsCopyingContent(true);
const success = await copyToClipboard(fileContent);
if (success) {
console.log('File content copied to clipboard');
} else {
console.error('Failed to copy file content');
}
setTimeout(() => setIsCopyingContent(false), 500);
};
const operation = getOperationType(name, assistantContent);
const configs = getOperationConfigs();
const config = configs[operation];
@ -284,6 +313,24 @@ export function FileOperationToolView({
</div>
</div>
<div className='flex items-center gap-2'>
{/* Copy button - only show when there's file content */}
{fileContent && !isStreaming && (
<Button
variant="outline"
size="sm"
onClick={handleCopyContent}
disabled={isCopyingContent}
className="h-8 text-xs bg-white dark:bg-muted/50 hover:bg-zinc-100 dark:hover:bg-zinc-800 shadow-none"
title="Copy file content"
>
{isCopyingContent ? (
<Check className="h-3.5 w-3.5 mr-1.5" />
) : (
<Copy className="h-3.5 w-3.5 mr-1.5" />
)}
<span className="hidden sm:inline">Copy</span>
</Button>
)}
{isHtml && htmlPreviewUrl && !isStreaming && (
<Button variant="outline" size="sm" className="h-8 text-xs bg-white dark:bg-muted/50 hover:bg-zinc-100 dark:hover:bg-zinc-800 shadow-none" asChild>
<a href={htmlPreviewUrl} target="_blank" rel="noopener noreferrer">