diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e3b8bbe9..d371698eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## Unreleased + +Fixes for the three alpha3 field reports. + +### Fixed + +- Opening a PowerShell tab no longer raises an antivirus alert. The `touch` + compatibility command created files through a raw `File::Open` write call, + which — combined with the native-dispatch blocks in the same script — read + as a write-then-execute shape to antivirus heuristics and flagged the whole + module at load, on some engines with a user-visible warning for every new + tab. File creation now goes through `New-Item`; behavior is unchanged + (create if missing, update the timestamp without truncating otherwise), and + the module loads clean under the engine that previously flagged it. +- Completed commands now show ✓/✕ instead of staying `? Unknown`. The Enter + keypress heuristic that supplies the command-executed transition only set + buffer marks and never notified the shell-integration lifecycle, so the + chain never observed the executed stage, the pane's capability never read + Full, and the presentation downgraded every trusted result to Unknown. The + Enter path now reports the executed transition when the mark was + established by the shell and the input line is non-empty; a heuristic-only + mark (no shell integration, for example cmd.exe) still reports nothing. + This also restores the Running status while a command executes. +- A long Timeline command is now readable in full: every row carries a + tooltip with the complete command text, wrapped, with no marquee animation. +- The `Unknown` status now explains itself: hovering it shows that the shell + did not report a result for this command, and that success and failure are + shown only when shell integration reports them. + ## 1.3.0-alpha3 - 2026-08-04 Third alpha prerelease: fixes for the five alpha2 field reports. Like the diff --git a/scripts/winterm/test-command-timeline.ps1 b/scripts/winterm/test-command-timeline.ps1 index 74f03e313..e1d8fb92a 100644 --- a/scripts/winterm/test-command-timeline.ps1 +++ b/scripts/winterm/test-command-timeline.ps1 @@ -270,6 +270,7 @@ Assert-Contains -Content $resources -Values @( 'CommandTimelineCopyOutput', 'CommandTimelineJumpToOutput', 'CommandTimelineOutputUnavailable', + 'CommandTimelineStatusUnknownDetail', 'CommandTimelineMultilineBlocked', 'CommandTimelineConfirmLoad' ) -Failure 'Timeline entry-action localized strings are missing.' diff --git a/shell/powershell/winTerm.Shell/Public/Compatibility.ps1 b/shell/powershell/winTerm.Shell/Public/Compatibility.ps1 index acdbc6969..5237e2133 100644 --- a/shell/powershell/winTerm.Shell/Public/Compatibility.ps1 +++ b/shell/powershell/winTerm.Shell/Public/Compatibility.ps1 @@ -184,8 +184,12 @@ function touch try { - $stream = [System.IO.File]::Open($fileSystemPath, [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::Write, [System.IO.FileShare]::ReadWrite) - $stream.Dispose() + # The existence test above makes creation the only reachable case, + # so New-Item never truncates an existing file here. The previous + # raw File::Open write call also read as a write-then-execute shape + # to antivirus heuristics when combined with the native-dispatch + # blocks in this file, and flagged the whole module at load. + New-Item -ItemType File -Path $fileSystemPath -ErrorAction Stop | Out-Null } catch { diff --git a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw index de4b1842e..2c3dcd80f 100644 --- a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw @@ -376,6 +376,9 @@ Unknown + + The shell did not report a result for this command. Success and failure are shown only when shell integration reports them. + Copy command diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 4bd425e75..3640fc8ef 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -3013,6 +3013,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation Controls::TextBlock status; status.Text(winrt::hstring{ std::wstring{ statusGlyph } + L" " + std::wstring{ statusLabel } }); status.FontSize(11); + if (entry.executionResult == winTerm::CommandTimeline::ExecutionResult::Unknown) + { + // Unknown is a trust statement, not an error; say so where the + // question arises. + Controls::ToolTipService::SetToolTip(status, box_value(RS_(L"CommandTimelineStatusUnknownDetail"))); + } Controls::StackPanel content; content.Spacing(2); @@ -3026,6 +3032,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation item.HorizontalContentAlignment(HorizontalAlignment::Stretch); item.IsTabStop(false); + // The row shows one trimmed line; the tooltip carries the full + // command text so a long command stays readable without any + // marquee animation. The text is already on screen in this pane, + // so the tooltip introduces no new exposure. + Controls::TextBlock fullCommand; + fullCommand.Text(winrt::hstring{ commandText }); + fullCommand.TextWrapping(TextWrapping::Wrap); + fullCommand.MaxWidth(480.0); + Controls::ToolTip rowTip; + rowTip.Content(fullCommand); + Controls::ToolTipService::SetToolTip(item, rowTip); + const auto accessibleName = commandText + L", " + std::wstring{ statusLabel }; Windows::UI::Xaml::Automation::AutomationProperties::SetName(item, winrt::hstring{ accessibleName }); Windows::UI::Xaml::Automation::AutomationProperties::SetPositionInSet( diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 13aee09d2..72197b422 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -771,6 +771,18 @@ TerminalInput::OutputType Terminal::SendCharEvent(const wchar_t ch, const WORD s // This changed the scrollbar marks - raise a notification to update them _NotifyScrollEvent(); + } + else if (!_mainBuffer->CurrentCommandTimelineCommand().empty()) + { + // The shell established this mark through OSC 133 A/B, and + // this Enter keypress supplies the command-executed + // transition the shell itself does not report. Without this + // notification the lifecycle chain never observes the + // executed stage, the capability never reads Full, and every + // completed command presents as Unknown. A heuristic-only + // mark (createdMark above) stays buffer-local and reports + // nothing, and an empty input line starts no command. + NotifyShellIntegrationMark(::Microsoft::Console::VirtualTerminal::ShellIntegrationMarkKind::CommandExecuted, std::nullopt); } } } diff --git a/src/cascadia/UnitTests_Control/CommandTimelineTests.cpp b/src/cascadia/UnitTests_Control/CommandTimelineTests.cpp index 204759145..197dc6c47 100644 --- a/src/cascadia/UnitTests_Control/CommandTimelineTests.cpp +++ b/src/cascadia/UnitTests_Control/CommandTimelineTests.cpp @@ -23,6 +23,7 @@ namespace ControlUnitTests TEST_METHOD(ColdBootstrapScansOnceAndWarmAccessDoesNotRescan); TEST_METHOD(LifecycleUpdatesAreIncrementalAndIdempotent); TEST_METHOD(OutOfOrderLifecycleStaysUnknown); + TEST_METHOD(EnterKeypressSuppliesExecutedTransition); TEST_METHOD(TrustedCompletionMapsResults); TEST_METHOD(CapabilityRequiresCompleteNativeLifecycle); TEST_METHOD(InvalidationAndEvictionPruneGhostEntries); @@ -227,6 +228,62 @@ namespace ControlUnitTests VERIFY_IS_NULL(index.Current()); } + void CommandTimelineTests::EnterKeypressSuppliesExecutedTransition() + { + // The supported PowerShell flow emits 133;A/B/D from the shell and + // relies on the Enter keypress heuristic for the executed transition. + // This drives that exact flow through a live core: no 133;C ever + // arrives on the wire. + auto settings = winrt::make_self(); + settings->AutoMarkPrompts(true); + auto connection = winrt::make_self(); + auto core = winrt::make_self(*settings, *settings, *connection); + core->_inUnitTests = true; + auto cleanup = wil::scope_exit([&]() noexcept { + try + { + if (core) + { + core->Close(); + } + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + } + core = nullptr; + connection = nullptr; + settings = nullptr; + }); + VERIFY_IS_TRUE(core->Initialize(270, 380, 1.0)); + + connection->WriteInput(winrt_wstring_to_array_view(L"\x1b]133;A\aPS> \x1b]133;B\aecho hi")); + VERIFY_IS_TRUE(core->CommandTimelineSnapshot().entries.empty()); + + core->SendCharEvent(L'\r', 0, {}); + auto running = core->CommandTimelineSnapshot(); + VERIFY_ARE_EQUAL(size_t{ 1 }, running.entries.size()); + VERIFY_ARE_EQUAL(std::wstring{ L"echo hi" }, running.entries[0].cachedCommandText); + VERIFY_ARE_EQUAL(static_cast(ExecutionResult::Running), static_cast(running.entries[0].executionResult)); + + connection->WriteInput(winrt_wstring_to_array_view(L"\r\nhi\r\n\x1b]133;D;0\a\x1b]133;A\aPS> \x1b]133;B\a")); + auto finished = core->CommandTimelineSnapshot(); + VERIFY_ARE_EQUAL(size_t{ 1 }, finished.entries.size()); + VERIFY_ARE_EQUAL(static_cast(ExecutionResult::Succeeded), static_cast(finished.entries[0].executionResult)); + VERIFY_ARE_EQUAL(uint32_t{ 0 }, *finished.entries[0].trustedExitCode); + VERIFY_ARE_EQUAL(static_cast(ShellIntegrationCapability::Full), static_cast(finished.capability)); + + // An Enter on an empty integrated prompt starts no command and adds + // no entry. + core->SendCharEvent(L'\r', 0, {}); + VERIFY_ARE_EQUAL(size_t{ 1 }, core->CommandTimelineSnapshot().entries.size()); + + core = nullptr; + connection = nullptr; + settings = nullptr; + cleanup.release(); + } + void CommandTimelineTests::TrustedCompletionMapsResults() { CommandTimelineIndex index{ PaneOne };