Skip to content

fix: restore dropped IPC handlers and fix several main-process bugs - #288

Closed
devoliverluccas wants to merge 3 commits into
stoatchat:mainfrom
devoliverluccas:lucas-oliveira/fix-critical-bugs
Closed

fix: restore dropped IPC handlers and fix several main-process bugs#288
devoliverluccas wants to merge 3 commits into
stoatchat:mainfrom
devoliverluccas:lucas-oliveira/fix-critical-bugs

Conversation

@devoliverluccas

Copy link
Copy Markdown

Six main-process bugs, three of which have open issues that turn out to have the same root cause: an IPC handler that is not registered at runtime.

1. Autostart and badge IPC handlers were never registered

native/autoLaunch.ts and native/badges.ts register their handlers as an import side effect, and nothing imports either module.

For autostart this is a regression in 1.5.0:

ipcMain.handle("getAutostart"/"setAutostart") went with it, so desktopConfig.setAutostart() rejects with "No handler registered" and the settings toggle silently does nothing — matching #287 exactly ("I had removed the option for Stoat to auto start in the options menu. However it still auto started. I verified that the windows startup option was still ticked").

Badges were never wired up at all: git log -S 'native/badges' shows the module has not been imported since the initial commit, even though the preload bridge has exposed native.setBadgeCount() since #25.

Verifiable on the current main:

$ pnpm package && grep -c 'getAutostart\|setBadgeCount' .vite/build/main.js
0

Both modules now expose an explicit init*() called from main.ts, so registration is tied to a call site the linter cannot quietly drop.

Closes #287. Refs #213 — this only restores the plumbing; the UX described in that issue still needs client-side work.

2. Discord RPC (refs #203)

  • Turning it on did nothing. The discordRpc setter ran initDiscordRpc() before writing the new value to the store, and initDiscordRpc reads that setting back out of the store — so it observed the stale value and took the if (!config.discordRpc) return early exit. The setters now persist first and act second.
  • Turning it off left the old client running. initDiscordRpc only called removeAllListeners() on the previous client and never destroyed it, so every toggle stacked another client that stayed logged in to Discord and kept broadcasting the activity.
  • login() is asynchronous, so its rejection escaped the try/catch as an unhandled rejection and never triggered a retry.
  • destroy() rejects when the transport never connected, which is now swallowed rather than surfacing as an unhandled rejection.
  • disconnected only schedules a reconnect for the client that is still current, and reconnects no longer stack.

3. Spellchecker not applied on launch (refs #69)

webPreferences.spellcheck was hardcoded to true and setSpellCheckerEnabled only ever ran from the setter, so the stored preference was never applied at startup — disabling it silently reverted on the next launch.

4. Screen sharing breaks after a cancelled picker (refs #267, #263)

screenPickerCallback is registered with ipcMain.once, so a picker the user dismissed without choosing a source leaves it attached forever. The next screen share request registers a second listener behind the stale one, which then consumes the reply and hands it to a request that is already dead — leaving screen sharing broken until the app restarts. Stale listeners are now cleared before registering, mirroring what the renderer side already does in world/window.ts.

Alongside it:

  • the bounds check rejected idx < 0 || idx > sources.length, so idx === sources.length passed and sources[idx] handed undefined to the callback as if it were a real source
  • getSources() had no rejection path, so a failure left the renderer waiting on a callback that was never invoked
  • NativeImage.resize() returns a new image instead of mutating in place, so discarding the result sent full-size icons over IPC

5. Window restored onto a disconnected display (refs #176)

The saved position was restored without checking that it still lands on a connected display, so unplugging or rearranging a monitor left the window opening off screen with no way to reach it. It now falls back to centring the window.

The guard also changes from x > 0 || y > 0 to x !== 0 || y !== 0, since negative coordinates are legitimate for displays positioned left of or above the primary one.


Testing

mise lint, mise format and mise build all pass.

I have not exercised these interactively — no manual autostart, screen share or RPC run — so runtime confirmation from someone on each platform would be welcome, particularly the screen picker path. The autostart and badge handlers are confirmed by diffing the packaged bundle before and after (0 → 1 occurrences of each handler, plus the 12 badge assets now resolved).

🤖 Generated with Claude Code

devoliverluccas and others added 3 commits August 19, 2026 18:42
…red up

Both `native/autoLaunch.ts` and `native/badges.ts` registered their IPC
handlers as an import side effect, and nothing imported either module, so
the handlers did not exist at runtime.

For autostart this was a regression:

  - e00f3a8 (stoatchat#237) removed the last direct use of `autoLaunch`, leaving the
    import unused
  - 6894231 (stoatchat#252) then removed the now-unused import during the lint pass

`ipcMain.handle("getAutostart"/"setAutostart")` went with it, so the
renderer's `desktopConfig.setAutostart()` rejects with "No handler
registered" and the toggle in settings silently does nothing. Closes stoatchat#287.

Badges were never wired up at all -- `git log -S 'native/badges'` shows the
module has not been imported since the initial commit, even though the
preload bridge has exposed `native.setBadgeCount()` since stoatchat#25. Refs stoatchat#213.

Both modules now expose an explicit `init*()` that `main.ts` calls, so the
registration is tied to a call the linter cannot quietly drop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Lucas Oliveira <lucasferreiradeoliveira.20@gmail.com>
Two separate problems kept the RPC presence alive after it was turned off,
and kept it from coming back on without a restart. Refs stoatchat#203.

Turning it on did nothing: the `discordRpc` setter ran `initDiscordRpc()`
*before* writing the new value to the store, and `initDiscordRpc` reads the
setting back out of that store. It therefore observed the previous value,
took the `if (!config.discordRpc) return` early exit, and gave up. The
setters now persist first and act second.

Turning it off left the old client running: `initDiscordRpc` only called
`removeAllListeners()` on the previous client and never destroyed it, so
every toggle stacked another client that stayed logged in to Discord and
kept broadcasting the activity. Teardown now detaches the listeners,
destroys the client and cancels any pending reconnect.

Also fixes the reconnect path around it:

  - `login()` is asynchronous, so its rejection escaped the `try`/`catch`
    as an unhandled rejection and never triggered a retry; it is awaited now
  - `destroy()` rejects when the transport never connected, which is now
    swallowed instead of surfacing as an unhandled rejection
  - `disconnected` only schedules a reconnect for the client that is still
    current, so a client being torn down cannot resurrect itself
  - reconnects no longer stack when several fire before the timer elapses

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Lucas Oliveira <lucasferreiradeoliveira.20@gmail.com>
…on screen

Three unrelated main-window bugs.

Spellchecker (refs stoatchat#69): `webPreferences.spellcheck` was hardcoded to
`true` and `setSpellCheckerEnabled` only ever ran from the setter, so the
stored preference was never applied at startup and disabling it silently
reverted on the next launch.

Screen picker (refs stoatchat#267, stoatchat#263): the `screenPickerCallback` listener is
registered with `ipcMain.once`, so a picker the user dismissed without
choosing a source left it attached forever. The next screen share request
registered a second listener behind the stale one, which then consumed the
reply and handed it to a request that was already dead -- leaving screen
sharing broken until the app restarted. Stale listeners are now cleared
before registering, mirroring what the renderer side already does in
`world/window.ts`. Alongside it:

  - the bounds check rejected `idx < 0 || idx > sources.length`, so
    `idx === sources.length` passed and `sources[idx]` handed `undefined`
    to the callback as if it were a real source
  - `getSources()` had no rejection path, so a failure left the renderer
    waiting on a callback that was never invoked
  - `NativeImage.resize()` returns a new image instead of mutating in
    place, so discarding the result sent full-size icons over IPC

Window position (refs stoatchat#176): the saved position was restored without
checking it still lands on a connected display, so unplugging or
rearranging a monitor left the window opening off screen with no way to
reach it. It now falls back to centring the window. The guard also changed
from `x > 0 || y > 0` to `x !== 0 || y !== 0`, since negative coordinates
are legitimate for displays positioned left of or above the primary one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Lucas Oliveira <lucasferreiradeoliveira.20@gmail.com>
@devoliverluccas
devoliverluccas force-pushed the lucas-oliveira/fix-critical-bugs branch from 9c44642 to 5743d18 Compare August 19, 2026 21:42
@Dadadah

Dadadah commented Aug 20, 2026

Copy link
Copy Markdown
Member

for-desktop follows the same contribution guidelines as for-web. We do not accept contributions made by LLMs.

@Dadadah Dadadah closed this Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stoat auto starts when turning off button in options

2 participants