Skip to content

feat(desktop): support opening transient external files via preview grants - #154

Open
krit22 wants to merge 9 commits into
liliu-z:mainfrom
krit22:feat/152-transient-external-files
Open

feat(desktop): support opening transient external files via preview grants#154
krit22 wants to merge 9 commits into
liliu-z:mainfrom
krit22:feat/152-transient-external-files

Conversation

@krit22

@krit22 krit22 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Resolves #152

Summary

This PR introduces support for opening transient, read-only external files (triggered via desktop file drop on the main content pane or native OS file-open events like Finder "Open With", CLI argv, or second-instance launches) using secure, window-scoped preview grants. External files are rendered in-place using existing document viewers without copying them to the library folder or triggering indexing pipelines.

What changed

Electron & IPC Bridge

  • Added an in-memory activePreviewGrants registry on the main process to track allowed file paths per window ID.
  • Implemented IPC handlers grant:register (reuses internal workspace tab if path resides in active folder, otherwise registers a UUID grantId) and grant:revoke (removes grant).
  • Automatically clears preview grants when a window is closed.
  • Queues command-line arguments and macOS open-file events until the renderer fires a ready signal, then dispatches them to avoid race conditions. Routes second-instance requests to the last focused live window.

Express Server Validators

  • Mounted /api/internal/grants routes to sync registered preview grants.
  • Implemented /api/grant/:grantId/text and /asset-preview-grant/:grantId to validate that the request's window ID (currentWindowId()) matches the grant's registered window. Denial returns 403 Forbidden. Sibling directory traversal is blocked.

Store State & Reducer

  • Extended OpenFile to carry transient properties: isExternal, isReadOnly, grantId, and absolutePath.
  • Configured the FILE_OPEN reducer to set outOfFolder = true when isExternal is set, isolating external tabs from selection highlighting, search results, vector indexing, Quick Open, and recents logging.
  • Set up automatic grant revocation when a transient tab is closed in the renderer.
  • Custom-tailored refreshActiveTabFromDisk to reload external tabs via getExternalFileText or HEAD requests. If the file is unmounted or deleted, the tab transitions to an unavailable warning state (⚠️ This external file is no longer available.) instead of duplicating.

Split Drag Overlay & UI

  • Updated useGlobalDragDrop.ts to identify cursor hover zones ('sidebar' | 'main') using the .sidebar group class, setting pointer dropEffect to 'copy' on the Files sidebar and 'link' on the main pane.
  • Routed main-pane and document iframe drops to open external files temporarily (filtering directories and warning the user).
  • Redesigned MotionDropVeil to draw two side-by-side drag regions ("Copy to library" vs "Open temporarily") with interactive borders and custom background highlights.
  • Rendered an "External File" identity header banner in the main pane and mapped tooltips to show the absolute path of transient files.

Impact

Users can drop files onto the main editor area or open them via command line/double-click to preview documents instantly without cluttering their local database, uploading assets, or triggering vector embedding runs.

Validation

  • pnpm typecheck (SUCCESS)
  • npx vite build --config web-src/vite.config.ts (SUCCESS)
  • pnpm test:renderer (All 212 tests passed, including reducer test coverage for external file properties)
  • pnpm test:library-files (All 7 tests passed, including the new /server/routes/internal-grants.test.ts checking grant boundary security limits)
  • pnpm test:electron (All 15 tests passed)

@krit22
krit22 marked this pull request as ready for review August 11, 2026 19:05
@krit22
krit22 marked this pull request as draft August 11, 2026 19:07
@krit22
krit22 marked this pull request as ready for review August 11, 2026 19:15

@PPRAMANIK62 PPRAMANIK62 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Request changes required:

  • External HTML/PDF/image/DOCX/audio viewers never receive or use activeTab.file.grantId; they continue to load normal folder-scoped /asset URLs, so external previews fail without an active folder or can resolve the wrong same-named library file. Browser-loaded grant URLs must also include the target window identity, since they cannot send the window header.
  • Video extensions are accepted but the renderer has neither a video OpenFile.format nor a video viewer, producing blank tabs.
  • path.resolve() does not canonicalize a symlink; a granted symlink can later be repointed to a different file. Resolve/store the real path before granting and for containment checks.
  • The global pre-ready native-open queue can be drained by a different window from the most-recently-focused target; queue entries by target window.
  • Unsupported multi-file drops emit one toast per file instead of the required bounded summary.

Please address these before merge.

@krit22
krit22 force-pushed the feat/152-transient-external-files branch from 5d1f80b to b9ac612 Compare August 16, 2026 09:31
@krit22

krit22 commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review @PPRAMANIK62! All requested changes have been addressed and validated:

  1. Document/Image/Audio Preview Grants & URL Prefixing:

    • Updated server/http.ts and server/routes/internal-grants.ts to support window context in the URL path (/asset-preview-grant/__window/<windowId>/<grantId>), allowing browser elements (<iframe src>, <img>, <audio src>) to load grant assets without custom headers.
    • Passed sourceGrantId to versionedAssetUrl(...) across HtmlPreview, PdfPreview, DocxPreview, ImagePreview, and AudioPreview.
    • Suppressed AppData preparation/conversion failure notices and transcript polling when viewing external media.
  2. Video & Media Formats:

    • Explicitly mapped media container extensions (.mp4, .mov, .m4v, .webm, .mkv, .avi) and .json in getFileFormat() in electron/main.cjs.
  3. Symlink Resolution & TOCTOU Protection:

    • Applied fs.realpathSync to canonicalize file paths at registration and containment checks.
    • Added runtime fs.realpathSync validation on /api/grant/:grantId/text and /asset-preview-grant/* to reject repointed symlinks (returning 403).
    • Added unit test in server/routes/internal-grants.test.ts verifying symlink repointing attacks are rejected.
  4. Multi-Window Native File Queue Isolation:

    • Integrated createNativeOpenQueueCoordinator in electron/multi-window.cjs to isolate pending native-open queues by webContents.id.
    • Wired readiness resets on navigation (did-start-loading / did-finish-load) and active window selection.
  5. Multi-File Drop Toast Aggregation:

    • Updated useDocumentActions.ts (openExternalFiles) to aggregate failures into a single bounded error summary toast.
  6. Additional Invariants & Hardening:

    • Enforced read-only behavior for external tabs across Markdown/JSON viewers, preventing Cmd+E edit mode toggles or autosaving into the active workspace.
    • Cleaned up active preview grants on tab close, folder reset (resetFolderScopedState), and window destruction.
    • Updated documentation contracts in code-review/document-viewers.md, code-review/window-lifecycle.md, code-review/file-transactions.md, and design-docs/design/documents.md.

All pre-commit verification gates (typecheck, build:web, test:docs, test:renderer, test:library-files, test:electron, test:electron:smoke) pass cleanly.

@krit22
krit22 requested a review from PPRAMANIK62 August 16, 2026 09:37
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.

feat(library): distinguish imports from transient external-file opening

2 participants