* Fix terminal backlog when window is unfocused in background.
Extend the background write fast path to occluded-but-visible windows (common on Windows Alt+Tab), skip wasteful repaints during that path, and flush any remaining coalescer/queue backlog when the app returns to foreground.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix resume hook wiring and keep unfocused repaint in fast path.
The onWindowShown resume flush never fired because useTerminalBackend did not expose the bridge listener. Also restore scheduleTerminalRepaintWhenUnfocused on the background fast path: unfocused-but-visible windows have no rAF render loop, so skipping it froze visible output (#1761 regression).
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Tighten renderer output back-pressure (128KB/32KB watermarks), account
coalescer and write-queue backlog, drain output on user keystrokes, and
add main-process IPC ACK flow control so SSH floods cannot starve Ctrl+C
and other keyboard input.
Closes#1711
- 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.
Record commands as users type across sessions with dedup and AI-noise
filtering, and browse them alongside per-host remote history. Refine the
scope switcher UI and route fullscreen layout recovery through the terminal
backend hook. Closes#1253.
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.
Resolve 'et' protocol in tray panel and host connection handlers with
priority over mosh. Propagate etEnabled through session factories and
useSessionState. Expose etAvailable guard and startEtSession wrapper
in useTerminalBackend.
The hook returned a fresh object literal every render. The 26 methods
inside were already useCallback([])-stable, but the wrapping object
was not — so every consumer's effect with `terminalBackend` in deps
(e.g. cwd polling, lifecycle wiring, write-to-session) re-ran on
every parent render even though nothing semantic had changed, and
ESLint flagged the one site that depended on a property access
(`terminalBackend.onHostKeyVerification`) because it could not prove
that path safe.
Wrap the return in useMemo with all stable callbacks listed as deps
so the object is computed once and cached for the hook's lifetime.
Switch the host-key-verification effect's dep to the now-stable
`terminalBackend`, clearing the warning at the root rather than
patching it locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: auto-detect network devices from SSH banner and skip stats polling (#674)
Fixes rapid AAA session churn reported on Cisco/HPE/similar network
devices running Netcatty. The root cause was two separate polls that
both open fresh exec channels (each counted as its own AAA session on
many network devices):
- runDistroDetection() opens a brand new SSH connection every time a
host connects to run `cat /etc/os-release || uname -a`
- useServerStats polls `conn.exec(statsCommand)` every 5 seconds
Both commands fail on non-POSIX CLIs, but the channels still hit AAA.
This change avoids both by reading the SSH server identification
string that ssh2 already captures during the handshake
(`conn._remoteVer`). No extra network round-trips, zero additional
AAA entries.
## Changes
**sshBridge.cjs**
- Store `conn._remoteVer` on the session object at connect time as
`session.remoteSshVersion`
- New IPC handler `netcatty:ssh:remoteInfo` (`getSessionRemoteInfo`)
returning the captured SSH server software string
**preload.cjs / global.d.ts / useTerminalBackend.ts**
- Thread `getSessionRemoteInfo(sessionId)` through to the renderer
**domain/host.ts**
- `NETWORK_DEVICE_OPTIONS` constant listing the vendor IDs we can
recognize (cisco, juniper, huawei, hpe, mikrotik, fortinet,
paloalto, zyxel)
- `detectVendorFromSshVersion()` — pure function that parses an SSH
server software string and returns a vendor ID or ''. Pattern set
is sourced from Nmap nmap-service-probes (authoritative), the
ssh-audit software.py reference, and vendor docs; see code
comments for the exact matches used.
- `classifyDistroId()` returns `linux-like | network-device | other`
so features that require a POSIX shell can gate on the result.
**createTerminalSessionStarters.ts (runDistroDetection)**
- Before running the /etc/os-release probe, call
`getSessionRemoteInfo` on the already-connected session and feed
the banner into `detectVendorFromSshVersion`. If the vendor maps
to a known network device, emit the vendor ID via the existing
`onOsDetected` callback and skip the shell probe entirely. For
unknown or generic OpenSSH/Dropbear banners the existing behavior
is preserved.
**Terminal.tsx**
- `isSupportedOs` now derives from `classifyDistroId(effectiveDistro)`
combined with `host.deviceType !== 'network'`, so neither explicit
network-device hosts nor banner-detected ones trigger the stats
polling loop.
**useServerStats.ts**
- Add a consecutive-failure counter. After 3 consecutive failed
polls, stop the interval for this session (reset on disconnect /
sessionId change / settings toggle). This is the fallback for
hosts the banner classifier cannot identify (Juniper JUNOS,
Cisco NX-OS, Arista EOS — all present as plain `OpenSSH_*` but
do not support the POSIX stats pipeline).
**DistroAvatar.tsx / HostDetailsPanel.tsx**
- Add 8 network-device vendor icons (Cisco, Juniper, Huawei, HPE,
MikroTik, Fortinet, Palo Alto, ZyXEL) alongside the existing
Linux distro icons, with brand colors. Icons sourced from Simple
Icons (CC0) where available; HPE and ZyXEL use simple
abbreviation placeholders.
- Network device vendors are added to the manual distro override
dropdown so users can pin an icon even if their device has an
exotic banner we don't auto-detect.
**i18n**
- English + Chinese labels for the new vendor options in the
Host Details distro selector.
Closes#674
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: gate network-device detection on raw host.distro, not manual icon override
Per Codex review on PR #680: the stats-polling gate was passing
`host` through getEffectiveHostDistro() before classifying, which
honors the manual distro override (`distroMode: 'manual'` +
`manualDistro`). That meant a user who previously pinned an
"ubuntu" icon on a host that later gets banner-detected as Cisco
would still be classified as linux-like and keep generating the
AAA session flood #674 is meant to eliminate.
Separate display from gating:
- Display (DistroAvatar, host cards): keeps using
getEffectiveHostDistro so users can cosmetically override the
icon.
- Gating (useServerStats via Terminal.tsx isSupportedOs): reads
host.distro directly — the value populated by banner detection —
alongside the explicit host.deviceType flag. Manual icon choice
can no longer re-enable polling on a detected network device.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: guard distro detection against stale session timers
Per Codex review on PR #680: runDistroDetection is scheduled on a
600ms setTimeout after connection and also makes async calls of its
own. A quick disconnect + reconnect on the same session slot could
fire the old timer against the new session, reading host B's SSH
banner via getSessionRemoteInfo and writing host B's vendor onto
host A's distro field — wrong icon and wrong stats-polling state.
Follow the same pattern already used for the startup-command timer
in this file (scheduledSessionId captured at schedule time, checked
inside the timer). Capture `id` at schedule time, bail out if
ctx.sessionRef.current no longer matches, and re-check after every
async await inside runDistroDetection so that a reconnect during
the banner fetch or the os-release probe also bails cleanly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address local codex review on PR #680
Addresses three issues found in a local Codex review pass after the
remote reviewer gate was flaky:
## P0 — session tokens instead of sessionId for stale-timer guard
The previous guard captured `id` returned from startSSHSession and
compared against `ctx.sessionRef.current` inside the setTimeout and
the async runDistroDetection. But the renderer passes
`sessionId: ctx.sessionId` into startSSHSession (see
createTerminalSessionStarters.ts:543), meaning a tab reuses the
SAME sessionId across disconnect+reconnect. The comparison
`T1 === T1` always passed, so the guard was a no-op.
Replaced with a module-level Map<sessionId, object> that stores the
live "connection token" for each sessionId slot. Each call to
startSSH mints a fresh `{}` token and overwrites the entry. Timers
and async continuations compare their captured token against the
current map value by reference — a reconnect replaces the map entry
with a new token, so stale callbacks bail cleanly.
## P1 — run os-release probe on the existing SSH connection
The fallback /etc/os-release probe used `execCommand` which creates
a brand-new SSHClient() on every call. On network devices that
present as plain `OpenSSH_*` and fall through to this step
(JUNOS, NX-OS, EOS) it added one extra full-auth AAA session log
entry per connect, in addition to the failing stats polls.
Added `getSessionDistroInfo(sessionId)` as a new IPC handler that
runs the same probe via `session.conn.exec()` — an exec channel on
the already-open connection, no new handshake. Plumbed through
preload.cjs, global.d.ts, and useTerminalBackend.ts.
runDistroDetection uses this instead of execCommand in the fallback
path, also removing the unused auth-credentials argument (we are no
longer opening a new connection, so no credentials are needed).
## P2.1 — don't re-arm timers after giving up
After the consecutive-failure counter trips, useServerStats cleared
the interval but a subsequent effect rerun (visibility change,
settings tweak, etc.) would schedule a fresh `setTimeout` and
`setInterval` that would just call the early-return path forever.
The scheduling block now checks `givenUpRef.current` before arming
either timer. The flag is still cleared on the normal disconnect /
sessionId-change reset path so a reconnect gets a fresh attempt.
## P2.2 — drop the ambiguous IPSSH-* → cisco mapping
Nmap's `match ssh m|^SSH-([\d.]+)-IPSSH-` line is labelled as
`Cisco/3com IPSSHd` — it cannot identify a specific vendor from the
banner alone. Mapping it to `cisco` would risk showing the wrong
vendor icon on a 3Com device. Removed the rule entirely and
documented why with a code comment; users with such devices can
still use the Host Details manual distro override.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address remaining gaps from local codex follow-up review
P0 gap — delete connection token on session exit. Previously the map
entry lingered after disconnect, so a very late-firing timer could
still pass the isConnectionTokenCurrent check even though the session
no longer existed. Functionally harmless (the IPC calls would fail)
but semantically wrong. Now connectionTokensBySessionId.delete() is
called in the onSessionExit handler.
P1 new — exec channel leak on timeout in getSessionDistroInfo. The
timeout branch resolved the promise but didn't close the stream, so
a hanging remote command would leave the exec channel open until the
SSH connection itself dropped. Added a settled guard (resolve-once)
and stream.close() on timeout.
P2.1 gap — givenUpRef not reset on sessionId change. The failure
counter reset only happened in the !isConnected branch of the main
effect, so a sessionId swap while still connected (rare, but
possible if the tab reconnects without toggling connected state)
would permanently suppress polling. Added a small dedicated effect
that resets both counters when sessionId changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve SSH chain connection hang and improve connection progress
- Fix Promise never settling when conn 'close' fires before 'ready'
during chain connections, which caused "reply was never sent" error
- Replace fake timed progress animation with real backend events
- Send granular connection progress for all SSH connections (not just
chain), including: connecting, key exchange, auth attempts, forwarding,
shell opening
- Surface auth method attempts (SSH agent, key names, password) in
progress logs so users can diagnose authentication failures
- Include error details in progress events for better error visibility
Closes#463
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: scope progress events by sessionId, prevent duplicate errors, hide chain UI for direct SSH
- Add sessionId to chain progress payload so events are scoped per session (P1)
- Set settled=true in error/timeout handlers to prevent close handler from
emitting a second misleading 'closed unexpectedly' error (P2)
- Only show chain progress UI when total > 1 so direct SSH connections
don't render as 'Chain 1/1' (P3)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: mark shell-open failure as settled before closing connection
The conn.shell() error branch calls conn.end() which triggers the close
handler, but settled was not set yet, causing a duplicate 'closed
unexpectedly' error to overwrite the real shell-open failure message.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: auto-close tab when user actively exits a session
When a user intentionally exits a session (e.g. typing `exit`, `logout`,
or Ctrl+D), the tab is now automatically closed instead of showing the
"Start Over" disconnected page. This matches the behavior of macOS
Terminal and other popular terminal emulators.
Network errors, timeouts, and server-initiated disconnects still show
the disconnected page with the Start Over option, so users can reconnect.
In workspace mode, only the individual terminal pane is closed, not the
entire workspace.
Implementation:
- Backend bridges now include a `reason` field in exit events to
distinguish stream-level exits ("exited") from connection errors
("error"), timeouts ("timeout"), and connection closes ("closed")
- SSH bridge captures real exit code from stream "exit" event instead
of hardcoding 0
- Frontend auto-closes session only when reason is "exited"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address review feedback for auto-close feature
1. Pass exit event to onSessionExit in local shell path (line 757)
to prevent undefined access when checking evt.reason
2. Change Telnet socket close reason from "exited" to "closed" since
a clean socket close can also be server-initiated (idle timeout,
remote shutdown), not just user exit
3. Change Serial port close reason from "exited" to "closed" since
port close can be from device disconnect, not user action
Only SSH stream close and local/mosh process exit (node-pty onExit)
now use reason "exited", which correctly represents user-initiated exits.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: only mark SSH exit as "exited" when stream exit event fired
ssh2's stream "close" event fires whenever the channel closes, not
only on normal shell exit. If the network drops and the channel closes
without a preceding "exit" event, the reason was incorrectly set to
"exited", causing the tab to auto-close instead of showing the
disconnected/Start Over page.
Now tracks whether stream "exit" actually fired via a flag, and only
uses reason "exited" in that case. Otherwise falls back to "closed".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: classify mosh non-zero exits as errors
Mosh process exiting with a non-zero code typically indicates a
connection or auth failure. Mark these as reason "error" so the
disconnected/Start Over UI is shown instead of auto-closing the tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: treat SSH signal-terminated exits as disconnects
ssh2's stream "exit" event also fires for signal terminations (e.g.
SIGHUP from server idle timeout, SIGTERM from admin kill), where code
is null and signal is set. These are not user-initiated exits and
should show the disconnected/Start Over page.
Now only sets streamExited=true when there's a numeric exit code and
no signal present.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: distinguish abnormal local PTY exits from user exits
Local shell terminated by signal or crashing on startup should show
the disconnected UI, not auto-close the tab. Now only marks as
reason "exited" when exitCode is 0 and no signal, matching the same
logic used for mosh.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use signal presence to distinguish local shell exit reason
For local shells, non-zero exit codes are common in user-initiated
exits (e.g. typing `exit` after a failed command returns that
command's exit code). Use signal presence instead: signal means the
process was killed externally (show disconnected UI), no signal
means normal process exit (auto-close tab).
Mosh keeps exitCode-based logic since non-zero there indicates
connection/auth failure, not user exit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Allow users to switch between UTF-8 and GB18030 encoding mid-session
via a toolbar popover, fixing garbled output when viewing mixed-encoding
logs on remote servers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add real-time monitoring of Linux server resources in the terminal statusbar:
- CPU usage with per-core breakdown in hover popup
- Memory usage with htop-style colored bar (Used/Buffers/Cache/Free)
- Top 10 processes by memory consumption
- Disk usage for all mounted partitions
- Network speed (RX/TX) with per-interface details
Features:
- Only enabled for Linux servers (detected automatically)
- Configurable refresh interval (default 5 seconds)
- Toggle in Settings > Terminal tab
- Hover-triggered popups using Radix UI HoverCard
- BusyBox compatibility with fallback commands
Closes#108
Co-Authored-By: Claude <noreply@anthropic.com>
Integrates Monaco Editor for enhanced syntax highlighting in the text editor modal, improving editing experience for code and config files.
Tracks and synchronizes the terminal's current working directory via OSC 7 escape sequences, enabling SFTP dialogs to open at the detected shell path even after user or directory changes.
Introduces IPC and backend support for querying the current shell directory from active SSH sessions, with fallback and path validation logic for remote SFTP browsing.
Improves file type detection by analyzing file content in addition to extensions.
Updates i18n strings and UI elements for clarity and future system application integration.
Adds necessary dependencies for Monaco Editor and updates content security policy for web workers.
Replaces all direct usage of browser globals and infrastructure service imports in UI components with dedicated application/state backend hooks. Introduces lint rules to prevent direct access to backend bridges and localStorage from components, promoting a cleaner separation of concerns and improved maintainability.
Moves user preferences (e.g., port forwarding form mode) to persistent state hooks, updates port forwarding and SFTP logic to rely on backend hooks, and centralizes logging through a logger utility. Cleans up debug code and removes obsolete scripts from HTML.
Improves testability, prepares for alternative backend implementations, and enforces architectural boundaries.