diff --git a/doc/release-check-list.md b/doc/release-check-list.md index 688cb9ab8b..a2d36581d2 100644 --- a/doc/release-check-list.md +++ b/doc/release-check-list.md @@ -167,6 +167,7 @@ Net effect: UT shrinks the manual matrix to "did the wiring and UI connect", not - [ ] `C263` `[new]` `[UT✓]` `[E2E]` **Keyboard selection keeps focused completed turns visible:** Tab and Up/Down navigation scrolls the chat only as needed to keep the focused completed turn visible, without overriding later manual scrolling. _(UT: `render_chat_keeps_keyboard_selected_completed_turn_visible`; E2E: `Feature.CompletedTurnSelection`.)_ - [ ] `C264` `[new]` `[UT✓]` `[E2E]` **Completed turns toggle and select from rendered prompt targets:** A left-button Down/Up anywhere on the same visible prompt row, including its triangle, prefix, text, explicit blank row, or unused row-end space, collapses or expands only that turn and reuses the keyboard selection, highlight, navigation, Esc, and Enter behavior. Hover uses only a hand cursor: action metadata must not add hyperlink underline or tooltip text. Details, separators, drags, overlays, tab changes, scrolling, and stale/offscreen rows do not toggle; clicking the live input dialog clears the history selection and restores draft input. Multiline, wrapped, wide-cell, and double/triple-click paths preserve their expected geometry and text-selection behavior. _(UT: `clicking_multiline_completed_turn_prompt_selects_and_reuses_enter_toggle`, `completed_turn_user_input_hit_spans_full_row_with_wide_cells`, `completed_turn_prompt_rows_expose_state_aware_action_links`, `overlay_preserves_full_rows_and_cell_styles`, `CompletedTurnActionHyperlinksSuppressUnderlines`, `clicking_input_dialog_restores_input_navigation_after_mouse_turn_selection`, `completed_turn_user_input_multi_click_preserves_turn_state_and_text_selection`, `completed_turn_mouse_selection_continues_with_keyboard_navigation`, `completed_turn_triangle_click_ignores_text_drag_and_hidden_chat`, `completed_turn_triangle_hits_follow_visible_scrolled_turns`, `completed_turn_prompt_hits_survive_a_clipped_header_row`; E2E: `Feature.AgentMouse` `CompletedTurnMouse` / `CompletedTurnPromptMouse`; hand cursor and absence of action underline manually verified on the exact deployed Dev build.)_ - [ ] `C242` `[new]` `[UT✓]` `[E2E]` **Mouse wheel scrolls chat without changing the draft:** Wheel input scrolls the chat viewport while Up/Down remain prompt-history controls and the unsent draft stays intact. _(UT: `mouse_wheel_scrolls_chat_without_changing_input_history`; #506; E2E: `Feature.AgentMouse`.)_ +- [ ] `C295` `[new]` `[UT✓]` `[E2E]` **Ctrl+wheel zooms the agent pane while plain wheel scrolls chat:** Physical Ctrl+wheel changes the agent pane font size before VT mouse tracking can consume the event, while physical plain wheel still scrolls chat and both paths preserve the unsent draft. _(UT: `AgentPaneCtrlWheelZoomsBeforeVtMouse`; #790; E2E: `Feature.AgentMouse`.)_ - [ ] `C243` `[new]` `[UT✓]` `[E2E]` **Mouse selection copies text and clears after copy:** Double-click selection survives release, `Ctrl+C` copies the exact selected text with confirmation instead of canceling/closing the pane, and the selection is then cleared. _(UT: `mouse_release_does_not_return_text_for_automatic_copy`, `clearing_selection_resets_multi_click_sequence`; #506; E2E: `Feature.AgentMouse`.)_ - [ ] `C287` `[new]` `[UT✓]` `[E2E]` **Ctrl+A selects and copies the current agent frame:** Plain `Ctrl+A` selects the current rendered WTA frame through the existing text-selection path; `Ctrl+C` copies it with the existing confirmation and clears selection so a later `Ctrl+C` cannot replay stale text. _(UT: `select_all_extracts_and_highlights_the_current_frame`, `select_all_tracks_the_latest_frame_snapshot`, `ctrl_a_selects_current_rendered_frame_without_altering_input`; E2E: `Feature.AgentSelectAll`.)_ - [ ] `C265` `[new]` `[UT✓]` `[E2E]` **Right-click copies the agent text selection:** A physical right-click copies the exact WTA-owned selection through the OS clipboard, clears it, shows the existing copied confirmation, and never also pastes; the next right-click follows the no-selection Default Paste path without replaying selected text. _(UT: `right_click_copy_event_is_forwarded`, `right_click_copies_and_clears_text_selection`; E2E: `Feature.AgentMouse` `RightClickCopy`.)_ @@ -465,4 +466,4 @@ Net effect: UT shrinks the manual matrix to "did the wiring and UI connect", not - Slash commands: `tools\wta\src\commands.rs`. - Session state model: `tools\wta\src\agent_sessions.rs`, `tools\wta\AGENTS.md`. - Multi-window agent pane architecture: `doc\specs\Multi-window-agent-pane.md`. -- Autofix flow, logging, and runtime layout: `AGENTS.md`. +- Autofix flow, logging, and runtime layout: `AGENTS.md`. diff --git a/src/cascadia/TerminalApp/AgentPaneContent.cpp b/src/cascadia/TerminalApp/AgentPaneContent.cpp index 5a0801d82d..0246ca7b0b 100644 --- a/src/cascadia/TerminalApp/AgentPaneContent.cpp +++ b/src/cascadia/TerminalApp/AgentPaneContent.cpp @@ -419,6 +419,7 @@ namespace winrt::TerminalApp::implementation { impl->UpdateSettings(settings); } + GetTermControl().EnableAgentMouseWheelZoom(true); const winrt::Microsoft::Terminal::Control::KeyChord ctrlV{ Windows::System::VirtualKeyModifiers::Control, 'V', 0 }; if (const auto actionMap = settings.ActionMap()) diff --git a/src/cascadia/TerminalControl/ControlInteractivity.cpp b/src/cascadia/TerminalControl/ControlInteractivity.cpp index 2e3f519d63..ea50013c4a 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.cpp +++ b/src/cascadia/TerminalControl/ControlInteractivity.cpp @@ -528,8 +528,29 @@ namespace winrt::Microsoft::Terminal::Control::implementation const Core::Point delta, const Core::Point pixelPosition, const Control::MouseButtonState buttonState) + { + return MouseWheel(modifiers, delta, pixelPosition, buttonState, false); + } + + bool ControlInteractivity::MouseWheel(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, + const Core::Point delta, + const Core::Point pixelPosition, + const Control::MouseButtonState buttonState, + const bool prioritizeZoom) { const auto terminalPosition = _getTerminalPosition(til::point{ pixelPosition }, false); + const auto ctrlPressed = modifiers.IsCtrlPressed(); + const auto shiftPressed = modifiers.IsShiftPressed(); + + if (prioritizeZoom && + ctrlPressed && + !shiftPressed && + delta.Y != 0 && + _core->Settings().ScrollToZoom()) + { + _mouseZoomHandler(delta.Y); + return false; + } // Short-circuit isReadOnly check to avoid warning dialog. // @@ -552,9 +573,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation buttonState); } - const auto ctrlPressed = modifiers.IsCtrlPressed(); - const auto shiftPressed = modifiers.IsShiftPressed(); - if (ctrlPressed && shiftPressed && _core->Settings().ScrollToChangeOpacity()) { _mouseTransparencyHandler(delta.Y); diff --git a/src/cascadia/TerminalControl/ControlInteractivity.h b/src/cascadia/TerminalControl/ControlInteractivity.h index 7f80c2f8e5..8b44391566 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.h +++ b/src/cascadia/TerminalControl/ControlInteractivity.h @@ -77,6 +77,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation const Core::Point delta, const Core::Point pixelPosition, const Control::MouseButtonState state); + bool MouseWheel(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, + const Core::Point delta, + const Core::Point pixelPosition, + const Control::MouseButtonState state, + bool prioritizeZoom); void UpdateScrollbar(const float newValue); diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 6f304c07ff..fbaa985667 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -2171,12 +2171,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation const auto point = args.GetCurrentPoint(*this); auto delta = point.Properties().MouseWheelDelta(); - auto result = _interactivity.MouseWheel(ControlKeyStates{ args.KeyModifiers() }, - point.Properties().IsHorizontalMouseWheel() ? - Core::Point{ delta, 0 } : - Core::Point{ 0, delta }, - _toTerminalOrigin(point.Position()), - TermControl::GetPressedMouseButtons(point)); + const auto modifiers = ControlKeyStates{ args.KeyModifiers() }; + const auto wheelDelta = point.Properties().IsHorizontalMouseWheel() ? + Core::Point{ delta, 0 } : + Core::Point{ 0, delta }; + const auto interactivity = winrt::get_self(_interactivity); + const auto result = interactivity->MouseWheel(modifiers, + wheelDelta, + _toTerminalOrigin(point.Position()), + TermControl::GetPressedMouseButtons(point), + _agentMouseWheelZoomEnabled); if (result) { args.Handled(true); @@ -2206,7 +2210,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation WI_SetFlagIf(state, Control::MouseButtonState::IsMiddleButtonDown, midButtonDown); WI_SetFlagIf(state, Control::MouseButtonState::IsRightButtonDown, rightButtonDown); - return _interactivity.MouseWheel(modifiers, delta, _toTerminalOrigin(location), state); + const auto interactivity = winrt::get_self(_interactivity); + return interactivity->MouseWheel(modifiers, delta, _toTerminalOrigin(location), state, _agentMouseWheelZoomEnabled); } // Method Description: diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 3965d3e0e2..b7ccf55e08 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -64,6 +64,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool CopySelectionToClipboard(bool dismissSelection, bool singleLine, bool withControlSequences, const CopyFormat formats); void PasteTextFromClipboard(); void EnableAgentPasteShortcutFallback(bool enabled) noexcept { _agentPasteShortcutFallbackEnabled = enabled; } + void EnableAgentMouseWheelZoom(bool enabled) noexcept { _agentMouseWheelZoomEnabled = enabled; } void SelectAll(); bool ToggleBlockSelection(); void ToggleMarkMode(); @@ -257,6 +258,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation Control::ControlInteractivity _interactivity{ nullptr }; Control::ControlCore _core{ nullptr }; Control::IKeyBindings _keyBindings{ nullptr }; + bool _agentMouseWheelZoomEnabled{ false }; TsfDataProvider _tsfDataProvider{ this }; winrt::com_ptr _searchBox; diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 36c374f314..0c0c27270b 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -170,5 +170,6 @@ namespace Microsoft.Terminal.Control void ClearQuickFix(); void Detach(); + void EnableAgentMouseWheelZoom(Boolean enabled); } } diff --git a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp index 6f08ce37ca..4d8dabe56c 100644 --- a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp +++ b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp @@ -42,6 +42,7 @@ namespace ControlUnitTests TEST_METHOD(IncrementCircularBufferWithSelection); TEST_METHOD(GetMouseEventsInTest); + TEST_METHOD(AgentPaneCtrlWheelZoomsBeforeVtMouse); TEST_METHOD(AltBufferClampMouse); TEST_METHOD(ParseCompletedTurnActionHyperlinks); TEST_METHOD(CompletedTurnActionHyperlinksSuppressUnderlines); @@ -1009,6 +1010,58 @@ namespace ControlUnitTests cursorPosition0.to_core_point()); } + void ControlInteractivityTests::AgentPaneCtrlWheelZoomsBeforeVtMouse() + { + WEX::TestExecution::DisableVerifyExceptions disableVerifyExceptions{}; + + auto [settings, conn] = _createSettingsAndConnection(); + settings->ScrollToZoom(true); + auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn); + _standardInit(core, interactivity); + + std::deque expectedOutput{}; + auto validateDrained = _addInputCallback(conn, expectedOutput); + + auto& term{ *core->_terminal }; + term.Write(L"\x1b[?1000h\x1b[?1006h"); + + const auto modifiers = ControlKeyStates{ CTRL_PRESSED }; + interactivity->MouseWheel(modifiers, + Core::Point{ 0, WHEEL_DELTA }, + Core::Point{ 0, 0 }, + {}, + true); + + VERIFY_ARE_EQUAL(1.0f, core->_accumulatedFontSizeDelta); + VERIFY_ARE_EQUAL(0u, expectedOutput.size()); + + expectedOutput.push_back(L"\x1b[<64;1;1M"); + interactivity->MouseWheel({}, + Core::Point{ 0, WHEEL_DELTA }, + Core::Point{ 0, 0 }, + {}, + true); + VERIFY_ARE_EQUAL(0u, expectedOutput.size()); + VERIFY_ARE_EQUAL(1.0f, core->_accumulatedFontSizeDelta); + + expectedOutput.push_back(L"\x1b[<83;1;1M"); + interactivity->MouseWheel(modifiers, + Core::Point{ WHEEL_DELTA, 0 }, + Core::Point{ 0, 0 }, + {}, + true); + VERIFY_ARE_EQUAL(0u, expectedOutput.size()); + VERIFY_ARE_EQUAL(1.0f, core->_accumulatedFontSizeDelta); + + expectedOutput.push_back(L"\x1b[<80;1;1M"); + interactivity->MouseWheel(modifiers, + Core::Point{ 0, WHEEL_DELTA }, + Core::Point{ 0, 0 }, + {}); + VERIFY_ARE_EQUAL(0u, expectedOutput.size()); + VERIFY_ARE_EQUAL(1.0f, core->_accumulatedFontSizeDelta); + } + void ControlInteractivityTests::AltBufferClampMouse() { // This is a test for diff --git a/test/e2e/ItE2E/ItE2E.psm1 b/test/e2e/ItE2E/ItE2E.psm1 index b28bd6784b..028b0da4b7 100644 --- a/test/e2e/ItE2E/ItE2E.psm1 +++ b/test/e2e/ItE2E/ItE2E.psm1 @@ -36,7 +36,7 @@ $publicFns = @( # Ui 'Get-UiTree', 'Find-UiElement', 'Get-UiElement', 'Test-UiElementEnabled', 'Invoke-UiElement', 'Invoke-UiClick', 'Get-UiTextBounds', 'Invoke-UiMouseDrag', 'Set-UiValue', 'Get-UiValue', 'Wait-UiElement', 'Test-UiElementExists', 'Save-UiScreenshot', 'Get-WtWindowHwnds', 'Test-WinAppAvailable', - 'Send-WtWindowKey', 'Set-WtWindowForeground', 'Test-WtWindowKeyFocusable', 'Open-WtSettings', 'Test-CommandPaletteOpen', 'Invoke-SettingsNav', + 'Send-WtWindowKey', 'Invoke-WtWindowWheel', 'Set-WtWindowForeground', 'Test-WtWindowKeyFocusable', 'Open-WtSettings', 'Test-CommandPaletteOpen', 'Invoke-SettingsNav', # Observe 'Get-ItLogDir', 'Initialize-LogOffsets', 'Get-ItLogText', 'Start-WtEventListener', 'Get-WtEvents', 'Wait-WtEvent', 'Stop-WtEventListener', 'Get-ContextBundle', 'ConvertTo-ContextText', diff --git a/test/e2e/ItE2E/Public/Ui.ps1 b/test/e2e/ItE2E/Public/Ui.ps1 index 660760940e..fdc5f1ead2 100644 --- a/test/e2e/ItE2E/Public/Ui.ps1 +++ b/test/e2e/ItE2E/Public/Ui.ps1 @@ -51,6 +51,10 @@ function Initialize-WtWin32Input { [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd); [DllImport("user32.dll")] public static extern bool AllowSetForegroundWindow(uint dwProcessId); [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid); + [DllImport("user32.dll", SetLastError=true)] public static extern bool GetCursorPos(out POINT point); + [DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y); + [DllImport("user32.dll")] public static extern short GetAsyncKeyState(int virtualKey); + [DllImport("user32.dll", SetLastError=true)] public static extern uint SendInput(uint count, INPUT[] inputs, int size); [DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId(); [DllImport("user32.dll")] public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach); [DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, System.UIntPtr dwExtraInfo); @@ -63,6 +67,62 @@ function Initialize-WtWin32Input { const uint ASFW_ANY = unchecked((uint)-1); const byte VK_MENU = 0x12; // ALT const uint KEYUP = 0x2; + const uint INPUT_MOUSE = 0; + const uint MOUSEEVENTF_WHEEL = 0x0800; + + [StructLayout(LayoutKind.Sequential)] + public struct POINT { + public int X; + public int Y; + } + + [StructLayout(LayoutKind.Sequential)] + public struct MOUSEINPUT { + public int dx; + public int dy; + public uint mouseData; + public uint dwFlags; + public uint time; + public UIntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Explicit)] + public struct INPUTUNION { + [FieldOffset(0)] public MOUSEINPUT mouse; + } + + [StructLayout(LayoutKind.Sequential)] + public struct INPUT { + public uint type; + public INPUTUNION data; + } + + public static uint GetWindowProcessId(IntPtr hWnd) { + uint pid; + GetWindowThreadProcessId(hWnd, out pid); + return pid; + } + + public static int[] GetCursorPosition() { + POINT point; + if (!GetCursorPos(out point)) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); + return new int[] { point.X, point.Y }; + } + + public static bool IsKeyDown(int virtualKey) { + return (GetAsyncKeyState(virtualKey) & 0x8000) != 0; + } + + public static bool SendMouseWheel(int delta, int count) { + for (int wheel = 0; wheel < count; wheel++) { + var inputs = new INPUT[1]; + inputs[0].type = INPUT_MOUSE; + inputs[0].data.mouse.mouseData = unchecked((uint)delta); + inputs[0].data.mouse.dwFlags = MOUSEEVENTF_WHEEL; + if (SendInput(1, inputs, Marshal.SizeOf(typeof(INPUT))) != 1) return false; + } + return true; + } // Aggressively bring a window to the foreground, defeating the foreground-lock that otherwise // makes SetForegroundWindow a no-op when the caller doesn't own foreground. Combines every @@ -195,6 +255,60 @@ function Test-WtWindowKeyFocusable { } } +function Invoke-WtWindowWheel { + <# + .SYNOPSIS + Send physical mouse-wheel input at an absolute screen coordinate in the test-owned window. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory, ValueFromPipeline)]$App, + [Parameter(Mandatory)][int]$ScreenX, + [Parameter(Mandatory)][int]$ScreenY, + [ValidateSet(-120, 120)][int]$Delta, + [ValidateRange(1, 100)][int]$Count = 1, + [switch]$Ctrl, + [bool]$RestoreCursor = $true + ) + process { + if (-not $App.Hwnd -or -not $App.Pid) { + throw 'Invoke-WtWindowWheel requires App.Hwnd and App.Pid from Start-Terminal.' + } + Initialize-WtWin32Input + $hwnd = [IntPtr][int64]$App.Hwnd + $actualPid = [ItE2E.ItWtWin32Input]::GetWindowProcessId($hwnd) + if ([int]$actualPid -ne [int]$App.Pid) { + throw "HWND $($App.Hwnd) belongs to process $actualPid, not expected process $($App.Pid)." + } + if (-not (Set-WtWindowForeground -App $App)) { + throw "Invoke-WtWindowWheel could not bring HWND $($App.Hwnd) to the foreground; no input was sent." + } + + $original = if ($RestoreCursor) { [ItE2E.ItWtWin32Input]::GetCursorPosition() } else { $null } + $pressedCtrl = $false + try { + if (-not [ItE2E.ItWtWin32Input]::SetCursorPos($ScreenX, $ScreenY)) { + throw "Invoke-WtWindowWheel could not move the cursor to screen coordinate ($ScreenX,$ScreenY)." + } + if ($Ctrl -and -not [ItE2E.ItWtWin32Input]::IsKeyDown(0x11)) { + [ItE2E.ItWtWin32Input]::keybd_event(0x11, 0, 0, [UIntPtr]::Zero) + $pressedCtrl = $true + } + if (-not [ItE2E.ItWtWin32Input]::SendMouseWheel($Delta, $Count)) { + $code = [Runtime.InteropServices.Marshal]::GetLastWin32Error() + throw "Invoke-WtWindowWheel SendInput failed with Win32 error $code." + } + } + finally { + if ($pressedCtrl) { [ItE2E.ItWtWin32Input]::keybd_event(0x11, 0, 0x2, [UIntPtr]::Zero) } + if ($original) { [void][ItE2E.ItWtWin32Input]::SetCursorPos($original[0], $original[1]) } + } + + Start-Sleep -Milliseconds 250 + $App + } +} + function Open-WtSettings { <# .SYNOPSIS diff --git a/test/e2e/README.md b/test/e2e/README.md index 9295ad0d12..693ec8c9ba 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -26,7 +26,7 @@ authenticated ACP agents. Current status (run on the Store package): | `Feature.ByokProvider.Tests.ps1` | PR #447: Settings-selected OpenAI-compatible provider request path, credential handling, and BYOK-to-cloud restart lifecycle | 2 | | `Feature.AgentCompactLayout.Tests.ps1` | PR #580: compact-height recommendation, input, and Insert interaction at the real splitter minimum | 1 | | `Feature.ProposalMcpRouting.Tests.ps1` | PR #560: per-session proposal MCP names and two-tab Helper routing isolation | 1 | -| `Feature.AgentMouse.Tests.ps1` | PR #506: chat wheel scrolling, draft preservation, text selection/copy, and stale-selection suppression; completed-turn full-row clicks across multiline prompts with shared keyboard selection/Enter behavior, row-end/drag guards, and input-dialog focus recovery | 4 | +| `Feature.AgentMouse.Tests.ps1` | PR #506 and issue #790: physical chat wheel scrolling, Ctrl+wheel zoom, draft preservation, text selection/copy, and stale-selection suppression; completed-turn full-row clicks across multiline prompts with shared keyboard selection/Enter behavior, row-end/drag guards, and input-dialog focus recovery | 7 | | `Feature.AgentSelectAll.Tests.ps1` | Plain Ctrl+A selects the current WTA-rendered frame; Ctrl+C copies through the existing clipboard path and clears selection without stale replay | 1 | | `Feature.PromptHistory.Tests.ps1` | PR #478: per-tab Up/Down prompt recall, draft restoration, and multiline preservation; PR #614: completed-turn collapse/expand rendering | 4 | | `Feature.CompletedTurnSelection.Tests.ps1` | Completed-turn Tab/Up/Down selection keeps focused history inside the chat viewport | 1 | @@ -57,11 +57,11 @@ authenticated ACP agents. Current status (run on the Store package): | `Feature.AgentChat.Tests.ps1` / `Feature.AgentPopup.Tests.ps1` | agent chat + `/` popup/menu interaction | 1 + 3 | | `Feature.AgentPaneMove.Tests.ps1` | PR #429: `/move` stays per-tab, preserves global position, and restores agent input focus | 1 | -**Coverage: 147 of 149 automatable `[E2E]` checklist items are implemented.** -**Test status: 127 baseline feature cases pass + 3 documented skips** (`wta sessions list` is +**Coverage: 148 of 150 automatable `[E2E]` checklist items are implemented.** +**Test status: 128 baseline feature cases pass + 3 documented skips** (`wta sessions list` is identity-gated — see `Feature.SessionList.Tests.ps1`), plus 2 PR #481 WSL-backend cases and 2 PR #488 delegate-source cases that run only when a runnable distro (and, for the #481 chat -case, an installed+authenticated native agent) is available. The 147 implemented checklist +case, an installed+authenticated native agent) is available. The 148 implemented checklist items map to the baseline cases plus the deterministic settings/persistence assertions. The remaining new items are the two profile agent picker UIs; they stay explicit E2E work rather than being falsely credited by the JSON-level runtime tests. Other diff --git a/test/e2e/tests/Feature.AgentMouse.Tests.ps1 b/test/e2e/tests/Feature.AgentMouse.Tests.ps1 index e6f23b9f0a..0c797720cd 100644 --- a/test/e2e/tests/Feature.AgentMouse.Tests.ps1 +++ b/test/e2e/tests/Feature.AgentMouse.Tests.ps1 @@ -115,6 +115,130 @@ Describe 'Feature: agent pane mouse interactions' -Tag 'Feature' -Skip:(-not $sc } } +BeforeDiscovery { + $script:WheelZoomReady = [bool]( + (Get-AppxPackage | Where-Object { $_.Name -like '*IntelligentTerminal*' }) -and + (Get-Command pwsh -ErrorAction SilentlyContinue) -and + (Get-Command winapp -ErrorAction SilentlyContinue) + ) +} + +Describe 'Feature: agent pane physical wheel routing' -Tag 'Feature' -Skip:(-not $script:WheelZoomReady) { + BeforeAll { + Import-Module (Join-Path $PSScriptRoot '..\ItE2E\ItE2E.psd1') -Force + $script:getAgentMouseTermControlBounds = { + Add-Type -AssemblyName UIAutomationClient + Add-Type -AssemblyName UIAutomationTypes + $root = [System.Windows.Automation.AutomationElement]::FromHandle([IntPtr][int64]$script:wheelApp.Hwnd) + $condition = [System.Windows.Automation.PropertyCondition]::new( + [System.Windows.Automation.AutomationElement]::ClassNameProperty, + 'TermControl') + $controls = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, $condition) + for ($index = 0; $index -lt $controls.Count; $index++) { + $control = $controls.Item($index) + if ($control.Current.Name -ne 'Agent Pane' -or $control.Current.IsOffscreen) { continue } + $rect = $control.Current.BoundingRectangle + if ($rect.Width -gt 0 -and $rect.Height -gt 0) { + return [pscustomobject]@{ + CenterX = [int][Math]::Round($rect.Left + ($rect.Width / 2)) + CenterY = [int][Math]::Round($rect.Top + ($rect.Height / 2)) + } + } + } + $null + } + $fixture = (Resolve-Path (Join-Path $PSScriptRoot '..\fixtures\Mock-AcpChatAgent.ps1')).Path + $script:wheelFixtureDir = Join-Path $env:TEMP "ItE2E agent wheel $([guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $script:wheelFixtureDir | Out-Null + $script:wheelFixtureLog = Join-Path $script:wheelFixtureDir 'fixture.log' + $invocation = "& '$($fixture.Replace("'", "''"))' -LogPath '$($script:wheelFixtureLog.Replace("'", "''"))'" + $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($invocation)) + $command = "pwsh -NoProfile -EncodedCommand $encoded" + + $script:wheelApp = Start-Terminal -Package (Get-ItTestPackage) -PassFre $true -Settings @{ + acpAgent = 'custom:wheel-fixture' + acpCustomCommand = $command + 'experimental.scrollToZoom' = $true + 'warning.confirmOnClose' = 'never' + } + $shell = Get-ActivePane -App $script:wheelApp + Open-AgentPane -App $script:wheelApp | Out-Null + $session = Wait-NewAgentPaneSession -App $script:wheelApp -OwnerPaneSessionId $shell.session_id -TimeoutSec 30 + $script:wheelAgentPane = $session.PaneSessionId + Wait-AgentReady -App $script:wheelApp -PaneSessionId $script:wheelAgentPane -TimeoutSec 60 | + Should -BeTrue -Because 'the deterministic ACP fixture must connect before physical wheel input' + $script:getAgentViewport = { + $text = Get-AgentPaneText -App $script:wheelApp -PaneSessionId $script:wheelAgentPane -MaxLines 500 + $lines = @($text -split "`r?`n") + [pscustomobject]@{ + Rows = $lines.Count + Columns = [Math]::Max(1, [int](($lines | ForEach-Object Length | Measure-Object -Maximum).Maximum)) + Text = $text + } + } + } + + AfterAll { + if ($script:wheelApp) { Stop-Terminal -App $script:wheelApp } + if ($script:wheelFixtureDir -and (Test-Path -LiteralPath $script:wheelFixtureDir)) { + Remove-Item -LiteralPath $script:wheelFixtureDir -Recurse -Force + } + } + + It 'Ctrl+wheel zooms the agent pane while plain wheel scrolls chat' -Tag 'Issue790' { + if (-not (Test-WtWindowKeyFocusable -App $script:wheelApp)) { + Set-ItResult -Skipped -Because 'WT window cannot take foreground for physical wheel input' + return + } + + $viewport = & $script:getAgentViewport + $turnCount = [Math]::Max(12, $viewport.Rows + 4) + $turns = @() + for ($index = 0; $index -lt $turnCount; $index++) { + $marker = "SCROLL_TURN_$($index.ToString('00'))_$([guid]::NewGuid().ToString('N'))" + $turns += $marker + Send-AgentPrompt -App $script:wheelApp -PaneSessionId $script:wheelAgentPane -Text $marker | Out-Null + Assert-AgentPaneText -App $script:wheelApp -PaneSessionId $script:wheelAgentPane ` + -Pattern ([regex]::Escape("ACK_$marker")) -TimeoutSec 10 + } + + $draftMarker = "PHYSICAL_WHEEL_DRAFT_$([guid]::NewGuid().ToString('N'))" + $beforeDraft = & $script:getAgentViewport + $fillerCount = [Math]::Ceiling(($beforeDraft.Columns * 2) / ' ZOOM_FILLER'.Length) + $draft = "$draftMarker$((' ZOOM_FILLER' * $fillerCount))" + Send-AgentPrompt -App $script:wheelApp -PaneSessionId $script:wheelAgentPane -Text $draft -NoSubmit | Out-Null + $bounds = & $script:getAgentMouseTermControlBounds + $beforePlain = & $script:getAgentViewport + $bounds | Should -Not -BeNullOrEmpty -Because 'the visible agent TermControl must expose physical screen bounds' + $oldest = $turns[0] + $delta = if ($beforePlain.Text -match [regex]::Escape($oldest)) { -120 } else { 120 } + + Invoke-WtWindowWheel -App $script:wheelApp -ScreenX $bounds.CenterX -ScreenY $bounds.CenterY ` + -Delta $delta -Count 24 | Out-Null + $afterPlainText = Wait-Until -TimeoutSec 8 -IntervalSec 0.25 -Quiet -Condition { + $text = Get-AgentPaneText -App $script:wheelApp -PaneSessionId $script:wheelAgentPane -MaxLines 500 + if ($text -ne $beforePlain.Text) { $text } + } + $afterPlain = & $script:getAgentViewport + $afterPlainText | Should -Not -BeNullOrEmpty -Because 'physical plain wheel must scroll the WTA chat viewport' + $afterPlain.Text | Should -Match ([regex]::Escape($draftMarker)) -Because 'plain wheel must preserve the unsent draft' + $afterPlain.Rows | Should -Be $beforePlain.Rows -Because 'plain wheel must not zoom the agent pane' + $afterPlain.Columns | Should -Be $beforePlain.Columns -Because 'plain wheel must not change the agent font width' + + Invoke-WtWindowWheel -App $script:wheelApp -ScreenX $bounds.CenterX -ScreenY $bounds.CenterY ` + -Delta 120 -Ctrl | Out-Null + $afterZoom = Wait-Until -TimeoutSec 5 -IntervalSec 0.2 -Quiet -Condition { + $current = & $script:getAgentViewport + if ($current.Columns -lt $afterPlain.Columns) { $current } + } + $afterZoom | Should -Not -BeNullOrEmpty -Because "Ctrl+wheel must reduce visible columns from $($afterPlain.Columns)" + $afterZoom.Text | Should -Match ([regex]::Escape($draftMarker)) -Because 'Ctrl+wheel zoom must preserve the unsent draft' + + Invoke-WtWindowWheel -App $script:wheelApp -ScreenX $bounds.CenterX -ScreenY $bounds.CenterY ` + -Delta -120 -Ctrl | Out-Null + } +} + BeforeDiscovery { $script:TriangleClickReady = [bool]( (Get-AppxPackage | Where-Object { $_.PackageFamilyName -eq 'IntelligentTerminal_rd9vj3e6a2mbr' }) -and