- Remove outdated SFTP upload message and replace it with ZMODEM-specific messages in English, Russian, and Chinese locales.
- Add a new function to handle ZMODEM drag-and-drop uploads in the terminal backend.
- Update terminal components to support ZMODEM drag-and-drop functionality.
- Enhance error handling for file uploads and provide user feedback for no files to upload.
- Introduce tests to verify ZMODEM upload behavior and fallback to SFTP for network devices.
Introduce workspace-aware System side panel with remote process/tmux/Docker management, terminal popup for interactive attach, capability warmup, review-hardened IPC, performance optimizations, toast action errors, and SSH channel recovery on reconnect.
* feat(terminal): add remote command history side panel
Read remote shell history over SSH/ET/Mosh exec channels, browse it in a virtualized side panel with search, paste, and save-as-snippet actions. Closes#1381.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(history): expand command detail inline below selected row
Move the detail strip from a fixed slot above the list into the row
immediately below the clicked entry so expansion reads top-to-bottom.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(history): filter Netcatty AI PTY commands from remote history
Drop shell history lines containing the __NCMCP_ marker so AI exec noise
does not clutter the command history panel.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(history): tighten detail strip and add run action
Size the expanded row to its content, add a run-in-terminal button, and
use clearer snippet icon/tooltip for save-as-snippet.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(history): address review findings before merge
Key cache by host+session, retry Mosh pending reads, and clamp virtual
list scroll position when filtered items shrink.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(et): support server stats for EternalTerminal sessions
- Generalize the Mosh stats companion into reusable connection helpers
- Open a companion SSH connection so the host-info bar works for ET sessions
- Fall back to execOnEtSession for jumped ET sessions without a direct connection
- Forward host-key and algorithm options to the ET backend for companion parity
- Close the ET stats companion on session close, cleanup, and PTY exit
* fix(et): harden stats exec host-key trust and cleanup
Enforce StrictHostKeyChecking=yes for background ET stats/distro probes
instead of accept-new, merge vault known_hosts for parity with ssh2
companions, and wrap companion connection teardown in try/catch.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Fix terminal drag-and-drop uploads so they target the active terminal cwd and avoid fallback home/login-shell cwd when the active cwd cannot be confirmed.
Expose whole-window transparency via setOpacity with settings and a top-bar quick control, persisting across restarts and syncing across windows.
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a tab context menu action to duplicate a terminal into an independent peer window, with per-window active-tab titles and multi-window lifecycle safeguards.
* feat(sftp): add Open with system default for native Windows file association (fixes#1236)
Adds a new 'Open with system default' option to the SFTP context menu
that uses Electron's shell.openPath() to invoke the native OS file opening
mechanism. On Windows, this uses ShellExecute, which works with UWP/WinUI
apps (Photos, Paint, etc.) whose executable paths change with updates.
The existing 'Open with...' (browse for executable) is preserved.
Closes#1236
* fix(sftp): add file watch support to openWithSystemDefault for SFTP auto-sync
P2: The new default-app open flow was not starting a file watch
when SFTP auto-sync was enabled, so edits saved in the system
default app were not uploaded back to the remote host.
Mirrors the downloadToTempAndOpen behavior — accepts an optional
{ enableWatch } parameter and starts a file watch when set.
* fix(sftp): mark transfer as failed when system default open fails
P2: When shell.openPath fails for a remote file, the transfer queue
still shows success because downloadToTemp had already completed the
temp download. Now updates externalTransferId to 'failed' before
throwing, matching downloadToTempAndOpen's behavior.
* fix: route Cmd+W through existing tab close flow
Keep the original tab-close behavior intact, and close the main or settings window only when there is no active closable tab to handle.
* fix: fall back to closing non-listener windows on Cmd+W
Treat BrowserWindow instances that do not participate in Netcatty's command-close bridge as regular closable windows. Keep the existing command-close path for the main and settings windows, and add tests that cover both the fallback close behavior and the renderer-capable send path.
* fix(auto-update): commit app to quit before quitAndInstall on macOS (#1215)
macOS in-place auto-update downloaded and unpacked the new version but
never installed it: the app appeared to close, ShipIt never ran, and no
restart happened. A full uninstall + reinstall did not help; only a
manual DMG replace worked.
Root cause is a code-level coordination bug, not the release pipeline.
The published mac zips are correctly Developer-ID signed, notarized, and
stapled (Team H7WS5L2ML4, consistent across 1.1.17 and 1.1.20), and
latest-mac.yml is well-formed — so Squirrel.Mac signature validation
passes. The failure is that quitAndInstall() drives app.quit() while two
normal-quit behaviors keep the process alive:
1. the main-window close handler hides to tray when close-to-tray is
enabled (it only closes when isQuitting is true), and
2. the before-quit dirty-editor guard preventDefault()s the quit for a
5s renderer round-trip.
Either keeps the parent process running, so Squirrel.Mac's ShipIt helper
— which waits on the parent PID to die before swapping the bundle —
lands in launchd "pending spawn / on-demand-only" limbo and the service
is removed without installing. This matches the reporter's diagnosis
exactly ("ShipIt 没有真正启动安装器", launchd on-demand-only).
Fix: before quitAndInstall fires app.quit(), mark the app as quitting
for an update via windowManager.setQuittingForUpdate(true). That sets
isQuitting (bypassing close-to-tray) and the before-quit handler now
returns early when isQuittingForUpdate() is true (skipping the
dirty-editor round-trip), so the process exits cleanly and ShipIt can
run. The same fix also covers the latent Windows NSIS case where
close-to-tray would block an in-place update.
If the install never actually quits the app (quitAndInstall throws, or
returns without app.quit() on a Squirrel follow-up error / stale
download), the quitting-for-update flags are rolled back — synchronously
on throw, and via a short unref'd watchdog otherwise — so the app does
not get stuck permanently bypassing close-to-tray and the quit guard.
Tests: unit tests for the new windowManager flags and for the install
handler — ordering (setQuittingForUpdate before quitAndInstall), tray
cleanup still runs, no-op when the updater fails to load, rollback on
synchronous throw, and watchdog rollback when the app never quits.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(auto-update): keep dirty-editor guard during update install
setQuittingForUpdate only bypasses close-to-tray (so the window actually
closes and Squirrel.Mac's ShipIt can swap the bundle); it must NOT skip the
unsaved-work guard, or clicking "Restart Now" with a dirty SFTP editor would
silently lose edits. If the user cancels to save, the quit aborts and
autoUpdateBridge's watchdog clears the quitting-for-update flags.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(auto-update): clear update-quit state when the quit is cancelled
When the user clicks "Restart Now" with a dirty editor open, the before-quit
guard cancels the quit (settle "stay"). The update path had already called
setQuittingForUpdate(true) (which flips isQuitting=true to bypass close-to-tray
for the install), so without clearing it the app stays in a quitting state —
close-to-tray and other !isQuitting-gated behavior bypassed — until the 10s
watchdog fires. Clear it immediately on the cancelled-quit path (#1215 review).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(auto-update): check for unsaved editors before quitAndInstall (#1215)
The previous fix bypassed close-to-tray so the app process actually exits and
Squirrel.Mac's ShipIt can swap the bundle, while keeping the before-quit
dirty-editor guard as the unsaved-work safety net. But on macOS that net has a
hole: quitAndInstall() closes the window FIRST and only then fires before-quit.
Once setQuittingForUpdate(true) lets the main window truly close (instead of
hiding to tray), the before-quit guard can run after the window is already gone
— isReachableByUser is false, so it commits the quit and silently drops unsaved
SFTP edits.
Fix: move the dirty-editor check to the moment the user clicks "Restart Now",
in the install handler, BEFORE setQuittingForUpdate / quitAndInstall — while the
window and renderer are still alive:
- dirty -> abort the install (don't set the quitting flags, don't
quitAndInstall) and broadcast netcatty:update:needs-save so the
renderer prompts the user to save and retry.
- clean -> proceed with the existing flow (commit-to-quit, tray cleanup,
quitAndInstall, watchdog).
- no reachable main window / crashed renderer -> install directly (no user to
ask), matching the before-quit fail-open path.
The before-quit dirty guard is kept as defense-in-depth: if the window is still
reachable it re-checks (clean, since we just verified), and if it's already gone
it lets the quit through — which is now safe because the install handler already
confirmed there were no unsaved editors.
The request/reply/timeout round-trip is extracted into a shared helper,
electron/bridges/dirtyEditorGuard.cjs (queryDirtyEditors), so the install
handler and main.cjs's before-quit guard use one implementation. main.cjs's
before-quit is refactored onto it (behavior preserved: sender-filtered reply,
fail-open timeout, and the setQuittingForUpdate(false) rollback when the user
cancels to save).
The needs-save notice is BROADCAST to every window, not just the queried main
window: "Restart to Update" can be clicked from the Settings window, which would
otherwise see the click do nothing. preload exposes onUpdateNeedsSave; the
subscription lives in useUpdateCheck (state layer), and both consumers — App.tsx
(main window) and SettingsPage (settings window) — pass an onNeedsSave callback
that shows an actionable toast ("save your editors, then click Restart Now
again") in en / zh-CN / ru.
Also lengthen the quitting-for-update rollback watchdog from 10s to 60s. On
macOS quitAndInstall() can return while Squirrel is still pulling the downloaded
ZIP from the local update server before it closes the windows; on a large/slow
update that can exceed 10s. Clearing isQuitting that early would let the eventual
native quit hit a non-quitting close-to-tray handler and strand the install
again — the exact #1215 failure. The longer window only fires when the app is
realistically stuck, at the cost of close-to-tray staying bypassed a little
longer in the rare genuine-failure case.
Tests: queryDirtyEditors (result / no-dirty / timeout / wrong-sender / dead or
crashed webContents / send-throws / no-ipcMain paths); install handler pre-check
(dirty -> no quitAndInstall + needs-save broadcast to all windows; clean ->
quitAndInstall runs; no main window -> installs without asking). Existing
install-handler tests updated for the now-async handler.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Show host info for Mosh sessions via a stats companion SSH connection
Mosh sessions run over UDP through a local mosh-client PTY and carry no
ssh2 connection (session.conn), so getServerStats could not open an exec
channel and the terminal's host-info bar (CPU/memory/disk/network) stayed
empty — unlike SSH sessions (issue #1198).
Add a best-effort, non-interactive companion SSH connection that is opened
lazily on the first stats poll for a Mosh session, reusing the credentials
the Mosh handshake already validated, and assign it to session.conn so the
existing stats path works unchanged:
- electron/bridges/sshBridge/moshStatsConnection.cjs: new helper that
builds the companion connection. It never prompts (only stored password,
parseable private key, unencrypted/stored-passphrase identity files, or
ssh-agent), shares one in-flight attempt across concurrent polls, treats
auth rejection as permanent but transient errors as retryable, and skips
host-key verification like the existing one-off execCommand path.
- sessionOps.getServerStats: establish the companion connection for Mosh
sessions that lack session.conn before running the stats command;
degrades gracefully to the existing "not connected" error otherwise.
- moshSession.swapToMoshClient: stash the handshake credentials and
algorithm settings on session.moshStatsAuth once the handshake succeeds.
- terminalBridge closeSession / cleanupAllSessions and the mosh-client exit
handler: tear down the companion connection (it has no session.stream).
- Forward legacyAlgorithms / skipEcdsaHostKey / algorithmOverrides through
the renderer's Mosh starter and the bridge type so the companion
negotiates the same algorithms the interactive session would.
Tests cover the helper (auth selection, agent fallback, dedup, permanent vs
transient failure, late-ready discard), the getServerStats integration, and
the moshStatsAuth stash + companion teardown on close.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Address codex review: target SSH host and settle on mid-handshake close
- moshSession: store options.hostname (the SSH endpoint) on moshStatsAuth
instead of parsed.host. A `MOSH IP` line advertises the UDP endpoint for
mosh-client, which can differ from the SSH host on NAT / multi-homed
setups; the companion is an SSH connection and must target the SSH host.
- moshStatsConnection: resolve the pending attempt from the "close" handler
when the socket drops mid-handshake without a prior "ready"/"error", so an
awaiting getServerStats call (and session.moshStatsConnPromise) cannot
hang indefinitely. Treated as transient so the next poll may retry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Address codex review: keyboard-interactive password for stats companion
PAM-backed SSH servers often offer password auth only via
keyboard-interactive, not the plain "password" method. The Mosh handshake's
system ssh handles that through its PTY responder, so without it the
companion stats connection would fail auth on those hosts even with a saved
password, leaving the stats bar empty.
When a saved password is present, enable tryKeyboard and attach a
non-interactive keyboard-interactive handler that auto-fills the password
for a single password prompt (using the existing
isAutoFillablePasswordChallenge predicate) and finishes empty on
2FA/OTP/multi-prompt challenges. It auto-fills at most once so a wrong
password can't drive a retry loop, and it never shows a modal — the
companion stays fully non-interactive.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Address codex review: verify host key before sending a saved password
A background companion connection that auto-submits a saved password to an
unverified host could disclose it to a spoofed / MITM server (P1). Gate
password auth (plain and keyboard-interactive) behind a silent, trusted-only
host-key check against Netcatty's known-hosts store:
- moshStatsConnection: when the companion would authenticate with a
password, attach an ssh2 hostVerifier that accepts only a key already
"trusted" in known-hosts (via hostKeyVerifier.classifyHostKey) and rejects
unknown/changed keys outright — no prompt, so the password is never sent to
an unvetted host and no host-key dialog pops for a background poll.
Public-key / agent auth proves possession via a signature and discloses no
reusable secret, so it is not gated (matches the existing execCommand
precedent; the handshake already vetted the host via system ssh).
- Thread knownHosts through the renderer Mosh starter, the bridge type, and
session.moshStatsAuth.
Note: a password-auth Mosh host that Netcatty has never seen via its own SSH
path (so it is absent from Netcatty's known-hosts) will not get the stats
companion until its key is known — the safe default. Key/agent-auth hosts are
unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Address codex review: transient pre-handshake polls and agent+password auth
Two functional gaps in the stats companion:
- Missing moshStatsAuth is now transient, not permanent. The renderer can
mark a Mosh session "connected" (and start polling) from the SSH
bootstrap's visible PTY output before the swap to mosh-client assigns
moshStatsAuth. Previously that first poll set moshStatsConnFailed
permanently, so the companion was never attempted after the handshake
actually completed. Now it just returns null and a later poll retries.
- A saved password no longer suppresses ssh-agent auth. A public-key host
that authenticates via the agent may still carry a stored password; the
companion now offers the agent alongside the password (ssh2 tries agent
first) instead of attempting password-only and failing permanently. An
explicit private key still suppresses the agent fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Address codex review: gate password at authHandler, not whole connection
The previous fix installed a trusted-only hostVerifier whenever a password
was present. Because ssh2 verifies the host before any auth method, that
rejected the entire connection on a host absent from Netcatty's known-hosts
— blocking key/agent auth too, even though those never need to send the
password.
Move the gate from the transport to the auth layer:
- A trust-tracking hostVerifier records whether the live host key is trusted
(during the transport handshake) and then accepts the transport so
public-key / agent auth can proceed on any host.
- A function-form authHandler offers none -> agent -> publickey always, and
appends password + keyboard-interactive only when the host key is trusted.
Result: key/agent auth works on hosts Netcatty hasn't vetted, while a saved
password is still never sent to an untrusted host. Public-key / agent auth
remains ungated (no reusable secret is disclosed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Address codex review: isolate Mosh stats connection from session.conn
Storing the stats companion on session.conn made it look like the session's
primary interactive SSH connection. Other bridges key off session.conn —
getSessionPwd assumes its exec channel is a sibling of the interactive shell,
and SFTP / MCP exec run over session.conn — but a Mosh session's shell lives
on the UDP mosh-client, not this background connection. After a stats poll
they could return a bogus cwd or operate over the wrong connection.
Keep the companion strictly on session.moshStatsConn:
- ensureMoshStatsConnection stores/reuses/clears only session.moshStatsConn.
- getServerStats reads session.conn || session.moshStatsConn (real SSH still
uses conn; Mosh uses the companion) and only opens one when neither exists.
- closeSession / cleanupAllSessions / the mosh-client exit handler tear down
session.moshStatsConn.
This leaves session.conn untouched for Mosh, so getSessionPwd / SFTP / MCP
exec behave exactly as before (no primary SSH connection for Mosh).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Address codex review: don't count pre-handshake Mosh polls as failures
useServerStats gives up after 3 consecutive failures. A Mosh session can be
marked "connected" (and start polling) from the SSH bootstrap's visible
output before swapToMoshClient stores moshStatsAuth, during which
ensureMoshStatsConnection returns null. Previously getServerStats reported
that as a normal failure, so a handshake taking ~15s (3 polls) would
permanently disable stats for the session even after credentials became
available.
Introduce a `pending` result:
- getServerStats returns { success: false, pending: true } for a Mosh session
that has no connection yet and no moshStatsAuth and hasn't permanently
failed. Once moshStatsAuth is set (or the companion permanently fails), it
reports a normal failure again.
- useServerStats treats `pending` as neutral: it does not update stats and
does not increment the consecutive-failure counter, so polling continues
until the handshake completes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Address codex review: verify host key for all Mosh stats companion auth
The companion installed a trust-tracking host verifier only when a saved
password was present; key/agent-only connections fell back to ssh2's
default of accepting any host key. A background, user-invisible connection
that authenticated against an unverified host could let a MITM/DNS-spoofed
host feed bogus host-info to the user and enumerate the ssh-agent's public
keys — breaking the host-key guarantee the interactive session enforces.
Attach the host verifier for every auth method and reject an unknown or
changed host key outright (never prompting; stats just stay empty). Treat
an untrusted host as a permanent failure so polling stops reconnecting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(mosh): trust system known_hosts for the stats companion host-key check
The Mosh stats companion opens a background ssh2 connection and only rides
on a host whose live key is already trusted, rejecting unknown/changed keys
as a permanent failure. Trust was sourced solely from Netcatty's in-app
known-hosts snapshot (options.knownHosts).
But a Mosh session is bootstrapped by the system `ssh`, which vets and
records the host key in the user's OpenSSH known_hosts (~/.ssh/known_hosts,
etc). Netcatty's snapshot is never updated by that handshake, so a host
trusted purely via system ssh was misread as "unknown" and the companion
permanently disabled — Mosh stats never appeared unless the user manually
scanned/imported the host into Netcatty (codex P2).
Add a system-known_hosts trust source (systemKnownHosts.cjs) and consult it
in the companion verifier when the in-app snapshot does not already vouch
for the key. Matching is by the LIVE key's SHA-256 fingerprint, so trust is
granted only for the exact key the user's own OpenSSH already trusts; an
arbitrary or mismatched key is never accepted. Unknown/changed keys stay
rejected and remain a permanent failure.
The parser handles the OpenSSH known_hosts(5) format that the in-app scan
parser does not fully cover for matching: plain hosts, comma lists,
[host]:port, hashed |1|salt|HMAC-SHA1(salt,token) entries (with the
bracketed token for non-default ports, verified against ssh-keygen -H),
multiple key types, @revoked (forces NOT trusted) and @cert-authority
(skipped). Wildcard/negation patterns are deliberately not honored. Paths
mirror localFsBridge.readKnownHosts and are cross-platform (incl. Windows
%PROGRAMDATA%\ssh\known_hosts). All errors fail closed.
ssh2 ships no known_hosts parser, so this is implemented in CommonJS using
node:crypto and covered by unit tests (real ssh-keygen hashed fixtures,
revoked/cert-authority, fingerprint mismatch, fail-closed) plus companion
integration tests (system-only trust accepts; neither source trusts ->
rejected + permanent; key rotation not rescued; optional-dependency safety).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Add startEtSession to the preload API surface, routing through the
netcatty:et:start IPC channel. Define the startEtSession options type
in netcatty-bridge-session.d.ts with ET-specific parameters (etPort,
terminalPath, jumpHosts, etc.).