From b555156fa7d49b9bdec81a58ee649b3e1580d752 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Wed, 24 Dec 2025 08:04:57 -0800 Subject: [PATCH] Update TODO list and enhance job logging configuration - Added critical tasks for remote file access and updater functionality to the TODO list. - Updated job logging configuration to include an option for logging ephemeral jobs, improving logging flexibility. - Enhanced the job manager to conditionally log job events based on persistence settings, ensuring better tracking of job statuses. - Refactored the inspector component to remove unnecessary console logs, streamlining the codebase. - Improved the selection context to eliminate redundant logging during file synchronization, enhancing performance. --- TODO | 17 +- .../tauri/src-tauri/capabilities/default.json | 2 +- .../src-tauri/gen/schemas/capabilities.json | 2 +- apps/tauri/src-tauri/src/windows.rs | 25 ++- core/examples/indexing_demo.rs | 1 + core/examples/job_logging_test.rs | 1 + core/src/config/app_config.rs | 5 + core/src/infra/job/manager.rs | 76 ++++--- core/src/ops/indexing/handlers/persistent.rs | 188 +++++++++++++++++- core/src/ops/indexing/verify/action.rs | 59 +----- core/src/testing/integration_utils.rs | 1 + packages/interface/src/Inspector.tsx | 12 -- .../components/Explorer/SelectionContext.tsx | 1 - 13 files changed, 288 insertions(+), 102 deletions(-) diff --git a/TODO b/TODO index 54b4a8b4c..f8e12d091 100644 --- a/TODO +++ b/TODO @@ -23,11 +23,14 @@ Journey to v2.0.0-pre.1: ✔ Get all desktop release CI working @done(25-12-16 23:51) ☐ Ephemeral sidecars @critical +☐ Remote file access on demand @critical +☐ Open files with default app (cross platform) @critical ☐ Sometimes quick preview reporting file not found @today (happens in the second column of column view) +☐ Ensure updater and changelog are working @today +☐ Drop external items INTO spacedrive explorer @today +☐ Connection info on device panel (lan/relay) ✔ Grid view render bug, shows as column for split second on first render of results @done(25-12-22 07:49) -☐ Ensure updater and changelog are working -☐ Drop external items INTO spacedrive explorer -☐ Job sound: make copy sound a varient, not play also +✔ Job sound: make copy sound a varient, not play also @done(25-12-24 07:24) ☐ Run now button in Location Inspector doesn't work well @today ✔ Sidebar active based on Explorer path @today @done(25-12-20 07:59) ☐ Delete location UX improvement @@ -36,10 +39,9 @@ Journey to v2.0.0-pre.1: # It has an issue where it conflicts with the tab focus navigation and is out of order often ☐ Device owned data count mismatch reconciliation ☐ Synced directories seem to be missing relationships to form tree (sometimes) -☐ Drag selection area + command to add to selection +☐ Drag selection area + command to add to selection @today ✔ Back/forward button navigation not working @critical @done(25-12-22 05:43) # Investigate sync integrity -☐ Remote file access on demand @critical ✔ Refetch all queries when window focus @today @done(25-12-20 09:55) ✔ Fix job reactivity @today @done(25-12-21 06:14) # CLI reactivity works fine, but interface job manager often doesn't show up for jobs, generating thubmnails ALWAYS does, but speech, splat and indexing don't show in the UI @@ -60,9 +62,10 @@ Journey to v2.0.0-pre.1: ☐ Make default home folders SpaceItems ☐ Choose custom device name on mobile setup flow # Decide how this works in the context of sync, likely organize by device -☐ New Space Item resource event not working +✔ New Space Item resource event not working @done(25-12-24 07:25) ✔ Can't drag and drop onto space items to copy paste @done(25-12-16 23:51) ☐ Sometimes device shows as online even if its not +☐ Stale detection background discovery reindex @critical # Looks like cache state issue, even though the devices due ☐ Eject volume button ☐ Index mode not showing in location inspector @@ -82,10 +85,8 @@ Journey to v2.0.0-pre.1: ☐ History tab of the File Inspector is dummy data ☐ Improve copy/paste/move flow to actually validate before showing modal ☐ Fix three device sync -☐ Add box selections ☐ Refactor the CLI ☐ Improve inspector design -☐ Stale detection background discovery reindex # See INDEX-009 for stale detection ☐ RAW support ☐ Sign Windows build diff --git a/apps/tauri/src-tauri/capabilities/default.json b/apps/tauri/src-tauri/capabilities/default.json index c95f51076..cc4eee9f3 100644 --- a/apps/tauri/src-tauri/capabilities/default.json +++ b/apps/tauri/src-tauri/capabilities/default.json @@ -2,7 +2,7 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Default permissions for Spacedrive", - "windows": ["main"], + "windows": ["main", "inspector-*", "quick-preview-*", "settings-*", "job-manager"], "permissions": [ "core:default", "core:event:allow-listen", diff --git a/apps/tauri/src-tauri/gen/schemas/capabilities.json b/apps/tauri/src-tauri/gen/schemas/capabilities.json index 073ee28d4..d7dbaae36 100644 --- a/apps/tauri/src-tauri/gen/schemas/capabilities.json +++ b/apps/tauri/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"default":{"identifier":"default","description":"Default permissions for Spacedrive","local":true,"windows":["main"],"permissions":["core:default","core:event:allow-listen","core:event:allow-emit","core:window:allow-create","core:window:allow-close","core:window:allow-get-all-windows","core:window:allow-start-dragging","core:webview:allow-create-webview-window","core:path:default","dialog:allow-open","dialog:allow-save","shell:allow-open","fs:allow-home-read-recursive","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}} \ No newline at end of file +{"default":{"identifier":"default","description":"Default permissions for Spacedrive","local":true,"windows":["main","inspector-*","quick-preview-*","settings-*","job-manager"],"permissions":["core:default","core:event:allow-listen","core:event:allow-emit","core:window:allow-create","core:window:allow-close","core:window:allow-get-all-windows","core:window:allow-start-dragging","core:webview:allow-create-webview-window","core:path:default","dialog:allow-open","dialog:allow-save","shell:allow-open","fs:allow-home-read-recursive","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}} \ No newline at end of file diff --git a/apps/tauri/src-tauri/src/windows.rs b/apps/tauri/src-tauri/src/windows.rs index b07f2cdf2..c8e9bf033 100644 --- a/apps/tauri/src-tauri/src/windows.rs +++ b/apps/tauri/src-tauri/src/windows.rs @@ -159,9 +159,19 @@ impl SpacedriveWindow { (320.0, 400.0), true, true, // always on top - false, + true, // transparent for macOS styling )?; + // Apply macOS titlebar styling + #[cfg(target_os = "macos")] + { + if let Ok(ns_window) = window.ns_window() { + unsafe { + sd_desktop_macos::set_titlebar_style(&ns_window, false); + } + } + } + // Listen for window close to notify main window let app_handle = app.clone(); window.on_window_event(move |event| { @@ -372,7 +382,7 @@ fn create_window( always_on_top: bool, transparent: bool, ) -> Result { - let window = WebviewWindowBuilder::new(app, label, WebviewUrl::App(url.into())) + let mut builder = WebviewWindowBuilder::new(app, label, WebviewUrl::App(url.into())) .title(title) .inner_size(size.0, size.1) .min_inner_size(min_size.0, min_size.1) @@ -380,12 +390,21 @@ fn create_window( .decorations(decorations) .transparent(transparent) .always_on_top(always_on_top) - .center() + .center(); + + // Enable DevTools in dev mode + #[cfg(debug_assertions)] + { + builder = builder.devtools(true); + } + + let window = builder .build() .map_err(|e| format!("Failed to create window: {}", e))?; window.show().ok(); window.set_focus().ok(); + Ok(window) } diff --git a/core/examples/indexing_demo.rs b/core/examples/indexing_demo.rs index 7e2014611..826cef222 100644 --- a/core/examples/indexing_demo.rs +++ b/core/examples/indexing_demo.rs @@ -47,6 +47,7 @@ async fn main() -> Result<(), Box> { log_directory: "job_logs".to_string(), max_file_size: 10 * 1024 * 1024, // 10MB include_debug: true, // Include debug logs for full detail + log_ephemeral_jobs: false, }; config.save()?; diff --git a/core/examples/job_logging_test.rs b/core/examples/job_logging_test.rs index e35f9b14c..f89ef17a0 100644 --- a/core/examples/job_logging_test.rs +++ b/core/examples/job_logging_test.rs @@ -33,6 +33,7 @@ async fn main() -> Result<(), Box> { log_directory: "job_logs".to_string(), max_file_size: 10 * 1024 * 1024, include_debug: true, + log_ephemeral_jobs: false, }; config.save()?; diff --git a/core/src/config/app_config.rs b/core/src/config/app_config.rs index 86410ceaa..f48ca63c6 100644 --- a/core/src/config/app_config.rs +++ b/core/src/config/app_config.rs @@ -85,6 +85,10 @@ pub struct JobLoggingConfig { /// Whether to include debug logs pub include_debug: bool, + + /// Whether to create log files for ephemeral (non-persistent) jobs + #[serde(default)] + pub log_ephemeral_jobs: bool, } impl Default for JobLoggingConfig { @@ -94,6 +98,7 @@ impl Default for JobLoggingConfig { log_directory: "job_logs".to_string(), max_file_size: 10 * 1024 * 1024, // 10MB default include_debug: false, + log_ephemeral_jobs: false, } } } diff --git a/core/src/infra/job/manager.rs b/core/src/infra/job/manager.rs index 60137fd40..aa106a857 100644 --- a/core/src/infra/job/manager.rs +++ b/core/src/infra/job/manager.rs @@ -306,6 +306,20 @@ impl JobManager { // Create persistence completion channel let (persistence_complete_tx, persistence_complete_rx) = tokio::sync::oneshot::channel(); + // Only enable job file logging for persistent jobs (or if explicitly configured) + let job_logging_config = if should_persist + || self + .context + .job_logging_config + .as_ref() + .map(|c| c.log_ephemeral_jobs) + .unwrap_or(false) + { + self.context.job_logging_config.clone() + } else { + None + }; + // Create executor using the erased job let executor = erased_job.create_executor( job_id, @@ -321,7 +335,7 @@ impl JobManager { handle.output.clone(), networking, volume_manager, - self.context.job_logging_config.clone(), + job_logging_config, Some(library.job_logs_dir()), Some(persistence_complete_tx), ); @@ -367,18 +381,16 @@ impl JobManager { info!("Job {} status changed to: {:?}", job_id_clone, status); match status { JobStatus::Running => { - // Only emit events for persistent jobs - if should_persist { - event_bus.emit(Event::JobStarted { - job_id: job_id_clone.to_string(), - job_type: job_type_str.to_string(), - device_id, - }); - info!("Emitted JobStarted event for job {}", job_id_clone); - } + // Emit event for all jobs + event_bus.emit(Event::JobStarted { + job_id: job_id_clone.to_string(), + job_type: job_type_str.to_string(), + device_id, + }); + info!("Emitted JobStarted event for job {}", job_id_clone); } JobStatus::Completed => { - // Only emit events and trigger statistics for persistent jobs + // Emit completion event for all jobs if should_persist { // Get the final output from the handle before removing the job let output = { @@ -440,7 +452,7 @@ impl JobManager { break; } JobStatus::Failed => { - // Only emit events for persistent jobs + // Emit event for all jobs if should_persist { event_bus.emit(Event::JobFailed { job_id: job_id_clone.to_string(), @@ -455,7 +467,7 @@ impl JobManager { break; } JobStatus::Cancelled => { - // Only emit events for persistent jobs + // Emit event for all jobs if should_persist { event_bus.emit(Event::JobCancelled { job_id: job_id_clone.to_string(), @@ -684,6 +696,20 @@ impl JobManager { // Create persistence completion channel let (persistence_complete_tx, persistence_complete_rx) = tokio::sync::oneshot::channel(); + // Only enable job file logging for persistent jobs (or if explicitly configured) + let job_logging_config = if should_persist + || self + .context + .job_logging_config + .as_ref() + .map(|c| c.log_ephemeral_jobs) + .unwrap_or(false) + { + self.context.job_logging_config.clone() + } else { + None + }; + // Create executor let executor = JobExecutor::new( job, @@ -700,7 +726,7 @@ impl JobManager { handle.output.clone(), networking, volume_manager, - self.context.job_logging_config.clone(), + job_logging_config, Some(library.job_logs_dir()), Some(persistence_complete_tx), ); @@ -745,18 +771,16 @@ impl JobManager { info!("Job {} status changed to: {:?}", job_id_clone, status); match status { JobStatus::Running => { - // Only emit events for persistent jobs - if should_persist { - event_bus.emit(Event::JobStarted { - job_id: job_id_clone.to_string(), - job_type: job_type_str.to_string(), - device_id, - }); - info!("Emitted JobStarted event for job {}", job_id_clone); - } + // Emit event for all jobs + event_bus.emit(Event::JobStarted { + job_id: job_id_clone.to_string(), + job_type: job_type_str.to_string(), + device_id, + }); + info!("Emitted JobStarted event for job {}", job_id_clone); } JobStatus::Completed => { - // Only emit events and trigger statistics for persistent jobs + // Emit completion event for all jobs if should_persist { // Get the final output from the handle before removing the job let output = { @@ -818,7 +842,7 @@ impl JobManager { break; } JobStatus::Failed => { - // Only emit events for persistent jobs + // Emit event for all jobs if should_persist { event_bus.emit(Event::JobFailed { job_id: job_id_clone.to_string(), @@ -833,7 +857,7 @@ impl JobManager { break; } JobStatus::Cancelled => { - // Only emit events for persistent jobs + // Emit event for all jobs if should_persist { event_bus.emit(Event::JobCancelled { job_id: job_id_clone.to_string(), diff --git a/core/src/ops/indexing/handlers/persistent.rs b/core/src/ops/indexing/handlers/persistent.rs index 2766fa44d..ccc4eac91 100644 --- a/core/src/ops/indexing/handlers/persistent.rs +++ b/core/src/ops/indexing/handlers/persistent.rs @@ -136,7 +136,7 @@ impl PersistentEventHandler { "Handler is running, creating worker for location {}", location_id ); - self.ensure_worker(meta).await?; + self.ensure_worker(meta.clone()).await?; } else { debug!( "Handler not running yet, worker will be created on start for location {}", @@ -407,8 +407,16 @@ impl PersistentEventHandler { context: Arc, config: PersistentHandlerConfig, ) -> Result<()> { + use sd_fs_watcher::FsEventKind; + use std::collections::HashMap; + info!("Location worker started for {}", meta.id); + // Buffer for pending removes - maps inode to (path, timestamp, is_directory) + // These are Remove events that might be part of a rename operation. + let mut pending_removes: HashMap)> = HashMap::new(); + const RENAME_TIMEOUT: Duration = Duration::from_millis(1000); + while let Some(first_event) = rx.recv().await { // Start batching window let mut batch = vec![first_event]; @@ -432,12 +440,51 @@ impl PersistentEventHandler { meta.id ); + // Evict expired pending removes + let now = Instant::now(); + let expired: Vec = pending_removes + .iter() + .filter(|(_, (_, ts, _))| now.duration_since(*ts) > RENAME_TIMEOUT) + .map(|(inode, _)| *inode) + .collect(); + + // Process expired removes as actual deletions + let mut expired_events = Vec::new(); + for inode in expired { + if let Some((path, _, is_dir)) = pending_removes.remove(&inode) { + debug!( + "Evicting expired pending remove: {} (inode {})", + path.display(), + inode + ); + expired_events.push(FsEvent::remove(path)); + } + } + + // Detect renames by matching Remove+Create pairs using database inodes. + // On macOS, renames arrive as separate Remove and Create events across batches. + let batch = Self::detect_renames_from_db( + &context, + meta.library_id, + batch, + &mut pending_removes, + ) + .await; + + // Combine expired removes with processed batch + let mut final_batch = expired_events; + final_batch.extend(batch); + + if final_batch.is_empty() { + continue; + } + // Pass FsEvent batch directly to responder if let Err(e) = responder::apply_batch( &context, meta.library_id, meta.id, - batch, + final_batch, meta.rule_toggles, &meta.root_path, None, // volume_backend - TODO: resolve from context @@ -451,6 +498,143 @@ impl PersistentEventHandler { info!("Location worker stopped for {}", meta.id); Ok(()) } + + /// Detect renames by matching Remove+Create pairs using database inodes. + /// + /// Remove events with inodes are buffered in `pending_removes`. Create events + /// check against this buffer to detect renames. This handles the case where + /// Remove and Create arrive in separate batches (common on macOS FSEvents). + async fn detect_renames_from_db( + context: &Arc, + library_id: Uuid, + events: Vec, + pending_removes: &mut std::collections::HashMap)>, + ) -> Vec { + use sd_fs_watcher::FsEventKind; + + let Some(library) = context.get_library(library_id).await else { + return events; + }; + let db = library.db().conn(); + + let mut result: Vec = Vec::new(); + + for event in events { + match &event.kind { + FsEventKind::Remove => { + // Query database for inode + let inode = Self::get_inode_from_db(db, &event.path).await; + if let Some(inode) = inode { + debug!( + "Buffering Remove event: {} with inode {} for potential rename", + event.path.display(), + inode + ); + // Buffer for potential rename detection + pending_removes + .insert(inode, (event.path, Instant::now(), event.is_directory)); + } else { + // No inode in DB, emit as regular Remove + result.push(event); + } + } + FsEventKind::Create => { + // Get inode from filesystem + let inode = Self::get_inode_from_fs(&event.path).await; + + if let Some(inode) = inode { + // Check if this matches a pending remove + if let Some((old_path, _, is_dir)) = pending_removes.remove(&inode) { + // Found a match - emit Rename + info!( + "Detected rename via database inode {}: {} -> {}", + inode, + old_path.display(), + event.path.display() + ); + let rename_event = if let Some(is_dir) = is_dir.or(event.is_directory) { + FsEvent::rename_with_dir_flag(old_path, event.path, is_dir) + } else { + FsEvent::rename(old_path, event.path) + }; + result.push(rename_event); + continue; + } + } + // No matching pending remove, emit as regular Create + result.push(event); + } + _ => { + result.push(event); + } + } + } + + result + } + + /// Get the inode for a path from the database. + async fn get_inode_from_db( + db: &sea_orm::DatabaseConnection, + path: &std::path::Path, + ) -> Option { + use crate::infra::db::entities::{directory_paths, entry}; + use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; + + // First try as directory (lookup via directory_paths) + let path_str = path.to_string_lossy().to_string(); + if let Ok(Some(dir_record)) = directory_paths::Entity::find() + .filter(directory_paths::Column::Path.eq(&path_str)) + .one(db) + .await + { + if let Ok(Some(entry_record)) = + entry::Entity::find_by_id(dir_record.entry_id).one(db).await + { + if let Some(inode) = entry_record.inode { + return Some(inode as u64); + } + } + } + + // Try as file (lookup via parent directory + name) + let parent = path.parent()?; + let name = path.file_stem()?.to_str()?; + let ext = path.extension().and_then(|e| e.to_str()); + + let parent_str = parent.to_string_lossy().to_string(); + let parent_dir = directory_paths::Entity::find() + .filter(directory_paths::Column::Path.eq(&parent_str)) + .one(db) + .await + .ok()??; + + let mut query = entry::Entity::find() + .filter(entry::Column::ParentId.eq(parent_dir.entry_id)) + .filter(entry::Column::Name.eq(name)); + + if let Some(e) = ext { + query = query.filter(entry::Column::Extension.eq(e.to_lowercase())); + } else { + query = query.filter(entry::Column::Extension.is_null()); + } + + let entry_record = query.one(db).await.ok()??; + entry_record.inode.map(|i| i as u64) + } + + /// Get the inode for a path from the filesystem. + async fn get_inode_from_fs(path: &std::path::Path) -> Option { + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + tokio::fs::metadata(path).await.ok().map(|m| m.ino()) + } + #[cfg(not(unix))] + { + None + } + } } #[cfg(test)] diff --git a/core/src/ops/indexing/verify/action.rs b/core/src/ops/indexing/verify/action.rs index 2e0cd9631..21034b06e 100644 --- a/core/src/ops/indexing/verify/action.rs +++ b/core/src/ops/indexing/verify/action.rs @@ -58,7 +58,7 @@ impl LibraryAction for IndexVerifyAction { ); // Step 1: Scan filesystem to get current state - let fs_entries = self.run_ephemeral_index(&library, &context, &path).await?; + let fs_entries = self.run_ephemeral_index(&library, &path).await?; // Step 2: Query database for existing entries in this path let db_entries = self.query_database_entries(&library, &path).await?; @@ -95,7 +95,6 @@ impl IndexVerifyAction { async fn run_ephemeral_index( &self, library: &Arc, - context: &Arc, path: &Path, ) -> Result, ActionError> { @@ -109,9 +108,6 @@ impl IndexVerifyAction { ActionError::from(std::io::Error::new(std::io::ErrorKind::Other, e)) })?)); - // Subscribe to job events before dispatching - let mut event_subscriber = context.events.subscribe(); - // Create indexer job config for ephemeral scanning let config = IndexerJobConfig { location_id: None, // Ephemeral - no location @@ -133,55 +129,22 @@ impl IndexVerifyAction { ActionError::Internal(format!("Failed to dispatch indexer job: {}", e)) })?; - let job_id = job_handle.id().to_string(); + let job_id = job_handle.id(); tracing::debug!( "Waiting for ephemeral indexer job {} to complete...", job_id ); - // Listen for job completion events - loop { - match event_subscriber.recv().await { - Ok(event) => match event { - crate::infra::event::Event::JobCompleted { - job_id: completed_id, - .. - } if completed_id == job_id => { - tracing::debug!("Ephemeral indexer job {} completed", job_id); - break; - } - crate::infra::event::Event::JobFailed { - job_id: failed_id, - error, - .. - } if failed_id == job_id => { - return Err(ActionError::Internal(format!( - "Ephemeral indexer job failed: {}", - error - ))); - } - crate::infra::event::Event::JobCancelled { - job_id: cancelled_id, - .. - } if cancelled_id == job_id => { - return Err(ActionError::Internal( - "Ephemeral indexer job was cancelled".to_string(), - )); - } - _ => { - // Not our job event, keep listening - } - }, - Err(e) => { - return Err(ActionError::Internal(format!( - "Failed to receive job event: {}", - e - ))); - } - } - } + // Wait for the job to complete using the handle's built-in wait mechanism + job_handle + .wait() + .await + .map_err(|e| ActionError::Internal(format!("Ephemeral indexer job failed: {}", e)))?; - tracing::debug!("Ephemeral indexer job completed, extracting results"); + tracing::debug!( + "Ephemeral indexer job {} completed, extracting results", + job_id + ); // Extract the results from our shared ephemeral index let entries = { diff --git a/core/src/testing/integration_utils.rs b/core/src/testing/integration_utils.rs index 78f1c4609..1b1a011cf 100644 --- a/core/src/testing/integration_utils.rs +++ b/core/src/testing/integration_utils.rs @@ -211,6 +211,7 @@ impl TestConfigBuilder { log_directory: "job_logs".to_string(), max_file_size: 10 * 1024 * 1024, // 10MB include_debug: false, + log_ephemeral_jobs: false, }, services: ServiceConfig { networking_enabled: self.networking_enabled, diff --git a/packages/interface/src/Inspector.tsx b/packages/interface/src/Inspector.tsx index 0aeea5d28..5cef05445 100644 --- a/packages/interface/src/Inspector.tsx +++ b/packages/interface/src/Inspector.tsx @@ -152,14 +152,12 @@ export function PopoutInspector() { platform.onSelectedFilesChanged((fileIds) => { if (mounted) { - console.log("[PopoutInspector] Received selection change:", fileIds); setSelectedFileIds(fileIds); } }).then((unlistenFn) => { if (mounted) { unlisten = unlistenFn; } else { - // Component unmounted before listener was set up, clean up immediately unlistenFn(); } }).catch((err) => { @@ -175,11 +173,6 @@ export function PopoutInspector() { // Fetch the first selected file const firstFileId = selectedFileIds[0] || null; - console.log("[PopoutInspector] Current state:", { - selectedFileIds, - firstFileId, - }); - const { data: file, isLoading } = useLibraryQuery( { type: "files.by_id", @@ -190,11 +183,6 @@ export function PopoutInspector() { } ); - console.log("[PopoutInspector] Query result:", { - file: file?.id, - isLoading, - }); - // Compute inspector variant const variant: InspectorVariant = file ? { type: "file", file } diff --git a/packages/interface/src/components/Explorer/SelectionContext.tsx b/packages/interface/src/components/Explorer/SelectionContext.tsx index f82215744..db472cfa8 100644 --- a/packages/interface/src/components/Explorer/SelectionContext.tsx +++ b/packages/interface/src/components/Explorer/SelectionContext.tsx @@ -30,7 +30,6 @@ export function SelectionProvider({ children }: { children: ReactNode }) { const fileIds = selectedFiles.map((f) => f.id); if (platform.setSelectedFileIds) { - console.log("[SelectionContext] Syncing selected files to platform:", fileIds); platform.setSelectedFileIds(fileIds).catch((err) => { console.error("Failed to sync selected files to platform:", err); });