refactor: unify clipboard copy flow across desktop/web/vscode runtimes (#458)

* fix: prevent copying incomplete diagnostics report

* refactoring: clipboard writes to unified cross-runtime fallback helper
This commit is contained in:
Bohdan Triapitsyn
2026-02-20 14:50:26 +02:00
committed by GitHub
parent 881e8bdf4f
commit 47cecfc356
22 changed files with 232 additions and 209 deletions
+20 -14
View File
@@ -27,6 +27,7 @@ import {
RiFileCopyLine,
} from '@remixicon/react';
import { toast } from '@/components/ui';
import { copyTextToClipboard } from '@/lib/clipboard';
import {
DropdownMenu,
@@ -447,8 +448,13 @@ const FileRow: React.FC<FileRowProps> = ({
)}
<DropdownMenuItem onClick={(e) => {
e.stopPropagation();
void navigator.clipboard.writeText(node.path);
toast.success('Path copied');
void copyTextToClipboard(node.path).then((result) => {
if (result.ok) {
toast.success('Path copied');
return;
}
toast.error('Copy failed');
});
}}>
<RiFileCopyLine className="mr-2 h-4 w-4" /> Copy Path
</DropdownMenuItem>
@@ -2049,8 +2055,8 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
variant="ghost"
size="sm"
onClick={async () => {
try {
await navigator.clipboard.writeText(fileContent);
const result = await copyTextToClipboard(fileContent);
if (result.ok) {
setCopiedContent(true);
if (copiedContentTimeoutRef.current !== null) {
window.clearTimeout(copiedContentTimeoutRef.current);
@@ -2058,7 +2064,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
copiedContentTimeoutRef.current = window.setTimeout(() => {
setCopiedContent(false);
}, 1200);
} catch {
} else {
toast.error('Copy failed');
}
}}
@@ -2079,8 +2085,8 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
variant="ghost"
size="sm"
onClick={async () => {
try {
await navigator.clipboard.writeText(displaySelectedPath);
const result = await copyTextToClipboard(displaySelectedPath);
if (result.ok) {
setCopiedPath(true);
if (copiedPathTimeoutRef.current !== null) {
window.clearTimeout(copiedPathTimeoutRef.current);
@@ -2088,7 +2094,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
copiedPathTimeoutRef.current = window.setTimeout(() => {
setCopiedPath(false);
}, 1200);
} catch {
} else {
toast.error('Copy failed');
}
}}
@@ -2437,8 +2443,8 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
variant="ghost"
size="sm"
onClick={async () => {
try {
await navigator.clipboard.writeText(fileContent);
const result = await copyTextToClipboard(fileContent);
if (result.ok) {
setCopiedContent(true);
if (copiedContentTimeoutRef.current !== null) {
window.clearTimeout(copiedContentTimeoutRef.current);
@@ -2446,7 +2452,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
copiedContentTimeoutRef.current = window.setTimeout(() => {
setCopiedContent(false);
}, 1200);
} catch {
} else {
toast.error('Copy failed');
}
}}
@@ -2467,8 +2473,8 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
variant="ghost"
size="sm"
onClick={async () => {
try {
await navigator.clipboard.writeText(displaySelectedPath);
const result = await copyTextToClipboard(displaySelectedPath);
if (result.ok) {
setCopiedPath(true);
if (copiedPathTimeoutRef.current !== null) {
window.clearTimeout(copiedPathTimeoutRef.current);
@@ -2476,7 +2482,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
copiedPathTimeoutRef.current = window.setTimeout(() => {
setCopiedPath(false);
}, 1200);
} catch {
} else {
toast.error('Copy failed');
}
}}
+7 -7
View File
@@ -6,6 +6,7 @@ import { useFireworksCelebration } from '@/contexts/FireworksContext';
import type { GitIdentityProfile, CommitFileEntry } from '@/lib/api/types';
import { useGitIdentitiesStore } from '@/stores/useGitIdentitiesStore';
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
import { copyTextToClipboard } from '@/lib/clipboard';
import {
useGitStore,
useGitStatus,
@@ -473,14 +474,13 @@ export const GitView: React.FC<GitViewProps> = ({ mode = 'full' }) => {
const [stashDialogBranch, setStashDialogBranch] = React.useState('');
const handleCopyCommitHash = React.useCallback((hash: string) => {
navigator.clipboard
.writeText(hash)
.then(() => {
void copyTextToClipboard(hash).then((result) => {
if (result.ok) {
toast.success('Commit hash copied');
})
.catch(() => {
toast.error('Failed to copy');
});
return;
}
toast.error('Failed to copy');
});
}, []);
const handleToggleCommit = React.useCallback((hash: string) => {
@@ -21,6 +21,7 @@ import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { EditorView } from '@codemirror/view';
import { useInlineCommentDraftStore } from '@/stores/useInlineCommentDraftStore';
import { toast } from '@/components/ui';
import { copyTextToClipboard } from '@/lib/clipboard';
const normalize = (value: string): string => {
if (!value) return '';
@@ -428,8 +429,8 @@ export const PlanView: React.FC = () => {
variant="ghost"
size="sm"
onClick={async () => {
try {
await navigator.clipboard.writeText(content);
const result = await copyTextToClipboard(content);
if (result.ok) {
setCopiedContent(true);
if (copiedContentTimeoutRef.current !== null) {
window.clearTimeout(copiedContentTimeoutRef.current);
@@ -437,7 +438,7 @@ export const PlanView: React.FC = () => {
copiedContentTimeoutRef.current = window.setTimeout(() => {
setCopiedContent(false);
}, 1200);
} catch {
} else {
// ignored
}
}}
@@ -455,8 +456,8 @@ export const PlanView: React.FC = () => {
variant="ghost"
size="sm"
onClick={async () => {
try {
await navigator.clipboard.writeText(displayPath ?? resolvedPath);
const result = await copyTextToClipboard(displayPath ?? resolvedPath);
if (result.ok) {
setCopiedPath(true);
if (copiedTimeoutRef.current !== null) {
window.clearTimeout(copiedTimeoutRef.current);
@@ -464,7 +465,7 @@ export const PlanView: React.FC = () => {
copiedTimeoutRef.current = window.setTimeout(() => {
setCopiedPath(false);
}, 1200);
} catch {
} else {
// ignored
}
}}
@@ -8,6 +8,7 @@ import {
} from '@remixicon/react';
import { toast } from '@/components/ui';
import { Button } from '@/components/ui/button';
import { copyTextToClipboard } from '@/lib/clipboard';
import { cn } from '@/lib/utils';
import { ProviderLogo } from '@/components/ui/ProviderLogo';
import { useAgentGroupsStore, type AgentGroup, type AgentGroupSession } from '@/stores/useAgentGroupsStore';
@@ -88,14 +89,13 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
toast.error('No worktree path available');
return;
}
navigator.clipboard
.writeText(selectedSession.path)
.then(() => {
void copyTextToClipboard(selectedSession.path).then((result) => {
if (result.ok) {
toast.success('Worktree path copied');
})
.catch(() => {
toast.error('Failed to copy path');
});
return;
}
toast.error('Failed to copy path');
});
}, [selectedSession?.path]);
const handleRemoveSelectedWorktree = React.useCallback(async () => {