Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ jobs:
packages/ui/annotationDraftPersistence.test.tsx
packages/ui/codeAnnotationDraftPersistence.test.tsx
packages/ui/components/html-viewer/srcdoc.test.ts
packages/ui/utils/clipboard.test.ts
packages/ui/components/InlineMarkdown.resolveLinkedDoc.test.tsx
packages/ui/components/MarkdownDiff.frozen.test.tsx
packages/ui/components/MarkdownEditor.extensions.test.tsx
Expand Down
13 changes: 6 additions & 7 deletions packages/editor/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { getCallbackConfig, CallbackAction, executeCallback } from '@plannotator
import { useAgents } from '@plannotator/ui/hooks/useAgents';
import { useActiveSection } from '@plannotator/ui/hooks/useActiveSection';
import { storage } from '@plannotator/ui/utils/storage';
import { copyTextToClipboard } from '@plannotator/ui/utils/clipboard';
import { configStore, useConfigValue } from '@plannotator/ui/config';
import { CompletionOverlay } from '@plannotator/ui/components/CompletionOverlay';
import { useUpdateCheck } from '@plannotator/ui/hooks/useUpdateCheck';
Expand Down Expand Up @@ -3829,10 +3830,9 @@ const App: React.FC = () => {
// (utils/agentInstructions.ts) so it's easy to edit independently of UI code.
const handleCopyAgentInstructions = async () => {
const payload = buildPlanAgentInstructions(window.location.origin);
try {
await navigator.clipboard.writeText(payload);
if (await copyTextToClipboard(payload)) {
toast.success('Agent instructions copied');
} catch {
} else {
toast.error('Failed to copy');
}
};
Expand All @@ -3845,10 +3845,9 @@ const App: React.FC = () => {
toast.error('Failed to create share link');
return;
}
try {
await navigator.clipboard.writeText(url);
if (await copyTextToClipboard(url)) {
toast.success('Share link copied');
} catch {
} else {
toast.error('Failed to copy');
}
};
Expand Down Expand Up @@ -4726,7 +4725,7 @@ const App: React.FC = () => {
onClose={() => setIsPanelOpen(false)}
onQuickCopy={async () => {
const output = getCurrentFeedbackPayload();
await navigator.clipboard.writeText(wrapCopiedFeedback(output));
return copyTextToClipboard(wrapCopiedFeedback(output));
}}
onShare={canShareCurrentSession ? () => { setIsPanelOpen(false); setInitialExportTab('share'); setShowExport(true); } : undefined}
otherFileAnnotations={otherFileAnnotations}
Expand Down
26 changes: 12 additions & 14 deletions packages/review-editor/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ import { TextShimmer } from '@plannotator/ui/components/TextShimmer';
import type { PRMetadata } from '@plannotator/shared/pr-types';
import type { PRDiffScope, PRDiffScopeOption, PRStackInfo, PRStackTree } from '@plannotator/shared/pr-stack';
import { altKey } from '@plannotator/ui/utils/platform';
import { copyTextToClipboard } from '@plannotator/ui/utils/clipboard';
import { TourDialog } from './components/tour/TourDialog';
import { DEMO_TOUR_ID } from './demoTour';
import { GuideScreen } from './components/guide/GuideScreen';
Expand Down Expand Up @@ -620,10 +621,9 @@ const ReviewApp: React.FC = () => {
// module (utils/reviewAgentInstructions.ts) so it's easy to edit independently.
const handleCopyAgentInstructions = useCallback(async () => {
const payload = buildReviewAgentInstructions(window.location.origin);
try {
await navigator.clipboard.writeText(payload);
if (await copyTextToClipboard(payload)) {
toast.success('Agent instructions copied');
} catch {
} else {
toast.error('Failed to copy');
}
}, []);
Expand Down Expand Up @@ -2483,12 +2483,11 @@ const ReviewApp: React.FC = () => {
// Copy raw diff to clipboard
const handleCopyDiff = useCallback(async () => {
if (!diffData) return;
try {
await navigator.clipboard.writeText(diffData.rawPatch);
if (await copyTextToClipboard(diffData.rawPatch)) {
setCopyRawDiffStatus('success');
setTimeout(() => setCopyRawDiffStatus('idle'), 2000);
} catch (err) {
console.error('Failed to copy:', err);
} else {
console.error('Failed to copy');
setCopyRawDiffStatus('error');
setTimeout(() => setCopyRawDiffStatus('idle'), 2000);
}
Expand Down Expand Up @@ -2523,12 +2522,11 @@ const ReviewApp: React.FC = () => {
setShowNoAnnotationsDialog(true);
return;
}
try {
await navigator.clipboard.writeText(feedbackMarkdown);
if (await copyTextToClipboard(feedbackMarkdown)) {
setCopyFeedback('Feedback copied!');
setTimeout(() => setCopyFeedback(null), 2000);
} catch (err) {
console.error('Failed to copy:', err);
} else {
console.error('Failed to copy');
setCopyFeedback('Failed to copy');
setTimeout(() => setCopyFeedback(null), 2000);
}
Expand Down Expand Up @@ -3661,8 +3659,8 @@ const ReviewApp: React.FC = () => {
</div>
<div className="p-4 border-t border-border flex justify-end gap-2">
<button
onClick={async () => {
await navigator.clipboard.writeText(feedbackMarkdown);
onClick={() => {
void copyTextToClipboard(feedbackMarkdown);
}}
className="px-3 py-1.5 rounded-md text-xs font-medium bg-primary text-primary-foreground hover:opacity-90 transition-colors"
>
Expand Down Expand Up @@ -3708,7 +3706,7 @@ const ReviewApp: React.FC = () => {
<div>
<span className="text-[10px] uppercase tracking-wider text-muted-foreground/60 font-semibold">Path</span>
<button
onClick={() => navigator.clipboard.writeText((agentCwd || gitContext?.cwd)!)}
onClick={() => { void copyTextToClipboard((agentCwd || gitContext?.cwd)!); }}
className="mt-1 w-full text-left font-mono text-xs bg-muted/50 border border-border/50 rounded-md px-3 py-2 text-foreground hover:bg-muted transition-colors cursor-pointer break-all"
title="Click to copy"
>
Expand Down
6 changes: 2 additions & 4 deletions packages/review-editor/components/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type React from 'react';
import { useState } from 'react';
import { copyTextToClipboard } from '@plannotator/ui/utils/clipboard';

interface CopyButtonProps {
text: string;
Expand Down Expand Up @@ -29,12 +30,9 @@ export const CopyButton: React.FC<CopyButtonProps> = ({ text, className = '', va

const handleCopy = async (e: React.MouseEvent) => {
e.stopPropagation();
try {
await navigator.clipboard.writeText(text);
if (await copyTextToClipboard(text)) {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch {
// Clipboard API may not be available
}
};

Expand Down
7 changes: 4 additions & 3 deletions packages/review-editor/components/FileTreeNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { ContextMenu } from '@base-ui/react/context-menu';
import type { FileTreeNode as TreeNode } from '../utils/buildFileTree';
import { ViewedControl, ChangeTypeLetter, StageControl, AnnotationBadge, DiffCounts, CommittedDot } from './FileRowBits';
import { copyTextToClipboard } from '@plannotator/ui/utils/clipboard';

interface FileTreeNodeProps {
node: TreeNode;
Expand Down Expand Up @@ -186,20 +187,20 @@ export const FileTreeNodeItem: React.FC<FileTreeNodeProps> = ({
<ContextMenu.Positioner className="z-50">
<ContextMenu.Popup className="min-w-[160px] bg-popover text-popover-foreground border border-border rounded shadow-lg overflow-hidden py-1 transition-opacity data-starting-style:opacity-0 data-ending-style:opacity-0">
<ContextMenu.Item
onClick={() => navigator.clipboard.writeText(node.path)}
onClick={() => { void copyTextToClipboard(node.path); }}
className="flex items-center gap-2 mx-1 px-2 py-1.5 text-xs rounded cursor-pointer outline-none text-foreground/80 data-[highlighted]:bg-muted data-[highlighted]:text-foreground"
>
Copy path
</ContextMenu.Item>
<ContextMenu.Item
onClick={() => navigator.clipboard.writeText(node.name)}
onClick={() => { void copyTextToClipboard(node.name); }}
className="flex items-center gap-2 mx-1 px-2 py-1.5 text-xs rounded cursor-pointer outline-none text-foreground/80 data-[highlighted]:bg-muted data-[highlighted]:text-foreground"
>
Copy filename
</ContextMenu.Item>
{repoRoot && (
<ContextMenu.Item
onClick={() => navigator.clipboard.writeText(`${repoRoot.replace(/\/$/, '')}/${node.path}`)}
onClick={() => { void copyTextToClipboard(`${repoRoot.replace(/\/$/, '')}/${node.path}`); }}
className="flex items-center gap-2 mx-1 px-2 py-1.5 text-xs rounded cursor-pointer outline-none text-foreground/80 data-[highlighted]:bg-muted data-[highlighted]:text-foreground"
>
Copy full path
Expand Down
8 changes: 4 additions & 4 deletions packages/review-editor/components/ReviewSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { AIChatEntry } from '../hooks/useAIChat';
import type { AgentJobInfo, AgentCapabilities } from '@plannotator/ui/types';
import type { DiffFile } from '../types';
import type { AIProviderOption } from '@plannotator/ui/utils/aiProvider';
import { copyTextToClipboard } from '@plannotator/ui/utils/clipboard';
import { artifactAnchorLabel, artifactAnnotationQuote } from '../utils/artifactAnnotations';

export type ReviewSidebarTab = 'annotations' | 'ai' | 'agents';
Expand Down Expand Up @@ -181,12 +182,11 @@ export const ReviewSidebar: React.FC<ReviewSidebarProps> = /* React.memo */({

const handleQuickCopy = async () => {
if (!feedbackMarkdown) return;
try {
await navigator.clipboard.writeText(feedbackMarkdown);
if (await copyTextToClipboard(feedbackMarkdown)) {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (e) {
console.error('Failed to copy:', e);
} else {
console.error('Failed to copy');
}
};

Expand Down
8 changes: 6 additions & 2 deletions packages/ui/components/AnnotationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ interface PanelProps {
editorAnnotations?: EditorAnnotation[];
onDeleteEditorAnnotation?: (id: string) => void;
onClose?: () => void;
onQuickCopy?: () => Promise<void>;
/** Copy the full feedback payload. May resolve a success boolean; resolving
* `false` suppresses the "Copied" flash. A void resolution (existing hosts)
* is treated as success, preserving the original behavior. */
onQuickCopy?: () => Promise<void | boolean>;
onShare?: () => void;
otherFileAnnotations?: { count: number; files: number };
onOtherFileAnnotationsClick?: () => void;
Expand Down Expand Up @@ -249,7 +252,8 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
{onQuickCopy && (
<button
onClick={async () => {
await onQuickCopy();
const result = await onQuickCopy();
if (result === false) return;
setCopiedText(true);
setTimeout(() => setCopiedText(false), 2000);
}}
Expand Down
16 changes: 4 additions & 12 deletions packages/ui/components/AnnotationToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AnnotationType } from "../types";
import { createPortal } from "react-dom";
import { useDismissOnOutsideAndEscape } from "../hooks/useDismissOnOutsideAndEscape";
import { type QuickLabel, getQuickLabels } from "../utils/quickLabels";
import { copyTextToClipboard } from "../utils/clipboard";
import { FloatingQuickLabelPicker } from "./FloatingQuickLabelPicker";

type PositionMode = 'center-above' | 'top-right';
Expand Down Expand Up @@ -72,19 +73,10 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
const codeEl = element.querySelector('code');
textToCopy = codeEl?.textContent || element.textContent || '';
}
try {
await navigator.clipboard.writeText(textToCopy);
} catch {
const textarea = document.createElement('textarea');
textarea.value = textToCopy;
textarea.style.cssText = 'position:fixed;opacity:0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
textarea.remove();
if (await copyTextToClipboard(textToCopy)) {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};

// Update position on scroll/resize
Expand Down
8 changes: 4 additions & 4 deletions packages/ui/components/CodeFilePopout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useTheme } from './ThemeProvider';
import { CommentPopover } from './CommentPopover';
import { ImageThumbnail } from './ImageThumbnail';
import type { CodeAnnotation, ImageAttachment } from '../types';
import { copyTextToClipboard } from '../utils/clipboard';

export interface CodeFileAnnotationInput {
filePath: string;
Expand Down Expand Up @@ -457,12 +458,11 @@ export const CodeFilePopout: React.FC<CodeFilePopoutProps> = ({
}, [onAddAnnotation, openCommentForRange]);

const handleCopy = async () => {
try {
await navigator.clipboard.writeText(contents);
if (await copyTextToClipboard(contents)) {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch (err) {
console.error('Failed to copy:', err);
} else {
console.error('Failed to copy');
}
};

Expand Down
8 changes: 4 additions & 4 deletions packages/ui/components/ExportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getObsidianSettings, getEffectiveVaultPath } from '../utils/obsidian';
import { getBearSettings } from '../utils/bear';
import { getOctarineSettings } from '../utils/octarine';
import { wrapFeedbackForAgent } from '../utils/parser';
import { copyTextToClipboard } from '../utils/clipboard';
import { OverlayScrollArea } from './OverlayScrollArea';

/** POST body shape sent to the notes endpoint (mirrors what the Notes tab builds today). */
Expand Down Expand Up @@ -123,12 +124,11 @@ export const ExportModal: React.FC<ExportModalProps> = ({
const isOctarineReady = octarineSettings.enabled && octarineSettings.workspace.trim().length > 0;

const handleCopy = async (text: string, which: 'short' | 'full' | 'annotations') => {
try {
await navigator.clipboard.writeText(text);
if (await copyTextToClipboard(text)) {
setCopied(which);
setTimeout(() => setCopied(false), 2000);
} catch (e) {
console.error('Failed to copy:', e);
} else {
console.error('Failed to copy');
}
};

Expand Down
8 changes: 4 additions & 4 deletions packages/ui/components/MenuVersionSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { TextShimmer } from './TextShimmer';
import type { UpdateInfo } from '../hooks/useUpdateCheck';
import type { Origin } from '@plannotator/core/agents';
import { isWindows } from '../utils/platform';
import { copyTextToClipboard } from '../utils/clipboard';

const PI_INSTALL_COMMAND = 'pi install npm:@plannotator/pi-extension';

Expand Down Expand Up @@ -32,12 +33,11 @@ export const MenuVersionSection: React.FC<MenuVersionSectionProps> = ({
const hasUpdate = !!updateInfo?.updateAvailable;

const handleCopy = async () => {
try {
await navigator.clipboard.writeText(getInstallCommand(origin, isWSL));
if (await copyTextToClipboard(getInstallCommand(origin, isWSL))) {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (e) {
console.error('Failed to copy:', e);
} else {
console.error('Failed to copy');
}
};

Expand Down
7 changes: 2 additions & 5 deletions packages/ui/components/OpenInAppButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { ChevronDown, Check, Copy, MoreHorizontal } from 'lucide-react';
import { AppIcon } from './icons/AppIcon';
import { getLastOpenInApp, setLastOpenInApp } from '../utils/storage';
import { copyTextToClipboard } from '../utils/clipboard';
import type { OpenInKind } from '@plannotator/core/open-in-apps';
import {
DropdownMenu,
Expand Down Expand Up @@ -163,11 +164,7 @@ export const OpenInAppButton: React.FC<OpenInAppButtonProps> = ({
};

const copyText = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
} catch {
/* ignore */
}
await copyTextToClipboard(text);
setMenuOpen(false);
};

Expand Down
7 changes: 6 additions & 1 deletion packages/ui/components/PopoutDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import React, { useCallback } from 'react';
import { Dialog } from '@base-ui/react/dialog';

const ANNOTATION_SELECTORS = [
export const ANNOTATION_SELECTORS = [
'.annotation-toolbar',
'[data-comment-popover="true"]',
'[data-floating-picker="true"]',
// Transient hidden textarea created by the legacy clipboard fallback
// (packages/ui/utils/clipboard.ts). It briefly steals focus during copy;
// without this guard the focus-out close reason would dismiss the popout
// whenever a copy button inside it falls back to execCommand.
'[data-clipboard-fallback="true"]',
];

interface PopoutDialogProps {
Expand Down
8 changes: 4 additions & 4 deletions packages/ui/components/Viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import hljs from 'highlight.js';
import { AnnotationType, type Block, type Annotation, type EditorMode, type InputMethod, type ImageAttachment, type ActionsLabelMode } from '../types';
import { computeListIndices, groupBlocks, type Frontmatter } from '../utils/parser';
import { buildHeadingSlugMap } from '../utils/slugify';
import { copyTextToClipboard } from '../utils/clipboard';
import { BlockRenderer } from './BlockRenderer';
import { CodeBlock } from './blocks/CodeBlock';
import { TableBlock } from './blocks/TableBlock';
Expand Down Expand Up @@ -239,12 +240,11 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
const globalCommentButtonRef = useRef<HTMLButtonElement>(null);

const handleCopyPlan = async () => {
try {
await navigator.clipboard.writeText(markdown);
if (await copyTextToClipboard(markdown)) {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (e) {
console.error('Failed to copy:', e);
} else {
console.error('Failed to copy');
}
};
const containerRef = useRef<HTMLDivElement>(null);
Expand Down
Loading