From 630916d87337636a9e717653e9c97de3dff1102e Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Thu, 27 Aug 2026 12:54:37 -0500 Subject: [PATCH 1/4] Design doc for clipboard image paste in Monaco source editor (bd-706b0ixu) Design exploration + decision log for pasting images from the clipboard into the hub-client source editor: clipboard format landscape, Monaco pass-through behavior (verified against monaco 0.55.1 sources), SVG security analysis, filename/concurrency scheme (content-hash names to sidestep index-map LWW races), and scoped v1 rules. Follow-ups filed: bd-yspyic32 (mixed payloads), bd-myoj9kp5 (SVG posture). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Bs2YW5vUbm1krBkQicB5nr --- .../plans/2026-08-27-paste-image-clipboard.md | 404 ++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 claude-notes/plans/2026-08-27-paste-image-clipboard.md diff --git a/claude-notes/plans/2026-08-27-paste-image-clipboard.md b/claude-notes/plans/2026-08-27-paste-image-clipboard.md new file mode 100644 index 000000000..3b69849c7 --- /dev/null +++ b/claude-notes/plans/2026-08-27-paste-image-clipboard.md @@ -0,0 +1,404 @@ +# Paste images from clipboard into the Monaco source editor + +**Braid strand:** bd-706b0ixu +**Created:** 2026-08-27 +**Status:** Approved — executing (design decisions resolved 2026-08-27) +**Follow-up strands:** bd-yspyic32 (mixed text+image payloads), bd-myoj9kp5 +(pipeline-wide SVG posture) + +## Decision log (2026-08-27, user-approved) + +1. **Precedence:** v1 uses the simple file-only rule (§2a, as refined + below): take over iff the payload's files are all accepted raster + images **and** `text/plain` is empty or is just the filename rider. + Payloads with meaningful text (Office, Excel/Sheets) paste as text; + offering their image rendition is bd-yspyic32. +2. **Filename:** `pasted-.` (8-char SHA-256 prefix). +3. **Destination:** P1 — same directory as the current document. +4. **SVG:** S1 confirmed — raster-only paste; SVG stays dialog-only. + Pipeline-wide SVG decision filed as bd-myoj9kp5. +5. **Multi-file separator:** spaces (indentation-sensitive contexts rule + out newlines; see §D4). +6. **Declined file-only pastes:** keep status quo (Monaco inserts the + filename as text); revisit after real-world usage. + +## Overview + +Add clipboard-paste support for images in the hub-client source editor +(Monaco), analogous to the existing drag-and-drop flow but with one key UX +difference: **no dialog**. On paste we automatically pick a filename, create +the binary file in the project, and insert a markdown image reference at the +cursor, so the image shows up in the rendered preview as soon as the paste is +processed. Because there is no user-chosen filename, the scheme must be safe +against another peer *concurrently* pasting an image into the same CRDT +session. + +This document covers: (1) what the existing code gives us, (2) a primer on +how clipboard images actually arrive in JavaScript, (3) the security +analysis (SVG in particular), and (4) the design options with a +recommendation. + +## 1. What already exists (survey of current code) + +The drag-and-drop feature (plan: `2026-01-10-monaco-image-drag-drop.md`, +generic uploader: `2026-04-21-generic-file-uploader.md`) built almost all the +machinery we need: + +- **`Editor.tsx`** (`hub-client/src/components/Editor.tsx:814-965`) + - `handleEditorDrop` attaches to the Monaco container's DOM node, + distinguishes internal sidebar drags (`application/x-hub-file`) from + external file drops, stashes the drop position in + `pendingDropPositionRef`, and routes external files to `NewAssetDialog`. + - `handleUploadAsset` runs `processFileForUpload` → `createBinaryFile`, + then inserts markdown at the pending position via + `editorRef.current.executeEdits(...)`. Monaco's `onChange` fires + synchronously from `executeEdits` and propagates to the CRDT via the + splice path — so text insertion needs no extra sync work. + - The editor sets `pasteAs: { enabled: false }` in Monaco options + (quarto-dev/kyoto#3) — this disables Monaco's paste-as *widget* for text + pastes; it does not conflict with a DOM-level paste listener. +- **`fileUpload/dropMarkdown.ts`** — `buildDropMarkdown('image', + currentFilePath, targetPath)` produces `![](href)` with the target + correctly relativized against the containing document (bd-jzqswvh0). +- **`fileUpload/resolveDefaultDestination.ts`** — destination defaults to + the current file's parent folder. +- **`services/resourceService.ts`** — `computeSHA256`, `getHashPrefix`, + `generateHashedFilename` ("diagram.png" → "diagram-a1b2c3d4.png"), + `sanitizeFilename`, `processFileForUpload` (reads bytes, hashes, resolves + MIME from `file.type` falling back to extension), and + `FILE_SIZE_LIMITS.MAX_FILE_SIZE` (10 MB). +- **`quarto-sync-client/src/client.ts:1278` — `createBinaryFile(path, + content, mimeType)`** — the CRDT write. Already conflict-aware, per-client: + - If `path` exists **with the same SHA-256** → returns + `{ deduplicated: true }` and creates nothing. + - If `path` exists **with different content** → renames to + `name-.ext` and creates a new doc. + - Then sets `doc.files[path] = docId` in the index document (an Automerge + map — concurrent writes to the *same key* resolve last-writer-wins). +- **Preview side** (`ts-packages/preview-renderer`): `assetWalker.ts` mints + blob URLs for project-relative image targets; `inlines/Image.tsx` renders + them as `` inside the preview iframe + (`sandbox="allow-scripts allow-same-origin"`). The asset manifest updates + reactively when files are added, so a newly created binary file becomes + visible in the preview as soon as the AST references it. + +**Notable pre-existing fact:** the drop path already accepts SVG — every +image check is `file.type.startsWith('image/')`, and `image/svg+xml` +matches. So paste does not *introduce* the SVG question; it inherits it (see +§3). + +## 2. Clipboard images in JavaScript: formats and APIs + +There are two browser APIs; only one is right for this feature. + +### 2a. The `paste` event (`ClipboardEvent.clipboardData`) — the right one + +Fires on the focused element when the user presses Cmd/Ctrl-V. Synchronous, +no permission prompt, and hands us a `DataTransfer` — the same interface the +drop handler already consumes (`items`, `files`, `types`, `getData`). In +Monaco, the event target is the hidden `