* Add telnet URL deep link support
* Fix telnet deep link review issues
* Keep ssh deep links independent from telnet registration
* Address Codex telnet deep link feedback
* Support JumpServer jms:// client launch links
Register Netcatty as an optional jms:// protocol handler (opt-in,
default off to avoid conflicting with the official JumpServer client).
SSH/SFTP connection payloads decode to an ephemeral host that connects
to the JumpServer gateway with the one-time connection token, without
saving anything to the vault.
Fixes#1958
* Mark jms deep link hosts as ephemeral for session restore exclusion
* Open the SFTP side panel for JumpServer sftp launch links
jms:// payloads with protocol "sftp" previously landed in a plain
shell. Mark the ephemeral host with an autoOpenSftpPanel hint that the
session factory forwards as autoOpenSidePanel, and let TerminalLayer
open the SFTP side panel for that session once it connects.
* Support JumpServer telnet deep links
* Keep JumpServer telnet links on SSH gateway
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Support password in ssh deep links with ephemeral connect
ssh://user:password@host URLs now connect directly using the URL
credentials without saving the host to the vault, covering bastion
one-time password flows. Password-less links keep the existing
match-or-prefill behavior.
Fixes#1890
Co-authored-by: Cursor <cursoragent@cursor.com>
* Keep ephemeral deep-link hosts out of vault update paths
Terminal-layer host updates (e.g. the SFTP follow-CWD toggle) map over
the merged terminal host list, which could persist an ephemeral host
and its plaintext password into the vault. Route terminal-layer host
updates through a splitter that applies ephemeral entries to in-memory
state only and forwards just the vault hosts to updateHosts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Exclude ephemeral deep-link sessions from session restore
Sessions backed by an in-memory ephemeral host cannot survive a
relaunch (the one-time password only lives in memory), so persisting
them would leak the ephemeral host metadata into restore storage and
restore a tab that can never reconnect. Mark ephemeral hosts and their
sessions, and filter them out of the session-restore payload.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Treat ephemeral hosts as unsaved in appearance classification
The terminal theme/font panel and pane appearance resolution decided
"saved vault host" via hostMap membership. Ephemeral deep-link hosts
are now in that map, so theme/font changes were routed to the
persisted-host updater and silently dropped. Classify via
isSavedVaultHost (ephemeral flag aware) so those changes fall back to
the session/global setting path as before.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Reuse saved host settings for password deep links and keep ephemeral marker on clones
Password deep links now run saved-host matching first: a uniquely
matched vault host contributes its non-credential settings (proxy,
jump chain, charset, group, ...) to the ephemeral host while the URL
credentials override vault identities and key references. Session
clones (split/copy/copy-to-new-window) now carry the ephemeralHost
marker so cloned tabs stay excluded from session restore.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Block group credential inheritance on password deep-link ephemeral hosts
buildSshDeepLinkEphemeralHostFromSaved now takes the group-resolved
effective host and clears group/identityFilePaths in addition to
identity references, so a later resolveEffectiveHost pass cannot
re-inherit group default credentials (identity, key, password) over
the one-time URL password. Non-credential group defaults (proxy,
jump chain, charset, ...) are preserved via the effective host.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Route ephemeral-host font zoom through the per-session font-size path
Ctrl+zoom on a non-workspace ephemeral deep-link session hit the
saved-host update path, where the vault updater silently dropped the
change (ephemeral ids are not in the persisted hosts array). Zoom and
the theme panel font-size slider/reset now store the size on the
session (rendered via applySessionFontSizeToHost) for ephemeral
hosts, matching the workspace-pane behavior.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix ephemeral session font zoom routing
* Prevent ephemeral deep-link sudo autofill
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(terminal): detach stale keychain identity when saving retry credentials
When a host references a Keychain identity (host.identityId) with wrong
credentials, re-entering credentials via the auth-retry dialog's
"Continue & Save" saved them onto the host but kept the identity
reference. On reconnect, resolveHostAuth prefers identity credentials
over host fields, so the stale keychain credentials kept failing.
Clear identityId when the user explicitly saves new credentials so the
saved username/password (or key) actually takes effect.
Fixes#1956
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(terminal): keep saved retry credentials over group-inherited identity
Writing identityId: undefined is treated as "unset" by applyGroupDefaults,
so a group-level identity would be re-inherited on reconnect and shadow the
credentials saved from the auth-retry dialog. Store an empty string instead
and register identityId in EMPTY_STRING_OVERRIDES_GROUP_DEFAULT so the
explicit "no identity" override sticks. All consumers check identityId
truthiness, so "" behaves as "no identity".
Addresses Codex review on #1970.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Re-inject tool call names on OpenAI-compatible stream desync
Some OpenAI-compatible gateways emit tool_call chunks the AI SDK
rejects or ignores (non-standard `type` values, tool calls outside
choices[0]). The chunk normalizer had already recorded the tool call
id, so later nameless continuation chunks were forwarded with an
injected id and the SDK threw "Expected 'function.name' to be a
string." on what it saw as a brand-new tool call.
Remember each tool call's function name when its id is assigned and
re-inject it (plus type "function") into continuation chunks so every
forwarded chunk is self-describing, and coerce non-standard tool_call
type strings to "function" so the naming chunk itself is not dropped
by schema validation.
Fixes#1954.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Preserve argument prefixes from SDK-invisible tool call chunks
Codex review on #1966 flagged that when the chunk establishing a tool
call is invisible to the AI SDK (which only reads choices[0]), any
argument fragment it carried was lost: the recovery path re-injected
the name but forwarded only the suffix arguments, producing truncated
tool input.
Track the full concatenation of argument fragments per tool call key
and which keys have been forwarded at choices array position 0. The
first chunk forwarded where the SDK can see it now carries the full
accumulated arguments; already-visible tool calls keep streaming
incremental fragments as before.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(terminal): raise SSH flow high-water mark to stop WAN pause ping-pong (#1961)
Bulk terminal output over SSH (e.g. `tail -2000f big.log`) was far slower
than other clients on high-latency links. The renderer's output flow control
pauses the source stream once its unacked backlog crosses FLOW_HIGH_WATER_MARK
(previously 100KB). For SSH the source is the ssh2 channel, so pause() stops
the remote until resume() plus a full round-trip.
ssh2's own channel window is 2MB (WINDOW_THRESHOLD 1MB), so a 100KB watermark
made Netcatty pause/resume dozens of times during a multi-MB dump — a 4MB dump
triggered ~61 pause cycles, each costing ~1 RTT. At 300ms RTT that turned a
~2s dump into ~21s.
Raise FLOW_HIGH_WATER_MARK to 1MB so it sits near the ssh2 window; the same
4MB dump now settles in ~4 pause cycles (~2-3s at 300ms RTT) while still
bounding renderer memory. FLOW_LOW_WATER_MARK stays at 8KB to keep interactive
acks responsive.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(terminal): align deferred-ack test with 1MB flow high-water mark
The deferred IPC ack batch threshold (XTERM_WRITE_CALLBACK_BATCH_BYTES)
now sits far below FLOW_HIGH_WATER_MARK, so deferral alone can no longer
accumulate up to the watermark. Assert the batch-flush invariant instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(zmodem): keep transfer listeners across same-session reconnects
The preload netcatty:exit handler dropped the per-session zmodem
listener sets, but auth-failure retries and auto-reconnects reuse the
same sessionId without remounting the terminal, so useZmodemTransfer
never re-subscribed and rz/sz progress + overwrite dialogs silently
stopped appearing (#1955). Move listener cleanup to explicit
closeSession and prune empty sets on subscriber dispose instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(zmodem): gate event delivery instead of dropping listeners on close
Codex review caught that reconnects also call closeSession with the
terminal still mounted (startReconnect -> cleanupSession), so clearing
listener maps there reintroduced the bug for manual/auto reconnects.
Keep listener sets owned by renderer subscribers and gate zmodem event
delivery on closedTerminalDataSessions, which is cleared when a session
restarts with the same id.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix directory tracking setup after su / sudo su (#1942)
After switching users in the terminal (e.g. `sudo su -`), SFTP directory
following stops: the new user's shell has no OSC 7 prompt hook, and the
backend pwd probe runs on an exec channel owned by the login user, which
cannot read another user's /proc/<pid>/cwd. The advertised remedy — the
OSC 7 setup action — also failed in this exact scenario because its silent
exec-channel script cannot verify or configure a foreground shell owned by
another user.
The setup script now detects that the active foreground shell belongs to a
different uid, reports the target shell via a marker (exit 5), and the
renderer falls back to typing the setup command into the terminal itself,
where it runs as the target user. The config lands in that user's rc file,
so OSC 7 reporting resumes immediately and persists for future su sessions.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address Codex review: stage typed setup remotely, forward shell-local config paths
The typed fallback previously pasted the whole multi-line setup script into
the terminal, which could leave many entries in the target user's bash
history when cmdhist is off (the appended history -d only removes the last
one). The setup script is now staged into a self-deleting remote temp file
over the silent exec channel, and the typed command becomes a short single
line — one history entry that the existing bash cleanup reliably removes.
The typed wrapper also forwards shell-local (possibly unexported) ZDOTDIR /
XDG_CONFIG_HOME to the child sh via the NETCATTY_* overrides the script
already honors, so zsh users with a custom config dir get the prompt hook in
the right rc file (mirrors buildOsc7SetupCommand).
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address Codex review: hash-verify the staged setup script before the su shell runs it
The staged temp file stays writable by the login user until the typed
command runs, so a same-uid process could swap its contents in that window
and have the su-target (e.g. root) shell execute arbitrary code. The typed
runner now reads the file into memory once, deletes it, verifies the
renderer-computed SHA-256 of the in-memory copy, and only then pipes that
same copy to sh — no TOCTOU window between verification and execution.
Verification failure prints an error instead of running anything.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
The "Authentication failed…" retry prompt and progress-log line in
createTerminalSessionStarters were hardcoded English and bypassed i18n,
so they rendered in English regardless of the selected language.
Route both through the existing tr() helper and add
terminal.auth.retryMessage / terminal.auth.retryLog to en, ru, zh-CN
and zh-TW.
* Run agent commands in a subshell so failures cannot close the session
Agent-generated commands were eval'd directly in the user's active login
shell, so a generated script containing `set -e` (or `exit`) terminated
the whole SSH session on the first failure. Wrap the eval in a subshell
unconditionally instead of trying to detect dangerous commands — the
detection approaches from PRs #1852/#1882 grew into shell parsing and
were abandoned. Output stays live in the user's terminal; the trade-off
is that cd/export no longer persist between calls, which the tool
descriptions now tell the model about.
Also fix multi-line command display: synthetic echo now normalizes line
breaks to CRLF (no more staircase indentation in xterm), and the POSIX
wrapper rebuilds newlines via printf so the typed command stays on one
physical line and PS2 continuation echoes ("> ...") no longer leak past
the marker filter.
Fixes#1850
Co-authored-by: Cursor <cursoragent@cursor.com>
* Move shellUtils require to module scope in cattyExecHandlers
The inline require("./ai/shellUtils.cjs") inside the with(ctx) block did
resolve correctly at runtime (bare `require` hits ctx.require, based in
electron/bridges/), but the path only works through that indirection and
reads as wrong relative to this file. Require at module scope with the
unambiguous ../ai path instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ai): allow OpenCode native skill dirs in permission rules
Netcatty's OpenCode permission config denied external_directory access
for everything outside its own allowlist, so skills discovered by
OpenCode itself (~/.opencode/skills, ~/.config/opencode/skills,
~/.claude/skills, ~/.agents/skills, remote skill cache) could not read
their reference files. OpenCode evaluates rules last-match-wins and its
read permission matches worktree-relative paths, so wildcard-anchored
patterns for the well-known skill directories are added to both the
read and external_directory rules in MCP and skills+cli modes.
Fixes#1939
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ai): re-deny dot-env files inside OpenCode skill dir allows
The broad skill-directory read allows would win over OpenCode's default
.env / .env.* approval gates (last matching rule wins), silently exposing
secret files placed inside native skill directories. Append targeted deny
rules after the allow entries so dot-env files stay protected.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix manual session log capture in terminal worker mode
Manual session logs (and script session logs) start their write stream in
the main process, but with the default terminal worker runtime all session
output is appended to the worker process's own sessionLogStreamManager
instance, so the saved file only ever contained the initial prompt line.
Tap the worker output mirror into the main-process stream manager so
main-process log streams receive terminal output, and mirror sudo-autofill
and programmatic command log rewrites when forwarding writes to the worker
so those logs stay sanitized like worker-owned auto session logs.
Fixes#1938
Co-authored-by: Cursor <cursoragent@cursor.com>
* Dedupe fallback worker output before session log taps
The runtime sender announces every chunk with an output-tap message and
falls back to a plain output message when the output port is unusable;
the manager notified taps for both, double-writing manual/script session
logs in the fallback path. Mark fallback output as already tapped so taps
fire exactly once per chunk.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Every output chunk was walked char-by-char up to four times in JS
(sync-block filter, erase-scrollback rewrite, replay-safe log sanitizer,
line-timestamp segmenter) plus two more per-char scans for long-line
pressure, before xterm.js ever parsed it. Replace those loops with
native indexOf/regex hops and single-slice appends, and gate the
quadratic pending-suffix scan in the sync filter behind a cheap
incomplete-escape check. Output is byte-for-byte identical (fuzzed
old-vs-new over 4000 mixed chunks); colored-output floods drop from
~350ms to ~0.03ms per 64KB batch in the sync filter alone.
Co-authored-by: Cursor <cursoragent@cursor.com>
* 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>
* Add macOS Dock new connection menu
* Fix Dock menu window readiness
* Wait for renderer before Dock menu delivery
* Queue Dock host connects until vault ready