Skip to content

fix(stealth): dead keys vanishing on Windows, and stealth typing lost to a screenshot - #506

Closed
evinjohnn wants to merge 0 commit into
mainfrom
fix/windows-stealth-deadkey-and-screenshot-restore
Closed

fix(stealth): dead keys vanishing on Windows, and stealth typing lost to a screenshot#506
evinjohnn wants to merge 0 commit into
mainfrom
fix/windows-stealth-deadkey-and-screenshot-restore

Conversation

@evinjohnn

Copy link
Copy Markdown
Collaborator

Ports the two defects from the unmerged fix/stack-review-followups that main has not already fixed by another route. That branch was never pushed and had no PR; PR #434 merged only "the low-risk subset of the #420#424 review findings" and the rest sat on one local branch for 21 days.

Its third fix — stop() being gated on isAvailable() — is already on main (StealthKeyboardManager.stop() now guards only on !this.tap / !this.active), so it is deliberately not included here.

1. A dead key no longer vanishes (keyboard_hook_windows.rs)

unicode_for_key treated every non-positive ToUnicodeEx result as "no character". For n == 0 that is correct. For n < 0 it is not — that is a dead key, and where the layout allows it the OS has already written the spacing form of the accent to the buffer.

Returning "" lost the key outright, because the only empty-chars escape hatch is AltGr-gated:

if altgr && key_code == 0 && chars.is_empty() { return pass(); }

A plain dead key is not AltGr, so it skipped that guard, reached send_payload with chars: "", and fell through to LRESULT(1)"Swallow: the foreground meeting app never sees this keystroke." The key therefore reached neither the overlay nor the foreground app. That silently kills every accent key on German T1, AZERTY, Spanish, Nordic and US-International layouts while stealth typing is engaged.

This is not dead-key composition. wFlags=0x4 deliberately stops the kernel accumulating the dead key — composing properly would corrupt what every other app on the machine sees — so an acute accent followed by e yields two characters, not one precomposed glyph. Visible and correctable beats silently eaten.

The altgr guard is left narrow on purpose. Widening it to cover any no-text printable key is a genuine improvement, but WindowsPlatformParity pins the literal narrow form, so that change belongs with its own test update rather than smuggled in here.

2. Stealth typing survives a screenshot (main.ts)

hideWindowsForScreenshothideMainWindow()WindowHelper.stopStealthTyping() tears the hook down on the way into a capture; that call site's comment names "screenshot hide" explicitly. restoreWindowsAfterScreenshot put the main window, Settings and the model selector back — but never the hook. The overlay returned looking identical with stealth typing gone, so the user's next keystrokes went to the foreground meeting app.

ScreenshotCaptureSession now records wasStealthTypingActive, and the restore re-engages when it was set.

The re-engage runs last, and the ordering is forced in both directions:

  • start() refuses on win32 unless the overlay is already visible, so it cannot precede the main-window restore.
  • start() calls hideAuxWindowsForStealth(), so running it before the Settings / ModelSelector restores would find those windows already hidden, do nothing, and they would then be re-shown under an engaged hook — visible windows whose input is dead because every keystroke routes to the overlay.

Consequence named rather than hidden: Settings + stealth collapses to stealth-only, a valid state, rather than a window that looks usable and is not.

Validation

  • Build validated on macOStsc -p electron/tsconfig.json --noEmit, 0 errors
  • Covered by automated Windows branch testsWindowsPlatformParity 22/22 (it string-asserts the Rust file, including the char-translation contract)
  • Covered by automated testsScreenshotCaptureFocusSafety 6/6, ScreenshotCaptureRuntime 6/6, StealthBlockInputFocusGuards 19/19, StealthShortcutGuard 6/6, StealthIpcHandlerRegistration 4/4 — 63/63 across the six suites. ScreenshotCaptureFocusSafety string-matches main.ts, so the comments added here were checked against its indexOf anchors rather than assumed safe.
  • Reviewed but not executed on Windows
  • Requires physical Windows verificationkeyboard_hook_windows.rs is #![cfg(target_os = "windows")] and its dependencies need the Windows SDK, so it cannot be compiled from macOS. The dead-key path is reasoned from the Win32 return-value contract, not observed. Please do not merge on a green macOS run alone — this needs a Windows build and a real keyboard on a dead-key layout.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JJS2xjF8uDPMa4EpXey5FL

@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

React Doctor found 6 issues in 1 file · 6 warnings · score 91 / 100 (Great) · vs main

6 warnings

electron/main.ts

  • ⚠️ L771 Chained array iterations js-combine-iterations
  • ⚠️ L1234 Import from a barrel file no-barrel-import
  • ⚠️ L2575 .map().filter(Boolean) loops twice js-flatmap-filter
  • ⚠️ L4692 Array lookup inside a loop js-set-map-lookups
  • ⚠️ L4918 Chained array iterations js-combine-iterations
  • ⚠️ L8180 Chained array iterations js-combine-iterations
⚠️ Warning: .github/workflows/react-doctor.yml is configured incorrectly. See below to fix.

React Doctor compares against main to report only the issues this pull request introduces. This run couldn't complete that comparison (usually a shallow CI checkout with no merge base), so it listed every issue in the changed files, including ones that already existed on main.

Add fetch-depth: 0 to the actions/checkout step in .github/workflows/react-doctor.yml so the checkout includes the history React Doctor needs:

 jobs:
   react-doctor:
     steps:
       - uses: actions/checkout@v5
+        with:
+          fetch-depth: 0

       - uses: millionco/react-doctor@v2

To silence this warning, set silence-missing-baseline-warning: true on the React Doctor action.

Reviewed by React Doctor for commit d9ad1e4. See inline comments for fixes.

@greptile-apps

greptile-apps Bot commented Aug 26, 2026

Copy link
Copy Markdown

Greptile Summary

The PR preserves spacing characters returned for Windows dead keys and restores stealth typing after screenshot capture.

  • Distinguishes dead-key translations from keys with no Unicode translation.
  • Snapshots pre-capture stealth state and restores the hook after windows are shown.
  • Updates README branding and repository-view metadata.

Confidence Score: 4/5

The PR is not yet safe to merge because screenshot restoration can re-enable global stealth key interception after the user explicitly requested it to stop.

The asynchronous capture records only the original active boolean; intervening stop and overlay-hide paths record no durable intent, and restoration later shows the overlay and restarts the hook from that stale snapshot.

Files Needing Attention: electron/main.ts

Important Files Changed

Filename Overview
electron/main.ts Adds screenshot-session restoration for stealth typing, but the pre-capture boolean can override an explicit stop request made during the asynchronous capture.
native-module/src/keyboard_hook_windows.rs Preserves the spacing character supplied by ToUnicodeEx for dead-key results while retaining the no-translation path.
README.md Updates the product heading and repository-view badge.

Sequence Diagram

sequenceDiagram
    participant User
    participant App as Screenshot session
    participant Hook as Stealth keyboard manager
    participant Capture as Screenshot capture
    App->>App: "Snapshot stealthActive = true"
    App->>Hook: Stop for window hiding
    App->>Capture: Await capture
    User->>Hook: Request stop or leave overlay
    Hook-->>User: Already inactive
    Capture-->>App: Capture completes
    App->>App: Restore overlay
    App->>Hook: Start from stale snapshot
    Hook-->>User: Keystrokes intercepted again
Loading

Reviews (2): Last reviewed commit: "docs: update Repo Views badge to 1.8M" | Re-trigger Greptile

Comment thread electron/main.ts
Comment on lines +7148 to +7149
if (session.wasStealthTypingActive) {
this.restoreStealthTypingAfterScreenshot();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stale snapshot reactivates stealth typing

When the user stops stealth typing or leaves the overlay while screenshot capture is awaiting completion, this restoration uses the earlier wasStealthTypingActive snapshot and starts the hook again, causing subsequent keystrokes to be intercepted after the explicit stop.

Knowledge Base Used:

Fix in Claude Code

@evinjohnn evinjohnn closed this Aug 26, 2026
@evinjohnn
evinjohnn force-pushed the fix/windows-stealth-deadkey-and-screenshot-restore branch from d9ad1e4 to 2ca5b70 Compare August 26, 2026 00:56
@greptile-apps

greptile-apps Bot commented Aug 26, 2026

Copy link
Copy Markdown

No reviewable files after applying ignore patterns.

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.

1 participant