diff --git a/packages/webui/src/components/layout/FileLink.tsx b/packages/webui/src/components/layout/FileLink.tsx index 1f8a77a95..9c8945dd5 100644 --- a/packages/webui/src/components/layout/FileLink.tsx +++ b/packages/webui/src/components/layout/FileLink.tsx @@ -69,6 +69,7 @@ function buildFullPath( * - Support line and column number navigation * - Hover to show full path * - Optional display mode (full path vs filename only) + * - Full keyboard accessibility (Enter and Space keys) * * @example * ```tsx @@ -86,22 +87,18 @@ export const FileLink: React.FC = ({ }) => { const platform = usePlatform(); - /** - * Handle click event - Open file using platform-specific method - */ - const handleClick = (e: React.MouseEvent) => { - // Always prevent default behavior (prevent tag # navigation) - e.preventDefault(); + // Check if file opening is available + const canOpenFile = platform.features?.canOpenFile !== false; + const isDisabled = disableClick || !canOpenFile; - if (disableClick) { - // If click is disabled, return directly without stopping propagation - // This allows parent elements to handle click events + /** + * Open file using platform-specific method + */ + const openFile = () => { + if (isDisabled) { return; } - // If click is enabled, stop event propagation - e.stopPropagation(); - // Build full path including line and column numbers const fullPath = buildFullPath(path, line, column); @@ -116,6 +113,32 @@ export const FileLink: React.FC = ({ } }; + /** + * Handle click event + */ + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + if (!isDisabled) { + e.stopPropagation(); + openFile(); + } + }; + + /** + * Handle keyboard event - Support Space key for button behavior + */ + const handleKeyDown = (e: React.KeyboardEvent) => { + if (isDisabled) { + return; + } + // Space key triggers button action (Enter is handled by default for buttons) + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault(); + e.stopPropagation(); + openFile(); + } + }; + // Build display text const displayPath = showFullPath ? path : getFileName(path); @@ -123,31 +146,36 @@ export const FileLink: React.FC = ({ const fullDisplayText = buildFullPath(path, line, column); return ( - {displayPath} {line !== null && line !== undefined && ( @@ -156,6 +184,6 @@ export const FileLink: React.FC = ({ {column !== null && column !== undefined && <>:{column}} )} - + ); }; diff --git a/packages/webui/src/components/toolcalls/shared/copyUtils.tsx b/packages/webui/src/components/toolcalls/shared/copyUtils.tsx index 9ead4cee0..e71169aa1 100644 --- a/packages/webui/src/components/toolcalls/shared/copyUtils.tsx +++ b/packages/webui/src/components/toolcalls/shared/copyUtils.tsx @@ -7,20 +7,28 @@ */ import type React from 'react'; -import { useState } from 'react'; +import { useState, useCallback } from 'react'; +import { usePlatform } from '../../../context/PlatformContext.js'; /** - * Handle copy to clipboard + * Handle copy to clipboard using platform-specific API with fallback * @param text Text to copy * @param event Mouse event to stop propagation + * @param platformCopy Optional platform-specific copy function */ export const handleCopyToClipboard = async ( text: string, event: React.MouseEvent, + platformCopy?: (text: string) => Promise, ): Promise => { event.stopPropagation(); // Prevent triggering the row click try { - await navigator.clipboard.writeText(text); + // Use platform-specific copy if available, otherwise fall back to navigator.clipboard + if (platformCopy) { + await platformCopy(text); + } else { + await navigator.clipboard.writeText(text); + } } catch (err) { console.error('Failed to copy text:', err); } @@ -35,21 +43,36 @@ interface CopyButtonProps { /** * CopyButton - Shared copy button component with Tailwind styles + * Uses PlatformContext for platform-specific clipboard access with fallback * Note: Parent element should have 'group' class for hover effect */ export const CopyButton: React.FC = ({ text }) => { const [showTooltip, setShowTooltip] = useState(false); + const platform = usePlatform(); + + const handleClick = useCallback( + async (e: React.MouseEvent) => { + await handleCopyToClipboard(text, e, platform.copyToClipboard); + setShowTooltip(true); + setTimeout(() => setShowTooltip(false), 1000); + }, + [text, platform.copyToClipboard], + ); + + // Check if copy feature is available + const canCopy = platform.features?.canCopy !== false; + + if (!canCopy) { + return null; + } return (