mirror of
https://github.com/coleam00/Archon.git
synced 2025-12-24 02:39:17 -05:00
feat: Universal clipboard utility with improved copy functionality (#663)
* feat: Add universal clipboard utility with enhanced copy functionality - Add comprehensive clipboard utility (src/utils/clipboard.ts) with: - Modern Clipboard API with automatic fallback to document.execCommand - Cross-browser compatibility and security context handling - Detailed error reporting and debugging capabilities - Support for secure (HTTPS) and insecure (HTTP/localhost) contexts - Update components to use new clipboard utility: - BugReportModal: Enhanced copy functionality with error handling - CodeViewerModal: Improved copy-to-clipboard for code snippets - IDEGlobalRules: Robust clipboard operations for rule copying - McpConfigSection: Enhanced config and command copying - DocumentCard: Reliable ID copying functionality - KnowledgeInspector: Improved content copying - ButtonPlayground: Enhanced CSS style copying - Benefits: - Consistent copy behavior across all browser environments - Better error handling and user feedback - Improved accessibility and security context support - Enhanced debugging capabilities Fixes #662 * fix: Improve clipboard utility robustness and add missing host configuration Clipboard utility improvements: - Prevent textarea element leak in clipboard fallback with proper cleanup - Add SSR compatibility with typeof guards for navigator/document - Use finally block to ensure cleanup in all error cases Host configuration fixes: - Update MCP API to use ARCHON_HOST environment variable instead of hardcoded localhost - Add ARCHON_HOST to docker-compose environment variables - Ensures MCP configuration shows correct hostname in different deployment environments Addresses CodeRabbit feedback and restores missing host functionality * fix: Use relative URLs for Vite proxy in development - Update getApiUrl() to return empty string when VITE_API_URL is unset - Ensures all API requests use relative paths (/api/...) in development - Prevents bypassing Vite proxy with absolute URLs (host:port) - Maintains existing functionality for explicit VITE_API_URL configuration - Fix TypeScript error by using bracket notation for environment access Addresses CodeRabbit feedback about dev setup relying on Vite proxy * fix: Resolve TypeScript error in API configuration Use proper type assertion to access VITE_API_URL environment variable * Address PR review comments: Move clipboard utility to features architecture - Move clipboard.ts from src/utils/ to src/features/shared/utils/ - Remove copyTextToClipboard backward compatibility function (dead code) - Update all import statements to use new file location - Maintain full clipboard functionality with modern API and fallbacks Addresses: - Review comment r2348420743: Move to new architecture location - Review comment r2348422625: Remove unused backward compatibility function 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix SSR safety issue in clipboard utility - Add typeof navigator !== 'undefined' guard before accessing navigator.clipboard - Add typeof document !== 'undefined' guard before using document.execCommand fallback - Ensure proper error handling when running in server-side environment - Maintain existing functionality while preventing ReferenceError during SSR/prerender Addresses CodeRabbit feedback: Navigator access needs SSR-safe guards 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
89fa9b4b49
commit
9ffca825ff
@@ -11,6 +11,7 @@ import {
|
|||||||
BugContext,
|
BugContext,
|
||||||
BugReportData,
|
BugReportData,
|
||||||
} from "../../services/bugReportService";
|
} from "../../services/bugReportService";
|
||||||
|
import { copyToClipboard } from "../../features/shared/utils/clipboard";
|
||||||
|
|
||||||
interface BugReportModalProps {
|
interface BugReportModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -99,13 +100,21 @@ export const BugReportModal: React.FC<BugReportModalProps> = ({
|
|||||||
// Fallback: copy to clipboard
|
// Fallback: copy to clipboard
|
||||||
const formattedReport =
|
const formattedReport =
|
||||||
bugReportService.formatReportForClipboard(bugReportData);
|
bugReportService.formatReportForClipboard(bugReportData);
|
||||||
await navigator.clipboard.writeText(formattedReport);
|
const clipboardResult = await copyToClipboard(formattedReport);
|
||||||
|
|
||||||
showToast(
|
if (clipboardResult.success) {
|
||||||
"Failed to create GitHub issue, but bug report was copied to clipboard. Please paste it in a new GitHub issue.",
|
showToast(
|
||||||
"warning",
|
"Failed to create GitHub issue, but bug report was copied to clipboard. Please paste it in a new GitHub issue.",
|
||||||
10000,
|
"warning",
|
||||||
);
|
10000,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showToast(
|
||||||
|
"Failed to create GitHub issue and could not copy to clipboard. Please report manually.",
|
||||||
|
"error",
|
||||||
|
10000,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Bug report submission failed:", error);
|
console.error("Bug report submission failed:", error);
|
||||||
@@ -118,15 +127,15 @@ export const BugReportModal: React.FC<BugReportModalProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyToClipboard = async () => {
|
const handleCopyToClipboard = async () => {
|
||||||
const bugReportData: BugReportData = { ...report, context };
|
const bugReportData: BugReportData = { ...report, context };
|
||||||
const formattedReport =
|
const formattedReport =
|
||||||
bugReportService.formatReportForClipboard(bugReportData);
|
bugReportService.formatReportForClipboard(bugReportData);
|
||||||
|
|
||||||
try {
|
const result = await copyToClipboard(formattedReport);
|
||||||
await navigator.clipboard.writeText(formattedReport);
|
if (result.success) {
|
||||||
showToast("Bug report copied to clipboard", "success");
|
showToast("Bug report copied to clipboard", "success");
|
||||||
} catch {
|
} else {
|
||||||
showToast("Failed to copy to clipboard", "error");
|
showToast("Failed to copy to clipboard", "error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -372,7 +381,7 @@ export const BugReportModal: React.FC<BugReportModalProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={copyToClipboard}
|
onClick={handleCopyToClipboard}
|
||||||
className="sm:order-1"
|
className="sm:order-1"
|
||||||
>
|
>
|
||||||
<Copy className="w-4 h-4 mr-2" />
|
<Copy className="w-4 h-4 mr-2" />
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import 'prismjs/components/prism-graphql'
|
|||||||
import 'prismjs/themes/prism-tomorrow.css'
|
import 'prismjs/themes/prism-tomorrow.css'
|
||||||
import { Button } from '../ui/Button'
|
import { Button } from '../ui/Button'
|
||||||
import { Badge } from '../ui/Badge'
|
import { Badge } from '../ui/Badge'
|
||||||
|
import { copyToClipboard } from '../../features/shared/utils/clipboard'
|
||||||
|
|
||||||
export interface CodeExample {
|
export interface CodeExample {
|
||||||
id: string
|
id: string
|
||||||
@@ -102,11 +103,15 @@ export const CodeViewerModal: React.FC<CodeViewerModalProps> = ({
|
|||||||
setActiveExampleIndex(0)
|
setActiveExampleIndex(0)
|
||||||
}, [searchQuery])
|
}, [searchQuery])
|
||||||
|
|
||||||
const handleCopyCode = () => {
|
const handleCopyCode = async () => {
|
||||||
if (activeExample) {
|
if (activeExample) {
|
||||||
navigator.clipboard.writeText(activeExample.code)
|
const result = await copyToClipboard(activeExample.code)
|
||||||
setCopied(true)
|
if (result.success) {
|
||||||
setTimeout(() => setCopied(false), 2000)
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
} else {
|
||||||
|
console.error('Failed to copy to clipboard:', result.error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Copy, Check, Link, Unlink } from 'lucide-react';
|
|||||||
import { NeonButton, type CornerRadius, type GlowIntensity, type ColorOption } from '../ui/NeonButton';
|
import { NeonButton, type CornerRadius, type GlowIntensity, type ColorOption } from '../ui/NeonButton';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { cn } from '../../lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
|
import { copyToClipboard } from '../../features/shared/utils/clipboard';
|
||||||
|
|
||||||
export const ButtonPlayground: React.FC = () => {
|
export const ButtonPlayground: React.FC = () => {
|
||||||
const [showLayer2, setShowLayer2] = useState(true);
|
const [showLayer2, setShowLayer2] = useState(true);
|
||||||
@@ -279,10 +280,14 @@ export const ButtonPlayground: React.FC = () => {
|
|||||||
return colors[color];
|
return colors[color];
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyToClipboard = () => {
|
const handleCopyToClipboard = async () => {
|
||||||
navigator.clipboard.writeText(generateCSS());
|
const result = await copyToClipboard(generateCSS());
|
||||||
setCopied(true);
|
if (result.success) {
|
||||||
setTimeout(() => setCopied(false), 2000);
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
} else {
|
||||||
|
console.error('Failed to copy to clipboard:', result.error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Corner input component
|
// Corner input component
|
||||||
@@ -654,7 +659,7 @@ export const ButtonPlayground: React.FC = () => {
|
|||||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">CSS Styles</h3>
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">CSS Styles</h3>
|
||||||
<button
|
<button
|
||||||
onClick={copyToClipboard}
|
onClick={handleCopyToClipboard}
|
||||||
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors flex items-center gap-2 shadow-lg shadow-purple-600/25"
|
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors flex items-center gap-2 shadow-lg shadow-purple-600/25"
|
||||||
>
|
>
|
||||||
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { FileCode, Copy, Check } from 'lucide-react';
|
|||||||
import { Card } from '../ui/Card';
|
import { Card } from '../ui/Card';
|
||||||
import { Button } from '../ui/Button';
|
import { Button } from '../ui/Button';
|
||||||
import { useToast } from '../../features/ui/hooks/useToast';
|
import { useToast } from '../../features/ui/hooks/useToast';
|
||||||
|
import { copyToClipboard } from '../../features/shared/utils/clipboard';
|
||||||
|
|
||||||
type RuleType = 'claude' | 'universal';
|
type RuleType = 'claude' | 'universal';
|
||||||
|
|
||||||
@@ -472,8 +473,9 @@ archon:manage_task(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyToClipboard = async () => {
|
const handleCopyToClipboard = async () => {
|
||||||
try {
|
const result = await copyToClipboard(currentRules);
|
||||||
await navigator.clipboard.writeText(currentRules);
|
|
||||||
|
if (result.success) {
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
showToast(`${selectedRuleType === 'claude' ? 'Claude Code' : 'Universal'} rules copied to clipboard!`, 'success');
|
showToast(`${selectedRuleType === 'claude' ? 'Claude Code' : 'Universal'} rules copied to clipboard!`, 'success');
|
||||||
|
|
||||||
@@ -481,8 +483,8 @@ archon:manage_task(
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setCopied(false);
|
setCopied(false);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch (err) {
|
} else {
|
||||||
console.error('Failed to copy text: ', err);
|
console.error('Failed to copy text:', result.error);
|
||||||
showToast('Failed to copy to clipboard', 'error');
|
showToast('Failed to copy to clipboard', 'error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,29 +5,17 @@
|
|||||||
* and handles different environments (development, Docker, production)
|
* and handles different environments (development, Docker, production)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Get the API URL from environment or construct it
|
// Get the API URL from environment or use relative URLs for proxy
|
||||||
export function getApiUrl(): string {
|
export function getApiUrl(): string {
|
||||||
// For relative URLs in production (goes through proxy)
|
// Check if VITE_API_URL is explicitly provided (for absolute URL mode)
|
||||||
if (import.meta.env.PROD) {
|
const viteApiUrl = (import.meta.env as any).VITE_API_URL as string | undefined;
|
||||||
return '';
|
if (viteApiUrl) {
|
||||||
|
return viteApiUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if VITE_API_URL is provided (set by docker-compose)
|
// Default to relative URLs to use Vite proxy in development
|
||||||
if (import.meta.env.VITE_API_URL) {
|
// or direct proxy in production - this ensures all requests go through proxy
|
||||||
return import.meta.env.VITE_API_URL;
|
return '';
|
||||||
}
|
|
||||||
|
|
||||||
// For development, construct from window location
|
|
||||||
const protocol = window.location.protocol;
|
|
||||||
const host = window.location.hostname;
|
|
||||||
// Use configured port or default to 8181
|
|
||||||
const port = import.meta.env.VITE_ARCHON_SERVER_PORT || '8181';
|
|
||||||
|
|
||||||
if (!import.meta.env.VITE_ARCHON_SERVER_PORT) {
|
|
||||||
console.info('[Archon] Using default ARCHON_SERVER_PORT: 8181');
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${protocol}//${host}:${port}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the base path for API endpoints
|
// Get the base path for API endpoints
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useInspectorPagination } from "../hooks/useInspectorPagination";
|
|||||||
import { ContentViewer } from "./ContentViewer";
|
import { ContentViewer } from "./ContentViewer";
|
||||||
import { InspectorHeader } from "./InspectorHeader";
|
import { InspectorHeader } from "./InspectorHeader";
|
||||||
import { InspectorSidebar } from "./InspectorSidebar";
|
import { InspectorSidebar } from "./InspectorSidebar";
|
||||||
|
import { copyToClipboard } from "../../../shared/utils/clipboard";
|
||||||
|
|
||||||
interface KnowledgeInspectorProps {
|
interface KnowledgeInspectorProps {
|
||||||
item: KnowledgeItem;
|
item: KnowledgeItem;
|
||||||
@@ -92,12 +93,12 @@ export const KnowledgeInspector: React.FC<KnowledgeInspectorProps> = ({
|
|||||||
}, [viewMode, currentItems, selectedItem]);
|
}, [viewMode, currentItems, selectedItem]);
|
||||||
|
|
||||||
const handleCopy = useCallback(async (text: string, id: string) => {
|
const handleCopy = useCallback(async (text: string, id: string) => {
|
||||||
try {
|
const result = await copyToClipboard(text);
|
||||||
await navigator.clipboard.writeText(text);
|
if (result.success) {
|
||||||
setCopiedId(id);
|
setCopiedId(id);
|
||||||
setTimeout(() => setCopiedId((v) => (v === id ? null : v)), 2000);
|
setTimeout(() => setCopiedId((v) => (v === id ? null : v)), 2000);
|
||||||
} catch (error) {
|
} else {
|
||||||
console.error("Failed to copy to clipboard:", error);
|
console.error("Failed to copy to clipboard:", result.error);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
|||||||
import { useToast } from "../../ui/hooks";
|
import { useToast } from "../../ui/hooks";
|
||||||
import { Button, cn, glassmorphism, Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/primitives";
|
import { Button, cn, glassmorphism, Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/primitives";
|
||||||
import type { McpServerConfig, McpServerStatus, SupportedIDE } from "../types";
|
import type { McpServerConfig, McpServerStatus, SupportedIDE } from "../types";
|
||||||
|
import { copyToClipboard } from "../../shared/utils/clipboard";
|
||||||
|
|
||||||
interface McpConfigSectionProps {
|
interface McpConfigSectionProps {
|
||||||
config?: McpServerConfig;
|
config?: McpServerConfig;
|
||||||
@@ -185,10 +186,16 @@ export const McpConfigSection: React.FC<McpConfigSectionProps> = ({ config, stat
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCopyConfig = () => {
|
const handleCopyConfig = async () => {
|
||||||
const configText = ideConfigurations[selectedIDE].configGenerator(config);
|
const configText = ideConfigurations[selectedIDE].configGenerator(config);
|
||||||
navigator.clipboard.writeText(configText);
|
const result = await copyToClipboard(configText);
|
||||||
showToast("Configuration copied to clipboard", "success");
|
|
||||||
|
if (result.success) {
|
||||||
|
showToast("Configuration copied to clipboard", "success");
|
||||||
|
} else {
|
||||||
|
console.error("Failed to copy config:", result.error);
|
||||||
|
showToast("Failed to copy configuration", "error");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCursorOneClick = () => {
|
const handleCursorOneClick = () => {
|
||||||
@@ -202,10 +209,16 @@ export const McpConfigSection: React.FC<McpConfigSectionProps> = ({ config, stat
|
|||||||
showToast("Opening Cursor with Archon MCP configuration...", "info");
|
showToast("Opening Cursor with Archon MCP configuration...", "info");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClaudeCodeCommand = () => {
|
const handleClaudeCodeCommand = async () => {
|
||||||
const command = `claude mcp add --transport http archon http://${config.host}:${config.port}/mcp`;
|
const command = `claude mcp add --transport http archon http://${config.host}:${config.port}/mcp`;
|
||||||
navigator.clipboard.writeText(command);
|
const result = await copyToClipboard(command);
|
||||||
showToast("Command copied to clipboard", "success");
|
|
||||||
|
if (result.success) {
|
||||||
|
showToast("Command copied to clipboard", "success");
|
||||||
|
} else {
|
||||||
|
console.error("Failed to copy command:", result.error);
|
||||||
|
showToast("Failed to copy command", "error");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedConfig = ideConfigurations[selectedIDE];
|
const selectedConfig = ideConfigurations[selectedIDE];
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type React from "react";
|
|||||||
import { memo, useCallback, useState } from "react";
|
import { memo, useCallback, useState } from "react";
|
||||||
import { Button } from "../../../ui/primitives";
|
import { Button } from "../../../ui/primitives";
|
||||||
import type { DocumentCardProps, DocumentType } from "../types";
|
import type { DocumentCardProps, DocumentType } from "../types";
|
||||||
|
import { copyToClipboard } from "../../../shared/utils/clipboard";
|
||||||
|
|
||||||
const getDocumentIcon = (type?: DocumentType) => {
|
const getDocumentIcon = (type?: DocumentType) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -67,11 +68,13 @@ export const DocumentCard = memo(({ document, isActive, onSelect, onDelete }: Do
|
|||||||
const [isCopied, setIsCopied] = useState(false);
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
|
||||||
const handleCopyId = useCallback(
|
const handleCopyId = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
async (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
navigator.clipboard.writeText(document.id);
|
const result = await copyToClipboard(document.id);
|
||||||
setIsCopied(true);
|
if (result.success) {
|
||||||
setTimeout(() => setIsCopied(false), 2000);
|
setIsCopied(true);
|
||||||
|
setTimeout(() => setIsCopied(false), 2000);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[document.id],
|
[document.id],
|
||||||
);
|
);
|
||||||
|
|||||||
139
archon-ui-main/src/features/shared/utils/clipboard.ts
Normal file
139
archon-ui-main/src/features/shared/utils/clipboard.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* Universal clipboard utility with modern API and fallback support
|
||||||
|
* Handles various security contexts and browser compatibility issues
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ClipboardResult {
|
||||||
|
success: boolean;
|
||||||
|
method: 'clipboard-api' | 'execCommand' | 'failed';
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy text to clipboard with automatic fallback mechanisms
|
||||||
|
* @param text - Text to copy to clipboard
|
||||||
|
* @returns Promise<ClipboardResult> - Result of the copy operation
|
||||||
|
*/
|
||||||
|
export const copyToClipboard = async (text: string): Promise<ClipboardResult> => {
|
||||||
|
// Try modern clipboard API first with SSR-safe guards
|
||||||
|
if (
|
||||||
|
typeof navigator !== 'undefined' &&
|
||||||
|
navigator.clipboard &&
|
||||||
|
navigator.clipboard.writeText
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return { success: true, method: 'clipboard-api' };
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Clipboard API failed, trying fallback:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to document.execCommand for older browsers or insecure contexts
|
||||||
|
// Add SSR guards for document access
|
||||||
|
if (typeof document === 'undefined') {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
method: 'failed',
|
||||||
|
error: 'Running in server-side environment - clipboard not available'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let textarea: HTMLTextAreaElement | null = null;
|
||||||
|
try {
|
||||||
|
// Ensure document.body exists before proceeding
|
||||||
|
if (!document.body) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
method: 'failed',
|
||||||
|
error: 'document.body is not available'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea = document.createElement('textarea');
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
textarea.style.top = '-9999px';
|
||||||
|
textarea.style.left = '-9999px';
|
||||||
|
textarea.style.opacity = '0';
|
||||||
|
textarea.style.pointerEvents = 'none';
|
||||||
|
textarea.setAttribute('readonly', '');
|
||||||
|
textarea.setAttribute('aria-hidden', 'true');
|
||||||
|
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
textarea.setSelectionRange(0, text.length);
|
||||||
|
|
||||||
|
const success = document.execCommand('copy');
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return { success: true, method: 'execCommand' };
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
method: 'failed',
|
||||||
|
error: 'execCommand copy returned false'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
method: 'failed',
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
// Always clean up the textarea element if it was created and added to DOM
|
||||||
|
if (textarea && document.body && document.body.contains(textarea)) {
|
||||||
|
try {
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
} catch (cleanupError) {
|
||||||
|
// Ignore cleanup errors - element may have already been removed
|
||||||
|
console.warn('Failed to cleanup textarea element:', cleanupError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if clipboard functionality is supported in current context
|
||||||
|
* @returns boolean - True if any clipboard method is available
|
||||||
|
*/
|
||||||
|
export const isClipboardSupported = (): boolean => {
|
||||||
|
// Check modern clipboard API with proper SSR guards
|
||||||
|
if (
|
||||||
|
typeof navigator !== 'undefined' &&
|
||||||
|
typeof navigator.clipboard !== 'undefined' &&
|
||||||
|
typeof navigator.clipboard.writeText === 'function'
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check execCommand fallback with SSR guards
|
||||||
|
if (
|
||||||
|
typeof document !== 'undefined' &&
|
||||||
|
typeof document.queryCommandSupported === 'function'
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return document.queryCommandSupported('copy');
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return false if running in SSR or globals are unavailable
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current security context information for debugging
|
||||||
|
* @returns string - Description of current security context
|
||||||
|
*/
|
||||||
|
export const getSecurityContext = (): string => {
|
||||||
|
if (typeof window === 'undefined') return 'server';
|
||||||
|
if (window.isSecureContext) return 'secure';
|
||||||
|
if (window.location.protocol === 'https:') return 'https';
|
||||||
|
if (window.location.hostname === 'localhost' ||
|
||||||
|
window.location.hostname === '127.0.0.1') return 'localhost';
|
||||||
|
return 'insecure';
|
||||||
|
};
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ services:
|
|||||||
- ARCHON_MCP_PORT=${ARCHON_MCP_PORT:-8051}
|
- ARCHON_MCP_PORT=${ARCHON_MCP_PORT:-8051}
|
||||||
- ARCHON_AGENTS_PORT=${ARCHON_AGENTS_PORT:-8052}
|
- ARCHON_AGENTS_PORT=${ARCHON_AGENTS_PORT:-8052}
|
||||||
- AGENTS_ENABLED=${AGENTS_ENABLED:-false}
|
- AGENTS_ENABLED=${AGENTS_ENABLED:-false}
|
||||||
|
- ARCHON_HOST=${HOST:-localhost}
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ async def get_mcp_config():
|
|||||||
|
|
||||||
# Configuration for streamable-http mode with actual port
|
# Configuration for streamable-http mode with actual port
|
||||||
config = {
|
config = {
|
||||||
"host": "localhost",
|
"host": os.getenv("ARCHON_HOST", "localhost"),
|
||||||
"port": mcp_port,
|
"port": mcp_port,
|
||||||
"transport": "streamable-http",
|
"transport": "streamable-http",
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user