* feat(autocomplete): add zh-CN descriptions for zip, unzip, make, nano
Cover 32 missing Fig descriptions, generate zh-TW, add batch coverage tests, and record source and semantic checks.
* fix(autocomplete): correct zip, unzip, and make semantics
Limit zip -9 to the compression level, warn that unzip -0 is a suspect upstream option, and describe make -N as the Apple/NeXT compatibility extension.
* fix(autocomplete): simplify unzip -0 warning and correct NeXT note
Keep the -O gbk version caveat in the research record and describe Next/NeXT as a capitalization difference.
* feat(autocomplete): complete remaining common command descriptions
Add 177 zh-CN descriptions for python, python3, node, vim, sort, awk, and sed; generate zh-TW, cover the batch with real Fig specs, and record one deferred shared node key.
* fix(autocomplete): correct sed n and sort parallel semantics
Add the automatic-print condition to sed n, remove the universal CPU default from sort --parallel, and remove duplicated adopted-entry sections from the source record.
* fix: repair macOS terminal setup and align autocomplete details
- Read OSC 7 setup payloads through base64 stdin so BSD/macOS works
- Keep the FIDO askpass socket path within the Unix sun_path limit
- Align autocomplete descriptions in a shared fixed column
- Fix an invalid optional-parameter declaration and lint warnings
- Harden macOS-sensitive OSC 7, SFTP, temp-dir and privacy tests
* feat(autocomplete): add zh-CN descriptions for file and text utilities
Cover ln, stat, wc, cut, uniq, tee and xargs with 70 localized
descriptions. Add coverage and localization tests, preload the new common
specs, and record the upstream wc/xargs wording corrections.
* fix(autocomplete): keep popup scrollbars draggable
Panel mousedown handlers no longer preventDefault across the whole box, so the right-side detail scrollbar and the suggestion list scrollbar can be dragged while padding clicks still preserve terminal focus.
* fix(autocomplete): keep detail panel open while selecting text
Delay row hover clearing so the pointer can move into the right-side detail panel, and allow native text selection there without stealing terminal focus on ordinary padding clicks.
* fix(terminal): clear sticky broadcast latch on vim ex (#4107) (#4109)
Fixes#4107.
Merge gate: GitHub Codex clean on tip b7efbe347; local CLEAN residual (unknown colon-prefixed alt-screen challenges) left as documented thrash; CI green (lint-and-test / windows-fido / windows-broadcast-ssh); Electron smoke PASS NC_OUT=/workspace/netcatty-smoke-shots/pr-4109-b7efbe347-20261104-122316 (both panes exited vim via toolbar broadcast :q!).
* perf(ai): stop freezing the app during agent discovery and chat backups (#4116)
Both paths ran synchronous I/O on the Electron main process, which freezes
every window and the terminal keystroke relay while it runs:
- Agent discovery (opening the AI panel or Settings > AI) probed auth with
execFileSync: `security find-generic-password` (4 s timeout), `gh auth
status` (network, 5 s), `which cursor-agent` (4 s) and `cursor-agent status`
(8 s), one after another. The probes now use a non-rejecting async execFile
helper with the same result shape; discovery awaits them.
- Cursor SDK detection evaluated `import("@cursor/sdk")` (~4 MB of ESM plus
its transport stack) only to check that it is installed, and kept it
resident afterwards. It now checks for the ESM entry that the dynamic import
loads; the SDK itself still loads on first use. The package does not export
package.json and its CJS build is stripped from releases, so the check walks
require.resolve.paths() instead of using require.resolve.
- The AI chat backup wrote the whole history (often several MB) with
writeFileSync on every debounced change. Writes now use fs.promises and are
serialized so generations cannot interleave.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* perf(ui): stop whole-shell re-renders on clicks, focus, typing and modifiers (#4117)
Several everyday interactions re-rendered the entire shell (AppView, TopTabs,
TerminalLayer and every pane) even though nothing they show had changed:
- Clicking or focusing a split pane always replaced the workspaces array
(twice per click: onClick + onFocusCapture), cascading through the whole
app and re-saving the session restore snapshot. setWorkspaceFocusedSession
and moveFocusInWorkspace now keep the same array when focus is unchanged
(domain/workspace.ts setWorkspaceFocusedSessionId).
- Every window focus/visibility change resynced app lock and produced a new
runtime state object even when nothing changed, re-rendering the provider
tree. Equal states keep the current object and useAppLockState's result is
memoized.
- Quick switcher keystrokes went through AppSideEffects state and the
app-local UI store, re-rendering AppSideEffects, every host island, AppShell,
AppView and TopTabs per character. QuickSwitcherOverlay now owns the query
and host results; the overlay unmounts on close, which resets the query
exactly where setQuickSearch('') used to.
- Pressing/releasing Ctrl/Cmd (every ⌘C/⌘V/Ctrl+C) re-rendered the whole tab
bar to toggle shortcut number badges. The held flag now lives in a small
store read only by each tab's TabIconSlot.
- AppView passed fresh inline callbacks that defeated TopTabs' memo comparator
and every open editor tab; TextEditorTabView is now memoized. TopTabs
memoizes per-workspace session lists and hoists the root tab icons.
- Workspace drag previews only update state when the drop hint actually moves.
React commits / render time for 10 repetitions in the real app (profiling
build, 2 split panes, 300 hosts), main -> this change:
window focus 42 / 17.1 ms -> 2 / 0.4 ms
press/release Cmd 50 / 41.9 ms -> 22 / 6.6 ms
click focused pane 42 / 69.6 ms -> 3 / 0.4 ms
quick switcher typing 41 / 61.3 ms -> 16 / 34.8 ms
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* perf(terminal): cap live WebGL contexts, release them eagerly, bound note undo history (#4122)
* perf(terminal): cap live WebGL contexts and stop the context-loss cascade
Chromium keeps at most 16 WebGL contexts per renderer and force-loses the
one that drew least recently past that. Hidden terminal panes keep their
WebGL renderer unless hibernation is on, so with more than 16 terminal
tabs every forced loss was rebuilt 50 ms later, evicting another context.
With 20 local tabs, half of all tab switches landed on a lost context
(xterm waits 3 s before falling back), and terminals that lost three
contexts within a minute were parked on the DOM renderer for good.
- Add a shared context budget (12). Past it, renderers of panes hidden for
at least 3 s are released, least recently seen first. Visible panes are
never released or refused; a released pane rebuilds when shown.
- A context lost while hidden waits for the reveal instead of rebuilding
at once, and no longer counts toward the breaker.
- The short-hide CSS reveal path now also asks for the renderer (a no-op
while it is live).
- Disposing the addon left its context alive until GC. Lose it explicitly
so GPU memory and the Chromium slot come back right away.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* perf(notes): bound the source editor's undo history
Every undo step in the markdown source editor holds a full copy of the
note, and the stack never shrank. A long session on a large note kept
every intermediate version alive (a 100 KB note with 1,000 steps holds
~200 MB of UTF-16 text).
Keep the 200 most recent steps and at most 8M characters across them,
dropping the oldest first. The newest step always survives.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* perf(terminal): stop per-second churn while connecting and while idle (#4118)
* perf(terminal): stop per-second churn while connecting and while idle
- Connecting sessions: the 200 ms progress animation and 1 s countdown were
Terminal state threaded into the TerminalView ctx, so every connecting
session (hidden tabs and startup restores included) re-rendered Terminal +
TerminalView up to six times a second. Terminal now only publishes the
timeout deadline and real progress events; the connection dialog animates
and counts down locally (connectionTicker.ts). HostConnectionTestDialog
keeps passing progress/timeLeft as before.
- Monitor runtime: polled every second unconditionally and woke every
subscriber each time because takenAt always changes. It now polls at 1 s
only while something is monitored (here or in another window), 10 s
otherwise, and skips notifying for unchanged idle replies.
- Auto-reconnect progress log: capped at the newest 300 lines (it grew by a
few lines every 5 s retry for as long as an outage lasted).
- App Lock: report activity at most once a second instead of an IPC per
keystroke / wheel tick / pointer event.
- Tray/Dock: the renderer resends hosts only when the host list changed, and
the main process rebuilds the macOS Dock menu (sort + one native item per
host) only then, not on every session status change.
Real app, profiling build, main -> this change:
3 SSH sessions connecting (3 s) 14 commits / 35.3 ms -> 8 / 1.7 ms
idle (10 s) 13 commits / 3.1 ms -> 2 / 0.1 ms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(terminal): update observe-popup reconnect deps assertion for capped progress logs
CI failed because #4118 wrapped setProgressLogs in useCallback to apply
capConnectionProgressLogs and added it to scheduleAutoReconnect deps.
The observe-popup source assertion still expected the old deps list.
The attachExistingSession early-return behavior is unchanged; only the
stale dependency regex needed updating.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* perf(state): stop handler churn on session updates and repeated JSON work (#4121)
* perf(state): stop handler churn on session updates and repeated JSON work
- Shell handlers: AppSideEffects registers ~58 glue callbacks for the Host
islands. Several close over sessions/workspaces/hosts (split/copy/duplicate,
close tabs, delete host, keyboard-interactive, connect...), so every session
status change and host update (connect bursts, last-connected times)
re-notified the bridge and rebuilt all four shell bags, re-rendering AppView
and TopTabs. They are now registered behind per-key stable wrappers that
call the latest implementation (stabilizeAppHandlers); local callbacks and
their effects are unchanged. All registered entries are actions, setters or
refs, never read during render.
- Session actions: moveFocusInWorkspace, copySession and copyWorkspace read
workspaces / orphan sessions / log views through refs at call time (the same
snapshot the closure captured), so they keep a stable identity. Getters read
during render (isBroadcastEnabled, getOrderedWorkTabs, ...) are untouched.
- AI history: the live store was parsed five times when the AI state root
mounted; parseSessionsStore parses once per call (two parses total).
- AI history writes: the synced branch serializes once and reuses the string
for the disk mirror, and rewriting the same immutable sessions array (save
path, then cloud-sync collection) is skipped while storage still holds that
exact write in the same mode.
- Command history: saving after each Enter parsed the whole store (up to 10k
entries) only to read its clear generation, and every other window parsed it
again on the storage event. The generation is now serialized first and read
from the raw string (full parse only to adopt a peer clear), and the
debounced write runs when the renderer is idle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: retrigger CI after cancelled lint-and-test run
Previous lint-and-test was cancelled mid-run.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: binaricat <binaricat@users.noreply.github.com>
* chore: fix outstanding ESLint warnings (#4112)
* chore: fix outstanding ESLint warnings
- useSftpFollowTerminalCwd: drop activeSessionId/focusedSessionId from the
syncFollowToTerminalCwd deps; the callback reads them through refs and the
only consumer effect already re-runs on those ids.
- useSftpPaneDialogs: list the stable setShowDeleteConfirm callback in the
handleDelete/openDeleteConfirm deps.
- csvProxy: remove the unnecessary `\[` escapes inside character classes
(same matches under the `u` flag).
- Remove unused test imports/variables.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: retrigger CI after runner SIGTERM cancel
Previous lint-and-test was cancelled by runner shutdown, not a real failure.
* chore: retrigger CI after second runner cancel
Lint had already passed; plugin-contract step was cancelled by the runner.
* chore: retrigger CI after third runner cancel
Lint and schema steps passed; plugin-contract step was cancelled by
the runner again. Empty commit to re-queue lint-and-test.
* chore: retrigger CI after repeated runner cancel
plugin-contract step keeps being cancelled by the Actions runner
(not a test failure). Empty commit to re-queue; Actions rerun API
denied for this PAT.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: binaricat <binaricat@users.noreply.github.com>
* perf: add opt-in startup timeline and real-app startup harness (#4114)
* perf: add opt-in startup timeline and real-app startup harness
- electron/perfTrace.cjs records first-occurrence startup marks in the main
process when NETCATTY_PERF_TRACE=1 (a no-op otherwise), then writes the
main-window renderer timeline (navigation, paint, netcatty:* marks, JS heap)
and app.getAppMetrics() to JSON after the first renderer-ready signal.
- Marks: main.cjs entry / modules loaded / whenReady, main window creation,
registerBridges and plugin-host start/end, loadURL, did-finish-load,
renderer ready, window shown. The renderer adds three performance.mark()
calls (entry evaluated, App mounted, renderer ready), also visible in
DevTools.
- scripts/perf-startup.live.cjs (npm run perf:startup) launches the built app
N times against a throwaway --user-data-dir profile with ssh:// deep-link
registration disabled, optionally seeds synthetic hosts over CDP, and
reports cold plus warm p50/p90.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* perf(harness): add --profile option to reuse a startup profile
Lets the harness keep one profile across invocations, e.g. to compare warm
V8 code caches across builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: retrigger CI after flaky monitorRuntimeIsolation failure
Previous lint-and-test failed on known-preexisting
`useEffect is not defined` in monitorRuntimeIsolation.test.ts.
* chore: retrigger CI after monitorRuntimeIsolation useEffect flake
Known pre-existing flake: monitor controls test fails with
`useEffect is not defined`. Empty commit to re-run once.
* test(app): bind React hooks in monitorRuntimeIsolation VM sandbox
Extracting component declarations drops their React imports, leaving bare hooks such as useEffect undefined in the VM. Inject common React hooks and APIs while preserving explicit binding overrides to prevent intermittent monitor isolation failures in CI.
Stub the startup performance mark through App test bindings so the mount effect can execute without browser globals. Production components remain unchanged.
Validation: monitorRuntimeIsolation passed five consecutive runs. Plain npm run lint found 17 existing errors in git-ignored scratch files; excluding only _t.mjs and sim-4107.mjs passed with zero errors and eight existing warnings.
* chore: retrigger CI after plugin-contract runner cancel
Lint/schema passed; plugin-contract step cancelled by the runner
(not a test failure). Empty commit to re-queue.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: binaricat <binaricat@users.noreply.github.com>
* perf(main): stop blocking the first window on validator compilation (#4115)
The main process ran ~1.4 s of synchronous work in registerBridges before the
main window could start loading (app:// assets are served from the main
thread too), even for users without plugins:
- contractValidator: every createDefinitionValidator() built a fresh Ajv
instance and re-registered the 200 KB plugin contract (~17 ms each, ~90 of
them across the plugin host at startup, plus one per plugin view resource
request). Share one Ajv per process, compile each definition on first use
and memoize it; each validator still exposes its own `.errors`.
- distributionSnapshot: compile the three distribution schemas lazily on the
shared contract Ajv instead of a second Ajv at module load (273 ms -> 37 ms).
- pluginBridge: hoist the PluginViewResourceRequest validator to module scope.
- cloudSyncBridge: load webdav / @aws-sdk/client-s3 / @smithy/node-http-handler
on first sync instead of at startup (108 ms -> 4 ms).
- app:// scheme: enable codeCache so V8 reuses compiled renderer bundles
across launches and windows. A same-length in-place edit of the cached entry
chunk runs the new code, so stale cache is not reused after upgrades.
Startup harness (2000 hosts, 10 runs, warm p50): registerBridges 1421 ->
116 ms, plugin host 871 -> 30 ms, renderer ready 2147 -> 723 ms, total memory
923 -> 823 MB.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ai): keep sessions past 2MB localStorage budget via overflow (#4123)
Urgent fix for #4123: retain AI chat history past the 2MiB primary localStorage budget via overflow chunks + pre-split disk backup, with disk-only recovery markers, aggregate-quota degradation, import live-state retention, and count-cap marker resolution.
Gate: local Codex CLEAN (no P1) on 084ed813; CI green on acabe0bbc; Electron smoke PASS (pr-4124-acabe0bbc). Residual P2s noted (cross-window reconcile; vault-restore marker clear; retention-cap during pending recovery).
No release.
* perf(renderer): cut the startup bundle by a third (#4120)
* perf(renderer): cut the startup bundle by a third
Every window parsed ~6.1 MB of JavaScript before the first render. The entry
chunk carried all five UI locales, and the whole AI SDK chunk was preloaded
even though the AI panel is lazy.
- i18n: only English stays in the entry; other dictionaries load on demand
(plus their same-base fallback, e.g. zh-TW -> zh-CN). index.tsx awaits the
saved language before the first render, and I18nProvider re-renders when a
runtime switch finishes loading.
- AI runtime: aiStateSnapshots cleaned up chats through getAgentRuntime(),
and the plugin AI/agent hosts created their runtimes on mount, which pulled
the Catty harness and @ai-sdk providers into startup. Cleanup now goes
through a light handle that the runtime registers (the reversible privacy
flag and main-process privacy mapping are still cleared without a runtime),
and both plugin hosts create their runtime on the first request, re-checking
liveness after the async load.
- Chunking: manualChunks' object form also moved Vite's dynamic-import
preload helper into vendor-ai (the AI SDK itself uses import()), so the
entry imported it from there and vendor-ai (649 KB) was preloaded on every
launch. Vendor chunks now use the function form and the helper gets its own
1 KB chunk.
- jsqr is loaded when a QR image is imported instead of with the host editor.
- startupBundle.architecture.test.ts guards these invariants.
Startup JS (index.html): 5.29 MB entry + 649 KB vendor-ai + 138 KB radix ->
3.80 MB entry + 1 KB helper + 174 KB radix (-35%). Startup harness, 2000 hosts,
10 runs, warm p50: loadURL -> renderer entry evaluated 321 -> 207 ms,
loadURL -> renderer ready 481 -> 323 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(i18n): preload locale messages before static rendering
* test(i18n): preload locales in remaining zh-CN UI tests
Non-English dictionaries now load lazily, so I18nProvider can initially render English before its effect finishes loading zh-CN. Explicitly await loadLocaleMessages before rendering these localized UI tests to fix the remaining CI failures without timing-based waits or production changes.
* chore: retrigger CI after locale preload fix
Previous tip 38108ce66 was on the branch but the PR head stayed
stuck on d49ad024e after a GitHub 500 during push. Empty commit
to refresh the PR head and re-queue checks.
* chore: retrigger CI after plugin-contract runner cancel
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* perf(terminal): cache ligature font parses across terminals (#4126)
* fix(pty): forward childProcesses queries to terminal worker (#4127)
Worker-owned PTY sessions register their PIDs in the worker, leaving the main process map empty during close-busy checks.
Forward child-process queries to the terminal worker and register a worker handler for string and object payloads. Fall back to the main process tree if the worker is unavailable or returns a non-array result.
* fix(ui): keep follow-system themeSource from chrome override (#4131)
When Interface Theme is Follow System, chrome refresh called setTheme(light|dark) and locked nativeTheme.themeSource, so OS appearance flips no longer updated the UI. Keep setTheme('system') on system chrome preference.
Fixes#4130
Gate: GH Codex CLEAN on 2c0c1c77fd (6635017901); CI 41860537287 success; Electron smoke PASS NC_OUT=/workspace/netcatty-smoke-shots/pr-4131-37c0f491a (same tree as prior tip).
* fix(transfer): keep SSH alive when canceling mid-transfer (#4134) (#4135)
* fix(transfer): keep SSH alive when canceling mid-transfer (#4134)
Cancel of a large isolated SFTP upload/download used to call sftp.end()
mid-WRITE on a channel that shares the terminal SSH transport. That
CHANNEL_CLOSE with in-flight traffic can make the remote drop the whole
connection, so the shell shows [session closed by remote (no exit status)].
Abort now drains like the shared browse path (no mid-flight end); dispose
only after settle. Cancel returns { cancelled: true } so the pool does not
treat channel-closed as session loss.
* fix(transfer): deferred-dispose soft-cancelled isolated SFTP (#4134)
Codex P2 on 88b4890: softDispose dropped cancelled isolated download
channels from the pool without ending them, leaking remote subsystems.
Schedule sftp.end() after a ~2s quiet settle instead of abandoning forever
or ending mid-flight on the shared SSH transport.
Aligned cheap fix for isolated fastPut cancel: after force-settle, defer
end of the disposable channel the same way. Stopping ssh2 fastXfer
internal scheduling still needs invasive hooks — left as documented
residual; deferred end after quiet is the minimal leak fix.
Tests cover mid-flight isolated upload/download cancel (no end while
held) and eventual deferred end after settle.
* Revert "fix(transfer): deferred-dispose soft-cancelled isolated SFTP (#4134)"
This reverts commit 5b85ee70c0.
* fix(transfer): dispose soft-cancelled SFTP only after request drain (#4134)
Codex 4475686359 on 5b85ee70: fixed 2s deferred sftp.end() still fired while
READs were pending and could recreate the shared-SSH drop. Poll sftp._requests
for quiescence before ending; if still pending past the hard max, skip end
(abandon until session close) rather than ending mid-flight.
Tests: held isolated download READs past 2s must not end until released;
fastPut gate cancel still deferred-ends after drain.
* Revert "fix(transfer): dispose soft-cancelled SFTP only after request drain (#4134)"
This reverts commit 7e9934cd7e.
* chore: retrigger CI after lint-and-test runner cancel (#4135)
---------
Co-authored-by: binaricat <binaricat@users.noreply.github.com>
* fix(terminal-worker): stop EPIPE ignore path from log-feedback looping (#4137)
Fixes#4136.
When the terminal utilityProcess console pipe is already dead, classifyProcessError marks EPIPE / ERR_STREAM_DESTROYED as ignore, but installTerminalWorkerErrorGuards still called logError → console.error. That write failed with EPIPE, re-entered uncaughtException, and busy-looped.
- On action === ignore: only report, never logError.
- Latch consoleBroken on stdout/stderr error so the default console.error path no-ops after the transport dies.
- Harden compressed extract cancel rollback under SIGTERM (ignore HUP/INT/TERM during cleanup, unlink archive first) and stabilize the FIFO cancel test that was flaking CI.
Out of scope: #4128 main-process EIO twin.
* fix(terminal): stop Shift+Insert CSI leaking to broadcast peers (#4142)
Fixes#4142
Squash-merge gate:
- GH Codex CLEAN on tree-identical tip c59ea270e (receipt 6653109730)
- CI SUCCESS https://github.com/binaricat/Netcatty/actions/runs/41995749622
- Electron smoke PASS /workspace/netcatty-smoke-shots/pr-4143-c59ea270e (marker both panes; Insert CSI absent)
* fix(main): stop the process error guards from logging into a dead stdio sink (#4140)
* fix(main): stop the process error guards from logging into a dead stdio sink
#4128 reports the main process freezing with an `EIO` write error raised from
`logError` -> `console.error`. That is the same feedback loop as #4136: the
guard writes diagnostics through the sink that just failed, the failure comes
back as another process-level error, and the handler logs again.
The `ignore` short-circuit that fixed the terminal worker does not cover this
one, because `EIO` is not a benign stream teardown: it is classified as
`suppress`, which logs.
- Add `createDiagnosticLogger()`. It holds an `error` listener on the
diagnostics streams so a failed stdio write is no longer re-raised as an
uncaught exception, and it latches closed after an async sink error or a
synchronous throw, so later diagnostics are dropped instead of re-entering.
- Use it for the main-process logger and warner in `main.cjs`.
Errors are still captured for the crash log through `captureError`.
* fix(error-guards): forward the first stdio failure to the crash log
createDiagnosticLogger holds an `error` listener on stdout/stderr, so it
consumes the stream failure before the process guards can see it. Once the
sink latched closed, the failure was dropped from the crash log entirely:
a closed stdout pipe produced one report before the sink guard landed and
none after.
- Add an `onError` option to `createDiagnosticLogger`. It latches the sink
closed first, then forwards only the first stream error; a throwing
reporter cannot escape.
- Main process: report the failure through
`crashLogBridge.captureError("stdio", err)`.
- Terminal worker: fold the inline `consoleBroken` latch onto
`createDiagnosticLogger` and forward the first failure through
`report`/`reportWorkerError`.
Regression coverage spawns a subprocess whose stdout/stderr pipe is closed
by the parent and asserts exactly one report is produced.
* ci: retrigger tests after unrelated plugin SIGKILL flake
PluginLogSettings.test.tsx was SIGKILL'd and the lint-and-test job was
canceled; the failure is unrelated to the stdio guard change.
---------
Co-authored-by: shibo.li <shibo.li@d-robotics.cc>
Co-authored-by: 陈大猫 <16399091+binaricat@users.noreply.github.com>
* fix(ai): persist AI sessions per record in IndexedDB instead of a 2MB localStorage blob (#4125)
* fix(ai): persist AI sessions per record in IndexedDB instead of a 2MB localStorage blob
Closes#4123.
The legacy store serialized every session into one localStorage JSON key
under a 2MB byte budget. Each write re-serialized the whole store, and the
degradation ladder dropped whole oldest sessions when the budget was
exceeded — silently destroying capped chats (~1MB each) on every write.
- add aiSessionIdbStorage: db netcatty_ai_sessions, one record per session
(keyPath id) plus a meta store (schemaVersion, lastWriteAt, mirrored
active-session map); BroadcastChannel replaces the localStorage storage
event for cross-window sync
- one-time boot migration from netcatty_ai_sessions_v1; the legacy key
stays untouched for one release as a read-only fallback
- writeSessionsForStorage now diffs against the last persisted set and
writes only changed records through a serialized queue; the 2MB budget
and its degradation ladder are removed (the global re-serialization cost
they defended no longer exists)
- retention becomes a pure session-count cap (raised 50 -> 1000); the
per-session 200-message cap is unchanged
- when the cap evicts sessions, a pre-eviction backup generation is
written before the records are dropped, closing the #4018 mirror blind
spot; explicit deletions and cloud-authoritative removals do not
trigger it
- boot restore chain: IndexedDB -> legacy localStorage (migration) ->
#4018 disk backup generations; a corrupt/unavailable IndexedDB falls
back to legacy localStorage writes
- tests: IDB roundtrip/migration/active-map mirror via a minimal fake
indexedDB, per-record put/delete diffing, count-cap eviction with
pre-eviction generation, explicit-delete suppression, synced-history
no-prune, corrupt-IDB fallback; byte-ladder tests removed with the
ladder
* test(ai): update fork/undo reload tests to the IndexedDB persistence contract
Issue #4123: session persistence moved to IndexedDB per-record writes; the
persisted set no longer lands in the legacy localStorage key (read-only
fallback for one release) and the retention cap is a session count of 1000
instead of 50.
- fork/undo reload tests read the persisted set via
getPersistedAiSessionsSnapshot() instead of parsing localStorage
- cap-boundary assertions (49/50 newer chats) become preservation
assertions (newerCount + 2 records survive; the 1000-cap boundary is
covered by aiStateSnapshots.test.ts)
- the reload step seeds the read-only legacy store with the persisted set,
the stand-in for what the migration persists on a profile without
IndexedDB
* feat(ai): make the per-session message cap user-configurable
* fix(ai): sequence session sync after boot hydration
Cloud-sync collection (collectAiSessionsForSync) could run before the
IndexedDB boot hydration settled and writeSessionsForStorage() the stale
legacy localStorage list over newer per-record IndexedDB data. The
collection now skips the sync round until the boot load settles; the next
auto-sync picks up the hydrated store.
The hydration signal lives in aiStateSnapshots (isAiSessionsHydrationComplete
/ markAiSessionsHydrationComplete) and is shared with useAIState's boot
effect and the cross-window refresh handler.
* fix(ai): make the legacy session migration one-shot via a persisted marker
The migration keyed on 'IndexedDB empty + legacy populated'. Because the
legacy key is intentionally kept for one release as a read-only fallback,
any boot whose store was empty re-ran the migration and resurrected chats
the user had deleted (P1②).
A migrationCompleted marker is now persisted in the meta store after the
migration data is durable. Once present, the legacy key is never consulted
again: an empty store is authoritative. The marker also records a store
that was already populated by a pre-marker build, and an uninitialized
empty store, so 'intentionally empty' is distinguishable from 'uninitialized'.
* fix(ai): harden boot persistence failure paths
Four review findings around the boot load's failure modes:
- P2①: when IndexedDB fails at boot and sessions come from the legacy
store, the diff baseline was seeded even though nothing was persisted,
so a later edit diff-wrote only the edited record and the rest were
lost after recovery. The legacy set is now persisted best-effort (one
re-open retry) before seeding; if that fails the baseline stays unset
so the next edit rewrites every record.
- P2②: the active-pointer validation effect ran against the
initially-empty in-memory session array and nulled every pointer
before the async load finished. It now waits for the hydration signal.
- P2③: when IndexedDB stayed unopenable and the legacy store was
missing/corrupt, the unconditional recheck threw into the swallowing
outer catch and the #4018 disk backup was never applied. The recheck
failure is now contained (one retry already happens inside open()),
the newest backup generation is applied as the recovery source, and
the error is surfaced via console.warn.
- P2④: a cross-window empty getAll (another window deleted the last
chat) hit an early return, kept the deleted session alive locally and
resurrected it on the next edit. After hydration an empty store is
adopted; the early-return only covers the pre-hydration window.
Scenario regression tests live in useAIState.bootHydration.test.ts;
fakeIndexedDb gains failOpenTimes for counted open failures.
* fix(ai): gate session maintenance and migrate legacy overflow history
* fix(ai): skip backup restore of empty IDB and resume failed overflow migration
* ci: retrigger lint-and-test after runner shutdown
The previous Actions runner received a shutdown signal during plugin-contract
verification; lint had already passed and unit tests never started.
---------
Co-authored-by: GOUKI9999 <GOUKI9999@users.noreply.github.com>
Co-authored-by: 陈大猫 <16399091+binaricat@users.noreply.github.com>
* fix(ai): keep surrogate pairs whole when truncating model context (#4088)
* fix(ai): keep surrogate pairs whole when truncating model context
Head/tail truncation sliced strings by UTF-16 code unit, so a cut could
split an emoji into a lone surrogate. JSON.stringify then emits a
`\udXXX` escape that strict providers reject ("lone leading surrogate in
hex escape"), and because the same cut is reapplied every step, the chat
stays stuck on that 400 for every later request.
- Add textBoundaries helpers (sliceHead, sliceTail, safeSliceBounds) and
use them at every truncation site on the model request path.
- Wrap provider models with a middleware that replaces any remaining lone
surrogate with U+FFFD before the request body is serialized.
* fix(ai): keep own __proto__ keys when normalizing prompt values
toWellFormedValue cloned changed objects by plain assignment, so an own
__proto__ key (e.g. from JSON.parse) hit the inherited setter: the field
was dropped from the request and the clone's prototype changed. Clone
with the source prototype and define own properties instead.
* fix(theme): continue Windows system-theme fix with registrar coverage (#4108)
* fix(theme): broadcast native system theme updates to renderers
nativeTheme.updated only fires for listeners in the main process, so a renderer sitting in system mode never learned that Windows changed the application theme. Forward the resolved preference to every window and let the settings hook re-apply it.
* fix(theme): keep terminal chrome separate from native system policy
refreshActiveChromeThemeSurfaces called setTheme(light|dark), which the main process writes into nativeTheme.themeSource. The stored preference stayed 'system' while Electron was pinned to a fixed color, so Windows application theme changes were ignored.
Only paint the surfaces here. Also re-assert a fixed independent terminal theme when the OS preference changes: the fingerprint short-circuit skipped the refresh, and the hook effect never re-ran while the terminal props stayed stable.
* test(theme): bound the new jsdom chrome tests
A hanging jsdom/React render should fail the test with its name instead of consuming the CI job budget.
* test(theme): provide native theme in bridge registrar fixtures
* test(theme): provide nativeTheme in child-process registrar fixture
The worker tests introduced in #4127 omitted nativeTheme after the system-theme listener registration became part of the production registrar. Provide a real EventEmitter, matching the transfer-limit fixture, so registrar setup succeeds without weakening production behavior.
* ci: retrigger lint-and-test after runner shutdown
The previous ubuntu-latest lint-and-test run was canceled during plugin
contract verification (annotation: "The operation was canceled."). Lint
had already passed; tests never started. Tree is unchanged.
---------
Co-authored-by: HHXXYY123 <117068187+HHXXYY123@users.noreply.github.com>
* feat(terminal): continue Directory Picker with OSC 7 and reconnect fixes (#4113)
* feat(terminal): add Directory Picker context chip
* fix(terminal): bind directory reads to the exact SSH session
* fix(terminal): follow manually changed working directories
* fix(terminal): isolate cwd updates and gate local platform support
* docs(terminal): research Directory Picker cross-platform follow-up
* fix(terminal): confirm OSC 7 navigation and reset picker loading
* fix(terminal): preserve active shell OSC 7 during directory refresh
* test(terminal): retain cross-session OSC 7 submission guard
---------
Co-authored-by: Joseph Hau <hexart@gmail.com>
* feat(ai): support extended reasoning effort with GPT-5.5 xhigh (#4111)
* feat(ai): support xhigh and max reasoning effort levels
The provider-level reasoning default only offered low/medium/high, so models
that document the full graded ladder could not be configured to their top
tiers. DeepSeek V4/V4.1 grade `reasoning_effort` continuously up to `max`, and
Anthropic exposes `max` on 4.6+ plus `xhigh` from 4.7 onward.
Beyond the missing select options, three separate layers silently discarded
the higher values:
- REASONING_RANK stopped at `high`, so the visibility resolver treated
`xhigh`/`max` as stale and fell back to `low`.
- The OpenAI capability check only matched `deepseek-r1`, which left the
DeepSeek V4 family unrecognised and hid the thinking chip entirely.
- The pinned `@ai-sdk/openai@4.0.8` validates provider options against a zod
enum capped at `xhigh`, so `max` on Chat Completions threw
"invalid openai provider options" before any request was made.
Add a shared PROVIDER_REASONING_EFFORT_OPTIONS list used by both the settings
select and its save guard, extend the rank ladder, and teach the per-model
derivation about DeepSeek V4 grading plus the Anthropic effort generations.
Bump `@ai-sdk/openai` to 4.0.11, the first release whose enum accepts `max`;
it keeps 4.0.8's data-URL image fix.
Models whose vocabulary stops at `high` now clamp these levels onto their own
ceiling instead of dropping the setting, so an explicit user choice is never
silently ignored and no unsupported value reaches a provider.
* fix(ai): keep extended effort for explicit custom relay aliases
A custom OpenAI-compatible provider with an explicit reasoning default is the
escape hatch for relay model IDs that match no capability pattern. That branch
still advertised only low/medium/high, so resolveVisibleCattyThinkingLevel
treated a configured `xhigh` or `max` as stale and downgraded it to `high`
before the request was built.
Advertise the full ladder there: the explicit provider default is already the
user's assertion that the alias accepts reasoning_effort, so it should also
carry the ceiling they picked. Unrecognized aliases without a default keep
advertising nothing.
* test(ai): verify max effort with the pinned OpenAI SDK
---------
Co-authored-by: Arcadia <57169358+RayYiHang@users.noreply.github.com>
* fix(ai): safely gate OSC 133 readiness across input and output
* fix(ai): recover OSC readiness after erased input padding
* fix(ai): distinguish typed spaces from erased terminal cells
* fix(ai): recognize multi-cell readline erase padding
* fix(ai): gate ambiguous blank edits and cover readline kill paths
* fix(ai): preserve mixed blanks and pending prompt redraws
* fix(terminal): count Unicode display cells for shell readiness
* fix(ai): keep ambiguous readline edits gated until a trusted lifecycle
* ci: retrigger checks after rebase onto main
Empty commit; tree identical to the rebased OSC 133 tip.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: binaricat <binaricat@users.noreply.github.com>
Co-authored-by: expoli <31023767+expoli@users.noreply.github.com>
Co-authored-by: shibo.li <shibo.li@d-robotics.cc>
Co-authored-by: GOUKI9999 <gouki9999@hotmail.com>
Co-authored-by: GOUKI9999 <GOUKI9999@users.noreply.github.com>
Co-authored-by: DeepFal <41166795+DeepFal@users.noreply.github.com>
Co-authored-by: HHXXYY123 <117068187+HHXXYY123@users.noreply.github.com>
Co-authored-by: Joseph Hau <hexart@gmail.com>
Co-authored-by: Arcadia <57169358+RayYiHang@users.noreply.github.com>
* perf: add opt-in startup timeline and real-app startup harness
- electron/perfTrace.cjs records first-occurrence startup marks in the main
process when NETCATTY_PERF_TRACE=1 (a no-op otherwise), then writes the
main-window renderer timeline (navigation, paint, netcatty:* marks, JS heap)
and app.getAppMetrics() to JSON after the first renderer-ready signal.
- Marks: main.cjs entry / modules loaded / whenReady, main window creation,
registerBridges and plugin-host start/end, loadURL, did-finish-load,
renderer ready, window shown. The renderer adds three performance.mark()
calls (entry evaluated, App mounted, renderer ready), also visible in
DevTools.
- scripts/perf-startup.live.cjs (npm run perf:startup) launches the built app
N times against a throwaway --user-data-dir profile with ssh:// deep-link
registration disabled, optionally seeds synthetic hosts over CDP, and
reports cold plus warm p50/p90.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* perf(harness): add --profile option to reuse a startup profile
Lets the harness keep one profile across invocations, e.g. to compare warm
V8 code caches across builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: retrigger CI after flaky monitorRuntimeIsolation failure
Previous lint-and-test failed on known-preexisting
`useEffect is not defined` in monitorRuntimeIsolation.test.ts.
* chore: retrigger CI after monitorRuntimeIsolation useEffect flake
Known pre-existing flake: monitor controls test fails with
`useEffect is not defined`. Empty commit to re-run once.
* test(app): bind React hooks in monitorRuntimeIsolation VM sandbox
Extracting component declarations drops their React imports, leaving bare hooks such as useEffect undefined in the VM. Inject common React hooks and APIs while preserving explicit binding overrides to prevent intermittent monitor isolation failures in CI.
Stub the startup performance mark through App test bindings so the mount effect can execute without browser globals. Production components remain unchanged.
Validation: monitorRuntimeIsolation passed five consecutive runs. Plain npm run lint found 17 existing errors in git-ignored scratch files; excluding only _t.mjs and sim-4107.mjs passed with zero errors and eight existing warnings.
* chore: retrigger CI after plugin-contract runner cancel
Lint/schema passed; plugin-contract step cancelled by the runner
(not a test failure). Empty commit to re-queue.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: binaricat <binaricat@users.noreply.github.com>
Move infrequent Tool Access / MCP / privacy / HTTP setup into focused dialogs; keep status and clear actions on the main page. Rebased onto current main after locale/settings conflicts.
Codex CLEAN (gpt-6.1-terra). CI lint-and-test green. Focused privacy/MCP/guidance tests passed on rebase tip. No release.
* fix(ssh): support ChaCha20 in Electron crypto runtimes
* test(ssh): verify packaged cipher transport across platforms
* test(ssh): isolate unused Electron UI APIs in packaged transport harness
* test(ssh): isolate packaged dependency resolution from the checkout
* feat(serial): support local Windows named pipe consoles
* test(serial): synchronize pipe acceptance and cover saved-host form
* fix(serial): preserve named-pipe paths with spaces when editing hosts
* test(serial): verify Windows pipes through native Electron IPC
* test(serial): keep Electron listener callbacks in renderer context
* test(serial): cover busy pipe cancellation and bounded slow-reader writes
* test(serial): construct single-instance fixture pipe path correctly
* docs: losslessly shrink README images and mirror Trendshift badge
Recompress local images linked from the READMEs only when the file gets
smaller and decoded pixels stay identical. Copy the Trendshift badge from
README.md into the zh-CN, zh-TW, and ja-JP READMEs.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* docs: replace README screenshots with lossless WebP
Store the ten README screenshots as lossless WebP under docs/assets/readme
and point every language README at those files. Keep them out of app.asar.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* feat(system-manager): add container runtime switcher and kubectl pods
Keep the existing Docker tab and select docker, podman, nerdctl, or
Kubernetes from runtimes detected on the host. Kubernetes replaces the
switcher with context and namespace selectors in the same panel.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): address container runtime review findings
Move the runtime adapter into the domain layer, keep a context namespace
when namespace listing is forbidden, and emit PowerShell or cmd startup
commands for Kubernetes shell, logs, and describe.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): cover Windows container popups and stale namespaces
Run container shell and logs through the host shell's inspect wrapper on
PowerShell and cmd, and reset the namespace when a remembered Kubernetes
context is no longer present.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): parse kubectl JSON after banners and scope inspect cache
Skip bracketed banners before kubectl JSON, and include the container
runtime in image and container inspect cache keys.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): parse Podman image lists emitted as JSON arrays
Podman applies {{json .}} to the whole image slice. Accept that array,
including Names and byte sizes, and keep Docker's JSON lines.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): parse Podman stats arrays
Accept Podman's stats JSON array and its id/cpu_percent fields, and keep
Docker's one-stat-per-line output.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): list every Podman image tag
Expand each name in a Podman image's Names array into its own row so
extra tags stay visible and searchable.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): quote kubectl list and delete for the session shell
Namespace, pod, and delete commands follow PowerShell or cmd quoting when
the session shell is not POSIX, so apostrophes in a context name survive
local PowerShell.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): drop renamed runtime launch commands from shell history
Container popups now probe with _nc_rt_err and also cover podman, nerdctl,
and kubectl. History sanitizers still required _nc_docker_err, so those
launches stayed in persisted history. Confirm-dialog resets also follow the
selected runtime.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): sum Podman interface stats and quote kubectl for the executor
Podman {{json .}} stats keep per-interface counters on Network, so the
details panel now sums RxBytes and TxBytes. Background kubectl runs in
powershell.exe on local Windows even when the terminal is cmd, so those
commands are quoted for that executor instead of the interactive shell.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): remove a selected image tag instead of the shared id
Podman rows that share one image id were all deleted with rmi <id>, which
either fails while other tags remain or drops every name. Tagged rows now
run rmi repository:tag; dangling images still use the id.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): reject cmd kubectl arguments that contain percent signs
Interactive cmd.exe expands %VAR% inside double quotes, so a context such
as prod-%USERNAME% would be rewritten before kubectl sees it. Cmd popups
now refuse that argument. PowerShell quoting still preserves the percent.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* refactor(system-manager): move runtime selection into an application hook
Persisted source, context, and namespace state plus the kubectl discovery
effects now live in useContainerRuntimeSelection. DockerManagerTab only
renders the switcher and panels.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): offer kubectl shell only for running containers
Succeeded and failed pods have no running container, and a completed init
container cannot accept exec. Shell targets now come from container
statuses that are running; logs still include init containers.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): keep Windows popup commands out of shell history
PowerShell and cmd container popups inspect then run, and kubectl popups
are typed directly. History filtering only matched the POSIX sh -c wrapper,
so those launches were stored as user commands.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): mark Windows popups instead of matching kubectl syntax
Filtering kubectl --context logs and describe also dropped commands the
user typed. Windows popups now carry __NC_POPUP__, and ordinary kubectl
lines stay in history.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): reject cmd arguments that contain delayed-expansion marks
cmd.exe with /V:ON expands !VAR! inside double quotes. Contexts such as
prod!USER! are refused for cmd popups, same as percent signs.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): hide raw nerdctl and unreachable cluster errors
Only offer nerdctl after a short info probe succeeds, or when the failure is a socket permission error that sudo can still recover. Map dead containerd and unreachable kube API errors to short localized messages and keep the CLI text in the log.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): show terminating pods and ephemeral containers
A deletion timestamp is Terminating even while the phase is still Running. Ephemeral containers are included in the logs list, and a running one is a shell target.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): keep the runtime select on Kubernetes and avoid covering it
Kubernetes uses the same compact source menu as Docker, with context and namespace beside it. Shell popups open in the free part of the window so they no longer sit on top of the pod badges.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): restore the saved engine when Kubernetes disappears
A host that was on Podman before Kubernetes keeps Podman if kubectl is gone, instead of falling back to Docker and overwriting that choice.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): avoid the visible system panel when others are parked
A hidden parking-host panel can be the first system-manager match and
has a zero-size rect. Scan every match and pass the first panel that
actually has size so the shell popup still sits beside the open panel.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): keep a runtime choice across probe gaps and show podman ports
A failed nerdctl or kubectl probe was saved as the fallback engine, so the
previous choice never returned. Reconciliation now restores the stored source
when it is detected again, and automatic fallbacks are not written back.
Podman ps port mappings and exposed ports are formatted into the same display
string the container detail strip already shows.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): show a pod's Evicted reason
Pod status.reason is the eviction signal, while the phase stays Failed and
the container often reports Error. Use that reason after a container waiting
state and before the generic phase so the badge stays Evicted.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): keep an avoided shell popup inside the work area
When the source window sits partly off-screen, clamping a slot back onto
the display could cover the system panel, and the fallback then used the
raw slot origin. Place the popup in the on-screen part of the slot instead.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): left-align the runtime selector row
The Kubernetes context, namespace, and runtime selects sat on the right of
the header, so they did not line up with the search field. Keep the same
padding, let the context select fill the remaining width, and leave the
runtime and namespace selects at their natural width.
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(ssh): add per-host TOTP with QR scan and MFA auto-fill
Store an optional otpauth TOTP secret on the host, encrypt and sync it
like a password, and auto-submit it once when a keyboard-interactive
prompt matches JumpServer or PAM TOTP text.
Closes#3323
Refs #3415
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): address Codex TOTP review findings
Reset authenticator editor state when the host changes, match custom
prompts with RE2, offer keyboard-interactive during connection tests
when TOTP is configured, and drop the secret when a host becomes a plugin.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): fill TOTP from the active prompt and ship re2js
Attach the connection's otpauth URI to the keyboard-interactive dialog
so Fill current code uses that attempt, including unsaved hosts. Move
re2js into production dependencies so packaged builds can match custom prompts.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): ship the shared TOTP runtime in packaged builds
electron-builder allowlists domain CommonJS modules individually, and
sshAuthHelper requires domain/totp.shared.cjs at load.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): save a pasted TOTP secret and ignore stale QR imports
Host save waits for the authenticator draft to validate before writing
the host. QR import completions from the previous host no longer change
busy state or errors on the newly selected host.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* test(host): ignore TOTP copy in the proxy command summary check
The assertion was matching the word secret in the authenticator UI.
It now checks that the ProxyCommand token itself is not rendered.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* feat(ssh): simplify host TOTP setup and type codes into the terminal
Move authenticator setup into a dialog so the host form keeps a single setting row, show keyboard-interactive prompts as field labels, and add a terminal context-menu action that types the current code without Enter.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): keep TOTP fill inside the auth dialog
Remove the terminal context-menu action. Offer the verification-code button on each non-password prompt, highlight OTP prompts, and never write the code into the password field.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): keep MFA instructions and scan every screen
Show keyboard-interactive name and instructions as plain text when they are not already the field label, and scan each unmatched desktop source once instead of repeating the first display.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): keep multiline MFA prompt directions
Show lines before the last keyboard-interactive prompt as plain-text guidance, and keep that last line as the field label.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): scan TOTP QR codes at each display's full size
Request a thumbnail as wide as the widest display and as tall as the tallest, so a portrait screen is not scaled into a landscape frame.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): keep TOTP fill off menu prompts
Judge the fill action from the last prompt line and the shared menu, SMS, and confirmation exclusion list. Use a secondary button on the label row, and simplify the host authenticator row.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Follow-up to #3773: Traditional Chinese (Taiwan) autocomplete description dictionary via OpenCC s2twp, lazy-loaded like zh-CN.
Closes#3157.
Real-app smoke PASS on 4d2bd32e.
Overlay bundled Fig description strings when the UI locale is zh-CN.
Cached specs stay in English, and missing entries fall back to the original text.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* ci: hide only risky download links in issue comments
Stop deleting comments that merely mention an archive or executable
name. Match http(s) URLs whose path contains a risky extension, skip
the issue author as well as maintainers and bots, and minimize matches
as spam instead of deleting them.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* ci: relay edited issue comments with truncated bodies
Forward issue_comment edited events and include comment identity,
association, timestamps, and bodies clipped to 4000 characters.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Keep renderer-only libraries available as development dependencies so Vite
bundles their runtime code without shipping their dependency trees again.
Exclude native compilation helpers from packaged resources, and prune
node-pty/serialport prebuilds for other platforms and architectures while
preserving matching universal binaries and rebuilt runtime files.
Validation:
- npm run lint; 33 packaging tests pass.
- npm run pack:dir, real packaged application UI, and 29 packaged runtime
smoke checks pass (SDK imports, PTY, serial port listing, SQLite query).
- All 999 UI files retain their code/content; differences are generated
asset references and the embedded dependency classification metadata.
- Dependency versions are unchanged.
- macOS arm64 app: 394,959,135 -> 360,740,625 bytes.
- macOS arm64 ZIP: 150,847,369 -> 143,151,622 bytes; ZIP integrity passes.
- Full suite: 14,035 pass, 36 skipped, 5 fail. The four FIDO helper failures
and transfer FileHandle-GC failure reproduce on unchanged main with Node 26.