Skip to content

Commit 190f7ec

Browse files
dgrissen2claude
andauthored
feat: clipboard paste for per-annotation images & fix absolute path resolution (#255)
Add capture-phase paste listener to AttachmentsButton so users can Cmd+V / Ctrl+V images directly into per-annotation attachments when the popover is open. Uses stopPropagation to prevent the global paste handler in App.tsx from also processing the event. Adds a platform-aware keyboard shortcut hint in the drop zone. Also fixes a regression from #237 where resolveMarkdownFile rejected absolute paths outside the project root. The original behavior allowed any absolute path the user explicitly provided; the security boundary should only apply to relative path / bare filename search. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2a46138 commit 190f7ec

2 files changed

Lines changed: 27 additions & 4 deletions

File tree

packages/server/resolve-file.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,10 @@ export async function resolveMarkdownFile(
4242
return { kind: "not_found", input };
4343
}
4444

45-
// 1. Absolute path — use as-is
45+
// 1. Absolute path — use as-is (no project root restriction;
46+
// the user explicitly typed the full path)
4647
if (input.startsWith("/")) {
4748
const normalized = resolve(input);
48-
if (!normalized.startsWith(projectRoot + "/") && normalized !== projectRoot) {
49-
return { kind: "not_found", input };
50-
}
5149
if (await Bun.file(normalized).exists()) {
5250
return { kind: "found", path: normalized };
5351
}

packages/ui/components/AttachmentsButton.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,28 @@ export const AttachmentsButton: React.FC<AttachmentsButtonProps> = ({
8787
return () => window.removeEventListener('keydown', handleKeyDown);
8888
}, [isOpen]);
8989

90+
// Paste image from clipboard when popover is open (per-annotation attachments).
91+
// Uses capture phase + stopPropagation to prevent the global paste handler in
92+
// App.tsx from also processing the same event.
93+
useEffect(() => {
94+
if (!isOpen || annotatorImage) return;
95+
const handlePaste = (e: ClipboardEvent) => {
96+
const items = e.clipboardData?.items;
97+
if (!items) return;
98+
for (const item of items) {
99+
if (item.type.startsWith('image/')) {
100+
e.preventDefault();
101+
e.stopPropagation();
102+
const file = item.getAsFile();
103+
if (file) handleFileSelect(file);
104+
return;
105+
}
106+
}
107+
};
108+
document.addEventListener('paste', handlePaste, true);
109+
return () => document.removeEventListener('paste', handlePaste, true);
110+
}, [isOpen, annotatorImage]);
111+
90112
const handleFileSelect = (file: File) => {
91113
// Derive name before opening annotator so user sees it immediately
92114
const initialName = deriveImageName(file.name, images.map(i => i.name));
@@ -295,6 +317,9 @@ export const AttachmentsButton: React.FC<AttachmentsButtonProps> = ({
295317
<span className="text-xs text-muted-foreground">
296318
Drop image or click to browse
297319
</span>
320+
<span className="text-[10px] text-muted-foreground/70">
321+
{navigator.platform?.includes('Mac') ? '⌘' : 'Ctrl'}+V to paste from clipboard
322+
</span>
298323
</>
299324
)}
300325
</div>

0 commit comments

Comments
 (0)