Skip to content

feat(screenshot): briefly highlight captured area with orange-red flash#232

Open
exalsch wants to merge 9 commits into
CursorTouch:mainfrom
exalsch:feat/screenshot-flash-overlay
Open

feat(screenshot): briefly highlight captured area with orange-red flash#232
exalsch wants to merge 9 commits into
CursorTouch:mainfrom
exalsch:feat/screenshot-flash-overlay

Conversation

@exalsch
Copy link
Copy Markdown

@exalsch exalsch commented May 8, 2026

Summary

After every Screenshot / Snapshot(use_vision=True) capture, briefly draw a glowing orange-red border over the captured area as a visual confirmation. Useful for end-users supervising agent activity — you can see at a glance which area was just sent to the LLM.

  • Region capture: solid orange-red border outlines the region, then fades out over the last ~35% of the duration.
  • Full-screen capture: an inner border (12 px inset) on each monitor fades in and back out (bell curve, peaks at the midpoint).
  • Duration: ~2.5 s. Color: #FF4500. Border: 6 px (concentric rectangles for soft glow).
  • The overlay is rendered on a transparent always-on-top tkinter window on a daemon thread, started after capture, so it never appears in the captured image. Any active overlay is cancelled before the next capture so it never bleeds into a follow-up screenshot.
  • Disable with WINDOWS_MCP_DISABLE_FLASH=1 (also accepts true/yes/on).

Why

Right now there is no visual indication that a screenshot just happened — the agent silently captures the screen and the user only finds out via tool logs. A subtle, time-bounded glow makes the action observable without being intrusive.

Implementation notes

  • New module src/windows_mcp/desktop/flash_overlay.py with show_capture_flash(rects, full_screen) and cancel_active_flash(). Module-level lock and _active_overlay track the currently visible overlay so cancellation is synchronous (uses an Event the Tk loop polls each frame).
  • Desktop.get_screenshot (the single funnel for both Screenshot and Snapshot(use_vision=True)) calls cancel_active_flash() before capture and show_capture_flash(...) after. For region captures, the rect is the existing capture_rect. For full-screen, uia.GetMonitorsRect() is used so each monitor gets its own inner border.
  • Standard library only — tkinter is used for the overlay window. If the import fails (e.g., headless build), the module logs and silently skips.
  • tests/conftest.py sets WINDOWS_MCP_DISABLE_FLASH=1 so the unit suite never starts a Tk thread (which would race with pytest teardown). The flash-overlay tests themselves manage the env var via monkeypatch.

Tests

  • tests/test_flash_overlay.py — 18 tests covering env-var gating, dispatch behaviour (no-op when disabled / empty rects), overlay registration/cancellation lifecycle, and graceful fallthrough when tkinter import fails.
  • Full suite: 217 passed, 6 failed (the 6 failures are pre-existing on main and unrelated — ScrollElementNode.to_row / TreeElementNode.to_row not yet implemented).

Test plan

  • Run Screenshot / Snapshot(use_vision=True) from the MCP client and observe the orange-red border on the captured area
  • Run with multi-monitor full-screen capture and confirm each monitor gets its own inner glow
  • Run two captures back-to-back and confirm the first flash is cancelled before the second capture (no flash bleeds into the second image)
  • Set WINDOWS_MCP_DISABLE_FLASH=1 and confirm no flash appears
  • pytest tests/test_flash_overlay.py passes (18/18)

🤖 Generated with Claude Code

After every screenshot, draw a glowing orange-red border over the
captured area on a transparent always-on-top Tk overlay for ~2.5s.
Region captures get a solid border that fades out at the end; full-
screen captures show a bell-fading inner border on the union of all
monitor rects. The overlay is started *after* capture and cancelled
before any subsequent capture so it never appears in the captured
image.

Set WINDOWS_MCP_DISABLE_FLASH=1/true/yes/on to suppress the effect.
The conftest disables the flash for the test suite to avoid Tk
threads racing with pytest teardown.
@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Add orange-red flash overlay for screenshot visual confirmation

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add visual feedback flash overlay after screenshot capture
  - Orange-red glowing border highlights captured area for ~2.5s
  - Region captures show solid border that fades out; full-screen captures show bell-curve fade
• Overlay rendered on transparent always-on-top Tk window on daemon thread
  - Started after capture so never appears in captured image
  - Cancelled before next capture to prevent bleed-through
• Disable with WINDOWS_MCP_DISABLE_FLASH=1 environment variable
• Comprehensive test coverage with 18 unit tests for flash overlay module
Diagram
flowchart LR
  A["Screenshot/Snapshot<br/>Capture"] --> B["cancel_active_flash()"]
  B --> C["capture_image()"]
  C --> D["show_capture_flash()"]
  D --> E["Daemon Thread:<br/>Tk Overlay"]
  E --> F["Fade Animation<br/>~2.5s"]
  F --> G["Destroy Overlay"]
Loading

Grey Divider

File Changes

1. src/windows_mcp/desktop/flash_overlay.py ✨ Enhancement +189/-0

New flash overlay module for screenshot visual feedback

• New module implementing screenshot flash overlay functionality
• show_capture_flash() creates daemon thread with Tk overlay window
• cancel_active_flash() tears down active overlay before next capture
• _run_overlay() handles Tk window creation, animation, and graceful fallthrough if tkinter
 unavailable
• Environment variable WINDOWS_MCP_DISABLE_FLASH gates the feature
• Module-level lock and _active_overlay track currently visible overlay for synchronous
 cancellation

src/windows_mcp/desktop/flash_overlay.py


2. src/windows_mcp/desktop/service.py ✨ Enhancement +49/-19

Integrate flash overlay into screenshot capture flow

• Import flash_overlay module
• Call cancel_active_flash() before screenshot capture
• Call show_capture_flash() after capture with appropriate rects and full_screen flag
• For region captures, pass single rect; for full-screen, pass all monitor rects
• Wrap flash calls in try-except to prevent overlay errors from breaking screenshot
• Minor formatting fixes (line wrapping, spacing)

src/windows_mcp/desktop/service.py


3. tests/conftest.py 🧪 Tests +8/-0

Disable flash overlay for test suite safety

• Set WINDOWS_MCP_DISABLE_FLASH=1 by default in test suite
• Prevents Tk daemon threads from racing with pytest teardown
• Individual flash overlay tests override via monkeypatch

tests/conftest.py


View more (3)
4. tests/test_flash_overlay.py 🧪 Tests +117/-0

Comprehensive unit tests for flash overlay module

• New test module with 18 comprehensive unit tests
• TestFlashDisabled class: tests environment variable parsing (truthy/falsy values)
• TestShowCaptureFlash class: tests dispatch behavior, thread registration, env-var gating, empty
 rects handling
• TestCancelActiveFlash class: tests overlay cancellation and cleanup
• TestRunOverlayFallthrough class: tests graceful fallthrough when tkinter unavailable
• Uses mocking and monkeypatch to avoid actual Tk window creation

tests/test_flash_overlay.py


5. CLAUDE.md 📝 Documentation +1/-0

Document flash overlay environment variable

• Add WINDOWS_MCP_DISABLE_FLASH environment variable documentation
• Document that it suppresses the orange-red glowing border after screenshots
• Resolved in desktop/flash_overlay.py

CLAUDE.md


6. README.md 📝 Documentation +2/-1

Document flash overlay in README and tool descriptions

• Add WINDOWS_MCP_DISABLE_FLASH to environment variables table
• Document that it suppresses the orange-red glowing border
• Explain that flash is rendered after capture so never appears in image
• Update Screenshot tool description to mention visual confirmation flash

README.md


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 8, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (2) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Tests lack type hints 📘 Rule violation ⚙ Maintainability
Description
New test functions/fixtures are added without parameter and return type hints. This violates the
requirement that all new or modified function signatures include type hints, reducing
maintainability and static analyzability.
Code

tests/test_flash_overlay.py[18]

+def _reset_active_overlay():
Evidence
PR Compliance ID 4 requires type hints on all new/modified function signatures. The newly added
fixture _reset_active_overlay and multiple test methods are defined without any type annotations
on parameters or return types.

CLAUDE.md
tests/test_flash_overlay.py[18-28]
tests/test_flash_overlay.py[31-44]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Newly added test functions and fixtures do not include type hints for parameters and return types.
## Issue Context
Compliance requires type hints on all new/modified function signatures, including in test code.
## Fix Focus Areas
- tests/test_flash_overlay.py[18-117]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Flash API docstrings non-Google 📘 Rule violation ⚙ Maintainability
Description
Public functions cancel_active_flash and show_capture_flash have docstrings that are not in
Google-style format (missing Args:/Returns: sections). This violates the docstring standard for
public APIs and makes generated docs inconsistent.
Code

src/windows_mcp/desktop/flash_overlay.py[R37-66]

+def cancel_active_flash(timeout: float = 0.25) -> None:
+    """Tear down any flash overlay currently on screen.
+
+    Call this immediately before taking a screenshot so the previous flash
+    can never bleed into the new capture.
+    """
+    global _active_overlay
+    with _lock:
+        ov = _active_overlay
+        _active_overlay = None
+    if ov is None:
+        return
+    ov.stop_event.set()
+    ov.closed_event.wait(timeout=timeout)
+
+
+def show_capture_flash(
+    rects: list[tuple[int, int, int, int]],
+    *,
+    full_screen: bool,
+) -> None:
+    """Show a fade-in/out orange-red border over each rect.
+
+    ``rects`` are ``(left, top, right, bottom)`` tuples in virtual-screen
+    coordinates. ``full_screen=True`` draws an inner border that fades in
+    then out (used when no region was specified). ``full_screen=False`` keeps
+    the border solid for most of the duration and fades out at the end.
+
+    Returns immediately; rendering happens on a daemon thread.
+    """
Evidence
PR Compliance ID 5 requires Google-style docstrings for public functions/classes. The added public
API functions include docstrings, but they are narrative/reST-like and do not follow Google style
(e.g., no Args:/Returns: blocks).

CLAUDE.md
src/windows_mcp/desktop/flash_overlay.py[37-66]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Public APIs in `flash_overlay.py` use non-Google-style docstrings.
## Issue Context
Compliance requires Google-style docstrings for public functions/classes to keep documentation consistent.
## Fix Focus Areas
- src/windows_mcp/desktop/flash_overlay.py[37-66]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Bad geometry for negatives ✓ Resolved 🐞 Bug ≡ Correctness
Description
_run_overlay builds the Tk geometry as f"{width}x{height}+{left}+{top}", which produces strings
containing '+-' when left/top are negative. Monitor/capture rects originate from Win32
virtual-screen coordinates that can be negative, so the overlay may be skipped or misplaced on
multi-monitor layouts with a display left/above the primary screen.
Code

src/windows_mcp/desktop/flash_overlay.py[123]

+        root.geometry(f"{width}x{height}+{left}+{top}")
Evidence
The overlay window is positioned using left/top derived from the min of the passed rects, and
those rects are sourced from Win32 monitor/virtual-screen coordinates. Win32 APIs in this repo
return raw coordinates (not normalized to >=0), so negative offsets are a supported/expected input
into the overlay placement code path.

src/windows_mcp/desktop/flash_overlay.py[97-124]
src/windows_mcp/desktop/service.py[1055-1074]
src/windows_mcp/uia/core.py[632-643]
src/windows_mcp/uia/core.py[646-678]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`flash_overlay._run_overlay()` constructs the Tk geometry string using `+{left}+{top}`. When `left`/`top` are negative (valid in virtual-screen coordinates), the resulting geometry string contains `+-...`, which can cause incorrect positioning or failure to place the overlay.
### Issue Context
Rectangles passed into the overlay come from `uia.GetMonitorsRect()` and virtual-screen APIs, which may return negative coordinates for monitors arranged left/above the primary display.
### Fix Focus Areas
- src/windows_mcp/desktop/flash_overlay.py[97-124]
### Suggested change
Build the geometry string using explicit signed formatting for offsets, e.g.:
- `root.geometry(f"{width}x{height}{left:+d}{top:+d}")`
(or equivalent logic that ensures a single leading sign for each offset).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Overlay not cancelled on replace ✓ Resolved 🐞 Bug ☼ Reliability
Description
show_capture_flash overwrites the module-level _active_overlay with a new overlay without stopping
any previously running overlay. If multiple flashes are started before earlier ones end,
cancel_active_flash() can only stop the most recently registered overlay, leaving earlier overlays
running until they self-expire (extra daemon threads/Tk loops).
Code

src/windows_mcp/desktop/flash_overlay.py[R67-80]

+    if _flash_disabled() or not rects:
+        return
+    rects = [tuple(r) for r in rects]
+    overlay = _Overlay()
+    overlay.thread = threading.Thread(
+        target=_run_overlay,
+        args=(rects, full_screen, overlay),
+        name="windows-mcp-flash",
+        daemon=True,
+    )
+    with _lock:
+        global _active_overlay
+        _active_overlay = overlay
+    overlay.thread.start()
Evidence
show_capture_flash() assigns _active_overlay = overlay unconditionally, while
cancel_active_flash() only signals and waits on the overlay currently referenced by
_active_overlay. Once a prior overlay is overwritten, it becomes unreachable to the cancellation
mechanism.

src/windows_mcp/desktop/flash_overlay.py[37-51]
src/windows_mcp/desktop/flash_overlay.py[53-80]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`show_capture_flash()` replaces `_active_overlay` without cancelling the previously active overlay. This breaks the module’s single-active-overlay contract: after an overwrite, the earlier overlay is no longer cancellable via `cancel_active_flash()`.
### Issue Context
Even though `Desktop.get_screenshot()` calls `cancel_active_flash()` before capture, overlapping/parallel calls (or any other direct caller of `show_capture_flash`) can still start a second overlay before the first completes.
### Fix Focus Areas
- src/windows_mcp/desktop/flash_overlay.py[37-80]
### Suggested change
In `show_capture_flash()`, atomically swap overlays under `_lock` and stop the previous one:
1. Under `_lock`, store `prev = _active_overlay` then set `_active_overlay = overlay`.
2. After releasing the lock, if `prev` is not `None`, set `prev.stop_event` and optionally wait briefly for `prev.closed_event`.
This preserves the ability for `cancel_active_flash()` to reliably tear down any overlay that was previously visible.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo



@pytest.fixture(autouse=True)
def _reset_active_overlay():
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Tests lack type hints 📘 Rule violation ⚙ Maintainability

New test functions/fixtures are added without parameter and return type hints. This violates the
requirement that all new or modified function signatures include type hints, reducing
maintainability and static analyzability.
Agent Prompt
## Issue description
Newly added test functions and fixtures do not include type hints for parameters and return types.

## Issue Context
Compliance requires type hints on all new/modified function signatures, including in test code.

## Fix Focus Areas
- tests/test_flash_overlay.py[18-117]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +37 to +66
def cancel_active_flash(timeout: float = 0.25) -> None:
"""Tear down any flash overlay currently on screen.

Call this immediately before taking a screenshot so the previous flash
can never bleed into the new capture.
"""
global _active_overlay
with _lock:
ov = _active_overlay
_active_overlay = None
if ov is None:
return
ov.stop_event.set()
ov.closed_event.wait(timeout=timeout)


def show_capture_flash(
rects: list[tuple[int, int, int, int]],
*,
full_screen: bool,
) -> None:
"""Show a fade-in/out orange-red border over each rect.

``rects`` are ``(left, top, right, bottom)`` tuples in virtual-screen
coordinates. ``full_screen=True`` draws an inner border that fades in
then out (used when no region was specified). ``full_screen=False`` keeps
the border solid for most of the duration and fades out at the end.

Returns immediately; rendering happens on a daemon thread.
"""
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Flash api docstrings non-google 📘 Rule violation ⚙ Maintainability

Public functions cancel_active_flash and show_capture_flash have docstrings that are not in
Google-style format (missing Args:/Returns: sections). This violates the docstring standard for
public APIs and makes generated docs inconsistent.
Agent Prompt
## Issue description
Public APIs in `flash_overlay.py` use non-Google-style docstrings.

## Issue Context
Compliance requires Google-style docstrings for public functions/classes to keep documentation consistent.

## Fix Focus Areas
- src/windows_mcp/desktop/flash_overlay.py[37-66]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

exalsch added 4 commits May 8, 2026 22:29
Replace the single transparent-canvas overlay with stacked thin
Toplevel "strip" windows along each side of the rect (top, bottom,
left, right). Each strip is a solid orange-red Toplevel with its own
-alpha, so per-layer fade is reliable — the previous -alpha +
-transparentcolor combination renders inconsistently on Windows.

Eight layers per side with quadratic alpha falloff: layer 0 sits on
the rect edge at full opacity, outer layers radiate further out (or
inward, for full-screen) with progressively lower alpha to produce a
soft halo. Time-based modulation still gives a quick fade-in, hold,
and slow fade-out for region captures, and a bell-curve fade for
full-screen.

Tests: tests/test_flash_overlay.py adds TestBuildStripDefs covering
strip placement (region outward, full-screen inward), alpha falloff,
and the no-room edge case.
The previous strip-Toplevels rewrite hung Tk's mainloop when launched
on a non-main thread (creating ~32 Toplevels with overrideredirect
froze the loop), so the flash never appeared even though the thread
was alive.

Switch back to a single transparent Tk window with a Canvas, but
produce the halo by drawing concentric border rectangles whose RGB
fades from full orange-red toward pure black. The canvas's
transparent-colour key is set to pure black, so dim outer layers
genuinely become transparent — no -alpha + -transparentcolor combo
(which Windows renders unreliably) is needed. The time fade is done
by re-rendering each frame with a scaled intensity, so the glow
fades in/out smoothly.

12 layers, ~4 px outer halo for region captures, inner glow inset by
4 px from the monitor edge for full-screen captures.

Tests: replace TestBuildStripDefs with TestLayerColor covering full
intensity, half-intensity blend, and the zero-intensity safeguard
(never returns pure black, which would punch through the transparent
key).
Two prior rewrites failed in different ways on the user's machine:
1. The single-window transparent canvas approach (-transparentcolor +
   per-frame redraw) rendered nothing — some Windows configurations
   refuse to map the layered window when -transparentcolor is set.
2. The multi-layer Toplevel-strip approach hung Tk's mainloop when
   ~8 or more Toplevels were created back-to-back on a non-main
   thread (overrideredirect + alpha + topmost).

Reduce the halo to a single ring of 4 thick (8 px) opaque Toplevel
strips per rect — the only configuration confirmed to actually render
and complete cleanly. No transparentcolor, no glow gradient — just
a solid orange-red border that fades in (~15% of duration), holds,
and fades out (~35%).

Drop the test that asserted falling alpha across layers since there
is now only one layer; the remaining strip-placement tests still
exercise the geometry.

Logs the strip count at INFO level so it's possible to verify the
overlay actually fired from server logs.
Drop Tk entirely — neither -transparentcolor (rendered nothing on
some Windows configs) nor multi-Toplevel strips (hangs Tk's mainloop
on a non-main thread once ~6+ Toplevels are created back-to-back)
delivered a usable result.

The new implementation creates a Win32 layered window
(WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOPMOST | WS_EX_TOOLWINDOW
| WS_EX_NOACTIVATE) and feeds UpdateLayeredWindow a 32-bit BGRA
top-down DIB section with premultiplied alpha. The halo itself is
rendered with PIL: solid orange-red ring plus a Gaussian-blurred
copy composited underneath, giving a real soft glow that fades from
the rect edge outward. Time fade is a linear scale of the alpha
channel re-pushed each frame (~30 ms cadence), quantised to 32 levels
to skip redundant uploads.

ctypes argtypes/restypes are set explicitly on every Win32 entry
point used because the pointer-sized handle/LRESULT defaults overflow
on x64.

Tests: replace TestBuildStripDefs with TestIntensityCurve (verifies
the bell-curve / hold-then-fade envelopes) and TestPremultipliedBgra
(verifies BGRA byte order, premultiplication, and alpha scaling for
the per-frame intensity multiplier).
@JezaChen
Copy link
Copy Markdown
Collaborator

JezaChen commented May 9, 2026

Looks good. Do you have a screenshot or a short screen recording/GIF to demonstrate how the orange-red flash looks in practice?

Comment thread src/windows_mcp/desktop/service.py Outdated
flash_overlay.cancel_active_flash()
image, used_backend = screenshot_capture.capture(capture_rect)
self._last_screenshot_backend = used_backend
try:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since desktop/service.py is already quite large, we want to keep it as clean as possible. Please consider moving the code added here into the show_capture_flash function.

@Jeomon
Copy link
Copy Markdown
Member

Jeomon commented May 9, 2026

Could you please share a video demonstration of the working

exalsch and others added 4 commits May 11, 2026 08:07
The orange ring was nested inward inside the captured rect, so for window-
targeted screenshots (especially borderless Tauri / WebView2 apps where the
DWM extended frame matches the content edge) the halo visibly overlapped
the window content instead of reading as a surround.

Switch the ring direction to outward by default — it now grows into the
existing 42px margin around the rect — and keep the inward nesting only
for the full-screen inner halo via outward=False.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lity

User feedback: the glow was easy to miss for window/region captures —
the 4px band against a 14px blur radius wasn't bright enough to register
when the bulk of the halo sits outside the captured rect on desktop
background.

Double the sharp-ring thickness to 8px and bump the total duration to
3.5s so the user has time to spot it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two review asks on PR CursorTouch#232:

JezaChen — desktop/service.py is already large; move the flash setup
code into show_capture_flash. Done: show_capture_flash now takes a
single uia.Rect (or None for full-desktop) and resolves rects /
enumerates monitors internally. get_screenshot collapses to a one-liner.

Qodo — show_capture_flash overwrote _active_overlay without signalling
the prior one, so a follow-up call would orphan the running overlay
and cancel_active_flash() could no longer reach it. Fixed with an
atomic swap: save prev under _lock, install new, then signal prev
outside the lock so the prior overlay tears down without blocking the
new caller.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rder

The two new full-screen tests monkeypatched ``sys.modules['windows_mcp.uia']``
but that is bypassed once another test in the suite imports the real uia —
``import windows_mcp.uia as uia`` then resolves via the cached parent-package
attribute rather than sys.modules. Patch ``GetMonitorsRect`` on the real
module instead so the override holds regardless of import order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@exalsch
Copy link
Copy Markdown
Author

exalsch commented May 14, 2026

There you go :-)
Win-MCP_PR#232_Demo

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.

3 participants