Skip to content

fix: close a PuTTY tab in one step, without PuTTY's own prompt - #158

Open
jafin wants to merge 1 commit into
robertpopa22:mainfrom
jafin:fix/putty-tab-close-double-prompt
Open

fix: close a PuTTY tab in one step, without PuTTY's own prompt#158
jafin wants to merge 1 commit into
robertpopa22:mainfrom
jafin:fix/putty-tab-close-double-prompt

Conversation

@jafin

@jafin jafin commented Aug 6, 2026

Copy link
Copy Markdown

Closing a PuTTY session from the tab's X asked the user to confirm twice, and the tab stayed behind afterwards.

The double prompt

TryClosePuttyGracefully posts WM_CLOSE, and PuTTY answers that with its own warn-on-close box ("Are you sure you want to close this session?") — on top of the confirmation mRemoteNG had just shown. The wait for the process to exit now polls every 50 ms (same 1 s budget as before) and acknowledges that box on the user's behalf.

The match is deliberately narrow — owning process, dialog class #32770, and an "Exit Confirmation" title — so PuTTY's host key Security Alert and the settings dialog are never auto-answered. That predicate is extracted as PuttyBase.IsPuttyExitConfirmation and covered by tests.

Side effect: PuTTY now normally exits on its own instead of being killed after the 1 s timeout.

The tab that would not close

KeepTabsOpenAfterDisconnect (on by default) cancelled the form close on every close path, so an explicitly closed tab stayed put showing the reconnect panel. That option now applies only to a disconnect request:

HandleProtocolClosed also no longer revives a tab that is already disposing.

Tests

New ConnectionTabCloseTests drives ConnectionTab.OnFormClosing with a stub protocol (headless — confirmation setting forced to Never) across all three combinations of close path and setting, plus 6 cases for IsPuttyExitConfirmation in PuttyBaseTests.

Full suite: 6379/6381. The 2 remaining failures are one pre-existing environment-dependent test (StartupConnectionPathReturnsSavedPathWhenItIsTheSoleCandidate, counted in two groups) that fails identically on a clean tree — a confCons.xml next to the test DLL wins over the temp path the test sets.

Closing a PuTTY session from the tab's X asked the user to confirm twice
and then left the tab behind:

- mRemoteNG posts WM_CLOSE to the PuTTY window, which answers with its own
  warn-on-close box ("Are you sure you want to close this session?") on top
  of the confirmation mRemoteNG had already shown. The graceful close now
  polls while waiting for the process to exit and acknowledges that box on
  the user's behalf. The match is deliberately narrow - owning process,
  dialog class #32770 and an "Exit Confirmation" title - so the host key
  Security Alert and the settings dialog stay under user control.

- KeepTabsOpenAfterDisconnect (on by default) cancelled the form close for
  every close path, so an explicitly closed tab stayed in place showing the
  reconnect panel. That option now applies only to a disconnect request:
  the tab context menu's Disconnect entry sets the new disconnectOnly flag,
  while the tab's X and Ctrl+W always take the tab down. HandleProtocolClosed
  no longer revives a tab that is already disposing.
Copilot AI lite review requested due to automatic review settings August 6, 2026 01:26
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix PuTTY tab close double-prompt and ensure tab closes on explicit close

🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Auto-dismiss PuTTY's warn-on-close dialog during graceful shutdown to avoid double prompts.
• Apply KeepTabsOpenAfterDisconnect only to explicit disconnects; tab X/Ctrl+W now closes the tab.
• Add targeted unit tests for PuTTY exit-confirmation detection and tab-close/disconnect behavior.
Diagram

graph TD
  UIX["Tab X / Ctrl+W"] --> CT["ConnectionTab.OnFormClosing"] --> DEC{"Keep tab open?"} -->|"no"| CLOSED["Tab removed"]
  MENU["Menu: Disconnect"] --> CT
  DEC -->|"yes (disconnectOnly + option)"| HPC["HandleProtocolClosed"]
  CT --> PB["PuttyBase graceful close"] --> DLG(("PuTTY Exit Confirmation"))

  subgraph Legend
    direction LR
    _ui["UI / component"] ~~~ _dec{"Decision"} ~~~ _dlg(("Dialog"))
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Disable PuTTY warn-on-close via session settings
  • ➕ No Win32 window scanning/automation logic
  • ➕ Avoids any risk of dismissing the wrong dialog
  • ➖ Not always possible (embedded/external PuTTY sessions, user-managed profiles)
  • ➖ Would change PuTTY behavior globally rather than only for tab-close UX
2. Always force-kill PuTTY after WM_CLOSE
  • ➕ Simpler implementation
  • ➕ Avoids interacting with PuTTY dialogs
  • ➖ Higher risk of data loss (abrupt termination)
  • ➖ Reintroduces prior behavior where PuTTY often gets killed after timeout
3. Hook child window handles from the PuTTY main window only
  • ➕ Potentially tighter dialog matching than EnumWindows
  • ➕ Less chance of scanning unrelated top-level windows
  • ➖ More complex Win32 code (child enumeration, ownership chains)
  • ➖ May miss dialogs depending on PuTTY window parenting/owner semantics

Recommendation: Current approach is a good balance: it preserves graceful shutdown, removes the redundant prompt, and uses a deliberately narrow predicate (process id + dialog class + title substring) to avoid auto-answering security alerts or settings dialogs. Keep the predicate narrow (as implemented) and consider documenting any localization assumptions around the dialog title if non-English PuTTY is supported.

Files changed (6) +247 / -6

Bug fix (4) +106 / -6
NativeMethods.csAdd GetWindowText P/Invoke for dialog title inspection +3/-0

Add GetWindowText P/Invoke for dialog title inspection

• Introduces a user32 GetWindowText import to allow reading window titles when scanning for PuTTY dialogs during shutdown.

mRemoteNG/App/NativeMethods.cs

PuttyBase.csAuto-dismiss PuTTY warn-on-close during graceful WM_CLOSE shutdown +82/-1

Auto-dismiss PuTTY warn-on-close during graceful WM_CLOSE shutdown

• Reworks graceful close to poll for up to 1s and, while waiting, finds and acknowledges PuTTY’s exit-confirmation dialog. Extracts a narrowly-scoped IsPuttyExitConfirmation predicate for safe matching and reuse/testing.

mRemoteNG/Connection/Protocol/PuttyBase.cs

ConnectionTab.csDifferentiate disconnect vs tab close with new disconnectOnly flag +17/-4

Differentiate disconnect vs tab close with new disconnectOnly flag

• Adds disconnectOnly and uses it to apply KeepTabsOpenAfterDisconnect only for disconnect requests. Explicit tab close (X/Ctrl+W) now closes the tab even when the option is enabled.

mRemoteNG/UI/Tabs/ConnectionTab.cs

ConnectionWindow.csMark Disconnect menu closes as disconnect-only and avoid reviving disposing tabs +4/-1

Mark Disconnect menu closes as disconnect-only and avoid reviving disposing tabs

• Sets selectedTab.disconnectOnly when closing via the Disconnect menu path. Prevents HandleProtocolClosed from re-showing the closed-state panel for tabs already disposing.

mRemoteNG/UI/Window/ConnectionWindow.cs

Tests (2) +141 / -0
PuttyBaseTests.csAdd unit coverage for PuTTY exit-confirmation detection predicate +16/-0

Add unit coverage for PuTTY exit-confirmation detection predicate

• Adds positive/negative test cases to ensure only PuTTY’s exit-confirmation dialog matches, while security alerts, settings dialogs, and terminal windows do not.

mRemoteNGTests/Connection/Protocol/PuttyBaseTests.cs

ConnectionTabCloseTests.csAdd headless tests for tab close vs disconnect behavior under KeepTabsOpenAfterDisconnect +125/-0

Add headless tests for tab close vs disconnect behavior under KeepTabsOpenAfterDisconnect

• Introduces STA, non-parallel tests that invoke ConnectionTab.OnFormClosing via reflection using a stub protocol. Validates correct close-cancel behavior across close path and KeepTabsOpenAfterDisconnect settings.

mRemoteNGTests/UI/Tabs/ConnectionTabCloseTests.cs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR improves tab/session closing behavior by (1) auto-dismissing PuTTY’s warn-on-close dialog after mRemoteNG has already confirmed the disconnect, and (2) ensuring “Keep tabs open after disconnect” only preserves tabs for explicit disconnect actions (not tab close).

Changes:

  • Update PuTTY graceful-close logic to detect and acknowledge PuTTY’s “Exit Confirmation” dialog during shutdown polling.
  • Adjust tab close vs. disconnect behavior via a new disconnectOnly flag and prevent closed-state UI from reviving a tab that’s already closing.
  • Add STA, headless unit tests covering tab close/disconnect permutations and PuttyBase.IsPuttyExitConfirmation.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
mRemoteNGTests/UI/Tabs/ConnectionTabCloseTests.cs Adds tests validating tab-close vs disconnect-only behavior under KeepTabsOpenAfterDisconnect.
mRemoteNGTests/Connection/Protocol/PuttyBaseTests.cs Adds tests for the “Exit Confirmation” window predicate used to auto-dismiss PuTTY’s warn-on-close box.
mRemoteNG/UI/Window/ConnectionWindow.cs Marks Disconnect-menu closes as disconnect-only and avoids reviving already-closing tabs with the closed-state panel.
mRemoteNG/UI/Tabs/ConnectionTab.cs Applies KeepTabsOpenAfterDisconnect only for disconnect-only closes.
mRemoteNG/Connection/Protocol/PuttyBase.cs Polls for graceful exit and auto-acknowledges PuTTY’s warn-on-close confirmation dialog.
mRemoteNG/App/NativeMethods.cs Adds P/Invoke for GetWindowText used by the PuTTY dialog detection.

Comment on lines +1845 to 1846
selectedTab.disconnectOnly = true;
selectedTab.Close();
@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (2) 📜 Skill insights (0)

Grey Divider


Action required

1. disconnectOnly never cleared 🐞 Bug ≡ Correctness
Description
ConnectionTab.disconnectOnly is set on the Disconnect menu path but is never reset, so when the same
tab is later reused for reconnect, closing the tab via X/Ctrl+W can still be treated as
“disconnect-only” and the close gets cancelled again when KeepTabsOpenAfterDisconnect is enabled.
Code

mRemoteNG/UI/Window/ConnectionWindow.cs[R1845-1846]

+                selectedTab.disconnectOnly = true;
                selectedTab.Close();
Evidence
The PR introduces disconnectOnly and uses it to decide whether to cancel a form close, but the
flag is set on the Disconnect path and never cleared. Since the codebase reuses closed tabs for
reconnection without resetting the flag, a later normal tab close can still be cancelled
incorrectly.

mRemoteNG/UI/Window/ConnectionWindow.cs[1819-1847]
mRemoteNG/UI/Tabs/ConnectionTab.cs[198-246]
mRemoteNG/UI/Tabs/ConnectionTab.cs[258-265]
mRemoteNG/UI/Window/ConnectionWindow.cs[858-876]
mRemoteNG/Connection/ConnectionInitiator.cs[498-502]
mRemoteNG/UI/Tabs/ConnectionTab.cs[152-163]

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

## Issue description
`disconnectOnly` is a persistent per-tab flag that is set to `true` when the user chooses **Disconnect** from the tab context menu. The PR uses this flag to decide whether to cancel `OnFormClosing` (keeping the tab open with the reconnect panel).

However, the flag is never reset to `false`. Because tabs can be *reused* for reconnect, the stale `disconnectOnly=true` can leak into subsequent sessions in the same tab and cause normal tab closes (X/Ctrl+W) to be cancelled again.

## Issue Context
- Disconnect menu sets `disconnectOnly=true` and calls `Close()`.
- `ConnectionTab.OnFormClosing` uses `disconnectOnly && KeepTabsOpenAfterDisconnect` to decide whether to cancel the close.
- Reconnect/open paths reuse closed tabs (`GetOrAddConnectionTab`), but do not clear `disconnectOnly`.

## Fix Focus Areas
- mRemoteNG/UI/Tabs/ConnectionTab.cs[198-246]
- mRemoteNG/UI/Tabs/ConnectionTab.cs[258-265]
- mRemoteNG/UI/Window/ConnectionWindow.cs[858-876]

### Concrete fix approach (one acceptable option)
1. Make `disconnectOnly` a **one-shot** flag:
  - In `ConnectionTab.OnFormClosing`, capture `bool keepOpen = KeepTabOpenAfterDisconnect;` early.
  - Immediately reset `disconnectOnly = false;` once a disconnect-only close attempt is being handled.
  - Use the captured `keepOpen` to set `e.Cancel`.

2. Additionally (defensive), when reusing a closed tab in `ConnectionWindow.GetOrAddConnectionTab`, clear any stale close flags:
  - `reusableTab.disconnectOnly = false;`
  - (Optional) also clear `silentClose`/`protocolClose` if those can persist across reuse.

3. Add/extend a test:
  - Disconnect via menu (sets `disconnectOnly`), then simulate tab reuse for reconnect, then close via X and assert `Cancel == false` even when KeepTabsOpenAfterDisconnect is enabled.

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



Remediation recommended

2. disconnectOnly property not PascalCase 📘 Rule violation ⚙ Maintainability
Description
A new public property disconnectOnly is introduced with a non-PascalCase name. This violates the
required naming convention for properties and reduces consistency/readability across the API
surface.
Code

mRemoteNG/UI/Tabs/ConnectionTab.cs[38]

+        public bool disconnectOnly { get; set; }
Evidence
PR Compliance ID 1562182 requires PascalCase for property names. The PR adds a new public property
named disconnectOnly, which starts with a lowercase letter and therefore violates the rule.

Rule 1562182: Property names must use PascalCase
mRemoteNG/UI/Tabs/ConnectionTab.cs[33-40]
mRemoteNG/UI/Window/ConnectionWindow.cs[1843-1847]

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

## Issue description
A new public property `disconnectOnly` was added using camelCase, but property names must be PascalCase.

## Issue Context
This property is referenced from production code and tests, so renaming requires updating all call sites.

## Fix Focus Areas
- mRemoteNG/UI/Tabs/ConnectionTab.cs[33-39]
- mRemoteNG/UI/Window/ConnectionWindow.cs[1845-1847]
- mRemoteNGTests/UI/Tabs/ConnectionTabCloseTests.cs[72-96]

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


3. Test names missing underscores 📘 Rule violation ▣ Testability
Description
New/modified test methods do not follow the required MethodName_Scenario_ExpectedBehavior naming
pattern. This reduces consistency and makes tests harder to scan/search by convention.
Code

mRemoteNGTests/UI/Tabs/ConnectionTabCloseTests.cs[60]

+        public void ClosingTheTabClosesItEvenWhenTabsAreKeptOpenAfterDisconnect()
Evidence
PR Compliance ID 1563791 requires test method names to be exactly three underscore-separated
segments. The PR introduces/changes test method names like
ClosingTheTabClosesItEvenWhenTabsAreKeptOpenAfterDisconnect (no underscores) and
IsPuttyExitConfirmation_MatchesTheWarnOnCloseMessageBox (only two segments).

Rule 1563791: Test method names must follow MethodName_Scenario_ExpectedBehavior pattern
mRemoteNGTests/UI/Tabs/ConnectionTabCloseTests.cs[59-96]
mRemoteNGTests/Connection/Protocol/PuttyBaseTests.cs[129-143]

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

## Issue description
Several newly added/modified NUnit test methods do not follow the `MethodName_Scenario_ExpectedBehavior` naming convention (exactly 3 segments separated by 2 underscores).

## Issue Context
This convention is required for all test methods in the changed files.

## Fix Focus Areas
- mRemoteNGTests/UI/Tabs/ConnectionTabCloseTests.cs[59-96]
- mRemoteNGTests/Connection/Protocol/PuttyBaseTests.cs[129-143]

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


Grey Divider

Context used
✅ Compliance rules (platform): 52 rules

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

/// showing the reconnect panel when KeepTabsOpenAfterDisconnect is enabled.
/// Closing the tab itself always removes the tab, regardless of that option.
/// </summary>
public bool disconnectOnly { get; set; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. disconnectonly property not pascalcase 📘 Rule violation ⚙ Maintainability

A new public property disconnectOnly is introduced with a non-PascalCase name. This violates the
required naming convention for properties and reduces consistency/readability across the API
surface.
Agent Prompt
## Issue description
A new public property `disconnectOnly` was added using camelCase, but property names must be PascalCase.

## Issue Context
This property is referenced from production code and tests, so renaming requires updating all call sites.

## Fix Focus Areas
- mRemoteNG/UI/Tabs/ConnectionTab.cs[33-39]
- mRemoteNG/UI/Window/ConnectionWindow.cs[1845-1847]
- mRemoteNGTests/UI/Tabs/ConnectionTabCloseTests.cs[72-96]

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

}

[Test]
public void ClosingTheTabClosesItEvenWhenTabsAreKeptOpenAfterDisconnect()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Test names missing underscores 📘 Rule violation ▣ Testability

New/modified test methods do not follow the required MethodName_Scenario_ExpectedBehavior naming
pattern. This reduces consistency and makes tests harder to scan/search by convention.
Agent Prompt
## Issue description
Several newly added/modified NUnit test methods do not follow the `MethodName_Scenario_ExpectedBehavior` naming convention (exactly 3 segments separated by 2 underscores).

## Issue Context
This convention is required for all test methods in the changed files.

## Fix Focus Areas
- mRemoteNGTests/UI/Tabs/ConnectionTabCloseTests.cs[59-96]
- mRemoteNGTests/Connection/Protocol/PuttyBaseTests.cs[129-143]

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

Comment on lines +1845 to 1846
selectedTab.disconnectOnly = true;
selectedTab.Close();

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

3. Disconnectonly never cleared 🐞 Bug ≡ Correctness

ConnectionTab.disconnectOnly is set on the Disconnect menu path but is never reset, so when the same
tab is later reused for reconnect, closing the tab via X/Ctrl+W can still be treated as
“disconnect-only” and the close gets cancelled again when KeepTabsOpenAfterDisconnect is enabled.
Agent Prompt
## Issue description
`disconnectOnly` is a persistent per-tab flag that is set to `true` when the user chooses **Disconnect** from the tab context menu. The PR uses this flag to decide whether to cancel `OnFormClosing` (keeping the tab open with the reconnect panel).

However, the flag is never reset to `false`. Because tabs can be *reused* for reconnect, the stale `disconnectOnly=true` can leak into subsequent sessions in the same tab and cause normal tab closes (X/Ctrl+W) to be cancelled again.

## Issue Context
- Disconnect menu sets `disconnectOnly=true` and calls `Close()`.
- `ConnectionTab.OnFormClosing` uses `disconnectOnly && KeepTabsOpenAfterDisconnect` to decide whether to cancel the close.
- Reconnect/open paths reuse closed tabs (`GetOrAddConnectionTab`), but do not clear `disconnectOnly`.

## Fix Focus Areas
- mRemoteNG/UI/Tabs/ConnectionTab.cs[198-246]
- mRemoteNG/UI/Tabs/ConnectionTab.cs[258-265]
- mRemoteNG/UI/Window/ConnectionWindow.cs[858-876]

### Concrete fix approach (one acceptable option)
1. Make `disconnectOnly` a **one-shot** flag:
   - In `ConnectionTab.OnFormClosing`, capture `bool keepOpen = KeepTabOpenAfterDisconnect;` early.
   - Immediately reset `disconnectOnly = false;` once a disconnect-only close attempt is being handled.
   - Use the captured `keepOpen` to set `e.Cancel`.

2. Additionally (defensive), when reusing a closed tab in `ConnectionWindow.GetOrAddConnectionTab`, clear any stale close flags:
   - `reusableTab.disconnectOnly = false;`
   - (Optional) also clear `silentClose`/`protocolClose` if those can persist across reuse.

3. Add/extend a test:
   - Disconnect via menu (sets `disconnectOnly`), then simulate tab reuse for reconnect, then close via X and assert `Cancel == false` even when KeepTabsOpenAfterDisconnect is enabled.

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

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