Reconcile the app-lock feature with main's refactored renderer
architecture (publishers/hosts/AppShell) and main-process changes:
- index.tsx/App.tsx: AppLockGate keeps ownership of useSettingsState and
useAppLockState; new AppLockRuntimePublisher publishes the runtime via
appRuntimeBridge; SettingsPublisher now binds the gate-owned settings.
- AppSideEffects: defer deep links, tray port-forward toggles, global
hotkeys and window close commands while locked; flush on unlock.
- main.cjs: keep fresh-session re-lock before main's reusable-window
fast path; before-quit guard delegates to handleBeforeQuit with main's
async plugin-shutdown commitQuit and pending-update cancellation.
- globalShortcutBridge: keep locked-state tray redaction on top of
main's deferred tray panel show logic.
- preload: adopt main's lastPayload replay for terminal popup config
(superset of the gate-delay replay fix).
- vite config: keep dev CSP plugin, adopt main's warn-only stale
optimize-dep handling; electron-builder: keep Windows Hello helper
extraResource with main's CLI-controlled arch targets.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(#2826): automated Cursor CLI fix
* fix(scripts): confirm bulk delete and clear host bindings
Scripts side-panel mass-delete skipped confirmation and, when
onSnippetsChange was wired, filtered snippets without clearing host
login/connect script bindings. Confirm first, always dispatch the shared
delete event, and apply deletes via deleteSelectedSnippetsFromVault.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(scripts): harden delete handler against stale vault state
Read snippets/hosts from refs in the shared delete listener so rapid
confirms cannot resurrect already-removed items, and clear pending bulk
delete when the scripts panel hides so the confirm dialog does not
resurface after a mid-prompt hide.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(scripts): keep delete refs current within the same tick
Update snippetsRef/hostsRef immediately after applying a delete so a
same-tick second event cannot rebuild vault state from the pre-delete
snapshot and resurrect removed snippets or host bindings.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: address Codex review on PR #2828
* fix(scripts): hoist bulk-delete confirm outside toolbar popover
Portalled Dialog focus dismissed the compact Scripts popover and cleared
pendingDeleteIds before confirm. Own the confirm in TerminalToolbar and
defer via onBulkDeleteRequest when nested in that popover.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: serialize snippet saves with bulk-delete vault lock
updateSnippets wrote STORAGE_KEY_SNIPPETS without withVaultImportLock, so a
concurrent edit could land between bulk-delete's snapshot read and journal
commit and be discarded. Queue ordinary snippet writes on the shared lock and
include them in waitForPendingVaultWrites.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: rebase queued snippet saves onto latest vault snapshot
Locking updateSnippets alone still wrote a pre-delete full array, so a
popup bulk-delete could be resurrected (or a storage-event version bump
could cancel the local edit). Rebase under the vault lock and supersede
only newer local writers via snippetsWriteOwnerRef.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
* fix: address Codex review on PR #2828
---------
Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* Cursor: Apply local changes for cloud agent
* fix(terminal): stop first SSH fail under StrictMode remount
Defer backend boot past the double-invoke, cancel superseded vault init,
gate start on vault hydration, and harden hotkey/attach cleanup so the
first connect no longer races a stale close or empty keychain.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* perf: finish StrictMode mega-subscriber plan (accent isolation + docs)
Stabilize published terminal themes across custom-accent drag, apply accent
at chrome/log leaves and CSS injection only, land Phase 0 design/plan docs,
and lock Host-bag / Bugbot-clean contracts for the isolation checklist.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* docs: add StrictMode mega-subscriber design and plan artifacts
Force-add Phase 0 spec/plan under docs/superpowers (gitignored locally)
so the delivery branch carries the approved design and plan copy.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: clear remaining StrictMode doubles and notes editor jank
Gate every AI send and reject concurrent runTurn, latch tray connect flush
and settings persistMounted remounts, and stop notes content drafts from
rebuilding MDXEditor/shell on each keystroke.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* perf: gate hidden notes panel subscribe and skip host-link annotate on keystrokes
Hidden terminal notes mounts keep a live snapshot read but stop listening to
publishes. Host-link DOM annotation follows hosts/mode (preview MutationObserver)
instead of every markdown value change.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: latch SSH transport idle TTL notify under StrictMode
Avoid duplicate main-process notifySettingsChanged on effect double-invoke
when the idle TTL value has not changed.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* perf: freeze inactive vault notes against hosts churn and prefetch MDXEditor
Gate the retained VaultNotesSection notesStore subscription while hidden,
ignore hosts identity in its memo compare when inactive, and warm the lazy
InlineMarkdownEditor chunk when notes become active.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: clear remaining AI StrictMode double-fire hazards
Latch Ask-AI terminal selection on requestId before draft mutation, hoist
Codex App Server IPC bridge to App (ref-counted), and make Catty
requestApproval join an existing waiter for the same toolCallId.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: remount-safe AI send latch and skip stale MCP approval IPC
Replace the component sendInFlightRef with a module key latch so StrictMode
panel remount cannot reopen a mid-flight send, and no-op resolveApproval when
the pending entry is already drained.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* test: allow gated useNotesStore in VaultNotesSection memo contract
The retained notes section now passes enabled:isActive so hidden mounts do
not subscribe; update the VaultView memo source assertion accordingly.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: keep note drafts across mode toggle and external note publishes
Flush ref-only content before edit/preview remount, and skip prop→editor
setMarkdown while a local draft diverges from the last synced value.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* chore: strip StrictMode sermon comments after Bugbot/Comment Sicko pass
Keep effect-order and public-export contracts; drop remount/perf narration
that the latches and tests already encode.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: remount-safe Ask-AI terminal selection latch
Move consumed requestIds to a module Set so StrictMode remount cannot
append the same selection chip twice before parent state clears.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* chore: drop remount latch comment
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Split vault/session/settings into publishers and store-backed Hosts so hot
fields no longer rebuild the whole shell, and harden Terminal boot / startup
effects for StrictMode double-mounts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor: migrate architecture-debt hooks into application/state
Move SFTP Wave A stores/hooks and related vault/AI/terminal/systemManager
hooks out of components into application/state, update importers, and keep
thin deprecated re-exports at the old paths.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* refactor(sftp): align local bookmarks store API with global bookmarks
Export subscribe/get/set/rehydrate helpers from localSftpBookmarks to match
the globalSftpBookmarks external-store pattern.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(electron): reduce baseline memory via spellcheck/GPU/prewarm guards
Disable spellcheck on BrowserWindows and the default session, drop spare
renderers, gate aggressive GPU switches behind NETCATTY_COMPAT_GPU, and make
settings prewarm opt-in via NETCATTY_PREWARM_SETTINGS.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* Isolate notes and customAccent from TerminalLayer hot props
Route vault notes through notesStore (shellHistoryStore pattern) and
app accent through appearanceChromeStore so note edits and accent-drag
ticks no longer rebuild TerminalLayer. Notes/AI slots and Terminal leaves
subscribe locally; VaultView and open-note callbacks stay unchanged.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ui): cut unnecessary AppView/VaultView/settings re-renders
Stabilize TopTabs close handler, memo AppHostTreeLayer, exclude accent from
settingsVersion, ignore connectionLogs off the logs section, share lastConnected
host refs, and keep rename dialog drafts local so chrome domain stays stable.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* chore: package size, dead code, and consolidation cleanup
Remove unused direct deps, exclude Vite-bundled renderer packages from
electron-builder, delete high-confidence dead code/no-op transfer warm
wiring, consolidate cloud provider IDs and KnownHost/SFTP helpers, and
land UI/AGENTS quick wins (rAF text batching, notes draft debounce,
streaming plain text, tempDirBridge, diagnostics adapter, External MCP
setters).
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: stabilize rAF text batching and diagnostics tests
Polyfill requestAnimationFrame for Node, flush pending text before
steer buffering, bypass rAF while steer is in flight, and stub
global localStorage for AI panel diagnostics adapter tests.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: correct import paths after application/state hook migrations
Migrated vault/AI/system hooks still used components/-relative imports;
point them at repo-root domain/components/types/lib instead.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: address Bugbot/Codex regressions from perf cleanup
- Keep blank Enter from closing RenameDraftDialog; only reset rename state after a non-empty submit
- Flush note drafts before vault mutations so pending title/body edits are not overwritten
- Re-apply appearanceChromeStore accent on appearanceTheme in Terminal, and keep layer themes unaccented
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(notes): flush draft before markdown export paths
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: keep editor-close hotkey ref fresh during render
Assign handleRequestCloseEditorTabRef in render instead of useEffect so
Cmd/Ctrl+W never hits the App stub before the effect runs.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: keep accent in chrome themes without TerminalLayer thrash
Restore accent in globalAppearance for chrome/injection consumers.
Keep resolveFocusedAppearance identity stable across accent drag via
accent ref, while Terminal leaf still re-applies appearanceChromeStore.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: keep compose chrome accent live and merge note renames
Subscribe theme-panel focusedAppearance to appearanceChromeStore so
compose-bar chrome updates during color-picker drag even when
resolveFocusedAppearance identity stays stable. Tree renames now merge
title into the post-flush note instead of replacing from a stale row.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: clear terminal accent when switching back to theme mode
Re-resolve appearanceTheme by catalog id before applyCustomAccent so a
stale baked custom cursor/selection cannot stick after accentMode returns
to theme while TerminalLayer memo ignores accent props.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: register unsaved prompt singleton during render
AppView close handlers call promptUnsavedChanges outside the old
render-prop. Assign the singleton during render so Cmd/Ctrl+W and tab
close never hit the pre-effect null window that auto-cancels.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: keep vault import hooks UI-free and flush notes on teardown
Move ImportOptions into application/state and inject a notifier so
useVaultImportHandlers no longer imports components. Notes drafts also
flush on pagehide/beforeunload when Electron tears down without React
unmount.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: move AI terminal session context builders into domain
buildAITerminalSessionInfo and AIPanelContext now live under domain/ so
application hooks no longer import TerminalLayerSupport and create a
UI → application → UI cycle.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: drop duplicate AIPanelContext type from TerminalLayerSupport
The type is re-exported from domain; the local definition conflicted.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: keep migrated state hooks free of UI-layer imports
Move aiChatStreamingSupport into infrastructure/ai and poll list helpers
into domain/systemManager so useAIChatStreaming / useSystemManager no
longer import from components. Leave thin re-exports at the old paths.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: move remaining migrated-hook deps out of components
Host-key verification types/helpers now live in domain/, and SFTP column
layout types live in application/state/sftp/columnLayout. Application
hooks no longer import those definitions from the UI layer.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: clear lint blockers and flush notes when panels hide
Move AI panel diagnostics persistence into application/state so
components stop importing localStorageAdapter. Flush note drafts when
retained NotesManager mounts become inactive, and silence related lint
warnings in theme/host-key helpers.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(notes): publish notesStore sync on vault note updates
updateNotes/updateNoteGroups now publishNotesSnapshot immediately so
AI/notes consumers do not briefly read a stale catalog after a hide
flush. Flush inactive drafts in useLayoutEffect before paint.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* feat(terminal): add disconnect/reconnect controls on session status bar
Workspace panes could not reach tab-level reconnect. Expose disconnect
(without closing the pane) and reconnect beside the host info actions so
split sessions stay operable. Closes#2769.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): abort in-flight boot on status-bar disconnect
Disconnect keeps the pane mounted, so cancel's unmount cleanup never runs.
Flip isBootActiveRef (and isCancelling) so a late startSSH/startMosh attach
cannot resurrect the session after the user disconnects mid-connect.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): ignore stale start UI after status-bar disconnect
When disconnect aborts an in-flight connect, starter catch paths must not
reopen auth or repopulate errors. Gate SSH/telnet/mosh/ET/local/serial
failure UI on isTerminalBootActive and harden the boot effect awaits.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): hide disconnect/reconnect on compact popup terminals
Owned command popups use compactToolbar with a one-shot startup command.
Exposing reconnect there can re-run that command; keep the controls on
normal tabs/workspace panes only.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): use boot epoch so reconnect cannot revive aborted starts
Disconnect alone flipping isBootActiveRef is not enough: an immediate
reconnect re-arms boot, and a late startSSH from the aborted attempt can
still attach. Capture a boot epoch per attempt and ignore stale attaches/UI.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): do not let stale attach abort a newer reconnect
tryAttach failure for an expired boot epoch must close the orphan session
only. Calling abortSessionStartAfterUnmount() would force disconnected on
a reconnect that already re-armed boot.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): reject late host-key prompts after disconnect
Disconnect can land before pendingHostKeyRequestId is set. Auto-deny
onHostKeyVerification when boot is inactive or status is disconnected
so approval UI cannot reopen on an aborted pane.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): cancel wake and MFA when status-bar disconnects
Invalidate hibernation reconnect-wake tokens on disconnect so a late wake
cannot re-arm boot. Reject/cancel keyboard-interactive prompts for
disconnected panes and clear any already-queued MFA for that session.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): ignore stale SSH catch UI and password fallback
Check boot attempt currency before resetting reconnect UI state on
SSH start failure, and before launching key→password fallback after
disconnect so a replacement reconnect is not interrupted.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): guard stale SSH progress and host-key prompts
Ignore chain-progress callbacks from superseded boot attempts, and
thread bootEpoch through SSH host-key verification so a late prompt
from an aborted start cannot overwrite a replacement reconnect.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): correlate MFA and jump host-keys with boot epoch
Forward bootEpoch through jump-host verification and keyboard-interactive
prompts, and reject superseded MFA when the retained tab has already
started a replacement reconnect.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): keep replacement SSH session alive across stale starts
Skip closeSession for superseded starts while a newer boot still owns
the shared sessionId, and avoid clearing MFA wait state before the
attempt-freshness guard on SSH success.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): own shared sessionIds by boot epoch
Claim SSH/local registry slots with bootEpoch so a stale start cannot
overwrite a replacement, and make closeSession no-op when the payload
epoch does not match the live session owner.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): claim bootEpoch for all reconnectable backends
Pass bootEpoch through Telnet/Mosh/ET/serial starts, claim registry slots
the same way as SSH/local, and dispose displaced owners when a newer
boot replaces the shared sessionId.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): protect preload listeners and ET artifacts on displace
Defer epoch-scoped closeSession listener teardown until main confirms
ownership, and remove ET external-auth temp artifacts when a newer boot
displaces the previous registry owner.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): clean rejected Mosh starts and keep replacement listeners
Kill superseded Mosh/ET PTYs and auth temp files when claimSessionSlot
rejects, and never clear shared preload listeners on epoch-scoped closes.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): settle superseded boots and stop displaced log streams
Reject Telnet/SSH claim failures through Promise reject/failReuse instead of
uncaught EventEmitter/.then throws, and stop a displaced session's log stream
so a logging-disabled reconnect cannot append into the old file.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): keep woken runtime when disconnect cancels hibernate wake
Disconnect invalidates in-flight hibernated reconnects with keep mode so a
successful wake that already cleared hibernatedRef is not disposed into a
dead pane that can never reconnect. Unmount still disposes orphan runtimes.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): bind reused SSH shellPid only to the claimed session
Late PID discovery after Copy Tab reuse must not write onto a newer
bootEpoch that already replaced the shared sessionId registry slot.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): await plugin cancel cleanup and clear boot epochs on teardown
Track plugin start/cancel promises so Disconnect→Reconnect waits until the
external session finishes unregistering, and stop unmount teardown from
republishing boot epochs into the process-wide map.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): cancel SSH passphrase prompts on disconnect
Register a pending-boot AbortSignal for SSH starts, abort it (and session-
scoped passphrase requests) from closeSession even before a registry slot
exists, and filter/drop late passphrase modals by boot epoch / disconnect.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): close owned boot epoch and scope passphrase cancels
Capture the pre-bump epoch for disconnect/teardown closeSession so live
transports are not skipped by epoch mismatch, cancel only passphrase prompts
at or below that epoch, and revalidate boot currency after async passphrase
lookups before queuing a modal.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): tag Telnet auto-login and Mosh ready with bootEpoch
Emit bootEpoch on telnet auto-login and mosh:ready events so overlapping
Disconnect→Reconnect boots ignore cross-delivered callbacks meant for an
older shared sessionId.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): update disconnect assertion and cancel pending serial opens
Align the disconnect source regression with invalidateBootEpochForClose, and
abort in-flight serialPort.open attempts via pending boot AbortSignal so
Disconnect→Reconnect cannot leave a stale open racing the replacement.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): keep flow-pause leases on skipped epoch closes
Worker close:await previously cleared pause ownership in finally even when
the worker returned skipped for an epoch mismatch, dropping the live
replacement's lease. Only clear on non-skipped closes; epoch-scoped
fire-and-forget close similarly defers to onSessionClosed.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(sftp): keep cancel shared write OPEN drain gated on callback
Cancel settle must not force-complete sharedWriteOpenDrain — only channel
error does — so stage cleanup cannot race a late truncating OPEN after #2720.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): bound cancel OPEN drain and unlink late truncates
Restore cancel drain force-complete so stalled shared OPENs cannot hang
the lease, and best-effort unlink late truncating "w" opens after close so
force-complete cannot leave a recreated stage orphan.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): only unlink generated stages on cancel OPEN
Gate cancel-time truncating OPEN unlink on generatedStagePath so in-place
and symlink destinations keep their final path; cover with a late-OPEN test.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): unlink late staged OPEN after channel-error settle
Late truncating generated-stage OPENs after settle must unlink even when
transfer.cancelled is false, so channel-error force-complete cannot leave
a recreated .netcatty-upload orphan.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): skip late OPEN unlink when same-id retry owns stage
After shared write OPEN drain force-complete, a late truncating "w"
callback still closed and unlinked generated stages. Resumable retries
reuse `.netcatty-<transferId>.part`, so a stale callback could delete
the new attempt's stage. Gate unlink on activeTransfers ownership
(generated stages + truncating only), matching the in-place final
target guard.
* fix(sftp): only skip late OPEN unlink for reusable resume stages
Same-id retry ownership must not suppress unlink for every generated
stage path. Non-resumable uploads use unique .netcatty-upload-* paths;
a stale late OPEN on those must still clean orphans. Only deterministic
.netcatty-<transferId>.part resume stages are protected.
* fix(sftp): gate late OPEN unlink skip on actual staged path
Only suppress late generated-stage unlink when the same-id retry's
active.stagedRemote.path matches. Do not treat resumable+targetPath or
path shape alone as ownership for in-place retries.
* fix(sftp): compare late OPEN stage paths in the same representation
OPEN filePath may be a session-encoded Buffer while stagedRemote.path is the
logical string, so strict equality failed and could unlink a same-id retry
stage. Normalize via encodePathForSession / Buffer equals before ownership skip.
* fix(sftp): gate truncating shared OPEN against same-path retries
Serialize truncating shared-write OPENs per remote path so a same-id
retry cannot issue OPEN "w" while a prior attempt is still unsettled.
When a drain force-complete already released the gate and a late stale
OPEN re-truncates a retry-owned resume stage, invalidate that retry
instead of allowing a sparse promote. Keep late-OPEN unlink skip for
retry-owned stages.
* fix(sftp): bound best-effort unlink so cancel cannot hang
Shared/sudo cancel after OPEN "w" waited on unlink before finishCancel.
A dead channel that never invokes the unlink callback left the transfer
and its session lease active indefinitely. Cap unlink at the same 2s
grace used for shared-write drain force-complete.
* fix(sftp): make truncating shared OPEN path-gate waits cancelable
A same-path upload queued behind a prior truncating OPEN could keep its
lease/admission slot forever if cancelled before runOpen. Race the gate
wait with transfer.abort and release the gate + drain on cancel.
* fix(sftp): scope truncating OPEN path gates per SFTP session
A module-global path-only key serialized unrelated hosts that shared the
same remote pathname. Include transfer/sftp session identity in the gate
key so only same-endpoint opens wait on each other.
* fix(sftp): refuse stage promote after stale OPEN truncation
A late force-completed OPEN w from a prior same-id attempt can zero the
deterministic stage after concurrent write finishes. Latch
staleOpenTruncatedStage through assertCanPromote and uploadFileConcurrent
so promotion cannot rename a corrupt stage.
* fix(sftp): keep path gate chained when canceling a queued OPEN wait
Releasing a cancelled waiter's gate immediately cleared the barrier while
the prior truncating OPEN was still in flight. Chain the waiter's release
to waitForPrior so later same-path attempts stay serialized.
* fix(sftp): hold reusable-stage path gate past OPEN force-complete
Force-completing drain no longer frees the path gate for generated stages
immediately. Same-id retries stay serialized until the late OPEN settles
(or a 10s hard cap) so promotion cannot race a still-processing OPEN w.
* test(sftp): wait for generated-stage path gate hold on retry OPEN
After force-complete, generated stages keep the path gate for 10s more.
Align the late-OPEN retry test wait with that hold.
* fix(sftp): hold truncating OPEN path gates for in-place targets too
In-place shared OPEN force-complete no longer frees the path gate
immediately, and late OPEN invalidation applies to final-path retries so
a stale truncating OPEN cannot wipe an in-flight same-id rewrite.
* fix(sftp): keep path gate held until OPEN settles (no hard cap)
Remove the post-force-complete gate timeout so a different transferId
cannot reopen the same target while a stale truncating OPEN is pending.
* test(sftp): accept normal {transferId,totalBytes} retry success
startTransferNow returns transferId/totalBytes on success, not success:true.
* fix(sftp): reject path-gate waits when the SFTP channel errors
Queued same-path OPEN waiters now observe channel error while waiting on
the prior gate, so a dead session cannot pin lease/admission forever.
* fix(sftp): treat superseded OPEN as cancelled, not failed
Late OPEN rejections after a same-id retry must not broadcast a terminal
failed event that marks the live retry task failed in transfer center.
* fix(sftp): emit cancelled channel for superseded OPEN failures
Worker fan-out only maps transfer:error as cancelled for cancel messages.
Superseded attempts now use transfer:cancelled so retries are not failed.
* fix(sftp): silence superseded OPEN terminal events for live retries
When a same-id retry already owns activeTransfers, do not emit cancelled
or failed for the stale superseded attempt or the transfer center sticks.
* fix(sftp): release path gate on OPEN force-complete for dead channels
When the OPEN callback never arrives, free same-path waiters after drain
force-complete so later uploads do not hang forever on the gate.
* fix(sftp): hold path gate until OPEN settles; 30s dead-channel grace
Force-complete no longer frees the gate immediately. Late OPEN release
keeps retries serialized; a 30s grace frees dead channels.
* fix(sftp): never force-release path gate on OPEN drain force-complete
A dead-channel force-complete must free the transfer/lease, but keep the
path gate held until the OPEN callback actually settles. Force-releasing
lets a same-path retry write then get wiped by a late truncating OPEN.
* fix(sftp): skip upload fallback when OPEN is superseded by a retry
A same-id retry can already own the transfer while the old OPEN callback
still rejects. Mark that rejection noTransferFallback so we never truncate
the shared stage via prepareUploadFallbackCheckpoint under the retry.
* fix(sftp): bound late OPEN close before releasing path gate
After drain force-complete, a late truncating OPEN must still close the
handle before releasing the path gate — but a dead channel may never
invoke CLOSE. Race close with a 2s timeout so same-path uploads cannot
wait forever on pathGate.waitForPrior.
* chore: re-trigger Codex review automation
* fix(sftp): always suppress terminal events for superseded OPEN attempts
Superseded means this attempt lost ownership. Do not require the retry to
still own activeTransfers — if the retry already completed, emitting
cancelled for the same transferId rewrites a completed transfer.
* chore: re-trigger Codex for path-gate head
* chore: nudge Codex automation after cancelled run
* fix(sftp): keep SFTP leases when suppressing superseded attempts
Superseded OPEN cleanup must not release transferId leases — a same-id
retry shares that lease set entry and would lose its hold if we release.
* fix(sftp): do not return result.error for superseded transfer attempts
Renderer directory ops treat any startStreamTransfer error as failure.
Superseded same-id OPEN attempts should return a non-error marker so the
live retry is not marked failed.
* fix(sftp): bound CLOSE on pre-settle cancel OPEN path
Reuse the 2s close timeout for cancel-before-settle so a dead channel
cannot leave the path gate held after finishCancel never runs.
* fix(sftp): hold path gate until unlink callback settles
Remove the 2s force-complete on late shared OPEN unlink so a delayed
server unlink cannot race a same-id retry that already reopened the stage.
* fix(sftp): path-gate r+ resume OPENs; release only unused superseded leases
Serialize all shared write opens including resume r+. On superseded, release
only SFTP leases not also held by the live same-id retry.
* fix(sftp): serialize shared and isolated OPENs by host+path
Path gates keyed only by SFTP object let an isolated-channel retry race a
stale shared truncating OPEN on the same host path. Prefer targetHostId so
both channels wait on the same gate.
* fix(sftp): path-gate isolated writes; revalidate checkpoint after gate
Gate all write OPENs including isolated channels so a shared stale OPEN "w"
cannot race. After waiting on the path gate, re-stat the stage and shrink
the resume checkpoint (and flags) before issuing r+/w.
* fix(sftp): dynamic truncating flags; cross-id stale OPEN; bound unlink
Resolve r+/w at OPEN/unlink time so afterPathGate flag getters still unlink
generated stages on cancel. Invalidate any same-path active transfer (not only
same-id) when a late truncating OPEN lands, and force checkpoint restart when
the stage was wiped before OPEN accepted. Bound best-effort unlink so a dead
channel cannot hang cancel settle.
* fix(sftp): clear staleOpenTruncatedStage after gated full restart
When a late truncating OPEN zeros the stage while a retry waits on the path
gate, afterPathGate restarts from offset zero. Consume the stale flag there so
a successful full rewrite is not failed by the post-upload guard.
* fix(sftp): keep cancel active during post-gate checkpoint revalidation
After the path gate opens, afterPathGate may stat a remote stage. Keep the
transfer abort hook and channel-error listener live for that await so cancel
or a dead channel can still release the path gate and lease.
* fix(sftp): hold path gate for isolated write OPENs until settle
Isolated truncating OPENs previously released the host/path gate on cancel
as soon as sftp.end() was called, allowing a same-path retry to race a still
in-flight OPEN that can truncate the stage without invalidation. Keep the gate
until the OPEN callback (or a bounded force-release after end), and run late
OPEN invalidation for isolated channels too.
* fix(sftp): close late shared read OPEN handles after settle
Restores close of late shared/sudo OPEN "r" handles after cancel or channel
error settle. Write-only gate invalidation remains for isolated late OPENs.
* fix(sftp): copy host IDs onto transfer for path-gate keys
dedicatedTransferResume passes targetHostId with a new targetSftpId. Keep
those host IDs on the transfer object so sharedWriteOpenSessionKey stays
host-scoped across session replacement instead of falling back to sftpId.
* fix(sftp): do not complete directory children on superseded invoke
When a same-id retry steals ownership mid-OPEN, the old startStreamTransfer
returns { superseded: true }. Wait for the live owner's terminal task status
instead of treating that result as a successful child completion.
* fix(sftp): pass host IDs on background restart; scope stale-OPEN invalidation
Background agent resume now includes source/target host IDs so path gates stay
host-scoped after openSftpForSession. Stale OPEN invalidation requires a
matching host or sftp session instead of treating missing host as a wildcard.
* fix(sftp): do not unlink stage after same-transfer fastPut fallback
When concurrent isolated OPEN dies without a callback and upload falls back
to fastPut on the same generated stage, a late OPEN must not unlink that
stage. Skip unlink when the same transfer already accepted an OPEN, owns
stagedRemote, or moved to a fastPut/stream strategy.
* fix(sftp): narrow fastPut unlink skip; handle superseded in all callers
Only suppress late-stage unlink when the same transfer moved to
fastPut/stream — ordinary cancel still unlinks. Background restart and
dedicated resume wait for the live same-id owner instead of treating
superseded invokes as completed.
* fix(sftp): never force-release isolated write path gates
Hold host/path gates for isolated truncating OPENs until the OPEN callback
settles, matching shared-channel behavior. Force-releasing after sftp.end()
allowed a late OPEN to wipe a completed same-path destination with no live
transfer left to invalidate.
* fix(sftp): hold path gate until late unlink settles
Transfer cancel can settle after a 2s unlink grace so dead channels do not
hang invokes, but the host/path gate stays held until the unlink callback
actually fires so a delayed unlink cannot delete a same-path retry stage.
* fix(sftp): wait for live owner without fixed superseded deadline
Superseded invokes now wait only on the live same-id transfer's terminal
status (or cancel). A 30-minute timeout could mark a still-running long
transfer failed and stick that status after the owner later completed.
* fix(sftp): classify superseded before cancelled cleanup
When a cancelled attempt loses ownership to a same-id retry mid-OPEN, the
error is Transfer superseded while cancelled is still true. Handle supersede
first so cancel cleanup cannot delete the retry stage, free its leases, or
emit a terminal cancelled event for the live owner.
* fix(sftp): wait for pending write OPEN before fastPut fallback
Expose pendingWriteOpenPathGate on the transfer so non-resumable concurrent
isolated failures wait for their own truncating OPEN to settle before
fastPut rewrites the same stage.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
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(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>
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.
- 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.
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.
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.
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.
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.
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.