Skip to content

Add --no-activate flag: launch the Windows console without stealing focus - #56

Open
NoiZzz3R wants to merge 1 commit into
yotsuda:mainfrom
NoiZzz3R:feature/no-activate-flag
Open

Add --no-activate flag: launch the Windows console without stealing focus#56
NoiZzz3R wants to merge 1 commit into
yotsuda:mainfrom
NoiZzz3R:feature/no-activate-flag

Conversation

@NoiZzz3R

@NoiZzz3R NoiZzz3R commented Aug 31, 2026

Copy link
Copy Markdown

Summary

The Windows launcher calls CreateProcessW with CREATE_NEW_CONSOLE and a STARTUPINFOW whose dwFlags / wShowWindow are never set. Windows then grants the new console foreground privilege, so it takes focus the moment it appears.

For a short-lived console that is the right behaviour — you asked the AI to run something and you want to see it. For a long-lived shared console it is not: the console is spawned once, but the user keeps working in other applications while the AI drives it, and the window pulls focus mid-keystroke.

This adds an opt-in --no-activate proxy flag that creates the console with STARTF_USESHOWWINDOW + SW_SHOWNOACTIVATE.

Why not ForegroundLockTimeout

Worth recording, because it looks like the obvious fix and is not. I measured it first:

HKCU\Control Panel\Desktop live SPI_GETFOREGROUNDLOCKTIMEOUT
before 0 2147483647

The registry value was stale; the lock was already at maximum. It still did not help — that lock only blocks explicit SetForegroundWindow calls from background processes, not the foreground privilege granted at process creation. The fix has to be in STARTUPINFOW.

Switching the default terminal from Windows Terminal to conhost does not help either. It does give the console its own top-level window instead of a tab (pwsh.MainWindowHandle goes from 0 to a real HWND, which is a nice side effect), but the newly created window still takes focus.

Design

Deliberately shaped after --no-profile (#49): off by default, so nothing changes for anyone who does not ask for it. I read your reasoning there — that these are real human-facing shells and defaults should respect that — and it applies here too. A console that appears in front is usually what you want; this is for the case where it is not.

The window still appears and stays fully visible and interactive. Read-Host, Get-Credential and the elevation confirmation prompt all keep working — SW_SHOWNOACTIVATE only withholds activation, it does not hide anything. That mattered to me specifically because of the elevation interceptor: hiding the window would deadlock it, whereas not activating the window does not.

Windows only. On macOS/Linux the terminal emulator owns activation, so the flag is a no-op there — same asymmetry --no-profile already has between its interactive and headless paths.

The flag mapping is extracted into PwshLauncherShared.BuildWindowsShowWindow so it is unit-testable without spawning a console. Since wShowWindow is ignored unless STARTF_USESHOWWINDOW is present, the default path leaves both fields zeroed and is bit-for-bit the previous behaviour.

Validation

E2E harness calling the real PwshLauncherWindows.LaunchPwsh twice, measuring GetForegroundWindow() before and after each spawn:

--- noActivate=False ---
BEFORE fg=0x20518   AFTER fg=0x160C9E   stoleFocus=True  expected=True  => PASS
--- noActivate=True ---
BEFORE fg=0x20518   AFTER fg=0x20518    stoleFocus=False expected=False => PASS
  • dotnet build PowerShell.MCP.Proxy -c Release — 0 errors
  • dotnet test Tests/PowerShell.MCP.Tests.csproj -c Release -f net9.0 — 406 passed, 1 failed

The one failure is PipelineHelperTests.FormatBusyStatus_NullStatusLine_BuildsFromParameters. It reproduces on an unmodified checkout of main (verified via git stash), so it is pre-existing and unrelated to this change.

Environment: Windows 11 26100, PowerShell 7.5.5, .NET SDK 9.0.317.

Usage

claude mcp add pwsh -s user -- "$(Get-MCPProxyPath)" --no-activate

README updated with a Background consoles section alongside the existing --no-profile docs.

Summary by CodeRabbit

  • New Features

    • Added an optional --no-activate setting to prevent the Windows PowerShell console from taking focus when launched.
    • The option is available for Windows configurations and has no effect on macOS or Linux.
  • Documentation

    • Added setup examples and platform-specific behavior details to the README.
  • Tests

    • Added coverage verifying default behavior and the no-activation option.

@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e503170a-91db-4109-a6b5-b94036def84f

📥 Commits

Reviewing files that changed from the base of the PR and between 5e62797 and 4a2cae1.

📒 Files selected for processing (4)
  • PowerShell.MCP.Proxy/Program.cs
  • PowerShell.MCP.Proxy/Services/PowerShellProcessManager.cs
  • README.md
  • Tests/Unit/Proxy/PwshLauncherSharedTests.cs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Changes

No-activate launch option

Layer / File(s) Summary
Option contract and validation
PowerShell.MCP.Proxy/Services/PowerShellProcessManager.cs, PowerShell.MCP.Proxy/Program.cs, Tests/Unit/Proxy/PwshLauncherSharedTests.cs
The proxy parses case-insensitive --no-activate. BuildWindowsShowWindow returns the documented Win32 values when enabled and zero values by default. Tests cover both paths and the constants.
Windows startup integration
PowerShell.MCP.Proxy/Services/PowerShellProcessManager.cs, README.md
The Windows launcher applies the show-window values to STARTUPINFOW before CreateProcessW. The README documents the option, examples, platform scope, and combination with --no-profile.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 4a2ca

The optional flag prevents launched Windows consoles from taking focus while preserving existing behavior by default. No actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: yotsuda

Sequence Diagram(s)

sequenceDiagram
  participant ProgramMain as Program.Main
  participant PwshLauncherShared
  participant PwshLauncherWindows
  participant CreateProcessW
  ProgramMain->>PwshLauncherShared: Set SuppressWindowActivation
  PwshLauncherWindows->>PwshLauncherShared: BuildWindowsShowWindow
  PwshLauncherShared-->>PwshLauncherWindows: Return startup flags
  PwshLauncherWindows->>CreateProcessW: Pass STARTUPINFOW
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 3 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding the --no-activate flag to prevent the Windows console from stealing focus.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 3 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

… focus

The Windows launcher calls CreateProcessW with CREATE_NEW_CONSOLE and a
STARTUPINFOW whose dwFlags/wShowWindow are never set. Windows then grants the
new console foreground privilege, so it takes focus the moment it appears.

For a short-lived console that is the right behaviour: you asked the AI to run
something and you want to see it. For a long-lived shared console it is not --
the console is spawned once, but the user keeps working in other applications
while the AI drives it, and the window pulls focus mid-keystroke.

ForegroundLockTimeout does not help here: it only blocks explicit
SetForegroundWindow calls from background processes, not the foreground
privilege granted at process creation. The fix has to be in STARTUPINFOW.

This adds an opt-in `--no-activate` proxy flag, mirroring the shape of
`--no-profile` (yotsuda#49): off by default, so nothing changes unless the operator
asks for it. When set, the console is created with STARTF_USESHOWWINDOW +
SW_SHOWNOACTIVATE. The window still appears and stays fully interactive --
Read-Host, Get-Credential and the elevation prompt all keep working -- it
simply does not take focus. Windows only; on macOS/Linux the terminal emulator
owns activation, so the flag is a no-op there.

The flag mapping is extracted into PwshLauncherShared.BuildWindowsShowWindow so
it can be unit-tested without spawning a console. Because wShowWindow is ignored
unless STARTF_USESHOWWINDOW is present, the default path leaves both fields
zeroed and is bit-for-bit the previous behaviour.

Verified on Windows 11 / PowerShell 7.5.5 with a harness calling the real
PwshLauncherWindows.LaunchPwsh twice:

  noActivate=false -> foreground changed  (steals focus, as before)
  noActivate=true  -> foreground unchanged (does not steal focus)

dotnet build: 0 errors. Unit tests: 406 passed. One pre-existing failure in
PipelineHelperTests.FormatBusyStatus_NullStatusLine_BuildsFromParameters, which
reproduces on an unmodified checkout of main and is unrelated to this change.
@yotsuda

yotsuda commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Thanks — nicely scoped, and the ForegroundLockTimeout write-up saved me the same detour. Merged to a local branch (nothing pushed yet) and ran it on Windows 11 26200 / PowerShell 7.6.5: 407/407 with -f net9.0. Your one failure is a locale artifact of mine — FormatBusyStatus formats {duration:F2} in the current culture while the test asserts 5.50s — I'll fix that separately.

The focus behaviour does not reproduce here, though.

20:17:16.483  0xE90D0A   #43920 Daisy   <- foreground before the spawn
20:17:19.443             (new console's pwsh starts)
20:17:20.427  0x40D109E  #25624 Lotus   <- new console takes it, holds it to 20:17:50

The flag was in effect: pwsh 25624's parent is 8236, whose command line is PowerShell.MCP.Proxy.exe --no-activate. I sampled GetForegroundWindow() every 100ms for 30s instead of reading it once either side — my first attempt showed no steal and was just me clicking back a moment later. And nothing in the tree calls SetForegroundWindow, so it isn't the module re-activating the window afterwards.

It looks like console handoff. My default terminal is "Let Windows decide" (DelegationConsole and DelegationTerminal are both the zero GUID), and every console window here is a top-level window owned by WindowsTerminal.exe — created by Windows Terminal, not by your CreateProcessW call, so STARTUPINFOW.wShowWindow never reaches it.

Which default terminal was active when your harness printed stoleFocus=False? You mention switching Windows Terminal to conhost while investigating, but not which one the passing run used. If it was conhost, the flag works as you designed it and the only open question is the README, which currently reads as applying to Windows generally.

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.

2 participants