From 2ecd3bc7d19a7e818dfccb4ee70ecb687de07c87 Mon Sep 17 00:00:00 2001 From: HelloThisWorld Date: Tue, 4 Aug 2026 20:02:39 +0800 Subject: [PATCH] fix: address the three alpha3 field reports Local testing of v1.3.0-alpha3 reported three issues. 1. Opening a PowerShell tab raised an antivirus alert on every launch. Bisection against a local engine that reproduces the field report shows each function of Compatibility.ps1 loads clean in isolation, but touch combined with any sibling function was flagged: the raw [System.IO.File]::Open(OpenOrCreate, Write) call plus the "& $native.Source" dispatch blocks in the same file read as a write-then-execute dropper shape. File creation now goes through New-Item, reachable only when the file does not exist, so behavior is unchanged: create if missing, update the timestamp without truncating otherwise. After the change the full module imports with zero failed components and all eleven exports in both Windows PowerShell 5.1 and PowerShell 7 on the engine that previously flagged it. 2. Every completed command displayed as "? Unknown". The supported flow documents that the Enter keypress heuristic supplies the command-executed transition, but the heuristic only set buffer marks and never notified the shell-integration lifecycle. The Timeline therefore never observed the executed stage: the per-mark capability chain never read Full and the presentation downgraded every trusted exit code to Unknown. The Enter path now reports CommandExecuted when the current 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, and an empty input line starts no command. This also restores the Running status while a command executes, since entries materialize at execution rather than at completion. A new end-to-end test drives a live core with 133;A/B on the wire, a real Enter keypress, and 133;D, asserting Running, then Succeeded, capability Full, and that an empty-prompt Enter adds no entry. 3. A long command was unreadable in the Timeline. Every row now carries a tooltip with the full command text, wrapped, with no marquee animation; the text is already on screen in that pane, so the tooltip introduces no new exposure. The Unknown status also explains itself on hover through the new CommandTimelineStatusUnknownDetail resource, and test-command-timeline.ps1 asserts the resource exists. No persistent history, no output cache, no heuristic prompt detection for unsupported shells, no telemetry. Workspace schema, docking model, shell protocol, theme schema, update manifest schema, package identity, and signing policy are unchanged. --- CHANGELOG.md | 29 ++++++++++ scripts/winterm/test-command-timeline.ps1 | 1 + .../winTerm.Shell/Public/Compatibility.ps1 | 8 ++- .../Resources/en-US/Resources.resw | 3 + src/cascadia/TerminalControl/TermControl.cpp | 18 ++++++ src/cascadia/TerminalCore/Terminal.cpp | 12 ++++ .../CommandTimelineTests.cpp | 57 +++++++++++++++++++ 7 files changed, 126 insertions(+), 2 deletions(-) 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 };