@@ -761,6 +761,46 @@ function updateTitleDisplay(): void {
761761 titleEl . title = pdfUrl ;
762762}
763763
764+ /**
765+ * Debug overlay: fixed-position bubble, bottom-left. Pretty-printed JSON
766+ * dump of whatever the server stuffed into `_meta._debug`. Tooltips inside
767+ * sandboxed iframes are unreliable; this survives the cross-origin barrier
768+ * and shows up in screenshots.
769+ */
770+ function showDebugBubble ( debug : unknown ) : void {
771+ const bubble = document . createElement ( "div" ) ;
772+ const base =
773+ "position:fixed;bottom:8px;left:8px;z-index:99999;" +
774+ "background:rgba(20,20,30,0.92);color:#cfe;padding:8px 12px;" +
775+ "font:11px/1.4 monospace;border-radius:6px;" +
776+ "box-shadow:0 2px 8px rgba(0,0,0,0.4);white-space:pre;cursor:pointer;" +
777+ "transition:max-width 0.15s ease;" ;
778+ // Collapsed: clip to 60vw. Hover: expand to fit full paths (up to ~96vw),
779+ // scrollable both axes in case the JSON is tall.
780+ const collapsed =
781+ base +
782+ "max-width:60vw;max-height:40vh;overflow:hidden;text-overflow:ellipsis;" ;
783+ const expanded =
784+ base + "max-width:calc(100vw - 32px);max-height:80vh;overflow:auto;" ;
785+ bubble . style . cssText = collapsed ;
786+ // Latch expanded on click so hover-collapse doesn't fight text selection.
787+ let pinned = false ;
788+ bubble . onmouseenter = ( ) => {
789+ bubble . style . cssText = expanded ;
790+ } ;
791+ bubble . onmouseleave = ( ) => {
792+ if ( ! pinned ) bubble . style . cssText = collapsed ;
793+ } ;
794+ bubble . onclick = ( ) => {
795+ pinned = true ;
796+ bubble . style . cssText = expanded ;
797+ } ;
798+ bubble . ondblclick = ( ) => bubble . remove ( ) ;
799+ bubble . title = "Click: pin open • Double-click: dismiss" ;
800+ bubble . textContent = "🐞 " + JSON . stringify ( debug , null , 2 ) ;
801+ document . body . appendChild ( bubble ) ;
802+ }
803+
764804function updateControls ( ) {
765805 // Show URL with CSS ellipsis, full URL as tooltip, clickable to open
766806 updateTitleDisplay ( ) ;
@@ -4872,6 +4912,8 @@ app.ontoolresult = async (result: CallToolResult) => {
48724912 viewUUID = result . _meta ?. viewUUID ? String ( result . _meta . viewUUID ) : undefined ;
48734913 interactEnabled = result . _meta ?. interactEnabled === true ;
48744914 fileWritable = result . _meta ?. writable === true ;
4915+ // TODO remove — debug: dump writability inputs so we can eyeball the mismatch
4916+ if ( result . _meta ?. _debug !== undefined ) showDebugBubble ( result . _meta . _debug ) ;
48754917
48764918 // Restore saved page or use initial page
48774919 const savedPage = loadSavedPage ( ) ;
@@ -5128,18 +5170,23 @@ async function processCommands(commands: PdfCommand[]): Promise<void> {
51285170 renderAnnotationPanel ( ) ;
51295171 break ;
51305172 case "get_pages" :
5131- // Handle async — don't block the poll loop. But if it rejects,
5132- // submit an empty payload so interact returns an error promptly
5133- // instead of blocking 45s in waitForPageData.
5134- handleGetPages ( cmd ) . catch ( ( err ) => {
5173+ // Await so the next poll doesn't start until submit_page_data has
5174+ // been SENT. The host (Claude Desktop/Nest) serializes iframe→server
5175+ // tool calls — if we re-poll immediately, submit_page_data queues
5176+ // behind the 30s long-poll and interact times out. Awaiting costs a
5177+ // few seconds of poll gap, but interact is blocked in waitForPageData
5178+ // anyway so no commands are lost.
5179+ try {
5180+ await handleGetPages ( cmd ) ;
5181+ } catch ( err ) {
51355182 log . error ( "get_pages failed — submitting empty result:" , err ) ;
5136- app
5183+ await app
51375184 . callServerTool ( {
51385185 name : "submit_page_data" ,
51395186 arguments : { requestId : cmd . requestId , pages : [ ] } ,
51405187 } )
51415188 . catch ( ( ) => { } ) ;
5142- } ) ;
5189+ }
51435190 break ;
51445191 case "file_changed" : {
51455192 // Skip our own save_pdf echo: either save is still in flight, or the
@@ -5179,8 +5226,14 @@ async function processCommands(commands: PdfCommand[]): Promise<void> {
51795226 }
51805227 }
51815228
5182- // Persist after processing batch
5183- persistAnnotations ( ) ;
5229+ // Persist after processing batch — but only if anything mutated.
5230+ // get_pages / file_changed are read-only; writing localStorage and
5231+ // recomputing the diff for them is wasted work.
5232+ if (
5233+ commands . some ( ( c ) => c . type !== "get_pages" && c . type !== "file_changed" )
5234+ ) {
5235+ persistAnnotations ( ) ;
5236+ }
51845237}
51855238
51865239let polling = false ;
0 commit comments