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>
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>
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>
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>
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>
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(#2737): automated Cursor CLI fix
* fix: address Codex review on PR #2739
* fix(vault): harden destination re-dedupe passphrase and counts
Keep collapsed destination imports aligned with CSV merge policy for key
passphrases, and correct plugin importer added/duplicate counters when
hosts collapse into one group.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(vault): preserve SecureCRT and plugin hosts under destination group
Skip endpoint collapse for SecureCRT imports, and keep plugin hosts that
differ by configuration when an import destination rewrites their group.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(vault): skip plugin imports that collide after destination rewrite
After rewriting imported hosts into the destination group, drop ones that
now share a fingerprint with existing vault hosts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(vault): keep group case in host merge keys
Vault group paths are compared exactly, so Prod and prod must remain
distinct session identities during import/create dedupe.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(vault): prune orphan credentials after destination collisions
When a destination rewrite drops colliding imported hosts, also remove
identities/keys that were only referenced by those skipped hosts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(vault): only prune newly imported credentials on collision
Pass existing identity/key counts into destination handling so remapped
duplicates of vault credentials are never deleted when a host is skipped.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(vault): merge credential refs when collapsing destination hosts
Carry identityId/telnetIdentityId/identityFileId onto the retained host
so destination-group collapse does not drop referenced auth.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(vault): only graft credential refs onto bare collapsed hosts
Avoid copying identityId onto a retained host that already has key or
password auth, which would silently change resolveHostAuth preference.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
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>
* feat(plugins): add sync providers and encrypted sidecar sync
Implement PR 8 of the plugin platform (#2269): namespaced sync Providers
with provider.sync, encrypted-object storage operations, WebDAV through the
shared storage surface, dynamic cloud provider IDs, and non-cascade sidecars
for sync:true plugin settings plus account/CRDT baselines.
Related to #2269
* fix(plugins): wire sync providers and sidecars into production paths
Close PR 8 integration gaps: dynamic plugin provider registry and
getConnectedAdapter createPluginStorage, WebDAV through EncryptedObjectStorage,
cloud payload collect/apply for pluginSidecars, and IPC/preload bridges.
Related to #2269
* fix(plugins): preserve WebDAV auth reuse and resourceId through EOS wrap
encryptedObjectStorageAsCloudAdapter now reports initiallyAuthenticated when
config exists (matching raw WebDAVAdapter) so getConnectedAdapter reuses the
cached instance, and preserves/refreshes resourceId from the backing adapter
instead of always forcing the default object key.
Related to #2269
* fix(plugins): address Codex PR 8 review findings
Carry pluginSidecars through merge/convergent uploads, lazy-connect before
object I/O with revision-aware writes, fail closed on sidecar collect errors,
expose connectPluginProvider, preserve remote timestamps, and drop deleted
installed-plugin settings from collection.
Related to #2269
* fix(plugins): close remaining Codex P1/P2 sync issues
Lazy stream cancel for inline sync reads, must-not-exist conditional writes,
three-way sidecar merge for local resets, validated apply path for settings,
and merge remote sidecars into convergent uploads.
Related to #2269
* fix(plugins): harden sidecar collect/apply and write verification
Preserve last-known sidecars when the host is offline, apply remote
deletions for installed plugins, force convergent upload on sidecar
divergence, verify plugin object writes, and pick plugin providers in
auto-sync startup checks.
Related to #2269
* fix(plugins): close remaining Codex sidecar/sync path gaps
Include sidecars in auto-sync payload and hash, keep explicit empty bundles,
fail apply when host is offline, require getCapabilities, and carry merged
sidecars into convergent local apply/upload decisions.
Related to #2269
* fix(plugins): close Codex P1 sidecar collect/apply/merge gaps
Treat null host responses as unavailable, persist collected settings into
the non-cascade table, retain decoded convergent payloads, use three-way
sidecar merge for deletions, and fail apply only on operational errors.
Also treat non-empty plugin sidecars as meaningful sync data and re-run
auto-sync when plugin contributions change.
Related to #2269
* fix(plugins): close remaining Codex P1 sidecar and provider gaps
Replay offline-cached sidecars before collect, keep explicit empty
sidecar fields after three-way merge, thread sidecars through convergent
conflict/downgrade materialization, keep gated-off plugin providers from
joining sync as connected, and rebind plugin EOS sessions after runtime
replacement.
Related to #2269
* fix(plugins): separate pending remote sidecars from last-known cache
Replay only host-offline remote applies into the DB, keep last-known as
upload fallback, use contribution reset for deleted settings, probe real
plugin host readiness, rebind storage events for plugin providers, clear
plugin bases on reset, and swallow disconnect rejections.
Related to #2269
* fix(plugins): hydrate retained sidecars and gate missing providers
Materialize installed-plugin settings from retained sidecars before
collect pruning, clear sidecars on setting reset, fail closed when
pending remote replay fails, and only restore plugin providers that are
contribution-available. Also initialize dynamic provider counters on
connect.
Related to #2269
* fix(plugins): surface plugin sync providers and preserve conflict sidecars
Collect live sidecars before convergent conflict apply/upload, include
dynamic providers in status and disconnect paths, and list plugin sync
providers in Cloud Sync settings for connect/disconnect.
Related to #2269
* fix(plugins): per-provider sidecar baselines and safer collect/UI
Merge convergent sidecars against each provider's own baseline, keep
retained settings in sidecars without unvalidated plugin_settings writes,
guard isConnectDisabled for never-connected plugins, and fall back plugin
provider labels/icons in the status popup.
Related to #2269
* fix(plugins): correct plugin provider discovery and sidecar reverify
Read nested contribution metadata for sync provider cards, gate empty-config
connects, enforce single-provider disconnect for plugins, and re-verify after
sidecar-only convergent uploads so baselines track the post-write payload.
Related to #2269
* fix(plugins): refresh sync provider availability from live contributions
Replace the contribution-available provider ID set when plugins change so
disabled or uninstalled sync providers leave the auto-sync ready set and
drop to disconnected with config retained.
Related to #2269
* fix(plugins): preserve sidecars in backups, pending queue, and migration
Attach last-known plugin sidecars to protective local vault backups, fail
operationally when pending/last-known storage writes are rejected, carry
sidecars through convergent migration publish, and reject trailing garbage
on encrypted object parse.
Related to #2269
* fix(plugins): abort conflict collect failures and merge migration sidecars
Rethrow operational sidecar collection errors during convergent conflict
resolution, LWW-union all local/provider sidecars during convergent
migration, re-collect last-known after apply, and prefer live host collect
for protective local vault backups.
Related to #2269
* fix(plugins): restore pluginBridge tests, fix import path, union downgrade sidecars
Correct SettingsSyncTab dynamic import path so production builds resolve,
add ipcMain.on to the plugin bridge test double, union all provider
sidecars on convergent downgrade, keep empty last-known resets, and skip
force-delete when contribution reset validation rejects.
Related to #2269
* fix(plugins): abort backup collect failures and honor empty remote wipes
Only fall back to last-known sidecars when the host is unavailable during
protective backups, treat explicit empty remote sidecar bundles as
authoritative wipes, remove lint-blocking anys from harness tests, and
use a stable equal-timestamp sidecar tie-break.
Related to #2269
* fix(plugins): include sidecars in equality, preserve merge base on reconnect
Compare pluginSidecars in cloudSyncPayloadsEqual for migration freshness,
clear plugin merge state only when account/resource changes on reconnect,
cancel accepted sync read streams on failure, and stop re-connecting the
plugin EOS adapter on every I/O.
Related to #2269
* fix(plugins): drop cached adapters on contribution refresh
Invalidate plugin sync adapters when live contributions change so a
restarted runtime gets a fresh connect, take offline any non-disconnected
missing providers, and latch version-change backups only after cancellation
checks pass.
Related to #2269
* fix(plugins): preserve omitted sidecars and hydrate on plugin enable
Treat missing pluginSidecars as legacy/unsupported in three-way merge
(only explicit empty is a wipe), hydrate retained settings when a plugin
enables, and re-enable retained provider configs when contributions return.
Related to #2269
* fix(plugins): keep newer retained sidecars over older stored settings
When collection merges existing sidecars with plugin_settings rows, prefer
the newer updatedAt so a schema-rejected remote value retained only in
the sidecar table is not overwritten by a stale local setting.
Related to #2269
* fix(plugins): seal plugin provider config and clear base on config change
Encrypt opaque plugin provider configuration before persistence, clear
merge state when reconnect configuration changes, and stream sync objects
below a base64-safe inline threshold so control-plane JSON stays in budget.
Related to #2269
* fix(plugins): seal plugin configs by provider id and hydrate before start
Encrypt plugin provider configuration only for non-builtin provider IDs
so field-name collisions cannot skip sealing, hydrate retained sidecars
before plugin enable starts the runtime, and stop tearing down live
adapters on no-op availability refreshes.
Related to #2269
* fix(plugins): seal scalar configs, safe inline reads, stable config fingerprint
Encrypt any non-builtin plugin configuration JSON shape, enforce the
base64-safe inline read cutoff, and compare reconnect configurations with
order-independent fingerprints so merge bases are not cleared spuriously.
Related to #2269
* fix(plugins): thread sync credentials and harden availability checks
Pass SyncConnectPayload.credential through the object-storage and IPC host
path, treat only null/undefined config as missing, and avoid wiping the
plugin provider availability catalog on transient discovery failures.
Related to #2269
* fix(plugins): unblock schema-required sync connect and scalar configs
Add a JSON config dialog for plugin sync providers that declare a
required configurationSchema, reuse retained configs on reconnect, and
treat falsy scalar configs as present in readiness and adapter lookup.
Related to #2269
* fix(plugins): rebind sync adapters on contribution refresh
Drop cached plugin sync adapters when live contributions still include
the provider so replaced runtimes get a fresh connect, and seal falsy
scalar plugin configs with nullish presence checks end-to-end.
Related to #2269
* fix(plugins): apply sidecars under current declared setting scopes
When a plugin update changes a syncable setting scope, write under the
live declaration and drop the obsolete scope row so collection does not
republish duplicates.
Related to #2269
* fix(plugins): seal config envelopes and stabilize provider counters
Use an unambiguous host-owned sealed-config envelope, initialize
dynamic-provider sequence counters before cross-window decrypt, keep
three-way sidecar deletions during convergent migration, and create the
sidecar table for existing schema-1 plugin databases.
Related to #2269
* fix(plugins): avoid mid-sync adapter drops and sidecar resurrection
Keep plugin adapters across setting-only contribution refreshes and
rebind sessions on ensureConnected; stop preserving local-only missing
plugin sidecars when remote already mentions the plugin; compare configs
with nullish semantics across windows; swallow quick-connect failures
after toasting.
Related to #2269
* fix(plugins): restore remoteKeys for installed sidecar resets
Reintroduce the remote key set used when dropping installed-plugin
settings the remote no longer carries so applyFromSync cannot throw
after replacing the sidecar table.
Related to #2269
* fix(plugins): honor conflict policy and re-key scoped sidecars
Pass preferCloud/preferLocal into three-way sidecar merge, use
per-provider baselines on convergent downgrade so local resets are not
resurrected, and re-key applied settings under the current declared
scope coordinates.
Related to #2269
* fix(plugins): keep strategy on final sidecar merge and block dual legacy
Pass preferCloud/preferLocal through the final verified sidecar merge,
avoid auto-reactivating a retained plugin while another legacy provider
is already connected, cache plugin adapters only after initializeSync,
and preserve explicit null configuration values.
Related to #2269
* fix(plugins): strip plugin avatars and protect last-known sidecars
Do not load plugin-supplied avatar URLs in the main renderer, and avoid
overwriting last-known sidecars with an unmerged remote bundle when the
post-apply collect fails or is non-authoritative.
Related to #2269
* fix(plugins): strip top-bar plugin avatars and stop always-rebind
Hide plugin avatar URLs in the sync status popover, drop always-on
rebindSession for plugin adapters, and read convergent mode from the
local config module when deciding whether to auto-reactivate providers.
Related to #2269
* fix(plugins): use window.netcatty for sync IPC and allow null configs
Read plugin sync and sidecar APIs from the production preload global,
and treat an explicit config property (including JSON null) as present
for readiness and adapter lookup.
Related to #2269
* fix(plugins): close residual Codex P2 sync edge cases
Three-way merge sidecars during v2 convergent migration so local
resets are not resurrected, preserve explicit null plugin configs on
adapter rebuild, rebind EOS sessions after I/O failure for replaced
runtimes, and accept the public inlineObjectBytes limit on readObject.
Related to #2269
* fix(plugins): keep inline read cutoff under control-plane budget
Restore the base64-safe inline cutoff so advertised-size reads cannot
overflow the 128 KiB provider JSON envelope; providers near the public
inlineObjectBytes limit must stream.
Related to #2269
* fix(plugins): harden sidecar empty-vault, hydrate LWW, and credential seals
Close remaining Codex/review gaps for PR8: keep empty sidecar shells from
bypassing upload guards, skip startup hydrate when local settings are newer,
seal/validate durable plugin credential refs, and cover schema 1→2 migration.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(plugins): rebind plugin sync sessions and keep null configs
Address Codex P2 on 8ae52c54: re-issue connect after runtime replacement,
and treat stored config:null as present in the dashboard reconnect path.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(plugins): defer empty sidecar last-known until sync succeeds
Keep prior non-empty last-known across an authoritative empty collect so the
empty-vault guard can still recognize a plugin-only reset, then commit the
empty cache after a successful upload.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(plugins): avoid restricted localStorage global in sidecar test
Bind the mock to a local binding so eslint no-restricted-globals stays clean.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(plugins): commit applied sidecar cache and reset orphan settings
Prefer merged payload sidecars when updating last-known after sync, and clear
syncable plugin_settings that lack sidecar rows on authoritative remote deletes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(plugins): reseal marker-shaped plugin configs instead of skipping
Exact __netcatty_plugin_config_v1 collisions no longer bypass encryption; trusted
host envelopes are unwrapped then resealed, and false collisions are sealed whole.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(plugins): keep merged sidecars in last-known after apply collect fails
Return applyFromSync entries from IPC and use them when follow-up collect fails,
so preserved missing-plugin rows are not dropped from the offline upload cache.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(plugins): delete stale-scope settings at stored coordinates on reset
After resetSetting against the current declared scope, also remove the row at
the sidecar's stored scope so scope migrations cannot republish remote deletes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(plugins): close sync-provider credential, stream, and sidecar gaps
Make SecretRef usable end-to-end via lease-bound network.request, keep
renderer/main transfers abortable and bounded, and stop sidecar
last-known/pending from corrupting backups and conflict apply.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(plugins): harden single-provider, secret schema, and conditional conflicts
Keep legacy restart from dual-connecting providers, extract writeOnly schema
secrets into OS storage, and surface conditional-write precondition failures
as conflict UI instead of a generic sync error.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(plugins): dedupe sync type imports for lint
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: address Codex review on PR #2713
* fix(plugins): wait for sidecar host before version backup latch
Defer the one-shot upgrade snapshot until the plugin sidecar host is ready (or a short grace), and catch getDataHash failures so pending/baseline paths do not reject unhandled.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(plugins): require sidecar grace before version backup
Always wait for contributions/grace tick before the upgrade snapshot, and only latch after a non-cancelled attempt so cleanup cannot suppress retries.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com>
* fix(#2727): automated Cursor CLI fix
* fix(sftp): drop external-edit retainers when browse sessions close
closeSftp already deletes registered temps on disconnect/reconnect; clear
the matching renderer retainers so hasActiveWork cannot stick on a gone file.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): clear external-edit retainers when watches force-stop
When the temp file disappears, fileWatcherBridge force-stops the watch
with localPath; drop the matching retainer so hasActiveWork cannot stick.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: address Codex review on PR #2730
---------
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(ai): stop OpenCode turns dying on terminal tool failures
OpenCode MCP terminal_execute treated non-zero exits as bare
"Error: Operation failed" (dropping stdout), and the OpenCode driver
aborted the whole turn on tool status=error. Preserve command output
and surface tool errors as tool results so multi-step agent runs can
continue.
Closes#2718
* fix(ai): use neutral MCP text for silent terminal_execute success
Codex P2 on #2724: ok:true empty serial/raw PTY results no longer fall
back to "Error: Operation failed". Prefer Catty's "Command completed
(no output)" and keep operational failures / non-zero exits unchanged.
Also prefer non-empty OpenCode tool error/output when formatting tool
results.
Add single-entry delete on the Global history side panel, remove the
matching host autocomplete record when no duplicate row remains, and
keep the change set intentionally slim (no multi-window journal/sequence
hardening).
Fixes#2701
* fix(terminal): keep broadcast through command colons
Treat trailing colons after a confirmed shell prompt as typed command
input so workspace broadcast is not paused mid-command (#2709).
Require terminator + whitespace so mid-label shapes like Challenge #1:
stay fail-closed; standalone auth prompts remain untrusted.
* fix(terminal): require real PS1 body for colon broadcast bypass
Title-case challenge labels that embed shell markers (Challenge # 1:,
Account $ code:) must stay fail-closed. Only bare markers, user@host,
paths, shell-version PS1s, and lowercase identities count as a real
prompt prefix for the #2709 mid-command colon exemption.
* fix(hosts): keep draft connect scripts before onConnect promote
Host details queue editing pruned non-onConnect scripts immediately, so
default manual automation scripts could be selected but never appeared
in the queue. Keep an editable draft queue and only prune missing IDs
until save promotes triggers.
Closes#2705
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hosts): promote persisted manual connect-queue scripts on save
Editable queue retention left already-saved manual script IDs unpromoted
when reopen/save compared only newly added IDs. Promote every queued
script that still needs onConnect + host targeting.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(hosts): drop unused HostDetailsPanel snippet imports
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hosts): do not demote global scripts during queue save sync
Promote-on-save must leave targetsAllHosts onConnect scripts unchanged
when they appear in a host connectScriptIds list.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(hosts): cover global connect scripts during queue save sync
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hosts): promote connect-queue scripts on new host save
New hosts have no initialData, so queue promote-on-save was skipped and
manual scripts stayed non-onConnect after the first save.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hosts): harden connect-queue promote against Codex review
Preserve global scope when promoting manuals, skip re-promoting scripts
demoted while the host editor is open, sync peer host queues for retained
targets, and keep test fixtures ASCII-only.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hosts): persist peer connect queues with edited host save
Peer queue sync via onHostsChange was overwritten by onSave rebuilding
from a stale hosts snapshot. Write peers+edited host together and make
save paths apply functional host updates.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hosts): keep connect queues across hydration and concurrent edits
Skip queue rewrite while snippets are still empty, and do not promote
entries whose non-onConnect trigger changed while the host editor stayed
open.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hosts): skip connect-queue rewrite before snippets hydrate
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hosts): preserve concurrent target removals and merge peer deltas
Drop stale queue entries when targets were unlinked while editing, and
apply peer connectScriptIds updates via functional host state merges.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hosts): hydrate connect-queue baseline and append peer deltas
Capture the first hydrated snippet catalog as the editor baseline, and
merge peer queue updates by appending only newly queued script IDs.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hosts): snapshot opening queue and keep peer updates host-safe
Use the editor-opening connect queue as sync previous IDs, and never
overwrite the edited host inside peer delta merges.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hosts): clear deleted queues after hydrate and honor manual unlinks
Treat a hydrated empty snippet catalog as ready for queue reconciliation,
and drop concurrently untargeted non-onConnect queue entries on save.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hosts): clear loginScriptId when connect queue is emptied
sanitizeHost reconstitutes connectScriptIds from loginScriptId when the
explicit queue is absent, so clearing the last queued script must also
drop the legacy field.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Shell-open retries already applied to every Copy Tab reuse, but resilient
PID scans were still gated on jumpHosts. Direct bastion targets that
rate-limit session channels could reuse the shell while leaving the
copied tab without shellPid. Always retry rate-limited discovery on the
reuse path; keep the optional pre-scan pause jump-host-only.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
If the jump-host Copy Tab pre-open PID scan is rejected with channelOpen
too offen, a single delayed attempt left discovery unavailable and skipped
post-open assignment. Retry rate-limited pre-open scans so shellPid
tracking still succeeds before opening the reused shell.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
After a bastion-rate-limited shell open succeeds on Copy Tab, the follow-up
PID discovery exec can hit the same "channelOpen too offen" limit. Delay and
retry that discovery on jump-host reuse so the copied session still gets a
shellPid for unambiguous cwd lookup.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Address review feedback: permanently skipping PID discovery on jump-host
Copy Tab left shared transports without shellPid and broke cwd lookup.
Delay discovery briefly instead, and keep shell-open retries for the
bastion "channelOpen too offen" rate limit.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Bastion/jump hosts often reject rapid session channel opens with
"channelOpen too offen". Copy Tab was opening a discovery exec then a
shell on the same authenticated transport, failing reuse and falling
back to a fresh login that prompts for OTP again.
Skip shell PID discovery when jump hosts are present, and retry shell
opens with short backoff on the bastion rate-limit error so duplicated
tabs keep reusing the live connection.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
If residual/stale HKLM keys cannot be rewritten without elevation, register
per-user HKCU verbs instead so Settings can still enable Open in Netcatty
for portable and unelevated installs.
Persist and compare development app entry args so moving a checkout
re-registers Explorer shell commands instead of keeping a stale
electron.exe + old app path launch line.
Clear only ProgrammaticAccessOnly when unsuppressing keys that still have
commands, and unsuppress after a failed HKLM refresh so preference-write
rollback cannot leave the only working portable integration hidden.
When only one ProgrammaticAccessOnly write succeeds, remove just that
value (or a newly created empty key) instead of deleting the whole HKCU
shell subtree that may still hold a working portable command.
When the saved preference says enabled, still confirm both shell verbs
remain registered; otherwise re-apply so interrupted installer rewrites
or partial cleanup cannot leave a permanently missing menu entry.
When HKLM registration exists, disable only writes ProgrammaticAccessOnly.
Deleting portable HKCU install verbs before a failed suppression left users
with no working Explorer integration and an unrepairable machine path.
Record a separate probe marker for ZIP/portable no-verb defaults so a
shared AppData profile cannot suppress a later NSIS install. Register
development Explorer verbs as electron.exe + absolute app entry.
After a failed machine repair, report any surviving shell verb as enabled
so Settings can disable it. Persist enabled:false for ZIP/portable defaults
so warm starts skip repeated registry probes.
When a partial machine registration exists, keep working portable HKCU
verbs until both HKLM shell entries are current, so unelevated enable
attempts cannot destroy the user's only working context-menu path.
Treat residual portable HKCU verbs as a hard enable failure when HKLM
exists, require suppression cleanup on user-scope enable, and restore
the previous preference when apply partially mutates the registry.
Codex P2 on #2671: portable/ZIP builds leave real HKCU shell verbs that
shadow later all-users HKLM registrations. When machine keys exist,
delete the per-user Netcatty verbs so Explorer uses the current install.
Codex P2 on #2671: if only one ProgrammaticAccessOnly value is written
during disable, delete the partial key so disable never leaves a single
hidden verb while the other remains visible.
Omit sourceSessionId for sftpSudo hosts in the picker and normalize the
browse connect path so reusedConnection (spinner/overlay/logs) matches the
dedicated sudo open. Addresses Codex review on #2674.
Codex P2s on #2671: remove treats any leftover runnable verb as still
enabled, and install restores ProgrammaticAccessOnly when enabling fails
after clearing a prior per-user suppression.
Codex P2 on #2671: when no preference exists but a single leftover shell
verb remains, still run install/repair so the Settings toggle cannot stay
off while Explorer still shows one Open-in-Netcatty entry.
Skip used to mark transfers cancelled without clearing conflict metadata, so reopening SFTP rehydrated the dialog while the transfer center looked empty. Clear conflict on skip/stop, only restore attention conflicts in the dialog, and strip stale payloads when loading persisted history.
Fixes#2636
Codex P2 on #2671: unelevated refreshes must not report success merely
because non-empty HKLM commands exist. Require writes to succeed or both
verbs to already match the expected executable/command contract.
Codex P2 on #2671: treat the integration as user-suppressed only when
both folder and background keys have ProgrammaticAccessOnly, so a partial
suppress write cannot hide one menu while claiming the feature is off.
List open SSH terminals in the SFTP Connected section even when sftpSudo
is enabled. Connection still opens a dedicated sudo SFTP channel and does
not reuse the terminal shell. Closes#2656.
Address Codex P2s on #2671: disabled preferences re-suppress on every
startup so NSIS updates cannot resurrect the menu, and registration is
only considered complete when both folder and background commands exist.
Codex P2 on #2671: a partially written registry verb (key exists, no
command value) must not count as an active menu registration, so install
and Settings toggle state stay honest and repair can still run.
Persist download fingerprints as sha256:pN:hex so coverage is explicit,
treat planned zero bytes as the empty digest, and fall back to a full-file
check for legacy bare sha256:hex identities saved by older builds.
Standalone dedicated reconnect was still coercing totalBytes=0 to
undefined via ||, so the bridge re-statted a grown remote and abandoned
the empty snapshot plan. Keep finite zeros in the stream payload.
Persist the last-applied executablePath in the preference record and
re-apply shell verbs when it differs from the current launcher path so
renamed/moved portable builds keep Open-in-Netcatty working.
Drop growth-aware compact identity experiments that either lost rewrite
proof or needed renderer node:fs. Compact entries keep full size+mtime
identities (retransfer on remote growth). Preserve explicit totalBytes=0
through bridge preflight so empty download snapshots do not re-stat into
a grown remote size.
Codex P2 on #2671: electron-builder portable apps expose the stable
launcher via PORTABLE_EXECUTABLE_FILE while process.execPath points at a
temp unpack that is deleted on exit. Resolve that path for shell verbs
and bump the preference schema so existing installs re-register once.
Path-only compact identities were too weak for rewritten/truncated files.
Download trees now key entry identity to path+planned size (omit mtime),
revalidate covered compact entries against local destination sizes, and
preserve explicit zero-byte snapshot plans through resume validation.
Address Codex P2 on #2671: the Settings toggle is per-user, so disable
only clears HKCU and suppresses machine verbs via ProgrammaticAccessOnly
instead of deleting HKLM (even when elevated). Also persist a schemaVersion
so healthy warm starts skip registry work after the first migration.
Codex P2 on #2671: warm starts no longer spawn a full reg.exe write pass
when installer/user shell verbs already match the expected command, label,
and icon. Still rewrites stale --open-terminal-path entries on upgrade.
Preserve the planned snapshot size for interrupted directory download
children, use path-stable compact identities for download trees so
append-only growth does not invalidate covered prefixes, and treat
zero-byte planned snapshots as complete in the sequential fallback.
Codex review on #2670: accept remote growth only after the full planned
prefix still matches the staged file, and skip the sequential stream when
the checkpoint already covers the snapshot so grown logs do not resume
from the old EOF.
Address Codex review on #2671: when all-users (HKLM) shell verbs cannot be
deleted unelevated, hide them for the current user via ProgrammaticAccessOnly
instead of failing the Settings toggle. Also stop mirroring machine verbs into
HKCU so uninstall does not leave a broken Open-in-Netcatty entry.
Add a Settings > System switch to enable/disable the Explorer folder
context menu, and fix shell launch so Chromium does not swallow
--open-terminal-path (args after --, =path form, trailing-dot roots).
Closes#2651Closes#2655
Address Codex review: after the async clipboard read, only insert when
the find input is still document.activeElement and still connected, so
a delayed paste cannot steal focus or hit the editor body.
SFTP downloads take a fixed size snapshot at start, then rejected the
transfer if the remote file grew before completion. Live logs and other
append-only sources commonly hit that post-check and failed with a size
mismatch. Accept remote growth when the planned prefix was transferred,
and keep resume identity keyed to that snapshot so pause/resume still works.
Closes#2653
Custom Ctrl/Cmd+V handling for Electron overwrote paste while the
snippet script editor find bar had focus, so search text landed in the
script. Route find-widget paste through shared helpers and reuse them
in the text editor.
Fixes#2661
Bound the main-process TCC probe to 3s, only treat real private IPv4/IPv6
literals as LAN, prefer proxy as first hop when set, and annotate
unreachability using either the target or the first hop. Closes the
remaining merge blockers for #2663.
Show manual compact for any Catty session with a configured model, not
only when enough history exists for a large head. External agents still
do not get the command.
Gate /compact behind canCompact so the composer is not cleared as a
silent no-op, hide the usage ring until a real context snapshot arrives,
and summarize only the UI head that the persisted compaction boundary
will drop. Drop unused clearSessionMessages surface and add slash-command
regression tests.
Emit plan items as { text, completed } with top-level running|completed status so sdkAgentAdapter and the activity UI can render labels and completion instead of empty rows stuck on running.
Remove the async rpc.request hop that let promptDone win Promise.race on session/prompt JSON-RPC errors. Gate emitDone on turnCompleted, treat any incomplete process close (including exit 0) as failure, and align streaming-json toolResult emission with ACP when rawOutput is present.
Permission replies must select an optionId offered in params.options instead of inventing allow-once. Process close(null, signal) before turnCompleted is a failure for both ACP and streaming-json drivers.
Attach a stdin error listener so async EPIPE after peer exit is not an
unhandled stream error. Treat EPIPE/ERR_STREAM_DESTROYED as benign
(processErrorGuards/terminalBridge); fail the turn only for other mid-turn
write errors.
Only turnCompleted (headless end / ACP prompt success) may ignore teardown
exit codes. streamedAssistantText must not suppress errors, or mid-response
crashes look successful.
Root cause of Token ~1: onPromptComplete resolved promptDone in the same
tick as session/prompt, so Promise.race often discarded the result and the
UI fell back to estimated usage (prompt length / 4). Emit usage when the
prompt response is handled, and map live Grok _meta.usage fields including
cachedReadTokens.
Drop the inactive acp/streaming-json in-memory session when switching
runtimes so switch-back cannot resume a pre-switch thread without
intervening turns. Forward optional session/prompt usage to emitter.usage.
Tool-only turns leave no assistant text; Windows teardown often exits with
code 1. Mark turnCompleted when session/prompt succeeds so close does not
emitError or skip emitDone.
Windows .cmd/.bat installer shims fail with spawn EINVAL when launched
without shell. Route ACP, headless, and models spawns through the same
helper other managed agents use.
When a stored session id cannot resume/load, still inject renderer history
into the new session prompt so multi-turn context is not dropped. Successful
resume/load paths remain unseeded to avoid stacked prior replies.
Prefer ACP session/resume (then load/new) without replaying history into the
current turn, authenticate when Grok advertises methods, isolate acp vs
streaming-json session identities, and apply MCP local-tool lockdown plus
--no-auto-update on both transports.
Suppress session/update emissions until session/prompt starts so
session/load history replay does not fill the current assistant bubble.
Also treat Grok like CodeBuddy: when a resume session id exists, do not
also inject renderer conversation history into the prompt.
Expose a Settings toggle on the Grok Build card (like Codex App Server)
to choose ACP agent stdio or the original headless streaming-json path.
Persist grokRuntime on ExternalAgentConfig and pass it into the driver
via NETCATTY_GROK_RUNTIME on the agent env.
Grok agent stdio session/new rejects plain object env (Invalid params /
McpServer enum). Emit type:stdio servers with env as [{name,value},...]
matching the live protocol, and assert the shipped shape in unit tests.
In MCP tool-integration mode, pass --disallowed-tools for Grok built-in
shell/edit/write so remote session work goes through injected Netcatty MCP,
matching Claude-style policy. Skills mode leaves local shell available for
the Netcatty CLI skill path.
Curate GROK_MODEL_PRESETS and wire getAgentModelPresets so the chat model
picker still has a non-empty list when live grok models discovery fails,
matching the Claude/Codex fallback pattern.
Expose Grok as a managed Settings agent card, icon, coding-CLI provider,
and runtime model discovery target. Add en/zh/ru strings for in-app Grok
Build (distinct from External MCP client install).
Add grok to ManagedAgentKey/meta, discovery known-agents list, XAI_API_KEY
and ~/.grok/auth.json auth probe, and closed sdkBackend unions so the CLI
is discoverable and routable like other managed agents.
Register a headless streaming-json driver for backend key grok that maps
ACP-derived NDJSON events into the shared emitter protocol and injects
Netcatty MCP via a project .grok/config.toml merge with restore.
Codex P2: restoreWorkspace() rebuilds the workspace object field-by-field
and omitted autoTitle, so with Restore Previous Session enabled an
explicitly named "Workspace" (rename / creation dialog / snippet) lost its
autoTitle:false on restore and got relabeled with a host name.
Carry autoTitle through restoreWorkspace with a boolean guard so a
deliberate false survives and legacy workspaces stay flagless. Adds restore
coverage for both the explicit-false and legacy-absent cases.
Codex P2: resolveWorkspaceTabLabel treated any workspace whose title equals
"Workspace" as an untouched default and replaced it with a host label — so a
workspace a user renamed to "Workspace" (or a snippet named "Workspace")
lost its chosen name.
Add an explicit Workspace.autoTitle flag. It is authoritative when present:
- set false on user rename, snippet-created, named "New Workspace", and any
createWorkspaceFromTargets call given an explicit name;
- absent/true means the tab may derive a host label.
Legacy workspaces without the flag fall back to the old default-title string
check, so already-open default splits still get host labels and previously
renamed workspaces keep their names.
- domain/models/workspace.ts: add autoTitle field.
- domain/workspace.ts: thread autoTitle through createWorkspaceFromSessionIds.
- application/state/useSessionState.ts: mark rename / snippet / named
creation paths as explicit; createWorkspaceFromTargets takes an optional
name and only marks custom when one is given.
- domain/sessionTabTitle.ts: derive only when autoTitle (with legacy string
fallback); tests for explicit "Workspace" name and explicit auto-title.
Third Codex P2: submitSessionRename rewrites BOTH customName and hostLabel
(useSessionState.ts:585,597), so counting distinct hosts by hostLabel still
mislabeled a renamed same-host pane as a separate host and added a spurious
"+1".
Count by session.hostId instead — the only identity a rename leaves
untouched. All local terminals share the stable LOCAL_TERMINAL_HOST_ID and
remote sessions carry their host config id, so same-host panes collapse to
one regardless of renames. The focused pane still shows its rename as the
primary label. Tests updated to carry hostId (including a rename that
rewrites customName + hostLabel but keeps hostId).
Follow-up Codex P2: the "+N" distinct-host set used getSessionConnectionLabel
(customName || hostLabel), so two panes on the same host where one session
was renamed produced different keys and a spurious "+1", presenting the
workspace as spanning multiple hosts.
Count distinct hosts by hostLabel alone. The focused pane still shows its
rename as the primary label; only the host count ignores per-session
renames. Adds coverage for the renamed-same-host and rename-with-multi-host
cases.
Review feedback (Codex P2 + Copilot): resolveWorkspaceTabLabel derived
its label and its distinct-host "+N" count from resolveSessionTabTitle,
which folds in the dynamic shell/agent title. Two problems:
- those live titles are published only to the session presentation store,
not the plain session records passed to the tab, so the label went stale
under dynamicTabTitleMode 'agent'/'all';
- per-pane dynamic titles differ on a single host, so the "+N" suffix
fired even when every pane was on the same host.
Use getSessionConnectionLabel (rename / host label) — the stable host
identity — for both the primary label and the distinct-host set. This is
also what the tab is meant to convey (which host), and it needs no
presentation-store subscription. Drops the now-unused dynamicTabTitleMode
argument from resolveWorkspaceTabLabel.
A tab holding a split workspace showed the generic default title
"Workspace", giving no hint which host it belonged to. Derive the tab
label from the workspace's focused session when the workspace still
carries the default title: a local shell reads as its host label
(e.g. "Localhost"), an SSH session as its configured host name. If the
workspace spans more than one distinct host, the focused host is shown
with a "+N" suffix. A user-renamed workspace keeps its custom title.
- domain/sessionTabTitle.ts: add DEFAULT_WORKSPACE_TITLE sentinel and
resolveWorkspaceTabLabel(), reusing resolveSessionTabTitle so labels
match what single-pane tabs already show.
- domain/workspace.ts, application/state/useSessionState.ts: reference the
shared DEFAULT_WORKSPACE_TITLE constant for the default title.
- components/top-tabs/TopTabItems.tsx: render the derived label (with a
title tooltip) instead of workspace.title.
- domain/sessionTabTitle.test.ts: cover rename, single/multi-host, empty.
Copilot review caught that the new showContextMenuOverFullscreenApps flag
only affected the TerminalContextMenu suppression helpers, but the
capture-phase contextmenu interceptor in useTerminalEffects
(shouldInterceptMouseTrackingContextMenu) still called preventDefault +
stopImmediatePropagation on every non-Shift right-click while mouse
tracking was active. That killed the contextmenu event before it could
reach Radix, so the menu never opened even with the setting on.
Route an unmodified right-click through the same path as Shift+right-click
when the setting is on and rightClickBehavior is 'context-menu':
- shouldInterceptMouseTrackingContextMenu returns false (let contextmenu
reach Radix so the app menu opens)
- shouldStopShiftRightClickMouseTrackingMouseDown returns true (stop the
button press from reaching the TUI, so tmux/vim don't also react)
The setting is menu-only: paste / select-word right-click behaviors are
unchanged. Adds predicate coverage in middleClickBehavior.test.ts.
Add an opt-in "Show menu over fullscreen apps" terminal setting
(showContextMenuOverFullscreenApps, default off). When enabled, the
right-click context menu opens even while a fullscreen app (tmux, vim,
less) holds mouse tracking, instead of the click being passed through to
the app.
Threaded via a new forceMenuInAlternateScreen flag through the
shouldSuppressMouseTrackingContextMenu predicate (single source of truth)
and its callers. Default off keeps current behavior unchanged; Shift+
Right-Click still works as the escape hatch when off.
- domain/models/terminal.ts: new field + default false
- application/syncPayload.ts: include key in synced terminal settings
- TerminalContextMenu.tsx: forceMenuInAlternateScreen through predicates + prop
- TerminalView.tsx: wire setting to prop
- TerminalBehaviorSettings.tsx: toggle row
- i18n en/ru/zh-CN/zh-TW: label + description
- TerminalContextMenu.test.ts: coverage for the opt-out
Chinese glyphs (PingFang etc.) paint outside a 1.0 em box, so leading-none
plus truncate was slicing vault tree, tabs, and dialog titles. Use safer
line-heights and slightly taller note title inputs.
The title-bar terminal icon only appeared on Vault/SFTP and vanished on
terminal surfaces with the host tree open. Drop the redundant control;
local shell remains on the host-tree toolbar and quick switcher.
Finish-path metadata checks treated mtime/ctime bumps as hard source
changes. On macOS, xattr/Spotlight can move ctime without rewriting
bytes, so repeated pause/resume often failed near 100%. Prefer digest
verification for uploads; keep strict timestamps for downloads.
After soft-resume, main may advance lifecycleEpoch past the worker-local
value. Progress fanout now stamps max(main, worker) so the transfer center
does not reject live progress as stale.
In worker-backed mode soft-resume must stamp the normalized main epoch
(broadcast by nextWorkerLifecycleEpoch), not the worker-local value, so
later progress is not rejected as stale.
Foreground Resume no longer joins the multi-minute background wait promise;
each caller waits with its own maxWaitMs, and only the final truncate is
single-flight. Failed settle attempts are not sticky on later Resume clicks.
Codex P1s: exclusive single-flight deferred truncate so background settle
cannot race resume unpause, and soft resume waits for/uses the pause-time
full SHA-256 fingerprint when available before unpausing.
- Soft resume identity: size + mtime (+ local head sample), not size-only,
so same-size in-place rewrites are rejected without full-file SHA-256.
- resumeQueuedTransfer returns lifecycleEpoch so soft-resume does not invent
a higher epoch and drop live progress.
- Ignore left-/right- SFTP pane connection ids as openSftpForSession targets.
- Pass resumeHosts (vault + ephemeral) separately from vault hosts used by tray.
Pause then resume was flaky: soft resume cleared lifecycle epochs so stale
paused events re-applied, soft failures always rehomed to dedicated-resume
(panel queue vanished, "reconnecting" then stuck paused), and concurrent
range soft-drain only waited 1.5s before "still finishing".
Soft resume now stamps bridge lifecycle epochs, only hard-reconnects when
the live stream is gone, dedicated resume reuses terminal/parked transports
(including ephemeral hosts), and deferred sparse settle runs in the
background with a longer resume wait for in-flight ranges.
Call the existing focus primitive directly on tab switch (no outer rAF
wrap). Resolve workspace Detach Session labels via per-session
usePresentedSession so dynamic titles stay live without remapping the
whole TopTabs bar.
Stop TerminalLayerWorkspaceSection from falling back to memoized ctx when
sidePanelLiveStore clears activeWorkspace (workspace → solo tab). Refuse
session-restore active-tab patches without a trusted in-memory full payload
so a connect+activate race cannot write stale disk sessions under a new
activeTabId; force full rebuild from live state instead.
Stop AppView from re-subscribing to activeTabId so shell composition no
longer rebuilds on every top-tab click; move plugin keybindings and host
editor surface visibility into leaf subscribers. Patch session-restore
activeTabId without re-serializing every session on tab switches, defer
chrome theme CSS work with fingerprint short-circuit + rAF, let terminal
workspace chrome read active workspace from the live store, apply TopTabs
presentation per tab, and keep hibernate wake chunked/idle with leaner
focus scheduling.
Encode null vs missing presentation fields for useSyncExternalStore, and
compare AI history list title chrome so renames refresh without stream thrash.
Skip clipboard writes on selection reattach, bound xterm wait polling, and
detect AI session create/delete for history lists without sibling stream thrash.
- Store null presentation tombstones so clears override stale snapshots
- Compare selected AI session identity for cross-scope history resumes
- Attach selection overlay after xterm runtime exists; skip copy-on-select
while preserveSelectionOnInput restores the selection
- Align system-sidebar hibernation source contract with resolvedSession
updateHosts now reuses unchanged host object refs instead of re-sanitizing the
whole vault, and terminal host patches use a functional update. Line-timestamp
toolbar toggles optimistically so the gutter paints before vault write work.
Auto-focus no longer claims keyboard focus when the caret is in the AI/side
panel. Selection overlay and copy-on-select move into TerminalSelectionAIOverlay
so clearing selection (e.g. focusing the AI input) does not re-render the whole
Terminal tree.
App/TopTabs consume useEditorTabChromeList so content keystrokes no longer
rebuild shell domains; dirty dots use useEditorTabDirty. AI side panels keep
the full sessions list for fuzzy history but areEqual only on exact-scope
session object refs so sibling stream ticks skip retained panels.
Stop appTerminalDomain from depending on the whole settings bag and stabilize
hotkey/capture callbacks via refs so opacity/settings/log writes do not rebuild
the terminal domain. Gate hidden History shellHistory subscriptions, use a
stable empty quick-switcher list, and move TopTabs activity dots onto per-tab
store snapshots so sibling activity no longer re-renders the whole tab strip.
useSyncExternalStore now reads a per-session snapshot string so sibling title
updates notify the store but do not re-render unaffected TerminalPane instances.
Merge main into the PR branch and keep the top-tabs local terminal control as a
fallback when the host-tree toolbar is disabled, collapsed, or not on a work
surface (Codex review).
Use the shared show/collapse/hide layout for the host-list toolbar, drop the
redundant top-tabs local-terminal button, and switch the local shell control to
the borderless Terminal glyph as the primary one-click entry.
Address Codex P2/P3 on #2628: feed live titles/providers to TerminalFocusSidebar
and TerminalPane chrome via sessionPresentationStore, and keep aiPanelContextsEqual
on domain-local types so domain does not import UI modules.
useThemeRuntime now returns a memoized bag with setters-stable callbacks, and
App domain memos depend on clear/pick/resolve members instead of the whole
runtime object so unrelated App re-renders no longer rebuild appTerminalDomain.
Split the main-window mega bag into domain-scoped AppView slices so Vault,
terminal, chrome, dialogs, and mounts only re-render when their slice identity
changes. Drive tab titles/coding-CLI icons through sessionPresentationStore
(store-only live updates + orphan map overlay), retain presentation-stable
sessions for domain memos, stabilize AI panel context Maps with structural
equal/retain, and keep shell history on shellHistoryStore so Vault host lists
ignore history-only churn.
- Memo side-panel chrome on stable equal; read live theme when shell is open
- Theme/System live subscribe only when those panels need it; scripts runs
subscribe only while Scripts is visible
- Treat workspaceById focus-only identity as stable for side panels
- Include knownHosts / add-known-host / transfer-count handlers in stable keys
- setScriptRuns publishes scriptRunsStore so automation has a single source
Address correctness and residual jank found in review:
- Expand pane session equality for connect/startup fields (pendingInitialCwd,
startupCommand, pending scripts, serial/local shell, etc.).
- Cache coding-CLI provider in the signal controller so memo-skipped
TerminalLayer renders cannot clear titles via a stale sessionsRef.
- Heal terminalCwdStore drift when the renderer ref already has the path.
- Publish shell history synchronously on write/load and via useLayoutEffect.
- Move script runs to scriptRunsStore; Scripts slot subscribes so automation
ticks no longer re-render TerminalLayerInner.
- TerminalLayerView equal uses stable side-panel keys only (cwd/theme live
via sidePanelLiveStore, not chrome re-renders).
- Compare terminalFontFamilyId / session log props; stabilize HostTreeView
callbacks and end-session-drag handler.
Decouple high-frequency UI state from TerminalLayer's memo boundary so
npm run dev is less dominated by full-layer reconciliation.
- Publish shell history through shellHistoryStore; History side panel
subscribes directly instead of receiving shellHistory props.
- Track terminal CWD in terminalCwdStore so OSC 7 updates do not setState
TerminalLayerInner; TabBridge updates side-panel live snapshots only.
- Move remote-history ownership into the History side-panel slot.
- Treat sessions as equal for pane/layer memo when only dynamicTitle or
codingCliProviderId changes (TopTabs still sees full session updates).
- Stabilize AppView terminal handlers; thin AI vault catalog props when
the AI panel is not visible; memo HostTreeView.
Keep retained AI panels mounted for session continuity, but skip
ChatMessageList/ChatInput while CSS-hidden so tab switches do not
reconcile Streamdown trees. Render assistant text as plain content
during streaming, lazy-load Streamdown only after the turn settles,
and split message layout from the markdown chunk for a lighter open path.
Bump @xterm/* betas so shared-atlas clear (#6055), no-mipmap upload
(#5987), and capacity eviction (#6043) ship in addon-webgl. Replace the
local isolation/backport postinstall mutator with a verify-only gate.
Default new-terminal scrollback to 3000 (closer to VS Code's 1000).
The post-install userns probe runs as root under dpkg/rpm, and root can
create a user namespace even when unprivileged userns is restricted (e.g.
Ubuntu 23.10+ with apparmor_restrict_unprivileged_userns=1). The probe
therefore always passes at install time and leaves chrome-sandbox at 0755,
so on machines where the app itself cannot use the userns sandbox Chromium
aborts: 'The SUID sandbox helper binary was found, but is not configured
correctly ... must be owned by root and have mode 4755'.
Set 4755 unconditionally (the historical electron/chrome default) so the
SUID sandbox works as a fallback whenever the userns + AppArmor path fails.
Refs: binaricat/Netcatty#2607
A cloned SSH session whose first connection fails before the inherited
cd is consumed lost the directory on manual retry: startReconnect(manual)
calls prepareRestoredReconnect(), which clears restoreCwdIntentRef for
non-restored sessions, and the initial-cwd prep only ran at runtime boot.
Re-run prepareInitialCwdIntent() in the manual branch; it no-ops once the
cwd was consumed or when there is no pending inherited cwd.
- hostById is a Map, not a function: my previous commit called it as
hostById(id), throwing TypeError on every Copy Tab / Split. Use
hostById.get(id), and fall back to terminalHosts for ephemeral hosts absent
from the saved-host map (P1)
- classify the DETECTED host.distro (not getEffectiveHostDistro, which returns
a cosmetic manual override) when deciding the network-device probe gate, so a
pinned Linux icon can't re-enable the exec channel on a Huawei/Cisco host (P1)
- clear pendingInitialCwd via app state when the inherited cd is consumed
(report the new cwd through onTerminalCwdChange), so shells that never emit
OSC 7 don't keep the transient seed and re-inject a stale dir on remount (P2)
Adds regression tests: Map-based hostById, network-device gate (deviceType,
detected distro, ephemeral terminalHosts).
- Local clones now inherit the live tracked cwd: expose getSessionRestoreCwd
(the OSC 7 cwd map, which is never mirrored onto session.lastCwd) and feed it
into captureInheritedCwd as the primary source, so Copy Tab / Split of a
running local terminal open in its current dir, not the launch dir (P1)
- Gate the SSH /proc probe like the app's own cwd-probe path: never probe
network devices (Huawei VRP etc. can drop the session on the extra exec
channel) and consult the SSH banner for unclassified hosts (P1)
- Mark the inherited cd consumed only when it is actually sent (from the
restore-cwd consumption callback), so a failed first connect can re-arm the
intent on retry instead of starting in the login dir (P2)
- captureInheritedCwd: race the SSH cwd probe against a 1.5s timeout so a
slow/wedged connection can't block clone/split tab creation; fall back to
lastCwd on timeout/failure
- clone factory: only set pendingInitialCwd for inject-eligible remotes
(ssh/undefined, non-mosh/et), not telnet/serial/mosh/et where it would be
dead, never-cleared state
- AppView: pass a stable useCallback for the new-local-terminal icon instead
of an inline arrow that defeated the TopTabs memo comparison
Reconnect can replace browse connection ids before save completes, so close
handlers that only matched sessionId skipped dirty editors and tore down their
writer. Remap as soon as reconnect is detected and match owners by sftpTabId.
Co-authored-by: Cursor <cursoragent@cursor.com>
- captureInheritedCwd: probe live SSH cwd before stale lastCwd snapshot (Codex P2)
- new-local-terminal icon routes through handleCreateLocalTerminal so sessions get a ConnectionLog + output capture (Codex P2)
- use single-quote import in AppHandlers (Copilot)
Reconnect during save changes the browse connection id before remapSessionId
runs, so session-id-only ownership briefly reports no owned editor and parks
the channel mid-write. Match promoted editors by sftpTabId as well.
Co-authored-by: Cursor <cursoragent@cursor.com>
Re-resolve the pinned pane after ensureRemoteSftpId so editorTabSave
remaps session ownership to the regenerated connection id.
Co-authored-by: Cursor <cursoragent@cursor.com>
Track sftpTabId on editor tabs, remap sessionId after reconnect, and
subscribe SFTP side panels to editor presence only so typing does not
rerender every mounted panel.
Co-authored-by: Cursor <cursoragent@cursor.com>
Drop hostId-based pane fallback so non-owning SFTP writers keep throwing
"connection is no longer available" and editorSftpBridge can dispatch to the
correct owner when multiple useSftpState instances are mounted.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep browse sessions alive when promoting the built-in editor to a tab, and
reconnect the SFTP session on save when the browse mapping is missing.
Closes#2576
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(terminal): suggest Network Device Mode via a session-header tip
When a connected host is classified as a network device (auto-detected
vendor banner/os-release, or a manual distro-icon override) but Network
Device Mode is off, show a non-blocking one-line tip at the top of the
session. The user can enable the mode (persisted to the host) or dismiss
it; we only suggest once per host.
Driven by the host's effective distro so it also previews without real
hardware: set a host's device icon to a network vendor and reconnect.
- domain: shouldSuggestNetworkDeviceMode (exact vendor match, once-per-host)
- application/state: networkDeviceModeSuggestion persistence boundary
- TerminalView: header tip + reserve top offset so it never covers output
- i18n: terminal.networkDevice.tip.* for en/zh-CN/zh-TW/ru
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(terminal): gate network device mode tip to plain SSH sessions
Codex review: the suggestion tip used the host's effective distro (incl.
manual icon override), so a Mosh/ET/serial/telnet session with a
network-vendor classification would still offer it and persist a
host-level deviceType:'network'. The Host Details toggle is exposed only
for plain SSH hosts, so this could set a hidden mode that surprises later
SSH reconnects. Add an effectiveProtocol gate to shouldSuggestNetworkDeviceMode
and pass effectiveTerminalProtocol from TerminalView. (#2367)
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(terminal): make network device tip gate effective and popup/multi-pane safe
Addresses Codex re-review on the tip:
- The effectiveProtocol gate was dead: ctx never carried
effectiveTerminalProtocol, so it was always undefined (unrestricted).
Derive it from the host via resolveEffectiveTerminalProtocol instead.
- Suppress the tip when host updates are unavailable (detached popup has no
onUpdateHost) so Enable can't silently no-op; plumb canUpdateHost through ctx.
- Sync dismiss/enable across panes/windows for the same host via an in-process
subscription in the suggestion persistence module. (#2367)
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(terminal): make network device tip once-per-host, ephemeral- and multi-window-safe
Addresses Codex re-review round 3 on the tip:
- Exclude ephemeral deep-link/quick-connect hosts (isSavedVaultHost): they are
not in the persisted vault array, so Enable would silently no-op even though
canUpdateHost is true.
- Track first display separately from user interaction: record "shown" silently
on first display so simply closing the session no longer re-nags on every
reconnect, while the showing instance keeps the tip until the user acts.
- Propagate resolve/dismiss across separate BrowserWindows (#/session-window)
via a native storage-event listener that diffs newly handled host ids.
(#2367)
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(terminal): don't touch localStorage at import; keep network tip clear of speed dial
Addresses Codex re-review round 4:
- P1: networkDeviceModeSuggestion read localStorage at module scope
(knownHandledIds initializer), which threw ReferenceError under the Node
test runner and broke `npm test`. Initialize it lazily inside the existing
browser-only window guard instead.
- P2: when the host info bar is hidden and search is closed the full-width tip
strip overlapped the compact speed-dial toggle (top-right) and swallowed its
clicks. Reserve right-side clearance for the toggle via a pure
resolveNetworkDeviceTipRightInset helper (with unit test).
(#2367)
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(terminal): drop network tip once the host is classified elsewhere
Addresses Codex re-review round 5: while the tip is latched open, enabling
Network Device Mode from Host Details or a synced pane updates the host to
deviceType:'network', but the eligibility effect's early return kept the tip
(and its now-redundant Enable action) visible. Clear the latch whenever
host.deviceType becomes 'network'. (#2367)
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(terminal): persist only the deviceType override, not the effective host
Addresses Codex re-review round 5 (P1): the tip's Enable spread the *effective*
session host (group defaults / proxy profile already materialized) into
onUpdateHost, so mergeTerminalHostUpdate froze every inherited field
(credentials, appearance, proxyConfig) as host-level overrides and later
group/profile changes stopped propagating. Send a sparse
{ id, deviceType: 'network' } update instead. (#2367)
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(terminal): move network device tip lifecycle into a state hook; per-host keys
Addresses Codex re-review round 6:
- P1 (architecture): TerminalView no longer imports the persistence API or owns
the handled-state subscription/eligibility/writes. Extract
useNetworkDeviceModeSuggestion under application/state; the component consumes
{ visible, enable, dismiss } and keeps only view glue (sparse host update +
toast), per AGENTS.md's layer boundaries.
- P2 (cross-window clobber): persist one key per host instead of a single shared
array. localStorage has no atomic read-modify-write, so concurrent appends
from two windows could drop a marker; independent keys never clobber, and the
storage event recovers the host id directly from the key.
(#2367)
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Main-process progress IPC was ~10 Hz and each tick remapped the full
transfer list plus published the global center, pegging the main window
renderer at 100%+ CPU during large copies. Raise IPC throttle to 250ms/1MB
and coalesce progress-only React/store patches to one paint per frame.
Restart resume and bulk dedicated opens were still calling openSftp, which
silently borrowed a parked/shared terminal transport. That reattached
checkpoint resume to a lease-bound conn instead of an independent dial.
Dedicated transfer sessions now pass reuseTransport:false so they always
open a fresh vault SSH connection.
Remove ensureTransportShape upgrade of bare { count, conn } descriptors.
Borrow requires a registry transport from createConnectionRef/createTransport.
Update tests to construct real transports instead of hand-rolled count objects.
acquireConnectionRef / borrowTransport now ensure leases Map and registry
fields exist on pre-registry { count, conn } descriptors so older tests and
in-memory sessions do not throw on transport.leases.has.
Password-only automatic login now digests the password so vault rotation
invalidates parked reuse, while multi-material auto keeps publicKey-based
matching so key-first transfer opens still find the shared transport.
Auth material fingerprints are method-scoped so key-first / password-retry
SFTP opens still match a parked multi-material transport. Password rotation
invalidates password auth; key/cert content rotation invalidates those methods.
Digest password/privateKey/certificate/passphrase/identity paths into the
auth fingerprint so key or password rotation under the same keyId invalidates
parked transports. Secrets are not retained on the transport registry — only
the digest is indexed.
Bind-failure cleanup must remove close/error handlers while conn is still
attached; releasing first cleared conn and left stale listeners on the
shared transport after local bind errors.
Add resolveSftpTransferSourceSessionId that walks all reusable terminal
sessions instead of the picker one-per-hostId list, so transfers can still
borrow a live transport when another tab for the same hostId has a different
endpoint.
Dedicated dials that registered a transport lease were leaving the lease
held after local/dynamic server errors. Always release managed leases so
authenticated connections are not orphaned until quit.
Record remote bind target before forwardIn so cancel-during-listen always
unforwards (or discards). Shared PF transport errors discard instead of park;
identical idle TTL re-sends no longer reset idle deadlines.
Remote unforward callback errors now discard the shared transport like
timeouts, so a half-open remote listen cannot be idle-parked. Shared
port-forward reuse attaches close/error lifecycle before bind so mid-listen
transport drops cannot leave zombie local listeners.
Shell and parked-shell lookup now use endpointAllowsReuse(..., "shell") so
disabling ForwardAgent cannot reattach to a warm conn that still exposes the
local agent. SFTP/port-forward keep asymmetric channel reuse so they can still
borrow a ForwardAgent-enabled terminal transport.
Store agentForwarding on the session/transport endpoint and apply an
asymmetric reuse check: opens that need ForwardAgent reject parked conns
that never enabled it, while SFTP/port-forward can still borrow a
ForwardAgent-enabled terminal transport.
Transfer pool source sessions now require hostname/user/port parity so
multi-tab or overridden endpoints do not borrow the wrong shared transport.
Remote forward cancel-during-forwardIn and stop paths share one timed
unforward helper that discards the transport when the server listen stalls.
Avoid shell/sftp lease id collisions across reconnect generations, and
discard the shared transport when remote unforward cannot be confirmed
within the stop timeout.
If the user stops a shared local/dynamic/remote forward while listen or
forwardIn is still pending, close the listener and resolve cancelled
instead of re-registering the tunnel as active.
Track accepted forward sockets per tunnel and destroy them on stop, end
already-dead transports instead of parking them, and fingerprint
command-proxy routes so edits invalidate warm reuse.
Catch transport registration failures inside async bind callbacks so a
dead shared socket cannot crash the main process, and fingerprint jump
hops by both vault id and effective endpoint/proxy.
Include proxy/jump profile identity in transport keys, normalize loopback
remote-forward addresses, detach shared listeners on cancel, and avoid
MFA-retrying dedicated dials for local bind failures.
Attach error/close lifecycle to reused port-forward transports, match
wildcard remote binds by expected listen address, and end unusable
transports whose sockets are destroyed during lookup.
Scope transport keys by vault hostId so different profiles cannot share
an authenticated connection. Fix createConnectionRef to preserve hostId.
Route remote-forward accepts by destPort/bind address, await unforwardIn
before success, return stopAllPortForwards as a promise, and update
async cancel tests.
Introduce a main-process SSH transport registry (borrow/return leases,
ControlPersist-style idle park) shared by shell, SFTP, transfer, and
port-forward. Transfer pool becomes channel concurrency only; remove the
separate transfer-pool idle TTL UI in favor of a single SSH keep-alive
setting. Register dedicated SFTP dials, reuse parked transports, and fix
TTL 0 (park until quit), jump-host endpoint keys, and remote unforwardIn.
* fix(terminal): suppress autocomplete popup on alternate screen (#2530)
Codex CLI's TUI (and other full-screen apps like vim/htop/less) render
their own input UI on the alternate screen buffer. Netcatty's autocomplete
only gated on its shell-prompt heuristic, which codex's composer line
matches — so typing "/" inside codex popped up Netcatty's path/command
list on top of codex's slash-command menu.
Bail out of fetchSuggestions (the single query path for both popup and
ghost text) when term.buffer.active is the alternate buffer. Reuses the
existing isTerminalAlternateScreenActive helper already used by hibernate.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(terminal): keep simple alt-screen autocomplete ban (#2530)
Drop the bloated TUI-vs-multiplexer heuristics from the codex auto-fix loop.
Disable autocomplete unconditionally on the alternate screen buffer — same
simple tradeoff as other hosts; tmux/screen attach has no Netcatty completions.
* fix(terminal): drop alt-screen completions after async lookup too
Same unconditional policy as the entry gate: if the buffer switched to
alternate while getCompletions was in flight, discard results so popup/ghost
cannot paint over a TUI. #2530
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(ai): route auto-detected network devices to raw PTY exec (#2367)
AI exec paths (Catty, worker, MCP) decide whether to send commands raw
(no shell wrapping) based on meta.deviceType === "network". That field
only reflected the manual Network Device Mode toggle (host.deviceType),
so auto-detected switches (Huawei VRP, Cisco IOS, …) still went through
the POSIX shell wrapper. Their vendor CLIs reject the wrapped command,
the start marker never arrives, and the call stalls until the startup
timeout.
Report deviceType: "network" when the detected distro/vendor classifies
as a network device too, mirroring the terminal's own gating. This also
gives the AI system prompt vendor-CLI guidance before the user manually
flips the toggle. Mosh/ET sessions stay suppressed since they require a
shell-backed PTY.
Extract the pure buildAITerminalSessionInfo helper into its own module
for testability and add unit coverage.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(ai): guard network-device classification against substring false positives
Address Codex review: replace non-ASCII ellipsis with ASCII per the
ASCII-only convention, and add a regression test proving classification
is exact-match against the vendor id list (so a custom distro that merely
embeds "cisco"/"huawei" is not treated as a network device).
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* perf(vault): virtualize host pickers for large inventories
Virtualize Select Host, Quick Switcher, SFTP host picker, and workspace
dialogs so thousands of hosts only mount a viewport window. Shared scroll
and keyboard clamp helpers keep selection and filter-while-scrolled paths
stable under virtualization.
* fix: address Codex review on PR #2553
* fix(vault): plugin row heights and Select Host keyboard types
Give Quick Switcher plugin command/view rows a taller virtual slot so
two-line chrome does not overflow into the next absolute row. Narrow
Select Host navigable rows to group|host so Enter keyboard activation
typechecks cleanly.
* fix(vault): finish host-picker review feedback
Clip and truncate fixed-height Quick Switcher / workspace rows so long
labels never overlap the next virtual slot. Wire Select Host listbox
aria-activedescendant to stable option ids for keyboard AT feedback.
* fix: address Codex review on PR #2553
* fix: address Codex review on PR #2553
* fix: address Codex review on PR #2553
* fix: address Codex review on PR #2553
* fix(vault): clear remaining host-picker codex findings
Declare multi-select listboxes, drop empty SFTP Hosts headers when only
connected rows remain, size the SFTP list to content under a 360px cap,
and give Quick Switcher / workspace pickers a definite max scroll height
so large inventories remain reachable.
---------
Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com>
Packaged Netcatty launched from Finder often has process.cwd() === "/", so
merging .cursor/mcp.json failed silently and netcatty-remote-hosts never
mounted. Point CLI turns at Netcatty temp and fail loudly on merge errors.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: switch .gitignore to allowlist layout
Default-deny root paths so a mistaken git add cannot pick up secrets,
agent state, or build artifacts. Re-include only known source paths and
keep the existing classical denylist for nested outputs.
Closes#2534
* fix: address Codex review on PR #2554
---------
Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com>
* feat(snippets): open quick add/edit as a centered modal
Replace the scripts side-panel AsidePanel drawer with a Dialog so
snippet create/edit no longer slides over the terminal workspace, while
keeping the same form fields and shortkey capture behavior.
* fix: address Codex review on PR #2552
---------
Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com>
* feat(vault): optimize large imports and vault rendering (#2526)
* fix(vault): address import review findings
* fix(vault): preserve filtered selection and import consistency
* fix(vault): complete import review fixes
* fix(vault): make import cancellation concurrency-safe
* fix(vault): preserve concurrent edits during import rollback
* fix(vault): handle concurrent saves and reset import target
* fix(vault): drop empty source groups after targeted import
* fix(vault): preserve sidebar accessibility check
* fix(vault): correct group counts and selection semantics
* fix(vault): preserve imports across concurrent saves
* fix(vault): preserve concurrent group changes during import
* fix(vault): close concurrent import edge cases
* fix(vault): retry rollback and enable tree keyboard selection
* fix(vault): retain concurrent sources and visible selections
* fix(vault): merge imported sources with persisted state
* fix(vault): serialize duplicate managed imports
* fix(vault): keep imports non-blocking and grouped views responsive
* fix(vault): preserve concurrent managed sessions
* fix(vault): preserve concurrent groups and keyboard access
* fix(vault): serialize cross-window import commits
* fix(vault): make import commits atomic
* fix(vault): surface import rollback failures
* fix(vault): recover import transactions safely
* fix(vault): commit imported hosts atomically
* fix(vault): preserve unfinished import recovery
* fix(vault): wait for edits and preserve keyboard focus
* fix(vault): protect group settings and lock fallback
* fix(vault): keep virtual focus aligned
* fix(vault): complete keyboard navigation semantics
* fix(vault): finish keyboard navigation
* fix(vault): close large-vault concurrency gaps
* fix(vault): finish large-vault interaction safety
* fix(vault): harden filtered large-vault flows
* fix(vault): keep managed deletion atomic
* fix(vault): coordinate managed file deletion
* fix(vault): close cross-window managed races
* fix(vault): refresh persisted managed state
* fix(vault): preserve distinct imported sessions
* fix(vault): keep failed progress accurate
* fix(vault): keep search tree collapsible
* fix(vault): close final review gaps
* fix(vault): reveal filtered tree matches
* fix(vault): close final review edge cases
* fix(vault): preserve concurrent import changes
* fix(vault): serialize all vault saves under one cross-window lock
Hold the shared import lock for ordinary host/group/key writes and the
full import commit path so check-then-write can no longer interleave
across windows. Make the lock re-entrant in-window to avoid nested
deadlocks, and announce large tree item positions for screen readers.
* fix(vault): keep concurrent saves serialized and fix tree ARIA siblings
Stop treating same-window concurrent work as nested lock re-entry; nested
helpers skip re-acquire only when the outer critical section already owns
the lock. Announce treeitem position among true siblings at each level.
* fix(vault): require explicit lock handle for nested saves
Independent concurrent vault writes always queue on the shared lock.
Nested import/deletion helpers continue only when they pass the active
lock handle from their outer critical section.
* fix(vault): avoid lock deadlocks with encrypt-then-write saves
Encrypt outside the shared lock and only acquire it for the final write so
importers waiting on pending work cannot deadlock on lock acquisition.
Retry plugin commits outside the lock when concurrent edits intervene, and
rebase imports onto in-memory host edits when disk is still stale.
* fix(vault): release import lock before concurrent-edit retries
Drain encrypt work under the lock and full disk writes only outside it.
When an import or group deletion is superseded by a concurrent edit, release
the shared lock so that edit can finish, then retry. Functional group updates
now base on the latest in-memory snapshot while a write is still pending.
* fix(vault): tidy import dialog destination and SecureCRT prompts
Move import destination settings behind a bottom-left control with its own
step, and keep SecureCRT guidance in a dedicated chooser instead of inside
the format tile.
* test(vault): fix import dialog layout guard test under ESM
* fix(vault): simplify import destination footer control
Replace the heavy two-line destination card with a single quiet text link
that shows the current placement choice.
* fix(vault): simplify import destination settings page
Drop the nested card and duplicate headings. Show only the three placement
choices, finish immediately for preserve, and keep Done only when a group
name is needed.
* fix(vault): simplify SecureCRT chooser and iconify destination options
Remove redundant SecureCRT prompt copy while keeping the two source cards
with icons. Add small folder icons to the import-location choices so that
page is less plain.
* fix(vault): use SecureCRT logo in import chooser header
* fix(vault): drop border around SecureCRT import logo
* fix(vault): clarify import location footer with icon and label
Show a folder icon plus "Import location · current choice" so the control
is recognizable without becoming a heavy card again.
* fix(vault): remove border from import progress status icon
* fix(terminal): keep Enter reconnect working after disconnect
Disconnected sessions advertised "Press Enter to reconnect" but often
swallowed the key when focus left xterm (body blur or dialog controls),
and an open terminal search bar globally hid the hint and disabled the
shortcut. Give the disconnected overlay a focus sink + local Enter
handler, and stop treating search-open as a global reconnect gate while
still leaving real buttons/inputs alone.
Closes#2544Closes#2546
* fix(terminal): stop disconnected dialog focus theft
Only claim Enter-reconnect focus when it is already lost or still inside
this session tree, and restore xterm focus when the overlay unmounts.
Avoid stealing focus from other panes/side panels on disconnect.
* fix(terminal): restore dialog focus only when overlay leaves
Split claim/restore effects so showLogs/error rerenders no longer
yank focus off dialog buttons onto xterm mid-interaction.
* fix(terminal): restore Enter-reconnect focus in popup terminals
Popup Terminal windows lack data-session-id ancestors. Walk up from
the dialog to the local xterm root so claim/restore still work there.
* fix: address Codex review on PR #2550
* fix: address Codex review on PR #2550
* fix: address Codex review on PR #2550
* fix: address Codex review on PR #2550
---------
Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com>
- Remove sdkRequestRuntimes entries on chat session teardown to prevent
stale runtime references accumulating across sessions.
- Cancel pending elications (resolve with deny) when the owning chat
session closes, so elicitations never hang indefinitely.
- Use crypto.randomUUID() as fallback elicitation id when the SDK does
not provide one, avoiding collisions on empty-string keys.
- Surface interrupt failures via console.debug for easier diagnostics
without changing best-effort stop behavior.
Behavior preserved:
- Session key fingerprinting (config-based reuse) unchanged.
- Stop/interrupt flow: signal-first, then queryRef.interrupt().
- Permission mode routing (auto/confirm/observer) unchanged.
- Stream handler event protocol and dedup logic unchanged.
- Upgrade @tencent-ai/agent-sdk to 0.3.230 (elicitation support)
- Add CodebuddyElicitationCard with schema-driven form fields and
accept/decline/cancel actions; wire full event chain from driver
through shared elicitation store to ChatMessageList
- Pass agents/thinking/effort to V2 SessionOptions instead of forcing
query() fallback; include them in the session fingerprint
- Close stale V2 session before query() fallback to prevent context
divergence when resuming the same persisted conversation
- Forward codebuddyOptions from renderer config through IPC to the
main-process driver context
- Add i18n strings (en/ru/zh-CN/zh-TW) and test coverage
- Fix test process hang: stale-session test no longer triggers real SDK
import which spawned a live CodeBuddy backend and prevented exit
- Fix plugin management API: remove non-existent removePlugin, correct
enablePlugin/disablePlugin second param to marketplace string
- Wire elicitation handler in production path via session manager's
pending map; expose elicitation-response IPC to renderer
- Skip V2 Session when query-only options are configured (maxBudgetUsd,
sandbox, fallbackModel, enableFileCheckpointing, agents, outputFormat,
effort, thinking) so settings are not silently ignored
- Expose plugin/mcp-status/account-info IPC channels in preload + types
- Add renderer handling for hook, elicitation-create/complete events
OpenSSH keeps the first HostName value. Setting HostName to the jump
alias under the forced -F session config blocked later Include of
~/.ssh/config from resolving HostName aliases, and could diverge from
the vault snapshot built via ssh -G against the real config.
Drop the process-lifetime ssh -G cache so HostName/HostKeyAlias and
known_hosts path edits are observed on the next connect. Keep a
per-prepare Map only so target and jump discovery in one session can
share probes.
Reset Host context with Match all so Included user/system ssh_config
applies to every hop, not only the trailing jump Host stanza. Prepend
the ssh -F wrapper onto the session PATH from options.env so custom
ProxyCommand helpers remain available.
-F replaces both the per-user and system-wide ssh_config. Append
Include directives for the real ~/.ssh/config and system ssh_config
after session Host blocks so HostName aliases and other user settings
still apply while session ProxyJump/vault overrides keep first-match
priority.
Avoid removing the current target's system known_hosts entry when the
vault only contains unrelated pins, so strict ET stats probes keep
working for already-trusted hosts.
When rewriting vault pins under a resolved HostName, retain
[host]:port encoding for non-default ports so OpenSSH lookup finds the
vault key. HostKeyAlias continues to use the bare default-port form.
OpenSSH ignores $HOME for user config. When ET generates a private
config for ProxyJump and jump host-key policy, put a PATH-fronted ssh
wrapper that always passes -F so interactive ET and follow-up probes
both honor the vault-authoritative Host stanzas.
When the connection target is an SSH-config alias, OpenSSH records and
looks up host keys under the resolved HostName. Rewrite the vault pin
and filter selectors accordingly so accept-new cannot ignore the vault.
OpenSSH ignores $HOME for user config on POSIX, so pass -F to
execOnEtSession so ProxyJump host-key policy is actually loaded. Cache
real ssh -G probes briefly to limit main-process blocking.
Disable KnownHostsCommand when enforcing vault authority so dynamic trust
cannot override vault pins. Rewrite multi-host known_hosts lines instead
of dropping them so unpinned hop patterns remain trusted.
ET --ssh-option applies only to the destination hop. Keep vault-authoritative
UserKnownHostsFile and StrictHostKeyChecking in the ProxyJump Host stanza so
the separate jump OpenSSH process still enforces key verification.
Apply vault-authoritative known_hosts through --ssh-option so OpenSSH
enforces it even when a temp-HOME Host config is not read. Reject
fingerprint-only publicKey tokens when building vault pins.
Write vault pins under the effective HostKeyAlias when present, filter
system entries for both alias and hostname, and invoke ssh -G with -p/-l
so %p/%r known_hosts path tokens expand correctly.
Build separate authoritative known_hosts files for target and jump hops
using each hop's hostname for ssh -G discovery. Reuse the same vault
authority builder for requireTrustedHost exec probes so system pins
cannot override vault keys on stats/shell follow-ups.
Apply vault-authoritative known_hosts only on hops the vault pins, so
unpinned ET jump/target hops keep the persistent user known_hosts.
Also merge ssh -G UserKnownHostsFile paths and preserve spaced path
names when discovering configured trust files.
Discover administrator-configured global known_hosts paths via ssh -G
when building the vault-authoritative snapshot, so replacing
GlobalKnownHostsFile does not drop custom admin pins or revocations.
Evaluate OpenSSH positive/negative host patterns when filtering system
known_hosts so vault-covered hosts do not drop jump-host pins that use
negation. For ET follow-up ssh, do not prepend accept-new when the
session already set StrictHostKeyChecking (including verifyHostKeys=no).
Only switch to a session known_hosts snapshot when the vault pins the
active target or jump host, so ET accept-new still persists first-seen
keys otherwise. Filter OpenSSH wildcard patterns covering vault hosts
and keep @revoked markers so admin revocations remain authoritative.
OpenSSH accepts a match from any known_hosts source, so a rotated system
pin could override a vault pin. Build an authoritative snapshot that
keeps vault keys exclusive for vault-covered hosts, force Mosh
StrictHostKeyChecking=ask, and quote trust-file paths that contain
whitespace.
Prefer waking the terminal runtime so executor encoding (including
bracketed-paste mode checks) is used, instead of inventing paste markers
on a blind backend fallback path.
Preserve OpenSSH default global known_hosts when injecting vault pins by
merging both into a single GlobalKnownHostsFile. When verifyHostKeys is
off, neutralize UserKnownHostsFile and GlobalKnownHostsFile with an empty
file so StrictHostKeyChecking=no cannot still block password auth against
a stale pin.
When multi-tab fan-out falls back to a direct backend write (hibernated
pane), wrap multi-line paste/noAutoRun payloads so newlines do not submit
as Enter.
Mosh and Eternal Terminal bootstrap via system OpenSSH, so Netcatty vault
known_hosts were ignored and a key rotated after a prior SSH connect could
still be accepted. Inject usable vault entries as GlobalKnownHostsFile and
honor verifyHostKeys=false so key-change interception matches SSH trust.
Closes#2501
Skip terminal focus steal during multi-tab snippet fan-out and restore the
original pane afterward. Trap Tab focus inside the quick-add drawer and
restore the opener element on close.
Restore modal dialog contract for quick-add (role/data-state/escape),
harden package path validation, skip sensitive sessions on multi-tab
snippet sends, and show a single run-on-all-tabs action for plain snippets.
Address #2502: bind shortkeys when quick-adding snippets, add package
creation via a split + menu, convert quick-add to a right-side drawer,
and let code snippets run on all workspace tabs from the context menu.
Capture tabId/connectionId at paste time and resolve external folder/entry
uploads against that pane after awaits so mid-transfer tab switches cannot
retarget remaining multi-folder items to another host. Prefer the pinned
pane host when reconnecting and reconnect by tab id without clobbering the
active tab's lastConnectedHost.
PR #2509's TransferRuntime unify accidentally regressed per-task external
upload cancel (#2495), clipboard confirm close-before-upload (#2478), and
main SftpView interactive browse sessions (#2498). Restore those behaviors
on top of the process-level control plane, keep Pausing copy, and gate
broadcastGlobalTransferEvent so bare Node unit tests do not download Electron.
When Observer → Confirm/Auto → Observer, drop the inactive ask/agent
in-memory session so switch-back cannot resume a stale thread and skip
the intervening conversation history.
Update Settings copy to use `cursor-agent login` and note that a saved API
key is retained while CLI login mode is active.
Co-authored-by: Cursor <cursoragent@cursor.com>
Re-apply the stored key explicitly when toggling CLI login and API Key so
mode switches cannot drop a previously saved credential.
Co-authored-by: Cursor <cursoragent@cursor.com>
Stop falling back to bare `agent` on PATH so other CLIs (for example Grok)
cannot be mistaken for Cursor during auth probes, model presets, or turns.
Co-authored-by: Cursor <cursoragent@cursor.com>
Skip duplicate stream-json assistant flushes that include model_call_id, and
isolate Cursor CLI resume sessions by ask vs agent so switching Observer to
Confirm/Auto starts a writable turn instead of keeping Ask.
Only automation bot PRs re-check source-issue pending comments before
mark-ready, so maintainer Fixes-only PRs are not stuck draft. Write
WebSearch/WebFetch denials after the isolated research pass so classify
and follow-up research can still call those tools.
Pause and resume now go through one process-level control plane that survives
panel/terminal close: TransferRuntime + soft latches/epochs/walk registry,
soft-then-hard resume, dual-epoch-safe progress, and no-owner soft control.
Load @xterm/addon-image in the shared xterm runtime so remote programs can
draw real raster images (kitten icat, chafa --format kitty, image previews
in file managers and TUIs) instead of ANSI approximations.
- domain/terminalInlineImages.ts owns the limits and turns TerminalSettings
into addon options; disabled settings resolve to null so the addon is not
loaded at all and the terminal keeps advertising no graphics capability.
- The addon is loaded right after term.open, before the WebGL renderer, so it
patches IRenderService.setRenderer first and its canvas layers survive every
DOM <-> WebGL swap (deferred WebGL, soft-hide suspend, context-loss rebuild).
It is disposed before term.dispose so bitmaps never outlive their terminal.
- Hibernate restores a tab from a text-only snapshot, which cannot carry image
data. XTermRuntime.hasInlineImages reads the live image cache, and Terminal
degrades such a session to soft-hide: hibernateRuntime takes the soft-hide
branch, canFinishHibernate re-checks after every async step, and an eviction
request leaves the session soft-hidden. The block lifts by itself once the
cache is emptied by a terminal reset or FIFO eviction.
- Settings > Terminal > Inline Images exposes the master switch, per-protocol
switches (Kitty / SIXEL / iTerm IIP) and the memory limits, translated for
en, zh-CN, zh-TW and ru. Protocol switches cloud-sync; the memory limits stay
local like the other per-device renderer tuning.
Co-authored-by: Wine Fox <fox@ling.plus>
Issue titles and bodies are auto-validated; agents reading only AGENTS.md or
CLAUDE.md were missing the [Bug]/[Feature]/[Other] prefix and template
requirements. Point all three guides at the shared .github templates.
Closes#2492
When scanning the system known_hosts file or importing from a file,
exclude well-known public service hostnames (github.com, gitlab.com,
bitbucket.org, Azure DevOps) from being added to the Known Hosts vault.
These entries represent third-party services rather than user-managed
hosts and are not meaningful in an SSH host management context.
- Add PUBLIC_SERVICE_HOSTNAMES exclusion list
- Apply filter in both system scan and file import paths
- Show a toast notification when entries are filtered
- Add locale strings for filtered count (en, zh-CN, zh-TW, ru)
Make folder pause latch-first with barriers and a child pause watchdog so
workers stop claiming files after soft-drain; soft-resume live walks instead
of full rewalks. Surface path conflicts with toasts, keep pause progress
visible, and show a resume spinner. Restore verified shell rm -rf with SFTP
confirmation for fast directory delete, and prefer reconnect over wiping the
pane to the host picker when listing fails on some hosts.
Do not paint transferring when every backend resume misses after restart.
Refuse starting or resuming a second job with the same source and target
while another is still active, matching FileZilla-style single-writer
destination handling.
Track UploadController instances per transfer task so queue cancel of one
external upload does not abort another concurrent paste/drop upload.
Scope pending conflict prompts to the owning controller as well.
SFTP queue pause/resume/cancel now call sftpTransferCenterStore (same as the
global transfer center), harden action-button pointer handling, retry
"cannot be paused yet", and keep progress updates from undoing a paused row.
Paint paused optimistically, stop counting paused rows as "active", freeze
visible progress while paused, and soft-drain concurrent pause in ~50ms
without long writeStream waits on the contiguous-range path.
* fix(sftp): close clipboard upload dialog before transfer
Clear the confirmation request before awaiting the upload so the modal
overlay no longer blocks side-panel and standalone SFTP during large
clipboard transfers (issue #2478). Keep the existing background transfer
path and post-upload refresh; guard double-clicks and skip refresh on
transfer failure.
* fix(sftp): scope clipboard confirm guard to request identity
Only block double-clicks on the same confirmation request so a later paste
can still Upload while an earlier background transfer is in flight.
Prefer contiguous bridge checkpointBytes over high-water transferred for
resume offsets, read remote modifyTime for meta pause identity, clear
deferredSparseTruncate on failed pause unpause, and forward fingerprint
fields through drag-drop upload progress.
Wait for in-flight concurrent ranges on resume with a deadline and cancel
checks so stalled SFTP requests cannot hang Resume forever. Propagate
sourceFingerprint through external-upload progress updates.
Embed self-hosted light/dark star history charts after api.star-history.com
stopped serving public embeds, and add a weekly workflow that refreshes them
via RELEASE_TOKEN.
Confirm pause only after a cheap size+mtime source identity is recorded
and published for transfer-center persistence. Wait for in-flight
concurrent ranges before sparse-tail truncation on resume, and refuse
truncate after cancel during that wait.
Pause felt stuck because the panel hid the pausing state, and the
backend waited for every in-flight range plus a full source fingerprint
hash. Soft-drain concurrent ranges, skip blocking fingerprint work, and
show clear pausing feedback in the panel and transfer center.
Raise UPLOAD_TRANSFER_CONCURRENCY from 32 to 64 so the default in-flight
window is ~2MiB, matching Electerm transfer.js / ssh2-style defaults while
keeping the safe 32KiB chunk size.
Remove the unused uploadFileResumableFast alias and the always-null
activeSftpUploads.stream field from the buffer-upload cancel path after
serial WriteStream crawl was fail-closed for #2449.
On promoteLocalTransfer failure after publish, only unlink/restore when the
target still matches the published ready identity. Concurrent post-publish
replacements leave the concurrent file and backup intact.
Do not publish a staged local download over a concurrent file that appears
at the target path after the original was moved to backup. Leave the
concurrent target in place and retain the backup for recovery.
Always content-hash remote destinations before staged promotion (even when
ino/fileId is stable), pass AbortSignal through destination hashing so cancel
closes read streams, and revalidate local download backups before publish.
* fix(sftp): raise upload WRITE fanout for higher throughput
Increase UPLOAD_TRANSFER_CONCURRENCY from 8 to 32 so resumable and
fastPut uploads keep ~1MB in flight (32 × 32KB). Keeps the safe 32KB
packet size from #2022 while closing most of the gap with ssh2/Electerm
defaults on multi-ms RTT paths (#2449).
* fix(sftp): never fall straight into serial WriteStream uploads
Serial ssh2 WriteStream/put is RTT-bound (one 32KB WRITE at a time) and
is the usual cause of sub-MB/s speeds, not the fanout setting alone.
- Prefer concurrent WRITE ranges on an isolated channel, then fastPut,
then concurrent ranges on the shared browse channel; serial stream is
last resort only.
- Do not swallow post-upload size/content verification into fallback.
- uploadLocal and memory-buffer progress uploads use fastPut with the
configured fanout instead of serial put.
- Add a regression test that sudo/shared sessions stay pipelined.
* fix(sftp): fail closed instead of serial WriteStream fallback
Align with OpenSSH/Electerm/WinSCP: default SFTP uploads stay on
pipelined WRITE strategies only. When concurrent isolated, fastPut,
and concurrent-shared all fail, throw the last error — do not crawl via
serial createWriteStream/put (RTT-bound, #2449).
uploadLocal and writeSftpBinaryWithProgress require fastPut; no silent
serial put path. Tests assert createWriteStream is never invoked on
forced concurrent failure.
* fix(sftp): restore pipelined upload on session-backed clients
Session-backed SFTP clients (openForSession / terminal reuse) are not
ssh2-sftp-client instances and lacked client.fastPut after serial put
was removed. Expose fastPut via the raw channel and route uploadLocal /
writeSftpBinaryWithProgress through pipelinedUploadLocalFile so
session-backed paths keep concurrency fanout.
* fix(sftp): make pipelined fastPut abortable and throttle progress
Address codex review on the upload fanout PR:
- Prefer a disposable SFTP channel for cancelable uploads so
cancelSftpUpload / AbortSignal can end the transfer immediately.
- Honor abort signals for session-backed fastPut instead of ignoring
them after the call starts.
- Restore 100ms / 1MB progress throttling for binary-with-progress
fastPut step callbacks.
* fix(sftp): wait for shared-channel fastPut to settle on abort
When abort uses a non-disposable browse/sudo SFTP channel, do not reject
until fastPut's callback fires so temp/staged cleanup cannot race
in-flight WRITEs. Disposable channels still end immediately.
* fix(sftp): harden fastPut abort, channel errors, and pause ads
Codex re-review fixes:
- Attach SFTP channel error listeners during fastPut to avoid unhandled
EventEmitter crashes on disposable channels.
- Wait for fastPut callback (with short force timeout) after disposable
channel abort so temp files are not unlinked while still open.
- Mark pause unsupported during transferBridge fastPut fallback; restore
pause ads for concurrent-shared.
- Create AbortController before staging ArrayBuffer to disk and pass
signal to writeFile.
* fix(sftp): settle fastPut after errors and verify fastPut content
Codex P2 follow-ups:
- Channel errors during fastPut wait for the callback or a short force
timeout before settling (same pattern as cancel) so temp files are not
unlinked while still open.
- transferBridge uploadViaFastPut uses the same settle discipline so
fallback strategies do not overlap a still-running isolated fastPut.
- Resumable fastPut fallback captures and rechecks a local content
fingerprint before promoting success.
* fix(sftp): reset fastPut checkpoint and verify staged size
Codex re-review:
- After isolated fastPut fails, reset checkpointBytes so concurrent-shared
does not resume past non-contiguous progress holes.
- Rethrow noTransferFallback/sourceChanged after fastPut fingerprint check.
- uploadLocal verifies staged remote size against local before rename.
* fix(sftp): avoid hang on cancelled close and unsafe fastPut checkpoints
- Skip/timeout remote CLOSE after disposable channel abort/failure so
concurrent upload finally does not hang waiting on a dead channel.
- fastPut progress updates UI without advancing durable checkpointBytes
(non-contiguous chunk progress is not crash-safe resume state).
* fix(sftp): address remaining remote Codex review notes
- Fall back to the shared browse SFTP channel when opening a disposable
upload channel fails (MaxSessions / subsystem refused), unless aborted.
- Force-settle concurrent range uploads after cancel if in-flight WRITEs
stall on a non-disposable shared channel (2s grace).
* fix(sftp): bound shared-channel fastPut cancel with force-settle
Shared browse/sudo channels cannot call sftp.end() on abort without
killing the session, but a stalled fastPut callback must still settle.
Schedule the same 2s force-finish used for disposable channels, and
cover it with a stall test that asserts the shared channel is not ended.
* fix(sftp): force-settle shared-channel fastPut on channel error
Shared browse/sudo channels cannot be ended on error, but a stalled
fastPut callback after an error event still must not hang forever.
Schedule the same 2s force-finish for shared-channel errors without
calling sftp.end(), and cover it with a stall regression test.
* fix(sftp): stage binary uploads and cleanup shared-channel cancel
writeSftpBinaryWithProgress now uploads to a remote .part path and
renames into place, so a stalled shared-channel fastPut cannot keep
mutating the final destination after cancel returns. Shared-channel
force-settle also best-effort unlinks the in-progress remote target
without ending the browse/sudo session.
* fix(sftp): harden staged upload replace semantics
Default to stage+rename for cancel-safe finals, but:
- write symlinks in-place so the link node is not replaced
- restore previous mode bits after rename overwrite
- fall back to in-place when creating a .part is permission-denied
Covers mode restore, symlink, and parent-dir permission regressions.
* fix(sftp): encode after staging names; never unlink finals
- Build stage/backup names from the logical path, then encode (GB18030-safe)
- Existing files/symlinks write in-place to preserve inode/metadata
- Shared-channel force-settle only unlinks .netcatty-upload-*.part paths
* fix(sftp): keep AbortSignal aborted in dispose cancel handler
When cancel replaces transferControl.abort for a disposable channel,
also abort the AbortController so staged rename/promotion cannot
complete after the UI has already cancelled the upload.
* fix(sftp): stage regular overwrites; throttle fastPut progress
Regular files always stage+rename (cancel cannot mutate finals on shared
channels) and restore mode after promotion. Symlinks stay in-place.
Clip stage/backup basenames to NAME_MAX and stop forcing every fastPut
step progress event through IPC.
* fix(sftp): tighten staging plan and permission fallback
- Without native lstat, existing paths write in-place to avoid clobbering links
- Permission fallback uses SFTP/error codes only (not message substrings)
- Cover size-mismatch paths whose names contain words like "access"
* fix: address Codex review on PR #2452
* fix(sftp): write SCP uploads through existing symlinks in-place
SCP stage+rename was replacing the symlink node itself. Detect symlink
destinations first and upload through them so the link target is updated.
* fix(sftp): drain shared writes before cleanup
* fix(sftp): track generated upload stages explicitly
* fix(sftp): preserve upload recovery paths
---------
Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com>
- Format recovery dispatches triage before clearing invalid-format
- Shared CJK-friendly title rules; codex label terminal state machine
- Research steps in classify; agent TITLE + PR body for bot PRs
- Single @codex review via CODEX_REQUEST_GITHUB_TOKEN (binaricat)
Use TRIAGE_GITHUB_TOKEN for Codex review loop jobs. Treat convert-to-draft
and mark-ready permission errors, and closed PRs, as warnings so @codex
still runs instead of failing the whole automation job.
Public issue comments no longer show a generated-by line. Grounding stays
in code_findings/reasoning only so reply can use UI language without file
paths, symbols, or dense parentheses. Classify prompt and rewrite templates
match that tone.
Use the bot PAT for classify prepare/apply and implement needs-human
comments so issue activity shows netcatty-bot instead of github-actions.
Treat netcatty-bot as an own actor for automation PR loops.
* fix(ci): stop Cursor issue triage crashing on Search pagination
Octokit normalizes Search API pages so response.data is already the items
array. Mapping response.data.items returned undefined and then crashed on
candidate.user during daily-limit admission — so every valid new issue
failed before Cursor classify could run.
* fix(ci): classify already-available issues with how-to and auto-close
Add an already_available triage category for requests the product already
ships: after code-grounded inspection, reply with the concrete UI entry
path, then close as completed. Low confidence falls back to other instead
of closing. Keeps the Search pagination fix so new issues can classify.
* fix SFTP transfer throughput regression
* fix: address Codex review on PR #2429
* fix resumable transfer review findings
* fix remaining resumable transfer review findings
* fix: address Codex review on PR #2429
* fix(sftp): harden resumable range fallback and channel cleanup
Truncate staged sparse tails to the contiguous checkpoint before stream
fallback or pause, always release isolated download channels on cancel and
failed fast paths, and stabilize concurrency unit tests.
* fix(sftp): abort range transfers on isolated channel errors
Attach a temporary SFTP error listener during concurrent range copies so
channel resets reject cleanly instead of crashing main, and force-publish
the contiguous checkpoint when pausing.
* fix(sftp): never truncate S2S temp source on upload fallback
stagedLocalPath during SFTP→SFTP upload is the fully-downloaded temp
source, not a partial target. Only truncate partial download/local
targets and remote .part staging files.
* fix(sftp): truncate direct-download sparse tails on range fallback
Direct SFTP→local resumable transfers keep resumeStage "direct" and never
copied targetType onto the transfer object, so prepareStreamFallback skipped
truncating the staged .part after out-of-order range failure. A later stream
pause could then stat the oversized sparse file and advance past holes.
Treat any staged local path outside the S2S upload phase as a partial target,
persist target metadata on the transfer, and fix the sparse-tail test to assert
against the real staged path.
* fix(sftp): always end isolated upload channel on open failures
uploadFileResumableFast assumed callers only saw throws after the isolated
channel was already ended, but a failed local fs.open left the channel open
while the catch path cleared fastSftp without disposing it. End the channel
in a single finally covering local open, remote open, and range transfer.
* test(sftp): stabilize local-open channel cleanup regression
Make the missing-source upload case delete the file only after the isolated
channel opens, and keep stream-fallback errors handled so the suite asserts
channel disposal without unhandled stream ENOENTs.
* fix resumable transfer fallback safety
* fix(sftp): allow cancel while resumable OPEN is in flight
Install transfer.abort before awaiting local/remote handles so cancel ends
the isolated channel if the server stalls SFTP OPEN. Covers upload and
download fast paths, with a regression test for stalled upload OPEN.
* fix(sftp): detect same-size source rewrites via content samples
Metadata-only checks can miss in-place rewrites when mtime/ctime do not
advance. Sample SHA-256 digests through the open local handle before range
upload and re-check after, and do the same path-level check for stream
fallback. Harden the regression test by freezing utimes after rewrite.
---------
Co-authored-by: netcatty-cursor-bot <41898282+github-actions[bot]@users.noreply.github.com>
Classification must open real source files first. The prompt mandates
search→read→classify, JSON now requires code_paths/code_findings, and
normalizeClassification rejects ungrounded replies so agents cannot answer
from issue text alone.
Cursor's --sandbox enabled fails on ubuntu-latest due to AppArmor, so classify
and implement jobs never ran. Use --sandbox disabled and keep secret isolation
via token stripping, frozen helpers, and post-run scans.
Isolate publish onto fresh runners, harden Codex loop head scoping,
protect fork bootstrapping, and polish classification replies and
mark-ready error handling after local codex review.
Route checked out main for trusted helpers, but scripts/cursor-automation.cjs
only exists on this PR until merge, so require() failed with MODULE_NOT_FOUND.
Fetch the PR head helper when the default branch does not have it yet.
The SFTP transfer center trigger was missing the shared
--top-tabs-muted color used by adjacent utility buttons, so it
rendered as pure foreground white.
Tighten re-triage, Codex single-flight signals, protected-path checks on
commit ranges, trusted helper loading from RUNNER_TEMP, and failure
handling so own/bot loops stay safe and forks only re-@codex.
Add missing Terminal snapshot-effect deps, drop unused cursorAuthMode
deps in SettingsAITab, and remove dead test imports/state so dev lint
starts clean.
Drop Cursor CLI review for third-party/fork PRs. Initial Codex review
stays with the existing connector; after contributor synchronize we only
comment @codex review (deduped per head SHA).
Wire GitHub Actions to classify and implement issues via Cursor CLI,
open draft PRs, request @codex review for own/bot PRs, fix findings,
and only post read-only review comments on third-party PRs.
Cancel wind-down can leave child transferIds latched when they are not in
activeTransfers; clearPendingTransferCancel before reusing those ids so
startStreamTransfer does not abort immediately. Regression test models the
sticky latch.
Soft-unpause only live streams; directory dedicated pause winds down then
startFresh. Resume replace-mode under stagedTargetPath and promote on full
success. Refuse pre-stream pause, force R2R progress without corrupting stage
offsets, and cover the directory pause-resume path with a regression test.
Urgent interrupt bypasses xterm's native scrollOnUserInput. Treat Ctrl+C
as scroll-on-input so interrupting a scrolled-up stream (e.g. tail logs)
returns the viewport to the bottom when that setting is enabled.
Keep browse sessions independent of pool transfers, complete dedicated resume
for files/folders after restart, roll back partial pause safely, clamp crash
checkpoints to durable .part size, and open upload targets by connecting the
host when no terminal tab is active.
Seed erase-scrollback with open sync-block state from prior chunks so a
split H/2J redraw that returns to the live bottom does not append CSI 3 J
and wipe history (Codex review on #2415).
Keep cursor-home and strip only erase-display while reading scrollback,
and ignore 1-row sticky lag so agent TUI frames cannot append under the
previous frame (#2291).
* feat(ai): support Cursor CLI login for subscription quota
Allow the managed Cursor agent to authenticate via local `agent` CLI
login as an alternative to CURSOR_API_KEY, so Catty can use subscription
Auto quota. Keep API key and CLI login mutually exclusive, isolate
resume sessions by auth mode, and expose selectable CLI models with
auto as the default.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ai): harden Cursor CLI login path after review
Address multi-agent review findings: soft-cancel abort without late
stream errors, prefer cursor-agent over ambiguous agent binaries,
mode-aware availability without wiping API keys or sticky-disabling,
and concurrent MCP merge refcount restore. Add unit coverage for the
new contracts.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
* fix(terminal): batch hidden log output
* fix(terminal): preserve ordered hidden output
* fix(terminal): bound hidden output overhead
* fix(terminal): respect custom tab stops in timestamps
* fix(terminal): preserve terminal parser state
* fix(terminal): preserve hidden output boundaries
* fix(terminal): retain batching with prompt formatting
* fix(terminal): preserve prompt boundaries through transforms
* fix(terminal): handle unbatched prompt output
* fix(terminal): preserve batched prompt and time boundaries
* fix(terminal): retain background timestamp boundaries
* fix(terminal): preserve visible timestamp boundaries
* fix(terminal): separate shell output from tui frames
* fix(terminal): separate output after tui exit
* fix(terminal): preserve prompts across output slices
* fix(terminal): flush output before context reads
* fix(terminal): time-slice timestamp marker cleanup
* fix(terminal): preserve prompt columns after bare LF
* fix(terminal): preserve background write yielding
* fix(terminal): retain yields across timestamp boundaries
* perf(terminal): scan output boundaries once
* fix(terminal): track prompt cursor movement
* fix(terminal): resume prompt scan after resets
* fix(terminal): preserve stateful prompt ordering
* fix(terminal): drain close output before capture
* fix(terminal): measure control-only prompt prefixes
* fix(terminal): preserve prompt control boundaries
* fix(terminal): preserve prompt mode toggles
* fix(terminal): bound hidden batches and unfocus policy
Always drain write-coalescer size caps even when scheduled frames are gated,
keep unfocused-but-visible panes on throttled batching instead of per-chunk
force flush, compute prompt starts on post-transform display data, and model
full-width wrap so prompt-on-newline does not insert a blank line.
* perf(terminal): skip off-path timestamps and share alt-screen probes
When host line timestamps are disabled, write plain bytes without the
segmenter/marker hot path. Reuse one alt-screen CSI probe per coalesced
chunk for frame split and schedule mode, and reuse a single full-batch
visual-row measure on the single-segment batched timestamp path.
* fix(terminal): read line-timestamp enable from live hostRef
Session data handlers close over boot-time ctx.host, so toolbar/vault
toggles of showLineTimestamps were ignored. Prefer hostRef.current for
the marker hot-path gate and keep hostRef synced on every Terminal render.
* perf(terminal): per-second timestamp ledger while gutter is off
When line timestamps are hidden, keep a lightweight one-stamp-per-second
ledger without xterm markers. Opening the gutter materializes recent
ledger entries into markers for paint; while visible, keep per-line
markers for accuracy.
* perf(terminal): separate timestamp record from gutter paint
Always record a one-stamp-per-second ledger on the write path with no
xterm markers. Gutter rendering maps the ledger to visible rows, so open
and closed modes share the same cheap record path.
* fix(terminal): fill gutter gaps with previous second stamp
When painting line timestamps from the per-second ledger, blank rows
between stamps inherit the preceding stamp's label so multi-line output
in the same second is fully labeled.
* fix(terminal): stop empty gutter paint and false scrollback rebase
Only rebase ledger anchors when the buffer is actually full and baseY
grows from top trim — not when baseY rises while the buffer is still
growing (which deleted MOTD/login stamps on scroll). Cap gutter paint at
the last content line so empty viewport rows past the cursor stay blank.
* fix(terminal): soft-wrap stamp placement and sparse reflow anchors
Estimate ledger lines with visual-row measurement so narrow terminals
do not pin the next second on a wrap continuation. After each write,
attach at most one xterm marker per stamped second so reflow and
scrollback trim keep anchors honest without per-line marker storms.
* fix(terminal): ignore alt-screen rows when placing timestamp anchors
Stamp line estimates froze incorrectly through vim/less alt-screen
output, so sparse reflow markers landed far below the restored normal
buffer. Skip visual-row advancement while DEC alt-screen is active,
key one-stamp-per-second from the batch arrival date, and only attach
anchors still present in the ledger after capacity trim.
* fix(terminal): seed stamp lines from normal buffer when already on alt
When a write begins on the alternate screen, active cursor is the TUI
row — freezing that through 1049 leave mis-pinned sparse anchors and
dropped them past normal bufferLength. Seed and restore stamp estimates
from buffer.normal, and cover multi-write already-on-alt leave+prompt.
* fix(terminal): keep same-chunk pre-enter line on alt leave
Leave CSI must not always re-read pre-write buffer.normal: when enter
happened in the same walk, the frozen estimate already includes normal
advances before the TUI. Only restore buffer.normal when the write
started already on the alternate screen.
* fix(terminal): pure alt stamp transitions that never rewind lines
Stamp estimates stay in normal-buffer coordinates: seed once (active or
buffer.normal when already on alt), freeze while alt is active, and on
leave only clear altActive — never re-read the live term mid-walk.
Export applyAltScreenAction with a table-driven unit test covering
spurious leave, plus write-path cases for already-on-alt, same-chunk
pre-enter advance, and leave-without-enter.
Parent resume clears sticky per-child cancelledTasksRef entries so directory
re-walk can retry cancelled files. Route clearPendingCancel to the terminal
worker (where skipAdmission latches live), re-check cancel on live resume,
and clean per-child stage artifacts on parent cancel.
Re-check cancel inside setTransfers so late cancels are not overwritten,
keep partial progress on cancelled directory downloads, and clear the
pre-start cancel latch on intentional resume/retry.
downloadToLocal previously treated cancelled child counts as partial
failure and overwrote the parent to failed. Cancel now wins, matching
processTransfer, with a regression test for the final-status helper.
Keep replace/staging on directory adopt, skip byte size validation for
directory soft-resume, and treat per-child cancel as cancelled without
aborting sibling workers or overwriting status as failed.
Reassign parent+children together on adopt, stop spurious cancelTransfer
after failed dedicated reconnect, and clear pending-cancel when a queued
job is cancelled so same-id resume is not permanently poisoned.
When a directory transfer is re-adopted after restart, pull orphaned child
rows into the new owner so completed checkpoints are honored. Show badge
count/attention for paused and interrupted work after restore.
Server-to-server dedicated soft-fail rehomes via adopt instead of a no-op
resume, Resume/Pause All skip child rows, and orphaned failed tasks remain
controllable with a clean Retry from byte zero.
Server-to-server soft-fail from dedicated resume rehomes ownership so a
live panel can resume; pause during dedicated reconnect no longer cancels
(so quit confirmation does not destroy in-flight resume).
Pause on dedicated reconnect cancels the open, resolveConflict never
resurrects cancelled rows after async duplicate naming, and background
agent restart admits before opening session-backed SFTP handles.
Guard processTransfer against cancel after conflict stats, and make batch
Stop cancel scheduler jobs, release pause waiters, sync refs, and clean
artifacts like single-task cancel.
When the user stops a conflict batch, cancel every non-terminal sibling
(including attention conflict rows) so they do not remain stuck without
resolve UI or resume.
No-op pause on conflict rows, keep transfer/conflict refs in sync on cancel,
eagerly clear conflictsRef on resolve to stop double processTransfer, and
surface adoption failures when resolving orphan conflicts.
Keep conflictsRef updated during adopt so global conflict resolution applies
immediately, re-check cancel after adopt, gate replace/merge like the panel
dialog, and ignore dedicated progress while paused/cancelled.
Drop panel-local rows reassigned to dedicated-resume, seed conflicts on
adopt without auto-resume, cancel directory children on orphan cancel,
gate dedicated resume on !conflict, and hide invalid global merge actions.
Exclude conflict-pending transfers from dedicated resume and Resume All so
users cannot overwrite targets without resolving Replace/Skip/Duplicate.
Also skip Pause All on dedicated reconnect spinner rows.
Reassign store ownership to dedicated-resume before reconnect work so
panel publishOwner cannot clobber progress, and open vault SFTP sessions
only after the renderer admission pool grants a slot.
Directory cancel now stops all non-terminal children on the renderer
scheduler, and transfer workers re-check cancel before acquiring pool
leases so cancelled bulk jobs do not open dedicated sessions.
Remember cancels before startTransferNow registers the id, cancel the
renderer scheduler on orphan cancel, avoid pause completion resurrecting
cancelled rows, forward skipAdmission for temp downloads, and persist
progress checkpoints on background events.
Share dedicated resume with the renderer admission pool, preserve queued
pause checkpoints, refuse to resurrect cancelled agent rows, and complete
dedicated resumes that finish after a reconnect-spinner pause.
Settle directory streams on cancel return values, detach idle pool slots
before close, make force hard-close lease-aware, honor skipAdmission on
the worker IPC path, fix MCP non-worker admission, and block dedicated
resume from resurrecting cancelled/paused rows.
Keep soft-close sticky across re-acquire, re-check before hard-close,
holder-aware pool discard, admit-then-lease for uploads, directory/cancel
resume edge cases, MCP skipAdmission, and vault-based reconnect identity.
Add dedicated transfer connections (1–2 per host) so bulk uploads/downloads
no longer block browse sessions. Also cover session leases, resume after
restart, auto-reconnect, skipAdmission, and transfer-center UX fixes for #2396.
* Improve keychain identity layout
* Preserve key search results with identities
* Show keychain search empty state
* Show all matching keychain results
* Restore full keychain key view
- Add computeOptionsFingerprint to detect option changes between turns;
close stale session and create fresh one when model/env/mode/etc change
- Store resolved binPath in sdkRequestRuntimes so steer builds the same
session key as runTurn (fixes steer returning 'unsupported')
- Register abort listener that calls session.interrupt() immediately,
so Stop reaches the CLI even while parked in stream().next()
- Update tests for { session, fingerprint } map structure (35 passing)
* fix(sftp): follow terminal cwd on macOS/BSD remotes and first open
The "follow terminal directory" probe read the interactive shell's cwd
only via /proc, which does not exist on macOS/BSD remotes, so
getSessionPwd fell back to the home directory (or failed) and the SFTP
panel never tracked the terminal. Add an lsof fallback (read_shell_cwd)
so the probe works on non-Linux remotes; Linux keeps using /proc.
Also fix the first open: while the panel is closed the per-command cwd
probe does not run, so activeTerminalCwd can be stale (login home) even
after the terminal cd-ed elsewhere. On that stale value the follow sync
saw currentPath === terminalCwd and did nothing, leaving the panel at
home. Force one fresh backend probe when the panel first becomes visible
for a connected remote, and re-sync on reopen.
Fixes#2335
Co-authored-by: Cursor Agent <cursor-agent@cursor.com>
* fix(sftp): keep first-open follow from snapping back to stale cwd
Addresses PR review: the first-open resync navigated to the freshly
probed terminal cwd but left the stale `activeTerminalCwd` cache (login
home) in place. Because the one-shot ref was already set, a later
dependency change that re-runs the normal follow sync (e.g. closing an
editor toggles hasActiveWork) would navigate the pane back to that stale
home. Mark the stale cached cwd as already handled before navigating so
the normal sync no-ops until the live cwd catches up;
invalidateInFlightFollowSync clears the guard once activeTerminalCwd
actually changes, letting follow resume.
Co-authored-by: Cursor Agent <cursor-agent@cursor.com>
* fix(sftp): re-check follow eligibility before first-open navigation
Addresses PR review: the first-open resync awaited a fresh terminal-cwd
probe but only re-validated the connection afterward. If the panel was
hidden or interactive work started (editor/dialog) while the probe was
in flight, it still navigated, moving the pane while the follow feature's
own visible/work guards say it should be paused.
Add a live followStillEligible() check (generation, follow enabled/
allowed, visible, no active work, same connection) that gates both the
post-probe continuation and the navigateTo call via shouldApply, matching
the normal follow path. Reset the one-shot guard when bailing mid-probe
so the resync can retry once follow is eligible again.
Co-authored-by: Cursor Agent <cursor-agent@cursor.com>
* test(host-details): align auth row assertion with unified row height
The HostDetailsSection row was unified to `flex h-12 ... px-3` (#2378)
but HostDetailsConnectionSections.test still expected the old
`min-h-12 ... px-3 py-2` class, failing lint-and-test on main. Update the
regex to match the shipped markup.
Co-authored-by: Cursor Agent <cursor-agent@cursor.com>
* fix(sftp): harden terminal cwd resync
---------
Co-authored-by: Cursor Agent <cursor-agent@cursor.com>
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
* fix(ui): open AI silent sessions in an attach terminal popup
Tray jumps for hidden MCP sessions attach the existing terminal popup
to the live PTY instead of activating a tab-less main-window surface.
Output is rebound to the popup and restored when the popup closes.
* fix(terminal): register rebindOutput handlers under worker mode
Worker-enabled builds returned early from terminalBridge.registerHandlers
without registering rebind/restore IPC, so the first AI silent-session
attach popup always failed with "No handler registered". Register those
handlers in both paths and rebind display routing via terminalWorkerManager.
* fix(terminal): harden AI attach popup for worker mode lifecycle
Address codex review P1s: resume flow control before restoring output,
route exit/events to the rebound renderer, and replay home scrollback
via a snapshot request before rebinding display output.
* fix(terminal): restore attach routing on popup close and dual-notify exit
Hardening for AI observe popups: restore output from BrowserWindow closed
lifecycle (not only React cleanup), keep attach home for exit events so
main/tray lifecycle updates, and resume paused backend flow on attach.
* fix(terminal): resume flow on attach restore and close snapshot gaps
Resume backend output when attach popups are force-closed, fan out
lifecycle exit events to home+display in non-worker mode, and pause
during snapshot→rebind so live bytes are not dropped from the popup.
* fix(terminal): harden attach restore and worker lifecycle edges
Prefer live display for ZMODEM dialogs after rebind, dual-notify worker
crash exits to attach-home, and avoid clearing attach-home when restore
targets a dead renderer.
* fix(terminal): sync observe-popup state home and fan out SSH exits
Push popup serializations home before restore, fall back to hibernate
snapshots, settle briefly after pause for worker handoff, and dual-notify
SSH exits to attach-home owners in non-worker mode.
* fix(terminal): fan out exits for mosh/telnet/et attach owners
Use shared fanout for in-process protocol exits so attach-home and
display renderers both receive netcatty:exit under non-worker mode.
* fix(terminal): restore attach only onto main app windows
Avoid routing silent-session output to settings/prewarm windows when the
original home renderer is gone; fall back only to registered main windows.
* fix(terminal): harden silent session popup handoff
* fix(terminal): close observed popup with session
* fix(terminal): keep observe popup from reconnecting
* fix(terminal): preserve cleared observe snapshots
* fix(terminal): route transfer prompts to observer
* fix(terminal): keep context after observe restore
* feat: fold top-bar utilities into a control panel
Collapse External MCP, window opacity, theme, and cloud sync into one top-bar control panel so the right rail only keeps AI, controls, and settings.
* fix: keep the cloud sync panel and append quick controls
Restore the original detailed cloud sync popover as the host surface, and place External MCP, window opacity, and theme controls in a footer section beneath it.
* fix: split quick controls from the cloud sync panel
Restore the original cloud sync popover and move External MCP, window opacity, and theme into a dedicated quick-controls entry so the two surfaces stay separate.
* fix: simplify quick controls panel chrome
Drop the explanatory subtitle and card-heavy rows so the panel reads as a compact control list.
* fix: separate appearance controls from External MCP
Group window opacity and theme under one appearance section, with a divider from the External MCP switch.
* fix: compact window opacity controls in the quick panel
Replace the tall slider-plus-presets block with a single-row preset selector to free vertical space.
* fix: make window opacity controls expandable
Keep the opacity row compact by default and reveal the slider plus presets only when the row is expanded.
* fix: put appearance controls above External MCP
Reorder the quick controls panel so window opacity and theme sit first, with External MCP below the divider.
* fix: harden quick controls opacity matching and drop dead copy
Compare opacity presets by percent to avoid float mismatch, and remove the unused quick-controls description strings.
* fix: clean quick controls accessibility and dead top-bar opacity code
Move Switch accessible names onto the focusable control, restore popover autofocus, delete the unused window opacity button, and drop obsolete top-bar theme copy.
* fix: tighten quick controls peer-window and switch labeling
Assert showExternalMcpToggle is forwarded into the quick panel, and name the External MCP switch from its visible label only.
* fix: add icon to quick controls panel title
Prefix the panel header with the same sliders icon used by the top-bar entry.
* fix: remove icon from quick controls panel title
Keep the header as plain text after the title icon felt unnecessary.
* feat: add external MCP top bar toggle
* fix: polish external MCP toggle accessibility
* fix: harden External MCP top-bar toggle ownership and status sync
Hide the quick toggle in peer session windows, poll runtime status at the same cadence as settings, and drop an unrelated zh-CN copy change from this branch.
* fix: prevent External MCP top-bar poll from clearing startup state
Wait for main-window startup reconcile before runtime status can turn the shared switch off, and keep peer windows from owning the main-process enable path.
* fix: await External MCP startup enable before top-bar status polling
Mark startup ready only after the async enable/disable reconcile settles so persistent mode is not cleared by a still-disabled runtime.
* fix: scope External MCP startup gate and preserve enable intent
Only main App shells wait on startup reconcile, keep waiters single-flight, re-read storage after await gaps, and avoid clearing always-on preference on start errors.
* fix: ignore stale External MCP startup enable after concurrent toggles
Bump an enable generation on top-bar changes and skip outdated startup apply work so boot reconcile cannot overwrite a newer user toggle.
* test: fix External MCP toggle lint errors in unit coverage
Remove a duplicate import and use globalThis.localStorage in tests so eslint no-restricted-globals passes.
---------
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
* fix(sftp): preserve active transfers when panel closes
* fix(sftp): preserve completed transfer history after panel close
* fix(sftp): bound retained transfer history lifetime
* fix(sftp): close retained panel cleanup races
* fix(sftp): protect transfer history during panel reopen
Jump hosts reuse the same SSH bridge authentication flow as the target host, but the renderer did not serialize the per-host requiresMfa preference into jumpHosts. A bastion configured to prefer keyboard-interactive was therefore still authenticated with the default password-first order when used in a host chain.
Forward requiresMfa from each resolved chain host into the NetcattyJumpHost payload and extend the startSSH serialization test to cover the flag alongside existing jump host proxy and timeout settings.
The terminal search bar left the previous match decorations on screen
after the input was cleared. Two causes:
- TerminalSearchBar only called onSearch when the term was non-empty, so
clearing the field never notified the search hook.
- handleSearch's empty-term branch returned early without clearing
decorations or resetting the stored term.
Now clearing the input fires onSearch(''), and the empty-term branch
resets searchTermRef and calls clearDecorations() so highlights vanish
immediately and a subsequent find-next won't reuse the stale term.
Problem: some SSH servers require the login password and the MFA challenge through keyboard-interactive. They reject the plain SSH password authentication method even when the password is correct, which makes Netcatty report password failures and can close the session before a usable shell opens.
Fix: add an explicit per-host Prefer interactive auth setting, reuse the existing requiresMfa host flag, forward it from the renderer to the native SSH bridge, and place keyboard-interactive before password only when that setting is enabled and a password is available. Hosts that do not enable the setting keep the existing auth ordering.
Tests: add a bridge regression test for the MFA auth order and extend the terminal starter test to assert the host option is forwarded.
MCP host_open unconditionally surfaces and focuses the main window,
which interrupts whatever the user is doing when an AI agent connects
to a host in the background. Add a toggle in Settings -> AI -> External
MCP (defaults to on, preserving existing behavior) so users can opt out.
* feat(settings): support local CJK fonts
* refactor: move CJK font rules to domain
* fix(settings): improve CJK font preview safety
* fix(settings): refresh local font previews
* fix(settings): improve font picker keyboard behavior
* fix(settings): handle unavailable font detection
* fix(settings): cover font picker edge cases
* fix(settings): quote custom font families safely
The onTerminalOutput handler had two near-identical branches (with/without an
external onTerminalOutput callback), each inlining the same
/password|passphrase|口令/i test that runs on every output chunk. Collapse them
into a single handler and hoist the pattern to a module-level constant so it is
compiled once and not duplicated. Behavior is unchanged.
* fix: keep non-hibernated tabs active in background
* fix: scope background work to hibernation policy
* fix: bind background monitoring to its tab
* fix: measure hidden local workspaces
* fix(ui): replace native confirms in system manager panels
Windows native confirm dialogs steal focus after process/tmux/docker
destructive actions. Use SystemPanelConfirmDialog so the side panel
stays usable after confirm/cancel.
Fixes#2242
* fix(ui): clear system manager confirms on session switch
Drop pending process/docker confirms when sessionId changes so a
dialog opened for one host cannot execute against another.
* fix(ui): clear docker image action busy on session switch
Reset actionBusy when sessionId changes so a previous host's
in-flight remove/prune cannot leave the new session's confirm
dialog disabled. Guard stale completions with a generation counter.
Covers password-only, dual KI (TOTP / secondary password), password+KI
partial (mock model A), and password-full-skip-MFA gap (mock model C).
Live cases run against the lab host when SSH_MFA_LIVE=1.
Always try password before keyboard-interactive for the first factor. After
any partial success, prefer keyboard-interactive when the server still allows
it so multi-factor/EDR second factors work without a per-host switch. Remove
the requiresMfa UI and "remember MFA" checkbox; keep secondary-prompt
protections and hostId-aware password save.
Default stays password-first for ordinary hosts. Hosts with requiresMfa
prefer keyboard-interactive so EDR secondary factors are not skipped.
Secondary prompts can suggest enabling MFA mode for that host.
Reject recovered-trailing-garbage parses during upload verification, pad
stale fixed temp files before MOVE, and re-check the destination so a
successful upload cannot leave non-whitespace junk on non-truncating
servers.
Fail closed when the existing vault length cannot be read, re-read and
re-pad after upload to heal concurrent stale-length races, and reuse a
single .tmp path so cleanup failures cannot pile up full vault copies.
When MOVE is unavailable, fall back to in-place PUT padded to the previous
byte length with spaces. Non-truncating servers then leave only JSON-legal
whitespace, the canonical vault never disappears mid-replace, and concurrent
clients cannot observe an empty remote (#2223).
Avoid empty remotes when MOVE falls back to DELETE+PUT: do not in-place
PUT after a failed delete, and retain the temp recovery copy if the final
PUT fails after the old vault was removed (#2223).
Only fall back to DELETE+PUT after a successful temp write, and abort if
destination delete fails so a non-truncating in-place PUT cannot corrupt
or wipe the previous vault (#2223).
Some WebDAV servers overwrite existing files without truncating when the
new body is shorter, leaving leftover bytes that break JSON.parse on the
next download.
Upload now uses temp PUT + MOVE (DELETE + PUT fallback), and download
recovers trailing garbage when V8 reports a position after valid JSON.
Home/scp probes during Auto fallback or forced SCP can hang forever if
the remote accepts exec but never closes. Abort after 15s so the file
panel cannot stay stuck connecting.
Store host sftpFileProtocol on SSH sessions so openSftpForSession used by
Catty/MCP/clipboard respects SFTP-only or forced SCP without payload.
Destroy the local read stream when the peer ACK loses the upload race.
Auto/utf-8 first-open used lossy UTF-8 on non-UTF-8 $HOME bytes, so
Chinese homes opened as mojibake and failed to list. Strict-decode with
gb18030 fallback and promote session encoding for subsequent shell ops.
When writeAll fails after waitForAck is armed, stream close would reject
the ACK promise unhandled. Centralize write+ACK in writeAndWaitAck and
guard Promise.all losers in uploadFile.
Listen for local write-stream errors before opening the SCP channel, pass
encoding/signal into download metadata stat, and restore remote mode after
external-editor SCP uploads.
Track active SCP exec streams per client and close them when the handle is
released, so agent Stop/timeout cancels transfers without killing the terminal SSH.
Detect GB18030 basenames under auto encoding for SCP listings, pass encoding
into source stat for transfers, and allow POSIX backslash in SCP basenames.
Await stream and final ACK together, keep SCP remote-to-remote paths as
strings, decode source control-line names with session encoding, and cancel
SCP homeDir via abort signal.
Reject failed local writers during download, always use fresh local file size
for SCP headers, pass encoding into parent mkdir, and require `scp` on the
remote before advertising SCP-mode connectivity.
Arm final ACK before trailing NUL, pass encodings through transferBridge,
harden base64 listing fallback, encode control-line basenames, and fail open
when SCP shell probe cannot run.
Do not end terminal-reused SSH sockets when closing SCP panes, and pass the
session filename encoding through SCP read/write/stat/rename/delete/chmod paths.
Tear down underlying SSH clients on SCP close, pass AbortSignal into homeDir
probes, and use session filename encoding when quoting remote paths for shell/SCP.
Avoid invalid do; in list for-loops, preserve setuid/setgid/sticky when
parsing ls modes, and clear sftpSudo when the host file protocol is set to SCP.
Stage SCP AI downloads/uploads before replacing destinations, use metadata
for remote list sizes, include file protocol in reconnect cache keys, refuse
Auto→SCP fallback when sudo elevation was requested, and apply download
backpressure. Update reuse-credentials contract tests.
Some hosts (e.g. NAS/Synology root) expose SSH but not the SFTP subsystem.
Add hybrid SCP mode: shell-backed list/manage and OpenSSH scp -t/-f transfers
behind the existing SFTP UI, transfer queue, and sftp.* AI/CLI/MCP tools.
Hosts can choose Auto (SFTP then SCP), SFTP only, or force SCP. Include unit
tests and optional env-gated real OpenSSH integration tests.
Same-endpoint session rebind was replacing the pane in place and closing the
SFTP session mid-transfer. Force a new tab when pending/transferring tasks
still reference the current connection id.
Same-endpoint rebind on terminal focus change was replacing the active SFTP
connection in place, which invalidates promoted editor saves keyed by the old
connection id. Force a new tab when editors still reference the current one.
Do not reuse a healthy same-endpoint SFTP tab across terminal session focus
changes — proxy/jump can differ while host keys match. Capture the visible
path first and reconnect in place with remembered initialPath + new
sourceSessionId so the directory stays sticky without stacking tabs.
closeSession clears preload ready listeners for the session id. A React
effect alone can lose a race with xterm-driven startMosh on retry; arm
the UI ready subscription synchronously after cleanup and again right
before startMosh.
Do not advance the last source session id until auto-connect can actually
rebind, so closing an editor/dialog still switches SFTP to the focused
terminal session.
Session switches can share a saved host key while live credentials/endpoints
differ, so always rebind to the focused session. Restore the last browsed path
on that reconnect so multi-terminal use no longer drops back to $HOME.
A always-truthy backend wrapper made the no-ready-event fallback dead
code and left scripts waiting forever on older bridges. Surface the
method only when the preload API actually implements it.
Use the connect-time connection cache key for skip/reuse and path memory so
same-hostId endpoint switches are not mis-attributed. Only restore previous
path on reconnect when the target host matches the prior connection.
Do not cancel ready/startup work when disposeExitRef is cleared for
hibernate detach. Dispose the pre-start ready subscription when
startMoshSession rejects before attach so retries do not leak listeners.
When a menu cannot fit above or below the trigger, stop flipping it
off-screen. Prefer the side with more space and cap max-height so
overflow scrolls instead of clipping trailing actions.
Require the active tab's mapped connection key to match the focused host
before skipping auto-connect, and re-key last-browsed paths from the live
connection so host-picker switches do not poison another host's memory.
Subscribe to mosh ready before startMoshSession and drain a ready that
fires during the await after attach. Default moshSessionReadyListeners
in createPreloadApi so lightweight test contexts do not throw on close.
Fix DropdownContent side=top so the menu sits fully above the trigger
instead of covering it. Match QuickConnect continue/save menu styling
to TerminalAuthDialog (open below, same z-index).
Fixes#2194
Closing follow mode still dropped back to $HOME when switching terminals or
after SFTP reconnect. Reuse a healthy same-host tab and restore the last
browsed path on rebind/reconnect.
Fixes#2193
closeSession clears preload mosh ready listeners for the session id.
Re-register on every transition into connecting so retry/auto-reconnect
do not leave scripts stuck unready or replaying into the handshake PTY.
Codex review P2s:
- Dismiss the connection overlay on first handshake output so password/OTP
prompts remain reachable; only defer startup commands and connect/pending
scripts until mosh-client ready.
- Dispose the ready listener via disposeExitRef and closeSession so cancel
does not leak preload callbacks when exit is suppressed.
Do not treat shell verbs after git-status markers as directory chrome, and
only strip multi-word path prefixes for privilege commands or clear path
sigils so `git status` and `My Project ls` stay correct (#2191).
Mosh connects through an ephemeral SSH handshake PTY before swapping to
mosh-client. Marking connected and firing startup commands too early
sent snippet/script input into that handshake PTY, so run-on-connect
scripts and host startup commands never reached the real shell (#2199).
Tag handshake output with moshHandshake meta, emit mosh ready after the
client swap, and defer connected status + scheduleStartupCommand until
then.
Accept resolved trailing privilege commands in shouldRecordShellHistory so
themed multi-word dirs still arm. Scan no-space prompts without a cache,
and do not peel echo/ls prefixes as directory chrome (#2191).
When password-prompt assist is open, Enter is hijacked to paste the
session host password. Clipboard paste did not clear that pending state,
so after pasting a different password (e.g. nested SSH via jump host)
Enter could append the wrong secret and fail sudo (#2198).
Dismiss the assist on paste/typed content input so Enter submits what
the user actually provided.
Never promote painted same-token tails over a non-empty keystroke buffer
(g + git status stays g); only prefer the full line when history yields a
privilege command the buffer does not. Accept unicode/punctuated themed
directory names before su/sudo, and keep double-space glyph first words.
Prefer trailing su/sudo/doas when the themed prefix looks like a path
(including multi-word dirs). Avoid over-peeling double-space glyph prompts
and do not treat a trailing-padded cwd token named su as a command (#2191).
Do not absorb post-cursor paint when it looks like a space-prefixed
suggestion (git + status). Prefer the keystroke buffer for single-word
incomplete echo (su→sudo), while still using the live line when history
shortens a multi-word typed buffer (#2191).
When typed text is only a trailing argv of a recalled sudo/su command,
prefer the full live line. Apply lastPromptText before peel so multi-word
directory themes resolve the real command first (#2191).
Peel path chrome on prefixed themed prompts (⚡ ➜ ~ su -). Prefer live
when history shortens a typed command (sudo→su), and prefer the typed
buffer when live still ends with chrome+command (#2191).
Reject decoration-only cached remainders and single cwd tokens on prefixed
themed prompts. When an aligned buffer is only a mid-line prefix of the
full recalled command, prefer the expanded live line (#2191).
Use lastPromptText when boundary detection fails for prompts without a
trailing space, and expand submitted input across wrapped rows so mid-line
Enter still sees the full history-recalled command (#2191).
Skip non-executable desktop CLI candidates, keep Claude on the macOS
app-bundle path, and surface desktop-resolved absolute paths in the
External MCP copyable setup command without breaking PATH installs.
Allow glyph-only one-word su/sudo/doas history to arm password assist,
limit path decoration filtering to bare themed prompts, and expand past
the cursor so mid-line Enter still resolves the full recalled command.
Empty Enter on ➜/git-status decorations must not record cwd tokens or
status glyphs as commands, which would pollute history and false-arm
password assist (#2191).
Validate cached prompt remainders against the live detector split so a
partial cache cannot yield git-status chrome as the command. Accept
multiword userInput on complete Powerline prompts, and allow history
recording when themed chrome reconciles away from the command.
Prefer the live line when ↑/Ctrl+R replace a typed prefix, while still
keeping the keystroke buffer for direct-send and echo lag. Peel themed
prompt decorations via reconcile when lastPromptText is missing or stale
after cd so su/sudo still arm (#2191).
- Strip themed prompt chrome via lastPromptText before falling back to
live userInput; reuse getCommandToRecordOnEnter so decoration pollution
is never armed or recorded (#806/#2191).
- Soft-dismiss password assist on all printable keys except plain Ctrl+C
so AltGr/Option characters still switch to manual entry.
Shell history (↑) redraws the line remotely without updating commandBuffer,
so Enter never armed su/sudo assist. Resolve the submitted command from the
live prompt line when the keystroke buffer is empty.
Ctrl+C was soft-dismissing the picker (same path as typing a password),
leaving dismissedWhileArmed and blocking the next bare Password: line.
Hard-abort on interrupt and ignore Ctrl/Meta/Alt keys for soft-dismiss.
* fix(ai): stop OpenCode process spawn storms when unused (#2184)
OpenCode model catalog loads were spawning full servers for discovered
agents, skipping main-process cache, and not aborting on timeout—so
idle Netcatty could accumulate many opencode processes.
Only load catalogs for the selected agent, cache/coalesce list-models,
abort and tear down on timeout, and share a short-lived list-models
server pool per binary.
* fix(ai): address OpenCode list-models review findings
Restore the main-process env immediately after spawning OpenCode so an
idle list-models server cannot leave PATH/OPENCODE_BIN on a temp shim.
Key model caches and the list-models pool by catalog-affecting env so
profiles that share a binary do not receive each other's catalogs.
* fix(ai): tighten OpenCode model-cache invalidation
Include profile-affecting env in the renderer runtime model cache key so
OpenCode config switches do not reuse a stale catalog. Skip caching empty
list-models results in the main process so transient OpenCode failures can
recover before TTL expiry.
* fix(terminal): prevent osc7_cwd errors after su
OSC 7 setup installed a bare osc7_cwd PROMPT_COMMAND entry. After
non-login su, that string could be inherited without the function
definition, printing "osc7_cwd: command not found".
Use a guarded prompt hook, unexport PROMPT_COMMAND, and upgrade
existing v1 rc snippets in place when setup is re-run.
* fix(terminal): safe OSC7 rc upgrade when markers are incomplete
Do not rewrite/truncate shell rc files when the Netcatty OSC 7 block is
missing an end marker or markers are unbalanced. Append a complete v2
snippet instead so setup still installs tracking without dropping user
config after an open start marker.
* fix(terminal): harden OSC7 rc upgrade safety
Address Codex review findings for OSC 7 setup upgrades:
- Never truncate incomplete/unbalanced marker regions; append v2 only
- Detect a complete v2 block for idempotent recovery
- Resolve full symlink chains; atomic same-dir replace with mode preserve
- Build stripped+v2 content in one temp write so read-only configs cannot
lose the legacy block without receiving the replacement
* fix(terminal): replace OSC7 v1 blocks in place
Upgrade complete legacy OSC 7 blocks by substituting the marked region
at its original location instead of stripping and appending at EOF. This
keeps surrounding control flow (if/fi wrappers) valid and still writes
the full result atomically with mode preservation.
* fix(terminal): match OSC7 markers as whole lines only
Use trimmed whole-line equality for start/end markers so echo statements
that embed the marker text are not treated as block boundaries during
legacy upgrades.
* fix(terminal): preserve ownership on OSC7 rc atomic replace
When upgrading a legacy block via temp+mv, restore the original
uid:gid on the temp file before replacing so a root-run setup against
another user's HOME cannot leave a root-owned bashrc.
* fix(terminal): harden OSC7 PROMPT_COMMAND array and chown
Process every PROMPT_COMMAND array element when installing the guarded
hook so leftover osc7_cwd entries cannot duplicate OSC 7. Abort the
atomic rc replace when ownership cannot be restored, leaving the
original file untouched.
* fix(terminal): fix exported array PROMPT_COMMAND handling
Detect array PROMPT_COMMAND via the -a attribute even when combined with
-x, and stop force-unexporting PROMPT_COMMAND now that the guarded hook
is safe to inherit across su.
* test(terminal): update OSC7 assertions for export-preserving hook
* fix(terminal): fall back when OSC7 upgrade temp dir is not writable
If same-directory mktemp fails (writable file in a non-writable managed
home directory), leave the original config intact and fall back to the
append path instead of aborting setup with set -e.
* feat(ui): customizable dense toolbars with show/collapse/hide
Add a shared toolbar layout model so terminal, SFTP, and side-panel
action strips can right-click customize placement, overflow collapsed
items into the more menu, and persist order plus placement.
* fix(ui): harden toolbar layout placement after review
Address review findings: availability-aware reachability, skip
unavailable reorder neighbors, Popover overflow for nested SFTP menus,
path chrome outside customize context menu, and side-panel hide close.
* fix(ui): only close side panel when hide placement sticks
Return the next layout from setPlacement so hide-of-last-reachable
does not tear down the panel, keep an active hidden tab chip visible,
and rebuild terminal shown order with highlight collapse coercion.
* fix(ui): narrow SFTP overflow spill and close ⋮ after leaf actions
Respect user show/collapse/hide, then temporarily move non-pinned shown
actions into ⋮ when the SFTP toolbar is narrow. Close overflow after
leaf clicks while keeping nested bookmark/encoding panels usable.
* feat(terminal): configurable su/sudo password prompt assist (#2156)
Extend the existing confirm-to-fill sudo assist to arm on su as well, and
add a Terminal setting for off / quick Enter fill / WindTerm-like credential
picker. Picker candidates come from the host password plus keychain password
identities; secrets are never shown in the list or sent without confirmation.
* fix(terminal): tighten password prompt assist after codex review
- Hint mode only uses the session host password, never an arbitrary
keychain identity as a silent Enter target.
- Build autofill candidates from the effective session host so group-
inherited passwords participate.
- Clear a stale credential picker when the session disconnects.
* fix(terminal): more password-assist polish from codex review
- Do not swallow arrow keys while the quick-fill hint is pending; only the
multi-credential picker consumes them.
- Clear the picker overlay on hibernate and wake so it cannot outlive the
autofill controller.
- Show the resolved auth username on the host candidate row (identityId /
group identity), not a stale host.username after password dedupe.
* fix(terminal): keep password picker select callback stable
Avoid forcing TerminalView memo invalidation on every Terminal render by
using a stable useCallback for credential selection.
* fix(terminal): do not offer full keychain on unarmed sudo prompts
Picker mode only lists all password identities after a real su/sudo
command was armed. Unarmed explicit [sudo] lines still get host-password
quick-fill only, blocking remote prompt phishing of other credentials.
* fix(terminal): fix picker mode switch and armed picker test
Arm before inspecting the picker payload, and when switching settings
mid-prompt recompute whether the command arm is still valid so picker
mode can open the full list.
* fix(terminal): reopen password picker after a wrong credential
Keep the su/sudo arm active after confirm-to-fill so a rejected password
re-prompt can offer the full credential list again instead of falling
back to host-only unarmed assist.
* fix(terminal): only re-assist real auth retries after fill
Keep a short post-fill arm for wrong-password re-prompts, but require an
explicit [sudo] line or authentication-failure text before reopening
assist. Prevents sending the host password into child prompts after a
successful sudo (e.g. sudo mysql -p).
* fix(terminal): scope sudo assist away from child password prompts
Track whether the armed command was sudo or su. su still accepts bare
Password: lines; sudo only assists on [sudo] tags or user-scoped prompts
(password for … / 输入密码), not generic Enter password when sudo is cached.
* fix(terminal): tighten when the full credential picker may open
Exclude child-program prompts (Enter password, Password for user …).
Full keychain picker only for explicit [sudo] lines or armed su Password:
prompts; Kylin-style sudo bare prompts still get host-password hint only.
* fix(terminal): limit full credential picker to armed su only
Multi-identity picker is the WindTerm-style su workflow (#2156). Sudo
keeps host-password quick-fill only, so a cached sudo child cannot open
the full keychain list via a forged or nested password prompt.
* fix(terminal): derive password assist candidates in Terminal + mode gate
- Standalone Terminal popups without TerminalLayer now resolve candidates
from host/keys/identities so su picker mode still works.
- When switching to picker mid-prompt, keep the su-only full-list gate
(do not open multi-identity list for an armed sudo prompt).
* fix(terminal): reject SSH-style prompts for su assist and drop dupe keys
- su bare matching only accepts short Password:/密码 lines, not user@host
SSH password prompts after passwordless su -c.
- Remove duplicate sudoAutofillCandidates keys that failed eslint.
* fix(terminal): cursor-anchor password picker and soft-dismiss Esc
Position the su credential picker with the same cursor-relative layout
helpers as autocomplete instead of fixed bottom-center. Esc only closes
the UI while keeping the arm so Esc/arrows can reopen the list on the
same Password: prompt.
Selected list borders were eating into content and looking like extra
left padding. Use inset primary ring (no layout shift) and reduce list
row horizontal padding from px-3 to px-2.
Unselected rows keep hover-only chrome with no extra border/bg.
Selected list/tree add a primary border; grid only recolors the
existing soft-card border to the accent color.
Selection was easy to miss because list mode reused hover glass fill.
Use full primary border, light primary wash, and ring so focus reads
clearly in grid, list, and tree.
Include hostClickBehavior in VaultView memo equality so the settings
toggle applies immediately, clear focus when the mode changes, and
apply select-then-expand to tree group rows with focus highlighting.
Add a Settings > Appearance vault option that defaults to single-click
connect, but can switch to select-then-click-again to connect. Grid mode
shows an accent ring on focus; list/tree reuse the hover fill. Groups
follow the same pattern when the option is enabled.
Store probed --version on ExternalAgentConfig from settings resolve-cli
and discovery enable, and prefer that when gating GPT-5.6 so custom
paths not on PATH still unlock Sol/Terra/Luna when compatible.
Hide minCliVersion presets when CLI version is unknown so pre-discovery
cannot default to Sol on old installs. Require exact path matches for
filesystem command configs so PATH discovery cannot gate a different
manual Codex binary.
Netcatty is BYO-CLI: upgrading codex-sdk does not replace the user's
binary. Hide Sol/Terra/Luna when discovery reports Codex CLI < 0.144.0
so old installs do not auto-default to an unsupported model.
Address PR #2170 Codex review: stop auto-selecting the last thinking
level (which defaulted Sol to ultra), add defaultThinkingLevel from the
official catalog, and keep gpt-5.2 selectable so stored model IDs do not
silently fall back to Sol.
Surface GPT-5.6 Sol/Terra/Luna in the curated Codex model picker and
bump @openai/codex-sdk to 0.144.3 so the bundled CLI meets the official
minimal client version for those models. Align reasoning efforts
(max/ultra) with the Codex catalog.
Fixes#2169
Always run filter, erase-scrollback, and paste prepare/cleanup so long
multi-line pastes still clear residual cells under large-output pressure.
Bulk-plain only skips prompt cosmetics when the post-paste stream is plain.
Decide bulk-plain shortcut after the stateful filter so retained CSI
prefixes still get erase/prompt transforms. Yield inside flood-merged
steps on hop count or WRITE_QUEUE_TURN_BUDGET_MS wall time.
Always run stateful filterTerminalSessionData before bulk-plain shortcuts so
split CSI continuations are not emitted as text. Cap synchronous recursion
inside flood-merged write steps to avoid Maximum call stack size exceeded.
Do not skip forcePromptNewLine preparation when bulk-plain degrade is active.
Bulk highlight catch-up only waits on time-bounded largeOutput, not sticky
longLine, so a single long line cannot leave a permanent 50ms poll.
Split timestamp skip from general bulk degrade. Full-scrollback multi-line
output (docker ps) no longer drops gutter markers; only high-rate floods
(>=64KB/100ms) or long lines suppress registerMarker storms.
Under large-output pressure, plain batches skip paste/prompt/filter prep and
go straight to xterm (bytes unchanged). Keyword highlighting no longer
schedules per-write scans during bulk — one quiet catch-up after pressure
drops. Sizable bulk queue items also yield after the write callback so paint
and input can interleave, matching Tabby FlowControl intent.
Lower the high-rate threshold to 16KB/100ms and treat multi-line writes on
a near-full scrollback as large-output immediately so second dumps skip
timestamp/highlight work. Extend the quiet window while saturated so
prompt gaps do not reopen the expensive path between seq runs.
Cap plain xterm writes (including short-line dumps like seq) to ~128KB and
yield after each intermediate slice so the UI can paint and accept input.
Under large-output pressure use the flood coalesce cap and rAF scheduling
instead of microtask packing. Leave SSH flow high-water at 1MB for #1961.
- abortTerminalWriteCoalescer always clears alt-screen schedule state so a
direct oversized enter-alt probe cannot stick after Ctrl-C drops the batch
- flood merge no longer forces a timer after every shard; inter-step yields
honor per-step yieldAfter (drain-budget pacing from writeLargeTerminalBatch)
Call resolveScheduleMode before early-returning when pending exceeds the
byte cap while auto-flush is gated (hidden pane). Without this, enter-alt
CSI in oversized hidden batches never latched rAF for follow-up repaints
after reveal.
Abort drops enter-alt CSI that never reaches xterm, so reset schedule
latches; treat ESC c (RIS) as leaving alternate so normal output returns
to the microtask path.
Drop enter-pending once the alternate buffer is seen or after an unobserved
exit (RIS), and reprocess ESC/C1 bytes that abort an incomplete private CSI.
Replace raw 16-char tail retention with a phase/params parser that handles
7-bit and 8-bit CSI in encounter order, long split sequences, and oversized
batches that bypass the coalescer frame arm.
xterm parses enter-alt-screen asynchronously, so buffer.active can stay
normal after ESC[?1049h is written. Keep frame scheduling until the
alternate buffer is observed (or leave-alt is seen).
xterm accepts \x9b?1049h as well as ESC[?1049h; force rAF for both forms
so the first TUI repaint is not microtask-scheduled while still on the
normal buffer.
Keep incomplete private CSI tails across coalescer flushes without
quadratic pending joins, and measure bulk-output rate with a true rolling
sample window so high-rate bursts are not missed at window boundaries.
Avoid control-character regex for alt-screen detection, join pending PTY
chunks so split CSI still upgrades to rAF, and reset timestamp segmenter
state when marker work is skipped under output pressure.
Restore debounced keyword refresh while panes are hidden so decorations
still apply after reveal, and force rAF scheduling when output enters the
alternate screen (including the first burst before buffer.active flips).
Reduce stutter from tiny write shards and vsync batching while keeping flow
control, TUI rAF coalescing, and interrupt safety intact. Bulk dumps use larger
shards, fewer yields, microtask scheduling on the normal screen, and degraded
timestamp/highlight side work under output pressure.
#2106 rewired the workspace toolbar X to detach the pane into a
standalone tab, duplicating the existing detach control. Restore X to
closeSession so it closes the pane again; detach stays on the arrow
button.
Expose Settings > Terminal > Normalize terminal text on copy (default on).
When off, all copy paths use raw xterm getSelection(); when on, strip
display padding and join soft wraps as before.
- Compact disposed timestamp entries on every write so clears cannot leave
a huge dead store while the gutter is off.
- Select bulk markers by measured visual rowOffset window rather than
timestamp count, so soft-wrapped floods do not pin out-of-buffer markers.
Drop ambiguous mid-token alnum continuation for padded wraps. Keep strong
continuations (path punctuation, hyphen at split, CJK) and treat multi-space
padding after a finished token as a word boundary.
Queue dropped/duplicate markers and dispose them with a per-pass budget
instead of abandoning forever or mass-disposing in one frame. Keeps
rewrite/scroll-region sessions from leaking unbounded xterm markers
while avoiding O(n²) freezes on flood output.
Deduping rewritten marker.line entries no longer calls dispose() for each
older duplicate. Mass dispose is O(n) per marker in xterm and can stall
rewrite-heavy output; abandoned markers leave with scrollback trim.
DECSTBM regions recycle rows without trimming normal scrollback. Bulk
row-offset measurement would invent history and prune still-valid labels,
so fall back to segmented marker registration when margins are active.
Collapse multi-space TUI padding between ordinary words to a single space so
Pi-style wrapped prose stays readable. Keep strong token continuations (CJK,
path punctuation, URL query) tight, and only treat URL mid-token alnum joins
as tight when padding is multi-space (not a single separator after a URL).
Codex review follow-up:
- Stop wiping prior timestamp entries when a bulk batch hits capacity;
DECSTBM regions can recycle rows without trimming scrollback.
- Throttle onScroll only under time-bounded largeOutput pressure so
sticky longLine state cannot lag user scrolling after output stops.
Codex review follow-up:
- Only register the last capacity timestamps in a bulk flood batch, and drop
excess store entries without per-marker dispose (xterm dispose is O(n)).
- Size capacity as scrollback + viewport with headroom above the unlimited
scrollback cap so oldest still-visible rows keep labels.
- Do not treat sentence punctuation as path continuations
- Scope URL/path detection to the trailing token only
- Cover Hangul and CJK punctuation on both sides of the wrap
Codex review follow-up:
- Treat xterm onScroll as throttled under large-output pressure so
output-driven scroll events cannot bypass the gutter paint budget.
- Collapse duplicate marker.line entries to the latest label before
applying the timestamp capacity cap.
- Prefer the latest marker label when cursor reposition rewrites an earlier
line (entries are not always sorted by marker.line).
- Advance the flood throttle clock on every render attempt so same-second
labels still rate-limit viewport rescans during sustained output.
- Prefer getTrimmedLength() columns when deciding full-row vs partial selection
- Apply token-continuation checks before single-space word-boundary handling
- Detect URL/path context so mid-token wraps do not gain spaces
- Do not invent spaces for URL/path/CJK token continuations across padding
- Keep a single space only for clear word-boundary soft wraps
- Preserve explicitly selected trailing spaces on partial last rows and column selections
Keep always-on timestamp semantics, but cut flood-path work:
- Merge validate+measure into one pass and fast-path printable ASCII
CR/LF floods (seq-style) without escape/unicode scans.
- Binary-search sorted marker entries for the viewport window.
- Reuse gutter row DOM nodes and throttle paints under large-output
pressure while keeping scroll/resize immediate.
- Collapse soft-wrap padding to a single word separator (no CJK space insert)
- Intercept the native copy event so Edit>Copy uses the same normalization
Always-on per-line timestamp markers used an O(n) filter on every
scrollback dispose, which made seq 1 500000 freeze the UI and back-pressure
SSH flow control. Keep always-on history for gutter expand, but compact
disposed entries lazily, cap capacity to scrollback, intern same-second
labels, and skip listener notify when the gutter is closed.
xterm getSelection() turns every physical buffer row into a hard newline, so
soft-wrapped TUI output (e.g. Pi agent responses) pastes with broken wraps and
display padding. Rebuild clipboard text from buffer coordinates: trim row
padding, join isWrapped rows, keep real hard breaks.
Use the helper on copy-on-select, context/hotkey copy, paste-selection, and
add-selection-to-AI paths.
Fixes#2162
- Keep unlocked encrypted keys eligible after passphrase retry even when
the target is password-only (jump-chain retry path).
- Fire onAuthAttempt from the simple ordered auth handler so jump/SFTP
progress logs still update for password-only sessions.
Direct terminal sessions still tried ~/.ssh default keys after a saved
password failed, while jump-host and SFTP paths (buildAuthHandler) only
tried password. That made a wrong/stale host password look fine on direct
login but fail ProxyJump and SFTP until credentials were re-saved (e.g.
via Keychain).
Align startSession with #266 password-only policy and pin buildAuthHandler
behavior with regression tests.
Fixes#2079
Only suppress KI password auto-fill when the password method already
succeeded as a prior factor (EDR step-up). publickey/agent partialSuccess
still allows filling the saved host password into a following Password:
challenge (AuthenticationMethods publickey,keyboard-interactive).
Do not suppress saved-password prefill for multi-prompt keyboard-interactive
forms just because the challenge name mentions Duo or two-factor. Prefill
still targets only the password slot; skipAutoFill covers true second factors.
autoFilledOnce still blocks re-prefilling a stale login password into the
retry modal, but no longer hides the save checkbox. Saving is suppressed
only after partialSuccess or for an explicit single secondary/OTP field.
createOrderedStringAuthHandler no longer permanently consumes methods
that are merely absent from the current methodsLeft list. After a
publickey partialSuccess the server can re-request password even if it
was skipped earlier when only publickey was advertised.
allowSavePassword only turns off for post-first-factor rounds or a single
secondary/OTP field. A combined Password: + Verification code: challenge
still offers saving the password slot (Codex P3 on #2151).
Pass allowSavePassword=false for post-partialSuccess / EDR / multi-round
keyboard-interactive challenges so the modal cannot overwrite the host
login password with a secondary secret (Codex P2 on #2151).
Expand secondary-password regex to allow words between "secondary" and
"password", and scan keyboard-interactive name/instructions so Chinese
"二次认证密码" banners still block auto-fill when only the English field
label is in the prompt text.
Multi-round keyboard-interactive exchanges may not report partialSuccess
between prompts. After the first auto-fill, later password-looking rounds
open an empty modal so an EDR secondary secret is not pre-filled.
Track partialSuccess on simple/agent ordered auth handlers, omit saved
password from second-factor modal payloads, and align modal prefill
deny-list with the backend MFA vocabulary.
Do not auto-fill keyboard-interactive challenges that look like
secondary/EDR passwords, and skip saved-password auto-fill after a
first-factor partialSuccess so step-up prompts always surface the modal.
When the host info bar is hidden, the script execution banner used a
fixed top:34 under empty space and full right edge under the circular
toggle. Raise it and reserve a right gutter so it sits beside the dial
instead of being pushed under it.
Replace the dense radial-dot “chessboard” grip in the hidden-host-info
speed-dial with a lucide GripVertical control sized like the other
toolbar icons, so drag remains available without looking like a grid
button.
When the host info bar is hidden, the circular action toggle is an
overlay. Reserving a right inset for it pushed the xterm scrollbar
inward and left a dead strip beside the button. Keep the normal body
inset only so the dial floats on top of the terminal.
Match toggle and action pill at h-7 with the same border/shadow treatment,
swap the dense dots for ChevronsLeft / X, and pad the expand clip box so
the soft downward shadow is no longer sliced by overflow-hidden.
.terminal-topbar sets container-type:inline-size, which size-contains the
inline axis so the spring-out tray stayed width 0 and no controls appeared.
Expand with a 0fr→1fr grid instead, drop that class from the tray, shrink
the circular toggle, and use three horizontal dots.
Replace the cramped top-right strip with a circular toggle. On click, the
full action toolbar springs out to the left as a rounded pill (scale +
slide + fade), so every control is visible instead of stacking on the
dots button.
When the host info bar is hidden, the action strip used to open at the
same top-right corner as the dots toggle. That stacked the first control
(host keyword highlight) and its tooltip on the toggle, so it looked like
only “关键词高亮” appeared. Open the full toolbar as a bordered panel
under the toggle instead.
When the host info bar is hidden, the action cluster sits at the top-right
edge. Opening host keyword highlighting with side=top clipped the panel
and left the tooltip stacked on the trigger. Open both below the toolbar,
align the panel to the end of the cluster, and suppress the tooltip while
the panel is open.
Raise the packaging floor past 0.1.5 so Netcatty pulls host-before-ack
Confirm, Pending-continue, and related mosh-client fixes. Local resources
fetch to moshcatty-0.1.6 (binaries remain gitignored).
Raise the packaging floor from 0.1.4 to 0.1.5 so Netcatty bundles the
stock-aligned Diff-path speculative echo fix for high-latency typing.
Handshake diagnostics already landed via #2128.
When SSH exits without a parsed MOSH CONNECT line, users only saw the
mosh-server banner and "[Mosh session closed (code 0)]", which looked
like a successful session that immediately died (#2121 residual
connect failures).
Print an explicit handshake-failure hint and put a matching error on
the exit payload. Document that speculative local echo lives in
MoshCatty 0.1.5+ (prediction is not implemented in the renderer).
Workspace toolbar X was calling closeSession and destroying the pane
instead of returning it as its own tab. Route that control through
detach when available, harden close/detach against stale session state,
and record the workspace id in tabOrder when drag-to-split creates a
workspace so later detach can put tabs back correctly.
#2096 gated split layout on viewMode === "split", but drag-to-split
workspaces from createWorkspaceFromSessions leave viewMode undefined
(default tiled split). Visible panes then fell back to full-size stacking
and looked like a broken split. Treat any non-focus workspace as split
geometry, matching pre-2096 isSplitViewVisible behavior.
Refresh AppImage SRI hashes for the re-published v1.1.62 release assets
(including the SFTP opaque-background fix). Release workflow cannot push
directly because main requires a pull request.
After continuous terminal rendering (#2096), inactive terminal panes stay
visible under other surfaces. SFTP had no solid background, so terminal
content bled through when switching from a terminal tab. Use bg-background
so the layer is opaque and still follows the active theme.
Mirror the published AppImage SRI hashes from the v1.1.62 release so Nix packaging tracks the new version. The release workflow could not push this update directly because main requires changes through a pull request.
When the terminal side panel is docked on the left, default top tooltips
for the tab rail (SFTP, scripts, etc.) rendered under the traffic lights.
Match the host-tree toolbar and force side="bottom" (#2095).
* feat(mosh): default bundled client to MoshCatty releases
Wire Netcatty packaging and npm run dev to pull pure Rust mosh-client
binaries from binaricat/MoshCatty (moshcatty-* tags), document the cutover,
and mark the old Cygwin Windows fetch path as legacy-only.
* fix(mosh): accept pure MoshCatty packages (exe-only, no dlls/terminfo)
Align fetch/packaging with MoshCatty pure-Rust releases: require only
mosh-client[.exe], treat Cygwin dll bags and terminfo as optional legacy
layout. Document moshcatty-0.1.1 (ConPTY Ctrl+C + static MSVC CRT).
* refactor(mosh): drop Cygwin legacy; MoshCatty-only pipeline
Remove FluentTerminal/mosh-bin fallbacks, build-mosh C++ CI, and
dll/terminfo runtime helpers. Fetch accepts only moshcatty-* pure
binaries; packaging ships a single mosh-client per platform.
* docs: align ET checklist and et README with MoshCatty-only mosh
Update mosh template references after removing Cygwin build pipeline and
legacy fallbacks. Point ET packaging notes at fetch-mosh / MoshCatty
instead of deleted build-mosh-binaries workflow.
* docs(mosh): require MoshCatty 0.1.2+ for Linux glibc floors
Codex review vs main: defaulting to MoshCatty Linux binaries built on
ubuntu-latest required GLIBC 2.34, above Netcatty's package floors
(x64 2.28 / arm64 2.31). Document the floors, prefer moshcatty-0.1.2+,
and drop trailing whitespace in the design note. Upstream MoshCatty
release CI now builds in almalinux:8 / debian:bullseye with assert.
* fix(mosh): reject MoshCatty releases below 0.1.2
Codex re-review: docs alone still allowed MOSH_BIN_RELEASE=moshcatty-0.1.0/0.1.1,
which produce packages that fail to start on Netcatty's Linux glibc floors.
Validate the minimum tag in resolve and fetch, skip pre-0.1.2 when picking
latest, and cover the reject path in unit tests.
* fix(mosh): pin MoshCatty owner and reject floor prereleases
Codex review vs main (P2):
- Always default MOSH_BIN owner/repo to binaricat/MoshCatty; do not
inherit fork owner from GITHUB_REPOSITORY in resolve or fetch.
- Treat moshcatty-X.Y.Z-rcN as below final X.Y.Z so pins like
moshcatty-0.1.2-rc1 cannot bypass the glibc floor.
* fix(mosh): accept build metadata in MoshCatty release tags
TAG_RE rejected '+' so moshcatty-0.1.2+build.1 failed validation even
though the version parser and isAtLeastMinRelease accepted it. Align
the regex and require a successful semver parse.
* Fix system-to-dark theme race
* Cover peer windows in theme race fix
* Exercise production theme sync path
* fix(settings): announce all changed appearance fields over IPC
Theme sync only broadcast STORAGE_KEY_THEME, so receivers that hit the
stale-storage race for UI theme or accent kept the old value. Emit a
keyed notify for each appearance field that actually changed so the
reducer race guard covers light/dark UI theme ids and accent state too.
* fix(settings): keep current appearance fields across keyed IPC
Sequential notifies for one multi-field appearance change were
re-reading non-announced keys from still-stale storage, so a later
UI-theme IPC could revert an earlier theme update. Preserve in-memory
current values for non-matching keys, compose IPC applies via a ref,
and cover the multi-notify sequence in tests.
Build Linux x64 native modules in an AlmaLinux 8 environment with a newer toolchain while keeping the RHEL 8 compatibility target. Stabilize RPM and pacman packaging in that container, and harden bundled mosh/ET binary builds against transient dependency download failures.
After the 1MB high-water mark fix, bulk dumps like `tail -2000f` still slowed
down because pause/flood mode shrank the coalescer to 8KB batches. That made
the renderer drain the 1MB backlog slowly, so each pause/resume RTT stayed
long and total time regressed toward ~10s on WAN links.
Keep the full bulk coalesce size while flow is paused so the backlog drains
fast enough to resume the SSH channel quickly; only use the flood cap when the
write queue is flooded but flow is still open. Also raise flood coalesce to
128KB and per-turn queue drain to 256KB so plain-text floods do not shard into
tiny setTimeout yields.
Show the active write permission mode as a dedicated status row, and move the
lengthy how-to / security copy into a header help tooltip so the card stays
scannable. Also fix usage copy that incorrectly said the toggle was "below".
Agents could list vault hosts and run commands on open sessions, but could not
open a host themselves. Add vault.host.open (MCP: host_open) that opens a
terminal tab via the vault agent bridge, merges the new session into MCP scope,
and documents the flow in the Catty system prompt.
Also redesign External MCP client configuration into segmented tabs with
shared copyable code blocks so setup matches the settings design system.
* fix(ai): probe remote login shell before PTY wrappers (#1854)
SSH/Telnet sessions never set shellKind, so AI exec defaulted to the
posix wrapper and broke fish login shells with "Unsupported use of '='".
Probe the remote login shell once via a silent SSH exec channel before
AI/MCP PTY execution, cache session.shellKind, and leave et/mosh
shellKind unset so they can take the same path. Fish wrapper coverage
includes a real-fish binary regression test.
* fix(ai): harden remote shell kind probing
* fix(ai): keep PowerShell prompt override after posix login probe
Do not pin probed posix login shells onto session.shellKind. Unset still
defaults to posix wrapping, while leaving the kind open preserves the
live PS ...> prompt override for bash/zsh login shells that entered
pwsh interactively (Codex P2 on #2061 / issue #841 path).
* fix(ai): make remote shellKind probe cancellable before PTY write
Register a pending activePtyExecs marker for the pre-exec shell probe so
Stop during the probe window latches cancellation and does not still type
the command after the probe resolves (Codex P2 on #2061). Wire through
Catty, MCP, and worker foreground exec paths.
* fix(ai): release session lock if job shell probe rejects
terminal_start reserves the session before ensureSessionShellKind; if that
promise rejects, the previous .then-only path never released the lock.
* fix(ai): cancel pending terminal_start during shellKind probe
Register background job entries before the remote shell probe so chat
cancel can latch and abort job start before startPtyJob types into the
PTY (MCP + worker paths; Codex P2 on #2061).
* fix(ai): do not pin login shell as active; default to posix_sh
Login-shell probe no longer sets session.shellKind for fish/posix — only
powershell/cmd are pinned. Unset shellKind resolves to posix_sh (sh -c
inner POSIX wrapper), which both fish and bash interactive shells can
parse, fixing #1854 without assuming login shell === active shell
(Codex P2 on #2061).
* fix(ai): use login shell as soft hint, not sh -c / dash
Drop posix_sh default that ran commands under /bin/sh (often dash) and
broke bash/zsh source, [[ ]], and functions. Keep fish/posix probe as
session._loginShellKind soft hint for resolveEffectiveShellKind so fish
login still gets the fish wrapper and bash/zsh keep the native posix
wrapper, while live PS prompts can still override.
* test(ai): cover shell probe cancellation gaps
* fix: build Linux x64 packages on Debian Buster for glibc 2.28
Rebuild node-pty (and serialport) inside debian:buster so x64 releases
load on RHEL 8 / CentOS 8 / UOS / Deepin instead of requiring glibc 2.35
from ubuntu-22.04. Mirrors the arm64 containerized build pattern (#2062).
* fix: run Buster build-deps step with bash
Container jobs default to /bin/sh; dash does not support [[ so the
archive.debian.org rewrite would be skipped and apt-get update would fail.
* fix: disable apt Valid-Until for archived Buster mirrors
archive.debian.org security metadata for Buster is expired; without
Acquire::Check-Valid-Until false, apt-get update fails in the container.
* fix: install Python 3.11 for node-gyp on Buster x64 builds
Buster ships Python 3.7; node-gyp 12 uses walrus syntax and fails during
npm ci when rebuilding node-pty. Keep the glibc 2.28 container and supply
a modern interpreter via actions/setup-python.
* fix: install Python 3.11 via uv inside Buster x64 builds
actions/setup-python is not reliable on glibc 2.28 containers. Install a
manylinux CPython with uv so node-gyp 12 can rebuild node-pty during npm ci.
Keep only the confirmed product fixes for 奇安信-style auto-login:
1. Seed startup screen from viewport or live buffer so on-screen menus
stay waitable (never mark the whole buffer consumed at script start).
2. sendLine emits body then CR separately, matching real keyboard input.
3. After sendLine, consume only pre-send buffer length so peer prompts
that arrive between body and Enter remain matchable for the next step.
Same-host Connected preference depends on session order; ignoring
reorder left the picker bound to a stale sourceSessionId.
Co-authored-by: Cursor <cursoragent@cursor.com>
Connected picker names a specific live session; renderer username/port
can lag the authenticated bridge endpoint, so reuseOnly must not reject
on endpoint mismatch before attaching to that session.
Co-authored-by: Cursor <cursoragent@cursor.com>
When session.port is undefined, use SSH default 22 instead of a vault
host.port that may have been edited after the terminal connected.
Co-authored-by: Cursor <cursoragent@cursor.com>
Host-ready only refreshes an already-issued external token so cold-start enable cannot write a short-lived discovery token before resolveHostCredentials rotates it.
Co-authored-by: Cursor <cursoragent@cursor.com>
Endpoint-only Connected picker attempts must not fall through to a
fresh SSH dial. Also correct zh-CN/zh-TW Connected section labels.
Co-authored-by: Cursor <cursoragent@cursor.com>
Ignore Catty/CLI authToken from onBridgeHostReady and re-assert the cancel flag after scoped metadata cleanup so soft-revoke stays intact.
Co-authored-by: Cursor <cursoragent@cursor.com>
Try endpoint-only openSftp with sourceSessionId before getHostCredentials
so Connected picker rows work when saved password/key is missing or
undecryptable on this device.
Co-authored-by: Cursor <cursoragent@cursor.com>
Issue a rotatable external-only TCP token on enable, soft-revoke it on disable without killing long-lived stdio sockets, force external sockets onto the reserved chat scope, and only seed empty external metadata when the scope key was never written.
Co-authored-by: Cursor <cursoragent@cursor.com>
Reject disabled/in-flight scope resurrection, move session sync to App mount, cancel the external chat session on stop, and broadcast approval cards to main + settings windows.
Co-authored-by: Cursor <cursoragent@cursor.com>
Only hide a vault host when its endpoint matches the live Connected
entry, so post-connect host edits still offer the new saved target.
Co-authored-by: Cursor <cursoragent@cursor.com>
Skip peer/settings windows, listen for cross-window enable, only mark payloads sent after success, and mount the MCP approval IPC bridge at app startup before TerminalLayer lazy-loads.
Co-authored-by: Cursor <cursoragent@cursor.com>
Sudo SFTP never reuses a terminal shell connection; advertising those
hosts as Live would fall back to a fresh auth while hiding the normal row.
Co-authored-by: Cursor <cursoragent@cursor.com>
Vault host edits after connect must not break sourceSessionId reuse;
overlay hostname/username/port from the terminal session onto the picker host.
Co-authored-by: Cursor <cursoragent@cursor.com>
Serialize enable/disable, cancel in-flight external work on stop, refresh discovery env for live permission mode, sync all terminals from TerminalLayer, host confirm approvals globally, and treat Claude local/project scope as upgradeable.
Co-authored-by: Cursor <cursoragent@cursor.com>
Skip rebroadcast when HTTP proxy settings are unchanged across windows, and force rejectUnauthorized onto the tunneled target TLS hop when allowInsecure is set.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep app-wide external hosts when a Catty sidebar push arrives, and re-read discovery permissionMode so long-lived stdio clients pick up Safety changes without restart.
Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the vault-host mosh/et filter so plain SSH terminals opened against
a mosh/et-default host (e.g. ssh:// deep links) still appear as reusable.
Co-authored-by: Cursor <cursoragent@cursor.com>
In terminal-worker mode mirror renderer session payloads into
__external_mcp__ and seed from other chat scopes when the live map is empty.
Co-authored-by: Cursor <cursoragent@cursor.com>
Lets Windows users (and others) route Google Drive / OneDrive / GitHub sync and AI provider traffic through system or custom proxies without touching SSH ProxyJump settings.
Co-authored-by: Cursor <cursoragent@cursor.com>
In terminal-worker mode the main-process sessions map can be empty; avoid
overwriting renderer-pushed __external_mcp__ metadata with an empty sync.
Co-authored-by: Cursor <cursoragent@cursor.com>
Merge prior scoped host metadata when syncing live sessions, and parse
Claude Environment: KEY=VALUE blocks so configured entries are not
misclassified as missing discovery env.
Co-authored-by: Cursor <cursoragent@cursor.com>
Do not fall back to the expected launcher path when the existing Grok
entry carries args; that masked conflicts as configured.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep disabled same-name entries visible to classifiers so one-click setup
can mcp remove them before writing the discovery-env launcher config.
Co-authored-by: Cursor <cursoragent@cursor.com>
Exclude connecting and Mosh/ET sessions so Connected rows always map
to a live ssh2 shell that SFTP can reuse without a fresh login.
Co-authored-by: Cursor <cursoragent@cursor.com>
Parse Scope/Args from `claude mcp get`, remove the blocking scope before
re-adding with discovery env, and treat non-empty Args as a conflict.
Co-authored-by: Cursor <cursoragent@cursor.com>
When multiple live terminals share a hostId, pick the later same-status
session so Connected picker reuse targets the newest endpoint match.
Co-authored-by: Cursor <cursoragent@cursor.com>
Treat launcher-only Codex/Claude/Grok entries as not_configured so one-click
setup can remove and re-add them with NETCATTY_EXTERNAL_MCP_DISCOVERY_FILE.
Co-authored-by: Cursor <cursoragent@cursor.com>
Always show reserved-scope approval cards in Catty, stop rewriting
discovery after startup errors, and fail closed when an explicit discovery
env path is missing instead of falling back to another profile.
Co-authored-by: Cursor <cursoragent@cursor.com>
Drop unsupported `-s` from `claude mcp get`, treat only starting/running as
enabled for RPC gates, and reject launcher commands that carry extra args.
Co-authored-by: Cursor <cursoragent@cursor.com>
Disconnect marked external sockets on disable, await in-flight stop before
re-enable, write Claude MCP entries to user scope, and stop spawning client
CLIs on the quiet status poll.
Co-authored-by: Cursor <cursoragent@cursor.com>
Expose the existing catalog MCP as a Settings-managed local bridge with
discovery lifecycle, one-click client setup, and reserved external scope
so third-party agents can use Netcatty without a sidebar chat session.
Co-authored-by: Cursor <cursoragent@cursor.com>
Use sftpPickerSessionsEqual in SftpView and SftpSidePanel memo
comparisons so OSC title updates do not force full SFTP re-renders.
Co-authored-by: Cursor <cursoragent@cursor.com>
Avoid re-rendering mounted side panels on OSC title churn by comparing
id/hostId/protocol/status for the Connected picker, and ignore local
.omo run-continuation state.
Co-authored-by: Cursor <cursoragent@cursor.com>
Include sessions in the side-panel stable context equality check so the
Connected host list updates when terminal sessions connect or change.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep WindowOpacityButton in sync with Settings so 96–99% stays
selectable from the top-tabs control as well.
Co-authored-by: Cursor <cursoragent@cursor.com>
The appearance slider previously stepped by 5%, so 96–99% was unreachable
even though the clamp already allows any value in [0.5, 1]. Fixes#2019.
Co-authored-by: Cursor <cursoragent@cursor.com>
Let users pick currently connected SSH sessions from the SFTP host
picker and reuse the existing connection, including ephemeral deep-link
hosts.
Co-authored-by: Cursor <cursoragent@cursor.com>
Local Terminal used a per-session hostId, so autocomplete history never
accumulated, and the store never imported ~/.zsh_history. Stabilize the
hostId and seed from local histfiles so prefix suggestions match Ghostty.
Closes#2037
Co-authored-by: Cursor <cursoragent@cursor.com>
Do not trim password values when validating auth forms or attaching
credentials to SSH connect options, so a single-space password works
(issue #2036).
Co-authored-by: Cursor <cursoragent@cursor.com>
Closing the hibernate setting must restore already soft-hidden or
hibernated tabs immediately, instead of waiting until the user selects
them again.
Co-authored-by: Cursor <cursoragent@cursor.com>
Solo tab switches only flip isVisible. Skipping that prop in Terminal's
memo left isVisibleRef stale, so hidden panes kept the visible write path
and #1995/#1996 tab-return recovery never ran (#1985).
Co-authored-by: Cursor <cursoragent@cursor.com>
Detect complete password prompts on the last line independently of the
whole-chunk promptCandidateBytes cap, so a large stale prefix plus a
sudo/password prompt is not dropped during Ctrl+C drain.
Co-authored-by: Cursor <cursoragent@cursor.com>
Attackers switched to hash-named zips with soft bait, so drop the
scoring heuristics and treat any zip/dangerous attachment from
non-maintainers as spam.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: let script waits match the visible startup screen
Auto-login scripts that wait for bastion menus already on screen were
timing out because startup snapshot sync marked the whole buffer as
consumed. Replace the script buffer with the current viewport so waits
can match visible text while still ignoring scrolled-off scrollback.
Fixes#1960
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: keep startup viewport waitable across sync races
Address Codex review on #2035: merge the visible snapshot with any
output that arrives during the snapshot IPC (e.g. bastion BEL), and seed
the whole viewport as fresh so waitFor* can match headers beyond the
512-byte tail slack on long menus.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: do not seed buffer-fallback scrollback as visible screen
When the renderer misses the snapshot request, requestScreenSnapshot
returns the full script buffer, not the viewport. Mark that path and
keep the consumed baseline so scrolled-off keywords cannot rematch.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: preserve live output during fallback snapshot sync
Baseline only the pre-sync buffer length on empty/fallback snapshots so
bytes that arrive while waiting for the snapshot stay matchable, matching
the viewport path's trailingFresh behavior.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: keep waitForPrompt scoped to the live tail after seeding
Seeded full-viewport freshness is for waitFor/waitForText/waitForRegex
only. waitForAny still uses the 512-byte tail window so an older prompt
still visible above long-running output cannot satisfy waitForPrompt.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: advance scan offset when consuming a preserved startup prompt
waitForPrompt could resolve a seeded live-tail prompt without consuming
the viewport above it, so a later waitForText rematched older visible
lines. Advance scanOffset through the preserved prompt end.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: consume the full seeded viewport after startup prompt
When waitForPrompt resolves a preserved live-tail prompt, advance
scanOffset through the entire seeded snapshot — not just the prompt —
so later waitForText cannot rematch older visible text after the prompt.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: keep sync-race trailingFresh outside seeded viewport
seededLength must end at the visible snapshot so waitForPrompt does not
consume bytes that arrived during the snapshot IPC. Those trailingFresh
bytes stay matchable for later waitForText/waitForRegex.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: dedupe trailingFresh against blank-padded viewport snapshots
Full-viewport snapshots often end with blank rows after live content, so
a plain endsWith check missed sync-race duplicates. Compare after
stripping trailing blank lines before appending trailingFresh.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: let generic waitForAny match the full seeded viewport
Tail-only freshness is for waitForPrompt (allowPreservedTailMatch).
Generic waitForAny must still see menu labels near the top of a long
seeded startup screen, matching waitForText/waitForRegex.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: trim partial overlap from sync-race trailingFresh
When the viewport snapshot already contains a prefix of trailingFresh,
append only the non-overlapping suffix so waitForText cannot rematch a
phantom duplicate of the overlapped text.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: match partial trailingFresh overlap against full viewport
Use the unstripped viewport when trimming sync-race prefixes so an
overlapped trailing newline is not left behind as a phantom blank line.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: keep waitForPrompt from rematching short seeded prompts
After live output clears preservedTailMatch, currentTailFreshBoundary
must start at seededLength so a short startup prompt cannot satisfy
waitForPrompt through the normal 512-byte window.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: invalidate preserved prompts once scanOffset passes them
If waitForText/waitForRegex already consumed output after a preserved
startup prompt, clear that prompt via advanceScanOffset so waitForPrompt
cannot reuse it. markOutputConsumedThrough still keeps the one-shot
startup prompt for waitForPrompt.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: trim blank-padded overlap without dropping duplicate sync markers
Use syncStartText to keep genuinely new duplicate suffixes during snapshot
sync, while still trimming blank-padded and partial overlaps from trailingFresh.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: treat scrollback-backed stale viewports as pre-sync
When syncStart has scrollback above the visible suffix, a stale snapshot can
still equal that suffix without matching syncStart exactly — keep trailingFresh
so duplicate markers printed during the IPC round-trip are not trimmed away.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: invalidate startup seed after automated script input
sendLine/send must not leave the seeded viewport or preserved prompt
matchable, or common send-then-wait scripts can complete against pre-command
output. Keep sync-race trailingFresh beyond seededLength still waitable.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: keep long sync-race output waitable after startup prompt
After waitForPrompt consumes the preserved prompt, re-seed remaining
trailingFresh so markers ahead of a >512-byte burst stay matchable. Also
consume all buffered bytes on automated input, including pre-input trailingFresh.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
When multiple restore controls are preserved, peel complete leading
restore sequences that are a suffix of preserved from the password
prefix so droppedBytes stay accurate and restores are not re-emitted.
Co-authored-by: Cursor <cursoragent@cursor.com>
Hold incomplete CSI with password prefixes, but drop standalone SGR
fragments so they cannot leak into a later shell prompt. Restore/OSC
prefixes remain holdable alone.
Co-authored-by: Cursor <cursoragent@cursor.com>
Only strip a full preserved restore prefix from password holds, and when
discarding a mid-CSI held prefix also consume the CSI final byte so
shell prompts are not corrupted.
Co-authored-by: Cursor <cursoragent@cursor.com>
When a drain chunk preserves alternate-screen restore and also holds a
password prefix on the same line, strip the overlapping restore from
pending so droppedBytes stay accurate and restore is not re-emitted.
Co-authored-by: Cursor <cursoragent@cursor.com>
Styled prompts can split mid-CSI (Pass + ESC[ / 0mword:). Hold both
the password-prefix body and the trailing control prefix so the prompt
still completes across chunks during Ctrl+C drain.
Co-authored-by: Cursor <cursoragent@cursor.com>
Include the new resume reason in TerminalInterruptDisplayFilterReason
so typed returns from the one-chunk password path type-check.
Co-authored-by: Cursor <cursoragent@cursor.com>
Password prompts often emit nothing further until the user types, so
dropping a complete prompt while waiting for promptQuietMs leaves a
blank terminal. Resume immediately on complete password candidates.
Co-authored-by: Cursor <cursoragent@cursor.com>
Match password-prefix candidates after stripAnsi so SGR-styled chunks
like ESC[31mPass still hold across chunks, while keeping raw pending
bytes for display when the prompt completes.
Co-authored-by: Cursor <cursoragent@cursor.com>
Classify keyword bodies without punctuation/spaces so full-width
colon targets do not collapse minLen to 1 and hold lone "p"/"pa".
Co-authored-by: Cursor <cursoragent@cursor.com>
A held Pass prefix followed by \nPassword: is a fresh flood line, not a
same-line completion. Discard the prefix and keep quiet-gap so stale
prompts cannot bypass drain via the held-prefix resume path.
Co-authored-by: Cursor <cursoragent@cursor.com>
Only bypass promptQuietMs when a held password-prefix chunk completes.
Fresh Password:/Enter password: lines in the flood wait for the quiet
gap like shell prompts, so canceled prompts do not resume drain early.
Co-authored-by: Cursor <cursoragent@cursor.com>
Detect trailing password-keyword prefixes at word boundaries so split
prompts like "alice@host's pass"+"word:" or "用户 的密"+"码" survive
Ctrl+C drain instead of being dropped mid-keyword.
Co-authored-by: Cursor <cursoragent@cursor.com>
Align complete-password-prompt detection with terminalSudoAutofill so
prompts like "用户 的密码" resume Ctrl+C drain instead of being dropped.
Co-authored-by: Cursor <cursoragent@cursor.com>
Limit held prefixes to true prompt-prefix forms so ordinary lines like
"Password authentication failed" are drained, and include discarded held
prefix bytes in returned droppedBytes so renderer flow ack stays accurate.
Co-authored-by: Cursor <cursoragent@cursor.com>
A held "Pass" that never completes as a password prompt must be dropped
before the generic shell-prompt matcher runs, otherwise "Pass$ " leaks
into the terminal after Ctrl+C drain.
Co-authored-by: Cursor <cursoragent@cursor.com>
Buffer probable password-prompt prefixes (e.g. "Pass") across chunks so a
split "Password:" / "[sudo] password…" is not dropped before it completes,
and resume immediately once the held prefix forms a complete prompt.
Co-authored-by: Cursor <cursoragent@cursor.com>
Settings can probe ~/.nvmd/bin/codex.cmd, but the SDK path needs the real
native binary under versions/<ver>/. Without that override, packaged builds
fall back to optional platform deps we deliberately exclude.
Fixes#2027
Co-authored-by: Cursor <cursoragent@cursor.com>
Opening SFTP (or other side panels) can shrink the terminal in pixels
without changing cols/rows, so term.onResize never clears the atlas and
WebGL keeps painting stale glyphs as black squares (#2013).
Co-authored-by: Cursor <cursoragent@cursor.com>
Interrupt output gates only resumed on shell-style prompts, so a password
prompt arriving in the quiet window was dropped as stale flood. Treat
sudo/password prompts as resume points on both main and renderer gates.
Co-authored-by: Cursor <cursoragent@cursor.com>
Version opacity updates and skip rebroadcasting incoming IPC/storage
echoes so a fast drag in Settings no longer fights delayed main-window
values (#2018).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): prevent corrupt uploads from oversized fastPut chunks
Restore ssh2's 32KB default chunk size, verify remote size after upload,
and treat stream finish (not close) as success so truncated zip/tar.gz
transfers fail loudly instead of looking complete (#2022).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): wait for stream close after finish on upload fallback
Address Codex review: resolving on finish alone can miss close-time
server errors while the remote handle is still open. Require finish then
close, and fail on premature close.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
SSH key passphrase prompts previously checked "Remember this passphrase" by default; make it opt-in for safer defaults (#2024).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: make Windows Mosh handshake survive ConPTY and Cygwin terminfo
Windows ConPTY can inject CSI into MOSH CONNECT lines and exit without a
trailing newline, so the sniffer never swapped to mosh-client. Also convert
bundled TERMINFO paths to /cygdrive/... because Cygwin ncurses splits on ':'.
Fixes#2025
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: defer unterminated MOSH CONNECT parse until EOF
Avoid accepting a 22-char key prefix mid-stream before optional "=="
padding arrives; recover unterminated CONNECT only on SSH PTY flush.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Clarify Netcatty MCP environment context so external agents that read resources can see the terminal execution tools, and cover the MCP resource path with a test.
* Add telnet URL deep link support
* Fix telnet deep link review issues
* Keep ssh deep links independent from telnet registration
* Address Codex telnet deep link feedback
* Support JumpServer jms:// client launch links
Register Netcatty as an optional jms:// protocol handler (opt-in,
default off to avoid conflicting with the official JumpServer client).
SSH/SFTP connection payloads decode to an ephemeral host that connects
to the JumpServer gateway with the one-time connection token, without
saving anything to the vault.
Fixes#1958
* Mark jms deep link hosts as ephemeral for session restore exclusion
* Open the SFTP side panel for JumpServer sftp launch links
jms:// payloads with protocol "sftp" previously landed in a plain
shell. Mark the ephemeral host with an autoOpenSftpPanel hint that the
session factory forwards as autoOpenSidePanel, and let TerminalLayer
open the SFTP side panel for that session once it connects.
* Support JumpServer telnet deep links
* Keep JumpServer telnet links on SSH gateway
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Support password in ssh deep links with ephemeral connect
ssh://user:password@host URLs now connect directly using the URL
credentials without saving the host to the vault, covering bastion
one-time password flows. Password-less links keep the existing
match-or-prefill behavior.
Fixes#1890
Co-authored-by: Cursor <cursoragent@cursor.com>
* Keep ephemeral deep-link hosts out of vault update paths
Terminal-layer host updates (e.g. the SFTP follow-CWD toggle) map over
the merged terminal host list, which could persist an ephemeral host
and its plaintext password into the vault. Route terminal-layer host
updates through a splitter that applies ephemeral entries to in-memory
state only and forwards just the vault hosts to updateHosts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Exclude ephemeral deep-link sessions from session restore
Sessions backed by an in-memory ephemeral host cannot survive a
relaunch (the one-time password only lives in memory), so persisting
them would leak the ephemeral host metadata into restore storage and
restore a tab that can never reconnect. Mark ephemeral hosts and their
sessions, and filter them out of the session-restore payload.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Treat ephemeral hosts as unsaved in appearance classification
The terminal theme/font panel and pane appearance resolution decided
"saved vault host" via hostMap membership. Ephemeral deep-link hosts
are now in that map, so theme/font changes were routed to the
persisted-host updater and silently dropped. Classify via
isSavedVaultHost (ephemeral flag aware) so those changes fall back to
the session/global setting path as before.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Reuse saved host settings for password deep links and keep ephemeral marker on clones
Password deep links now run saved-host matching first: a uniquely
matched vault host contributes its non-credential settings (proxy,
jump chain, charset, group, ...) to the ephemeral host while the URL
credentials override vault identities and key references. Session
clones (split/copy/copy-to-new-window) now carry the ephemeralHost
marker so cloned tabs stay excluded from session restore.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Block group credential inheritance on password deep-link ephemeral hosts
buildSshDeepLinkEphemeralHostFromSaved now takes the group-resolved
effective host and clears group/identityFilePaths in addition to
identity references, so a later resolveEffectiveHost pass cannot
re-inherit group default credentials (identity, key, password) over
the one-time URL password. Non-credential group defaults (proxy,
jump chain, charset, ...) are preserved via the effective host.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Route ephemeral-host font zoom through the per-session font-size path
Ctrl+zoom on a non-workspace ephemeral deep-link session hit the
saved-host update path, where the vault updater silently dropped the
change (ephemeral ids are not in the persisted hosts array). Zoom and
the theme panel font-size slider/reset now store the size on the
session (rendered via applySessionFontSizeToHost) for ephemeral
hosts, matching the workspace-pane behavior.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix ephemeral session font zoom routing
* Prevent ephemeral deep-link sudo autofill
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(terminal): detach stale keychain identity when saving retry credentials
When a host references a Keychain identity (host.identityId) with wrong
credentials, re-entering credentials via the auth-retry dialog's
"Continue & Save" saved them onto the host but kept the identity
reference. On reconnect, resolveHostAuth prefers identity credentials
over host fields, so the stale keychain credentials kept failing.
Clear identityId when the user explicitly saves new credentials so the
saved username/password (or key) actually takes effect.
Fixes#1956
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(terminal): keep saved retry credentials over group-inherited identity
Writing identityId: undefined is treated as "unset" by applyGroupDefaults,
so a group-level identity would be re-inherited on reconnect and shadow the
credentials saved from the auth-retry dialog. Store an empty string instead
and register identityId in EMPTY_STRING_OVERRIDES_GROUP_DEFAULT so the
explicit "no identity" override sticks. All consumers check identityId
truthiness, so "" behaves as "no identity".
Addresses Codex review on #1970.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Re-inject tool call names on OpenAI-compatible stream desync
Some OpenAI-compatible gateways emit tool_call chunks the AI SDK
rejects or ignores (non-standard `type` values, tool calls outside
choices[0]). The chunk normalizer had already recorded the tool call
id, so later nameless continuation chunks were forwarded with an
injected id and the SDK threw "Expected 'function.name' to be a
string." on what it saw as a brand-new tool call.
Remember each tool call's function name when its id is assigned and
re-inject it (plus type "function") into continuation chunks so every
forwarded chunk is self-describing, and coerce non-standard tool_call
type strings to "function" so the naming chunk itself is not dropped
by schema validation.
Fixes#1954.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Preserve argument prefixes from SDK-invisible tool call chunks
Codex review on #1966 flagged that when the chunk establishing a tool
call is invisible to the AI SDK (which only reads choices[0]), any
argument fragment it carried was lost: the recovery path re-injected
the name but forwarded only the suffix arguments, producing truncated
tool input.
Track the full concatenation of argument fragments per tool call key
and which keys have been forwarded at choices array position 0. The
first chunk forwarded where the SDK can see it now carries the full
accumulated arguments; already-visible tool calls keep streaming
incremental fragments as before.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(terminal): raise SSH flow high-water mark to stop WAN pause ping-pong (#1961)
Bulk terminal output over SSH (e.g. `tail -2000f big.log`) was far slower
than other clients on high-latency links. The renderer's output flow control
pauses the source stream once its unacked backlog crosses FLOW_HIGH_WATER_MARK
(previously 100KB). For SSH the source is the ssh2 channel, so pause() stops
the remote until resume() plus a full round-trip.
ssh2's own channel window is 2MB (WINDOW_THRESHOLD 1MB), so a 100KB watermark
made Netcatty pause/resume dozens of times during a multi-MB dump — a 4MB dump
triggered ~61 pause cycles, each costing ~1 RTT. At 300ms RTT that turned a
~2s dump into ~21s.
Raise FLOW_HIGH_WATER_MARK to 1MB so it sits near the ssh2 window; the same
4MB dump now settles in ~4 pause cycles (~2-3s at 300ms RTT) while still
bounding renderer memory. FLOW_LOW_WATER_MARK stays at 8KB to keep interactive
acks responsive.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(terminal): align deferred-ack test with 1MB flow high-water mark
The deferred IPC ack batch threshold (XTERM_WRITE_CALLBACK_BATCH_BYTES)
now sits far below FLOW_HIGH_WATER_MARK, so deferral alone can no longer
accumulate up to the watermark. Assert the batch-flush invariant instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(zmodem): keep transfer listeners across same-session reconnects
The preload netcatty:exit handler dropped the per-session zmodem
listener sets, but auth-failure retries and auto-reconnects reuse the
same sessionId without remounting the terminal, so useZmodemTransfer
never re-subscribed and rz/sz progress + overwrite dialogs silently
stopped appearing (#1955). Move listener cleanup to explicit
closeSession and prune empty sets on subscriber dispose instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(zmodem): gate event delivery instead of dropping listeners on close
Codex review caught that reconnects also call closeSession with the
terminal still mounted (startReconnect -> cleanupSession), so clearing
listener maps there reintroduced the bug for manual/auto reconnects.
Keep listener sets owned by renderer subscribers and gate zmodem event
delivery on closedTerminalDataSessions, which is cleared when a session
restarts with the same id.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix directory tracking setup after su / sudo su (#1942)
After switching users in the terminal (e.g. `sudo su -`), SFTP directory
following stops: the new user's shell has no OSC 7 prompt hook, and the
backend pwd probe runs on an exec channel owned by the login user, which
cannot read another user's /proc/<pid>/cwd. The advertised remedy — the
OSC 7 setup action — also failed in this exact scenario because its silent
exec-channel script cannot verify or configure a foreground shell owned by
another user.
The setup script now detects that the active foreground shell belongs to a
different uid, reports the target shell via a marker (exit 5), and the
renderer falls back to typing the setup command into the terminal itself,
where it runs as the target user. The config lands in that user's rc file,
so OSC 7 reporting resumes immediately and persists for future su sessions.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address Codex review: stage typed setup remotely, forward shell-local config paths
The typed fallback previously pasted the whole multi-line setup script into
the terminal, which could leave many entries in the target user's bash
history when cmdhist is off (the appended history -d only removes the last
one). The setup script is now staged into a self-deleting remote temp file
over the silent exec channel, and the typed command becomes a short single
line — one history entry that the existing bash cleanup reliably removes.
The typed wrapper also forwards shell-local (possibly unexported) ZDOTDIR /
XDG_CONFIG_HOME to the child sh via the NETCATTY_* overrides the script
already honors, so zsh users with a custom config dir get the prompt hook in
the right rc file (mirrors buildOsc7SetupCommand).
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address Codex review: hash-verify the staged setup script before the su shell runs it
The staged temp file stays writable by the login user until the typed
command runs, so a same-uid process could swap its contents in that window
and have the su-target (e.g. root) shell execute arbitrary code. The typed
runner now reads the file into memory once, deletes it, verifies the
renderer-computed SHA-256 of the in-memory copy, and only then pipes that
same copy to sh — no TOCTOU window between verification and execution.
Verification failure prints an error instead of running anything.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
The "Authentication failed…" retry prompt and progress-log line in
createTerminalSessionStarters were hardcoded English and bypassed i18n,
so they rendered in English regardless of the selected language.
Route both through the existing tr() helper and add
terminal.auth.retryMessage / terminal.auth.retryLog to en, ru, zh-CN
and zh-TW.
* Run agent commands in a subshell so failures cannot close the session
Agent-generated commands were eval'd directly in the user's active login
shell, so a generated script containing `set -e` (or `exit`) terminated
the whole SSH session on the first failure. Wrap the eval in a subshell
unconditionally instead of trying to detect dangerous commands — the
detection approaches from PRs #1852/#1882 grew into shell parsing and
were abandoned. Output stays live in the user's terminal; the trade-off
is that cd/export no longer persist between calls, which the tool
descriptions now tell the model about.
Also fix multi-line command display: synthetic echo now normalizes line
breaks to CRLF (no more staircase indentation in xterm), and the POSIX
wrapper rebuilds newlines via printf so the typed command stays on one
physical line and PS2 continuation echoes ("> ...") no longer leak past
the marker filter.
Fixes#1850
Co-authored-by: Cursor <cursoragent@cursor.com>
* Move shellUtils require to module scope in cattyExecHandlers
The inline require("./ai/shellUtils.cjs") inside the with(ctx) block did
resolve correctly at runtime (bare `require` hits ctx.require, based in
electron/bridges/), but the path only works through that indirection and
reads as wrong relative to this file. Require at module scope with the
unambiguous ../ai path instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ai): allow OpenCode native skill dirs in permission rules
Netcatty's OpenCode permission config denied external_directory access
for everything outside its own allowlist, so skills discovered by
OpenCode itself (~/.opencode/skills, ~/.config/opencode/skills,
~/.claude/skills, ~/.agents/skills, remote skill cache) could not read
their reference files. OpenCode evaluates rules last-match-wins and its
read permission matches worktree-relative paths, so wildcard-anchored
patterns for the well-known skill directories are added to both the
read and external_directory rules in MCP and skills+cli modes.
Fixes#1939
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ai): re-deny dot-env files inside OpenCode skill dir allows
The broad skill-directory read allows would win over OpenCode's default
.env / .env.* approval gates (last matching rule wins), silently exposing
secret files placed inside native skill directories. Append targeted deny
rules after the allow entries so dot-env files stay protected.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix manual session log capture in terminal worker mode
Manual session logs (and script session logs) start their write stream in
the main process, but with the default terminal worker runtime all session
output is appended to the worker process's own sessionLogStreamManager
instance, so the saved file only ever contained the initial prompt line.
Tap the worker output mirror into the main-process stream manager so
main-process log streams receive terminal output, and mirror sudo-autofill
and programmatic command log rewrites when forwarding writes to the worker
so those logs stay sanitized like worker-owned auto session logs.
Fixes#1938
Co-authored-by: Cursor <cursoragent@cursor.com>
* Dedupe fallback worker output before session log taps
The runtime sender announces every chunk with an output-tap message and
falls back to a plain output message when the output port is unusable;
the manager notified taps for both, double-writing manual/script session
logs in the fallback path. Mark fallback output as already tapped so taps
fire exactly once per chunk.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Every output chunk was walked char-by-char up to four times in JS
(sync-block filter, erase-scrollback rewrite, replay-safe log sanitizer,
line-timestamp segmenter) plus two more per-char scans for long-line
pressure, before xterm.js ever parsed it. Replace those loops with
native indexOf/regex hops and single-slice appends, and gate the
quadratic pending-suffix scan in the sync filter behind a cheap
incomplete-escape check. Output is byte-for-byte identical (fuzzed
old-vs-new over 4000 mixed chunks); colored-output floods drop from
~350ms to ~0.03ms per 64KB batch in the sync filter alone.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix terminal backlog when window is unfocused in background.
Extend the background write fast path to occluded-but-visible windows (common on Windows Alt+Tab), skip wasteful repaints during that path, and flush any remaining coalescer/queue backlog when the app returns to foreground.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix resume hook wiring and keep unfocused repaint in fast path.
The onWindowShown resume flush never fired because useTerminalBackend did not expose the bridge listener. Also restore scheduleTerminalRepaintWhenUnfocused on the background fast path: unfocused-but-visible windows have no rAF render loop, so skipping it froze visible output (#1761 regression).
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add macOS Dock new connection menu
* Fix Dock menu window readiness
* Wait for renderer before Dock menu delivery
* Queue Dock host connects until vault ready
Added a contributing guide for the Netcatty project, detailing the code of conduct, ways to contribute, development setup, build and packaging instructions, linting, pull request process, commit message conventions, and architecture guidelines.
useTerminalEffects no longer consumes setIsSearchOpen (the runtime now uses
requestSearchFocus), so stop destructuring and passing it through. Caught by
codex review as a harmless leftover.
Co-Authored-By: Claude <noreply@anthropic.com>
Codex review of the first pass found a stale-closure bug: createXTermRuntime
captures its ctx once (the effect only re-runs on host.id/sessionId change),
so the original requestSearchFocus — which branched on a captured isSearchOpen
— locked in the initial state and never bumped the focus token on later
Cmd/Ctrl+F presses.
Make requestSearchFocus state-independent: always setIsSearchOpen(true) and
bump the focus token. When closed, opening mounts the SearchBar (isOpen effect
focuses); when open, the token bump refocuses. The callback now has a stable
identity and behaves correctly regardless of when it was captured.
Also drop the now-unused setIsSearchOpen from the runtime ctx type and its
ref/construction sites.
Co-Authored-By: Claude <noreply@anthropic.com>
The searchTerminal hotkey called setIsSearchOpen(true), which is a no-op
when the bar is already open — React bails on the unchanged boolean, the
SearchBar's focus effect (dep [isOpen]) never re-runs, and focus stays
wherever the user moved it. Route the hotkey through a new
requestSearchFocus that bumps a focusToken when already open; the
SearchBar watches that token and refocuses its input.
Closes#1789
Co-Authored-By: Claude <noreply@anthropic.com>
Add clean:vite/dev:clean npm scripts and reload the renderer when Vite
returns 504 for stale pre-bundled deps after pull or npm install.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add Traditional Chinese (Taiwan) translations, derived from zh-CN and
adapted to Taiwan terminology and phrasing (e.g. 軟體/網路/設定/工作階段/
檔案/連接埠/伺服器). Register zh-TW in messages.ts and append it to
SUPPORTED_UI_LOCALES so it appears in the language selector.
Keep in-flight until optional pending script run finishes; do not mark
unstarted connect scripts as completed on batch failure.
Co-authored-by: Cursor <cursoragent@cursor.com>
Use tail-fresh matching instead of script-start baseline so waitForPrompt
still works while stale scrollback no longer satisfies waitFor (#1776).
Defer subscribeScriptRuns notifications, pre-check terminal runs, and
run pending connect scripts when an earlier connect batch fails without retry.
Co-authored-by: Cursor <cursoragent@cursor.com>
Mark the session output scan baseline after syncing the screen snapshot
so waitFor/waitForAny only match output that arrives after the run begins.
Fixes interactive prompts firing sendLine before the server asks (#1776).
Co-authored-by: Cursor <cursoragent@cursor.com>
subscribeScriptRuns invokes listeners synchronously; finish() must not call
unsubscribe before assignment when the run is already terminal. Drop the
connectScriptRetryTick loop so a post-run error no longer re-queues scripts.
Co-authored-by: Cursor <cursoragent@cursor.com>
Softens the scroll edge before the fixed view controls so toolbar buttons do not look abruptly cut off in split layout.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep toolbar buttons visible with a scrollable action row instead of collapsing them to zero width when the edit panel opens.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move convert/delete controls to the item trailing edge with standard ghost icon buttons instead of tiny corner overlays.
Co-authored-by: Cursor <cursoragent@cursor.com>
Expose snippets and automation scripts to Catty, MCP, and CLI with full run lifecycle, vault bridge CRUD, and clickable tool-result cards in the chat message list.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add yyyymmdd hhmm timestamps to cloud sync history and only show the green status dot when local changes are still pending upload.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(scripts): add terminal script automation, recording, and side panel
Introduce nct script runtime in the main process with terminal-side recording,
execution overlay, Monaco editor, connect-script queues, and scripts side panel
integration. Fix recording to wire scriptRecorderRef into xterm, use waitForPrompt
for root/user shells, and save recorded scripts through the global editor dialog.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): address Bugbot review findings for recording and side panel
Refresh script run state in side panel memo, guard toolbar recording against
multi-session conflicts, reset connect scripts on reconnect, resolve focused
session via getActiveTerminalSessionId, and avoid wiping host edit form when
snippets list updates.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): prevent pending script re-run and improve workspace run feedback
Track one-shot pendingScript execution separately from onConnect queue so
reconnects do not replay Run-now scripts. Fall back to focused session when
running on all tabs outside a workspace, and toast when every tab is still
connecting.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): consume connect-script window once and filter serial hosts
Mark the onConnect script evaluation as consumed even when the queue is
empty so later snippet/host edits do not rerun automation mid-session.
Restore serial-host filtering in SelectHostPanel confirm handler.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): grace period before giving up on connect-script queue
Only mark onConnect automation as consumed after scripts run or a short
post-connect grace window expires, so late-loading snippets still trigger
while mid-session Vault edits do not.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): gate connect-script consumption on vault hydration
Replace the grace timer with vault-init and unresolved host script binding
checks so late-loading snippets still run onConnect automation while
mid-session edits stay suppressed. Clear pending script run state on
disconnect so reconnect retries workspace pending scripts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): honor run targets and retry failed connect automation
Route QuickScriptEditorDialog Run through onRunSnippet for multi-host
workspaces. Defer connect/pending script completion markers until each
script succeeds and retry the queue after failures.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): stabilize pending runs and global connect queue sync
Keep pending script run state across reconnects, run onConnect scripts
before one-shot workspace pending scripts, and strip per-host queue entries
when an onConnect script targets all hosts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): defer host queue snippet sync and guard connect runs
Persist snippet target links only when the host panel is saved, block
duplicate connect-script scheduling while a batch is running, and require
runnable hosts before showing Run in the snippets editor footer.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): retry connect failures and surface output-trigger errors
Use a retry tick in connect-script effect deps, toast automation failures
from output triggers, and require runnable hosts before QuickScript Run.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): track id-less pending runs and re-arm output triggers
Remember anonymous pendingScript objects after first run and allow
onOutput triggers to fire again when new terminal output arrives.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): unblock paused connect runs and pass session meta
Resolve waitForScriptRun on pause, run workspace scripts per session
with hostname context, defer output-trigger arming until runs start, and
add noRunnableHosts i18n for QuickScript Run.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): serialize output triggers and defer empty connect finalize
Fire at most one onOutput trigger per scan while a run is active, and
only finalize an empty connect queue after vault snippets are loaded.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): guard output launch race and script run waits
Block overlapping onOutput launches, timeout stuck waitForScriptRun
promises, and disable vault Run when no connectable targets exist.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): match output triggers on new chunks only
Scan onOutput patterns against recent terminal output instead of buffer
length dedupe, and restore overflow-visible on SelectHostPanel new-host flow.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): await sequential workspace runs and output launch
Wait for each tab script to finish in sequential workspace mode, and hold
output-trigger launch guard until the session run becomes active.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): wait through pause and hold output trigger until done
Keep connect and sequential workspace queues blocked until scripts
complete or fail, and retain output-trigger guard until the launched run
finishes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): harden output trigger wait helpers
Add explicit timeouts to output-trigger wait helpers and stop rescanning
the buffer after script completion to avoid sticky-match rerun loops.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): allow empty connect queue and match output in new chunks
Treat an empty connectScriptIds array as an explicit cleared queue instead
of re-migrating from snippet targets, and only fire onOutput triggers when
the latest terminal output contains the pattern.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): require fresh output overlap for onOutput triggers
Only fire output-matched scripts when the regex match extends into the
latest terminal chunk, preventing reruns from stale buffer text.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): promote manual scripts added to host connect queue
Diff connect queue changes from raw connectScriptIds so saving a host
updates manual scripts to onConnect when they are newly queued.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): dedupe output triggers by match position
Track the buffer end offset of each output trigger match and ignore
stale re-scans, with a longer wait for run registration before retry.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): record output trigger matches only after run starts
Propagate automation launch failures to output triggers and commit match
dedupe only once the session run is registered as active.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): address Codex review on wait cursor and sensitive codegen
Track scan offset so waitFor/waitForAny only match output after the previous wait, and emit sendLine after sensitive prompt in recorded scripts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(scripts): harden onOutput triggers against PTY echo and local input
Filter server output before pattern matching so onOutput fires only after
Enter on real stdout, not on keystroke/paste echo, OSC/SGR sequences, or
alternate-screen apps. Add dev diagnostics and scope triggers to the active
session when no explicit host targets are configured.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(scripts): remove onOutput trigger debug logging
Drop temporary diagnostics module and console traces now that PTY echo
filtering is verified; keep the production trigger pipeline unchanged.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Bypass the RAF-bound output coalescer for visible unfocused panes so flow control does not pause the PTY before unfocused repaints can run.
Co-authored-by: Cursor <cursoragent@cursor.com>
Only run the synchronous unfocused repaint path when the pane is visible, avoiding wasted row rendering for background tabs while the window is unfocused.
Co-authored-by: Cursor <cursoragent@cursor.com>
Force synchronous xterm repaints while the app stays visible but unfocused so Windows Chromium does not stall write callbacks, output flow control, and tmux redraws until refocus.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep write-queue and flow ack flushing when stale display drain is disabled for non-SSH protocols, and preserve terminal restore controls on split ^C caret echoes.
Co-authored-by: Cursor <cursoragent@cursor.com>
Force refit and PTY resize when a hidden pane becomes visible again so full-screen TUIs receive SIGWINCH instead of staying at a stale default size.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep connection dialog and chrome surfaces in sync when toggling follow-app vs manual themes, and isolate side-panel live state per tab so switching workspace tabs no longer re-renders every mounted panel.
Co-authored-by: Cursor <cursoragent@cursor.com>
Share tree-order fallback between resolveActiveChromeTheme and resolveWorkTabActiveHostId so stale focusedSessionId after restore keeps sidebar theme and highlight in sync.
Co-authored-by: Cursor <cursoragent@cursor.com>
Align AppHostTreeLayer with resolveActiveChromeTheme, use inline terminal colors for side panel tab bars/borders, and fall back workspace active host when focused session is stale.
Co-authored-by: Cursor <cursoragent@cursor.com>
Drive the action-row gradient from theme.termBg instead of falling back to
the app background, and satisfy useMemo exhaustive-deps for preview theme.
Co-authored-by: Cursor <cursoragent@cursor.com>
Include theme fingerprint in the sidebar memo comparator so inline host-tree
colors update when terminal themes change without host list churn.
Co-authored-by: Cursor <cursoragent@cursor.com>
Derive sidebar row colors from resolvedPreviewTheme again and limit manual
chrome var injection to terminal-layer surfaces only.
Co-authored-by: Cursor <cursoragent@cursor.com>
Include pane visibility in the theme layout effect dependencies so pending
scheduler updates are cancelled and the current theme applies immediately.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep follow-app intent until the side panel closes, scope manual picks to
the focused host, and cancel stale hidden-pane scheduler updates when a
visible terminal applies its theme synchronously.
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace parallel preview/pending follow-app pipelines with a single runtime
that drives xterm, side panel, host tree, and top tabs together in both
follow-app and manual modes.
Co-authored-by: Cursor <cursoragent@cursor.com>
Persist connection-log and fallback serialize payloads before teardown, defer only xterm disposal via microtask, and ignore stale async close work when the boot effect restarts.
Co-authored-by: Cursor <cursoragent@cursor.com>
teardown is recreated every render; including it in the boot effect deps caused cleanup to run repeatedly and exceed React update depth.
Co-authored-by: Cursor <cursoragent@cursor.com>
Prefer the incremental connection log buffer on unmount, fall back to capped hibernate serialize, tear down xterm before async capture, and avoid blocking the UI on full SerializeAddon output.
Co-authored-by: Cursor <cursoragent@cursor.com>
Register per-session prioritize handlers so broadcast interrupt clears xterm backlog on peer tabs, not just the focused session.
Co-authored-by: Cursor <cursoragent@cursor.com>
Prevent stale flood bytes from bypassing the interrupt output gate when only a short quiet gap is observed.
Co-authored-by: Cursor <cursoragent@cursor.com>
Align main-process caret splitting and renderer prompt-candidate filtering with the display gate, and route broadcast Ctrl+C through interruptSession.
Co-authored-by: Cursor <cursoragent@cursor.com>
terminalFlowAck.cjs requires infrastructure/config/terminalFlowConstants.cjs
at main-process startup; omitting it caused v1.1.46 to crash immediately on launch.
Co-authored-by: Cursor <cursoragent@cursor.com>
createRequire() in terminalFlowConstants.ts breaks Vite/browser bundling
and caused "Terminal could not load". Share thresholds through
terminalFlowConstants.json consumed by both renderer and main process.
Deferred batching must not inflate renderer pendingBytes after xterm
has already parsed output; only main-process IPC ACKs are batched.
Flush/teardown/prioritize paths now emit deferred IPC without double
counting flow.written.
- Share flow thresholds via infrastructure/config/terminalFlowConstants.cjs
- Drop main-process ptyOutputBuffer data when session flow is paused
- Tighten rAF coalescer cap to 128KB during renderer flood mode
- Batch deferred flow ACKs (~100KB) for small writes; flush on input/teardown
- Release coalescer/queue backlog and clear rendererPaused before hibernate
- Flush ACK buffers and resume flow on hibernate listener setup and force-close
- Reuse releaseTerminalFlowBeforeHibernate in cleanupSession when xterm is alive
- Track IPC ingress bytes separately from display expansion (serial LF→CRLF)
- Bind write-queue drop handlers before the queue is first created
- Flush/clear session ACK buffers on cleanup; reset flow on input prioritize
- Add tests for ingress accounting, drop-handler binding, and constant parity
- Match VS Code watermarks (100K/5K) and CharCountAckSize (5K)
- ACK ingress bytes (not filtered length) to stay in sync with trackEmitted
- Add AckDataBufferer-style batching via terminalFlowAckBuffer
- Hibernate listeners ack buffered output so main-process backpressure works
Tighten renderer output back-pressure (128KB/32KB watermarks), account
coalescer and write-queue backlog, drain output on user keystrokes, and
add main-process IPC ACK flow control so SSH floods cannot starve Ctrl+C
and other keyboard input.
Closes#1711
* fix(logs): preserve connection log terminal replay data (#1733)
Persist unsaved terminal replay buffers in a side store and merge on reload so pruned localStorage writes and cross-window sync no longer wipe captured session output. Also bind hotkey local terminals to sessionId and match captures strictly by session.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(logs): merge side-store writes and handle quota failures (#1733)
Fold existing localStorage terminal replay data before persisting the side store so multi-window writes do not clobber each other. Skip pruning the main connection-log blob when the side-store write fails so unsaved replay data is not dropped from disk.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(logs): retain orphan side-store entries across window sync (#1733)
Keep terminal replay buffers for log ids not yet present in this window's connection log snapshot, and warn when the main blob fallback write also fails under quota pressure.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(logs): gate orphan side-store retention on persisted log ids (#1733)
Only retain side-store replay buffers for log ids still present in the persisted connection-log blob, and write the pruned main blob before syncing the side store so deletions are not resurrected.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(logs): prefer fresher local data when retaining orphan replays (#1733)
When restoring side-store entries for log ids not yet in memory, use the merged local/ref snapshot instead of stale on-disk side-store bytes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(logs): skip orphan side-store sync when main blob write fails (#1733)
Avoid resurrecting deleted replay buffers from a stale persisted log-id set when the pruned connection-log blob could not be written.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Let users pick icon styles in Appearance settings with live Dock/window updates, dev public asset resolution, and cross-window sync.
Co-authored-by: Cursor <cursoragent@cursor.com>
Route global hotkey/tray show paths through restoreWindowInputFocus, recover renderer focus on visibility/show, dismiss transient overlays before native hide, and cancel deferred focus callbacks on hide.
Closes#1722
* Add group device type inheritance
* Sync package lock for CI
* Persist group device type setting
* Fix group device type inheritance controls
* Fix device type inheritance with shell transports
* Use resolved hosts for AI executor context
* Reduce device type inheritance diff scope
---------
Co-authored-by: mam15mon <mam15mon@gmail.com>
Two related fixes for the autocomplete popup flipping below the visible
terminal area when navigating suggestions with the cursor at the bottom:
1. Clamp popup bounds to the actual .xterm-screen rect instead of the
.xterm-container rect. The container can be a few pixels taller due to
padding/rounding, which created a false positive spaceBelow value and
caused short suggestion lists to flip downward off-screen.
2. Refresh the popup anchor after live-preview writes and after sub-dir
panels update. Cursor movement from preview text or width changes from
sub-dir panels could leave the stored anchor stale.
Add unit tests covering short-popup bottom-row placement, clamp viewport
using the screen rect, and split-pane precedence.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Improve Catty context budget, step guards, and compaction reliability.
Anchors session state across compactions and surfaces compaction to the UI so long tool-heavy turns stay within context limits.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix Codex review: gate reinjection and match sftp_read_file.
Only reinject session context after actual compaction adjustments, and recognize the generated sftp_read_file tool name in stale read pruning.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Scope stale pruning by session for SFTP reads and terminals.
Include sessionId in read fingerprints and keep the last two terminal outputs per session instead of globally across the conversation.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Cap output reserve for small context windows in budget math.
Prevent default 4096-token output reserve from collapsing compaction thresholds on 4k/8k models into near-zero values.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Gate terminal stale pruning on budget pressure and clear old assistant status.
Only omit older terminal_execute output when compaction budget is tight, and clear step-compaction statusText from the prior assistant when a new assistant message starts after tool results.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Gate SFTP read supersession on budget pressure like terminal pruning.
Preserve earlier file reads during read-edit-reread compare flows until context compaction is actually needed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Use compaction summary budget for pre-summarize pruning.
Reserve the 1600-token summary output when fitting messages for LLM compaction, and skip superseding successful reads when a later read failed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Prune oldest messages first when fitting compaction summarize input.
Keep the portion of pre-tail history closest to the current task instead of dropping recent summarize candidates from the end.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Detect JSON tool errors before superseding stale reads.
Treat structured error payloads as failed reads so a later SFTP failure does not replace the last successful file contents.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Show compaction as a transient inline shimmer instead of a banner.
Emit compaction_start/compaction events and render a single ephemeral status line in the message list that clears when compaction finishes.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Adopts streamText instructions/runtimeContext/toolApproval APIs, lifecycle events, and denied-tool result rendering so observer blocks and approval rejections stay visible in chat history.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(ai): unify capability registry, harness tools, and agent runtime
Centralize MCP/CLI/Catty tool surfaces in the capability catalog with codegen,
move Catty-only harness tools into catalog with local execution, converge
mcpServerBridge builtin dispatch via registry, and wire AgentRuntime with
vault bridge, permission grants, and Round 2 catalog parity.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(ai): vault host tools, artifact cards, and external agent vault guidance
Unify vault host create/import with capability RPC routing, add chat artifact cards with Vault navigation, and align external MCP agents with Catty rules so host creation never falls back to Vault Notes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ai): address Bugbot findings for streaming, vault bridge, and grants
Clear Catty streaming by chat sessionId, read latest vault state via getters on agent mutations, and stack grant persisters so unmounting one AI panel does not break others.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(vault): clear pending openNoteId after chat navigation focus
NotesManager notifies VaultView once the one-shot openNoteId request is applied so the id does not stick for the rest of the session.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(vault): serialize vault agent IPC mutations to prevent lost writes
Queue vault agent bridge handlers so concurrent hosts.create or note.create tool calls cannot overwrite each other via stale read-modify-write snapshots.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(vault): keep synchronous vault snapshot for agent IPC reads
Mirror hosts, notes, and groups in a ref updated immediately on agent writes so serialized mutations see the latest state before React re-renders.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ai): sync Catty MCP sessions and defer runtime cleanup on delete
Register terminal sessions with the main process before Catty tool calls, and clear ToolOutputStore only after stopping an active turn when deleting a chat session.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ai): avoid deleting a newer abort controller on late stop
Only remove the session abort-controller entry when stop finishes if it still refers to the controller that was stopped.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ai): sync vault snapshot on render and await active turn on stop
Refresh vault agent reads when React props change without clobbering in-flight agent writes, and wait for runTurn to finish before clearing ToolOutputStore on session delete.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ai): reload permission grants from storage before auto-approve
Re-read persisted grant rules on each approval check so revocations in Settings take effect without requiring a chat hook remount.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ai): sync permission grants to main process immediately on write
Push MCP permissionGrantsSnapshot as soon as Always allow persists a rule so the next tool call in the same agent turn sees the grant without waiting for useEffect.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Add a global dynamic tab title mode, default to agent-only titles, remove the per-host disable control, and keep terminal UI refresh paths in sync with the setting.
Fix#1647: on Windows/Linux the macOS-only `menlo` default left terminal ASCII garbled on cold start until a manual resize, because the primary glyphs fell through to a bundled `font-display: swap` webfont (JetBrains Mono) that swapped in after xterm measured the cell grid.
- Store the terminal font default as a platform-neutral `auto` sentinel, resolved to a locally-installed font per OS at render (win32→consolas, linux→dejavu-sans-mono, darwin→menlo). Safe to persist/sync; never leaks one OS's font to another.
- Explicitly preload the bundled webfonts the terminal actually renders with and remeasure on every `document.fonts` `loadingdone`, so any late webfont swap (incl. user-chosen) recovers without a manual resize.
Reviewed by local Codex (3 adversarial passes); findings on default persistence/sync and side-panel no-op writes addressed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Inline thinking levels under each model row so they are not clipped by the
scrollable model list container introduced for OpenCode.
Co-authored-by: Cursor <cursoragent@cursor.com>
Use Electron userData for CLI state fallback, include Netcatty temp dir and staged attachment paths, and drop duplicate cliStateDir when discovery file is already scoped.
Co-authored-by: Cursor <cursoragent@cursor.com>
Use ../../../cli/discoveryPath.cjs from the sdk folder so Electron dev startup no longer fails with MODULE_NOT_FOUND.
Co-authored-by: Cursor <cursoragent@cursor.com>
Make tab hibernation opt-in so new users keep terminal renderers unless they explicitly enable memory-saving hibernate in settings.
Co-authored-by: Cursor <cursoragent@cursor.com>
Schedule layout suppress store updates on the next animation frame so tab
switches and resize drags no longer trigger React setState-during-render
warnings in TerminalComponent.
Co-authored-by: Cursor <cursoragent@cursor.com>
Mark Telnet as connected once the socket session attaches so devices
that send no banner until client input no longer leave the connecting
overlay blocking keyboard input (fixes#1632).
Co-authored-by: Cursor <cursoragent@cursor.com>
Batch PTY output per animation frame (superset write-coalescer) so Codex
and Claude Code frames render atomically. Distinguish full H+2J redraws
from incremental sync blocks, skip preserve-before-clear inside DEC 2026,
and preserve viewport position across terminal resize.
Co-authored-by: Cursor <cursoragent@cursor.com>
Hold long incomplete private-mode sequences across chunks, terminate CSI
parsing at the real final byte, and avoid treating complete `\x1b[2J`
output as an unfinished escape prefix.
Co-authored-by: Cursor <cursoragent@cursor.com>
Recognize `\x1b[?1049;1000h`-style sequences when deciding whether sync-block
clears must be preserved, without swallowing DEC 2026 markers.
Co-authored-by: Cursor <cursoragent@cursor.com>
Minimal terminal stubs used in session-starter tests do not expose
xterm buffer state; treat that as "not scrolled up" instead of throwing.
Co-authored-by: Cursor <cursoragent@cursor.com>
Use xterm viewportY/baseY to decide when sync-block clears may be stripped,
and re-arm the abandonment timeout whenever a new DEC 2026 block starts.
Co-authored-by: Cursor <cursoragent@cursor.com>
Preserve `\x1b[2J` inside DEC 2026 blocks for alternate-screen TUIs and
for normal-buffer redraws at the bottom, while still preventing xterm.js
viewport jumps when the user is reading scrollback history.
Co-authored-by: Cursor <cursoragent@cursor.com>
Only start the DEC 2026 abandonment timer when entering a block, and
preserve trailing partial marker bytes when the timeout fires.
Co-authored-by: Cursor <cursoragent@cursor.com>
Mirror xterm.js synchronized-output safety timeout so an unclosed sync
block cannot keep stripping later clear-screen sequences in the session.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add regression tests for SYNC_END marker splits and for releasing a
held trailing ESC when the following chunk is ordinary terminal text.
Co-authored-by: Cursor <cursoragent@cursor.com>
Address Codex review: DEC 2026 start/end and clear-screen sequences can
be split across arbitrary PTY/IPC chunk boundaries. Keep a pending suffix
between calls so the sync-block filter still strips in-block clears.
Co-authored-by: Cursor <cursoragent@cursor.com>
Filter `\x1b[2J` inside DEC 2026 synchronized-output blocks before PTY
data reaches xterm.js so full-screen coding CLI redraws no longer reset
viewportY and make earlier output appear eaten.
Fixes#1612
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep all action buttons in the overflow row so the toolbar does not leave empty gaps beside the pinned close control.
Co-authored-by: Cursor <cursoragent@cursor.com>
Pin the collapse control outside the clipped action row and fade overflow so narrow widths stay usable without hiding the close button.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): stop follow-terminal-cwd retry loop on unreachable paths
When per-host follow navigates to a terminal cwd that SFTP cannot reach, stop retrying and allow the toolbar toggle to update immediately.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): address Codex review on follow-terminal-cwd blocking
Return navigateTo success explicitly and scope blocked follow paths per connection so successful navigations and host switches are not misclassified.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): clear follow block after manual go-to-terminal-cwd
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): honor cleared follow-cwd overrides from vault sync
Use an explicit pending toggle override instead of stale session host values when the vault clears per-host follow settings back to inherit-global.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): ignore stale follow navigation when follow is disabled mid-flight
Bump a follow-sync generation when follow is turned off so pending navigateTo failures cannot restore a blocked cwd after the user disables and re-enables follow.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Reject separator-only queries before substring matching and skip compact
matching on hostname fields to avoid IP digit false positives.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix default SSH key fallback after Keychain reference key selection.
Treat identityFilePaths as an explicit user key choice so terminal SSH sessions no longer fall back to ~/.ssh/id_* when a host uses a Keychain file-referenced key.
Fixes#1614
Co-authored-by: Cursor <cursoragent@cursor.com>
* Skip default-key primaries when explicit key config fails to load.
Address Codex review: if identityFilePaths were set but loadFirstIdentityFileForAuth returns null, do not install ~/.ssh/id_* as the primary auth method.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): reconnect terminal side panel after SSH session loss
When SSH disconnects while the terminal SFTP side panel is browsing a stale
path, auto-connect reused a dead tab and left the pane stuck on loading.
Rebind SFTP when the terminal session changes and skip reuse unless the
backend session is healthy; also stop follow-terminal-cwd retry loops to
missing directories.
Fixes#1608
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): address review findings for side panel reconnect
Harden auto-connect after bugbot review: skip tab reuse on SSH session
change, move follow-cwd backoff ref into the interactive body, and make
navigateTo return reached/failed/aborted so follow mode only backs off on
real path failures.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* improve terminal keyword highlight refresh
* fix(terminal): address Codex review on keyword highlight refresh
Map viewport probe diffs to line numbers so off-cursor ANSI redraws mark the
rewritten line dirty without forcing full-viewport rescans on normal typing,
and cache wrapped-line context per processLineRange to avoid repeated scans.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: mam15mon <mam15mon@gmail.com>
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(notes): allow clearing the default note title while editing
Stop sanitizeNoteTitle from forcing empty titles back to "Untitled note", so users can delete the placeholder and type a custom title.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(notes): allow clearing titles from tree rename
Remove the empty-title guard in sidebar tree rename so it matches the main title editor behavior.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(copilot): support Skills CLI mode alongside MCP
Wire Copilot sessions to respect toolIntegrationMode: MCP keeps the existing
netcatty-mcp-only gate, while Skills exposes bash/skill tools, passes CLI
discovery env to the runtime, and approves shell commands that invoke
netcatty-tool-cli.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(copilot): restore driver tests and cover skills CLI mode
Merge the new Skills-mode assertions with the existing copilot driver suite
instead of replacing it.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(copilot): harden Skills shell gate against chained commands
Validate only the local netcatty-tool-cli prefix (before exec `--` payloads),
reject shell wrappers/chaining operators, and require the invocation to start
with the CLI launcher or script path.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(copilot): block backtick substitution and drop skill builtin
Reject backticks in the local CLI prefix and expose only builtin:bash in
Skills mode; CLI workflow instructions already come from the host prompt.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(copilot): quote-aware shell gate and strict CLI name match
Use the last unquoted exec delimiter for local-prefix checks, scan metachars
outside quotes only, and require an exact netcatty-tool-cli launcher token.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(copilot): tighten exec delimiter and Windows CLI launcher match
Only honor ` -- ` payloads for exec/job-start, scan full local commands for
redirection/chaining, and accept netcatty-tool-cli.cmd with Windows paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(copilot): reject quoted substitution and unqualified CLI paths
Block $(...) and backticks inside double quotes, require absolute/relative
path prefixes for non-node launchers, and fix quoted launcher path matching.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(copilot): anchor shell metachar scan and block exec chaining
Match metacharacters only at the current scan offset, scan the full shell
command for exec turns, and require quoted remote payloads for chained ops.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(capabilities): add shared agent capability layer for MCP and CLI
Introduce a registry-driven capability foundation so terminal, SFTP, vault,
and port-forward tools can be extended from one policy and surface definition.
Wire mcpServerBridge permission checks and the Skills CLI RPC client through
the shared layer without changing existing AI runtime behavior.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(capabilities): restore SFTP cancel gating and home RPC timeout
Match legacy mcpServerBridge behavior by blocking SFTP write RPCs after
chat cancel and treating sftp/home as a long-running operation for timeout
budget calculation.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Windows system fonts omit flag glyphs and show ISO country codes instead
of flag icons. Bundle a Noto Color Emoji regional-indicator subset and
prepend it to the Windows UI sans stack so host labels display real flags.
Co-authored-by: Cursor <cursoragent@cursor.com>
Popup and other standalone Terminal mounts do not publish paneVisibilityStore state; hibernate now respects the isVisible prop instead of treating missing entries as hidden.
Co-authored-by: Cursor <cursoragent@cursor.com>
Skip backend pwd probes after commands once the shell reports OSC 7, even when the path is unchanged, so sudo sessions no longer alternate between login-shell fallback and the interactive cwd. Also silence OSC 7 setup script status text in the terminal since the toast already confirms success.
Co-authored-by: Cursor <cursoragent@cursor.com>
Loop pending-buffer replay until caught up before stopping listeners, and re-arm hibernate when wake finishes while the pane is still off-screen.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep hibernate listeners active during async replay, append any output that arrived mid-replay, then stop listeners immediately before reattach.
Co-authored-by: Cursor <cursoragent@cursor.com>
Stop hibernate IPC listeners before reading the wake payload so post-snapshot output is included and skipped reattach does not leave stale listeners.
Co-authored-by: Cursor <cursoragent@cursor.com>
Read pending buffer after async mount and verify connectivity before reattaching so in-flight SSH output and late disconnects are handled correctly.
Co-authored-by: Cursor <cursoragent@cursor.com>
Read live session connectivity at wake time, persist hibernated output on unmount, and skip scrollToBottom for alternate-screen resumes.
Co-authored-by: Cursor <cursoragent@cursor.com>
Route session exit to the hibernate buffer instead of a disposed xterm, and only tear down hibernate IPC listeners after a successful disconnected wake.
Co-authored-by: Cursor <cursoragent@cursor.com>
Defer clearing hibernate replay state until wake succeeds, skip reattach when the backend session is disconnected, preserve alternate-screen detection on serialize failure, and wake immediately when hibernate is disabled.
Co-authored-by: Cursor <cursoragent@cursor.com>
Dispose xterm for off-screen tabs while keeping SSH connected, with configurable delay, file-transfer exemptions, and wake replay for alternate-screen apps.
Co-authored-by: Cursor <cursoragent@cursor.com>
Include host and session in handleDetachPointerDown dependencies so drag
label stays in sync when dynamic tab titles or host settings change.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(terminal): narrow snippet terminal restore to RHEL-family hosts
Limit auto-run snippet stty protection to distros affected by #1498 and
suppress wrapper echo so Ubuntu, Rocky, and macOS users see their raw
commands again.
Fixes#1594
Co-authored-by: Cursor <cursoragent@cursor.com>
* revert(terminal): remove auto-run snippet stty protection (#1594)
The wrapper added in #1498 caused visible preamble noise for all snippet
executions. Revert to inserting the original command directly; the underlying
CentOS 7 terminal issue reproduces when running the script manually too.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix tmux detach focus regression on Windows (#1587)
Replace native confirm with an in-app dialog and restore terminal cursor/focus after detach so sidebar tmux actions keep working.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Skip terminal refocus while app dialogs are open
Avoid deferred tmux detach focus from stealing Radix modal focus when users open the new-session dialog immediately after detach.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Windows was showing regional-indicator letters instead of composed flag
emoji in host labels because bundled UI fonts win font matching. Prepend
Segoe UI Emoji to the UI sans stack on Windows, including when settings
apply a custom UI font at runtime (#1589).
Co-authored-by: Cursor <cursoragent@cursor.com>
Try passwordless sudo before requiring a saved sudo password when Docker
socket access is denied, matching the popup terminal behavior.
Co-authored-by: Cursor <cursoragent@cursor.com>
Send auto-run multi-line snippets line-by-line with delay instead of bracketed-paste burst. Fixes#1590.
Also limit broadcast line delay to explicit snippet options so normal multi-line broadcast pastes stay byte-for-byte.
Route Netcatty context through body.system, filter echoed user parts,
keep OpenCode env/shim alive for server lifetime, and cap model preset dropdown height.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: add dynamic terminal tab titles for shell-reported session names
Follow OSC 0/2 window titles on session tabs so tools like Claude Code can show live session state, with a per-host opt-out to keep connection names instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: address dynamic tab title review findings
Restore the missing cn import, clear dynamic titles when the shell sends an empty OSC title, and drop stored dynamic titles when a host disables the feature.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: preserve renamed session labels and align focus sidebar titles
Prefer user-assigned customName over shell titles, skip no-op dynamic title updates, and use resolveSessionTabTitle in the workspace focus sidebar.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: show coding agent icons on session tabs
Detect Claude Code, Codex, OpenCode, Droid, and other CLIs from OSC titles and launch commands, reusing shared agent icon assets inspired by freshell pane icons.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: harden coding CLI detection across SSH and startup paths
Wire output scanning through attachSessionToTerminal, seed providers from
auto startup commands, prevent title signals from overriding command
detection, clear sticky state on shell titles, and release output scanners
on session close.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: reset CLI output scanner when provider state clears
Prevent exited-agent banners from immediately re-sticking the tab icon
after the shell title returns to a normal shell name.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: update sticky CLI provider when title signals a different agent
Allow agent switches in the same terminal while still resetting the output
scanner whenever the provider identity changes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: avoid clearing CLI provider on blank titles and cap output scan
Ignore transient empty OSC titles and stop banner scanning after 16KB of
unmatched output so regular SSH sessions do not pay perpetual scan cost.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: prefer runtime CLI provider over host startup command on tabs
Runtime detection from commands, output, and titles should win over static
host startup commands when resolving the session tab icon.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: satisfy eslint for terminal escape stripping regexes
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep sortedNotesRef in sync on render and route note mutations through commitNotes so imports merge against the latest vault state.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add folder context-menu import, show copy buttons only in preview, tighten code block spacing, and fix language selector/dropdown sizing.
Co-authored-by: Cursor <cursoragent@cursor.com>
Read Markdown payloads first, then merge against the latest note list so in-flight edits are not overwritten during import.
Co-authored-by: Cursor <cursoragent@cursor.com>
Skip fenced code blocks when deriving import titles, guard concurrent imports with a ref, and tighten the file picker accept list.
Co-authored-by: Cursor <cursoragent@cursor.com>
Support bulk .md/.txt import into the current notebook folder and add hover copy buttons on note code blocks in edit and preview modes.
Co-authored-by: Cursor <cursoragent@cursor.com>
Sync AI API keys through cloud sync while keeping device-local external tool paths local. Also preserves local AI keys in backups and stops sync if a local encrypted key cannot be read.
* fix(ssh): complete publickey+password MFA in a single connection
Send the key and saved password in one connection when both are set, and
re-offer a previously-rejected method after the server's partial success.
* fix(ssh): preserve local identity files for key auth; trim comments
* test: align SSH key password MFA expectation
---------
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
* fix: let Ctrl+C send SIGINT when no text is selected in terminal
When the copy shortcut is customized to Ctrl+C (default Ctrl+Shift+C),
the terminal always consumed the event for copy even without a text
selection, preventing the standard Ctrl+C → SIGINT interrupt signal.
Added a guard in attachCustomKeyEventHandler: if the matched action is
'copy' and term.hasSelection() is false, return true to pass the event
through to xterm.js, which encodes it as \x03 (ETX) for normal SIGINT
delivery. When text IS selected, copy proceeds as before.
This change is platform-agnostic (renderer-side only) and does not
affect any other terminal actions or default key bindings.
* fix: gate copy passthrough on Ctrl+C/⌃C interrupt chord specifically
The previous fix passed ALL no-selection copy bindings through to xterm,
which would send unintended escape/control sequences to the remote
process if copy were bound to keys like F5 or Ctrl+L.
Now gate the passthrough on the exact interrupt chord: Ctrl+C (PC) or
⌃C (Mac) — key 'c' with only Ctrl pressed, no Shift/Alt/Meta. Any other
copy binding with no selection is consumed as a safe no-op.
* fix: preserve Ctrl-C passthrough for copy shortcut
---------
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
* feat: auto-poll Docker capabilities while Docker tab is active
When the Docker tab is visible and hasDocker is not yet true,
poll refreshCapabilities() at the process refresh interval.
Stop polling once hasDocker becomes true, or when switching
to a different tab.
* fix: use resolvedTab instead of activeTab for Docker auto-poll condition
The auto-poll useEffect condition used activeTab, which stays stale
when Docker becomes unavailable. Changed to resolvedTab which reflects
the actual displayed tab. Also updated the dep array.
* fix: replace setInterval with setTimeout recursion in Docker tab probe
Replace setInterval-based polling with setTimeout recursion in the Docker
tab capability probe effect. This ensures the next probe only starts after
the previous one finishes, avoiding overlapping probes when SSH timeout
exceeds the polling interval.
- Add dockerPollTimerRef to track the timeout handle
- Use async pollOnce() that awaits refreshCapabilities() before scheduling next
- Use cancelled flag in cleanup to prevent scheduling after unmount
- Keep same dependency array for correctness
* fix: stabilize docker poll timer by using useRef for refreshCapabilities
refreshCapabilities() can return a new reference on every render, causing
the useEffect to re-run on every render — cleanup cancels the polling timer,
then the effect immediately calls pollOnce(), effectively bypassing the
configured timeout interval.
Fix: store refreshCapabilities in a useRef (refreshRef), use
refreshRef.current() inside pollOnce(), and replace refreshCapabilities
with refreshRef in the useEffect dependency array.
Closes #PR1456 Codex P2 review item.
* fix: delay auto-poll first probe by one interval to avoid overlap with tab-switch probe
When switching to the Docker tab, two mechanisms were triggering probes:
1. tab-switch effect (line 67-76): immediate probe via refreshCapabilities()
2. auto-poll effect: pollOnce() executing immediately on mount
This caused duplicate probes that waste SSH channel resources.
Fix: pollOnce() no longer fires on mount. Instead, the effect schedules the
first probe with setTimeout(pollOnce, capabilitiesTtlMs), so the first probe
happens after one full interval. Subsequent probes continue at interval pace
via the setTimeout recursion in pollOnce itself.
The tab-switch effect still fires the immediate probe (the correct one),
so responsiveness on tab switch is preserved.
* fix: reset cancelledRef in effect body to prevent permanent stalling of Docker polling
The cancelledRef was set to true in the cleanup function when dependencies
changed, but never reset when the effect re-ran. This caused pollOnce to
always early-return on subsequent timer ticks, permanently halting
Docker capability probing after the first dependency change.
* fix(system-manager): replace cancelledRef with closure variables for per-effect cancellation
Each effect generation now has its own and closure
variables instead of shared / . This
prevents stale probes from surviving cleanup when the panel hides and
re-shows (Codex P2 review).
* fix: wrap refreshCapabilities in try/catch to keep polling on exception
If refreshCapabilities throws (instead of returning {success: false}),
the await would exit pollOnce without scheduling the next setTimeout,
silently killing Docker auto-detection polling.
* fix: add in-flight probe guard to prevent tab-switch and auto-poll concurrent probes
Add probingRef to track whether a capabilities probe is already in-flight.
- Tab-switch effect for Docker branch checks probingRef before starting a new probe
- Auto-poll pollOnce checks probingRef at entry and sets/clears it around the actual probe
- Tmux branch left unchanged as it has no auto-poll overlap risk
* fix: re-schedule next poll timer when probe is in-flight
When probingRef.current is true (tab-switch probe still running),
pollOnce was returning early without scheduling the next timer,
causing auto-poll to stop permanently afterward.
Now it schedules the next poll within the interval and returns,
so the polling loop keeps running until a slot where no probe is
active.
* fix: convert comments to ASCII-only English
- Line 105: translate Chinese comment to 'probe is in-flight, reschedule for next cycle'
- Line 113: replace em dash (U+2014) with ASCII dash
* feat: session inline rename, closeSession shortcut, pane zoom
* fix: sidebar inline rename with local state
* fix: add sessionDisplayName to terminalPropsAreEqual comparator
The Terminal component is wrapped with React.memo(…, terminalPropsAreEqual),
but the comparator was missing a check for sessionDisplayName. After renaming
a session, the pane title bar would show the old name until some other prop
changed and triggered a re-render.
Add prev.sessionDisplayName === next.sessionDisplayName to the comparator
so that display name changes cause the Terminal to re-render immediately.
* fix: add onStartSessionRename to TerminalLayerWorkspaceSection ctx destructuring and TerminalPanesHost props
* fix: add toggleWorkspaceViewMode to executeHotkeyActionImpl destructuring
The togglePaneZoom handler calls toggleWorkspaceViewMode() but it
wasn't destructured from getCtx(), causing a ReferenceError at runtime.
* fix: restore truncated ctx object in TerminalView render call
The TerminalView ctx object literal on line 1265 was truncated to
'showSele...' due to an editing tool truncation bug, causing
Parsing error: ',' expected on npm run lint / tsc --noEmit.
Restored the missing fields from the base commit:
showSelectionAIAction, snippets, status, statusDotTone, sudoHintRef,
sudoHintText: t("terminal.sudoHint.pressEnter"), t, termRef,
terminalBackend, terminalContextActions, terminalCwdTracker,
terminalPreviewVars, terminalSettings, timeLeft, toast, zmodem
Kept the PR's new additions (isVisible, onRename, sessionDisplayName)
intact.
* fix: add toggleWorkspaceViewMode to executeHotkeyAction context and add terminal.menu.rename translations
- Add toggleWorkspaceViewMode to the context getter in executeHotkeyAction (App.tsx)
- Add terminal.menu.rename translation for en (Rename), zh-CN (重命名), ru (Переименовать)
* fix: validate focusedSessionId before closing in closeSession hotkey
When closeSession hotkey fires, workspace.focusedSessionId may reference
a session that was already closed by another trigger (e.g., mouse click
on tab close button). Collect alive session IDs from the workspace root
and fall back to the first living pane if the stored focusedSessionId
is stale.
* fix(auto-poll): check useSessionCapabilities probing state in pollOnce
When auto-poll timer fires before the initial probe (from
useSessionCapabilities) completes, probingRef.current is still false
because the initial probe doesn't set it — causing a second overlapping
probe.
Add check so that any in-flight probe from any path
(initial/auto-poll/tab-switch) prevents auto-poll overlap.
PR #1459
* fix: address remaining Codex review issues
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: add detach session from workspace with toolbar button and context menu
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: use customName in pane header display name for renamed sessions
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: refine workspace terminal detach interactions
* fix: preserve workspace detach tab ordering
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(system): increase process list limit and improve Docker detection for openEuler
Root cause analysis for issue #1453:
1. Process list limit too low: The `head -n 200` pipeline capped the process
list at 200 entries, causing the displayed count to mismatch `ps aux | wc -l`
on systems with many processes (common on openEuler servers with Docker,
databases, etc.). Increased limit from 200 to 2000 for both Linux/SSH
(ps) and Windows (PowerShell) backends.
2. Docker detection failure on openEuler 24.03: The capability probe only
relied on `docker info >/dev/null 2>&1`, which can fail even when Docker
is running due to:
- SSH exec channel environment differences vs interactive shell
- Docker socket permission variations in non-interactive sessions
- Different socket path configurations on openEuler
Added a fallback: if `docker info` fails but the Docker socket exists at
`/var/run/docker.sock`, Docker is still detected as available. This
matches the behavior of other SSH terminal clients.
* fix(system): also add Docker socket fallback to fallback probe script for consistency
* fix(system): remove hardcoded process list limit, add capability probe TTL, auto-reprobe on tab switch
Three remaining issues from PR #1455:
1. Remove hardcoded `head -n 2000` / `Select-Object -First 2000`
process list limits — virtual list handles rendering efficiently.
2. Add 60-second TTL to sessionCapabilitiesStore cache. `get()` returns
undefined for expired entries, forcing re-probe on next access.
`set()` always refreshes `probedAt`. Export CAPABILITIES_TTL_MS
constant for future tuning.
3. Auto-trigger capability re-probe when switching to Docker/Tmux tab
whose tool was previously reported unavailable — handles the case
where Docker/Tmux was installed after the last probe.
* fix: replace Docker socket -S check with -r for permission accuracy; sync capabilities TTL with process refresh interval
- Change [ -S /var/run/docker.sock ] to [ -r /var/run/docker.sock ] in
both the main capability probe script and the POSIX fallback (electron bridge).
-r verifies the socket exists AND the current user has read permission,
preventing false-positive Docker detection that leads to failed Docker ops.
- Remove hardcoded CAPABILITIES_TTL_MS (60s) from sessionCapabilitiesStore.
Store now computes expiresAt internally in set(ttlMs) and checks it in get()
without requiring a parameter at call sites.
- useSessionCapabilities and useSystemCapabilitiesWarmup accept a
capabilitiesTtlMs parameter derived from
terminalSettings.systemManagerProcessRefreshInterval (default 3s → 3 000ms).
- SystemManagerSidePanel passes the TTL from terminalSettings to the hook.
- TerminalLayerTabBridge passes TTL from stableRef settings to warmup hook.
- Fix missing refreshCapabilities destructuring in SystemManagerSidePanel.
* fix: restore process list safety cap (head -n 2000 / -First 2000)
Codex review flagged that removing the process list cap entirely could cause
timeout/maxBuffer issues on process-dense hosts. Restore head -n 2000 (POSIX)
and -First 2000 (Windows) as a safety guard with comments clarifying this is
NOT a functional limit — monitored processes still show accurate metrics.
* fix: hoist useRef/useEffect before early returns to fix React hook order violation
The useRef and useEffect for tab-switch re-probe were placed after early returns
for missing/disconnected sessions. When a session later connects, React discovers
new hooks that weren't registered before, causing hook order violation crashes.
Moved both hooks immediately after the resolvedTab computation, before any early
return path, satisfying React's Rules of Hooks.
* fix: change Docker detection from OR to AND (CLI + socket)
Both capability detection and fallback probe now require:
- docker CLI is on PATH (command -v docker)
- docker.sock is readable ([ -r /var/run/docker.sock ])
Previously used OR logic (docker info || socket readable),
which could report hasDocker=true even when docker CLI
was unavailable (e.g., non-login SSH shell).
Fixes#1453
* fix(system-monitor): prefer docker info, fallback to CLI+socket
Co-authored-by: Codex <codex@anthropic.com>
Changes:
- Line 12: Replace strict CLI+socket check with docker info first,
falling back to CLI+socket check only if docker info fails.
- Line 139: Same fix in the fallback probe script.
This handles DOCKER_HOST, Docker contexts, and rootless Docker.
* fix: notify subscribers when TTL expires in sessionCapabilitiesStore.get()
When capabilitiesBySessionId.get() finds an expired entry, it deletes the
entry but did not notify session subscribers. This caused components to
stale capabilities until the next successful set() call.
Now get() calls notifySession() on expiry, matching the notification
behavior already present in delete().
Resolve conflicts in terminal backend, Terminal, and TerminalView to keep ZMODEM drag-drop alongside serial YMODEM receive and terminal selection AI settings from main.
Co-authored-by: Cursor <cursoragent@cursor.com>
Syncs the terminal selection Add to Conversation preference with AI settings so cloud sync, local backups, restores, and settings-only auto-sync detect the value.
Follow-up for #1436.
Adds a Settings > AI switch to hide the automatic Add to Conversation button on terminal selection while keeping the context menu action available.
Closes#1397
- Remove outdated SFTP upload message and replace it with ZMODEM-specific messages in English, Russian, and Chinese locales.
- Add a new function to handle ZMODEM drag-and-drop uploads in the terminal backend.
- Update terminal components to support ZMODEM drag-and-drop functionality.
- Enhance error handling for file uploads and provide user feedback for no files to upload.
- Introduce tests to verify ZMODEM upload behavior and fallback to SFTP for network devices.
Hide Netcatty-managed Docker and tmux terminal launch commands from command history.
Validated locally with lint, full tests, and build. Multi-agent review completed with no remaining issues.
Hide timestamp controls in popup shell windows and keep the terminal host sidebar width while root pages are shown so returning to terminal tabs does not replay the open animation.
Show an X control beside the host sidebar search input so users can reset the filter without manually deleting text.
Co-authored-by: Cursor <cursoragent@cursor.com>
Expose a quick Activity icon on SSH Linux sessions so users can jump directly to the system manager side panel from the terminal header.
Co-authored-by: Cursor <cursoragent@cursor.com>
Use the shared bg-muted selected state so history host/global tabs match the system manager tab styling.
Co-authored-by: Cursor <cursoragent@cursor.com>
Introduce workspace-aware System side panel with remote process/tmux/Docker management, terminal popup for interactive attach, capability warmup, review-hardened IPC, performance optimizations, toast action errors, and SSH channel recovery on reconnect.
Record commands as users type across sessions with dedup and AI-noise
filtering, and browse them alongside per-host remote history. Refine the
scope switcher UI and route fullscreen layout recovery through the terminal
backend hook. Closes#1253.
Co-authored-by: Cursor <cursoragent@cursor.com>
Fixes#1382
Deleting the active AI chat session left panelViewByScope pointing at a removed session, causing the side panel UI to blank. Sync panel view cleanup with session deletion, stop in-flight streams on delete, and return to draft with the deleted session's agent preserved.
* feat(terminal): add remote command history side panel
Read remote shell history over SSH/ET/Mosh exec channels, browse it in a virtualized side panel with search, paste, and save-as-snippet actions. Closes#1381.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(history): expand command detail inline below selected row
Move the detail strip from a fixed slot above the list into the row
immediately below the clicked entry so expansion reads top-to-bottom.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(history): filter Netcatty AI PTY commands from remote history
Drop shell history lines containing the __NCMCP_ marker so AI exec noise
does not clutter the command history panel.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(history): tighten detail strip and add run action
Size the expanded row to its content, add a run-in-terminal button, and
use clearer snippet icon/tooltip for save-as-snippet.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(history): address review findings before merge
Key cache by host+session, retry Mosh pending reads, and clamp virtual
list scroll position when filtered items shrink.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Allow the quick switch shortcut to close the panel when pressed again,
including while the search input is focused, so users are not limited to
clicking outside to dismiss it.
Closes#1355
Co-authored-by: Cursor <cursoragent@cursor.com>
Restore resize when macOS App Nap or GPU eviction leaves xterm stale after
switching away, by refitting on visibility/focus/fullscreen and fixing the
ResizeObserver race with async xterm boot.
Co-authored-by: Cursor <cursoragent@cursor.com>
Cold-start local terminals on Linux could cache Powerline icon tofu when the
shell prompt arrived before Symbols Nerd Font Mono finished loading. Preload
the icon fallback at the active cell size and clear the xterm atlas so
already-drawn prompt glyphs re-rasterize (fixes#1363).
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(et): support server stats for EternalTerminal sessions
- Generalize the Mosh stats companion into reusable connection helpers
- Open a companion SSH connection so the host-info bar works for ET sessions
- Fall back to execOnEtSession for jumped ET sessions without a direct connection
- Forward host-key and algorithm options to the ET backend for companion parity
- Close the ET stats companion on session close, cleanup, and PTY exit
* fix(et): harden stats exec host-key trust and cleanup
Enforce StrictHostKeyChecking=yes for background ET stats/distro probes
instead of accept-new, merge vault known_hosts for parity with ssh2
companions, and wrap companion connection teardown in try/catch.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Store per-session font size in workspace splits so Ctrl+wheel zoom no longer
changes sibling panes or reverts on blur when terminalSettings re-sync runs.
Co-authored-by: Cursor <cursoragent@cursor.com>
Fixes#1375 by letting Cmd/Ctrl+[1...9] target only work tabs when enabled, while keeping the existing Terminus-style default.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the compose bar inside the terminal workspace so SFTP side panels stay full height, refit xterm when the bar toggles, and remeasure split-pane geometry when side panels open or close.
Co-authored-by: Cursor <cursoragent@cursor.com>
Restore xterm keyboard focus after top-level tab changes so macOS users
can type immediately without an extra click (discussion #1339).
Co-authored-by: Cursor <cursoragent@cursor.com>
Add a persistent quick-snippet strip, draggable height, and terminal-matched UI to the compose bar, addressing quick-command and resize requests from community discussions.
Co-authored-by: Cursor <cursoragent@cursor.com>
* perf(settings): reduce Mac settings window input lag (#1347)
Debounce custom CSS commits, memoize heavy tabs, and replace Radix ScrollArea
with native scrolling so typing and navigation stay responsive on macOS.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(settings): flush debounced textarea on unmount
Avoid losing custom CSS edits when the settings window closes before the
debounce timer fires.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(terminal): resolve bold font weight without document.fonts.check false positives
Chromium reports unavailable bold weights as available, so xterm tried to rasterize weight 700 while the bundled JetBrains Mono fallback only ships 400/500/600. Bold glyphs then rendered as black blocks on Linux local terminals (fixes#1364).
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: drop unused primaryFontFamily from terminal effects context
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ui): improve host tree inline group rename interactions
Cancel rename when clicking another tree row and prevent parent drag from blocking text selection in the rename input.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ui): block group toggle keyboard while inline renaming
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ui): cancel host inline rename when clicking other tree rows
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Root cause: FPM-generated .pacman packages copy icons directly to
/usr/share/icons/hicolor/*/apps/netcatty.png, bypassing Arch's alpm
hooks that normally run gtk-update-icon-cache. Without a refreshed
cache, KDE Plasma cannot resolve Icon=netcatty and falls back to a
generic document icon in the app menu.
Fix:
- Copy electron-builder's default after-install template to
scripts/linux/after-install.tpl, append gtk-update-icon-cache call
- Create scripts/linux/after-remove.tpl with the same cache refresh
- Wire into pacman.afterInstall/pacman.afterRemove
(NOT linux.afterInstall — the schema places these under target-level
options like PacmanOptions/DebOptions, not LinuxConfiguration)
- Add test in electron-builder-config.test.cjs
The command is idempotent on systems without gtk-update-icon-cache
(hash guard) and uses || true to never break package installation.
Align window controls with utility icons, extend hover to the full title bar height, restore red close-button hover, flush close to the right edge, and use neutral gray hover for top-bar utility buttons.
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace immersive instant-switch with animated active chrome theme sync so
top tabs match terminal sessions immediately on tab click, and clamp
autocomplete popups to the active pane so they stay anchored to the cursor
in split workspaces.
Co-authored-by: Cursor <cursoragent@cursor.com>
Windows + Node >= 24: spawning .cmd files with shell=false causes EINVAL.
Claude Code v2.1.169 ships as native binary (no cli.js), npm global install
creates only claude.cmd. Netcatty detected claude.cmd but spawned it with
shell:false -> EINVAL.
Changes:
- resolveWindowsShimToNativeExe: new function that reads .cmd/.bat shims
and resolves to the real .exe using "%~dp0\...\*.exe" pattern matching
- prepareCommandForSpawn: tries native exe resolution first, falls back
to shell:true wrapping
- resolveClaudeCodeExecutableForSdk: when cli.js not found, looks for
bin/claude.exe native binary
- 3 new tests for shim resolution and spawn spec
- Codex CLI unaffected (already handles native exe resolution)
Test: 38/38 shellUtils tests pass, npx tsc --noEmit clean
Fix terminal drag-and-drop uploads so they target the active terminal cwd and avoid fallback home/login-shell cwd when the active cwd cannot be confirmed.
- event.preventDefault() must be called synchronously before the
async IPC call, otherwise the browser processes the default paste
action before we can intercept it
- When clipboard has no files (or on error), fall back to text paste
via pasteTextIntoTerminal since the default action was already
prevented
When pasting (Ctrl+V / right-click paste) in a local terminal,
if the clipboard contains files, insert their paths instead of
doing nothing.
- New hook useTerminalFilePaste: capture-phase paste listener
on terminal container, reads clipboard files via Electron bridge,
formats paths (spaces quoted, deduped), writes to session
- Updated useTerminalContextActions: right-click paste checks
clipboard files first, falls back to text paste
- New terminalHelpers.extractRootPathsFromClipboardFiles
- Tests: 9 unit tests for path extraction logic
- Verified via headless Chromium integration test (15 tests)
- Build: npm run build ✅, npm test ✅ (1899 pass)
* fix(linux): restore multi-size hicolor icons for Ubuntu launchers (#1340)
PR #816 set linux.icon to a single 1024px PNG, which regressed the #274
fix and left only hicolor/1024x1024 on .deb installs. Drop the override
so electron-builder uses build/icons again, regenerate those PNGs from the
tight-crop icon-win source, and add a helper script plus a config test.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(linux): set linux.icon to icons dir for proper multi-size hicolor icons (#1340)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(vault): add duplicate host action to tree view context menu
Wire the existing onDuplicateHost handler into vault host tree menus so
tree view matches grid/list duplicate behavior. Fixes#1329.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(vault): switch to hosts section when duplicating from terminal tree
Ensure the host details panel is visible after duplicate is triggered
from the terminal host tree sidebar.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ai): cap Catty agent request payload to prevent HTTP 413
Long-running chats accumulated full terminal tool outputs in SDK history
while token-based compaction only triggered near the model context window,
so nginx gateways could reject oversized JSON bodies before the model saw them.
Add a byte-budget pass that compresses verbose output, tail-preserving
truncation, and a safe sliding window before each Catty agent turn.
Fixes#1323
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ai): compaction was using unfiltered sdkMessages, fix didAdjust and emergency loop
* fix(ai): compact and retry oversized Catty requests
* fix(ai): preserve current input while guarding 413 retries
* fix(ai): avoid false 413 detection and fit oversized current input
* fix(ai): pair replayed tool results chronologically
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* perf(terminal): smooth layout drags and faster tab switching
Defer xterm refit during split, sidebar, and host-tree drags while keeping pane containers in sync with live layout measurements. Refactor TerminalLayer into focused sections with TabBridge/memo optimizations and add the terminal host tree sidebar.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(terminal): keep side panels alive and guard session attach races
Prevent terminal boot unmount from leaking backend sessions, keep SFTP/scripts/theme/AI state when switching side tabs, and defer heavy SFTP UI mount so first entry stays responsive.
Co-authored-by: Cursor <cursoragent@cursor.com>
* perf(terminal): reduce tab switch jank
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Avoid accidentally persisting built-in editor as the default for all
extensionless files when double-clicking binaries without an extension.
Co-authored-by: Cursor <cursoragent@cursor.com>
Introduce shared SettingCard and SettingsSection primitives so AI, SFTP,
system, and terminal tabs use the same white-card + row control pattern.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(sftp): add follow terminal directory mode for sidebar (#1266)
Add a toolbar toggle that keeps the side-panel SFTP browser synced with the linked SSH terminal cwd, inspired by MobaXterm's follow-folder behavior.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): probe pwd after commands when follow mode lacks OSC 7
Add a deferred getSessionPwd fallback after terminal commands when follow-terminal-cwd is enabled and the shell did not report OSC 7, and fix settings sync hook dependencies.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): repair follow toggle UI and backend cwd sync fallback
Fix SftpPaneView memo skipping follow prop updates, probe fresh pwd when OSC 7 is missing, and broaden linked terminal session resolution for sidebar follow mode.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): harden follow sync after review findings
Reuse shouldFollowTerminalCwdNavigate in production path, re-read connection
after async cwd probe, skip redundant navigate on toggle, and only probe pwd
when the SFTP side panel is open.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Require bug/feature reports via issue forms with automated format checks, while accepting legacy Bug: titles from older app builds.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add issue #1293 screenshot-exact cases for sudo/telnet autofill and
include English password in the handleOutput fast-path bypass.
Co-authored-by: Cursor <cursoragent@cursor.com>
Kylin Professional's sudo prompt doesn't include the [sudo] tag and
doesn't end with a colon. The existing regex patterns required either
[sudo] or a trailing colon, causing the autofill hint to never fire.
Changes:
- Make trailing colon optional in SUDO_PROMPT_PATTERN and
EXPLICIT_SUDO_PROMPT_PATTERN (terminalSudoAutofill.ts)
- Update fast path to not skip output containing Chinese password
keywords (密码/口令) that lack a colon
- Make trailing colon/angle-bracket optional in telnet username and
password prompt patterns (telnetAutoLogin.cjs)
- Also relax LAST_LOGIN_PATTERN for consistency
- Add Kylin-style test cases for both sudo and telnet auto-login
Expose whole-window transparency via setOpacity with settings and a top-bar quick control, persisting across restarts and syncing across windows.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: align top bar right-side icon buttons (#1298)
Unify AI/sync/theme/settings buttons to h-7 in one row aligned with tabs.
SyncStatusButton was h-8 and settings lived in a separate container, causing
misalignment. Preserve Windows spacing before window controls (mr-2).
Fixes#1298
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: align window controls with utility icons in top bar
Merge window controls into the same row as utility buttons, match h-7 height, add left margin separation, and restore rectangular gray hover for all three buttons.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Expose data-section selectors for SFTP/side panel, split panes, and resizers
so custom CSS can target the correct regions. Clarify docs that
terminal-workspace-sidebar is focus-mode only.
Fixes#1301
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ai): infer MIME type from file extension for YAML and other code files
When uploading YAML files (and other code/text files) via Electron,
file.type is often empty, causing the system to default to
'application/octet-stream'. AI providers reject this media type
with 'functionality not supported'.
Fix by inferring the correct MIME type from the file extension
when file.type is empty. Includes mappings for YAML, JSON, TOML,
shell scripts, and 50+ common code/text file extensions.
Fixes#1287
* fix(ai): use text/plain for all code/text files to ensure provider compatibility
Change all non-standard MIME types (text/x-*, application/x-*) to
text/plain for maximum provider compatibility. Anthropic and other
providers reject non-standard MIME types like application/x-yaml
with 'UnsupportedFunctionalityError'.
Changes:
- All code files (js, ts, py, rb, rs, go, java, c, cpp, sh, etc.) → text/plain
- Web component/stylesheet files (vue, svelte, scss, sass, less) → text/plain
- yaml/yml → text/plain (was application/x-yaml)
- dockerfile → text/plain (was text/x-dockerfile)
- Standard types (html, css, json, xml, csv, md, txt, pdf) preserved
* fix(telnet, sudo): support Chinese-localized prompts with full-width colons (#1286)
Two bugs in prompt detection for Chinese-locale users:
1. telnetAutoLogin: USERNAME_PROMPT_PATTERN, PASSWORD_PROMPT_PATTERN,
and LAST_LOGIN_PATTERN only matched half-width colon ':' or '>'.
Chinese-locale telnet prompts use full-width colon ':' (U+FF1A),
e.g. '登录:', '密码:'. Changed [:'>] to [::'|] in all three
patterns to accept both colon variants.
2. terminalSudoAutofill: EXPLICIT_SUDO_PROMPT_PATTERN required
'[sudo]' (closing bracket immediately after 'sudo'), but Chinese
sudo prompts use '[sudo: authenticate] 密码:' format where sudo
is followed by colon. Changed \[sudo\] to \[sudo[^\]]*\] to
match any '[sudo...]' variant, making the explicit (no-arm-needed)
hint detection work for Chinese locale.
Fixes#1286
* fix: restore OSC stripping pattern broken in previous commit
The regex negated character class [^\x07]* was truncated to just \x07,
breaking OSC sequence stripping (e.g. window title changes embedded in
terminal output). Restore the original negated class so stripTerminalControl-
Sequences continues to remove OSC title sequences before prompt detection.
This was caught by Codex review of PR #1288.
Sudo hints were flaky for manually typed commands: arming depends on
recognizing the submitted line as a command (recordedCommand), which is
unreliable while echo round-trips over SSH — so the hint sometimes didn't
fire for "sudo -i" / "sudo -s".
Explicit "[sudo] …" prompts are sudo-specific, so hint on them without
requiring an arm — reliable regardless of command recording. Bare
"Password:" still needs the arm window to avoid noise on unrelated prompts
(ssh, mysql). Filling still requires explicit Enter, so showing the hint
without arming stays safe. Added a colon fast-path so bulk output skips the
detection regex.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sudo autofill only read host.password and never resolved a host's reference
to a Keychain identity (host.identityId). When the account password lived in
a referenced identity, the autofill got nothing — while SSH login worked
because it goes through resolveHostAuth, which resolves the identity.
Add domain resolveHostAutofillPassword (same resolveHostAuth resolution:
identity.password ?? host.password, honoring savePassword and dropping
undecryptable placeholders) and use it as the terminal autofill password
source. Login and autofill now share one resolution path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rework sudo password autofill from auto-fill to a Tabby-style hint + Enter to confirm. When a sudo command is armed and a password prompt appears, show a dimmed inline hint instead of sending the password; Enter pastes the saved password and submits, any other key dismisses it.
Confirmation removes the credential-leak class (nothing is sent without the user pressing Enter at a visible hint), so detection is relaxed to a broad match (Ubuntu/PAM bare "Password:", "[sudo] password for…", localized prompts) and the per-host toggle is removed — always available when the host has a saved password.
Safety guards:
- don't arm when the hint can't render (no overlay) so Enter isn't silently intercepted;
- swallow Escape/Backspace so the byte never reaches the no-echo prompt;
- clear the pending hint once output moves past the prompt (sudo timeout/failure/returns to shell) so a later Enter can't leak the password to the shell.
Implementation ~140 lines; full suite green; manually verified on a real Linux host.
Replace the command-rewriting scheme (inject sudo -p marker, sanitize the echo, Ctrl-U retype) with passive observation: arm a short window on a sudo command and fill the password when a real sudo prompt appears.
- Fixes the Ubuntu/PAM no-fill, the cursor jump below the prompt, and the typed-vs-autocomplete discrepancy from #1281.
- Detection requires the [sudo] tag, or a whole-line bare "Password:" / "密码:"; prefixed prompts (mysql -p "Enter password:", ssh "x@h's password:", psql "Password for user x:") are rejected so the sudo password can't leak to a child program when sudo's creds are warm.
- Disarms when a non-sudo command follows, so a stale window can't fill a later prompt.
Implementation: 322 -> ~140 lines.
The dedupe in startSession.cjs runs under `with(ctx)`, where ctx is wired
with sshBridge.cjs's own local findDefaultPrivateKey /
findAllDefaultPrivateKeys — not the sshAuthHelper.cjs copies. The
characterization test targeted the helper's exports, which the connect
path never calls (sshAuthHelper.findDefaultPrivateKey has no production
consumers at all), so it gave false confidence and the in-code comment
pointed at the wrong test.
Expose the local pair as _findDefaultPrivateKey / _findAllDefaultPrivateKeys
(matching the existing _-prefixed test-export convention) and retarget the
test at them, so it actually guards the path the optimization depends on.
Behavior is unchanged; the two local functions are verified equivalent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each terminal that loads the WebGL addon holds a live WebGL context for
its whole lifetime, and all session panes stay mounted (hidden ones
off-screen). Batch connect therefore created a WebGL context per host up
front, contending for the GPU on the main thread — also the root of the
"garbled / 花屏" corruption in #1049/#1063. Defer WebGL creation for panes
that mount hidden (background tabs of a batch) and upgrade them on first
visibility via an idempotent ensureWebglRenderer(); a hidden pane renders
through xterm's DOM renderer until shown. Visible panes (single connect,
the active tab) keep creating WebGL immediately — unchanged behavior.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Batch-connecting N hosts called onConnect() in a synchronous forEach, so
all N terminals mounted in one React commit and each createXTermRuntime()
(which spins up a live WebGL context) ran back-to-back on the main
thread, freezing the UI until the whole batch finished (~2-3s per host,
linear). Spread the connects across frames via a small injectable-scheduler
helper: the first host still connects synchronously so its tab appears
immediately, the rest are deferred one step apart so no two heavy mounts
land on the same frame.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every SSH connect ran two separate ~/.ssh scans back-to-back:
findDefaultPrivateKey() then findAllDefaultPrivateKeys(). They share
identical filter/sort/encrypted-skip logic, so the first scan's result
is exactly findAllDefaultPrivateKeys()[0]. Derive the preferred default
key from the full list (scanned once) instead, and kick that single scan
off before the identity-file / inline-key preparation so the filesystem
work overlaps the key prep instead of running serially after it.
Behavior is unchanged: auth order and fallback keys are identical. The
equivalence the dedupe relies on is pinned by a new characterization
test against a faked ~/.ssh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a tab context menu action to duplicate a terminal into an independent peer window, with per-window active-tab titles and multi-window lifecycle safeguards.
When sourceSessionId is requested but the bridge falls back to a
fresh connection, the pane remains non-interactive (loading=true)
with stale cached files shown — acceptable trade-off vs always
showing the distracting spinner for near-instant reused connections.
The flag was persisting forever, suppressing loading UI for all
subsequent navigations and refreshes. Clear it once status
becomes 'connected' so only the initial reuse skips the spinner.
When SFTP reuses an existing terminal SSH connection, the
connection is near-instant so the loading spinner and overlay
are distracting noise. Added a reusedConnection flag to
SftpConnection and skip the loading UI when set.
Changes:
- SftpConnection model: +reusedConnection boolean
- useSftpConnections: set reusedConnection when sourceSessionId exists
- SftpPaneToolbar: skip animate-spin for reused connections
- SftpPaneFileList: skip loading overlay for reused connections
- SftpPaneTreeView: skip loading overlay for reused connections
Follow-up to #1254
When activeSessionId arrives after activeHost (e.g. focus
update in workspace), the effect must re-run to pass the
session ID to connect() — otherwise SFTP falls back to a
fresh SSH connection.
When opening the SFTP side panel for a host that already has an
active terminal session, reuse the terminal's authenticated SSH
connection instead of creating a new one.
Changes:
- TerminalLayer: compute activeTerminalSessionIdForSftp, matching
hostname/port/username against the active session
- TerminalLayerView: pass activeSessionId to SftpSidePanel
- SftpSidePanel: accept activeSessionId, pass to connect()
- useSftpConnections: pass sourceSessionId to bridge.openSftp()
- sftpBridge/openConnection: try to find and reuse terminal session's
SSH connection via findReusableSession, fall back to fresh connection
- sftpBridge: wire up acquireConnectionRef/releaseConnectionRef for
shared connection lifecycle
Only SSH (non-mosh/et/local) connected sessions are reused. Falls
back gracefully to a fresh connection on any reuse failure.
When a host is in a group with telnetPort configured, switching
protocol should not override the port to 23 — let the group default
take effect. Also handles undefined port during switch (fallback to
protocol default when no group defaults exist).
When a host inherits its Telnet port from a group config
(groupDefaults.telnetPort), the save handler should leave
port as undefined rather than materialising 23.
Added hasGroupTelnetPortDefault parameter to
resolvePrimaryProtocolSavePort to preserve inheritance.
When a user toggles the primary protocol to Telnet, the port field
previously stayed at 22 (SSH default). Now it auto-switches:
- SSH → Telnet: port 22 → 23
- Telnet → SSH: port 23 → 22
Custom ports are preserved.
Also fixes the save handler to fall back to 23 for telnet when
no explicit port is set, matching the telnet protocol default.
Closes#1251
* fix(ai): make SDK deps optional, degrade gracefully when missing
- Move @anthropic-ai/claude-agent-sdk and @github/copilot-sdk
from dependencies to optionalDependencies so npm install does
not fail when they are unavailable
- claudeDriver.listClaudeModels: catch import error, return [] silently
- copilotDriver.listCopilotModels: catch import error, return [] silently
- sdkStreamHandlers: downgrade log from console.error to console.debug
The renderer already falls back to curated model presets when
list-models returns [], so no functional change.
* fixup: honor queryFn before SDK import; regenerate lockfile with optional markers
* fixup: guard runTurn against missing SDK modules
runClaudeTurn and runCopilotTurn now catch dynamic import errors
and emit a user-friendly error message instead of crashing with
a raw module-not-found error.
* feat(sftp): add Open with system default for native Windows file association (fixes#1236)
Adds a new 'Open with system default' option to the SFTP context menu
that uses Electron's shell.openPath() to invoke the native OS file opening
mechanism. On Windows, this uses ShellExecute, which works with UWP/WinUI
apps (Photos, Paint, etc.) whose executable paths change with updates.
The existing 'Open with...' (browse for executable) is preserved.
Closes#1236
* fix(sftp): add file watch support to openWithSystemDefault for SFTP auto-sync
P2: The new default-app open flow was not starting a file watch
when SFTP auto-sync was enabled, so edits saved in the system
default app were not uploaded back to the remote host.
Mirrors the downloadToTempAndOpen behavior — accepts an optional
{ enableWatch } parameter and starts a file watch when set.
* fix(sftp): mark transfer as failed when system default open fails
P2: When shell.openPath fails for a remote file, the transfer queue
still shows success because downloadToTemp had already completed the
temp download. Now updates externalTransferId to 'failed' before
throwing, matching downloadToTempAndOpen's behavior.
Add full EternalTerminal (ET) protocol support alongside the existing SSH, Mosh, Telnet, and Serial protocols. ET automatically reconnects when the network drops or the user's IP changes, making it ideal for mobile and unreliable networks.
* fix: route Cmd+W through existing tab close flow
Keep the original tab-close behavior intact, and close the main or settings window only when there is no active closable tab to handle.
* fix: fall back to closing non-listener windows on Cmd+W
Treat BrowserWindow instances that do not participate in Netcatty's command-close bridge as regular closable windows. Keep the existing command-close path for the main and settings windows, and add tests that cover both the fallback close behavior and the renderer-capable send path.
TerminalPane forwards sshDebugLogEnabled to its child at render (L717) but
never destructured it from props, so rendering threw
"ReferenceError: sshDebugLogEnabled is not defined". The prop already exists
in TerminalPaneProps and the memo comparator; only the destructuring was
missing. tsc flags it (TS2304) but the build does not gate on tsc. Introduced
in 85f486e6.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(auto-update): commit app to quit before quitAndInstall on macOS (#1215)
macOS in-place auto-update downloaded and unpacked the new version but
never installed it: the app appeared to close, ShipIt never ran, and no
restart happened. A full uninstall + reinstall did not help; only a
manual DMG replace worked.
Root cause is a code-level coordination bug, not the release pipeline.
The published mac zips are correctly Developer-ID signed, notarized, and
stapled (Team H7WS5L2ML4, consistent across 1.1.17 and 1.1.20), and
latest-mac.yml is well-formed — so Squirrel.Mac signature validation
passes. The failure is that quitAndInstall() drives app.quit() while two
normal-quit behaviors keep the process alive:
1. the main-window close handler hides to tray when close-to-tray is
enabled (it only closes when isQuitting is true), and
2. the before-quit dirty-editor guard preventDefault()s the quit for a
5s renderer round-trip.
Either keeps the parent process running, so Squirrel.Mac's ShipIt helper
— which waits on the parent PID to die before swapping the bundle —
lands in launchd "pending spawn / on-demand-only" limbo and the service
is removed without installing. This matches the reporter's diagnosis
exactly ("ShipIt 没有真正启动安装器", launchd on-demand-only).
Fix: before quitAndInstall fires app.quit(), mark the app as quitting
for an update via windowManager.setQuittingForUpdate(true). That sets
isQuitting (bypassing close-to-tray) and the before-quit handler now
returns early when isQuittingForUpdate() is true (skipping the
dirty-editor round-trip), so the process exits cleanly and ShipIt can
run. The same fix also covers the latent Windows NSIS case where
close-to-tray would block an in-place update.
If the install never actually quits the app (quitAndInstall throws, or
returns without app.quit() on a Squirrel follow-up error / stale
download), the quitting-for-update flags are rolled back — synchronously
on throw, and via a short unref'd watchdog otherwise — so the app does
not get stuck permanently bypassing close-to-tray and the quit guard.
Tests: unit tests for the new windowManager flags and for the install
handler — ordering (setQuittingForUpdate before quitAndInstall), tray
cleanup still runs, no-op when the updater fails to load, rollback on
synchronous throw, and watchdog rollback when the app never quits.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(auto-update): keep dirty-editor guard during update install
setQuittingForUpdate only bypasses close-to-tray (so the window actually
closes and Squirrel.Mac's ShipIt can swap the bundle); it must NOT skip the
unsaved-work guard, or clicking "Restart Now" with a dirty SFTP editor would
silently lose edits. If the user cancels to save, the quit aborts and
autoUpdateBridge's watchdog clears the quitting-for-update flags.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(auto-update): clear update-quit state when the quit is cancelled
When the user clicks "Restart Now" with a dirty editor open, the before-quit
guard cancels the quit (settle "stay"). The update path had already called
setQuittingForUpdate(true) (which flips isQuitting=true to bypass close-to-tray
for the install), so without clearing it the app stays in a quitting state —
close-to-tray and other !isQuitting-gated behavior bypassed — until the 10s
watchdog fires. Clear it immediately on the cancelled-quit path (#1215 review).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(auto-update): check for unsaved editors before quitAndInstall (#1215)
The previous fix bypassed close-to-tray so the app process actually exits and
Squirrel.Mac's ShipIt can swap the bundle, while keeping the before-quit
dirty-editor guard as the unsaved-work safety net. But on macOS that net has a
hole: quitAndInstall() closes the window FIRST and only then fires before-quit.
Once setQuittingForUpdate(true) lets the main window truly close (instead of
hiding to tray), the before-quit guard can run after the window is already gone
— isReachableByUser is false, so it commits the quit and silently drops unsaved
SFTP edits.
Fix: move the dirty-editor check to the moment the user clicks "Restart Now",
in the install handler, BEFORE setQuittingForUpdate / quitAndInstall — while the
window and renderer are still alive:
- dirty -> abort the install (don't set the quitting flags, don't
quitAndInstall) and broadcast netcatty:update:needs-save so the
renderer prompts the user to save and retry.
- clean -> proceed with the existing flow (commit-to-quit, tray cleanup,
quitAndInstall, watchdog).
- no reachable main window / crashed renderer -> install directly (no user to
ask), matching the before-quit fail-open path.
The before-quit dirty guard is kept as defense-in-depth: if the window is still
reachable it re-checks (clean, since we just verified), and if it's already gone
it lets the quit through — which is now safe because the install handler already
confirmed there were no unsaved editors.
The request/reply/timeout round-trip is extracted into a shared helper,
electron/bridges/dirtyEditorGuard.cjs (queryDirtyEditors), so the install
handler and main.cjs's before-quit guard use one implementation. main.cjs's
before-quit is refactored onto it (behavior preserved: sender-filtered reply,
fail-open timeout, and the setQuittingForUpdate(false) rollback when the user
cancels to save).
The needs-save notice is BROADCAST to every window, not just the queried main
window: "Restart to Update" can be clicked from the Settings window, which would
otherwise see the click do nothing. preload exposes onUpdateNeedsSave; the
subscription lives in useUpdateCheck (state layer), and both consumers — App.tsx
(main window) and SettingsPage (settings window) — pass an onNeedsSave callback
that shows an actionable toast ("save your editors, then click Restart Now
again") in en / zh-CN / ru.
Also lengthen the quitting-for-update rollback watchdog from 10s to 60s. On
macOS quitAndInstall() can return while Squirrel is still pulling the downloaded
ZIP from the local update server before it closes the windows; on a large/slow
update that can exceed 10s. Clearing isQuitting that early would let the eventual
native quit hit a non-quitting close-to-tray handler and strand the install
again — the exact #1215 failure. The longer window only fires when the app is
realistically stuck, at the cost of close-to-tray staying bypassed a little
longer in the rare genuine-failure case.
Tests: queryDirtyEditors (result / no-dirty / timeout / wrong-sender / dead or
crashed webContents / send-throws / no-ipcMain paths); install handler pre-check
(dirty -> no quitAndInstall + needs-save broadcast to all windows; clean ->
quitAndInstall runs; no main window -> installs without asking). Existing
install-handler tests updated for the now-async handler.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The terminal output path decodes remote bytes with an iconv decoder built
from the user's configured charset (GB18030, etc.), but the input path
serialized keystrokes as UTF-8 unconditionally. On a non-UTF-8 device that
made input and output asymmetric: with GB18030 selected the device's command
completion decoded correctly while manually typed Chinese went out as UTF-8
bytes and showed up garbled (and the reverse with UTF-8 selected).
Make input symmetric with output:
- Add electron/bridges/terminalEncoding.cjs as the single source of truth for
charset normalization plus encodeTerminalInput(), which encodes a keystroke
string with the same iconv charset (returning the string untouched for UTF-8
and for unset/unknown encodings so the transport's native serialization and
the Mosh/local-PTY paths are unchanged). ASCII control bytes and CSI escape
sequences pass through byte-for-byte under GB18030.
- terminalBridge.writeToSession() now encodes outgoing data via
encodeTerminalInput(session.encoding) before writing to the SSH stream,
telnet socket, or serial port. Telnet IAC 0xFF escaping still runs on the
encoded bytes.
- session.encoding (the input charset) is now kept in lock-step with the
output decoder everywhere the decoder is configured:
* Telnet/serial already stored session.encoding; writeToSession now reads
it for input too.
* SSH mirrors session.encoding wherever it sets sessionEncodings: the
GB-variant pre-seed at session start and the runtime setEncoding handler.
The pre-seed stays gated to GB variants to match the renderer's two-value
encoding state, so behavior for other/arbitrary charsets is unchanged —
the renderer still pushes the effective encoding via setEncoding on
attach, and that handler keeps both halves in sync.
- The SSH startup command is encoded with the same charset as interactive
input.
Mosh stays UTF-8 (mosh-client is UTF-8-only and sets LANG accordingly), and
local PTY stays UTF-8 — neither sets session.encoding, so their input is
untouched.
Adds unit tests for the encoding helper (GB18030/UTF-8 round-trips, ASCII
control preservation, symmetry with the output decoder) and integration tests
driving writeToSession over a raw TCP device to assert GB18030 bytes on the
wire and a UTF-8 regression guard.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
GoogleDriveAdapter refreshed its access token only in memory: the rotated
tokens were never written back, so the next launch loaded a stale access
token and the user was forced to reconnect. This is the same defect #1208
fixed for OneDrive, which GoogleDriveAdapter never received because it did
not expose setOnTokensRefreshed — making the shared
attachTokenRefreshPersistence a no-op for Google.
Add setOnTokensRefreshed to GoogleDriveAdapter and route both refresh
points (setTokens / ensureValidToken) through a refreshTokens helper that
stores the rotated tokens in memory and notifies the persistence callback,
so attachTokenRefreshPersistence now encrypts and persists them.
Google's refresh response usually omits refresh_token (it does not rotate
on every refresh), so refreshTokens carries the previous refresh token
forward when the response lacks one — otherwise the persisted connection
would become unrefreshable.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Auto-refresh OneDrive sync token and prompt reconnect on dead refresh token
OneDrive cloud sync (#1189) broke for users on 1.1.20 and only recovered
after disconnecting and re-authorizing. Two gaps caused this:
1. Refreshed tokens were never persisted. When OneDriveAdapter silently
refreshed the access token mid-session, the rotated tokens lived only in
the adapter's in-memory state — CloudSyncManager never read them back or
saved them. Microsoft consumer refresh tokens rotate on every refresh and
invalidate the previous one, so the next app launch loaded a stale,
rotated-out refresh token. Eventually that stored token was dead and sync
failed, forcing a manual reconnect.
Fix: OneDriveAdapter now exposes setOnTokensRefreshed(); the manager wires
it so every silent refresh writes the rotated tokens back into provider
state and encrypted storage (attachTokenRefreshPersistence /
persistRefreshedProviderTokens), keeping the stored refresh token current.
2. A genuinely dead refresh token surfaced as a raw, generic error with no
guidance. The bridge now detects invalid_grant / interaction_required /
consent_required / login_required on refresh and tags the error with a
stable marker. OneDriveAdapter normalizes these to
OneDriveReauthRequiredError; the marker survives IPC and error re-wrapping
so the condition stays detectable, and the UI strips it to show a clean
"OneDrive session expired, please reconnect." message.
Shared marker + detection/clean helpers live in domain/sync.ts so the bridge,
adapter, and UI use one source of truth. Scope is limited to OneDrive; other
providers (Gist/iCloud/Google/WebDAV/S3) are untouched.
Tests: OneDriveAdapter refresh-persistence + reauth detection, manager
token-persistence wiring, and bridge invalid_grant tagging.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Drop OneDrive to a reconnect state when its refresh token is dead
codex review: detecting a dead refresh token was not enough. A provider in
`error` state that still holds tokens stays "ready for sync"
(isProviderReadyForSync), and syncAllProviders resets such providers back to
`connected` and retries — so auto-sync kept hammering the dead refresh token
and the user never got a stable reconnect prompt.
Now, when a sync/download error indicates OneDrive reauth is required, the
manager clears the stale tokens and tears down the cached adapter
(handleProviderReauthRequired), leaving the provider in an error state with no
credentials. With no tokens, isProviderReadyForSync returns false so auto-sync
stops retrying, and the card shows a clean "please reconnect" message with a
Connect button. The account is preserved for display; the error message is
stripped of the internal marker.
Wired into the syncToProvider / uploadToProvider / downloadFromProvider error
paths. Added tests for the clear-on-reauth behavior and provider/error scoping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Clear OneDrive reauth during inspection paths; include service tests in npm test
codex review round 2:
P2 — A dead OneDrive refresh token can also surface during startup remote
inspection and the syncAllProviders preflight conflict check, both of which run
through inspectProviderRemoteState. That path swallowed the error into an
{error} tuple without clearing the stale tokens, so the provider stayed
retryable. Wired the reauth handler into inspectProviderRemoteState's catch,
covering sync preflight, syncAll preflight, and startup inspection in one place.
Made the handler idempotent so the operation's own catch can also call it
without re-saving.
P3 — New tests live under infrastructure/services/, which npm test's globs did
not cover (the pre-existing syncAllStorageMethods.test.ts was also uncovered).
Added infrastructure/services/*.test.ts and infrastructure/services/*/*.test.ts
to the test script.
Full suite: 1400 tests, 0 failures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test: drop unmatched services test glob from npm test
The npm test script listed both infrastructure/services/*.test.ts and the
nested infrastructure/services/*/*.test.ts. No .test.ts files live directly
under infrastructure/services/ (the OneDrive adapter and cloudSync tests are
in adapters/ and cloudSync/ subdirs), so the flat glob never matched.
Under a default POSIX shell (sh/bash without nullglob), an unmatched glob is
passed through literally, so node --test received the raw pattern. On Node
versions without test-runner glob support this aborts with
"Could not find '.../infrastructure/services/*.test.ts'" before any test runs,
breaking npm test.
Remove the redundant flat glob; the nested glob already covers the new
OneDrive adapter and cloudSync tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Show host info for Mosh sessions via a stats companion SSH connection
Mosh sessions run over UDP through a local mosh-client PTY and carry no
ssh2 connection (session.conn), so getServerStats could not open an exec
channel and the terminal's host-info bar (CPU/memory/disk/network) stayed
empty — unlike SSH sessions (issue #1198).
Add a best-effort, non-interactive companion SSH connection that is opened
lazily on the first stats poll for a Mosh session, reusing the credentials
the Mosh handshake already validated, and assign it to session.conn so the
existing stats path works unchanged:
- electron/bridges/sshBridge/moshStatsConnection.cjs: new helper that
builds the companion connection. It never prompts (only stored password,
parseable private key, unencrypted/stored-passphrase identity files, or
ssh-agent), shares one in-flight attempt across concurrent polls, treats
auth rejection as permanent but transient errors as retryable, and skips
host-key verification like the existing one-off execCommand path.
- sessionOps.getServerStats: establish the companion connection for Mosh
sessions that lack session.conn before running the stats command;
degrades gracefully to the existing "not connected" error otherwise.
- moshSession.swapToMoshClient: stash the handshake credentials and
algorithm settings on session.moshStatsAuth once the handshake succeeds.
- terminalBridge closeSession / cleanupAllSessions and the mosh-client exit
handler: tear down the companion connection (it has no session.stream).
- Forward legacyAlgorithms / skipEcdsaHostKey / algorithmOverrides through
the renderer's Mosh starter and the bridge type so the companion
negotiates the same algorithms the interactive session would.
Tests cover the helper (auth selection, agent fallback, dedup, permanent vs
transient failure, late-ready discard), the getServerStats integration, and
the moshStatsAuth stash + companion teardown on close.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Address codex review: target SSH host and settle on mid-handshake close
- moshSession: store options.hostname (the SSH endpoint) on moshStatsAuth
instead of parsed.host. A `MOSH IP` line advertises the UDP endpoint for
mosh-client, which can differ from the SSH host on NAT / multi-homed
setups; the companion is an SSH connection and must target the SSH host.
- moshStatsConnection: resolve the pending attempt from the "close" handler
when the socket drops mid-handshake without a prior "ready"/"error", so an
awaiting getServerStats call (and session.moshStatsConnPromise) cannot
hang indefinitely. Treated as transient so the next poll may retry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Address codex review: keyboard-interactive password for stats companion
PAM-backed SSH servers often offer password auth only via
keyboard-interactive, not the plain "password" method. The Mosh handshake's
system ssh handles that through its PTY responder, so without it the
companion stats connection would fail auth on those hosts even with a saved
password, leaving the stats bar empty.
When a saved password is present, enable tryKeyboard and attach a
non-interactive keyboard-interactive handler that auto-fills the password
for a single password prompt (using the existing
isAutoFillablePasswordChallenge predicate) and finishes empty on
2FA/OTP/multi-prompt challenges. It auto-fills at most once so a wrong
password can't drive a retry loop, and it never shows a modal — the
companion stays fully non-interactive.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Address codex review: verify host key before sending a saved password
A background companion connection that auto-submits a saved password to an
unverified host could disclose it to a spoofed / MITM server (P1). Gate
password auth (plain and keyboard-interactive) behind a silent, trusted-only
host-key check against Netcatty's known-hosts store:
- moshStatsConnection: when the companion would authenticate with a
password, attach an ssh2 hostVerifier that accepts only a key already
"trusted" in known-hosts (via hostKeyVerifier.classifyHostKey) and rejects
unknown/changed keys outright — no prompt, so the password is never sent to
an unvetted host and no host-key dialog pops for a background poll.
Public-key / agent auth proves possession via a signature and discloses no
reusable secret, so it is not gated (matches the existing execCommand
precedent; the handshake already vetted the host via system ssh).
- Thread knownHosts through the renderer Mosh starter, the bridge type, and
session.moshStatsAuth.
Note: a password-auth Mosh host that Netcatty has never seen via its own SSH
path (so it is absent from Netcatty's known-hosts) will not get the stats
companion until its key is known — the safe default. Key/agent-auth hosts are
unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Address codex review: transient pre-handshake polls and agent+password auth
Two functional gaps in the stats companion:
- Missing moshStatsAuth is now transient, not permanent. The renderer can
mark a Mosh session "connected" (and start polling) from the SSH
bootstrap's visible PTY output before the swap to mosh-client assigns
moshStatsAuth. Previously that first poll set moshStatsConnFailed
permanently, so the companion was never attempted after the handshake
actually completed. Now it just returns null and a later poll retries.
- A saved password no longer suppresses ssh-agent auth. A public-key host
that authenticates via the agent may still carry a stored password; the
companion now offers the agent alongside the password (ssh2 tries agent
first) instead of attempting password-only and failing permanently. An
explicit private key still suppresses the agent fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Address codex review: gate password at authHandler, not whole connection
The previous fix installed a trusted-only hostVerifier whenever a password
was present. Because ssh2 verifies the host before any auth method, that
rejected the entire connection on a host absent from Netcatty's known-hosts
— blocking key/agent auth too, even though those never need to send the
password.
Move the gate from the transport to the auth layer:
- A trust-tracking hostVerifier records whether the live host key is trusted
(during the transport handshake) and then accepts the transport so
public-key / agent auth can proceed on any host.
- A function-form authHandler offers none -> agent -> publickey always, and
appends password + keyboard-interactive only when the host key is trusted.
Result: key/agent auth works on hosts Netcatty hasn't vetted, while a saved
password is still never sent to an untrusted host. Public-key / agent auth
remains ungated (no reusable secret is disclosed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Address codex review: isolate Mosh stats connection from session.conn
Storing the stats companion on session.conn made it look like the session's
primary interactive SSH connection. Other bridges key off session.conn —
getSessionPwd assumes its exec channel is a sibling of the interactive shell,
and SFTP / MCP exec run over session.conn — but a Mosh session's shell lives
on the UDP mosh-client, not this background connection. After a stats poll
they could return a bogus cwd or operate over the wrong connection.
Keep the companion strictly on session.moshStatsConn:
- ensureMoshStatsConnection stores/reuses/clears only session.moshStatsConn.
- getServerStats reads session.conn || session.moshStatsConn (real SSH still
uses conn; Mosh uses the companion) and only opens one when neither exists.
- closeSession / cleanupAllSessions / the mosh-client exit handler tear down
session.moshStatsConn.
This leaves session.conn untouched for Mosh, so getSessionPwd / SFTP / MCP
exec behave exactly as before (no primary SSH connection for Mosh).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Address codex review: don't count pre-handshake Mosh polls as failures
useServerStats gives up after 3 consecutive failures. A Mosh session can be
marked "connected" (and start polling) from the SSH bootstrap's visible
output before swapToMoshClient stores moshStatsAuth, during which
ensureMoshStatsConnection returns null. Previously getServerStats reported
that as a normal failure, so a handshake taking ~15s (3 polls) would
permanently disable stats for the session even after credentials became
available.
Introduce a `pending` result:
- getServerStats returns { success: false, pending: true } for a Mosh session
that has no connection yet and no moshStatsAuth and hasn't permanently
failed. Once moshStatsAuth is set (or the companion permanently fails), it
reports a normal failure again.
- useServerStats treats `pending` as neutral: it does not update stats and
does not increment the consecutive-failure counter, so polling continues
until the handshake completes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Address codex review: verify host key for all Mosh stats companion auth
The companion installed a trust-tracking host verifier only when a saved
password was present; key/agent-only connections fell back to ssh2's
default of accepting any host key. A background, user-invisible connection
that authenticated against an unverified host could let a MITM/DNS-spoofed
host feed bogus host-info to the user and enumerate the ssh-agent's public
keys — breaking the host-key guarantee the interactive session enforces.
Attach the host verifier for every auth method and reject an unknown or
changed host key outright (never prompting; stats just stay empty). Treat
an untrusted host as a permanent failure so polling stops reconnecting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(mosh): trust system known_hosts for the stats companion host-key check
The Mosh stats companion opens a background ssh2 connection and only rides
on a host whose live key is already trusted, rejecting unknown/changed keys
as a permanent failure. Trust was sourced solely from Netcatty's in-app
known-hosts snapshot (options.knownHosts).
But a Mosh session is bootstrapped by the system `ssh`, which vets and
records the host key in the user's OpenSSH known_hosts (~/.ssh/known_hosts,
etc). Netcatty's snapshot is never updated by that handshake, so a host
trusted purely via system ssh was misread as "unknown" and the companion
permanently disabled — Mosh stats never appeared unless the user manually
scanned/imported the host into Netcatty (codex P2).
Add a system-known_hosts trust source (systemKnownHosts.cjs) and consult it
in the companion verifier when the in-app snapshot does not already vouch
for the key. Matching is by the LIVE key's SHA-256 fingerprint, so trust is
granted only for the exact key the user's own OpenSSH already trusts; an
arbitrary or mismatched key is never accepted. Unknown/changed keys stay
rejected and remain a permanent failure.
The parser handles the OpenSSH known_hosts(5) format that the in-app scan
parser does not fully cover for matching: plain hosts, comma lists,
[host]:port, hashed |1|salt|HMAC-SHA1(salt,token) entries (with the
bracketed token for non-default ports, verified against ssh-keygen -H),
multiple key types, @revoked (forces NOT trusted) and @cert-authority
(skipped). Wildcard/negation patterns are deliberately not honored. Paths
mirror localFsBridge.readKnownHosts and are cross-platform (incl. Windows
%PROGRAMDATA%\ssh\known_hosts). All errors fail closed.
ssh2 ships no known_hosts parser, so this is implemented in CommonJS using
node:crypto and covered by unit tests (real ssh-keygen hashed fixtures,
revoked/cert-authority, fingerprint mismatch, fail-closed) plus companion
integration tests (system-only trust accepts; neither source trusts ->
rejected + permanent; key rotation not rescued; optional-dependency safety).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
From the local codex review of the ET jump-host changes:
- A jump host authenticated by a saved reference key had its on-disk key path
dropped: privateKey is undefined for reference keys and only
jumpHost.identityFilePaths was forwarded, so ET jump auth fell back to
defaults despite a valid key being selected. Mirror startSSH and forward the
reference key's filePath as an IdentityFile (with the same password-method
and keyId fallback guards).
- A jump host listening on a non-default ET server port was always contacted
on 2022: the bridge reads jump.etPort (--jport) but the renderer never sent
it. Forward jumpHost.etPort and add etPort to NetcattyJumpHost.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot review: for an ET host the connection dialog displayed host.port
(the SSH port, e.g. 22), but ET connectivity hinges on the etserver port
(host.etPort, default 2022). Showing 22 is misleading when a connection is
actually stuck on the ET port. Display etPort (falling back to 2022) for ET
hosts instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot review: startEt only checked resolvedChainHosts.length, so a
configured jump chain that failed to resolve (missing/invalid host ID) would
silently fall back to a direct connection — possibly to the wrong target —
unlike startSSH, which explicitly rejects missing chain host IDs.
Add the same getMissingChainHostIds check startSSH uses, erroring early with
a clear message when a configured jump host cannot be resolved. Also base the
"at most one jump host" limit on the configured chain (host.hostChain.hostIds)
rather than only the resolved list, so a second hop whose ID fails to resolve
cannot slip past the check.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses two correctness issues from the codex review of the ET protocol
support.
codex P1 #1 (Unix askpass helper): on macOS/Linux the SSH_ASKPASS helper
was the askpass .cjs itself, relying on its `#!/usr/bin/env node` shebang.
Packaged Electron builds put no `node` on the user's PATH, so ssh could not
run the helper and ET could not supply the saved password / key passphrase,
failing the connection outright. Mirror the existing Windows .cmd wrapper:
write a small /bin/sh wrapper that execs the helper through process.execPath
with ELECTRON_RUN_AS_NODE=1 (process.execPath is POSIX single-quoted to
survive spaces in an .app path).
codex P1 #2 (jump host routing): a jumped ET host only got an ssh
ProxyCommand, which fixes the SSH bootstrap but leaves ET opening its TCP
socket straight at the destination etserver — which fails whenever the
destination ET port is not directly reachable. Use ET's own
--jumphost/--jport so ET connects its socket to the jumphost's etserver and
reaches the destination over the SSH tunnel ET sets up (`ssh -J`). Per-hop
jump credentials move from the ProxyCommand into a `Host <jumphost>` ssh_config
block (OpenSSH applies command-line -o only to the final hop, so jump settings
must come from config); the destination's comma/space options are scoped under
a `Host <dest>` block with a ProxyJump so the standalone ssh used for distro
detection tunnels through the jump too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the existing mosh bundling pipeline so that release and tag
builds automatically fetch and package the EternalTerminal `et`
client alongside mosh-client.
Two more review findings on the connection-reuse path:
- Verify the source connection's endpoint matches the duplicate's
requested hostname/port/username before reusing it. A saved host edited
after the source tab connected would otherwise let the copy silently
open on the old connection and run commands on the wrong machine.
findReusableSession now takes the requested target and requires an exact
endpoint match; the session records its actual SSH endpoint at connect.
- Wrap conn.shell() in the reuse path with try/catch. ssh2 can throw
synchronously (e.g. "Not connected") if the borrowed transport dropped
between findReusableSession and the shell request; without this the
up-front connection ref hold would leak. On a synchronous throw we now
drop the error listener, release the ref, and fall back to a fresh
connection.
Adds tests for endpoint mismatch and synchronous shell failure.
Refs #1204
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two reference-counting races found in review:
- Reuse path read the source's connRef only inside the async conn.shell()
callback. If the source tab closed while the shell was opening,
releaseConnectionRef could drop the count to zero and end the shared
connection out from under the opening channel. Now pin the connection
(acquireConnectionRef) before issuing the async shell request and hand
the hold over to the real session once the channel opens; release it on
any failure so fallback to a fresh connection doesn't leak the count.
- closeSession read session.connRef *after* stream.close(), but closing the
channel can synchronously fire the stream "close" handler that nulls
connRef and releases the connection — so the post-close check fell into
the legacy path and ended the shared connection a second time. Snapshot
the multiplexing flag before closing the channel.
Adds a regression test covering "source closed while the copied shell is
still opening".
Refs #1204
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reading _logStreamToken back off the session map in the connection-level
close/error/timeout handlers could let a late close from an old transport
stop a newer same-sessionId log stream after a reconnect, regressing the
token guard from #916. Capture the owner channel's log stream token in the
connection closure and pass it to stopStream, matching the original
closure-scoped behavior.
Refs #1204
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Duplicating an SSH tab ("Copy Tab") used to open a brand-new SSH
connection, forcing a second MFA prompt on hosts with multi-factor auth.
Like Tabby's session multiplexing, open a new shell *channel* on the
source tab's already-authenticated connection instead, so the duplicate
reuses the existing transport and skips key exchange + authentication.
- copySession records the source session id (reuseConnectionFromSessionId)
for connected, non-mosh SSH sessions; it is threaded down to the SSH
bridge as options.sourceSessionId.
- startSession.cjs gains a reuse path that opens conn.shell() on the
source connection. The shell wiring is extracted into a shared
setupShellSession helper used by both the fresh and reuse paths.
- Connection lifecycle is reference-counted via a new sshConnectionPool
module (mirrors Tabby ref/unref/destroy): the shared transport + jump
host chain are torn down only when the last channel closes, so closing
a copy — or the original while a copy is open — never kills siblings.
terminalBridge.closeSession routes SSH teardown through the same release.
- Reuse falls back to a fresh connection when the source is gone, and is
skipped for X11 hosts (X11 is negotiated per channel).
Tests: sshConnectionPool refcount unit tests, bridge reuse integration
tests (reuse vs fresh, sibling survival, X11 skip, fallback), and
terminalBridge close lifecycle tests.
Refs #1204
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex P2: the panel maxWidth constants used by the horizontal clamp's
totalWidth excluded each panel's padding + border (inline styles default to
content-box), so a padded detail tooltip at its 280px max could still render
wider than reserved and spill off-screen.
Set box-sizing: border-box on the shared panel style so every maxWidth is the
true outer width and totalWidth is exact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex P2: totalWidth and the vertical height reservation depended on the
hovered/selected item's detail panel, so moving the mouse between a
no-detail row and a detail row could change the clamped position and shift
the popup under the pointer (flicker, unreliable clicks near the edge).
Reserve detail-panel width/height from a set-level flag (any non-path row
with a description) so placement stays stable while hovering; the tooltip
itself still renders only for the active row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The terminal completion popup could spill past the window edges and ignored
the theme accent color (#1202):
- Horizontal: the left-edge clamp only reserved the main list width (400px),
so expanding a directory near the right edge pushed the cascading sub-dir
panels and the detail tooltip off-screen. Now the clamp accounts for the
full assembly width (main list + sub-dir panels + detail tooltip) and pins
to the left padding when it is wider than the viewport.
- Vertical: extracted the flip/clamp math into a pure, unit-tested
computeAutocompletePopupPlacement() so "not enough room below -> flip up
and bound the height" is verifiable. The detail tooltip is now height-
bounded and scrolls instead of overflowing for long snippet descriptions.
- Accent: the selected/hover row background and the active-row accent rail now
derive from the active terminal theme's cursor/selection colors (which track
the user's accent setting) instead of a hardcoded blue.
Adds terminalAutocompleteLayout.test.ts covering downward/upward placement,
bottom-of-viewport flip, height clamping, and horizontal clamping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The top-bar AI button previously dispatched netcatty:toggle-ai-panel,
which was wired to handleOpenAI -> handleSwitchSidePanelTab('ai'). That
switch is a no-op when AI is already the open sub-panel, so the panel
could not be dismissed by clicking the button again.
Add a dedicated handleToggleAiFromTopBar that routes through a new
resolveAiSidePanelToggleIntent helper: a second click on an already-open
AI panel closes the side panel, while a click from a closed panel or a
different sub-panel switches to AI. The top-bar event listener now uses
this toggle handler. handleOpenAI stays a plain switch so the AI icon in
the side-panel rail remains idempotent like the other rail tabs.
Also remove the non-functional reminder (bell) button from the top bar;
it had no click handler and no backing feature.
Refs #1194
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Alibaba Cloud Linux hosts (os-release ID="alinux") previously showed the
generic Linux icon because normalizeDistroId fell through to the catch-all
`linux` branch ('alinux'.includes('linux') is true).
- Add a dedicated `alinux` branch in normalizeDistroId, matching the
os-release ID, the legacy `aliyun` ID, and the NAME/PRETTY_NAME text
"Alibaba Cloud Linux"; place it before the generic linux fallback.
- Register `alinux` in LINUX_DISTRO_OPTIONS so it is selectable in the
manual distro override and classified as linux-like.
- Add the brand SVG (public/distro/alinux.svg) plus logo/color mappings
in DistroAvatar (brand color #FF6A00).
- Add the localized label in en / ru / zh-CN.
- Cover normalizeDistroId's alinux cases (ID, legacy aliyun, PRETTY_NAME)
with unit tests, including a regression guard against the generic linux
fallback.
Icon source: simple-icons "Alibaba Cloud" mark (CC0-1.0, public domain),
matching the existing distro icon set already sourced from simple-icons.
Closes#1200
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document the implementation status of EternalTerminal integration
across all layers (domain, electron, application, components, i18n)
with testing notes and known limitations.
Add ET configuration section in HostDetailsAdvancedSections (etPort,
etTerminalPath). Add ET option to ProtocolSelectDialog. Wire ET session
creation in createTerminalSessionStarters with proxy and multi-jump
validation. Add ET badges and labels in Terminal, TerminalToolbar,
TerminalConnectionDialog, and TerminalLayerSupport. Propagate ET
settings through GroupDetailsPanel, GroupSshSettingsSection, and
VaultView.
Terminal: protocol label, proxy-unsupported warning, multi-jump
limitation notice. Vault: ET settings section title, etPort and
etTerminalPath field labels with descriptions.
Resolve 'et' protocol in tray panel and host connection handlers with
priority over mosh. Propagate etEnabled through session factories and
useSessionState. Expose etAvailable guard and startEtSession wrapper
in useTerminalBackend.
Add startEtSession to the preload API surface, routing through the
netcatty:et:start IPC channel. Define the startEtSession options type
in netcatty-bridge-session.d.ts with ET-specific parameters (etPort,
terminalPath, jumpHosts, etc.).
Add etSession.cjs: full ET session lifecycle including SSH bootstrap with
host-key verification, etclient PTY spawning, temp directory management,
and external auth artifact cleanup. Wire the session API into
terminalBridge.cjs with IPC handler registration. Add bundledEtClient
resolver that locates the platform-specific et binary in both packaged
and dev environments. Include unit tests for both etSession and
bundledEtClient.
Add 'et' to HostProtocol union type. Add etEnabled, etPort, and
etTerminalPath fields to Host and GroupConfig interfaces. Update
vaultImport type guards to exclude 'et' from importable protocols.
Register ET fields in groupConfig inheritable keys.
Download and package the et binaries produced by build-et-binaries:
- resolve-et-bin-release picks the latest et-bin-* release; fetch-et-binaries
downloads the platform client into resources/et/, verifying SHA256SUMS.
- et-extra-resources emits the electron-builder extraResources entry only
when the binary is on disk, so pack still works without a bundled et.
- electron-builder.config.cjs wires et into the mac/win/linux bundles;
package.json adds the fetch:et scripts.
Build the EternalTerminal `et` client in CI the way mosh-client is:
- build-et-binaries.yml builds et for linux x64/arm64, macOS universal
and windows x64, uploads artifacts, and optionally publishes them to a
dedicated binary repository on manual workflow_dispatch.
- scripts/build-et/{linux,macos,windows} compile et from upstream.
- .gitignore excludes the fetched binaries; resources/et/README.md
documents source provenance.
* feat(vault): show/hide toggle for the host Telnet password
Issue #1099 (part 2): the host-level Telnet password field was a bare type="password" with no way to reveal it. Add an eye toggle matching the SSH password field and the group-level Telnet password.
Refs #1099
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(vault): reset Telnet password reveal when the edited host changes
HostDetailsPanel is reused across hosts without remounting, so the new showTelnetPassword state has to be cleared in the initialData effect alongside showPassword — otherwise revealing one host's Telnet password leaves the next host's shown unmasked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Issue #1099: a Telnet host shows the protocol picker on every connect. Connect resolution is now explicit, and the picker only appears when it's genuinely a choice:
- Telnet set as default (protocol = telnet) -> connect Telnet (single-click and batch)
- Telnet enabled but not the default -> show the picker (unchanged)
- Mosh enabled -> connect Mosh
- otherwise -> connect SSH
A "Connect with Telnet by default" switch in the Telnet card sets the host's primary protocol. SSH+Mosh hosts now connect Mosh directly instead of prompting every time. Addresses part 1 of #1099.
Refs #1099
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The multi-select toolbar already supported batch delete; add a "Connect" button next to it so connecting many hosts no longer has to be done one by one. Connects each selected host in list order with its configured protocol (multi-protocol hosts use their default rather than prompting per host).
Closes#870
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The delete and overwrite confirmation dialogs are opened from a context menu, whose focus-return could leave focus outside the dialog, so pressing Enter did nothing. Focus the confirm button when each dialog opens via onOpenAutoFocus so Enter activates it. Esc and Tab+Enter behavior is unchanged.
Fixes#1150
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ssh): repair mangled PEM private keys before parsing
A valid PEM key whose framing was damaged in transit — newlines
collapsed to spaces, turned into literal "\n", or lines indented —
fails ssh2's parser with "Unsupported key format" even though the key
material is intact. This commonly happens when a key is copy/pasted
through a field or app that strips line breaks. (follow-up to #1139)
When parsing fails, rebuild clean PEM framing from the BEGIN/END markers
(which survive newline loss) and the base64 body, then retry through the
existing parse and PKCS#8 conversion paths. The body is preserved
byte-for-byte and a repaired key is only used if it re-validates, so
this can never produce a different or invalid key. Encrypted legacy PEM
(Proc-Type/DEK-Info) and truncated keys are left untouched.
Refs #1139
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ssh): detect encryption on mangled OpenSSH keys
A mangled encrypted OpenSSH key (line breaks flattened to literal "\n")
was not recognized as encrypted: the literal escapes corrupt the base64
decode used to read the cipher name, so isKeyEncrypted() returned false
and preparePrivateKeyForAuth routed the key to the unencrypted branch
with no passphrase prompt — and the repaired candidate was discarded
because it can't parse without one.
Repair the PEM framing before reading the OpenSSH cipher name, so such
keys are detected as encrypted and reach the passphrase prompt, where
normalizePrivateKeyForSsh2(key, passphrase) already repairs and
validates them. Addresses Codex review feedback on #1147.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
ssh2's key parser only accepts OpenSSH, legacy PKCS#1/SEC1 and PuTTY
keys, so PKCS#8 keys (-----BEGIN PRIVATE KEY----- / BEGIN ENCRYPTED
PRIVATE KEY) fail with "Cannot parse privateKey: Unsupported key
format" even though they are valid and work in other clients such as
Termius. (#1139)
Add privateKeyNormalizer: when ssh2 can't parse a key but it is PKCS#8,
read it with Node's crypto and re-export RSA->PKCS#1 / EC->SEC1, the
legacy PEM forms ssh2 understands. Encrypted PKCS#8 is decrypted with
the passphrase first. Ed25519 (and other) PKCS#8 keys, which have no
legacy PEM form, now surface a clear "convert with ssh-keygen" message
instead of ssh2's opaque error.
Wired into sshAuthHelper's preparePrivateKeyForAuth and
loadIdentityFileForAuth, so SSH, SFTP and port-forwarding all benefit.
Also fixes a latent bug where a correct passphrase on an encrypted
PKCS#8 key was rejected and re-prompted indefinitely.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
- TopTabs: the Vaults root tab no longer paints an active background fill when
selected (text/icon still brighten). Clear the imperatively-set hover fill on
click so it can't get stuck when active bg stays transparent.
- VaultView: when the host side panel is open, drive the grid column count from
the container width as a fixed N columns instead of viewport-based grid-cols-*
(which can't see the narrowed area) or auto-fit+1fr (which stretched a lone
card across the whole row). Fills the row with no trailing gap and keeps a
single card at one column's width. The count rides on a CSS variable set
imperatively via ResizeObserver, so reflowing the grid never re-renders this
large component.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Netcatty's AI / Skill+CLI integration sends marker-wrapped commands
(__NCMCP_xxx=0; { ... eval ...; }) straight into the user's interactive
shell. preload.cjs filters the PTY echo of those wrappers from the
visible terminal, but they still land in ~/.bash_history — making the
user's shell history hard to read after each AI session (#1126 user
report on v1.1.16).
Prefix the POSIX (bash/zsh/dash) and fish wrappers with a single space.
On the shells/configurations that already honor "ignore leading-space"
in history recording, those wrappers now skip the history file
entirely:
- bash with HISTCONTROL containing `ignorespace` (Debian/Ubuntu default
via /etc/bash.bashrc, also part of `ignoreboth` which is the most
common explicit setting)
- zsh with HIST_IGNORE_SPACE set (Oh-My-Zsh and most prezto templates
enable this)
- fish with a user-defined fish_should_add_to_history function (opt-in
via fish config)
Known limitations (no behavior change needed on netcatty's side):
- bash on bare RHEL/CentOS ships HISTCONTROL=ignoredups by default —
leading space is not honored. Users on those distros can opt in with
`HISTCONTROL=ignoreboth` in their ~/.bashrc.
- zsh without HIST_IGNORE_SPACE: same; add `setopt HIST_IGNORE_SPACE`.
- Fish without a custom history filter: leading space is not honored.
- PowerShell, cmd, network-device CLIs: unaffected (their wrappers are
not changed, and the persistent-history semantics differ).
This is intentionally a minimal change — 4 characters of behavior plus
the explanatory comments. We rely on the user's existing shell config
instead of trying to mutate HISTCONTROL ourselves at session start,
which would either be visible in the terminal echo, mis-fire on hosts
that already had ignorespace (deleting a real previous history entry),
or error on non-POSIX shells.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(ai): upgrade ACP packages and unwrap Skill+CLI command in tool-call panel
Package bumps:
- @zed-industries/claude-agent-acp 0.22.2 → @agentclientprotocol/claude-agent-acp 0.37.0
(old npm package is deprecated; scope rename)
- @zed-industries/codex-acp 0.10.0 → 0.15.0
- @mcpc-tech/acp-ai-provider 0.2.8 → 0.3.3
- electron-builder asarUnpack glob + bridge require.resolve switched to the new scope
After the upgrade Codex tool-call cards started showing the local
worktree path for every step — "Run /Users/.../netcatty-tool-cli session
--session …" — instead of the remote command. Three things lined up:
1. The new acp-ai-provider maps ACP's `title` to `toolName`, and Codex's
title is the full shell invocation it's about to run.
2. Codex local_shell ships args as ["/bin/zsh","-lc","<full>"], so the
old `typeof args.command === 'string'` branch in ToolCall never fired
and we fell through to printing `name` (i.e. the title).
3. The bridge serializes tool args under `args`, but the ACP adapter
only read `event.input`, so even when args were available the
renderer received {}.
Fixes:
- acpAgentAdapter: read tool input from both `event.input` and
`event.args` so bridge-serialized chunks and direct AI SDK chunks
both work.
- ai-elements/tool-call: new extractDisplayCommand() unwraps the shell
array, then the netcatty-tool-cli wrapper (exec/job-start … -- <cmd>),
and renders the real remote command. session/env/job-poll/etc. fall
back to short labels ("netcatty: inspect session", …) instead of
exposing the binary path.
- shellUtils.cjs: defensive JSON-parse the ACP wrapper input in case
the AI SDK ever stops auto-parsing it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(build): exclude bundled Claude CLI binaries from the installer
@anthropic-ai/claude-agent-sdk@0.3.x bundles the native Claude Code CLI
(~211MB per arch) as optional sibling packages. Including them would
silently regress Netcatty's "bring your own Claude" design — the project
has always required users to install Claude Code locally, and the entire
path-discovery flow exists precisely to honor that contract:
- useAgentDiscovery.ts scans the user's PATH for `claude` and writes
the absolute path into the agent config's CLAUDE_CODE_EXECUTABLE env.
- aiBridge.cjs runs normalizeClaudeCodeExecutableEnvForAcp on every ACP
spawn, forwarding the env var to the child process.
- The @agentclientprotocol/claude-agent-acp wrapper's claudeCliPath()
(acp-agent.js) prefers process.env.CLAUDE_CODE_EXECUTABLE over the
bundled binary and only falls back to sibling-package resolution when
the env var is empty.
So the right place to enforce the design is electron-builder: exclude
node_modules/@anthropic-ai/claude-agent-sdk-* from `files`. Dev mode is
unaffected (optional deps still install for `npm run dev`); only the
packaged installer drops the binaries, saving ~150MB. Users without
Claude Code installed get the same SDK error they got pre-upgrade.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zed-industries/claude-agent-acp ships dist/index.js with a
`#!/usr/bin/env node` shebang. We bundle the package and unpack it from
asar, but Windows ignores the shebang entirely and macOS/Linux only
honours it when `node` is on the user's PATH. When `node` was missing,
the resolver fell back to spawning the bare `claude-agent-acp` command,
which only works if the user manually ran
`npm install -g @zed-industries/claude-agent-acp` — see #1118.
Run the bundled script through `process.execPath` with
`ELECTRON_RUN_AS_NODE=1` (matching `resolveMcpServerRuntimeCommand` in
the MCP server bridge) so the embedded Electron acts as the Node
runtime. This makes the bundled copy work with zero external deps on
every supported platform.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the AI sidebar is dragged narrow, the model chip used to wrap
to its own line (and the perm chip to a third line), wasting vertical
space. Switch the toolbar from flex-wrap to a single-row flex container
where the +/perm chips stay shrink-0 and the model chip absorbs all
the squeeze via min-w-0, letting the existing truncate kick in and
ellipsize the model label instead of wrapping.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ssh): per-host skipEcdsaHostKey toggle + advanced algorithm overrides (#1027)
#1027 reported an old Huawei S7706 (SSH banner `SSH-2.0--`, empty
software-version field) where the legacy-algorithms toggle still
couldn't get a connection through. The debug log shows the handshake
makes it through every negotiation step, picks `ecdsa-sha2-nistp521`
for the host key, then dies at:
Handshake failed: signature verification failed
i.e. ssh2's strict RFC verifier rejects the ECDSA signature the
switch produces. OpenSSH on the same machine connects because its
known_hosts is already pinned to an RSA fingerprint, so it never
advertises ECDSA in the first place.
This adds two layers of escape hatch:
A. `host.skipEcdsaHostKey` (one-click) — drops every `ecdsa-sha2-*`
from the offered host-key list. Forces the fallback to
ssh-rsa / ssh-dss / ssh-ed25519 that those old stacks implement
correctly. Wired through ssh / sftp / port-forwarding bridges,
inheritable from the group default.
B. `host.algorithms` (advanced) — per-category override lists
(`kex`, `cipher`, `hmac`, `serverHostKey`, `compress`). When a
category's array is non-empty, it fully replaces the negotiated
list for that category. Exposed in a collapsible "Advanced
algorithm overrides" panel on both host and group settings,
inspired by Tabby's per-profile algorithm UI. Empty arrays
normalize to "use default" so picking zero algorithms in a
category doesn't bench the connection with
"no matching algorithm".
Overrides apply BEFORE `skipEcdsaHostKey` so the latter stays an
unconditional kill switch even if the user explicitly puts
`ecdsa-*` back into the host-key list.
Behavior with default values (neither toggle set, no override list)
is identical to before — zero change for hosts that aren't opted in.
Tests cover the new options on both `buildAlgorithms` and
`buildSftpAlgorithms`, plus group→host inheritance for both fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ssh): address Codex review on #1116 — proper seed + jump-host carry + missing call sites
Four review findings from the Codex pass on #1116, all real:
1. AlgorithmOverridesPanel was seeding the first customization in a
category from `SUPPORTED_ALGORITHMS_BY_CATEGORY`, which contains
legacy algorithms (CBC, arcfour, MD5, ssh-dss, group1-sha1...).
Unchecking a single modern algorithm in a host that had legacy mode
*off* would silently start offering those legacy algorithms. Now
seeds from `effectiveDefaultAlgorithms(legacyEnabled)`, mirroring
what `buildAlgorithms` actually emits at connect time.
- New `effectiveDefaultAlgorithms` pure helper in
`domain/sshAlgorithmList.ts` with its own test suite (also asserts
the modern subset contains no SHA-1 KEX / CBC / arcfour / MD5)
- `AlgorithmOverridesPanel` takes a `legacyEnabled` prop
- `isChecked` now reflects the effective-default state for an
untouched category, so unchecked rows visually represent
algorithms the connection wouldn't currently advertise
2. The chain-mode jump-host loop in `sshBridge.cjs` was applying the
target host's `skipEcdsaHostKey` / `algorithmOverrides` to every
bastion hop, which is wrong when the bastion has its own settings.
This was actually a pre-existing issue with `legacyAlgorithms` too
— `NetcattyJumpHost` simply didn't carry any of these fields.
- `NetcattyJumpHost` gains `legacyAlgorithms`, `skipEcdsaHostKey`,
`algorithmOverrides`
- `createTerminalSessionStarters` populates them from each jump
host's own configuration
- The jump-host bridge call now reads `jump.* ?? options.*` so a
hop with its own setting wins, but unset hops still fall back to
the target's settings (preserves historic chain-wide behavior
when nothing is overridden)
3. `infrastructure/services/portForwardingService.ts` only forwarded
`legacyAlgorithms` to the port-forwarding bridge, so a host that
needed the ECDSA skip or advanced overrides could connect through
the terminal but its auto-start tunnels would still hit the
original handshake failure.
- Forward `skipEcdsaHostKey` and `algorithmOverrides` at the target
call site and at the jump-host map.
4. `application/state/sftp/useSftpHostCredentials.ts` built
`NetcattySSHOptions` for `openSftp` without any of the algorithm
fields — and on inspection it didn't even forward `legacyAlgorithms`
to begin with, so SFTP panes for legacy-mode hosts were silently
negotiating with the modern default list. Same gap at the jump-host
map.
- Forward all three fields at both the target and the jump-host map.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* polish(hosts): make the advanced-algorithm collapsible trigger a real button
The first cut used a plain underlined caption for the
"Advanced algorithm overrides" collapsible trigger. That blends into
the surrounding helper text and doesn't read as an interactive
control — users couldn't tell that the per-category checkbox editor
was reachable at all.
Match the project's existing collapsible-trigger pattern (see
`SerialConnectModal`): full-width ghost Button, label on the left,
ChevronDown / ChevronUp on the right that flips with open state,
controlled via `useState`. Applied in both `HostDetailsPanel` and
`GroupDetailsPanel`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* polish(hosts): rename Legacy Algorithms card to SSH Algorithms; split Backspace into its own section
The card now carries three algorithm controls (Allow Legacy / Skip
ECDSA / Advanced overrides) plus a Backspace Behavior dropdown that
doesn't belong with the rest. Two cleanups:
1. Rename the card from "Legacy Algorithms" to "SSH Algorithms".
The original title only described the first toggle; the section
now covers the whole algorithm-negotiation surface, including the
ECDSA host-key skip and the per-category override editor.
- i18n key renamed `hostDetails.section.legacyAlgorithms`
-> `hostDetails.section.sshAlgorithms` in zh-CN / en / ru
- `HostDetailsPanel` references the new key
2. Move the Backspace Behavior control out of the algorithm card.
- `HostDetailsPanel`: new dedicated "Terminal Behavior" card
(TerminalSquare icon) placed between SSH Algorithms and
Keepalive. New i18n key `hostDetails.section.terminalBehavior`.
- `GroupDetailsPanel`: no separate Card scaffolding for group
defaults — moved Backspace to the bottom of the SSH section
(after Mosh) so it visually separates from the algorithm block
above without introducing a new card heading.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sftp): per-hop algorithm settings in SFTP chain (Codex review on #1116)
`sshBridge.cjs`'s jump loop already reads `jump.* ?? options.*` for
the three algorithm fields, but `sftpBridge.cjs#connectThroughChainForSftp`
was missed in that change and still applied the target host's
`options.legacyAlgorithms` / `skipEcdsaHostKey` / `algorithmOverrides`
to every bastion. A bastion that needed the ECDSA skip or a custom
algorithm list while the target didn't would still fail the SFTP
handshake before reaching the target.
Mirror the sshBridge fix: read each setting from the jump host first,
falling back to the target options when the hop didn't override.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ssh): keychain SSH exec + advanced editor seed honor inherited algorithm settings
Two findings from a local Codex review pass on the branch:
1. The keychain "export public key to host" flow opens its own one-off
SSH connection through `sshBridge#execCommand`, but the connectOpts
built there didn't include any `algorithms`. ssh2 then negotiated
with its built-in modern defaults regardless of what the host had
set, so a host that needs the ECDSA skip (or legacy mode) would
connect in the terminal but the keychain export would fail with
the original signature-verification error.
- `sshBridge.cjs#execCommand` now sets `algorithms` from
`payload.legacyAlgorithms` / `skipEcdsaHostKey` /
`algorithmOverrides`, mirroring `startSSHSession`.
- `useKeychainBackend`'s `execCommand` typing gets the three new
optional fields.
- `KeychainManager` forwards the host's `legacyAlgorithms`,
`skipEcdsaHostKey`, and `algorithms` when invoking the export.
2. The Advanced Algorithm Overrides editor seeded its first
customization from `form.legacyAlgorithms`, which is the host's
own field — it doesn't reflect a value the host *inherits* from
its group's default. A user in a group with `legacyAlgorithms=true`
editing a host that hadn't explicitly set the flag would see the
editor seed in modern-only mode, and saving could silently drop
the legacy algorithms the host actually needed.
- `HostDetailsPanel` passes `form.legacyAlgorithms ?? groupDefaults?.legacyAlgorithms`.
- `GroupDetailsPanel` adds an `inheritedLegacyAlgorithms` memo
resolved from the parent group chain via `resolveGroupDefaults`,
and the editor uses `form.legacyAlgorithms ?? inheritedLegacyAlgorithms`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(keychain): export-public-key honors group-inherited algorithm settings
Second-pass Codex review on PR #1116 flagged that the keychain
"export public key to host" flow now carries the three algorithm
fields end-to-end, but only reads them from `exportHost.*` directly —
which doesn't reflect values the host inherits from its group's
defaults. A host that left `legacyAlgorithms` / `skipEcdsaHostKey` /
`algorithms` unset but sat inside a group that turned them on would
work fine in the terminal (the terminal starter applies group
defaults before sending the IPC payload) but its keychain export
would silently fall back to ssh2's modern defaults and hit the
original signature-verification failure.
Resolve the effective host with `applyGroupDefaults` +
`resolveGroupDefaults` before the `execCommand` call, then read the
algorithm fields off the effective host. Requires plumbing
`groupConfigs` into `KeychainManager` (added as an optional prop,
forwarded by `VaultView`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(host-details): algorithm-overrides editor reads currently selected group's defaults
Third-pass Codex review caught that the editor seed was reading from
the `groupDefaults` prop, which is whatever group the host belonged
to when the panel opened. If a user switched the host into a different
group inside the panel and then opened the Advanced Algorithm
Overrides collapsible before saving, the editor would seed from the
old group's `legacyAlgorithms` flag and could save the wrong list.
The panel already memoizes `effectiveGroupDefaults` from
`form.group`/`defaultGroup`/`groupConfigs` for exactly this kind of
re-resolution (used by theme/font effective lookups). Read the
inherited flag from there instead of the stale prop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(algorithms): trim UI cipher list to algorithms ssh2 actually supports
Fourth-pass Codex review flagged that `SUPPORTED_CIPHER_ALGORITHMS`
included `blowfish-cbc`, `cast128-cbc`, and the `arcfour*` family,
but OpenSSL 3 disabled those primitives, so ssh2's `canUseCipher`
filter drops them from `SUPPORTED_CIPHER` at startup. Selecting any
of them in the Advanced Algorithm Overrides editor would make
`ssh2.Client.connect()` throw `Unsupported algorithm` synchronously,
turning the override into a "host now unreachable" footgun instead
of a narrowing knob.
Realigned each `SUPPORTED_*_ALGORITHMS` list to ssh2's actual
`SUPPORTED_*` constants:
- `SUPPORTED_CIPHER_ALGORITHMS`: dropped blowfish / cast128 / arcfour;
added the no-suffix `aes128-gcm` / `aes256-gcm` variants ssh2 also
accepts.
- `SUPPORTED_KEX_ALGORITHMS`: added `diffie-hellman-group15-sha512`
and `diffie-hellman-group17-sha512` (present in ssh2 but missing
from the UI list); reordered to ssh2's canonical order.
- `SUPPORTED_HMAC_ALGORITHMS`: reordered so the ETM/SHA-2 grouping
matches ssh2's `DEFAULT_MAC` and lookups are predictable.
Locked the invariant in `sshAlgorithmList.test.ts` with a new test
that asserts every UI-offered algorithm is a member of ssh2's
`SUPPORTED_*` for its category. A future ssh2 bump that drops an
algorithm we still expose will fail this test instead of silently
becoming a connect-time error for users.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(algorithms): run KEX override through the runtime fixed-DH filter
Codex round-N flagged a gap in `applyAlgorithmOverrides`: when the
user supplies a custom KEX list via the advanced editor, the previous
implementation copied it verbatim into the negotiated `algorithms.kex`
field. The default builder already passes its KEX list through
`filterSupportedFixedDhKex` to drop fixed-DH groups the runtime
doesn't support (notably `diffie-hellman-group1-sha1` on
Electron/BoringSSL, which lacks modp2), but the override path
bypassed that filter — so an Electron user enabling legacy mode and
saving any KEX checkbox state would re-advertise group1-sha1 and the
handshake would crash with "Unknown DH group" instead of failing fast.
Apply the same `filterSupportedFixedDhKex` to the override list. New
test in `sshAlgorithms.test.cjs` exercises a simulated BoringSSL
runtime that lacks modp2 and asserts the override-filtered KEX no
longer includes group1-sha1, while group14-sha1 / group-exchange-sha1
remain.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(algorithms): run HMAC override through the FIPS MD5 filter
Codex flagged the same runtime-bypass pattern Round-N-1 fixed for KEX,
now in HMAC: `applyLegacyHmacAlgorithms` gates `hmac-md5` behind
`md5Supported()` so FIPS Node builds don't get it, but the UI's
`effectiveDefaultAlgorithms(true)` seed adds `hmac-md5` /
`hmac-md5-96` unconditionally. A user with legacy mode on who saves
any HMAC checkbox change would push those MD5 entries through
`applyAlgorithmOverrides`, which previously copied the override
verbatim — bypassing the FIPS gate and making ssh2 throw
"Unsupported algorithm" before negotiation.
New `filterRuntimeUnsupportedHmac` helper applies the same
`md5Supported()` gate to a user override. `applyAlgorithmOverrides`
routes the HMAC override through it (mirrors how the same function
already routes KEX overrides through `filterSupportedFixedDhKex`).
New test simulates a FIPS-disabled MD5 runtime and asserts MD5
variants drop from the final HMAC list while SHA-1 / SHA-2 entries
remain.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ssh): jump-host overrides no longer inherit the leaf's algorithm overrides
Codex flagged that the jump-loop fallback I added for chain
convenience applied the target's per-host \`algorithmOverrides\` to
every bastion whose own override wasn't set, which is wrong: a target
restricted to e.g. \`serverHostKey: [\"ssh-rsa\"]\` would lock the hop
to ssh-rsa too and break negotiation against an Ed25519-only bastion.
The fallback IS still correct for \`legacyAlgorithms\` and
\`skipEcdsaHostKey\` — those are append/safety toggles that widen the
offered list, so propagating them to a bastion is safe and matches
the historic chain-wide behavior of \`options.legacyAlgorithms\`
(single-toggle convenience for a chain with one old leaf).
Treat \`algorithmOverrides\` strictly per-host instead. Same change in
both \`sshBridge.cjs\` and \`sftpBridge.cjs\` jump loops, with a
comment block explaining the asymmetry so a future refactor doesn't
"clean up" the distinction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ui(host-details): surface inherited algorithm overrides in the advanced editor
Codex flagged a real gap in the inheritance model: when a parent
group has set algorithm overrides (e.g. \`algorithms.kex: [...]\`),
a host or child group under it can't simply Reset a category back to
NetCatty's defaults — \`applyGroupDefaults\` treats an unset host
field as "inherit", so the local Reset falls back to the group's
list rather than to ssh2's defaults. Cleanly distinguishing "reset
to NetCatty defaults" from "inherit from group" needs a new schema
field or sentinel, which is a non-trivial design change well beyond
the scope of this PR.
For now, surface the situation in the UI so the user understands
why Reset doesn't behave the way they might expect and where to go
to actually clear the restriction:
- \`AlgorithmOverridesPanel\` accepts an optional \`inheritedFromGroup\`
prop and, when populated, renders a blue inline notice listing the
inherited categories and directing the user to the group's
algorithm settings if they need to opt out.
- \`HostDetailsPanel\` passes \`effectiveGroupDefaults?.algorithms\`.
- \`GroupDetailsPanel\` adds a new \`inheritedAlgorithmOverrides\`
memo that resolves the same way the existing
\`inheritedLegacyAlgorithms\` does, and forwards it.
- i18n strings added in zh-CN / en / ru.
Follow-up (out of scope for this PR): if real users do hit this,
introduce a \`host.algorithms = null\` (or explicit
\`algorithmsOverride: boolean\`) sentinel and a Reset-to-defaults
button that uses it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ssh): jump-host skipEcdsaHostKey is per-host; panel seeds from inherited overrides
Three findings from the latest Codex pass:
1 & 2. `skipEcdsaHostKey` on the leaf was still falling back onto
jump hosts in both `sshBridge.cjs` and `sftpBridge.cjs`. I had
classified it with `legacyAlgorithms` as a "safety widening" knob,
but Codex is right that it actually *narrows* the offered host-key
list by dropping every `ecdsa-sha2-*`. An ECDSA-only bastion (or
one where the operator pinned ECDSA via known_hosts) would still
negotiate when ECDSA is offered, but fails when the leaf's skip is
propagated to it. Same fix as `algorithmOverrides` last round:
strictly per-host. Only `legacyAlgorithms` keeps the chain-wide
fallback (append-only — it widens the offer, can never break a
hop that wasn't already failing).
3. `AlgorithmOverridesPanel` was seeding the first customization of a
category from the NetCatty/legacy effective default, ignoring any
list the host inherits from its group for OTHER categories.
Because `applyGroupDefaults` treats `host.algorithms` as an
all-or-nothing inherit boundary, the moment the user saved any
host-local override `{ cipher: [...] }`, the group's
`{ serverHostKey: ["ssh-rsa"] }` restriction silently dropped from
the effective host. Now:
- `toggleAlgorithm`'s first-click seed reads
`inheritedFromGroup?.[category] ?? effectiveDefault[category]`,
so customizing one category preserves the group's narrowing on
the others.
- `updateCategory` initializes `next` from
`{ ...inheritedFromGroup, ...value }` so saved overrides carry
the inherited categories alongside the host's own edits.
- `isChecked` reflects the inherited list when there's no local
value, so the visible checkbox state matches what would actually
be advertised.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(host-details): Reset preserves the inherited list instead of silently widening
Codex caught a follow-on bug from the previous "carry inheritance"
fix: pressing Reset on a category that's inherited from the group
deleted that category from \`host.algorithms\` while other host-local
or carried-inherited categories remained. Because
\`applyGroupDefaults\` treats \`host.algorithms\` as all-or-nothing,
the moment any host-local entry exists the group's \`algorithms\`
object stops being inherited as a whole — so the freshly-deleted
category fell back to NetCatty defaults rather than the group's
narrower list. Effective result: Reset on an inherited category
*widened* the offer instead of restoring it.
Reset now persists the inherited list verbatim onto the host when
the group has an override for that category, so "Reset" means "use
what this host would otherwise inherit" in all cases.
Also tightened \`isCustomized\` to suppress the "customized" badge
(and the per-category Reset button) when the host's stored list is
identical to the inherited list — those rows haven't really been
customized by the user.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ui(host-details): gate the inherited-notice / checkbox baseline on no host override
Codex caught the asymmetry between the read and write sides of the
panel. The write side (`updateCategory`, `toggleAlgorithm`, Reset)
intentionally carries `inheritedFromGroup` onto the host on the first
edit so the runtime's all-or-nothing inherit boundary in
`applyGroupDefaults` doesn't silently widen the offer. But the read
side (the inherited-notice banner and the `isChecked` baseline for
unfilled categories) was applying inherited values *unconditionally*
— including when the host already had any local `algorithms` object,
which makes `applyGroupDefaults` stop inheriting from the group as a
whole. Net effect on a host that only locally overrode `cipher`: the
UI claimed the group's `serverHostKey` restriction was still in
effect, while the runtime would actually use NetCatty's modern
defaults for that category.
Introduce `inheritedForDisplay`, defined as `value === undefined ?
inheritedFromGroup : undefined`, and route the notice + the
`isChecked` baseline through it. The write side keeps consulting the
unconditional `inheritedFromGroup` so the carry-over still happens on
first edit — that part is what makes the runtime behavior match what
the UI used to advertise.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ui(host-details): legacy/skipEcdsa toggles reflect group-inherited value
Codex caught that the Skip ECDSA toggle in both the host and group
panels read `form.skipEcdsaHostKey` directly, so a host whose group
turned the flag on (and `applyGroupDefaults` therefore applied it to
the runtime SSH negotiation) still saw the toggle rendered as off.
Worse, clicking it computed `!form.skipEcdsaHostKey` — which is `!undefined`
= `true` — so the first click could not actually disable the
inherited setting on a per-host basis; it would just store `true`
explicitly, the same effective state.
The Allow Legacy Algorithms toggle had the same pre-existing issue.
Fix both at once: each toggle now derives its enabled state from
`form.<field> ?? <inherited>` and the onToggle handler flips that same
effective value, so the toggle accurately represents what the runtime
would do and a single click off correctly stores `false`
(which `applyGroupDefaults` then leaves alone, breaking inheritance
for this host).
- `HostDetailsPanel` reads from `effectiveGroupDefaults`.
- `GroupDetailsPanel` adds an `inheritedSkipEcdsaHostKey` memo
alongside the existing `inheritedLegacyAlgorithms` and uses both.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(terminal): use client OS for local-shell autocomplete clear sequence (#1112)
Synthetic fallback Host in TerminalLayer hardcoded os: 'linux', so the
'host.os || navigator.platform check' expression in Terminal.tsx never
ran the client-OS detection branch for local shells. Result: autocomplete
emitted Ctrl-U (\x15) for line-clear on Windows local PowerShell/cmd,
where it's rendered literally as ^U instead of erasing the input — the
popup live-preview, fuzzy-accept, and snippet-accept paths all leak it.
Fix both ends: Terminal.tsx prefers client OS detection for local
protocol via the existing detectLocalOs helper; TerminalLayer.tsx
populates the synthetic host's os field from detectLocalOs too, so
other host.os readers (notably the server-stats gate in
Terminal.tsx:642) also stop treating local Windows as Linux.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(terminal): keep non-local fallback host on 'linux'
The TerminalLayer fallback path also triggers for unsaved serial sessions
and orphaned remote sessions (where the saved host was deleted while the
session lived on). Terminal.tsx trusts host.os for non-local protocols,
so tagging the fallback with the Windows client OS would push POSIX
remote/serial shells onto the Windows autocomplete/path-completion
branch. Gate the client-OS detection on protocol === 'local'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #1110 added an \`// eslint-disable-next-line no-console\` above
\`console.warn\` when suppressing SDK reasoning/text state-machine
errors, but this file already permits \`console.*\` calls (there are
several pre-existing \`console.error\` lines in the same hook) so the
directive is unused and ESLint flags it:
\`\`\`
666:13 warning Unused eslint-disable directive (no problems were
reported from 'no-console')
\`\`\`
Removes the directive; the \`console.warn\` stays.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue #1101 follow-up. When a third-party Anthropic-compat backend
streams thinking deltas without first emitting a `reasoning-start`
content-block signal (DeepSeek's \`-v4-flash\` is the canonical
offender), the Vercel AI SDK has nothing registered for the incoming
\`part.id\` and enqueues an \`error\` chunk on \`fullStream\` with the
text \`reasoning part <id> not found\` — once per orphan delta. The
analogous error exists for text parts.
These are internal SDK bookkeeping noise, not user-facing errors. Our
\`case 'error'\` handler was treating each one as a real error: it
appended an empty assistant message carrying the SDK string. The
visible damage was a wall of red banners in the chat panel — but the
worse damage was protocol-level. On the next turn we rebuild
\`sdkMessages\` from local history; the placeholder assistants slot
in between the assistant that holds the parallel \`tool_use\` blocks
and the subsequent \`role: 'tool'\` messages with their results. The
Anthropic SDK's \`groupIntoBlocks\` only merges *consecutive* tool
messages into a single user block, so the tool_results no longer
immediately follow their tool_use parent — and the backend rejects
the request as
\`400 messages.N: tool_use ids were found without tool_result blocks
immediately after\`. \`messages.N\` was literally counting our 12-ish
phantom assistants.
Adds \`isSdkStreamStateError(error)\` to
\`infrastructure/ai/shared/streamStateErrors.ts\` (matches
\`/^(reasoning|text)\\s+part\\s+\\S+\\s+not\\s+found$/i\` against
string / Error / \`{ message }\` shapes), and skips placeholder
emission for matching errors in the streaming hook. Drops a
\`console.warn\` so the noise is still visible in devtools when
debugging.
5 unit tests cover positive matches, the Error/object wrappings,
non-matches that should stay surfaced, and non-string inputs.
Refs #1101.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): serialize Catty terminal_execute calls per session
Issue #1101 problem 3. When the LLM emits multiple tool_use blocks in
one assistant turn (DeepSeek's Anthropic-compat happily does this for
parallel info-gathering), the Vercel AI SDK dispatches every tool
through `Promise.all(toolCalls.map(execute))`. All of them then race
into the same per-session mutex inside the main-process bridge
(`mcpServerBridge.reserveSessionExecution`). One wins; the rest get
`{ ok: false, error: "Session already has another command in
progress..." }` synthesized back as the tool result. The LLM sees a
turn full of synthetic errors instead of the answers it asked for, the
UI cards look stuck on "executing", and the Anthropic API has
sometimes rejected the resulting trace as
`tool_use ids were found without tool_result blocks`.
Adds an in-renderer Promise-chain queue keyed by
`${chatSessionId}:${terminalSessionId}` so the actual
`bridge.aiExec()` calls are issued one at a time per terminal. The LLM
can still parallel-call as many tool_use blocks as it wants — they
just resolve sequentially with real output. Approval prompts are kept
*outside* the queue: three parallel tool_use blocks still surface
three approval cards together, so the user can dispatch them in any
order rather than waiting for each prior command to finish before the
next prompt appears. The bridge-side mutex stays as defense-in-depth
for non-LLM paths (terminal_start, MCP, etc.).
`chainBySessionKey` is a self-contained helper with cleanup logic so
the queue map doesn't leak across many short-lived sessions:
- Each task awaits the previous tail via `.then(task, task)` so a
failure doesn't poison the chain.
- A non-rejecting wrapper is stored as the new tail to keep that
contract regardless of how the task settles.
- The cleanup `finally` only deletes the map entry when the current
tail is still ours, so a caller that arrived between
`set` and `finally` doesn't have its tail evicted.
Four unit tests cover dispatch ordering, rejection isolation,
per-key parallelism, and the cleanup path. Full suite 1255/1255.
Refs #1101.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): preserve LLM emission order + honor abort in serialized queue
Codex local review on the first cut of this branch flagged two real
issues plus a weak cleanup test:
1. **Approval order vs queue order** — the previous version awaited
approval *before* reserving a queue slot. With three parallel
tool_use blocks the user could approve B's prompt first, and B
would slip into the queue ahead of A, running out of LLM emission
order. Reserving the slot synchronously up front fixes that:
`Promise.all`-dispatched executes each grab a slot at the same
instant in their dispatch order, then wait on approval while
holding it, then await `slot.ready` and run.
2. **Abort not honored** — once approvals were settled, queued-but-
not-started commands ran to completion even after the user hit
Stop. Their results were ignored by the SDK but the side effects
still happened. Two `abortSignal` checks now short-circuit before
the queue wait and again after.
3. **Cleanup test was vacuous** — the previous "drains" test would
pass even if the cleanup logic were removed. Exposed
`getSessionExecutionQueueSizeForTests` and assert the Map is empty
after the only queued task settles.
Queue API now has two entry points: high-level `chainBySessionKey`
for run-it-when-it's-your-turn callers, and lower-level
`reserveSessionSlot` for callers (like `terminal_execute`) that
need to do non-blocking pre-work in parallel with the queue wait.
Two new tests cover the reservation-order-vs-prework-order guarantee
and the skip-without-serialized-work path.
Refs #1101.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): re-issue cancel on abort to close IPC-transit race
Codex local review (round 2) flagged a small remaining race: between
the abort check after \`slot.ready\` and the main-process bridge
registering the new exec into \`activePtyExecs\`, the user can hit
Stop. \`handleStop\` does issue \`aiCattyCancelExec\`, but if that
cancel IPC arrives at main *before* the exec has finished
registering, the cancel finds nothing to cancel and the exec keeps
running. Result: the user already cancelled, but the command runs
anyway.
Plug the gap by re-issuing the cancel from the tool's own abort
listener. Once the exec has registered into \`activePtyExecs\` (a
synchronous step on the main side once the IPC lands), the duplicate
cancel finds the entry and cancels it. \`cancelPtyExecsForSession\`
is already idempotent — it iterates the live tracker and skips
entries with mismatched \`chatSessionId\` — so double-firing from
both \`handleStop\` and the tool is safe.
Extends \`NetcattyBridge\` with the optional
\`aiCattyCancelExec(chatSessionId)\` method. Already exposed in
\`electron/preload.cjs\` so the runtime call works on the live
bridge; the type addition just makes it visible to the tool.
Refs codex review on #1101 problem 3 fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): register pending-cancel marker before SSH exec-channel opens
Codex local review (round 3) caught a remaining race in
`execViaChannel`: the cancellation marker is only registered inside
`sshClient.exec`'s open callback, which is async. If a cancel arrives
in the window between `sshClient.exec(...)` being dispatched and the
callback firing, `cancelPtyExecsForSession` finds nothing for the
session and is a no-op. The channel then opens, the marker
registers, and the command runs to completion — exactly the "user
already cancelled, but the command still ran" failure mode.
Plug the window by registering a *pending* marker synchronously
before `sshClient.exec`. The pending marker carries a `cancel()` that
just latches a `cancelled` flag and a `cleanup()` that is a no-op.
When the open callback fires it removes the pending marker and
checks the latch: if it tripped, close the just-opened stream and
resolve with `{ ok: false, error: "Cancelled" }`. If not, the normal
post-open registration takes over with the real `execStream` close.
The PTY (`execViaPty`) and raw-serial (`execViaRawPty`) paths
already register their marker before any async wait, so they don't
share this race.
Two regression tests in `ptyExec.test.cjs`:
- The pending marker is present immediately after `execViaChannel`
returns, before the open callback runs.
- A cancel during the pre-open window short-circuits the eventual
callback with `{ ok: false, error: "Cancelled" }` and closes the
now-unwanted stream.
Refs codex review on #1101 problem 3 fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): clean up pending-cancel marker when sshClient.exec throws sync
Codex local review (round 4) noted that if `sshClient.exec(...)`
itself throws synchronously — e.g. because the underlying ssh2 client
was destroyed between the session lookup and the actual `.exec`
call — the pending-cancel marker stays in `activePtyExecs`
indefinitely and the surrounding Promise rejects instead of
resolving with the normal `{ ok, error }` shape the tool layer
expects.
Wraps the `sshClient.exec` invocation in `try/catch` so:
- Pending marker is removed when the throw escapes.
- The Promise resolves cleanly with `{ ok: false, error: err.message }`
so the tool layer gets the same shape it does for every other
failure mode (closed stream, timeout, cancellation).
Adds a regression test covering this exact path: a fake client whose
`.exec` throws synchronously; the result is a clean failure and the
cancellation map is empty afterwards.
Refs codex review on #1101 problem 3 fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ai): provider switcher chip in the Catty Agent chat input
Catty Agent currently has no model chip in the chat input — modelPresets
is empty for the catty agentId so `hasModelPicker` is false. The
provider/model the chat ends up using comes from the global
`activeProviderId` / `activeModelId`, which means switching providers
requires opening Settings → AI → Providers. Closes the gap surfaced in
the original feedback on #1101 (problem 2) and partially addresses
#986 (per-context model switching) by going per-agent.
State layer adds an `agentProviderMap` (`Record<agentId, providerId>`)
alongside the existing `agentModelMap`. Both are written together when
the user picks from the new dropdown; together they form a per-agent
override that beats the global active provider/model when set. Cross-
window sync mirrors the agentModelMap pattern.
ChatInput grows a `providerSwitcher` prop carrying the enabled
ProviderConfigs, the selected (providerId, modelId), and an onSelect
callback. When supplied, the model chip switches from the Cpu glyph
to the provider's ProviderIconBadge + `providerName · modelId`, and
the popover renders a two-column layout — providers on the left
(icon + name + default model caption), that provider's known model(s)
on the right. ACP agents (Claude/Codex) are untouched because they
plumb their provider through the CLI, not the Vercel AI SDK.
AIChatSidePanel resolves `effectiveActiveProvider` /
`effectiveActiveModelId` for the catty agent from the per-agent map,
falling back to the global selection. handleSend now passes those
through to sendToCattyAgent so a provider picked in one Catty session
applies everywhere Catty runs.
For v1, each provider's right column shows just its configured
`defaultModel`. The two-level structure is in place to absorb richer
listings (cached /models, manual additions) later without UI
surgery — left a note in the popover code.
Refs #1101, #986.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* polish(ai): flatten provider picker to a single list
Two-level dropdown was theatre — each ProviderConfig exposes exactly
one model (its `defaultModel`), so the right column ended up showing
the same string already captioned on the left row. Picking a provider
implicitly picks its model; one click is enough.
Also drops the `p.enabled !== false` filter on the picker list. The
user's expectation, confirmed by feedback on the first cut, is that
the chat-input list mirrors what Settings → AI → Providers shows. The
per-provider `enabled` toggle is an "active-ish" flag, not a
visibility gate; hiding disabled providers in the picker silently
made everything but the one currently active disappear, which is what
the reviewer hit.
Pares the popover down to a vertical list — provider icon + name +
default-model caption (mono if set, italic "configure default model"
hint if not) + a Check on the bound row. Removes the `hoveredProviderId`
state along with the right-column rendering since it's no longer
driving anything.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* polish(ai): tune provider icon sizes in the chat input
Chip icon was sitting at sm (20px badge) and crowded the h-6 toolbar
chip alongside the truncated label; popover rows used the same sm, so
nothing differentiated the picker from the chip visually.
Adds an xs size to ProviderIconBadge (16px badge, 10px glyph, 9px
letter fallback) and uses it on the chip. Popover rows step up to md
(32px badge, 16px glyph) and the row padding grows accordingly so the
larger brand mark has room to breathe — the picker now reads like a
list of choices, the chip reads like a status line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): keep per-agent override honest in model fallback + chip label
Two codex P1/P2 findings on #1107:
P1 — When Catty has a per-agent provider override but no per-agent
model (agentModelMap['catty'] empty), cattyAgentModelId fell through
to the global `activeModelId`. That id belongs to whichever provider
was globally active, not the one Catty is now bound to, so e.g.
overriding to DeepSeek without a defaultModel happily sent gpt-4o
(global) to DeepSeek and produced a wrong-model error. The fallback
is now: stored agent model → override provider's defaultModel → empty.
Only the no-override path keeps the activeModelId tail; the
no-provider send guard catches the empty case for everyone else.
P2 — ChatInput's selectedSwitcherProvider used `?? providers[0]` so
the chip always displayed *some* provider even when none was actually
bound (selectedProviderId missing). The rest of the pipeline (send
guard, agentProviderMap) treats that as "no provider", so the chip
was lying. Dropped the fallback; when nothing is bound the chip now
shows a generic Cpu glyph + "Select provider" label until the user
picks one from the popover. Adds `ai.chat.selectProvider` in en /
zh-CN / ru.
Refs codex review on #1107.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): purge per-agent binding when the bound provider is deleted
Codex local review (round 2) flagged that a saved Catty model id could
outlive its provider. The chain:
- catty bound to provider A (DeepSeek): agentProviderMap['catty']='A',
agentModelMap['catty']='deepseek-v4-flash'
- user deletes provider A
- cattyAgentProvider falls back to the global active provider (B, OpenAI)
- cattyAgentModelId still returned the saved 'deepseek-v4-flash'
- send dispatched the DeepSeek model id to OpenAI → wrong-model error
Two layers of fix:
1. **Defensive resolution** (AIChatSidePanel) — cattyAgentModelId now
looks at the override provider before trusting the stored model id.
If the override is stale (provider deleted), the stored model id is
treated as orphan and the resolution falls back to the global active
selection just like cattyAgentProvider already does.
2. **Cleanup on remove** (useAIState.removeProvider) — when a provider
is deleted, any agent whose agentProviderMap entry pointed at it
gets both maps cleared (provider override + saved model). Same
rationale: that model id is now meaningless against every other
provider. Mirrors the existing activeProviderId cleanup.
Adds a small agentProviderMapRef so removeProvider can snapshot which
agents to clean up without relying on the captured-at-callback-create
state (which would be stale).
Refs codex local review on #1107.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): include agentProviderMap in settings sync + block empty-model send
Codex local review (round 3) found two issues:
P2 — `agentProviderMap` was missing from the cross-device sync surface.
Added it to the sync key list, the build path, the apply path, and the
SyncPayload type alongside the existing `agentModelMap`. Sync tests in
`syncPayload.test.ts` now cover the new field on both the build and
apply sides.
P2 — Provider rows with no `defaultModel` were still clickable in the
chat-input picker, so selecting one would save a binding with empty
model id; the send path then dispatched an empty model name and the
SDK would surface a vague backend error. Two changes:
1. ChatInput disables the row (\`disabled\`, \`aria-disabled\`, dimmed
styling, tooltip pointing the user to Settings) when the provider
has no \`defaultModel\`. Click is suppressed.
2. AIChatSidePanel \`handleSend\` adds a model-required guard mirroring
the existing no-provider guard — surfaces \`ai.chat.noProviderModel\`
as an assistant message. Catches stale bindings (e.g. user edited
the provider's \`defaultModel\` to empty after binding) before they
reach the SDK.
Refs codex local review on #1107.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): trim whitespace-only model ids + reconcile orphan bindings on sync
Codex local review (round 4) found two more leaks:
P2 — Whitespace-only `defaultModel` passed the picker's `disabled` gate
(which trims) but every other layer (cattyAgentModelId resolution,
the send-guard `!sendActiveModelId` check, the SDK call) compared the
raw string. A provider whose default model was " " could still reach
the SDK and surface a vague backend error. Normalized: trim at the
resolution boundary (catty model fallback chain) and trim before the
send-guard's existence check.
P2 — Sync apply did not reconcile per-agent bindings against the
incoming provider set. A payload could change `providers` without
shipping a fresh `agentProviderMap`, leaving local overrides pointing
to provider ids the synced set no longer includes — the same ghost-
binding bug that `removeProvider` already handles for explicit user
deletes. Added `pruneOrphanPerAgentBindings()` that runs after every
AI settings apply: it walks `agentProviderMap`, drops any entry whose
provider id isn't in the current `providers` list, and clears the
saved model id for those agents alongside it (mirroring removeProvider
cleanup). A new test in `syncPayload.test.ts` exercises the ghost-
binding case end-to-end.
Refs codex local review on #1107.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): notify open AI state of cross-device sync apply in the same window
Codex local review (round 4) flagged that `applySyncPayload` writes
straight to localStorage but `useAIState` only re-reads on browser
`storage` events. Those events only fire in *other* windows — the
window doing the apply (the one with the active chat panel) keeps
showing pre-sync providers and per-agent bindings until reload.
Concrete leak: cloud sync swaps in a new provider set, pruning runs,
localStorage is correct, but the open Catty chip still references the
ghost provider binding until the user reloads.
Extracts the existing `AI_STATE_CHANGED_EVENT` constant +
`emitAIStateChanged` helper out of `useAIState` and into a new
`application/state/aiStateEvents.ts` so non-React call sites (sync
apply, future IPC handlers) can fire it without pulling in the hook.
After AI settings apply, `syncPayload.ts` walks every AI key it
touched (including the agentProviderMap / agentModelMap that the
reconcile step may have mutated even when the payload didn't ship
them) and emits a same-window nudge. `useAIState`'s existing local
listener routes unknown keys back through its storage handler, so
each affected piece of React state rehydrates from localStorage.
Adds a regression test in `syncPayload.test.ts` that asserts the
expected `netcatty:ai-state-changed` keys are dispatched for the
providers + per-agent map keys.
Refs codex local review on #1107.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The icon badge next to the Display Name input opens the icon picker
but had no visual cue — users had no reason to suspect it was
clickable, and reviewer feedback flagged it as too hidden.
Adds a primary-tinted ring on hover/focus plus a small pencil glyph
overlaid on the bottom-right corner of the badge (also hover/focus
gated). The badge itself doesn't change shape, so the layout stays
stable; the affordance is purely additive.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ai): customizable provider name, icon, and protocol style
Issue #1101 reported that the only way to wire DeepSeek through the
Catty Agent was to add an "Anthropic" provider with a DeepSeek base URL,
which (a) wasn't relabelable except on the "custom" providerId and
(b) implicitly conflated wire-protocol style with providerId, locking
"custom" to the OpenAI-compatible client.
ProviderConfig now carries three optional fields — `style`
(anthropic/openai/google), `iconId` (built-in brand glyph key), and
`iconDataUrl` (user upload). createModelFromConfig routes on the
resolved style first, falling back to providerId only for per-vendor
quirks (ollama's throwaway apiKey and openrouter's baseURL). Display
name becomes editable for every provider, not just custom.
The icon picker exposes a curated lobe-icons subset (MIT) covering the
common Anthropic/OpenAI-compatible third parties — DeepSeek, Moonshot,
Kimi, Qwen, Zhipu, Doubao, Mistral, Cohere, Grok, Perplexity, Groq,
Hugging Face — plus the existing six built-ins. Uploads are downsampled
to 64×64 WebP on a canvas so localStorage doesn't blow up. See
public/ai/providers/NOTICE.md for attribution.
Closes part of #1101 (problem 2). PR2 will reuse the new name/icon
plumbing in the Catty Agent chat input to surface a provider switcher.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* polish(ai): bigger labelled icon tiles and auto-fill display name
The first pass shipped the icon picker as an 8-column grid of bare
20px badges. Reviewer feedback: the icons are too small to read at a
glance, every preset should announce its brand name, and a second
click on a selected preset should let the user back out.
Switches the grid to auto-fill 120px tiles with a 32px icon plus a
truncated label so the picker reads like a brand list, not a sprite
sheet. Selecting a preset now also writes the brand's canonical
English display name into the Display Name field — the picker labels
keep their "/ 中文" suffix as a bilingual hint, but a new `name` field
on each catalog entry supplies a clean string for autofill (e.g.
"Qwen / 通义" → "Qwen"). Re-clicking the selected tile clears
iconId/iconDataUrl but leaves the typed name alone, so users who
already edited the name don't lose their edit on accidental toggle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* polish(ai): add explicit close button to icon picker
The picker only opened via the icon badge above the Display Name field,
which left no obvious affordance to collapse it once a preset was
chosen — users either had to scroll past or click back up to the icon.
Drops a ghost Close button in the bottom-right of the picker's action
row (next to Upload/Reset) so dismissing the panel is one click and
visible without hunting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): wire model discovery to the resolved provider style
Codex review on #1105 flagged that ModelSelector still derives auth
headers from `providerId` (`x-api-key` only when providerId is the
literal "anthropic"). Now that ProviderConfig lets the user override
`style` independently, the form persists the override but the model
discovery call ignored it — so picking an Anthropic providerId pointed
at an OpenAI-compatible backend (or vice versa) would send the wrong
header and fail to list models even though chat routing already used
the override.
Extracts `buildModelDiscoveryHeaders(style, apiKey)` as a pure helper
in infrastructure/ai/. ModelSelector now resolves the protocol family
via `resolveProviderStyle` with the explicit `style` prop taking
precedence, then asks the helper for headers. ProviderConfigForm
passes the resolved style through. The `needsApiKey = providerId !==
"ollama"` shortcut stays as-is — that's a per-vendor concession, not a
style decision.
Refs codex review on #1105.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): apply per-vendor URL fallbacks across every style override
Codex review on #1105 caught that the openrouter (and ollama) baseURL
fallback was sitting inside the `style === 'openai'` branch, so a user
who picked OpenRouter providerId + Anthropic/Google style with an
empty baseURL would skip the fallback entirely. The SDK then routed
to its own default endpoint (api.anthropic.com /
generativelanguage.googleapis.com) using the user's OpenRouter key —
silent misrouting plus auth failures.
Pulls the per-vendor quirks out into a new pure helper
`resolveProviderEndpoint(config, style, safeApiKey)`. The URL
fallback now fires for every style — the user picked openrouter for a
reason, even if they overrode the wire format. The ollama-only
`'ollama'` literal apiKey swap stays gated on `style === 'openai'`
because Anthropic/Google clients need a real key, not the throwaway
placeholder.
Refs codex review on #1105.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): send x-goog-api-key for google-style model discovery
Codex review on #1105 flagged that buildModelDiscoveryHeaders bucketed
google-style providers with the OpenAI-compat default, so a model
refresh against any google-routed endpoint was sending `Authorization:
Bearer …`. That's the wrong auth header — Google Generative AI
rejects Bearer entirely and expects `x-goog-api-key` (or `?key=`).
The runtime chat path already uses `createGoogleGenerativeAI`, which
authenticates with the Google header family, so discovery was the
odd one out.
Adds an explicit `google` branch returning `{ "x-goog-api-key":
<apiKey> }` and updates the unit test. The default google providerId
never hits this path today (PROVIDER_PRESETS["google"] has no
modelsEndpoint), but the style override surface created in this PR
lets users compose providerId + style pairs where discovery does
fire — e.g. an openai/anthropic-style providerId pointed at a Google
endpoint via baseURL override.
Refs codex review on #1105.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): pick discovery endpoint from resolved provider style
Codex review on #1105 pointed out the lopsided fix in ae7394c8: I
switched discovery *headers* to honor the resolved `style` but left
the URL path coming straight from `PROVIDER_PRESETS[providerId]`. So
the moment style and providerId disagree the request shape is half
correct — e.g. Anthropic providerId + style=openai sends Bearer
(right) at `/v1/models` (wrong; the OpenAI-compat backend exposes
/models). 404s either way.
Adds `STYLE_DEFAULT_MODELS_ENDPOINT` (`anthropic → /v1/models`,
`openai → /models`, `google → undefined`) and a
`resolveModelsDiscoveryEndpoint(style, presetEndpoint)` helper. The
style's convention wins; the caller-supplied `presetEndpoint` is the
fallback for styles with no listing convention (currently just
google).
Behavior for stock configs is unchanged — every preset's existing
modelsEndpoint matches its style's default. Mismatches now line up
(headers + path together), and `custom` providers gain a sensible
discovery attempt when the user sets a style. My earlier inline reply
ducking this was wrong; codex's call was right.
Added three unit tests covering style defaults, the override-on-flip,
and the google-style passthrough.
Refs codex review on #1105.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The group details panel rendered Startup Command as a single-line <Input>,
so users couldn't type newlines into it — only the first line ever made it
into the saved config, which then broke the multi-line sequencing behavior
shipped in #1096 for any host inheriting the command from its group.
Switch to a 3-row <Textarea> to match the per-host details panel, so a
multi-line command typed on the group is preserved end to end.
Refs #1083.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(autocomplete): add snippet completion source
* feat(autocomplete): merge snippets at the command position
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(autocomplete): place snippet tests where the test runner finds them
The npm test glob covers components/terminal/*.test.ts but not the
autocomplete/ subdirectory, so the snippet tests added in the previous two
commits weren't actually running in the suite. Move them up to
components/terminal/ (the existing convention for autocomplete tests) with
corrected import paths; the engine snippet cases go in a separate
completionEngineSnippets.test.ts to avoid colliding with the existing
completionEngine.test.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(autocomplete): snippet ghost-exclusion, preview skip, and accept path
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(autocomplete): wire snippets into the terminal + preview snippet command
* fix(autocomplete): show only label in snippet popup row; keep snippet over colliding history
The popup row for a snippet now omits the inline command echo — the full
command lives in the detail preview only, matching the "label-only row"
design. The completion engine pushes snippet suggestions without the early
seen-text skip so that when a snippet's label collides with a history
entry's text, the higher-scored snippet survives the final dedup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): broadcast snippet command so popup acceptance mirrors peers
In broadcast mode, accepting a snippet from the autocomplete popup cleared
peer input (the line-clear keystrokes flow through the broadcast-aware path)
but never sent the command, since executeSnippetCommand wrote only to the
active session. Broadcast the normalized snippet data (matching the snippet
shortkey path) so peers receive both the clear and the command, keeping all
sessions in sync.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): broadcast wrapped snippet bytes to preserve noAutoRun
Broadcasting the raw normalized command sent un-wrapped newlines to peers,
so a multi-line noAutoRun snippet was pasted-but-not-run on the active
session yet executed line-by-line on broadcast peers (handleBroadcastInput
writes bytes directly without re-wrapping). Broadcast the exact bytes the
active session receives instead — bracketed-paste wrapping plus the auto-run
\r — so peers mirror the active session and noAutoRun is preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(terminal): add global startupCommandDelayMs setting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(terminal): add startup-command line-split and delay-clamp helpers
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(terminal): run multi-line startup commands in sequence with configurable delay
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(terminal): expose startup command delay in Terminal settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(terminal): keep startup-command line content verbatim
splitStartupCommandLines now only drops blank/whitespace-only lines and
normalizes CRLF, but no longer trims each line's content. This keeps a
single-line startup command byte-identical to what the user typed (e.g. a
leading space for HISTCONTROL=ignorespace is preserved), while still
supporting multi-line sequencing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sync): include startupCommandDelayMs in synced terminal settings
Terminal settings sync via the SYNCABLE_TERMINAL_KEYS allowlist; the new
startupCommandDelayMs preference was missing, so it wouldn't propagate across
devices. Add it (it's a user preference like keepaliveInterval, not
device-specific).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ai): add Claude auth-presence detection helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ai): surface actionable Claude auth errors and reap stuck agent processes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ai): add pure helpers for Claude config dir + env editor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ai): let users set Claude config dir and env vars in settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* polish(ai): harden Claude config env, de-dupe error text, label a11y
- buildClaudeEnv: drop managed keys (CLAUDE_CONFIG_DIR/CLAUDE_CODE_EXECUTABLE)
if a user types them into the free-text env editor, so they can't clobber
the config-dir field or the discovered executable path (+ regression test).
- bridge: only append error data fields not already shown as message/code,
so the actionable error text doesn't echo the same code/message twice.
- ClaudeCodeCard: associate the new config-dir/env labels with their inputs
via htmlFor/id.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ai): make the Claude auth & config section collapsible
The optional "Authentication & config" section now has a collapsible
header (chevron toggle). Collapsed by default to keep the card tidy, but
auto-expanded when the user already has a config directory or env vars set
so existing config isn't hidden. Local UI state, not persisted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): preserve raw env editor text; don't encourage plaintext secrets
P1: the env textarea was bound to the persisted value, which is the parsed
env re-serialized — so typing a key before its "=" was erased mid-entry
(buildClaudeEnv drops lines without "="). Keep the raw typed text in local
draft state and only resync from the persisted value on genuine external
changes (not our own parse→serialize round-trip).
P2: the env editor persists to localStorage in plaintext (no credential
encryption). Stop suggesting ANTHROPIC_API_KEY in the placeholder and warn
that values are stored in plaintext, steering credentials to the config
directory (a `claude` login) — consistent with keeping Claude auth
CLI/config-owned (#705).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): expand ~ in Claude config directory before passing to the agent
CLAUDE_CONFIG_DIR is handed to the spawned agent as an env var, which is not
shell-expanded — so "~/.claude" was treated as a literal "~" directory.
Expand a leading ~ at consume time (normalizeAgentEnv + getClaudeConfigDir)
rather than on save, so the stored value stays portable across machines
(cloud sync) and each device expands to its own home.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): recreate cached Claude provider when its env config changes
Claude ACP providers are cached per chat session and reused unless one of
the fingerprinted dimensions changes. authFingerprint was null for Claude,
so editing the config directory / env vars in Settings didn't take effect on
an already-running session. Fingerprint the Claude agent env so a config
change invalidates the cached provider and the next turn respawns with it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(terminal): add per-mode follow-theme resolver and storage keys
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(terminal): persist per-mode follow-theme selections in settings state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(sync): include per-mode follow terminal themes in cloud sync
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* i18n(terminal): add per-mode follow-theme picker strings
* feat(terminal): add type filter and auto option to theme picker
* feat(terminal): pick dark/light terminal theme when following app theme
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(terminal): don't flag the auto sentinel as a missing theme in the picker
The per-mode follow-theme pickers default to the 'auto' sentinel, which is
not a real theme id, so ThemeList's deletedSelectedTheme check classified it
as a deleted custom theme and rendered a spurious "Missing Theme" banner above
the Auto entry on first open. Exclude TERMINAL_THEME_AUTO from that check.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* i18n(terminal): align zh-CN dark/light wording with app convention
Use 深色/浅色 (matching the theme picker section headers and global
appearance settings) instead of 暗色/亮色 for the per-mode terminal
theme labels, so the picker modal reads consistently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(terminal): match follow-theme preview fallback to runtime resolution
The per-mode preview memos fell back straight to TERMINAL_THEMES[0] when a
selection resolved to a deleted theme, while the runtime currentTerminalTheme
memo falls back to the manual terminalThemeId first. Mirror the runtime chain
so the Settings preview matches the actual terminal for users with a
non-default manual theme.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
fetchSuggestions ran the full completion pipeline (history scan, fig specs, remote path lookups) on the main thread even when both the popup and ghost text were disabled — the results were then discarded. Add a shouldQueryCompletions(settings) gate and bail out early (clearing any stale state) when neither display mode is on.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The autocomplete hook (useState) lived in Terminal, so every suggestion / selection / live-preview update re-rendered the whole ~2775-line Terminal component. Move the hook and its popup into a dedicated <TerminalAutocomplete> component so those frequent state updates re-render only that small subtree.
The hook's handlers are surfaced back to Terminal via refs (the same refs already used to wire the xterm runtime), and the component is mounted unconditionally so the hook keeps recording command history and intercepting completion keys for the session's lifetime. No behavior change intended.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* perf(terminal): bound the connection log with a chunk ring buffer
The connection log kept the last 1,000,000 chars via `log += chunk; log = log.slice(-MAX)`. Once a session emits more than that, the slice flattens a ~1M-char string on every subsequent output chunk — on the render thread, for each echoed keystroke included — on long/busy sessions.
Replace the string with a small chunk-queue ring buffer that trims only the boundary chunk (amortized O(chunk) append) and materializes the full string once on read. Behavior is unchanged: it still retains exactly the last MAX_CONNECTION_LOG_DATA_CHARS characters.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* perf(terminal): coalesce connection log into bounded blocks (O(1) trim)
The first cut used one array entry per append and trimmed with chunks.shift(). For interactive output (many tiny chunks) the array grows toward the cap in entries, so once full, shift() reindexes ~N elements on every append — O(appends) per chunk, no better than the slice it replaced.
Coalesce appends into a small, bounded set of fixed-size blocks (~maxChars/blockSize). New data fills an open tail that seals into a block at blockSize; trimming only drops/slices a handful of blocks. Adds segmentCount() and a test asserting the segment count stays bounded across many tiny appends.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* perf(terminal): flush shell output on the event-loop turn, not a fixed 8ms timer
SSH/PTY output was coalesced and shipped to the renderer on a fixed 8ms timer. For interactive use that interval is pure added latency: every echoed keystroke waits out the timer before it can paint, so typing feels slightly behind.
Replace the timer with turn-based (setImmediate) coalescing in a single shared ptyOutputBuffer module, used by the SSH, local, telnet, and mosh paths. A single echoed keystroke is now forwarded almost immediately, while data arriving in the same turn still collapses into one IPC send, and a 16KB size cap still forces an immediate flush under heavy output.
Also de-duplicates two copies of the buffering logic (SSH had an inline copy; local/telnet/mosh shared another) and adds unit tests for the buffer.
Related to #1084.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(terminal): drop orphaned flushTimeout reference in SSH close handler
The SSH stream "close" handler still cleared `flushTimeout`, a variable that lived in the inline buffer removed when this path moved to the shared ptyOutputBuffer. Reading it now throws ReferenceError on every channel close, aborting the cleanup and exit signaling. The shared buffer's flush() cancels any pending flush internally, so the timer bookkeeping is removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat #826: optional Option+←/→ word jump on macOS
Adds a Terminal → Keyboard toggle "Option+←/→ jumps by word" (off by default,
synced). When on, a bare Option+Left/Right sends Meta-b / Meta-f instead of
xterm's default ^[[1;3D / ^[[1;3C, so readline/zle moves by word without
per-host bindkey setup (Termius-style).
The key→sequence mapping is a tested pure function; the handler reads the
setting live (no reconnect) and runs after kitty mode + autocomplete so it
doesn't override them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix#826: gate Option+←/→ word jump to macOS
The setting is syncable, so without a platform gate, enabling it on a Mac
would also rewrite Alt+←/→ to Meta-b/f on synced Linux/Windows devices,
breaking apps/shells that expect the default ^[[1;3D / ^[[1;3C. Pass
isMacPlatform() into the mapping so it only applies on macOS; add a test
for the non-macOS case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix#1079: preserve remote file mode when rz overwrites a same-named file
#1070's overwrite path rm's the remote file and lets rz re-create it, which
writes with the remote umask and drops the original permission bits — e.g. a
0755 script became 0644 after choosing "replace". (It didn't happen before
because rz used to skip same-named files, leaving the original untouched.)
Capture each conflicting file's mode during the pre-upload probe
(stat -c %a, BSD stat -f %Lp fallback) and chmod it back once the transfer
finishes and the files are on disk. Restore is best-effort: any failure
silently falls back to today's behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix#1079: probe file mode with `stat -- "$n"` for dash-prefixed names
Without `--`, `stat -c %a "-x.sh"` (and the BSD `-f %Lp` fallback) parse a
leading-dash filename as options, so the mode was never captured and overwrite
fell back to rz defaults — losing permission preservation for a valid filename
class. Mirrors the existing `rm -f --` handling. (chmod left as-is: its path is
always absolute, and BSD chmod doesn't accept `--`.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"Use Option as Meta key" was read into `altIsMeta` but only applied to the
mouse alt-click options (`altClickMovesCursor`). xterm.js's `macOptionIsMeta`
— the option that actually makes Option emit ESC-prefixed (Meta) sequences —
was never set, so on macOS Option kept producing layout characters (ƒ, ∫, …)
and readline/zle word shortcuts (Alt+f, Alt+b, Alt+Backspace) were dead.
Extract the altAsMeta→xterm mapping into one tested helper used by both the
terminal init path (createXTermRuntime) and the live settings sync
(Terminal.tsx) so the two can't drift again.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop the manualChunks 'vendor-react' entry: react/react-dom already land
in another chunk, so it only ever produced an empty chunk + a build
warning, with no caching benefit.
- Import domain/syncMerge statically in useAutoSync. It's already in the
eager graph via CloudSyncManager's static import, so the dynamic
`import()` couldn't be code-split anyway and only emitted a mixed
static/dynamic-import warning.
No behavior change; production build is warning-free.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of the persistent split-view 花屏: xterm's WebGL addon shares
ONE TextureAtlas across terminal instances with equal config (font / size
/ theme / DPR) — acquireTextureAtlas does `if (configEquals) { ownedBy.push;
return atlas }`. Two split panes then share an atlas, so the
clearTextureAtlas calls netcatty makes to recover from glyph corruption
(on resize / DPR / font change / tab show, from #1049 and #1066) clobber
the *other* pane's rendering. That's why the earlier redraw/clear-based
recovery attempts didn't help and only bounced the garble between panes.
Disable the sharing: remove the "reuse a matching atlas" loop so every
terminal creates its own atlas. The published bundle is minified, so this
is done with a small idempotent postinstall script (a patch-package patch
would be a ~550KB unreadable blob of the whole minified line). It
string-replaces the exact loop in the CJS + ESM builds, runs after
patch-package, and warns without failing if @xterm/addon-webgl changes.
Verified: split-view WebGL no longer garbles; script is idempotent
(patched=2 → already=2) and the production build is unaffected.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat #1064: add buildUploadPlan for rz overwrite/skip/cancel resolution
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat #1064: handle remote filename conflicts in rz handleUpload
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat #1064: SSH exec probe + remove for rz upload conflicts
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat #1064: IPC for rz overwrite-conflict prompt
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat #1064: renderer prompt for rz overwrite conflicts
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix#1064: repair sshBridge test mock (ipcMain.on) and i18n the overwrite dialog
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix#1064: make upload plan index-based to preserve per-file decisions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix#1065: resolve terminal cwd through su/sudo for the SFTP locate
The SFTP "locate to terminal's current directory" feature kept showing the
login user's home (e.g. /root) after the user switched accounts with su /
sudo -s and cd'd elsewhere.
getSessionPwd walks the remote process tree from a sibling exec channel to
find the interactive shell's cwd, but it only followed children whose comm
is a shell name (bash/zsh/...). su and sudo are named "su"/"sudo", so the
walk stopped at the login shell and read its cwd. The actual shell the user
is typing in lives *under* su/sudo as the controlling tty's foreground
process group.
Rewrite the walk to pick the deepest foreground shell ("+" in stat) within
the login shell's whole process subtree, which transparently follows
through su/sudo to the active shell, falling back to the login shell when
no foreground shell is found.
Verified on a real server (root -> su user -> cd /tmp):
before: /root after: /tmp
and confirmed the no-su case is unchanged (cd /var -> /var).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fall back to login shell cwd when the active shell's /proc is unreadable (Codex review)
When an unprivileged user runs `sudo -s` / `su root`, find_active_shell
correctly selects the root-owned foreground shell, but the exec channel
(running as the login user) cannot readlink another uid's /proc/<pid>/cwd
due to ptrace permissions. Without a fallback the script dropped straight
to the home directory, regressing user→root sessions.
Retry readlink on the same-uid login shell before falling back to home.
Verified live (user -> cd /var -> sudo -s -> cd /tmp): the root shell's
cwd is unreadable, and the result is now /var (login shell cwd) instead of
/home/<user>.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Select the interactive (tty-bearing) login shell deterministically (Codex review)
find_login_shell picked the first shell child of sshd and exited, but ps
output is unsorted, so when other exec channels (server-stats polls, etc.)
are running on the same connection their transient sh could be chosen,
making find_active_shell walk the wrong subtree.
Prefer the shell child that has a controlling tty: the interactive shell
has a pts, while non-PTY probe exec channels have tty "?". This is
deterministic regardless of ps order, in both the su and no-su cases (the
old "prefer foreground" heuristic was itself nondeterministic under su).
Falls back to any shell child if none has a tty.
Verified live with a concurrent no-tty `sh -c sleep` under the same sshd:
the pts/0 bash is selected and the result is /tmp, not the probe shell.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix#1062: treat SSH shell TMOUT auto-logout as a timeout, not a normal exit
A shell-level TMOUT idle auto-logout makes bash/csh exit cleanly (numeric
exit code, no signal), which is byte-for-byte indistinguishable from a
user-typed `exit` at the SSH protocol level. PR #1057 keyed the
close-vs-keep decision on `streamExited` (numeric code + no signal), so
TMOUT exits were reported as reason "exited" and the tab was auto-closed —
reintroducing the problem from #977.
Verified against a real server that bash TMOUT exits with code 0 / no
signal and prints "timed out waiting for input: auto-logout" to the
channel before it closes. Since exit code/signal can't distinguish it from
an intentional exit, detect that banner in the session's existing rolling
output tail (_promptTrackTail) and report reason "timeout" instead, which
routes to the existing markDisconnected path (keep tab + reconnect). A
normal `exit`/`logout` (no "auto-" prefix) still auto-closes the tab, so
PR #1057's behavior is preserved.
zsh's TMOUT raises SIGALRM (a signal), so it already took the
keep-tab/reconnect path and is unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Anchor TMOUT auto-logout match to the banner's final line (Codex review)
The detector matched "auto-logout" as an unanchored substring within the
last 256 chars, so command output that merely mentions it (e.g. `grep
auto-logout /etc/profile` while investigating TMOUT) followed by an
intentional `exit` could be misclassified as a timeout and wrongly keep
the tab open. Anchor on the final non-empty line of output instead — the
banner the shell prints right before exiting — which loses no true
positives (verified against the real-server output shape) while rejecting
mid-stream mentions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hidden tabs stay mounted off-screen (visibility:hidden) so each keeps a
live WebGL context. Creating another terminal's WebGL context — or the GPU
dropping a non-composited off-screen canvas — leaves the hidden terminals'
drawing buffers corrupted ("花屏"). This reproduces on both Windows and
macOS: opening 2 tabs garbles the 1st, opening 3 garbles the 1st and 2nd,
while the just-created (visible) one is always fine. The DOM renderer is
immune because it uses real DOM nodes.
A window resize recovers the display because it triggers a full repaint
(clearTextureAtlas + RenderService._renderRows). A tab switch did not:
the visibility effect only calls safeFit, which early-returns when the
pane's dimensions are unchanged, so no redraw happened.
Perform the same recovery a resize does when a tab becomes visible:
clear the texture atlas (no-op on the DOM renderer) and synchronously
repaint every row. Verified against xterm core that _renderRows draws
unconditionally, independent of dimension changes or dirty-row tracking.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat #1005: add live-preview keystroke calculator for popup autocomplete
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat #1005: live-render the selected popup suggestion on arrow navigation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat #1005: free Tab for the shell; Enter runs the rendered line; Esc reverts
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat #1005: show key hint (→ expand / ↵ run) on the selected popup row
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat #1005: live-render full path while navigating sub-directory panels
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test #1005: move live-preview test into the npm test glob
The test runner only scans components/terminal/*.test.ts (not the
autocomplete/ subdir), matching where the other autocomplete-module tests
live (e.g. completionEngine.test.ts). Relocate so it actually runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix#1005: center and refine the popup key-cap hint
Use inline-flex centering (the ↵ glyph was vertically off with line-height +
padding), softer color-mixed border/background, a system-sans font so the
glyph renders consistently regardless of the terminal font, and the more
balanced ⏎ return symbol.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix#1005: record the actual executed line on Enter, not the stale suggestion
Codex review (P2): the popup Enter handler recorded selected.text and
suppressed handleInput's recorder, so editing a previewed command (select
docker, type ' ps', Enter before the re-query) logged the stale 'docker'
instead of 'docker ps'. Delegate to handleInput's Enter path, which records
lastAcceptedCommandRef on a clean select and falls back to the live buffer
after an edit (typing nulls that ref).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix#1005: don't revert user edits when Escape closes the popup
Codex review (P2): previewActiveRef stayed true after the user edited a
previewed command, so Escape (before the debounced re-query reset state)
called renderPreviewSelection(-1) and rewrote the line back to the stale
baseline, dropping the edits. Clear previewActiveRef when the user types
(alongside the existing lastAcceptedCommandRef reset), so Escape only reverts
a pristine preview and otherwise just dismisses the popup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Middle-clicking a tab (mouse wheel click) is a conventional "close tab"
gesture in browsers and editors. Wire it to every closeable tab strip:
the top session / workspace / log-view / editor tabs and the SFTP tab bar.
A small shared helper (lib/tabInteractions.ts) handles the gesture:
onAuxClick closes the tab when button === 1, and onMouseDown calls
preventDefault for the middle button so the Chromium/Electron autoscroll
overlay does not appear. Left-click activation and right-click context
menus are untouched.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Heavy full-screen TUIs (claude code / gemini cli / opencode), font changes,
and device pixel ratio changes can leave xterm.js's WebGL glyph texture atlas
in a corrupted state that persists for the life of the terminal — users see
persistent "garbled / 花屏" output that only clears when a brand-new terminal
is opened (most often on Windows with display scaling / multi-monitor setups).
Clear the texture atlas so glyphs re-rasterize at the correct scale instead of
forcing users to reopen the terminal:
- Add watchDevicePixelRatio() helper (TDD, unit-tested) that re-registers a
matchMedia listener across DPI changes and fires a repair callback.
- Wire it into createXTermRuntime: on devicePixelRatio change, clear the atlas
and refit; also clear the atlas on reflow (term.onResize). Watcher is torn
down on dispose.
- Expose clearTextureAtlas() on XTermRuntime and call it after font changes in
Terminal.tsx (xterm.js #3280). All calls are no-ops under the DOM renderer.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inline (ghost-text) suggestions render suggestion.substring(trackedInput.length)
after the cursor, where trackedInput is a client-side reconstruction of the
command line (buffer heuristics + keystroke prediction, to mask SSH echo
latency). On hosts with non-standard echo — hardware bastion hosts / network OS
like `ecOS#` (#1013, previously #756 / #906) — that reconstruction drifts and
the ghost gets painted over characters the user already typed (`int` + ghost
`terface` -> `intterface`).
Add a fail-safe consistency check: on each post-echo render, if the real
terminal line before the cursor contains the tracked input followed by more
untracked, non-whitespace characters (reality is AHEAD of what we tracked),
hide the ghost instead of drawing it over real text. SSH echo latency is the
opposite case (the line is a prefix-behind of the tracked input) and is
deliberately not flagged, so the ghost stays responsive on slow links. The
check is ASCII-only (wide-char column mapping is ambiguous) and fail-open, so
it can only ever suppress a ghost that would otherwise corrupt — never change
correct behaviour.
This converts the recurring "ghost shows already-typed characters" bug into
"ghost simply doesn't show" on devices we can't track reliably.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
macOS keys the "Local Network" privacy permission on the main executable's
Mach-O LC_UUID (Apple TN3179). Electron's prebuilt binary is linked with LLD,
which derives the UUID from a content hash, so every app built from the same
Electron version ships the *same* LC_UUID even with a different bundle id. That
collision makes the grant unreliable: a user who enables Local Network for
Netcatty can still hit `connect EHOSTUNREACH` on LAN / VMware host-only
addresses, while loopback-forwarded connections work.
Add an electron-builder afterPack hook that rewrites the packaged macOS
executable's LC_UUID to a value derived deterministically from the appId —
stable across builds (so the grant survives updates) but distinct from every
other app. It runs before code signing, so signature/notarization cover the
patched binary. No-op on Windows/Linux.
Verified the rewrite on a copy of Electron's binary (LC_UUID changes, file
stays a valid Mach-O, deterministic) and added unit tests for the Mach-O
patcher (thin + fat) and the UUID derivation.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Electron's BoringSSL dropped several standard MODP groups from the named
crypto.createDiffieHellmanGroup() API — notably the 1024-bit Oakley Group 2
(modp2) that backs SSH's diffie-hellman-group1-sha1. ssh2 calls
createDiffieHellmanGroup('modp2') for that kex, so connecting to legacy
network devices that only speak group1-sha1 failed with "Error: Unknown DH
group".
The underlying DH math still works on BoringSSL via createDiffieHellman()
with an explicit prime, so add a compatibility shim that wraps
createDiffieHellmanGroup and falls back to the well-known prime constants
when (and only when) the runtime can't resolve a group by name. On OpenSSL
builds the original call succeeds and the fallback is never used.
The shim is installed in main.cjs before any ssh2-using bridge loads, since
ssh2 destructures createDiffieHellmanGroup at module load. Once installed,
the existing legacy-group probe detects modp2 as supported again and offers
group1-sha1, so affected devices actually connect (still gated behind the
per-host legacy-algorithms toggle).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The fixed-DH-group support probe called crypto.createDiffieHellmanGroup()
for each MODP group to feature-detect runtime support. Under Electron's
BoringSSL, instantiating the large groups is pathologically slow
(modp18/8192-bit takes ~20s on first call), and the result is only cached
in-process, so the first connection after every app launch froze for ~24s.
The standard modern groups (modp14/16/18) are universally supported and
always pass the probe anyway, so treat them as supported without probing.
Only groups a runtime may genuinely drop (e.g. BoringSSL removed the weak
1024-bit group1/modp2) are still feature-detected; those fail instantly.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(terminal): separate prompt after unterminated command output
Add a display-layer prompt line break handler so recognized shell prompts move to the next visual line when the final command output line is not newline terminated.
Also add a terminal setting to toggle the behavior, sync support, i18n copy, and focused tests for prompt insertion.
* fix review issue
* Fix prompt cache initialization
* Serialize terminal output writes for prompt breaks
* Keep terminal status lines ordered with output
* Fix prompt arming without command callback
* Keep prompt display breaks out of session logs
* Avoid prompt breaks for output suffix matches
---------
Co-authored-by: yuzifu <yuzifu@TB16PGen5.Info>
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
After the GitHub Release is published, push an updated Cask to
binaricat/homebrew-netcatty so `brew install binaricat/netcatty/netcatty`
stays current within minutes of the release. Stable tags only — prerelease
tags (v1.2.0-rc.1 etc.) are skipped to keep brew users on stable.
Implementation:
- New script .github/scripts/bump-homebrew-cask.sh computes SHA-256 of the
arm64 + x64 DMGs already downloaded by the release job, sed-patches the
Cask file in the tap repo, sanity-checks the result parses as Ruby, and
pushes the bump. Idempotent on re-run when checksums match.
- New homebrew-tap job in build.yml runs after the release job on the same
stable-tag gate, downloads the macOS artifact bundle, then runs the
bump script with HOMEBREW_TAP_TOKEN.
Requires HOMEBREW_TAP_TOKEN secret with contents:write on
binaricat/homebrew-netcatty. With the secret missing the job will fail
fast at the env-var check with no side effects (no push attempted).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix#969: auto-fill saved password into PAM-style keyboard-interactive prompts
Servers running stock PAM Linux configurations (most distros) only advertise
`keyboard-interactive` as their auth method, not `password` — so even when
the user has saved a password on the host, Netcatty was popping a modal
asking them to type it again. Every connect ended up being a two-password
flow: one to dispatch, one in the modal.
The shared `createKeyboardInteractiveHandler` factory now recognizes the
classic "PAM-wrapped password" challenge (a single prompt with
`echo === false`) and finishes it with the saved password directly,
skipping the modal. Real multi-prompt or echo-visible challenges (2FA / OTP
/ security questions) still go to the modal as before, and a wrong-password
auto-fill on the first attempt falls back to the modal on the retry so the
user can correct it.
Also consolidated startSSHSession's inline keyboard-interactive handler —
which duplicated ~45 lines of the factory logic without the auto-fill
fix — to use the factory with progress callbacks. The chain / SFTP /
port-forwarding bridges already went through the factory and pick up the
auto-fill for free.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address Codex review: only auto-fill prompts that mention a password
The previous heuristic ("single prompt + echo=false + saved password →
auto-fill") would also fire for OTP / Duo / hardware-token challenges,
which are single hidden-echo prompts too. That would burn one auth
attempt per reconnect on those servers and could trip pam_faillock /
pam_tally2 lockout policies before the user ever saw the modal.
Add a prompt-text gate: auto-fill only when the prompt contains a known
password keyword (Latin "password" / "passwd"; CJK "密码" / "口令").
Custom-localized prompts that don't match fall through to the modal,
which is the same behavior as the pre-#969 baseline — strictly no
worse than before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address Codex review (round 2): exclude OTP vocabulary from auto-fill
The previous PASSWORD_PROMPT_PATTERN matched anything containing "password"
/ "passwd" / "密码" / "口令", which still let through OTP shapes that
happen to include those words: "Enter your one-time password", "动态密码"
(Chinese for "dynamic password" = OTP), "动态口令", "一次性密码", etc.
Add an OTP/MFA vocabulary check that runs before the password keyword
check. Any prompt containing OTP terminology (one-time, OTP, verification,
passcode, token, 2FA, two-factor, MFA, Duo, 动态, 一次性, 验证码, 令牌,
双因素, 多因素, 短信验证, 手机验证) is disqualified from auto-fill even
if it also matches the password keywords.
Tests cover both English "One-time password" and the three common Chinese
OTP phrasings, plus a regression guard that normal sudo-style password
prompts still auto-fill.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The host-key verifier was misclassifying connections as `changed` in three
situations that had nothing to do with a real key rotation:
1. Records imported from the system `~/.ssh/known_hosts` (or older builds)
landed in localStorage without a `fingerprint` field. The verifier then
re-derived the fingerprint from the stored `publicKey` blob on every
connect — a brittle path that produced a different value than ssh2 if
anything about the serialization differed by even one byte.
2. `classifyHostKey` had a loose "single candidate with unknown / empty
keyType → changed" heuristic. Any imported record whose keyType failed
to parse would be promoted to a rotation warning the first time the
server presented a real algorithm, even though the user had never
actually trusted any fingerprint for that algorithm.
3. A host that genuinely had multiple algorithms (e.g. one stored ssh-rsa
record plus a live ssh-ed25519 handshake) was being reported as
`changed` instead of `unknown`, even though we had no comparable
record for the algorithm the server presented.
Tabby (`tabby-ssh/src/session/ssh.ts`) and OpenSSH both treat case (3) as a
first-time prompt rather than a mismatch; this change brings Netcatty in
line with that model.
Changes:
- `domain/knownHosts.ts` ports `fingerprintFromPublicKey` to TS and adds
`normalizeKnownHost` / `normalizeKnownHosts` so the renderer can backfill
legacy records on hydration. Pure-JS SHA-256 keeps the migration
synchronous so it can run inline in `useVaultState` without async
plumbing.
- `application/state/useVaultState.ts` runs the migration on hydration
and on cross-window storage events. When anything changes on hydration
the migrated list is written back to localStorage so the next launch
starts clean.
- `components/KnownHostsManager.tsx` populates `fingerprint` at import
time instead of leaving it for the verifier to re-derive.
- `electron/bridges/hostKeyVerifier.cjs` simplifies `classifyHostKey` to
fingerprint-first, then strict (host, port, keyType) match for the
changed branch, then fall through to `unknown`. Two existing tests
that locked in the loose heuristic are updated to assert the new
(safer) behavior, and a new test covers the multi-algorithm
first-encounter case.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up on #966 which added `hover:bg-accent` to the existing raw
`<button>` element. That element is `h-full w-10`, so the new hover
fill spanned the entire title-bar height — a giant vertical accent
strip instead of the small icon-button highlight we wanted.
Replace the raw element with the same shadcn `Button variant="ghost"
size="icon" h-6 w-6` that every other icon on the same row already
uses. Wrap it in a centered container that keeps the title-bar height
for window-control alignment and carries `app-drag` so the empty
space around the icon still drags the window; the button itself stays
`app-no-drag`.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hovering the gear icon in the top tab bar left no visual response while
every other icon on the same row (AI, theme toggle, sync) lights up on
hover with the accent fill. The gear button is a raw `<button>` rather
than the shadcn `Button variant="ghost"` because it spans the full
title-bar height to align with the window controls, so it never picked
up the ghost variant's `hover:bg-accent`.
Adds the matching `hover:bg-accent` class so the gear behaves the same
as its neighbours. The inline `color` style for the resting state stays
in place; the accent fill on hover is what was missing.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The right-click menu on host cards in the Pinned and Recently Connected
sections only exposed Connect / Edit / Pin-Unpin / Delete, while the
canonical "All hosts" listing also offers Duplicate and Copy Credentials.
There is no reason to omit those two for hosts you've pinned or recently
opened — the underlying handlers are already wired up.
Add the missing entries in the same order as the All-hosts menu so the
three context menus stay visually identical.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The copy-host-address, broadcast and focus-mode buttons sit on the
per-host statusbar directly under the top tab bar. With the default
top-side tooltip placement, hovering any of them paints the tooltip
on top of the tab title above (the visible "Copy host address …"
covering "Rainyun-114.66.26.174" in the bug report screenshot).
Drop the tooltips on the bottom side instead, matching the
HoverCardContent panels already used for the CPU/Memory/Disk stats
buttons on the same bar.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix#919: harden built-in Telnet handshake for legacy gear
The built-in Telnet client failed to advance past the welcome banner on
some older switch firmware (HP ProCurve 2610 reported in #919) and, in
the same session, leaked snippets of subnegotiation payloads into the
terminal display as random-looking characters. Three independent
correctness gaps in the old implementation, all rolled into one PR:
1. The negotiation parser was stateless per chunk. An IAC sequence
split across TCP frames either dropped the lone IAC (lost command)
or, for IAC SB...IAC SE blocks whose terminator landed in the next
frame, fell through to "skip IAC SB and treat the rest as data" —
spilling the subnegotiation payload (TERMINAL-TYPE strings,
environment data) into the user's terminal as garbage.
2. The client was purely reactive — it only ever responded to options
the server raised. Quite a bit of legacy equipment waits for the
client to commit to SUPPRESS-GO-AHEAD / TERMINAL-TYPE / NAWS before
it will continue past its banner, so connections silently hung at
"Press any key to continue" forever.
3. Outbound user input was never IAC-escaped, so any 0xFF byte the user
pastes (or that an alternate input encoding emits) would be read by
the peer as the start of a command and eat the following byte.
Approach:
- New `electron/bridges/telnetProtocol.cjs` owns RFC 854 framing as a
pure module. `createTelnetParser` is a stateful machine that buffers
any partial command (lone IAC, IAC + verb, unterminated SB) across
feeds and replays it once the rest arrives. Emits clean stream
bytes, option commands and complete subnegotiations through
callbacks. `escapeIacForWire` doubles 0xFF bytes on the way out with
a cheap fast-path for the common (no 0xFF) case.
- `terminalBridge.cjs` flips telnet handling into a lazy mode: until
the peer sends an IAC byte the connection is plain passthrough, so
raw-TCP-on-port-23 services are not corrupted by the protocol layer.
Once the protocol activates, we proactively request DO
SUPPRESS-GO-AHEAD, WILL TERMINAL-TYPE and WILL NAWS, and track those
in a `requestedOptions` Set so the peer's acknowledgement does not
trigger another reply (the classic negotiation loop).
- TERMINAL-TYPE is now advertised as "XTERM-256COLOR" (upper-case);
legacy boxes that case-sensitive-match termcap names recognise it.
- Resize-driven NAWS subnegotiations now only fire after the protocol
has actually activated, so a passthrough session is never poisoned.
- Outbound writes for telnet sockets convert strings to UTF-8 buffers
and run them through `escapeIacForWire`, so paste of binary content
and non-ASCII input encodings round-trip safely.
Tests:
- 17 unit tests in `telnetProtocol.test.cjs` cover normal data,
option commands, subnegotiation (including IAC IAC inside payload),
every cross-frame split point (lone IAC, IAC + verb, mid-SB), the
specific regression that previously leaked SB payload as data,
ordering of data vs command callbacks, and the IAC escape helper.
- Existing 18 telnet auto-login tests still pass, exercising the
end-to-end socket → parser → renderer path. Full suite: 825 / 0 / 3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address review: per-direction Telnet negotiation tracking
RFC 858 §"Default Specification" treats WILL/WONT and DO/DONT as two
independent option streams. The first revision of this PR used a single
`requestedOptions` Set keyed by option byte, which incorrectly swallowed
a peer's independent request on the opposite direction whenever we had
our own request still pending for the same option.
Concrete failure mode (highlighted by code review on the PR): we send
`DO SGA` and the peer simultaneously sends `DO SGA` asking us to enable
SGA on our outgoing side. The old check matched the peer's DO against
our pending DO and returned silently, leaving the peer's request
unanswered — strict implementations would either time out or proceed in
the wrong mode.
Fix: split pending requests into `pendingDoRequests` (we sent DO,
awaiting WILL/WONT) and `pendingWillRequests` (we sent WILL, awaiting
DO/DONT). Acknowledgement matching is now direction-aware; the peer's
independent request on the orthogonal direction is treated as a fresh
negotiation and replied to.
While in there, the related bug uncovered by reviewing this code: when
the peer's `DO NAWS` acknowledges our own `WILL NAWS`, we previously
just dropped it on the floor — but the actual window-size SB payload
needs to follow the WILL handshake either way (whether the DO is an
acknowledgement of our WILL or an independent fresh request). The
negotiator now always pushes the size subnegotiation on `DO NAWS`.
Refactor: the negotiation policy lives in a new
`createTelnetNegotiator` factory inside `telnetProtocol.cjs`, separate
from the parser. That keeps `terminalBridge.cjs` thin and — more
importantly — makes the policy directly unit-testable. 13 new tests
cover the bidirectional-collision regression, the missing NAWS
follow-through, fresh vs ack handling for each verb, the canonical
handshake sequence, unsupported-option WONT/DONT replies, the
TERMINAL-TYPE SEND→IS roundtrip, and the 80×24 fallback for invalid
sizes.
Total: 30 parser+negotiator unit tests, 18 existing telnet auto-login
integration tests, full suite 838 / 0 / 3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two changes addressing both halves of #958:
1. IPv6 highlighting
The built-in 'URL, IP & MAC' rule only shipped URL, IPv4 and MAC
patterns, so compressed IPv6 addresses such as 2001:11:22:33::5 or
fe80::d2dd:bff:fe79:f2bb were never highlighted. Add an IPv6 regex
covering full and compressed forms (including ::1 and leading-/trailing-
:: variants) and merge it into the same 'ip-mac' rule's patterns. The
normalizer's existing "fill missing defaults" path means existing users
pick this up on next start with no migration step.
2. Editable built-in rules
Add an optional `customized` flag to KeywordHighlightRule. When false /
absent, normalize re-syncs the rule's label/patterns with the shipped
defaults (so future default-pattern upgrades reach users automatically).
When true, normalize keeps the user's label/patterns/color/enabled
verbatim, allowing built-ins like 'ip-mac' to be tailored.
SettingsTerminalTab:
- Pencil icon now appears on built-ins too. Editing one routes through
the same dialog and flips `customized` on save.
- The pattern field becomes a Textarea so multi-pattern built-ins (e.g.
'error' ships seven spellings) can all be edited in one go.
- A per-rule "↺" reset icon appears on customized built-ins and restores
the shipped label/patterns while preserving the user's color/enabled.
- The footer's "Reset to default colors" button is broadened into
"Reset built-ins to defaults", restoring every built-in to shipped
label/patterns/color and clearing `customized`.
Tests:
New domain/keywordHighlight.test.ts (6 tests) covers IPv6 matches for
both #958 examples plus loopback and full-form, IPv4/MAC still match,
normalize migrates legacy non-customized 'ip-mac' to include IPv6,
normalize preserves customized patterns, and normalize keeps user
custom rules verbatim. Full suite: 808/0/3.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix#954: unify Tooltip styling + replace native selects
Replace native HTML title= tooltips and native <select> dropdowns
with the existing Radix-based Tooltip / Select components so they
share the app's rounded styling, theme tokens and i18n pipeline.
Adds a global TooltipProvider in AppWithProviders so every
descendant Tooltip works without a per-file Provider wrapper.
Scope (driven by the issue #954 examples and "全部都处理" follow-up):
- TerminalLayer toolbar: Add Terminal / Split View / SFTP / Scripts
/ Theme / AI Chat / Move panel / Close panel.
- TopTabs middle bar: quick switcher, more tabs, AI assistant, theme
toggle, settings; window-control buttons (min/max/close), tray
close and hotkey reset/disable have their native title dropped per
the user's explicit opt-out ("可以不用Tooltip,直接全局禁用
原生title 属性").
- AI panels: AIChatSidePanel session history / new chat / delete,
ConversationExport, AgentSelector, ChatInput attach / expand /
permission, ModelSelector, ProviderCard, ai-elements/tool-call.
- SFTP: SftpSidePanel header, SftpBreadcrumb, SftpFileRow,
SftpPaneToolbar, SftpTabBar, SftpTransferQueue.
- Settings: SettingsPage close, SettingsAppearanceTab theme/accent
swatches, SettingsFileAssociationsTab edit/remove, SettingsSystemTab
crash-log paths and global hotkey reset.
- Host vault: HostDetailsPanel (clear / suggestions / show-password /
key path / browse key), GroupDetailsPanel, KnownHostsManager,
ConnectionLogsManager, KeychainManager, SyncStatusButton,
CloudSyncSettings, LogView, QuickSwitcher, ScriptsSidePanel,
Terminal status bar copy-host + broadcast/focus, ZmodemProgressIndicator.
- Terminal subcomponents: HostKeywordHighlightPopover, TerminalComposeBar,
TerminalConnectionDialog, TerminalSearchBar.
- Editor: TextEditorPane (subtitle, search, wrap, promote-to-tab).
- TrayPanel session rows and port-forwarding rows.
Native <select> migrated to custom Select component:
- SerialConnectModal (data bits, stop bits, parity, flow control)
- SerialHostDetailsPanel (same four fields)
- HostDetailsPanel backspace behavior
- GroupDetailsPanel backspace behavior
- SettingsTerminalTab local shell picker
- terminal/ThemeSidePanel font weight
Hardcoded English strings extracted to i18n. New keys for both
en and zh-CN: terminal.layer.*, topTabs.*, ai.chat.* (sessionHistory,
attach, collapse, expand, enableAgent), zmodem.*, settings.shortcuts.
resetToDefault. Inline help text on SnippetsManager package-name input
removed because the same hint is already shown in a visible <p> below
the input.
Existing per-file <TooltipProvider> wrappers (SnippetsManager,
ScriptsSidePanel, SelectHostPanel, RuleCard, HostDetailsPanel proxy
section) are left in place — they nest harmlessly under the global
provider and stay self-sufficient for component tests.
Tests:
- tsc clean for changed files (pre-existing repo-wide errors
unrelated to this PR).
- All 802 tests pass (3 skipped pre-existing).
- HostDetailsPanel.proxyProfile.test and TextEditorPane.test
updated to wrap with TooltipProvider, matching the runtime
context now needed by the migrated components.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix#954: wrap Settings + Tray windows with TooltipProvider
Settings and the tray panel mount as separate Electron windows with
their own React root in index.tsx, so they do not inherit the global
TooltipProvider added under AppWithProviders. After the unified
Tooltip migration, any settings tab that used a Tooltip (Appearance,
Application, FileAssociations, System, Shortcuts, Terminal, AI
ProviderCard, AI ModelSelector) — and TrayPanel — threw
"Tooltip must be used within TooltipProvider" and rendered nothing.
Wrap both branches with TooltipProvider at the same level as
ToastProvider in index.tsx.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
useVaultState hydrates knownHosts asynchronously — its init awaits the
decryption of hosts, keys, identities and proxyProfiles before reading
knownHosts from localStorage. The state is briefly [] at boot even when
localStorage has saved entries.
The host-key verifier introduced in bce33f34 reads the renderer's
knownHosts state at connect time. Any SSH connect that fires inside
that hydration window (manual click or auto-restored session) sees an
empty trust list, marks every host as unknown, and prompts again. The
fix accepted by the user is saved to localStorage, but next restart
the same race repeats, giving the impression that fingerprints are
never persisted.
Use the existing getEffectiveKnownHosts helper at the two sites that
feed the SSH connect path (VaultView + TerminalLayerMount). The helper
falls back to localStorage while state is still settling, mirroring
the same pattern already applied to sync payloads (App.tsx:479).
Memoised on the knownHosts state so the prop reference is stable and
the TerminalLayer/VaultView React.memo equality checks still hold.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sftp): add drive switcher dropdown for local Windows panes
On Windows, the SFTP breadcrumb's first segment (drive letter) now shows
a dropdown to switch between available drives. This makes it easy to
navigate across drives without manually editing the path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sftp): probe drives async to avoid blocking main process
fs.accessSync in the listDrives IPC handler could stall the Electron
main process for seconds per disconnected mapped drive or empty optical
drive. Use fs.promises.access with Promise.allSettled so the 26 probes
run in parallel without blocking the event loop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
Adds a small clipboard-copy icon next to the host label / status dot in
the terminal pane's statusbar. Clicking copies the host's hostname
(IP or DNS name — what users called "machine IP" in #951) to the
clipboard and surfaces a toast.
The button only renders for non-local SSH/serial/telnet sessions —
local shells don't have an addressable hostname so showing it would
be confusing.
Placed in the pane statusbar (not the top tab) because the statusbar
is per-host: a workspace pane carries exactly one host, so the button
always identifies the right address. Top tabs in a workspace can share
multiple panes / hosts and would be ambiguous.
Visual treatment matches the surrounding stats buttons: 10px icon,
inline with the existing host label + status dot, opacity-60 →
opacity-100 on hover, `title` attribute for the tooltip to match the
pattern of the CPU/MEM/disk stats triggers right next to it.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ssh): per-host keepalive override + cloud-friendly defaults (#939, #581)
Issues #939 (cloud / Aliyun sessions silently freezing after 15-20 min idle
because no SSH keepalive packets are sent) and #581 (older routers like
NOKIA / ALCATEL being killed by ssh2 after a few unanswered keepalives) are
in direct tension at the global-setting level: cloud users want keepalive
ON, embedded-device users want it OFF, and any single global default hurts
the other group.
Resolves the conflict by moving keepalive to a per-host setting (mirroring
the existing `legacyAlgorithms` per-host pattern), with cloud-friendly
global defaults:
Domain:
- Host gains `keepaliveOverride?: boolean` + `keepaliveInterval?: number`
+ `keepaliveCountMax?: number`. When override is true, the host's
values are used; otherwise the global TerminalSettings values apply.
Per-field fallback so a host can override interval only or countMax only.
- TerminalSettings gains `keepaliveCountMax: number` so the second knob
(number of unanswered keepalives before declaring dead) is no longer
hardcoded at 3 in the bridge.
- DEFAULT_TERMINAL_SETTINGS: keepaliveInterval bumped from 0 to 30, and
keepaliveCountMax = 10. Cloud LBs / NAT tables stay populated; brief
network glitches don't trip the dead-connection check; an actually
dead session is detected within ~5 minutes. Existing users with 0
saved keep their value (no migration) — they were the #581 router
cohort and their setup still works untouched.
Plumbing:
- domain/host.ts adds resolveHostKeepalive(host, globalSettings) with
five unit tests covering both directions of the override flag and
per-field fallback.
- components/terminal/runtime/createTerminalSessionStarters.ts uses the
resolver when building startSSHSession options.
- electron/bridges/sshBridge.cjs reads keepaliveCountMax from options
(defaulting to 10) at both connection sites (direct + jump host) and
still routes interval=0 through to a fully disabled keepalive
(preserving #581's escape hatch).
UI:
- Settings → Terminal → Connection grows a second input next to the
existing interval: "Max unanswered keepalives".
- Host details panel gains a Keepalive section with a "Override global
keepalive" toggle that, when on, exposes per-host interval +
countMax inputs and an inline hint when interval = 0 (explaining
the implications). Same visual pattern as the existing Legacy
Algorithms section.
Sync:
- keepaliveCountMax added to SYNCABLE_TERMINAL_KEYS so the new global
field rides existing sync infrastructure. Per-host fields ride the
hosts array passthrough automatically (older clients receiving them
ignore unknown fields, per the existing lenient sync contract).
i18n: en + zh-CN strings for the new settings row, the host section
header, and the override toggle / inputs / disabled hint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ssh): resolve keepalive per jump host, not just the final target
Addresses codex review on PR #947:
https://github.com/binaricat/Netcatty/pull/947#discussion_r3217027xxx
The first cut only resolved keepalive for the final target host and
forwarded a single interval/countMax pair across the whole start-SSH
call. connectThroughChain in sshBridge.cjs then applied that one pair
to every hop, so a chain like:
router (bastion, needs keepalive=0) → cloud target (needs 30s)
would either kill the router (with cloud-friendly defaults) or fail
to keep the target alive (with router-friendly 0). The per-host
override was effectively useless for bastion hosts.
Fix:
- NetcattyJumpHost gains optional keepaliveInterval / keepaliveCountMax.
- createTerminalSessionStarters runs resolveHostKeepalive() per
jumpHost when building the chain, so each hop carries its own
resolved pair.
- sshBridge.cjs's chain connector reads jump.keepaliveInterval /
jump.keepaliveCountMax for each hop, falling back to the call's
target-level options for backward compatibility with older
serializers that don't yet populate the per-hop fields.
The final target's keepalive path is unchanged — it still reads
options.keepaliveInterval / options.keepaliveCountMax that the
session starter resolves from the target host.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ssh): per-host keepalive for SFTP + port forwarding too
Follow-up to the maintainer review on PR #947 — terminal SSH was the
only path that honored per-host keepalive overrides. SFTP and port
forwarding share the same NetcattyJumpHost type but their builders
weren't resolving keepalive per-hop, and their bridges hardcoded the
old 10s/3 defaults. Net result: a router-as-bastion in a chain still
got killed when reached via the SFTP file panel or a port-forwarding
tunnel, even though the user had toggled per-host override.
Plumbing:
- useSftpHostCredentials / buildSftpHostCredentials: accept optional
terminalSettings; call resolveHostKeepalive() for the target and
each jump entry; emit keepaliveInterval / keepaliveCountMax in the
returned NetcattySSHOptions.
- useSftpConnections + useSftpState + SftpStateOptions thread the
setting down. SftpSidePanel passes the global terminalSettings prop
it already has from TerminalLayer.
- portForwardingService.startPortForward: accepts terminalSettings
as an 8th argument, resolves per-host (target + each jump), and
populates the bridge payload.
- usePortForwardingState.startTunnel and usePortForwardingAutoStart
forward the new parameter; App.tsx supplies terminalSettings (via
a ref in the once-on-launch auto-start effect so changing global
keepalive later doesn't re-fire it).
Bridges:
- sftpBridge.cjs target connect: now also reads keepaliveCountMax
from options (was hardcoded 3). 10s/3 stays as the bridge-level
fallback to preserve the #669 protection when the renderer hasn't
supplied a value.
- sftpBridge.cjs jump hop: reads jump.keepaliveInterval /
jump.keepaliveCountMax, then falls back to the target-call options
(matches the symmetric SSH bridge change).
- portForwardingBridge.cjs: reads keepaliveInterval /
keepaliveCountMax from the IPC payload; same 10s/3 fallback.
Types:
- NetcattyJumpHost already grew keepalive fields earlier; this
commit also adds them to PortForwardOptions so the IPC contract
is explicit.
End-to-end: a chain `[router-as-bastion, cloud-host]` with the
router host's keepaliveOverride=true / interval=0 now correctly
disables keepalive on the router hop for terminal SSH AND SFTP AND
port forwarding, while the cloud target still gets the resolved
30s/10 default for each path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ssh): honor explicit keepalive=0 in SFTP + port forwarding bridges
Addresses codex review on PR #947:
- https://github.com/binaricat/Netcatty/pull/947#discussion_r3217448xxx
- https://github.com/binaricat/Netcatty/pull/947#discussion_r3217449xxx
The previous follow-up commit (5c8bc923) plumbed per-host keepalive
into SFTP / port forwarding but kept the existing bridge-level
"if interval > 0 use it, else 10s" fallback. That collapsed two
semantically distinct inputs:
- "user explicitly resolved interval = 0" (host with keepaliveOverride
+ interval=0; the whole point of the override)
- "no value supplied at all" (legacy serializer)
Both ended up as 10s in the bridge, so a router-as-bastion / direct
router connection through SFTP or a port-forward tunnel still got
ssh2-killed after countMax unanswered probes — exactly the case
per-host override was supposed to fix.
Fix: bridges now distinguish on `== null`:
- positive value → honor it
- explicit 0 → truly disabled (0 ms, 0 countMax — ssh2 skips its
dead-connection check entirely on this connection)
- undefined / null → fall back to 10s/3 (preserves #669 idle-NAT
protection for older callers that pre-date per-host plumbing)
Applies to both SFTP target connect and SFTP jump hop builders, plus
the port forwarding target builder. Terminal SSH bridge is unchanged
since it already treated 0 as disabled.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ssh): plumb terminalSettings to all remaining keepalive call sites
Addresses codex review on PR #947:
- PortForwardingNew + TrayPanel were not passing terminalSettings into
startTunnel, so tunnels started from the main port-forwarding UI or
from the tray menu silently used the FALLBACK 30/10 instead of the
user's actual global keepalive settings. Hosts inheriting global
policy could see different behavior depending on the entry point.
- SftpView was not threading terminalSettings into useSftpState, so
SFTP connections opened from the main tab UI also fell back to the
same hardcoded default and ignored the user's settings.
Wiring:
- PortForwardingProps gains `terminalSettings`; VaultView accepts it
on the same prop and forwards from its own new prop; App.tsx
supplies it from useSettingsState. The startTunnel call site uses
it directly and includes it in the useCallback dep list so the
handler updates when settings change.
- SftpViewProps gains `terminalSettings`; SftpViewMount accepts and
forwards it; the sftpOptions memo includes it in its dep list.
- TrayPanelContent gains a `terminalSettings` prop; the TrayPanel
wrapper (which already calls useSettingsState for uiLanguage)
passes it down so the standalone tray window agrees with the main
window's settings.
Also updates the explicit `startTunnel` signature in
UsePortForwardingStateResult so callers see the new 8th parameter
through the hook's return type, not just through the implementation.
Net result: every place that starts an SSH-derived connection
(terminal session, SFTP browse, port-forward tunnel) now consistently
sees the user's configured global keepalive policy and any per-host
overrides; the FALLBACK_KEEPALIVE constants in the service /
credentials builder are now only reached by genuinely-decoupled call
sites (tests, headless usage) rather than masking missing wiring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ssh): include terminalSettings keepalive fields in memo comparators
Addresses codex review on PR #947 — all three components that grew a
`terminalSettings` prop (SftpView, SftpSidePanel, VaultView) are wrapped
in React.memo with manual equality comparators, and none of those
comparators were updated to include the new prop. React would skip the
re-render when global keepalive changed, so new SFTP / port-forwarding
connections from those subtrees would silently keep using the old
keepalive policy until some other tracked prop happened to flip.
Each comparator now compares the keepalive fields directly rather than
the whole terminalSettings object — only those two fields drive
connection resolution in this subtree, and ignoring the rest avoids
unnecessary re-renders for unrelated terminal-setting changes (fonts,
themes, etc.) that already have their own targeted comparator entries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a TUI app enables SGR mouse tracking (opencode, tmux with
`mouse on`, vim with `set mouse=a`, etc.), Terminal.tsx attaches a
capture-phase contextmenu listener that calls
stopImmediatePropagation. The original purpose is to bypass xterm.js's
own right-click handler — which calls textarea.select() and dismisses
TUI popup menus — but stopImmediatePropagation also kills the bubble
that React's onContextMenu delegation relies on, so
TerminalContextMenu's handleRightClick never fires.
Result: with `rightClickBehavior` set to "paste" (or "select-word"),
right-click silently does nothing inside any mouse-tracking TUI. Menu
mode still works because Radix opens via pointerdown (not affected by
the contextmenu capture block). Middle-click paste works because its
auxclick listener in createXTermRuntime is also unrelated to
contextmenu.
Fix: have the capture handler itself dispatch the user's chosen
right-click action when it intercepts the event. terminalContextActions
already exposes onPaste / onSelectWord; mirror them into a ref so the
once-bound capture handler can call the current implementation
without re-binding on every action identity change.
'context-menu' mode is intentionally not handled in the capture path —
Radix's pointerdown listener opens the menu independently.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(fonts): add CJK font pairing composition module
Introduces composeFontFamilyStack() which builds the xterm fontFamily
CSS string at runtime from:
- the user's primary Latin font
- an explicit CJK font (TerminalSettings.fallbackFont) if set
- otherwise a per-Latin-font recommended CJK pairing
- a hardcoded system CJK fallback stack
- a Nerd Font icon fallback stack
- the universal monospace generic
14 unit tests cover composition order, deduplication, OS defaults,
quoting, and recommendation override behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(fonts): expose raw Latin families and add CJK-coverage entries
- TERMINAL_FONTS[].family no longer bakes in the CJK fallback stack;
composition is deferred to runtime via composeFontFamilyStack().
- Drops withCjkFallback helper from this module and its caller in
lib/localFonts.ts.
- Adds 6 CJK-coverage primary fonts to the dropdown: Sarasa Mono SC/TC,
Maple Mono CN, LXGW WenKai Mono, Microsoft YaHei UI, PingFang SC.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(terminal): compose font-family stack with user-configurable CJK fallback
resolvedFontFamily now passes through composeFontFamilyStack(), which
prepends the user's TerminalSettings.fallbackFont (if set) ahead of the
per-Latin-font recommended CJK pairing and the system fallback stack.
The platform argument is derived from navigator.platform inside the
useMemo, so the same Latin font may pair with PingFang SC on macOS and
Microsoft YaHei UI on Windows out of the box.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(settings): add CJK font picker to terminal settings
Adds a new "CJK font" select row right under the main font selector in
the Terminal settings tab. Bound to TerminalSettings.fallbackFont (an
already-existing-but-unused field), so this needs no schema or sync
payload change.
Default value "Auto" leaves fallbackFont empty, which lets the new
per-Latin-font pairing in cjkFonts.ts pick a CJK font automatically.
Selecting any explicit option (Sarasa Mono SC, PingFang SC, Microsoft
YaHei UI, etc.) takes precedence over the per-font pairing.
Includes en + zh-CN i18n strings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(sync): cover fallbackFont round-trip + legacy payload tolerance
Four new test cases verify cloud-sync compatibility for the new CJK
font setting:
- buildSyncPayload includes fallbackFont when set
- buildSyncPayload omits fallbackFont when unset
- applySyncPayload writes incoming fallbackFont to TERM_SETTINGS
- applySyncPayload from a legacy client (no fallbackFont) does NOT
wipe the local value — critical for old-to-new upgrades
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(fonts): add font availability detection (canvas + document.fonts API)
Three-layer detection used by isFontInstalled(family):
1. Known @fontsource-bundled families (e.g. JetBrains Mono) always
count as installed.
2. document.fonts.check() — picks up @font-face and system-loaded fonts.
3. Canvas width measurement against serif / sans-serif / monospace
fallbacks; only counts if the target font produces a width that
differs from ALL three generics for a probe string.
detectInstalledWithContext is a pure function taking an injected
measurement context, which keeps the canvas / DOM behind a seam and
lets the logic be unit-tested without a browser. 11 tests cover
quoted-family parsing, the three-generic-fallback rule, bundled
short-circuit, and document.fonts.check fast-path.
Results are cached per process; clearFontAvailabilityCache() invalidates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(fonts): filter dropdowns to fonts actually installed on this machine
Layer 3 of #931 added Sarasa Mono SC / Maple Mono CN / Microsoft YaHei UI
/ PingFang SC etc. to the terminal font dropdown, but users who don't
have these installed would still see them and pick them — resulting in
"I changed the font and nothing happened" confusion.
This commit filters both dropdowns through isFontInstalled():
- TerminalFontSelect: drops any built-in or system-discovered font
that detection can't render. If filtering would leave fewer than 4
fonts (detection misfire safety net), shows the full list.
- TerminalCjkFontSelect: keeps the "Auto" sentinel always, drops
concrete CJK choices that aren't present on this machine.
Both selects always keep the currently-selected value visible — even
when the underlying font is missing — so users can read and clear
their setting without surprise.
Also expands `npm test` globs to pick up infrastructure/config/*.test.ts
and lib/*.test.ts, which previously matched no patterns and meant the
new cjkFonts and fontAvailability suites were silently excluded from
CI runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(fonts): never recommend proportional CJK fonts for terminal use
The previous PingFang SC / Microsoft YaHei UI / Hiragino Sans GB choices
were proportional sans-serif fonts whose CJK glyphs aren't designed to
fit a terminal's 2x cell grid — the rendered Chinese ended up visibly
wider than its allocated cells, breaking grid alignment (reported on
macOS with PingFang SC selected as the CJK font).
Changes:
- TerminalCjkFontSelect: drops PingFang SC / Microsoft YaHei UI /
Hiragino Sans GB from the dropdown. Legacy explicit selections
still surface as a synthetic "not recommended" option so users can
see and re-pick.
- CJK_SYSTEM_FALLBACK_FONTS: monospace-only list. Sarasa Mono SC/TC,
Maple Mono CN, LXGW WenKai Mono, Noto Sans Mono CJK SC, Source Han
Mono SC, NSimSun, SimSun. Proportional fonts removed.
- PER_FONT_CJK_PAIRING: every entry now points at a true monospace
CJK font. Cascadia / Consolas / Menlo etc. all recommend Sarasa
Mono SC, which the next commit bundles via @font-face.
- getDefaultCjkFallback: Windows = SimSun (always installed,
monospace); macOS = Sarasa Mono SC (will be bundled); Linux =
Noto Sans Mono CJK SC. A regression test enforces that no
per-OS default is a known proportional font.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(fonts): bundle Sarasa Mono SC as the universal CJK monospace
Previous commit removed proportional CJK fonts (PingFang SC, etc.)
from the picker and switched per-OS defaults to true monospace, but
macOS ships NO system-installed monospace CJK font — leaving macOS
users with a broken default unless they manually install Sarasa or
similar. This commit closes that gap by bundling Sarasa Mono SC as
an @font-face webfont, so the recommended pairings and macOS default
"just work" out of the box.
Details:
- public/fonts/SarasaMonoSC-Regular.woff2 (~4.8 MB): subsetted from
be5invis/Sarasa-Gothic v1.0.37 SarasaMonoSC-Regular.ttf (24 MB).
Covers ASCII, Latin-1, common punctuation/symbols, CJK Unified
Ideographs main block, Hiragana/Katakana, halfwidth/fullwidth,
box-drawing — the everyday-Chinese coverage that matters for a
terminal. Rare CJK Ext-A/B/historical chars fall through to the
system fallback stack.
- public/fonts/SarasaMono-LICENSE.txt: OFL-1.1 verbatim, required
by the license.
- index.css: @font-face declaration with font-display: swap so the
user doesn't see a flash of nothing while the woff2 loads.
- KNOWN_BUNDLED_FAMILIES: "Sarasa Mono SC" added so the dropdown
availability filter doesn't hide it.
Installer impact: ~+4.8 MB (vs current ~100-200 MB Electron baseline).
The font replaces what would otherwise have been "Chinese chars look
broken in the terminal" for every macOS user without a manually
installed CJK monospace font.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(fonts): use Local Font Access API as the authoritative install check
document.fonts.check() turned out to be unreliable as an installed-font
signal in Chromium — it returns true for any syntactically-valid family
name regardless of whether the font is actually installed, as a
deliberate fingerprinting-mitigation. The previous detector took it as
a positive signal and ended up keeping uninstalled fonts in the dropdown
(reported by a macOS user seeing dozens of fonts they don't have).
This commit pivots the detection chain:
- lib/localFonts.ts: getAllSystemFontFamilies() exposes the unfiltered
set of installed family names from queryLocalFonts(), reusing the
same underlying call as getMonospaceFonts() via a shared cache.
- lib/fontAvailability.ts: drops the document.fonts.check fast-path.
Adds setSystemFamilies() / hasAuthoritativeData(). When the set has
been populated, isFontInstalled answers from membership lookup
directly — no canvas guessing. Canvas remains as a fallback for
environments where the Local Font Access API is unavailable or
permission is denied.
- application/state/fontStore.ts: during initialize(), runs the
monospace-only query and the full-system-families query together,
then pipes the result into fontAvailability.
- TerminalFontSelect: with authoritative data, drops the "if filtered
list is suspiciously small, show all" safety net. Empty would now
really mean empty (highly unlikely since Sarasa Mono SC is bundled).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(fonts): drop PingFang SC / Microsoft YaHei UI from primary dropdown
Step 1 of this PR removed proportional CJK fonts from the CJK fallback
picker but left them in BASE_TERMINAL_FONTS, so PingFang SC and
Microsoft YaHei UI were still selectable as the *primary* terminal
font. Picking PingFang SC as primary produced visibly bloated Latin
character spacing (xterm.js samples cell width from the primary font;
the wide proportional 'M' inflates every cell), reported by a macOS
user in the same thread that opened #931.
Both entries are removed from BASE_TERMINAL_FONTS. A new
infrastructure/config/fonts.test.ts asserts that no known proportional
CJK font name (including PingFang TC/HK, Microsoft YaHei variants,
Hiragino Sans GB, Heiti SC/TC) is ever shipped in TERMINAL_FONTS as a
primary choice.
Migration for users already saved to one of the removed ids:
useSettingsState rewrites STORAGE_KEY_TERM_FONT_FAMILY to the default
(Menlo) on read when it sees a deprecated id, so the bad value also
stops getting carried into cloud-sync uploads. Per-host fontFamily
overrides are NOT migrated automatically — they still gracefully
fall through to the dropdown's first entry via the existing
getFontById fallback; users can re-pick from the host settings UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(fonts): drop Comic Sans MS — it's a proportional handwriting font
Same symptom as the PingFang SC / Microsoft YaHei UI removal: Comic
Sans MS was historically in the primary font dropdown labeled
"Casual, non-traditional terminal font", but Comic Sans is a
handwriting-style proportional sans-serif. Picking it as the terminal
primary inflates cell width and spaces every Latin character far
apart (reported in the same #931 thread).
- BASE_TERMINAL_FONTS: comic-sans-ms entry removed.
- DEPRECATED_PRIMARY_FONT_IDS: gains comic-sans-ms so existing
selections silently migrate to Menlo on read.
- fonts.test.ts: the proportional-font ban list now also covers
Latin proportional fonts (Comic Sans MS, Arial, Helvetica, Times
New Roman, Georgia, Verdana, Trebuchet MS, Tahoma) so the test
catches any future mislabeled body-text font from being added to
the terminal dropdown.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(fonts): keep monospace ahead of CJK fallbacks in composed stack
Addresses codex P1 review comment on PR #940
(https://github.com/binaricat/Netcatty/pull/940#discussion_r3216017737).
The previous behavior of withCjkFallback() had monospace immediately
after the primary family, before any CJK fallback. composeFontFamilyStack
had moved monospace to the very end, which means: when the primary
font isn't installed on the user's machine (common for Layer 3 CJK
choices that aren't bundled and not present on a given OS, or for any
built-in id like cascadia-code on a Linux system without it), CSS
per-glyph fallback resolves Latin glyphs from a CJK font's full-width
Latin variants before ever reaching monospace generic. That breaks
xterm.js's fixed cell-grid alignment.
The composed stack now reads:
<primary>, monospace, <userFallback>, <recommended-cjk>,
<system-cjk-stack>, <nerd-font-stack>
Per-glyph CSS fallback behavior:
- Latin → primary if installed → monospace generic. Cell width
stays consistent.
- CJK → primary (no) → monospace (no Chinese glyphs) → walks into
CJK fallbacks.
- Nerd PUA → falls past all of the above into the Nerd Font stack.
Updates the position-invariant tests and adds a regression test that
explicitly asserts monospace appears before every CJK family in the
output stack.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(fonts): dedupe Local Font Access API calls under concurrent init
Addresses codex P2 review on PR #940:
https://github.com/binaricat/Netcatty/pull/940#discussion_r3216246xxx
fontStore.initialize() runs getMonospaceFonts() and
getAllSystemFontFamilies() in Promise.all; both internally called
queryAllSystemFontsOnce(), whose cache check (`if (cache) return`) was
only useful once the result had been written. Concurrent callers both
passed the empty-cache check and fired their own queryLocalFonts()
request — two real Local Font Access API invocations on cold start,
with the risk of one succeeding while the other was denied (leaving
the authoritative set unset).
Fix: cache the *in-flight promise itself*, so subsequent callers
await the same single invocation. The first await populates the
family-set cache as a side effect, and the resolved promise keeps
returning the same value to every subsequent caller.
Adds lib/localFonts.test.ts with three regression tests:
- concurrent getMonospaceFonts + getAllSystemFontFamilies = 1 API call
- sequential repeats also reuse the resolved promise
- missing API returns null authoritative set (canvas fallback signal)
Exports __resetLocalFontsCacheForTesting() so each test gets a fresh
module-level state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(fonts): retry LFA on transient failure + notify on availability changes
Two follow-up fixes from codex P2 review on PR #940:
1) queryAllSystemFontsOnce() previously kept its in-flight promise even
when queryLocalFonts threw. Subsequent callers reused the cached
empty result for the rest of the session, so any transient failure
at boot (permission state not ready, AbortError, etc.) permanently
blinded the rest of the app to installed fonts. Catch now clears
queryPromise so the next caller retries. Regression test added.
2) TerminalCjkFontSelect.visibleOptions and TerminalFontSelect
.visibleFonts were memoized on [value] / [fonts, value] only, but
the filter calls isFontInstalled() which reads module-level
systemFamilies — a value that arrives asynchronously after the
initial render. The memos never recomputed when authoritative
availability data landed, so the dropdowns could continue showing
stale "filtered" results until the user changed selection.
fontAvailability now exposes subscribeFontAvailability() and
getFontAvailabilityVersion() (monotonic counter bumped on
setSystemFamilies / clearFontAvailabilityCache). Both selects
subscribe via useSyncExternalStore and include the version in
their memo deps; tests cover subscriber notification and version
monotonicity.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(fonts): migrate host/group deprecated font ids + localize CJK labels
Two follow-up fixes from codex review on PR #940:
P2 — Host/group level font migration
====================================
The earlier deprecated-id migration only rewrote
STORAGE_KEY_TERM_FONT_FAMILY, so hosts and group configs that had
explicitly opted into a now-removed font id (e.g. pingfang-sc,
microsoft-yahei, comic-sans-ms) kept `fontFamily` set with
`fontFamilyOverride=true`. After the dropdown entries were dropped
in 9f2bd282/c9b622d8, those records silently fell through to the
first font in the registry (Menlo) while the override flag still
read "true" — users saw a host claiming a custom font but rendering
the global default with no way to tell what happened.
Fix:
- infrastructure/config/fonts.ts gains migrateDeprecatedFontOverride(),
a structurally-shared helper that drops fontFamily and clears
fontFamilyOverride when the id is deprecated.
- sanitizeHost now runs it on every host load.
- domain/groupConfig.ts grows sanitizeGroupConfig(); useVaultState
applies it both on initial load and on cross-tab storage events.
- Existing decrypt → sanitize → encrypt round-trip in useVaultState
means the migrated values are persisted back to localStorage and
propagate through cloud sync naturally.
Tests: two each in domain/host.test.ts and domain/groupConfig.test.ts
covering deprecated-id reset and untouched-valid-id preservation.
P3 — Localize CJK font option labels
====================================
TerminalCjkFontSelect previously hardcoded Chinese option labels
("Auto · 按主字体智能搭配", "Sarasa Mono SC (更纱黑体 简)", etc.) and
the synthetic "not recommended" warning. Non-Chinese locales saw a
mixed-language UI despite the rest of the setting going through i18n.
OPTIONS now references i18n keys; the component looks them up via
useI18n(). Both en and zh-CN locales gain matching keys, including
`...option.legacy` with `{font}` interpolation for the synthetic
"not recommended" item that surfaces saved-but-removed values.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(fonts): also sanitize group configs on the write/import path
Addresses codex P2 review on PR #940:
https://github.com/binaricat/Netcatty/pull/940#discussion_r3216314xxx
The previous commit (09c87820) added sanitizeGroupConfig() but only
plumbed it into the decrypt paths (initial load + storage event).
updateGroupConfigs() — which is also the write path used by
applySyncPayload / importVaultData when ingesting a legacy payload —
still set state from raw input. A sync from an older client carrying
{ fontFamily: "pingfang-sc", fontFamilyOverride: true } would land in
memory unsanitized AND be re-persisted with the bad override active
until the next reload re-ran the decrypt path.
Fix mirrors updateHosts → sanitizeHost: map every incoming entry
through sanitizeGroupConfig before both setGroupConfigs and the
encrypt-and-persist step. Same call site now feeds the cleaned data
to localStorage, so legacy values are scrubbed on first import.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(fonts): migrate deprecated terminal font ids on every ingest path
Addresses codex P2 review on PR #940:
https://github.com/binaricat/Netcatty/pull/940#discussion_r3216517xxx
The previous migration only ran in the initial useState() initializer
for terminalFontFamilyId, so deprecated ids (pingfang-sc /
microsoft-yahei / comic-sans-ms) could still re-enter state via:
- rehydrateAllFromStorage() at line ~527 — runs on remote-import
completion and re-reads STORAGE_KEY_TERM_FONT_FAMILY raw.
- The notifySettingsChanged IPC handler at line ~663 — fires when a
cloud sync or programmatic localStorage write announces a change.
- The cross-window storage event handler at line ~873.
Any of these paths could pull a deprecated id back into state after
the initial migration ran, leaving the font selector with no matching
option and silently rendering the global default while continuing to
propagate the stale value through subsequent sync uploads.
Centralizes the migration in migrateIncomingTerminalFontId(raw):
- returns null when raw is empty
- if raw is deprecated, writes DEFAULT_FONT_FAMILY back to
localStorage AND returns it
- otherwise returns raw unchanged
All four ingest sites (initial init, rehydrate, IPC, storage event)
now route through this helper. The rewrite-on-deprecated semantics
also guarantee that the moment any path sees a bad value, the next
sync upload carries the cleaned default — not the deprecated id.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(fonts): use bundled Latin-only fallback instead of monospace generic
Resolves the tension between codex's two P1 reviews on PR #940:
Round 1 (da1fe4cd): "monospace must come BEFORE CJK fallbacks" —
otherwise Latin glyphs fall into a CJK font's full-width Latin
when the primary font is missing.
Round 2 (this commit): "monospace must come AFTER CJK fallbacks" —
otherwise on macOS Chrome, the generic `monospace` pulls in
PingFang via Chromium's CJK system fallback and silently masks
the user's CJK picker.
Both are right; using a single `monospace` token can't satisfy both
roles because `monospace` is a generic family whose CJK-glyph
coverage is platform-dependent.
Fix mirrors Tabby's approach (their "monospace-fallback" SourceCodePro
sitting before any CJK in the chain): insert a known Latin-only
bundled font between the primary and CJK fallbacks. JetBrains Mono is
already shipped via @fontsource/jetbrains-mono and carries no CJK
glyphs, so it catches Latin without intercepting Chinese.
New stack order:
<primary>, "JetBrains Mono", <userFallback>, <recommended-cjk>,
<system-cjk-stack>, <nerd-font-stack>, monospace
Per-glyph CSS fallback now behaves as intended on every platform:
- Latin: primary (if installed) → JetBrains Mono. Cells stay aligned.
- CJK: primary (no) → JetBrains Mono (no CJK glyphs) → user CJK pick.
- Nerd PUA: all of the above → Nerd Font stack.
Replaces the two prior positional-invariant tests with one for each
codex review concern: JetBrains Mono precedes every CJK family
(Latin alignment), and user CJK precedes generic monospace (CJK
picker effectiveness).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(fonts): use OR-of-fallbacks for canvas font detection
Addresses codex P2 review on PR #940:
https://github.com/binaricat/Netcatty/pull/940#discussion_r3216556xxx
detectInstalledWithContext required the target font to produce a
different rendered width from *all three* generic fallbacks (serif,
sans-serif, monospace) to be counted as installed. That's too strict:
on macOS the `monospace` generic resolves to Menlo itself, so
measure(`"Menlo", monospace`) === measure(`monospace`), and the
detector reported Menlo as missing even when it was clearly installed.
The same false-negative trap exists for any font that happens to
share metrics with one of the three generics on a given platform.
Switches to OR-of-fallbacks: a font counts as installed if its
rendered width differs from at least one generic baseline. A truly
uninstalled font still falls through to each generic in turn and
matches all three baselines, so this doesn't introduce false positives.
Regression tests added for both directions:
- Menlo with metrics identical to `monospace` generic → installed.
- "Definitely Not Installed" font → still reported missing.
The path only fires when the Local Font Access API is unavailable or
denied — when LFA succeeds, `setSystemFamilies` short-circuits ahead of
canvas — so this primarily improves the degraded-permission scenario.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(fonts): quote-aware tokenizer for font-family lists
Addresses codex P2 review on PR #940:
https://github.com/binaricat/Netcatty/pull/940#discussion_r3216559xxx
composeFontFamilyStack and extractPrimaryFamily both tokenized their
input with a raw String.split(',') — which corrupts any CSS family
list whose quoted family name contains a comma (CSS allows that, e.g.
`"Foo, Inc. Mono"` is a single family). A naive split would shred
that into `"Foo` / `Inc. Mono"` and emit a malformed font-family back
out.
No current TERMINAL_FONTS entry hits this case, but lib/localFonts.ts
builds family strings from arbitrary system fonts via the Local Font
Access API — a user with a comma-bearing family name would have
silently broken filtering until now.
Adds splitFontFamilyList(css) in cjkFonts.ts: an exported quote-aware
tokenizer that splits on commas only when outside quoted segments
(handles both " and '). composeFontFamilyStack uses it instead of raw
split; extractPrimaryFamily in lib/fontAvailability.ts imports it for
symmetry so the two call sites can't drift.
Tests cover the tokenizer directly (simple list, quoted-with-comma,
single quotes, double commas) and end-to-end (a quoted primary with
an internal comma survives composition intact).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(fonts): translate Layer 3 CJK font descriptions to English
The 4 CJK-coverage entries added in earlier commits (Sarasa Mono SC,
Sarasa Mono TC, Maple Mono CN, LXGW WenKai Mono) had hardcoded Chinese
description strings, while every other TERMINAL_FONTS entry uses
English ('Adobe's professional programming font', 'Iosevka variant
mimicking Berkeley Mono style', etc.). The dropdown rendered a
mixed-language list — flagged by the maintainer.
Converted the 4 descriptions to English in the same style as the
existing entries. No i18n scaffolding added; the existing convention
is "English-only `description` field, not routed through t()", and
the rest of the registry stays consistent with that.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The line printed once per terminal session and offered no diagnostic
value beyond what window.__xtermRenderer already exposes for ad-hoc
introspection. Keep the detection + retry + window publish; just
stop polluting the console. Rename logRenderer → trackRenderer to
match the now-narrowed responsibility.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hook returned a fresh object literal every render. The 26 methods
inside were already useCallback([])-stable, but the wrapping object
was not — so every consumer's effect with `terminalBackend` in deps
(e.g. cwd polling, lifecycle wiring, write-to-session) re-ran on
every parent render even though nothing semantic had changed, and
ESLint flagged the one site that depended on a property access
(`terminalBackend.onHostKeyVerification`) because it could not prove
that path safe.
Wrap the return in useMemo with all stable callbacks listed as deps
so the object is computed once and cached for the hook's lifetime.
Switch the host-key-verification effect's dep to the now-stable
`terminalBackend`, clearing the warning at the root rather than
patching it locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The reliability gate at handleInput's adjustToInput call froze the
ghost at its last show()-time tail in any path where the typed buffer
becomes unreliable (Tab pass-through to shell, history recall, cursor
moves). When the user kept typing into that gap, the next render
advanced the cursor past the ghost's anchor while the ghost text
stayed put — a → -accept then pasted the stale tail on top of the
just-typed glyphs (e.g. "systemctl s" + typing "t" → screen showed
"systemctl sttop firewalld").
Add GhostTextAddon.applyKeystroke so the ghost can evolve its own
currentInput off raw keystrokes (printable / Backspace / Ctrl-W),
seeded by whatever the last show() captured from the live xterm
reading. handleInput now uses the existing adjustToInput on the
reliable path (preserves multi-char paste re-alignment) and routes
single-keystroke events through applyKeystroke on the unreliable
path, fixing the visual misalignment and the duplication-on-accept
in one shot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ExternalAgentConfig.command/acpCommand/args/env are OS- and
machine-specific (binary paths, .exe suffixes, platform-dependent
environment values). Pushing them to other devices either fails to
resolve or silently runs the wrong thing.
Stop collecting/applying STORAGE_KEY_AI_EXTERNAL_AGENTS and remove the
field from the SyncPayload type. apply silently ignores the field on
legacy snapshots that still carry it, so existing remote data is safe.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`collectSyncableSettings` strips device-bound encrypted apiKeys from
provider entries and webSearchConfig before upload, but
`applySyncableSettings` was writing them back wholesale, silently wiping
local credentials whenever any other setting changed on a second device.
Merge by id (providers) and by providerId (web search) so a synced
payload only overrides the apiKey when it explicitly carries one.
Also include `application/*.test.ts` in the npm test glob so the
syncPayload tests added in this PR actually run in CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The upstream `webdav` package builds the `Authorization: Basic …` header
through `base-64`, which Latin1-encodes the credentials. RFC 7617 (and
servers that follow it, like Hetzner Storage Box) expect UTF-8, so any
non-ASCII character in the password (e.g. `ö`, `ä`) produces a different
byte sequence on the wire than what the server stored, and the request
gets a 401 even though the credentials are correct (#891).
Skip the upstream auth path for password mode and pass an Authorization
header we built ourselves with UTF-8 encoding. ASCII-only passwords are
byte-identical, so existing setups are unaffected. Digest and token
modes are untouched.
Tested with a local HTTP server that enforces UTF-8-encoded Basic Auth
for a password containing umlauts (the exact failing case from #891).
Extend cloud sync to cover AI provider config, external agents,
permission/tool modes, command policy, web search settings,
workspace focus style, terminal follow-app theme, SFTP default view,
and additional terminal options. Device-bound encrypted apiKey
placeholders are stripped from providers and webSearchConfig before
upload. Auto-sync now reacts to syncable localStorage changes via a
new adapter-level event.
Center the Settings window on the display of the window that opened
it instead of always using the main window, fixing issue #920.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The SFTP file-list "Upload File(s)" context menu items only make sense
on remote panes — local panes have no upload semantic. Plumb a new
`isLocal` prop into SftpPaneFileList and suppress both the menu items
and the hidden file inputs when the active pane is local.
Also add an "Upload Folder..." item alongside "Upload File(s)..." that
opens a `<input type="file" webkitdirectory>` picker. The resulting
FileList is routed through a new `uploadExternalFolder` /
`onUploadExternalFolder` callback that calls `uploadFromFileList`, so
folder structure is preserved via webkitRelativePath without any new
IPC. When invoked from a directory row, the folder is uploaded INTO
that directory (matching drag-and-drop semantics).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes#916.
When the user clicks "Restart" after a session disconnects, the
renderer reuses the same sessionId and the bridges call startStream
again to open a fresh log file for the new connection. The previous
connection's close handlers (e.g. SSH conn.once('close'),
stream.on('close'), serial 'close', telnet 'close', mosh PTY exit)
all still fire asynchronously and call stopStream(sessionId)
unconditionally. If they land after the new stream is already
active, they silently destroy it and subsequent terminal output for
the reconnected session is dropped, matching the bug report where
the first connection's IO is saved but the reconnect's is not.
Make startStream return a unique token and require stopStream
callers to pass it. A stale stop call carrying the previous
incarnation's token is now a no-op, so a late close handler from
the previous connection cannot kill the freshly-started stream.
Each reconnect therefore produces its own timestamped log file,
which mirrors the existing auto-save-on-close semantics and is the
simpler of the two options the issue offered.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Provides a discoverable entry point to the Settings panel for users
who don't use the Cmd/Ctrl+, hotkey. Sits at the right edge of the
title bar on macOS and immediately to the left of the custom window
controls on Windows/Linux. Reuses the existing onOpenSettings prop
already wired through from App.tsx.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Right-click on an SFTP pane now offers an "Upload File(s)" menu item
that opens a native multi-file picker, so users no longer have to drag
and drop to upload (issue #915). Selected files are wrapped in a
DataTransfer and dispatched through the existing onUploadExternalFiles
pipeline; right-clicking a directory uploads into that folder. Folder
upload via the picker is intentionally out of scope.
Fixes#915
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Cmd+, on macOS and Ctrl+, on Windows/Linux to open Settings,
matching the platform convention. Previously Settings was only
reachable via Vaults -> Settings (#912).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Keep file-backed SSH keys intact across app restarts and keep bad key passphrases in the dedicated retry flow instead of falling back to generic SSH auth. Also clear invalid saved passphrases from both legacy storage and reference-key records after auth failures.
macOS Terminal/iTerm export LC_CTYPE=UTF-8 (a bare value, not a real
locale name). The system ssh_config has SendEnv LC_*, so the value
leaks to the remote and bash warns "cannot change locale (UTF-8)" on
every login. mosh-server sets its own locale separately, so dropping
LC_* from the spawned ssh's env is the cleanest fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a stateful terminal log sanitizer for txt/html session logs so saved output handles backspace, carriage-return overwrites, erase controls, split CSI/OSC sequences, and ANSI styling without leaking terminal control bytes.
Stream txt/html logs through a persistent renderer and write rendered snapshots directly to the final file, avoiding raw temp files and redundant full rewrites.
Preserve prior log history across clear-screen transitions while coalescing TUI repaint loops to avoid stale frame growth.
Add regression coverage for tmux/zellij-style clears, repeated ED2/ED3 clears, home-clear repaint loops, and shell clear behavior.
* feat: add SFTP upload conflict handling
Add conflict resolution for SFTP uploads so files and folders can be stopped, skipped, replaced, duplicated, or merged depending on the target state. Support batch uploads with Apply to All behavior, route external upload conflicts through the shared SFTP conflict dialog, and add the bridge operations needed to stat and delete existing upload targets.
* fix review issue
* Fix SFTP conflict cancellation cleanup
---------
Co-authored-by: yuzifu <yuzifu@TB16PGen5.Info>
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
* fix(autocomplete): recognize Nerd Font / Powerline glyphs as prompt terminators
oh-my-posh and similar themed prompts end with PUA codepoints (e.g. U+F105
chevron, U+E0B0 powerline arrow) that aren't in the hardcoded PROMPT_CHARS
set, so findPromptBoundary returned -1 and both ghost-text and popup
autocomplete went silent. Treat any Private Use Area char (U+E000-U+F8FF)
followed by a space as a candidate prompt terminator — real shell commands
essentially never contain PUA codepoints, so this is high-confidence.
* Fix Powerline glyph prompt splitting
---------
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
* Run CI on every push/PR; gate release on strict v<X>.<Y>.<Z> tags
The build-packages workflow used to trigger only on `push: tags: v*`,
so branches and PRs never built and the only way to test the matrix
was to push a tag — which also auto-published a GitHub Release. That
made it impossible to verify a CI change without either skipping
testing or shipping a junk release.
Restructure the triggers:
- `push: branches: ['**']` + `pull_request` so any push or PR runs
the build matrix and uploads workflow artifacts.
- `push: tags` accepts only strict semver: `v<MAJOR>.<MINOR>.<PATCH>`
with an optional pre-release suffix like `v1.2.3-rc.1`. Loose tags
(`v-test`, `vNEXT`, `v1.0`) no longer match.
- The release job's `if:` enforces the same rule independently — even
if someone re-broadens the trigger later, branches and PRs can't
publish a release.
- `Set version` produces semver-compliant `0.0.0-sha.<short>` for
non-tag runs so `npm pkg set` / electron-builder don't choke on a
bare commit SHA like `abc1234`.
- Add a concurrency group that cancels superseded branch/PR builds
to save runner minutes; tag builds use a unique group so releases
never get cancelled by a follow-up commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Apply strict-semver Set-version step to Linux jobs too
The previous commit only patched the matrix job's Set version step
(macOS/Windows) because the Linux legs had a slightly different
template (no comments). The Linux Set version step kept setting
package.json's version to a bare 7-char commit SHA like "812f296",
which electron-builder rejects with `Invalid version: "812f296"`
during normalizePackageData.
Replicate the same strict regex + 0.0.0-sha.<short> fallback in both
Linux jobs so non-tag runs produce a valid semver across the matrix.
Reproduced from build-linux-x64 logs of the run on 112bf3a1:
Setting version to 812f296
⨯ Invalid version: "812f296" failedTask=build
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix build workflow trigger review issues
* Address build workflow review findings
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Bundle mosh-client via CI build pipeline
Add a GitHub Actions workflow that builds a static, distro-portable
mosh-client for linux-x64, linux-arm64, darwin-universal (arm64+x86_64)
from upstream mobile-shell/mosh source, plus a pinned win32-x64 binary
sourced from FluentTerminal (GPL-3.0). Releases attach SHA256SUMS so
scripts/fetch-mosh-binaries.cjs can verify and pull the right binary
into resources/mosh/<platform-arch>/ during npm run pack.
electron-builder.config.cjs gains a moshExtraResources() helper that
adds the binary to extraResources only when present on disk, keeping
local dev packages working without bundled mosh.
terminalBridge.cjs now exports bundledMoshClient() and prefers the
bundled static client over whatever the system mosh wrapper would
resolve via PATH (via the MOSH_CLIENT env var). The Windows branch
throws a clear error pointing at Settings instead of silently falling
back to a literal "mosh.exe" string when no wrapper is installed.
This is Phase 1 — Phase 2 (follow-up) replaces the FluentTerminal
Windows binary with an in-CI Cygwin static build and adds a Node-side
mosh-server bootstrap so Mosh works out-of-the-box on Windows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 2: Node-side Mosh handshake (no Perl wrapper required)
Reimplement what the upstream Mosh Perl wrapper does in pure Node:
spawn `ssh [user@]host -- mosh-server new`, sniff the byte stream
for `MOSH CONNECT <port> <key>`, then spawn `mosh-client` locally
with MOSH_KEY in the environment.
The new electron/bridges/moshHandshake.cjs module exposes the parser,
sniffer, and command builders as pure functions so they can be unit
tested without spawning real ssh. terminalBridge.startMoshSession now
prefers this path whenever a bare mosh-client (bundled, explicit, or
system) and ssh (in-box OpenSSH on Win10 1809+, system everywhere
else) are both detectable. The legacy path through the system mosh
Perl wrapper is preserved as a fallback so users with custom mosh
setups don't regress.
Auth is delegated to system ssh, so keys, agent, ssh_config, and
known_hosts all keep working. Password / 2FA need a controlling TTY
which the bootstrap doesn't provide; affected users keep the legacy
wrapper path until interactive UI lands.
Tests:
- moshHandshake.test.cjs (20 tests) — parser corner cases, command
builders, sniffer split-chunk handling, ring-buffer trim, exec
resolver
- terminalBridge.bareMoshClient.test.cjs (4 tests) — explicit-path
basename gating
317 → 341 passing tests; lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 3: in-CI Cygwin Windows build + visible PTY handshake
Phase 3a — in-CI Cygwin Windows build
- scripts/build-mosh/build-windows.sh builds mosh-client.exe from
upstream mobile-shell/mosh source inside Cygwin, then walks the
cygcheck import graph to bundle every required Cygwin DLL
(cygwin1.dll, cygcrypto, cygprotobuf, cygncursesw, etc) into a
tar.gz alongside the exe.
- The `build-mosh-binaries` workflow swaps the FluentTerminal-pinned
fetch job for a real Cygwin build (windows-latest + cygwin-install-
action). fetch-windows.sh is preserved as an emergency fallback but
no longer wired into the matrix.
- fetch-mosh-binaries.cjs unpacks the tar.gz into resources/mosh/
win32-x64/ so mosh-client.exe sits next to its DLLs.
- mosh-extra-resources.cjs ships the entire win32-x64/ dir
(exe + DLL bundle) into Resources/mosh/, so the packaged installer
runs on a stock Windows host with no Cygwin install.
Phase 3b — visible PTY handshake (password / 2FA prompts)
- terminalBridge.startMoshSession now spawns ssh inside node-pty so
the user sees and can answer password / 2FA / known-hosts prompts
in their terminal. When `MOSH CONNECT` is sniffed from the byte
stream, session.proc is atomically swapped from the ssh PTY to a
freshly-spawned mosh-client PTY. The MOSH CONNECT line itself is
redacted from the visible output.
- writeToSession / resizeSession read session.proc lazily, so input
arriving after the swap goes to mosh-client without extra wiring.
- The ZMODEM sentry is recreated for the new proc since its
writeToRemote closure captured the previous handle.
- Removes the earlier non-PTY child_process.spawn handshake — the
PTY-based one supersedes it.
Phase 3c — win32-arm64 deferred
- Cygwin's arm64 port has no stable cygwin1.dll release yet, so we
do not attempt an arm64 Windows build. arm64 Windows installs fall
through to the legacy `mosh` wrapper path that the bridge already
handles. Documented in the workflow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Allow branch/PR pushes to test the mosh-binaries workflow
Mirrors the build-packages workflow change in #868: any push or PR
that touches the mosh build pipeline triggers the matrix (artifacts
only, no release), while only `mosh-bin-*` tag pushes (or an
explicit workflow_dispatch with release_tag) publish a release.
`paths` filter keeps unrelated commits from running this expensive
workflow (~30min for the Cygwin leg). Concurrency group cancels
superseded branch/PR builds; tag builds use a unique group so a
follow-up commit can't kill an in-progress release.
Release job's `if:` enforces the same rule independently — even if
the trigger gets re-broadened, branches/PRs can't leak a release.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix mosh binary workflow runners
* Fix Windows mosh workflow invocation
* Keep shell scripts LF in workflow checkouts
* Trigger mosh workflow on attributes changes
* Fix mosh build tool dependencies
* Fix Linux mosh static build
* Fix macOS mosh build tool lookup
* Skip macOS ncurses terminfo install
* Fix mosh PR review findings
* Allow Linux system mosh dependencies
* Fix Windows mosh DLL bundling
* Limit bundled Windows mosh DLLs
* Honor configured PATH for mosh handshake
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#838.
Adds stable `data-role="user|assistant|system|tool"` attributes plus
`ai-chat-message` / `ai-chat-message-content` classnames on the chat
message rows in Catty Agent's chat panel. Users can now distinguish
their own messages from agent replies via Settings → Appearance →
Custom CSS, e.g.
.ai-chat-message[data-role="user"] .ai-chat-message-content {
background: rgba(91, 124, 250, 0.12);
}
The default theme is intentionally minimal (bordered user bubble,
plain assistant text). Rather than change the default — different
users want different distinctions — this exposes a hook so anyone
can colour the rows however they prefer without forking.
The attribute names are part of the UI's stable contract; a comment
on the Message component flags this for future renames.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Harden the dirty-editor quit guard
Follow-up to #840. Three concrete failure modes that round-2 review
turned up:
1. `webContents.send` is unguarded. If the renderer is destroyed
between the reachability check and the send (e.g. a dying GPU
process), the throw escapes the `before-quit` handler with
`quitGuardChannelBusy = true` already set and no timeout scheduled
yet — the app becomes un-quittable until restart. Wrap the send,
and tear the listener/timer down on failure.
2. The timeout vs. response race silently commits a quit on
`hasDirty=true`. Once `setTimeout` has already enqueued its
callback for the next tick, `clearTimeout` is a no-op and the
timeout callback runs even after the response arrived — which
unconditionally calls `commitQuit()`, overriding the user's
"save first" intent. Funnel both paths through a `settle()` helper
that only acts the first time it's called.
3. The reply listener accepted any sender. A rogue or future-buggy
`webContents` could decide the quit by sending the channel name
first. Validate `evt.sender === wc` and ignore non-matches; switch
from `.once` to `.on` + explicit `removeListener` so a rogue early
reply doesn't consume the listener slot.
Also wrap the renderer-side handler in try/catch so an unexpected
throw inside `editorTabStore.getTabs()` reports `hasDirty=false`
immediately instead of stranding the main process for 5 s on a
silent timeout.
Verify `webContents.isCrashed()` before sending so a known-dead
renderer skips the round-trip and quits instantly instead of waiting
on the timeout fallback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Tighten dirty-editor quit-guard validation
Codex round-2-2 review suggested two small follow-ons:
1. Sender check should reject missing/falsy `evt.sender` outright. In
real Electron IPC the sender is always populated; a falsy sender
is anomalous and treating it as legit defeats the rogue-reply
defence we just added.
2. Wrap `bridge.reportDirtyEditorsResult` in try/catch on the
renderer side. If the IPC bridge is in a bad state and the call
throws, the rest of the listener body is fine but the React
useEffect callback would propagate the error — and an uncaught
error in the listener would silently disable the quit guard for
the rest of the session.
Both are pure tightening; no behaviour change on the happy path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(quit): target main window for dirty-editor check on quit
Use getMainWindow() instead of BrowserWindow.getAllWindows()[0] so the
app:query-dirty-editors round-trip isn't sent to the tray panel or
settings window, and skip the check when the main window is hidden to
avoid the 5s timeout fallback during tray-initiated quit.
* Also gate dirty-editor check on isMinimized for cross-platform robustness
A minimized main window has a taskbar/Dock entry the user can click to
restore, so the dirty-editor toast is still useful even though the
window isn't currently in the foreground. On some platforms isVisible()
can return false for a minimized window (see the comment at
globalShortcutBridge.cjs:478), so the original `!isVisible()`
short-circuit would silently lose dirty-editor protection in that case.
Treat a window as "reachable by the user" when either isVisible() or
isMinimized() is true. Truly hidden windows (close-to-tray, app.hide()
on macOS) still skip the round-trip and quit instantly, which is the
behaviour this PR set out to introduce.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to #851 (Codex review comment on 32bab2d4). After that PR,
`resolveEffectiveShellKind` flips an unknown-shell session to PowerShell
based on `session.lastIdlePrompt`, but that field is updated only when
`trackSessionIdlePrompt` recognizes a known prompt shape (default
PowerShell or `user@host[:path][#$]`). On an SSH/Telnet session that
enters PowerShell and then leaves it for a shell with an unrecognized
prompt — cmd.exe (`C:\>`), oh-my-posh / starship / a custom PS1 — the
cached `PS ...>` value persists indefinitely, and every subsequent MCP
command keeps getting wrapped as PowerShell against a non-PowerShell
shell. The new shell errors on the wrapper syntax once per command, and
nothing self-heals until the user reconnects.
Add `getFreshIdlePrompt(session)` which returns the cached prompt only
when the rolling PTY tail (`session._promptTrackTail`) still ends with
it. If the visible last line has moved on — even to a prompt shape we
don't recognize — the cache is treated as expired and downstream
wrapper selection / suffix matching falls back to `shellKind` alone,
which is the correct behavior for the unknown-shell case.
Wire this into the three call sites that previously read
`session.lastIdlePrompt || ""`:
- `aiBridge.cjs:1325` (Catty Agent foreground exec)
- `mcpServerBridge.cjs:1496` (MCP `terminal_execute`)
- `mcpServerBridge.cjs:1584` (MCP `terminal_start` background job)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix PowerShell MCP command execution
* Harden PowerShell prompt detection and document its scope
- Annotate isPowerShellPrompt and the matching regex in shellUtils with
a "default prompt only" caveat, so future readers know custom prompt
themes (oh-my-posh, starship, custom prompt functions) are out of
scope on purpose, and keep the two regexes in sync.
- Cover edge cases that the original tests left implicit: trailing
whitespace after the `>`, ANSI-coloured prompts, bare `PS>` with no
working directory, empty/undefined inputs, and command output that
merely starts with `PS` (e.g. `PSO>`, `ZIPS>`) so we don't regress
into mis-wrapping non-PowerShell sessions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address multi-agent review findings on PowerShell prompt detection
- Refuse to override an explicit non-PowerShell shellKind. The override
is only useful when the session has no confirmed shell type (the
issue #841 case is an SSH session, where shellKind is undefined). On
a confirmed bash/zsh/fish session a malicious remote process emitting
a `PS ...>` line could otherwise coerce one mis-wrapped command; this
closes that foothold while still fixing the original bug.
- Tighten the regex to /^PS(?:\s+\S.*)?>$/ so a literal `"PS >"` line
is rejected. The default PowerShell prompt never emits that shape, so
it's a clean spoof signal to ignore.
- Treat `\r` as a line break, not a stripped character, when extracting
the last idle line. PSReadLine / ConPTY emit bare `\r` to repaint the
current line; without this, `"PS C:\\old>\rPS C:\\new>"` would match
as one long doubled prompt that never round-trips through the live
PTY tail.
- Hoist the regex into shellUtils as `isDefaultPowerShellPromptLine` so
prompt extraction and wrapper selection share one source of truth.
- Drop a redundant optional-chain on `String.prototype.split().pop()`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Drop dead 'powershell' entry from override set; document shellKind universe
Round-2 review noted that listing "powershell" in
SHELL_KINDS_OPEN_TO_PROMPT_OVERRIDE was a no-op: when the configured
shell kind is already powershell, the override path returns "powershell"
on a match and the fall-through returns "powershell" on a miss, so the
entry only mattered if reverse PS-to-POSIX detection were added later.
Removing it makes the gate's intent ("override only when there's no
confirmed shell type") obvious from the data alone.
Also enumerate the full universe of shellKind values in a comment next
to the set so the next reader doesn't have to grep terminalBridge and
localShell.cjs to know what's excluded and why ("raw" sessions bypass
buildWrappedCommand entirely; "cmd"/"fish" are confirmed and shouldn't
flip to PowerShell on a spoofed remote line).
Add a regression test that locks the current behavior for an explicit
shellKind="powershell" session whose visible prompt looks POSIX (e.g.
nested into WSL/bash) — we keep powershell wrapping. Lock this so a
future maintainer doesn't accidentally introduce reverse detection
without also handling the cross-shell quoting implications.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add Mosh client detection and override in Settings → Terminal
Builds on PR #847 (auto-detection across PATH gaps). Power users with
non-standard install locations (containers, custom builds, multiple
mosh versions) can now point the app at a specific mosh binary; less
technical users get a one-click "Detect" button to confirm where mosh
was found, with a Browse fallback for clicker-only flows.
Backend (electron/bridges/terminalBridge.cjs):
- detectMoshClient() returns { platform, found, path, searchedPaths }.
Reuses resolvePosixExecutable; surfaces the searched dirs so the UI
can tell users where to look when nothing was found.
- pickMoshClient() opens a native file picker via dialog.showOpenDialog.
- startMoshSession honors options.moshClientPath when provided. Strict
failure: a missing/non-executable explicit path produces a clear
error instead of falling back to auto-detect, so users notice typos
and stale paths instead of getting silent recovery.
UI (components/settings/tabs/SettingsTerminalTab.tsx):
- New SettingRow under "Connection" with text input + Detect + Browse
buttons, mirroring the localShell validation pattern. Shows inline
validation (notFound/isDirectory) and the last detect result with
searched directories on miss.
Plumbing:
- TerminalSettings.moshClientPath: string field with default "" so
empty == auto-detect (matches existing PR #847 semantics).
- preload exposes detectMoshClient + pickMoshClient.
- createTerminalSessionStarters passes terminalSettings.moshClientPath
into the IPC call, undefined when blank.
- en.ts / zh-CN.ts get the 9 new strings.
Verified locally:
- vite build succeeds; settings tab renders.
- detectMoshClient() against the live machine returns
/opt/homebrew/bin/mosh with the expected searchedPaths list.
- Existing PR #847 auto-detection path is unchanged when the field is
empty.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Skip POSIX execute-bit check for explicit Windows mosh path
Address Codex P2 on PR #849 commit 88e5c596. isExecutableFile used
`(stat.mode & 0o111) !== 0` to gate the explicit moshClientPath in
startMoshSession, but Windows Node returns mode 0o100666 even for
.exe / .bat / .cmd files (NTFS has no POSIX execute bits). Result:
a Windows user who picked a perfectly valid `mosh.exe` via the new
Browse dialog or typed an absolute path was rejected with
"Configured Mosh client not usable…" — making the manual override
unusable on Windows.
Make isExecutableFile platform-aware: still require isFile() and
the Unix execute bit on POSIX, but treat any regular file as
executable on Win32 and let spawn-time PATHEXT / extension handling
filter non-executables.
Resolver paths are unaffected — resolvePosixExecutable returns null
on Win32 before isExecutableFile is reached.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Augment Windows env when explicit mosh path is outside PATH
Address Codex P2 on PR #849 commit 69782471. When a Windows user
selected a mosh.exe outside %PATH% via Browse / custom path, the
explicit-client branch left resolvedMoshDir null, so the later
PATH/MOSH_CLIENT injection was skipped. The Mosh wrapper still
exec's `mosh-client` (and `ssh`) by name, so a valid selection
failed unless that directory was already on PATH.
- Always set resolvedMoshDir for explicit moshClientPath, regardless
of platform.
- Use path.delimiter so PATH composition uses ";" on Win32 and ":"
on POSIX. Compare directory membership with path.normalize so
trailing-slash / case differences don't double-add.
- When picking mosh-client, try .exe / .bat / .cmd extensions on
Win32 before the bare name; POSIX still uses just `mosh-client`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Validate Mosh client is executable in Settings UI
Address Codex P2 on PR #849 commit b6c384af. UI's debounced validator
called validatePath which only reported exists / isFile / isDirectory,
so a regular file without the POSIX execute bit (e.g. a stray
/etc/hosts-style path) was marked as valid in Settings — but
startMoshSession's isExecutableFile check then rejected the same path
at connect time, deferring the error until the user actually tried to
use Mosh.
- validatePath now returns `isExecutable: boolean`, mirroring
isExecutableFile semantics (POSIX: stat.mode & 0o111; Win32: any
regular file is treated as executable since NTFS lacks POSIX bits).
Existing callers (localShell, localStartDir) ignore the new field.
- global.d.ts ValidatePath return type extended.
- SettingsTerminalTab Mosh validator surfaces a `notExecutable`
message when the file exists but lacks exec permissions, keeping
the UI in lockstep with main-process gating.
- en / zh-CN strings for the new state.
Verified: /bin/sh -> isExecutable:true, /etc/hosts -> false, /etc ->
false (directory). UI now warns immediately on the regression case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Require absolute Mosh client paths in Settings UI and main
Address Codex P2 on PR #849 commit 2eba549e. The shared validatePath
bridge resolves bare names through PATH (necessary for localShell
where 'powershell.exe' is a valid choice), so a user typing 'mosh' or
'mosh.exe' into the new Mosh field would get a green check in
Settings — but startMoshSession treats moshClientPath as a literal
filesystem path and calls isExecutableFile on the raw value. The
saved setting then disables auto-detection and Mosh sessions fail
unless a matching file happens to exist in the app's cwd.
Gate on absolute paths at both layers so UI validation and the
runtime check agree:
- startMoshSession: path.isAbsolute(expanded) before isExecutableFile,
with a distinct error message naming the constraint.
- SettingsTerminalTab: same shape — UI checks looksAbsolute (POSIX
/, leading ~, Windows drive letter, or UNC \\\\) before sending the
IPC, surfacing notAbsolute inline. Tolerant across platforms so
pasting a Windows-style path on macOS still produces a real
downstream error rather than a misleading 'not absolute'.
- en / zh-CN strings.
Verified against the full case matrix (relative names, ./, ../, bare
basenames, POSIX absolute, ~/, Windows drive, UNC) — UI flags every
relative entry without an IPC round-trip, and any value that passes
UI also passes main-process validation (or both reject).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Resolve mosh client by absolute path on macOS / Linux
Closes#842.
macOS GUI Electron apps inherit launchd's reduced PATH
(/usr/bin:/bin:/usr/sbin:/sbin), missing /opt/homebrew/bin and other
common package-manager directories. The previous startMoshSession
called pty.spawn('mosh') with a bare name, so on Apple Silicon
Homebrew installs the spawn either failed silently or produced a
process that exited before the renderer could observe anything,
matching the issue: no terminal tab, no error toast, no DevTools log,
no network traffic.
- Add resolvePosixExecutable() that searches the inherited PATH and
then a curated set of fallback directories (Homebrew arm64/x64,
MacPorts, ~/.nix-profile, ~/.cargo, ~/.local).
- Resolve `mosh` to an absolute path before spawning. When it cannot
be located, throw an Error with an installation hint instead of
letting pty.spawn fail in a way that stays invisible — the
renderer's existing catch in createTerminalSessionStarters already
surfaces the message via term.writeln + setError.
- Prepend the resolved binary's directory to env.PATH and set
MOSH_CLIENT, so the mosh wrapper script (Perl) finds mosh-client
and ssh next to it even when the launchd PATH is reduced.
Verified the resolver against a fake binary placed only in a fallback
dir while the simulated PATH was reduced to /usr/bin:/bin — the
function correctly returns the fallback hit. Win32 path through
findExecutable() is left unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Resolve mosh against the merged child PATH
Address Codex P2 on PR #847 commit 314d396a: the resolver only checked
process.env.PATH plus hardcoded fallbacks, so a host that sets a custom
PATH via environmentVariables (later merged into the child env) could
trip the new "Mosh client not found" error even though the spawned
process would have had a valid PATH all along.
- Accept a { pathOverride } option on resolvePosixExecutable so the
caller can pass the PATH the child will actually see.
- Pre-merge the host-supplied options.env.PATH (falling back to
process.env.PATH when absent) and pass it to the resolver.
- Fallback dirs (Homebrew arm64/x64, MacPorts, ~/.nix-profile, etc.)
still run after the override, so users who override PATH but forget
to include their custom mosh location get the same silent rescue.
Verified four regression cases: no-override, Codex's custom-PATH
override, empty-string override, and opts-without-pathOverride —
each resolves the way the spawned process would.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(session-logs): render terminal control sequences in saved logs
Add a stateful terminal log sanitizer for txt/html session logs so saved output handles backspace, carriage-return overwrites, erase-line/display controls, and split CSI/OSC sequences correctly.
Stream txt/html auto-save through a persistent renderer and write rendered snapshots directly to the final log file, avoiding raw temp files and redundant full rewrites on session close. Keep raw log format unchanged.
* fix review issue
---------
Co-authored-by: yuzifu <yuzifu@TB16PGen5.Info>
* Bundle Symbols Nerd Font Mono as terminal icon fallback
PR #845 added "Symbols Nerd Font Mono" to the terminal fontFamily
fallback chain so PUA glyphs (powerline / devicons / etc.) resolve
even when the user's primary font lacks them. That only worked if the
user had separately installed the symbol font; ship it ourselves so
icons render out of the box regardless of the chosen base font.
- Drop SymbolsNerdFontMono-Regular.ttf into public/fonts (~2.5 MB);
Vite copies it to dist/fonts and the existing app:// protocol
handler already knows the font/ttf MIME type.
- Register an @font-face in index.css pointing at the bundled file.
font-display: block prevents tofu while the (instantly-available
bundled) face loads, only affecting PUA glyphs since the base font
is listed earlier in the fallback chain.
- Include the upstream LICENSE next to the font.
Source: ryanoasis/nerd-fonts NerdFontsSymbolsOnly v3.4.0 (MIT).
Refs #843
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Reference bundled font by absolute path so prod build resolves
Address Codex P2 on PR #846: the relative `./fonts/...` URL was emitted
verbatim into dist/assets/index-*.css, where the browser resolved it
against the CSS file's location and 404'd on
dist/assets/fonts/SymbolsNerdFontMono-Regular.ttf — the actual file
lives in dist/fonts/, so the icon fallback never loaded in packaged
builds and Nerd Font glyphs still rendered as tofu.
Switch the @font-face url() to `/fonts/...`. Vite's `base: "./"`
config rewrites that to the correct dist-relative form during build
(`../fonts/SymbolsNerdFontMono-Regular.ttf` from dist/assets/), and in
dev the same path is served by the Vite dev server out of public/.
Verified by re-running `vite build` and grepping the produced CSS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Enable Nerd Font glyphs in terminal font picker and rendering
- Grant local-fonts permission on the default session so queryLocalFonts()
can enumerate user-installed fonts; without it the picker only showed
the 20 hard-coded built-ins, hiding Nerd Font sub-families like
"JetBrainsMono Nerd Font Mono".
- Append a Symbols Nerd Font fallback to the terminal fontFamily chain so
PUA icons (powerline / devicons / etc.) resolve even when the primary
font lacks them, matching the cross-font fallback behavior CoreText-based
terminals like Ghostty already provide.
- Whitelist "Symbols Nerd Font" / "Symbols Nerd Font Mono" in the local
monospace allow-list so the symbol-only icon font is not filtered out.
Refs #843
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Restrict permission handler to app origin
Address review feedback on PR #845: the previous permissive fallthrough
granted every permission request/check that hit the default session,
which the in-app OAuth flow uses too. That meant remote OAuth pages
(accounts.google.com, login.microsoftonline.com, ...) could be auto-
approved for camera, microphone, geolocation, notifications, etc.
Gate the handler on the requesting origin: only the app's own renderer
(app://netcatty plus the dev server in dev) gets the local-fonts grant
and the prior approve-by-default behavior. Anything loaded from a
third-party origin is denied outright.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Use explicit permission allow-list for app origin
Address Codex P1 on PR #845 commit 975ca7e8: even after gating on the
app origin, the previous fallthrough still called callback(true) for
every non-local-fonts permission, so the main/settings renderers were
silently auto-granted notifications, geolocation, pointer lock, media,
etc. — none of which the app uses.
Replace the fallthrough with an explicit allow-list of the permissions
the renderer actually exercises (local-fonts plus clipboard read/write
for terminal + SFTP copy-paste). Anything outside that set is now
denied for the app origin too, matching the deny-by-default posture
Codex flagged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Match app:// origin by protocol+host, not URL.origin
Address Codex P1 on PR #845: in the packaged build the renderer loads
app://netcatty/index.html, but Node's WHATWG URL parser does not treat
app: as a standard scheme, so `new URL('app://netcatty/...').origin`
evaluates to the string "null". The previous Set-based origin check
therefore never matched the production renderer, causing the new
permission handlers to deny local-fonts as well as the existing
clipboard-read / clipboard-sanitized-write — breaking the font picker
and clipboard flows in release builds.
Compare protocol + host directly for app://, and keep the .origin
lookup for the dev server (which is HTTP-family and parses normally).
Verified against the relevant URL shapes (packaged main + settings,
dev server, third-party OAuth, file://).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The post-handoff `resetProviderStatus(provider)` call destroyed the
adapter that `startProviderAuth` had just created, because the hardened
`resetProviderStatus` now restores from the auth snapshot (which has
`adapter: null` for first-time connects). The subsequent OAuth callback
then failed with `google/onedrive adapter not initialized`, and the
error was persisted onto the provider state.
Introduce `clearConnectingStatus` for the "release connecting UI"
intent and switch the PKCE flow to use it, so adapter and auth
restore-snapshot are left untouched until the callback completes.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Google Drive / OneDrive PKCE flow bound a temporary callback server on
a hardcoded 127.0.0.1:45678. If anything on the user's machine already
holds that port (another desktop app, a leftover process, a firewall rule)
the listen fails with EADDRINUSE and the user sees
"Error invoking remote method 'oauth:startCallback': EADDRINUSE".
Split the bridge into a two-step flow so the chosen port is known before
we build the authorization URL:
- oauthBridge.prepareOAuthCallback(): tries the preferred 45678 first,
falls back to an OS-assigned free port (listen(0)) if it's in use, and
returns { port, redirectUri }.
- oauthBridge.awaitOAuthCallback(state): awaits the code on the
already-prepared server.
CloudSyncManager.startProviderAuth now requires the redirectUri to be
passed in; useCloudSync calls prepare → startProviderAuth(redirectUri) →
await, and cancels the prepared server if anything fails before the
browser hop.
windowManager's in-app-popup allow-list reads the active port from
oauthBridge at popup-open time instead of hardcoding 45678, so the
loopback callback keeps working regardless of which port was chosen.
Also: unref() the callback server and closeAllConnections() on teardown
so the OS port is released promptly between flows and test runs don't
leave zombie listeners.
Tests: new electron/bridges/oauthBridge.test.cjs covers the preferred-
port path, the busy-port fallback (#823 regression), the state-mismatch
rejection, the provider-error rejection, the "await without prepare"
guard, and cancel/release semantics. All 85 bridge tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fix attached a 32x32 @2x representation to the 16x16 PNG,
which only covers 100% and 200% scale factors. Users on 125/150/175/
250%+ still got a blurry tray icon because Windows had to resample from
one of those two sizes.
Ship a proper multi-size tray-icon.ico (16, 20, 24, 32, 40, 48, 64) and
point the Windows tray loader at it. Windows picks the closest size per
DPI scale on its own, so no addRepresentation / resize juggling is
needed. Linux keeps the existing PNG + @2x path; macOS is unchanged.
Also add scripts/generate-tray-ico.py so the .ico can be regenerated
from public/icon-win.png whenever the source artwork changes.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(settings): guard customKeyBindings cross-window sync against echo loop (closes#818)
customKeyBindings was the only synced setting whose two cross-window
handlers (DOM storage event + IPC onSettingsChanged) called
setCustomKeyBindings unconditionally. Every broadcast landed with a
fresh parsed object reference, so React re-rendered and the persist
effect re-broadcast, echoing across windows indefinitely.
While the echoes carry the same content, a rapid second click from
the user can arrive between the outbound broadcast and an older
in-flight echo — the echo's setState then clobbers the latest click
and the UI "bounces" from Disabled back to the original binding.
This matches the report in #818 (disable and reset operations
flicker between values when clicked in quick succession).
Fix: mirror the equality guards used by every other synced field.
Compare the incoming payload (stringified for objects) against the
current value from settingsSnapshotRef, and skip setCustomKeyBindings
when they match. Add customKeyBindings to settingsSnapshotRef so the
IPC handler has access without pulling it into the effect's closure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(settings): stop shortcut sync bounce flicker
* fix(settings): harden shortcut sync ordering
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #763 captured and restored the mouse selection in a keydown-only
microtask. That covers lowercase letters — xterm's _keyDown calls
triggerDataEvent synchronously, so the selection is cleared before the
microtask drains and the restore runs.
Space (keyCode 32) and A–Z (the _keyDown macOS-IME HACK) are instead
routed through the keypress event, which fires in a *later* macrotask.
The keydown microtask drains first, sees the selection still intact, and
no-ops. Then keypress clears it without any restore.
Fix: hook both keydown and keypress in attachCustomKeyEventHandler. The
keypress path gives us a second microtask that drains after _keyPress
has cleared the selection, so the restore actually runs for those keys.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Fixed 8% brightness causes compositers to have severe rendering issues. (Only effected on the Midnight color scheme) 10% seems to be okay.
- Reduced backdrop-blur as it's expensive CSS.
- Removed radial-gradient backgrounds (they don't show up)
Closes#813.
#803 enlarged public/icon.svg's squircle to ~88% of the canvas so the
macOS dock icon would match third-party apps that don't leave Apple's
HIG grid margin. That fix is right for macOS — the dock already
rounds / shadows its own icons and the grid margin lines Netcatty up
with neighbors. But every non-mac launcher (Windows taskbar, Start
menu, desktop shortcuts, KDE / GNOME launchers, AppImage integrations)
renders icons full-bleed into a fixed-size slot, so that ~12% padding
shows up as visible empty space around the squircle — the reporter's
"taskbar icon looks smaller and blurrier than other apps".
Split the icon sources by platform:
- public/icon.svg / public/icon.png — unchanged, keeps the #803 88%
fill. mac.icon (implicit via top-level) still uses it.
- public/icon-win.svg — new source with viewBox="100 100 824 824"
(tight-cropped to the squircle) and the faint white outline stroke
disabled. Rendered at 1024×1024 into public/icon-win.png.
- electron-builder.config.cjs wires win.icon and linux.icon to the
new tight-crop source. Top-level icon: stays the padded version so
the mac path is unchanged.
electron-builder generates a multi-size .ico from a ≥256px PNG on
Windows and scales PNG variants for Linux, so a single
1024×1024 source covers both platforms without new build steps.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): sync ghost text to live input on every keystroke
Ghost text was displayed based on whatever input was passed to
GhostTextAddon.show() at fetch time. Between a user's keystroke and
the next debounced fetchSuggestions firing (~100ms), the on-screen
line had already advanced one character but ghost.getGhostText() still
returned the pre-update tail. Pressing → during that window pasted the
stale tail on top of the new char — e.g. type "do", suggestion shows
"cker ls"; type "c", accept immediately → "doc" + "cker ls" lands as
"doccker ls" instead of the expected "docker ls".
Two-layer fix:
1. New GhostTextAddon.adjustToInput(newInput) that re-renders the ghost
against a fresh input without waiting for a new fetch: shrinks /
grows the tail if the suggestion still prefix-matches, hides
otherwise. Called from handleInput after every buffer mutation
(printable, backspace, Ctrl-W, paste tail) when the buffer is
reliable. Unreliable-buffer paths skip the call to avoid making the
ghost lie.
2. Defense-in-depth at both ghost-accept sites (→ and Ctrl-→):
recompute the tail against the live typed buffer instead of trusting
getGhostText's show()-time state. If the suggestion no longer
prefixes the live buffer, hide without writing. Ctrl-→ additionally
resyncs ghost.show() to the live buffer before picking the next word
so getNextWord operates on an up-to-date tail.
* fix(autocomplete): defer ghost text updates to the next xterm render
The previous pass made adjustToInput re-show the ghost synchronously on
every keystroke, but xterm hasn't echoed the triggering char yet at
that moment — cursorX is still the pre-keystroke position. Painting
the shrunken tail there left it visibly overlapping with the char
xterm was about to draw, and the ghost only snapped to the right
column on the next onRender tick. That one-frame overlap is the
"jitter" the reporter still saw.
Switch adjustToInput to a defer-and-reapply pattern:
- On every keystroke that should re-align the ghost, stash the desired
input in pendingInput and hide the element immediately. The
transient blank frame is preferable to an overlap glyph.
- The existing term.onRender listener now checks for a pending update
first: by that tick xterm has processed the echo, cursorX has
advanced, and we can paint the new tail at the correct column via
applyInputUpdate.
- New isActive() exposes "has a live suggestion even if hidden waiting
for render" so a fast "type + →" / "type + Ctrl-→" sequence in the
hide-until-render gap still hits the accept branch and grabs the
recomputed tail from the live buffer.
show() and hide() clear pendingInput so an explicit state change
supersedes any queued adjust.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): restore ghost text, predict-anchor-shift on each keystroke
The previous refactor broke inline completion entirely:
1. useTerminalAutocomplete force-disabled showGhostText whenever
showPopupMenu was on — and both are true by default, so ghost
never rendered.
2. GhostTextAddon put its overlay container *under* xterm's screen
via insertBefore + no z-index. xterm's default renderer paints
theme.background across every cell including empty ones, so the
ghost was fully occluded by the canvas even when the hook *did*
call show().
Fixes both issues and lands the correct per-keystroke strategy the
jitter report was asking for:
- Drop the showGhostText-vs-showPopupMenu gate; respect user settings.
- Put the ghost container back on top of the screen (appendChild +
z-index 1).
- Track anchorInputLength at show() time. adjustToInput now advances
the ghost's left by (newInput.length - anchorInputLength) cells
*synchronously* — i.e. it predicts where xterm's cursor will land
once the echo arrives, instead of re-reading the live cursorX that
hasn't advanced yet. textContent is trimmed in the same call, so
ghost + real-input stay aligned across SSH echo latency with no
one-frame overlap or blank gap.
- Updated GhostTextAddon.test.ts expectations for the new behavior
(and cast the fake-document through unknown to fix the pre-existing
TS error).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): address ghost text review feedback
Follow-ups on the predict-anchor-shift from the previous commit,
based on a code-reviewer pass:
- Backspace / Ctrl-W de-sync: updatePosition's Math.max(0, ...) was
clamping the delta to zero when newInput shrank below the show-time
input length. The ghost then stayed pinned at the original anchor
column while the real cursor walked back left, leaving a gap
between the cursor and the ghost. Let the delta go negative so the
ghost tracks the cursor backwards; clamp the resulting left at 0
instead of clamping the delta.
- Resize staleness: onResize now also resets lastLeft/lastTop and
re-renders, so the dedup cache in updatePosition doesn't hide a
now-stale pixel coordinate after xterm recomputes cell dims.
- Added a regression test for the backspace path covering both the
step-back-below-anchor case and the clamp-at-0-on-overshoot case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): don't accept whole suggestion when buffer is unreliable
Codex flagged (#815 P1 ×2) that the live-buffer recompute on → and
Ctrl-→ falls into a degenerate path when typedBufferReliableRef is
false. My previous cut used live = "" as the fallback, but
fullSuggestion.startsWith("") is always true — so:
- → would write the entire suggestion over whatever is on the line
(post history-recall ↑, Ctrl-R reverse search, etc.).
- Ctrl-→ would reanchor the ghost at the start and getNextWord would
hand back the first token, duplicating leading content on top of
the recalled command.
When the buffer is unreliable, empty buffer ≠ empty line — the line
has content we're not tracking. Fall back to the ghost's own cached
state instead of recomputing:
- → reliable: recompute tail vs live buffer, flip buffer to the
accepted suggestion, reliability back on.
- → unreliable: use ghost.getGhostText() (shown-at-show-time tail)
and don't touch the buffer/reliability flag.
- Ctrl-→ reliable: resync ghost to live, then proceed as before.
- Ctrl-→ unreliable: skip the resync, derive the shrink baseline from
fullSuggestion - current-ghost-tail so the next-word logic still
works off whatever the ghost was actually showing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): hide ghost on single-byte cursor/recall control chars
Reviewer caught that Ctrl-P / Ctrl-N / Ctrl-R / Ctrl-A / Ctrl-E and
friends flip typedBufferReliableRef to false but don't hide the
ghost — leaving it rendering a tail tied to the pre-recall line. The
previous commit's unreliable-→ fallback then reads that stale tail
via ghost.getGhostText() and writes it onto the recalled line,
reproducing the very duplication class the fallback was meant to
prevent (just triggered by Ctrl-P instead of ↑).
Mirror what the escape-sequence branch already does: clearState() +
return. Once the ghost is hidden, ghost.isActive() is false at the →
and Ctrl-→ gates, so the accept-path doesn't fire at all until a
fresh fetchSuggestions re-anchors it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): drop accepted-command cache on cursor/recall keys
Reviewer pointed out that the early returns in the single-byte
ctrl-char and escape-sequence branches leave lastAcceptedCommandRef
untouched. If the user accepts a suggestion via → and then immediately
hits Ctrl-R or ↑ to pick a different command, the fast Enter path
(lines ~611-612) still reads the cached accepted command and records
it — logging the old suggestion instead of whichever command the
reverse-search or history-recall actually ran.
Null lastAcceptedCommandRef at the top of both branches (same place
we hide the ghost and flip reliability off) so accept + recall + Enter
records the recalled command, not the stale accept.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): also null accepted-command cache on Ctrl-C / Ctrl-U
Reviewer flagged this class of bug is still reachable via Ctrl-C /
Ctrl-U. The branch handling those kills the zle line, but the early
return leaves lastAcceptedCommandRef pointing at a command that is
no longer on the line: accept "git status" via → → Ctrl-C to abandon
→ type "ls" → Enter logs "git status" via the fast path instead of
"ls".
Same one-liner as the other early-return branches: null the cache
alongside clearState(). Now the cache's lifetime truly ends at any
event that invalidates the accept.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): null accepted-command cache on bracketed paste too
Fifth-pass reviewer caught the last symmetric gap: the bracketed-paste
branch appends pasted bytes to the buffer but leaves lastAcceptedCommandRef
set. Accept "git status" via → then bracketed-paste " --short" (no
embedded newline), press Enter — the fast path at line 611 still reads
"git status" and logs that instead of "git status --short".
Mirror the non-bracketed paste branch: null the cache before clearState()
returns. All handleInput paths that extend or invalidate the line now
consistently end the cache's lifetime.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): predict ghost column by cell width + wrap at EOL
Review caught two geometry bugs in GhostTextAddon.updatePosition that
only surfaced outside the ASCII happy path:
- CJK / fullwidth / emoji glyphs occupy two xterm cells but the
predictor advanced by one char-length per code unit, so ghost
drifted one cell left for every wide char typed and visibly
overlapped the user's glyph.
- When the predicted column crossed term.cols the real cursor wrapped
to the next row, but the predictor just piled more pixels onto
`left` — ghost walked off the right edge instead of following
onto the next line.
Fix both by switching from code-unit count to a small EAW-style
width classifier, then applying row wrapping via
col = (anchorX + cellDelta) % cols
rowOffset = Math.floor((anchorX + cellDelta) / cols)
against the current term.cols. Fake terminal in the test suite now
exposes cols/rows so the unit tests can exercise both invariants:
- "advances the anchor by two cells when a CJK glyph is typed"
- "wraps the ghost to the next row when the predicted column crosses cols"
Known limitation the review already flagged: on backspace-after-wide
we don't have per-grapheme widths to reverse exactly, so the negative
delta falls back to code-unit width on the deleted slice. The slice
is `currentSuggestion[currentInput.length..anchorInputLength]` which
is the same text the user would have typed, so it's correct when
only ASCII edits; wide-char backspace can still drift by one cell.
Fixing this cleanly needs a per-grapheme buffer and is out of scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): honor showGhostText toggle while a ghost is on screen
Codex flagged (#815 P2) that fetchSuggestions gates new ghost shows
on settingsRef.current.showGhostText, but handleInput's adjustToInput
call had no such guard. A ghost that was already active at the moment
the user turned showGhostText off would keep tracking the typed
buffer via adjustToInput on every keystroke, so the "disabled" setting
only took hold after some unrelated path called clearState().
Two-part fix:
- Add a useEffect watching settings.showGhostText. When it flips false,
hide the active ghost immediately so the disabled setting applies to
whatever was already on screen.
- Gate the adjustToInput call in handleInput behind
settingsRef.current.showGhostText too, so subsequent keystrokes under
the disabled setting don't try to move or re-show a ghost.
Codex's earlier P2 about wrap-at-EOL on line 236 is already resolved
by e61f0e8b (predict-column-with-wrap + CJK width); that comment is
against an older commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): self-heal stale anchor + handle backward-wrap on delete
Codex flagged two real geometry gaps in the predict-anchor-shift math:
1. Stale anchor on high-latency shells. show() captures cursorX from
xterm at debounce-fire time, but under SSH round-trip latency the
user's latest keystroke may not have echoed yet — cursorX is still
the pre-echo column. With updatePosition now purely anchor-based
(no longer reading live cursorX on every render), that stale anchor
becomes frozen; the ghost stays one-plus cells off for the whole
suggestion session until another show() rebuilds it.
2. Backspace crossing a wrapped row boundary. Math.max(0, ...) clamped
targetCol at zero, so deletions past column 0 stayed pinned to the
current row instead of wrapping back to the previous row — exactly
the symmetric case the forward wrap added in e61f0e8b handles.
Fixes:
- Self-heal in updatePosition: while no adjustToInput has moved us
from the show-time baseline (currentInput.length === anchorInputLength),
re-read live cursorX/Y each render tick. Once the user starts typing
the anchor is frozen and delta math takes over.
- Normalize the wrap for negative targetCol: `col = targetCol % cols`
plus `if (col < 0) col += cols`, `rowOffset = Math.floor(targetCol/cols)`
naturally yielding -1 on underflow. Clamp `top` at row 0 so a
runaway negative doesn't render above the terminal.
Two new tests cover both invariants:
- "self-heals a stale anchor on render while no adjustToInput has fired"
- "wraps the ghost to the previous row when deletion crosses a row boundary"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): restore ghost/popup mutual-exclusivity guard in hook
Codex flagged (#815 P2) that dropping the popup-wins-over-ghost
normalization inside useTerminalAutocomplete weakens the hook's own
defensive invariant. The repo enforces mutual exclusivity in two
places already — SettingsTerminalTab toggles one off when the other
turns on, and domain/models.ts normalizes stored settings so
autocompletePopupMenu === true forces autocompleteGhostText to false
— so on the normal Terminal.tsx → store path only one of the two
arrives as true. But the hook's own defaults (DEFAULT_AUTOCOMPLETE_SETTINGS)
have both flags true, and any caller that builds settings directly
from those defaults (tests, future embedders) would end up rendering
popup + inline ghost simultaneously against the repo-wide contract.
Restore the guard, comment it as defensive rather than load-bearing
so future readers don't mistake it for the hiding-invisible-ghost
bug I was fixing last time (that was really the insertBefore /
z-index issue in GhostTextAddon.ts, not this normalization).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): honor typed keystrokes when the prompt parser over-captures
Closes#806.
## Root cause
findPromptBoundary stops at the first "PROMPT_CHAR + space" it sees on
the current line. Themes that render additional content after the
prompt char — most notably oh-my-zsh robbyrussell's "➜ ~ " where "~"
is the cwd — trip it: promptText becomes "➜ ", userInput becomes
"~ sudo id". Every consumer downstream treats the theme's cwd marker
as part of the user's command, so:
1. recordCommand logs entries like "~ sudo id" into history.
2. fuzzyQueryHistory later returns those polluted entries as
suggestions.
3. When the user hits Tab, insertSuggestion compares
suggestion.text ("~ ls") against userInput ("~ lo"), falls into
the Ctrl-U-plus-rewrite path, and the phantom "~ " ends up on
the real command line.
The reporter hit this right after `sudo` because sudo's password
interaction gave history enough polluted entries to start winning
fuzzy matches; without sudo the popup stays empty so the Ctrl-U
rewrite path never fires and the bug is invisible.
## Fix
Track what the user actually typed in an independent keystroke buffer
(typedInputBufferRef) inside the autocomplete hook:
- Append every printable char / paste chunk.
- Pop on backspace, word-kill on Ctrl+W.
- Clear on Enter, Ctrl+C, Ctrl+U, and any escape sequence / unhandled
control char (cursor moves we can't follow invalidate the buffer).
Introduce reconcilePromptWithTypedInput: if detectPrompt's userInput
ends with the typed buffer and is longer, the parser over-captured —
move the excess back to promptText so userInput matches what was
actually typed. Apply at every detectPrompt call site
(fetchSuggestions, the stale-result recheck, insertSuggestion).
For Enter-record the typed buffer wins outright when present, but
only after a live detectPrompt confirms we're at a shell prompt —
otherwise a password-entry Enter would log the password as a
command.
insertSuggestion / ghost-text accept update the typed buffer to the
accepted text so a subsequent Enter records the right command.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): track keystroke-buffer reliability, skip it after cursor moves
Codex flagged (#814 P1) that clearing typedInputBufferRef on escape /
control sequences and then re-appending printable keys leaves the
buffer holding only the post-navigation suffix of the real line.
A classic Up-arrow-recall workflow — ↑ to pull "git commit -m fix"
out of history, append one char, Enter — would record just that one
char as the command, polluting history and skewing future fuzzy
matches.
Add typedBufferReliableRef as a companion flag:
- Reset (reliable=true) on Enter / Ctrl-C / Ctrl-U (zle wipes the
line, our buffer is a true view of the empty line again).
- Also reset by insertSuggestion and ghost-text right-arrow accept
once they write the full accepted text and we re-align the buffer
to it.
- Cleared (reliable=false) when any escape sequence, unhandled
control char (Ctrl-P / Ctrl-N / Ctrl-R / Ctrl-A / Ctrl-E / ...)
arrives — those can move the cursor or swap the zle line in ways
an append-only buffer can't follow.
All four call sites now gate on the flag:
- reconcilePromptWithTypedInput receives the buffer only when
reliable, so an unreliable buffer never trims the detector's
userInput (avoids a symmetric flavor of the original bug where
the detector is right and the buffer is wrong).
- Enter-record prefers the buffer only when reliable; otherwise it
falls straight through to detectPrompt.
- The Ctrl+Right (next-word ghost accept) append is skipped when
unreliable so we don't seed the buffer with just that word.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): resync typed buffer when sub-dir select rewrites the line
Codex flagged (#814 P2) that handleSubDirSelect rewrites the command
line via writeToTerminal(Ctrl-U + cmdPrefix + fullPath) but never
touches typedInputBufferRef. After the rewrite the buffer still holds
whatever was typed before, so pressing Enter records that stale partial
input as the executed command — polluting history and steering later
suggestions off course.
Same commit also routes handleSubDirSelect through
reconcilePromptWithTypedInput. The raw detectPrompt would include the
robbyrussell "~ " cwd marker in the command prefix it reconstructs,
which is the original symmetric #806 bug leaking into this path too.
After the rewrite, set the buffer to the newly written command string
and flip reliability back on — the terminal line content now matches
it exactly, so the next Enter-record does the right thing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): reset typed buffer when a paste chunk carries a newline
Codex flagged (#814 P2) that multi-character paste payloads skip the
top-of-handleInput Enter guard (which compares data === "\r" exactly),
so a paste like "cmd\r" goes through the paste branch and the "\r" gets
appended to typedInputBufferRef verbatim. The shell executes "cmd", but
our buffer is left holding "cmd\r...", still marked reliable. The next
Enter then records whatever combined stale string lives there.
Detect line terminators inside multi-char paste chunks: slice from the
last \r or \n onward and keep only that tail as the new buffer content
(and flip reliability back on, since the tail now matches the shell's
zle line). Skip synthesizing recordCommand entries for the flushed
intermediate lines — onCommandExecuted in createXTermRuntime already
tracks pasted multi-line input independently, so duplicating the logic
here would risk double-counting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): clear lastAcceptedCommandRef on paste-with-newline early return
Codex flagged (#814 P2) that the multi-line-paste branch clears the
keystroke buffer and bails out before the rest of handleInput runs —
including the line that resets lastAcceptedCommandRef. If the user had
just accepted a suggestion (Tab / → / popup click), the embedded
newline still flushes it in the shell, but our fast-path cache keeps
holding it. The next Enter then takes the lastAcceptedCommandRef
shortcut and logs that old suggestion as the executed command,
polluting history with something the user didn't actually run.
Null lastAcceptedCommandRef.current at the same point we reset the
typed buffer so the fast path stays aligned with the shell.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): require typed buffer to align with live line before recording
Codex flagged (#814 P1) that paste paths which bypass handleInput —
the createXTermRuntime hotkey / context-menu / middle-click handlers
all call writeToSession(...) directly — leave typedInputBufferRef
stale while still marked reliable. A "type prefix → paste remainder →
Enter" flow would then record just the keyboard-typed prefix, feeding
garbage back into autocomplete ranking.
Require alignment: livePrompt.userInput must end with the typed buffer
before we trust it. reconcilePromptWithTypedInput already snaps the two
together when they *are* aligned — if its endsWith check fails, the
buffer is stale (or mid-navigation) and we fall back to
livePrompt.userInput instead. That drops the #806 fix for this one
paste-bypass case, but the same flow would have hit the same pollution
before this PR, so it's a no-regression fallback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): route out-of-band paste writes through handleInput
Codex flagged (#814 P1) that the reconcile path in fetchSuggestions
has the same stale-buffer failure mode the Enter-record path now
guards against: snippet / keyboard-paste / selection-paste /
middle-click-paste handlers in createXTermRuntime call
writeToSession directly, so typedInputBufferRef only holds whatever
was typed *after* the paste. reconcilePromptWithTypedInput then
treats the pasted prefix as prompt text and trims it, completions
fetch on the truncated input, and accepting a suggestion rewrites
the command incorrectly.
Fix at the source: notify the autocomplete hook with the raw
(pre-bracket-wrap) bytes at every paste site so its keystroke
buffer absorbs them through the same handleInput path keyboard
input uses. handleInput's multi-char paste branch already resets /
aligns the buffer (and invalidates on embedded escape sequences),
so this single extra call per paste site is enough — no new hook
API needed. The existing onData-driven notification at line 684
already covers the non-paste keyboard path, and the snippet /
paste / pasteSelection / middle-click handlers are the only
remaining paths that bypass it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): preserve inner newlines of bracketed-paste input
Codex flagged (#814 P2) that the multi-char-paste branch in
handleInput drops everything before the last newline, but when
bracketed paste is active those newlines are literal input staying on
the zle line — not command terminators. A multi-line paste like
"cmd1\ncmd2" then left only "cmd2" in typedInputBufferRef and the
next Enter recorded / trusted just the tail.
Teach handleInput to recognize the bracketed-paste wrapper
"\x1b[200~...\x1b[201~" and append the enclosed content verbatim
(reliability flag stays on — we know exactly what was added).
Matching change in createXTermRuntime: pass the final (possibly
bracket-wrapped) bytes to ctx.onAutocompleteInput instead of the raw
pre-wrap text so the handle sees the markers when applicable.
Non-bracketed pastes still hit the existing newline-split branch so
each "\n" resets the buffer to the post-terminator tail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(autocomplete): route every prompt consumer through getAlignedPrompt
Each Codex round on #814 surfaced one more code path that needed the
"consume the keystroke buffer only when it's aligned with the live
line" gate: Enter-record, fetchSuggestions (×2), insertSuggestion,
handleSubDirSelect, fetchSubDirForIndex. The fixes were correct but
the guard ended up spelled three different ways across the file:
reconcilePromptWithTypedInput(detectPrompt(term), reliable ? buf : "")
plus a separate `userInput.endsWith(buf)` check in the Enter branch.
That scatter is exactly how the next out-of-band writer gets missed
and regresses #806.
Collapse all six sites onto one helper:
getAlignedPrompt(term, buffer, reliable) → { prompt, alignedTyped }
The helper owns the policy — reliability + endsWith alignment — in one
place. Non-aligned buffers fall through as raw detector output (same
pre-PR behavior, so the worst case for any future forgotten path is
a degrade, not a pollution). Enter-record additionally consumes
alignedTyped, which is only non-null when the buffer truly matches
the tail, so it can record the clean typed command directly without
redoing the endsWith check.
No behavior change from the previous commit; this is purely
deduplication of the alignment guard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(autocomplete): inherit reliability on bracketed paste instead of resetting
Codex flagged (#814 P1 follow-up) that the bracketed-paste branch
unconditionally flipped typedBufferReliableRef back to true. A
history-recall-then-paste flow (↑ marks the buffer unreliable, then
bracketed paste arrives) would then set reliable=true even though
the buffer only contains the pasted tail, not the recalled head.
getAlignedPrompt's endsWith check can pass trivially for a short
paste tail that happens to equal the last N chars of the recalled
line, and Enter would record just the pasted fragment.
Reliability is now inherited across a bracketed paste rather than
reset: if the buffer was already aligned, appending the paste keeps
it aligned; if the buffer was unreliable (post-recall / post-cursor-
move), it stays unreliable and the alignment guard in getAlignedPrompt
falls through to the raw detector result the way it should.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(terminal): extend quick encoding switcher to telnet and serial sessions
Closes#804.
TerminalToolbar only showed the UTF-8 / GB18030 encoding menu for SSH
sessions. Telnet and serial sessions had no runtime control — their
decoder was fixed at session start via charsetToNodeEncoding + Node's
StringDecoder, which only knows utf8/latin1/ascii/utf16le. Users
connecting to legacy telnet daemons or MCU consoles emitting GBK were
stuck with the encoding chosen at connect time and could not switch to
read non-latin text correctly.
Main side (terminalBridge.cjs):
- Swap StringDecoder for iconv-lite on the telnet + serial paths so
GB18030 actually decodes. Local PTY and mosh keep StringDecoder —
local follows the OS locale and mosh frames its own UTF-8, neither
needs a runtime swap.
- Store the decoder through a mutable decoderRef on the session object
so the onData closures stay untouched while a new IPC handler can
swap in a fresh decoder mid-session.
- Add normalizeTerminalEncoding that resolves user-facing charset
names (utf-8/gbk/gb2312/gb18030) into iconv identifiers.
- Register netcatty:terminal:setEncoding, which updates the session's
encoding + decoderRef (and mirrors to serialEncoding for aiBridge /
mcpServerBridge exec calls that still read the legacy field).
Renderer + preload:
- preload.setSessionEncoding now tries the SSH handler first and falls
through to the new terminal handler when the SSH side reports ok:
false (non-SSH sessions don't have session.stream). Single preload
method, one extra IPC round-trip only for telnet/serial, which only
happens on explicit user click.
- Drop the isSSHSession gate in TerminalToolbar; replace with
encodingSwitchSupported = not local, not mosh, not localhost-PTY.
- Terminal.tsx onSessionAttached now syncs the initial encoding for
every protocol that supports it (same gate as the toolbar), not
only SSH.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): decode serial exec output with iconv for non-Buffer encodings
Codex flagged (#812 P1) that session.serialEncoding can now be an
iconv-only label like gb18030 after a user switches encoding via the
new terminal toolbar menu. execViaRawPty then called
data.toString(encoding) on the raw Buffer, which throws
"TypeError: Unknown encoding" for anything outside Node's
utf8/latin1/ascii/utf16le set. The throw landed inside the data
listener so Catty Agent / MCP serial exec calls failed and, worse,
the uncaught path could destabilize the process.
Route the decode through a small decodeBufferAs helper: Node encoding
labels still use Buffer.toString for speed; anything else falls back
to iconv-lite (which already handles the toolbar's GB18030). A last-
resort utf8 fallback keeps the listener from throwing even if iconv
itself rejects an unrecognized label.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(terminal): don't overwrite telnet/serial charset on session attach
Codex flagged (#812 P1) that extending onSessionAttached to sync the
UI encoding for telnet and serial sessions corrupts any host charset
outside the toolbar's two values. terminalEncodingRef is derived from
a useState that only ever resolves to 'utf-8' or 'gb18030', so a host
configured with latin1 / shift_jis had its correct decoder immediately
clobbered with one of those two as soon as the session attached.
SSH is the only protocol that actually needs this sync: its backend
starts in utf-8 regardless of host.charset. startTelnetSession and
startSerialSession already apply options.charset through
normalizeTerminalEncoding, so leaving them alone keeps arbitrary
iconv labels intact; the toolbar's runtime switch remains the path
for users who do want to flip to UTF-8 / GB18030 mid-session.
Restore the SSH-only gate on the sync and document why the new
protocols are intentionally excluded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style(terminal): align encoding menu rows with the rest of the popover
The encoding section used a different template from every other row in
the overflow menu: an uppercase "TERMINAL ENCODING" section header,
then two indented rows with a leading check mark instead of a leading
icon. Next to Open SFTP / Scripts / Terminal settings it read as a
different component and made the popover feel disjointed.
Drop the section header and render both encoding options as plain
menuItemClass rows — Languages icon on the left to match the Zap /
Palette leading-icon pattern, label in the flex-1 slot, and the active
row gets a trailing Check in place of a right-side accessory. A single
divider above them still groups the choice visually without the
uppercase label.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style(terminal): collapse encoding picker into a proper submenu
The previous pass put UTF-8 and GB18030 as flat rows under a separator
inside the main overflow popover. It matched the top rows better but
still looked like a disjoint block of two choices stuck at the bottom.
Turn the encoding picker into a nested submenu so the parent popover
stays a flat list of actions and the choice lives behind a single row
that mirrors the other menu items exactly: Languages icon on the left,
t("terminal.toolbar.encoding") label in the flex slot, the current
value as a muted caption, and a ChevronRight to signal the submenu.
The submenu itself is a second Popover anchored to the right of the
parent. Both popovers are now controlled so picking a value closes
the whole chain in one click, and the parent's onInteractOutside
ignores clicks that land in the submenu portal — otherwise Radix
would treat the submenu click as "outside" the parent and dismiss it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(terminal): drop hostname gate, simplify encoding row label
Two issues in one pass:
1. Codex P2 (#812): encodingSwitchSupported still hard-disabled the
menu when host.hostname === 'localhost'. That was a leftover from
when the only "local" escape hatch was hostname-based, but it
incorrectly blocks telnet / SSH sessions aimed at localhost (test
daemons, forwarded endpoints) which do have a real backend decoder
we can drive. The isLocalTerminal / isMoshSession gates already
cover the true local PTY and mosh cases — drop the hostname check.
2. UI: the submenu trigger carried the current value as a muted
caption next to the label. At w-48 the row ran out of room and
truncated "Terminal Encoding" to "Terminal Enc...". Since the
submenu already marks the active choice with a check, the caption
is redundant. Remove it so the full label fits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): stream-decode serial output with a stateful per-command decoder
Codex flagged (#812 P2) that decoding each serial data event with a
stateless decodeBufferAs call corrupts multi-byte characters on
GBK/GB18030 consoles: serial ports deliver chunks at arbitrary byte
boundaries, so the leading half of a 2-byte char in one event gets
emitted as replacement bytes before the trailing half ever arrives.
Build a stateful decoder once per execViaRawPty call (StringDecoder
for Node-native encodings, iconv.getDecoder for iconv-only labels
like gb18030) and feed every chunk through decoder.write(). On
finish, decoder.end() flushes any partial bytes the decoder is still
holding into the final output before it's handed back to the caller.
Strings pass through untouched, same as before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(terminal): sync SSH encoding on localhost sessions too
Codex flagged (#812 P2) that dropping the 'localhost' check from the
toolbar's encodingSwitchSupported gate left an inconsistency:
Terminal.tsx onSessionAttached still skipped setSessionEncoding when
host.hostname === 'localhost', so a user could pick GB18030, reconnect
a localhost SSH tab, and the backend would restart in utf-8 while the
UI still showed GB18030 — mojibake until manually toggled again.
Drop the hostname clause from the isSSH check here as well. SSH to
localhost is still a real SSH session whose backend starts in utf-8;
the sync is what keeps the UI's picked encoding aligned across
reconnects.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(terminal): re-sync telnet/serial encoding after user opt-in
Codex flagged (#812 P2) that the SSH-only sync left telnet/serial with
a silent UI/backend mismatch across reconnects: a user picks GB18030,
the tab disconnects and retries, startTelnetSession/startSerialSession
re-apply host.charset, and the UI still shows GB18030 — garbled output
until the user toggles again.
An unconditional sync isn't right either (earlier review: it would
clobber arbitrary host.charset values like latin1 / shift_jis that
the UI's two-value state can't represent). Track whether the user
has actually clicked the toolbar menu this session via
userPickedEncodingRef — once set, any subsequent onSessionAttached
for telnet/serial re-applies the picked value; on first attach with
no user action the backend's configured charset stays intact.
SSH keeps the unconditional sync (its backend always starts in utf-8,
so there's no configured charset to preserve).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#805.
The SFTP file-list context menu's Download action only passed the
right-clicked entry to the single-file handler, so selecting N files
and hitting Download still downloaded only one — matching copy/move/
delete, which already iterate selectedFiles, this is the odd one out.
Add onDownloadFiles through the SftpContext → pane callbacks → file-
list chain. In the context menu, if the right-clicked row is part of
pane.selectedFiles and the selection has >1 entry, fall into the new
multi-file path; single selection stays on the existing handler so
its save-dialog UX is unchanged.
The new handleDownloadFilesForSide iterates local selections with the
existing blob path (browser auto-saves each file). For remote panes
it prompts for a target directory once via selectDirectory and streams
every selected file into it — avoids the N-save-dialog prompt storm
that a naive loop would trigger. Mirrors the existing directory-
download branch.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ssh): include legacy HMAC algorithms when legacy toggle is enabled
buildAlgorithms() adds legacy kex, cipher, and host-key algorithms when
the user enables "allow legacy algorithms", but never specified hmac at
all — so ssh2's built-in modern HMAC defaults applied even in legacy
mode. Very old servers (FreeBSD 6.1's OpenSSH circa 2006, per issue #807)
only speak hmac-sha1 / hmac-md5, so MAC negotiation silently settled on
something the server couldn't actually compute. The resulting wrong
exchange-hash MAC then failed host-key signature verification, surfacing
as "Handshake failed: signature verification failed" which misleadingly
looks like a host-key algorithm problem.
Add an explicit algorithms.hmac list in the legacy branch that keeps
modern MACs at the top and appends hmac-sha1 / hmac-md5. Modern servers
will still prefer SHA-2; only servers that literally can't do SHA-2 will
fall back to SHA-1/MD5.
Closes#807.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ssh): skip hmac-md5 when OpenSSL build disables MD5 (FIPS)
Codex flagged (#810 review) that ssh2 validates exact algorithm lists
strictly and FIPS-enabled Node/OpenSSL builds disable MD5. With an
unconditional 'hmac-md5' entry in algorithms.hmac, those builds would
throw "Unsupported algorithm" before the SSH handshake even begins,
turning the legacy toggle into a hard failure even for servers that
only needed hmac-sha1.
Feature-detect MD5 via crypto.getHashes() at module load and only append
'hmac-md5' when it's actually available. hmac-sha1 stays unconditional
— FIPS 140-2 permits HMAC-SHA1 even where SHA-1 is disallowed for other
uses, and ssh2 ships with it in its defaults anyway.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ssh): preserve EtM SHA-1 MAC in legacy algorithm list
Codex flagged (#810 P2) that replacing ssh2's default MAC set with an
exact list omitted 'hmac-sha1-etm@openssh.com', which is present in
ssh2's DEFAULT_MAC. Hosts that only offer EtM SHA-1 MACs would then
fail legacy-mode negotiation with "no matching C->S MAC" even though
they negotiated successfully before the legacy HMAC list was introduced.
Insert 'hmac-sha1-etm@openssh.com' between the SHA-2 EtM entries and
plain hmac-sha1 so modern MACs still take priority and the fallback
chain matches ssh2's own default ordering.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Running `eslint .` from the repo root traversed into local git worktrees
under .worktrees/ and linted their source copies, which don't match the
relative ignore patterns like `electron/**` and `scripts/**`. Result: a
thousand no-undef errors from Node/browser globals in worktree-mirrored
.cjs / .mjs files.
Add .worktrees/** to the global ignores list so worktrees are skipped
regardless of whether node_modules is symlinked or fresh-installed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): address Codex review feedback on PR #808
Three issues raised on the merged editor-tab-form PR:
P1 — Host-picker switch ignored onDisconnect cancellation
SftpPaneDialogs' onSelectLocal / onSelectHost awaited onDisconnect() and
unconditionally called onConnect() regardless of the dirty-editor prompt
outcome. A user who hit Cancel on the "unsaved changes" dialog would still
end up switched to the new host, stranding the editor tabs on a now-stale
connection. Change onDisconnect to return Promise<boolean> (true when the
disconnect actually ran, false on prompt cancel) and gate onConnect on it.
Propagate the new signature through SftpPaneCallbacks, the pane-actions
hook result, and both left/right implementations.
P2 — setIsQuitting leaked across canceled quits
electron/main.cjs called windowManager.setIsQuitting(true) at the top of
before-quit, before the dirty-editor check returned. If the renderer
reported hasDirty=true and the quit was canceled, isQuitting stayed true,
changing later window-close behavior (close-to-tray paths gated on
!isQuitting would stop firing). Move the setIsQuitting call into a
commitQuit() helper that only runs once we've decided to actually proceed
— on hasDirty=true we leave state untouched.
P2 — SftpSidePanel unmount only cleaned active-pane connections
The cleanup effect inspected only leftPane / rightPane (the active tab
per side), missing editor tabs tied to inactive tabs in the same side
panel. On unmount those tabs would survive with a dead save bridge.
Iterate leftTabs.tabs and rightTabs.tabs and collect every connection id
before calling forceCloseBySessions.
npm test — 212/212 pass, tsc error count unchanged from main, lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* perf(editor): stabilize bridge registration effect and memoize filename dedup
Two perf concerns from a focused leak/perf audit of PR #808:
1. Bridge writer effect re-ran on every SFTP state change.
SftpView / SftpSidePanel registered their bridge writer in an effect
with `[sftp]` deps. The `sftp` object identity changes on every SFTP
state update — transfer progress, directory listing, pane updates,
tab switches — so the effect would unregister+reregister constantly
during routine SFTP use. Not a leak (React runs cleanup before each
re-effect), just high-frequency churn on the hot path.
Route through sftpRef and run the effect once; writeTextFileByConnection
is a methodsRef-backed dispatcher that stays valid across sftp re-renders.
2. O(n²) filename disambiguation scan in TopTabs render.
Each editor tab ran `editorTabs.filter(same fileName)` inside the per-tab
render branch. Negligible at ~20 tabs but trivially fixable: build a
fileName→count map in a useMemo keyed on editorTabs and look up in O(1).
Separately noted but NOT fixed here (needs a store refactor and deserves
its own PR): App.tsx subscribing to useEditorTabs() means every keystroke
in an editor tab re-renders the App root. Would need a useEditorTabIds()
selector that only notifies on add/remove.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: ignore local .worktrees/ directory
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): editorTabStore scaffold with single-tab ops
Implements the EditorTabStore class singleton (matching activeTabStore pattern)
with updateContent, markSaved, setWordWrap, setSavingState, close, and subscribe.
Includes useSyncExternalStore hooks and 6 passing unit tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): editorTabStore promoteFromModal with per-session path dedup
* feat(editor): confirmCloseBySession for session teardown
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(sftp): writeTextFileByConnection for pane-agnostic saves
Adds a new `writeTextFileByConnection(connectionId, expectedHostId, filePath, content, filenameEncoding?)` method to `useSftpExternalOperations` that looks up the SFTP pane by connection ID (with a hostId safety check) instead of the left/right-side coupling used by `writeTextFile`. Threads the existing `getPaneByConnectionId` callback through the call site and re-exports the new method via `SftpStateApi`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(editor): editorSftpBridge singleton for out-of-React saves
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(editor): extract TextEditorPane from TextEditorModal
Lift Monaco editor body + toolbar + theme sync + paste fallback into a
pure TextEditorPane component. Adds sftp.editor.maximize i18n key to
en.ts and zh-CN.ts locale files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(editor): drop unused getLanguageId import in TextEditorPane
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(editor): TextEditorModal delegates to TextEditorPane
Replace the monolithic modal (560 lines including full Monaco setup)
with a thin Dialog shell (~150 lines) that owns content/saving/saveError/
languageId state, save orchestration, and dirty-check on close, then
delegates all editor chrome to <TextEditorPane chrome="modal" />.
Exports TextEditorModalSnapshot for the optional onPromoteToTab callback
so callers can later wire tab promotion (Task 12) without breaking the
existing interface — the new prop is optional and existing callers
(SftpOverlays.tsx) are source-compatible with zero changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(editor): include fileName and wordWrap in TextEditorModalSnapshot
Task 12 will populate the promoted tab with these fields, so the snapshot
must carry them from the modal at maximize time.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): UnsavedChangesDialog three-button confirm
* fix(editor): resolve UnsavedChangesDialog re-entrance and unmount leaks
- Re-entrance: if prompt() is called while a prior prompt is still pending,
cancel the prior one so its caller doesn't hang forever.
- Unmount: resolve any in-flight prompt as "cancel" in the effect cleanup
so awaiters don't leak when the provider unmounts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): TextEditorTabView tab-form shell
Add TextEditorTabView component that binds an editorTabStore entry to
TextEditorPane, with CSS display:none toggling for inactive tabs so the
Monaco instance persists across tab switches. Also adds setLanguage
public method to EditorTabStore (lands Task 15's intent early — Task 15
can be a no-op).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(editor): read live store state in TextEditorTabView handlers
React state snapshot lags the store by a microtask. Closing over `tab`
meant a keystroke between Monaco's onChange and a Ctrl+S would write
stale content and mark a stale baseline. Read via editorTabStore.getTab
at call time instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): dispatch editor:* tab ids in App and activeTabStore
- Add EDITOR_PREFIX, isEditorTabId, toEditorTabId, fromEditorTabId helpers
- Add useIsEditorTabActive hook to activeTabStore
- Update useIsTerminalLayerVisible to exclude editor tabs
- Import useEditorTabs and TextEditorTabView into App.tsx
- Append editor tab ids (editor:<id>) to allTabs in hotkey handler
- Mount TextEditorTabView per editorTab with CSS visibility toggling
- Add editorTabs to executeHotkeyAction useCallback dependency array
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(editor): render editor tabs in TopTabs with icon/dirty/tooltip
- Add `fromEditorTabId`, `isEditorTabId` imports to TopTabs.tsx
- Add `FileCode`, `FileText` icons; use FileCode for code-like extensions
- Extend `TopTabsProps` with `editorTabs`, `onRequestCloseEditorTab`, `hostById`
- Build `editorTabMap` for O(1) lookup; add `editor` branch in `orderedTabItems`
- Render editor tab chrome matching terminal tab style: file icon, dirty dot (●),
filename with disambiguation suffix for duplicate filenames, close button
- In App.tsx: add stub `handleRequestCloseEditorTab`, `orderedTabsWithEditors`,
pass new props to `<TopTabs>`
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(editor): hoist editor-tab code-extension regex and use onSelectTab
- Move CODE_EXTENSIONS_RE to module scope so it isn't recompiled per render.
- Call onSelectTab(tabId) for consistency with other tab types, instead of
reaching into activeTabStore directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): maximize modal to tab and dirty-confirm tab close
Wire onPromoteToTab from TextEditorModal through SftpOverlays and
useSftpViewFileOps so clicking the maximize button snapshots editor
state into editorTabStore and activates the new editor tab.
Replace the stub handleRequestCloseEditorTab in App.tsx with a real
dirty-confirm flow using UnsavedChangesProvider render-prop: clean tabs
close immediately, dirty tabs prompt save/discard/cancel, and save
routes through editorSftpBridge with markSaved on success.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(editor): register SFTP bridge and gate session close on dirty editor tabs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(editor): make onDisconnect async so host-picker waits for dirty check
The session-close dirty gate added in Task 13 made onDisconnect async, but
the host-picker in SftpPaneDialogs still called it synchronously before
kicking off onConnect — a fire-and-forget that raced past the dirty prompt
and let unsaved editor tabs slip through. Propagate the Promise return type
through SftpPaneCallbacks / SftpPaneDialogs / useSftpViewPaneActionsResult
and await it at the host-picker call sites.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): block app quit while editor tabs are dirty
Add a before-quit IPC guard that asks the renderer whether any editor
tab has unsaved changes. If dirty tabs exist, preventDefault() blocks
the quit and a warning toast is shown. The app quits normally once
editors are clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(editor): add 5s timeout fallback to quit-guard IPC check
If the renderer crashes or throws before reporting back, the quitGuard
would stay busy forever and the app could not be quit. Fall back to
force-quit after 5 s if no reply arrives.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): quit-guard uses quitConfirmed flag to prevent re-entry loop
The prior flow reset quitGuardChannelBusy before calling app.quit(), which
on macOS re-fires before-quit and re-entered the dirty check with the flag
cleared — creating an infinite IPC loop. Introduce a separate quitConfirmed
flag that commits to quitting before app.quit() fires, so the re-entry takes
the fast path.
Also extract QUIT_GUARD_TIMEOUT_MS and clarify that a concurrent quit while
a check is in flight is swallowed (preventDefault) rather than letting the
second event through.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): use absolute inset-0 for tab panel and add sr-only DialogTitle
Two bugs surfaced during the first dev-server smoke test:
1. Editor tab content was blank because TextEditorTabView used only
className="h-full", while its sibling panels (VaultView, SftpView,
TerminalLayerMount, LogView) all fill their flex-1 parent via
`absolute inset-0`. In normal flow the editor tab collapsed to zero
height. Match the sibling convention.
2. Radix printed an accessibility warning because the Task 7 refactor
pulled the DialogTitle out of DialogContent and into the Pane header
(now a plain span). Add a visually hidden DialogTitle that mirrors the
filename, so screen readers have a title without showing it twice.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): raise tab panel z-index to 20 so it sits above TerminalLayer
TerminalLayer's root is visibility:hidden when the active tab is an editor
tab, but its inner panels set `absolute inset-0 z-10` on their own and those
still paint. Without an explicit z on the editor tab panel, TerminalLayer's
inner bg-background div was covering the Monaco content, producing a blank
screen.
Also add bg-background to the wrapper so the editor tab paints an opaque
surface (matches the pattern VaultViewContainer / TerminalLayer follow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): show host label and remote path next to filename in tab header
The editor tab form previously only showed the bare filename in its header,
which is ambiguous when the same filename is open against multiple hosts.
Add an optional subtitle prop on TextEditorPane and populate it from the
tab form with `<hostLabel>:<remotePath>` rendered in muted text beside the
filename. The modal keeps its existing filename-only header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): bridge supports multiple useSftpState instances
useSftpState is instantiated in both the top-level SftpView and the
terminal's SftpSidePanel, each owning its own pane registry. The editor
bridge previously stored only one writer, so maximizing a file opened from
the terminal side panel registered nothing (bridge was owned by SftpView
which may never have mounted) and save failed with "bridge not registered".
Change the bridge to track a Set of writers and dispatch by trying each
until one owns the connectionId (signalled by its specific "connection no
longer available" error). Add registerEditorSftpWriterScoped that returns
an unregister fn so each instance's cleanup removes only its own entry.
Register in both SftpView and SftpSidePanel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): Cmd+W closes editor tab + terminal close forces tab close
Two behaviors added after user feedback from dev-server smoke-test:
1. Cmd/Ctrl+W (the closeTab hotkey) previously did nothing on editor tabs
because executeHotkeyAction had no branch for editor:* ids. Add one that
reaches into the UnsavedChangesProvider render-prop's close flow via a
ref, routing through the existing dirty-confirm path.
2. Closing a terminal tab unmounts its SftpSidePanel which destroys the
useSftpState instance that owned the connection. Any editor tab promoted
from that panel would then be stuck — bridge gone, save channel dead.
On SftpSidePanel unmount, gather the connection ids it owned and call a
new editorTabStore.forceCloseBySessions to drop matching editor tabs.
Dirty state is dropped because the user closed the terminal knowing the
file was open — there is no save channel left anyway.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): Cmd/Ctrl+W works when focus is inside Monaco
Monaco's internal key-event dispatcher swallows keydown before the
capture-phase handler on the Pane's root div can see it, so the global
hotkey dispatcher never got the chance to close the editor tab when the
editor had focus. Register a Monaco editor command for the close-tab
keybinding and route it through a handleCloseRef — mirrors the same
pattern used for Cmd/Ctrl+S. Also drop the modal-only guard in the
capture-phase handler so the outer-chrome path works in tab mode too.
TextEditorTabView now receives an onRequestClose(tabId) prop that App.tsx
wires via the render-prop-exposed handleRequestCloseEditorTabRef, same
mechanism as the hotkey-dispatcher path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): fall back to Vaults when forceCloseBySessions removes the active tab
Closing a terminal tab triggers SftpSidePanel unmount which force-closes its
editor tabs. If the editor tab being removed happened to be the active tab
(user maximized → then closed the owning terminal from another path), the
app ended up on a stale activeTabId with no selected tab and blank content.
Inside forceCloseBySessions, if the active tab was one of the removed
editor ids, redirect to 'vault'. Picking a more sophisticated neighbor
would need the full orderedTabs list which isn't reachable from this layer;
Vaults is always valid.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Enlarge app icon squircle so it matches other macOS dock apps
public/icon.png was generated from logo.svg which keeps the Apple HIG
grid margin (~100px all around the 824x824 squircle in a 1024 canvas).
Most third-party macOS apps (WeChat, Office, Messages, etc.) enlarge
their squircle to fill ~90% of the canvas, so Netcatty's icon looks
visibly smaller than its neighbors in the dock.
Introduce public/icon.svg as a dedicated app-icon source that tightens
the viewBox to 68 68 888 888 so the squircle renders at ~93% fill, then
regenerate public/icon.png from it. logo.svg stays untouched since it
is shared with the splash screen and tray template.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Dial back icon squircle fill from 93% to 88%
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Snippet rows used a padding-based offset to account for the chevron
column in package rows, but the flex gap between chevron and icon
wasn't being compensated so the FileCode icon sat 4-6px to the left of
the Package icon above it. Mirror the package row's flex layout
literally by rendering an invisible chevron placeholder, so both row
types share the same column structure.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Render snippets sidebar as an expandable tree (#800)
The terminal sidebar used breadcrumb navigation, so switching between
packages meant clicking out and back in. Replace that with a single
tree view where each package row has a chevron to expand/collapse
(SFTP-style), so snippets across multiple packages stay visible and
reachable without drilling.
- All discovered packages default to expanded, so the tree matches the
user's expectation of seeing everything at once.
- Search flattens to a list of matching snippets regardless of nesting,
each annotated with its package path so the origin is still clear.
- Implicit ancestor packages (e.g. "a/b/c" implies "a" and "a/b") are
materialized so deeply nested snippets aren't orphaned when a parent
package isn't explicitly listed.
- Depth-based left padding + chevron rotation mirror the SFTP tree
view's affordances.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Unify snippet row typography with tree + move command to tooltip
Snippet rows were rendered as two-line blocks (label + inline command
preview), which made them visually taller and heavier than the
single-line package rows in the tree, and long commands overflowed the
container. Collapse them to single-line rows that match the package row
layout exactly (same text size, same padding, aligned icon column) and
surface the full label + command text in a tooltip on hover.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Preserve collapsed packages across snippet refreshes (codex)
The auto-expand effect compared prev.size to normalizedPackages.size to
decide whether to repopulate, but collapsed rows shrink prev.size, so any
later snippet/package change would trip the condition and overwrite the
user's collapse state with a bulk re-expand.
Track the set of packages ever observed in a ref and only auto-expand
paths that are new since the previous render.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous template icon was a tiny solid silhouette that didn't fill
the menu bar slot. Rebuild it by extracting the cat head, ears, paws,
squinty eyes and nose/mouth paths directly from public/logo.svg so the
tray icon matches the app icon character, then tighten the viewBox so
the cat fills the canvas.
Windows/Linux tray-icon.png is unchanged.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The tray icon was force-resized to 16x16 on all non-macOS platforms, so
Windows had to upscale it at every DPI scale above 100%. Attach the
existing @2x asset as a HiDPI representation instead and let the OS pick
the right pixel size per scale factor.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bulk-action bar for multi-select (selected count, Select All /
Deselect All / Delete / close) was rendered inside the Hosts
section, so it scrolled out of view as soon as the user moved
past the first row of cards.
Hoist the bar out of the scroll container and render it as a
sibling right after the top header. It is now always visible below
the header while multi-select is active in the Hosts section, and
slims down visually:
- Single flat row (no inner pill, no secondary border)
- Compact button sizing: h-7, px-2, text-xs, icon-12
- Bottom-only border for separation from the scroll area
- Count label forced to h-7 + leading-none so it vertically
centers against the buttons
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Collapse four terminal toolbar actions behind a "More" popover
The terminal status-bar toolbar had seven visible icon buttons
(SFTP, Encoding, Scripts, Theme, Highlight, Compose, Search) plus
the close button. That's a lot of icons for a toolbar that sits
right above the terminal output — it reads as cluttered and pushes
the connection info / host name around on narrow tabs.
Fold the four "opener" actions — SFTP, Encoding, Scripts, Terminal
Settings — behind a single `MoreHorizontal` (⋮) popover. The three
mid-session toggles (Highlight, Compose, Search) stay in the bar
because they're used repeatedly during a session.
- components/terminal/TerminalToolbar.tsx:
* Add MoreHorizontal import, a shared `menuItemClass` style for
popover rows.
* Replace the four inline Buttons with a single Popover whose
content lists each action as an icon + label row.
* Inline the Encoding sub-popover into the same menu: a
Languages-icon section header followed by two `Check`-marked
radio-like rows for UTF-8 / GB18030 — still only rendered when
`isSSHSession && onSetTerminalEncoding`.
* SFTP row respects the existing connected-state: disabled +
50% opacity until the session is connected, and label falls back
to "availableAfterConnect".
- application/i18n/locales/en.ts, zh-CN.ts:
* New `terminal.toolbar.more` key — "More actions" / "更多操作"
— used as the ⋮ button's aria-label and tooltip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Move terminal overflow menu to end and use vertical dots
The ⋮ overflow trigger was the first icon in the toolbar with a
horizontal-dots glyph. Visually it read as the primary action and
competed with the mid-session toggles next to it.
Move the Popover to the end of the toolbar (just before the close
X when shown), switch the icon to MoreVertical, and flip the
popover alignment to `end` so it opens leftward from the right
edge.
Toolbar order is now: Highlight → Compose → Search → ⋮ → (X).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add terminals to workspace + New Workspace from QuickSwitcher
Two entry points share a single multi-select picker that lets the
user add Local Terminal + any combination of hosts into a workspace:
1. Focus-mode sidebar "+" button appends the selected targets to the
active workspace as new panes.
2. QuickSwitcher "New Workspace" button (small inline action next to
the Jump To hint) spins up a brand-new workspace tab populated
with the selected targets.
## Changes
### domain/workspace.ts
- pruneWorkspaceNode now rebalances surviving siblings to EQUAL
sizes after removal, instead of re-normalising the prior skew.
Matches the "auto-redistribute on close" expectation.
- New appendPaneToWorkspaceRoot(root, sessionId, direction='vertical'):
if root already splits in the requested direction, pushes the new
pane onto its children and resets sizes to equal; otherwise wraps
root + new pane in a new 0.5/0.5 split. Flattens long chains of
appends instead of producing degenerate nested trees.
### application/state/useSessionState.ts
- appendHostToWorkspace(workspaceId, host, direction?) — atomic
"build a session for this host and append it to the root", keeps
activeTab on the workspace and focuses the new pane.
- appendLocalTerminalToWorkspace(workspaceId, options?, direction?)
— mirror of the above for local shells.
- createWorkspaceFromTargets(targets, name?) — accepts a mixed list
of {kind:'local',...} / {kind:'host',host} and creates a new
workspace with one pane per target. Defaults viewMode to 'focus'
so the QuickSwitcher flow lands in the sidebar layout.
- All three exported from the hook.
### components/workspace/AddToWorkspaceDialog.tsx (new)
QuickSwitcher-styled multi-select picker:
- Fixed top-center overlay, same chrome as QuickSwitcher (border,
shadow, rounded-xl, borderless search input, bg-primary/15 cursor).
- Two sections: Local Shells (currently just Local Terminal) and
Hosts. Hover follows keyboard cursor.
- Toggle rows with click or Space / Enter; ⌘/Ctrl+Enter submits;
Esc closes. Right-side Check marks visible items.
- Thin footer bar with Cancel + "Add N" button.
### App.tsx
- Root-mounted single instance of AddToWorkspaceDialog with a
discriminated-union state:
{ mode: 'append'; workspaceId } | { mode: 'create' } | null.
- onAdd dispatches based on mode — append loops through the picker
targets calling the two append helpers; create calls
createWorkspaceFromTargets once.
- TerminalLayer's focus "+" now sends an onRequestAddToWorkspace
(workspaceId) up to App instead of owning its own dialog.
- QuickSwitcher's onCreateWorkspace callback repurposed to open the
dialog in create mode (replaces the older CreateWorkspaceDialog
route for this specific flow).
### components/TerminalLayer.tsx
- Dropped the inline AddToWorkspaceDialog + addHostPanelOpen state;
replaced the two append callbacks with a single
onRequestAddToWorkspace prop wired to the "+" button.
- Focus-sidebar header: replaced the "Terminals · N" counter with an
immersive borderless search input (bg-transparent, shadow-none,
termFg color) for filtering the terminal list; "+" and Columns2
buttons moved to the right.
- Session list filtered client-side by the search term across
hostLabel / hostname / username.
### components/QuickSwitcher.tsx
- Re-introduced onCreateWorkspace prop (was removed as unused).
- "New Workspace" inline button (Plus icon + label) sits on the
right of the Jump To hint row: border, rounded, hover bg. Click
fires onCreateWorkspace then closes QS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add configurable New Workspace shortcut
Mirrors QuickSwitcher's "+ New Workspace" button via a keyboard
binding so the dialog can open in one keystroke without passing
through QS.
- domain/models.ts: new DEFAULT_KEY_BINDINGS entry id=new-workspace,
action=newWorkspace, default ⌘+Shift+J (Mac) / Ctrl+Shift+J (PC).
Audited the defaults — only quick-switch uses J (⌘+J), so the
shifted combo is free. The binding sits in the 'app' category so
it shows up in Settings → Shortcuts and can be rebound by the user.
- application/state/useGlobalHotkeys.ts: wire newWorkspace into the
HotkeyActions interface, getAppLevelActions() allowlist, and the
global keydown switch so the scheme-driven handler dispatches it.
- App.tsx: handle case 'newWorkspace' inside executeHotkeyAction by
calling setAddToWorkspaceDialog({ mode: 'create' }) — same entry
as QuickSwitcher's button, just without having to open QS first.
- application/i18n/locales/zh-CN.ts: add '新建工作区' translation for
settings.shortcuts.binding.new-workspace. English falls back to
the KeyBinding.label field ("New Workspace"), so no en.ts change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address codex P1: don't check setState flag after the updater returns
Codex flagged that appendHostToWorkspace / appendLocalTerminalToWorkspace
were racy: both flipped an `inserted` flag inside setWorkspaces'
updater and then read it synchronously to decide whether to commit
the matching session via setSessions. React does NOT guarantee
updaters run synchronously (concurrent rendering, StrictMode
double-invoke, etc.), so the flag could still be false at the read
site even though the workspace exists. In that case setSessions was
skipped while the queued workspace update could still insert a new
pane referencing newSessionId — leaving a pane with no backing
session in state.
Fix: add a workspacesRef kept in sync with the workspaces state on
every render, and perform the existence check synchronously *before*
queuing any setState. Once we've confirmed the workspace exists on
the latest committed state, both setWorkspaces and setSessions are
called unconditionally, so they can never diverge.
The ref approach also correctly handles the multi-target append
loop path — React batches the updaters and applies them in sequence,
so sibling pane/session writes land in matching order.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address codex P1+P2: narrow prune rebalance; append in root direction
### P1 — pruneWorkspaceNode over-rebalanced ancestor splits
The equal-sizes rebalance was unconditional during the recursive
walk, so closing a pane deep in one branch also rewrote unrelated
ancestor ratios (e.g., a root 0.8/0.2 vertical split got normalised
to 0.5/0.5 when a grand-child horizontal pane closed).
Now each split level tracks whether it actually lost a DIRECT
child. Only splits where a direct child disappeared get their
siblings reset to equal sizes. Ancestors whose direct children all
survived keep their original ratios (defensively re-normalised in
case a descendant subtree collapsed shape).
### P2 — Append path ignored the root's current direction
onAdd in App.tsx called the two append helpers without a direction,
so both defaulted to 'vertical'. appendPaneToWorkspaceRoot only
flattens into the root split when the directions match; if the
workspace root was horizontal (e.g., user split top/bottom earlier),
each append wrapped the entire existing tree into one side of a new
vertical split — existing panes crammed into one branch, new pane
hoarding half the space.
Read the current root direction out of the target workspace and
pass it down so new panes become peers of the existing root
siblings regardless of horizontal vs vertical.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address codex P2: allow serial hosts in create-workspace picker
The picker used to filter out every host with protocol='serial'
regardless of mode. That was correct for append mode (the
appendHostToWorkspace helper has no serial path and early-returns)
but a regression for create mode — the old createWorkspaceWithHosts
flow passed serial hosts through and createWorkspaceFromTargets
still builds a SerialConfig-backed session for them, so there was
no reason to block them in the "+ New Workspace" entry.
Move the filter from the dialog up to App.tsx:
- AddToWorkspaceDialog drops the serial filter; selectableHosts is
simply the hosts prop.
- App.tsx passes `hosts.filter(h => h.protocol !== 'serial')` when
mode is 'append', and the full list when mode is 'create'.
Result: users can once again build a workspace from serial hosts
via QuickSwitcher's "+ New Workspace" button or the ⌘/Ctrl+Shift+J
hotkey, while append-to-existing keeps its earlier safe behaviour.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address codex P2: don't commit session when append target disappears
Follow-up to the earlier ref-based guard. The ref check eliminates
the common "workspace already gone" case but still leaves a small
race: if closeWorkspace runs between the ref read and setWorkspaces'
updater firing, prev.map returns the unchanged workspaces but
setSessions / setActiveTabId still execute — leaving an orphan
session whose workspaceId points at a deleted workspace and jumping
activeTabId to a closed tab.
Nest setSessions + setActiveTabId inside the setWorkspaces updater
so the writes are gated on the same authoritative match used for
the tree update. The setSessions updater also de-dupes by newSessionId
so React 18 StrictMode's dev-time double-invoke of the outer updater
doesn't append the same row twice. Same pattern applied to
appendLocalTerminalToWorkspace.
The existing closeSession already uses the nested-setState shape, so
this matches the codebase convention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Settings > Application used `text-3xl font-semibold` on
`{appInfo.name}`, which resolved to lowercase "netcatty" (from
electron's app.getName() / package.json). The Vault sidebar already
renders the brand as `text-xl font-black italic tracking-tight`
with mixed-case "Netcatty", so the two brand surfaces didn't
match — same logo, different wordmark weights and capitalization.
Use the Vault's italic/heavy treatment in Settings too (keeping
the hero text-3xl size) and hardcode "Netcatty" mixed-case so the
wordmark is consistent everywhere the app presents its identity.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Polish workspace focus-mode sidebar
- Decouple from side panel position: replace flex-row-reverse on the
outer row with order-last on the side panel itself, so the workspace
focus-mode sidebar and terminal area stay in source order (sidebar
on the left) regardless of whether the terminal side panel is
pinned left or right.
- Make the sidebar width user-resizable. New storage key
STORAGE_KEY_WORKSPACE_FOCUS_SIDEBAR_WIDTH with a useStoredNumber
default of 224px (matches the old w-56), clamped 160..480. Drag
handle sits on the right edge using the same pattern as the side
panel; rAF-throttled mousemove, persisted on mouseup.
- Paint the sidebar with resolvedPreviewTheme.colors.background /
.foreground so it reads as one continuous surface with the focused
terminal's output area instead of a distinct tinted panel. The
border-r is kept as a thin separator from the terminal column.
- Session rows swapped from <div> to RippleButton to match the Vault
sidebar's click ripple feel, and restyled to avoid the old
primary-tinted selection:
* selected: bg-foreground/10 text-foreground (soft neutral over
the terminal-theme sidebar bg)
* unselected: bg-transparent text-foreground/75
* font weight upgrades to semibold on selected; font-size is fixed
* hover:text-inherit pins text color on hover so the ghost
variant's hover:text-accent-foreground doesn't flip the title
color when the cursor passes over a row
- Drop the former `border border-primary/30` selection outline and
the primary-tinted row bg entirely.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address codex P1: use terminal-theme colors for focus sidebar rows
Codex flagged that the session rows were mixing two theme systems:
the sidebar now paints with resolvedPreviewTheme (terminal theme),
but row classes like bg-foreground/10, text-foreground, and
hover:bg-foreground/15 resolve against the app theme CSS vars. With
followAppTerminalTheme off and app/terminal themes diverging (e.g.
light app + dark terminal), row text and selection tint no longer
match the surface and can become low-contrast or invisible.
Derive every row color from resolvedPreviewTheme.colors via
color-mix and apply via inline style:
- selectedBg = foreground 10% over transparent
- selectedHoverBg = foreground 15%
- unselectedHoverBg = foreground 10%
- unselectedFg = foreground 75% mixed toward termBg
- mutedFg = foreground 55% mixed toward termBg (used for
"Terminals · N" counter, switch-to-split icon color, fallback Server
icon, and the username@host secondary line).
- separator = foreground 10% over termBg (right-border and
header bottom-border now use this instead of border-border/50,
which was also app-theme bound).
Hover bg swap goes through onMouseEnter/Leave rather than
hover:bg-* utilities, since Tailwind arbitrary values can't easily
inject color-mix hover variants and we want terminal-theme alpha
either way.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The old compose bar had a rounded gradient card with an inset box
shadow, a bordered inner textarea, and a prominent filled Send button
— visually heavy, and sitting on top of the terminal it looked like a
separate panel instead of a prompt line.
Rework it to sit flush on the terminal-theme background, Claude Code
compose-area style:
- Outer container uses resolvedBg directly (no gradient, no rounding,
no box-shadow); separator from terminal output is a single 8%-alpha
hairline border-top.
- Textarea is fully borderless and transparent — no bg, no border, no
focus ring, no inner shadow. Text sits directly on the terminal bg.
- Send button removed entirely; Enter was already the send key, and
the filled button was just visual weight. Shift+Enter still inserts
a newline, Esc still closes.
- Close (X) button shrunk to a minimal 6x6 ghost; transparent at rest,
only gains a 10% overlay + full fg on hover.
- Placeholder bumped from opacity-40 to opacity-70 so the "press Enter
to send" hint is legible against dark and light terminal themes.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The terminal-side ScriptsSidePanel was the surface the #780 reporter
was actually looking at when they asked for right-click delete/modify
on snippets. PR #783 closed the issue by adding a trash icon in the
Vault edit panel, but the sidepanel snippet rows were still plain
<button>s with no context menu — so the original complaint
("右键可以弹出一个菜单, 可以包含'删除, 修改'等操作") remained unaddressed
at the exact spot the screenshot came from.
Changes:
- ScriptsSidePanel: wrap each snippet row in a ContextMenu with Edit
and Delete items. Menu actions dispatch window events instead of
threading new callbacks — matches the existing netcatty:snippets:add
pattern the + button already uses.
- QuickAddSnippetDialog: accept an optional onUpdateSnippet prop and
listen for netcatty:snippets:edit. Prefills label/command/package
from the dispatched snippet, and on save preserves the snippet's
original tags/targets/shortkey/noAutoRun (the dialog only exposes
the three quick-edit fields). Title flips to snippets.panel.editTitle
in edit mode.
- App.tsx: pass onUpdateSnippet wired to updateSnippets(map-replace),
and register a window listener for netcatty:snippets:delete that
filters the deleted id out of snippets. Delete needs no UI so it
doesn't go through a dialog.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Replace app logo across window icon, tray, splash, and in-app brand
- public/logo.svg: new netcatty mark
- public/icon.png: regenerated 1024x1024 from new SVG (source for
electron-builder — .icns/.ico rebuilt automatically at pack time)
- public/dmg-fix-icon.png: regenerated 1024x1024
- public/tray-icon{,@2x}.png: regenerated color 16/32px for Linux/Windows
- public/tray-iconTemplate{,@2x}.png: regenerated monochrome silhouette
for macOS menu bar (background stripped, foreground flattened to
black on transparent so template-image rendering produces a clean
mask)
- components/AppLogo.tsx: render the new logo as a static <img>. The
old hand-coded inline SVG bound fills to the accent CSS variable;
the new mark has a fixed palette, so callers keep their sizing /
rounding classes via className while the asset itself is a single
file served from /public.
- index.html: splash screen now uses the same /logo.svg via <img>,
with border-radius for the rounded-square frame.
* Polish logo: theme the in-app mark, gloss the OS icon, shrink cat
- components/AppLogo.tsx: back to an inline SVG. Background rect fills
with hsl(var(--primary)) so the in-app brand follows the theme
accent (was fixed navy when imported as <img>). Cat scaled to 68%
of the frame and centred so it doesn't crowd the edges at small
sidebar sizes.
- public/logo.svg + regenerated PNGs: polished OS icon variant with a
large rounded-square clip (rx 224 on 1024), top-left spotlight
radial gradient, subtle top sheen + bottom darkening, and an inner
edge vignette for a slight chamfer. The cat is shrunk to the same
68% as the in-app logo for visual consistency.
- Monochrome tray template (macOS menu bar) is rebuilt from the
shrunk-cat path set with all fills flattened to black; keeps a
clean silhouette instead of a filled rounded square.
* Smooth paws, richer gloss on app icon
- Drop the dark toe/claw detail paths from the source illustration
(indices 22-25, 30, 35, 37, 39 — the ones tracing vertical claw
dividers inside the paws). At small sizes those read as teeth/
claws; paws now render as clean rounded blobs.
- public/logo.svg (OS icon source): richer depth pass —
* two-tone navy vertical gradient (lighter top, deeper bottom)
* brighter upper-left spotlight for glassy highlight
* top sheen + bottom darkening for sheen-across-curve effect
* soft elliptical ground shadow beneath the cat to anchor it
* 2% inner edge stroke to crisp the rounded-square chamfer
- components/AppLogo.tsx: regenerated with the same cleaned cat set,
still themed via hsl(var(--primary)). The in-app mark stays flat
(no gloss) because the effect adds nothing at 20-40px sidebar
sizes and would fight theme accents.
- All raster variants (icon.png, dmg-fix-icon.png, tray color + tray
macOS template) rebuilt from the cleaned sources.
* Respect Apple icon safe area; drop gloss, add thin border
macOS icon was rendering to the full 1024x1024 canvas, so it looked
noticeably larger than neighbour apps (VS Code, Ghostty, Zed) in the
Dock. Apple's Big Sur+ convention puts the artwork body inside an
~824x824 safe area centred in a 1024 canvas, which is how those apps
are sized.
- public/logo.svg: artwork body is now 824x824 centred with ~100px
transparent padding. Corner radius 185 (close enough to the macOS
squircle at Dock scale). Cat rescaled so it keeps the same 68%
proportion within the smaller body.
- Gloss layers (spotlight / sheen / ground shadow / vignette) removed
per request — went for a Ghostty-style clean look instead.
- Thin white inner border (stroke 3px, 22% opacity) outlines the
rounded square for definition.
- Tray PNGs for Linux/Windows keep the full-bleed variant (tray slots
expect the icon to fill the space, unlike the Dock safe area).
- components/AppLogo.tsx unchanged conceptually — it still fills its
own bounding box via hsl(var(--primary)); the Apple safe-area rule
is Dock-specific, not relevant to in-app rendering.
* AppLogo: tighten corner radius to match previous (rx 18.75%)
Previous AppLogo used rx=12 on a 64 viewBox (18.75%). The inline
replacement had rx=224 on a 1024 viewBox (21.9%), which combined
with the caller's rounded-xl class read noticeably rounder in the
sidebar. Drop to rx=192 on 1024 viewBox so the in-app mark matches
the old proportions.
* Beef up icon border so it survives Dock downscaling
3 px at 22% opacity disappeared when rasterised down to ~128 px Dock /
Launchpad size. Bumped stroke-width to 8 px and opacity to 40% so the
inner highlight reads as ~1 px at Dock scale. Stroke is inset by
stroke-width/2 so it sits fully inside the rounded-square body (no
anti-alias bleed outside the safe area). Same treatment applied to the
full-bleed tray variant.
* Enlarge cat inside icon tile (68% -> 85% of body)
Dock render had too much navy margin around the mark. Bump the cat's
scale so it fills 85% of the Apple safe-area body while keeping a
visible bezel to the rounded corners and the inner border. Tray color
variant and macOS template (scale 0.9, no border) follow the same
scale-up.
* Add ripple effect on sidebar nav and tidy logo in vault header
- Add RippleButton wrapper + ripple keyframe; use it for the six vault
sidebar nav entries (Hosts, Keychain, Port Forwarding, Snippets,
Known Hosts, Logs) so clicks get a subtle material-style ripple.
- Shrink vault sidebar AppLogo to h-8 w-8 and drop the outer rounded-xl
so the visible corner comes from the SVG's own rx instead of the
container clip.
- Relax AppLogo tile rx/ry to 144 for a more moderate corner radius.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* AppLogo: bump tile corner radius back up to rx 18.75%
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Unify manager toolbars, tighten tabs and vault sidebar title
- Manager toolbars (Keychain, KnownHosts, PortForwarding, Snippets)
normalised to h-14 / h-10 controls with bg-secondary/80 backdrop-blur
and the shared bg-foreground/5 secondary button treatment, so Hosts /
Keychain / Known Hosts / Port Forwarding / Snippets headers size and
tint identically.
- Keychain filter tabs: drop primary tint and cert-count pill; reuse
the same foreground/5 vs foreground/10 active states as other
managers. Search input grown to h-10 to match.
- Known Hosts: removed the leftover text-xs on Scan System / Import
File so they inherit Button's text-sm like every other action.
- TopTabs: drop the 2px active-accent top line and add rounded-t-md +
overflow-hidden so active tabs read as a clean soft tab shape rather
than a banner.
- VaultView sidebar: wordmark grown to text-xl font-black italic with
tightened tracking; logo gap trimmed from 3 to 2.5; outer bg dropped
from secondary/80 to flat secondary to sit flush against the
toolbars.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Vault global search spans all groups/packages (#777)
Search was scoped to the current group (hosts page) or the current
package (snippets page), so a host or snippet the user wanted to find
could stay hidden unless they first navigated into the right group —
especially confusing with the "root only shows ungrouped hosts" setting
enabled.
When the search box is non-empty:
- hosts: skip the selectedGroupPath / showOnlyUngroupedHostsInRoot
filters entirely. Each matching card shows a small outline badge with
the host's group so cross-group origin is visible.
- snippets: skip the current-package filter. Hide the sub-package grid
(would be redundant alongside a flat cross-package match list). Each
snippet card shows the package path as a small badge.
Tree view already followed this "search crosses groups" shape — see
`treeViewHosts` — so this aligns the flat grid/list views with it.
* Show no-results feedback when snippet search is empty (#777)
Addresses Codex P2 review on PR #785. With the package tile grid hidden
during search and no matching snippets, the content area was blank and
the global empty state did not render (it requires snippets.length === 0).
Add a dedicated no-results panel for the "user is searching and nothing
matched but there are other snippets" case, with i18n for en and zh-CN.
* Drop group/package badges on search results (#777)
Search is itself a filter, so decorating each result card with the
group/package it came from added visual noise without adding
information. Only difference vs. pre-search rendering now is that the
result set spans all groups/packages.
* Fix snippet no-results empty state with packages present (#777)
Addresses Codex P2 on 4a778e63. The empty-state gate was
displayedPackages.length === 0, but package tiles are hidden during
search regardless of count. Any workspace that had packages was
rendering a blank content area on zero-match queries because that
guard never passed. Drop the package-count condition — the flat
snippet list is the only visible surface while searching.
* Cover package-only workspaces in snippet search no-results (#777)
Addresses Codex P2 on ccdf6afc. snippets.length > 0 also excluded
workspaces where the user has only created packages (no snippets yet).
The correct gate is the inverse of the global empty state's condition,
so we fall back whenever the workspace isn't completely empty.
* Block empty/shrunk pushes when sync base is null (#779)
The shrink guard (detectSuspiciousShrink) returned suspicious:false
whenever base was null, which is exactly the condition on a fresh
install, after unlock-key re-derivation, or when the encrypted base
blob fails to decrypt. A device in that state could push a
degraded/empty payload and overwrite populated cloud data — the
failure mode reported in #779 (Mac → OneDrive → Win11 wiping the
keychain on both ends).
Accept an optional remote-payload fallback in the guard and use it
when base is missing. Plumb the already-decrypted remote payload
from the merge branch, and decrypt checkResult.remoteFile on demand
in the direct-upload and syncAll branches when base is null.
Legitimate cases stay untouched:
- no base AND no remote → still not-suspicious (genuinely empty).
- outgoing grew past remote → lost is negative, guard skips.
- base present → behaviour unchanged, remote fallback ignored.
* Harden OneDrive 404 handling, restore barrier, multi-provider divergence (#779)
Follow-up fixes on top of the shrink-guard change for the same root
incident.
- OneDriveAdapter: findSyncFile/downloadSyncFile now retry with short
backoff when the Graph API returns "not found". A file uploaded by
another device can transiently 404 for seconds while the OneDrive
client propagates it, and treating that as "cloud is empty" was a
key step in how #779 escalated. The retry is bounded (2 extra
attempts, 1.5s/3s backoff) and only fires on null/404 results.
- useAutoSync.isRestoreInProgress: self-clear the restore-barrier
storage key when its deadline is in the past, and treat a deadline
more than 10 minutes in the future as corrupt (clock skew, pathological
holdMs, or tampered value) instead of letting it lock auto-sync.
- CloudSyncManager + SyncEvent: when the existing divergent-provider-
bases check fires, emit a PROVIDERS_DIVERGED event in addition to the
console.warn so the UI can surface the warning (was otherwise silent
and a known path for one provider's merged payload to overwrite a
differently-configured provider's data).
The keybinding recorder couldn't assign the 'Disabled' sentinel — pressing
Esc just cancels. Add a Ban-icon button next to 'Reset to default' that
writes 'Disabled' for the active scheme, and render the button label using
the localized 'Disabled' string instead of the raw sentinel.
A right-click Delete already exists in the snippet grid's context menu,
but users overwhelmingly open snippets by clicking — and the edit panel
had no delete affordance, so many concluded the feature was missing.
Surface a Trash2 icon next to Save when editing an existing snippet;
it calls the existing onDelete and closes the panel.
* Preload compact history on first turn after app restart (#753 hedge)
Symptom (confirmed on Copilot CLI, originally reported on Codex in
#753): after closing and reopening Netcatty, the AI chat UI still
shows the prior conversation but the agent responds "this is the
beginning of our conversation, no previous records". Earlier context
is lost entirely.
Root cause: the bridge relied on session/load throwing "not found" to
trigger the catch-block fallback that replays compact history. Some
ACP agents (Copilot CLI, some Codex builds) silently spawn a new
session when handed a stale id instead of erroring. The catch-block
never fires → historyReplayFallback stays false → the first turn
sends only the latest prompt → agent sees zero context.
Fix: when we're creating a new provider process AND telling it to
resume an existing session id AND the renderer gave us compact
history, preload historyReplayFallback=true as a hedge. If the agent
really did reload the session, the replay is ~3KB of redundant
context (small waste). If the agent silently started fresh, the
replay restores durable constraints + last few raw turns so the
first response is coherent.
After the first successful streamed turn clears the flag (the round-2
post-stream hook), steady state is back to sending only the latest
prompt. Cost is bounded to one replay per app-restart-and-prompt.
Test: "replays compact history on the first turn after app restart
even when session/load 'succeeds'" — mocks createACPProvider to
behave like Copilot CLI (no error thrown, no real resume), asserts
the first streamText call carries history+latest (length 2) and the
second only latest (length 1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix AI session resume and agent switching
* Preserve hidden draft when switching agents
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symptom: when an AI request is proxied through nginx (or any gateway)
and the request body exceeds client_max_body_size, the proxy returns a
413 HTML error page. The Vercel AI SDK then fails to parse the HTML
as a chat completion and surfaces a cryptic Zod validation error like
"Expected 'id' to be a string." through the UI — users have no idea
what's wrong.
Root cause: classifyError only did light sanitization and returned the
raw SDK message. It also string-coerced the error before inspection, so
the structured statusCode / responseBody fields that APICallError
attaches were thrown away.
Fix: classifyError now accepts `unknown` and inspects the full error
shape. Adds explicit branches for:
- HTTP 413 (from statusCode, cause.statusCode, or message text) →
"Request too large — exceeded proxy size limit. Try shorter
message, fewer attachments, or raise client_max_body_size."
- HTTP 502/503/504 → retryable upstream-gateway message
- HTML response body (starts with <!DOCTYPE/<html> or contains such
tags anywhere) → "Server returned HTML error page, likely a proxy
intercept."
- Zod/schema parse shapes ("Expected 'X' to be …", "Invalid JSON
response", "Type validation failed") → "Response could not be
parsed; proxy may have replaced/truncated the body."
In every classified case the raw SDK text is still appended ("Raw: …")
so users can report the underlying error verbatim.
useAIChatStreaming.ts callers now pass the raw error to classifyError
instead of `.message`, so the new structured branches actually fire.
Also wired infrastructure/ai/*.test.ts into the npm test glob.
Closes#765
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Batch Windows hidden-attribute detection in local FS listing (#766)
Symptom: opening a local directory with ~800 files in the SFTP panel
hangs for ~30 s on Windows. Reported on netcatty 1.0.93.
Root cause: listLocalDir spawns attrib.exe once per entry inside the
worker pool to detect the Windows hidden flag. 800 subprocess spawns
× ~40 ms each is precisely the reported 30 s. fs.promises.stat and
readdir on their own are nearly free; the subprocess flood dominates.
Fix: replace the per-entry attrib call with a single
`attrib.exe "<dir>\*"` invocation up front, parse its output into a
Set<basename>, and have the workers do an O(1) set lookup. One
subprocess per directory listing instead of one per entry.
Expected speedup for the #766 case: ~30 s → <1 s. Behavior is
unchanged — hidden files keep their hidden flag, non-hidden files
stay not-hidden; only the mechanism is different. Broken-symlink
handling (lstat fallback) also uses the same set.
Tests:
- parseAttribOutput is extracted as a pure function and unit-tested
against real attrib output shapes: drive-letter paths, UNC paths,
the trailing [DIR] marker that some Windows versions emit, mixed
flag columns (A/H/R), malformed "Parameter format not correct"
lines, empty input.
- listWindowsHiddenBasenames short-circuits on non-Windows without
spawning anything.
- Parser uses path.win32.basename explicitly so the tests pass under
non-Windows CI.
I cannot reproduce or test on Windows directly. The diagnosis is
mechanical (we can count subprocess calls) and the fix is a local
rewrite that preserves behavior, but Windows verification is still
desirable before release.
Closes#766
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address codex review on #767: pass /d so batched attrib includes hidden directories
Codex flagged that attrib.exe treats `<dir>\*` as file-centric by
default — without `/d`, hidden directories (node_modules, .git, etc.)
never appear in the output, so listWindowsHiddenBasenames misses them
and the SFTP browser shows those folders as not-hidden. This is a
behavior regression from the per-file path, which passed each entry's
full path directly and therefore covered both files and directories.
Added `/d` to the execFileAsync argv and a regression test that
module-mocks child_process.execFile to capture the argv and assert
`/d` is present. The parser-level [DIR] marker test is also still
there, so both the attrib call shape and the parser behavior are
locked down.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address codex round 2 on #767: tighten [DIR] strip to the literal marker
Codex flagged that /\s+\[[^\]]+\]\s*$/ also swallows legitimate trailing
bracketed text, so a hidden file named "Notes [old]" gets stored as
"Notes" in hiddenSet and hiddenSet.has("Notes [old]") returns false —
the entry is misclassified as not-hidden, a regression from the old
per-entry attrib path which never saw a "[DIR]" marker to strip.
Narrowed the regex to /\s+\[DIR\]\s*$/ — only the literal attrib/d
marker. Added a regression test covering "Notes [old]", "Draft [v2].md",
"archived [2024]" alongside the existing [DIR] case to lock down both
behaviors together.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix ACP history replay and compaction
* Fix PR keyword importance matching
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address codex review on #754: preserve short constraints + cancel-clear
Two recovery-path regressions flagged by codex review:
1. Compact ACP history dropped short load-bearing user constraints
(acpHistory.ts:55). The blanket length<10 rule treated short
non-trivial messages like "Use ssh2" or "中文输出" as filler,
while longer generic follow-ups still ate the budget. After
stale-session recovery the fresh ACP session would resume without
constraints that were present in the original chat. Removed the
length heuristic; the TRIVIAL_USER_MESSAGE_PATTERNS regex already
filters actual filler ("ok", "yes", "继续", "thanks").
2. historyReplayFallback was only cleared on non-aborted streams
(aiBridge.cjs:2837). If the user stopped the first turn after
stale-session recovery, the flag stayed set. The next turn would
then trigger shouldResetProviderForHistoryReplay, discard the
freshly recovered ACP session (resumeSessionId is forced to
undefined in that path), and re-spend tokens on another compact
replay — breaking the cancel-preserves-session contract. Now we
also clear on abort; the empty-but-not-aborted retry path in the
if-branch above is unchanged.
Tests:
- New test in acpHistory.test.ts asserts "Use ssh2" / "中文输出"
survive when pushed outside the recent raw window
- New test asserts "ok" / "继续" still drop (sanity check that the
trivial regex still does its job without the length backstop)
- Updated "does not treat pr inside ordinary words as important" to
no longer assert that approach/improve/prepare are absent — the
test's real intent (priority-2 line still wins) is preserved by
the 不要提交 assertion
- New test in aiBridge.test.cjs simulates a user cancelling the first
turn after recovery and verifies the next turn reuses the
recovered session (no extra provider creation, no re-replay)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address codex re-review: preserve replay flag across orthogonal recreation + keep tool output in raw window
Two more P2 regressions flagged on the second review pass:
1. historyReplayFallback was only carried over in the reset-for-replay
branch of the provider recreation path. An orthogonal change between
an empty recovered turn and its retry — a permission-mode toggle,
MCP scope/fingerprint flip, or auth rotation — would flip
shouldReuseProvider to false, enter the !shouldReuseProvider branch,
and drop the flag because preserveHistoryReplayFallback only covered
the shouldResetProviderForHistoryReplay case. The next turn then
sent only the latest prompt and lost the recovered conversation.
Now the flag is preserved on any recreation where a replay is still
pending.
2. Tool messages didn't flow through toRawHistoryMessage at all, so on
stale-session recovery they only survived as the 500-char compact
summary in summarizeToolMessage. Any follow-up referencing the last
tool output ("use that output", "what did cat show?") lost the
actual bytes when they exceeded the compact cap. Now tool results
travel through the recent raw window up to MAX_RAW_MESSAGE_CHARS
(2000), flattened to the "assistant" role since ACP only accepts
user/assistant.
Tests:
- aiBridge.test.cjs: new "preserves history-replay across provider
recreation caused by permission-mode / MCP / auth change" —
exercises the gap via a permission-mode toggle between an empty
recovered turn and its retry. Extends mock to support a dynamic
getPermissionMode.
- acpHistory.test.ts: new "preserves recent tool results verbatim" —
pushes a ~1500-char tool output through the pipeline and asserts the
replay still contains enough bytes to exceed the 500-char compact
cap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address codex round 3: inline tool_call context + bound durable scan
Two findings from the third codex review pass, both legitimate:
1. [P2] When the raw window starts mid-tool-interaction, the preceding
assistant tool_call message can fall outside the 6-item slice while
the tool_result stays in. Without the call's name+arguments, the
result was opaque bytes and follow-ups like "use that output" had
no provenance. The compact pass only preserved calls that matched
IMPORTANT_PATTERNS, so read_file / grep / terminal_exec were
silently dropped.
Fix: build a toolCallId → { name, arguments } index from every
assistant message and inline a `[from <name>(<args>)]` label next
to each Tool result line in the raw window. Args are truncated to
MAX_TOOL_CALL_LABEL_CHARS (200) so a verbose JSON payload can't eat
the entire raw budget.
2. [P3] buildCompactContext scanned messages.entries() over the full
transcript for durable-user/assistant candidates, even though
MAX_MESSAGES_TO_SCAN (20) suggested the path was meant to be
bounded. On a long ACP chat, every send did O(N) regex work plus
an O(N log N) sort — the very chat-length-dependent latency the
token-compaction PR was meant to address.
Fix: introduce MAX_DURABLE_SCAN_MESSAGES (200) and restrict the
durable scan to that tail. 200 is large enough to cover realistic
sessions (99th-percentile chats are << 200 turns) while giving a
constant-time worst case. Constraints older than the window age
out of the compact replay; the live ACP provider's own persisted
session still carries them when it can resume, which is the
common path.
Tests:
- "inlines tool_call name+args so tool_result is interpretable without
the preceding assistant turn" — pushes the tool_call out of the raw
window and asserts the result line carries [from <tool>(<args>)].
- "bounds the durable-candidate scan to avoid O(N) work per send on
long chats" — builds a 600+ message chat with an ancient priority-2
constraint outside the scan window and a recent one inside; asserts
only the recent one survives.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address codex round 4: preserve short assistant decisions + provenance on older tool results
Two P2 findings from the fourth codex pass, both mirror-images of earlier
fixes on a different code path:
1. Short assistant decisions dropped from compact replay
(acpHistory.ts:75-83). isSubstantiveAssistantMessage required length
>= 40 OR a small English keyword match OR a numbered list. Short but
load-bearing replies like "Use ssh2", "rebase instead", "中文输出"
satisfied none of those and were silently dropped from the durable-
assistant compact section. Once they fell outside the 6-item raw
window, "do what you suggested earlier" would replay only the user
question without the assistant's actual decision.
Fix: mirror the user-side loosening — drop the length/keyword gate,
rely on TRIVIAL_ASSISTANT_MESSAGE_PATTERNS to filter actual filler
("ok", "ack", "got it", "明白").
2. Older tool results lost provenance (acpHistory.ts:108-114). The
raw-window fix (round 3) only covered the last 6 items. Once a tool
result fell into the compact section via summarizeToolMessage, the
paired assistant tool_call was usually gone too, so multiple older
outputs surfaced as indistinguishable "Tool result (callN): ...".
Follow-ups like "use the resolv.conf output" had no way to map to
the right call.
Fix: plumb the toolCallIndex through summarizeMessage →
summarizeToolMessage and inline `[from <name>(<args>)]` labels in
the compact section too, the same shape the raw window uses.
Tests:
- New: preserves short non-trivial assistant decisions that miss the
keyword heuristic (Use ssh2 / 中文输出 / rebase instead)
- New: still drops trivial assistant filler like 'ack' / 'ok' / '明白'
- New: inlines tool_call context on OLDER summarized tool results
- Updated earlier raw-window tool regex tests to match the [from X(Y)]
shape ([^)] was failing to cross the args JSON's closing paren)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address codex round 5: de-dup raw ∩ compact + wire userSkills test into npm test
[P2] The scanned loop (last 20) overlaps with recentRaw (last 6), so
without a raw-window skip in the summarizeMessage path the same last-6
turns were summarized into the compact section AND appended verbatim
in the raw section. Important user turns and large tool output paid
the budget twice — eating into the 3k compact cap and crowding out
older durable context the replay is meant to preserve. Added the
same recentRawSourceIds skip the durable-user / durable-assistant
passes already use, and a regression test that asserts markers inside
the raw window don't surface in compact while still appearing in raw.
[P3] electron/bridges/ai/userSkills.test.cjs (added by this PR) sat
in a subdirectory that the default "npm test" glob
(electron/bridges/*.test.cjs) didn't pick up. The new routing /
index-budget regressions would never run locally or in CI until
someone noticed. Extended the glob to also match
electron/bridges/*/*.test.cjs; the userSkills tests are now included
in the 148-test run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address codex round 6: cancel+immediate-send race + tool-call id collision
Two P2 regressions in the recovery path:
1. If the user clicks Stop and immediately sends the next prompt, the
new stream handler's existingRun path unconditionally called
cleanupAcpProvider — destroying the fresh ACP session the cancel
IPC had just promised to preserve. The round-2 clear-on-abort
fix ran too late (in post-stream code) to help, because the new
stream can arrive before the aborted stream fully unwinds. In
that common timing window the follow-up still started from a
bare provider and lost all recovered conversation state.
Fix: (a) cancel IPC now synchronously clears
historyReplayFallback on the preserved provider entry, so the
next stream can't trigger shouldResetProviderForHistoryReplay
and tear the session down via that path; (b) the existingRun
path skips cleanupAcpProvider when the prior run was already
cancelled via the cancel IPC (captured via existingRun.cancelRequested
before we overwrite it). True interrupt-and-restart without an
explicit cancel still falls back to the old clean-slate behavior.
2. The tool-call provenance index used raw toolCall.id as the key.
Nothing in ChatMessage or the ACP event path enforces per-chat
unique ids, so a provider reusing "call1" across turns would
overwrite the older entry and mis-label older tool results
(e.g., an /etc/hosts result annotated as /etc/resolv.conf in
the compact summary). That makes stale-session recovery
misleading whenever a follow-up refers back to an earlier tool
output.
Fix: key the index by `${toolResultMessageId}:${toolCallId}` and
walk the message stream in order, resolving each tool_result to
the most recent preceding assistant tool_call with matching id.
Each result keeps its own historically-correct label regardless
of later id reuse.
Tests:
- aiBridge: "preserves recovered ACP session when user cancels then
immediately sends the next prompt" — fires the next stream request
after cancel but BEFORE releasing the first stream's blocked read,
asserts providerCreationArgs.length stays at 2 (no third creation)
and the second turn sends only the latest prompt.
- acpHistory: "resolves tool_call provenance correctly when tool ids
are reused across turns" — two interactions sharing id "call1",
asserts each tool_result carries its own call's args label.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address codex round 7: turn-based scan bound + single-pass history build
Two P2 regressions in long-chat / tool-heavy recovery paths:
1. MAX_DURABLE_SCAN_MESSAGES (200) bounded the scan by raw message
count. ACP tool interactions store the user turn, assistant
tool_call turn, and each tool_result as separate messages, so a
tool-heavy chat can produce 5+ messages per logical turn. 200
messages could be only 30-40 user turns — early constraints
like "不要提交" from turn 5 fell out of the compact replay long
before the turn count justified aging them out.
Fix: bound by MAX_DURABLE_SCAN_TURNS (100 user turns) instead.
Walk backwards from the end and stop after seeing 100 user
messages. Realistic tool-heavy 30-turn chats now keep their
early constraints alive, while true 100+ turn chats still
benefit from the bound.
2. buildToolCallIndex(messages) and messages.flatMap(...).slice(-6)
both walked the entire transcript on every send, even after the
bounded compaction window landed. Compaction's stated purpose
was to remove chat-length-dependent latency, but these per-send
linear passes kept it.
Fix: compute the scan start once via computeDurableScanStart,
then do all subsequent work over messages.slice(durableScanStart).
buildToolCallIndex walks only the window; the raw-6 flatMap also
runs over the window. On a 1000-message chat with 100-turn
window, send-time cost drops from O(1000) to O(~window_size).
Acceptable trade: if a tool_call's matching tool_result straddles
the window boundary (result inside, call outside), the single
surviving result loses its [from X(Y)] label. Tool_calls and their
results are almost always adjacent, so this affects at most the
first 1-2 messages of the window.
Tests:
- "preserves an early constraint in a tool-heavy chat where message
count balloons past the raw-count limit" — 35 turns × 6 msgs/turn =
212 messages. The old bound would have dropped the early
EARLY_CONSTRAINT_MARKER; with turn-based bound it survives.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds three bulk-close items to the right-click context menu on tabs:
- Close Others
- Close Tabs to the Right
- Close All
Anchor is the right-clicked tab (matches VSCode/JetBrains/FinalShell
UX), not the active tab. The "to the right" item is disabled when the
anchor is already the rightmost tab; "Close Others" is disabled when
it's the only tab.
To avoid spamming a busy-shell modal per tab, the new closeTabsBatch
helper in App.tsx expands workspace ids into their session ids, runs
ONE confirmIfBusyLocalTerminal probe across the whole batch, and only
proceeds when the user confirms. The probe + close path itself reuses
the existing PR #739 plumbing (ptyProcessTree + confirmCloseBusy).
Closes#748
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add opt-in setting to preserve mouse selection across keystrokes
Closes#755.
xterm.js hardcodes a "clear selection on user input" listener
(SelectionService.ts: coreService.onUserInput → clearSelection) with
no public option to disable. The user-reported workflow this breaks:
select a path with the mouse, type a command prefix like `sz `, then
middle-click-paste the still-live selection — but the very first
keystroke wipes the selection, so there's nothing left to paste.
Modern terminals (iTerm2, GNOME Terminal, Windows Terminal) preserve
the selection across input by default. We expose this as an opt-in
toggle for now since the visual semantics are a behavior change.
Implementation is capture-and-restore via xterm.js public APIs
(getSelectionPosition / select); xterm clears the selection
synchronously, then a queueMicrotask reapplies it on the next tick.
A ref (isRestoringSelectionRef) gates copy-on-select so the restore
doesn't redundantly rewrite the clipboard and clobber whatever the
user copied elsewhere in between.
Defaults to false (opt-in); can flip to default-on later if reception
is positive. Selection still clears on:
- Mouse click in empty space (xterm's mouse-driven path is untouched)
- Terminal scroll past the selected rows (existing buffer-trim logic)
- Programmatic clearSelection() callers
Files:
- domain/models.ts — new field, default false
- application/syncPayload.ts — added to SYNCABLE_TERMINAL_KEYS
- components/terminal/runtime/createXTermRuntime.ts — capture in
attachCustomKeyEventHandler, restore via queueMicrotask
- components/Terminal.tsx — owns isRestoringSelectionRef, passes it
through context, checks in copy-on-select listener
- components/settings/tabs/SettingsTerminalTab.tsx — UI toggle
- application/i18n/locales/{en,zh-CN}.ts — labels
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Trim verbose i18n descriptions to match neighboring rows
Both clearWipesScrollback and preserveSelectionOnInput descriptions
were too long. Cut to one sentence each, matching the brevity of
adjacent rows like Bracketed paste and OSC-52. Historical context and
edge-case caveats belong in the changelog/PR, not the settings UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symptom: in the Settings window (especially AI > Add Provider, but also
seen in Add Host), clicking an input occasionally shows no caret and
typed characters don't appear, yet select-all + delete still works on
the input's content.
Root cause: PR #502 introduced settings-window prewarming and
hide-on-close reuse. On Windows, calling `BrowserWindow.focus()` from
a non-foreground process is restricted by SetForegroundWindow rules —
the window is shown on top but never actually receives OS foreground
focus. With `document.hasFocus() === false`, Chromium deliberately
suppresses caret blink and keyboard routing, even though clicking an
input still moves activeElement to it (so non-keyboard interactions
like select-all-then-delete keep working — exactly the reported
symptom).
Fix: introduce `showAndFocusWindow(win)` and call it everywhere the
settings window is shown:
- Apply the alwaysOnTop toggle on win32 to bypass the
SetForegroundWindow restriction (established Electron workaround)
- Always call `webContents.focus()` after `win.focus()` so the renderer
marks the document as focused regardless of what the OS decided —
this is what restores the caret + keyboard routing
Scope intentionally limited to the settings window (the path PR #502
introduced). Other windows use a different show path (ready-to-show
event) and were not reported to have the issue.
I cannot test this on Windows directly. The fix follows a
well-documented Electron pattern and the diagnosis matches the
reported symptoms (Windows-only, intermittent, post-1.0.81 only).
Closes#760
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Honor CSI 3 J by default; add toggle to preserve scrollback on `clear`
Default `clear` (ncurses ≥ 2013) emits CSI 2 J + CSI 3 J to wipe both
visible screen and scrollback. PR #633 unconditionally intercepted CSI
3 J to keep history across `clear`, which broke POSIX semantics — users
running standard `clear` could not wipe scrollback at all (#757).
Restore the standard behavior as the default and expose a toggle for
the iTerm2-style "preserve history" preference (matches what #622
asked for):
- domain/models.ts: add `clearWipesScrollback: boolean` (default true)
- createXTermRuntime.ts: CSI 3 J handler now reads the setting and
only intercepts when the user opts out
- SettingsTerminalTab.tsx + i18n: expose the toggle with a description
explaining the tradeoff
- The right-click "Clear Buffer" menu action keeps its independent
semantics (always preserves scrollback) regardless of this setting,
since it goes through `clearTerminalViewport`, not the CSI path
Closes#757
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: include clearWipesScrollback in cloud-sync terminal keys
Codex review on PR #761 caught that the new toggle was added to
TerminalSettings but not to SYNCABLE_TERMINAL_KEYS, so it would never
travel across devices via cloud sync — users disabling it on one
device would silently get the default back on another after sync.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#741. Bash/zsh use Tab for native completion, but our ghost-text
accept on single Tab was swallowing the keystroke before it reached the
PTY. Ghost text is still accepted with →; Tab in popup-menu mode is
unchanged (popup is an explicit UI so intent is clear).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Improve tab UX: insert duplicated tabs adjacent to source, enable wheel scroll on tab bar
Addresses #737.
- Duplicating a tab now inserts the new tab immediately after the source
in the tab order, instead of appending it to the far right where it
was hard to find with many tabs open.
- The top tab strip now translates vertical mouse-wheel deltas into
horizontal scrolling, so users with many tabs can reach the ends of
the strip without dragging. Trackpad gestures that already carry
horizontal delta are left alone to preserve native two-finger swiping.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address Codex review: read source session inside functional updater
Codex flagged that reading `session` from the closure broke the atomicity
guarantee of the previous implementation — rapid repeated duplicates could
miss freshly queued state.
- Pre-allocate the new session id outside both setters so it stays stable
across StrictMode double-invocations.
- Move the source lookup back into `setSessions`' functional updater so it
always reads the freshest committed/queued state.
- Drop `sessions` from the useCallback dependency list now that we no
longer read it.
- Fast-path tabOrder insertion when the source is already in tabOrder to
avoid re-deriving the full effective order in the common case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address Codex review: gate active-tab and tab-order updates on successful create
Codex flagged that `setActiveTabId(newSessionId)` and `setTabOrder(...)` ran
unconditionally even when `setSessions` bailed out (source tab was closed
before the duplicate handler ran). That left activeTabId pointing at an id
that was never appended to sessions, putting the terminal layer into an
invalid "no matching tab" state.
Move both nested setState calls inside the `setSessions` functional updater
so they only fire when the source is actually present. Mirrors the original
pre-PR pattern; nested updates are idempotent so StrictMode's
double-invocation is harmless.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sync-guard): extend SyncState with BLOCKED + add shrink event variants
* feat(sync-guard): add detectSuspiciousShrink pure function with 12 unit tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* polish(sync-guard): drop unnecessary cast, sharpen test naming, pin priority invariant
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(test): include domain/*.test.ts in npm test glob
* feat(sync-guard): gate syncToProvider with shrink detection + force-push override
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(sync-guard): reset overrideShrinkOnce before early return for invariant strictness
* fix(sync-guard): extend shrink guard to syncAllProviders (the actual sync entry point)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(sync-guard): apply empty-vault guard uniformly to auto and manual sync
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(sync-guard): preserve merge base on same-account re-auth
Adds providerAccountId persistence; completePKCEAuth and completeGitHubAuth
now only clear syncBase/anchor when the authenticated account id differs from
the previously stored one, preventing zombie-entry resurrection on token
refresh. disconnectProvider clears the stored id so a reconnect starts fresh.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(sync-guard): add i18n strings for sync-blocked banner + force-push modal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(sync-guard): add SyncBlockedBanner showing shrink findings with restore/force-push actions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(sync-guard): stable subscribeToEvents reference + type-safe finding narrowing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(sync-guard): force-push confirmation modal + scroll restore button into view
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ux(local-backups): show version as title, demote reason+timestamp to meta line
* feat(local-backups): record + display sync data version (v5/v6...) on each backup
Each backup now captures the live CloudSyncManager.localVersion at creation
time. UI shows it as title (v5, v6, ...) with timestamp + reason demoted to
the meta line. Backups created before this field existed (or before any
successful cloud sync) fall back to timestamp as title.
Replaces the earlier app-version-transition title which conflated app
version with sync data version.
* fix(sync-guard): consume override flag at sync entry + restore provider status on block
- Snapshot+clear overrideShrinkOnce at top of syncToProvider and
syncAllProviders so an early-return cannot leak the flag to a later
unrelated sync (Codex P1).
- Restore provider status to 'connected' when shrink-block returns from
syncToProvider; previously left provider stuck on 'syncing' in the
UI (Codex P2).
- Process pre-existing check errors before returning from the
shouldBlockAll branch in syncAllProviders so a check-failed provider
isn't dropped from results (Codex P2).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(sync-guard): refactor force-push to parameter passing + add credential-availability guard
The previous design used a one-shot boolean flag on CloudSyncManager set
by forcePushOverrideShrink(). Even with snapshot+clear at sync entry
points, the renderer wrapper's await ensureUnlocked() could throw before
the flag was consumed, leaving it armed for the next unrelated sync.
Fix: pass overrideShrink as a call-time parameter through the chain.
Eliminates the persistent flag and its leak surface.
Also: force-push now runs the same ensureSyncablePayload(...) guard the
other manual sync entry points use, so a vault with encrypted-credential
placeholders won't be uploaded via the force path either.
Addresses the latest two Codex P1/P2 findings on #742.
* fix(sync-guard): backfill account id from in-memory state for upgrade-path re-auth
Users upgrading to this PR have no netcatty.sync.accountId.* persisted yet.
On their first re-auth the guard saw previousId=null and cleared the
merge base anyway, defeating the point of the same-account preservation.
Snapshot the in-memory account id BEFORE overwriting providers[provider]
and use it as a fallback when the persisted id is missing. New users
(no prior connection at all) still get the clear-on-first-auth path.
Addresses Codex P1 on #742.
* fix(sync-guard): inspect force-push results + mark blocked single-provider as error
- Force-push handler now inspects syncNow result entries: applies any
mergedPayload to local state, only clears the banner when all providers
report success, surfaces a toast error otherwise. Previously the banner
cleared unconditionally regardless of network/auth failures (Codex P1).
- syncToProvider shrink-block branches now mark provider status as
'error' with a 'Sync blocked: would delete too much' message instead
of 'connected'. Status aggregators treat 'connected' as healthy, so
the blocked upload was surfacing as 'synced' in the UI (Codex P2).
syncAllProviders already used this pattern; this brings the
single-provider path in line.
* fix(sync-guard): exempt USE_LOCAL conflict + clear post-merge BLOCKED + expose 'blocked' status
- USE_LOCAL conflict resolution now passes { overrideShrink: true }: the
conflict modal already served as user confirmation, and shrink-blocking
it left users with a closed modal and an opaque banner (Review C-1).
- Post-merge round-trip in useAutoSync now detects shrink-blocked results
and resets syncState to IDLE via new manager.clearShrinkBlockedState().
The merged data is already applied locally; the next user-triggered
sync will re-check, and we don't wedge the manager in BLOCKED with no
visible banner outside the Settings tab (Review I-1).
- overallSyncStatus now reports 'blocked' as a distinct value from
'error', so downstream UI (status icon, future badges) can offer
shrink-block-specific affordances (Review I-2).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(sync-guard): stabilize banner subscription dep + map 'blocked' status to error indicator
- The SyncBlockedBanner subscription useEffect depended on [sync] (the
whole hook return object), which gets a new reference every render.
This caused the listener to be unsubscribed+resubscribed on every
render, opening a tiny race window where a SYNC_BLOCKED_SHRINK event
could be missed and the banner would never appear. Destructure
subscribeToEvents (already useCallback-stable) and depend on it
directly, so the effect runs exactly once on mount.
- SyncStatusButton's status mapping had no arm for the new 'blocked'
value, falling through to 'none' (idle). The global status indicator
said healthy while the in-page banner said paused. Map 'blocked' to
the same error indicator used for 'conflict' so the UI is consistent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(sync-guard): only clear banner on actual success + hydrate from manager state
- Banner subscription now clears only on SYNC_COMPLETED with result.success.
SYNC_STARTED (auto-sync timer ticks) and SYNC_FORCED (fires BEFORE upload)
could clear the banner prematurely, removing the user's recovery affordance
while the underlying issue was unresolved (Codex P2).
- Manager now persists the last shrink finding in state.lastShrinkFinding
alongside the SYNC_BLOCKED_SHRINK emission. New public getter
getShrinkBlockedFinding() returns it when syncState is BLOCKED. Renderer
hydrates the banner on mount so a block that happened off-screen
(auto-sync while user was on another tab) is still visible when they
open Sync Settings (Codex P2).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(sync-guard): unified BLOCKED-cleared event + USE_LOCAL inspects results
- USE_LOCAL conflict resolution now inspects syncNow() results, applies
any mergedPayload to local state, surfaces a toast error and KEEPS the
modal open on failure (so user can switch to USE_REMOTE). Mirrors the
force-push handler pattern. Without this, USE_LOCAL silently 'succeeded'
even when providers failed (Codex CLI P1).
- New SYNC_BLOCKED_CLEARED event emitted on every BLOCKED -> non-BLOCKED
transition via a private exitBlockedState() helper. Banner subscribes to
this single signal instead of guessing from per-provider SYNC_COMPLETED
events. Fixes:
- Multi-provider scenarios where first SYNC_COMPLETED clears the banner
while a later provider was still going to fail (Codex CLI P1).
- clearShrinkBlockedState() (post-merge self-heal) silently leaving
the banner stuck because no event was emitted (Codex CLI P2).
- disconnectProvider() now also exits BLOCKED state. Disconnecting
implicitly resolves any pending shrink-block warning, otherwise the
stale alert carried over to the next-account reconnect (Codex CLI P2).
- All BLOCKED -> non-BLOCKED transitions consolidated through
exitBlockedState() so lastShrinkFinding cleanup + event emission are
always paired (Codex CLI P3 #6 covered).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(sync-guard): only clear BLOCKED on actual success, not on transient ERROR/SYNCING/CONFLICT
Previous patch called exitBlockedState() at every BLOCKED -> non-BLOCKED
transition, but this clears the banner on transitions that don't actually
resolve the shrink concern:
- SYNCING (sync just started — about to try, may fail)
- ERROR (transient transport failure, shrink concern still real)
- CONFLICT (separate concern; doesn't resolve the shrink)
If a user was in BLOCKED then triggered a sync that failed for an unrelated
reason (network, auth), the banner cleared and they lost the warning.
Restrict exitBlockedState() to terminal-success transitions:
- IDLE on successful upload (data made it to cloud — concern resolved)
- explicit clears (disconnectProvider, clearShrinkBlockedState)
- conflict resolution (USE_REMOTE/USE_LOCAL also end in IDLE)
Found by Codex CLI review of commit 12d7fa7b.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ctrl-w): add ps-node + windows-process-tree + tsx deps for close-priority feature
* fix(ctrl-w): drop ps-node dep and add windows-process-tree to asarUnpack
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ctrl-w): add ptyProcessTree bridge with per-platform child-process enumeration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ctrl-w): ptyProcessTree uses args= for full command + warns on pid overwrite
- Replace `comm=` with `args=` in defaultListPosix so the full command
line is captured on both macOS (BSD ps) and Linux (GNU ps), avoiding
the 15-char TASK_COMM_LEN truncation.
- Add console.warn in registerPid when the same sessionId is overwritten
with a different pid, making the race condition visible in logs.
- Add test: registerPid warns exactly once on a pid change, not on a
same-pid re-registration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ctrl-w): register local PTY pid with ptyProcessTree on spawn/exit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ctrl-w): unregister pids in cleanupAllSessions to match per-delete invariant
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ctrl-w): add IPC handlers for pty child processes and confirm-close dialog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ctrl-w): guard BrowserWindow.fromWebContents null and document dialog dismiss contract
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ctrl-w): expose ptyGetChildProcesses and confirmCloseBusy on window.netcatty
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ctrl-w): add i18n strings for close-busy-terminal dialog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ctrl-w): add resolveCloseIntent pure function with 8 unit tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ctrl-w): expose handleCloseSidePanel via ref to App.tsx
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ctrl-w): wire resolveCloseIntent + local-shell busy confirmation into closeTab hotkey
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(ctrl-w): add re-entrancy guard, aggregate busy count, sync sidebar ref, dedupe intent branches
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ctrl-w): auto-close workspace when its last session is closed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ctrl-w): sidebar close wins over focused terminal in priority chain
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ctrl-w): sidebar priority applies to single-session tabs too, not just workspaces
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ctrl-w): compute empty-workspace auto-close outside setSessions updater
Addresses Codex P2 on #739: React 18+ does not guarantee updater
execution timing under concurrent scheduling. Moving the decision
outside the updater makes the microtask queue deterministic.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
We actively maintain the latest stable release of Netcatty. Security fixes are applied to the current release only.
| Version | Supported |
| ------- | ------------------ |
| Latest | :white_check_mark: |
| Older | :x: |
## Reporting a Vulnerability
If you discover a security vulnerability in Netcatty, **please do not open a public GitHub Issue**.
Instead, report it privately via one of the following methods:
- **GitHub Private Vulnerability Reporting**: Use the [Security tab](https://github.com/binaricat/Netcatty/security/advisories/new) to submit a private advisory.
- **Email**: Send details to support@netcatty.com.
Please include the following details in your report:
- A description of the vulnerability
- Steps to reproduce the issue
- The potential impact
- Any suggested mitigation or fix
## Response Timeline
We aim to:
- Acknowledge receipt within **3 business days**
- Provide a status update within **7 business days**
- Release a patch as soon as possible depending on severity
## Scope
This policy applies to the Netcatty desktop application (`netcatty.app`) and its source code in this repository.
Vulnerabilities in third-party dependencies should be reported directly to the respective upstream projects.
## Disclosure Policy
We follow responsible disclosure. Once a fix is available, we will publish a security advisory and credit the reporter (unless they prefer to remain anonymous).
apple_silicon:`[](${baseUrl}/${files.mac.arm64})`,
git commit -m "chore: update star history charts [skip ci]"
# Auth comes from actions/checkout (RELEASE_TOKEN), not GH_TOKEN above.
git push -u origin "HEAD:${branch}" --force
existing="$(
gh pr list \
--head "$branch" \
--base "$default_branch" \
--state open \
--json number \
--jq '.[0].number // empty'
)"
if [[ -n "$existing" ]]; then
echo "Reusing open PR #${existing}"
pr_number="$existing"
else
pr_url="$(
gh pr create \
--base "$default_branch" \
--head "$branch" \
--title "chore: update star history charts" \
--body "Automated weekly refresh of the static star history SVG charts embedded in the README. Generated by the Star History workflow via mystarhistory."
)"
echo "Created ${pr_url}"
pr_number="${pr_url##*/}"
fi
# required_approving_review_count is 0; merge immediately when possible.
if gh pr merge "$pr_number" --squash --delete-branch; then
echo "Merged PR #${pr_number}"
else
echo "Could not auto-merge PR #${pr_number}; left open for manual merge."
External SDK turns normalize file changes, web searches, plan updates, recoverable warnings, and token usage into the shared event protocol. These activity and usage records are stored on assistant messages so the compact activity view is restored with chat history.
**Stop** always goes through `stopAgentTurn()` (UI, `/stop`, MCP). Do not add parallel abort paths in hooks.
### Codex App Server (experimental)
Codex can opt into a persistent `codex app-server --stdio` runtime under
`electron/bridges/aiBridge/codexAppServer/`; the existing TypeScript SDK remains
the default. The main process owns JSONL RPC correlation, thread/turn routing,
native approvals, `request_user_input`, model discovery, and process cleanup.
- Session identities include the Codex runtime; SDK and App Server threads must
never resume across runtimes.
- Observer maps to `read-only + never`, Confirm to `read-only + on-request`, and
Auto to `danger-full-access + never`.
- App Server native “allow for session” decisions are session-scoped Codex
grants and must not become persistent Netcatty permission grants.
-`turn/completed` is the terminal lifecycle event. Retryable `error`
notifications are warnings; process exit is fatal.
- Regenerate the committed protocol contract with
`npm run generate:codex-app-server-schema` after upgrading Codex, and verify it
with `npm run check:codex-app-server-schema`.
### AI SDK v7 (Catty path)
Catty sidebar turns use **Vercel AI SDK 7** via `streamText` in `turnDrivers/cattyStreamProcessor.ts`. Key conventions:
| Concern | Module | Notes |
|---------|--------|-------|
| `runtimeContext` | `cattyRuntimeContext.ts` | Per-turn orchestration state (`chatSessionId`, `turnId`, `agentKind`, `permissionMode`, scope, `lastCompaction`). Passed to `prepareStep` and lifecycle callbacks. **Does not replace** pre-turn compaction or 413 handling. |
| `toolsContext` | `capabilityTools.ts` | Per-tool keyed context (`bridge`, `getExecutorContext`, `toolOutputStore`, …). Tools read deps from `{ context }` in `execute`, not closure capture. |
| `toolApproval` | `cattyToolApproval.ts` | Write-tool gating for Catty `streamText` only. Calls `requestApproval()` in confirm mode; observer auto-denies writes. **External MCP agents** still approve via main-process `mcpServerBridge` → `setupMcpApprovalBridge()` — unchanged. |
| AI context | `buildAITerminalSessionInfo` + `useTerminalAiContexts` | Per-session `hostChain` + `activePortForwards`; mirrored in `getContext` and Catty system prompt |
**Handles:**`ToolOutputStore` persists across turns per chat session; cleared on chat session delete. Large `sftp.read` results spill to `tool_output_read`.
**Harness domain (`catalog/harness.cjs`):** Catty-only surface (`surfaces.catty.toolName`). Registered in the capability catalog but executed locally in `capabilityTools.executeLocalCattyCapability` (not MCP/CLI). `harness.web.search` is omitted when web search is not configured.
## Plugin host runtime (internal preview)
The phase-2 plugin host lives under `electron/plugins/` and is disabled unless
`NETCATTY_PLUGIN_DEV=1` is present at application launch. Public wire and package
types still come only from `packages/plugin-contract/schema/`; do not add a
second private RPC shape when extending the host.
-`PackageStore` validates an immutable `.ncpkg` snapshot, extracts only into
private staging, and atomically publishes an installed version.
-`PluginManager` serializes install, enable/disable, restart and uninstall
mutations. Do not bypass it from renderer IPC.
- Ordinary plugins run in a sandboxed, offline `BrowserWindow` session and can
reach only their runtime-scoped `netcatty-plugin://` authority.
- Node-only plugins run in a dedicated `utilityProcess`; they remain an advanced
development-only path until permission and distribution phases land.
-`PluginRpcRouter` owns correlation, cancellation, deadlines, stream credit and
protocol-failure containment. Runtime identity is assigned by the host and is
never accepted from request parameters.
- App quit goes through `runPluginShutdown()` after the dirty-editor guard; do
not add another independent quit interception path.
Run `npm run test:plugin-runtime` for main-process boundaries and
`npm run test:plugin-runtime:electron` for real BrowserWindow/utilityProcess
smoke coverage. Packaged-resource changes must also pass `npm run pack:dir`.
## Extending the System
1) **New domain logic**: Add pure functions/types under `domain/`; avoid side effects.
2) **New stateful behavior**: Wrap it in a hook under `application/state/`; keep external I/O behind adapters.
@@ -33,6 +142,14 @@ This project is wired around three layers: domain (pure logic), application stat
- **Temporary files**: All temporary files (e.g., SFTP downloaded files for external editing) must be written to Netcatty's dedicated temp directory via `tempDirBridge.getTempFilePath(fileName)`. Do not write directly to `os.tmpdir()`. This ensures proper cleanup and user visibility in Settings > System.
## Terminal Side Panel Splits
-`domain/sidePanelLayout.ts` owns the pure pane/split tree operations, including focus, unique tools, close collapse, resize, and the pane limit.
-`application/state/useTerminalSidePanelLayoutState.ts` owns per-terminal layouts and keeps the legacy focused-tool map compatible with external open paths.
-`TerminalLayerSidePanelSection.tsx` renders one shared toolbar plus the nested pane chrome. Each pane content host must remain `overflow-hidden` and layout-contained.
- Mounted tool panels use a stable portal node that moves between their pane host and the hidden parking host. Never fall back to an `absolute inset-0` side-panel root when a pane host is not ready.
- Closing the whole side panel clears that terminal's split tree. Switching terminal tabs must not reuse another tab's layout.
## Testing & Safety
- Favor unit tests for domain helpers (e.g., `workspace.ts`, `host.ts`) and hook-level tests for application state.
- When changing storage keys or schema, provide migration or backward-compat handling.
@@ -44,6 +161,16 @@ This project is wired around three layers: domain (pure logic), application stat
- Avoid direct network/fetch in components; add a service/adaptor first.
- Maintain ASCII-only unless required by existing file content.
## Reporting Issues & PRs
Issues opened without the required format are **auto-closed** by the issue-format bot. Agents that file issues via `gh` or the API must still follow these rules:
- **Title:** start with `[Bug]`, `[Feature]`, or `[Other]`, then a short summary (≥ 4 characters after the prefix). Example: `[Bug] SFTP upload fails on Windows`.
- **Body:** use the templates under [`.github/ISSUE_TEMPLATE/`](.github/ISSUE_TEMPLATE/) (Bug Report or Feature Request) and fill every required field. Blank issues are disabled.
Full contributor guidance (setup, commits, PR process): [CONTRIBUTING.md](./CONTRIBUTING.md).
## Review Boundaries
- Treat `electron/cli/*`, `netcatty-tool-cli`, the CLI discovery file, and the local TCP bridge as internal Netcatty integration surfaces unless a task explicitly says otherwise.
- Do not review those surfaces as public APIs by default, and do not assume they must support third-party callers, manual launches, or non-Netcatty agents.
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
# Install dependencies
npm install
# Start dev server (runs lint first, then Vite + Electron concurrently)
npm run dev
# Lint
npm run lint
npm run lint:fix
# Run all tests
npm test
# Run a single test file
node --test --import tsx path/to/file.test.ts
# Build renderer
npm run build
# Package for current platform
npm run pack
# Package for specific platforms
npm run pack:mac
npm run pack:win
npm run pack:linux
```
## Architecture
Netcatty is an Electron + React desktop app (SSH manager, terminal, SFTP browser). It has two runtimes:
### Electron Main Process (`electron/`)
- **`main.cjs`** — entry point; wires crash logging, process error guards, and delegates to `main/registerBridges.cjs`
- **`bridges/`** — one `.cjs` file per capability domain (sshBridge, sftpBridge, terminalBridge, portForwardingBridge, aiBridge, etc.). Each bridge exposes IPC handlers via `ipcMain`. Tests live alongside the bridge file (`*.test.cjs`).
- **`preload.cjs`** — exposes a typed `window.electron` API to the renderer via `contextBridge`. Uses `preload/api.cjs` for the generated API surface.
- **`cli/`** — `netcatty-tool-cli.cjs` is a separate internal binary for tool/MCP integration; treat as internal surface only.
### Renderer Process (React + Vite)
Three-layer architecture (see `AGENTS.md` for full detail):
- **`domain/`** — pure TypeScript logic, no side effects. Models (`models.ts`), host helpers, workspace tree operations.
- **`application/state/`** — React hooks that own state and persistence boundaries. Key hooks: `useVaultState` (hosts/keys/snippets), `useSessionState` (terminal sessions/workspace), `useSettingsState` (theme/config).
- **`infrastructure/`** — external edges: `persistence/localStorageAdapter.ts` for storage, `services/` for network calls (Gemini AI, GitHub Gist sync), `config/` for defaults, storage keys, and terminal themes.
- **`components/`** — presentation only. `App.tsx` wires hooks to components; no business logic in components.
### IPC Pattern
UI calls `window.electron.*` (preload API) → IPC → bridge handler in main process. Never call `ipcRenderer` directly from components.
### Key Conventions
- All storage reads/writes go through `localStorageAdapter`; storage keys are in `infrastructure/config/storageKeys.ts`.
- Temporary files must use `tempDirBridge.getTempFilePath(fileName)` — never `os.tmpdir()` directly.
- Aside panels (VaultView subpages) use the shared design system in `components/ui/aside-panel.tsx` — see `AGENTS.md` for usage patterns.
- Renderer code is TypeScript/ESM; Electron main/bridges are CommonJS (`.cjs`).
- Path alias `@/` resolves to the repo root (configured in `vite.config.ts` and `tsconfig.json`).
## Reporting Issues & PRs
Issues that skip the format gate are auto-closed. When opening issues (including via `gh` / API):
- Title must start with `[Bug]`, `[Feature]`, or `[Other]` plus a short summary.
- Body must use `.github/ISSUE_TEMPLATE/` (Bug Report or Feature Request) and fill required fields.
- PRs should follow `.github/PULL_REQUEST_TEMPLATE.md`.
See [CONTRIBUTING.md](./CONTRIBUTING.md) and the "Reporting Issues & PRs" section in [AGENTS.md](./AGENTS.md).
Thank you for your interest in contributing to Netcatty — an AI-powered SSH client, SFTP browser, and terminal manager built with Electron, React, and xterm.js.
Please read this guide before submitting issues or pull requests.
## Code of Conduct
This project follows the [Contributor Covenant Code of Conduct](./CODE_OF_CONDUCT.md). By participating, you agree to uphold this standard.
## Ways to Contribute
- Report bugs via [GitHub Issues](https://github.com/binaricat/Netcatty/issues)
- Suggest features or improvements
- Fix bugs or implement features listed in Issues
- Improve documentation
## Reporting Issues
Issues that do not follow the required format are closed automatically by the
issue-format bot.
- **Title prefix (required):** start with `[Bug]`, `[Feature]`, or `[Other]`,
then a short summary of at least 4 characters.
Example: `[Bug] SFTP upload fails on Windows`
- **Template (required):** open from
[New Issue](https://github.com/binaricat/Netcatty/issues/new/choose) and pick
**Bug Report** or **Feature Request**. Fill every required field. Blank
issues are disabled (`blank_issues_enabled: false`).
> 🚀 **Boost your IT ops daily work with AI power.** Catty Agent is the built-in AI assistant that understands your servers, executes commands, and handles complex multi-host operations — all through natural conversation.
Watch Catty Agent orchestrate a Docker Swarm cluster across two servers in one conversation. It handles the init, token exchange, and node joining — you just tell it what you want.
@@ -275,8 +207,30 @@ Download the latest release for your platform from [GitHub Releases](https://git
Or browse all releases at [GitHub Releases](https://github.com/binaricat/Netcatty/releases).
### Code signing and privacy
Netcatty is applying to the SignPath Foundation open-source program. Once
approved, covered Windows release artifacts will use **Free code signing provided by SignPath.io, certificate by SignPath Foundation**.
See the [code signing policy](CODE_SIGNING_POLICY.md) and
[privacy policy](PRIVACY.md). Windows releases may remain unsigned until the
application and integration are complete.
> **Windows portable data:** Exit Netcatty, then create a folder named `data` beside `Netcatty.exe` (zip build) or beside the portable launcher. Netcatty will store its profile there on the next launch. Saved passwords and private keys remain protected by the Windows user account that created them, so they must be re-entered after moving the folder to another computer or Windows account.
> **Open folders in Netcatty on Windows:** The installed version adds **Open in Netcatty** to Explorer's folder context menu and folder-background context menu. It opens a local terminal in that folder. On Windows 11, choose **Show more options** first. Turn the menu off or on in **Settings → System → Windows Explorer**. ZIP and portable versions do not add this menu by default.
> **macOS Users:** Current releases are expected to be code-signed and notarized. If Gatekeeper still warns, make sure you downloaded the latest official build from GitHub Releases.
### Nix / NixOS
Netcatty provides a flake that wraps the official Linux AppImage release for Nix and NixOS users:
```bash
nix run github:binaricat/Netcatty
```
For declarative installs, add the Netcatty flake as an input and use `inputs.netcatty.packages.${pkgs.system}.default` in your NixOS or Home Manager package list.
### Prerequisites
- Node.js 18+ and npm
- macOS, Windows 10+, or Linux
@@ -295,25 +249,6 @@ npm install
npm run dev
```
### Project Structure
```
├── App.tsx # Main React application
├── components/ # React components
│ ├── Terminal.tsx # Terminal component
│ ├── SftpView.tsx # SFTP browser
│ ├── VaultView.tsx # Host management
│ ├── KeyManager.tsx # SSH key management
│ └── ...
├── application/ # State management & i18n
├── domain/ # Domain models & logic
├── infrastructure/ # Services & adapters
├── electron/ # Electron main process
│ ├── main.cjs # Main entry
│ └── bridges/ # IPC bridges
└── public/ # Static assets & icons
```
---
<a name="build--package"></a>
@@ -386,11 +321,11 @@ This project is licensed under the **GPL-3.0 License** - see the [LICENSE](LICEN
'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.codex.description':'Connect OpenAI Codex. Sign in with ChatGPT here, or enable an OpenAI-compatible provider API key and custom endpoint in Settings.',
'ai.codex.appServer.description':'Use the persistent Codex protocol for native approvals, sandbox controls, live models, and mid-turn questions. SDK remains the default.',
'ai.codex.appServer.checking':'Checking App Server support…',
'ai.codex.appServer.available':'App Server is available for this Codex CLI.',
'ai.codex.appServer.modelCatalogWarning':'The live Codex model catalog is unavailable. Using the built-in model list.',
'ai.codex.appServer.approval.allowSession':'Allow for session',
'ai.codex.appServer.userInput.title':'Codex needs your input',
'ai.codex.appServer.userInput.description':'Answer these questions to continue the current turn.',
'ai.codex.appServer.userInput.other':'Enter another answer',
'ai.codex.appServer.userInput.autoResolve':'Codex will continue automatically if no answer is provided in time.',
'ai.codex.steer.placeholder':'Add an instruction while Codex is working…',
'ai.codex.steer.notSteerableReview':'This Codex review turn cannot accept additional instructions. Your draft was kept.',
'ai.codex.steer.notSteerableCompact':'This Codex compaction turn cannot accept additional instructions. Your draft was kept.',
'ai.codex.steer.busy':'Another instruction is already being sent to Codex.',
'ai.codex.steer.inactive':'The Codex turn has already ended. Your draft was kept.',
'ai.codex.steer.unsupported':'In-flight instructions require the Codex App Server runtime.',
'ai.codex.steer.failed':'Codex could not accept the additional instruction. Your draft was kept.',
'ai.codex.detecting':'Detecting...',
'ai.codex.notFound':'Not found',
'ai.codex.awaitingLogin':'Awaiting login',
'ai.codex.connectedChatGPT':'Connected via ChatGPT',
'ai.codex.connectedApiKey':'Connected via API key',
'ai.codex.connectedCustomConfig':'Connected via ~/.codex/config.toml',
'ai.codex.customConfigIncomplete':'Custom config detected (env var missing)',
'ai.codex.customConfigHint':'Using custom provider "{provider}" configured in ~/.codex/config.toml — no ChatGPT login needed.',
'ai.codex.customConfigMissingEnvKey':'Warning: {envKey} is not set in your shell environment. Export it (or launch netcatty from a shell that has it) so Codex can authenticate.',
'ai.codex.notConnected':'Not connected',
'ai.codex.statusUnknown':'Status unknown',
'ai.codex.path':'Path:',
'ai.codex.notFoundHint':'Could not find codex in PATH. Install it or specify the executable path below.',
'ai.claude.configDir.placeholder':'~/.claude (leave blank for default)',
'ai.claude.configDir.hint':'Sets CLAUDE_CONFIG_DIR — point at a folder where you have run `claude` login (contains settings.json + credentials).',
'ai.claude.settings':'Settings file',
'ai.claude.settings.placeholder':'~/team-settings.json (path, or inline {"model":"..."})',
'ai.claude.settings.hint':'Optional. A settings.json path or inline JSON, passed to the SDK as `settings`. Additive to — and independent of — the config directory above (merged on top, not a replacement).',
'ai.claude.envVars.hint':'One KEY=VALUE per line, passed to the Claude agent. Stored locally in plaintext — for API keys / credentials, prefer the config directory above (a `claude` login).',
'ai.claude.check':'Check',
'ai.claude.resetPath':'Reset',
// AI GitHub Copilot CLI
'ai.copilot.title':'GitHub Copilot CLI',
'ai.copilot.description':'Uses the GitHub Copilot CLI. Once detected, it can be selected as an external coding agent.',
'ai.copilot.detecting':'Detecting...',
'ai.copilot.detected':'Detected',
'ai.copilot.notFound':'Not found',
'ai.copilot.path':'Path:',
'ai.copilot.notFoundHint':'Could not find copilot in PATH. Install it or specify the executable path below.',
'ai.codebuddy.description':'Uses CodeBuddy Code via the official Agent SDK (`@tencent-ai/agent-sdk`). Once detected, it can be selected as an external coding agent.',
'ai.codebuddy.detecting':'Detecting...',
'ai.codebuddy.detected':'Detected',
'ai.codebuddy.notFound':'Not found',
'ai.codebuddy.path':'Path:',
'ai.codebuddy.notFoundHint':'Could not find codebuddy in PATH. Install it or specify the executable path below.',
'ai.codebuddy.envVars.hint':'One KEY=VALUE per line, passed to the CodeBuddy agent. Set CODEBUDDY_API_KEY or CODEBUDDY_AUTH_TOKEN here for authentication. Stored locally in plaintext.',
// AI Grok Build (in-app managed agent — distinct from External MCP client install)
'ai.grok.title':'Grok Build',
'ai.grok.description':"xAI's Grok Build coding agent CLI. Install the Grok CLI, sign in with `grok login` or set XAI_API_KEY, then select it as an external agent.",
'ai.grok.detecting':'Detecting...',
'ai.grok.detected':'Detected',
'ai.grok.notFound':'Not found',
'ai.grok.path':'Path:',
'ai.grok.notFoundHint':'Could not find grok in PATH. Install Grok Build CLI or specify the executable path below.',
'Talk to Grok over Agent Client Protocol (grok agent stdio). Injects Netcatty MCP on session/new. Turn off to use the original headless streaming-json CLI path.',
'ai.grok.runtime.streamingJson.hint':
'Using headless streaming-json (grok -p --output-format streaming-json). Project .grok/config.toml is used for MCP injection.',
// AI Default Agent
'ai.defaultAgent':'Default Agent',
'ai.defaultAgent.description':'Agent to use when starting a new AI session',
'ai.defaultAgent.catty':'Catty (Built-in)',
'ai.toolAccess.title':'Tool Access',
'ai.toolAccess.mode':'Netcatty Access Mode',
'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',
// External MCP (productized catalog MCP for Codex / Claude Code / Cursor)
'ai.externalMcp.title':'External MCP',
'ai.externalMcp.description':'Expose Netcatty as an MCP server for external clients such as Codex, Claude Code, Cursor, and Grok. Uses the same catalog tools as in-app agents (terminal, SFTP, Vault, port forwarding). Keep Netcatty running while clients are connected.',
'ai.externalMcp.sessionsExposed':'Sessions in scope: {count}',
'ai.externalMcp.mode':'Availability mode',
'ai.externalMcp.mode.temporary':'Temporary',
'ai.externalMcp.mode.persistent':'Always on',
'ai.externalMcp.mode.description':'Temporary mode auto-disables after idle timeout. Always-on restores External MCP when Netcatty starts.',
'ai.externalMcp.idleTimeout':'Idle timeout',
'ai.externalMcp.idleTimeout.description':'In temporary mode, disable External MCP after this many minutes with no MCP operations.',
'ai.externalMcp.idleTimeout.minutes':'min',
'ai.externalMcp.focusOnHostOpen':'Focus window on host_open',
'ai.externalMcp.focusOnHostOpen.description':'When an MCP client opens a host, bring the main window to the foreground. Turn off to keep working without interruption.',
'ai.externalMcp.silentSessions.description':'Sessions opened by AI stay out of your tab bar and are not restored after restart. View them anytime from the tray panel.',
'ai.externalMcp.sessionIdleTimeout.description':'Automatically close sessions opened by an AI after this many minutes without terminal or file activity.',
'ai.externalMcp.usage.title':'How to use',
'ai.externalMcp.usage.keepRunning':'1. Turn on External MCP and keep Netcatty running.',
'ai.externalMcp.usage.localhost':'2. Clients connect via the local launcher (127.0.0.1 only). Discovery is removed when you disable the switch.',
'ai.externalMcp.usage.permissions':'3. Write operations follow Settings → AI → Safety (observer / confirm / auto) and the command blocklist.',
'ai.externalMcp.usage.capabilities':'4. Full catalog tools are available: terminal, SFTP, Vault, and port forwarding. Secrets (passwords / private keys) are never returned.',
'ai.externalMcp.security.description':'Listens on 127.0.0.1 with a rotating token, reuses AI Permission Mode for writes, and removes discovery when disabled. No OAuth — this is a local desktop bridge.',
'ai.externalMcp.permissionMode.hint':'Same setting as Settings → AI → Safety. Auto runs Netcatty write tools without Netcatty approval prompts; Confirm asks each time. External clients (Codex / Claude / Grok) may still show their own tool approval UI.',
'ai.externalMcp.clientConfiguration.description':'Pick a client to one-click install, or copy CLI / config snippets.',
'ai.externalMcp.client.codex':'Codex',
'ai.externalMcp.client.claude':'Claude Code',
'ai.externalMcp.client.grok':'Grok',
'ai.externalMcp.client.cursor':'Cursor',
'ai.externalMcp.cliCommand':'CLI command',
'ai.externalMcp.configSnippet':'Config snippet',
'ai.externalMcp.addToCodex':'Add to Codex',
'ai.externalMcp.addToClaude':'Add to Claude Code',
'ai.externalMcp.addToGrok':'Add to Grok',
'ai.externalMcp.codexAdded':'Codex MCP entry added. Restart Codex or open a new Codex session.',
'ai.externalMcp.claudeAdded':'Claude Code MCP entry added. Restart Claude Code or open a new Claude Code session.',
'ai.externalMcp.grokAdded':'Grok MCP entry added. Restart Grok or open a new Grok session.',
'ai.externalMcp.installCodex':'Install Codex separately, then click Refresh.',
'ai.externalMcp.installClaude':'Install Claude Code separately, then click Refresh.',
'ai.externalMcp.installGrok':'Install the Grok CLI separately, then click Refresh.',
'ai.externalMcp.conflict.description':'A netcatty-external entry already exists and points elsewhere. Remove or edit it manually.',
'ai.externalMcp.enableForLauncher':'Enable External MCP to get a usable launcher path.',
'ai.externalMcp.cursor.title':'Cursor / other clients',
'ai.externalMcp.cursor.description':'Merge this into your MCP config (for example ~/.cursor/mcp.json). Do not replace the whole file if you already have other servers.',
'ai.externalMcp.status.codexNotFound':'Codex not found',
'ai.externalMcp.status.claudeNotFound':'Claude Code not found',
'ai.externalMcp.status.grokNotFound':'Grok not found',
'ai.externalMcp.status.conflict':'Conflict',
'ai.userSkills.title':'User Skills',
'ai.userSkills.description':'Open the Netcatty skills folder to add your own skill directories. Netcatty scans these skills automatically and injects only lightweight indexes unless a skill clearly matches the current request.',
'ai.userSkills.openFolder':'Open Skills Folder',
'ai.userSkills.reload':'Reload Skills',
'ai.userSkills.location':'Location',
'ai.userSkills.loading':'Scanning user skills...',
'ai.userSkills.empty':'No user skills found yet. Open the folder to add skill directories with a SKILL.md file.',
'ai.userSkills.unavailable':'User skills are unavailable in this environment.',
'ai.userSkills.status.ready':'Ready',
'ai.userSkills.status.warning':'Warning',
// AI Quick Messages
'ai.quickMessages.title':'Quick Messages',
'ai.quickMessages.description':'Create reusable prompts you can insert from the AI chat with / or the quick-message button. Unlike user skills, quick messages fill the composer with text.',
'ai.chat.loadMoreSessions':'Load more sessions ({n} more)',
'ai.chat.noSessions':'No previous sessions',
'ai.chat.retryHint':'You can retry by sending your message again.',
'ai.chat.approvalTimeout':'Tool approval timed out after 5 minutes. You can retry by sending your message again.',
'ai.chat.menuHosts':'Hosts',
'ai.chat.menuContext':'Context',
'ai.chat.menuFiles':'Files',
'ai.chat.menuImage':'Image',
'ai.chat.menuMentionHost':'Mention Host',
'ai.chat.menuUserSkills':'User Skills',
'ai.chat.menuSlashCommands':'Slash Commands',
'ai.chat.slashCommands':'Slash commands',
'ai.chat.slashSystemCommands':'Commands',
'ai.chat.slashQuickMessages':'Quick messages',
'ai.chat.slashUserSkills':'User skills',
'ai.chat.quickMessages':'Slash commands',
'ai.chat.slashNoResults':'No matching commands',
'ai.chat.slashEmptyHint':'Add prompts in Settings → AI → Quick Messages.',
// AI Chat Shortcuts
'ai.chatShortcuts.title':'Chat Shortcuts',
'ai.chatShortcuts.selectionAction':'Show Add to Conversation when selecting terminal text',
'ai.chatShortcuts.selectionAction.description':'Show a small AI button next to selected terminal text.',
// AI Error
'ai.codex.bridgeError':'Codex main-process handlers are not loaded yet. Fully restart Netcatty, or restart the Electron dev process, then try again.',
// AI Web Search
'ai.webSearch.title':'Web Search',
'ai.webSearch.enable':'Enable Web Search',
'ai.webSearch.enable.description':'Allow the AI agent to search the web for current information.',
'ai.webSearch.provider':'Search Provider',
'ai.webSearch.provider.description':'Choose a web search API provider.',
'ai.webSearch.apiKey':'API Key',
'ai.webSearch.apiKey.description':'API key for the selected search provider.',
'ai.webSearch.apiKey.placeholder':'Enter API key...',
'ai.webSearch.apiHost':'API Host',
'ai.webSearch.apiHost.description':'Custom API endpoint. Leave default unless you use a proxy.',
'ai.webSearch.apiHost.searxngDescription':'URL of your SearXNG instance (required).',
'ai.webSearch.maxResults':'Max Results',
'ai.webSearch.maxResults.description':'Maximum number of search results to return (1-20).',
// AI Safety Settings
'ai.safety.title':'Safety',
'ai.safety.permissionMode':'Permission Mode',
'ai.safety.permissionMode.description':'Controls how the AI interacts with your Netcatty terminal sessions. Observer mode blocks write operations that go through Netcatty. External agent CLIs may still have their own local tools and approval flow.',
'ai.safety.permissionMode.observer':'Observer - Read only, no actions',
'ai.safety.permissionMode.confirm':'Confirm - Ask before actions',
'ai.safety.commandTimeout.description':'Maximum seconds a command can run before being terminated through Netcatty execution.',
'ai.safety.commandTimeout.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.',
'ai.safety.blocklist':'Command Blocklist',
'ai.safety.blocklist.description':'Regex patterns to block dangerous commands executed through Netcatty.',
'ai.safety.grants.description':'Confirm mode asks before running an operation. Saved rules automatically allow matching operations across all terminal sessions/nodes, and can be edited manually.',
'ai.safety.grants.empty':'No saved rules yet. Approve a tool with “Always allow”, or add one manually.',
'ai.safety.note':'These safety settings are enforced for actions that go through Netcatty. External agent CLIs may also expose local tools that are governed by the agent itself.',
// Unified tooltips for terminal workspace and top tabs (issue #954)
'terminal.layer.addTerminal':'Add Terminal',
'terminal.layer.switchToSplitView':'Switch to Split View',
'terminal.layer.sftp':'SFTP',
'terminal.layer.scripts':'Scripts',
'terminal.layer.history':'History',
'terminal.layer.theme':'Theme',
'terminal.layer.notes':'Notes',
'terminal.layer.aiChat':'AI Chat',
'terminal.layer.movePanelLeft':'Move panel to left',
'terminal.layer.movePanelRight':'Move panel to right',
'terminal.layer.closePanel':'Close panel',
'terminal.layer.closePane':'Close split',
'terminal.layer.resizeSplit':'Resize split',
'terminal.layer.splitHorizontal':'Split top and bottom',
'terminal.layer.splitVertical':'Split left and right',
'terminal.layer.openInNewSplit':'Open in new split',
'scripts.trigger.onOutputHint':'Fires when server output matches the pattern (user keystroke echo is ignored). With no target hosts configured, listens on the current connected session; with targets, only those hosts. Disabled in alternate-screen apps such as vim or htop. Suppressed while another script is running on this session; rechecked after it finishes. For in-flow waits, use nct.screen.waitForText or nct.screen.waitForRegex in script code.',
'scripts.actions.save':'Save',
'scripts.actions.runNow':'Run now',
'scripts.actions.openEditor':'Open editor',
'scripts.actions.openEditorHint':'Edit script in a larger window',
'scripts.editor.modalTitle':'Script editor',
'scripts.editor.modalSubtitle':'Edit metadata and script code in a larger workspace.',
'scripts.editor.lineCount':'{count} lines',
'scripts.editor.resize':'Resize editor',
'scripts.targets.hint':'Automatic triggers apply to selected hosts, or to all hosts when that option is enabled.',
'scripts.targets.connectOrderHint':'Run order for connect scripts is configured per host under Host details → Automation.',
'scripts.actions.runNowHint':'Run on selected targets, or on all connectable hosts when that option is enabled.',
'scripts.actions.runParallel':'Run on all tabs (parallel)',
'scripts.actions.runSequential':'Run on all tabs (sequential)',
'scripts.actions.runOnAllTabs':'Run on all tabs',
'scripts.actions.skippedConnectingSessions':'{count} tab(s) still connecting and were skipped',
'scripts.actions.noRunnableHosts':'No connectable hosts match this script\'s targets',
'scripts.sidePanel.library':'Library',
'scripts.sidePanel.running':'Running',
'scripts.sidePanel.newScript':'New script',
'scripts.running.empty':'No scripts are running in this session.',
'scripts.running.unnamed':'Untitled script',
'scripts.running.status.running':'Running',
'scripts.running.status.paused':'Paused',
'scripts.running.status.completed':'Completed',
'scripts.running.status.failed':'Failed',
'scripts.running.waitingFor':'Waiting for {pattern}',
'scripts.running.waitingForLabel':'Waiting for',
'scripts.running.waitingForShellPrompt':'shell prompt (# or $)',
'scripts.running.lastSent':'Sent: {command}',
'scripts.recording.start':'Start recording',
'scripts.recording.active':'Stop recording',
'scripts.recording.startHint':'Record actions in the focused terminal and generate nct script code',
'scripts.recording.unavailableHint':'Recording is not available here — use the scripts sidebar on the right',
'scripts.recording.activeHint':'Recording this terminal. Type commands as usual; click Stop or use the REC control in the toolbar to finish and save.',
'scripts.recording.started':'Recording started — operate in the terminal',
'scripts.recording.noSession':'Connect a terminal first (single tab or workspace)',
'scripts.recording.alreadyActive':'Another terminal is already recording',
'scripts.recording.stop':'Stop recording',
'scripts.recording.pause':'Pause recording',
'scripts.recording.resume':'Resume recording',
'scripts.recording.saveTitle':'Save recorded script',
'scripts.recording.packagePlaceholder':'Save to folder',
'scripts.recording.rootPackage':'Root',
'scripts.recording.save':'Save',
'scripts.recording.saveAndEdit':'Save and edit',
'scripts.recording.helpTitle':'How to record a script',
'scripts.recording.helpIntro':'Recording turns what you do in the terminal into a reusable automation script — useful for deploys, health checks, and other repetitive tasks.',
'scripts.recording.helpStep1':'Connect to a host. A standalone terminal tab or a terminal inside a workspace both work — open the scripts sidebar on the right.',
'scripts.recording.helpStep2':'Click Start recording. A red REC badge in the terminal toolbar means recording is active.',
'scripts.recording.helpStep3':'Type commands in the terminal as you normally would. Each Enter press is captured as one step.',
'scripts.recording.helpStep4':'When you are done, click Stop recording, or use the stop control next to REC in the terminal toolbar.',
'scripts.recording.helpStep5':'Name the script and save. Choose Save and edit if you want to fine-tune the generated code in the script editor.',
'scripts.recording.helpTipsTitle':'Tips',
'scripts.recording.helpTip1':'Pauses longer than 1 second between actions are recorded as wait time so playback is not too fast.',
'scripts.recording.helpTip2':'After each command, recording waits for a shell prompt (such as $ or #) before the next step.',
'scripts.recording.helpTip3':'Password input is marked sensitive and is not stored in plain text in the script.',
'scripts.recording.helpTip4':'For vim, menus, or other interactive flows, record the main commands first, then refine the script manually.',
'scripts.dialog.title':'Script',
'scripts.dialog.ok':'OK',
'scripts.dialog.required':'Required',
'scripts.dialog.numberInvalid':'Enter a valid number',
'scripts.dialog.numberMin':'Must be at least {min}',
'scripts.dialog.numberMax':'Must be at most {max}',
'scripts.dialog.numberStep':'Must use increments of {step}',
'terminal.osc7Setup.desc':'Netcatty will add OSC 7 prompt hooks for the current remote user. This helps SFTP follow the terminal directory after sudo or su.',
'terminal.osc7Setup.targets':'Possible files to update',
'terminal.auth.noKeysHint':'No keys available. Add keys in Keychain.',
'terminal.auth.continueSave':'Continue & Save',
'terminal.auth.credentialsUnavailable':'Saved credentials cannot be decrypted on this device. Please re-enter and save them again.',
'terminal.auth.jumpCredentialsUnavailable':'A jump host has saved credentials that cannot be decrypted on this device. Open host settings and re-enter them.',
'terminal.auth.proxyCredentialsUnavailable':'Proxy credentials cannot be decrypted on this device. Open host settings and re-enter the proxy password.',
'terminal.auth.keyUnavailableFallbackPassword':'Saved SSH key is unavailable on this device. Falling back to password authentication.',
'terminal.progress.timeoutIn':'Timeout in {seconds}s',
'terminal.progress.waitingForUserInput':'Waiting for user input',
'terminal.progress.disconnected':'Disconnected',
'terminal.progress.cancelling':'Cancelling...',
'terminal.progress.startOver':'Start over',
'terminal.progress.enterReconnectHint':'Press Enter to reconnect',
'terminal.progress.autoReconnectScheduled':'Connection lost. Reconnecting in {seconds}s (attempt {attempt}).',
'cloudSync.convergent.desc':'Uses an encrypted CRDT replica to preserve offline edits, concurrent deletions, and changes from every connected provider.',
'cloudSync.convergent.active':'CRDT v2 is active. Provider writes are verified after upload.',
'cloudSync.convergent.paused':'CRDT v2 is paused on this device; cloud metadata is retained.',
'cloudSync.localBackups.restoreMissing':'Backup not found.',
'cloudSync.localBackups.protectiveBackupFailed':'Safety backup could not be created, so the restore was aborted to protect your current data. Resolve the underlying issue (e.g. keychain access) and try again. Details: {message}',
'cloudSync.localBackups.restoreConfirmTitle':'Restore this backup?',
'cloudSync.localBackups.restoreConfirmDesc':'Your current hosts, keys, snippets and settings will be replaced with the contents of this backup. A protective snapshot of your current data is taken automatically first.',
'cloudSync.localBackups.unavailableDesc':'This platform does not expose a secure keychain to Netcatty, so local backups cannot be written safely. Install Netcatty on a system with a supported keychain to enable the local backup history.',
'cloudSync.clearLocal.desc':'Reset local version and sync history. Next sync will download from cloud.',
'cloudSync.clearLocal.button':'Clear',
'cloudSync.clearLocal.dialog.title':'Clear Local Vault Data?',
'cloudSync.clearLocal.dialog.desc':'This will reset local version to 0 and clear sync history. Your next sync will download data from the cloud, replacing local data.',
'cloudSync.clearLocal.dialog.cancel':'Cancel',
'cloudSync.clearLocal.dialog.confirm':'Clear Local Data',
'cloudSync.clearLocal.toast.title':'Local data cleared',
'cloudSync.clearLocal.toast.desc':'Local version reset to 0. Sync to download from cloud.',
'snippets.import.modal.noFile':'No file selected yet. Use the example format below, or a plain JSON array of snippets, then choose one or more files.',
'vault.import.progress.persistFailed':'Imported hosts could not be saved. Free some local storage space and try again.',
'vault.import.progress.rollbackFailed':'The import could not be rolled back safely. Some imported hosts may remain; review the Vault before retrying.',
'vault.import.sshConfig.importOnlyDesc':'One-time import. Changes won\'t sync back to the file.',
'vault.import.sshConfig.managed':'Managed Sync',
'vault.import.sshConfig.managedDesc':'Keep in sync. Changes will be saved back to the file.',
'vault.import.sshConfig.managedDestinationHint':'Managed sync always uses its own managed group; the destination above applies to one-time import only.',
'vault.import.sshConfig.managedSuccess':'Imported {count} hosts. File is now managed.',
'vault.import.sshConfig.alreadyManaged':'This file is already being managed.',
'vault.import.sshConfig.alreadyManagedDesc':'This file is already managed under group "{group}". Remove the existing managed source first if you want to re-import.',
'vault.import.sshConfig.noFilePath':'Cannot manage this file.',
'vault.import.sshConfig.noFilePathDesc':'Unable to determine the file path. Managed sync requires access to the file system.',
// Known Hosts
'knownHosts.search.placeholder':'Search known hosts...',
'knownHosts.action.scanSystem':'Scan System',
'knownHosts.action.importFile':'Import File',
'knownHosts.action.browseFile':'Browse File',
'knownHosts.empty.title':'No Known Hosts',
'knownHosts.empty.desc':
"Known hosts are SSH servers you've connected to before. Import from your system's known_hosts file to get started.",
'knownHosts.results.showingLimited':
'Showing {shown} of {total} hosts. Use search to find specific hosts.',
'knownHosts.toast.scanUnavailable':'System scan is unavailable on this platform.',
'knownHosts.toast.scanNoFile':'No system known_hosts file found.',
'knownHosts.toast.scanNoEntries':'No usable entries found in known_hosts.',
'knownHosts.toast.scanImported':'Imported {count} new hosts.',
'knownHosts.toast.scanNoNew':'No new hosts found.',
'knownHosts.toast.scanFailed':'Failed to scan system known_hosts.',
// Port Forwarding
'pf.empty.title':'Set up port forwarding',
'pf.empty.desc':'Save port forwarding to access databases, web apps, and other services.',
'sftp.transfers.pathConflict.paused':'Another transfer for "{name}" is paused. Resume or cancel it first.',
'sftp.transfers.pathConflict.inProgress':'Another transfer for "{name}" is already in progress.',
'sftp.transferCenter.title':'File Transfers',
'sftp.transferCenter.quitConfirm':'{count} file transfer(s) are unfinished. Progress will be saved and must be resumed manually next time. Quit anyway?',
'settings.sftp.transferConcurrency.desc':'Maximum number of files transferred in parallel for the same endpoint, including folder contents and separately selected files. Higher values may improve speed but can overwhelm some servers.',
'settings.sftp.transferPoolIdleTtl.desc':'How long dedicated transfer SFTP connections stay warm after idle. Longer values reduce cold-start delay before the next upload or download. Busy transfers are never closed.',
'settings.ssh.transportIdleTtl.desc':'How long a shared SSH connection stays warm after the last terminal, SFTP browse, bulk transfer, or port-forward lease is released. While warm, reconnecting to the same host can skip re-authentication (including MFA). Active sessions and transfers are never closed.',
'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.jmsDeepLink.enableDesc':'Register Netcatty as the handler for JumpServer jms:// client links. May conflict with the official JumpServer client.',
// Settings > SFTP Auto Sync
'settings.sftp.autoSync':'Auto-sync to remote',
'settings.sftp.autoSync.desc':'Automatically sync file changes back to the remote server when opening files with external applications',
'settings.sftp.showHiddenFiles.desc':'Display hidden files (dotfiles on Unix/macOS and files with the hidden attribute on Windows) in the SFTP file browser.',
'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.',
'hostDetails.hostname.placeholder':'IP or Hostname',
'hostDetails.section.general':'General',
'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.',
'hostDetails.sftp.fileProtocol.auto':'Auto',
'hostDetails.sftp.fileProtocol.sftp':'SFTP only',
'hostDetails.sftp.fileProtocol.scp':'SCP mode',
'hostDetails.sftp.sudo':'Sudo Mode',
'hostDetails.sftp.sudo.desc':'Automatically acquire Root privileges using stored password',
'hostDetails.sftp.sudo.passwordWarning':'Sudo mode requires a password. Configure one above, or ensure the server allows passwordless sudo.',
'hostDetails.sftp.encoding':'Filename Encoding',
'hostDetails.sftp.encoding.desc':'Select the encoding used to decode and send SFTP filenames.',
'hostDetails.label.placeholder':'Label (e.g., Production Server)',
'hostDetails.agentForwarding.desc':'Allow remote server to use your local SSH keys (e.g., for git operations)',
'hostDetails.agentForwarding.agentNotRunning':'SSH Agent is not available',
'hostDetails.agentForwarding.agentNotRunningHint':'No SSH agent detected. Enable OpenSSH Authentication Agent in Windows Services, or use a compatible agent such as Bitwarden, 1Password, or gpg-agent.',
'hostDetails.deviceType.desc':'Enable for network equipment (switches, routers, firewalls) connected via SSH. Commands are sent as-is without shell wrapping, compatible with vendor CLIs like Huawei VRP and Cisco IOS.',
'hostDetails.deviceType.warning':'AI agent commands will be sent directly without exit code tracking. Only enable for devices that do not run a standard shell.',
'hostDetails.legacyAlgorithms.desc':'Enable deprecated SSH algorithms (diffie-hellman-group1, ssh-dss, 3des-cbc, etc.) for connecting to older network equipment.',
'hostDetails.legacyAlgorithms.warning':'These algorithms have known security weaknesses. Only enable for legacy devices that do not support modern cryptography.',
'hostDetails.skipEcdsaHostKey.desc':'Some old Huawei / Cisco switches produce non-standard ECDSA host-key signatures that cause "signature verification failed". Turning this on drops every ecdsa-sha2-* from the client offer so negotiation falls back to RSA / Ed25519.',
'hostDetails.algorithms.advanced.desc':'Replace the offered algorithm list for any category on a per-host basis. Leaving a category untouched uses the default; selecting a subset fully replaces the default list. Incorrect values can make the host unreachable.',
'hostDetails.algorithms.inheritedNotice':'The current group has algorithm overrides set for: {categories}. The "Reset" button here falls back to the group\'s lists, not NetCatty\'s defaults. To ignore the group restriction, clear the override in the group\'s algorithm settings.',
'hostDetails.keepalive.override':'Override global keepalive',
'hostDetails.keepalive.desc':'Use a custom keepalive policy for this host instead of the global setting. Useful for older routers or switches whose SSH server does not reply to keepalive@openssh.com requests — set interval to 0 to disable keepalive entirely on this host.',
'hostDetails.keepalive.disabledHint':'Interval = 0 disables keepalive for this host. The session will rely on TCP-level timeouts to detect a dead connection.',
'ai.providers.contextWindow.help':'Оставьте пустым, чтобы использовать значение из списка моделей, если оно доступно; иначе Netcatty применит безопасное значение по умолчанию.',
'ai.providers.contextWindow.error':'Введите положительное целое число или оставьте поле пустым.',
'ai.providers.refreshModels':'Обновить модели',
'ai.providers.searchModel':'Искать или ввести ID модели...',
'ai.providers.advancedParams.default':'По умолчанию у провайдера',
// AI Codex
'ai.codex':'Codex',
'ai.codex.title':'Codex CLI',
'ai.codex.description':'Подключение OpenAI Codex. Здесь можно войти через ChatGPT или включить API-ключ OpenAI-совместимого провайдера и пользовательский endpoint в настройках.',
'ai.codex.appServer.description':'Постоянный протокол Codex с нативными подтверждениями, песочницей, актуальными моделями и вопросами во время выполнения. SDK остаётся режимом по умолчанию.',
'ai.codex.appServer.checking':'Проверка поддержки App Server…',
'ai.codex.appServer.available':'Эта версия Codex CLI поддерживает App Server.',
'ai.codex.appServer.modelCatalogWarning':'Динамический каталог моделей Codex недоступен. Используется встроенный список моделей.',
'ai.codex.appServer.approval.allowSession':'Разрешить для сеанса',
'ai.codex.appServer.userInput.title':'Codex требуется ваш ответ',
'ai.codex.appServer.userInput.description':'Ответьте на вопросы, чтобы продолжить текущую задачу.',
'ai.codex.appServer.userInput.other':'Введите другой ответ',
'ai.codex.appServer.userInput.autoResolve':'Если ответа не будет вовремя, Codex продолжит автоматически.',
'ai.codex.customConfigHint':'Используется пользовательский провайдер "{provider}", настроенный в ~/.codex/config.toml — вход через ChatGPT не требуется.',
'ai.codex.customConfigMissingEnvKey':'Предупреждение: {envKey} не задана в переменных окружения вашей оболочки. Экспортируйте её (или запустите netcatty из оболочки, где она задана), чтобы Codex мог пройти аутентификацию.',
'ai.codex.notConnected':'Не подключено',
'ai.codex.statusUnknown':'Статус неизвестен',
'ai.codex.path':'Путь:',
'ai.codex.notFoundHint':'Не удалось найти codex в PATH. Установите его или укажите путь к исполняемому файлу ниже.',
'ai.claude.configSection':'Аутентификация и конфигурация (опционально)',
'ai.claude.configDir':'Каталог конфигурации',
'ai.claude.configDir.placeholder':'~/.claude (пусто — по умолчанию)',
'ai.claude.configDir.hint':'Задаёт CLAUDE_CONFIG_DIR — укажите папку, где выполнен вход `claude` (содержит settings.json и учётные данные).',
'ai.claude.settings':'Файл настроек',
'ai.claude.settings.placeholder':'~/team-settings.json (путь или встроенный {"model":"..."})',
'ai.claude.settings.hint':'Опционально. Путь к settings.json или встроенный JSON, передаётся в SDK как `settings`. Дополняет «Каталог конфигурации» выше и независим от него (накладывается сверху, не заменяет).',
'ai.claude.envVars.hint':'По одному KEY=VALUE в строке, передаётся агенту Claude. Хранится локально в открытом виде — для API-ключей и учётных данных используйте «Каталог конфигурации» выше (вход `claude`).',
'ai.claude.check':'Проверить',
'ai.claude.resetPath':'Сбросить',
// AI GitHub Copilot CLI
'ai.copilot.title':'GitHub Copilot CLI',
'ai.copilot.description':'Использует GitHub Copilot CLI. После обнаружения может быть выбран как внешний агент для программирования.',
'ai.copilot.detecting':'Обнаружение...',
'ai.copilot.detected':'Обнаружен',
'ai.copilot.notFound':'Не найден',
'ai.copilot.path':'Путь:',
'ai.copilot.notFoundHint':'Не удалось найти copilot в PATH. Установите его или укажите путь к исполняемому файлу ниже.',
'ai.cursor.description':'Использует Cursor SDK или локальный вход Agent CLI.',
'ai.cursor.detecting':'Обнаружение...',
'ai.cursor.detected':'Доступен',
'ai.cursor.notFound':'Недоступен',
'ai.cursor.path':'Среда:',
'ai.cursor.notFoundHint':'Укажите API-ключ или переключитесь на CLI login.',
'ai.cursor.notInstalledHint':'Cursor SDK / Agent CLI не обнаружен.',
'ai.cursor.installStatus':'Среда Cursor',
'ai.cursor.installed':'Обнаружено',
'ai.cursor.notInstalled':'Не обнаружено',
'ai.cursor.modeCli':'CLI login',
'ai.cursor.modeApiKey':'API Key',
'ai.cursor.modeCliHint':'Использует локальный `cursor-agent login` и квоту подписки Auto. Сохранённый API-ключ сохраняется, но в этом режиме не используется.',
'ai.cursor.modeApiKeyHint':'Использует платный Cursor API. CLI login в этом режиме игнорируется.',
'ai.cursor.cliLoginStatus':'CLI login',
'ai.cursor.cliLoginOk':'Вход выполнен',
'ai.cursor.cliLoginAs':'Вход как {{email}}',
'ai.cursor.cliLoginMissing':'Нет входа',
'ai.cursor.cliLoginHint':'Выполните `cursor-agent login` в терминале, затем нажмите Check.',
'ai.cursor.apiKeyPlaceholder.env':'Используется CURSOR_API_KEY; введите ключ для замены',
'ai.cursor.apiKeyEnvHint':'Cursor может использовать CURSOR_API_KEY из shell. Сохраняйте ключ здесь только если хотите переопределить его в Netcatty.',
'ai.cursor.apiKeyOverrideHint':'Netcatty сначала использует сохранённый здесь ключ, затем CURSOR_API_KEY.',
'ai.codebuddy.description':'Использует CodeBuddy Code через официальный Agent SDK (`@tencent-ai/agent-sdk`). После обнаружения может быть выбран как внешний агент для программирования.',
'ai.codebuddy.detecting':'Обнаружение...',
'ai.codebuddy.detected':'Обнаружен',
'ai.codebuddy.notFound':'Не найден',
'ai.codebuddy.path':'Путь:',
'ai.codebuddy.notFoundHint':'Не удалось найти codebuddy в PATH. Установите его или укажите путь к исполняемому файлу ниже.',
'ai.codebuddy.envVars.hint':'По одной записи KEY=VALUE на строку, передаются агенту CodeBuddy. Укажите CODEBUDDY_API_KEY или CODEBUDDY_AUTH_TOKEN для аутентификации. Хранятся локально в открытом виде.',
'ai.codebuddy.elicitation.title':'CodeBuddy требуется ваш ответ',
'ai.codebuddy.elicitation.description':'Проверьте запрос, чтобы продолжить текущий ход.',
'ai.codebuddy.elicitation.validation.invalidType':'Поле «{field}» содержит недопустимое значение.',
'ai.codebuddy.elicitation.validation.integer':'Поле «{field}» должно быть целым числом.',
'ai.codebuddy.elicitation.validation.notInteger':'Поле «{field}» должно быть целым числом.',
'ai.codebuddy.elicitation.validation.minimum':'Значение «{field}» должно быть не меньше {limit}.',
'ai.codebuddy.elicitation.validation.maximum':'Значение «{field}» должно быть не больше {limit}.',
'ai.codebuddy.elicitation.validation.minLength':'Поле «{field}» должно содержать не менее {limit} символов.',
'ai.codebuddy.elicitation.validation.maxLength':'Поле «{field}» должно содержать не более {limit} символов.',
'ai.codebuddy.elicitation.validation.minItems':'Выберите не менее {limit} вариантов для «{field}».',
'ai.codebuddy.elicitation.validation.maxItems':'Выберите не более {limit} вариантов для «{field}».',
'ai.codebuddy.elicitation.validation.format':'Поле «{field}» должно соответствовать формату {format}.',
'ai.codebuddy.elicitation.validation.option':'Выберите допустимый вариант для «{field}».',
// AI Grok Build (in-app managed agent — distinct from External MCP client install)
'ai.grok.title':'Grok Build',
'ai.grok.description':'Агент программирования Grok Build от xAI (CLI). Установите Grok CLI, выполните `grok login` или задайте XAI_API_KEY, затем выберите его как внешнего агента.',
'ai.grok.detecting':'Обнаружение...',
'ai.grok.detected':'Обнаружен',
'ai.grok.notFound':'Не найден',
'ai.grok.path':'Путь:',
'ai.grok.notFoundHint':'Не удалось найти grok в PATH. Установите Grok Build CLI или укажите путь к исполняемому файлу ниже.',
'Подключение к Grok через Agent Client Protocol (grok agent stdio). Netcatty MCP внедряется в session/new. Выключите, чтобы использовать исходный headless streaming-json CLI.',
'ai.defaultAgent.description':'Агент, который будет использоваться при запуске новой AI-сессии',
'ai.defaultAgent.catty':'Catty (встроенный)',
'ai.toolAccess.title':'Доступ к инструментам',
'ai.toolAccess.mode':'Режим доступа Netcatty',
'ai.toolAccess.description':'Выберите, как внешние агенты получают доступ к сессиям Netcatty. MCP предоставляет встроенный сервер, а Skills + CLI указывает агентам на локальный skill Netcatty и команды CLI.',
'ai.toolAccess.mode.mcp':'MCP',
'ai.toolAccess.mode.skills':'Skills + CLI',
'ai.externalMcp.title':'Внешний MCP',
'ai.externalMcp.description':'Откройте Netcatty как MCP-сервер для Codex, Claude Code, Cursor и Grok. Набор инструментов тот же, что у встроенных агентов. Держите Netcatty запущенным, пока клиенты подключены.',
'ai.externalMcp.sessionsExposed':'Сессий в области: {count}',
'ai.externalMcp.mode':'Режим доступности',
'ai.externalMcp.mode.temporary':'Временный',
'ai.externalMcp.mode.persistent':'Постоянный',
'ai.externalMcp.mode.description':'Временный режим отключается после простоя. Постоянный восстанавливает External MCP при запуске Netcatty.',
'ai.externalMcp.idleTimeout':'Таймаут простоя',
'ai.externalMcp.idleTimeout.description':'В временном режиме отключить External MCP после указанного числа минут без MCP-операций.',
'ai.externalMcp.idleTimeout.minutes':'мин',
'ai.externalMcp.focusOnHostOpen':'Активировать окно при host_open',
'ai.externalMcp.focusOnHostOpen.description':'Когда MCP-клиент открывает хост, выводить главное окно на передний план. Отключите, чтобы работать без прерываний.',
'ai.externalMcp.silentSessions.description':'Сеансы, открытые ИИ, не отображаются на панели вкладок и не восстанавливаются после перезапуска. Их можно посмотреть в любой момент через панель в трее.',
'ai.externalMcp.sessionIdleTimeout.description':'Автоматически закрывать сеансы, открытые ИИ, после указанного числа минут без операций терминала или файлов.',
'ai.externalMcp.usage.title':'Как пользоваться',
'ai.externalMcp.usage.keepRunning':'1. Включите External MCP и держите Netcatty запущенным.',
'ai.externalMcp.usage.localhost':'2. Клиенты подключаются через локальный launcher (только 127.0.0.1). Discovery удаляется при отключении.',
'ai.externalMcp.usage.permissions':'3. Запись следует Settings → AI → Safety и списку блокировки команд.',
'ai.externalMcp.usage.capabilities':'4. Доступен полный catalog: терминал, SFTP, Vault и проброс портов. Секреты не возвращаются.',
'ai.externalMcp.security.description':'Слушает только 127.0.0.1 с ротацией token, использует AI Permission Mode для записи и удаляет discovery при отключении.',
'ai.externalMcp.permissionMode':'Текущий режим разрешений: {mode}',
'ai.externalMcp.permissionMode.label':'Режим разрешений на запись',
'ai.externalMcp.permissionMode.hint':'Тот же параметр, что Settings → AI → Safety. Auto выполняет write-инструменты Netcatty без запросов; Confirm спрашивает каждый раз. Внешние клиенты (Codex / Claude / Grok) могут показывать своё подтверждение инструментов.',
'ai.externalMcp.addToClaude':'Добавить в Claude Code',
'ai.externalMcp.addToGrok':'Добавить в Grok',
'ai.externalMcp.codexAdded':'Запись MCP для Codex добавлена. Перезапустите Codex или откройте новую сессию.',
'ai.externalMcp.claudeAdded':'Запись MCP для Claude Code добавлена. Перезапустите Claude Code или откройте новую сессию.',
'ai.externalMcp.grokAdded':'Запись MCP для Grok добавлена. Перезапустите Grok или откройте новую сессию.',
'ai.externalMcp.installCodex':'Сначала установите Codex, затем нажмите Обновить.',
'ai.externalMcp.installClaude':'Сначала установите Claude Code, затем нажмите Обновить.',
'ai.externalMcp.installGrok':'Сначала установите Grok CLI, затем нажмите Обновить.',
'ai.externalMcp.conflict.description':'Запись netcatty-external уже существует и указывает в другое место. Удалите или измените её вручную.',
'ai.externalMcp.enableForLauncher':'Включите External MCP, чтобы получить путь launcher.',
'ai.externalMcp.cursor.title':'Cursor / другие клиенты',
'ai.externalMcp.cursor.description':'Объедините с MCP-конфигом (например ~/.cursor/mcp.json). Не заменяйте весь файл, если там уже есть другие серверы.',
'ai.externalMcp.status.codexNotFound':'Codex не найден',
'ai.externalMcp.status.claudeNotFound':'Claude Code не найден',
'ai.externalMcp.status.grokNotFound':'Grok не найден',
'ai.externalMcp.status.conflict':'Конфликт',
'ai.userSkills.title':'Пользовательские skills',
'ai.userSkills.description':'Откройте папку skills Netcatty, чтобы добавить свои каталоги skills. Netcatty автоматически сканирует их и добавляет только лёгкие индексы, если skill явно не соответствует текущему запросу.',
'ai.userSkills.empty':'Пользовательские skills пока не найдены. Откройте папку, чтобы добавить каталоги skills с файлом SKILL.md.',
'ai.userSkills.unavailable':'Пользовательские skills недоступны в этой среде.',
'ai.userSkills.status.ready':'Готово',
'ai.userSkills.status.warning':'Предупреждение',
// AI Quick Messages
'ai.quickMessages.title':'Быстрые сообщения',
'ai.quickMessages.description':'Создавайте часто используемые подсказки и вставляйте их в AI-чат через / или кнопку быстрых сообщений. В отличие от user skills, быстрые сообщения заполняют поле ввода текстом.',
'ai.chat.slashEmptyHint':'Добавьте подсказки в Настройки → AI → Быстрые сообщения.',
// AI Chat Shortcuts
'ai.chatShortcuts.title':'Быстрые действия чата',
'ai.chatShortcuts.selectionAction':'Показывать «Добавить в чат» при выделении в терминале',
'ai.chatShortcuts.selectionAction.description':'Показывать небольшую кнопку AI рядом с выделенным текстом терминала.',
// AI Error
'ai.codex.bridgeError':'Обработчики главного процесса Codex ещё не загружены. Полностью перезапустите Netcatty или dev-процесс Electron и попробуйте снова.',
// AI Web Search
'ai.webSearch.title':'Веб-поиск',
'ai.webSearch.enable':'Включить веб-поиск',
'ai.webSearch.enable.description':'Разрешить AI-агенту искать в интернете актуальную информацию.',
'ai.webSearch.provider':'Провайдер поиска',
'ai.webSearch.provider.description':'Выберите провайдера API веб-поиска.',
'ai.webSearch.apiKey':'API-ключ',
'ai.webSearch.apiKey.description':'API-ключ для выбранного провайдера поиска.',
'ai.webSearch.maxResults':'Макс. число результатов',
'ai.webSearch.maxResults.description':'Максимальное количество результатов поиска для возврата (1-20).',
// AI Safety Settings
'ai.safety.title':'Безопасность',
'ai.safety.permissionMode':'Режим разрешений',
'ai.safety.permissionMode.description':'Управляет тем, как AI взаимодействует с вашими терминалами. Режим наблюдателя блокирует все операции записи через Netcatty и применяется как к встроенным, так и к внешним агентам. Режим подтверждения носит рекомендательный характер для внешних агентов (они управляют собственным потоком одобрения инструментов).',
'ai.safety.permissionMode.observer':'Наблюдатель — только чтение, без действий',
'ai.safety.permissionMode.confirm':'Подтверждение — спрашивать перед действиями',
'ai.safety.permissionMode.auto':'Авто — выполнять свободно',
'ai.safety.commandTimeout':'Тайм-аут команды',
'ai.safety.commandTimeout.description':'Максимальное число секунд, которое команда может выполняться до принудительного завершения. Применяется как к встроенным, так и к внешним агентам.',
'ai.safety.commandTimeout.unit':'с',
'ai.safety.maxIterations':'Макс. число итераций',
'ai.safety.maxIterations.description':'Максимальное число циклов использования инструментов AI, чтобы предотвратить бесконтрольное выполнение. У внешних агентов могут быть собственные внутренние лимиты итераций, имеющие приоритет.',
'ai.safety.blocklist':'Чёрный список команд',
'ai.safety.blocklist.description':'Regex-шаблоны для блокировки опасных команд. Применяется как к встроенным, так и к внешним агентам через механизм выполнения Netcatty.',
'ai.safety.blocklist.reset':'Сбросить по умолчанию',
'ai.safety.blocklist.add':'Добавить шаблон',
'ai.safety.grants.title':'Память разрешений',
'ai.safety.grants.heading':'Правила для режима подтверждения',
'ai.safety.grants.description':'В режиме подтверждения действие сначала требует подтверждения. Сохранённые правила автоматически разрешают похожие действия во всех терминальных сессиях, их можно редактировать вручную.',
'ai.safety.grants.empty':'Правил пока нет. Выберите «Всегда разрешать» при одобрении или добавьте вручную.',
'ai.safety.grants.commandPattern':'Шаблон команды (необяз.)',
'ai.safety.grants.note':'Заметка (необяз.)',
'ai.safety.grants.add':'Добавить правило',
'ai.safety.grants.remove':'Удалить',
'ai.safety.grants.export':'Экспорт JSON',
'ai.safety.grants.import':'Импорт JSON',
'ai.safety.note':'Эти настройки безопасности применяются к действиям, выполняемым через Netcatty. Внешние CLI-агенты могут иметь собственные локальные инструменты и собственные правила управления ими.',
// Unified tooltips for terminal workspace and top tabs (issue #954)
'terminal.layer.addTerminal':'Добавить терминал',
'terminal.layer.switchToSplitView':'Переключить в режим разделения',
'placeholder.workspaceName':'Имя рабочего пространства',
'placeholder.sessionName':'Имя сессии',
'placeholder.searchHosts':'Поиск хостов...',
'toast.settingsUnavailable':'Окно настроек недоступно на этой платформе.',
'credentials.protectionUnavailable.title':'Защита учётных данных недоступна',
'credentials.protectionUnavailable.message':'Сохранённые пароли и ключи не могут быть автоматически расшифрованы на этом устройстве. Перед подключением введите учётные данные заново.',
'settings.system.tempDirectoryHint':'Временные файлы создаются при открытии удалённых файлов во внешних приложениях. Они автоматически очищаются при закрытии SFTP-сессий.',
'settings.system.credentials.unknown':'Неизвестно (не поддерживается в этой среде)',
'settings.system.credentials.unavailableHint':'Учётные данные, зашифрованные в другом профиле пользователя или на другой машине, здесь расшифровать нельзя. Повторно введите и сохраните их на этом устройстве.',
'settings.system.credentials.portabilityHint':'Облачная синхронизация переносима, потому что использует шифрование вашим мастер-ключом. Локальное шифрование safeStorage привязано к устройству и пользователю.',
'settings.system.networkProxy.url.desc':'Поддерживаются URL вида http://, https:// и socks5:// host:port. Учётные данные в URL прокси не поддерживаются.',
'settings.sshDebugLogs.hint':'Когда включено, новые SSH-подключения записывают диагностические события для разбора бастионов, аутентификации и неожиданных отключений.',
'settings.globalHotkey.enabledDesc':'Регистрировать системные сочетания клавиш. Когда отключено, все глобальные горячие клавиши снимаются с регистрации.',
'settings.globalHotkey.hint':'Глобальная горячая клавиша работает на уровне всей системы и позволяет быстро показывать или скрывать окно (терминал в стиле Quake).',
// Tray Panel
'tray.openMainWindow':'Открыть главное окно',
'tray.sessions':'Сессии',
'tray.portForwarding':'Проброс портов',
'tray.status.connected':'Подключено',
'tray.status.connecting':'Подключение',
'tray.status.disconnected':'Отключено',
'tray.status.active':'Активно',
'tray.status.inactive':'Неактивно',
'tray.status.error':'Ошибка',
'tray.recentHosts':'Недавние хосты',
'tray.empty.title':'Пока здесь ничего нет',
'tray.empty.subtitle':'Подключитесь к серверу, они по вам скучают 🚀',
'settings.application.whatsNew.subtitle':'Показать примечания к релизу',
'settings.application.openExternal.failedTitle':'Не удалось открыть ссылку',
'settings.application.openExternal.failedBody':'Не удалось открыть ссылку ни в системном браузере, ни во встроенном окне браузера.',
'settings.vault.title':'Хранилище',
'settings.vault.showRecentHosts':'Показывать недавно подключённые хосты',
'settings.vault.showRecentHostsDesc':'Показывать раздел недавно подключённых хостов в верхней части хранилища',
'settings.vault.selectBeforeConnect':'Сначала выбрать, потом подключать',
'settings.vault.selectBeforeConnectDesc':'Один клик выбирает хост (акцентная рамка в сетке), повторный клик подключает. Группы работают так же. По умолчанию — подключение одним кликом.',
'settings.vault.showOnlyUngroupedHostsInRoot':'Показывать в корне только хосты без группы',
'settings.vault.showOnlyUngroupedHostsInRootDesc':'Если включено, в корневом списке хостов будут показаны только хосты без группы. Откройте группу на боковой панели, чтобы увидеть сгруппированные хосты.',
'settings.vault.showSftpTabDesc':'Показывать отдельный SFTP-вид в верхней панели вкладок. Если скрыто, используйте боковую панель SFTP внутри сессии.',
'settings.vault.autoImportSystemKnownHostsDesc':'При открытии хранилища тихо импортировать записи из OpenSSH known_hosts. Отключите, чтобы держать известные хосты Netcatty отдельно; кнопка «Сканировать систему» по-прежнему импортирует вручную.',
// Update notifications
'update.available.title':'Доступно обновление',
'update.available.message':'Доступна новая версия {version}. Нажмите, чтобы скачать.',
'update.checking':'Проверка обновлений...',
'update.upToDate.title':'Актуальная версия',
'update.upToDate.message':'У вас установлена последняя версия ({version}).',
'update.error':'Не удалось проверить наличие обновлений',
'Добавьте пользовательский CSS, чтобы настроить внешний вид. Изменения применяются сразу.',
'settings.appearance.customCss.help.ariaLabel':'Справка по пользовательскому CSS',
'settings.appearance.customCss.help.title':'Руководство по пользовательскому CSS',
'settings.appearance.customCss.help.body':`Основные области интерфейса помечены атрибутом \`data-section\` для CSS-селекторов. Изменения применяются сразу после правки.
###Каквыбиратьэлементы
Используйтеселекторыпоатрибуту:
\`\`\`css
[data-section="terminal-side-panel"]{
border: 2pxsolid#00c851!important;
border-radius: 6px!important;
}
\`\`\`
Унекоторыхстрокипанелейтакжеестьхукисостояниявроде\`[data-selected="true"]\` и \`[data-focused="true"]\`.
'settings.appearance.windowOpacity.desc':'Настройте прозрачность всего окна приложения. При низких значениях текст терминала тоже бледнеет. В некоторых средах Linux это может не поддерживаться.',
'settings.appearance.appIcon.desc':'Выберите стиль значка для Dock/панели задач, чтобы Netcatty было проще найти при переключении приложений. Применяется во время работы; значок установленного пакета не меняется.',
'settings.terminal.font.cjk.refresh':'Обновить список шрифтов',
'settings.terminal.font.cjk.alignmentWarning':'Этот шрифт не проверен как моноширинный. Проверьте выравнивание в предварительном просмотре.',
'settings.terminal.font.cjk.unavailableWarning':'Этот шрифт не установлен на устройстве. Netcatty сохранит настройку и использует безопасный запасной шрифт.',
'settings.terminal.font.size':'Размер шрифта',
'settings.terminal.font.size.desc':'Размер текста терминала',
'settings.terminal.font.weight':'Толщина шрифта',
'settings.terminal.font.weight.desc':'Толщина обычного текста (100-900)',
'Показывать контекстное меню, даже когда tmux, vim или другие полноэкранные приложения перехватывают мышь. Когда выключено, правый щелчок передаётся приложению (Shift+правый щелчок всё равно открывает меню).',
'settings.terminal.behavior.autoCloseOnExit':'Автоматически закрывать терминал при выходе',
'settings.terminal.behavior.copyOnSelect':'Копировать при выделении',
'settings.terminal.behavior.copyOnSelect.desc':'Автоматически копировать выделенный текст. В tmux/vim с режимом мыши удерживайте Option на macOS или Shift на Windows/Linux для выделения',
'settings.terminal.behavior.normalizeTextOnCopy':'Нормализовать текст терминала при копировании',
'При копировании из терминала убирать отображающие хвостовые пробелы и объединять soft-wrap строки, чтобы вставка соответствовала логическому тексту. Отключите, чтобы копировать сырое выделение экрана (включая padding TUI и визуальные переносы).',
'settings.terminal.behavior.middleClickPaste':'Вставка средней кнопкой мыши',
'Символы, которые считаются границами слова при выделении двойным щелчком. Пробел тоже учитывается; добавьте =, запятую или : для строк вроде uid=name.',
'Оборачивать вставляемый текст escape-последовательностями, чтобы оболочка отличала вставку от обычного ввода. Отключите, если видите артефакты вида ^[[200~.',
'settings.terminal.behavior.autoUploadClipboardImage':'Авто-загрузка изображений буфера обмена при вставке',
'Когда в буфере обмена есть изображение, вставка в удалённой сессии загружает его по SFTP в каталог .netcatty-paste-images/ в текущем удалённом каталоге и вводит удалённый путь вместо вставки текста.',
'settings.terminal.behavior.shiftEnterNewline':'Текст для Shift+Enter',
'Выберите, когда вкладки сеансов должны следовать заголовкам окна оболочки и обновлять значки coding CLI. «Отключено» также останавливает распознавание значков.',
'settings.terminal.startupCommandDelay.label':'Задержка команды запуска (мс)',
'settings.terminal.startupCommandDelay.desc':'Сколько ждать после подключения перед отправкой команды запуска. Также используется между строками, если команда запуска многострочная. Увеличьте для медленных соединений.',
'settings.terminal.localShell.shell.desc':'Путь к исполняемому файлу оболочки (например, /bin/zsh, pwsh.exe). Оставьте пустым, чтобы использовать системную оболочку по умолчанию.',
'settings.terminal.localShell.shell.placeholder':'Системная по умолчанию',
'settings.terminal.localShell.shell.customArgs.desc':'Аргументы, передаваемые оболочке. Некоторым оболочкам они необходимы — например, msys2 bash требует --login -i для загрузки окружения.',
'settings.terminal.localShell.startDir.desc':'Каталог, в котором будет открываться локальный терминал. Оставьте пустым, чтобы использовать домашний каталог.',
'settings.terminal.connection.keepaliveInterval.desc':'Как часто (в секундах) отправлять keepalive-пакеты на уровне SSH. Установите 0, чтобы отключить глобально. Учтите, что отдельные хосты могут переопределять это значение в своих настройках.',
'settings.terminal.connection.keepaliveCountMax':'Макс. число пропущенных keepalive',
'settings.terminal.connection.keepaliveCountMax.desc':'Количество пропущенных keepalive, после которого соединение считается мёртвым. Более высокие значения лучше переносят краткие сетевые сбои и медленные ответы SSH-серверов.',
'settings.terminal.connection.x11Display.desc':'Необязательный адрес локального дисплея для перенаправления X11. Оставьте пустым, чтобы использовать системное значение по умолчанию.',
'settings.terminal.connection.x11Display.placeholder':'Авто (:0 или DISPLAY)',
'settings.terminal.hostInfoBar.show':'Показывать сведения о хосте',
'settings.terminal.hostInfoBar.show.desc':'Показывать адрес хоста и сведения о сервере над терминалом. Кнопки действий остаются доступными после скрытия.',
'settings.terminal.hostInfoBar.titleMode':'Заголовок сведений о хосте',
'settings.terminal.hostInfoBar.titleMode.desc':'Показывать в панели адрес подключения (user@host) или метку хоста из хранилища.',
'settings.terminal.rendering.renderer.desc':'Выберите технологию рендеринга терминала. В режиме "Авто" на устройствах с малым объёмом памяти будет использоваться DOM. Изменения применяются к новым терминальным сессиям.',
'settings.terminal.inlineImages.enabled.desc':'Показывать настоящие изображения, которые выводят удалённые программы (kitten icat, chafa, предпросмотр в файловых менеджерах и TUI). Изменения применяются к новым терминальным сессиям.',
'settings.terminal.inlineImages.iip.desc':'Принимать последовательности OSC 1337 из iTerm2 (PNG, JPEG, первый кадр GIF).',
'settings.terminal.inlineImages.storageLimit':'Кэш изображений на терминал',
'settings.terminal.inlineImages.storageLimit.desc':'Сколько памяти терминал может занимать декодированными изображениями до вытеснения самых старых (8–512 МБ).',
'settings.terminal.inlineImages.maxMegapixels':'Максимальный размер изображения',
'settings.terminal.inlineImages.maxMegapixels.desc':'Наибольшее изображение, которое будет декодировано; более крупные отбрасываются без изменения буфера терминала (1–64 Мпикс, 16 Мпикс — это 4096 × 4096).',
'settings.terminal.inlineImages.sequenceLimit':'Максимальный размер последовательности',
'settings.terminal.inlineImages.sequenceLimit.desc':'Наибольшая escape-последовательность изображения, принимаемая до начала декодирования (1–64 МБ).',
'settings.terminal.inlineImages.hibernateNote':'Вкладки, показавшие встроенное изображение, сохраняют свой рендерер и не уходят в спящий режим, потому что снимок восстанавливает только текст.',
'settings.shortcuts.disableTerminalFontZoom.desc':'Отключает быстрый масштаб текста в терминале, включая Cmd/Ctrl + колесо мыши.',
'settings.shortcuts.shellOnlyTabNumberShortcuts.label':'Сочетания без закреплённых вкладок',
'settings.shortcuts.shellOnlyTabNumberShortcuts.desc':'Если включено, Cmd/Ctrl+[1...9] и Ctrl+Tab переключают только рабочие вкладки (терминалы, рабочие области, редакторы), а не закреплённые Vault и SFTP.',
'sync.autoSync.inspectFailedMessage':'Не удалось подключиться к облаку для проверки изменений. Автосинхронизация повторит попытку при изменении данных или после перезапуска приложения.',
'sync.autoSync.syncedTitle':'Синхронизировано из облака',
'sync.autoSync.syncedMessage':'Ваши данные были обновлены из облака.',
'sync.autoSync.noProvider':'Облачный провайдер не подключён. Откройте Настройки → Синхронизация и облако, чтобы подключить его.',
'sync.autoSync.alreadySyncing':'Синхронизация уже выполняется.',
'sync.autoSync.restoreInProgress':'В другом окне уже выполняется восстановление хранилища. Подождите, пока оно завершится.',
'sync.autoSync.interruptedApplyTitle':'Синхронизация приостановлена — предыдущее восстановление прервано',
'sync.autoSync.interruptedApplyMessage':'Предыдущее восстановление завершилось некорректно, поэтому локальное хранилище может быть в несогласованном состоянии. Откройте Настройки → Синхронизация и облако → Восстановление и примените защитную резервную копию перед возобновлением автосинхронизации.',
'sync.autoSync.vaultLocked':'Хранилище заблокировано. Откройте Настройки → Синхронизация и облако, чтобы разблокировать его.',
'sync.autoSync.conflictDetected':'Обнаружен конфликт синхронизации. Откройте Настройки → Синхронизация и облако, чтобы разрешить его.',
'sync.autoSync.syncFailed':'Синхронизация не удалась',
'sync.autoSync.emptyVaultConflict.description':'Ваше локальное хранилище пусто, но в облаке есть данные. Обычно это происходит после обновления или сброса хранилища. Что вы хотите сделать?',
'sync.autoSync.emptyVaultManual':'Синхронизация невозможна: локальное хранилище пусто. Сначала восстановите его из локальной резервной копии или включите принудительную отправку в панели синхронизации.',
'sync.blocked.reason.bulkShrink':'Будет удалено {lost} из {baseCount} сущностей типа {entityType} из облака (сокращение на {percent}%).',
'sync.blocked.reason.largeShrink':'Будет удалено {lost} сущностей типа {entityType} из облака.',
'sync.blocked.detail':'Обычно это вызвано повреждённым локальным состоянием (сбой keychain, частичная загрузка данных). Восстановите данные из локальной резервной копии или выполните принудительную отправку, если вы действительно хотели удалить эти записи.',
'sync.blocked.restoreButton':'Восстановить из локальной резервной копии',
'sync.blocked.forcePushButton':'Всё равно отправить принудительно',
'sync.credentialsUnavailable':'Это устройство не может расшифровать некоторые сохранённые учётные данные. Перед синхронизацией повторно введите их локально.',
'vault.groups.deleteDialog.desc':'Группа будет безвозвратно удалена, а все хосты будут перемещены в корень.',
'vault.groups.deleteDialog.managedDesc':'Это управляемая группа SSH-конфига. При её удалении также будут удалены все хосты и снята связь с исходным файлом.',
'vault.groups.deleteDialog.deleteHosts':'Также удалить все хосты в этой группе',
'scripts.trigger.onConnect':'Запуск при подключении',
'scripts.trigger.onOutput':'Запуск при совпадении вывода',
'scripts.trigger.onOutputHint':'Срабатывает, когда вывод сервера совпадает с шаблоном (эхо нажатий клавиш игнорируется). Если целевые хосты не настроены, отслеживается текущая подключённая сессия; иначе — только указанные хосты. Не работает в приложениях альтернативного экрана, таких как vim или htop. Не запускается, пока в этой сессии выполняется другой скрипт; после его завершения проверка повторяется. Для ожидания внутри сценария используйте nct.screen.waitForText или nct.screen.waitForRegex в коде скрипта.',
'scripts.actions.save':'Сохранить',
'scripts.actions.runNow':'Запустить',
'scripts.actions.openEditor':'Открыть редактор',
'scripts.actions.openEditorHint':'Редактировать скрипт в большом окне',
'scripts.editor.modalTitle':'Редактор скриптов',
'scripts.editor.modalSubtitle':'Редактируйте метаданные и код скрипта в большом рабочем пространстве.',
'scripts.editor.lineCount':'{count} строк',
'scripts.editor.resize':'Изменить размер редактора',
'scripts.targets.hint':'Автоматические триггеры применяются к выбранным хостам или ко всем хостам, если включена эта настройка.',
'scripts.targets.connectOrderHint':'Порядок запуска скриптов подключения настраивается для каждого хоста в разделе «Сведения о хосте → Автоматизация».',
'scripts.actions.runNowHint':'Запускается для выбранных целей либо для всех доступных для подключения хостов, если включена эта настройка.',
'scripts.actions.runParallel':'Запустить на всех вкладках (параллельно)',
'scripts.actions.runSequential':'Запустить на всех вкладках (последовательно)',
'scripts.actions.runOnAllTabs':'Запустить на всех вкладках',
'scripts.actions.skippedConnectingSessions':'Пропущено вкладок, которые ещё подключаются: {count}',
'scripts.actions.skippedSensitiveSessions':'Пропущено вкладок с паролем/чувствительным вводом: {count}',
'scripts.actions.noRunnableHosts':'Нет доступных для подключения хостов, соответствующих целям этого скрипта',
'scripts.sidePanel.library':'Библиотека',
'scripts.sidePanel.running':'Выполняются',
'scripts.sidePanel.newScript':'Новый скрипт',
'scripts.running.empty':'В этой сессии не выполняются скрипты.',
'scripts.recording.startHint':'Записывайте действия в активном терминале и создавайте код скрипта nct',
'scripts.recording.unavailableHint':'Запись здесь недоступна — используйте панель скриптов справа',
'scripts.recording.activeHint':'Идёт запись этого терминала. Вводите команды как обычно; нажмите «Остановить» или кнопку REC на панели инструментов, чтобы завершить и сохранить запись.',
'scripts.recording.started':'Запись начата — работайте в терминале',
'scripts.recording.noSession':'Сначала подключите терминал (одну вкладку или рабочее пространство)',
'scripts.recording.alreadyActive':'В другом терминале уже идёт запись',
'scripts.recording.helpIntro':'Запись превращает ваши действия в терминале в повторно используемый сценарий автоматизации — удобно для развёртываний, проверок состояния и других повторяющихся задач.',
'scripts.recording.helpStep1':'Подключитесь к хосту. Подойдут как отдельная вкладка терминала, так и терминал в рабочем пространстве — откройте панель скриптов справа.',
'scripts.recording.helpStep2':'Нажмите «Начать запись». Красный значок REC на панели инструментов терминала означает, что запись активна.',
'scripts.recording.helpStep3':'Вводите команды в терминале как обычно. Каждое нажатие Enter сохраняется как отдельный шаг.',
'scripts.recording.helpStep4':'Закончив работу, нажмите «Остановить запись» или кнопку остановки рядом с REC на панели инструментов терминала.',
'scripts.recording.helpStep5':'Укажите название и сохраните скрипт. Выберите «Сохранить и редактировать», если хотите доработать созданный код в редакторе.',
'scripts.recording.helpTipsTitle':'Советы',
'scripts.recording.helpTip1':'Паузы между действиями дольше одной секунды записываются как время ожидания, чтобы воспроизведение не было слишком быстрым.',
'scripts.recording.helpTip2':'После каждой команды запись ждёт приглашения оболочки (например, $ или #), прежде чем перейти к следующему шагу.',
'scripts.recording.helpTip3':'Ввод пароля отмечается как конфиденциальный и не сохраняется в скрипте открытым текстом.',
'scripts.recording.helpTip4':'Для vim, меню и других интерактивных сценариев сначала запишите основные команды, а затем доработайте скрипт вручную.',
'hostDetails.automation.equivalenceHint':'Связь добавляет этот хост в список целей скрипта в разделе «Скрипты». Оба представления синхронизированы.',
'hostDetails.automation.queueHint':'Скрипты запускаются по порядку при подключении этого хоста. Сначала выполняются глобальные скрипты, затем эта очередь.',
// Network Device Mode auto-detection tip (session header)
'terminal.networkDevice.tip.message':'Похоже на сетевое устройство. Включите режим сетевого устройства, чтобы команды отправлялись как есть (без обёртки оболочки).',
'terminal.networkDevice.tip.action':'Включить',
'terminal.networkDevice.tip.dismiss':'Скрыть',
'terminal.networkDevice.tip.enabled':'Режим сетевого устройства включён для {host}',
// Connection logs
'logs.table.date':'Дата',
'logs.table.user':'Пользователь',
'logs.table.host':'Хост',
'logs.table.saved':'Сохранено',
'logs.empty.title':'Нет журналов подключений',
'logs.empty.desc':
'История ваших подключений будет отображаться здесь, когда вы подключаетесь к хостам или открываете локальные терминалы.',
'terminal.osc7Setup.desc':'Netcatty добавит хуки OSC 7 для текущего удалённого пользователя. Это помогает SFTP следовать за каталогом терминала после sudo или su.',
'terminal.osc7Setup.targets':'Возможные файлы для обновления',
'terminal.osc7Setup.command':'Команда для запуска',
'terminal.osc7Setup.run':'Запустить настройку',
'terminal.osc7Setup.running':'Настройка...',
'terminal.osc7Setup.configured':'Отслеживание каталога настроено',
'terminal.osc7Setup.failed':'Не удалось настроить отслеживание каталога',
'terminal.osc7Setup.sent':'Настройка отслеживания каталога отправлена в терминал',
'terminal.ymodem.selectFile':'Выберите файл для отправки',
'terminal.auth.noKeysHint':'Нет доступных ключей. Добавьте ключи в связке ключей.',
'terminal.auth.continueSave':'Продолжить и сохранить',
'terminal.auth.credentialsUnavailable':'Сохранённые учётные данные не могут быть расшифрованы на этом устройстве. Пожалуйста, введите и сохраните их заново.',
'terminal.auth.jumpCredentialsUnavailable':'У jump-хоста сохранены учётные данные, которые нельзя расшифровать на этом устройстве. Откройте настройки хоста и введите их заново.',
'terminal.auth.proxyCredentialsUnavailable':'Учётные данные прокси не могут быть расшифрованы на этом устройстве. Откройте настройки хоста и заново введите пароль прокси.',
'terminal.auth.keyUnavailableFallbackPassword':'Сохранённый SSH-ключ недоступен на этом устройстве. Выполняется переход на аутентификацию по паролю.',
'terminal.progress.timeoutIn':'Тайм-аут через {seconds}с',
'cloudSync.gate.title':'Синхронизация с end-to-end шифрованием',
'cloudSync.gate.desc':
'Ваши данные шифруются локально перед синхронизацией. Облачные провайдеры никогда не видят ваши данные в открытом виде. Задайте мастер-ключ, чтобы включить безопасную синхронизацию.',
'cloudSync.strategy.desc':'Выберите, что делать, когда изменились и локальные, и облачные данные.',
'cloudSync.strategy.smartMerge':'Умное объединение (рекомендуется)',
'cloudSync.strategy.smartMergeDesc':'По возможности объединять изменения с обеих сторон; если Netcatty не сможет безопасно выбрать, он попросит вас решить вручную.',
'cloudSync.localBackups.restoreMissing':'Резервная копия не найдена.',
'cloudSync.localBackups.protectiveBackupFailed':'Не удалось создать защитную резервную копию, поэтому восстановление было прервано для защиты ваших текущих данных. Устраните основную проблему (например, доступ к keychain) и попробуйте снова. Подробности: {message}',
'cloudSync.localBackups.restoreConfirmTitle':'Восстановить эту резервную копию?',
'cloudSync.localBackups.restoreConfirmDesc':'Ваши текущие хосты, ключи, сниппеты и настройки будут заменены содержимым этой резервной копии. Перед этим автоматически создаётся защитный снимок текущих данных.',
'cloudSync.localBackups.unavailableDesc':'Эта платформа не предоставляет Netcatty безопасное хранилище ключей, поэтому локальные резервные копии нельзя записывать безопасно. Установите Netcatty в систему с поддерживаемым keychain, чтобы включить историю локальных резервных копий.',
'cloudSync.localBackups.lockedDesc':'Настройте или разблокируйте мастер-ключ перед восстановлением резервной копии, чтобы восстановленные учётные данные оставались зашифрованными.',
'cloudSync.revisionHistory.viewButton':'История',
'cloudSync.revisionHistory.title':'История версий хранилища',
'cloudSync.revisionHistory.description':'Просматривайте и восстанавливайте предыдущие версии вашего хранилища из истории ревизий Gist.',
'cloudSync.revisionHistory.empty':'Ревизии не найдены.',
'cloudSync.clearLocal.desc':'Сбросить локальную версию и историю синхронизации. При следующей синхронизации данные будут скачаны из облака.',
'cloudSync.clearLocal.button':'Очистить',
'cloudSync.clearLocal.dialog.title':'Очистить локальные данные хранилища?',
'cloudSync.clearLocal.dialog.desc':'Локальная версия будет сброшена до 0, а история синхронизации очищена. При следующей синхронизации данные будут скачаны из облака и заменят локальные.',
'snippets.search.noResults.desc':'Ни один сниппет или пакет не соответствует запросу "{query}". Попробуйте другой поисковый запрос или очистите поиск для просмотра.',
'snippets.import.modal.desc':'Выберите один или несколько JSON-файлов сниппетов Netcatty. Netcatty сначала покажет результат разбора и ничего не импортирует без подтверждения.',
'snippets.import.modal.noFile':'Файл ещё не выбран. Используйте пример ниже или обычный JSON-массив сниппетов, затем выберите один или несколько файлов.',
'vault.import.progress.persistFailed':'Не удалось сохранить импортированные хосты. Освободите место в локальном хранилище и повторите попытку.',
'vault.import.progress.rollbackFailed':'Не удалось безопасно отменить импорт. Некоторые хосты могли остаться в хранилище; проверьте его перед повторной попыткой.',
'vault.import.sshConfig.managedDesc':'Поддерживать синхронизацию. Изменения будут сохраняться обратно в файл.',
'vault.import.sshConfig.managedDestinationHint':'Управляемая синхронизация всегда использует отдельную группу; выбранная выше группа применяется только к разовому импорту.',
'vault.import.sshConfig.managedSuccess':'Импортировано {count} хостов. Файл теперь находится под управлением.',
'vault.import.sshConfig.alreadyManaged':'Этот файл уже находится под управлением.',
'vault.import.sshConfig.alreadyManagedDesc':'Этот файл уже управляется в группе "{group}". Если хотите импортировать его заново, сначала удалите существующий управляемый источник.',
'vault.import.sshConfig.noFilePath':'Невозможно управлять этим файлом.',
'vault.import.sshConfig.noFilePathDesc':'Не удалось определить путь к файлу. Для управляемой синхронизации нужен доступ к файловой системе.',
// Known Hosts
'knownHosts.search.placeholder':'Поиск известных хостов...',
'sftp.transfers.pathConflict.paused':'Другая передача «{name}» приостановлена. Сначала возобновите или отмените её.',
'sftp.transfers.pathConflict.inProgress':'Другая передача «{name}» уже выполняется.',
'sftp.transferCenter.title':'Передача файлов',
'sftp.transferCenter.quitConfirm':'Незавершённых передач: {count}. Прогресс будет сохранён, а после следующего запуска передачу нужно продолжить вручную. Выйти?',
'settings.sftp.transferConcurrency.desc':'Максимальное число файлов, одновременно передаваемых для одной цели, включая содержимое папок и отдельно выбранные файлы. Большие значения могут ускорить передачу, но перегрузить некоторые серверы.',
'settings.sftp.transferPoolIdleTtl.desc':'Как долго простаивающие SFTP-соединения пула передачи остаются открытыми. Большее время снижает задержку перед следующей передачей. Активные передачи не закрываются.',
'settings.ssh.transportIdleTtl.desc':'Как долго общее SSH-соединение остаётся открытым после освобождения последнего lease терминала, SFTP-браузера, пакетной передачи или проброса портов. Пока оно тёплое, повторное подключение к тому же хосту может обойти повторную аутентификацию (включая MFA). Активные сессии и передачи не закрываются.',
'settings.ssh.transportIdleTtl.1m':'1 минута',
'settings.ssh.transportIdleTtl.5m':'5 минут',
'settings.ssh.transportIdleTtl.15m':'15 минут',
'settings.ssh.transportIdleTtl.30m':'30 минут',
'settings.ssh.transportIdleTtl.never':'До выхода из приложения',
'settings.sftp.defaultOpener':'Приложение для открытия по умолчанию',
'settings.sftp.defaultOpener.desc':'Выберите приложение по умолчанию для открытия файлов без конкретной ассоциации',
'settings.explorerContextMenu.enable':'Показывать «Open in Netcatty» в контекстном меню папок',
'settings.explorerContextMenu.enableDesc':'Добавляет пункт в меню папок и фона папки, чтобы открыть локальный терминал в этом каталоге. Применяется сразу, переустановка не нужна.',
'settings.jmsDeepLink.enableDesc':'Зарегистрировать Netcatty как обработчик клиентских ссылок JumpServer jms://. Может конфликтовать с официальным клиентом JumpServer.',
// Settings > SFTP Auto Sync
'settings.sftp.autoSync':'Автосинхронизация с удалённым сервером',
'settings.sftp.autoSync.desc':'Автоматически синхронизировать изменения файлов обратно на удалённый сервер при открытии файлов во внешних приложениях',
'settings.sftp.autoOpenSidebar.enableDesc':'Боковая панель SFTP будет автоматически открываться при подключении терминальной сессии к удалённому хосту',
'settings.sftp.followTerminalCwd':'Следовать за каталогом терминала',
'settings.sftp.followTerminalCwd.desc':'Автоматически синхронизировать боковую панель SFTP с рабочим каталогом терминала (переключатель на панели инструментов)',
'settings.sftp.followTerminalCwd.enable':'Включать следование по умолчанию',
'settings.sftp.followTerminalCwd.enableDesc':'При открытой боковой панели SFTP режим следования включён по умолчанию и обновляется после команд cd в терминале',
'settings.sftp.defaultViewMode':'Режим просмотра по умолчанию',
'settings.sftp.defaultViewMode.desc':'Выберите режим просмотра по умолчанию при открытии новой вкладки SFTP. Настройки конкретного хоста имеют приоритет.',
'settings.sftp.defaultViewMode.list':'Список',
'settings.sftp.defaultViewMode.listDesc':'Показывать файлы в виде плоского списка для текущего каталога',
'settings.sftp.defaultViewMode.tree':'Дерево',
'settings.sftp.defaultViewMode.treeDesc':'Показывать файлы в иерархической древовидной структуре',
'sftp.autoSync.success':'Файл синхронизирован с удалённым сервером: {fileName}',
'sftp.autoSync.error':'Не удалось синхронизировать файл: {error}',
// SFTP Folder Upload Progress
'sftp.upload.progress':'Загрузка файлов {current} из {total}...',
'settings.sftp.showHiddenFiles.desc':'Показывать скрытые файлы (dotfiles в Unix/macOS и файлы с атрибутом hidden в Windows) в файловом браузере SFTP.',
'settings.sftp.compressedUpload.enableDesc':'Автоматически сжимать папки с помощью tar перед передачей. Требует поддержки tar на сервере. Если она недоступна, будет использована обычная передача.',
// Quick Switcher
'qs.search.placeholder':'Поиск хостов или вкладок',
'hostDetails.sftp.fileProtocol.desc':'Авто сначала пробует SFTP, затем SCP-режим, если подсистема SFTP недоступна. Принудительный SCP — для хостов только с SCP.',
'hostDetails.agentForwarding.agentNotRunningHint':'SSH-агент не обнаружен. Включите OpenSSH Authentication Agent в службах Windows или используйте совместимый агент, например Bitwarden, 1Password или gpg-agent.',
'hostDetails.deviceType.desc':'Включайте для сетевого оборудования (коммутаторов, маршрутизаторов, межсетевых экранов), подключённого по SSH. Команды отправляются как есть, без обёртки оболочки, что совместимо с CLI вендоров вроде Huawei VRP и Cisco IOS.',
'hostDetails.deviceType.warning':'Команды AI-агента будут отправляться напрямую без отслеживания кода выхода. Включайте только для устройств, на которых нет стандартной оболочки.',
'hostDetails.legacyAlgorithms.desc':'Включить устаревшие SSH-алгоритмы (diffie-hellman-group1, ssh-dss, 3des-cbc и т. д.) для подключения к старому сетевому оборудованию.',
'hostDetails.legacyAlgorithms.warning':'У этих алгоритмов есть известные слабые места безопасности. Включайте только для устаревших устройств, которые не поддерживают современную криптографию.',
'hostDetails.skipEcdsaHostKey.desc':'Некоторые старые коммутаторы Huawei / Cisco выдают нестандартные подписи ECDSA host-key, из-за чего соединение падает с "signature verification failed". Включение этой опции убирает все ecdsa-sha2-* из предложения клиента, и согласование переходит к RSA / Ed25519.',
'hostDetails.algorithms.advanced.desc':'Заменить предлагаемый список алгоритмов для любой категории для конкретного хоста. Не трогать категорию = использовать значение по умолчанию; выбранное подмножество полностью заменяет список по умолчанию. Неверные значения могут сделать хост недоступным.',
'hostDetails.algorithms.inheritedNotice':'В текущей группе заданы переопределения алгоритмов для: {categories}. Кнопка «Сбросить» здесь возвращает к спискам группы, а не к значениям NetCatty по умолчанию. Чтобы игнорировать ограничение группы, очистите переопределение в настройках алгоритмов группы.',
'hostDetails.keepalive.desc':'Использовать для этого хоста собственную политику keepalive вместо глобальной настройки. Полезно для старых маршрутизаторов и коммутаторов, чей SSH-сервер не отвечает на запросы keepalive@openssh.com. Установите интервал 0, чтобы полностью отключить keepalive для этого хоста.',
'hostDetails.keepalive.countMax':'Макс. число пропущенных keepalive',
'hostDetails.keepalive.disabledHint':'Интервал = 0 отключает keepalive для этого хоста. Для определения разорванного соединения сессия будет полагаться на TCP-таймауты.',
Loaded 100 of 2686 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.