The file browser is the densest browser-side product surface in Sambee. It combines navigation, selection, editing entry points, viewer launch, keyboard behavior, and real-time directory refresh.
Core Component Split
The implementation is deliberately split between shared page logic and per-pane logic.
| Area | Responsibility |
|---|---|
pages/FileBrowser.tsx | owns shared browser-level concerns such as active pane routing, URL sync, WebSocket lifecycle, and global shortcut registration |
pages/FileBrowser/useFileBrowserPane.ts | owns per-pane state such as connection and path, selection, sorting, filtering, dialogs, viewer state, incremental search, and directory refresh handling |
pages/FileBrowser/routing.ts | parses and serializes typed directory, viewer, editor, and settings locations |
pages/FileBrowser/useWorkspaceNavigation.ts | resolves viewer targets and owns workspace navigation and safe overlay return entries |
hooks/useRoutedViewerEditing.tsx | connects the active editor’s existing confirmation dialog to React Router navigation blocking |
pages/FileBrowser/FileBrowserPane.tsx | renders one pane’s UI |
components/FileBrowser/CopyMoveDialog.tsx | manages copy and move confirmation flow |
config/keyboardShortcuts.ts | central shortcut definitions used across the browser |
This split matters because two visible panes still need one coherent browser workflow.
The parent-versus-hook split is intentional:
FileBrowser.tsxowns shared state such as the connections list, active-pane routing, responsive layout, WebSocket connection lifecycle, and browser-level shortcut registrationuseFileBrowserPane.tsowns per-pane state such as file loading, sort and filter state, viewer and dialog state, focus handling, multi-select state, and pane-specific directory-change reactions
Pane Model
Sambee supports one-pane and two-pane browsing, but the two-pane implementation is intentional rather than conditional duplication.
- the left pane is always active in routing terms
- the right pane is still instantiated even when not visible, which keeps the hook model stable
- the active pane decides which pane keyboard shortcuts and toolbar commands target
- dual-pane mode is desktop-first and collapses to single-pane on compact layouts
- dual-pane preference and active-pane preference are persisted so the browser can restore the user’s working style
- the viewer opens as a fullscreen overlay from whichever pane launched it rather than being constrained to one pane region
Visual state also matters:
- the active pane gets the stronger emphasis treatment
- the inactive pane is visually de-emphasized rather than treated as a separate route or different screen
Real-Time Directory Refresh
Directory Navigation Failures
Physical directory navigation has a pending target and a displayed location. Load the target through the content provider before committing its connection, path, rows, and route. A failed navigation keeps the previous displayed location, selection, and scroll state. Request cancellation and location guards reject late responses, including responses from automatic parent recovery.
Only successfully loaded physical directories become file-list subscription targets. Missing initial locations and deleted current directories recover to an accessible parent, or clear stale rows and keep recovery controls visible. Canonical recovery replaces the affected route; it does not use browser Back or change the other pane.
The SMB storage boundary translates named missing-name, missing-parent, access-denied, and wrong-type statuses into typed storage errors. Directory listing returns a structured API code and a safe message. Only a confirmed directory_not_found result permits stale history and directory-index removal. A connection-level HTTP 404 is not proof that a folder was deleted.
Companion directory listings use the same directory_not_found, directory_access_denied, not_a_directory, and directory_unavailable codes. Companion responses put code beside the safe detail string; SMB responses put the code inside detail. The pane accepts both formats. Local path validation and directory reads use the directory error mapper, while unavailable drive roots remain separate from missing folders. Do not remove folder history for an unavailable drive. The existing drive-boundary and path-traversal checks still apply.
components/OperationNotifications.tsx provides one viewport-centered notification host for both panes, viewers, and page operations. OperationNotificationProvider collects active notices from their existing owners. It does not own navigation, history, editor text, or locks. The host moves inside the top open dialog for modal focus access. It does not change file-row geometry.
Errors precede warnings and information. Equal-priority sources retain registration order. Only one toast is visible; Next notification exposes other active failures without dismissing the current failure. There is no historical event queue. Pending notices come only from mounted owners’ current state, and resolved or unmounted sources retire their notices. Optional failure keys deduplicate one failure across owners without merging different targets.
Toasts have a finite duration by default. NOTICE_DURATION_MS defines 6 seconds for information and completed results, 10 seconds for browser failures and service status, and 15 seconds for editor errors and safety warnings. Folder-recovery results use 6 seconds even though their severity is warning. Retry and Settings actions do not make a toast persistent.
Set persistent: true only for an active upload or download with Cancel, share preparation with Cancel, or a ready-to-share decision with Share and Cancel. Sharing after confirmation uses the 6-second information timeout. These exceptions use the existing toast controls; they do not add another progress or status surface. Unsaved-change, conflict, password, and draft-recovery dialogs keep their existing decision behavior.
A timer starts when its toast becomes visible, pauses during hover or keyboard focus, and does not restart for routine message updates. Expiry hides the toast without calling its onDismiss callback or its source’s dismissal callback. Only completed informational results use onExpire to clear their display flags, so a later identical result can appear again. Expiry must not clear unresolved errors, cancel operations, discard drafts, or mark pairing or monitoring as healthy. Occurrence identities allow a new attempt to show the same error after expiry or dismissal. Retry retains its original target rather than following the active pane or selected row.
Target-specific notices show the message first, followed by smaller, regular-weight context on a separate line. formatNoticeTarget captures the connection name and full connection-relative path, including the failed file or folder. The root is /. Render this context with DialogIdentifierDisplay in its text appearance. It uses the shared grapheme-aware abbreviatePath algorithm to fit the visual path on one line, preserving the connection prefix and useful trailing segments where space permits. It recalculates when the available width changes. The complete value remains in the accessible name and a hover-only tooltip; shortening must not change failure identity or the Retry target. Bidirectional text is isolated. These paths do not expose the server’s physical filesystem root. Global notices without a target have no context line. Known missing-file and missing-folder errors use short toast summaries without repeating the target name; inline errors and dialogs keep their detailed messages. Other error text is not rewritten.
Each failure captures its own target rather than following the active pane or selected row. A recovery result identifies the original missing path, even after an accessible parent loads. If a parent fails during recovery, its failure notice identifies that parent instead. Refresh always reloads the displayed folder; it does not replay a failed navigation or native launch. Native Retry uses the item resolved by the original attempt. Backend and Companion status recovery remains available from the existing header warning menu after notification expiry or dismissal, without covering bottom file actions. The existing monitoring control and initial-folder error state also remain available. No additional inline editor warning or progress control is added when a toast expires.
An unavailable initial directory uses an unframed error state in the list area, with the original location and recovery actions. A usable previous listing, including an empty folder, remains visible after a failed target request. Loading and onboarding have their own content states. Stopped automatic updates retain a compact recovery control after toast dismissal. Backend outages have one transition notice and a persistent status control; a brief reconnection does not start another notice cycle. Expected monitor startup failures do not produce duplicate tracebacks.
Automatic parent recovery skips missing, inaccessible, and wrong-type parents within the configured root. It stops on cancellation, a newer request, pairing failure, timeout, or unavailable storage. A directory-only route replacement retains the active viewer identity and selection revision. It must not reload content, unmount a dirty editor, or acquire another edit lock.
Subscription Acknowledgements
Both WebSocket transports attach a request ID to each directory subscription. Requested and acknowledged subscriptions are separate. A reply is accepted only for the current socket, request ID, and requested location. Unsubscribe and socket cleanup retire that state and its bounded acknowledgement timer.
Directory-change events are accepted only from the socket that acknowledged the current subscription. Events before acknowledgement, after failure, or after retirement cannot refresh the pane. A successful directory refresh alone does not clear a monitor failure; the matching acknowledgement clears it.
Subscription errors include the connection, path, request ID, and a safe code. Confirmed absence invalidates that exact location through the pane refresh scheduler; the fresh listing decides whether parent recovery is needed. Other subscription failures retain valid rows and show persistent Retry controls. They do not prove deletion or start an automatic subscribe/reload loop. A running monitor invalidates its deleted directory before retiring its watcher state.
Refresh Scheduling
Each pane handles directory-change notifications independently. An automatic refresh keeps an already loaded physical-directory listing mounted, including an empty listing, so unchanged rows retain selection, keyboard focus, and scroll position. Initial loading and navigation without a usable snapshot still show loading feedback.
Notifications don’t cancel an active listing request. Instead, the pane remembers that another refresh is needed and starts one follow-up request after the active request finishes. Automatic refreshes start at most once per second. Further notifications don’t move an existing deadline, so continuous writes still produce periodic updates. Navigation and unmount cancel queued work.
A listing with unprocessed change notifications can remain visible, but it isn’t cached as fresh. Deferred link-target metadata only updates cache entries that are still valid, so cancelling a follow-up refresh can’t make an outdated snapshot reusable on return.
SMB file-list subscriptions watch immediate children rather than the whole subtree. The separate backend directory-search cache retains recursive directory-name monitoring. See SMB Change Notification and Directory Freshness for watcher ownership and lifecycle.
URL-Driven Workspace Navigation
The URL selects the directory, pane layout, viewer, editing intent, and settings view. The file browser stays mounted under overlays. Loaded content, drafts, locks, selection, scroll, search panels, and zoom remain runtime state.
| Part | Meaning |
|---|---|
/browse/smb/<slug>/<path> | Primary SMB pane |
/browse/local/<drive-id>/<path> | Primary companion-drive pane |
p2=<kind>/<target>/<path> | Right pane, enabled when present |
active=2 | Right pane is active |
file=<kind>/<target>/<path> | Viewer target, independent of the directory and active pane |
viewer=image|markdown|pdf|text | Requested browser viewer |
mode=edit | Editing intent for text or Markdown |
pane=2 | Right pane owns the viewer; requires a right-pane target |
virtual=<JSON array> | Archive member descriptor: provider ID, source archive path, member path |
settings=main|<category> | Settings main view or category from the shared category registry |
For example:
/browse/smb/my-server/documents
/browse/smb/my-server/documents?p2=smb%2Fother-server%2Fphotos&active=2
/browse/smb/my-server/documents?file=smb%2Fmy-server%2Fdocuments%2Fnotes.md&viewer=markdown
/browse/smb/my-server/documents?file=smb%2Fmy-server%2Fdocuments%2Fnotes.md&viewer=markdown&mode=edit
/browse/smb/my-server/documents?settings=account
Use the shared parser/serializer and URLSearchParams, not concatenated query strings. File descriptors encode target IDs and individual path segments before query serialization. The archive provider is currently zip. Runtime session IDs, file contents, MIME guesses, gallery arrays, credentials, and lock capabilities never belong in navigation state.
Navigation and Dismissal
Opening or closing a viewer, entering or leaving editing, changing gallery images, opening or closing settings, and changing settings categories push history entries. Repeating the same location does nothing. Pane-focus updates, invalid-descriptor recovery, failed edit acquisition, and canonicalization replace the current entry. Unrelated query parameters and authentication fragments are preserved.
Back and Forward follow actual history without adding entries. Explicit Close pushes the current workspace with the dismissed layer removed, including for direct links and refreshed viewers. For example, opening a directory, opening an image, and closing it produces directory, image, and directory entries; Back reopens the image before returning to the earlier directory entry. A new action after Back discards the old Forward branch, following normal browser behavior. Close doesn’t depend on opening-entry metadata or history.length. Settings back controls traverse observed history; without an observed predecessor they replace the URL with an in-app parent.
Desktop settings and the mobile drawer are controlled by the same category descriptor. settings=main displays the desktop default category or the mobile category list. Resizing changes presentation without navigation. Standalone /settings/... routes and their aliases use the same category content and authorization checks. User Management owns its query keys through React Router, merging them with workspace parameters.
Direct Links and Recovery
Viewer links resolve metadata through the storage registry or content provider, without simulated file-row clicks. Resolution is cancelled when its identity changes. Missing resources show Retry and Close actions. A viewer URL never falls back to opening a native application.
The incoming URL always wins over a browser recovery snapshot. Snapshot hydration matches each pane’s resource identity, clears legacy viewer state, and ignores unmatched panes. A bare /browse uses normal default navigation. Captured snapshots use the current canonical workspace URL and contain neither callback fragments nor edit-session tokens.
Physical File Failures
utils/apiErrors.ts classifies nested SMB codes and top-level Companion codes. item_not_found means confirmed physical absence. Metadata also serves directories; not_a_file is reserved for operations that require a regular file. Missing connections, unavailable drive roots, archive members, pairing failures, and expired tokens remain separate. An HTTP 404 alone is not evidence that a physical file was deleted.
Physical viewer reads decode a structured binary error only when its content type is JSON and its size is bounded. An ambiguous image or PDF decode failure can make one guarded metadata request for the current resource. Existing metadata does not turn a decode failure into a deletion. Abort signals, viewer identities, and source URLs reject stale results.
services/fileRecoveryEvents.ts carries confirmed absence to the existing pane refresh owner. Each pane invalidates the physical parent cache; only a pane still displaying that connection and parent schedules a refresh. This event does not own notices, history deletion, or editor recovery. Activation errors use pane notices. Viewer errors use the existing viewer controls, without an additional toast.
Recent-file validation removes only the identified record for confirmed absence, a non-file target, an invalid stored target, or an authoritatively deleted stored connection. Item-level and connection-level access denial, generic launch failure, timeout, cancellation, pairing failure, unavailable storage, and unknown errors retain history. Publish the cross-tab change event only after actual cleanup. Recent-directory cleanup still requires an explicit missing-directory result.
Browser editor writes use ordinary create-or-overwrite behavior with the active editor lease. Save can recreate a deleted file at its original path. New lock acquisition still requires an existing regular file and must not create or truncate it. SMB requires existing parent directories; Companion uploads retain their normal parent-directory creation behavior within the available drive. A normal write can overwrite changes made at the same path; it does not compare the file with the editor baseline before writing. A failed save keeps the draft and dirty state. After a successful upload, a failed metadata refresh leaves the upload successful with an unknown modified timestamp. It must not cause an automatic repeat of the write. Native URI dispatch is not a launch acknowledgement. Do not infer later application success or failure from that handoff.
Viewer Loading and Transitions
Loading feedback belongs to the selected image, PDF, text, or Markdown viewer. Workspace metadata resolution and dynamic component imports do not show an intermediate full-screen loading dialog. On an initial opening, the browser remains visible until metadata and the viewer component are available; the viewer then displays its existing content-loading feedback.
One viewer component is mounted at a time. Metadata resolution and component imports can retain the displayed resource while controls and shortcuts are suspended; Escape remains available. Once the selected component and metadata are available, that viewer owns content loading and errors. There is no hidden candidate viewer or content-readiness promotion. The editor leave guard remains the sole owner of dirty-navigation decisions.
Image-gallery navigation updates the current index in the same viewer instance and publishes it to the route. The gallery keeps its local bounded cache and preloading; no image-byte cache is shared between viewer instances. An image is ready only when its decoded source exists. A failed image uses the selected image’s error controls without displaying the previous image under a new filename. Explicit Retry invalidates failed local bytes and makes a fresh request. Encrypted PDFs retain the standard responsive password dialog.
Viewer content failures use the selected viewer’s error state with Retry and Close, not a generic replacement toast. Individual loaders reject late results after a newer selection or close. There is no guarantee that a previous document remains displayed until the next document’s content is ready.
One dynamic viewer host serves both panes. An explicit viewer ID, not an image’s changing MIME type, controls component loading. Text and Markdown still reset resource-local state when the actual file changes; mode-only and settings-only navigation do not restart the viewer.
Editing and Router Integrity
The app uses a data router (createBrowserRouter and RouterProvider) for supported useBlocker behavior. One guard owner delegates resource-leaving transitions to the active text or Markdown editor when its draft is dirty, a save is pending, or a session is being acquired. Clean editor Back and Forward navigate directly, avoiding the history reversal required by a blocked browser navigation. Cancel keeps the draft and lock; successful Save or Discard continues the original transition; failed Save keeps the editor open. Changes that retain the same edited resource and mode don’t discard its draft. Dirty drafts also register beforeunload protection.
Edit bookmarks and history navigation back into editing acquire a fresh edit session after content and permission checks. Navigating away during acquisition invalidates the attempt and releases any late lock. Leaving editing releases its session once. The outgoing editor is read-only during cleanup, and re-entry waits for that cleanup to finish so an old release can’t clear a new session. History restores navigation intent, not old file contents, discarded drafts, or saved locks. An edit URL doesn’t grant permission.
Full page reloads, including F5 and the update prompt, can resume the same edit lock. The shared storage edit-session layer keeps lock credentials separately in tab-scoped session storage, keyed by user, connection, and file. On reload it validates those credentials with a heartbeat before reusing them. Only a structured lost-lock response permits fresh acquisition; network and permission failures don’t trigger replacement. Credentials follow replacement locks and are removed on successful release or logout. Unsaved text still uses the existing draft-recovery flow.
Recovery requires session storage and the browser Web Locks API, available in secure contexts such as HTTPS and localhost. An exclusive Web Lock prevents another tab from using copied recovery credentials, even if that tab reloads. Without those capabilities, editing still works, but a reload may need to wait for the abandoned server lock to expire. Fresh navigations and history entries don’t resume saved credentials, and no credentials appear in the URL or workspace recovery snapshot.
Use router navigation for all mounted-route URL writes, including authentication fragment cleanup. useRemoveLocationFragment replaces the entry, removes only consumed fragment keys, and preserves the full query and application state. Never clear router-managed history state with replaceState(null, ...): doing so can disable POP blocking.
Keyboard and Command Model
Keyboard behavior is part of the product contract, not just convenience.
- pane switching, selection, copy, and move use centralized shortcut definitions
- the quick bar supports smart navigation, command mode, and pane-local current-directory filtering
- browser defaults that conflict with the product’s navigation model are overridden intentionally
- shortcuts are disabled when dialogs or viewer states would make them unsafe
Changes here can easily affect power-user workflows, accessibility, and focus management all at once.
Use Keyboard Shortcuts and Command Model for the central registry, command-palette rules, and quick-bar interaction details. This page focuses on how those rules shape the file-browser architecture.
Pane-Local Current-Directory Filtering
Current-directory filtering is part of pane state, not just quick-bar UI state.
The important design choice is that the quick bar only supplies the input shell. The actual filter value and filtered list live in useFileBrowserPane.
- each pane owns its own
currentDirectoryFilter - the pane computes
sortedAndFilteredFilesfrom the normal sorted list plus the active filter text - the filtered list is the same list used by the virtualizer, keyboard navigation, viewer launch, selection helpers, and status bar
- the active filter is considered scoped to the pane’s current
connectionId + path
That arrangement is what makes the behavior consistent in both single-pane and dual-pane workflows.
Why the Filter Lives in the Pane Hook
The filter is not modeled as a dropdown provider result set.
- smart navigation is a navigation surface that benefits from dropdown results
- command mode is a command surface that benefits from dropdown results
- current-directory filtering is a local transformation of the pane’s main file list
Because the pane already owns the rendered list, focus state, virtualizer, and file actions, the filter belongs there as well.
User-Facing Behavior the Architecture Must Preserve
The implemented flow is:
Ctrl+Alt+Fopens filter mode for the active pane.- The shared input shows that pane’s current filter value.
- Typing immediately filters the pane’s main file list.
ArrowDownfrom the input focuses the filtered file list.- Arrow keys,
Home,End,PageUp,PageDown,Enter, and file actions operate on the filtered visible list. - Pressing
Ctrl+Alt+Fagain returns focus to the same pane’s filter input without clearing the term.
Reset and persistence rules are equally important:
- switching between smart navigation, command mode, and filter mode does not clear the active filter
- changing directories clears the filter for the new path scope
- changing the pane connection also clears the filter
- dual-pane mode keeps filter state isolated per pane
- browser recovery snapshots capture and restore the pane filter together with path, focused item, and selection state
Focus and Zero-Result Behavior
Filtering changes focus semantics because it can remove rows from the visible list.
- if the previously focused file still exists in the filtered list, focus stays on that file
- if it disappears, focus clamps to the nearest valid visible item
- if the filter produces zero visible files, focus remains in the filter input when possible
That is why the pane hook owns both the filtered list and the focus-adjustment logic.
Status Bar Visibility
The filter must remain visible even when the quick bar is no longer in filter mode.
- the desktop status bar renders the active filter term whenever the pane has a non-empty filter
- the status bar uses the filtered visible list for item counts and file metadata
- this keeps the active filter visible while the user switches to smart navigation or command mode
The status bar indicator is part of the product contract, not just decorative UI. It is the persistent signal that the pane is still filtered.
Selection, Copy, and Move
The browser maintains explicit multi-select state per pane, but still supports a focused-item fallback when nothing is explicitly selected.
In practice, the pane computes an effective selection from either:
- the explicit selected-file set
- or the focused file when nothing has been explicitly selected
Selection is pane-local and clears naturally when directory navigation changes the active file list.
Copy and move behavior depends on:
- the active pane as the source
- the other pane as the destination in dual-pane workflows
- backend or companion routing depending on which connection types are involved
- conflict and validation behavior that still comes from the destination backend contract
The copy and move dialog is not just a confirmation prompt. It also:
- shows the selected source files
- pre-fills the destination from the other pane
- lets the user adjust the destination before confirming
- reports progress while multiple operations execute
Important contract details:
F5means copy in dual-pane mode, but remains refresh in single-pane modeF6means move in dual-pane mode- cross-connection operations are not treated the same as same-share SMB operations
- backend validation still decides whether same-path, self-copy, or conflicting operations are allowed
For same-connection SMB copy and move, the backend uses server-side operations rather than streaming all file bytes through the browser app.
Refresh and Live State
Directory freshness is coordinated through WebSocket subscriptions.
- the browser subscribes to each visible pane’s directory
- directory-change messages are routed back to the matching pane
- local-drive and SMB-backed directories can use different backends while still feeding one browser experience
In dual-pane mode, both panes are live participants in refresh behavior:
- each pane can subscribe to a different directory
- each pane re-subscribes when its own path changes
- directory-change events are dispatched back to the pane that owns the matching path and connection context
This is why navigation and data freshness are coupled more tightly than they first appear.
Design Constraints and Deliberate Tradeoffs
Some file-browser behaviors are deliberate tradeoffs rather than incidental implementation detail.
- the UI caps visible panes at two even though the query-key pattern could theoretically scale further
- browser-default shortcuts such as tab switching are overridden when they conflict with the product’s pane model
- dual-pane mode is disabled on compact layouts instead of forcing a cramped mobile approximation
- one pane can stay hidden while still existing in state so the hook model remains stable
Why This Page Matters
Frontend changes in the file browser often look local, but they can break:
- URL-restoration behavior
- keyboard and focus contracts
- copy and move flows
- viewer launch behavior
- local-drive versus SMB routing assumptions
- refresh behavior across both panes
Where to Continue
- Use Keyboard Shortcuts and Command Model for centralized shortcut definitions, command-palette behavior, and focus rules.
- Use Viewer Architecture and Preview Contracts for preview and file-type behavior.
- Use Browser-to-Companion Trust Model when navigation or actions depend on paired local-drive access.
- Use Test Strategy Overview when changes cross backend, frontend, and companion boundaries.