Skip to content

fix: multi-strategy PowerPoint slide detection#26

Merged
EinfachMxrc merged 1 commit into
mainfrom
fix/office-init-timing
Apr 11, 2026
Merged

fix: multi-strategy PowerPoint slide detection#26
EinfachMxrc merged 1 commit into
mainfrom
fix/office-init-timing

Conversation

@EinfachMxrc

@EinfachMxrc EinfachMxrc commented Apr 11, 2026

Copy link
Copy Markdown
Owner

Problem

Das Add-in erkannte weder die aktuelle Folie noch die Gesamtanzahl.

Ursache 1 – Office.js Timing: isOfficeAvailable() prüfte Office.context bevor Office.js fertig war → Bridge wurde nie gestartet.

Ursache 2 – Debounce: Die 150ms Verzögerung in handleSelectionChanged ließ den Fokus zurück zum Taskpane wandern, bevor getSelectedDataAsync aufgerufen wurde → API schlug immer fehl.

Fixes

Bridge-Initialisierung

  • Nur typeof Office !== "undefined" prüfen (Script geladen?)
  • officeDetected wird erst nach Office.onReady() gesetzt

Slide-Erkennung (3 Strategien kombiniert)

  1. PowerPoint.run() + getSelectedSlides() (PowerPointApi 1.5) — fokusunabhängig, primäre Methode
  2. DocumentSelectionChanged ohne Debounce — liest sofort wenn Folie geklickt, Fokus ist noch auf der Präsentation
  3. window blur/focus Listener — zusätzliche Trigger wenn Fokus zwischen Taskpane und Präsentation wechselt
  4. Polling alle 500ms als Fallback

Nach dem Merge

Taskpane in PowerPoint schließen und neu öffnen — kein Manifest-Re-Download nötig.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Performance

    • Optimized selection change detection for immediate responsiveness
    • Increased sync polling frequency for faster synchronization with PowerPoint
    • Improved taskpane window focus detection
  • Bug Fixes

    • Enhanced error handling for transient synchronization failures with graceful fallbacks
    • Refined slide information retrieval with better fallback logic

Previous debounce of 150ms caused getSelectedDataAsync to be called
after focus returned to the taskpane, making it always fail.

Changes:
- Remove debounce: call syncCurrentSlide immediately on selection change
  so the presentation still has focus when getSelectedDataAsync is called
- Add window blur/focus listeners: blur fires when user clicks into
  presentation (good time to read slide), focus fires when returning
- Poll every 500ms (was 800ms) as reliable fallback
- PowerPoint.run + getSelectedSlides as primary (focus-independent)
- Pass knownTotalSlides from PowerPoint.run into legacy fallback to
  avoid redundant getSlideCountAsync call
- Add teardownWindowListeners in destroyOfficeBridge

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Apr 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
handout-powerpoint-addin Ready Ready Preview, Comment Apr 11, 2026 0:09am

@coderabbitai

coderabbitai Bot commented Apr 11, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e99bf8c8-3cb6-424e-aca3-214cf624c196

📥 Commits

Reviewing files that changed from the base of the PR and between 2cb65a1 and bfce2f5.

📒 Files selected for processing (1)
  • apps/web/src/lib/powerpoint/officeBridge.ts

📝 Walkthrough

Walkthrough

The Office Bridge module is refactored to remove debouncing from selection-change handling, reduce polling interval from 800ms to 500ms, introduce window focus/blur listeners for slide synchronization, improve error-handling behavior, and enhance cleanup paths.

Changes

Cohort / File(s) Summary
Office Bridge Refactoring
apps/web/src/lib/powerpoint/officeBridge.ts
Removed debounce mechanism for selection changes; decreased polling interval to 500ms; added window focus/blur listeners; modified event-handler wiring; simplified error callbacks; updated slide-info fallback logic with knownTotalSlides parameter; expanded destroyOfficeBridge() cleanup to deregister listeners and conditionally remove Office handlers.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 Debounce hops away, polling quickens its pace,
Window whispers "focus!"—a fresh embrace,
Error silence blooms, cleanup spreads its wings,
Faster slide-sync flow, simpler handler strings! ✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/office-init-timing

Comment @coderabbitai help to get the list of available commands and usage tips.

@EinfachMxrc EinfachMxrc merged commit e6d855b into main Apr 11, 2026
2 of 3 checks passed
@greptile-apps

greptile-apps Bot commented Apr 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR refactors apps/web/src/lib/powerpoint/officeBridge.ts to fix two root-cause bugs in the PowerPoint Add-in slide detection: premature Office availability checks and a debounce that caused getSelectedDataAsync to run after focus had left the slide. The fix introduces three complementary detection strategies — the focus-independent PowerPoint.run + getSelectedSlides() (PowerPointApi 1.5), immediate DocumentSelectionChanged handling (no debounce), and window blur/focus listeners — plus 500ms polling as a final fallback.

Key changes:

  • initOfficeBridge now guards with typeof Office === "undefined" instead of !isOfficeAvailable(), fixing the timing issue where Office.context wasn't yet set when the effect ran.
  • Debounce replaced by immediate handleSelectionChanged, fixing focus loss before getSelectedDataAsync could be called.
  • getCurrentSlideInfo now attempts PowerPoint.run + getSelectedSlides() first (focus-independent), falling back to the legacy getSelectedDataAsync path.
  • setupWindowListeners / teardownWindowListeners added for blur/focus-based triggering.
  • destroyOfficeBridge restructured to always reset module state, regardless of whether Office is still available.
  • PowerPointAddinClient.tsx already updated in a prior commit to use typeof (globalThis as any).Office !== "undefined" directly, consistent with the new bridge design.

Confidence Score: 4/5

Safe to merge — the multi-strategy approach correctly addresses both root causes, and all identified concerns are non-blocking style issues.

The two diagnosed root causes (premature Office.context check, debounce-induced focus loss) are properly fixed. The three fallback strategies are logically sound and PowerPointAddinClient.tsx was already updated correctly. Remaining comments are P2: untracked blur timeouts, missing double-registration guard on window listeners, and silent error suppression — none of these break the primary user path or introduce data loss.

apps/web/src/lib/powerpoint/officeBridge.ts — specifically setupWindowListeners (double-registration) and the blur timeout lifecycle.

Important Files Changed

Filename Overview
apps/web/src/lib/powerpoint/officeBridge.ts Multi-strategy slide detection with PowerPoint.run, immediate selection-change handler, and window blur/focus listeners; minor: blur handler creates untracked setTimeouts and setupWindowListeners lacks a guard against double-registration.

Sequence Diagram

sequenceDiagram
    participant App as PowerPointAddinClient
    participant Bridge as officeBridge.ts
    participant OfficeJS as Office.js Runtime
    participant PPAPI as PowerPoint JS API

    App->>Bridge: initOfficeBridge(callbacks)
    Note over Bridge: typeof Office === "undefined"?<br/>→ resolve("manual_only")

    Bridge->>OfficeJS: Office.onReady()
    OfficeJS-->>Bridge: {host: PowerPoint}

    Bridge->>Bridge: isInitialized = true
    Bridge->>Bridge: syncCurrentSlide() [immediate]
    Bridge->>Bridge: startPolling() [every 500ms]
    Bridge->>Bridge: setupWindowListeners()
    Note over Bridge: window.addEventListener(blur)<br/>window.addEventListener(focus)

    Bridge->>OfficeJS: addHandlerAsync(DocumentSelectionChanged)
    OfficeJS-->>Bridge: success → resolve(auto)

    Note over App,PPAPI: User clicks slide in PowerPoint

    OfficeJS->>Bridge: DocumentSelectionChanged (immediate, no debounce)
    Bridge->>Bridge: syncCurrentSlide()

    alt PowerPoint JS API 1.5 available
        Bridge->>PPAPI: PowerPoint.run(context => ...)
        PPAPI->>Bridge: context.presentation.getSelectedSlides()
        Bridge-->>App: onSlideChange(slideNumber, totalSlides)
    else Fallback: legacyGetSlideInfo
        Bridge->>OfficeJS: getSelectedDataAsync(SlideRange)
        OfficeJS-->>Bridge: slides[0].index
        Bridge->>OfficeJS: getSlideCountAsync()
        OfficeJS-->>Bridge: totalSlides
        Bridge-->>App: onSlideChange(slideNumber, totalSlides)
    end

    Note over Bridge: window blur (taskpane loses focus)
    Bridge->>Bridge: setTimeout(syncCurrentSlide, 80ms)

    App->>Bridge: destroyOfficeBridge()
    Bridge->>Bridge: clearInterval(pollTimer)
    Bridge->>Bridge: teardownWindowListeners()
    Bridge->>OfficeJS: removeHandlerAsync(DocumentSelectionChanged)
    Bridge->>OfficeJS: removeHandlerAsync(ActiveViewChanged)
    Bridge->>Bridge: isInitialized = false
Loading

Fix All in Codex

Reviews (1): Last reviewed commit: "fix: multi-strategy slide detection with..." | Re-trigger Greptile

function setupWindowListeners() {
if (typeof window === "undefined") return;

// Taskpane lost focus → user moved to presentation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Blur timeouts not tracked or cancelled on teardown

Each blur event creates a new setTimeout whose ID is never stored, so destroyOfficeBridge (and teardownWindowListeners) cannot cancel pending timers. If the bridge is torn down while an 80ms timer is still queued, syncCurrentSlide will run after activeCallbacks is set to null. The function already guards against a null activeCallbacks, so there's no crash — but unnecessary async work is triggered after teardown, and if many blur events fire in quick succession (user rapidly alt-tabs) multiple timers queue up.

Consider storing and cancelling the ID:

Suggested change
// Taskpane lost focus → user moved to presentation.
let blurTimer: ReturnType<typeof setTimeout> | null = null;
windowBlurListener = () => {
if (blurTimer) clearTimeout(blurTimer);
blurTimer = setTimeout(() => { blurTimer = null; void syncCurrentSlide(); }, 80);
};

Alternatively, promote blurTimer to module scope alongside windowBlurListener and clear it in teardownWindowListeners.

Fix in Codex

Comment on lines +112 to +131
}

function setupWindowListeners() {
if (typeof window === "undefined") return;

// Taskpane lost focus → user moved to presentation.
// Wait briefly for focus transfer to complete, then try reading.
windowBlurListener = () => setTimeout(() => void syncCurrentSlide(), 80);

// Taskpane gained focus → user just came from the slides.
// Read immediately before focus fully transfers to taskpane.
windowFocusListener = () => void syncCurrentSlide();

window.addEventListener("blur", windowBlurListener, { passive: true });
window.addEventListener("focus", windowFocusListener, { passive: true });
}

function teardownWindowListeners() {
if (typeof window === "undefined") return;
if (windowBlurListener) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 setupWindowListeners has no guard against double-registration

setupWindowListeners unconditionally overwrites windowBlurListener / windowFocusListener and calls addEventListener without first checking whether listeners are already attached. If initOfficeBridge is called a second time (e.g., React StrictMode double-invoke in development, or session re-initialization without a prior destroyOfficeBridge), both the old and new listener functions are registered on the window — teardownWindowListeners will only remove the second one because the module-level variable now points to it.

A simple guard prevents this:

Suggested change
}
function setupWindowListeners() {
if (typeof window === "undefined") return;
// Taskpane lost focus → user moved to presentation.
// Wait briefly for focus transfer to complete, then try reading.
windowBlurListener = () => setTimeout(() => void syncCurrentSlide(), 80);
// Taskpane gained focus → user just came from the slides.
// Read immediately before focus fully transfers to taskpane.
windowFocusListener = () => void syncCurrentSlide();
window.addEventListener("blur", windowBlurListener, { passive: true });
window.addEventListener("focus", windowFocusListener, { passive: true });
}
function teardownWindowListeners() {
if (typeof window === "undefined") return;
if (windowBlurListener) {
function setupWindowListeners() {
if (typeof window === "undefined") return;
if (windowBlurListener || windowFocusListener) return; // already registered
// Taskpane lost focus → user moved to presentation.
// Wait briefly for focus transfer to complete, then try reading.
windowBlurListener = () => setTimeout(() => void syncCurrentSlide(), 80);
// Taskpane gained focus → user just came from the slides.
// Read immediately before focus fully transfers to taskpane.
windowFocusListener = () => void syncCurrentSlide();
window.addEventListener("blur", windowBlurListener, { passive: true });
window.addEventListener("focus", windowFocusListener, { passive: true });
}

Fix in Codex

Comment on lines 100 to 107
if (info.slideNumber !== lastReportedSlide) {
lastReportedSlide = info.slideNumber;
activeCallbacks.onSlideChange(info);
}
} catch (error) {
activeCallbacks?.onError(`Folien-Update fehlgeschlagen: ${String(error)}`);
} catch {
// silently ignore transient errors
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent error suppression removes the onError signal path

Previously, syncCurrentSlide forwarded exceptions to activeCallbacks?.onError(...) so the UI could surface "Folien-Update fehlgeschlagen: …". The new catch block silently discards all errors.

Since syncCurrentSlide is now called on every blur/focus/selection event AND every 500ms, making every transient failure user-visible would indeed be noisy. However, errors from the initial void syncCurrentSlide() call in initOfficeBridge (right after isInitialized = true) are also silently discarded — if something is fundamentally broken, the user gets no feedback at all.

Consider at least logging to the console in development, or throttling the error report so only the first failure per session surfaces via onError.

Fix in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant