The keymap was scattered over ShortcutsService if-chains, the file panel's
key switch, macOS-only menu accelerators and hand-typed hints in 16
tooltips x 7 locales, and large parts of the app had no keyboard path at
all. src/shared/keymap.ts is now the one table: scopes, per-platform
defaults, exact matching, formatting with localized key names, conflicts,
reserved combos and native-menu accelerators, proven by keymap.spec.ts.
- ShortcutsService resolves against the effective keymap: claim() takes
the key (preventDefault, held keys repeat only movement, never mid-IME),
and from a text field only chords and F-keys fire. The file panel, App,
editor, manager and Settings windows all dispatch through it.
- FileActionsService.run is the one file-command dispatcher for the list
keys, the row menu, the macOS menu bar and the toolbar.
- New keys: the row menu (Shift+F10 / Menu / Ctrl+Return), the source
picker (Mod+K), the path bar (Mod+L), transfers (Mod+J), sorting
(Alt+1-7), Shift+movement selection, close other tabs, move and
disconnect tabs, refresh and edit on Windows/Linux, permissions, copy
path, reveal, trash, open in default app, editor find next/previous.
- Right-click menus: soft shortcut hints, icons on the everyday verbs
(the toolbar's glyphs), section headers, scrolling, item keys while
open, a highlighted first item when opened from the keyboard. The panel
source picker now renders through the same menu.
- Dialogs: role=dialog, a Tab trap via cycleFocus (WKWebView skips
buttons on Tab without Keyboard navigation), initial focus, Esc that
stops propagating; the chmod dialog takes focus and applies on Enter.
- Fixed: Ctrl+Tab also flipped the active panel; Esc in a manager dialog
closed the manager; Ctrl-F/T fired Cmd shortcuts on macOS; shortcuts
fired under dialogs; keys died after any button click on macOS; the
editor's save prompt and find box never got focus; WebView2 reload keys;
a window could save default settings before its first load.
- Settings closes on its own close-window key (new settingsClose request).
Co-Authored-By: Claude <noreply@anthropic.com>
Electrobun 2.x replaces the vendored CLI with Hutch: the npm package is now a
bootstrap whose exports are a throwing stub, and the SDK is projected into
.hutch/devkit. The app stays on the Bun main process — `mainProcess: "bun"` is
load-bearing, since the 2.x default (Cottontail) ignores build.bun and with it
the ssh2 externals — and the bundle now ships the pinned Bun 1.4.0.
- electrobun 2.0.2 as an exact devDependency. `electrobun prepare` takes the
old CLI re-sign step's slot in postinstall and every dev/build/start/release
script: tsc (root tsconfig extends the devkit's) and the Angular build
(electrobun/view mapped into the devkit) resolve the SDK there.
- Imports move to electrobun/main. The CLI re-sign script and the v1 "three"
type shim are gone (Hutch's binaries are validly signed).
- UI: Electroview's bunSocket is hostSocket; noImplicitReturns is off because
the devkit's raw-TS browser SDK compiles inside the UI program.
- Updater.applyUpdate() now consults before-quit, so the transfers/unsaved
guard's "Quit Anyway" re-runs the apply instead of quitting without it.
- 2.x applies trafficLightOffset from macOS's default button position (1.18.1
clamped it inside the titlebar); {12, 4} centres the lights in the header.
- Releases: mac-x64 dropped (2.x publishes no macOS x64 core, so no Rosetta
path). artifacts/ is emptied before each build and published verbatim.
Installers lost the stable- prefix (the update feed kept it), so release.ts,
deploy.sh and the site links follow. Linux ships Electrobun's Setup
installer instead of the portable tarball and its miniSCP.sh wrapper.
- Docs cover all of the above, plus re-checks against 2.0.2: still no native
modal windows; window close is now cancellable through will-close.
Verified: tsc, build:ui, a stable mac-arm64 release (dmg + the v1-compatible
stable-macos-arm64 feed) and dev launches (RPC, Settings window, traffic
lights). Windows/Linux builds and the v1 -> 2.x Windows update bridge still
need their first run on native machines.
Co-Authored-By: Claude <noreply@anthropic.com>
The tab-bar popover had outgrown itself. Settings now open in their own
native window (a fourth window kind, built like the connection manager but
not modal): a sidebar with Appearance, Transfers, Editor, File list and
About, holding every popover row plus the visible-columns choice. Each
control is an ordinary settings save, so the other windows follow live
while it stays open.
The gear button in the tab bar, Ctrl+, and the macOS app menu's
"Settings..." (Cmd+,) open it. On macOS Cmd+W arrives as the main window's
close-tab menu action, so bun closes the focused Settings window first
instead of a main-window tab.
Co-Authored-By: Claude <noreply@anthropic.com>
Ctrl/Cmd +/-/0 in editor and connection-manager windows called
SettingsService.persist() -> saveSettings, but only the main window's RPC
served that request: it went unanswered, the new scale applied locally, and
the next broadcast from main snapped it back.
All three window RPCs now serve saveSettings through one shared
saveSettingsHandler (persist, then hand off). index.ts's settingsSaved() is
the single place settings side effects run — transfer concurrency, locale +
app menu, the preload snapshot — whichever window saved, and it rebroadcasts
to every window EXCEPT the sender: an echo arriving behind a newer local
change (a held Cmd+) would drag it backwards. Editors are skipped by their
registry key, main and the manager by name.
Known ceiling, noted at the handler: whole-object writes, so two windows
saving in the same instant means the later one wins.
Co-Authored-By: Claude <noreply@anthropic.com>
The win7 skin is dropped. Everything else the branch built stays: Linux
support and the HTML context menus, the workspace-tab menu, the continuous
interface scale (rem everywhere, stepper, Ctrl/Cmd +/-/0) and the core/
service layer. The modern tree already carried all of them, and nothing
outside skins/7like/ imported the win7 tree except main.ts and styles.css.
Removed: the skins/7like/ tree (5.1k lines, fonts), the 7.css dependency,
the skin setting and its options row + strings in all 7 locales, the
frameless-window plumbing (window-control RPCs, windowState push,
trackMaximizeState, the synthesized resize grip and its postinstall
watchdog), the session-skin pinning in buildPreload/chromeOptions, the
skin/skinfix cascade layers, and the win7-only probes and screenshot
harness (the harness cannot render the modern tree: plain Firefox has no
RPC bridge). chromeOptions() stays as the one shared frame decision.
Fixed on the way out:
- index.html's per-theme pre-paint background is restored. The branch had
replaced it with a win7-only transparent rule, so modern dark windows had
silently lost one of the three flash guards.
- editor-windows.ts wrote its window-key separator as a literal NUL byte,
which made git and grep treat the file as binary; it is `\0` now (same
string at runtime).
No settings migration: a stale "skin" key is carried harmlessly by the
store's spread-merge and dropped on the next save.
UI bundle: 616.7 kB -> 405.4 kB, back under the 500 kB budget.
Docs: architecture, gotchas and platform-notes lose the win7 material but
keep what stays true (construction-time preloads/frames, drag regions
ignoring preventDefault, electrobun's missing resize/maximize primitives);
flash-prevention, dev-URL and Bun-panic notes move back under Electrobun;
stale context-menu/tab-bar links fixed; README documents the scale keys.
Co-Authored-By: Claude <noreply@anthropic.com>
The win7 skin is frameless so 7.css can draw the Aero caption, and on GTK that
means undecorated, which takes the window manager's resize borders and corner
grips with it. All three win7 windows were completely unresizable.
electrobun 1.18.1 offers nothing to re-enable, verified against the shipped
binary rather than assumed: libNativeWrapper exports startWindowMove/
stopWindowMove with no resize counterpart, imports gtk_window_begin_move_drag
but not gtk_window_begin_resize_drag, dragRegions.ts implements moving only,
and BrowserWindow has neither setResizable nor min-size options. But the
window is still programmatically resizable, so only the input affordance is
missing: eight handles overlay the frame and drag it through a new
windowGetFrame/windowSetFrame pair on the shared windowControlHandlers factory,
which gives all three window kinds resize with no per-window wiring.
Three things that would have made this quietly wrong:
- dragRegions.ts starts a window MOVE from a mousedown listener on document,
never checks defaultPrevented, and is not suppressed by a pointerdown
handler either (the compatibility mousedown still fires). Every handle
carries app-region-no-drag, which short-circuits its check first.
- deltas come from screenX, not clientX: a north/west resize moves the window
origin under a stationary pointer, so client coords feed that back into the
next delta and the window runs away.
- the min-size clamp re-anchors the opposite edge; clamping width alone lets x
keep tracking the pointer, walking the window off-screen while it looks
frozen.
This is a stopgap and the point is that it is removable on sight. The frame
math is a dependency-free module so scripts/resize-math-probe.ts asserts all
eight edges without a window (verified it catches the unanchored-clamp bug:
x jumps to 5100 instead of parking at 460). scripts/check-electrobun-resize.ts
scans the installed electrobun for the primitives this is waiting on and prints
the removal checklist when they appear; it runs from postinstall so a version
bump surfaces it, never fails an install, and is silent otherwise. Both trigger
paths were simulated. The component header carries the full checklist; docs
cover the platform consequence and the dragRegions traps.
Harness diffs exactly 0 in both themes; hover and menu probes still clean.
Not yet exercised with a real pointer — that needs a live window.
Co-Authored-By: Claude <noreply@anthropic.com>
main retired electrobun's native context menu (a documented no-op on Linux) in
favour of an HTML one on every platform. Merging that here needs the menu to
look right under the Win7 skin as well.
Conflicts, all resolved by keeping both sides:
- src/bun/index.ts — createAppRPC keeps the branch's mainWindow getter, drops
main's removed contextMenu dep.
- ui/src/app/app.ts — imports both TitleBar and ContextMenu.
- ui/src/app/platform.ts — kept the branch's version (its IS_MAC_CHROME accounts
for the frameless win7 chrome, and MOD_LABEL correctly keys on the real
platform rather than the chrome), plus exporting IS_MAC and adding IS_LINUX
for the menu's per-OS shell wording.
Aero menu (branch-only): 7.css has no menu component, so it is hand-authored
across both layers — the row <button>s shed their push-button slab in `skin`
(revert-layer only reaches `utilities` from there), and the square-cornered
slab, hairline border and gradient selection band are stated in `skinfix`.
Built strictly from existing --w7-* vars, never literal hex, so the
hand-written dark Win7 palette recolours it for free. Hover and the keyboard
cursor share one band (the component marks the cursor with `bg-accent`, which
is overridden alongside :hover) as a real Win7 menu does.
Also answers a question the merge raised: frameless windows DO work on Linux.
The GTK wrapper honours titleBarStyle "hidden" — it calls
gtk_window_set_decorated(false) and logs "GTK: Created window without
decorations (custom titlebar)". Verified on GNOME/Wayland and documented in
architecture.md.
Verified: backend `tsc --noEmit` clean, `bun run build:ui` clean (492.52 kB
initial, still under the 500 kB budget), app launches with the skin active,
frameless, no console errors and no GBM failures.
Co-Authored-By: Claude <noreply@anthropic.com>
`bun run start` died on Linux with ERR_DLOPEN_FAILED (missing
libayatana-appindicator3.so.1). Fixing the environment exposed the real gap:
electrobun's ContextMenu.showContextMenu is a documented no-op on Linux, and
since the UI suppresses the webview's own right-click menu, EVERY panel context
menu was silently dead there — Send to Other Panel, Open, Rename, Permissions,
Delete, Copy Path, Reveal and the column picker all unreachable. Electrobun
itself logs "Application menus are not supported on Linux. Implement menu UI in
your webview HTML instead." at boot.
- Context menus are now HTML on EVERY platform (ui/src/app/context-menu.ts +
context-menu.service.ts), not just as a Linux fallback: one implementation
cannot drift from a second one. The item builders are ported verbatim from
the old bun menu, so ordering, per-platform labels and the Windows-local
chmod hide are unchanged, and picks are emitted as the same `ctx-*` / `col:*`
strings through RpcService.emitMenuAction — App.handleMenuAction is untouched
and still the single dispatcher for both the macOS menubar and right-clicks.
Adds what the native menu gave for free: arrow/Enter/Escape keys, viewport
edge-flip, close on blur/resize/scroll. The Edit item now shows the real F4
hint instead of the native menu's "e" mnemonic.
- New shellItemAction RPC replaces showRowMenu/showColumnsMenu — the only part
the webview can't do itself (reveal, open-with-default, copy path). Deletes
src/bun/context-menu.ts entirely.
- release.ts gains a linux-x64 target. Electrobun emits only the .tar.zst
updater blob on Linux, so the artifact is a .tar.gz of the assembled app
folder, mirroring the Windows portable zip.
- Docs: platform-notes gets a Linux runtime-dependency table (AppIndicator is
linked HARD — a missing lib is a boot failure, not a degraded tray), the
ldd diagnostic, GNOME tray caveat, known issues and a first-boot smoke test;
gotchas gets the ERR_DLOPEN_FAILED trail and the benign GLX/GBM startup
noise; architecture/build-and-release/README updated accordingly.
Verified: backend `tsc --noEmit` clean, `bun run build:ui` clean (399 kB
initial), app boots on Linux with no errors beyond the documented benign
warnings. NOT verified on macOS/Windows — the native menu is retired on those
platforms too, so the menus need eyeballing on a Mac before the next release.
Co-Authored-By: Claude <noreply@anthropic.com>
Evaluation branch: a full Aero skin, including frameless windows with our own
title bar, switchable against the existing modern look.
The app already had a four-axis runtime skinning mechanism and zero component
stylesheets, so the skin is one new setting plus one stylesheet section rather
than a parallel UI. The key move is cascade order: 7.css is imported into a
`skin` layer ABOVE Tailwind's `utilities`, so its bare element rules outrank
the ~1900 inline utility classes already in the templates. Template changes are
limited to additive, skin-inert hooks (ARIA roles, `has-scrollbar`,
`w7-toolbar`, `w7-btn`).
- styles.css: explicit layer order, `7.css/dist/7.scoped.css` via `layer(skin)`,
a token bridge re-pointing the existing semantic tokens at Win7 values (no new
tokens, so `@theme inline` is untouched), a hand-authored dark Win7 palette,
and neutralizers that `revert-layer` 7.css's dialog-button metrics back to the
utility layer. Neutralizers must live inside `skin`, not `skinfix` —
`revert-layer` only reaches `utilities` from one layer up.
- Frameless chrome: `chromeOptions(skin)` is now the single place either skin's
frame is decided. `titleBarStyle: "hidden"` on every platform, which is also
the only way to drop the macOS traffic lights. New `title-bar` component
mounted by all three window roots.
- Window controls over RPC (minimize/toggle-maximize/close/is-maximized), served
per window kind so the X reuses each one's existing close path — editors still
get their dirty prompt. electrobun has no maximize event, so the state is
derived from `resize`; double-click-to-maximize is implemented here too.
- Font: Noto Sans vendored (OFL-1.1) under ui/src/fonts, since Segoe UI is
absent off Windows and not redistributable.
Verified: backend `tsc --noEmit` clean, `bun run build:ui` clean (486 kB initial,
under the 500 kB budget), app launches with the skin active and no console
errors. The rendered result has not been eyeballed — screen capture and
accessibility are both denied to the shell on this machine.
Co-Authored-By: Claude <noreply@anthropic.com>
Wires electrobun's Updater to the /dl download host. The packaged app checks
https://miniscp.com/dl on launch + every 6h; when a newer build exists, an
accent-colored "update tool" appears next to Connections in the tab bar. It
announces the update, becomes its own inline progress bar while downloading,
then flips to "Restart to apply" (two-step: download, then user-triggered
relaunch).
- electrobun.config.ts: release.baseUrl = https://miniscp.com/dl (baked into
version.json at build; miniscp.com fronted by an external proxy for TLS).
- rpc.ts: checkForUpdate/downloadUpdate/applyUpdate requests + updateStatus push.
- rpc-handlers.ts + index.ts: call Updater; poll on launch+6h; stream
onStatusChange progress. checkForUpdate self-guards the dev channel.
- rpc.service.ts + new updater.service.ts: signal-based state
(available -> downloading -> ready, ready derived from downloadUpdate ok).
- tab-bar.ts: single bg-primary pill; progress fills the pill itself.
- icons.ts: register mynaDownload. i18n: tabBar + menu keys across all locales.
- menu.ts + app.ts: "Check for Updates…" app-menu item -> UpdaterService.check().
- docs: build-and-release + architecture updated.
Unsigned builds still update (applyUpdate strips the macOS quarantine xattr).
Verified: tsc --noEmit (exit 0) + build:ui AOT (exit 0). Live pill + full
download/apply flow require a two-build test on the host.
Co-Authored-By: Claude <noreply@anthropic.com>
Local panels are fs.watched (LocalDirWatcher + setWatchedDirs -> pathRefresh);
remote panels poll every 3s. New refresh({silent}) skips the loading/busy
flags and touches no DOM, snapshotting sessionId+cwd to discard results if the
user navigated mid-flight. Only the active tab is live; switching tabs
refreshes the new pair. A future on/off setting is left as commented
breadcrumbs. Docs updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
External editor via cmd/start + exe picker; Permissions hidden for
Windows-local (chmod only flips read-only there); Explorer/Recycle Bin
wording; Ctrl vs Cmd shortcut labels; separator-aware path joins +
drive-root detection; drives list in the panel source picker; icon.ico
+ colored tray icon; icacls ACL on connections.json; README Windows
(beta) section with upstream known issues and a smoke-test checklist.
Runtime paths pending first boot on a real Windows machine.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Build n = commit count, baked at build time into a gitignored
version.gen.ts (postinstall + all build scripts regenerate it). About
lives in the macOS app menu and as a footer row in the options popover
(the cross-platform path), showing build, commit, date and engines.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Native checkable menu on the column headers toggles Size, Modified,
Kind, Rights, Owner and Group (persisted, both panels). Owner/group
come from the ls-style SFTP longname remotely and cached uid/gid
lookups locally (POSIX; undefined on Windows). All columns sort.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Single window centered over the main app. Electrobun has no native
modality, so it's approximated: bun bounces the main window's focus back
to the manager, the main webview shows a click-swallowing overlay, and
main-targeted menu/keyboard actions are dropped while open. Editor
windows stay fully usable. Connect/open-stack execute in the main
window via the existing connect: action channel; store edits push a
connectionsChanged refresh. Trigger moved from the panel dropdown to a
tab-bar button.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Real BrowserWindow per edited file with its own RPC instance, deduped by
sessionId+path. The same Angular bundle boots an editor-only root via
preload globals. Native close isn't cancellable in electrobun, so the
editor mirrors its draft to bun; traffic-light closes get a native
Save/Discard prompt after the fact, and failed saves park the draft in
the edit-leftovers dir. In-window closes (Cmd+W, Close) use a styled
prompt; the quit guard now also counts unsaved editors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Folders become Stacks (connections.json v2, no v1 migration): collapsible
stack-first list, open-stack-in-tabs (first reuses a pristine tab), drag
connections between stacks. More... reveals a per-connection Local
directory applied to the other panel when local and untouched. Import
reads a WinSCP.ini export incl. deobfuscated passwords.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Read-only group below saved connections; hosts resolve HostName/User/
Port/IdentityFile (Include, wildcards, %-tokens) and connect on click.
New ssh-agent auth kind covers hosts without a usable identity file;
ProxyJump/ProxyCommand hosts are shown disabled.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Choose... button beside the private-key field opens the native file dialog
starting in ~/.ssh (Utils.openFileDialog via a pickKeyFile RPC).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Native menus (electrobun ContextMenu) on rows and empty panel space. Rows:
Open (dirs navigate; local files open with the default app via Utils.openPath),
Edit, Copy to Other Panel, Rename, Permissions, Copy Path (works for remote
paths too), Reveal in Finder (local, Utils.showItemInFolder), Move to Trash
(local, Utils.moveToTrash — tooltip notes Finder Restore doesn't apply) and
Delete. Empty space / '..': New Folder, New File, Refresh, Reveal in Finder.
Right-click parks the cursor on the row (keeping the selection when clicking
inside it). Local shell actions run bun-side directly; everything else routes
through the existing menuAction channel back to the stored click target.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>