How the FileViewer Handles Automatic Refresh for Different File Types in pi-web

The FileViewer component in pi-web implements automatic refresh through Server-Sent Events (SSE), where each specialized viewer maintains a live connection to /api/files/.../watch and invalidates cached content via a bust counter when change events occur.

The FileViewer component in agegr/pi-web provides a unified live-refresh system across image, audio, document, and text file types. This article explains how automatic refresh for different file types is implemented, based on the source code in components/FileViewer.tsx and supporting files.

File Type Detection and Viewer Selection

Before handling refresh logic, the component determines which viewer to render. In lib/file-types.ts, helper functions classify files by extension:

  • isImagePath() — routes to ImageViewer
  • isAudioPath() — routes to AudioViewer
  • isDocumentPreviewPath() — routes to DocumentViewer
  • Default fallback — TextFileViewer

Each viewer implements the same SSE-based refresh pattern but adapts it to its specific content type.

Core Refresh Mechanism: The bust Counter

All viewers rely on a shared state variable called bust (a simple integer counter). When the server notifies the client of file changes, setBust(b => b + 1) triggers, forcing React to generate new URLs and re-fetch content.

This pattern appears consistently across viewers:

// From ImageViewer section (lines 84-92)
setBust(b => b + 1);  // Forces new <img src> with cache-busting query param

The bust value is appended as a v query parameter:

getFileApiUrl(filePath, "read", sourceSessionId, bust ? { v: bust } : undefined)

ImageViewer: Live Image Refresh

The ImageViewer creates its SSE connection at lines 76-78:

const source = new EventSource(getFileApiUrl(filePath, "watch", sourceSessionId));

When a change event fires:

  1. syncRequestRef.current increments to track the request
  2. size and naturalSize state are cleared
  3. bust counter increments, forcing a new src URL for the <img> element

The toolbar displays a live-sync indicator (green when watching is true, red otherwise) at lines 31-45.

AudioViewer: Duration and Source Reload

The AudioViewer follows an identical SSE setup at lines 48-50. Its change handler (lines 55-63) performs three actions:

  • Clears cached duration state
  • Increments bust to reload <audio src>
  • Updates file size metadata

This ensures the audio element fetches fresh content without manual page reload.

DocumentViewer: PDF and Office Document Preview

For PDF, DOCX, and other documents, the viewer at lines 90-92 establishes the SSE connection. On change (lines 98-106):

  • bust invalidates the preview cache
  • Size metadata refreshes
  • The <iframe> or embedded viewer re-loads with the updated document

TextFileViewer: Content and Diff Synchronization

The TextFileViewer has the most complex refresh logic. Its SSE connection is established in a useEffect hook watching filePath (lines 115-122).

On change, it executes two parallel fetches:

fetchContent();  // Reload source text
fetchGitDiff();  // Update diff view

The bust counter ensures raw file fetches bypass browser cache via the v query parameter (lines 130-132).

Server-Sent Events Lifecycle

All viewers follow this standardized SSE workflow:

  1. Initialization — Close any existing EventSource, set watching = false
  2. Connection — new EventSource(getFileApiUrl(filePath, "watch", sourceSessionId))
  3. Handshake — On connected event, set watching = true and perform initial sync
  4. Live updates — Each change event triggers type-appropriate refresh via bust invalidation

The watch endpoint implementation resides in app/api/files/[...path]/route.ts, emitting SSE events for connected and change states.

Supporting Infrastructure

File Purpose
components/FileViewer.tsx Central dispatcher with per-type SSE logic
lib/file-types.ts Extension-based file classification
lib/file-viewer-state.ts Persisted viewer preferences (display mode, scroll position, wrap lines)
app/api/files/[...path]/route.ts SSE watch endpoint implementation

Summary

  • FileViewer selects specialized viewers via isImagePath, isAudioPath, and isDocumentPreviewPath helpers in lib/file-types.ts
  • All viewers create SSE connections to /api/files/.../watch using new EventSource()
  • The bust counter provides uniform cache invalidation across image, audio, document, and text content
  • TextFileViewer uniquely refreshes both content and Git diffs on change events
  • The live-sync indicator visualizes connection state in each viewer's toolbar

Frequently Asked Questions

How does FileViewer know when a file has changed on disk?

The FileViewer does not poll. Instead, each viewer opens a persistent Server-Sent Events connection to the backend watch endpoint (/api/files/.../watch). When the filesystem detects a modification, the server pushes a change event through the SSE stream, triggering client-side refresh.

Why does the ImageViewer use a bust counter instead of directly modifying the <img src?

Incrementing a counter (setBust(b => b + 1)) and embedding it as a query parameter (?v=3) forces the browser to treat each version as a unique URL. This reliably bypasses image caching without complex state management or DOM manipulation.

What file types support live refresh in pi-web?

According to lib/file-types.ts, live refresh covers images (via isImagePath), audio (via isAudioPath), documents including PDF and DOCX (via isDocumentPreviewPath), and all other files through the fallback TextFileViewer.

Does the TextFileViewer refresh differently than other viewers?

Yes. While ImageViewer, AudioViewer, and DocumentViewer primarily invalidate cached previews, TextFileViewer explicitly calls fetchContent() and fetchGitDiff() on change events. This dual fetch ensures both raw file content and version control diffs remain synchronized.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →