diff --git a/src/components/LoginModal.jsx b/src/components/LoginModal.jsx
index 5c3af51..f391c6d 100644
--- a/src/components/LoginModal.jsx
+++ b/src/components/LoginModal.jsx
@@ -1,5 +1,5 @@
import { X } from 'lucide-react';
-import StandaloneShell from './StandaloneShell';
+import StandaloneShell from './standalone-shell/view/StandaloneShell';
import { IS_PLATFORM } from '../constants/config';
/**
diff --git a/src/components/NextTaskBanner.jsx b/src/components/NextTaskBanner.jsx
index 2a55cb8..49eb941 100644
--- a/src/components/NextTaskBanner.jsx
+++ b/src/components/NextTaskBanner.jsx
@@ -3,7 +3,7 @@ import { ArrowRight, List, Clock, Flag, CheckCircle, Circle, AlertCircle, Pause,
import { cn } from '../lib/utils';
import { useTaskMaster } from '../contexts/TaskMasterContext';
import { api } from '../utils/api';
-import Shell from './Shell';
+import Shell from './shell/view/Shell';
import TaskDetail from './TaskDetail';
const NextTaskBanner = ({ onShowAllTasks, onStartTask, className = '' }) => {
diff --git a/src/components/Shell.jsx b/src/components/Shell.jsx
deleted file mode 100644
index 4e6dc3a..0000000
--- a/src/components/Shell.jsx
+++ /dev/null
@@ -1,692 +0,0 @@
-import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
-import { Terminal } from '@xterm/xterm';
-import { FitAddon } from '@xterm/addon-fit';
-import { WebglAddon } from '@xterm/addon-webgl';
-import { WebLinksAddon } from '@xterm/addon-web-links';
-import '@xterm/xterm/css/xterm.css';
-import { useTranslation } from 'react-i18next';
-import { IS_PLATFORM } from '../constants/config';
-
-const xtermStyles = `
- .xterm .xterm-screen {
- outline: none !important;
- }
- .xterm:focus .xterm-screen {
- outline: none !important;
- }
- .xterm-screen:focus {
- outline: none !important;
- }
-`;
-
-if (typeof document !== 'undefined') {
- const styleSheet = document.createElement('style');
- styleSheet.type = 'text/css';
- styleSheet.innerText = xtermStyles;
- document.head.appendChild(styleSheet);
-}
-
-function fallbackCopyToClipboard(text) {
- if (!text || typeof document === 'undefined') return false;
-
- const textarea = document.createElement('textarea');
- textarea.value = text;
- textarea.setAttribute('readonly', '');
- textarea.style.position = 'fixed';
- textarea.style.opacity = '0';
- textarea.style.pointerEvents = 'none';
- document.body.appendChild(textarea);
- textarea.focus();
- textarea.select();
-
- let copied = false;
- try {
- copied = document.execCommand('copy');
- } catch {
- copied = false;
- } finally {
- document.body.removeChild(textarea);
- }
-
- return copied;
-}
-
-const CODEX_DEVICE_AUTH_URL = 'https://auth.openai.com/codex/device';
-
-function isCodexLoginCommand(command) {
- return typeof command === 'string' && /\bcodex\s+login\b/i.test(command);
-}
-
-function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell = false, onProcessComplete, minimal = false, autoConnect = false }) {
- const { t } = useTranslation('chat');
- const terminalRef = useRef(null);
- const terminal = useRef(null);
- const fitAddon = useRef(null);
- const ws = useRef(null);
- const [isConnected, setIsConnected] = useState(false);
- const [isInitialized, setIsInitialized] = useState(false);
- const [isRestarting, setIsRestarting] = useState(false);
- const [lastSessionId, setLastSessionId] = useState(null);
- const [isConnecting, setIsConnecting] = useState(false);
- const [authUrl, setAuthUrl] = useState('');
- const [authUrlCopyStatus, setAuthUrlCopyStatus] = useState('idle');
- const [isAuthPanelHidden, setIsAuthPanelHidden] = useState(false);
-
- const selectedProjectRef = useRef(selectedProject);
- const selectedSessionRef = useRef(selectedSession);
- const initialCommandRef = useRef(initialCommand);
- const isPlainShellRef = useRef(isPlainShell);
- const onProcessCompleteRef = useRef(onProcessComplete);
- const authUrlRef = useRef('');
-
- useEffect(() => {
- selectedProjectRef.current = selectedProject;
- selectedSessionRef.current = selectedSession;
- initialCommandRef.current = initialCommand;
- isPlainShellRef.current = isPlainShell;
- onProcessCompleteRef.current = onProcessComplete;
- });
-
- const openAuthUrlInBrowser = useCallback((url = authUrlRef.current) => {
- if (!url) return false;
-
- const popup = window.open(url, '_blank', 'noopener,noreferrer');
- if (popup) {
- try {
- popup.opener = null;
- } catch {
- // Ignore cross-origin restrictions when trying to null opener
- }
- return true;
- }
-
- return false;
- }, []);
-
- const copyAuthUrlToClipboard = useCallback(async (url = authUrlRef.current) => {
- if (!url) return false;
-
- let copied = false;
- try {
- if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
- await navigator.clipboard.writeText(url);
- copied = true;
- }
- } catch {
- copied = false;
- }
-
- if (!copied) {
- copied = fallbackCopyToClipboard(url);
- }
-
- return copied;
- }, []);
-
- const connectWebSocket = useCallback(async () => {
- if (isConnecting || isConnected) return;
-
- try {
- let wsUrl;
-
- if (IS_PLATFORM) {
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
- wsUrl = `${protocol}//${window.location.host}/shell`;
- } else {
- const token = localStorage.getItem('auth-token');
- if (!token) {
- console.error('No authentication token found for Shell WebSocket connection');
- return;
- }
-
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
- wsUrl = `${protocol}//${window.location.host}/shell?token=${encodeURIComponent(token)}`;
- }
-
- ws.current = new WebSocket(wsUrl);
-
- ws.current.onopen = () => {
- setIsConnected(true);
- setIsConnecting(false);
- authUrlRef.current = '';
- setAuthUrl('');
- setAuthUrlCopyStatus('idle');
- setIsAuthPanelHidden(false);
-
- setTimeout(() => {
- if (fitAddon.current && terminal.current) {
- fitAddon.current.fit();
-
- ws.current.send(JSON.stringify({
- type: 'init',
- projectPath: selectedProjectRef.current.fullPath || selectedProjectRef.current.path,
- sessionId: isPlainShellRef.current ? null : selectedSessionRef.current?.id,
- hasSession: isPlainShellRef.current ? false : !!selectedSessionRef.current,
- provider: isPlainShellRef.current ? 'plain-shell' : (selectedSessionRef.current?.__provider || 'claude'),
- cols: terminal.current.cols,
- rows: terminal.current.rows,
- initialCommand: initialCommandRef.current,
- isPlainShell: isPlainShellRef.current
- }));
- }
- }, 100);
- };
-
- ws.current.onmessage = (event) => {
- try {
- const data = JSON.parse(event.data);
-
- if (data.type === 'output') {
- let output = data.data;
-
- if (isPlainShellRef.current && onProcessCompleteRef.current) {
- const cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, '');
- if (cleanOutput.includes('Process exited with code 0')) {
- onProcessCompleteRef.current(0);
- } else if (cleanOutput.match(/Process exited with code (\d+)/)) {
- const exitCode = parseInt(cleanOutput.match(/Process exited with code (\d+)/)[1]);
- if (exitCode !== 0) {
- onProcessCompleteRef.current(exitCode);
- }
- }
- }
-
- if (terminal.current) {
- terminal.current.write(output);
- }
- } else if (data.type === 'auth_url' && data.url) {
- authUrlRef.current = data.url;
- setAuthUrl(data.url);
- setAuthUrlCopyStatus('idle');
- setIsAuthPanelHidden(false);
- } else if (data.type === 'url_open') {
- if (data.url) {
- authUrlRef.current = data.url;
- setAuthUrl(data.url);
- setAuthUrlCopyStatus('idle');
- setIsAuthPanelHidden(false);
- }
- }
- } catch (error) {
- console.error('[Shell] Error handling WebSocket message:', error, event.data);
- }
- };
-
- ws.current.onclose = (event) => {
- setIsConnected(false);
- setIsConnecting(false);
- setAuthUrlCopyStatus('idle');
- setIsAuthPanelHidden(false);
-
- if (terminal.current) {
- terminal.current.clear();
- terminal.current.write('\x1b[2J\x1b[H');
- }
- };
-
- ws.current.onerror = (error) => {
- setIsConnected(false);
- setIsConnecting(false);
- };
- } catch (error) {
- setIsConnected(false);
- setIsConnecting(false);
- }
- }, [isConnecting, isConnected, openAuthUrlInBrowser]);
-
- const connectToShell = useCallback(() => {
- if (!isInitialized || isConnected || isConnecting) return;
- setIsConnecting(true);
- connectWebSocket();
- }, [isInitialized, isConnected, isConnecting, connectWebSocket]);
-
- const disconnectFromShell = useCallback(() => {
- if (ws.current) {
- ws.current.close();
- ws.current = null;
- }
-
- if (terminal.current) {
- terminal.current.clear();
- terminal.current.write('\x1b[2J\x1b[H');
- }
-
- setIsConnected(false);
- setIsConnecting(false);
- authUrlRef.current = '';
- setAuthUrl('');
- setAuthUrlCopyStatus('idle');
- setIsAuthPanelHidden(false);
- }, []);
-
- const sessionDisplayName = useMemo(() => {
- if (!selectedSession) return null;
- return selectedSession.__provider === 'cursor'
- ? (selectedSession.name || 'Untitled Session')
- : (selectedSession.summary || 'New Session');
- }, [selectedSession]);
-
- const sessionDisplayNameShort = useMemo(() => {
- if (!sessionDisplayName) return null;
- return sessionDisplayName.slice(0, 30);
- }, [sessionDisplayName]);
-
- const sessionDisplayNameLong = useMemo(() => {
- if (!sessionDisplayName) return null;
- return sessionDisplayName.slice(0, 50);
- }, [sessionDisplayName]);
-
- const restartShell = () => {
- setIsRestarting(true);
-
- if (ws.current) {
- ws.current.close();
- ws.current = null;
- }
-
- if (terminal.current) {
- terminal.current.dispose();
- terminal.current = null;
- fitAddon.current = null;
- }
-
- setIsConnected(false);
- setIsInitialized(false);
- authUrlRef.current = '';
- setAuthUrl('');
- setAuthUrlCopyStatus('idle');
- setIsAuthPanelHidden(false);
-
- setTimeout(() => {
- setIsRestarting(false);
- }, 200);
- };
-
- useEffect(() => {
- const currentSessionId = selectedSession?.id || null;
-
- if (lastSessionId !== null && lastSessionId !== currentSessionId && isInitialized) {
- disconnectFromShell();
- }
-
- setLastSessionId(currentSessionId);
- }, [selectedSession?.id, isInitialized, disconnectFromShell]);
-
- useEffect(() => {
- if (!terminalRef.current || !selectedProject || isRestarting || terminal.current) {
- return;
- }
-
-
- terminal.current = new Terminal({
- cursorBlink: true,
- fontSize: 14,
- fontFamily: 'Menlo, Monaco, "Courier New", monospace',
- allowProposedApi: true,
- allowTransparency: false,
- convertEol: true,
- scrollback: 10000,
- tabStopWidth: 4,
- windowsMode: false,
- macOptionIsMeta: true,
- macOptionClickForcesSelection: true,
- theme: {
- background: '#1e1e1e',
- foreground: '#d4d4d4',
- cursor: '#ffffff',
- cursorAccent: '#1e1e1e',
- selection: '#264f78',
- selectionForeground: '#ffffff',
- black: '#000000',
- red: '#cd3131',
- green: '#0dbc79',
- yellow: '#e5e510',
- blue: '#2472c8',
- magenta: '#bc3fbc',
- cyan: '#11a8cd',
- white: '#e5e5e5',
- brightBlack: '#666666',
- brightRed: '#f14c4c',
- brightGreen: '#23d18b',
- brightYellow: '#f5f543',
- brightBlue: '#3b8eea',
- brightMagenta: '#d670d6',
- brightCyan: '#29b8db',
- brightWhite: '#ffffff',
- extendedAnsi: [
- '#000000', '#800000', '#008000', '#808000',
- '#000080', '#800080', '#008080', '#c0c0c0',
- '#808080', '#ff0000', '#00ff00', '#ffff00',
- '#0000ff', '#ff00ff', '#00ffff', '#ffffff'
- ]
- }
- });
-
- fitAddon.current = new FitAddon();
- const webglAddon = new WebglAddon();
- const webLinksAddon = new WebLinksAddon();
-
- terminal.current.loadAddon(fitAddon.current);
- // Disable xterm link auto-detection in minimal (login) mode to avoid partial wrapped URL links.
- if (!minimal) {
- terminal.current.loadAddon(webLinksAddon);
- }
- // Note: ClipboardAddon removed - we handle clipboard operations manually in attachCustomKeyEventHandler
-
- try {
- terminal.current.loadAddon(webglAddon);
- } catch (error) {
- console.warn('[Shell] WebGL renderer unavailable, using Canvas fallback');
- }
-
- terminal.current.open(terminalRef.current);
-
- terminal.current.attachCustomKeyEventHandler((event) => {
- const activeAuthUrl = isCodexLoginCommand(initialCommandRef.current)
- ? CODEX_DEVICE_AUTH_URL
- : authUrlRef.current;
-
- if (
- event.type === 'keydown' &&
- minimal &&
- isPlainShellRef.current &&
- activeAuthUrl &&
- !event.ctrlKey &&
- !event.metaKey &&
- !event.altKey &&
- event.key?.toLowerCase() === 'c'
- ) {
- copyAuthUrlToClipboard(activeAuthUrl).catch(() => {});
- }
-
- if (
- event.type === 'keydown' &&
- (event.ctrlKey || event.metaKey) &&
- event.key?.toLowerCase() === 'c' &&
- terminal.current.hasSelection()
- ) {
- event.preventDefault();
- event.stopPropagation();
- document.execCommand('copy');
- return false;
- }
-
- if (
- event.type === 'keydown' &&
- (event.ctrlKey || event.metaKey) &&
- event.key?.toLowerCase() === 'v'
- ) {
- // Block native browser/xterm paste so clipboard data is only sent after
- // the explicit clipboard-read flow resolves (avoids duplicate pastes).
- event.preventDefault();
- event.stopPropagation();
-
- navigator.clipboard.readText().then(text => {
- if (ws.current && ws.current.readyState === WebSocket.OPEN) {
- ws.current.send(JSON.stringify({
- type: 'input',
- data: text
- }));
- }
- }).catch(() => {});
- return false;
- }
-
- return true;
- });
-
- setTimeout(() => {
- if (fitAddon.current) {
- fitAddon.current.fit();
- if (terminal.current && ws.current && ws.current.readyState === WebSocket.OPEN) {
- ws.current.send(JSON.stringify({
- type: 'resize',
- cols: terminal.current.cols,
- rows: terminal.current.rows
- }));
- }
- }
- }, 100);
-
- setIsInitialized(true);
- terminal.current.onData((data) => {
- if (ws.current && ws.current.readyState === WebSocket.OPEN) {
- ws.current.send(JSON.stringify({
- type: 'input',
- data: data
- }));
- }
- });
-
- const resizeObserver = new ResizeObserver(() => {
- if (fitAddon.current && terminal.current) {
- setTimeout(() => {
- fitAddon.current.fit();
- if (ws.current && ws.current.readyState === WebSocket.OPEN) {
- ws.current.send(JSON.stringify({
- type: 'resize',
- cols: terminal.current.cols,
- rows: terminal.current.rows
- }));
- }
- }, 50);
- }
- });
-
- if (terminalRef.current) {
- resizeObserver.observe(terminalRef.current);
- }
-
- return () => {
- resizeObserver.disconnect();
-
- if (ws.current && (ws.current.readyState === WebSocket.OPEN || ws.current.readyState === WebSocket.CONNECTING)) {
- ws.current.close();
- }
- ws.current = null;
-
- if (terminal.current) {
- terminal.current.dispose();
- terminal.current = null;
- }
- };
- }, [selectedProject?.path || selectedProject?.fullPath, isRestarting, minimal, copyAuthUrlToClipboard]);
-
- useEffect(() => {
- if (!autoConnect || !isInitialized || isConnecting || isConnected) return;
- connectToShell();
- }, [autoConnect, isInitialized, isConnecting, isConnected, connectToShell]);
-
- if (!selectedProject) {
- return (
-
-
-
-
{t('shell.selectProject.title')}
-
{t('shell.selectProject.description')}
-
-
- );
- }
-
- if (minimal) {
- const displayAuthUrl = isCodexLoginCommand(initialCommand)
- ? CODEX_DEVICE_AUTH_URL
- : authUrl;
- const hasAuthUrl = Boolean(displayAuthUrl);
- const showMobileAuthPanel = hasAuthUrl && !isAuthPanelHidden;
- const showMobileAuthPanelToggle = hasAuthUrl && isAuthPanelHidden;
-
- return (
-
-
- {showMobileAuthPanel && (
-
-
-
-
Open or copy the login URL:
-
-
-
event.currentTarget.select()}
- className="w-full rounded border border-gray-600 bg-gray-800 px-2 py-1 text-xs text-gray-100 focus:outline-none focus:ring-1 focus:ring-blue-500"
- aria-label="Authentication URL"
- />
-
-
-
-
-
-
- )}
- {showMobileAuthPanelToggle && (
-
-
-
- )}
-
- );
- }
-
- return (
-
-
-
-
-
- {selectedSession && (
-
- ({sessionDisplayNameShort}...)
-
- )}
- {!selectedSession && (
-
{t('shell.status.newSession')}
- )}
- {!isInitialized && (
-
{t('shell.status.initializing')}
- )}
- {isRestarting && (
-
{t('shell.status.restarting')}
- )}
-
-
- {isConnected && (
-
- )}
-
-
-
-
-
-
-
-
-
- {!isInitialized && (
-
- )}
-
- {isInitialized && !isConnected && !isConnecting && (
-
-
-
-
- {isPlainShell ?
- t('shell.runCommand', { command: initialCommand || t('shell.defaultCommand'), projectName: selectedProject.displayName }) :
- selectedSession ?
- t('shell.resumeSession', { displayName: sessionDisplayNameLong }) :
- t('shell.startSession')
- }
-
-
-
- )}
-
- {isConnecting && (
-
-
-
-
-
{t('shell.connecting')}
-
-
- {isPlainShell ?
- t('shell.runCommand', { command: initialCommand || t('shell.defaultCommand'), projectName: selectedProject.displayName }) :
- t('shell.startCli', { projectName: selectedProject.displayName })
- }
-
-
-
- )}
-
-
- );
-}
-
-export default Shell;
diff --git a/src/components/StandaloneShell.jsx b/src/components/StandaloneShell.jsx
deleted file mode 100644
index 9f6f9f2..0000000
--- a/src/components/StandaloneShell.jsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import React, { useState, useCallback } from 'react';
-import Shell from './Shell.jsx';
-
-/**
- * Generic Shell wrapper that can be used in tabs, modals, and other contexts.
- * Provides a flexible API for both standalone and session-based usage.
- *
- * @param {Object} project - Project object with name, fullPath/path, displayName
- * @param {Object} session - Session object (optional, for tab usage)
- * @param {string} command - Initial command to run (optional)
- * @param {boolean} isPlainShell - Use plain shell mode vs Claude CLI (default: auto-detect)
- * @param {boolean} autoConnect - Whether to auto-connect when mounted (default: true)
- * @param {function} onComplete - Callback when process completes (receives exitCode)
- * @param {function} onClose - Callback for close button (optional)
- * @param {string} title - Custom header title (optional)
- * @param {string} className - Additional CSS classes
- * @param {boolean} showHeader - Whether to show custom header (default: true)
- * @param {boolean} compact - Use compact layout (default: false)
- * @param {boolean} minimal - Use minimal mode: no header, no overlays, auto-connect (default: false)
- */
-function StandaloneShell({
- project,
- session = null,
- command = null,
- isPlainShell = null,
- autoConnect = true,
- onComplete = null,
- onClose = null,
- title = null,
- className = "",
- showHeader = true,
- compact = false,
- minimal = false
-}) {
- const [isCompleted, setIsCompleted] = useState(false);
-
- const shouldUsePlainShell = isPlainShell !== null ? isPlainShell : (command !== null);
-
- const handleProcessComplete = useCallback((exitCode) => {
- setIsCompleted(true);
- if (onComplete) {
- onComplete(exitCode);
- }
- }, [onComplete]);
-
- if (!project) {
- return (
-
-
-
-
No Project Selected
-
A project is required to open a shell
-
-
- );
- }
-
- return (
-
- {/* Optional custom header */}
- {!minimal && showHeader && title && (
-
-
-
-
{title}
- {isCompleted && (
- (Completed)
- )}
-
- {onClose && (
-
- )}
-
-
- )}
-
- {/* Shell component wrapper */}
-
-
-
-
- );
-}
-
-export default StandaloneShell;
\ No newline at end of file
diff --git a/src/components/TaskDetail.jsx b/src/components/TaskDetail.jsx
index 8316983..4d88186 100644
--- a/src/components/TaskDetail.jsx
+++ b/src/components/TaskDetail.jsx
@@ -4,6 +4,7 @@ import { cn } from '../lib/utils';
import TaskIndicator from './TaskIndicator';
import { api } from '../utils/api';
import { useTaskMaster } from '../contexts/TaskMasterContext';
+import { copyTextToClipboard } from '../utils/clipboard';
const TaskDetail = ({
task,
@@ -79,7 +80,7 @@ const TaskDetail = ({
};
const copyTaskId = () => {
- navigator.clipboard.writeText(task.id.toString());
+ copyTextToClipboard(task.id.toString());
};
const getStatusConfig = (status) => {
diff --git a/src/components/TaskList.jsx b/src/components/TaskList.jsx
index 2fae577..b91bef2 100644
--- a/src/components/TaskList.jsx
+++ b/src/components/TaskList.jsx
@@ -4,7 +4,7 @@ import { cn } from '../lib/utils';
import TaskCard from './TaskCard';
import CreateTaskModal from './CreateTaskModal';
import { useTaskMaster } from '../contexts/TaskMasterContext';
-import Shell from './Shell';
+import Shell from './shell/view/Shell';
import { api } from '../utils/api';
import { useTranslation } from 'react-i18next';
diff --git a/src/components/TaskMasterSetupWizard.jsx b/src/components/TaskMasterSetupWizard.jsx
index 813889b..6bcb173 100644
--- a/src/components/TaskMasterSetupWizard.jsx
+++ b/src/components/TaskMasterSetupWizard.jsx
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { X, ChevronRight, ChevronLeft, CheckCircle, AlertCircle, Settings, Server, FileText, Sparkles, ExternalLink, Copy } from 'lucide-react';
import { cn } from '../lib/utils';
import { api } from '../utils/api';
+import { copyTextToClipboard } from '../utils/clipboard';
const TaskMasterSetupWizard = ({
isOpen = true,
@@ -175,7 +176,7 @@ const TaskMasterSetupWizard = ({
}
}
}`;
- navigator.clipboard.writeText(mcpConfig);
+ copyTextToClipboard(mcpConfig);
};
const renderStepContent = () => {
diff --git a/src/components/chat/tools/components/OneLineDisplay.tsx b/src/components/chat/tools/components/OneLineDisplay.tsx
index 52fc0f1..a73bd22 100644
--- a/src/components/chat/tools/components/OneLineDisplay.tsx
+++ b/src/components/chat/tools/components/OneLineDisplay.tsx
@@ -1,9 +1,9 @@
import React, { useState } from 'react';
+import { copyTextToClipboard } from '../../../../utils/clipboard';
type ActionType = 'copy' | 'open-file' | 'jump-to-results' | 'none';
interface OneLineDisplayProps {
-
toolName: string;
icon?: string;
label?: string;
@@ -25,52 +25,6 @@ interface OneLineDisplayProps {
toolId?: string;
}
-// Fallback for environments where the async Clipboard API is unavailable or blocked.
-const copyWithLegacyExecCommand = (text: string): boolean => {
- if (typeof document === 'undefined' || !document.body) {
- return false;
- }
-
- const textarea = document.createElement('textarea');
- textarea.value = text;
- textarea.setAttribute('readonly', '');
- textarea.style.position = 'fixed';
- textarea.style.opacity = '0';
- textarea.style.left = '-9999px';
- document.body.appendChild(textarea);
- textarea.select();
- textarea.setSelectionRange(0, text.length);
-
- let copied = false;
- try {
- copied = document.execCommand('copy');
- } catch {
- copied = false;
- } finally {
- document.body.removeChild(textarea);
- }
-
- return copied;
-};
-
-const copyTextToClipboard = async (text: string): Promise => {
- if (
- typeof navigator !== 'undefined' &&
- typeof window !== 'undefined' &&
- window.isSecureContext &&
- navigator.clipboard?.writeText
- ) {
- try {
- await navigator.clipboard.writeText(text);
- return true;
- } catch {
- // Fall back below when writeText is rejected (permissions/insecure contexts/browser limits).
- }
- }
-
- return copyWithLegacyExecCommand(text);
-};
-
/**
* Unified one-line display for simple tool inputs and results
* Used by: Bash, Read, Grep/Glob (minimized), TodoRead, etc.
@@ -92,7 +46,6 @@ export const OneLineDisplay: React.FC = ({
border: 'border-gray-300 dark:border-gray-600',
icon: 'text-gray-500 dark:text-gray-400'
},
- resultId,
toolResult,
toolId
}) => {
diff --git a/src/components/chat/view/subcomponents/Markdown.tsx b/src/components/chat/view/subcomponents/Markdown.tsx
index 7a0e43c..bbb3db1 100644
--- a/src/components/chat/view/subcomponents/Markdown.tsx
+++ b/src/components/chat/view/subcomponents/Markdown.tsx
@@ -7,6 +7,7 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { useTranslation } from 'react-i18next';
import { normalizeInlineCodeFences } from '../../utils/chatFormatting';
+import { copyTextToClipboard } from '../../../../utils/clipboard';
type MarkdownProps = {
children: React.ReactNode;
@@ -43,43 +44,6 @@ const CodeBlock = ({ node, inline, className, children, ...props }: CodeBlockPro
const match = /language-(\w+)/.exec(className || '');
const language = match ? match[1] : 'text';
- const textToCopy = raw;
-
- const handleCopy = () => {
- const doSet = () => {
- setCopied(true);
- setTimeout(() => setCopied(false), 1500);
- };
- try {
- if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(textToCopy).then(doSet).catch(() => {
- const ta = document.createElement('textarea');
- ta.value = textToCopy;
- ta.style.position = 'fixed';
- ta.style.opacity = '0';
- document.body.appendChild(ta);
- ta.select();
- try {
- document.execCommand('copy');
- } catch {}
- document.body.removeChild(ta);
- doSet();
- });
- } else {
- const ta = document.createElement('textarea');
- ta.value = textToCopy;
- ta.style.position = 'fixed';
- ta.style.opacity = '0';
- document.body.appendChild(ta);
- ta.select();
- try {
- document.execCommand('copy');
- } catch {}
- document.body.removeChild(ta);
- doSet();
- }
- } catch {}
- };
return (
@@ -89,7 +53,7 @@ const CodeBlock = ({ node, inline, className, children, ...props }: CodeBlockPro