Skip to content

Commit 296b592

Browse files
ochafikclaudegithub-actions[bot]
authored
pdf-server: annotations, interact tool, page extraction & prompt engineering (#506)
* pdf-server: add interact tool to allow model to modify existing view * pdf-server: add annotations, download, form fill, and highlight_text Add PDF annotation system with 7 annotation types (highlight, underline, strikethrough, note, rectangle, freetext, stamp), text-based highlighting, form filling, and annotated PDF download using pdf-lib. - Server: annotation Zod schemas, extended interact tool with add/update/remove annotations, highlight_text, and fill_form actions - Client: annotation layer rendering with PDF coordinate conversion, persistence via localStorage (using toolInfo.id key), pdf-lib-based download with embedded annotations and form fills, uses app.downloadFile() SDK with <a> fallback - Model context includes annotation summary Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add get_pages tool for batch text/screenshot extraction New tool `get_pages` lets the model get text and/or screenshots from arbitrary page ranges without navigating the visible viewer. - Server: `get_pages` tool with interval-based page ranges (optional start/end, open ranges supported), `getText`/`getScreenshots` flags, request-response bridge via `submit_page_data` app-only tool - Client: offscreen rendering (hidden canvas, no visual interference), text from cache or on-demand extraction, screenshots scaled to 768px max dimension, results submitted back to server - Max 20 pages per request, 60s timeout Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: move get_pages into interact tool as an action Fold get_pages into the interact tool to minimize tools requiring approval. Now accessed via `interact(action: "get_pages", ...)`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: improve interact tool annotation discoverability - Add concrete per-type schema docs with field names in tool description - Add JSON example showing add_annotations with highlight + stamp - Replace opaque z.record(z.string(), z.unknown()) with typed union of all annotation schemas (full + partial forms) so the model sees exact field names and types - Remove redundant manual safeParse since Zod inputSchema validates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: improve annotation discoverability + add E2E tests - display_pdf result text now explicitly lists annotation capabilities (highlights, stamps, notes, etc.) instead of vague "navigate, search, zoom, etc." - Restructured interact tool description: annotations promoted to top, with clear type reference, JSON example, and bold section headers - Added pdf-annotations.spec.ts with 6 E2E tests covering: - Result text mentions annotation capabilities - interact tool available in dropdown - add_annotations renders highlight - Multiple annotation types render (highlight, note, stamp, freetext, rectangle) - remove_annotations removes from DOM - highlight_text finds and highlights text Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add Claude API prompt discovery tests for annotations Tests that Claude can discover and use PDF annotation capabilities by calling the Anthropic Messages API with the tool schemas and simulated display_pdf result. Disabled by default — skipped unless ANTHROPIC_API_KEY is set: ANTHROPIC_API_KEY=sk-... npx playwright test tests/e2e/pdf-annotations-api.spec.ts 3 scenarios tested: - Model uses highlight_text when asked to highlight the title - Model discovers annotation capabilities when asked "can you annotate?" - Model uses interact (add_annotations or get_pages) when asked to add notes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add example prompts, testing docs, and updated tools table to README - Example prompts for annotations, navigation, page extraction, stamps, forms - Documents how to run E2E tests and API prompt discovery tests - Updated tools table to include interact tool - Updated key patterns table with annotations, command queue, file download - Added pdf-lib to dependencies list Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: simplify interact annotations schema (7802 → 2239 chars) The typed Zod union (14 anyOf variants: 7 full + 7 partial annotation types) produced a 5,817-char JSON schema for the annotations field alone. This bloated the interact tool schema to 7,802 chars, which may cause the model to struggle with or skip the tool. Replace with z.record(z.string(), z.any()) — annotation types are already fully documented in the tool description. Schema drops to 2,239 chars (71% reduction), annotations field to 254 chars (96% reduction). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: enumerate all interact actions in display_pdf result The display_pdf result text now lists every action by name (navigate, search, find, search_navigate, zoom, add_annotations, update_annotations, remove_annotations, highlight_text, fill_form, get_pages) so the model knows exactly what commands are available without needing to inspect the interact tool schema. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: validate viewUUID in interact, clarify in description The model was passing "pdf-viewer" instead of the actual UUID, causing get_pages to timeout (commands queued under wrong key, client never picks them up). - Add activeViewUUIDs set tracking UUIDs issued by display_pdf - Validate viewUUID at the top of interact handler with clear error - Add "IMPORTANT: viewUUID must be the exact UUID returned by display_pdf" to the interact tool description Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: fix annotation clickability and replace emoji icon with SVG - Raise annotation-layer z-index above text-layer so note annotations receive hover/click events (was z-index: 1, now 3; text-layer is 2) - Replace memo emoji (data-icon attr) with CSS mask SVG document icon that respects currentColor for consistent cross-platform rendering * pdf-server: add annotation side panel with bidirectional linking Right-side panel (250px) shows all annotations grouped by page with expand/collapse cards. Clicking a card navigates to the page and pulses the annotation; clicking a note icon on the PDF highlights its card in the panel. Panel auto-shows on first annotation, remembers user toggle preference via localStorage, and shows a badge count when collapsed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: replace 300ms interval polling with long-polling Server now holds poll_pdf_commands requests open (up to 30s) until commands arrive, waking waiters via enqueueCommand. Client loops sequentially instead of using setInterval, with 2s backoff on errors. Reduces idle RPC traffic from ~3 calls/sec to ~2 calls/min. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add interactive form filling via PDF.js AnnotationLayer Render PDF.js AnnotationLayer with renderForms:true so form fields appear as interactive HTML inputs. Build a fieldName→annotationID map via getFieldObjects() to bridge fill_form (which uses field names) with annotationStorage (which uses annotation IDs). Sync user input back to formFieldValues for persistence and PDF download. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add elicit_form_inputs to display_pdf for upfront form filling When enabled and the client supports elicitation, extracts form fields from the PDF via pdf-lib and prompts the user to fill them before the viewer loads. Elicited values are returned in content/structuredContent and enqueued as a fill_form command for the viewer. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: replace pdf-lib with pdfjs-dist for server-side form field extraction Use pdfjs-dist's getDocument() + getFieldObjects() instead of pdf-lib's PDFDocument.load() + getForm().getFields() in extractFormSchema(). This removes the pdf-lib import from the server bundle (pdf-lib is still used client-side for PDF modification in downloadAnnotatedPdf). Uses the legacy pdfjs-dist build to avoid DOMMatrix dependency in Node.js. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: fix form field font size and programmatic fill_form Set --scale-factor/--total-scale-factor CSS variables on the form layer so AnnotationLayer font-size rules resolve correctly instead of falling back to browser defaults. Also update live DOM elements directly in fill_form handler so values appear immediately without waiting for a full re-render. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: fix listbox font size overflow in AnnotationLayer After rendering the annotation layer, shrink select[size] font to fit within the PDF rect height. The default AnnotationLayer CSS uses a fixed 9px * scale-factor which overflows when many options share a small rect. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add trash icon to annotation sidebar cards Show a delete button on each annotation card that appears on hover. Clicking it removes the annotation from the PDF and persists the change. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: fix space key captured in form fields Skip keyboard navigation shortcuts (space, arrows, +/-) when any input, textarea, or select element is focused, not just the search/page inputs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: show form fields in annotation sidebar with clear buttons Form field values now appear in the sidebar under a "Form Fields" group, with trash icons to clear individual values. The badge count and auto-show logic include form fields. The panel open/close now calls requestFitToContent with width to avoid overflow in inline layout mode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: clarify Y coordinate system in interact tool description Explicitly state that Y=0 is the bottom edge and Y=792 is the top for US Letter, with concrete guidance on typical values for top/bottom placement to prevent models from using top-down screen coordinates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: validate fill_form field names and report valid fields Extract form field names during display_pdf and cache per viewer UUID. fill_form now soft-fails on unknown field names (applies valid ones, reports skipped ones). Both display_pdf and fill_form results include the list of valid field names so the model can self-correct. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add compact annotation strip for inline mode In inline mode, replace the 250px side panel with a slim bottom strip that shows one annotation/form-field at a time with prev/next navigation. The side panel is still used in fullscreen mode. - Strip shows item swatch, label, preview + counter (N of M · Page P) - Click item to navigate to its page; delete single or clear all - requestFitToContent includes strip height in size calculation - fieldNameToPage map built during init for form field page context - Display mode toggle switches between strip and panel automatically * pdf-server: improve display_pdf description for form/annotation use cases Add explicit guidance on when to use the tool: filling out forms (tax forms, applications), annotating PDFs, and interactive review. This helps models route user requests like 'help me fill out this form' to the display_pdf tool. * pdf-server: only show download button when host supports downloadFile * pdf-server: resizable sidebar + human-readable form field labels - Sidebar panel is now resizable by dragging its left edge (150px min, 50vw max). Width is persisted in localStorage. - Form fields show human-readable labels from PDF's TU (alternativeText) field when available. Mechanical names like ').F1_01[0]' are replaced with the generic label 'Field' when no TU text exists. - Empty values are no longer shown in the sidebar. * pdf-server: fix field label collection to use getAnnotations() API getFieldObjects() doesn't include alternativeText. Instead, iterate per-page annotations (only pages with known form fields) to collect the TU/alternativeText labels that PDF authors set as tooltips. * pdf-server: move strip below toolbar + follow form field focus - Strip is now positioned below the toolbar (above search bar) instead of at the bottom, for better visibility and proximity to the toolbar. - Form field focus changes automatically sync the strip to show the focused field's item. - Search bar top offset adjusts dynamically when strip is shown/hidden. * pdf-server: add Clear all to fullscreen sidebar + steer model to reuse views - Add 'Clear all' button in the fullscreen sidebar panel header - Update display_pdf description to instruct models to use the interact tool with existing viewUUID rather than calling display_pdf again for the same PDF (e.g. when user asks to annotate an already-open doc) * pdf-server: add page dimensions to model context + improve coordinate docs - Model context now includes 'Page size: 612x792pt (coordinates: origin at bottom-left, Y increases upward)' so the model knows actual page dimensions when placing annotations. - Interact tool description expanded with clearer coordinate guidance, margin hints, and a tip to use highlight_text for text annotations. - Download button temporarily shown unconditionally (capability check commented out with TODO for re-enabling). * pdf-server: auto-close panel when empty + stronger display_pdf reuse guidance - Sidebar/strip auto-closes when all items are cleared (via delete or Clear all), and the annotations toolbar button hides. - display_pdf description now uses CRITICAL warning to prevent models from re-calling it to add annotations to an already-open PDF. * pdf-server: skip elicitation when form fields have mechanical names Only use elicitation if all editable fields have human-readable names. Fields with brackets, dots, or ALL_CAPS_UNDERSCORE patterns (and no alternativeText label from the PDF) are considered mechanical and would be confusing in an elicitation form. Also use alternativeText as the elicitation field title when available. * pdf-server: paint annotations on screenshots + list coords in model context Screenshots sent to the model (both updateModelContext and get_pages) now include annotations drawn on top of the PDF canvas, so the model can visually verify placement. The text context also lists each annotation on the current page with its coordinates. * pdf-server: style multiselect/listbox form fields to match viewer theme * pdf-server: add command batching to interact tool Extract command processing into a helper function and accept an optional `commands` array for sequential batch execution. This lets the model send multiple actions (e.g. add annotations + get screenshot) in a single tool call, reducing round trips. Stops on first error in a batch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: fix multiselect rendering by removing font-size override The custom font-size: inherit on #form-layer select was conflicting with PDF.js AnnotationLayer's inline font-size, causing doubled/overlapping text in select options. Remove the override and keep only the checked-option highlight style. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: fix form reset button by passing fieldObjects to AnnotationLayer PDF.js AnnotationLayer needs the fieldObjects parameter to bind reset form button actions. Cache the result of doc.getFieldObjects() and pass it through to annotationLayer.render(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: improve annotation labels in sidebar and strip Use human-readable labels instead of raw type names (e.g. "Rectangle" instead of "rectangle", "Stamp: APPROVED" instead of "stamp"). Remove redundant preview text for types that have no additional content (rectangle, underline, strikethrough, stamp) to avoid "rectangle Rectangle" duplication. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: split get_pages into get_text and get_screenshot commands Replace the combined get_pages action (with getText/getScreenshots flags) with two focused commands: - get_text: extract text from pages (single page or intervals) - get_screenshot: capture a single page as PNG image This makes the API cleaner and returns each command's result as its own content block in batch mode. Also fix download function syntax error from earlier capability check commenting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: fix download by using pdfDocument.getData() instead of re-fetching The download was failing with "No PDF header found" because it re-fetched the PDF via read_pdf_bytes tool calls, but stateless MCP transport creates fresh server instances per request with empty caches. Using getData() from the already-loaded pdf.js document avoids the round-trip entirely. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: increase express body limit to fix get_screenshot hang The submit_page_data tool sends base64 PNG screenshots that exceed the default 100KB express.json() body limit from createMcpExpressApp. This caused the request to silently fail, making waitForPageData timeout and the get_screenshot command hang. Replaced createMcpExpressApp with manual express setup using a 50MB body limit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: use JPEG for screenshots to fix get_screenshot hang Switch from PNG to JPEG (quality 0.85) for both offscreen screenshots (get_screenshot) and model context images. JPEG is much smaller than PNG for document pages, keeping the submit_page_data payload well within the MCP SDK's default 100KB express body limit. This also reverts the temporary workaround of replacing createMcpExpressApp with a custom express setup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add ' - edited' suffix to download filename when modified Renames the download from '_annotated' to ' - edited' and also triggers the suffix when form fields have been filled, not just when annotations are present. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: include viewUUID in display_pdf structuredContent The viewUUID was only in _meta but not in structuredContent, so the model couldn't retrieve it from the tool result to pass to interact. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: move interact action list from display_pdf content to description The content response was bloated with static instructions about interact actions. Moved that to the tool description where it belongs, keeping only the dynamic PDF URL and viewUUID in the content. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: fix form reset setting dropdowns to first item PDF.js resetform handler deselects all options but doesn't re-add the hidden empty placeholder, so the browser falls back to showing the first real option. Work around this by listening for resetform on combo selects and re-inserting a hidden blank option when nothing is selected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: sort form fields in sidebar/strip by intrinsic PDF order Fields are now sorted by page number then top-to-bottom Y position on the page, matching the visual reading order of the PDF form rather than the arbitrary insertion order of formFieldValues. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add viewUUID to display_pdf outputSchema The viewUUID was added to structuredContent but not to the outputSchema, causing MCP validation error -32602. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: allow horizontal scrolling in inline mode When the PDF is zoomed or wider than the container, users can now scroll horizontally in inline mode instead of the content being clipped. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add viewUUID to model context, remove activeViewUUIDs validation Include viewUUID in the updateModelContext header so the model can reference it when calling interact. Remove the activeViewUUIDs set validation since it breaks after server restarts — command queue TTL pruning is sufficient cleanup. Also prune empty queues with no active pollers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: fix delete button alignment in sidebar annotation cards Annotations without preview text had the delete button hugging the type label instead of right-aligning. Add margin-left: auto so the button is consistently pushed to the right edge of the row. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add interactive annotations (select, drag, resize, rotate, undo/redo) Enable pointer-events on all annotation types so they can be clicked to select. Draggable types (rectangle, freetext, stamp, note) support smooth drag-to-move. Rectangles show corner resize handles, stamps show a rotate handle. Delete/Backspace removes the selected annotation, Ctrl+Z/Shift+Z provides unlimited undo/redo, and Escape deselects. Sidebar cards sync with on-canvas selection state. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: clicking sidebar card focuses form field input and scrolls into view Sidebar annotation cards now scroll the annotation into view, and form field cards navigate to the correct page and focus the input element. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: fix form widget double-text by making inputs opaque PDF.js AnnotationLayer renders interactive form inputs with a semi-transparent background, letting the canvas's static form appearance bleed through. Since the widget font (9px * scale, sans-serif) doesn't match the PDF's actual font metrics, this creates a "double text" misalignment at most zoom levels. Make text/choice widgets opaque so only the interactive layer's text is visible. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add selection/focus to model context, double-click to send messages, toggle resize/rotate - Model context now includes selected annotation ID and focused form field name so the model knows what the user is interacting with. - Double-clicking a sidebar annotation card sends a message prompting the model to change that annotation. - Double-clicking a sidebar form field card sends a message prompting the model to fill that field. - Double-clicking an annotation in the renderer toggles between resize and rotate handles (for rectangles and stamps). - Rectangles now support rotation via the rotate handle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: replace inline-mode strip with floating accordion sidebar Replace the compact horizontal annotation strip with a floating panel at top-right in inline mode. Uses accordion-style collapsible sections (per-page annotations + form fields) with at most one section open at a time. Refactors renderAnnotationPanel into reusable helpers (appendAccordionSection, createAnnotationCard, createFormFieldCard). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: remove dead annotation strip code The compact horizontal annotation strip is no longer used since inline mode now uses a floating accordion sidebar. Remove all strip-related HTML, CSS, JS (DOM refs, StripItem type, buildStripItems, renderStrip, navigateToStripItem, deleteStripItem, event listeners, stripHeight calculations). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: multi-select annotations, natural prompts, form reset sync - Convert selection from single ID to Set for multi-select (Shift+click) - Deselect annotations when focusing a form input field - Fix rotation to always use center origin (stamp was using left-bottom) - Simplify double-click sendMessage prompts (e.g. "update Notes: ") - Call updateModelContext before sendMessage to guarantee context is sent - Listen for PDF.js resetform events to clear formFieldValues, update sidebar badge/count, and hide icon when no items remain Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: draggable floating panel with magnetic corner anchoring - Reduce floating panel width (220→180px), add border-radius - Panel header is draggable; on drop, snaps to nearest corner - Persists corner preference to localStorage - Auto-docks to opposite side when overlapping selected annotations - Accordion chevron: ▶ when collapsed, ▼ when expanded - Slightly reduce section header size for compactness Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: allow all accordion sections to be collapsed Only auto-open the first section on initial render. Once the user has interacted with accordion headers, respect their choice to collapse all sections without forcing one back open. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: polish interactions and UI details - Remove auto-show annotation panel; wait for user to open it - Badge color: blue (accent) instead of red - Search bar: offset floating panel below it when anchored top-right - Text selection deselects annotations and clears field focus - Click on text layer / empty area clears field focus too - Zoom keeps selected annotation visible (scrollIntoView after render) - Fullscreen button: distinct exit icon + tooltip with Esc hint Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: fix text deselect, stamp dblclick, rotation direction, export - Text selection and text-layer mousedown now deselect annotations - Add user-select: none on draggable annotations (prevents stamp text selection on double-click, allowing resize/rotate toggle to work) - Fix rotation direction: remove CSS transform negation so clockwise drag = clockwise rotation on screen - Fix stamp export: rotate around center (matching CSS transformOrigin) and negate rotation for PDF coordinate convention - Add rotation support to rectangle PDF export - Remove auto-show annotation panel; badge uses blue accent color Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: allow any text as stamp label, not just predefined enum The StampLabel was unnecessarily restricted to 6 predefined values (APPROVED, DRAFT, etc). The PDF format supports arbitrary stamp text, so open it up to z.string() while keeping common labels as examples in the schema description. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: show resize + rotate handles simultaneously, dblclick sends message Remove the interactionMode toggle — instead show both corner resize handles and the rotate handle at the same time on selected annotations. Rectangle gets resize corners + rotate handle; stamp gets rotate handle only. Double-click on annotation elements now sends a message to modify the annotation (matching sidebar card behavior) instead of toggling modes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: SVG fullscreen icons, Y-coord docs, dirty flag - Replace Unicode fullscreen chars (⛶/⊠) with proper SVG expand/collapse icons - Add Cmd+Enter keyboard shortcut for fullscreen toggle - Improve coordinate system docs: emphasize rectangle x,y is BOTTOM-LEFT corner, add worked examples - Add isDirty state with * prefix on title for unsaved changes - Track dirty state through persistAnnotations, suppress during restore * pdf-server: diff-based annotation persistence, proper PDF annotation export, PDF annotation import Extract annotation logic into pdf-annotations.ts module with comprehensive tests. Replace content-stream drawing (drawRectangle/drawText) with proper PDF annotation dicts (/Type /Annot) that are editable in Acrobat/Preview. Persistence now stores only a diff (additions/removals) relative to the PDF's native annotations, keeping localStorage small. On load, annotations are imported from the PDF via pdf.js and merged with the user's diff. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: enhance page shadow for better depth perception Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: use floating panel in fullscreen, respect insets, direction-aware resize Always use the floating annotations panel (both inline and fullscreen) instead of the sidebar in fullscreen mode. Panel anchorage now respects safe area insets (especially bottom). Resize handle repositions to the correct edge based on which corner the panel is anchored to. Initial width reduced from 180px to 135px. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add save_pdf tool and save button for local files Add server-side save_pdf tool that writes annotated PDF bytes back to the original local file. Client shows a save button (floppy disk icon) next to the download button for file:// URLs. Ctrl/Cmd+S shortcut saves when dirty. Saving clears the dirty flag and localStorage diff. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add circle and line annotation types synced with PDF.js Add circle (ellipse) and line annotation support across all layers: server Zod schemas, pdf-annotations module (import/export), client renderers, canvas painting for screenshots, CSS, drag/resize handling, and tool description documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add searchability keywords to display_pdf tool description Include viewer, render, renderer, display, show, annotate, edit, and form keywords so the tool is easier to discover by models. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: switch model-facing annotation API to top-left origin coordinates Convert Y coordinates at the command boundary (add/update_annotations) so the model uses intuitive top-left origin while internal storage stays in PDF's native bottom-left origin. Also converts coordinates back to model space in context strings for consistency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: fix stamp rotation in PDF export via appearance stream matrix Stamp rotation was a no-op (empty if-block with a comment). Now computes a proper rotation matrix around the bounding box center and applies it to the appearance stream. Also adds verification tests for rectangle stroke/fill colors and stamp color/rotation in exported PDF dicts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: fix screenshots to include filled form field values Screenshots sent to the model were missing form field values because the canvas render didn't use ENABLE_STORAGE mode. Now both offscreen renders (model context and get_pages) pass annotationStorage to PDF.js so filled fields appear in screenshots. Also reuses renderPageOffscreen for model context screenshots to deduplicate rendering logic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: list form fields with page, bounding box, and label in display_pdf result The model needs field locations to fill forms accurately, especially for fields with mechanical names or no name at all. Extracts per-page bounding boxes in model coordinates (top-left origin) and includes labels where available from PDF alternativeText. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: include full field listing in fill_form result and update UI after fill fill_form results now include all form fields with names, types, pages, and bounding boxes so the model can self-correct on unknown field names. Also updates the annotation panel badge and visibility after programmatic fills. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add formFields to display_pdf structuredContent and output schema Programmatic consumers need structured access to form field metadata (names, types, pages, bounding boxes) without parsing text content. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add image annotation type with URL/base64 support, drag-drop, and PDF export Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: fix image annotation click/select, add Shift-resize for aspect ratio, add handle tooltips Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: fix image annotation to stretch to fill its bounding box Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: fix stamp PDF export to match on-screen appearance Use non-standard /Name to prevent Preview.app from substituting built-in stamp graphics, align padding with CSS (4pt/12pt), add ExtGState for 0.6 opacity, and fix text baseline positioning. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: improve annotation panel UX and image resize behavior - Page accordion click navigates to that page - Form fields appear in their page's accordion section (before annotations) - Selecting annotation or focusing form field auto-expands page accordion - Image resize preserves aspect ratio by default (Shift for free resize) - Add aspect field ("preserve"|"ignore") to ImageAnnotation type - Fix selection UI regression: remove overflow:hidden that clipped handles Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add signature/signing keywords and image example to tool descriptions - Add signing/signature keywords to display_pdf description for discoverability - Add image annotation example (signature placement) to interact tool description - Clarify that imageUrl supports file paths and HTTP URLs with auto-fetch Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: discourage base64 encoding in image annotation, clarify imageUrl accepts file paths Models tend to encode images as base64 themselves instead of passing file paths. Make the schema descriptions and tool docs explicit: prefer imageUrl with a file path, no data: URIs, don't encode images yourself. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: remove imageData from image annotation API, require imageUrl Models were encoding images as base64 themselves instead of passing file paths. Remove imageData from the model-facing Zod schema entirely — only imageUrl (file path or HTTPS URL) is accepted. The server fetches and embeds the image automatically. imageData remains as an internal field for client-side rendering and drag-drop. Also mention drag-and-drop as an alternative in the tool description. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add clipboard support (copy/cut/paste annotations and images) - Request clipboardWrite permission in resource metadata - Ctrl/Cmd+C: copy selected annotations as JSON to clipboard - Ctrl/Cmd+X: cut (copy + delete) selected annotations - Ctrl/Cmd+V / paste event: paste annotations from clipboard JSON, or paste images from clipboard (e.g. screenshots, copied images) - Refactor drag-drop image creation into shared addImageFromFile() - Pasted annotations get new IDs and slight offset to avoid overlap - Pasted images are centered on the current page Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: add disableInteract option for distributed deployments Adds a `disableInteract` boolean to `createServer()` options that skips registering the `interact`, `poll_pdf_commands`, and `submit_page_data` tools (all relying on an in-memory command queue). Adjusts `display_pdf` description and schema accordingly when disabled. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: flip interact to opt-in, enable only for --stdio Rename disableInteract → enableInteract (default false) so existing library consumers get read-only mode without code changes. The CLI entry point passes enableInteract: true only when running with --stdio. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * pdf-server: log client-roots warning once at startup, not per request * pdf-server: fix form widget transparency, poll spam, and pdf.js warnings - Form widgets: PDF.js writes inline background-color (from the annotation /BC entry, typically 'transparent'), defeating our CSS. Force opaque with !important. Fixes double-text in listboxes and text inputs at any zoom. - Poll spam: view was calling poll_pdf_commands in a tight loop when interact is disabled (HTTP mode) — server returns isError, which doesn't throw, so the loop never backed off. Server now signals interactEnabled via _meta; view only polls when true, and stops entirely on isError results. - Silence Helvetica / inset-border warnings: set verbosity=ERRORS for server-side getDocument (only used for form introspection, no rendering, so font fallback warnings are noise). * pdf-server: set standardFontDataUrl for server-side pdf.js Resolve pdfjs-dist/standard_fonts/ at runtime via createRequire. Works from both source (bun main.ts) and bundled dist/ since pdfjs-dist is --external. NodeStandardFontDataFactory reads via fs.readFile, so a filesystem path with trailing separator is what's expected. Keep verbosity=ERRORS as backstop for unrelated warnings (e.g. 'Unimplemented border style: inset'). * pdf-server: use unpkg CDN for pdf.js standard fonts, add to CSP - Single CDN URL pinned to the bundled pdfjs-dist version, used in both server (Node) and viewer (browser) - Node: default NodeStandardFontDataFactory uses fs.readFile which can't fetch URLs — pass a minimal FetchStandardFontDataFactory - Browser: DOMStandardFontDataFactory already fetches, just pass the URL - Add https://unpkg.com to CSP connectDomains (pdf.js loads font binaries via fetch(), maps to connect-src) * pdf-server: guard against missing totalBytes in viewer Without this, an invalid totalBytes flows to PDFDataRangeTransport.length → worker ChunkedStream sized at 0 → opaque 'Bad end offset: N' error. The guard turns that into an actionable message (rebuild hint). * pdf-server: re-probe file size at load time, ignore stale totalBytes from history The display_pdf result (including totalBytes) is baked into conversation history. If the user saves the annotated PDF (grows the file) then reloads the conversation, the host replays the stale size. pdf.js initializes its ChunkedStream at that stale length, then the first live read_pdf_bytes returns the current (larger) file — every chunk fails the 'end === bytes.length' check with 'Bad end offset: N'. Fix: drop the fileSizeBytes param and always probe via a live 1-byte read_pdf_bytes call before setting up the range transport. * pdf-server: file watching, save confirmation, disk-change reload Server: - fs.watch on local files when interact is enabled; pushes file_changed via the poll_pdf_commands channel. Re-attaches on rename to survive atomic writes (vim/vscode). Debounced 150ms. Skips unchanged mtime. - save_pdf returns {filePath, mtimeMs} so the saving viewer can recognise its own write's echo. Other viewers on the same file still get notified (their content is genuinely stale). - stopFileWatch in pruneStaleQueues TTL sweep. Viewer: - In-app confirm dialog (no window.confirm — would block the iframe). - Save button: hidden until first edit, enabled while dirty, disabled (but still visible) after save, re-enabled on next edit. - savePdf() asks for confirmation, records lastSavedMtime. - loadGeneration counter invalidates stale fetchChunk results and aborts the preloader when the PDF is reloaded underneath them. - reloadPdf() clears all per-document state (byte cache, annotations, form fields, undo/redo, text caches, field maps, localStorage diff) and loads fresh. - file_changed handler: suppresses own-save echo via saveInProgress + mtime match; auto-reloads when clean; prompts Keep/Discard when dirty. * pdf-server: add file watching tests Covers: external write → file_changed enqueued via poll_pdf_commands; debounced rapid writes; stopFileWatch prevents further events; save_pdf returns mtimeMs; watcher survives atomic rename (vim/vscode pattern). * pdf-server: fix confirm-btn-primary hover, show filename only in save prompt - .confirm-btn:hover's background cascaded to primary buttons too (same specificity, primary has both classes) — turning the blue button light grey on hover. Re-assert background in primary:hover. - Save confirm: extract just the filename from pdfUrl (strip file:// prefix, take basename). * pdf-server: match confirm dialog to host's native style Use host-provided CSS variables (--color-*, --font-*, --border-radius-*, --shadow-*) from applyHostStyleVariables, with local fallbacks. - Larger border-radius (--border-radius-xl), softer shadow (--shadow-lg) - Larger bold title (--font-heading-md-size, --font-weight-bold) - Primary button uses inverse colors (dark bg + light text) like the host's downloadFile dialog, not blue - New .confirm-detail box: monospace bordered rect for filename - Button order: Cancel first, primary last (native convention) - Escape resolves to first non-primary button * pdf-server: derive dirty from diff — undoing all changes clears save button persistAnnotations() was unconditionally setDirty(true). It already computes the diff vs baseline — use isDiffEmpty(diff) to decide instead. Now undoing all the way back to the original state marks the viewer clean again (save button disables, title loses asterisk). * pdf-server: import saved form field values from PDF as baseline getFieldObjects() returns the PDF's stored form values but we were only reading field IDs/pages from it. After saving a filled form and reopening, the panel showed nothing and there was no way to see what was filled. - New pdfBaselineFormValues map, populated in buildFieldNameMap() from each field's .value (skipping empty/Off/button values). Seeds formFieldValues so the panel shows PDF-stored values on open. - computeDiff takes an optional baselineFormFields param and only includes values that differ — opening a filled PDF doesn't mark dirty, editing a field does, reverting to the PDF's value marks clean again. - importFieldValue() normalises radio-group value lookup (parent entry has value=undefined, children have the real export value), checkbox→ true, listbox array→joined string. * pdf-server: fix form Reset button; split panel into Reset vs Clear all Reset-button regression: buildFieldNameMap() now seeds formFieldValues from the PDF's stored values, but syncFormValuesToStorage() was pushing them back into annotationStorage in our normalised repr (checkbox→true, radio→export string). pdf.js's native repr may differ, and overwriting it breaks the form's Reset button. Fix: skip syncing values that match baseline — the PDF's own values are already in storage natively. Panel buttons: - Reset: revert to what's in the PDF file. Restores baseline annotations and form values, clears undo/redo. Empty diff → clean. Disabled when not dirty. - Clear all: removes EVERYTHING including PDF-native items. Non-empty diff (baseline items are 'removed') → dirty; saving writes stripped PDF. Disabled when nothing to clear. * pdf-server: fix Clear all to push defaultValue; gate save on writability Clear all regression: annotationStorage.remove(id) only drops our override — the widget reverts to the PDF's stored /V (the baseline value). To actually clear, push each field's defaultValue (/DV) via setValue instead, which is what the PDF's native Reset button does. Save button visibility: server now checks fs.access(path, W_OK) and reports via _meta.writable. Viewer gates save on that instead of just 'is this a local path'. Hides the button for read-only mounts and Claude Desktop's uploads directory. Removed dead isLocalFileUrl(). * pdf-server: fix annotationStorage keys — use widget annotation IDs Root cause: getFieldObjects() returns field-dictionary refs (the /T tree, e.g. '86R'), but annotationStorage is keyed by WIDGET annotation refs (what page.getAnnotations() returns, e.g. '9R'). These differ when a PDF's field and its widget /Kids are separate objects. fieldNameToIds was built from getFieldObjects().id, so EVERY storage write was keyed wrong and silently no-op'd: - syncFormValuesToStorage: wrote to dead keys - fill_form: setValue + querySelector([data-element-id=...]) both missed - individual field delete: remove() on wrong key - clearAllItems: setValue to wrong keys Fix: build fieldNameToIds from page.getAnnotations() which gives the correct widget IDs. Keep cachedFieldObjects only for type/value/ defaultValue metadata (matched by name, not id) and for passing to AnnotationLayer.render() where pdf.js uses it internally. Also extract clearFieldInStorage() helper — pushes defaultValue for a single field, used by both individual delete and Clear all. * pdf-server: remap fieldObjects IDs to widget IDs for pdf.js Reset pdf.js _bindResetFormAction iterates fieldObjects using each entry's .id to (a) key annotationStorage and (b) querySelector([data-element-id=...]). Both expect WIDGET annotation IDs. fieldObjects contains field-dict IDs. Works only when field and widget share a PDF object — which pdf-lib's save breaks (it splits merged objects on write). This is a latent pdf.js bug, not a regression from our changes — it was broken the moment the user first saved via save_pdf. Fix: rebuild cachedFieldObjects with widget IDs (from fieldNameToIds) before passing to AnnotationLayer.render({fieldObjects}). Skip parent entries with no concrete id (radio group /T tree root). Preserve type/defaultValue/exportValues which Reset needs. * pdf-server: fix combo reset, scope writability, drop flaky CI assert Combo reset: pdf.js's resetform handler sets all option.selected = (option.value === defaultFieldValue); when defaultFieldValue is null nothing matches. Chrome then synchronously normalises the non-multiple <select> by auto-selecting option[0] — so by the time our fix-listener ran, selectedIndex was 0 and the state-check failed. Now: capture whether a real default exists before the event; if reset didn't land on it, force-prepend a hidden blank and select it explicitly. (Was latent — before the ID fix, resetform never dispatched on selects.) Writability scope: only mark writable if the file is explicitly in allowedLocalFiles (CLI arg = opt-in) OR strictly under an allowed root directory (isAncestorDir already excludes the root itself via rel !== ''), AND fs.access W_OK passes. A root passed by the client is a boundary, not a target. CI: drop the post-rename second-write assertion — fs.watch re-attach semantics differ between kqueue and inotify, inherently racy in CI. Only assert the rename itself is detected. * pdf-server: fix Reset bug; show cleared baseline fields w/ revert Reset-all bug: clearUserFormStorage skipped any field whose name was in pdfBaselineFormValues — but if the user had EDITED a baseline field, the edit sits in storage under that name. Skipping it left the widget showing the stale edit while the panel showed the restored baseline. Fix: remove ALL storage overrides on reset — every field reverts to the PDF's /V, which IS baseline. Removed the now-dead helper. Panel: cleared baseline fields now stay visible instead of vanishing. State is derived per-field by comparing formFieldValues to pdfBaselineFormValues (no new data structures — both maps already exist, the comparison was just never rendered): - unchanged: current === baseline → solid swatch, trash clears - modified: baseline exists, current differs → solid swatch, revert - cleared: baseline exists, current empty/absent → outlined cross swatch, struck-out label/value, revert restores - added: no baseline → solid swatch, trash removes Panel iterates union(formFieldValues, pdfBaselineFormValues) so cleared items don't disappear. sidebarItemCount uses the same union so the toolbar button stays visible as long as there are baseline items OR edits. Per-item revert: formFieldValues.set(name, baseline) + storage.remove(id) → widget reverts to /V. * pdf-server: import baseline from widget fieldValue, not field-dict page.getAnnotations().fieldValue is what AnnotationLayer actually renders. getFieldObjects().value reads the field-dict /V — which can be out of sync in an internally-inconsistent PDF (e.g. after a pdf-lib setText silently failed on a comb field, leaving the field-dict stale while the widget still shows the older value from the raw bytes). Your Form.pdf: ID widget shows 'eeeee', field-dict says 'eew2e'. Panel was importing 'eew2e' → mismatch with what's on screen. Fix: capture a.fieldValue during the page.getAnnotations scan (we're already iterating there for widget IDs) and prefer it over the field-dict value. Fall back to field-dict if the widget doesn't expose one. normaliseFieldValue() handles the format differences (choice widgets give arrays; field-dict gives strings). * pdf-server: fix radio input → 'on'; use export value for consistency pdf.js creates <input type=radio> without setting .value (pdf.mjs: 18138-18144), so target.value defaults to the HTML spec's 'on'. Our input listener read that → panel showed 'on' after clicking a radio, but baseline import correctly used the export value ('0') — so clicking then reverting showed two different strings for the same selection. Fix: store each radio widget's buttonValue during the annotation scan (we're already there for widget IDs), read it in the input listener via data-element-id. Now baseline, click, revert, and save all agree on the export value. (Form.pdf happens to use '0'/'1' as export values instead of 'Male'/'Female' — that's the form's design; the labels are just PDF text next to the widgets, not form data.) * pdf-server: panel Reset/Clear-all as icon buttons, keep tooltips * pdf-server: file roots from MCP client are read-only allowedLocalFiles conflated two sources: CLI args (user explicitly named the file when starting the server — overwriting is intentional) and MCP file roots (client-uploaded copies in ad-hoc hidden folders that the client doesn't expect to change). New cliLocalFiles set tracks only the CLI-sourced files. Writable now requires: file is in cliLocalFiles OR strictly under a directory root. Directory roots are mounted folders where saving is expected. save_pdf enforces the same scope server-side — the viewer hides the button based on _meta.writable, but we must not trust the client. * pdf-server: file root stays read-only even under a directory root Extracted isWritablePath() with the full policy in one place (was duplicated between display_pdf and save_pdf): - CLI file → writable (user explicitly named it) - MCP file root → read-only, ALWAYS. Even when the path happens to fall inside a mounted directory root — the client sending it as a file root is the stronger signal ('here's an upload') - under a directory root at any depth → writable (mounted folder) - the directory root itself → not writable (rel === '' excluded) - no roots and no CLI files → nothing writable 8 unit tests cover each case. * pdf-server: fix e2e (interact for HTTP test harness), skip rename test on Linux E2E: tests/e2e/pdf-annotations.spec.ts expects 'interact' in the tool dropdown and calls it for annotation tests. Since 3f2c209c interact is stdio-only; e2e runs HTTP basic-host → tool absent → all tests fail (broken since Mar 4). Add --enable-interact CLI flag for HTTP mode and pass it in 'npm run serve'. The in-memory command queue works fine for the e2e setup (one long-lived process); the stdio-only restriction is about stateless multi-instance deployments. Rename test: inotify watches inodes. When the directory entry is atomically swapped to a NEW inode, the watcher on the OLD inode gets no event. kqueue (macOS) watches paths and fires 'rename'. Gate with skipIf(platform !== 'darwin'). * pdf-server: list_pdfs walks directory roots for *.pdf files Recurse into allowedLocalDirs (depth ≤ 8, ≤ 500 files) and enumerate actual .pdf files as file:// URLs alongside the explicitly-registered ones. Skip dotfiles/dirs and node_modules. Dedup (a file may be both explicitly registered and under a walked directory). Report truncation. * pdf-server: fix stale e2e assertion after 2d47f14b Commit 2d47f14b moved the interact action list from display_pdf's content to the tool description, but the e2e test still asserted the result text contained 'add_annotations', 'highlight_text', etc. Now checks for viewUUID / interactEnabled / 'Displaying PDF' which are the actual result contents. * pdf-server: add unpkg to resourceDomains (font-src) for pdf.js FontFace pdf.js in the browser doesn't just fetch() the .ttf bytes — it also creates FontFace('name', 'url(https://unpkg.com/...)') which goes through CSP font-src, not connect-src. Nest's CSP blocked it ('font-src self assets.claude.ai'). pdf.js fell back to system fonts so nothing broke visually, but the CDN URL was doing nothing. (Separate finding re: the 'bricked UI' report — logs show fill_form processed cleanly at 16:27:18, then Nest's router navigated away 13s later. Poll id=17 returned to an already-unmounted iframe. Normal teardown, not a brick.) * pdf-server: validate imageUrl scheme before <img src> assignment Fixes CodeQL js/xss + js/client-side-unvalidated-url-redirection on line 1994 (renderImageAnnotation) and the canvas-paint fallback. The server resolves imageUrl to imageData before enqueueing, so def.imageUrl only survives to the client if the server-side fetch fails. safeImageSrc whitelists https/http/data/blob and returns undefined for javascript: et al. * pdf-server: fix get_pages timeout race; trim fill_form response get_pages round-trip (interact → enqueue → poll → render → submit): - GET_PAGES_TIMEOUT_MS 60s → 45s. The MCP SDK also defaults to 60s, so if the round-trip got close, the client timed out first and our error went nowhere. Server rejects first now. - waitForPageData wires extra.signal. If the client cancels interact (timeout/user abort), we clean up the pendingPageRequests entry immediately instead of leaking until our own timeout. Also simplified to a single settle callback instead of {resolve, reject, timer}. - handleGetPages .catch() → submit empty pages. Was fire-and-forget with no error path; any rejection before the inner try/catch meant no submit_page_data → server waited the full timeout. fill_form response: dropped the full field listing that was appended on every call. display_pdf already returns it; echoing it back after every successful fill is noise. Only list valid field names when the model got one wrong. * pdf-server: return URL.href from safeImageSrc so CodeQL sees sanitizer The previous attempt returned the raw input string after a scheme check, which CodeQL's taint tracker doesn't recognise as a sanitiser (it sees tainted in → tainted out). Now parse with the URL constructor and return the .href property — this is a canonical sanitiser pattern CodeQL models as a barrier for js/xss and unvalidated-url-redirection. Also folded the data: URI branch into safeImageSrc for a single call site responsible for producing the <img src> string. * feat(pdf-server): treat uploads-root dirs as read-only by default Dir roots whose basename is 'uploads' (e.g. Claude Desktop's attachment drop folder) are now treated as read-only unless --writeable-uploads-root is passed. This prevents the save button from appearing for attached PDFs that the client doesn't expect to be overwritten. Also gates the _debug diagnostic block on --debug and adds save/restore of writeFlags.allowUploadsRoot in test beforeEach/afterEach. * feat(pdf-server): add --debug flag, fix interact timeout, skip persist for read-only cmds - Wire --debug CLI flag through to createServer; _debug diagnostic block in display_pdf _meta is now only emitted when --debug is set. - Viewer: showDebugBubble renders _debug payload as a fixed overlay. - Fix interact get_pages timeout: await handleGetPages instead of fire-and-forget, so submit_page_data doesn't queue behind the next 30s long-poll on serialized host connections. - Skip persistAnnotations for read-only commands (get_pages, file_changed). * fix(pdf-server): rebase annotations baseline after save After a successful save_pdf, update pdfBaselineAnnotations and pdfBaselineFormValues to reflect what was written to disk. Without this, removing all annotations after a save produced an empty diff (compared to the stale pre-save baseline), incorrectly disabling the save button even though the file on disk still had annotations. * fix(pdf-server): sync annotation panel on canvas click, fix tooltip z-index Clicking an annotation on the canvas now re-renders the panel to expand the correct page section and scrolls the card into view. Previously only the selection class toggled without updating the accordion. Raise tooltip z-index to 100 within the annotation layer and promote the annotation layer above the form layer on note hover (:has selector) so tooltips aren't clipped by the form overlay. * debug(pdf-server): add __pdfDebug() helper + log skipped baseline annotations Adds window.__pdfDebug() that dumps annotationMap, baseline, layer children, and localStorage diff to diagnose ghost annotations (visible on canvas but not in panel, not selectable). Also logs when importPdfjsAnnotation returns null for non-widget annotations, and surfaces any thrown errors during baseline import instead of silently swallowing them. * debug(pdf-server): expose pdfDocument.annotationStorage + window.__pdf internals __pdfDebug() now also dumps PDF.js's annotationStorage contents (editor stamps live there, invisible to our tracking) and all localStorage keys matching pdf-annot pattern. After running __pdfDebug(), internals are exposed as window.__pdf.{pdfDocument, annotationMap, annotationLayerEl, formLayerEl} for interactive console poking. * fix(pdf-server): hide viewUUID when interact disabled, widen annotations panel - display_pdf result text no longer leaks viewUUID when the interact tool is not registered (model has no use for it). - Annotations panel default width: 250px -> 500px (max-width:50vw still caps on narrow viewports). * refactor(pdf-server): extract src/commands.ts, dedupe PdfCommand type PdfCommand was defined independently in server.ts and mcp-app.ts with no compiler check to keep them in sync. Extract to src/commands.ts as the single source of truth; both sides import the type. Uses the TS interfaces from pdf-annotations.ts (not Zod-inferred types) since those already describe the post-resolveImageAnnotation wire shape (imageData present, x/y/width/height non-optional). Also deletes ~150 lines of dead Zod schemas (PdfAnnotationDef and the 10 variant schemas that built it) -- they were never used as runtime validators, only for z.infer<typeof> type inference. Input remains z.record(z.any()) for model-API forgiveness. * fix(pdf-server): correctness cleanup bundle - findTextRectsFromCache: return [] instead of a hardcoded placeholder rect when text-layer DOM isn't rendered. The placeholder was persisted as real coordinates; caller already guards on empty. - onteardown: bump loadGeneration to stop the preloader (reuses the reload-abort check) and clear searchDebounceTimer. - startPreloading: re-check loadGeneration inside the pause-wait loop so teardown mid-pause exits instead of spinning. - app.sendMessage calls: wrap in .catch(log.error) so host-unsupported doesn't throw an unhandled rejection. - Remove dead ternary (Highlight:Highlight), stale TODO and commented downloadBtn line. * docs(pdf-server): fix action names in example prompts, document all flags - Example prompts referenced get_pages with getText/getScreenshots params; actual model-facing actions are get_text (intervals) and get_screenshot (single page). Fixed 3 prompts. - Document --debug, --enable-interact, --writeable-uploads-root flags. - Add image annotation type to the Annotation Types table. - Note that interact is disabled in HTTP mode unless --enable-interact. - Add Deployment section (stdio vs HTTP single-instance vs stateless). * test(pdf-server): add interact tool unit tests New describe("interact tool") block with 7 tests covering: - enqueue -> poll_pdf_commands roundtrip - missing-arg error paths for navigate, fill_form, add_annotations - command queue isolation across distinct viewUUIDs - fill_form passthrough when viewFieldNames not registered - (skip) unknown-UUID poll (LONG_POLL_TIMEOUT_MS not exported, can't bypass the 30s wait without changing server.ts) Surprises found: - interact never validates viewUUID exists; enqueues to any string - batch-mode early-exits on first error, silently dropping later commands * style: auto-fix prettier formatting * refactor(pdf-server): extract annotation panel to src/annotation-panel.ts Split ~930 lines from mcp-app.ts into two new modules: - src/viewer-state.ts (50 lines): shared Maps/Sets/Arrays + types (annotationMap, formFieldValues, selectedAnnotationIds, fieldNameTo*, undoStack/redoStack, TrackedAnnotation, EditEntry). Only containers with stable bindings — both modules mutate contents, never reassign. - src/annotation-panel.ts (1084 lines): floating panel positioning, accordion rendering, annotation/form-field cards, reset/clear-all. Coupling to mcp-app flows through PanelDeps injected at init — scalars like currentPage/pdfDocument reach the panel via a state() getter so the ~100 use sites in mcp-app stay unwrapped. mcp-app.ts: 5666 → 4721 lines. panelState.open replaces annotationPanelOpen; panelState.openAccordionSection is written by selectAnnotation. syncSidebarSelection moved to the panel module. * style: auto-fix prettier formatting * test(pdf-server): document computeDiff same-id modification behavior Two new tests: - User-added annotation modification: captured in diff.added (id not in baseline, so latest content persists correctly). - Baseline annotation in-place modification: KNOWN LIMITATION. Same-id edit produces empty diff (id-set based), so the change vanishes on reload. Viewer's addAnnotation() works around via remove+add, but updateAnnotation() (interact tool's update_annotations) does not. * chore(pdf-server): revert unrelated screenshot regenerations Pre-commit hook runs build:all which regenerates all example screenshots. These 24 PNGs have no semantic change; reverting to keep the PR diff focused on pdf-server. * fix(pdf-server): gate resolveImageAnnotation on validateUrl (arbitrary file read) resolveImageAnnotation() did fs.readFile on whatever imageUrl the model sent, bypassing the allowedLocalFiles/allowedLocalDirs machinery that protects display_pdf and save_pdf. Model could request {imageUrl:"/Users/x/.ssh/id_rsa"}, server would base64 the bytes into the add_annotations command, iframe stores it, get_screenshot reads it back. Also: fetch() branch accepted plain http:// (SSRF to local network). Fix: call validateUrl(imageUrl) before touching the filesystem or network. Throws on rejection; caller converts to {isError:true} so the model sees a clear error instead of silent skip. 3 tests: reject local path outside roots, reject http://, accept path under allowed dir (with real readFile + imageData population). * fix(pdf-server): deleted-baseline zombies, clear-all form strip, height-only coord shift Three bugs from PR review, all in viewer state/coord handling: 1. Deleted baseline annotations zombied back on reload (mcp-app.ts:2512). restoreAnnotations' apply loop was add-only. loadBaselineAnnotations runs between the two restore calls and re-seeds annotationMap with every baseline id, including the ones in diff.removed. The zombie survives, and the next persistAnnotations sees it in currentIds -> computeDiff produces removed=[] -> deletion permanently lost. Fix: delete diff.removed ids after the merge loop. 2. Clear All didn't strip form values from the saved PDF (mcp-app.ts:2756). clearAllItems() empties formFieldValues, but buildAnnotatedPdfBytes gates on formFields.size > 0 and only writes entries present in the map. Empty map -> zero setText/uncheck calls -> pdf-lib keeps the original /V values. Fix: at getAnnotatedPdfBytes time, inject an explicit clearing sentinel ("" or false by baseline type) for every baseline field absent from formFieldValues. 3. update_annotations shifted rect/circle/image when patching height without y (mcp-app.ts:4161). The old code spread existing.def (internal coords, y = bottom edge) with update (model coords, y = top-left), then key-filtered back to only update keys. But internal y = pageHeight - modelY - height; patching height with top fixed requires rewriting internal y, which the key-filter discarded. Fix: convert existing to model coords, merge in model space, convert back, pass the full merged def…
1 parent 83d82a1 commit 296b592

18 files changed

+11173
-126
lines changed

examples/pdf-server/README.md

Lines changed: 147 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,20 @@ bun examples/pdf-server/main.ts ./local.pdf https://arxiv.org/pdf/2401.00001.pdf
149149
bun examples/pdf-server/main.ts --stdio ./papers/
150150
```
151151

152+
### Additional Flags
153+
154+
- `--debug` — Enable verbose server-side logging.
155+
- `--enable-interact` — Enable the `interact` tool in HTTP mode (see [Deployment](#deployment)). Not needed for stdio.
156+
- `--writeable-uploads-root` — Allow saving annotated PDFs back to files under client roots named `uploads` (Claude Desktop mounts attachments there; writes are refused by default).
157+
158+
## Deployment
159+
160+
The `interact` tool relies on an in-memory command queue (server enqueues → viewer polls). This constrains how the server can be deployed:
161+
162+
- **stdio** (Claude Desktop) — `interact` is always enabled. The server runs as a single long-lived process, so the in-memory queue works.
163+
- **HTTP, single instance** — Pass `--enable-interact` to opt in. Works as long as all requests land on the same process.
164+
- **HTTP, stateless / multi-instance**`interact` will not work. Commands enqueued on one instance are invisible to viewers polling another. Leave the flag off; the tool will not be registered.
165+
152166
## Security: Client Roots
153167

154168
MCP clients may advertise **roots**`file://` URIs pointing to directories on the client's file system. The server uses these to allow access to local files under those directories.
@@ -174,34 +188,149 @@ When roots are ignored the server logs:
174188

175189
## Tools
176190

177-
| Tool | Visibility | Purpose |
178-
| ---------------- | ---------- | -------------------------------------- |
179-
| `list_pdfs` | Model | List available local files and origins |
180-
| `display_pdf` | Model + UI | Display interactive viewer |
181-
| `read_pdf_bytes` | App only | Stream PDF data in chunks |
191+
| Tool | Visibility | Purpose |
192+
| ---------------- | ---------- | ----------------------------------------------------- |
193+
| `list_pdfs` | Model | List available local files and origins |
194+
| `display_pdf` | Model + UI | Display interactive viewer |
195+
| `interact`¹ | Model | Navigate, annotate, search, extract pages, fill forms |
196+
| `read_pdf_bytes` | App only | Stream PDF data in chunks |
197+
| `save_pdf` | App only | Save annotated PDF back to local file |
198+
199+
¹ stdio only by default; in HTTP mode requires `--enable-interact` — see [Deployment](#deployment).
200+
201+
## Example Prompts
202+
203+
After the model calls `display_pdf`, it receives the `viewUUID` and a description of all capabilities. Here are example prompts and follow-ups that exercise annotation features:
204+
205+
### Annotating
206+
207+
> **User:** Show me the Attention Is All You Need paper
208+
>
209+
> _Model calls `display_pdf` → viewer opens_
210+
>
211+
> **User:** Highlight the title and add an APPROVED stamp on the first page.
212+
>
213+
> _Model calls `interact` with `highlight_text` for the title and `add_annotations` with a stamp_
214+
215+
> **User:** Can you annotate this PDF? Mark important sections for me.
216+
>
217+
> _Model calls `interact` with `get_text` to read content first, then `add_annotations` with highlights/notes_
218+
219+
> **User:** Add a note on page 1 saying "Key contribution" at position (200, 500), and highlight the abstract.
220+
>
221+
> _Model calls `interact` with `add_annotations` containing a `note` and either `highlight_text` or a `highlight` annotation_
222+
223+
### Navigation & Search
224+
225+
> **User:** Search for "self-attention" in the paper.
226+
>
227+
> _Model calls `interact` with action `search`, query `"self-attention"`_
228+
229+
> **User:** Go to page 5.
230+
>
231+
> _Model calls `interact` with action `navigate`, page `5`_
232+
233+
### Page Extraction
234+
235+
> **User:** Give me the text of pages 1–3.
236+
>
237+
> _Model calls `interact` with action `get_text`, intervals `[{start:1, end:3}]`_
238+
239+
> **User:** Take a screenshot of the first page.
240+
>
241+
> _Model calls `interact` with action `get_screenshot`, page `1`_
242+
243+
### Stamps & Form Filling
244+
245+
> **User:** Stamp this document as CONFIDENTIAL on every page.
246+
>
247+
> _Model calls `interact` with `add_annotations` containing `stamp` annotations on each page_
248+
249+
> **User:** Fill in the "Name" field with "Alice" and "Date" with "2026-02-26".
250+
>
251+
> _Model calls `interact` with action `fill_form`, fields `[{name:"Name", value:"Alice"}, {name:"Date", value:"2026-02-26"}]`_
252+
253+
## Testing
254+
255+
### E2E Tests (Playwright)
256+
257+
```bash
258+
# Run annotation E2E tests (renders annotations in a real browser)
259+
npx playwright test tests/e2e/pdf-annotations.spec.ts
260+
261+
# Run all PDF server tests
262+
npx playwright test -g "PDF Server"
263+
```
264+
265+
### API Prompt Discovery Tests
266+
267+
These tests verify that Claude can discover and use annotation capabilities by calling the Anthropic Messages API with the tool schemas. They are **disabled by default** — skipped unless `ANTHROPIC_API_KEY` is set:
268+
269+
```bash
270+
ANTHROPIC_API_KEY=sk-ant-... npx playwright test tests/e2e/pdf-annotations-api.spec.ts
271+
```
272+
273+
The API tests simulate a conversation where `display_pdf` has already been called, then send a follow-up user message and verify the model uses annotation actions (or at least the `interact` tool). Three scenarios are tested:
274+
275+
| Scenario | User prompt | Expected model behavior |
276+
| -------------------- | ----------------------------------------------------------------- | ------------------------------------------ |
277+
| Direct annotation | "Highlight the title and add an APPROVED stamp" | Uses `highlight_text` or `add_annotations` |
278+
| Capability discovery | "Can you annotate this PDF?" | Uses interact or mentions annotations |
279+
| Specific notes | "Add a note saying 'Key contribution' and highlight the abstract" | Uses `interact` tool |
182280

183281
## Architecture
184282

185283
```
186-
server.ts # MCP server + tools
187-
main.ts # CLI entry point
284+
server.ts # MCP server + tools
285+
main.ts # CLI entry point
188286
src/
189-
└── mcp-app.ts # Interactive viewer UI (PDF.js)
287+
├── mcp-app.ts # Interactive viewer UI (PDF.js)
288+
├── pdf-annotations.ts # Annotation types, diff model, PDF import/export
289+
└── pdf-annotations.test.ts # Unit tests for annotation module
190290
```
191291

192292
## Key Patterns Shown
193293

194-
| Pattern | Implementation |
195-
| ----------------- | ------------------------------------------- |
196-
| App-only tools | `_meta: { ui: { visibility: ["app"] } }` |
197-
| Chunked responses | `hasMore` + `offset` pagination |
198-
| Model context | `app.updateModelContext()` |
199-
| Display modes | `app.requestDisplayMode()` |
200-
| External links | `app.openLink()` |
201-
| View persistence | `viewUUID` + localStorage |
202-
| Theming | `applyDocumentTheme()` + CSS `light-dark()` |
294+
| Pattern | Implementation |
295+
| ----------------------------- | -------------------------------------------------------------- |
296+
| App-only tools | `_meta: { ui: { visibility: ["app"] } }` |
297+
| Chunked responses | `hasMore` + `offset` pagination |
298+
| Model context | `app.updateModelContext()` |
299+
| Display modes | `app.requestDisplayMode()` |
300+
| External links | `app.openLink()` |
301+
| View persistence | `viewUUID` + localStorage |
302+
| Theming | `applyDocumentTheme()` + CSS `light-dark()` |
303+
| Annotations | DOM overlays synced with proper PDF annotation dicts |
304+
| Annotation import | Load existing PDF annotations via PDF.js `getAnnotations()` |
305+
| Diff-based persistence | localStorage stores only additions/removals vs PDF baseline |
306+
| Proper PDF export | pdf-lib low-level API creates real `/Type /Annot` dictionaries |
307+
| Save to file | App-only `save_pdf` tool writes annotated bytes back to disk |
308+
| Dirty flag | `*` prefix on title when unsaved local changes exist |
309+
| Command queue | Server enqueues → client polls + processes |
310+
| File download | `app.downloadFile()` for annotated PDF |
311+
| Floating panel with anchoring | Magnetic corner-snapping panel for annotation list |
312+
| Drag, resize, rotate | Interactive annotation handles with undo/redo |
313+
| Keyboard shortcuts | Ctrl+Z/Y (undo/redo), Ctrl+S (save), Ctrl+F (search), ⌘Enter |
314+
315+
### Annotation Types
316+
317+
Supported annotation types (synced with PDF.js):
318+
319+
| Type | Properties | PDF Subtype |
320+
| --------------- | ------------------------------------------------------------------ | ------------ |
321+
| `highlight` | `rects`, `color?`, `content?` | `/Highlight` |
322+
| `underline` | `rects`, `color?` | `/Underline` |
323+
| `strikethrough` | `rects`, `color?` | `/StrikeOut` |
324+
| `note` | `x`, `y`, `content`, `color?` | `/Text` |
325+
| `rectangle` | `x`, `y`, `width`, `height`, `color?`, etc. | `/Square` |
326+
| `circle` | `x`, `y`, `width`, `height`, `color?`, etc. | `/Circle` |
327+
| `line` | `x1`, `y1`, `x2`, `y2`, `color?` | `/Line` |
328+
| `freetext` | `x`, `y`, `content`, `fontSize?`, `color?` | `/FreeText` |
329+
| `stamp` | `x`, `y`, `label`, `color?`, `rotation?` | `/Stamp` |
330+
| `image` | `x`, `y`, `width`, `height`, `imageData?`/`imageUrl?`, `rotation?` | `/Stamp` |
203331

204332
## Dependencies
205333

206-
- `pdfjs-dist`: PDF rendering (frontend only)
334+
- `pdfjs-dist`: PDF rendering and annotation import (frontend only)
335+
- `pdf-lib`: Client-side PDF modification — creates proper PDF annotation dictionaries for export
207336
- `@modelcontextprotocol/ext-apps`: MCP Apps SDK

examples/pdf-server/grid-cell.png

-4.54 KB
Loading

examples/pdf-server/main.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ import {
2020
pathToFileUrl,
2121
fileUrlToPath,
2222
allowedLocalFiles,
23+
cliLocalFiles,
2324
DEFAULT_PDF,
2425
allowedLocalDirs,
26+
writeFlags,
2527
} from "./server.js";
2628

2729
/**
@@ -93,17 +95,33 @@ function parseArgs(): {
9395
urls: string[];
9496
stdio: boolean;
9597
useClientRoots: boolean;
98+
enableInteract: boolean;
99+
debug: boolean;
96100
} {
97101
const args = process.argv.slice(2);
98102
const urls: string[] = [];
99103
let stdio = false;
100104
let useClientRoots = false;
105+
let enableInteract = false;
106+
let debug = false;
101107

102108
for (const arg of args) {
103109
if (arg === "--stdio") {
104110
stdio = true;
105111
} else if (arg === "--use-client-roots") {
106112
useClientRoots = true;
113+
} else if (arg === "--enable-interact") {
114+
// Force-enable interact for HTTP mode. Only use when running a
115+
// single long-lived server process (e.g. the e2e test harness) —
116+
// the command queue is in-memory per-process, so stateless
117+
// multi-instance deployments will drop commands.
118+
enableInteract = true;
119+
} else if (arg === "--debug") {
120+
debug = true;
121+
} else if (arg === "--writeable-uploads-root") {
122+
// Claude Desktop mounts attachments under a dir root named "uploads";
123+
// by default we refuse to write there. This flag opts back in.
124+
writeFlags.allowUploadsRoot = true;
107125
} else if (!arg.startsWith("-")) {
108126
// Convert local paths to file:// URLs, normalize arxiv URLs
109127
let url = arg;
@@ -124,11 +142,13 @@ function parseArgs(): {
124142
urls: urls.length > 0 ? urls : [DEFAULT_PDF],
125143
stdio,
126144
useClientRoots,
145+
enableInteract,
146+
debug,
127147
};
128148
}
129149

130150
async function main() {
131-
const { urls, stdio, useClientRoots } = parseArgs();
151+
const { urls, stdio, useClientRoots, enableInteract, debug } = parseArgs();
132152

133153
// Register local files in whitelist
134154
for (const url of urls) {
@@ -138,6 +158,7 @@ async function main() {
138158
const s = fs.statSync(filePath);
139159
if (s.isFile()) {
140160
allowedLocalFiles.add(filePath);
161+
cliLocalFiles.add(filePath);
141162
console.error(`[pdf-server] Registered local file: ${filePath}`);
142163
} else if (s.isDirectory()) {
143164
allowedLocalDirs.add(filePath);
@@ -153,10 +174,20 @@ async function main() {
153174

154175
if (stdio) {
155176
// stdio → client is local (e.g. Claude Desktop), roots are safe
156-
await startStdioServer(() => createServer({ useClientRoots: true }));
177+
await startStdioServer(() =>
178+
createServer({ enableInteract: true, useClientRoots: true, debug }),
179+
);
157180
} else {
158181
// HTTP → client is remote, only honour roots with explicit opt-in
159-
await startStreamableHTTPServer(() => createServer({ useClientRoots }));
182+
if (!useClientRoots) {
183+
console.error(
184+
"[pdf-server] Client roots are ignored (default for remote transports). " +
185+
"Pass --use-client-roots to allow the client to expose local directories.",
186+
);
187+
}
188+
await startStreamableHTTPServer(() =>
189+
createServer({ useClientRoots, enableInteract, debug }),
190+
);
160191
}
161192
}
162193

examples/pdf-server/mcp-app.html

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@
1919
<p id="error-message">An error occurred</p>
2020
</div>
2121

22+
<!-- Confirmation Dialog Overlay -->
23+
<div id="confirm-dialog" class="confirm-dialog" style="display: none">
24+
<div class="confirm-box">
25+
<div id="confirm-title" class="confirm-title"></div>
26+
<div id="confirm-body" class="confirm-body"></div>
27+
<div id="confirm-detail" class="confirm-detail"></div>
28+
<div id="confirm-buttons" class="confirm-buttons"></div>
29+
</div>
30+
</div>
31+
2232
<!-- PDF Viewer -->
2333
<div id="viewer" class="viewer" style="display: none">
2434
<!-- Toolbar -->
@@ -60,6 +70,31 @@
6070
<button id="zoom-in-btn" class="zoom-btn" title="Zoom in (+)">
6171
+
6272
</button>
73+
<button
74+
id="save-btn"
75+
class="save-btn"
76+
title="Save to file (overwrites original)"
77+
style="display: none"
78+
>
79+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M13 14H3a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h7l3 3v9a1 1 0 0 1-1 1z"/><path d="M11 14V9H5v5"/><path d="M5 2v3h5"/></svg>
80+
</button>
81+
<button
82+
id="download-btn"
83+
class="download-btn"
84+
title="Download PDF"
85+
style="display: none"
86+
>
87+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v8m0 0l-3-3m3 3l3-3"/><path d="M2 12v2h12v-2"/></svg>
88+
</button>
89+
<button
90+
id="annotations-btn"
91+
class="annotations-btn"
92+
title="Toggle annotations panel"
93+
style="display: none"
94+
>
95+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="1" width="14" height="14" rx="2"/><line x1="5" y1="5" x2="11" y2="5"/><line x1="5" y1="8" x2="11" y2="8"/><line x1="5" y1="11" x2="9" y2="11"/></svg>
96+
<span id="annotations-badge" class="annotations-badge" style="display: none"></span>
97+
</button>
6398
<button
6499
id="search-btn"
65100
class="search-btn"
@@ -68,9 +103,10 @@
68103
<button
69104
id="fullscreen-btn"
70105
class="fullscreen-btn"
71-
title="Toggle fullscreen"
106+
title="Toggle fullscreen (⌘Enter)"
72107
>
73-
108+
<svg class="expand-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>
109+
<svg class="collapse-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none"><path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/></svg>
74110
</button>
75111
</div>
76112
</div>
@@ -107,12 +143,31 @@
107143
</button>
108144
</div>
109145

110-
<!-- Single Page Canvas Container -->
111-
<div class="canvas-container">
112-
<div class="page-wrapper">
113-
<canvas id="pdf-canvas"></canvas>
114-
<div id="highlight-layer" class="highlight-layer"></div>
115-
<div id="text-layer" class="text-layer"></div>
146+
<!-- Canvas + Annotation Panel Row -->
147+
<div class="viewer-body">
148+
<!-- Single Page Canvas Container -->
149+
<div class="canvas-container">
150+
<div class="page-wrapper">
151+
<canvas id="pdf-canvas"></canvas>
152+
<div id="annotation-layer" class="annotation-layer"></div>
153+
<div id="highlight-layer" class="highlight-layer"></div>
154+
<div id="text-layer" class="text-layer"></div>
155+
<div id="form-layer" class="annotationLayer"></div>
156+
</div>
157+
</div>
158+
159+
<!-- Annotation Side Panel -->
160+
<div id="annotation-panel" class="annotation-panel" style="display: none">
161+
<div id="annotation-panel-resize" class="annotation-panel-resize"></div>
162+
<div class="annotation-panel-header">
163+
<span class="annotation-panel-title">Annotations (<span id="annotation-panel-count">0</span>)</span>
164+
<div class="annotation-panel-header-actions">
165+
<button id="annotation-panel-reset" class="annotation-panel-reset" title="Revert to what's in the PDF file"><svg width="14" height="14" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 6a4 4 0 1 1 1.2 2.85"/><path d="M2 9V6h3"/></svg></button>
166+
<button id="annotation-panel-clear-all" class="annotation-panel-clear-all" title="Remove everything, including items from the PDF file"><svg width="14" height="14" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M2 3h8M4.5 3V2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v1M5 5.5v3M7 5.5v3M3 3l.5 7a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1L9 3"/></svg></button>
167+
<button id="annotation-panel-close" class="annotation-panel-close" title="Close panel">&#x2715;</button>
168+
</div>
169+
</div>
170+
<div id="annotation-panel-list" class="annotation-panel-list"></div>
116171
</div>
117172
</div>
118173
</div>

0 commit comments

Comments
 (0)