Skip to content

feat(viewer): refresh button to reload the open model from disk (#1345)#1352

Merged
louistrue merged 5 commits into
mainfrom
feat/refresh-model-1345
Jun 24, 2026
Merged

feat(viewer): refresh button to reload the open model from disk (#1345)#1352
louistrue merged 5 commits into
mainfrom
feat/refresh-model-1345

Conversation

@louistrue

@louistrue louistrue commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #1345. Adds a Refresh button to reload the open model without closing and re-opening the same file, so it can be monitored during design.

The existing Open flow uses <input type="file">, which hands back a File that is a frozen snapshot of the bytes at pick time. Re-reading it can never reflect on-disk edits. Only the File System Access API (showOpenFilePickerFileSystemFileHandle.getFile()) returns a fresh snapshot of the current file. So the Open button now prefers that API when available (Chromium / secure context) to capture a live handle, and falls back to <input type="file"> everywhere else.

Changes (apps/viewer)

  • services/file-system-access.ts (new) — supportsFileSystemAccess(), openIfcFilesWithHandles() (picks files + captures live handles; user-cancel handled), readFreshFile(handle) (permission-checked re-read of current on-disk bytes; surfaces moved/deleted/denied).
  • types/file-system-access.d.ts (new) — minimal ambient declarations for the API surface missing from TS 6.0.3 lib.dom (Window.showOpenFilePicker, OpenFilePickerOptions, queryPermission/requestPermission). FileSystemFileHandle.getFile() already ships in lib.dom.
  • store/types.tsFederatedModel.sourceHandle?: FileSystemFileHandle (in-memory only, never serialized to cache; mirrors sourceFile).
  • hooks/useIfcLoader.tsloadFile accepts an optional { sourceHandle } and persists it on the primary model record.
  • components/viewer/MainToolbar.tsx — Open button prefers the FS Access API; new Refresh button (RefreshCw).

Behaviour / scope

  • Refresh is only shown when exactly one model is loaded and it has a live handle. Drag-drop, <input type="file">, recent-files-from-cache, and federated multi-model loads have no handle, so the button isn't offered (no dead/disabled control).
  • Multi-file Open keeps the existing federated/sequential paths unchanged.
  • Non-Chromium browsers (Firefox/Safari) and insecure contexts fall back to <input type="file"> transparently — no Refresh button, no regression.

Testing

  • tsc --noEmit clean for all touched files (remaining errors are pre-existing un-built-package gaps in the worktree, unrelated to this change).
  • Not yet smoke-tested in a running viewer (requires built wasm pkg + dev server).

Follow-up (not in this PR)

  • Drag-and-drop could also capture handles via Chromium DataTransferItem.getAsFileSystemHandle(), extending Refresh to dropped files.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a “Refresh” action that reloads models from their original disk source when supported.
    • Improved Open, Add Model, and drag-and-drop to use file picker handles when available, enabling refresh-aware loading.
    • Enhanced recent files to make faster, cache-aware loading decisions.
  • Bug Fixes
    • Updated command palette file-dialog activation to more reliably preserve browser interaction state.
    • Improved recent-file caching performance and resilience for more consistent loads.

Add a Refresh button that re-reads the currently open model from disk and
re-parses it, so a model can be monitored during design without closing and
re-opening the same file.

A `<input type="file">` File is a frozen snapshot of the bytes at pick time, so
re-reading it can never reflect on-disk edits. Only the File System Access API
(showOpenFilePicker -> FileSystemFileHandle.getFile()) returns a fresh snapshot.
The Open button now prefers that API when available (Chromium, secure context)
to capture a live handle, falling back to `<input type="file">` elsewhere.

- services/file-system-access.ts: feature-detected open + permission-checked
  fresh re-read helpers.
- types/file-system-access.d.ts: ambient decls for the API surface missing from
  TS lib.dom (showOpenFilePicker, options, queryPermission/requestPermission).
- FederatedModel.sourceHandle: in-memory live handle (never serialized to cache).
- loadFile() persists the handle on the primary model record.
- MainToolbar: Refresh shown only for a single model that has a live handle
  (drag-drop / input / cache-restored / federated loads have none).
@vercel

vercel Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
ifc-lite Ready Ready Preview, Comment Jun 24, 2026 3:53pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
ifc-lite-dev Ignored Ignored Preview Jun 24, 2026 3:53pm
ifc-lite-viewer-embed Ignored Ignored Jun 24, 2026 3:53pm

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds File System Access API support to viewer file open, add, drop, and refresh flows. Live file handles are propagated into model records, recent-file caching gains filename lookup, and command palette actions are adjusted to preserve browser file-dialog activation.

Changes

File System Access API Integration and Refresh

Layer / File(s) Summary
FS Access ambient types and service module
apps/viewer/src/types/file-system-access.d.ts, apps/viewer/src/services/file-system-access.ts
Adds ambient DOM declarations for File System Access APIs and a service module for opening, refreshing, and drag-dropping supported BIM files with live handles.
Handle propagation through loader and federation
apps/viewer/src/store/types.ts, apps/viewer/src/hooks/useIfcLoader.ts, apps/viewer/src/hooks/useIfcFederation.ts
FederatedModel gains sourceHandle, and loader/federation hooks thread optional handles through primary and sequential model loading.
Cached recent-file names
apps/viewer/src/lib/recent-files.ts
Adds getCachedFileNames() and refactors blob caching to stage records before the IndexedDB write transaction.
Command palette user-activation and cache-aware dispatch
apps/viewer/src/components/viewer/CommandPalette.tsx
Adds immediate command execution, preloads cached recent-file names, and dispatches open/load events for file commands.
ViewportContainer open, drop, and routeLoad
apps/viewer/src/components/viewer/ViewportContainer.tsx
Unifies supported-file loading across picker, input, and drag-and-drop paths and routes open actions through File System Access when available.
MainToolbar open, add, and refresh flows
apps/viewer/src/components/viewer/MainToolbar.tsx
Reworks open/add handling to use live handles, adds IFCX overlay-aware loading, and introduces conditional refresh from stored source handles.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • LTplus-AG/ifc-lite#130: This PR’s handle propagation and multi-model loading changes overlap with the federated-model foundations in that PR.
  • LTplus-AG/ifc-lite#166: This PR touches the same supported-file filtering logic used to accept .glb and related viewer file types.
  • LTplus-AG/ifc-lite#503: This PR updates the same recent-files cache path used by CommandPalette and the blob lookup helpers.

Poem

🐇 I hopped to files both old and new,
With live handles bright and cache names too.
A refresh tap made models bloom,
From disk they leapt right back to room.
My whiskers twitch, the viewer sings,
And files now dance on fleeter wings.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a refresh button to reload the open model from disk.
Linked Issues check ✅ Passed The refresh button and live-handle reload flow satisfy #1345 by reloading the currently open model from disk without reopening it.
Out of Scope Changes check ✅ Passed No clear out-of-scope changes; the added FS-access, handle, and cache updates all support the refresh-from-disk feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feat/refresh-model-1345

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Broaden the refresh feature so it works regardless of how a model was opened,
and refresh whole federations at once.

The first PR only captured a live handle from the toolbar Open button, so a
model opened from the empty-state "Open .ifc file" button, drag-drop, or the
Add-Model button had no handle and showed no Refresh button. Now every
in-session ingestion path on Chromium captures a handle:

- ViewportContainer empty-state open buttons -> showOpenFilePicker.
- Drag-drop -> DataTransferItem.getAsFileSystemHandle() (kicked off
  synchronously before any await, since the DataTransferItemList is neutered
  once the drop handler returns).
- Add-Model button + multi-file Open -> picker handles threaded through
  loadFilesSequentially -> addModel -> the federated finalize.

Refresh is now "Refresh all": it re-reads every handle-backed model's bytes,
then reloads a single model in place or rebuilds the federation (preserving id,
order, visibility, collapsed), reusing the proven reload sequence. Reads happen
before anything is cleared, so a failed read never empties the viewer; partial
failures are reported per file. The button shows only when every loaded model
has a handle (session-only, in memory), so a mixed session never risks dropping
handle-less models.

No polling/auto-watch and no cross-session persistence, per the agreed scope.
…1345)

`runCommand` deferred every action to the next animation frame, and the
recent-file action then awaited an IndexedDB read. Both discard the transient
user activation Chrome requires to open a file dialog, so "Open File" and the
recent-file cache-miss fallback silently no-opped. (A cache HIT still loaded,
since dispatching a load event needs no activation, which is why it looked
intermittent.)

- Command.immediate runs file-dialog actions synchronously inside the click,
  preserving activation.
- Recent-file clicks decide cache hit/miss from a key set pre-loaded on open
  (getCachedFileNames), so no await sits between the gesture and the picker:
  hit -> load the cached blob; miss -> re-pick.
- "Open File" and the re-pick path dispatch `ifc-lite:open-files`, which the
  (always-mounted) MainToolbar routes to its handle-capturing open. So palette
  opens now work AND capture a live handle, keeping the model refreshable.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (1)
apps/viewer/src/lib/recent-files.ts (1)

126-138: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log the swallowed error and close the DB on the failure path.

The catch silently discards the error, which makes IndexedDB issues invisible during debugging. Additionally, db.close() (Line 134) is only reached on success — if getAllKeys() rejects, control jumps to catch and the opened connection is never closed, leaking it. Wrap cleanup in finally and log before returning the fallback.

♻️ Proposed fix
 export async function getCachedFileNames(): Promise<string[]> {
+  let db: IDBDatabase | undefined;
   try {
-    const db = await openDB();
+    db = await openDB();
     const tx = db.transaction(STORE_NAME, 'readonly');
     const req = tx.objectStore(STORE_NAME).getAllKeys();
     const keys = await new Promise<IDBValidKey[]>((resolve, reject) => {
       req.onsuccess = () => resolve(req.result);
       req.onerror = () => reject(req.error);
     });
-    db.close();
     return keys.map(String);
-  } catch {
+  } catch (err) {
+    console.error('[recent-files] getCachedFileNames failed:', err);
     return [];
+  } finally {
+    db?.close();
   }
 }

As per coding guidelines: "No silent catch {} — log or rethrow errors".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/viewer/src/lib/recent-files.ts` around lines 126 - 138, The recent-files
IndexedDB read path swallows failures and only closes the connection on success,
so update the `openDB()`/`getAllKeys()` flow in `recent-files.ts` to log the
caught error instead of using a silent `catch`, and move `db.close()` into a
`finally` so it always runs even when `tx.objectStore(STORE_NAME).getAllKeys()`
rejects. Keep the fallback return of an empty array after logging, and preserve
the existing `keys.map(String)` success path.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/viewer/src/components/viewer/MainToolbar.tsx`:
- Around line 445-446: The IFCX handling in MainToolbar is inconsistent because
isSupportedModelFile now accepts case-insensitive filenames, but later logic
still checks raw string suffixes like .endsWith('.ifcx'), causing MODEL.IFCX to
skip IFCX-specific composition and overlay behavior. Update the downstream IFCX
detection in MainToolbar (including the branches around the supportedFiles flow
and the later composition/overlay checks) to normalize filenames before testing
suffixes, and use a single shared IFCX check so MODEL.IFCX is treated the same
regardless of case.
- Around line 937-947: The icon-only Refresh button in MainToolbar lacks an
accessible name, so add an aria-label directly on the Button used with RefreshCw
and handleRefresh. Keep the tooltip if desired, but ensure assistive tech can
identify the action from the button itself by giving it a clear label such as
“Refresh”.
- Around line 583-618: The refresh flow in MainToolbar’s reload handler is not
atomic: the multi-model path clears the viewer and reloads only the fresh
entries, which drops failed models, and the single-model path shows success
before loadFile finishes. Update the reload logic so the existing scene/state is
only replaced after all reload operations complete successfully, and keep failed
models untouched when any read fails. Use the existing symbols ok, failedNames,
loadFile, addModel, and clearAllModels to locate and adjust the refresh branch,
and move the success toast to fire only after the async reload work has fully
completed.
- Around line 536-557: The file picker flow in MainToolbar lets unsupported “all
files” selections through, so sanitize the results before any downstream work.
In the openIfcFilesWithHandles/handleFileSelect path, filter opened to only the
supported IFC/IFCX files before calling recordRecentFiles, cacheFileBlobs,
loadFile, or loadFilesSequentially, or tighten the picker configuration to
accept only the same formats as the input/Add Model flows.

In `@apps/viewer/src/components/viewer/ViewportContainer.tsx`:
- Around line 487-493: The drop handling in ViewportContainer is pairing files
to handles by filename, which collapses same-named files from different folders
and can assign the wrong sourceHandle. Update the logic around
handlesFromDataTransfer, supportedFiles, and the refresh path to preserve the
original captured handle/file ordering or direct pairing instead of using
handleByName. Ensure each dropped file keeps its own corresponding handle so
Refresh rereads the correct disk file.

In `@apps/viewer/src/services/file-system-access.ts`:
- Around line 137-140: The dropped-handle fallback in getAsFileSystemHandle is
silently swallowing exceptions, which hides drag/drop failures. Update the
try/catch in file-system-access.ts so the catch block either logs the caught
error with enough context or rethrows it instead of returning a silent null, and
keep the existing getAsFileSystemHandle() path behavior unchanged when it
succeeds.

In `@apps/viewer/src/store/types.ts`:
- Around line 339-344: Update the `sourceHandle` lifecycle documentation in
`types.ts` so it matches the new federation behavior:
`useIfcFederation.loadFilesSequentially()` can now attach live handles to
federated models for refresh support. Adjust the comment on `sourceHandle` to
exclude only non-handle federated loads, while still noting it is absent for
drag-drop, `<input type="file">`, cache-restored, and any federated load created
without a live handle.

---

Nitpick comments:
In `@apps/viewer/src/lib/recent-files.ts`:
- Around line 126-138: The recent-files IndexedDB read path swallows failures
and only closes the connection on success, so update the
`openDB()`/`getAllKeys()` flow in `recent-files.ts` to log the caught error
instead of using a silent `catch`, and move `db.close()` into a `finally` so it
always runs even when `tx.objectStore(STORE_NAME).getAllKeys()` rejects. Keep
the fallback return of an empty array after logging, and preserve the existing
`keys.map(String)` success path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0f4aba38-bb67-4697-b572-2e638ba7f479

📥 Commits

Reviewing files that changed from the base of the PR and between 0b73ebb and 9740184.

📒 Files selected for processing (9)
  • apps/viewer/src/components/viewer/CommandPalette.tsx
  • apps/viewer/src/components/viewer/MainToolbar.tsx
  • apps/viewer/src/components/viewer/ViewportContainer.tsx
  • apps/viewer/src/hooks/useIfcFederation.ts
  • apps/viewer/src/hooks/useIfcLoader.ts
  • apps/viewer/src/lib/recent-files.ts
  • apps/viewer/src/services/file-system-access.ts
  • apps/viewer/src/store/types.ts
  • apps/viewer/src/types/file-system-access.d.ts

Comment on lines +445 to +446
// Filter to supported files (IFC, IFCX, GLB, point clouds)
const supportedFiles = Array.from(files).filter(isSupportedModelFile);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Normalize IFCX detection after accepting case-insensitive filenames.

isSupportedModelFile accepts MODEL.IFCX, but the raw .endsWith('.ifcx') checks route it as non-IFCX, skipping IFCX composition/overlay behavior.

Suggested fix
 function isSupportedModelFile(f: File): boolean {
   const n = f.name.toLowerCase();
   return n.endsWith('.ifc') || n.endsWith('.ifcx') || n.endsWith('.glb')
     || n.endsWith('.las') || n.endsWith('.laz') || n.endsWith('.ply') || n.endsWith('.pcd')
     || n.endsWith('.e57') || n.endsWith('.pts') || n.endsWith('.xyz');
 }
+
+function isIfcxModelFile(f: File): boolean {
+  return f.name.toLowerCase().endsWith('.ifcx');
+}
 
-    const newFilesAreIfcx = supportedFiles.every(f => f.name.endsWith('.ifcx'));
+    const newFilesAreIfcx = supportedFiles.every(isIfcxModelFile);
 
-      const allIfcx = files.every(f => f.name.endsWith('.ifcx'));
+      const allIfcx = files.every(isIfcxModelFile);

Also applies to: 487-488, 548-548

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/viewer/src/components/viewer/MainToolbar.tsx` around lines 445 - 446,
The IFCX handling in MainToolbar is inconsistent because isSupportedModelFile
now accepts case-insensitive filenames, but later logic still checks raw string
suffixes like .endsWith('.ifcx'), causing MODEL.IFCX to skip IFCX-specific
composition and overlay behavior. Update the downstream IFCX detection in
MainToolbar (including the branches around the supportedFiles flow and the later
composition/overlay checks) to normalize filenames before testing suffixes, and
use a single shared IFCX check so MODEL.IFCX is treated the same regardless of
case.

Comment on lines +536 to +557
const opened = await openIfcFilesWithHandles();
if (!opened) return; // cancelled, unavailable, or picker failed

const files = opened.map(o => o.file);
recordRecentFiles(files.map(f => ({ name: f.name, size: f.size })));
void cacheFileBlobs(files);

if (opened.length === 1) {
// Single model: keep the handle so Refresh can re-read it from disk.
void loadFile(opened[0].file, { kind: 'primary' }, { sourceHandle: opened[0].handle });
} else {
// Multiple files mirror handleFileSelect's branching.
const allIfcx = files.every(f => f.name.endsWith('.ifcx'));
resetViewerState();
clearAllModels();
if (allIfcx) {
// IFCX layers compose into one shared store — no per-file handle.
void loadFederatedIfcx(files);
} else {
// Carry each file's handle so the whole federation stays refreshable.
void loadFilesSequentially(files, opened.map(o => o.handle));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does showOpenFilePicker with excludeAcceptAllOption: false allow users to choose files outside the declared accept types?

💡 Result:

Yes—setting excludeAcceptAllOption: false means the picker includes an “all files / no filter” option, so users can select files outside the declared accept types (when that “all files” option is available). [1][2] Specifically, the File System Access API defines excludeAcceptAllOption as controlling whether the picker “include[s] an option to not apply any file type filters.” By default it is false, so the “not apply any file type filters” option is present; setting it to true removes that option. [1][2] If you want to prevent selecting outside your declared types, set excludeAcceptAllOption: true. [1][2]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file(s) and search for the picker/filters used in the viewer code.
git ls-files 'apps/viewer/src/**/*' | rg 'MainToolbar\.tsx|openIfcFilesWithHandles|isSupportedModelFile|isIfcxModelFile|loadFile|loadFilesSequentially|handleFileSelect|showOpenFilePicker'

Repository: LTplus-AG/ifc-lite

Length of output: 206


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant source around the picker flow and helper functions.
files=$(git ls-files 'apps/viewer/src/**/*' | rg 'MainToolbar\.tsx|openIfcFilesWithHandles|isSupportedModelFile|isIfcxModelFile|loadFile|loadFilesSequentially|handleFileSelect')
printf '%s\n' "$files"

for f in $files; do
  echo "--- $f ---"
  wc -l "$f"
done

# Show AST outline for the main toolbar file if present.
if git ls-files 'apps/viewer/src/components/viewer/MainToolbar.tsx' >/dev/null 2>&1; then
  ast-grep outline apps/viewer/src/components/viewer/MainToolbar.tsx --view expanded
fi

Repository: LTplus-AG/ifc-lite

Length of output: 1650


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the picker helper and the toolbar branch around the reported lines.
sed -n '280,360p' apps/viewer/src/components/viewer/MainToolbar.tsx
printf '\n---\n'
sed -n '520,580p' apps/viewer/src/components/viewer/MainToolbar.tsx
printf '\n---\n'
rg -n "openIfcFilesWithHandles|showOpenFilePicker|excludeAcceptAllOption|isIfcxModelFile|loadFilesSequentially|loadFile\\(" apps/viewer/src/components/viewer/MainToolbar.tsx

Repository: LTplus-AG/ifc-lite

Length of output: 7702


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the surrounding implementation for the picker flow and model-file filtering.
sed -n '280,360p' apps/viewer/src/components/viewer/MainToolbar.tsx
printf '\n---\n'
sed -n '520,580p' apps/viewer/src/components/viewer/MainToolbar.tsx
printf '\n---\n'
rg -n "openIfcFilesWithHandles|showOpenFilePicker|excludeAcceptAllOption|isIfcxModelFile|loadFilesSequentially|loadFile\\(" apps/viewer/src/components/viewer/MainToolbar.tsx

Repository: LTplus-AG/ifc-lite

Length of output: 7702


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant implementation details for the file-picker branch.
sed -n '280,360p' apps/viewer/src/components/viewer/MainToolbar.tsx
printf '\n---\n'
sed -n '520,580p' apps/viewer/src/components/viewer/MainToolbar.tsx
printf '\n---\n'
rg -n "openIfcFilesWithHandles|showOpenFilePicker|excludeAcceptAllOption|isIfcxModelFile|loadFilesSequentially|loadFile\\(" apps/viewer/src/components/viewer/MainToolbar.tsx

Repository: LTplus-AG/ifc-lite

Length of output: 7702


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the picker flow and nearby helper functions.
sed -n '280,360p' apps/viewer/src/components/viewer/MainToolbar.tsx
printf '\n---\n'
sed -n '520,580p' apps/viewer/src/components/viewer/MainToolbar.tsx
printf '\n---\n'
rg -n "openIfcFilesWithHandles|showOpenFilePicker|excludeAcceptAllOption|isIfcxModelFile|loadFilesSequentially|loadFile\\(" apps/viewer/src/components/viewer/MainToolbar.tsx

Repository: LTplus-AG/ifc-lite

Length of output: 7702


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the File System Access helper to see what picker options it uses.
rg -n "function openIfcFilesWithHandles|const openIfcFilesWithHandles|showOpenFilePicker|excludeAcceptAllOption|accept:" apps/viewer/src

Repository: LTplus-AG/ifc-lite

Length of output: 1701


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the File System Access helper to see what picker options it uses.
rg -n "function openIfcFilesWithHandles|const openIfcFilesWithHandles|showOpenFilePicker|excludeAcceptAllOption|accept:" apps/viewer/src

Repository: LTplus-AG/ifc-lite

Length of output: 1701


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the File System Access helper to see what picker options it uses.
rg -n "function openIfcFilesWithHandles|const openIfcFilesWithHandles|showOpenFilePicker|excludeAcceptAllOption|accept:" apps/viewer/src

Repository: LTplus-AG/ifc-lite

Length of output: 1701


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' apps/viewer/src/services/file-system-access.ts
printf '\n---\n'
sed -n '430,510p' apps/viewer/src/components/viewer/MainToolbar.tsx

Repository: LTplus-AG/ifc-lite

Length of output: 9482


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' apps/viewer/src/services/file-system-access.ts
printf '\n---\n'
sed -n '430,510p' apps/viewer/src/components/viewer/MainToolbar.tsx

Repository: LTplus-AG/ifc-lite

Length of output: 9482


Filter unsupported picker results before loading. The File System Access picker still allows an “all files” choice here (excludeAcceptAllOption: false), so unsupported files can reach recordRecentFiles, cacheFileBlobs, and loadFile/loadFilesSequentially. Filter opened first, or make the picker accept-only, to match the <input> and Add Model paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/viewer/src/components/viewer/MainToolbar.tsx` around lines 536 - 557,
The file picker flow in MainToolbar lets unsupported “all files” selections
through, so sanitize the results before any downstream work. In the
openIfcFilesWithHandles/handleFileSelect path, filter opened to only the
supported IFC/IFCX files before calling recordRecentFiles, cacheFileBlobs,
loadFile, or loadFilesSequentially, or tighten the picker configuration to
accept only the same formats as the input/Add Model flows.

Comment on lines +583 to +618
const ok = reads.filter((r) => r.fresh) as { model: typeof targets[number]; fresh: File }[];
const failedNames = reads.filter((r) => !r.fresh).map((r) => `"${r.model.name}"`);

if (ok.length === 0) {
toast.error(`Couldn't re-read ${failedNames.join(', ')}. Files may have moved, been deleted, or access was denied.`);
return;
}

recordRecentFiles(ok.map((r) => ({ name: r.fresh.name, size: r.fresh.size })));
void cacheFileBlobs(ok.map((r) => r.fresh));

if (targets.length === 1) {
void loadFile(ok[0].fresh, { kind: 'primary' }, { sourceHandle: ok[0].model.sourceHandle });
} else {
// Rebuild the federation from fresh bytes, preserving id + order + state.
clearAllModels();
for (const r of ok) {
const reloadedId = await addModel(r.fresh, {
name: r.model.name,
modelId: r.model.id,
loadedAt: r.model.loadedAt,
visible: r.model.visible,
collapsed: r.model.collapsed,
sourceHandle: r.model.sourceHandle,
});
if (reloadedId && r.model.visible === false) {
useViewerStore.getState().setModelVisibility(r.model.id, false);
}
}
}

if (failedNames.length > 0) {
toast.error(`Refreshed ${ok.length}; couldn't re-read ${failedNames.join(', ')}.`);
} else {
toast.success(ok.length === 1 ? `Refreshed "${ok[0].fresh.name}"` : `Refreshed ${ok.length} models`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep Refresh atomic and report success only after reload completes.

On partial read failure, the multi-model branch clears the scene and reloads only ok, dropping failed models. The single-model branch also fires a success toast before loadFile finishes.

Suggested fix
-    const ok = reads.filter((r) => r.fresh) as { model: typeof targets[number]; fresh: File }[];
     const failedNames = reads.filter((r) => !r.fresh).map((r) => `"${r.model.name}"`);
 
-    if (ok.length === 0) {
+    if (failedNames.length > 0) {
       toast.error(`Couldn't re-read ${failedNames.join(', ')}. Files may have moved, been deleted, or access was denied.`);
       return;
     }
+    const ok = reads as { model: typeof targets[number]; fresh: File }[];
 
     recordRecentFiles(ok.map((r) => ({ name: r.fresh.name, size: r.fresh.size })));
     void cacheFileBlobs(ok.map((r) => r.fresh));
 
-    if (targets.length === 1) {
-      void loadFile(ok[0].fresh, { kind: 'primary' }, { sourceHandle: ok[0].model.sourceHandle });
-    } else {
-      // Rebuild the federation from fresh bytes, preserving id + order + state.
-      clearAllModels();
-      for (const r of ok) {
-        const reloadedId = await addModel(r.fresh, {
-          name: r.model.name,
-          modelId: r.model.id,
-          loadedAt: r.model.loadedAt,
-          visible: r.model.visible,
-          collapsed: r.model.collapsed,
-          sourceHandle: r.model.sourceHandle,
-        });
-        if (reloadedId && r.model.visible === false) {
-          useViewerStore.getState().setModelVisibility(r.model.id, false);
+    try {
+      if (targets.length === 1) {
+        await loadFile(ok[0].fresh, { kind: 'primary' }, { sourceHandle: ok[0].model.sourceHandle });
+      } else {
+        // Rebuild the federation from fresh bytes, preserving id + order + state.
+        clearAllModels();
+        for (const r of ok) {
+          const reloadedId = await addModel(r.fresh, {
+            name: r.model.name,
+            modelId: r.model.id,
+            loadedAt: r.model.loadedAt,
+            visible: r.model.visible,
+            collapsed: r.model.collapsed,
+            sourceHandle: r.model.sourceHandle,
+          });
+          if (reloadedId && r.model.visible === false) {
+            useViewerStore.getState().setModelVisibility(r.model.id, false);
+          }
         }
       }
+    } catch (err) {
+      console.error('[MainToolbar] Refresh reload failed', err);
+      toast.error(`Refresh failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
+      return;
     }
 
-    if (failedNames.length > 0) {
-      toast.error(`Refreshed ${ok.length}; couldn't re-read ${failedNames.join(', ')}.`);
-    } else {
-      toast.success(ok.length === 1 ? `Refreshed "${ok[0].fresh.name}"` : `Refreshed ${ok.length} models`);
-    }
+    toast.success(ok.length === 1 ? `Refreshed "${ok[0].fresh.name}"` : `Refreshed ${ok.length} models`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ok = reads.filter((r) => r.fresh) as { model: typeof targets[number]; fresh: File }[];
const failedNames = reads.filter((r) => !r.fresh).map((r) => `"${r.model.name}"`);
if (ok.length === 0) {
toast.error(`Couldn't re-read ${failedNames.join(', ')}. Files may have moved, been deleted, or access was denied.`);
return;
}
recordRecentFiles(ok.map((r) => ({ name: r.fresh.name, size: r.fresh.size })));
void cacheFileBlobs(ok.map((r) => r.fresh));
if (targets.length === 1) {
void loadFile(ok[0].fresh, { kind: 'primary' }, { sourceHandle: ok[0].model.sourceHandle });
} else {
// Rebuild the federation from fresh bytes, preserving id + order + state.
clearAllModels();
for (const r of ok) {
const reloadedId = await addModel(r.fresh, {
name: r.model.name,
modelId: r.model.id,
loadedAt: r.model.loadedAt,
visible: r.model.visible,
collapsed: r.model.collapsed,
sourceHandle: r.model.sourceHandle,
});
if (reloadedId && r.model.visible === false) {
useViewerStore.getState().setModelVisibility(r.model.id, false);
}
}
}
if (failedNames.length > 0) {
toast.error(`Refreshed ${ok.length}; couldn't re-read ${failedNames.join(', ')}.`);
} else {
toast.success(ok.length === 1 ? `Refreshed "${ok[0].fresh.name}"` : `Refreshed ${ok.length} models`);
}
const failedNames = reads.filter((r) => !r.fresh).map((r) => `"${r.model.name}"`);
if (failedNames.length > 0) {
toast.error(`Couldn't re-read ${failedNames.join(', ')}. Files may have moved, been deleted, or access was denied.`);
return;
}
const ok = reads as { model: typeof targets[number]; fresh: File }[];
recordRecentFiles(ok.map((r) => ({ name: r.fresh.name, size: r.fresh.size })));
void cacheFileBlobs(ok.map((r) => r.fresh));
try {
if (targets.length === 1) {
await loadFile(ok[0].fresh, { kind: 'primary' }, { sourceHandle: ok[0].model.sourceHandle });
} else {
// Rebuild the federation from fresh bytes, preserving id + order + state.
clearAllModels();
for (const r of ok) {
const reloadedId = await addModel(r.fresh, {
name: r.model.name,
modelId: r.model.id,
loadedAt: r.model.loadedAt,
visible: r.model.visible,
collapsed: r.model.collapsed,
sourceHandle: r.model.sourceHandle,
});
if (reloadedId && r.model.visible === false) {
useViewerStore.getState().setModelVisibility(r.model.id, false);
}
}
}
} catch (err) {
console.error('[MainToolbar] Refresh reload failed', err);
toast.error(`Refresh failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
return;
}
toast.success(ok.length === 1 ? `Refreshed "${ok[0].fresh.name}"` : `Refreshed ${ok.length} models`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/viewer/src/components/viewer/MainToolbar.tsx` around lines 583 - 618,
The refresh flow in MainToolbar’s reload handler is not atomic: the multi-model
path clears the viewer and reloads only the fresh entries, which drops failed
models, and the single-model path shows success before loadFile finishes. Update
the reload logic so the existing scene/state is only replaced after all reload
operations complete successfully, and keep failed models untouched when any read
fails. Use the existing symbols ok, failedNames, loadFile, addModel, and
clearAllModels to locate and adjust the refresh branch, and move the success
toast to fire only after the async reload work has fully completed.

Comment on lines +937 to +947
<Button
variant="ghost"
size="icon-sm"
onClick={(e) => {
(e.currentTarget as HTMLButtonElement).blur();
void handleRefresh();
}}
disabled={loading}
>
<RefreshCw className="h-4 w-4" />
</Button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add an accessible name to the icon-only Refresh button.

The new button has only an SVG icon and tooltip; give it an aria-label so assistive tech can identify the action.

Suggested fix
             <Button
               variant="ghost"
               size="icon-sm"
+              aria-label={models.size > 1 ? 'Refresh models from disk' : 'Refresh model from disk'}
               onClick={(e) => {
                 (e.currentTarget as HTMLButtonElement).blur();
                 void handleRefresh();
               }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button
variant="ghost"
size="icon-sm"
onClick={(e) => {
(e.currentTarget as HTMLButtonElement).blur();
void handleRefresh();
}}
disabled={loading}
>
<RefreshCw className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon-sm"
aria-label={models.size > 1 ? 'Refresh models from disk' : 'Refresh model from disk'}
onClick={(e) => {
(e.currentTarget as HTMLButtonElement).blur();
void handleRefresh();
}}
disabled={loading}
>
<RefreshCw className="h-4 w-4" />
</Button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/viewer/src/components/viewer/MainToolbar.tsx` around lines 937 - 947,
The icon-only Refresh button in MainToolbar lacks an accessible name, so add an
aria-label directly on the Button used with RefreshCw and handleRefresh. Keep
the tooltip if desired, but ensure assistive tech can identify the action from
the button itself by giving it a clear label such as “Refresh”.

Comment on lines +487 to +493
// Capture live handles synchronously — the DataTransferItemList is neutered
// once this handler returns, so this must run before any await.
const handlesPromise = handlesFromDataTransfer(e.dataTransfer);

// Filter to supported files (IFC, IFCX, GLB, point clouds)
const allDropped = Array.from(e.dataTransfer.files);
const supportedFiles = allDropped.filter(
f => f.name.endsWith('.ifc') || f.name.endsWith('.ifcx') || f.name.endsWith('.glb')
|| f.name.toLowerCase().endsWith('.las') || f.name.toLowerCase().endsWith('.laz') || f.name.toLowerCase().endsWith('.ply') || f.name.toLowerCase().endsWith('.pcd') || f.name.toLowerCase().endsWith('.e57') || f.name.toLowerCase().endsWith('.pts') || f.name.toLowerCase().endsWith('.xyz')
);
const supportedFiles = allDropped.filter(isSupportedFile);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve the captured handle/file pairing without a filename map.

handleByName collapses same-named files from different folders, so multiple dropped models can receive the same sourceHandle; Refresh would then re-read the wrong disk file.

Suggested fix
-    void handlesPromise.then((opened) => {
-      const handleByName = new Map((opened ?? []).map((o) => [o.file.name, o.handle]));
-      routeLoad(supportedFiles, supportedFiles.map((f) => handleByName.get(f.name)));
-    });
+    void handlesPromise
+      .then((opened) => {
+        const supportedOpened = (opened ?? []).filter((o) => isSupportedFile(o.file));
+        if (supportedOpened.length === supportedFiles.length) {
+          routeLoad(
+            supportedOpened.map((o) => o.file),
+            supportedOpened.map((o) => o.handle),
+          );
+          return;
+        }
+        routeLoad(supportedFiles);
+      })
+      .catch(() => routeLoad(supportedFiles));

Also applies to: 509-512

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/viewer/src/components/viewer/ViewportContainer.tsx` around lines 487 -
493, The drop handling in ViewportContainer is pairing files to handles by
filename, which collapses same-named files from different folders and can assign
the wrong sourceHandle. Update the logic around handlesFromDataTransfer,
supportedFiles, and the refresh path to preserve the original captured
handle/file ordering or direct pairing instead of using handleByName. Ensure
each dropped file keeps its own corresponding handle so Refresh rereads the
correct disk file.

Comment on lines +137 to +140
try {
return item.getAsFileSystemHandle?.() ?? Promise.resolve(null);
} catch {
return Promise.resolve(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Log the dropped-handle failure instead of swallowing it.

Line 139 silently discards getAsFileSystemHandle() errors, so drag/drop can lose refreshability with no signal about why. Please log or rethrow here instead of using an empty catch.

♻️ Proposed fix
     try {
       return item.getAsFileSystemHandle?.() ?? Promise.resolve(null);
-    } catch {
+    } catch (err) {
+      console.warn('[file-system-access] getAsFileSystemHandle failed', err);
       return Promise.resolve(null);
     }

As per coding guidelines, "No silent catch {} — log or rethrow errors".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
return item.getAsFileSystemHandle?.() ?? Promise.resolve(null);
} catch {
return Promise.resolve(null);
try {
return item.getAsFileSystemHandle?.() ?? Promise.resolve(null);
} catch (err) {
console.warn('[file-system-access] getAsFileSystemHandle failed', err);
return Promise.resolve(null);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/viewer/src/services/file-system-access.ts` around lines 137 - 140, The
dropped-handle fallback in getAsFileSystemHandle is silently swallowing
exceptions, which hides drag/drop failures. Update the try/catch in
file-system-access.ts so the catch block either logs the caught error with
enough context or rethrows it instead of returning a silent null, and keep the
existing getAsFileSystemHandle() path behavior unchanged when it succeeds.

Source: Coding guidelines

Comment thread apps/viewer/src/store/types.ts Outdated
Comment on lines +339 to +344
* Live File System Access handle captured when the model was opened via
* `showOpenFilePicker` (Chromium only). Unlike `sourceFile` — a frozen
* snapshot of the bytes at pick time — this can be re-read with `getFile()`
* to pull the current on-disk contents, powering the "Refresh" action
* (issue #1345). Absent for drag-drop, `<input type="file">`, cache-restored,
* and federated loads. Held in memory only; never serialized to cache.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the sourceHandle lifecycle comment.

Line 343 says this field is absent for federated loads, but useIfcFederation.loadFilesSequentially() now threads handles into federated models so the toolbar can refresh them. That comment should only exclude federated loads that were created without live handles.

♻️ Proposed fix
-   * (issue `#1345`). Absent for drag-drop, `<input type="file">`, cache-restored,
-   * and federated loads. Held in memory only; never serialized to cache.
+   * (issue `#1345`). Absent for `<input type="file">`, cache-restored, IFCX
+   * composition, and any load path that did not originate from a live FS Access
+   * handle. Held in memory only; never serialized to cache.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* Live File System Access handle captured when the model was opened via
* `showOpenFilePicker` (Chromium only). Unlike `sourceFile` a frozen
* snapshot of the bytes at pick time this can be re-read with `getFile()`
* to pull the current on-disk contents, powering the "Refresh" action
* (issue #1345). Absent for drag-drop, `<input type="file">`, cache-restored,
* and federated loads. Held in memory only; never serialized to cache.
* Live File System Access handle captured when the model was opened via
* `showOpenFilePicker` (Chromium only). Unlike `sourceFile` a frozen
* snapshot of the bytes at pick time this can be re-read with `getFile()`
* to pull the current on-disk contents, powering the "Refresh" action
* (issue `#1345`). Absent for `<input type="file">`, cache-restored, IFCX
* composition, and any load path that did not originate from a live FS Access
* handle. Held in memory only; never serialized to cache.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/viewer/src/store/types.ts` around lines 339 - 344, Update the
`sourceHandle` lifecycle documentation in `types.ts` so it matches the new
federation behavior: `useIfcFederation.loadFilesSequentially()` can now attach
live handles to federated models for refresh support. Adjust the comment on
`sourceHandle` to exclude only non-handle federated loads, while still noting it
is absent for drag-drop, `<input type="file">`, cache-restored, and any
federated load created without a live handle.

… txn)

cacheFileBlobs awaited file.arrayBuffer() between opening the IndexedDB
transaction and the store.put(). An IndexedDB transaction auto-commits the
moment control returns to the event loop with no pending request, so awaiting a
non-IDB promise inactivated it and the put() threw TransactionInactiveError,
which the empty catch swallowed. Nothing was ever cached, so every command
palette recent file missed the cache and fell back to re-picking (the file
dialog the user kept seeing).

Read all blobs up front, then write them in one synchronous burst inside the
transaction, and log instead of silently swallowing failures. Recent files now
load straight from cache; the picker fallback only fires for genuinely uncached
entries (evicted, oversized, or first seen before this fix).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/viewer/src/lib/recent-files.ts (1)

108-132: 🩺 Stability & Availability | 🟠 Major

Close the IndexedDB connection in finally. db.close() only runs on the success path; any failure after openDB() leaves the connection open. Wrap the transaction block in try/finally and close the DB there.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/viewer/src/lib/recent-files.ts` around lines 108 - 132, The IndexedDB
connection is only being closed on the success path in the recent-files
persistence flow. Update the logic in the openDB/transaction block inside the
recent-files helper so that db.close() always runs in a finally clause, even if
the transaction fails or an exception is thrown after opening the database. Keep
the transaction and eviction logic in the existing helper, but wrap it so the
connection cleanup is guaranteed regardless of outcome.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/viewer/src/lib/recent-files.ts`:
- Around line 95-105: `cacheFileBlobs()` is still loading every cacheable file
into memory before the transaction, which can exceed the intended cache
capacity; limit the staged reads to the cache size up front. Update the loop
that builds `records` so it only processes at most the first 5 eligible files
before calling `file.arrayBuffer()`, using the existing `MAX_CACHE_SIZE`/cache
logic in `cacheFileBlobs()` to skip oversized files and stop once the cache
limit is reached.

---

Outside diff comments:
In `@apps/viewer/src/lib/recent-files.ts`:
- Around line 108-132: The IndexedDB connection is only being closed on the
success path in the recent-files persistence flow. Update the logic in the
openDB/transaction block inside the recent-files helper so that db.close()
always runs in a finally clause, even if the transaction fails or an exception
is thrown after opening the database. Keep the transaction and eviction logic in
the existing helper, but wrap it so the connection cleanup is guaranteed
regardless of outcome.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: efd8ec61-bfb6-42ac-9bbc-5eaa7e883771

📥 Commits

Reviewing files that changed from the base of the PR and between 9740184 and 8475c8e.

📒 Files selected for processing (1)
  • apps/viewer/src/lib/recent-files.ts

Comment on lines +95 to +105
const records: { name: string; blob: ArrayBuffer; size: number; type: string; timestamp: number }[] = [];
for (const file of files) {
if (file.size > MAX_CACHE_SIZE) continue; // skip oversized files
records.push({
name: file.name,
blob: await file.arrayBuffer(),
size: file.size,
type: file.type,
timestamp: Date.now(),
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target file and inspect its structure before reading relevant sections.
git ls-files 'apps/viewer/src/lib/recent-files.ts'
wc -l apps/viewer/src/lib/recent-files.ts
ast-grep outline apps/viewer/src/lib/recent-files.ts --view expanded
sed -n '1,220p' apps/viewer/src/lib/recent-files.ts

Repository: LTplus-AG/ifc-lite

Length of output: 7300


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect references to the cache constants and the save path to understand actual behavior.
rg -n "MAX_CACHE_SIZE|MAX_CACHED_FILES|recent-files|arrayBuffer\\(" apps/viewer/src/lib apps/viewer/src -g '!**/*.map'

Repository: LTplus-AG/ifc-lite

Length of output: 5736


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the call sites to understand how many files can reach cacheFileBlobs().
sed -n '1,220p' apps/viewer/src/components/viewer/MainToolbar.tsx
printf '\n---\n'
sed -n '520,660p' apps/viewer/src/components/viewer/ViewportContainer.tsx
printf '\n---\n'
sed -n '1,180p' apps/viewer/src/components/viewer/MobileToolbar.tsx

Repository: LTplus-AG/ifc-lite

Length of output: 20018


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the file open flow to see whether cacheFileBlobs() receives small batches or arbitrary multi-file selections.
ast-grep outline apps/viewer/src/components/viewer/MainToolbar.tsx --view expanded
ast-grep outline apps/viewer/src/components/viewer/ViewportContainer.tsx --view expanded
ast-grep outline apps/viewer/src/components/viewer/MobileToolbar.tsx --view expanded

Repository: LTplus-AG/ifc-lite

Length of output: 2110


Bound staged blob reads to the cache capacity.

cacheFileBlobs() still reads every cacheable file with file.arrayBuffer() before the transaction, and multi-file opens can pass many files. That can spike peak memory far beyond the 5 entries you keep. Slice to the cache limit before reading bytes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/viewer/src/lib/recent-files.ts` around lines 95 - 105,
`cacheFileBlobs()` is still loading every cacheable file into memory before the
transaction, which can exceed the intended cache capacity; limit the staged
reads to the cache size up front. Update the loop that builds `records` so it
only processes at most the first 5 eligible files before calling
`file.arrayBuffer()`, using the existing `MAX_CACHE_SIZE`/cache logic in
`cacheFileBlobs()` to skip oversized files and stop once the cache limit is
reached.

…1345)

- Case-insensitive IFCX routing: add isIfcxModelFile so MODEL.IFCX takes the
  IFCX composition/overlay path (isSupportedModelFile already lowercases).
- Filter the picker's "all files" results in MainToolbar.handleOpenClick before
  loading, matching the <input> and Add Model paths.
- Await the single-model reload so Refresh reports success only after it lands.
- aria-label on the icon-only Refresh button.
- Drop handling: pair each file with its handle from the same dropped item
  instead of matching by filename (handles same-name collisions).
- Log getAsFileSystemHandle failures instead of swallowing them.
- Bound cacheFileBlobs staged reads to the cache capacity (MAX_CACHED_FILES).
- Correct the FederatedModel.sourceHandle lifecycle comment (drag-drop and
  federated loads now capture handles; <input>/cache/IFCX-layers do not).
@louistrue

Copy link
Copy Markdown
Collaborator Author

Addressed the CodeRabbit review in ac55152b:

  • Case-insensitive IFCX routing via isIfcxModelFile (so MODEL.IFCX composes correctly).
  • Filter the picker's "all files" results before loading in handleOpenClick.
  • Await the single-model reload so Refresh reports success only after it completes.
  • aria-label on the icon-only Refresh button.
  • Drop handling pairs file+handle from the same item (no filename matching).
  • Log getAsFileSystemHandle failures instead of swallowing them.
  • Bound cacheFileBlobs staged reads to the cache capacity.
  • Corrected the sourceHandle lifecycle comment.

Also fixed (separate finding, surfaced while testing on Chrome): cacheFileBlobs awaited file.arrayBuffer() mid-IndexedDB-transaction, which auto-committed the txn and made every put() throw TransactionInactiveError — so the recent-files cache never persisted and command-palette recents always re-picked. Reads are now staged before the write.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/viewer/src/components/viewer/ViewportContainer.tsx (1)

1131-1137: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don’t open the FS picker after an awaited cache miss.

Line 1137 calls handleOpenClick() after await getCachedFile(file). In Chromium, showOpenFilePicker needs transient user activation, which may be gone after the async IndexedDB lookup, so stale/uncached recent entries can fail to reopen. Preload cached names and decide synchronously whether to call handleOpenClick(), or show a “use Open” prompt after an async miss.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/viewer/src/components/viewer/ViewportContainer.tsx` around lines 1131 -
1137, The onClick flow in ViewportContainer currently awaits getCachedFile(file)
before calling handleOpenClick(), which can lose transient user activation
before the file picker opens. Update the recent-entry/open logic so the decision
to invoke handleOpenClick() is made synchronously from preloaded cache metadata,
or otherwise surface an explicit “use Open” prompt after an async miss instead
of opening the picker after the await. Keep the fix localized around
getCachedFile, loadFile, and handleOpenClick in ViewportContainer.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/viewer/src/components/viewer/ViewportContainer.tsx`:
- Around line 505-518: In ViewportContainer’s drag-drop handling, the current
handlesPromise.then logic treats any captured handle set as authoritative and
can ignore other supported dropped files when handle capture is partial or
rejected. Update the handling around supportedOpened, useHandles, and routeLoad
so you only prefer paired handles when all supported files were captured;
otherwise fall back to the plain supportedFiles list and ensure a rejected
handlesPromise still routes to the dropped files.

---

Outside diff comments:
In `@apps/viewer/src/components/viewer/ViewportContainer.tsx`:
- Around line 1131-1137: The onClick flow in ViewportContainer currently awaits
getCachedFile(file) before calling handleOpenClick(), which can lose transient
user activation before the file picker opens. Update the recent-entry/open logic
so the decision to invoke handleOpenClick() is made synchronously from preloaded
cache metadata, or otherwise surface an explicit “use Open” prompt after an
async miss instead of opening the picker after the await. Keep the fix localized
around getCachedFile, loadFile, and handleOpenClick in ViewportContainer.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ffad5955-721d-4252-b427-79628fbc78fa

📥 Commits

Reviewing files that changed from the base of the PR and between 8475c8e and ac55152.

📒 Files selected for processing (5)
  • apps/viewer/src/components/viewer/MainToolbar.tsx
  • apps/viewer/src/components/viewer/ViewportContainer.tsx
  • apps/viewer/src/lib/recent-files.ts
  • apps/viewer/src/services/file-system-access.ts
  • apps/viewer/src/store/types.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/viewer/src/store/types.ts
  • apps/viewer/src/lib/recent-files.ts
  • apps/viewer/src/services/file-system-access.ts
  • apps/viewer/src/components/viewer/MainToolbar.tsx

Comment on lines +505 to +518
void handlesPromise.then((opened) => {
// Prefer the handle-paired files (Chromium): each file + handle comes from
// the same dropped item, so no filename matching is needed. Fall back to
// the plain dropped files when no handles were captured (Firefox/Safari).
const supportedOpened = (opened ?? []).filter((o) => isSupportedFile(o.file));
const useHandles = supportedOpened.length > 0;
const files = useHandles ? supportedOpened.map((o) => o.file) : supportedFiles;
const handles = useHandles ? supportedOpened.map((o) => o.handle) : undefined;

if (hasModelsLoaded) {
// Models already loaded - add new files sequentially
loadFilesSequentially(supportedFiles);
} else if (supportedFiles.length === 1) {
// Single file, no models loaded - use loadFile
loadFile(supportedFiles[0]);
} else {
// Multiple files, no models loaded - use federation
resetViewerState();
clearAllModels();
loadFilesSequentially(supportedFiles);
}
}, [loadFile, loadFilesSequentially, resetViewerState, clearAllModels, webgpu.supported, hasModelsLoaded]);
recordRecentFiles(files.map((file) => ({ name: file.name, size: file.size })));
void cacheFileBlobs(files);
setRecentFiles(getRecentFiles().slice(0, 3));

routeLoad(files, handles);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fall back when drag handle capture is partial or rejected.

Line 510 treats any captured handle as authoritative, so a partial Chromium handle capture loads only that subset and silently drops the remaining supported files. Also add a rejection fallback so failed handle capture still loads the dropped files.

Proposed fix
-    void handlesPromise.then((opened) => {
+    void handlesPromise.then((opened) => {
       // Prefer the handle-paired files (Chromium): each file + handle comes from
       // the same dropped item, so no filename matching is needed. Fall back to
       // the plain dropped files when no handles were captured (Firefox/Safari).
       const supportedOpened = (opened ?? []).filter((o) => isSupportedFile(o.file));
-      const useHandles = supportedOpened.length > 0;
+      const useHandles = supportedOpened.length === supportedFiles.length;
       const files = useHandles ? supportedOpened.map((o) => o.file) : supportedFiles;
       const handles = useHandles ? supportedOpened.map((o) => o.handle) : undefined;
 
       recordRecentFiles(files.map((file) => ({ name: file.name, size: file.size })));
       void cacheFileBlobs(files);
       setRecentFiles(getRecentFiles().slice(0, 3));
 
       routeLoad(files, handles);
-    });
+    }).catch((err) => {
+      console.warn('[file-system-access] dropped file handle capture failed; loading files without handles', err);
+      recordRecentFiles(supportedFiles.map((file) => ({ name: file.name, size: file.size })));
+      void cacheFileBlobs(supportedFiles);
+      setRecentFiles(getRecentFiles().slice(0, 3));
+      routeLoad(supportedFiles);
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/viewer/src/components/viewer/ViewportContainer.tsx` around lines 505 -
518, In ViewportContainer’s drag-drop handling, the current handlesPromise.then
logic treats any captured handle set as authoritative and can ignore other
supported dropped files when handle capture is partial or rejected. Update the
handling around supportedOpened, useHandles, and routeLoad so you only prefer
paired handles when all supported files were captured; otherwise fall back to
the plain supportedFiles list and ensure a rejected handlesPromise still routes
to the dropped files.

@louistrue louistrue merged commit c7a6ddb into main Jun 24, 2026
14 checks passed
@louistrue louistrue deleted the feat/refresh-model-1345 branch June 24, 2026 16:00
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.

feature request | refresh model

1 participant