mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-13 18:21:00 +00:00
+1








16061720e6
* 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 on2c0c1c77fd(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 commit5b85ee70c0. * fix(transfer): dispose soft-cancelled SFTP only after request drain (#4134) Codex 4475686359 on5b85ee70: 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 commit7e9934cd7e. * 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 tipc59ea270e(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>
27 lines
1.6 KiB
JavaScript
27 lines
1.6 KiB
JavaScript
// Regenerate using the dev dependencies in the current lockfile.
|
|
// Production uses the generated CommonJS data, including on Node 22.0.
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const root = path.dirname(require.resolve('get-east-asian-width'));
|
|
const version = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version;
|
|
if (version !== '1.5.0') throw new Error(`Expected locked get-east-asian-width 1.5.0; found ${version}`);
|
|
const source = fs.readFileSync(path.join(root, 'lookup-data.js'), 'utf8');
|
|
const license = fs.readFileSync(path.join(root, 'license'), 'utf8');
|
|
let output = '// Generated by node scripts/generate-terminal-width-data.cjs\n'
|
|
+ '// Unicode 17.0.0 W/F data from get-east-asian-width 1.5.0:\n'
|
|
+ '// https://github.com/sindresorhus/get-east-asian-width/tree/v1.5.0\n'
|
|
+ '/*\n' + license.trim() + '\n*/\n\n';
|
|
for (const name of ['fullwidthRanges', 'wideRanges']) {
|
|
const match = source.match(new RegExp(`const ${name} = (\\[[^\\]]+\\]);`));
|
|
if (!match) throw new Error(`Missing source data: ${name}`);
|
|
const values = JSON.parse(match[1]);
|
|
output += `const ${name} = [\n`;
|
|
for (let i = 0; i < values.length; i += 12) output += ` ${values.slice(i, i + 12).join(', ')},\n`;
|
|
output += '];\n\n';
|
|
}
|
|
output += 'module.exports = { fullwidthRanges, wideRanges };\n';
|
|
const target = path.join(__dirname, '../electron/bridges/terminalWidthData.cjs');
|
|
if (process.argv.includes('--check')) {
|
|
if (fs.readFileSync(target, 'utf8') !== output) throw new Error('Terminal width data is out of date');
|
|
} else fs.writeFileSync(target, output);
|