.window's 6px radius and its outer drop shadow are painted by the webview
OUTSIDE the window's own rectangle, so on an opaque surface they were simply
clipped — square corners, no shadow. chromeOptions now passes
`transparent: true`, the page paints no background of its own, and .window
takes a 6px margin as the gutter the shadow falls into (h-screen shrinks by the
same amount so the gutter does not produce a scrollbar).
Confirmed on Linux/WebKitGTK in a real run, which is the half the Firefox
harness cannot model:
GTK: Created window without decorations (custom titlebar)
GTK: Created transparent window
GTK WebKit: Applied transparent background to webview
Also caught by the run rather than shipped: `transparent` is set in
chromeOptions, which openSecondaryWindow spreads too — so the editor and
connection-manager windows became transparent while still rendering bare
content, i.e. floating widgets on nothing. Both roots now take the same
.window.glass frame and their own windowFocused signal. The editor's blur
binding had to merge with the existing flushDraft() call; a duplicated host key
is a TS1117 build error rather than a merge, which is how it surfaced.
The old anti-flash <style> in index.html is gone with the opaque backgrounds it
guarded — windows are created hidden and revealed on dom-ready anyway.
Not handled yet: a maximized Win7 window has no radius, shadow or gutter. The
6px margin currently applies in every state.
Co-Authored-By: Claude <noreply@anthropic.com>
The app rendered a bare .title-bar with no .window around it, which is why the
Aero frame, glass and drop shadow were missing and the title-bar controls sat
permanently in their inactive colours: EVERY control-glyph rule in 7.css is
keyed on `.title-bar.active` / `.window.active .title-bar`, and nothing ever
set .active.
- app.html's root is now `.window.glass`, so it picks up the 6px radius, the
1px frame, the `inset 0 0 0 1px #fff9` highlight, the drop shadow and the
backdrop-filter glass layer.
- The content below the title bar moved into `.window-body`, 7.css's recessed
inner panel (6px inset + its own border) — the thing that gives a Win7 window
its depth.
- `.active` binds to a new `windowFocused` signal, seeded from
document.hasFocus() and updated from window focus/blur. The webview receives
those when the NATIVE window's focus changes, so no RPC is needed.
Harness renders the shell focused and unfocused: active shows the red close
button and crisp glyphs, inactive desaturates, matching the reference.
Native window transparency (for the drop shadow and rounded corners to fall
outside the window rather than being clipped by an opaque surface) is
deliberately NOT in this commit — it is a native flag with a real chance of
misbehaving on Linux, so it gets its own change and a real-app check.
Co-Authored-By: Claude <noreply@anthropic.com>
The previous model made the dotted rectangle the de-facto focus signal: the
cursor row got ONLY dots (and only in the active pane), so an unfocused pane's
cursor row disappeared entirely, and selection strength was the only band
difference. Wrong way around for a commander UI.
Now (matching Win7 Explorer / the commander convention):
- tr.highlighted = "selected via Space" OR "cursor row" — the template ORs
them, so the cursor row always carries the selection band in BOTH panes.
- Pane focus is what changes the band: strong Aero band (--w7-el-grad-h) in
the focused pane, 7.css's own faint inactive wash in the unfocused pane —
that wash IS Win7's inactive-selection colour, faint by design.
- The dotted rect is demoted to what it was in Win7: a keyboard-focus marker
layered ON TOP of the band, focused pane only. It is no longer the thing
that tells you which pane is active.
Harness now renders a focused and an unfocused pane stacked, with selected
rows and a cursor row in each; verified zoomed in light and dark.
Co-Authored-By: Claude <noreply@anthropic.com>
You were right that selecting a line looked off. Three separate problems, two
of them introduced by the Stage 2 dark-mode block:
1. The selection band was ERASED in dark mode. Stage 2 restated the dark
surface on `table > tbody > tr > *`, i.e. on the CELLS — but 7.css paints
tr.highlighted on the ROW, and a cell background paints over a row
background. The selected row kept only its 1px border and no fill. The base
surface now goes on `table` alone; cells stay transparent so the row band
shows through. Same trap re-applies to the cell separators, which 7.css
deliberately drops inside a selected row and the dark rule was re-adding.
2. The cursor rectangle was drawn PER CELL. `tr.is-cursor > *` puts a dotted
outline around every cell, so the focused row rendered as a row of dotted
boxes. Win7 drew one rectangle around the row — the outline belongs on the
`tr`.
3. Selection was barely visible even in light, and that one is 7.css's own
semantics rather than a bug: listview's `.highlighted` is Win7's *inactive*
selection, a near-white wash. Win7 drew the FOCUSED pane's selection as the
stronger Aero band. The table now carries `is-active` from the panel's own
isActive(), and the focused pane's selection uses --w7-el-grad-h/-bd-h,
which the dark palette already restates, so it recolours for free.
Verified by cropping and zooming the harness screenshots in both themes rather
than eyeballing a full page: full-row band, separators dropped inside it, and a
single dotted rectangle on the cursor row.
Co-Authored-By: Claude <noreply@anthropic.com>
The file list was a CSS grid of <div>s, so 7.css's listview component never
matched a single selector and Tailwind styled the whole thing. It is now the
plain <table> 7.css documents, which is what makes the Aero treatment appear.
- The column header row moved OUT of its own div and INTO <thead>: 7.css makes
thead position:sticky, so it stays put while the body scrolls, and it has to
be inside the same table to do that.
- Column widths moved from a computed grid-template-columns to <colgroup>,
because 7.css sets table-layout: fixed. `gridTemplate()` is deleted.
- Selection is now `tr.highlighted` (7.css's gradient band + #aaddfa border)
instead of bg-primary/15.
- The sort arrow was the text glyph ↑/↓ from `sortIndicator()`, which is also
deleted; the sorted column is now `th.highlighted.indicator[.up]` and 7.css
draws the real clip-path triangle in the header.
- Row height, padding, borders and fonts now come from 7.css, so --row-h and
the per-cell text/color/font utilities are gone.
Two things 7.css does not cover, added in `skinfix`:
- Dark mode. listview.css hardcodes light literals (#fff, #c0c1cd, #eee and a
white header gradient) rather than --w7-* vars, so it does NOT follow the
hand-authored dark palette — the header labels rendered invisible, dark on
dark. The dark arms restate those rules; light keeps 7.css's own values.
- The keyboard cursor. 7.css models selection only; Win7 drew the focused row
with a dotted focus rectangle over it, which is what tells "cursor" apart
from "selected" when they differ. That is `tr.is-cursor`.
Verified visually this time, not just by build log: harness screenshots in both
light and dark show the header gradient, sort triangle, cell separators,
selection band and cursor rectangle matching 7.css's reference markup. Backend
`tsc --noEmit` clean, `bun run build:ui` clean, bundle 489.83 kB (from 490.50).
Co-Authored-By: Claude <noreply@anthropic.com>
The dual-skin arrangement existed to let one build toggle between the modern
shadcn look and 7.css. Keeping both correct is what forced the cascade tricks
that fight 7.css, so the branch drops the toggle: this build IS the Windows 7
build.
- `skin` is gone from AppSettings/DEFAULT_SETTINGS, the settings store, the
SettingsService (signal, applySkin, persist payload), the options popover
control, and the i18n keys.
- buildPreload adds `.win7` unconditionally; `chromeOptions()` no longer takes a
skin and always returns frameless `titleBarStyle: "hidden"`.
- `IS_WIN7_CHROME` and `IS_MAC_CHROME` are deleted along with every
`isMacChrome` branch in the tab bar, editor window and connection manager.
Those reserved `pl-24` for macOS traffic lights, which frameless chrome
removes on every platform, so the branches were dead by construction.
- The title bar is no longer conditional — it is the window's only chrome.
- index.html keeps just the win7 anti-flash arm (four theme states).
Not done here, deliberately: stripping Tailwind's element styling. The
neutralizers in styles.css (notably `.win7 button { min-width: 0 }`) are still
load-bearing until each component's markup is converted to 7.css's real
contract, so they come out per component rather than in one sweep.
Verified: backend `tsc --noEmit` clean, `bun run build:ui` clean; bundle
490.50 kB, down from 492.52 kB.
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>
The dark palette was shadcn's cool zinc (#202023, hue ~286). Anchor it on
#1a1a19 instead — cds gray-830, a warm near-neutral (hue 107) — and retone
the whole dark neutral family to match, since a warm background under cool
panels clashes. Lightness ramp is preserved; chromatic tokens (destructive,
success, icon hues, accent formulas) and the light palette are untouched.
Also introduce --chrome so the app isn't one flat tone: frame strips (tab
bar, status bar, transfer bar, editor/manager header+footer, connection
sidebar) now sit one step below the content panes, with popovers and hover
above them — the standard dark-UI elevation order.
Dark ramp: #151514 chrome, #1a1a19 content, #20201f card, #2e2e2d hover;
the "darker" tone drops chrome to true black. Light gains only a zinc-50
chrome step.
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>
Custom signal-based runtime (no i18n dependency): shared src/shared/i18n
dictionaries, ui I18nService.t() reactive by construction so language
switches live, and a backend i18n.ts for native menus + thrown errors.
Every user-facing string keyed across templates, toasts, dialogs, file
kinds, columns, macOS menu/context menus, and errors. Language picker in
Options (auto-detects OS locale, falls back to English); switching
relabels the webview and native chrome instantly. English/Spanish/French/
German fully drafted; Chinese/Japanese/Basque drafted, flagged for native
review. Docs updated.
Co-Authored-By: Claude Fable 5 <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>
Reload/Inspect Element no longer appear on uncontrolled right-clicks
in any window; text fields keep the system text menu.
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>
Local left, remote right — the commander convention. Panel-dropdown
connects still target the panel they were opened from.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Import moves to the window header; sidebar widens with a muted tint;
the stack row's count and action buttons swap on hover instead of
sharing the row, so long names can't collide with them.
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>
Dialogs only needed refocus(); injecting PanelsService boots a whole
workspace, which secondary windows must not do.
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>
Connecting blanks the list to a fading spinner with the target name and
a Cancel button (token discards late results and orphan sessions).
Listings >300ms dim the list under a spinner. All panel input is
blocked while busy; the source picker stays usable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tone row appears under Theme when the effective theme is dark.
Grey palette uses macOS-like #26262a background.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Action buttons, server picker, and its dropdown now follow the size
setting via new --action-h/--action-font/--action-icon variables.
Default tier matches the old fixed sizes; tab bar stays fixed.
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>
Typing while the list is focused builds a filter query: a bar appears under
the column headers (search icon, query, live match count, Esc hint, clear
button) and the listing narrows to substring matches — '..' always stays.
Backspace edits the query, Escape clears it, cursor parks on the first match,
arrows/Enter keep working (the list never loses focus), and Space keeps its
select meaning until a query exists. Filter resets on directory change and
survives refresh; operations act only on visible (filtered) selection.
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>
Container query on the panel (Tailwind v4 @container): Copy, Edit, Rename and
Delete grow labels at >=30rem panel width; New folder, New file and
Permissions stay icon-only (self-explanatory icons). Tooltips unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>