The setImage-swapped progress frames lost the template treatment on macOS and
rendered black-on-dark once transfers started. Dial back per user decision:
the tray is now just the glyph (template image set once at construction, never
swapped) and clicking it activates the window — no menu, no progress effects.
Removes everything that only served the fancy version: progress frames +
generation, engine.aggregateProgress, the workspacesSync webview->bun channel
(schema, send proxy, panels effect, handlers) and the activate-tab action.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 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>
- Built-in editor (default): large modal with monospace textarea, Cmd+S save,
dirty guard; reads/writes through the provider streams so it works on local
and remote files identically (2MB cap with a friendly error)
- External editor: remote files download to appDataDir/edits/<id>, open via
macOS `open -a`, the temp DIR is watched (survives atomic rename-saves) and
every save enqueues a normal upload job (progress/errors in the transfer
list); watchers + temp dirs are cleaned when the session disconnects
- Editor setting: Built-in | Custom with a NATIVE app picker (electrobun
openFileDialog, /Applications, .app filter) and reset-to-default
- Leftover temp files from a crash prompt for cleanup at boot
- F4, toolbar button (mynaFileText), and File > Edit File (Cmd+E) menu entry
- README: feature notes + Portability notes (open/start/xdg-open seam
documented in code and README per user request)
Verified headless against the box: sftp read/write roundtrip incl. UTF-8,
size cap, watch->save->auto-upload->remote-updated, session cleanup, orphan
listing and clearing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Standard menus (App: hide/quit; Edit: clipboard roles enabling Cmd+C/V/X/Z;
View: Refresh Cmd+R + fullscreen; Window: minimize/zoom/cycle) plus File with
New/Close Tab (Cmd+T/W), New Folder/File, and a Connect submenu built from
saved connections (folders as nested submenus, Connection Manager entry).
Custom actions forward to the webview as menuAction messages; the menu rebuilds
whenever connections change. macOS-only per electrobun support.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Transfers: Single | Multi in the options popover, with an 'At once' picker
(2/3/4/6/8) when Multi. Engine concurrency becomes an instance field applied
live: at boot from settings.json and on every saveSettings (raising the limit
starts queued jobs immediately). Verified headless: 3 slow jobs overlap at
concurrency 3, stay strictly serial at 1, clamp works.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Overwrite confirmation: name-collision detection before enqueue; dialog shows
source vs destination (size, mtime, 'newer' badge) with per-item Skip/Replace
and batch Skip all / Replace all; type mismatches auto-skip with a toast;
cancel aborts the whole operation
- Transfer speed + ETA: UI computes smoothed bytes/sec from progress deltas
(callback bus makes deltas meaningful); shown in the expanded queue rows and
the status-bar summary; unit follows the speed-unit setting
- Settings: bun-side SettingsStore (settings.json, 0600, atomic) + get/save RPC;
SettingsService; gear popover on the tab row (theme segmented control, speed
unit); persisted across restarts
- Light theme: data-theme on <html> drives token overrides; absent = follow OS
- macOS fused titlebar: hiddenInset + trafficLightOffset, tab row becomes the
draggable titlebar (app-region drag/no-drag classes); other platforms keep a
separate titlebar (gated by a platform value injected via window preload)
- README: known-issues note for the upstream Bun/Electrobun FFI crash
Verified: tsc + ui build clean; SettingsStore roundtrip + 0600 headless;
app boots and the UI fetches settings over RPC at startup.
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>