- AppTray: template menu-bar icon (22pt, adapts to light/dark). While
transfers run it swaps to pre-rendered frames — faded arrows + a fill bar
showing aggregate doneBytes/totalBytes of the queue (nearest 5%, throttled
500ms); back to the glyph when idle. No runtime image encoding.
- Menu: one entry per tab (checkmark on the active one) with live per-tab
transfer count and percentage (jobs mapped via session ids), an overall
progress row while transferring, Show <app>, Quit. Selecting a tab
activates the window and switches to it.
- First use of webview->bun messages: PanelsService pushes workspacesSync
(tab names, sessions, active id) via a new ready-gated send proxy; verified
end-to-end in the running app.
- engine.aggregateProgress(): totals + per-session breakdown over non-terminal
jobs (unit-verified idle/mid-transfer/idle).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two panels on the same server hold two SSH sessions by design, and every
self-copy guard compared sessionId — so same-server same-path copies slipped
through all of them, and createWriteStream truncated the destination before
the read began: the file (or every file in a folder) became 0 bytes, with the
overwrite dialog making it look like a legitimate replace.
Fix, defense in depth, keyed on FILESYSTEM identity instead of session:
- fsKey on every provider: 'local', or resolvedIP:port for SFTP (host is
DNS-resolved at connect so hostname-vs-IP aliases compare equal; username
excluded — same host:port is the same filesystem regardless of login)
- engine guard blocks equal/nested paths when fsKeys match; per-file
belt-and-suspenders check right before streams open
- probe layer for hidden aliases (different addresses, same machine): when
fsKeys differ but paths collide, a token file written via dst and read via
src decides; visible => same machine => blocked. SFTP exposes no dev/inode
(what cp/rsync use), so probing is the only deterministic test; fails open
and cleans up after itself
- UI: same-folder toast now fires across two sessions to one server, and the
overwrite dialog auto-skips (never offers Replace) when the colliding item
IS the source file
Known residual, documented in README 'Same-file protection': one filesystem
mounted at different paths on two connections is undetectable client-side.
Regression suite against the real box: file-onto-itself and folder-onto-itself
blocked with contents byte-intact, folder-into-descendant blocked, legit
same-server different-dir copies still work, local same-path still blocked,
simulated alias caught by the probe with cleanup verified.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Folder select showed '(none)' regardless of the actual folder (Angular sets
[value] on the select before its options render), so 'move out of folder'
silently saved the old folderId. Per-option [selected] bindings fix both the
display and the save.
- Folder rows gain 'Open all in tabs': each connection opens in its own new
tab with the left panel connecting (right stays local); manager closes.
- Folder-row action buttons no longer shift the row on hover (opacity toggle
instead of display toggle); also canonicalized accent-primary classes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Synthetic cursor-navigable first row (hidden at /) — Enter/double-click goes to
the parent like the toolbar button. Excluded from selection, rename, delete,
chmod, and transfers; shown with an up-arrow icon.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tabs are draggable within the strip: HTML5 DnD with the WebKit setData quirk
handled, an inset primary insertion line at the drop position (no layout
shift), and the dragged tab dimmed. Reordering only touches the workspaces
array order; ids, sessions, and the active tab are unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Freeze root cause: TransfersService consumed transferUpdate through an effect
that both read and wrote the jobMap signal — the first progress message spun
the effect into an infinite re-run loop, pegging the webview thread while the
copy completed fine bun-side. Push messages now flow through plain callback
registries on RpcService (also stops signal coalescing from dropping progress
snapshots). Verified live: a 60MB boot-time self-test copy streamed progress
into the running UI and the post-completion panel refreshes arrived.
- Tabs: workspaces (left+right PanelState pair each) in PanelsService; tab bar
replaces the header; sessions persist in background tabs; Cmd+T/Cmd+W/Ctrl+Tab;
closing a tab disconnects its sessions; last tab can't close
- Per-panel toolbar (top, icon-only): direction-explicit copy (-> / <-),
rename, new folder/file, chmod, delete; panel footers removed
- VSCode-style status bar: transfer summary (spinner/percent/queued/failures,
click to expand the job list) + active panel info; function bar removed
- transfer details list is collapsible, auto-expands on failure
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- prompt/confirm/chmod dialogs (signals-driven via DialogsService)
- FileActionsService: acts on selection or cursor row of the active panel
- function bar + F2/F7/F8/Delete keys; delete confirms and recurses
- verified provider ops + walkTree + removeRecursive on a scratch tree
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- shared/types + shared/rpc: full wire contract (Result envelope, no exceptions cross the bridge)
- FileSystemProvider interface, LocalProvider, SessionRegistry with built-in 'local'
- walk.ts: provider-generic recursive walk + removeRecursive
- rpc-handlers wired into BrowserWindow; 10min request timeout for slow SFTP ops
- Angular: RpcService (Electroview bridge + plain-browser guard), PanelsService with
signals-based PanelState (sort/cursor/selection), FilePanel component with
keyboard nav (arrows/Space/Enter/Backspace/Tab), path bar, toasts
- verified: both panels listDir home over the bridge in the running app
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>