fix(settings): pasting a provider API key no longer blacks out the window - #97
Closed
BIackFIame wants to merge 2 commits into
Closed
BIackFIame wants to merge 2 commits into
BIackFIame wants to merge 2 commits into
Conversation
Pasting a MiniMax API key into Settings -> Agents -> Provider API keys turned the window black. The key field's onChange read event.currentTarget.value inside the setDrafts((current) => ...) updater. React runs that updater later, during render, whenever an earlier update of the component is still pending (a paste over a character already in the field, the second keystroke of fast typing). By then the event has finished dispatching and currentTarget is null, so the render threw "Cannot read properties of null (reading 'value')" and React unmounted the whole root. The renderer process stayed alive with an empty #root. Key length and shape are not the cause: a 120-character key pasted over a pending edit breaks the same way. The handler now reads the value while the event dispatches and passes it to the updater. No other renderer updater reads an event (checked with an AST scan over src/renderer). Tests: tests/provider-secrets-settings.test.mjs bundles the real component against a React stand-in whose updaters run after dispatch and pastes fake sk-cp-/sk-api- keys of 120 to 10 000 characters, with a trailing newline and space, over a pending edit (fails before this change with the same TypeError); a line-level guard keeps event reads out of state updaters in the renderer.
An error thrown while React renders unmounts the whole root, but the renderer process lives on, so render-process-gone never fires and the main process's crash reload never runs: the window stays black until the app is restarted. That is what the broken provider key field did. The React root now handles onUncaughtError. The first such error logs it with its component stack and reloads the application surface in place; sessions live in the main process and survive the reload, as after a renderer crash. A second one within 30 s (or with no session storage to remember the reload) shows a static page with a Reload button instead of reloading in a loop. Tests: tests/uncaught-error-recovery.test.mjs (reload, cooldown, clock change, no storage, root wiring). Checked in a hidden app: an injected render error reloads to a usable Settings screen, a second one within the cooldown shows the recovery page; forcefullyCrashRenderer still reloads through render-process-gone.
Contributor
Author
|
Combined into #100 together with the other post-merge fixes, so they can be reviewed in one place. The commits are unchanged. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Based on main (20f6855). Independent of #96; the two merge cleanly, and the packaged test build carries both.
Symptom
In Settings → Agents → Provider API keys, pasting a MiniMax key (a long
sk-cp-…/sk-api-…string) turned the whole window black, and it stayed black until the app was restarted.Root cause
The key field's change handler read the event inside a functional state updater:
React only runs an updater while the handler is still executing when the component has no update pending. When one is pending (a paste over a character already in the field, or a second keystroke before the render), React runs the updater later, during render. By then the event has finished dispatching and
currentTargetisnull. The render throwsTypeError: Cannot read properties of null (reading 'value'), and React 19 unmounts the whole root because nothing catches the error.#rootis left empty.The renderer process keeps running, so
render-process-gonenever fires and the main process's crash reload never runs. That is why the window stayed black.The key's length and shape are not the cause. A 120-character key pasted over a pending edit breaks the same way. Nothing in the renderer validates, masks or runs a regex over the key, and the main-process redaction registry is linear: registering a 100 000-character key and masking 200 KB takes under 15 ms.
Repro (before the fix)
Hidden-window harness (keychain stubbed,
--use-mock-keychain, throwaway user data), running the packaged build of main + #96. It opens Settings, focuses the MiniMax field and pastes a fake key with CDPInput.insertText:#rootwith 0 children and empty body text. The renderer console shows the TypeError from theprovider-secret__inputonChange updater. The renderer PID doesn't change,render-process-gonedoesn't fire, and nothing recovers.Fix
ProviderSecretsSettings: readevent.currentTarget.valuewhile the handler runs, then pass the value to the updater. An AST scan oversrc/rendererfound no other state updater that reads an event.onUncaughtError:Tests
tests/provider-secrets-settings.test.mjssk-cp-/sk-api-keys of 120, 200, 250, 500, 2 000 and 10 000 characters, plus one with a trailing newline and one with a trailing space, each over a pending edit.tests/uncaught-error-recovery.test.mjs: the first error reloads, a second within the cooldown shows the page, an error after the cooldown reloads again, a clock that moved backwards doesn't block the reload, no storage shows the page, and the root routes errors to the recovery.--test-concurrency=2, fake HOME), on this branch and on this branch merged with fix(startup): load the application surface only after the startup page settled #96. Typecheck,electron-vite build,audit:secretsandtest:evenpass.Checked in the packaged app
Built with
npm run packagefrom main + #96 + this branch. The harness loaded theapp.asarcontents, which matchout/byte for byte, with the window hidden, the keychain stubbed and a throwaway user data directory:forcefullyCrashRenderer()still recovers throughrender-process-gone. There is a new renderer PID, and the MiniMax field works afterwards.