* fix: bound AI command input for OpenWrt BusyBox
* fix: address Codex review on PR #3342
* fix: preserve custom prompts and split command history
* fix: preserve zsh history suppression for split AI input
---------
Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com>
* fix(ai): keep AI command execution connected through bastion hosts (#3146)
Approving an AI assistant command after jumping to a target through a
menu-driven bastion (BHostSSH, QiZhi TERM-SSHD) disconnected the session.
Two separate failures were involved:
1. The AI probe/wrapper was written to the interactive PTY as one
multi-character channel write. Strict bastions treat one SSH channel
write as a single keystroke and silently drop multi-character writes, so
the wrapper never ran; the inactivity timeout then sent Ctrl+C, which the
bastion read as "exit". Type the probe and wrapper one Unicode code point
per write to match human keystrokes.
2. ensureSessionShellKindForExec opened a second SSH channel (exec) to probe
the login shell. Bastions bind the transport to the first interactive
session and tear it down when a second channel opens. Skip the
exec-channel probe for bastion banners and let the PTY live shell probe
determine the wrapper instead.
* fix(ai): preserve paced delivery and scope bastion keystrokes
* fix(ai): pace bastion input for Windows shells too
---------
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
* fix: unify host system detection and simplify host details
* fix: retain detected systems for temporary terminal hosts
* fix: detect remote systems after Mosh becomes ready
* fix: show the effective device mode in host system details
* fix(terminal): align history preview with rendered cell dimensions
* fix(terminal): fit wide history rows without changing copied text
* fix(terminal): select every history preview row
* fix(port-forwarding): reconnect auto-start rules after inactive status
* fix(port-forwarding): honor current auto-start setting during reconnect
* fix(port-forwarding): ignore retries from replaced connections
* fix(port-forwarding): preserve exhausted retries after close
* fix(port-forwarding): schedule one retry per failed connection
* fix(port-forwarding): retain pending retry status after repeated errors
---------
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
Allow global broadcast hotkeys for visible orphan sessions and centralize target resolution with coverage for workspace, global, and direct-target modes.
Keep stored boolean updates pure and propagate global broadcast state through terminal panes so orphan sessions can toggle and receive broadcasts correctly.
Select default command-safety rules from the live session shell so legal
PowerShell subexpressions are allowed and PowerShell-native dangerous
commands are blocked. Untouched pre-shell-aware lists gain the new
defaults on read.
Closes#3256
The original bug is: lock + display sleep moves a secondary-display window
to the primary, and unlock never puts it back. Review follow-ups had grown
a general display-identity machine far beyond that.
Restore only while the session is locked/asleep, or shortly after unlock.
Keep user moves authoritative. Drop Win+Shift destination tracking and
multi-unknown-display correlation.
Hosts were hardcoded to os 'linux' at every creation and update entry
point, so Windows hosts were always reported as linux by the AI
environment (get_environment) and nothing could correct them.
- buildVaultHostFromDraft parses draft.os with a strict allowlist
(linux/windows/macos, common aliases included) and defaults to linux
- applyVaultHostUpdate accepts os with the same validation
- the AI host.update field list and the MCP vault host tool schemas
expose os so agents can set and correct it
Empty pane arrays are not a first-visit signal; they also happen after
the user closes every tab. Route every Open SFTP request through the
planner, and do not forceNewTab on an empty side.
The empty-left shortcut was racing first-visit auto-connect, but it also
fired after the user closed every left tab. Open SFTP then skipped the
planner and could replace the right pane. Only skip the planner when
both sides are still empty.
A matching hostId in error or disconnected state is not a live session.
Treat only connected/connecting panes as reusable so Open SFTP reconnects
in place instead of selecting a dead tab.
Skipping realpath on plain directories also dropped them from the ancestor
set, so a symlink back to a non-root ancestor duplicated that subtree.
Compose the resolved parent path with the child name instead, and still
realpath directory links.
Address the SFTP UX list in #3240: wider bookmark management with
confirm/reorder/rename, a compact file-list density, a host-list shortcut
into the existing local/remote dual pane, faster local folder discovery,
and higher small-file upload concurrency.
Tree view already supports double-click to enter directories, so that
item is left as-is.
Keychain identities can change after the host copied username. Use the
same resolveHostAuth username as sudo autofill so a stale host.username
does not skip or force elevation.
Keychain identities clear host.password. Reuse the same host-auth
resolution as sudo autofill so those saved identity passwords can
elevate a terminal drop into /root.
After sudo -i, terminal drag into /root uses the host's saved password for
SFTP sudo. If no usable password is saved, the drop fails with guidance to
enable host Sudo elevation or return to the user directory.
Automatic SFTP reconnect still prefers the originating session, then falls
through to a fresh open so a dead channel after sudo -i can re-elevate.
Do not treat a lagging split-pane focus as the user leaving the drop
origin, and do not bind a Mosh/ET drop to the still-focused SSH session.
First-open follow treats a missing SSH id as bound to the live focused
terminal so a probe from A cannot apply after focus moves to B.
A follow sync that started probing session A must not navigate after
focus moves to session B. Capture the expected session before the
await and reuse the same guard already used for go-to-terminal-cwd.
Keep confirmed active-shell destinations, originating-session bind, sudo
fidelity, and the longer ZMODEM start wait. Remove the dormant queue,
routeSessionId identity, and source-regex contracts that only locked
that extra routing in.
Add Duplicate Session as an explicit fresh-login alternative to Copy Tab. Keep authenticated transport reuse for Copy Tab and double-clicks, preserve fresh intent across session restoration, and avoid sending a previous target's directory command to a fresh bastion login.
Verified with focused and full tests, lint/build, independent local reviews, and real Electron menu operations against a local SSH server.
Fixes#3221.
Release consumed second-instance argument buffers after deriving connection requests from an independent ordered copy. This closes the late password-retention finding on #3220 without changing connection or Explorer path routing.
Verified with regression tests, the full local suite, lint/build, two independent local reviews, and a real two-process Electron SSH connection that checks credential delivery and removal of both transport copies.
Related to #3218 and #3220.
Restore full height through the AppLockGate background wrapper, which otherwise lets the tray content collapse to about half of its window height. Preserve the existing lock-screen interaction restrictions.
Add real Electron regression coverage for repeated opening, session lists, lock/unlock, short windows, and Escape. Run it on macOS and Windows, including 100% and 200% device scale on Windows, and trigger it for the relevant renderer changes.
Fixes#3181
A wrong master password can decrypt to a NUL mid-buffer and leave a short
printable UTF-8 prefix. Treat only a trailing NUL pad as a C string so
vectors like wrong25 no longer save garbage host passwords.
AES-CFB has no authentication, so a mistyped master password can decrypt
to printable Latin-1 junk that was previously saved as a host password.
Require valid UTF-8 plaintext, fail closed if any ciphertext does not
decrypt, and keep leading or trailing whitespace in the master password.
Sanitize lastConnectedAt in convergent equality, change summaries, and
v1-to-v2 conversion so legacy telemetry cannot block migration or leak
into CRDT state.
Bound the post-commit remote chmod with the same best-effort timeout
used for destination mtime stamps. A stalled chmod after promotion
now warns and lets completion/cleanup continue instead of pinning
the transfer, SFTP lease, and admission slot.
Drop the extra pre-transaction mode stat so cancel can settle and
existing call-count tests keep their first stat as the planner/final
size check. Restore replace mode from the transactional upload plan
snapshot instead, and hang the in-place cancel regression on the
post-OPEN size verification.
Replace uploads recreate the remote file via fastPut / pipelined WRITE
with umask defaults, which dropped executable and other mode bits.
Capture the existing remote mode before the transfer and chmod it back
on the published final path after success.
Fixes#2954
Vendor bastion/network-device CLIs disconnect when live-preview injects
Ctrl-U or backspaces while typing cd/cat. Keep the popup, but treat
network-device sessions as preview-off until the user accepts.
Fixes#1193
Claude Code Ajv does not load that dialect, so every triage died before
the model ran: no issue reply and no diagnosis. Strip $schema/$id before
--json-schema.
Route and several other jobs required $RUNNER_TEMP/ai-automation.cjs after
checking the helper out to the workspace, so GitHub never reached classify.
#3181 dispatch failed immediately with MODULE_NOT_FOUND.
Unindented Brave helper heredocs made GitHub reject ai-automation.yml.
The workflow never registered issues/opened, so new issues only got the
format check and template triage label.
Arm a one-shot window/document capture listener on non-primary
pointerdown so releasing over the portalled context menu still
restores draggable, and share the helper across both surfaces.
Identity recovery cleared the temp-root cache but kept the in-memory
signing key from the old inode. Drop that promise so the next load
picks up the replacement key file instead of signing with a stale key.
Point spawnSync children at isolated TEMP/TMP/TMPDIR so Windows cannot
rm the real Netcatty temp root. Keep the old inode allocated via rename
so the replacement test cannot pass against recycled inodes.
A resolved cursor-agent binary is a user install even when status JSON
is missing, unrecognized, or the status command throws. Login still
requires parsed authenticated status output.
Right-clicking a script package in the terminal scripts tree now shows
Rename and Delete. Vault package and snippet cards use asChild triggers
and ignore non-primary drags so Windows context menus are not swallowed.
Netcatty ships @cursor/sdk, so probing it from the Electron main process
always succeeded and Settings showed Cursor installed even with no
Cursor Agent CLI on the machine. Treat installed as CLI presence or
proven CLI login; keep sdkInstalled for API-key mode.
Swap issue triage, implement, follow-up, and Codex-fix jobs from Cursor
CLI to Claude Code against Ollama Cloud (glm-5.3-flash:cloud). Isolated
research now uses Brave Search. Automation files and repo variables use
an ai- prefix; existing cursor-* HTML markers and cursor/issue-*
branches still parse so in-flight work is not reprocessed.
SFTP downloads fail when the cached Netcatty temp root still exists but
its inode changed (Windows temp cleaner, worker mkdir race). Treat that
identity mismatch like ENOENT: clear the cache, re-run full safety
checks, and rebind instead of aborting the transfer.
Keep the retry markdown override only while the parent prop still contains
the stale body that originally failed MDX import. Once NotesManager's
source-draft debounce catches up, the editor goes back to normal prop
synchronization instead of keeping an old retry seed around.
Co-Authored-By: Claude <noreply@anthropic.com>
Codex review follow-ups for the unrenderable-markdown fallback:
- Retry now seeds the MDX import from the editor's latest accepted source
draft. NotesManager debounces source drafts (300 ms), so retrying right
after fixing the markdown could remount the rich editor with the stale
pre-fix `value` prop and drop straight back into the fallback.
- The read-only preview fallback no longer mutates the note: NoteSourceEditor
skips its custom Tab insertion, undo/redo handling and imperative
insertAction when readOnly is set (the DOM readOnly attribute alone did
not block those paths).
Co-Authored-By: Claude <noreply@anthropic.com>
The Vault notes rich editor parses note content as MDX. Some valid
CommonMark is not valid MDX — most commonly an unbalanced angle tag such
as `mysql -h <host> -u <user>` written in prose. When mdxeditor fails to
import the markdown it clears the lexical document and only reports the
failure through its onError callback, which netcatty never wired up. The
note then rendered as an empty editor with just the placeholder, while
the content was still intact in the vault (export/copy still worked).
This is exactly the note-view half of #3167: the AI wrote a skill
document into Vault → Notes, the note showed no content, and exporting
it produced a complete markdown file.
The editor now listens to MDXEditor's onError and falls back to the raw
markdown source view with a short notice, so the content is always
visible (and editable) instead of silently blank. Recovery is
deterministic: switching note or editor mode retries the rich import,
and the notice offers an explicit retry.
Co-Authored-By: Claude <noreply@anthropic.com>
Strict bastion prompts (QAX/奇安信) treat one SSH channel write as a single keystroke and silently drop every multi-character chunk, so an IME commit of two or more hanzi and a Chinese paste both reach the bastion's search prompt as nothing at all. Single characters work, which is why typing them one at a time is the only path that works today.
Split plain multi-character text at the send boundary instead of adding a setting: the write side of handleTerminalInputData now iterates one chunk per Unicode code point, and all bookkeeping (command buffer, script recorder, autocomplete, sudo autofill, broadcast, scroll) still sees the original payload exactly once. IME commits split whenever the committed text has more than one code point; raw (non-bracketed) onData splits up to 32 characters and longer pastes keep the single write. Anything containing an escape sequence - bracketed paste wrappers, CSI-u composition, forwarded Kitty key sequences, cursor reports - is never split, and broadcast peers still receive the original string once.
Each chunk carries the same writeToSession options as the original single write, so the sensitive marker and automation flags are kept per character. Bridge writes stay one channel write per IPC call, which reproduces what Xshell sends on the same bastion (#3077).
Co-Authored-By: Claude <noreply@anthropic.com>
When the punctuation keyup was dropped and a previously held key was released
next, the recovery flushed the deferred key (recording a Kitty press) but then
released only the unrelated key, leaving the deferred press forwarded until
focus loss.
Replace the sentinel-rewrite predicate with resolveDeferredKeyupRelease, which
enumerates all three cases: sentinel releases take over the deferred key's
release identity, the deferred key's own release pairs the press directly, and
an unrelated release keeps its identity while the deferred press gets a
synthesized release.
Clearing a history object reused by a new session kept the previous
session's cell column, so a leading tab in the fresh output padded to the
wrong tab stop.
Co-Authored-By: Claude <noreply@anthropic.com>
Follow-ups on the alternate-screen preview transcript:
- Cap the pending escape tail on its introducer, so an oversized control
string split across chunks is not re-parsed as transcript text.
- Write spans in bounded pieces, committing a full line at the character cap,
instead of clipping a span and dropping its remainder.
- Track the cursor in terminal cells (not UTF-16 length) so tab stops land
correctly after wide glyphs.
- Apply erase-in-line (CSI 0K / 2K) to the open line, so a shorter progress
redraw leaves no stale suffix text behind.
Co-Authored-By: Claude <noreply@anthropic.com>
A modified keydown (Ctrl+C, Alt+...) is a command, not the continuation of the
lost punctuation keystroke. Leaving the deferral armed across it meant the next
ordinary keydown flushed the pending character in front of the user's input,
right after the interrupt or shortcut ran.
Discard the stale deferral when the next keydown carries Ctrl/Alt/Meta; an
active composition still resolves it via insertText.
Follow-ups on the alternate-screen preview transcript:
- C1 control-string introducers (DCS/SOS/OSC/PM/APC) were filtered as stray
control bytes, leaving their payload as transcript text; consume them
through the BEL / ESC-\ / C1 ST terminator like their ESC-based forms.
- Tabs were kept as-is, so a row's cell width under-counted against how the
preview pre element renders tab stops and long tabbed lines were clipped
instead of wrapping. Expand each tab to the next 8-column stop while
writing, and route tab padding through the same open-line budget as text
spans.
Co-Authored-By: Claude <noreply@anthropic.com>
The keydown recovery for a swallowed punctuation keyup flushes the deferred
key, which records a Kitty press locally and for broadcast peers. Since that
branch only runs when the release will never arrive, the press stayed
forwarded and the TUI saw the key held until focus loss or session reset.
Extract the forwarded-press release pairing into releaseForwardedKittyPress and
call it from the recovery with a release synthesized from the deferred physical
key, so the press/release pair completes in the same keystroke.
Two robustness gaps in the alternate-screen preview transcript:
- A cursor-addressed redraw (cursor-home CSI stripped, no LF) appended every
frame to the open line, which the character cap only checked on commit, so
it grew without limit and every append copied it whole. Commit the open
line once it reaches the budget and clip each span to the remaining room.
- Escape sequences were assumed to be two bytes, so intermediate bytes leaked
their final byte into the transcript (ESC ( B from ncurses/reset left a
stray B). Consume intermediates (0x20-0x2f) through the final byte
(0x30-0x7e) per ECMA-48.
Co-Authored-By: Claude <noreply@anthropic.com>
A real keyup for another held key could land while the punctuation deferral
was still armed (the deferred key's own release was swallowed). That release
was rewritten to the deferred physical key, so in Kitty report-event modes the
held key's forwarded press never got its release and the TUI saw it stuck down
until focus loss or session reset.
Restrict the rewrite to IME sentinel releases (Process/229/Unidentified/
Dead/Compose, mirroring the non-text DOM keys the Kitty encoder already
refuses to send as text). Any real release still ends the stale deferral, so
the #3103 recovery is unchanged.
Plain wheel in screen/vim/codex still forwards arrow keys (xterm's
alternate-scroll), and shift+wheel previewed xterm's normal buffer, which
holds no scrollback once an app owns the alternate buffer - so the preview
clamped to a single screen and looked dead (#2516).
When the alternate buffer is active and the normal buffer has no rows above
the viewport, the preview now renders the session's captured output instead:
a bounded plain-text transcript kept next to the connection log capture,
after the programmatic command rewriter (masked commands stay masked) and
before the replay sanitizer, which drops alternate-screen output on purpose.
Rows are wrapped to the terminal columns with wide-character awareness and
reuse the existing wrap-flag selection joining.
Mouse-reporting apps are untouched: their wheel events still go to the app.
Co-Authored-By: Claude <noreply@anthropic.com>
The CJK IME punctuation deferral (#2833) armed on a punctuation keydown and
only resolved on an insertText remap, an exact-key keyup, or a session reset.
Windows IMEs report Process/keyCode 229 as the keyup of a key they consumed,
or drop the keyup, so the deferred key stayed armed: the deferral then blocked
every keypress, and a second "/" search in vi (or any later keystroke routed
through keypress) never reached the PTY (#3103).
The deferral now ends on any real key release (composing releases excluded, so
an active composition still resolves via insertText) and a stale deferral is
flushed when the next unmodified, non-composing keydown for a different key
arrives. The keypress block is scoped to the deferred key instead of every
keypress, so a stale deferral can no longer swallow unrelated input. A mangled
release pairs its kitty release from the deferred physical key instead of the
Process/229 event.
'sftp.fileName' read 檔名稱 and its placeholder 輸入檔名稱, which are both
missing a character — the term is 檔案名稱. Every other locale spells it
out (File name / 文件名称 / Имя файла / Nombre del archivo), and the
sibling folder keys directly above already use 資料夾名稱.
Visible in the SFTP "New file" dialog.
Two changes based on Codex bot review feedback on PR #3156:
1. Preserve ZWNJ (U+200C) and ZWJ (U+200D) in terminal input.
These carry meaning in Persian orthography and emoji sequences;
PTYs can process the raw bytes, so stripping them could corrupt
filenames, args, and passwords that legitimately contain them.
2. Sanitize composition text before Kitty encoding.
When Kitty report-all + associated-text mode is negotiated,
encodeKittyCompositionText converts U+200B into an escape sequence
(e.g. ESC[0;;8203u) BEFORE reaching the handleTerminalInputData
guard. Sanitize the raw text in both composition paths
(commitImeTextInput remap path and onData handler) so zero-width
artifacts are stripped before encoding, not after.
Update pattern-matching tests to reflect the new variable names
(sanitizedText / sanitizedData) in the Kitty composition paths.
CJK IMEs (notably Microsoft Pinyin / Sogou on Windows) occasionally emit
zero-width Unicode characters (U+200B ZWSP, U+FEFF BOM, etc.) when switching
composition modes. xterm.js sends these to the PTY via onData, and with the
15-graphemes Unicode version they render at width 0 — so the command line
looks normal but contains hidden characters that cause the executed command
to fail.
Add sanitizeTerminalInput() and call it at the top of
handleTerminalInputData so every input path (keyboard, IME composition,
paste, Kitty keyboard, Shift+Enter) strips zero-width formatting chars
before they reach the PTY. If the input was zero-width-only, return early
to skip all downstream processing (command buffer, broadcast, sudo autofill).
The folder conflict copy was clarified but the transfer-center accessibility test still expected the previous wording. Assert the current destructive warning while preserving the aria-describedby coverage.
The identity-file safety check correctly rejects the old placeholder strings used by four auth retry tests. Generate a real test key and return a realistic parsed private key so the fixtures exercise the intended retry behavior without weakening validation.
The repository ships AGENTS.md, but every README linked to lowercase
agents.md. GitHub serves blob paths case-sensitively, so the architecture
guide link 404s on all four pages.
Fixed in README.md, README.zh-CN.md and README.ja-JP.md as well, since the
same broken link exists there.
Add README.zh-TW.md for Traditional Chinese (Taiwan) readers and link it
from the language switcher in every README.
The localized download badges were also broken. README.zh-CN.md and
README.ja-JP.md put raw non-ASCII text straight into the shields.io query
string (label=下载最新版, label=最新版をダウンロード). shields.io rejects
those with HTTP 400, so both rendered as a broken image. Percent-encoding
the label fixes it; all four badges now return HTTP 200.
- add README.zh-TW.md, written for Taiwan usage rather than converted from
Simplified Chinese (伺服器, 用戶端, 分頁, 工作階段, 檔案總管, 程式碼簽章,
相依套件, 叢集, 權杖, 序列埠)
- percent-encode the badge label in README.zh-CN.md and README.ja-JP.md
- add 繁體中文 to the language switcher in all four READMEs
- allowlist README.zh-TW.md in .gitignore, which ignores root files by
default
Validate ssh2 parser results for actual private material and stop raw key fallback after resolution failures. Add generated-key regression coverage for public-only and malformed identity inputs while preserving encrypted-key handling.
Dragging a host card in grid view left it stuck at opacity-45 until the
user started another drag on it.
The grid preview reorder runs from onDragOverCapture, so the dragged card
can move into a different virtual row while the drag is still active. In
VirtualizedHostCollection each row is a separate React parent, so crossing
a row boundary unmounts and remounts the card. The browser still dispatches
dragend at the original node, but a detached node no longer bubbles the
event up to the container's onDragEndCapture, so draggingHostId was never
cleared. A short drag that stayed inside one row kept the node mounted,
which is why dragging again appeared to fix it.
- bind a native dragend listener on the source node at dragstart; it is
dispatched at that node and still fires after React unmounts it
- always reset drag state in onDropCapture: after a grid preview reorder
the card under the cursor is the dragged card itself, so
draggedHostId === targetHostId fell through every branch without
resetting
- apply the same fix in useVaultItemReorder, which backs the keychain,
port forwarding, proxy and known-hosts lists, and in SnippetsManager
zh-TW was missing 78 keys. I18nProvider resolves a missing zh-TW key
through the base "zh" language, and that lookup matches zh-CN first in
MESSAGES_BY_LOCALE, so Traditional Chinese users saw Simplified Chinese
for the whole App Lock surface rather than an English fallback.
- add the 78 missing keys: appLock.*, settings.appLock.*, topTabs.lockApp,
dialog.createWorkspace.title, field.selectHosts, placeholder.searchHosts
- fix Simplified leftovers: 云端 -> 雲端, 筆記本里 -> 筆記本裡, 賬密 -> 帳密
- switch mainland wording to Taiwan usage: 分組 -> 群組, 標籤頁 -> 分頁,
身份 -> 身分, 新建 -> 新增, 地址 -> 位址, 重置 -> 重設, 快捷鍵 -> 快速鍵,
生成 -> 產生, 審批/批准 -> 核准, 波特率 -> 鮑率, 停止位 -> 停止位元,
拖拽 -> 拖曳, 響應 -> 回應, 字號 -> 字型大小, 重啟 -> 重新啟動
zh-TW now covers every key present in en and zh-CN.
Avoid rewriting thinking/provider maps when values are unchanged so
mounted AI panels do not re-render. Put permission title and short
description on one line and drop the Cursor CLI parenthetical.
Stop advertising Gemini 3.7 Flash minimal, GPT-5.x-chat reasoning, and
Claude 4.7+ extended thinking. Map GPT-5 Off to minimal, send adaptive
effort on Claude 4.6+, and keep composer Enter on an exact or custom id.
Node's http.request falls back to Transfer-Encoding: chunked when no
Content-Length header is present. Both AI request paths pass the AI SDK's
headers through unchanged, and those headers carry no content-length (in the
browser fetch computes it), so every AI request body went out chunked.
The body is a fully-buffered string at that point, so its length is known.
Send an explicit Content-Length instead, measured with Buffer.byteLength so
multi-byte system prompts are not truncated.
Chunked request bodies are the less interoperable choice: on the plain-HTTP
path an HTTP proxy has to de-frame and re-serialize them, and some fail on
larger bodies. Xray's HTTP inbound drops the connection once the chunked body
exceeds roughly 8KB. Because Catty's system prompt plus tool definitions is
about 48KB, chat against an http:// provider behind a system HTTP proxy failed
with an opaque "socket hang up" while the connection test kept passing, since
that test issues GET /v1/models with no body and never exercises this path.
Adds three regression tests covering the streaming path, byte-accurate length
for multi-byte bodies, and the non-streaming path.
Closes#3083
Codex P2: switching to a catalog model dropped context_length, so
Catty budgeted 128k instead of the advertised window. Keep the
metadata on picker models and write it to modelContextWindows.
Codex P2: models whose menu includes Off still showed Off for users
with no stored preference, while the request sent no override. Leave
the chip unselected until the user picks a level.
Codex P2: missing agentThinkingMap was treated as Off, so GPT-5.1+
got reasoningEffort=none and Gemini 2.5 Flash got thinkingBudget=0
without a user choice. Store Off as an explicit value and leave an
absent preference unset so the provider default is used.
Codex P2: an empty agentThinkingMap was normalized to Off and then to
minimal, so every Gemini 3 Flash turn overrode the provider default.
Only send thinkingLevel when the user actually picked a level.
Codex P2: gpt-5-chat-latest matched the gpt-5 heuristic and received
reasoningEffort, which the OpenAI chat-only variants reject. Exclude
gpt-5-chat* from the thinking chip and request options.
Codex P2: agentThinkingMap was persisted locally but omitted from
collect/apply sync, so backups and other devices restored provider
and model without the chosen reasoning level.
Codex P2: hiding the custom-model row still left Enter falling back
to the typed query, which send-time validation then discarded. Only
use the query as a custom ID when Catty offers that action.
Codex P2: opening the composer on Gemini Pro persisted Low for users
who had never chosen a thinking level. Only remap an explicit stale
value; leave an empty preference unset so the request uses the
provider default.
Codex P2: pinning or recents in one parked ChatInput only updated that
instance. Subscribe every mounted composer so sibling terminals see
the same recent/pinned list without remounting.
Codex P2: gemini-3.1-pro-preview supports Medium, but Catty rewrote it
to High. Original Gemini 3 Pro still only has Low/High, so omit Medium
instead of silently converting it.
Codex P2: refreshed CLI catalogs still showed saved IDs that the
catalog no longer contains, then send-time validation swapped them.
Only Catty may keep a custom ID that is not in the live list.
Codex P2: a built-in provider with no stored baseURL never listed
live models even though chat still used the preset host. Fall back to
PROVIDER_PRESETS.defaultBaseURL before probing /models.
Codex P2: external-agent pickers advertised a custom ID that send-time
validation ignored. Offer Use custom model only for Catty providers.
Codex P2: the thinking budget was added to both compaction max output
and reservedTokens, so it was subtracted twice. Keep it only on
compaction max output.
Codex P2: medium/high thinking adds 10k/20k to the SDK max_tokens,
but pre-turn/step compaction still reserved only the 4,096 default.
Fold the selected thinking budget into output reserve and compaction
max output without changing the streamed maxTokens field.
Codex P2: Claude 3 Haiku still offered Low/Medium/High and would send
an enabled thinking budget. Gate the chip and the provider option on
Claude 3.7 / 4+ model ids. Also memoize Catty thinking levels.
Codex P2: switching away from Gemini 3 Flash left `minimal` selected
on models that do not advertise it. Persist a valid level for the new
model.
Codex P2: Gemini 3 Pro and 2.5 Pro cannot disable thinking, so Off
was a lie. Those menus only offer low/medium/high.
Codex P2: catalog cache ignored API key rotation, so a replaced
credential could keep the previous account's models. Fingerprint the
stored secret in the cache key.
Codex P2: Gemini 3 Flash cannot turn thinking off; Off was sending
thinkingLevel=minimal. Surface Minimal on the chip instead of Off.
Codex P1: o3/o4-mini/gpt-5 reject reasoning_effort=none. Only GPT-5.1+
get that Off mapping; older reasoning models omit the field.
Codex P2: Cursor ModelListItem.parameters.effort.values is the catalog
capability list. Use it when present instead of fabricating fallbacks
or dropping the thinking chip on unknown models.
Codex P2: OpenAI-style models such as gpt-4o rejected reasoning_effort
when the chip always offered Off/Low/Medium/High. Gate the chip and
the provider option on known reasoning model ids, and send explicit
none when Off is selected on a reasoning model.
Remove the provider divider and search-field outline in the sidebar
model picker. Size Save/Test/Cancel to the same h-8 box without
changing the global Button styles.
The Ollama preset always rewrote OpenAI-compat keys to the literal
"ollama" dummy. That is correct for local servers with no key, but it
replaced the IPC placeholder used for Cloud, so chat sent
Authorization: Bearer ollama and 401ed. Only fill the dummy when no key
is configured, and normalize a bare ollama.com origin to /v1.
Omit Gemini 3 Pro Off instead of sending unsupported minimal, clamp
medium to high, and merge advertised Cursor efforts with the fallback
table so CLI-listed gpt-5 still gets a thinking chip.
Do not rewrite bare CodeBuddy model ids to /medium, fail closed when
Google has no model id, and send Gemini 2.5 thinkingBudget instead of
Gemini 3 thinkingLevel.
Map Cursor effort variants and CodeBuddy SDK effort onto the shared
sidebar thinking chip, and send Gemini thinkingConfig for Catty
models that look like they support it.
Give Catty a two-column provider/model menu in the composer, add search
plus recent/pinned models, and move thinking effort onto its own chip
so Claude, Codex, Grok, and Catty share the same compact controls.
Closes#2137Closes#2637Closes#1884
Closes#3078
OpenAI-compatible providers always used Chat Completions. Some
relays cache poorly on that path. Add a per-provider Responses
opt-in so Catty can call /v1/responses while keeping Chat
Completions as the default.
Surface the remote file owner in the SFTP list and tree, using the
existing toggleable column menu. Parse the username from SFTP longname
or ls output, then fall back to uid when a name is unavailable.
Closes#3073
Malformed work factors must fail with the limits error even when
crypto.argon2Sync is missing, so CI on Node 22 still rejects crafted
headers instead of reporting an unsupported runtime.
Check encryption on the prepared key so SFTP/jump hops do not prompt
again after a PPK is rewritten to OpenSSH. Build Argon2-limit tests
from stub headers so they do not need crypto.argon2Sync. Keep the
empty HMAC key for unencrypted PPK v3, matching PuTTYgen.
The overlay window already uses native hasShadow. CSS shadow-lg on the
transparent rounded card paints a second halo, which macOS then
composites as an extra outline under the panel.
Reject non-positive or unbounded memory/passes/parallelism (and
malformed salt) before argon2Sync so a crafted PPK cannot stall the
Electron main process.
Closes#3072
Close-to-tray and app-hide always requested a background lock whenever
app lock was enabled. "Never lock automatically" only disabled the idle
timer, so clicking X on Windows still asked for the password on reopen.
Honor timeoutMinutes === 0 for background locks. Startup and manual
locks still apply. Setting copy now says Never skips both.
Codex P2: a keyboard or accessibility activation can fire with valid
event bounds while the pointer sits on another monitor. Keep those
bounds for display and placement; only reject stale tray.getBounds()
when it disagrees with the cursor.
Closes#3069
App Lock lives on the System tab but was never added to the
settings search catalog, so queries such as "应用锁" and
"App Lock" returned no matches. Index the section and wire a
jump anchor so search can open it.
ssh2 only parses PPK v2 RSA/DSA, so an encrypted PuTTYgen ed25519
key failed even with the correct passphrase. Convert PPK v2/v3
(Ed25519, ECDSA, RSA) to OpenSSH PEM before handing the key to ssh2.
Windows right-click ignored the event bounds Electron already provides
and reused tray.getBounds() / win.getBounds(), which often report y=0
or a blown-up size. Always set 360x520 and anchor above a bottom
taskbar (or below a top one), falling back to the cursor when bounds
are stale.
Closes#3064
Windows shows the system title-bar menu on right-click in drag regions.
The lock overlay is a drag region so the window stays movable, which also
exposed Minimize/Maximize/Close. Suppress BrowserWindow system-context-menu
while locked so the lock screen cannot quit the app.
Closes#3065
The lock screen is a full-window overlay, so it covers the title-bar
drag region and is the only chrome on a locked first paint. Mark the
backdrop as app-drag and keep unlock controls app-no-drag.
Closes#3058
Reconcile the app-lock feature with main's refactored renderer
architecture (publishers/hosts/AppShell) and main-process changes:
- index.tsx/App.tsx: AppLockGate keeps ownership of useSettingsState and
useAppLockState; new AppLockRuntimePublisher publishes the runtime via
appRuntimeBridge; SettingsPublisher now binds the gate-owned settings.
- AppSideEffects: defer deep links, tray port-forward toggles, global
hotkeys and window close commands while locked; flush on unlock.
- main.cjs: keep fresh-session re-lock before main's reusable-window
fast path; before-quit guard delegates to handleBeforeQuit with main's
async plugin-shutdown commitQuit and pending-update cancellation.
- globalShortcutBridge: keep locked-state tray redaction on top of
main's deferred tray panel show logic.
- preload: adopt main's lastPayload replay for terminal popup config
(superset of the gate-delay replay fix).
- vite config: keep dev CSP plugin, adopt main's warn-only stale
optimize-dep handling; electron-builder: keep Windows Hello helper
extraResource with main's CLI-controlled arch targets.
Co-authored-by: Cursor <cursoragent@cursor.com>
The HTTP stream layer used a hard 120s total deadline, so long
reasoning that was still emitting tokens got cut off. Re-arm a
2-minute idle timer on incoming bytes and keep a 30-minute hard cap
for runaway streams.
Closes#3051
MCP allows clients to omit tools/call arguments or send {}. Empty Zod
object schemas rejected the omitted case, so external agents could not
call get_environment and other no-arg tools.
Closes#3049
closeSessionWorkspaceLayoutState pruned the workspace tree but left
focusedSessionId pointing at the closed session. In a focus-mode workspace no
pane then qualified as visible, so the continuously rendered hidden panes all
painted on top of each other (reported in #3046 follow-up). Reassign focus to
a remaining session and drop closed ids from focusSessionOrder, matching the
detach path.
Co-authored-by: Cursor <cursoragent@cursor.com>
A pane that was once full-size kept those pixels in lastVisiblePaneSizeRef
through split view; after a viewport resize, switching back to focus pinned
the hidden pane (and its PTY) at the stale size. Split-visible panes now
invalidate the cache so hidden full-size layouts re-measure the current area.
Co-authored-by: Cursor <cursoragent@cursor.com>
Switching a workspace from split view to focus view left each newly hidden
pane pinned at the split fragment size cached in lastVisiblePaneSizeRef, so
the background PTY stayed narrow and the \r progress spam from #3046 could
still occur. Only full-size layouts feed the hidden-pane pinned size now.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(terminal): pass Esc+. through for readline yank-last-arg
Autocomplete was swallowing Escape, and macOS Option+. typed ≥ instead of Meta-., so the standard shell shortcut never reached bash/zsh.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(terminal): allow Shift when Esc+_ yanks the last argument
Underscore is Shift+Minus, so rejecting all shiftKey follow-ups dropped M-_ after autocomplete dismissed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(terminal): keep Esc+_ armed across the Shift keydown
Typing underscore emits Shift before Minus. Clearing the Meta prefix on that modifier event dropped M-_ after autocomplete dismiss.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(terminal): do not treat Esc+. as typed input
Yank-last-arg rewrites the shell line. Walking the ESC+. bytes appended a literal period to the command buffer and left autocomplete's typed buffer marked reliable.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(ssh): accept PuTTY-style CLI args from bastion hosts
Reuse the existing ssh:// and telnet:// deep-link path so PAM products that launch putty.exe -ssh user@host -P port -pw password can point at Netcatty instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ssh): consume operands of ignored PuTTY flags
Unknown value-taking options such as -m and -D were skipped while their next token was treated as a host or port, which could connect the supplied password to the wrong endpoint.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ssh): reject unsupported PuTTY protocols and unknown flags
-raw/-rlogin/-serial must not fall through to SSH with the supplied password, and valueless switches such as -batch must not consume the following host. Unknown dash flags are now rejected instead of guessed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ssh): reject missing or empty PuTTY option operands
An empty or trailing -pw was turned into ssh://user:@host, which the deep-link path treats as no password and can then authenticate with a saved vault credential.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ssh): reject unsupported -pwfile, -i, and -hostkey launches
Those options request credentials or a host-key pin this pipeline cannot honor, so connecting with -pw or a vault match would silently change the authentication the caller asked for.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ssh): keep .app/.exe hosts and reject -loghost
Suffix filtering now only covers the Electron script argument, so destinations like prod.app are not dropped. -loghost is rejected because this pipeline cannot honor the logical host-key identity.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ssh): keep packaged argv[1] hosts like prod.js
Suffix filtering now applies only after an Electron binary, so a packaged launch can still use a first-position hostname that looks like a script.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Focus-mode panes are always viewed full-size, but hidden non-focused panes
were laid out at their split rects when hibernate is off (the default). The
continuously rendered xterm and the remote PTY shrank to a 1/N-width
fragment, so \r-refresh progress output (rsync --info=progress2) soft-wrapped
and left one scrollback line per refresh, flooding the terminal when the
host was focused again (#3046). Focus mode now never uses split geometry.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(sftp): add right-click extract for archives
Let SFTP list and tree views unpack zip/tar/gz archives in place via remote SSH exec (or local tar/unzip), so users do not need to switch to another file manager.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): keep extract from clobbering files or dying on unzip output
Write gzip/bzip2/xz results to a staging path and replace the destination only after a zero exit. Drain unzip progress instead of treating routine stdout as a hard failure.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): quote extract cleanup trap and use unique staging files
Remote gzip/bzip2/xz nested a quoted path inside trap, so names with spaces failed before decode. Staging now exclusively creates a unique sibling, and local .gz uses Node zlib.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): refuse single-file extract onto an existing directory
Remote mv would otherwise drop the staging file inside that directory and still report success.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): restore the previous file if Windows extract replace fails
Rename the destination aside before publishing the staging file, instead of deleting it first.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Decrypt master-password and legacy SessionP credentials during MobaXterm
import so migrating hosts do not require re-entering passwords.
Co-authored-by: Cursor <cursoragent@cursor.com>
The backtracking regex cannot enumerate every control that homes the
cursor (DECOM, DECCOLM, ...). Snapshot the skipped start row's text before
a newline-prefixed write; if it changed, something climbed back onto that
row and it is restored and repainted with the rest of the range. This
closes the whole class instead of chasing individual sequences.
Co-authored-by: Cursor <cursoragent@cursor.com>
Setting a scroll region (`CSI Ps;Ps r`) homes the cursor, so a
newline-prefixed chunk using DECSTBM can land back on the highlighted
starting row like CUU/VPA. Keep the pre-write restore and repaint the
start row for such chunks.
Co-authored-by: Cursor <cursoragent@cursor.com>
`CSI Ps d` positions the cursor on an absolute row and can jump back onto
the highlighted starting row just like CUU/CUP, so newline-prefixed chunks
using VPA must also keep the pre-write restore and repaint the start row.
Co-authored-by: Cursor <cursoragent@cursor.com>
A chunk like `\r\n\x1b[1A\rOK\x1b[1B` (multi-line progress redraws) starts
with a line advance but returns to the previous row and partially
overwrites a highlighted keyword. The line-advance exemption skipped the
pre-write restore while the post-write repaint also skipped that row,
leaving the untouched cells with the injected color.
Keep the restore and include the start row in the repaint whenever the
chunk contains sequences that can move the cursor back up (CUU/CPL/CUP,
restore cursor, RI, DECRC).
Co-authored-by: Cursor <cursoragent@cursor.com>
Codex review: putting the turn prompt on a cmd.exe line hits the
8191-char limit, mishandles quotes/%VAR%, and leaves the Node child
alive after Stop. Resolve cursor-agent.cmd to versions/*/node.exe plus
index.js and spawn that argv with shell:false.
Closes review on #3039
A write chunk that starts with a line advance (bash 5.1+ fuses the echoed
newline with the bracketed-paste `\x1b[?2004l\r`, command output, and the
next prompt) restored the previous prompt row's highlight before writing,
while the post-write repaint deliberately skips that row. The keyword
highlight on the command line vanished until a background catch-up ran.
Only treat a chunk as an in-place rewrite of the cursor row when it does
not begin with a line advance: once the cursor leaves the row, later CR/EL
sequences can only touch rows the post-write repaint already covers.
Fixes#3000
Co-authored-by: Cursor <cursoragent@cursor.com>
Address the second Codex pass on #3037. A non-ST ESC now ends an
in-flight OSC 9/777/99 the same way xterm does, so hibernated
replay keeps the following CSI/text. OSC 99 fragments are also
concatenated raw so kitty can split on whitespace.
Settings → AI → Cursor Check ran `cursor-agent status` via execFileSync
on the path from `where`, which is a .cmd installer shim. Node cannot
spawn .cmd files directly (EINVAL), so the probe reported logged-out
even when the terminal showed authenticated.
Run the status probe, turns, and model listing through the existing
Windows shim spawn helper, without unwrapping cursor-agent.cmd to
node.exe (that would drop index.js). Also accept status JSON wrapped
in cmd noise.
Closes#3003
User-invoked Select All / Select Word now pulse the gesture so they
still copy. touchcancel releases the same way as touchend so an
interrupted touch cannot leave SearchAddon able to overwrite the
clipboard.
Address Codex review on #3037. Disabled mode no longer marks tab
activity. Hidden hibernated sessions now scan incoming chunks for
OSC 9/777/99 so Codex can still raise a toast before the tab wakes.
A post-mouseup contextmenu was re-arming the gesture with no later
pointer-up, so SearchAddon could keep copying. Pulse contextmenu and
observe it in document capture so tmux/vim select-word still counts.
Copy-on-select listened to every xterm selection change. SearchAddon
selects the active match on find and again 200ms after resize/write, so
searching for a term then selecting other text left the search term on
the clipboard (issue #3007).
Only write the clipboard after a user pointer gesture on the terminal.
Closes#3019
Codex and other agents emit OSC 9 when a turn completes or approval
is needed, but Netcatty ignored those sequences. Register OSC 9, 777
notify, and kitty OSC 99 handlers, map them to Electron notifications,
and reuse tray focus so a click jumps back to the emitting session.
Ignore ConEmu OSC 9;n progress sequences so they do not become toasts.
Closes#3023
A long line that wraps exactly on a space loses that space on copy
(hbase shell becomes hbaseshell). xterm.js 6 may serve a no-endColumn
translateToString(true) from its string cache via String.trimEnd(),
which cannot tell a real last-column space from empty-cell padding.
Prompt detection and similar callers populate that cache first, so
both the normalizer and raw getSelection drop the space.
Always pass an exclusive end column so the copy path uses
getTrimmedLength() instead of trimEnd().
#3026
An empty Ctrl+C copy binding kept the overlay up and still sent SIGINT.
The same happened for an unbound Ctrl/Cmd+A and for context-menu paste.
Keep the preview only when copy/select-all actually consume the chord,
and hide it before ordinary paste.
#3026
Dragging on the overlay can blur xterm, so native copy never hit the
container listener and the raw wrapped rows went to the clipboard.
Listen on document while the preview is open. Also keep a selected
hard newline at a row boundary instead of dropping it.
#3026
Select All built an element-boundary range, so wrap joining never ran
and copied extra newlines. Context-menu paste selection also left the
overlay up, unlike the keyboard path. Select the text node, accept
element-boundary offsets, and dismiss the preview before pasting.
#3026
The overlay had to keep pointer events so history could be copied, but
then every later click stayed on the preview. A click without a drag
now closes it and restores terminal focus. Copy also joins overlay
rows that xterm marked as soft-wrapped, so a long command does not
pick up extra newlines.
#3026
Urgent interrupt still asked only xterm for a selection. With history
preview text selected, Ctrl+C therefore sent SIGINT into vim before the
copy path ran. Use the same copyable-selection flag for interrupt,
shortcut passthrough, and Kitty broadcast.
#3026
Shift+scroll already overlays the normal-buffer history on the
alternate screen, but the overlay ignored pointer events and any
mousedown or copy chord dismissed it. Selection now stays on the
preview so the usual copy shortcuts, context menu, and copy-on-select
can take that text without leaving vi.
#2923
Copy Tab pins the source transport across preflight. If that shell
closes before the queued channel opens, pendingShellReconnectRisk is
already recorded and TERM-SSHD marks the conn non-reusable for shells.
Decide liveness and allowShellReuse at channel-open time so a pinned
captured source cannot accept an immediately exiting session.
#2923
Idle park is not the only last-shell state. An SFTP or forward lease
keeps the transport live and records pendingShellReconnectRisk; those
reconnects now run the same liveness settle. Known-bad banners also
refuse later shell reuse on that socket so start() dials fresh instead
of borrowing the dead session again.
Keep the liveness timer referenced so node --test cannot exit while
the settle is still pending.
#2923
QiZhi TERM-SSHD accepts a new session channel on a parked connection
and immediately exits 0, so reconnect after close flashes the tab
shut. 1.1.80 skipped PID discovery on that path but still reused the
parked transport.
Known-bad banners now end the transport instead of parking. Idle-park
reconnects on unknown banners wait for the new shell to stay open; an
immediate exit discards the transport, denylists the endpoint, and
falls back to a fresh dial. Copy Tab and live multiplex are unchanged.
When the keyboard shortcut scheme is off, snippet recording still
treated default Mac and PC bindings as occupied. Conflict checks now
use only bindings that can actually fire, and the error names the
conflicting action.
Closes#3025
Older AxonHub 0.9 and some intranet OpenAI-compat proxies emit
`data:{json}` instead of `data: {json}`. The stream parser required
the space, so chat chunks were dropped while the model-list probe
still succeeded.
Parse `data:` per the EventSource spec (optional single space) and
cover both the line and end-of-stream flush paths.
Closes#3020
- Add complete es message dictionaries (core, vault, terminal, ai,
systemManager, scripts) covering all en keys with matching placeholders
- Register 'es' in MESSAGES_BY_LOCALE and SUPPORTED_UI_LOCALES
- Extend locale test coverage to include es
A 300ms initial delay let ChatMessageList start the Streamdown import
while the user was still opening the panel to type. Restore the 4s
expand grace and drop a pending Shiki plugin setState if the message
unmounts before the idle callback lands.
Blocking hydrate on focus left history as raw markdown for as long
as the input was focused. Shiki still waits for a real idle gap.
Prose, lists, and inline code can mount after typing stops even if
the textarea still has focus.
messageResponse pulled @streamdown/code on first markdown import, so
opening a chat with history paid for the full highlighter even when
the reply was just prose.
Render with CJK only first. When a fence exists and the composer is
idle, lazily import the isolated Shiki plugin.
Typing a few characters then pausing still hitch because markdown
hydrate only checked the 800ms activity window. History rows would
mount Streamdown while the textarea still had focus.
Treat focused/composing as busy, wait 2s after the last key, and
keep idle IPC on the same predicate.
The first glyph still re-rendered the whole 1500-line ChatInput because
hasComposerText lived in the same component as the model picker. Token
usage snapshots also broke memo with a new object each time.
Move send-button emptiness onto a tiny external store so only that
control updates, and compare contextUsage by value in ChatInput's memo.
Clearing the box immediately after onSend overwrote the pending draft
while handleSend was still awaiting provider sync. A rejected sync or
stale-scope abort then left the prompt gone even though no user
message was created.
Leave the text in place until the parent clears the draft after the
send is accepted. Unmount commit still uses a ref so setter identity
changes cannot write the prompt back.
Typing still hitched because each IME commit started a 120ms
updateDraft, which re-ran the whole useAIState hook. Parent also
passed a new hosts array and providerSwitcher object on every
render, so ChatInput's memo never held. field-sizing-content
relayouted the textarea on every glyph.
Keep typed text in pending refs until send/hide/unmount. Memoize
mention hosts and the provider switcher. Use fixed field sizing
in the composer.
Send awaits provider sync before dropping the draft. During that
wait, ChatInput's onChange cleanup could fire on a new setter
identity and write the still-visible text back into the pending
draft, so the uncontrolled box kept the last prompt.
Clear the field right after onSend starts, and only commit on true
unmount via a ref so callback identity changes cannot restore it.
Codex P1: switching agent on a pristine composer called ensureDraft
then updateDraft. Missing-to-empty was treated as text-only, so the
store skipped notify and the panel kept the previous agent.
Keep first typed flush silent. Empty draft creation, including agent
switch, is a lifecycle change. Sibling empty drafts still do not
wake other panels.
New Chat wrote an empty composer-only draft, which the text-only
equality treated as identical to the previous typed draft. The panel
skipped re-render, so the uncontrolled textarea kept the old string
and send could still submit it.
Treat emptying draft text as a lifecycle change so listeners wake, and
commit the live box on ChatInput unmount so mid-IME hide still flushes.
Every keystroke was re-rendering ChatInput and the first character
created a store draft that broadcast AI state, so the first 1-2 glyphs
painted and the rest dumped after a stall. IME composition hit the
parent on every pinyin letter, and Chromium spellcheck loaded on the
first word.
Keep the textarea uncontrolled, commit parent state on idle/blur/send,
and treat missing-to-composer-only drafts as text-only so store
listeners do not wake. Draft flush, send, and hide still persist the
current box text.
The throughput test failed on CI because quiet catch-up sliced the
viewport into 32-line chunks. A 40-row terminal therefore called
refresh twice while catching up, which is not an atomic paint.
Recolor offscreen first, then refresh the visible range once after
catch-up finishes. Count post-settle compositor frames separately so
DOM's extra layout paint is not treated as a second catch-up render.
If the terminal or workspace goes away during provider/model preflight,
the awaited send must not create a session on a pruned scope. Bump a
send epoch on unmount or scope change and abort after the awaits.
Codex P2: Agent selector, New Chat, and history stayed clickable while
provider/model IPC ran. Switching agent or starting a new chat then let
the stale send finish and wipe the replacement draft. Disable those
header transitions until preflight ends, and abort if the view still
changed.
Codex P1s: isSending must end after sync so Codex App Server steering
works, and permission mode is frozen via lockTurnConfiguration during
preflight then read from a ref at launch. The first SDK send now uses
the catalog model from this same turn, not a stale curated fallback.
Codex: users could still attach files or change skills while provider
sync was in flight, and the first SDK turn could send a curated fallback
model. Disable the composer for the whole send, and load the runtime
model catalog before starting the turn.
Codex P1s: do not wipe text typed while provider sync is in flight, and
push web-search credentials before the first turn. Also resolve custom
Codex config on show instead of after the idle grace window.
Codex P2s: keep a stable pending draft identity so steer success can
clear the composer; take the send gate before awaiting provider sync;
and hydrate Streamdown after recent typing instead of waiting for blur.
Codex P1: first-launch send could race a 4s idle delay on
aiSyncProviders while main still had an empty provider list. Sync on
panel show and await it in handleSend.
Codex P2: slash-picker /stop and /compact called parent onChange('')
without updating local composerText, so the command stayed in the
textarea. Route those clears through commitComposerText.
Codex P2: focusing or typing cancelled the only scheduled callback, so
provider sync and discovery never ran if the user submitted without
blurring. Re-arm after activity and treat IPC as busy only while the
composer was recently used, not for the entire focused lifetime.
Review found three real gaps: send/new chat left pending composer text in
place, hiding the panel could leave body-portaled menus open, and idle IPC
could still start on the first frame after expand.
Discard pending text when the draft is cleared, close ChatInput and agent
menus when the panel is parked, and give composer-idle IPC the same expand
grace as Streamdown warmup.
Opening the AI panel and typing a few characters hit a main-thread stall.
Streamdown warmup, panel remount, draft-store fan-out, and expand-time
IPC all raced the first keystrokes. import() cannot be cancelled, so the
heavy renderer must not start while the composer is busy.
Keep the composer mounted on hidden retained panels, ignore composer-text
store notifications, debounce draft flushes, and defer discovery/skills/
model IPC until the composer is idle. History stays plaintext until
Streamdown is warm, then hydrates a few rows at a time.
Opening the lazy AI panel can 504 a pre-bundle. The serve plugin used
to send a full-reload, which flashed the splash and wiped renderer
state. Warn instead and pre-optimize the AI/streamdown deps.
ipcRenderer.invoke strips ssh2's status code. A server that says
"File not found" instead of "No such file" still uses code 2, so
classify absence in the main-process handler and return null.
Substring search could treat a permission error as missing if the
path contained "enoent". Unwrap the Electron invoke prefix and match
the known ssh2/Node absence kind at the start of that message.
After #2955, upload conflict checks lstat the destination first.
New files throw ssh2's "No such file"; Electron invoke strips `code`
and wraps the message, so isMissingStatError treated absence as a
hard error and every new upload toasted.
Crash-log and other system notes sat flush against the card when they
were wrapped with the card in SettingsAnchor. Use a shared SettingHint
with a top gap for every system-page footer note.
Add a clipped sheen across the active connection segments so the bar
reads as live work instead of a static slab. Skip it on error and
honor reduced motion.
The connecting dialog bar stretched, shrank, then stretched again
because the initial value was 15, the timeout effect snapped back to 5
whenever hop or phase changed, and hop updates could overwrite a higher
value. Seed once when status becomes connecting and only move forward.
The close-time animation frame survived effect cleanup. Reopening and
searching before it ran could consume the new marker and wipe the new
query. Cancel the frame and ignore it if search is open again.
Clearing the query resets highlights while the bar stays open. Closing
afterwards still needs a leftover sweep, but must not treat that stale
search as a reason to wipe a newer manual selection.
Related to #2980
SearchAddon can leak incremental-match decorations, and WebGL bakes
those yellow backgrounds into the glyph atlas. After Esc the handles
are gone but stained cells remain. Track search decorations on
registerDecoration, paint fill on the HTML overlay only, and resweep
after the search-bar close refit.
Version-only saveSyncConfig no longer check-then-writes SYNC_PREFERENCES.
That upgrade-time race could overwrite a concurrent Settings toggle with
stale autoSync=true. Legacy preference fields stay in SYNC_CONFIG until a
preference writer splits them out.
Key-chord fan-out now delivers remapped LF through handleTerminalInputData
on each peer. handlingKittyBroadcast already blocks re-broadcast, but that
made canBroadcastInput false and armed password assistance on the target.
Keep the re-fan-out guard and only arm autofill for locally originated
submits.
skipBroadcast only stops the raw-string fan-out so peers can re-resolve
the chord from their own buffer. Sudo autofill still has to treat that
key as a broadcast, or a main-buffer sudo/su submit arms password
assistance on the source session.
Pause implement, issue follow-up coding, and the Codex review loop
so Cursor only classifies new issues. Restore with
CURSOR_AUTOMATION_MODE=full and the commented schedule crons.
SearchAddon can re-select the prior active match ~200ms after a write
or resize. The revival guard now clears that selection unless the user
started a new one during the window.
#2962
AI terminal_execute used to append the wrapper to whatever the user
had already typed. Prefix interactive PTY writes with a short
line-clear sequence so the injected command starts on a clean prompt.
Raw and serial sessions are left unchanged.
* fix(terminal): avoid keyword highlight flicker
* fix: address Codex review on PR #2951
* fix: address Codex review on PR #2951
* perf(terminal): defer highlight catch-up under load
* perf(terminal): bound deferred highlight work
* fix(terminal): bound highlight catch-up backlog
* fix(terminal): mirror history wipes in highlighter
* fix(ci): load throughput baseline from PR base
* fix(terminal): preserve highlight rebuild ordering
* fix(terminal): drain highlights before exit capture
* fix(terminal): preserve line state across clear
* perf(terminal): slice history serialization
* fix(terminal): preserve parser state on recolor
* fix(terminal): preserve protocol state on recolor
* fix(terminal): retain live state during recolor
* fix(terminal): close recolor state gaps
* fix(terminal): retain saved colors across recolor
* fix(terminal): serialize recolor side effects
* fix(terminal): color keywords in place without history rebuild
The previous approach rewrote the stream and replayed a second
terminal, which forced private xterm state snapshots and still
risked flicker-adjacent reset work. Writes now stay pristine and
only dirty cells get a new foreground. Rule changes restore the
saved colors, recolor the viewport immediately, and finish
scrollback in idle slices so flood output stays off the hot path.
Related to #2784
Related to #2879
* fix(terminal): keep in-place keyword coloring correct under load
Enter no longer restyles the previous row, and write/catch-up
positions now follow xterm markers so saturated scrollback cannot
skip a just-written match. Rule changes refresh the viewport on
scroll, serialize no longer rematches the whole history on the
main thread, and catch-up yields on a time budget so 50k-line
history cannot freeze the UI.
Related to #2784
Related to #2879
* perf(terminal): keep flood and scroll off the highlight hot path
Pinned log tails were rematching the whole viewport on every
output-driven scroll, and every write allocated a marker. Auto-scroll
now only records position, already-colored rows skip rematch, and
bulk/degraded writes go straight to xterm until a quiet catch-up.
Related to #2784
Related to #2879
* perf(terminal): merge flood writes on a 16ms burst window
Electerm keeps log tails smooth by coalescing attach writes for one
frame after a flush. Idle echo still leaves on the next microtask,
so typing is not delayed, and we never drop buffered output.
Related to #2784
Related to #2879
* fix(terminal): keep keyword originals across append, clear, and alt screen
Same-line appends no longer throw away saved colors just because the
line fingerprint changed. Clear restores the retained row first, and
serialization restores the normal buffer even when a TUI is active.
Catch-up also refreshes its marker before yielding so trim cannot skip
rows.
Related to #2784
Related to #2879
* fix(ci): stop resetting highlight catch-up on every flood write
The throughput gate was comparing cell-color wrapping to raw xterm
and failing at ~18% on DOM CI. Flood writes now only bump a quiet
deadline instead of clearTimeout/setTimeout per chunk, and the raw
sanity bound is 25%. The product check remains 10% versus main.
Related to #2784
Related to #2879
* fix(terminal): rematch recycled rows and cap wrapped catch-up
Quiet catch-up now treats a recycled BufferLine as stale when its
saved highlight colors are gone, so repeated keywords on a full
scrollback still color. Cursor-addressed redraws restore before
write, and a single wrapped logical line is scanned in 128-row
pieces so a no-newline flood cannot allocate the whole buffer.
Related to #2784
Related to #2879
* fix(terminal): accept plugin #RRGGBBAA highlight colors
Plugin decorations allow 8-digit hex. Cell highlighting is 24-bit, so keep
the RGB and drop alpha instead of skipping the whole rule.
* fix(terminal): color cells for partial grapheme matches
A match that ends inside a combining cell maps both endpoints to the same
xterm column. Color that cell instead of building an empty range.
* fix(terminal): rematch flood rows after empty-line stamps
A hot-path \r\n write stamped the following empty cursor row as current.
The next flood chunk reused that BufferLine with ERROR text, and quiet
catch-up skipped it. Fingerprint the stamped content so filled or
recycled identical rows are recolored.
* perf(terminal): avoid flood-path registerMarker after trim
Once a catch-up range exists, later bypassed writes only keep the earliest
bound. A disposed marker collapses to 0 without allocating a new one on
every saturated-scrollback chunk.
* fix(terminal): recolor the row retained by clear
xterm clear keeps the cursor row after we restore originals. Recolor that
surviving line so enabled rules still apply until the next write.
* perf(terminal): stamp no-match rows without cell snapshots
Catch-up only needs a generation and fingerprint when a line has no
keyword. Allocate typed-array originals when a cell is actually colored.
* perf(terminal): treat pinned-bottom recycle as output scroll
Once scrollback is full, baseY and viewportY stay put while rows recycle.
That was treated as a user scroll, so every flood write rematched the
visible area. Keep pinned-to-bottom updates off the rematch path.
* perf(terminal): keep quiet catch-up to a single viewport paint
Catch-up already refreshes visible slices. A trailing recolorVisible()
caused a second settle paint and tripped the atomic-repaint gate.
---------
Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com>
Multi-terabyte pools rendered as raw gigabytes (118736.4G), which is hard
to read at a glance. Scale each side of the used/total pair independently
so a mostly-empty pool keeps its used figure (136.37G/6.93T instead of
0.13/6.93T), and emit a single shared suffix when both land in the same
unit so ordinary disks keep the familiar 71.12/878.15G shape.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codex P2: unconditional normalize broke custom proxy prefixes such as
https://proxy.example/anthropic that already complete the SDK base.
Restrict /v1 inference to scheme+host origins (Claude Code style) and
leave other path prefixes unchanged.
Reject sudo+forced-SCP before channel open, and fall back to the standard
SFTP subsystem on exit 127 so reused SSH transports do not re-prompt MFA
on hosts without a standalone sftp-server (e.g. ESXi).
Accept realpath('/') as home only after a successful readdir so virtual/chroot
roots from #2934 still work, while non-listable root no longer skips
/home/<user> and /root probing. Also retry those candidates if listing fails.
The storage event listener uses setShowTabNumberBadgesState but
omitted it from the useEffect dependency array, which triggered
react-hooks/exhaustive-deps.
Pointermove used to setTreeWidth every pixel and re-render NotesManager
including MDXEditor. Apply live width via rAF + aside.style, commit React
state and storage only on pointerup.
Radix ScrollArea's display:table viewport child blocked flex shrink, so
narrowing the notes tree clipped long titles mid-glyph. Constrain the
tree width chain and ellipsize labels so content adapts with the panel.
container-type on .netcatty-mdx-editor makes MDX adjust fixed-anchor coords
as if a containing block exists, but browsers do not. Add a zero transform so
the fixed CB is real and the preview no longer shifts into the notes tree.
Merge seed only wrote activeSessionIdMap, so unsent draft demoted the panel
to draft mode and send created a new session while the old chat still showed.
Prefer persisted workspace active over hasDraft, and materialize panelView.
* fix(#2872): automated Cursor CLI fix
* fix: address Codex review on PR #2875
* fix: address Codex review on PR #2875
* fix(session-logs): sample alternate-screen state after save dialog
Manual session log start previously snapshotted alternate-screen mode
and the initial line before showSaveDialog resolved. Split path selection
from stream start so the renderer re-samples buffer state at activation,
avoiding stale omission/leak if the user enters or leaves a TUI while the
dialog is open.
* fix(session-logs): honor C1 CSI alternate-screen modes in text renderer
8-bit CSI (U+009B) is a valid alternate form of ESC[. Recognize it in
TerminalTextRenderer so DECSET 47/1047/1049 enter/leave still omits TUI
paint from txt/HTML session logs when remotes use the C1 encoding.
* fix(session-logs): apply log-safe RIS reset in text renderer
RIS now leaves alternate-screen mode, resets SGR to defaults, and starts
post-reset shell text on a new log line when needed so mid-line TUI
exits do not glue onto prior output or keep pre-entry colors. History is
preserved because session logs are not live screen wipes.
* fix(session-logs): token-bind dialog path and rebase after RIS
- chooseManualSessionLogPath stores the selected path in main and returns
an opaque selectionToken; startManualSessionLog redeems that token and
rejects renderer-supplied filePath so the save dialog cannot be bypassed.
- RIS rebases screenBaseRow onto a fresh log screen (history preserved)
so post-reset CSI H cannot overwrite pre-RIS text.
* fix(session-logs): seed replay sanitizer on observe attach mid-TUI
When an observe popup restores a snapshot while vim/less is already on
the alternate buffer, re-seed the connection-log sanitizer from the
restored xterm active-buffer type so subsequent TUI redraws are omitted
until leave.
---------
Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com>
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
* fix(notes): domain paste policy + Codex review follow-ups
Move clipboard paste resolution to domain/notes (pure, no React) and
address post-#2868 review items: structured text/plain wins over
wrapper HTML, caret-safe insert for long pastes (no EOF append),
https/protocol-relative image src, entity decode, quote-aware img
attrs, code-region protection, nested HTML islands, hard-break
preservation, height-only image CSS, and complete small-icon widths.
* fix: address Codex review on PR #2869
* perf(notes): cut hover re-renders and heavy image :has() CSS
Reduce perceived lag in large notes by (1) coalescing link-hover
setState with equality checks and rAF, (2) replacing the combinatorial
:has(img[width]) matrix with data-note-img-size markers plus lazy
images, and (3) merging DOM decoration into one observer that debounces
in edit mode so Lexical keystrokes do not re-walk anchors every frame.
* perf(notes): render preview with Streamdown instead of MDXEditor
Vault note preview no longer mounts Lexical/MDXEditor/CodeMirror for
read-only viewing. Preview uses static Streamdown (same stack as chat)
with HTML islands for centered heroes and sized images; edit mode keeps
MDXEditor. This cuts large-note scroll and mode-switch cost while
preserving host-link click handling and compact badge image layout.
* fix(notes): render markdown inside center HTML for Streamdown preview
Streamdown/remark does not parse Markdown nested in HTML blocks, so
README-style <div align="center"> heroes showed raw # ** and badge
syntax. Preprocess preview markdown: expand centered islands via marked
GFM to HTML, and rewrite relative images so Streamdown no longer shows
"Image blocked" chips under app CSP.
* fix(notes): GitHub-style preview with react-markdown
Replace Streamdown note preview (chat-oriented Image blocked chrome and
poor README layout) with react-markdown + remark-gfm + rehype-raw/sanitize
and github-markdown-css. Center HTML islands still expand via marked so
pasted heroes render; relative images and empty links are dropped quietly
instead of alt-as-title or blocked chips.
* fix(notes): keep relative images; map Vite public/ to site root
Stop dropping non-https note images. Preserve absolute and relative src
as-is, except rewrite repo public/ assets to site-root paths
(public/icon.png → /icon.png) so Vite/Electron serve them without
/public/* warnings. Unwrap oversized center blocks so Features-style
body stays left-aligned in preview like edit mode.
* fix(notes): left-align preview body; center only README hero
Force markdown-body text-align left and direct block headings/lists left.
After the first hero thematic break, strip align=center / text-align:center
so Catty/Features sections no longer inherit full-width centering. Tighten
oversized-center heuristics (Catty Agent, lists, blockquotes) without
treating the Netcatty logo title as a body section.
* fix(notes): force preview body left-align (nuclear CSS + unwrap)
Catty/Features still rendered centered because small align=center shells
and inheritance beat partial CSS. Force every node left via * selector,
re-enable center only under [align=center], unwrap all center shells after
the hero --- band, and set inline textAlign left on the preview root.
* fix(notes): stop /public asset requests; fix preview center inheritance
Root cause of body still centering: CSS re-applied text-align:center to
all descendants of [align=center], so one large shell centered Catty/lists.
Keep * left; only the shell itself may center (inline badges).
Root cause of Vite /public/* warnings: stored notes still had /public/...
paths and MDX loaded them raw. normalizeNotePublicAssetPaths rewrites
/public/ and public/ to site-root paths for both edit and preview display.
* revert(notes): restore MDXEditor for preview (drop GitHub markdown path)
GitHub-style react-markdown preview caused layout and asset-path issues
that diverged from edit mode. Preview again uses MDXEditor with
readOnly + mode key remount, matching the pre-Streamdown/GitHub path.
Keep display-time public/ → / rewrite so Vite does not request /public/*.
* perf(notes): reuse MDX editor when switching notes
Remove key={noteId} remount that tore down Lexical/CodeMirror on every
note change. Pass noteId and call setMarkdown on switch instead, after
the existing draft flush, so selection stays snappy and only document
content is replaced.
* perf(notes): defer setMarkdown on switch so selection paints first
Yield a double rAF before Lexical import on note change, show a
blocking overlay for large notes, and ignore external sync while a
swap is in flight. Hardens flaky Suspense loading assertions in SSR tests.
* fix(notes): align task-list checkboxes and make them clickable in preview
Center GFM checkboxes on the first text line, give them a solid hit target,
and toggle markdown from preview clicks since Lexical readOnly blocks
CheckListPlugin. Pure toggle helper lives in domain/notes/taskList.
* fix(notes): close multi-agent review findings on switch/paste/tasks
Gate stale onChange during deferred note-switch import, keep draft refs
in display-markdown space, baseline paste settle on live getMarkdown to
avoid false success after muted setMarkdown, mask code regions in task
toggles, and drop abandoned GitHub/Streamdown preview module and deps.
* fix(notes): address remaining Codex review findings
Clear Lexical undo history on note switch, always inert-block during
deferred setMarkdown, re-annotate host links and image sizes on attribute
mutations, harden entity/code-region paste paths, and angle-bracket image
destinations that contain spaces.
* fix(notes): tighten Codex follow-ups on src, code masks, tasks
Prefer real img src over data-src, normalize public/ paths only outside
code, mask indented/blockquote fences, and accept 1) ordered task markers.
* fix(notes): keep swap payloads fresh; keep nested list tasks
Refresh deferred setMarkdown when the same note's value changes mid-yield,
and stop treating nested list lines as indented code so task toggles stay
aligned with the preview DOM.
* test(notes): update swap guard source assertions
* fix(notes): collision-free code masks, real href, cancel hover rAF
Use a document-unique mask sentinel, match anchor href with attribute
boundaries (not data-href), and cancel the pending link-hover frame on
mouse leave so chips cannot reappear after exit.
* fix(notes): accept longer closing fences in code masks
CommonMark allows closing fences with more backticks/tildes than the
opener; line-scan masking now matches length >= open so public/ paths and
task samples inside those blocks stay protected.
* fix(notes): harden CommonMark fence, span, and indented-code masks
Use homogeneous fence openers, multi-backtick inline spans, and list-context
aware indented-code detection so samples stay masked without breaking nested
task indexing.
* fix(notes): task spacing, angled image dests, deep blockquote fences
Require whitespace after task brackets, parse <angled> image destinations
with spaces, and mask fenced code at any blockquote depth.
* fix(notes): raw-text HTML elements and HTML comment task masks
Locate script/style closers without nested-tag walks so raw bodies with
'<' are removed, and mask HTML comments before task indexing so preview
toggles hit visible checkboxes only.
* perf(notes): drop fade animation on note switch and editor load
Use an instant solid cover instead of opacity/transform fade-in so Lexical
swaps do not pay extra composite cost that can feel more janky than a hard cut.
---------
Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com>
Restyle the ImageDialog overlay, panel, labels, inputs, dimension row,
and Save/Cancel buttons with Netcatty tokens so the form no longer
uses stock MDX light/flat chrome in the dark notes editor.
Linked shields/ko-fi badges were reduced to [Label](url) so icons vanished.
Normalize to single-line [](href) or <a><img></a>, allow relative
README image paths again, and style linked images as inline badge rows.
Wrap README-style align=center / text-align:center blocks in
<div align="center"> so MDX GenericHTML keeps layout. Restore ATX
heading markers inside centered h1–h6, skip re-turndown of center
wrappers, and add text-align CSS for align=center in the note editor.
README distro walls use width=48 SVGs that MDX stacks as blocks. Treat
common small width attrs (≤96) as inline-block on both the img and MDX
image wrapper, hide per-icon chrome until hover, and leave large
screenshots as full-width block images.
Preserve HTML img dimensions in note source (including large screenshots).
Side-panel display still uses max-width:100% and height:auto so content
fits without cropping; small logos can honor their smaller width attrs.
Product policy: notes are not a GitHub renderer. Collapse linked badge
images to text links (no ](url) debris), drop oversized screenshot width
attrs so the side panel scales with CSS instead of cropping, and keep
modest widths only for small assets. Center/align HTML remains out of scope.
Keep width/height by emitting sanitized HTML <img> (MDXEditor's HTML
image visitor understands them); plain  alone drops size.
Cap note images at max-width 100% with auto height so large README
screenshots fit the pane instead of overflowing at native pixel size.
GitHub-style pastes mix markdown with <img>/<a name> HTML. Running Turndown
on the whole string escaped headings and collapsed structure. Convert only
HTML islands (Turndown per fragment), keep markdown intact, and enable
MDXEditor imagePlugin so  from paste actually shows.
Replace the hand-rolled HTML→Markdown converter with Turndown + GFM.
Prefer text/html when present (GitHub/browser pastes) so raw <img>/<a name>
islands in text/plain no longer make MDX insertMarkdown silently no-op.
Browser/Word copies put structure in text/html while text/plain is flat.
Resolve HTML to markdown (headings, lists, links, emphasis, tables, code)
and intercept those pastes so Lexical no longer swallows rich HTML.
Structured plain markdown still wins when the user copies .md source.
Structured long paste was preventDefault'd then handed to insertMarkdown;
when Lexical no-op'd or took longer than the short settle poll, the
clipboard was swallowed with no UI change. Prefer document setMarkdown
merge for large pastes and fall back to the same path if a short
selection insert still no-ops after a size-scaled settle window.
- Use scoped historySessions for draft-force and ownership-clear guards
so out-of-scope sessions demote instead of sticking blank.
- On server-stats pause, keep the last snapshot but clear givenUp so
resume can auto-retry after hard failure.
- Fix ProcessRow undeclared vendorLabel via vendorDisplayLabel; make
GPU marks decorative (aria-hidden); attribute Simple Icons as CC0.
- Sticky icon-only when tab bar width is zero; include workspaces in
TerminalLayer ctx deps.
Prevent StrictMode draft demotion after send, keep workspace AI maintenance
mounted with a workspaces list, and keep live Streamdown markdown while
streaming. System Manager gains overflow-aware icon-only tabs, customize
layout, GPU vendor badges, and cache-preserving overview remounts. Settings
search scrolls only the content pane so traffic lights stay put.
When dissolve prefers a focused pane that did not own the inherited chat,
retarget the session and clear the old terminal selection so ownership
filters do not hide it as another scope's active thread.
Co-authored-by: Cursor <cursoragent@cursor.com>
Mount AI maintenance even when no side-panel tools are open, and clear a departed pane's active-map entry when a live workspace retargets its chat.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep inherited terminal-scoped chats visible in a still-live multi-pane workspace by reminting the active chat onto a surviving pane.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep a single SFTP portal/transfer owner when promoting or dissolving workspace tabs so the visible panel retains live browser state.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the surviving terminal on the dissolved workspace's active session view, and remint chats whose original pane is gone so history matching still resolves them.
Co-authored-by: Cursor <cursoragent@cursor.com>
Sidebar chats are scoped to already-open terminals/workspaces; exposing
host_open let Catty expand scope mid-turn and then fail to drive the new
session. Restrict vault.host.open to global/MCP/CLI and stop prompting
Catty to open hosts itself.
Fixes#2856
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Inherit the focused terminal's AI session when tabs merge, hand it back on dissolve, and remap side-panel open/layout/SFTP state so the panel does not look cleared.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep Home, the leading root, and the ellipsis fixed while only trailing chips scroll toward the current directory, and always show the ellipsis when hidden segments exist under a budget of one.
Co-authored-by: Cursor <cursoragent@cursor.com>
Always retain the first breadcrumb segment when truncating, clamp tiny budgets, and scroll overflow viewports to the trailing edge so narrow panes still show the current directory without RTL right-align.
Co-authored-by: Cursor <cursoragent@cursor.com>
Drop zsh ghost text past the cursor when cell fg/dim differs from the
accepted input, so mid-line history edits still record full commands
without absorbing unaccepted cross-token suggestions.
Co-authored-by: Cursor <cursoragent@cursor.com>
Drive-letter truncation kept C: visible, but WSL/UNC roots were dropped because isWindowsDrive is false for those paths. Keep the leading UNC share root the same way.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep inline SFTP actions left-aligned and push only the collapsed ⋮ control to the right of the actions row.
Co-authored-by: Cursor <cursoragent@cursor.com>
The first two-row layout right-aligned short paths via RTL clip and pushed toolbar actions with ml-auto. Keep the address on top, left-aligned, with controls underneath.
Co-authored-by: Cursor <cursoragent@cursor.com>
Only prefer the full painted line for mid-line edits when the keystroke
buffer is already a whole token there, and keep the cursor-visible
command when an unaccepted zsh ghost continues past the cursor.
Co-authored-by: Cursor <cursoragent@cursor.com>
Deep side-panel paths were squeezed beside action buttons and clipped at the end. Split path chrome onto a dedicated row and truncate/overflow toward the current directory.
Co-authored-by: Cursor <cursoragent@cursor.com>
Re-arm disposedRef when the autocomplete mount effect re-runs so Fast Refresh
cleanup no longer permanently kills fetchSuggestions. Allow local history/fig
matches during empty-echo lag while still blocking sensitive prompts, and fall
back pane visibility for standalone terminal mounts.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the one-click hostname copy from #2818 without crowding host cards;
expose it next to copy-credentials in list/grid and tree menus instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
Hide search behind an icon toggle, surface create actions as inline icons,
and move bulk delete into a selection bar so the Library toolbar stays compact.
Co-authored-by: Cursor <cursoragent@cursor.com>
Prefer the reliable keystroke buffer under SSH echo lag so local history/fig/snippet matches appear without waiting for remote echo or path listings. Soft-budget remote path completion and keep Enter recording on the stricter alignment path.
* fix(#2826): automated Cursor CLI fix
* fix(scripts): confirm bulk delete and clear host bindings
Scripts side-panel mass-delete skipped confirmation and, when
onSnippetsChange was wired, filtered snippets without clearing host
login/connect script bindings. Confirm first, always dispatch the shared
delete event, and apply deletes via deleteSelectedSnippetsFromVault.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(scripts): harden delete handler against stale vault state
Read snippets/hosts from refs in the shared delete listener so rapid
confirms cannot resurrect already-removed items, and clear pending bulk
delete when the scripts panel hides so the confirm dialog does not
resurface after a mid-prompt hide.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(scripts): keep delete refs current within the same tick
Update snippetsRef/hostsRef immediately after applying a delete so a
same-tick second event cannot rebuild vault state from the pre-delete
snapshot and resurrect removed snippets or host bindings.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: address Codex review on PR #2828
* fix(scripts): hoist bulk-delete confirm outside toolbar popover
Portalled Dialog focus dismissed the compact Scripts popover and cleared
pendingDeleteIds before confirm. Own the confirm in TerminalToolbar and
defer via onBulkDeleteRequest when nested in that popover.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: serialize snippet saves with bulk-delete vault lock
updateSnippets wrote STORAGE_KEY_SNIPPETS without withVaultImportLock, so a
concurrent edit could land between bulk-delete's snapshot read and journal
commit and be discarded. Queue ordinary snippet writes on the shared lock and
include them in waitForPendingVaultWrites.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: rebase queued snippet saves onto latest vault snapshot
Locking updateSnippets alone still wrote a pre-delete full array, so a
popup bulk-delete could be resurrected (or a storage-event version bump
could cancel the local edit). Rebase under the vault lock and supersede
only newer local writers via snippetsWriteOwnerRef.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
---------
Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Related to #2561.
Adds detect-first Ports/Services system manager tabs, network-appliance
read-only gating, and hardened collectors/parsers from Bugbot review.
* feat(portforward): make main process authoritative for runtime state
Phase 1 of #2288: add snapshot/runtime event protocol with epoch+revision,
stop persisting active/connecting/error into localStorage, migrate legacy
stored phases on read, and project unknown when snapshots are unavailable.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(portforward): access runtime bridge via netcattyBridge
Satisfy the no-restricted-properties lint rule for window.netcatty.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(portforward): honor snapshot authority and prune on resync
Default runtime projection to unknown until a successful snapshot,
reconcile (not sync-only) on epoch/revision gaps, serialize runtime
event handling, clear worker runtime subscribers on window destroy,
and align tray unknown UX with RuleCard.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* Cursor: Apply local changes for cloud agent
* fix(terminal): stop first SSH fail under StrictMode remount
Defer backend boot past the double-invoke, cancel superseded vault init,
gate start on vault hydration, and harden hotkey/attach cleanup so the
first connect no longer races a stale close or empty keychain.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* perf: finish StrictMode mega-subscriber plan (accent isolation + docs)
Stabilize published terminal themes across custom-accent drag, apply accent
at chrome/log leaves and CSS injection only, land Phase 0 design/plan docs,
and lock Host-bag / Bugbot-clean contracts for the isolation checklist.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* docs: add StrictMode mega-subscriber design and plan artifacts
Force-add Phase 0 spec/plan under docs/superpowers (gitignored locally)
so the delivery branch carries the approved design and plan copy.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: clear remaining StrictMode doubles and notes editor jank
Gate every AI send and reject concurrent runTurn, latch tray connect flush
and settings persistMounted remounts, and stop notes content drafts from
rebuilding MDXEditor/shell on each keystroke.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* perf: gate hidden notes panel subscribe and skip host-link annotate on keystrokes
Hidden terminal notes mounts keep a live snapshot read but stop listening to
publishes. Host-link DOM annotation follows hosts/mode (preview MutationObserver)
instead of every markdown value change.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: latch SSH transport idle TTL notify under StrictMode
Avoid duplicate main-process notifySettingsChanged on effect double-invoke
when the idle TTL value has not changed.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* perf: freeze inactive vault notes against hosts churn and prefetch MDXEditor
Gate the retained VaultNotesSection notesStore subscription while hidden,
ignore hosts identity in its memo compare when inactive, and warm the lazy
InlineMarkdownEditor chunk when notes become active.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: clear remaining AI StrictMode double-fire hazards
Latch Ask-AI terminal selection on requestId before draft mutation, hoist
Codex App Server IPC bridge to App (ref-counted), and make Catty
requestApproval join an existing waiter for the same toolCallId.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: remount-safe AI send latch and skip stale MCP approval IPC
Replace the component sendInFlightRef with a module key latch so StrictMode
panel remount cannot reopen a mid-flight send, and no-op resolveApproval when
the pending entry is already drained.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* test: allow gated useNotesStore in VaultNotesSection memo contract
The retained notes section now passes enabled:isActive so hidden mounts do
not subscribe; update the VaultView memo source assertion accordingly.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: keep note drafts across mode toggle and external note publishes
Flush ref-only content before edit/preview remount, and skip prop→editor
setMarkdown while a local draft diverges from the last synced value.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* chore: strip StrictMode sermon comments after Bugbot/Comment Sicko pass
Keep effect-order and public-export contracts; drop remount/perf narration
that the latches and tests already encode.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: remount-safe Ask-AI terminal selection latch
Move consumed requestIds to a module Set so StrictMode remount cannot
append the same selection chip twice before parent state clears.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* chore: drop remount latch comment
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Split vault/session/settings into publishers and store-backed Hosts so hot
fields no longer rebuild the whole shell, and harden Terminal boot / startup
effects for StrictMode double-mounts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(system-manager): parse CANN 24.x Ascend npu-smi info tables
ModelArts / CANN 24.1 tables include Hugepages-Usage and Bus-Id chip rows
that the old regex rejected, so the GPU tab showed zero Ascend devices
even when npu-smi was present. Parse summary+chip pairs like nputop,
always collect the info dump, and fall back to info -m for NPU IDs.
Closes#2811
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* feat(system-manager): harden Ascend parsing from nputop fixtures and polish GPU UI
Borrow real npu-smi samples from youyve/nputop (Apache-2.0) so CI covers
910B2C, 310B4, Atlas A3 multi-chip, and 310P3 process tables without
hardware. Aggregate multi-chip NPUs, parse info-dump processes, and surface
npu-smi driver versions.
GPU tab now keeps a short util/HBM sparkline history and ResourceBar uses
smooth width transitions with load-aware tones, closer to an nvtop feel
inside the existing system-manager chrome.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
downloadToTempWithProgress passed totalBytes:0, which transferBridge
treats as an explicit empty snapshot and skips the remote body, so
external editors opened empty files (fixes#2787).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): raise autocomplete suggestion default beyond 8
Snippet keyword matches were truncated at autocompleteMaxSuggestions=8
before the floating list, so users with many matching scripts could not
scroll to the rest (fixes#2793).
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): migrate persisted autocompleteMaxSuggestions 8 to 50
Existing installs saved the old default into localStorage with no settings
UI, so raising DEFAULT alone left the popup capped at 8. Treat 8 as the
legacy sentinel while preserving intentional custom caps.
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): stop keyword highlight flicker on Enter
Idle Enter was misclassified as user scroll so overscan decorations were
pruned before writeParsed cancel (Ubuntu RTT). Also swap cursor-line
decorations atomically to avoid clear-before-create flash (fixes#2784).
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): catch up highlights after Enter protection window
While enterInputPending blocked scroll-prune, viewport scrolls were also
ignored with no dirty mark. Mark dirty during the window and trigger a
write refresh when Enter protection clears.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): persist keyword highlights across scroll (#2784)
Replace Enter scroll-skip band-aid with mam15mon/#2348 persistence:
keep decorations across scroll, sync only newly exposed rows, bound
retention (~20 viewports / max 1200). Keep Enter write-path protections
and cursor-line atomic swap. Continuous Enter may scroll; prune-on-scroll
is gone.
Co-authored-by: mam15mon <mam15mon@users.noreply.github.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): bound persistent keyword prune to scroll path
Keep Enter/write deferred pruning intact by pruning the retention cap
only after scroll viewport indexing, not on every decoration apply.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): rescan dirty overlap on persistent scroll refresh
Overlapping scroll only scanned newly exposed rows then cleared the whole
viewport dirty set, which could drop in-place redraw work when scroll
outranked a pending write refresh. Rescan write-dirtied overlap lines and
only clear dirty for ranges that were actually processed.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Co-authored-by: mam15mon <mam15mon@users.noreply.github.com>
Closing the active tab fell back to last workspace/solo in array order,
so opening a temp tab then closing it jumped away from the prior work
tab (fixes#2788).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(notes): protect title IME, surface persist failures, harden paste
Note titles rewrote controlled value during CJK composition (Sogou Wubi).
Notes localStorage writes ignored QuotaExceededError so restarts lost
content. Markdown paste intercept could preventDefault with a no-op insert
(fixes#2783).
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(notes): commit title draft on blur for IME composition
IME-only title entry never called onCommit until compositionend; blur
could flush an empty draftTitle and then adopt the stale external value.
Commit the local draft on blur before parent flushNoteDraft.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(notes): stash IME title drafts in refs without controlled rewrite
Composition updates now live-stash into draftTitleRef for teardown and
note-switch flushes, while onCommit still waits for idle IME. Blur syncs
superseded external titles before flush so stale drafts cannot overwrite.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(notes): clear stashed title when IME composition is superseded
compositionEnd now live-stashes the adopted external title so pagehide
and note-switch flushes cannot persist rejected composed text.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(notes): cancel draft flush during IME title stash
Prior idle commits could still debounce-flush mid-composition and rewrite
the controlled title. Stash now clears the timer, and external supersede
in the adopt effect live-stashes the authoritative title.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(notes): dedupe quota failure toasts during autosave
Debounced draft flushes can hit QuotaExceededError repeatedly; rate-limit
the error toast so users are not spammed every 300ms.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Address Bugbot: do not skip every `# [` comment. Only ignore known app
debug tags (transferDiag, FileWatcher, SessionLogStream, ...), while
still failing on unknown bracket diagnostics like `# [fatal]`.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Ignore success-case TAP YAML diagnostics and normalize volatile console
noise in baseline comparison so red-but-unchanged candidate runs pass as
baseline_only. Align the session-backed fastPut expectation with live-path
uploads, harden SCP abort teardown races, drop mixed hallucinated research
citations instead of failing classify, and soften verification failure
copy so it no longer claims the patch introduced the failure.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* refactor: migrate architecture-debt hooks into application/state
Move SFTP Wave A stores/hooks and related vault/AI/terminal/systemManager
hooks out of components into application/state, update importers, and keep
thin deprecated re-exports at the old paths.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* refactor(sftp): align local bookmarks store API with global bookmarks
Export subscribe/get/set/rehydrate helpers from localSftpBookmarks to match
the globalSftpBookmarks external-store pattern.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(electron): reduce baseline memory via spellcheck/GPU/prewarm guards
Disable spellcheck on BrowserWindows and the default session, drop spare
renderers, gate aggressive GPU switches behind NETCATTY_COMPAT_GPU, and make
settings prewarm opt-in via NETCATTY_PREWARM_SETTINGS.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* Isolate notes and customAccent from TerminalLayer hot props
Route vault notes through notesStore (shellHistoryStore pattern) and
app accent through appearanceChromeStore so note edits and accent-drag
ticks no longer rebuild TerminalLayer. Notes/AI slots and Terminal leaves
subscribe locally; VaultView and open-note callbacks stay unchanged.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ui): cut unnecessary AppView/VaultView/settings re-renders
Stabilize TopTabs close handler, memo AppHostTreeLayer, exclude accent from
settingsVersion, ignore connectionLogs off the logs section, share lastConnected
host refs, and keep rename dialog drafts local so chrome domain stays stable.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* chore: package size, dead code, and consolidation cleanup
Remove unused direct deps, exclude Vite-bundled renderer packages from
electron-builder, delete high-confidence dead code/no-op transfer warm
wiring, consolidate cloud provider IDs and KnownHost/SFTP helpers, and
land UI/AGENTS quick wins (rAF text batching, notes draft debounce,
streaming plain text, tempDirBridge, diagnostics adapter, External MCP
setters).
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: stabilize rAF text batching and diagnostics tests
Polyfill requestAnimationFrame for Node, flush pending text before
steer buffering, bypass rAF while steer is in flight, and stub
global localStorage for AI panel diagnostics adapter tests.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: correct import paths after application/state hook migrations
Migrated vault/AI/system hooks still used components/-relative imports;
point them at repo-root domain/components/types/lib instead.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: address Bugbot/Codex regressions from perf cleanup
- Keep blank Enter from closing RenameDraftDialog; only reset rename state after a non-empty submit
- Flush note drafts before vault mutations so pending title/body edits are not overwritten
- Re-apply appearanceChromeStore accent on appearanceTheme in Terminal, and keep layer themes unaccented
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(notes): flush draft before markdown export paths
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: keep editor-close hotkey ref fresh during render
Assign handleRequestCloseEditorTabRef in render instead of useEffect so
Cmd/Ctrl+W never hits the App stub before the effect runs.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: keep accent in chrome themes without TerminalLayer thrash
Restore accent in globalAppearance for chrome/injection consumers.
Keep resolveFocusedAppearance identity stable across accent drag via
accent ref, while Terminal leaf still re-applies appearanceChromeStore.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: keep compose chrome accent live and merge note renames
Subscribe theme-panel focusedAppearance to appearanceChromeStore so
compose-bar chrome updates during color-picker drag even when
resolveFocusedAppearance identity stays stable. Tree renames now merge
title into the post-flush note instead of replacing from a stale row.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: clear terminal accent when switching back to theme mode
Re-resolve appearanceTheme by catalog id before applyCustomAccent so a
stale baked custom cursor/selection cannot stick after accentMode returns
to theme while TerminalLayer memo ignores accent props.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: register unsaved prompt singleton during render
AppView close handlers call promptUnsavedChanges outside the old
render-prop. Assign the singleton during render so Cmd/Ctrl+W and tab
close never hit the pre-effect null window that auto-cancels.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: keep vault import hooks UI-free and flush notes on teardown
Move ImportOptions into application/state and inject a notifier so
useVaultImportHandlers no longer imports components. Notes drafts also
flush on pagehide/beforeunload when Electron tears down without React
unmount.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: move AI terminal session context builders into domain
buildAITerminalSessionInfo and AIPanelContext now live under domain/ so
application hooks no longer import TerminalLayerSupport and create a
UI → application → UI cycle.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: drop duplicate AIPanelContext type from TerminalLayerSupport
The type is re-exported from domain; the local definition conflicted.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: keep migrated state hooks free of UI-layer imports
Move aiChatStreamingSupport into infrastructure/ai and poll list helpers
into domain/systemManager so useAIChatStreaming / useSystemManager no
longer import from components. Leave thin re-exports at the old paths.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: move remaining migrated-hook deps out of components
Host-key verification types/helpers now live in domain/, and SFTP column
layout types live in application/state/sftp/columnLayout. Application
hooks no longer import those definitions from the UI layer.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: clear lint blockers and flush notes when panels hide
Move AI panel diagnostics persistence into application/state so
components stop importing localStorageAdapter. Flush note drafts when
retained NotesManager mounts become inactive, and silence related lint
warnings in theme/host-key helpers.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(notes): publish notesStore sync on vault note updates
updateNotes/updateNoteGroups now publishNotesSnapshot immediately so
AI/notes consumers do not briefly read a stale catalog after a hide
flush. Flush inactive drafts in useLayoutEffect before paint.
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): instant folder-drop scan UI and drop full-tree pre-scan
Restore interleaved folder transfers as the only non-compressed path, remove
the folder pre-scan setting, and make local folder drops show a scanning task
with live discovered counts via native listLocalTree (with cancel + progress).
UI shows "done · found" instead of a fake grand-total percentage while
discovery is still growing.
* fix(sftp): make folder scan cancellation bridge-safe
* fix(sftp): preserve path-only terminal drops
* fix(terminal): accept native folder drop paths
* fix(sftp): keep scan task cancellable during setup
* fix(sftp): preserve scan failure state during setup errors
* fix(sftp): keep folder scan task across retries
* fix(sftp): cancel scan task immediately
* fix(sftp): keep cancellation active through session setup
* fix(sftp): settle failed drop upload retry
* fix(sftp): preserve cancellation in final scan cleanup
* fix(sftp): harden folder scan cancellation and conflicts
* fix(sftp): finish Codex follow-ups on folder scan cancel UX
- Abort sibling listLocalTree walks when one multi-root scan fails
- Prefer ERR_*_CANCELLED over matching "cancel" in error text
- Clear scanning phase on settle; only label live rows as Scanning
- Omit directory metadata sizes from drop entries used for totals
* fix(sftp): stream folder discovery into concurrent uploads
Local path-backed folder drops now edge-scan and edge-transfer: listLocalTree
streams entry batches while upload workers consume them, so the first files
start before a 100k-file walk finishes. Cancel aborts the walk, clears the
queue, and stops creating child tasks. Compressed uploads still materialize
fully then pack.
* fix(sftp): harden progressive folder UX, retry, and size-based uploads
Align the SFTP panel with transfer-center progress, keep nested tree batches
until tree-end, honor pause during progressive enqueue, add low-cost retry for
external drag-drop files, and drop whole-file upload digests for industry
size-based resume (download live-log prefix checks unchanged).
* fix(sftp): address multi-agent review on progressive upload and retry
Gate progressive drops to path-backed folders only, prefer pool sessions on
drag-drop retry with in-flight and cancel guards, soft-check upload mtime
without fail-open contentVerifiedSeparately, skip multi-root pause HOL,
and fail closed when the local tree stream misses tree-end.
* fix(sftp): finish multi-root pause HOL and parent rollup guards
Race-wait on all latched parents so resuming a non-head folder can proceed,
and only roll progressive parents to completed after terminal failed status.
* fix(sftp): address Codex progressive conflict and hang findings
Add root-level conflict preflight to progressive folder uploads, wake all
backpressure waiters, preserve cancel listeners across uploadEntriesDirect,
and surface empty-directory mkdir failures.
* fix(sftp): optimize growing-download verification
* fix(sftp): harden growing-download prefix verification
Skip unprivileged SSH digest commands for sudo SFTP sessions and reject
empty-input digests so root-only sources fall through to the elevated
SFTP channel. Hash remote prefixes in concurrency-sized windows so
verification keeps a bounded memory footprint instead of retaining the
full downloaded prefix.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): fall back when remote digest command times out
Treat SSH digest open/run timeouts as a miss for the optional
command-path optimization so growing-download verification can still
prove the prefix via SFTP ranges instead of failing a completed transfer.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): propagate digest open timeouts after transport invalidation
SSH_EXEC_OPEN_TIMEOUT invalidates the physical transport in
boundedSshExec, so falling through to SFTP on the same session is unsafe.
Keep run-timeout fallback to SFTP, but fail closed on open timeout.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): mark digest open timeouts as noTransferFallback
SSH_EXEC_OPEN_TIMEOUT already invalidates the transport. Tag it with
noTransferFallback so downloadFile's isolated→shared catch fails closed
instead of retrying verification on the poisoned session.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): fail closed on verify timeouts; range-hash checkpoint resumes
Mark SFTP verification request/stream timeouts with noTransferFallback so
downloadFile does not retry body transfer on a channel that still owns a
timed-out request. Allow checkpoint-complete growth verification to use
bounded prefix ranges under the session fast-download slot, including
sudo paths that skip unprivileged SSH digests.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): drop wedged SFTP channel after verification timeouts
Clear and end the cached SFTP channel when prefix verification hits an
OPEN/READ/stream watchdog or digest exec-open timeout, so later
browse/transfer work cannot reuse a channel that still owns cancelled
requests.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): use window inactivity watchdog for prefix range hashing
Reset a single per-window READ inactivity timer whenever any range
completes, instead of arming independent deadlines that all expire
together on slow or serialized SFTP servers.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): abandon channel on stalled CLOSE; share abort gate
Drop the cached SFTP channel when verification CLOSE times out or fails,
and fan out concurrent prefix READs through one shared AbortSignal gate
so the bounded window does not attach dozens of listeners.
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): parallel folder discovery and unblock transfer stalls
Address #2712 / #2556 leftovers after the pipelined fail-closed work
(#2449 / #2720 / #2761): keep bulk body I/O on concurrent READ/WRITE only,
surface upload digest prep as verifying instead of a silent hang, make the
fastPut path-gate wait cancelable (#2755), parallelize multi-select enqueue,
and bound-parallelize directory listings so folder totals appear faster.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* feat(sftp): folder pre-scan and skip unchanged files
Absorb rsync strengths without an rsync backend: optionally walk the
full tree before transfer for stable file totals, and skip files whose
size and mtime already match the destination.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): drop unused DiscoveredTransferFile import
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: address Codex review on PR #2772
* fix(sftp): address Codex review on path gate, aliases, and worker drain
- Invalidate same-transfer fallback when a force-released path gate gets a late truncating OPEN
- Use per-branch ancestors so sibling symlink aliases copy under pre-scan BFS
- Keep interleaved subdirectory walks sequential for deterministic manifest indices
- Drain transfer/bounded workers before propagating errors; ASCII punctuation
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): canonical pre-scan file order and pre-2001 mtime skip
Sort discovered files with compareDirectoryTraversalPaths before index
assignment, and treat timestamps >= 1e11 as milliseconds so year-2000
local mtimes still match SFTP second precision.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): bound multi-select downloads and preserve dest mtime
Address Codex review on PR #2772: serialize local blob reads, bound
multi-select root downloads before pre-scan, replace remaining Unicode
em dashes, and stamp destination mtimes after successful transfers so
skip-unchanged can match on a later pass.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): keep in-place OPEN poison until callback settles
Address Codex review on e2cc8241: do not force-release path gates for
in-place truncating OPENs, fail closed on pending-OPEN wait timeouts
without clearing poison, and replace remaining Unicode em dashes in
ASCII-only transfer modules.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): fail closed when in-place OPEN poison is held
When an isolated in-place truncating OPEN leaves the path gate poisoned
after channel error/cancel, mark the transfer terminal (noTransferFallback)
so fastPut/concurrent-shared cannot wait forever on the unreleased prior
gate.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): omit cyclic dirs from pre-scan plan and lower mtime cutoff
Only record discovered directories after a visit is accepted so symlink
cycles are not mkdir'd as empty targets, and treat magnitudes >= 1e10 as
milliseconds so early-1970s local Date values still match SFTP seconds.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): count max-depth symlink gaps and skip symlink quick-check
Pre-scan now surfaces max-depth symlink directory omissions as transfer
errors (matching interleaved walk), and discovered file symlinks carry
isSymlink so skip-unchanged does not trust link-node size/mtime.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): unit-aware skip mtimes and interleaved symlink bypass
Declare mtimeUnit on skip identities so early-1970 millisecond values
normalize without magnitude cutoffs, and bypass skip-unchanged for
symlinks on the interleaved (non-prescan) directory walk as well.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): bound local pre-scan and preserve SCP destination mtimes
Apply the same directory/entry traversal budget to local folder pre-scan,
and best-effort touch SCP upload targets after commit so skip-unchanged
can match on a later pass.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): address Codex review on pause, skip restat, mtime bound
- Honor transfer pause during folder pre-scan BFS waves/listings
- Re-stat source immediately before skip-unchanged on pre-scanned files
- Bound best-effort destination setStat/utimes so sendComplete cannot hang
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): harden skip restat and pre-transfer mtime capture
- Require a fresh source stat before skip-unchanged (prescan + interleaved)
- Capture source soft identity before bytes move for non-resumable/SCP
- Drop post-commit mtime re-stat fallback; keep ASCII in touched comments
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): propagate fresh re-stat size into interleaved transfers
Use the re-statted source size/mtime for child tasks on the non-prescan
path so grown files are not truncated to the stale listing size.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): omit stale listing size after re-stat failure
When source re-stat fails, clear child totalBytes/mtime so
startStreamTransfer omits an explicit snapshot and the bridge
re-stats on the dedicated session instead of truncating to the
stale listing size.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): make soft-identity capture cancelable
Race pre-transfer source soft-identity capture against cancel and
pass the transfer abort signal into requireSftpChannel so a dead
SFTP reopen cannot block cancel for the channel timeout.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com>
* fix(sftp): make fastPut pending OPEN gate wait cancelable
An isolated concurrent OPEN that dies without a callback leaves
pendingWriteOpenPathGate unresolved by design. Unconditionally awaiting
it before fastPut pinned the transfer after abort was cleared; race the
wait with cancel and bound it fail-closed so Cancel/lease cleanup can
settle without racing a still-pending truncate (#2755).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): end fastPut channel on gate wait abort
Clear the gate-wait timeout on cancel so abandoned rejects cannot fire,
and end the reopened isolated channel when the wait fails so Cancel /
fail-closed timeout cannot leak an SSH subsystem.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): poison path gate after fastPut OPEN wait timeout
When a dead write OPEN leaves truncatingSharedWriteOpenGates unresolved,
fail-closed fastPut timeout now marks that entry failed so later same-path
uploads reject promptly instead of hanging on waitForPrior. Keep the
barrier until a late OPEN release clears it; waitForPrior rejection no
longer starts OPEN.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): keep path-gate poison across successor waiters
Reinstall the poisoned truncatingSharedWriteOpenGates entry on fail() so a
same-path waiter that replaced the map slot cannot release the barrier for a
third OPEN. Propagate fail (not resolve) when abandonGateWait sees a poisoned
prior, and poison on cancel during the fastPut gate wait as well as timeout.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): let OPEN owner clear poison after successor fail
Successor waiters that propagate a poisoned prior must not reinstall
themselves into truncatingSharedWriteOpenGates. Otherwise the OPEN owner's
late release cannot delete the barrier and same-path uploads stay
fail-closed forever after the dangerous OPEN has settled.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): clear path-gate poison after transport end
Arm a 2s post-close clear when the owning isolated SFTP channel ends so a
host-keyed poison from a never-callback OPEN does not fail-closed every
later same-path upload until process restart. Keep the grace so a late
OPEN racing end() still has a barrier.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): arm path-gate poison clear without per-OPEN listeners
Install the post-poison transport-gone clear from fail() instead of
attaching end/close listeners on every write OPEN. Shared browse channels
never emit those events per transfer, so the old approach leaked listeners
and could hit MaxListenersExceededWarning.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): only OPEN owners arm path-gate poison cleanup
Track openIssued / priorEntry on truncatingSharedWriteOpenGates so a
waiter that reaches fastPut timeout poisons the OPEN-owning ancestor
instead of reinstalling itself and clearing the barrier while the prior
transport may still deliver a late truncating OPEN.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): do not arm ancestor poison cleanup from waiter fail
When a waiting upload times out and fail() walks to the OPEN-owning
ancestor, keep that ancestor's barrier until its own OPEN settles. Only
the OPEN owner entry arms releaseAfterTransportGone after its transport
is already gone.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* feat(terminal): add disconnect/reconnect controls on session status bar
Workspace panes could not reach tab-level reconnect. Expose disconnect
(without closing the pane) and reconnect beside the host info actions so
split sessions stay operable. Closes#2769.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): abort in-flight boot on status-bar disconnect
Disconnect keeps the pane mounted, so cancel's unmount cleanup never runs.
Flip isBootActiveRef (and isCancelling) so a late startSSH/startMosh attach
cannot resurrect the session after the user disconnects mid-connect.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): ignore stale start UI after status-bar disconnect
When disconnect aborts an in-flight connect, starter catch paths must not
reopen auth or repopulate errors. Gate SSH/telnet/mosh/ET/local/serial
failure UI on isTerminalBootActive and harden the boot effect awaits.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): hide disconnect/reconnect on compact popup terminals
Owned command popups use compactToolbar with a one-shot startup command.
Exposing reconnect there can re-run that command; keep the controls on
normal tabs/workspace panes only.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): use boot epoch so reconnect cannot revive aborted starts
Disconnect alone flipping isBootActiveRef is not enough: an immediate
reconnect re-arms boot, and a late startSSH from the aborted attempt can
still attach. Capture a boot epoch per attempt and ignore stale attaches/UI.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): do not let stale attach abort a newer reconnect
tryAttach failure for an expired boot epoch must close the orphan session
only. Calling abortSessionStartAfterUnmount() would force disconnected on
a reconnect that already re-armed boot.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): reject late host-key prompts after disconnect
Disconnect can land before pendingHostKeyRequestId is set. Auto-deny
onHostKeyVerification when boot is inactive or status is disconnected
so approval UI cannot reopen on an aborted pane.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): cancel wake and MFA when status-bar disconnects
Invalidate hibernation reconnect-wake tokens on disconnect so a late wake
cannot re-arm boot. Reject/cancel keyboard-interactive prompts for
disconnected panes and clear any already-queued MFA for that session.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): ignore stale SSH catch UI and password fallback
Check boot attempt currency before resetting reconnect UI state on
SSH start failure, and before launching key→password fallback after
disconnect so a replacement reconnect is not interrupted.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): guard stale SSH progress and host-key prompts
Ignore chain-progress callbacks from superseded boot attempts, and
thread bootEpoch through SSH host-key verification so a late prompt
from an aborted start cannot overwrite a replacement reconnect.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): correlate MFA and jump host-keys with boot epoch
Forward bootEpoch through jump-host verification and keyboard-interactive
prompts, and reject superseded MFA when the retained tab has already
started a replacement reconnect.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): keep replacement SSH session alive across stale starts
Skip closeSession for superseded starts while a newer boot still owns
the shared sessionId, and avoid clearing MFA wait state before the
attempt-freshness guard on SSH success.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): own shared sessionIds by boot epoch
Claim SSH/local registry slots with bootEpoch so a stale start cannot
overwrite a replacement, and make closeSession no-op when the payload
epoch does not match the live session owner.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): claim bootEpoch for all reconnectable backends
Pass bootEpoch through Telnet/Mosh/ET/serial starts, claim registry slots
the same way as SSH/local, and dispose displaced owners when a newer
boot replaces the shared sessionId.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): protect preload listeners and ET artifacts on displace
Defer epoch-scoped closeSession listener teardown until main confirms
ownership, and remove ET external-auth temp artifacts when a newer boot
displaces the previous registry owner.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): clean rejected Mosh starts and keep replacement listeners
Kill superseded Mosh/ET PTYs and auth temp files when claimSessionSlot
rejects, and never clear shared preload listeners on epoch-scoped closes.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): settle superseded boots and stop displaced log streams
Reject Telnet/SSH claim failures through Promise reject/failReuse instead of
uncaught EventEmitter/.then throws, and stop a displaced session's log stream
so a logging-disabled reconnect cannot append into the old file.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): keep woken runtime when disconnect cancels hibernate wake
Disconnect invalidates in-flight hibernated reconnects with keep mode so a
successful wake that already cleared hibernatedRef is not disposed into a
dead pane that can never reconnect. Unmount still disposes orphan runtimes.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): bind reused SSH shellPid only to the claimed session
Late PID discovery after Copy Tab reuse must not write onto a newer
bootEpoch that already replaced the shared sessionId registry slot.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): await plugin cancel cleanup and clear boot epochs on teardown
Track plugin start/cancel promises so Disconnect→Reconnect waits until the
external session finishes unregistering, and stop unmount teardown from
republishing boot epochs into the process-wide map.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): cancel SSH passphrase prompts on disconnect
Register a pending-boot AbortSignal for SSH starts, abort it (and session-
scoped passphrase requests) from closeSession even before a registry slot
exists, and filter/drop late passphrase modals by boot epoch / disconnect.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): close owned boot epoch and scope passphrase cancels
Capture the pre-bump epoch for disconnect/teardown closeSession so live
transports are not skipped by epoch mismatch, cancel only passphrase prompts
at or below that epoch, and revalidate boot currency after async passphrase
lookups before queuing a modal.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): tag Telnet auto-login and Mosh ready with bootEpoch
Emit bootEpoch on telnet auto-login and mosh:ready events so overlapping
Disconnect→Reconnect boots ignore cross-delivered callbacks meant for an
older shared sessionId.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): update disconnect assertion and cancel pending serial opens
Align the disconnect source regression with invalidateBootEpochForClose, and
abort in-flight serialPort.open attempts via pending boot AbortSignal so
Disconnect→Reconnect cannot leave a stale open racing the replacement.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): keep flow-pause leases on skipped epoch closes
Worker close:await previously cleared pause ownership in finally even when
the worker returned skipped for an epoch mismatch, dropping the live
replacement's lease. Only clear on non-skipped closes; epoch-scoped
fire-and-forget close similarly defers to onSessionClosed.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
When the terminal worker is disabled, startSSHSessionWrapper probed LAN
access but dropped the resolved address before startSSHSessionWithRetries.
Share attachMacLocalNetworkProbeResult with the worker path so split-DNS
FQDN first hops still get Local Network error guidance.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): harden macOS Local Network TCC trigger for LAN SSH
Replace the main-process TCP probe with Apple's TN3179 UDP discard-port
connect, resolve vault hostnames/.local before probing, hold the socket
briefly so TCC can present the alert, and annotate EHOSTUNREACH errors
that embed LAN IPs even when the host was stored as a DNS name.
Addresses recurring #2673 / #2663 / #1040 Local Network silent failures.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): address Codex Local Network review findings
Clear the UDP connect safety timer once connected so a slow .local
resolve cannot truncate the TCC hold. Mark worker-forwarded start
payloads after the main-process probe so utilityProcess skips a second
hold. Keep the new test comments ASCII-only.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): bound Local Network DNS lookup by probe timeout
Codex noted that hostname resolution before the UDP probe had no
deadline, so a hung DNS lookup could stall every non-literal SSH start.
Race the lookup against the existing probe timeout and skip probing when
it expires so the normal SSH dial can proceed.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): tighten Local Network error address parsing
Ignore LAN bind addresses in Node's "- Local (ip:port)" suffix so public
SSH failures are not mislabeled as Local Network denials. Also extract
embedded IPv6 ULA/link-local remotes from unreachable errors.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): only hint Local Network when the first hop is LAN
A .local final host reached through a public proxy or jump must not force
the macOS Local Network hint when the failing dial never touched LAN.
Annotate from the first hop and remote error address instead.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): probe jump-level proxy as the first Local Network hop
Match sshBridge first-hop wiring: a usable jump.proxy overrides the
session proxy and the jump hostname. Otherwise a .local jump behind a
public proxy probes the wrong host, and a LAN proxy in front of a public
jump is missed.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): retry .local Local Network probes over udp6
IPv6-only mDNS hosts never resolve on a udp4 socket, so the TCC trigger
could miss them. For unresolved hostnames try udp4 then udp6.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): share Local Network probe timeout across UDP families
udp4 then udp6 fallbacks for .local hosts now inherit one overall
deadline instead of restarting DEFAULT_PROBE_TIMEOUT_MS per family.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* chore: re-request Codex review after unanswered pass
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): skip Local Network probe for ProxyCommand hops
Treat an active jump/session ProxyCommand as a terminal first hop so we
do not fall through to a LAN hostname and spuriously prompt. Also share
one wall-clock deadline across DNS resolution and UDP family attempts.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* test(ssh): tolerate residual Local Network probe budgets in hold test
Shared-deadline math can arm the safety timer at 499ms instead of exactly
500ms. Identify that timer by being larger than the hold, not by equality.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(macos): preserve ProxyCommand skip during Local Network error annotation
Keep ProxyCommand's skipProbe sentinel through annotateMacLocalNetworkErrorMessage
so .local hop names do not get a false Local Network hint when TCP probing was
intentionally skipped. Split residual UDP budget evenly across remaining address
families so dual-stack and .local IPv6 attempts still get time.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* test(ssh): fire residual Local Network safety timers in hang probe test
Shared-deadline math arms the UDP safety timer with a residual budget
(e.g. 2999ms) rather than the exact DEFAULT_PROBE_TIMEOUT_MS constant,
so matching only the constant left the hang-socket test unable to settle.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(macos): never hint Local Network for ProxyCommand errors
skipProbe means an external ProxyCommand owns the dial. Do not append the
Netcatty Local Network TCC hint from child-process LAN addresses in the
error text — that guidance would send users to enable Netcatty for a
connection Electron never made.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(macos): ignore downstream LAN addresses behind public first hops
When a public proxy/jump owns the first dial, unreachable addresses embedded
in the error often name a private host the remote side tried to reach. Only
use those addresses as Local Network evidence when they identify the local
first-hop dial.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(macos): keep LAN hints for private DNS first hops and ASCII comments
Treat common private DNS suffixes (e.g. bastion.lan) as Local Network
evidence so hostname-based first hops still get guidance when Node reports
the resolved IP. Replace em dashes and arrows in this module with ASCII.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(macos): keep Local Network hints for unqualified first hops
Single-label jump/proxy names such as "dev-viet" resolve to LAN IPs that
appear in Node unreachable errors. Treat those hostnames as local-side
evidence so annotation still guides users to macOS Local Network settings.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(macos): carry resolved first-hop LAN addresses into error annotation
ensureAccess remembers the LAN IP it probed for a hostname first hop and
forwards it on the worker start payload. Annotation uses that resolved
address so private split-DNS FQDNs like bastion.example.com still get
Local Network guidance without treating public proxies as LAN.
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): restore hibernate scrollback before viewport
Hibernate wake was writing the viewport first and appending older
scrollback on idle. Under a finite xterm scrollback cap that trims the
just-restored newest rows, so switching back to a hibernated tab could
permanently drop the end of large output (#2762). Replay scrollback →
viewport → pending on the wake path while keeping chunked writes.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): prefer full hibernate snapshot on wake
SerializeAddon range slices omit the seam newline, so concatenating
scrollback+viewport can merge boundary lines. Prefer the coherent full
snapshot for wake history, with a scrollback-then-viewport fallback, and
cover the seam with a real SerializeAddon regression.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* style(terminal): keep hibernate wake comments ASCII-only
Address Codex P3: replace em dash / arrow punctuation in touched
comments per AGENTS.md ASCII convention.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): drain hibernate pending via take-and-clear on wake
Full-history replay widens the window where capped pending output can
grow without increasing length. Length-based slice drains then skip new
bytes. Capture and clear pending before replay, then drain pure deltas.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): final pending drain before hibernate listener teardown
After the bounded take-and-clear loop, take any remaining pending once
more before stopHibernateListeners so wake-time output is not dropped.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): stop hibernate listeners before final pending drain
Awaiting the final pending write while listeners are still live can
lose already-acked bytes that arrive during that write. Stop listeners
first, then take-and-replay any remainder.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): keep hibernate exit listener through final pending replay
Stop only the data listener before the final pending drain so pending
stops growing, but leave the exit listener registered until after the
reattach decision so an exit during that await is not missed.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): pause backend flow across hibernate history replay
Full-history wake no longer races the 512 KiB pending cap or the 64 KiB
preload backlog. Pause-and-wait drains into pending, stop the data
listener, take pending once, replay, reattach, then resume. Removes the
multi-pass drain that still dropped ACKed bytes under sustained output.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): harden hibernate wake against drain failure and exit tails
Check setSessionFlowPausedAndWait success. On drain failure keep the
hibernate data listener with the pending cap disabled for the wake window.
After history replay, take pending once more so exit "[session closed]"
tails and uncapped live arrivals are not discarded.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): pause reconnect hibernate wakes and recheck exit after late drain
Always pause when a backend session exists (including sessionConnected=false
reconnect wakes). Drain late pending plus an exit-tail take before deciding
reattach. Resume flow only after reattach, or on wake failure; successful
reconnect wakes stay paused until cleanupSession.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): restore hibernate listeners on failed wake
On wake failure, dispose the partial xterm runtime and reinstall hibernate
listeners (preserving pending) instead of unpausing into a listener-less
backlog gap. Successful reconnect wakes still keep the pause until
cleanupSession. Add a final pending take before exit-listener teardown.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): restore take-and-cleared pending on failed hibernate wake
Track every pending take during wake and pass it to restoreAfterFailedWake
so a thrown replay/reattach cannot permanently drop already-buffered output
with the disposed partial xterm. Also take once more before exit-listener
teardown.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): drain hibernate pending until empty before teardown
After history replay, loop take-and-append until an empty take so an exit
or residual chunk that arrives during an awaited replay cannot be dropped
by stopHibernateListeners. Data listener is already stopped, so the loop
stays short in practice.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Local terminal paste with an image-only clipboard now forwards raw Ctrl+V into the PTY so nested TUIs (e.g. Claude Code) can read the OS clipboard. Text and file-path paste, and remote sessions, are unchanged.
Fixes#2758
* fix(credentials): stop double-encrypting undecryptable safeStorage blobs
When local decrypt failed (e.g. OSCrypt key churn after reboot), encrypt
used to wrap the leftover enc:v1 ciphertext again, permanently poisoning
the vault. Startup sync could also push those placeholders to cloud and
make download restore the same poison.
- Keep real enc:v1 blobs unchanged on encrypt (header check, no wrap)
- Strip device-bound placeholders when applying portable sync payloads
- Guard startup local-wins / merge round-trips before upload
- Skip vault init re-encrypt writes when secrets are still undecrypted
Fixes#2702
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sync): strip enc:v1 secrets from smart-merge uploads
Bugbot found that after local apply sanitized placeholders, legacy v1
smart-merge could still decrypt an unstripped remote, merge it, and
re-upload the poison. Strip device-bound credentials on remote decrypt
and again on the merged payload before upload.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(credentials): address Codex P2 review on #2702
- Always re-encrypt vault init batches so plaintext siblings are not
left unprotected when one record is a stale enc:v1 placeholder
- Sanitize device-bound secrets before convergent restore prepare so
CRDT replica commit matches vault import
- Require a complete safeStorage blob (header + min 31 bytes) before
treating enc:v1 as ciphertext; encrypt header-only coincidences
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(credentials): address Codex P1 review on #2702
- Decode enc:v1 payloads with atob in the renderer-safe domain helper
- Heal poisoned remote secrets from local/base before smart-merge so
good credentials are not discarded as remote-only deletions
- Keep post-merge strip so leftover enc:v1 never uploads
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(credentials): address Codex P1 CBC min and startup heal
- Accept 19-byte v10/v11 CBC OSCrypt blobs (not only 31-byte GCM)
- Heal poisoned remote secrets before startup smart-merge
- Strip unresolved placeholders on merge round-trip upload
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sync): sanitize payloads at every apply/commit boundary
Codex P1: vault apply stripped enc:v1 while commitRemoteInspection and
convergent materialization could keep the poison. Sanitize merged/remote
payloads before apply+base commit, strip on CRDT materialization, and
sanitize convergent apply inputs.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sync): heal local and remote secrets before smart-merge
Smart-merge could select a locally-changed entity whose only secret
delta was enc:v1 poison, then strip and upload empty secrets. Heal
both sides from the opposite payload/base before merge on all sync
paths.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sync): keep enc:v1 intact during convergent materialize
Stripping device-bound secrets inside materializeSyncPayloadFromConvergentState
broke envelope validation for poisoned-but-consistent v2 snapshots, so decrypt
could not hydrate the clouds #2702 needs to recover. Portable stripping stays
at apply/upload boundaries.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(credentials): detect Windows DPAPI by decoded header bytes
Real DPAPI blobs start with 01 00 00 00 d0 8c... and base64-encode as
AQAAANCM..., so the previous AQAAAA string prefix rejected them and
allowed double-wrapping after key rotation. Match decoded headers in
both the main-process bridge and renderer predicate.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* test(credentials): use complete enc:v1 fixtures for stricter detector
Short placeholders like enc:v1:djEwAAAA no longer pass the platform
header + minimum-size checks. Update auth/SFTP/proxy/sync fixtures to
full v10-shaped blobs so npm test matches production validation.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sync): treat preferred credential deletions as authoritative
When healing enc:v1 before merge, an empty/missing secret on a present
preferred entity is an intentional clear and must not be revived from
base. Only fall back to base when preferred is absent or also poisoned.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(credentials): reject impossible v10/v11 ciphertext lengths
Accept only CBC-aligned sizes (19+16n) or GCM-sized blobs (>=31) so
coincidental enc:v1 plaintext of intermediate length is encrypted instead
of treated as an undecryptable placeholder.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(credentials): require full DPAPI provider GUID signature
Accept Windows safeStorage blobs only when they start with version
01 00 00 00 plus provider GUID df9d8cd0-1501-11d1-8c7a-00c04fc297eb,
so coincidental 01 00 00 00 prefixes are not treated as ciphertext.
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(windows): restore frameless rounded corners and tray transparency (#2505)
Windows frameless windows used a solid host backgroundColor under DWM
rounding, which showed as a dark rim, while the tray popup kept the
default opaque backdrop under CSS border-radius (square tips / 冒尖).
Use a clear transparent host with native roundedCorners for app windows,
and disable OS rounding for the CSS-shaped tray overlay. Keep html/body
transparent on the tray route so corner pixels stay see-through.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(windows): require chrome helpers outside with(ctx)
createMainWindowApi injects require from windowManager.cjs, so a relative
require inside with(ctx) resolved to electron/bridges/windowsWindowChrome.cjs
and crashed every main-window create (MODULE_NOT_FOUND). Load the helpers at
module scope in each factory so paths stay file-local.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(windows): keep resizable app windows opaque
Codex P2: Electron documents transparent BrowserWindows as not resizable,
and enabling resizable can break them. App content windows now only set
roundedCorners: true with a solid theme backdrop; the tray overlay stays
transparent because it is already non-resizable.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(tray): hide opaque splash on tray overlay route
Codex P2: the shared splash screen still painted an opaque full-window
fill when the tray panel showed before React mounted, recreating the
square-corner flash. Mark tray-window early, CSS-hide the splash, and
remove the splash node on DOM ready / tray render.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(tray): defer first show until ready; keep chrome comments ASCII
Wait to show the tray BrowserWindow until did-finish-load so the shared
opaque splash cannot flash as a square underlay. Also rewrite the new
helper file header in ASCII-only English per AGENTS.md.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(tray): cancel pending first-load show on second toggle
If the user toggles the tray again while the first renderer load is still
pending, clear trayPanelShowWhenReady so did-finish-load cannot open the
panel after an intentional dismiss.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(tray): keep shared splash transparent by default
Move the opaque splash fill behind html:not(.tray-window) so the tray
overlay route never inherits a solid full-window background from the
shared splash markup.
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(system): add GPU/NPU monitor tab when accelerators are detected
Probe nvidia-smi and npu-smi during System capabilities checks and show a
GPU sub-tab with utilization, memory/HBM, temperature, power, and compute
processes. Reuses the existing SSH exec + parse pattern (same approach as
open-source remote GPU monitors) without new dependencies.
Related to #2561
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system): address GPU monitor Bugbot findings
- Use PowerShell nvidia-smi collector for local Windows sessions
- Keep GPU tab in loading state while listAccelerators is pending
- Render compute processes even when device list is empty
- Stop treating Ascend HBM usage rate (%) as megabytes
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system): address Codex GPU collector review findings
- Wrap POSIX accelerator script with JSON.stringify so nested sed
quotes cannot break outer sh -c
- Parse Ascend proc-mem whitespace-delimited table rows
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system): show unknown GPU util/memory as -- not 0%
ResourceBar now treats null/undefined as unavailable, and the GPU
device cards pass through null metrics instead of coalescing to 0.
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(sync): rehydrate follow-app theme and guard partial groupConfig wipes
Second devices were keeping the local follow-app terminal theme flag after
cloud sync wrote peer appearance settings, which flickered black vs yellow.
Also block uploading an empty groupConfigs list while other vault data
remains so hydration races cannot clear startup-command group defaults.
Fixes#2757
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sync): narrow complete-wipe shrink guard to groupConfigs
Only treat empty groupConfigs with other vault data present as suspicious.
Wiping the last snippet/note/etc. while hosts remain stays a normal delete.
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): remove serial createReadStream bulk download path
Align downloads with upload fail-closed (#2449/#2719): use concurrent
isolated/shared READ fanout only, wait for the per-session fast slot,
and never fall back to RTT-bound serial createReadStream. Keep hash
helpers on createReadStream for verification only.
* fix(sftp): harden download slot cancel and fail-closed tests
Address multi-agent review: make session fast-slot wait cancel idempotent
with abort restore, recheck cancel after acquire, and lock fail-closed /
slot-wait cancel with condition-based waiter-depth assertions.
Hard-coded win.target arches made pack:win-x64 also emit win-arm64 and
a universal NSIS package that can leave Netcatty.exe missing on ARM
Windows (#2570). Let CLI --x64/--arm64 control packaging instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): keep cancel shared write OPEN drain gated on callback
Cancel settle must not force-complete sharedWriteOpenDrain — only channel
error does — so stage cleanup cannot race a late truncating OPEN after #2720.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): bound cancel OPEN drain and unlink late truncates
Restore cancel drain force-complete so stalled shared OPENs cannot hang
the lease, and best-effort unlink late truncating "w" opens after close so
force-complete cannot leave a recreated stage orphan.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): only unlink generated stages on cancel OPEN
Gate cancel-time truncating OPEN unlink on generatedStagePath so in-place
and symlink destinations keep their final path; cover with a late-OPEN test.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): unlink late staged OPEN after channel-error settle
Late truncating generated-stage OPENs after settle must unlink even when
transfer.cancelled is false, so channel-error force-complete cannot leave
a recreated .netcatty-upload orphan.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): skip late OPEN unlink when same-id retry owns stage
After shared write OPEN drain force-complete, a late truncating "w"
callback still closed and unlinked generated stages. Resumable retries
reuse `.netcatty-<transferId>.part`, so a stale callback could delete
the new attempt's stage. Gate unlink on activeTransfers ownership
(generated stages + truncating only), matching the in-place final
target guard.
* fix(sftp): only skip late OPEN unlink for reusable resume stages
Same-id retry ownership must not suppress unlink for every generated
stage path. Non-resumable uploads use unique .netcatty-upload-* paths;
a stale late OPEN on those must still clean orphans. Only deterministic
.netcatty-<transferId>.part resume stages are protected.
* fix(sftp): gate late OPEN unlink skip on actual staged path
Only suppress late generated-stage unlink when the same-id retry's
active.stagedRemote.path matches. Do not treat resumable+targetPath or
path shape alone as ownership for in-place retries.
* fix(sftp): compare late OPEN stage paths in the same representation
OPEN filePath may be a session-encoded Buffer while stagedRemote.path is the
logical string, so strict equality failed and could unlink a same-id retry
stage. Normalize via encodePathForSession / Buffer equals before ownership skip.
* fix(sftp): gate truncating shared OPEN against same-path retries
Serialize truncating shared-write OPENs per remote path so a same-id
retry cannot issue OPEN "w" while a prior attempt is still unsettled.
When a drain force-complete already released the gate and a late stale
OPEN re-truncates a retry-owned resume stage, invalidate that retry
instead of allowing a sparse promote. Keep late-OPEN unlink skip for
retry-owned stages.
* fix(sftp): bound best-effort unlink so cancel cannot hang
Shared/sudo cancel after OPEN "w" waited on unlink before finishCancel.
A dead channel that never invokes the unlink callback left the transfer
and its session lease active indefinitely. Cap unlink at the same 2s
grace used for shared-write drain force-complete.
* fix(sftp): make truncating shared OPEN path-gate waits cancelable
A same-path upload queued behind a prior truncating OPEN could keep its
lease/admission slot forever if cancelled before runOpen. Race the gate
wait with transfer.abort and release the gate + drain on cancel.
* fix(sftp): scope truncating OPEN path gates per SFTP session
A module-global path-only key serialized unrelated hosts that shared the
same remote pathname. Include transfer/sftp session identity in the gate
key so only same-endpoint opens wait on each other.
* fix(sftp): refuse stage promote after stale OPEN truncation
A late force-completed OPEN w from a prior same-id attempt can zero the
deterministic stage after concurrent write finishes. Latch
staleOpenTruncatedStage through assertCanPromote and uploadFileConcurrent
so promotion cannot rename a corrupt stage.
* fix(sftp): keep path gate chained when canceling a queued OPEN wait
Releasing a cancelled waiter's gate immediately cleared the barrier while
the prior truncating OPEN was still in flight. Chain the waiter's release
to waitForPrior so later same-path attempts stay serialized.
* fix(sftp): hold reusable-stage path gate past OPEN force-complete
Force-completing drain no longer frees the path gate for generated stages
immediately. Same-id retries stay serialized until the late OPEN settles
(or a 10s hard cap) so promotion cannot race a still-processing OPEN w.
* test(sftp): wait for generated-stage path gate hold on retry OPEN
After force-complete, generated stages keep the path gate for 10s more.
Align the late-OPEN retry test wait with that hold.
* fix(sftp): hold truncating OPEN path gates for in-place targets too
In-place shared OPEN force-complete no longer frees the path gate
immediately, and late OPEN invalidation applies to final-path retries so
a stale truncating OPEN cannot wipe an in-flight same-id rewrite.
* fix(sftp): keep path gate held until OPEN settles (no hard cap)
Remove the post-force-complete gate timeout so a different transferId
cannot reopen the same target while a stale truncating OPEN is pending.
* test(sftp): accept normal {transferId,totalBytes} retry success
startTransferNow returns transferId/totalBytes on success, not success:true.
* fix(sftp): reject path-gate waits when the SFTP channel errors
Queued same-path OPEN waiters now observe channel error while waiting on
the prior gate, so a dead session cannot pin lease/admission forever.
* fix(sftp): treat superseded OPEN as cancelled, not failed
Late OPEN rejections after a same-id retry must not broadcast a terminal
failed event that marks the live retry task failed in transfer center.
* fix(sftp): emit cancelled channel for superseded OPEN failures
Worker fan-out only maps transfer:error as cancelled for cancel messages.
Superseded attempts now use transfer:cancelled so retries are not failed.
* fix(sftp): silence superseded OPEN terminal events for live retries
When a same-id retry already owns activeTransfers, do not emit cancelled
or failed for the stale superseded attempt or the transfer center sticks.
* fix(sftp): release path gate on OPEN force-complete for dead channels
When the OPEN callback never arrives, free same-path waiters after drain
force-complete so later uploads do not hang forever on the gate.
* fix(sftp): hold path gate until OPEN settles; 30s dead-channel grace
Force-complete no longer frees the gate immediately. Late OPEN release
keeps retries serialized; a 30s grace frees dead channels.
* fix(sftp): never force-release path gate on OPEN drain force-complete
A dead-channel force-complete must free the transfer/lease, but keep the
path gate held until the OPEN callback actually settles. Force-releasing
lets a same-path retry write then get wiped by a late truncating OPEN.
* fix(sftp): skip upload fallback when OPEN is superseded by a retry
A same-id retry can already own the transfer while the old OPEN callback
still rejects. Mark that rejection noTransferFallback so we never truncate
the shared stage via prepareUploadFallbackCheckpoint under the retry.
* fix(sftp): bound late OPEN close before releasing path gate
After drain force-complete, a late truncating OPEN must still close the
handle before releasing the path gate — but a dead channel may never
invoke CLOSE. Race close with a 2s timeout so same-path uploads cannot
wait forever on pathGate.waitForPrior.
* chore: re-trigger Codex review automation
* fix(sftp): always suppress terminal events for superseded OPEN attempts
Superseded means this attempt lost ownership. Do not require the retry to
still own activeTransfers — if the retry already completed, emitting
cancelled for the same transferId rewrites a completed transfer.
* chore: re-trigger Codex for path-gate head
* chore: nudge Codex automation after cancelled run
* fix(sftp): keep SFTP leases when suppressing superseded attempts
Superseded OPEN cleanup must not release transferId leases — a same-id
retry shares that lease set entry and would lose its hold if we release.
* fix(sftp): do not return result.error for superseded transfer attempts
Renderer directory ops treat any startStreamTransfer error as failure.
Superseded same-id OPEN attempts should return a non-error marker so the
live retry is not marked failed.
* fix(sftp): bound CLOSE on pre-settle cancel OPEN path
Reuse the 2s close timeout for cancel-before-settle so a dead channel
cannot leave the path gate held after finishCancel never runs.
* fix(sftp): hold path gate until unlink callback settles
Remove the 2s force-complete on late shared OPEN unlink so a delayed
server unlink cannot race a same-id retry that already reopened the stage.
* fix(sftp): path-gate r+ resume OPENs; release only unused superseded leases
Serialize all shared write opens including resume r+. On superseded, release
only SFTP leases not also held by the live same-id retry.
* fix(sftp): serialize shared and isolated OPENs by host+path
Path gates keyed only by SFTP object let an isolated-channel retry race a
stale shared truncating OPEN on the same host path. Prefer targetHostId so
both channels wait on the same gate.
* fix(sftp): path-gate isolated writes; revalidate checkpoint after gate
Gate all write OPENs including isolated channels so a shared stale OPEN "w"
cannot race. After waiting on the path gate, re-stat the stage and shrink
the resume checkpoint (and flags) before issuing r+/w.
* fix(sftp): dynamic truncating flags; cross-id stale OPEN; bound unlink
Resolve r+/w at OPEN/unlink time so afterPathGate flag getters still unlink
generated stages on cancel. Invalidate any same-path active transfer (not only
same-id) when a late truncating OPEN lands, and force checkpoint restart when
the stage was wiped before OPEN accepted. Bound best-effort unlink so a dead
channel cannot hang cancel settle.
* fix(sftp): clear staleOpenTruncatedStage after gated full restart
When a late truncating OPEN zeros the stage while a retry waits on the path
gate, afterPathGate restarts from offset zero. Consume the stale flag there so
a successful full rewrite is not failed by the post-upload guard.
* fix(sftp): keep cancel active during post-gate checkpoint revalidation
After the path gate opens, afterPathGate may stat a remote stage. Keep the
transfer abort hook and channel-error listener live for that await so cancel
or a dead channel can still release the path gate and lease.
* fix(sftp): hold path gate for isolated write OPENs until settle
Isolated truncating OPENs previously released the host/path gate on cancel
as soon as sftp.end() was called, allowing a same-path retry to race a still
in-flight OPEN that can truncate the stage without invalidation. Keep the gate
until the OPEN callback (or a bounded force-release after end), and run late
OPEN invalidation for isolated channels too.
* fix(sftp): close late shared read OPEN handles after settle
Restores close of late shared/sudo OPEN "r" handles after cancel or channel
error settle. Write-only gate invalidation remains for isolated late OPENs.
* fix(sftp): copy host IDs onto transfer for path-gate keys
dedicatedTransferResume passes targetHostId with a new targetSftpId. Keep
those host IDs on the transfer object so sharedWriteOpenSessionKey stays
host-scoped across session replacement instead of falling back to sftpId.
* fix(sftp): do not complete directory children on superseded invoke
When a same-id retry steals ownership mid-OPEN, the old startStreamTransfer
returns { superseded: true }. Wait for the live owner's terminal task status
instead of treating that result as a successful child completion.
* fix(sftp): pass host IDs on background restart; scope stale-OPEN invalidation
Background agent resume now includes source/target host IDs so path gates stay
host-scoped after openSftpForSession. Stale OPEN invalidation requires a
matching host or sftp session instead of treating missing host as a wildcard.
* fix(sftp): do not unlink stage after same-transfer fastPut fallback
When concurrent isolated OPEN dies without a callback and upload falls back
to fastPut on the same generated stage, a late OPEN must not unlink that
stage. Skip unlink when the same transfer already accepted an OPEN, owns
stagedRemote, or moved to a fastPut/stream strategy.
* fix(sftp): narrow fastPut unlink skip; handle superseded in all callers
Only suppress late-stage unlink when the same transfer moved to
fastPut/stream — ordinary cancel still unlinks. Background restart and
dedicated resume wait for the live same-id owner instead of treating
superseded invokes as completed.
* fix(sftp): never force-release isolated write path gates
Hold host/path gates for isolated truncating OPENs until the OPEN callback
settles, matching shared-channel behavior. Force-releasing after sftp.end()
allowed a late OPEN to wipe a completed same-path destination with no live
transfer left to invalidate.
* fix(sftp): hold path gate until late unlink settles
Transfer cancel can settle after a 2s unlink grace so dead channels do not
hang invokes, but the host/path gate stays held until the unlink callback
actually fires so a delayed unlink cannot delete a same-path retry stage.
* fix(sftp): wait for live owner without fixed superseded deadline
Superseded invokes now wait only on the live same-id transfer's terminal
status (or cancel). A 30-minute timeout could mark a still-running long
transfer failed and stick that status after the owner later completed.
* fix(sftp): classify superseded before cancelled cleanup
When a cancelled attempt loses ownership to a same-id retry mid-OPEN, the
error is Transfer superseded while cancelled is still true. Handle supersede
first so cancel cleanup cannot delete the retry stage, free its leases, or
emit a terminal cancelled event for the live owner.
* fix(sftp): wait for pending write OPEN before fastPut fallback
Expose pendingWriteOpenPathGate on the transfer so non-resumable concurrent
isolated failures wait for their own truncating OPEN to settle before
fastPut rewrites the same stage.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(settings): 支持一键清空所有会话日志
在系统设置的会话日志卡片中新增清空按钮,删除保存目录下的
所有日志文件及主机子目录;操作前弹窗二次确认,完成后展示
删除/失败数量。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(session-logs): restrict clear-all to known log artifacts
Clearing the session logs directory no longer recursively deletes every
top-level entry. Only files matching Netcatty auto-save / export naming
patterns and host folders that exclusively contain those artifacts are
removed, so a shared Documents/Downloads save path cannot wipe unrelated
user files (Codex P1 on #2738).
* fix(session-logs): skip active auto-save streams during clear-all
Clear-all must not rm host dirs or log files still open by
sessionLogStreamManager write streams (orphan inode / silent stop).
Expose getActiveLogPaths and preserve those targets while still
deleting inactive siblings and pure host folders.
* fix(session-logs): preserve active snapshot-only host dirs on clear-all
txt/html streams may not create a file until the first snapshot, so treat
any active log path whose parent is the host directory as live work before
removing pure session-log folders.
* fix(session-logs): preserve worker-owned active streams on clear-all
Clear-all only queried the main-process sessionLogStreamManager, so
worker-owned auto-save files were invisible and could be deleted while
live. Union active paths from a running terminal worker via
netcatty:sessionLogs:getActivePaths, and skip cold-starting a stopped
worker.
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
* fix(#2697): automated Cursor CLI fix
* fix: address Codex review on PR #2734
* fix(ai): keep MCP absolute approval deadline and Escape reject
Canceling idle review no longer leaves MCP approvals unbounded past the
external agent tool-call window, and Enter-only key stopPropagation lets
Escape still reject from Copy/Expand controls.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ai): keep wrapped CLI target context on approval cards
Parse --session from netcatty-tool-cli wrappers into execution context and
still show invocation args when the pending card displays an unwrapped command.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ai): surface Codex reason on pending approval cards
Keep agent justification visible in the execution-context strip and still
show extra invocation args such as commandActions beside the command block.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ai): bound Catty approval cancel with absolute deadline
Keep Catty confirm-mode approvals from going unbounded after review:
cancelApprovalTimeout re-arms the creation absolute deadline (MCP-style)
so a late approve cannot outlive the original timeout/stream tool budget.
Also harden pending-card Enter: extract isNestedInteractiveApprovalTarget
so Copy/Expand (and other nested controls) never Approve Once via bubble.
* fix(ai): bound Codex approval cancel and unwrap CLI wrappers
cancelInteractionTimeout now re-arms the absolute creation deadline
(Catty/MCP style) instead of leaving review unbounded. Also treat
netcatty-tool-cli.cjs/.cmd as CLI wrappers so approval Command/Copy
shows the remote command after --.
* fix: address Codex review on PR #2734
* fix(ai): separate Catty approval idle timer from hard deadline
Review activity re-arms a full idle window from now instead of the original
absolute remainder, capped by a hard deadline (3x idle, max 30m). Stream
tool budgets use the hard deadline so mid-review approvals are not cut off.
* fix(ai): re-arm Catty approval idle and sync-deny hard deadline
Review interactions kept a one-shot timeoutCancelledRef, so later
cancelApprovalTimeout calls no longer re-armed idle. Remove the one-shot
and deny synchronously when the hard deadline has already elapsed so a
late approve cannot race setTimeout(0).
* fix(ai): re-arm approval idle on args-only scroll review
Pending approvals without a display command (Codex file-change/permissions,
write tools with JSON args) only show the overflow args block. Wire the same
onScroll/onWheel markReviewing path used by the command pre so continuous
wheel/trackpad review re-arms Catty and MCP/Codex idle timers.
---------
Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com>
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(#2745): automated Cursor CLI fix
* fix(sftp): reset IME composing guard when filter bar closes
If the filter input unmounts mid-composition (filter bar toggled closed),
compositionend never fires, leaving filterComposingRef stuck at true and
blocking every later onSetFilter commit. Clear the guard when the bar hides.
Also drop the now-unused startTransition prop from the toolbar (its only
use was the removed deferred filter path).
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): resync filter draft to committed filter when bar closes
Resetting only the composing guard left filterDraft diverged from pane.filter
when the bar closed mid-composition, so reopening showed stale uncommitted text.
Resync the draft to pane.filter on close.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): clear IME composing guard in commitFilterValue
Centralize the guard reset so every commit/clear path (composition end,
Escape, inline clear button, close button) finalizes the IME session. The
inline clear previously left filterComposingRef stuck true, blocking later
onChange commits and letting a delayed compositionend undo the clear.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): honor external filter change over stale draft on composition end
If navigation (e.g. follow-terminal-CWD) clears pane.filter mid-composition,
onCompositionEnd previously re-committed the stale composed draft, overwriting
the navigation-clear invariant. Capture pane.filter at composition start and
adopt any external change on end instead of committing the stale draft.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): supersede IME composition when navigation clears filter
Navigation can clear pane.filter mid-CJK composition. Adopt that external
clear into the draft while composing, arm a supersede latch so the common
post-compositionend onChange cannot re-commit the stale draft, and reset
composing/supersede state when the filter bar unmounts or a commit path runs.
* fix(sftp): supersede IME filter draft when path navigates
When the committed filter was already empty before composition,
directory navigation sets filter to "" again so pane.filter does not
change. Track path at composition start and supersede the draft when
the path changes so compositionend cannot re-commit stale IME text.
* fix: address Codex review on PR #2746
* fix(sftp): clear IME supersede latch if no post-change arrives
After compositionend arms the supersede latch, schedule a macrotask
fallback clear so ordinary keystrokes work when the browser never
fires the post-composition onChange that would clear it.
* fix: use ASCII punctuation in PR comments and tests
Replace em dashes with ASCII hyphens in comments/tests. Keep the guarded
setTimeout supersede-latch clear from the prior IME fix.
---------
Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
* fix(plugins): backfill sync provider bindings after schema upgrade
Promote legacy sync-provider-map secrets into the host binding table on
open, and infer plugin ownership from sync-credential rows so disconnect
still cleans credentials when bindings were never written.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(plugins): infer sync provider owners only via pluginId. namespace
Exact pluginId===providerId is not ownership for contribution provider
ids; prefer the longest pluginId. prefix when backfilling disconnect cleanup.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(plugins): stop inferring sync owner from credential prefixes
Parent plugin ids can namespace-prefix another plugin's provider id.
Guessing ownership from sync-credential rows would let disconnect wipe
the wrong secrets. Keep host bindings and legacy map backfill as the
only ownership sources when no live contribution is present.
* fix(plugins): do not overwrite existing sync provider bindings on backfill
Legacy sync-provider-map rows from a parent plugin must not steal a
provider that already has a host binding. Skip upsert when a binding
exists; still delete the stale map secret.
* fix(plugins): pick longest owner when legacy map rows conflict
Group sync-provider-map rows by provider before binding so a shorter
parent plugin cannot win just because SQLite returned it first. Still
preserve existing host bindings and delete all leftover map secrets.
* fix(plugins): never delete secrets during sync-provider-map backfill
The map key prefix is not reserved for host internals; plugins may store
ordinary secrets under the same shape. Only seed missing host bindings
from candidate rows and leave plugin_secrets untouched.
* fix(plugins): seed sync bindings from live provider metadata
Schema-2 installs only stored sync-credential* rows. After upgrade, seed
missing provider bindings from the host contribution catalog when those
credentials exist, instead of guessing from secret key prefixes.
* fix(plugins): seed sync bindings from installed manifests, not only enabled
listProviders skips disabled/quarantined plugins. Walk installed plugin
manifests at host init so schema-2 credential rows under disabled plugins
still get provider bindings for disconnect cleanup.
* fix(plugins): seed sync bindings from all installed version manifests
Active-only listPlugins() misses providers removed/renamed on upgrade
while credentials remain under the plugin. Walk every plugin_versions
manifest so disconnect can still resolve ownership after schema upgrade.
* fix(plugins): only auto-bind single-provider plugins from credentials
Plugin-scoped sync-credential* rows are shared across providers. Binding
every historical provider would let disconnect of a stale one wipe the
active provider's secrets. Seed bindings only when exactly one sync
provider is known for that plugin.
* fix: address Codex review on PR #2752
* fix(plugins): stop resurrecting sync bindings after unbind
Consume promoted sync-provider-map markers and leave an unbound
tombstone on disconnect so constructor backfill cannot re-bind a
provider the user already disconnected.
* fix(plugins): prefer credential-backed owners for legacy map backfill
Longest sync-provider-map namespace alone could bind a nested plugin over a
parent that still owns sync-credential* secrets. Only promote candidates with
credentials; pick the longest among those, and skip when none or ambiguous.
* fix(plugins): skip ambiguous multi-credential legacy map backfill
Only auto-bind when exactly one credential-backed map candidate exists.
When parent and nested plugins both hold credentials for the same provider,
longest pluginId is not a reliable owner signal — leave unbound.
* fix(plugins): skip cross-plugin provider conflicts in live backfill
When multiple credential-backed plugins declare the same providerId via
nested namespaces, do not bind the first writer. A later missing-provider
disconnect would otherwise delete the wrong sync-credential rows.
* fix(plugins): use full installed catalog for enable-time sync backfill
Enable-time binding must see the same active+inactive manifest catalog as
startup so cross-plugin nested provider ambiguity is not missed when only
the newly enabled plugin's live providers would be passed.
* fix(plugins): count non-credential manifest claims in backfill conflicts
Live nested plugins without credentials yet must still block binding a
credential-backed legacy parent to the same providerId, or disconnect can
prefer the live owner while the stale binding remains.
* fix(plugins): skip legacy map promote on installed-manifest conflicts
Constructor-time map backfill must not bind a sole credential-backed
parent when another installed package also declares the same sync
provider, or later disconnect cannot clear the stale binding.
* fix(plugins): require single-provider maps for legacy-map promotion
A plugin with one sync-credential row and maps for multiple historical
providers must not bind all of them — disconnecting a stale provider
would delete the live credentials via the shared prefix.
* fix(plugins): count installed manifest providers in legacy-map guard
A sole stale map row must not promote when the credential owner's live
manifest also declares another sync provider under the same credentials.
* fix(plugins): backfill sync bindings before startup activation
Seed installed-manifest bindings before contributionService.initialize so
an early syncDeleteSecrets cannot miss ownership and leave orphan credentials.
* fix(plugins): seed sync bindings at host construction
Run installed-manifest backfill when the plugin host is created, before
async manager.initialize, so early syncDeleteSecrets can resolve ownership.
* fix(plugins): re-seed sync bindings after package store recovery
PackageStore.recover can add installed versions after the constructor
seed; re-run the catalog backfill when package initialize finishes.
* fix(plugins): await manager init before syncDeleteSecrets ownership
Block secret cleanup until package recovery and binding seed finish so
early disconnect cannot miss retained sync-credential rows.
* fix(plugins): re-run legacy-map backfill after package recovery
After PackageStore.recover repopulates versions, re-run legacy-map promote
so recovered manifests participate in ownership conflict checks.
* fix(plugins): defer legacy-map backfill until after package recovery
Constructor no longer promotes sole map candidates before recovered
manifests are visible. hostService re-runs backfill after package init.
* fix(plugins): defer live binding seed until after package recovery
Construction-time live-provider backfill can bind a stale owner against an
incomplete catalog before PackageStore.recover() restores plugin_versions.
Seed only after recovery; syncDeleteSecrets already awaits manager init.
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com>
Put requestDisable/requestReset on the settings mutation queue so concurrent
timeout changes cannot re-enable App Lock after a successful disable. Capture
the runtime lock version before system auth and reject the prompt if the lock
epoch advanced (idle→background re-lock) while the dialog was open.
Password-change saves go through the settings mutation queue. System unlock
re-checks locked state after the OS prompt so a re-lock during the dialog
cannot unlock from a stale auth success.
Queue app-lock save mutations and use unique temp paths so concurrent
writes cannot ENOENT or clobber. Update the system-unlock toggle test to
match the already-enabled-while-unavailable disable path.
Write app-lock settings via temp file + rename to avoid truncating the
verifier on crash. Gate application-menu Close and window:close IPC while
locked so popups are not destroyed behind the overlay.
isVisible is false for minimized BrowserWindows, which made Dock reopen
re-lock while a minimized main/session window was still open. Treat
isMinimized() as a live session, while still ignoring hidden prewarm
windows.
Keep forceRenderChildren so popup config IPC still registers while locked,
but do not mark the popup ready (or mount Terminal/startup commands) until
allowTerminalStart is true after unlock.
The overlay helper import was accidentally inserted inside open named
import lists, which broke TypeScript parsing. Move it to a top-level
import statement in SnippetsManager and SFTP keyboard shortcuts.
Optimistic unlock used current.version+1, which can outrank a concurrent
main-process re-lock and make refreshRuntimeState ignore the authoritative
locked state. Keep the observed version so real main updates always win.
Raise the lock screen to max z-index and hide the context-menu portal while
locked. Centralize isAppLockOverlayActive and gate ChatInput slash picker,
SFTP shortcuts, theme modal, and shortkey/hotkey recorders so earlier
window-capture listeners cannot steal unlock keystrokes.
Settings prewarm stays as a hidden BrowserWindow and was blocking
ensureAppLockForFreshSession after the main window closed. Only visible
windows count as a live session for that emptiness check.
Earlier-registered capture handlers (shortkey/hotkey recorders, snippet
save) run before the overlay's stopImmediatePropagation. Skip those
handlers when [data-app-lock-overlay] is present so password keystrokes
cannot mutate settings behind the lock screen.
Fresh-session re-lock now considers settings/tray/popup windows via
getAppLockReopenWindows so an open Settings UI is not interrupted.
System unlock toggle stays clickable when already enabled even if the
authenticator is currently unavailable, so users can turn it off.
While locked, use window capture with stopImmediatePropagation so
shortkey/hotkey recorders never see lock-screen keys, and stop
document bubble so window-level shortcuts (e.g. Snippets save) cannot
run even when focus is on body.
inert does not block keydown bubbling to window listeners. While locked,
stop propagation for keys targeted at the overlay so panel shortcuts
(e.g. Snippets Ctrl/Cmd+S) cannot act behind the lock screen.
Mounted shortcut hooks (e.g. SFTP) suppress when a dialog has
data-state=open. Without that marker, keyboard actions could still run
behind the app lock overlay when focus is not on the password field.
System Manager terminal popups receive config immediately after loadURL.
AppLockGate used to withhold children until async lock state resolved,
so onTerminalPopupConfig could miss the one-shot IPC. Force-render the
popup route and keep the last preload config for late remounts.
Detached #/session-window windows register with registerAsMainWindow:false
and were omitted from app-lock runtime/settings broadcasts, so they stayed
interactive after a lock. Include getAppContentWindows() (deduped) in the
broadcast fan-out and cover session windows in the harness.
On darwin, closing every window leaves the process alive while app-lock
runtime stays unlocked. Re-apply startup lock when the last window closes
and before creating a fresh main window with no app-content windows.
Mark mounted app children inert/aria-hidden/pointer-events-none while locked,
trap Tab inside the lock dialog, and suppress global hotkeys. Queue tray
port-forward toggles in main (and renderer) until the runtime unlocks.
Dirty-quit focus restore already verified.
* fix(ci): dedupe same-head @codex review requests
Plain maintainer/Cursor-bot @codex review comments did not plant the
cursor-external-codex marker, so own_rerequest on synchronize posted a
second request for the same SHA. That concurrent pair produced a clean
issue comment from one job and a findings review from the other.
Skip re-request when any trusted or Cursor-bot author already requested
this head via external marker, cursor-codex-head pin, or a recent plain
@codex review.
* fix(ci): only dedupe plain @codex against the current head push
Plain unpinned requests no longer skip solely by age — that blocked
re-request after a new synchronize when an older plain @codex still
looked "recent". Callers pass the head commit time as notBefore so
only post-push plain requests suppress a duplicate.
* fix(ci): use PR head-change time for plain @codex dedupe
Commit author/committer dates predate cherry-picks and rebases, so a
plain @codex for the previous head could suppress re-request after a
new synchronize. Pass pull_request.updated_at (moves with the push)
as notBefore instead.
* fix(ci): dedupe @codex only via head-pinned markers
Drop plain-unpinned timestamp heuristics — they cannot safely know which
SHA a bare @codex review targeted. Skip re-request when a trusted author
already pinned this head with cursor-codex-head or cursor-external-codex.
When quit is cancelled because a renderer reports unsaved editors,
show and focus those windows first. A tray-hidden or unfocused main
window otherwise only posts a toast the user never sees, so quit
appears to fail silently.
Defer first App mount (and rendererReady) for any lock reason so
background/idle locks cannot deliver ssh:// connects behind the overlay.
Wait indefinitely for renderer readiness and re-queue failed deliveries so
startup unlock after the old 15s timeout no longer drops deep links. Queue
renderer-side deep links while already-mounted sessions re-lock.
* fix(ci): let maintainer @bot reclassify auto-closed issues
Author reopen disputes still stay on issue_followup when
triage:already-available/unclear remain, but an explicit maintainer
@netcatty-bot mention must re-enter issue_classify so re-triage can
run without an open bot PR.
* fix(ci): prefer maintainer @bot over author follow-up routing
When a trusted maintainer is also the issue author, route explicit
@netcatty-bot mentions as maintainer re-triage so auto-closed
already-available/unclear issues can re-enter classify.
* fix(ci): stop reopen from replaying issue triage
Already-admitted issue reopen events were routed back into full classify,
so follow-up handoffs and maintainer reopens could close again or overwrite
ready-for-human with needs-info. Skip bot reopen, hand auto-closed human
reopens to maintainers, and keep already-available disputes on follow-up.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ci): keep ready-for-human follow-ups off classify
After reopen handoff drops triage:already-available, actionable author
replies could still be refined into classify and auto-close again. Treat
ready-for-human like the dispute signal and keep those comments on
follow-up.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ci): preserve already-available dispute label on handoff
Keep triage:already-available when handing reopened auto-closed issues
to humans, and only use that label (not ready-for-human) to block
reclassify. This stops the close loop without blocking feature_defer or
other ready-for-human follow-ups from re-entering classify.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ci): keep unclear reopen disputes off classify
Match the already-available dispute guard for triage:unclear / unclear so
human reopen handoffs of auto-closed unclear issues cannot re-enter
classify and close again on the next author follow-up.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ci): revalidate auto-close labels before handoff
Queued ready_for_human_handoff must not overwrite a maintainer who already
cleared triage:already-available/unclear and moved the issue (for example
to ready-for-agent). Skip the handoff when those outcome labels are gone.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* test(ci): cover skipped reopen handoff without auto-close labels
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ci): do not reopen issues already closed before handoff
Queued ready_for_human_handoff must respect a maintainer who re-closed the
issue while the job was waiting. Skip when live state is closed instead of
forcing ensureOpen.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ci): reclassify non-auto-close issue reopens
After a merged automation fix, cleanup leaves triage:bug-ready/admitted on
the source issue. Human reopen of those labels must re-enter classify so a
failed fix can spawn another implementation; only bot reopen stays skipped.
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(plugins): address residual Codex sync secret and backup gaps
Retain provider→plugin secret bindings so disconnect can wipe credentials
after a plugin is disabled, keep overwritten secrets on failed reconnect,
and snapshot live-empty sidecars in protective backups.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(plugins): restore overwritten sync secrets on rejected reconnect
Stash prior plaintext and SecretRef id on overwrite, restore them when
connect fails, and discard the stash after a successful reconnect so
saved provider credentials keep resolving.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(plugins): make sync secret overwrite stash opt-in
Only the syncPutSecret path stashes prior plaintext/SecretRef so ordinary
secrets.set overwrites do not leave credentials in process memory.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(plugins): host-own sync provider bindings and clear failed stashes
Move provider→plugin bindings into a dedicated DB table outside
plugin-writable secrets, validate provider namespace ownership, and drop
overwrite stashes when replacement writes fail.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(plugins): keep overwrite stash until restore write succeeds
Only drop the prior plaintext after encrypt+upsert succeeds so a failed
restore can still retry and recover the saved SecretRef.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(plugins): preserve overwrite stash across retries and prefix deletes
Do not replace an existing restore stash on later puts, and clear matching
stash entries when deleteByKeyPrefix wipes sync credentials on disconnect.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
macOS copy defaults to Cmd+C. With no terminal selection, the shortcut
handler used to consume the chord as an empty clipboard write, so nested
TUIs that expect Kitty Super+C (e.g. Herdr) never received the key.
Pass plain Meta+C through the same no-selection path as Ctrl+C, while
keeping other unbound copy chords as a safe no-op (#1461).
Closes#2728
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
'ai.providers.style.help':'Selects which API format requests use. Override when a third-party endpoint speaks a different dialect than its provider type suggests.',
'ai.providers.openaiApi':'OpenAI API format',
'ai.providers.openaiApi.chat':'Chat Completions',
'ai.providers.openaiApi.responses':'Responses',
'ai.providers.openaiApi.help':'Chat Completions works with most OpenAI-compatible endpoints. Responses can raise cache hit rates on relays that support /v1/responses.',
'ai.providers.apiKey.placeholder':'Enter API key',
'ai.providers.apiKey.decrypting':'Decrypting...',
'ai.providers.baseUrl':'Base URL',
'ai.providers.baseUrl.anthropicHelp':'Anthropic-compatible: host with or without /v1 (for example https://gateway.example or https://gateway.example/v1). Detection and chat both use /v1/models and /v1/messages.',
'ai.providers.baseUrl.ollamaHelp':'Local Ollama: http://localhost:11434/v1 (no API key). Ollama Cloud: https://ollama.com/v1 plus your cloud API key.',
'ai.toolAccess.description':'Choose how external agents access Netcatty sessions. MCP exposes the built-in server, while Skills + CLI points agents to the local Netcatty skill and CLI commands.',
'ai.toolAccess.mode.mcp':'MCP',
'ai.toolAccess.mode.skills':'Skills + CLI',
'ai.toolAccess.mcpPrompt.title':'Prompt for your AI client',
'ai.toolAccess.mcpPrompt.description':'Paste this prompt into your AI client (Codex, Claude Code, …) and it will register Netcatty MCP for you.',
'ai.toolAccess.mcpPrompt.enableHint':'Turn on External MCP below to include the launcher path in this prompt.',
'ai.toolAccess.skills.file':'Skill file',
'ai.toolAccess.skills.description':'In Skills + CLI mode, agents are pointed to this local skill file automatically. The Netcatty CLI launcher path is provided to the agent in each session.',
'ai.safety.commandTimeout.description':'Maximum seconds a command can run before being terminated through Netcatty execution.',
'ai.safety.commandTimeout.unit':'sec',
'ai.safety.responseIdleTimeout':'Built-in AI Response Wait',
'ai.safety.responseIdleTimeout.description':'Cancel a built-in AI request after this many seconds without a new response. This setting does not control total response time or command execution.',
'ai.safety.responseIdleTimeout.unit':'sec',
'ai.safety.maxIterations':'Max Iterations',
'ai.safety.maxIterations.description':'Maximum number of AI tool-use loops to prevent runaway execution. External agents may have their own internal iteration limits that take precedence.',
'credentials.protectionUnavailable.message':'Saved passwords and keys cannot be auto-decrypted on this device. Re-enter credentials before connecting.',
'settings.system.credentials.unavailableHint':'Credentials encrypted on another user profile or machine cannot be decrypted here. Re-enter and save credentials on this device.',
'settings.system.credentials.portabilityHint':'Cloud Sync is portable because it uses your master key encryption. Local safeStorage encryption is device/user scoped.',
// Settings > System > App Lock
'settings.appLock.title':'App Lock',
'settings.appLock.description':'Require a local lock password when Netcatty opens, after inactivity, or when you lock it manually.',
'settings.appLock.setupTitle':'Set a lock password',
'settings.appLock.setupDescription':'Create the password below. App Lock turns on immediately after it is saved.',
'settings.appLock.manageTitle':'App Lock is ready',
'settings.appLock.enabledStatus':'Enabled. Netcatty locks on startup, after inactivity, and when you lock it manually.',
'settings.appLock.disabledStatus':'Password saved, but App Lock is currently off.',
'settings.appLock.disableTitle':'Turn off App Lock',
'settings.appLock.disableDescription':'Enter the current lock password to stop locking Netcatty. The saved password will be removed; turning App Lock on again requires creating a new one.',
'settings.system.networkProxy.description':'HTTP(S) proxy for cloud sync and AI providers. This is separate from SSH ProxyJump / ProxyCommand profiles.',
'settings.sessionRestore.startupLandingDesc':'Choose whether to open the host home page or a local terminal when Netcatty starts with no restored sessions.',
'settings.vault.showHostTreeSidebarDesc':'Display the host list sidebar and its top-bar toggle on terminal and editor tabs.',
'settings.vault.autoImportSystemKnownHosts':'Auto-import system known_hosts',
'settings.vault.autoImportSystemKnownHostsDesc':'When Vault loads, silently import entries from the OpenSSH known_hosts file. Turn this off to keep Netcatty known hosts separate; Scan System still imports on demand.',
'settings.vault.notesFont':'Code font',
'settings.vault.notesFontDesc':'Font family used for code blocks and inline code in notes',
'settings.vault.notesFontSize':'Note font size',
'settings.vault.notesFontSizeDesc':'Font size used for note body text',
'settings.vault.notesCodeFontSize':'Code font size',
'settings.vault.notesCodeFontSizeDesc':'Font size used for code blocks and inline code in notes',
// Update notifications
'update.available.title':'Update Available',
@@ -552,6 +655,11 @@ Highlight the focused split pane:
'settings.terminal.behavior.autoCloseOnExit':'Auto-close terminal on exit',
'Show a system notification when a program emits OSC 9, OSC 777 notify, or kitty OSC 99. Used by Codex and other agents for turn-complete and approval alerts.',
'settings.shortcuts.shellOnlyTabNumberShortcuts.desc':'When enabled, Cmd/Ctrl+[1...9] and Ctrl+Tab switch only work tabs (terminals, workspaces, editors), not the pinned Vault or SFTP tabs.',
'settings.shortcuts.showTabNumberBadges.label':'Show tab number badges',
'settings.shortcuts.showTabNumberBadges.desc':'While holding Cmd/Ctrl, show 1-9 on tabs that match Cmd/Ctrl+[1...9], so you can jump accurately.',
'terminal.dragDrop.notConnected':'Cannot drop files - terminal is not connected',
'terminal.dragDrop.errorTitle':'Drop Error',
'terminal.dragDrop.errorMessage':'Failed to process dropped files',
'terminal.dragDrop.destinationUnknown':'Could not determine the current terminal folder. Enable directory tracking, or open SFTP and choose an upload folder first.',
'terminal.dragDrop.uploadCancelled':'The upload was cancelled because the terminal connection changed or could not be reused. Drop the files again after reconnecting.',
'terminal.dragDrop.needsSudoElevation':'This folder is not writable as the login user. Enable Sudo elevation in host settings, or go back to your user directory and drop again.',
'terminal.search.placeholder':'Search...',
'terminal.search.noResults':'No results',
'terminal.search.prevMatch':'Previous match (Shift+Enter)',
'serial.field.byteOrientedBackspaceDesc':'Send one DEL per byte so the device deletes a whole multi-byte character (for devices that delete bytes). Disable for character-aware line editors (e.g. Linux readline).',
'notes.editor.unrenderableMarkdown':'This note contains Markdown the rich editor cannot display (for example unbalanced <tag> text). Showing the raw Markdown source instead — the content is intact.',
'notes.editor.retryRichView':'Retry rich view',
'notes.preview.empty':'Nothing to preview yet.',
'notes.empty.title':'No notes yet',
'notes.empty.desc':'Create Markdown notes for runbooks, reminders, and server handoff details.',
'sftp.tabs.copyCurrentPath':'Copy and go to current path',
'sftp.conflict.title':'File Conflict',
'sftp.conflict.desc':'A file with the same name already exists at the destination',
'sftp.conflict.folderTitle':'Folder Conflict',
'sftp.conflict.folderDesc':'Choose how to continue.',
'sftp.conflict.folderFileDesc':'A file with the same name already exists at the destination. A folder cannot be merged with a file.',
'sftp.conflict.folderSymlinkDesc':'A symbolic link with the same name already exists at the destination. A folder cannot be merged with a symbolic link.',
'sftp.conflict.folderUnknownDesc':'An item with the same name exists at the destination, but its type could not be confirmed. Replace is unavailable to protect destination data.',
'settings.sftp.doubleClickBehavior.transferDesc':'Transfer the file to the other pane\'s active host',
'settings.sshDeepLink.title':'SSH and Telnet links',
'settings.sshDeepLink.enable':'Open ssh:// and telnet:// links with Netcatty',
'settings.sshDeepLink.enableDesc':'Allow Netcatty to handle ssh:// and telnet:// links from browsers and other apps.',
'settings.sshDeepLink.enableDesc':'Allow Netcatty to handle ssh:// and telnet:// links from browsers and other apps, plus PuTTY-style launches such as -ssh user@host -P 22 -pw password.',
'settings.explorerContextMenu.enable':'Show “Open in Netcatty” in folder context menus',
'settings.explorerContextMenu.enableDesc':'Adds a right-click item for folders and folder backgrounds so you can open a local terminal there. Applies immediately; no reinstall needed.',
'settings.sftp.compressedUpload.enableDesc':'Automatically compress folders using tar before transfer. Requires tar support on the server. Falls back to regular transfer if not available.',
'settings.sftp.skipUnchanged.enableDesc':'Skip files whose size and modification time already match the destination (rsync-style). Disabled when replacing a folder into a staging path.',
'hostDetails.hostname.placeholder':'IP or Hostname',
'hostDetails.section.general':'General',
'hostDetails.os.title':'System identification',
'hostDetails.os.desc':'Prefer automatic detection. Override only when the detected system is incorrect. Network Device Mode is configured separately.',
'hostDetails.os.auto':'Automatic',
'hostDetails.os.unknown':'Other / unknown',
'hostDetails.os.selection':'System selection',
'hostDetails.os.current':'Current system',
'hostDetails.os.freebsd':'FreeBSD',
'hostDetails.os.linux':'Linux',
'hostDetails.os.windows':'Windows',
'hostDetails.os.macos':'macOS',
'hostDetails.section.sftp':'SFTP Settings',
'hostDetails.sftp.fileProtocol':'File Protocol',
'hostDetails.sftp.fileProtocol.desc':'Auto tries SFTP, then falls back to SCP mode when the SFTP subsystem is unavailable (e.g. some NAS). Force SCP for hosts that only support SCP.',
'ai.description':'Configura proveedores de IA, agentes y ajustes de seguridad',
'ai.providers':'Proveedores',
'ai.agents':'Agentes',
'ai.providers.empty':'No hay proveedores configurados. Agrega un proveedor para comenzar.',
'ai.providers.add':'Agregar proveedor',
'ai.providers.active':'Activo',
'ai.providers.apiKeyConfigured':'Clave de API configurada',
'ai.providers.noApiKey':'Sin clave de API',
'ai.providers.configure':'Configurar',
'ai.providers.remove':'Quitar',
'ai.providers.name':'Nombre para mostrar',
'ai.providers.name.placeholder':'p. ej. Mi proveedor',
'ai.providers.style':'Estilo de protocolo',
'ai.providers.style.anthropic':'Compatible con Anthropic',
'ai.providers.style.openai':'Compatible con OpenAI',
'ai.providers.style.google':'Compatible con Google',
'ai.providers.style.inherited':'auto',
'ai.providers.style.help':'Selecciona qué formato de API usan las peticiones. Anúlalo cuando un endpoint de terceros hable un dialecto distinto al que sugiere su tipo de proveedor.',
'ai.providers.openaiApi':'Formato de API de OpenAI',
'ai.providers.openaiApi.chat':'Chat Completions',
'ai.providers.openaiApi.responses':'Responses',
'ai.providers.openaiApi.help':'Chat Completions funciona con la mayoría de endpoints compatibles con OpenAI. Responses puede mejorar la tasa de aciertos de caché en relés que admiten /v1/responses.',
'ai.providers.baseUrl.anthropicHelp':'Compatible con Anthropic: host con o sin /v1 (por ejemplo https://gateway.example o https://gateway.example/v1). La detección y el chat usan /v1/models y /v1/messages.',
'ai.providers.baseUrl.ollamaHelp':'Ollama local: http://localhost:11434/v1 (sin API key). Ollama Cloud: https://ollama.com/v1 y tu clave de Cloud.',
'ai.providers.skipTLSVerify':'Omitir la verificación del certificado TLS (para certificados autofirmados)',
'ai.providers.contextWindow.help':'Déjalo en blanco para usar el valor de la lista de modelos cuando esté disponible; de lo contrario, Netcatty usa un valor predeterminado seguro.',
'ai.providers.contextWindow.error':'Ingresa un número entero positivo o déjalo en blanco.',
'ai.providers.advancedParams.default':'Predeterminado del proveedor',
// AI Codex
'ai.codex':'Codex',
'ai.codex.title':'Codex CLI',
'ai.codex.description':'Conecta OpenAI Codex. Inicia sesión con ChatGPT aquí, o habilita una clave de API de un proveedor compatible con OpenAI y un endpoint personalizado en Configuración.',
'ai.codex.appServer.description':'Usa el protocolo persistente de Codex para aprobaciones nativas, controles de sandbox, modelos en vivo y preguntas a mitad de turno. El SDK sigue siendo el predeterminado.',
'ai.codex.appServer.checking':'Comprobando la compatibilidad con App Server…',
'ai.codex.appServer.available':'App Server está disponible para esta Codex CLI.',
'ai.codex.appServer.modelCatalogWarning':'El catálogo de modelos de Codex en vivo no está disponible. Se usa la lista de modelos integrada.',
'ai.codex.appServer.approval.allowSession':'Permitir por sesión',
'ai.codex.appServer.userInput.title':'Codex necesita tu aporte',
'ai.codex.appServer.userInput.description':'Responde estas preguntas para continuar el turno actual.',
'ai.codex.appServer.userInput.other':'Ingresa otra respuesta',
'ai.codex.appServer.userInput.autoResolve':'Codex continuará automáticamente si no se proporciona una respuesta a tiempo.',
'ai.codex.steer.placeholder':'Agrega una instrucción mientras Codex está trabajando…',
'ai.codex.steer.notSteerableReview':'Este turno de revisión de Codex no puede aceptar instrucciones adicionales. Tu borrador se conservó.',
'ai.codex.steer.notSteerableCompact':'Este turno de compactación de Codex no puede aceptar instrucciones adicionales. Tu borrador se conservó.',
'ai.codex.steer.busy':'Ya se está enviando otra instrucción a Codex.',
'ai.codex.steer.inactive':'El turno de Codex ya terminó. Tu borrador se conservó.',
'ai.codex.steer.unsupported':'Las instrucciones en curso requieren el runtime de Codex App Server.',
'ai.codex.steer.failed':'Codex no pudo aceptar la instrucción adicional. Tu borrador se conservó.',
'ai.codex.detecting':'Detectando...',
'ai.codex.notFound':'No encontrado',
'ai.codex.awaitingLogin':'Esperando inicio de sesión',
'ai.codex.connectedChatGPT':'Conectado mediante ChatGPT',
'ai.codex.connectedApiKey':'Conectado mediante clave de API',
'ai.codex.connectedCustomConfig':'Conectado mediante ~/.codex/config.toml',
'ai.codex.customConfigIncomplete':'Configuración personalizada detectada (falta la variable de entorno)',
'ai.codex.customConfigHint':'Usando el proveedor personalizado "{provider}" configurado en ~/.codex/config.toml: no se necesita inicio de sesión con ChatGPT.',
'ai.codex.customConfigMissingEnvKey':'Advertencia: {envKey} no está definida en tu entorno de shell. Expórtala (o inicia netcatty desde un shell que la tenga) para que Codex pueda autenticarse.',
'ai.codex.notConnected':'No conectado',
'ai.codex.statusUnknown':'Estado desconocido',
'ai.codex.path':'Ruta:',
'ai.codex.notFoundHint':'No se pudo encontrar codex en el PATH. Instálalo o especifica la ruta del ejecutable a continuación.',
'ai.claude.configSection':'Autenticación y configuración (opcional)',
'ai.claude.configDir':'Directorio de configuración',
'ai.claude.configDir.placeholder':'~/.claude (déjalo en blanco para usar el predeterminado)',
'ai.claude.configDir.hint':'Define CLAUDE_CONFIG_DIR: apunta a una carpeta donde hayas ejecutado el inicio de sesión `claude` (contiene settings.json + credenciales).',
'ai.claude.settings':'Archivo de configuración',
'ai.claude.settings.placeholder':'~/team-settings.json (ruta o JSON en línea {"model":"..."})',
'ai.claude.settings.hint':'Opcional. Una ruta a settings.json o un JSON en línea, pasado al SDK como `settings`. Se suma a — y es independiente de — el directorio de configuración anterior (se combina por encima, no lo reemplaza).',
'ai.claude.envVars.hint':'Una KEY=VALUE por línea, pasada al agente de Claude. Se almacena localmente en texto plano; para claves de API o credenciales, prefiere el directorio de configuración anterior (un inicio de sesión `claude`).',
'ai.claude.check':'Verificar',
'ai.claude.resetPath':'Restablecer',
// AI GitHub Copilot CLI
'ai.copilot.title':'GitHub Copilot CLI',
'ai.copilot.description':'Usa la CLI de GitHub Copilot. Una vez detectada, se puede seleccionar como agente de codificación externo.',
'ai.copilot.detecting':'Detectando...',
'ai.copilot.detected':'Detectado',
'ai.copilot.notFound':'No encontrado',
'ai.copilot.path':'Ruta:',
'ai.copilot.notFoundHint':'No se pudo encontrar copilot en el PATH. Instálalo o especifica la ruta del ejecutable a continuación.',
'ai.cursor.description':'Usa el SDK de Cursor o el inicio de sesión local de la CLI de Agent.',
'ai.cursor.detecting':'Detectando...',
'ai.cursor.detected':'Disponible',
'ai.cursor.notFound':'No disponible',
'ai.cursor.path':'Runtime:',
'ai.cursor.notFoundHint':'Ingresa una clave de API para habilitar Cursor o cambia al inicio de sesión por CLI.',
'ai.cursor.notInstalledHint':'No se detectó el SDK de Cursor ni la CLI de Agent.',
'ai.cursor.installStatus':'Runtime de Cursor',
'ai.cursor.installed':'Detectado',
'ai.cursor.notInstalled':'No detectado',
'ai.cursor.modeCli':'Inicio de sesión por CLI',
'ai.cursor.modeApiKey':'Clave de API',
'ai.cursor.modeCliHint':'Usa tu sesión local de `cursor-agent login` y la cuota Automática de la suscripción. La clave de API guardada se conserva, pero no se usa en este modo.',
'ai.cursor.modeApiKeyHint':'Usa la API de Cursor medida. El inicio de sesión por CLI se ignora mientras este modo esté activo.',
'ai.cursor.cliLoginStatus':'Inicio de sesión por CLI',
'ai.cursor.cliLoginOk':'Sesión iniciada',
'ai.cursor.cliLoginAs':'Sesión iniciada como {{email}}',
'ai.codebuddy.description':'Usa CodeBuddy Code mediante el SDK oficial de Agent (`@tencent-ai/agent-sdk`). Una vez detectado, se puede seleccionar como agente de codificación externo.',
'ai.codebuddy.detecting':'Detectando...',
'ai.codebuddy.detected':'Detectado',
'ai.codebuddy.notFound':'No encontrado',
'ai.codebuddy.path':'Ruta:',
'ai.codebuddy.notFoundHint':'No se pudo encontrar codebuddy en el PATH. Instálalo o especifica la ruta del ejecutable a continuación.',
'ai.codebuddy.envVars.hint':'Una KEY=VALUE por línea, pasada al agente de CodeBuddy. Define CODEBUDDY_API_KEY o CODEBUDDY_AUTH_TOKEN aquí para la autenticación. Se almacena localmente en texto plano.',
'ai.codebuddy.effort.hint':'Controla la profundidad de razonamiento del modelo. Usa Bajo para comandos simples y ahorrar tokens, y Alto/XHigh para diagnósticos complejos.',
'ai.codebuddy.maxTurns':'Máximo de turnos',
'ai.codebuddy.maxTurns.hint':'Limita el máximo de turnos de conversación por petición para evitar bucles descontrolados. Déjalo vacío para usar el predeterminado.',
'ai.codebuddy.maxBudget.hint':'Gasto máximo (USD) por petición. Se detiene automáticamente al superarse. Déjalo vacío para no tener límite.',
'ai.codebuddy.sandbox':'Modo sandbox',
'ai.codebuddy.sandbox.hint':'Ejecuta llamadas a herramientas en un sandbox, restringiendo el acceso al sistema de archivos y a la red.',
'ai.codebuddy.fileCheckpointing':'Puntos de control de archivos',
'ai.codebuddy.fileCheckpointing.hint':'Habilita puntos de control de operaciones de archivos para poder revertir las modificaciones de IA a los archivos.',
'ai.codebuddy.elicitation.title':'CodeBuddy necesita tu aporte',
'ai.codebuddy.elicitation.description':'Revisa la petición para continuar el turno actual.',
'ai.codebuddy.elicitation.select':'Selecciona una opción',
'ai.codebuddy.elicitation.yes':'Sí',
'ai.codebuddy.elicitation.no':'No',
'ai.codebuddy.elicitation.decline':'Rechazar',
'ai.codebuddy.elicitation.accept':'Continuar',
'ai.codebuddy.elicitation.validation.required':'{field} es obligatorio.',
'ai.codebuddy.elicitation.validation.invalidType':'{field} tiene un valor no válido.',
'ai.codebuddy.elicitation.validation.integer':'{field} debe ser un número entero.',
'ai.codebuddy.elicitation.validation.notInteger':'{field} debe ser un número entero.',
'ai.codebuddy.elicitation.validation.minimum':'{field} debe ser al menos {limit}.',
'ai.codebuddy.elicitation.validation.maximum':'{field} debe ser como máximo {limit}.',
'ai.codebuddy.elicitation.validation.minLength':'{field} debe contener al menos {limit} caracteres.',
'ai.codebuddy.elicitation.validation.maxLength':'{field} debe contener como máximo {limit} caracteres.',
'ai.codebuddy.elicitation.validation.minItems':'Selecciona al menos {limit} opciones para {field}.',
'ai.codebuddy.elicitation.validation.maxItems':'Selecciona como máximo {limit} opciones para {field}.',
'ai.codebuddy.elicitation.validation.format':'{field} debe coincidir con el formato {format}.',
'ai.codebuddy.elicitation.validation.option':'Selecciona una opción válida para {field}.',
// AI OpenCode
'ai.opencode.title':'OpenCode',
'ai.opencode.description':'Usa OpenCode mediante el SDK oficial. Configura proveedores y claves en OpenCode y luego selecciónalo como agente de codificación externo.',
'ai.opencode.detecting':'Detectando...',
'ai.opencode.detected':'Detectado',
'ai.opencode.notFound':'No encontrado',
'ai.opencode.path':'Ruta:',
'ai.opencode.notFoundHint':'No se pudo encontrar opencode en el PATH. Instálalo o especifica la ruta del ejecutable a continuación.',
// AI Grok Build (in-app managed agent — distinct from External MCP client install)
'ai.grok.title':'Grok Build',
'ai.grok.description':"La CLI de agente de codificación Grok Build de xAI. Instala la CLI de Grok, inicia sesión con `grok login` o define XAI_API_KEY, y luego selecciónala como agente externo.",
'ai.grok.detecting':'Detectando...',
'ai.grok.detected':'Detectado',
'ai.grok.notFound':'No encontrado',
'ai.grok.path':'Ruta:',
'ai.grok.notFoundHint':'No se pudo encontrar grok en el PATH. Instala la CLI de Grok Build o especifica la ruta del ejecutable a continuación.',
'Habla con Grok mediante el Protocolo de Cliente de Agente (grok agent stdio). Inyecta el MCP de Netcatty en session/new. Apágalo para usar la ruta original de CLI headless con streaming-json.',
'ai.grok.runtime.streamingJson.hint':
'Usando streaming-json headless (grok -p --output-format streaming-json). El archivo .grok/config.toml del proyecto se usa para la inyección de MCP.',
// AI Default Agent
'ai.defaultAgent':'Agente predeterminado',
'ai.defaultAgent.description':'Agente que se usará al iniciar una nueva sesión de IA',
'ai.defaultAgent.catty':'Catty (Integrado)',
'ai.toolAccess.title':'Acceso a herramientas',
'ai.toolAccess.mode':'Modo de acceso de Netcatty',
'ai.toolAccess.description':'Elige cómo acceden los agentes externos a las sesiones de Netcatty. MCP expone el servidor integrado, mientras que Habilidades + CLI apunta a los agentes hacia las habilidades locales de Netcatty y los comandos de CLI.',
'ai.toolAccess.mode.mcp':'MCP',
'ai.toolAccess.mode.skills':'Habilidades + CLI',
'ai.toolAccess.mcpPrompt.title':'Prompt para tu cliente de IA',
'ai.toolAccess.mcpPrompt.description':'Pega este prompt en tu cliente de IA (Codex, Claude Code, …) y registrará el MCP de Netcatty por ti.',
'ai.toolAccess.mcpPrompt.enableHint':'Activa MCP externo abajo para incluir la ruta del launcher en este prompt.',
'ai.toolAccess.skills.file':'Archivo de skill',
'ai.toolAccess.skills.description':'En modo Habilidades + CLI, los agentes apuntan automáticamente a este archivo de skill local. La ruta del lanzador de la CLI de Netcatty se entrega al agente en cada sesión.',
'ai.toolAccess.skills.unavailable':'Ruta del archivo de skill no disponible',
// External MCP (productized catalog MCP for Codex / Claude Code / Cursor)
'ai.externalMcp.title':'MCP externo',
'ai.externalMcp.description':'Expón Netcatty como un servidor MCP para clientes externos como Codex, Claude Code, Cursor y Grok. Usa las mismas herramientas del catálogo que los agentes de la aplicación (terminal, SFTP, Vault, reenvío de puertos). Mantén Netcatty en ejecución mientras los clientes estén conectados.',
'ai.externalMcp.sessionsExposed':'Sesiones en alcance: {count}',
'ai.externalMcp.mode.description':'El modo temporal se desactiva automáticamente tras el tiempo de inactividad. El modo Siempre activo restaura el MCP externo cuando se inicia Netcatty.',
'ai.externalMcp.idleTimeout':'Tiempo de inactividad',
'ai.externalMcp.idleTimeout.description':'En modo temporal, desactiva el MCP externo después de esta cantidad de minutos sin operaciones de MCP.',
'ai.externalMcp.idleTimeout.minutes':'min',
'ai.externalMcp.focusOnHostOpen':'Enfocar ventana en host_open',
'ai.externalMcp.focusOnHostOpen.description':'Cuando un cliente de MCP abre un host, trae la ventana principal al frente. Apágalo para seguir trabajando sin interrupciones.',
'ai.externalMcp.silentSessions.description':'Las sesiones abiertas por la IA se mantienen fuera de tu barra de pestañas y no se restauran tras reiniciar. Puedes verlas en cualquier momento desde el panel de la bandeja.',
'ai.externalMcp.sessionIdleTimeout':'Tiempo de inactividad de la sesión abierta',
'ai.externalMcp.sessionIdleTimeout.description':'Cierra automáticamente las sesiones abiertas por una IA después de esta cantidad de minutos sin actividad de terminal o archivos.',
'ai.externalMcp.usage.title':'Cómo usarlo',
'ai.externalMcp.usage.keepRunning':'1. Activa el MCP externo y mantén Netcatty en ejecución.',
'ai.externalMcp.usage.localhost':'2. Los clientes se conectan mediante el launcher local (solo 127.0.0.1). El descubrimiento se elimina cuando desactivas el interruptor.',
'ai.externalMcp.usage.permissions':'3. Las operaciones de escritura siguen Configuración → IA → Seguridad (observador / confirmar / automático) y la lista de bloqueo de comandos.',
'ai.externalMcp.usage.capabilities':'4. Todas las herramientas del catálogo están disponibles: terminal, SFTP, Vault y reenvío de puertos. Los secretos (contraseñas / claves privadas) nunca se devuelven.',
'ai.externalMcp.help.ariaLabel':'Ayuda del MCP externo',
'ai.externalMcp.security':'Seguridad',
'ai.externalMcp.security.description':'Escucha en 127.0.0.1 con un token rotativo, reutiliza el modo de permisos de IA para las escrituras y elimina el descubrimiento al desactivarse. Sin OAuth: este es un puente local de escritorio.',
'ai.externalMcp.permissionMode':'Modo de permisos actual: {mode}',
'ai.externalMcp.permissionMode.label':'Modo de permiso de escritura',
'ai.externalMcp.permissionMode.hint':'La misma configuración que Configuración → IA → Seguridad. Automático ejecuta las herramientas de escritura de Netcatty sin solicitar aprobación de Netcatty; Confirmar pregunta cada vez. Los clientes externos (Codex / Claude / Grok) aún pueden mostrar su propia interfaz de aprobación de herramientas.',
'ai.externalMcp.bridgeUnavailable':'Puente de MCP externo no disponible',
'ai.externalMcp.copy':'Copiar',
'ai.externalMcp.copied':'Copiado',
'ai.externalMcp.copyFailed':'No se pudo copiar. Intenta copiarlo manualmente.',
'ai.externalMcp.refresh':'Actualizar',
'ai.externalMcp.clientConfiguration':'Configuración del cliente',
'ai.externalMcp.clientConfiguration.description':'Elige un cliente para instalarlo con un clic, o copia fragmentos de CLI o configuración.',
'ai.externalMcp.client.codex':'Codex',
'ai.externalMcp.client.claude':'Claude Code',
'ai.externalMcp.client.grok':'Grok',
'ai.externalMcp.client.cursor':'Cursor',
'ai.externalMcp.cliCommand':'Comando CLI',
'ai.externalMcp.configSnippet':'Fragmento de configuración',
'ai.externalMcp.addToCodex':'Agregar a Codex',
'ai.externalMcp.addToClaude':'Agregar a Claude Code',
'ai.externalMcp.addToGrok':'Agregar a Grok',
'ai.externalMcp.codexAdded':'Entrada de MCP de Codex agregada. Reinicia Codex o abre una nueva sesión de Codex.',
'ai.externalMcp.claudeAdded':'Entrada de MCP de Claude Code agregada. Reinicia Claude Code o abre una nueva sesión de Claude Code.',
'ai.externalMcp.grokAdded':'Entrada de MCP de Grok agregada. Reinicia Grok o abre una nueva sesión de Grok.',
'ai.externalMcp.installCodex':'Instala Codex por separado y luego haz clic en Actualizar.',
'ai.externalMcp.installClaude':'Instala Claude Code por separado y luego haz clic en Actualizar.',
'ai.externalMcp.installGrok':'Instala la CLI de Grok por separado y luego haz clic en Actualizar.',
'ai.externalMcp.conflict.description':'Ya existe una entrada netcatty-external que apunta a otro lugar. Quítala o edítala manualmente.',
'ai.externalMcp.enableForLauncher':'Habilita el MCP externo para obtener una ruta de launcher utilizable.',
'ai.externalMcp.cursor.title':'Cursor / otros clientes',
'ai.externalMcp.cursor.description':'Combina esto en tu configuración de MCP (por ejemplo, ~/.cursor/mcp.json). No reemplaces todo el archivo si ya tienes otros servidores.',
'ai.externalMcp.status.codexNotFound':'Codex no encontrado',
'ai.externalMcp.status.claudeNotFound':'Claude Code no encontrado',
'ai.externalMcp.status.grokNotFound':'Grok no encontrado',
'ai.externalMcp.status.conflict':'Conflicto',
'ai.userSkills.title':'Habilidades de usuario',
'ai.userSkills.description':'Abre la carpeta de habilidades de Netcatty para agregar tus propios directorios de habilidades. Netcatty escanea estas habilidades automáticamente e inyecta solo índices ligeros, salvo que una habilidad coincida claramente con la petición actual.',
'ai.userSkills.openFolder':'Abrir carpeta de habilidades',
'ai.userSkills.reload':'Recargar habilidades',
'ai.userSkills.location':'Ubicación',
'ai.userSkills.loading':'Escaneando habilidades de usuario...',
'ai.userSkills.empty':'Aún no se encontraron habilidades de usuario. Abre la carpeta para agregar directorios de habilidades con un archivo SKILL.md.',
'ai.userSkills.unavailable':'Las habilidades de usuario no están disponibles en este entorno.',
'ai.userSkills.status.ready':'Lista',
'ai.userSkills.status.warning':'Advertencia',
// AI Quick Messages
'ai.quickMessages.title':'Mensajes rápidos',
'ai.quickMessages.description':'Crea prompts reutilizables que puedes insertar desde el chat de IA con / o con el botón de mensajes rápidos. A diferencia de las habilidades de usuario, los mensajes rápidos llenan el compositor con texto.',
'ai.chat.mentionNoteUnavailable':'Este agente no puede leer notas de la bóveda en el modo de conexión actual.',
'ai.chat.mentionNoteTooMany':'No se pueden referenciar todas estas notas juntas. Selecciona menos notas.',
'ai.chat.mentionNoteInvalid':'No se pudo adjuntar «{{title}}»: la nota tiene un identificador no válido.',
'ai.chat.untitledNote':'Nota sin título',
'ai.chat.menuUserSkills':'Habilidades de usuario',
'ai.chat.menuSlashCommands':'Comandos de barra',
'ai.chat.slashCommands':'Comandos de barra',
'ai.chat.slashSystemCommands':'Comandos',
'ai.chat.slashQuickMessages':'Mensajes rápidos',
'ai.chat.slashUserSkills':'Habilidades de usuario',
'ai.chat.quickMessages':'Comandos de barra',
'ai.chat.slashNoResults':'No hay comandos que coincidan',
'ai.chat.slashEmptyHint':'Agrega prompts en Configuración → IA → Mensajes rápidos.',
// AI Chat Shortcuts
'ai.chatShortcuts.title':'Atajos de chat',
'ai.chatShortcuts.selectionAction':'Mostrar "Agregar a la conversación" al seleccionar texto de la terminal',
'ai.chatShortcuts.selectionAction.description':'Muestra un pequeño botón de IA junto al texto seleccionado de la terminal.',
// AI Error
'ai.codex.bridgeError':'Los controladores del proceso principal de Codex aún no están cargados. Reinicia Netcatty por completo o reinicia el proceso de desarrollo de Electron y vuelve a intentarlo.',
// AI Web Search
'ai.webSearch.title':'Búsqueda web',
'ai.webSearch.enable':'Habilitar búsqueda web',
'ai.webSearch.enable.description':'Permite que el agente de IA busque en la web información actual.',
'ai.webSearch.provider':'Proveedor de búsqueda',
'ai.webSearch.provider.description':'Elige un proveedor de API de búsqueda web.',
'ai.webSearch.apiKey':'Clave de API',
'ai.webSearch.apiKey.description':'Clave de API para el proveedor de búsqueda seleccionado.',
'ai.webSearch.apiKey.placeholder':'Ingresa la clave de API...',
'ai.webSearch.apiHost':'Host de API',
'ai.webSearch.apiHost.description':'Endpoint de API personalizado. Deja el predeterminado salvo que uses un proxy.',
'ai.webSearch.apiHost.searxngDescription':'URL de tu instancia de SearXNG (obligatoria).',
'ai.webSearch.maxResults':'Máximo de resultados',
'ai.webSearch.maxResults.description':'Cantidad máxima de resultados de búsqueda a devolver (1-20).',
// AI Safety Settings
'ai.safety.title':'Seguridad',
'ai.safety.permissionMode':'Modo de permisos',
'ai.safety.permissionMode.description':'Controla cómo interactúa la IA con tus sesiones de terminal de Netcatty. El modo Observador bloquea las operaciones de escritura que pasan por Netcatty. Las CLIs de agentes externos pueden seguir teniendo sus propias herramientas locales y flujo de aprobación.',
'ai.safety.permissionMode.observer':'Observador: solo lectura, sin acciones',
'ai.safety.permissionMode.confirm':'Confirmar: preguntar antes de las acciones',
'ai.safety.commandTimeout':'Tiempo de espera de comandos',
'ai.safety.commandTimeout.description':'Máximo de segundos que un comando puede ejecutarse antes de ser terminado mediante la ejecución de Netcatty.',
'ai.safety.commandTimeout.unit':'seg',
'ai.safety.responseIdleTimeout':'Espera de respuesta de la IA integrada',
'ai.safety.responseIdleTimeout.description':'Cancela una solicitud de la IA integrada tras este número de segundos sin una nueva respuesta. No controla la duración total ni la ejecución de comandos.',
'ai.safety.responseIdleTimeout.unit':'seg',
'ai.safety.maxIterations':'Máximo de iteraciones',
'ai.safety.maxIterations.description':'Máximo de bucles de uso de herramientas de IA para evitar una ejecución descontrolada. Los agentes externos pueden tener sus propios límites de iteración internos que tienen prioridad.',
'ai.safety.blocklist':'Lista de bloqueo de comandos',
'ai.safety.blocklist.description':'Patrones regex para bloquear comandos peligrosos ejecutados mediante Netcatty.',
'ai.safety.blocklist.reset':'Restablecer a los valores predeterminados',
'ai.safety.blocklist.add':'Agregar patrón',
'ai.safety.grants.title':'Memoria de permisos',
'ai.safety.grants.heading':'Reglas de permitir en modo Confirmar',
'ai.safety.grants.description':'El modo Confirmar pregunta antes de ejecutar una operación. Las reglas guardadas permiten automáticamente las operaciones que coinciden en todas las sesiones/nodos de terminal, y se pueden editar manualmente.',
'ai.safety.grants.empty':'Aún no hay reglas guardadas. Aprueba una herramienta con "Permitir siempre" o agrega una manualmente.',
'ai.safety.grants.capability':'Capacidad',
'ai.safety.grants.sessionPattern':'Patrón de sesión',
'ai.safety.grants.commandPattern':'Patrón de comando (opcional)',
'ai.safety.grants.note':'Nota (opcional)',
'ai.safety.grants.add':'Agregar regla',
'ai.safety.grants.remove':'Quitar',
'ai.safety.grants.export':'Exportar JSON',
'ai.safety.grants.import':'Importar JSON',
'ai.safety.note':'Estos ajustes de seguridad se aplican a las acciones que pasan por Netcatty. Las CLIs de agentes externos también pueden exponer herramientas locales regidas por el propio agente.',
// Unified tooltips for terminal workspace and top tabs (issue #954)
'terminal.layer.addTerminal':'Agregar terminal',
'terminal.layer.switchToSplitView':'Cambiar a vista dividida',
'terminal.layer.sftp':'SFTP',
'terminal.layer.scripts':'Scripts',
'terminal.layer.history':'Historial',
'terminal.layer.theme':'Tema',
'terminal.layer.notes':'Notas',
'terminal.layer.aiChat':'Chat de IA',
'terminal.layer.movePanelLeft':'Mover panel a la izquierda',
'terminal.layer.movePanelRight':'Mover panel a la derecha',
'scripts.meta.descriptionPlaceholder':'Notas opcionales sobre este script',
'scripts.meta.trigger':'Disparador',
'scripts.meta.triggerPattern':'Patrón de salida (regex)',
'scripts.meta.code':'Script',
'scripts.trigger.manual':'Ejecución manual',
'scripts.trigger.onConnect':'Ejecutar al conectar',
'scripts.trigger.onOutput':'Ejecutar al coincidir con la salida',
'scripts.trigger.onOutputHint':'Se dispara cuando la salida del servidor coincide con el patrón (el eco de las teclas del usuario se ignora). Sin hosts de destino configurados, escucha en la sesión conectada actual; con hosts de destino, solo en esos hosts. Deshabilitado en aplicaciones de pantalla alterna como vim o htop. Se suprime mientras otro script se ejecuta en esta sesión; se vuelve a revisar cuando termina. Para esperas dentro del flujo, usa nct.screen.waitForText o nct.screen.waitForRegex en el código del script.',
'scripts.actions.save':'Guardar',
'scripts.actions.runNow':'Ejecutar ahora',
'scripts.actions.openEditor':'Abrir editor',
'scripts.actions.openEditorHint':'Editar el script en una ventana más grande',
'scripts.editor.modalTitle':'Editor de scripts',
'scripts.editor.modalSubtitle':'Edita los metadatos y el código del script en un espacio de trabajo más amplio.',
'scripts.editor.lineCount':'{count} líneas',
'scripts.editor.resize':'Redimensionar el editor',
'scripts.targets.hint':'Los grupos seleccionados se resuelven dinámicamente, por lo que los hosts agregados después se incluyen automáticamente.',
'scripts.targets.connectOrderHint':'El orden de ejecución de los scripts de conexión se configura por host en Detalles del host → Automatización.',
'scripts.targets.currentHostMismatch':'Este script no está asignado al host actual.',
'hostDetails.automation.groupScripts':'Scripts de grupo heredados',
'hostDetails.automation.groupScriptsHint':'Estos scripts siguen al grupo del host dinámicamente y se ordenan antes de la cola específica del host.',
'scripts.actions.runNowHint':'Ejecutar en los destinos seleccionados, o en todos los hosts conectables cuando esa opción está habilitada.',
'scripts.actions.runParallel':'Ejecutar en todas las pestañas (en paralelo)',
'scripts.actions.runSequential':'Ejecutar en todas las pestañas (en secuencia)',
'scripts.actions.runOnAllTabs':'Ejecutar en todas las pestañas',
'scripts.actions.skippedConnectingSessions':'{count} pestaña(s) aún conectándose y fueron omitidas',
'scripts.actions.skippedSensitiveSessions':'{count} pestaña(s) omitidas (entrada de contraseña/datos sensibles)',
'scripts.actions.noRunnableHosts':'Ningún host conectable coincide con los destinos de este script',
'scripts.sidePanel.library':'Biblioteca',
'scripts.sidePanel.running':'En ejecución',
'scripts.sidePanel.newScript':'Nuevo script',
'scripts.running.empty':'No hay scripts en ejecución en esta sesión.',
'scripts.running.waitingForShellPrompt':'indicador del shell (# o $)',
'scripts.running.lastSent':'Enviado: {command}',
'scripts.recording.start':'Comenzar grabación',
'scripts.recording.active':'Detener grabación',
'scripts.recording.startHint':'Graba las acciones en la terminal enfocada y genera código de script nct',
'scripts.recording.unavailableHint':'La grabación no está disponible aquí — usa la barra lateral de scripts a la derecha',
'scripts.recording.activeHint':'Grabando esta terminal. Escribe comandos como de costumbre; haz clic en Detener o usa el control REC en la barra de herramientas para finalizar y guardar.',
'scripts.recording.started':'Grabación iniciada — opera en la terminal',
'scripts.recording.noSession':'Conecta una terminal primero (pestaña única o espacio de trabajo)',
'scripts.recording.alreadyActive':'Otra terminal ya está grabando',
'scripts.recording.namePlaceholder':'Nombre del script',
'scripts.recording.packagePlaceholder':'Guardar en carpeta',
'scripts.recording.rootPackage':'Raíz',
'scripts.recording.save':'Guardar',
'scripts.recording.saveAndEdit':'Guardar y editar',
'scripts.recording.helpTitle':'Cómo grabar un script',
'scripts.recording.helpIntro':'La grabación convierte lo que haces en la terminal en un script de automatización reutilizable — útil para despliegues, verificaciones de salud y otras tareas repetitivas.',
'scripts.recording.helpStep1':'Conecta a un host. Una pestaña de terminal independiente o una terminal dentro de un espacio de trabajo funcionan igual — abre la barra lateral de scripts a la derecha.',
'scripts.recording.helpStep2':'Haz clic en Comenzar grabación. Una insignia REC roja en la barra de herramientas de la terminal indica que la grabación está activa.',
'scripts.recording.helpStep3':'Escribe comandos en la terminal como lo harías normalmente. Cada pulsación de Enter se captura como un paso.',
'scripts.recording.helpStep4':'Cuando termines, haz clic en Detener grabación, o usa el control de detención junto a REC en la barra de herramientas de la terminal.',
'scripts.recording.helpStep5':'Nombra el script y guárdalo. Elige Guardar y editar si quieres ajustar el código generado en el editor de scripts.',
'scripts.recording.helpTipsTitle':'Consejos',
'scripts.recording.helpTip1':'Las pausas de más de 1 segundo entre acciones se registran como tiempo de espera para que la reproducción no sea demasiado rápida.',
'scripts.recording.helpTip2':'Después de cada comando, la grabación espera un indicador del shell (como $ o #) antes del siguiente paso.',
'scripts.recording.helpTip3':'La entrada de contraseñas se marca como sensible y no se almacena en texto plano en el script.',
'scripts.recording.helpTip4':'Para vim, menús u otros flujos interactivos, graba primero los comandos principales y luego refina el script manualmente.',
'scripts.dialog.title':'Script',
'scripts.dialog.ok':'Aceptar',
'scripts.dialog.required':'Obligatorio',
'scripts.dialog.numberInvalid':'Ingresa un número válido',
'scripts.dialog.numberMin':'Debe ser al menos {min}',
'scripts.dialog.numberMax':'Debe ser como máximo {max}',
'scripts.dialog.numberStep':'Debe usar incrementos de {step}',
'scripts.dialog.waitForTimeoutTitle':'La espera agotó el tiempo',
'hostDetails.automation.equivalenceHint':'Los vínculos agregan este host a la lista de destinos del script en Scripts. Ambas vistas se mantienen sincronizadas.',
'hostDetails.automation.queueHint':'Los scripts se ejecutan en orden cuando este host se conecta. Los scripts globales se ejecutan primero, luego esta cola.',
'hostDetails.automation.globalScripts':'Scripts de conexión globales',
'hostDetails.automation.globalScriptsHint':'Se ejecutan primero en cada conexión. Reordena según el orden de clasificación de la biblioteca de Scripts.',
'hostDetails.automation.connectQueue':'Cola de ejecución de este host',
'hostDetails.automation.connectQueueEmpty':'Aún no hay scripts de conexión en cola para este host.',
'hostDetails.automation.addToQueuePlaceholder':'Agregar script a la cola…',
'hostDetails.automation.moveUp':'Subir',
'hostDetails.automation.moveDown':'Bajar',
'hostDetails.automation.removeFromQueue':'Quitar de la cola',
'hostDetails.automation.dragHandle':'Arrastra para reordenar',
'hostDetails.automation.queueDragHint':'Arrastra los elementos para reordenar la cola de conexión.',
'systemManager.popup.startupFailed':'El comando de inicio no se completó correctamente. Verifica que el destino siga disponible e inténtalo de nuevo.',
'systemManager.errors.loadProcesses':'No se pudieron cargar los procesos',
'systemManager.errors.loadTmux':'No se pudieron cargar las sesiones de tmux',
'systemManager.errors.loadTmuxWindows':'No se pudieron cargar las ventanas de tmux',
'systemManager.errors.loadTmuxPanes':'No se pudieron cargar los paneles de tmux',
'systemManager.errors.loadTmuxClients':'No se pudieron cargar los clientes de tmux',
'systemManager.errors.loadDocker':'No se pudieron cargar los contenedores',
'systemManager.errors.loadDockerStats':'No se pudieron cargar las estadísticas de los contenedores',
'systemManager.errors.loadDockerImages':'No se pudieron cargar las imágenes',
'systemManager.errors.loadOverview':'No se pudo cargar el resumen del sistema',
'systemManager.errors.loadGpu':'No se pudieron cargar las estadísticas de GPU / NPU',
'systemManager.errors.loadPorts':'No se pudieron cargar los puertos en escucha',
'systemManager.errors.loadServices':'No se pudieron cargar los servicios de systemd',
'systemManager.errors.sshChannelUnavailable':'El servidor se negó a abrir un nuevo canal de ejecución. Inténtalo de nuevo más tarde o vuelve a conectar este host.',
'systemManager.overview.empty':'Aún no hay datos de resumen del sistema.',
'systemManager.overview.loading':'Cargando resumen del sistema…',
'terminal.passwordPicker.empty':'No hay contraseñas guardadas',
// Network Device Mode auto-detection tip (session header)
'terminal.networkDevice.tip.message':'Esto parece un dispositivo de red. Activa el Modo de Dispositivo de Red para enviar comandos tal cual (sin envoltorio de shell).',
'terminal.networkDevice.tip.action':'Activar',
'terminal.networkDevice.tip.dismiss':'Descartar',
'terminal.networkDevice.tip.enabled':'Modo de Dispositivo de Red activado para {host}',
// Terminal toolbar / search / context menu / auth
'terminal.toolbar.openSftp':'Abrir SFTP',
'terminal.toolbar.availableAfterConnect':'Disponible después de conectar',
'terminal.toolbar.sendYmodem':'Enviar con YMODEM',
'terminal.toolbar.receiveYmodem':'Recibir con YMODEM',
'terminal.toolbar.sftp':'SFTP',
'terminal.toolbar.more':'Más acciones',
'terminal.toolbar.scripts':'Scripts',
'terminal.toolbar.history':'Historial de comandos',
'terminal.toolbar.configureOsc7':'Configurar seguimiento de directorio',
'history.scope.label':'Alcance del historial',
'history.tab.host':'Host',
'history.tab.global':'Global',
'history.searchPlaceholder':'Buscar en el historial...',
'history.loading':'Cargando historial remoto...',
'history.meta.count':'{count} comandos',
'history.empty.noSession':'Abre una sesión remota para ver su historial de comandos.',
'history.empty.unsupportedProtocol':'El historial de comandos solo está disponible para sesiones SSH/Mosh/ET.',
'history.empty.noHistory':'No se encontró historial de comandos en este host.',
'history.empty.noGlobalHistory':'Aún no hay historial global de comandos. Los comandos que ejecutes aparecerán aquí.',
'history.action.refresh':'Actualizar',
'history.action.retry':'Reintentar',
'history.action.paste':'Pegar en la terminal',
'history.action.run':'Ejecutar en la terminal',
'history.action.saveAsSnippet':'Guardar como snippet',
'history.action.delete':'Eliminar del historial',
'terminal.toolbar.library':'Biblioteca',
'terminal.toolbar.noSnippets':'No hay snippets disponibles',
'terminal.toolbar.terminalSettings':'Configuración de la terminal',
'terminal.toolbar.searchTerminal':'Buscar en la terminal',
'terminal.toolbar.search':'Buscar',
'terminal.toolbar.startSessionLog':'Iniciar registro de sesión',
'terminal.toolbar.stopSessionLog':'Detener registro de sesión',
'terminal.toolbar.timestampsEnable':'Mostrar marcas de tiempo',
'terminal.toolbar.timestampsDisable':'Ocultar marcas de tiempo',
'terminal.toolbar.broadcast':'Difundir',
'terminal.toolbar.broadcastEnable':'Activar Modo de Difusión',
'terminal.toolbar.broadcastDisable':'Desactivar Modo de Difusión',
'terminal.toolbar.composeBar':'Barra de redacción',
'terminal.composeBar.placeholder':'Escribe el comando aquí y presiona Enter para enviar...',
'terminal.composeBar.send':'Enviar',
'terminal.composeBar.close':'Cerrar barra de redacción',
'terminal.composeBar.broadcasting':'Difundiendo a todas las sesiones',
'terminal.composeBar.resize':'Cambiar la altura de la barra de redacción',
'terminal.serverStats.network':'Velocidad de red',
'terminal.serverStats.latency':'Latencia de red SSH',
'terminal.serverStats.networkDetails':'Interfaces de red',
'terminal.serverStats.noData':'No hay datos disponibles',
'terminal.dragDrop.localTitle':'Soltar para insertar rutas',
'terminal.dragDrop.localMessage':'Las rutas de los archivos se insertarán en la terminal',
'terminal.dragDrop.remoteTitle':'Soltar para subir archivos',
'terminal.dragDrop.remoteZmodemMessage':'Los archivos se subirán mediante ZMODEM (PTY)',
'terminal.dragDrop.remoteSftpMessage':'Los archivos se subirán mediante SFTP',
'terminal.dragDrop.noFiles':'No hay archivos para subir',
'terminal.dragDrop.notConnected':'No se pueden soltar archivos: la terminal no está conectada',
'terminal.dragDrop.errorTitle':'Error al soltar',
'terminal.dragDrop.errorMessage':'No se pudieron procesar los archivos soltados',
'terminal.dragDrop.destinationUnknown':'No se pudo determinar la carpeta actual del terminal. Active el seguimiento de directorios o abra SFTP y elija primero una carpeta de carga.',
'terminal.dragDrop.uploadCancelled':'La carga se canceló porque la conexión del terminal cambió o no se pudo reutilizar. Vuelva a soltar los archivos después de reconectar.',
'terminal.dragDrop.needsSudoElevation':'Esta carpeta no es escribible con el usuario de inicio de sesión. Active la elevación Sudo en la configuración del host, o vuelva al directorio de usuario y suelte de nuevo.',
'terminal.clipboardImageUpload.noImage':'El portapapeles no contiene una imagen',
'terminal.clipboardImageUpload.failed':'No se pudo subir la imagen del portapapeles',
'terminal.osc7Setup.title':'Configurar seguimiento de directorio',
'terminal.osc7Setup.desc':'Netcatty agregará hooks de prompt OSC 7 para el usuario remoto actual. Esto ayuda a que SFTP siga el directorio de la terminal después de sudo o su.',
'terminal.osc7Setup.targets':'Posibles archivos a actualizar',
'terminal.osc7Setup.command':'Comando a ejecutar',
'terminal.ymodem.receiveEmpty':'No se recibieron archivos YMODEM',
'terminal.ymodem.receiveFailed':'Falló la recepción con YMODEM',
'terminal.ymodem.unavailable':'YMODEM no está disponible',
'terminal.selection.addToAI':'Agregar a la conversación',
'terminal.selection.addToAIDesc':'Adjuntar la salida seleccionada de la terminal al borrador de IA',
'terminal.auth.password':'Contraseña',
'terminal.auth.sshKey':'Clave SSH',
'terminal.auth.username':'Nombre de usuario',
'terminal.auth.username.placeholder':'root',
'terminal.auth.passwordLabel':'Contraseña',
'terminal.auth.password.placeholder':'Ingresa la contraseña',
'terminal.auth.passphrase':'Frase de contraseña',
'terminal.auth.passphrase.placeholder':'Frase de contraseña opcional para la clave privada seleccionada',
'terminal.auth.certificate':'Certificado',
'terminal.auth.selectKey':'Seleccionar clave',
'terminal.auth.retryMessage':'Falló la autenticación. Verifica tus credenciales e inténtalo de nuevo.',
'terminal.auth.retryLog':'Falló la autenticación. Inténtalo de nuevo.',
'terminal.auth.noKeysHint':'No hay claves disponibles. Agrega claves en Keychain.',
'terminal.auth.continueSave':'Continuar y guardar',
'terminal.auth.credentialsUnavailable':'Las credenciales guardadas no se pueden descifrar en este dispositivo. Vuelve a ingresarlas y guárdalas de nuevo.',
'terminal.auth.jumpCredentialsUnavailable':'Un host de salto tiene credenciales guardadas que no se pueden descifrar en este dispositivo. Abre la configuración del host y vuelve a ingresarlas.',
'terminal.auth.proxyCredentialsUnavailable':'Las credenciales del proxy no se pueden descifrar en este dispositivo. Abre la configuración del host y vuelve a ingresar la contraseña del proxy.',
'terminal.auth.keyUnavailableFallbackPassword':'La clave SSH guardada no está disponible en este dispositivo. Se usará autenticación por contraseña como respaldo.',
'terminal.progress.timeoutIn':'Tiempo de espera en {seconds}s',
'terminal.progress.waitingForUserInput':'Esperando entrada del usuario',
'terminal.progress.disconnected':'Desconectado',
'terminal.progress.cancelling':'Cancelando...',
'terminal.progress.startOver':'Empezar de nuevo',
'terminal.progress.enterReconnectHint':'Presiona Enter para reconectar',
'cloudSync.gate.title':'Sincronización cifrada de extremo a extremo',
'cloudSync.gate.desc':
'Tus datos se cifran localmente antes de sincronizarse. Los proveedores de la nube nunca ven tus datos en texto plano. Configura una clave maestra para habilitar la sincronización segura.',
'cloudSync.strategy.smartMergeDesc':'Combina los cambios de ambos lados cuando sea posible; si Netcatty no puede decidir con seguridad, te pedirá que elijas.',
'cloudSync.strategy.preferCloud':'Gana la nube',
'cloudSync.strategy.preferCloudDesc':'Cuando ambos lados cambiaron, descarga la versión de la nube y reemplaza los cambios locales.',
'cloudSync.strategy.preferLocal':'Gana lo local',
'cloudSync.strategy.preferLocalDesc':'Cuando ambos lados cambiaron, sube la versión local y reemplaza los cambios de la nube.',
'cloudSync.convergent.title':'Sincronización convergente de múltiples dispositivos',
'cloudSync.convergent.desc':'Usa una réplica CRDT cifrada para preservar ediciones sin conexión, eliminaciones concurrentes y cambios de cada proveedor conectado.',
'cloudSync.convergent.active':'CRDT v2 está activo. Las escrituras del proveedor se verifican después de la subida.',
'cloudSync.convergent.paused':'CRDT v2 está en pausa en este dispositivo; los metadatos de la nube se conservan.',
'cloudSync.convergent.downgrade.confirm':'¿Degradar cada proveedor conectado a la sincronización heredada? Esto elimina los metadatos CRDT después de la verificación de escritura.',
'cloudSync.history.title':'Historial de sincronización',
'cloudSync.history.upload':'Subida',
'cloudSync.history.download':'Descarga',
'cloudSync.history.resolved':'Resuelto',
'cloudSync.history.error':'Error',
'cloudSync.localBackups.title':'Historial de copias de seguridad locales',
'cloudSync.localBackups.desc':'Netcatty conserva puntos de restauración locales antes de los cambios de versión de la app y antes de las restauraciones de la bóveda.',
'cloudSync.localBackups.retentionTitle':'Retención de copias de seguridad',
'cloudSync.localBackups.retentionDesc':'Elige cuántas copias de seguridad locales debe conservar Netcatty.',
'cloudSync.localBackups.restoreSuccess':'Copia de seguridad local restaurada.',
'cloudSync.localBackups.restoreFailedTitle':'Falló la restauración',
'cloudSync.localBackups.restoreMissing':'No se encontró la copia de seguridad.',
'cloudSync.localBackups.protectiveBackupFailed':'No se pudo crear la copia de seguridad de protección, por lo que la restauración se canceló para proteger tus datos actuales. Resuelve el problema subyacente (p. ej., acceso al keychain) e inténtalo de nuevo. Detalles: {message}',
'cloudSync.localBackups.restoreConfirmTitle':'¿Restaurar esta copia de seguridad?',
'cloudSync.localBackups.restoreConfirmDesc':'Tus hosts, claves, snippets y configuraciones actuales se reemplazarán con el contenido de esta copia de seguridad. Se crea automáticamente una instantánea de protección de tus datos actuales primero.',
'cloudSync.localBackups.unavailableTitle':'Copias de seguridad locales no disponibles',
'cloudSync.localBackups.unavailableDesc':'Esta plataforma no expone un keychain seguro a Netcatty, por lo que las copias de seguridad locales no se pueden escribir de forma segura. Instala Netcatty en un sistema con un keychain compatible para habilitar el historial de copias de seguridad locales.',
'cloudSync.localBackups.lockedDesc':'Configura o desbloquea tu clave maestra antes de restaurar una copia de seguridad, para que las credenciales restauradas permanezcan cifradas.',
'cloudSync.unlock.title':'Ingresa la clave maestra',
'cloudSync.unlock.masterKey':'Clave maestra',
'cloudSync.unlock.desc':
'Ingresa tu clave maestra una vez para habilitar la sincronización cifrada. Se almacenará de forma segura usando el keychain de tu sistema operativo.',
'cloudSync.unlock.placeholder':'Ingresa tu clave maestra',
'cloudSync.unlock.empty':'Ingresa tu clave maestra',
'cloudSync.resolve.failedTitle':'Falló la resolución del conflicto',
'cloudSync.clearLocal.title':'Borrar datos locales',
'cloudSync.clearLocal.desc':'Restablece la versión local y el historial de sincronización. La próxima sincronización descargará desde la nube.',
'cloudSync.clearLocal.button':'Borrar',
'cloudSync.clearLocal.dialog.title':'¿Borrar los datos locales de la bóveda?',
'cloudSync.clearLocal.dialog.desc':'Esto restablecerá la versión local a 0 y borrará el historial de sincronización. Tu próxima sincronización descargará los datos de la nube, reemplazando los datos locales.',
'cloudSync.clearLocal.dialog.cancel':'Cancelar',
'cloudSync.clearLocal.dialog.confirm':'Borrar datos locales',
'snippets.search.noResults.desc':'Ningún script o paquete de scripts coincide con "{query}". Prueba con otro término de búsqueda o limpia la búsqueda para explorar.',
'snippets.section.packages':'Paquetes de scripts',
'snippets.section.snippets':'Scripts',
'snippets.kind.codeSnippet':'Snippet de código',
'snippets.kind.automationScript':'Script de automatización',
'snippets.import.modal.desc':'Elige uno o más archivos JSON de snippets de Netcatty. Netcatty los mostrará en vista previa antes de importar cualquier cosa.',
'snippets.import.modal.exampleTitle':'Ejemplo de JSON',
'snippets.import.modal.noFile':'Aún no se seleccionó ningún archivo. Usa el formato de ejemplo de abajo, o un arreglo JSON simple de snippets, y luego elige uno o más archivos.',
'snippets.import.modal.parsedSummary':'{files} archivo(s), {total} script(s), {packages} paquete(s) de scripts, {conflicts} comando(s) duplicado(s). Las vinculaciones de host se ignoran.',
'serial.field.localEchoDesc':'Hacer eco de los caracteres escritos localmente (para dispositivos sin eco remoto)',
'serial.field.lineMode':'Modo de línea',
'serial.field.lineModeDesc':'Almacenar en búfer la entrada y enviarla al presionar Enter (en lugar de carácter por carácter)',
'serial.field.backspaceBehavior':'Tecla de retroceso',
'serial.field.backspaceBehaviorDesc':'Usa Ctrl+H para dispositivos de red que no responden al código de Retroceso predeterminado.',
'serial.encoding.pendingInput':'Envía o borra la entrada actual antes de cambiar la codificación.',
'serial.field.byteOrientedBackspace':'Retroceso orientado a bytes',
'serial.field.byteOrientedBackspaceDesc':'Envía un DEL por byte para que el dispositivo elimine un carácter multibyte completo (para dispositivos que eliminan por bytes). Desactívelo para editores de línea conscientes de caracteres (p. ej. readline de Linux).',
'ai.providers.style.google':'Совместимый с Google',
'ai.providers.style.inherited':'авто',
'ai.providers.style.help':'Определяет, какой формат API используется для запросов. Переопределите, если стороннее API использует другой диалект.',
'ai.providers.openaiApi':'Формат API OpenAI',
'ai.providers.openaiApi.chat':'Chat Completions',
'ai.providers.openaiApi.responses':'Responses',
'ai.providers.openaiApi.help':'Chat Completions работает с большинством OpenAI-совместимых эндпоинтов. Responses может повысить попадание в кэш на ретрансляторах с поддержкой /v1/responses.',
'ai.providers.baseUrl.anthropicHelp':'Anthropic-совместимый: хост с /v1 или без (например https://gateway.example или https://gateway.example/v1). Проверка и чат используют /v1/models и /v1/messages.',
'ai.providers.contextWindow.help':'Оставьте пустым, чтобы использовать значение из списка моделей, если оно доступно; иначе Netcatty применит безопасное значение по умолчанию.',
'ai.providers.contextWindow.error':'Введите положительное целое число или оставьте поле пустым.',
'ai.toolAccess.description':'Выберите, как внешние агенты получают доступ к сессиям Netcatty. MCP предоставляет встроенный сервер, а Skills + CLI указывает агентам на локальный skill Netcatty и команды CLI.',
'ai.toolAccess.mode.mcp':'MCP',
'ai.toolAccess.mode.skills':'Skills + CLI',
'ai.toolAccess.mcpPrompt.title':'Промпт для вашего ИИ-клиента',
'ai.toolAccess.mcpPrompt.description':'Вставьте этот промпт в ИИ-клиент (Codex, Claude Code и т. д.), и он сам зарегистрирует MCP Netcatty.',
'ai.toolAccess.mcpPrompt.enableHint':'Включите внешний MCP ниже, чтобы в промпт попал путь к лаунчеру.',
'ai.toolAccess.skills.file':'Файл skill',
'ai.toolAccess.skills.description':'В режиме Skills + CLI агенты автоматически получают путь к этому локальному skill-файлу. Путь к лаунчеру Netcatty CLI передаётся агенту в каждой сессии.',
'ai.toolAccess.skills.unavailable':'Путь к skill-файлу недоступен',
'ai.externalMcp.title':'Внешний MCP',
'ai.externalMcp.description':'Откройте Netcatty как MCP-сервер для Codex, Claude Code, Cursor и Grok. Набор инструментов тот же, что у встроенных агентов. Держите Netcatty запущенным, пока клиенты подключены.',
'ai.safety.commandTimeout.description':'Максимальное число секунд, которое команда может выполняться до принудительного завершения. Применяется как к встроенным, так и к внешним агентам.',
'ai.safety.responseIdleTimeout.description':'Отменяет запрос встроенного ИИ после указанного числа секунд без нового ответа. Не управляет общей длительностью ответа и выполнением команд.',
'ai.safety.responseIdleTimeout.unit':'с',
'ai.safety.maxIterations':'Макс. число итераций',
'ai.safety.maxIterations.description':'Максимальное число циклов использования инструментов AI, чтобы предотвратить бесконтрольное выполнение. У внешних агентов могут быть собственные внутренние лимиты итераций, имеющие приоритет.',
'placeholder.workspaceName':'Имя рабочего пространства',
'placeholder.sessionName':'Имя сессии',
'placeholder.searchHosts':'Поиск хостов...',
'placeholder.searchHosts':'Поиск хостов или тегов...',
'toast.settingsUnavailable':'Окно настроек недоступно на этой платформе.',
'credentials.protectionUnavailable.title':'Защита учётных данных недоступна',
'credentials.protectionUnavailable.message':'Сохранённые пароли и ключи не могут быть автоматически расшифрованы на этом устройстве. Перед подключением введите учётные данные заново.',
'settings.system.credentials.unavailableHint':'Учётные данные, зашифрованные в другом профиле пользователя или на другой машине, здесь расшифровать нельзя. Повторно введите и сохраните их на этом устройстве.',
'settings.system.credentials.portabilityHint':'Облачная синхронизация переносима, потому что использует шифрование вашим мастер-ключом. Локальное шифрование safeStorage привязано к устройству и пользователю.',
// Settings > System > App Lock
'settings.appLock.title':'Блокировка приложения',
'settings.appLock.description':'Требовать локальный пароль блокировки при открытии Netcatty, после бездействия или при ручной блокировке.',
'settings.appLock.disableDescription':'Введите текущий пароль блокировки, чтобы Netcatty перестал блокироваться. Сохранённый пароль будет удалён; для повторного включения нужно создать новый.',
'settings.sessionLogs.timestampsDesc':'Добавлять локальное время в начало каждой строки в текстовых и HTML-журналах.',
'settings.sessionLogs.hint':'Журналы сессий сохраняют весь вывод терминала для диагностики и аудита.',
'settings.sessionLogs.clearAll':'Очистить все журналы',
'settings.sessionLogs.clearAllDesc':'Удалить все сохранённые файлы журналов сессий и вложенные папки хостов из папки сохранения.',
'settings.sessionLogs.clearConfirm':'Это приведёт к безвозвратному удалению всех файлов журналов сессий в папке сохранения. Это действие нельзя отменить. Продолжить?',
'settings.vault.autoImportSystemKnownHostsDesc':'При открытии хранилища тихо импортировать записи из OpenSSH known_hosts. Отключите, чтобы держать известные хосты Netcatty отдельно; кнопка «Сканировать систему» по-прежнему импортирует вручную.',
'settings.vault.notesFont':'Шрифт кода',
'settings.vault.notesFontDesc':'Шрифт для блоков кода и встроенного кода в заметках',
'Показывать компактное уведомление внутри уже подключённого терминала или полный диалог. Ошибки первого подключения по-прежнему показываются в диалоге.',
'Показывать системное уведомление, когда программа отправляет OSC 9, OSC 777 notify или kitty OSC 99. Так Codex и другие агенты сообщают о завершении хода и запросе подтверждения.',
'settings.shortcuts.disableTerminalFontZoom.desc':'Отключает быстрый масштаб текста в терминале, включая Cmd/Ctrl + колесо мыши.',
'settings.shortcuts.shellOnlyTabNumberShortcuts.label':'Сочетания без закреплённых вкладок',
'settings.shortcuts.shellOnlyTabNumberShortcuts.desc':'Если включено, Cmd/Ctrl+[1...9] и Ctrl+Tab переключают только рабочие вкладки (терминалы, рабочие области, редакторы), а не закреплённые Vault и SFTP.',
'settings.shortcuts.showTabNumberBadges.label':'Номера на вкладках',
'settings.shortcuts.showTabNumberBadges.desc':'Показывать 1-9 на вкладках при удержании Cmd/Ctrl, чтобы точнее переключаться через Cmd/Ctrl+[1...9].',
'scripts.editor.modalSubtitle':'Редактируйте метаданные и код скрипта в большом рабочем пространстве.',
'scripts.editor.lineCount':'{count} строк',
'scripts.editor.resize':'Изменить размер редактора',
'scripts.targets.hint':'Автоматические триггеры применяются к выбранным хостам или ко всем хостам, если включена эта настройка.',
'scripts.targets.hint':'Выбранные группы разрешаются динамически, поэтому новые хосты включаются автоматически.',
'scripts.targets.connectOrderHint':'Порядок запуска скриптов подключения настраивается для каждого хоста в разделе «Сведения о хосте → Автоматизация».',
'scripts.targets.currentHostMismatch':'Этот скрипт не назначен текущему хосту.',
'terminal.dragDrop.errorMessage':'Не удалось обработать перетащенные файлы',
'terminal.dragDrop.destinationUnknown':'Не удалось определить текущую папку терминала. Включите отслеживание каталогов или сначала откройте SFTP и выберите папку загрузки.',
'terminal.dragDrop.uploadCancelled':'Загрузка отменена: соединение терминала изменилось или его не удалось повторно использовать. После переподключения перетащите файлы снова.',
'terminal.dragDrop.needsSudoElevation':'Эта папка недоступна для записи пользователю входа. Включите повышение прав Sudo в настройках хоста или вернитесь в домашний каталог и перетащите файлы снова.',
'cloudSync.provider.disconnect.confirmMessage':'Это устройство перестанет синхронизироваться с {name}. Локальное хранилище останется на этом компьютере.',
'serial.field.byteOrientedBackspaceDesc':'Отправляет по одному DEL на байт, чтобы устройство удалило весь многобайтовый символ (для устройств, удаляющих побайтово). Отключите для редакторов, удаляющих посимвольно (например, readline в Linux).',
'notes.editor.unrenderableMarkdown':'Эта заметка содержит Markdown, который не может отобразить визуальный редактор (например, незакрытые теги вида <tag>). Показан исходный Markdown — содержимое не потеряно.',
'notes.editor.retryRichView':'Повторить в визуальном режиме',
'sftp.tabs.copyCurrentPath':'Копировать и перейти к текущему пути',
'sftp.conflict.title':'Конфликт файлов',
'sftp.conflict.desc':'В месте назначения уже существует файл с таким именем',
'sftp.conflict.folderTitle':'Конфликт папок',
'sftp.conflict.folderDesc':'Выберите действие.',
'sftp.conflict.folderFileDesc':'В месте назначения уже существует файл с таким именем. Папку нельзя объединить с файлом.',
'sftp.conflict.folderSymlinkDesc':'В месте назначения уже существует символическая ссылка с таким именем. Папку нельзя объединить с символической ссылкой.',
'sftp.conflict.folderUnknownDesc':'В месте назначения уже существует элемент с таким именем, но его тип не удалось определить. Замена недоступна для защиты данных в месте назначения.',
'sftp.conflict.folderMergeHint':'Объединить: сохранить содержимое, которое есть только в папке назначения.',
'sftp.conflict.folderReplaceWarning':'Заменить: удалить содержимое, которое есть только в папке назначения. Это нельзя отменить.',
'settings.sftp.doubleClickBehavior.transferDesc':'Передать файл на активный хост другой панели',
'settings.sshDeepLink.title':'SSH- и Telnet-ссылки',
'settings.sshDeepLink.enable':'Открывать ssh:// и telnet:// ссылки в Netcatty',
'settings.sshDeepLink.enableDesc':'Разрешить Netcatty обрабатывать ssh:// и telnet:// ссылки из браузеров и других приложений.',
'settings.sshDeepLink.enableDesc':'Разрешить Netcatty обрабатывать ssh:// и telnet:// ссылки из браузеров и других приложений, а также запуски в стиле PuTTY, например -ssh user@host -P 22 -pw password.',
'settings.explorerContextMenu.enable':'Показывать «Open in Netcatty» в контекстном меню папок',
'settings.explorerContextMenu.enableDesc':'Добавляет пункт в меню папок и фона папки, чтобы открыть локальный терминал в этом каталоге. Применяется сразу, переустановка не нужна.',
'settings.sftp.compressedUpload.enableDesc':'Автоматически сжимать папки с помощью tar перед передачей. Требует поддержки tar на сервере. Если она недоступна, будет использована обычная передача.',
'settings.sftp.skipUnchanged.enableDesc':'Пропускать файлы, у которых размер и время изменения уже совпадают с назначением (в стиле rsync). Отключается при замене папки во временный путь.',
// Quick Switcher
'qs.search.placeholder':'Поиск хостов или вкладок',
'hostDetails.hostname.placeholder':'IP или имя хоста',
'hostDetails.section.general':'Общие',
'hostDetails.os.title':'Определение системы',
'hostDetails.os.desc':'Используйте автоматическое определение. Укажите систему вручную только при ошибке. Режим сетевого устройства настраивается отдельно.',
'hostDetails.sftp.fileProtocol.desc':'Авто сначала пробует SFTP, затем SCP-режим, если подсистема SFTP недоступна. Принудительный SCP — для хостов только с SCP.',
Loaded 100 of 1409 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.