preventDefault on the first dragstart aborts the only HTML5 drag, so a
slow reorder that is still under 4px never reaches onTabDragStart. Leave
dragstart alone, start reorder chrome once travel hits 4px, and activate
on release when that drag cancelled the click and no reorder started.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Chromium fires dragstart after about 2px on a draggable tab and drops the
click. Track the primary press and preventDefault below 4px so activation
still runs, while deliberate reorders keep onTabDragStart.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Fixes#3886
Primary-button mousedown on plugin/editor/session/log-view top-tab close buttons now preventDefault+stopPropagation so HTML5 drag on the parent tab cannot swallow the click.
The list-models pipeline already served LKG on failure; this wires the
rest of the LKG-first, manual-refresh policy end to end.
Main: the list-models IPC accepts a force flag — manual refresh always
skips the in-memory cache and the persisted entry and goes live, and a
forced success overwrites the persisted catalog.
Renderer: the panel passes force only from the retry banner and the
picker's refresh button; panel-visibility and send-preflight loads stay
non-force and replay the renderer cache, so with a persisted catalog
main answers them without spending the live-fetch budget.
Picker: a footer refresh icon-button next to the status chip, visible
whenever any fetched catalog (live or last-known-good) or the degraded
warning is present; the spinner row remains the in-flight feedback.
Tooltip localized as ai.chat.refreshModelList in en/es/ru/zh-CN/zh-TW.
Fresh installs are unchanged (no persisted entry → live fetch as
before); existing users stop seeing jitter warnings on panel open and
pick up new CLI models via the refresh button.
A failed live catalog fetch serves the persisted last-known-good catalog
with a warning, and every TTL-expired automatic load re-attempted live —
so a backend whose discovery keeps failing re-armed the renderer's
「暂时无法更新实时模型列表」 banner forever, even though the persisted
models worked and selecting one never touched the banner state.
Serve the persisted last-known-good catalog without a live attempt on
every non-force load (the LKG-first policy, decided at the single
fetchSdkModelCatalog choke point): automatic loads now return a clean,
warning-less serve, so the next automatic load after the renderer's TTL
clears a stale banner. Warnings are reserved for an explicit force
refresh that fails, and for a first fetch with neither cache nor
persisted entry. The persisted current-id remap moves into a helper
shared by both serve paths.
Do not fall back to a fresh GUI login when a dedicated connection rejects.
Capture capability selection before awaiting so source cleanup cannot change
the failure policy. Preserve ordinary reusable-channel fallback and cover
both paths with real bridge regression tests.
Preserve PR #3875's original author commits and scoped TERM-SSHD transfer policy.
Copy persisted filename encoding into dedicated file connections. Capture only
host identities actually accepted by the terminal route, including jump hops,
and fail closed on later unverified changes without orphan host-key prompts.
Add first-trust, changed-key, route-scope and encoding regressions.
Deduplicate terminal reveal and focus recovery while preserving changed-layout and explicit resume/fullscreen recovery. Add behavioral regressions.
Related to #3825; this rendering optimization does not establish that the original Windows/macOS issue is resolved.
A managed tool call that races bridge shutdown was still in flight, so
the socket error rejected it with "TCP connection lost". Managed
children do not reread discovery; report that race as not connected
and leave external in-flight wording unchanged.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
When NETCATTY_EXTERNAL_MCP_DISCOVERY_FILE is set, rpcCall recovers on demand via ensureConnected(): re-read discovery, reset TCP buffer, connect to current port, auth/verify with refreshed token. Concurrent callers coalesce. Close/error reject in-flight RPCs without replaying. Managed SDK children keep fail-fast.
Fixes#3871
* feat(shortcuts): toggle the host list with Ctrl/Cmd+Shift+B
Ctrl/Cmd+B already switches broadcast mode and stays available to a focused terminal. The new shortcut reuses the existing host-tree toggle.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(shortcuts): let the host list chord through Monaco and idle terminals
Editor focus inside Monaco can toggle the host list. The chord is consumed only when that toggle runs, matching broadcast.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* feat(terminal): split compose-bar send target from toolbar broadcast
Toolbar broadcast still fans out keystrokes, paste, and terminal-path
snippets. The compose-bar indicator is now a toggle for whether Enter
sends to broadcast peers or only the active session. Turning the toolbar
switch on or off resets that target to follow it; later flips stay
independent.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): keep active-only compose sends off the toolbar fan-out
The focused workspace executor omitted broadcast, so executeSnippetCommand
fell back to the toolbar switch and still wrote every peer. Pass
broadcast: false on that path.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* perf(vault): store connection-log replay blobs in IndexedDB
Move unsaved connection-log terminal replay out of the multi-megabyte
localStorage blob into one IndexedDB row per log id. Persist only the
changed ids, keep the newest 50, and signal other windows with a tiny
revision key.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): keep connection-log replay across boot races and IndexedDB failure
Re-read connection-log metadata after the IndexedDB load before pruning,
return the capture promise through TerminalLayer, and write the unpruned
log blob when the side store rejects the commit.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): keep unmigrated connection-log replays until IndexedDB commits
Copy legacy replay ids that are missing from IndexedDB before deleting
the old blob, and do not treat an uncommitted legacy map as the diff
baseline.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): migrate legacy replays without clearing newer IndexedDB rows
Insert missing legacy ids in one transaction instead of replaceAll, so a
replay committed after the empty read is not wiped.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): rebase the IndexedDB fallback onto the latest connection logs
When the side store rejects a close, attach this window's replay to the
connection-log blob currently on disk instead of writing the stale snapshot.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): rebase queued connection-log writes onto the latest logs
A persist task can run after another window changes the log list. Write
the rebased list instead of the snapshot captured when the task was queued.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): keep queued connection logs after a remote snapshot
A storage event can replace memory with another window's logs before the
queued write runs. Keep ids from the queued snapshot unless this snapshot
deleted them.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): restore only locally changed queued logs
A queued persist was putting back every id from the pre-queue snapshot,
which undid another window's delete of a log this recipe never touched.
Track the ids the recipe added or replaced, and reload IndexedDB during
boot when the revision key moves across the migrate await so that peer
replay is hydrated before this window marks the revision seen.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): rebase connection logs after the side-store await
A non-pruned write stored the pre-await snapshot and could replace logs
another window saved during the IndexedDB round trip. Rebase immediately
before that write, shrink the quota fallback until the newest replay fits,
and leave the revision unconsumed when IndexedDB cannot be read.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): ignore same-window connection-log echoes
The deferred localStorage notification replays this window's own metadata
write. Applying it replaced connectionLogsRef with an older blob and dropped
logs that were added or finalized while that write was still queued.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): do not restore a queued log deleted from memory
A queued add or update was written back when the id was already gone from
memory, which let another window keep that row after a later local delete.
Rebase now applies a changed id only while memory still has it.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Attach close preparation clears the popup session ref, then unmount
cleanup called closeSession. That emits netcatty:exit to the home
renderer and can kill the shared PTY. Skip that second close.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
A timed-out SFTP OPEN used to pin the connection forever when its callback
never arrived. Wait up to 2s, then force-clear the gate and admit a new open.
Late channels are still closed. After TIMEOUT or PENDING, skip the fastPut
reopen and upload on the shared channel.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
canRetryFromCheckpoint and the upload prefix digest missed ECONNABORTED,
socket hang up, ERR_SFTP_CHANNEL_CLOSED, and client-socket. Those Retries
restarted at byte 0 and stranded the staged .part.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
New-file uploads have a null existingMode, so restoreRemoteMode is a no-op.
Recheck the destination only after a real mode restore and keep one
pre-rename safety lstat otherwise.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* feat: open saved session logs from terminal toolbar
Add one-click access to the current automatic or manual session log, flush before opening, and reuse application selection with a remembered log preference. Preserve recording controls and current-session ownership across worker routing and reconnects.
Refs #3807
* fix(logging): preserve saved logs on failed reconnect and localize failures
Address both Codex review findings with generation- and identity-checked replacement cleanup and localized user-facing error handling.
* fix(logging): guard successful main-process session replacement
Add shared inactive-log cleanup to fallback start handlers with regression coverage. Registration activation follows separately.
* fix(logging): enable log lifecycle cleanup for main-process fallback
* fix(terminal): keep log cleanup and tuck open-log into More actions
Always end a raw session-log write stream on finalize, including when a
flush already disabled it. Keep immediate snapshot retries, and pause
them only while prepare is waiting so a disk error can be reported.
Show open, reveal, and open-with in the toolbar overflow menu when
logging is active, instead of a default toolbar button.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(logging): back off failed snapshot writes and drop retired log actions
Persistent snapshot failures retry on a growing delay instead of a tight
loop, and still flush immediately when new output arrives. Hide open-log
actions after a successful reconnect retires a stopped manual log.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): keep open-log actions after automatic logging is turned off
The current automatic log keeps recording until reconnect, so the menu
stays available for that connection and clears on the next successful
connect.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): derive log menu availability from saved session logs
* fix(terminal): nest session log actions in one More actions submenu
Collapse Open, Show in folder, and the two open-with actions into a Session log submenu that follows the Terminal Encoding menu, and drop the remembered-application hint.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(cli): show the final server on PuTTY bastion tab titles
PuTTY jump launches kept the bastion as the TCP host and labeled the tab
with the opaque login string. Reuse the SecureCRT tabName path so an
embedded or leftover target address is the session title, without changing
where the session connects.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(cli): validate PuTTY jump tab addresses with net.isIP
The display-host check accepted malformed colon strings such as a:b:c and
rejected IPv4-mapped and zone-scoped IPv6 targets. Use Node's IP parser so
only real addresses become the tab title.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): close sessions when their owning renderer is destroyed
* test(terminal): keep a shared SSH transport when a copied window closes
Closing a child window must drop only the shells that window owns.
Cover several tabs on one connection split across the main window and
the copied window, for both the in-process lifetime tracker and the
worker close path.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(browser): keep the page live beside overlay side panels
The embedded host browser is a native WebContentsView that draws above
the renderer, and its occlusion check only yielded to dialogs, menus and
poppers. Opening New host / Edit host from the terminal host tree left
the page drawn over the overlay host editor panel.
Overlay-layout aside panels now carry data-native-view-occluder. Native
views cannot be clipped by a parent View on macOS, so the pane shrinks
the live page to the rectangle left beside a panel that covers a
full-height or full-width edge strip; the page stays interactive while
the editor is open. Poppers are checked against that live rectangle, so
selects inside the panel no longer freeze the page, and pictures for
poppers over the page are offset to where it sits. A panel over the
middle of the pane, or one leaving less than 160px, falls back to a
picture.
* test(browser): provide the pane geometry helper to the first-link harness
The first-link acceptance loads useHostBrowserPanel through a VM that
strips imports and injects dependencies by name, so it threw
"resolveHostBrowserPaneBesidePanels is not defined" once the hook
started using the domain helper. Load domain/hostBrowser.ts and pass
the helper in.
* fix(vault): keep the newest connection-log replays when the side store hits quota
Once the replay side store neared the localStorage quota (~48 x 1M-char
entries) every write failed with QuotaExceededError, so the stored map
froze with the oldest sessions and newer ones never survived a restart.
On a failed write, retry without the oldest unsaved replay buffers
(dropping 1, 2, 4, ... per attempt, so retries stay O(log n)) until it
fits or none are left. Storage format, per-entry cap, 50-entry cap and
log metadata are unchanged; saved logs and other windows' ids are never
dropped.
* perf(vault): start quota retries from the newest set that last fit
* fix(vault): keep the connection-log quota hint on the vault hook
The domain helper held a process-global hint, so the same logs and map
could keep a different replay set after an earlier caller. The hint now
lives in a useVaultState ref and is passed in and returned. Drop order,
saved and unknown ids, and the size estimate are unchanged.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Grok Bot <grokbot@local>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Settings, tray, and the terminal popup mount useVaultState but never
show connection-log replay. Skip the bulky side-store load and storage
handlers there so a main-window persist does not JSON.parse it in those
renderers. Main-window behaviour and the write path stay unchanged.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Unsaved logs kept up to 500×1M chars of terminalData in React state while
only 50 survive a restart. After add/update/toggle, drop terminalData from
the oldest unsaved entries beyond MAX_PERSISTED_UNSAVED_TERMINAL_DATA_ENTRIES.
List metadata stays; replay beyond 50 matches restart behaviour.
When the side-store write fails (QuotaExceeded once it nears ~50 MB), keep
the same capped merged map in memory instead of accumulating every log's
replay buffer in connectionLogTerminalDataRef.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Each connection-log write parsed the closed-session terminal-data JSON
twice, merged it twice, and JSON.stringify-compared it twice. Pass the
already-merged map through and compare keys without serializing the
multi-MB replay buffers. Storage format and in-memory state stay the same.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
The alt-screen preview tracker segmented every print span and recomputed
the last grapheme on every non-ASCII append. Skip the boundary walk when
the remainder already fits the piece cap, and segment the open grapheme
only when a later span actually rejoins it.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
ssh2 re-runs hostVerifier on every key re-exchange. When the knownHosts
snapshot for the connection doesn't contain the key (first connect with
"Add and continue", "Continue" without saving), the verifier showed the
host-key dialog again and answered asynchronously. While that answer is
pending ssh2 keeps sending channel data ahead of its NEWKEYS, and OpenSSH
drops the connection ("Bad packet length"), killing the shell, rz/sz and
SFTP on that transport. Servers or gateways with a RekeyLimit hit this
after a few MB of transfer.
Like OpenSSH, accept a rekey only if it presents the key already accepted
for this connection, and answer synchronously without prompting.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Copy style state on SGR changes rather than for each printable character. Preserve historical styles, cleared screens and cursor overwrites without dropping log content.
* docs: losslessly shrink README images and mirror Trendshift badge
Recompress local images linked from the READMEs only when the file gets
smaller and decoded pixels stay identical. Copy the Trendshift badge from
README.md into the zh-CN, zh-TW, and ja-JP READMEs.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* docs: replace README screenshots with lossless WebP
Store the ten README screenshots as lossless WebP under docs/assets/readme
and point every language README at those files. Keep them out of app.asar.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): Retry after a dropped connection resumes from the staged checkpoint
Retry on a failed live task always cloned it under a fresh transfer id with
checkpoint 0. After a connection drop that throws away the uploaded bytes, and
because the remote stage path is derived from the transfer id, the new attempt
stages a new .part while the old one stays orphaned on the server (cleanup runs
against the dead session).
When a top-level file failed with a session-loss error and has a checkpoint,
Retry now demotes it to interrupted/reconnectRequired and goes through the same
dedicated reconnect-resume path a restored task uses: same id, same stage,
truncated to the checkpoint. Content errors and orphaned rows keep the existing
restart-from-0 contract.
* fix(sftp): resume a dropped upload from the verified stage checkpoint
A failed upload never pauses, so Retry has a checkpoint and no SHA-256.
The bridge treated that as an unproven resume and restarted at byte 0,
truncating the same .part. Stat and trim the stage on the new connection
instead, and still restart when the .part does not cover the checkpoint.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): trim a resumed upload stage with SFTP setstat
ssh2 has no ftruncate and ssh2-sftp-client has no truncate, so a longer
.part after a reset always failed the checkpoint trim. SETSTAT {size},
or FSETSTAT on an r+ handle, shortens the stage on the live channel.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): require a stable local source before an unfingerprinted resume
A dropped upload has a checkpoint and no SHA-256. Retry kept that
checkpoint even after the local file was rewritten at the same size, and
the same predicate also resumed downloads and remote copies.
Remember size and mtimeMs when the local upload starts. On retry, keep
the checkpoint only when both still match; otherwise restart from 0.
Limit same-id resume to local-to-remote uploads.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): reject an unfingerprinted resume when prefix bytes changed
Size and mtime can both be restored after a same-size rewrite, so Retry
was appending the new suffix onto the old staged prefix. Keep the head
content sample, and a 64 KiB window ending at the published checkpoint
when that sample does not cover it. Resume only when those still match.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): resume an unfingerprinted upload only when the prefix digest matches
Hash the bytes actually acknowledged on the remote .part, in order, and
store that SHA-256 with the checkpoint offset. Retry re-reads the local
prefix and restarts when the digest or size differs, including a same-size
rewrite the head sample and checkpoint window both missed.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): cancel prefix verification and drop unused upload digests
Pass the transfer abort signal into the retry prefix re-hash so Cancel
stops the read and the task ends cancelled. Drop the in-memory digest
when an upload is cancelled, fails without a resumable checkpoint, or
the task is cleaned up.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: campaign <campaign@local>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(quit): don't freeze the main process when the user keeps unfinished transfers
Quitting with an unfinished transfer pauses it and asks for confirmation in the
renderer. Declining was reported to main as "dirty editors", so
handleBeforeQuit raised a synchronous "Unsaved changes" message box. That box
blocks the whole main process (IPC, every SFTP transfer) until it is dismissed,
names editors that do not exist, and the paused transfer was never resumed.
The renderer now reports the refusal with reason "transfers" and resumes the
tasks it paused for the prompt; main keeps the quit cancelled but skips the
native editor dialog for that case. Real unsaved editors behave as before.
* fix(quit): report a transfer refusal before resuming
A slow resume could miss the 5s dirty-check timeout and let the app
quit anyway. Reply with reason "transfers" first, then resume only the
rows that were transferring or queued before the prompt paused them.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: campaign <campaign@local>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): round-trip jump host chains in CSV import/export
CSV export dropped hostChain, so a vault imported on another machine
lost every jump host. Persist the ordered chain as an OpenSSH
ProxyJump column and bind each hop on import.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): drop inline hops from a rejected ProxyJump chain
A self-referential ProxyJump list created unresolved hops before the
chain was skipped, so the import kept those hosts. Discard inline hosts
created for that rejected chain.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): prefer a same-group hop when binding CSV ProxyJump
When an imported host and an existing vault host both match a hop
endpoint, pick the candidate in the target host's group before falling
back to the imported match.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): prefer a same-group label when binding CSV ProxyJump
Combine imported and existing label matches before pickCsvJumpCandidate so a cross-group imported label cannot beat an existing hop in the target group.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): reject CSV ProxyJump chains that contain a malformed hop
A multi-hop ProxyJump cell with any invalid hop now warns and leaves the
host chain unset, instead of saving the valid hops as a shortened route.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>