Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions scripts/winterm/test-command-timeline.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ Assert-Contains -Content $resources -Values @(
'CommandTimelineCopyOutput',
'CommandTimelineJumpToOutput',
'CommandTimelineOutputUnavailable',
'CommandTimelineStatusUnknownDetail',
'CommandTimelineMultilineBlocked',
'CommandTimelineConfirmLoad'
) -Failure 'Timeline entry-action localized strings are missing.'
Expand Down
8 changes: 6 additions & 2 deletions shell/powershell/winTerm.Shell/Public/Compatibility.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
3 changes: 3 additions & 0 deletions src/cascadia/TerminalControl/Resources/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,9 @@
<data name="CommandTimelineStatusUnknown" xml:space="preserve">
<value>Unknown</value>
</data>
<data name="CommandTimelineStatusUnknownDetail" xml:space="preserve">
<value>The shell did not report a result for this command. Success and failure are shown only when shell integration reports them.</value>
</data>
<data name="CommandTimelineCopyCommand" xml:space="preserve">
<value>Copy command</value>
</data>
Expand Down
18 changes: 18 additions & 0 deletions src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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(
Expand Down
12 changes: 12 additions & 0 deletions src/cascadia/TerminalCore/Terminal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand Down
57 changes: 57 additions & 0 deletions src/cascadia/UnitTests_Control/CommandTimelineTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<MockControlSettings>();
settings->AutoMarkPrompts(true);
auto connection = winrt::make_self<MockConnection>();
auto core = winrt::make_self<winrt::Microsoft::Terminal::Control::implementation::ControlCore>(*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<int>(ExecutionResult::Running), static_cast<int>(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<int>(ExecutionResult::Succeeded), static_cast<int>(finished.entries[0].executionResult));
VERIFY_ARE_EQUAL(uint32_t{ 0 }, *finished.entries[0].trustedExitCode);
VERIFY_ARE_EQUAL(static_cast<int>(ShellIntegrationCapability::Full), static_cast<int>(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 };
Expand Down