Extend resize mode to support targeting window by name#97
Extend resize mode to support targeting window by name#97Jeomon merged 1 commit intoCursorTouch:mainfrom
Conversation
- Extract shared window lookup logic into _find_window_by_name helper - Add name parameter to resize_app to target a specific window by name - Update App tool description in __main__.py and manifest.json
There was a problem hiding this comment.
Pull request overview
This PR extends the App tool’s resize mode so it can target a specific window by name (via fuzzy matching) instead of always operating on the active window, improving reliability when resizing non-foreground apps.
Changes:
- Added a shared
_find_window_by_name()helper to centralize fuzzy window lookup. - Updated
resize_appto accept an optionalnameand resize the matched window (refreshing state whennameis provided). - Updated
Apptool descriptions in__main__.pyandmanifest.jsonto document the new resize behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
src/windows_mcp/desktop/service.py |
Adds shared fuzzy window lookup and enables name-targeted resizing; refactors switch_app to reuse the helper. |
src/windows_mcp/__main__.py |
Updates the App tool description to clarify resize can use name. |
manifest.json |
Mirrors the App tool description update for the manifest. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @mcp.tool( | ||
| name="App", | ||
| description="Manages Windows applications with three modes: 'launch' (opens the prescibed application), 'resize' (adjusts active window size/position), 'switch' (brings specific window into focus).", | ||
| description="Manages Windows applications with three modes: 'launch' (opens the prescibed application), 'resize' (adjusts the size/position of a named window or the active window if name is omitted), 'switch' (brings specific window into focus).", |
There was a problem hiding this comment.
Spelling: the tool description says "prescibed"; this should be "prescribed".
| description="Manages Windows applications with three modes: 'launch' (opens the prescibed application), 'resize' (adjusts the size/position of a named window or the active window if name is omitted), 'switch' (brings specific window into focus).", | |
| description="Manages Windows applications with three modes: 'launch' (opens the prescribed application), 'resize' (adjusts the size/position of a named window or the active window if name is omitted), 'switch' (brings specific window into focus).", |
| { | ||
| "name": "App", | ||
| "description": "Manages Windows applications with three modes: 'launch' (opens the prescibed application), 'resize' (adjusts active window size/position), 'switch' (brings specific window into focus)." | ||
| "description": "Manages Windows applications with three modes: 'launch' (opens the prescibed application), 'resize' (adjusts the size/position of a named window or the active window if name is omitted), 'switch' (brings specific window into focus)." |
There was a problem hiding this comment.
Spelling: the tool description says "prescibed"; this should be "prescribed".
| "description": "Manages Windows applications with three modes: 'launch' (opens the prescibed application), 'resize' (adjusts the size/position of a named window or the active window if name is omitted), 'switch' (brings specific window into focus)." | |
| "description": "Manages Windows applications with three modes: 'launch' (opens the prescribed application), 'resize' (adjusts the size/position of a named window or the active window if name is omitted), 'switch' (brings specific window into focus)." |
|
|
||
| def resize_app( | ||
| self, size: tuple[int, int] = None, loc: tuple[int, int] = None | ||
| self, name: str | None = None, size: tuple[int, int] = None, loc: tuple[int, int] = None |
There was a problem hiding this comment.
resize_app parameters size/loc default to None but are annotated as non-optional tuple[int, int]. Consider updating them to tuple[int, int] | None (consistent with app()'s loc/size annotations) to keep type hints accurate.
| self, name: str | None = None, size: tuple[int, int] = None, loc: tuple[int, int] = None | |
| self, name: str | None = None, size: tuple[int, int] | None = None, loc: tuple[int, int] | None = None |
| windows = {window.name: window for window in window_list} | ||
| matched_window = process.extractOne(name, list(windows.keys()), score_cutoff=70) | ||
| if matched_window is None: | ||
| return None, f"Application {name.title()} not found." | ||
| window_name, _ = matched_window | ||
| return windows.get(window_name), "" | ||
|
|
There was a problem hiding this comment.
_find_window_by_name builds a {window.name: window} dict, so multiple windows with the same title will collide and one will be silently dropped. With resize_app(name=...) this can resize the wrong window in common cases (e.g., multiple Explorer windows titled "Downloads"). Consider matching against a list of windows and, when there are duplicate titles, either (a) disambiguate using additional attributes (handle/process id/class) or (b) detect duplicates for the best match and return an error asking for a more specific name.
| windows = {window.name: window for window in window_list} | |
| matched_window = process.extractOne(name, list(windows.keys()), score_cutoff=70) | |
| if matched_window is None: | |
| return None, f"Application {name.title()} not found." | |
| window_name, _ = matched_window | |
| return windows.get(window_name), "" | |
| # Map window names to lists of windows to avoid silently dropping duplicates | |
| name_to_windows: dict[str, list[Window]] = {} | |
| for window in window_list: | |
| name_to_windows.setdefault(window.name, []).append(window) | |
| matched_window = process.extractOne(name, list(name_to_windows.keys()), score_cutoff=70) | |
| if matched_window is None: | |
| return None, f"Application {name.title()} not found." | |
| window_name, _ = matched_window | |
| candidates = name_to_windows.get(window_name, []) | |
| if not candidates: | |
| # Should not normally happen, but handle defensively | |
| return None, f"Application {name.title()} not found." | |
| if len(candidates) > 1: | |
| # Multiple windows share this title; avoid choosing one arbitrarily | |
| return ( | |
| None, | |
| f"Multiple windows named '{window_name}' were found. " | |
| "Please make the desired window active and try again, or close other windows with the same title.", | |
| ) | |
| return candidates[0], "" |
| elif active_window.status == Status.MAXIMIZED: | ||
| return f"{active_window.name} is maximized", 1 | ||
| if name is not None: | ||
| target_window, error = self._find_window_by_name(name, refresh_state=True) |
There was a problem hiding this comment.
resize_app(name=...) forces refresh_state=True, which calls get_state() and triggers a full desktop capture (including tree.get_state(...)). This can make frequent resize operations unexpectedly expensive/slow. Consider adding a lightweight refresh path for window lookup (e.g., re-enumerate windows/active window without rebuilding the full DesktopState/tree) and have _find_window_by_name use that when only window handles/titles are needed.
| target_window, error = self._find_window_by_name(name, refresh_state=True) | |
| target_window, error = self._find_window_by_name(name) |
| reader = csv.DictReader(io.StringIO(response)) | ||
| return "".join([row.get("DisplayName") for row in reader]) | ||
|
|
||
| def _find_window_by_name(self, name: str, refresh_state: bool = False) -> tuple["Window | None", str]: |
There was a problem hiding this comment.
_find_window_by_name annotates its return type as tuple["Window | None", str] even though Window is imported in this module. Using tuple[Window | None, str] (or tuple[Window | None, str] with postponed evaluation) would be clearer and avoids an unusual forward-ref expression string in type hints.
| def _find_window_by_name(self, name: str, refresh_state: bool = False) -> tuple["Window | None", str]: | |
| def _find_window_by_name(self, name: str, refresh_state: bool = False) -> tuple[Window | None, str]: |
|
Thanks and well done Bro, can we connect just to talk. |
|
Sure, I will mail you after work, haha. |
Problem
Currently the
resizemode of theApptool only operates on the active window and thenameparameter is ignored.This leads to a subtle bug in practice. When an AI agent (e.g., Claude) is asked to resize a non-active window, it usually first calls
switchto bring the target window to the foreground, then callsresize. However, the resize still operates on the window that was active at snapshot time — before the switch happened. So the agent complains that it is always resizing the wrong window and tries rolling back to the pwsh command.video1.mp4
Solution
nameparameter support toresize_app: Whennameis provided, the tool fuzzy-matches and targets the specified window directly; when omitted, it falls back to the active window, preserving full backward compatibility._find_window_by_namehelper: Bothswitch_appandresize_appneed to locate a window by fuzzy name match. This duplicated logic is refactored into a single reusable method.__main__.pyandmanifest.jsonso the agent understandsnameis effective in resize mode and uses it correctly.With these changes, when Claude is asked to resize a non-active window, it will pass the window name directly to the
resizecall rather than issuingswitchfirst, resulting in correct and reliable behavior.Changes
src/windows_mcp/desktop/service.py_find_window_by_name(name, refresh_state)method encapsulating window list retrieval and fuzzy matchingresize_appnow accepts an optionalnameparameter and delegates to_find_window_by_namewhen providedswitch_apprefactored to use_find_window_by_name, removing duplicated codesrc/windows_mcp/__main__.py— UpdatedApptool description to reflect thatnameworks in resize modemanifest.json— Updated tool description to matchThe demo after modification
video2.mp4