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
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
# Changelog

## Unreleased

Fixes for the four alpha1 field reports, ahead of the 1.3.0 beta.

### Added

- Automatic PowerShell shell integration. When a profile's commandline is a
bare `powershell.exe` or `pwsh.exe` invocation (optionally with `-NoLogo` or
`-NoExit`), winTerm now appends a `-NoExit -Command` fragment that imports
the packaged `winTerm.Shell` module, so OSC 133 marks — and therefore the
Command Timeline — work out of the box for the stock Windows PowerShell
profile. Any customized invocation launches unchanged, execution policy is
never altered, and an import failure leaves a working shell. The new
per-profile setting `"shellIntegration.autoInject"` (default `true`) turns
the rewrite off. This fixes typed commands never appearing in the Command
Timeline on a fresh install.

### Fixed

- The Command Timeline handle no longer covers terminal content. It now rests
as a 6-pixel strip flush against the terminal's left edge, in the manner of
an auto-hiding scrollbar, and widens to show its chevron on hover, keyboard
focus, or while the overlay is open.
- Clicking the terminal area while the Command Timeline is open now
light-dismisses the overlay; the click still reaches the terminal.
- The Visual Progress bar no longer keeps animating after ordinary output such
as a `dir` listing. Recognition of Gradle-style output now requires
per-record build-tool evidence instead of a bare product-name mention, an
established provider claim no longer rematches arbitrary later records, a
still-running provider bar structurally clears after two consecutive
ordinary records, and slashed dates such as `2025/10/13` are no longer read
as completed/total meters. Success and Error results still persist until a
later publication replaces them.

## 1.3.0-alpha1 - 2026-08-04

First public prerelease of the Command Timeline. This is an alpha for local
Expand Down
5 changes: 5 additions & 0 deletions doc/cascadia/profiles.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2979,6 +2979,11 @@
"description": "When set to true, prompts will automatically be marked.",
"type": "boolean"
},
"shellIntegration.autoInject": {
"default": true,
"description": "When set to true and the profile commandline is a bare PowerShell invocation, winTerm imports its packaged shell integration module at startup so command marks work without profile changes. Customized commandlines are never rewritten.",
"type": "boolean"
},
"experimental.autoMarkPrompts": {
"type": "boolean",
"description": "[Deprecated] Replaced with the \"autoMarkPrompts\" setting.",
Expand Down
32 changes: 26 additions & 6 deletions docs/current-progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ Last updated: 2026-08-04

## Repository state

- Branch: `release/v1.3.0-alpha1`
- Base branch: `main` at `ac760eab` (Command Timeline Phase 4, pull request #30)
- Branch: `fix/alpha1-feedback`
- Base branch: `main` at `ed707550` (1.3.0-alpha1 release metadata, pull
request #31)
- Microsoft Terminal upstream revision:
`1cea42d433253d95c4487a3037db48197b5e72f4`
- Application version: `1.3.0-alpha1`
Expand Down Expand Up @@ -47,13 +48,32 @@ prerelease suffix. The package version stays four-part numeric for MSIX and the
Win32 resource fields, and the PowerShell module version stays numeric with the
suffix carried in `PrivateData.PSData.Prerelease`.

## Alpha1 field reports

Local testing of `v1.3.0-alpha1` surfaced four issues, all addressed on this
branch:

1. The Timeline handle covered terminal content. It is now a thin auto-hiding
strip on the terminal's left edge that widens on hover, focus, or while the
overlay is open.
2. Clicking the terminal area did not close an open Timeline. The overlay now
light-dismisses on a terminal press, which still reaches the terminal.
3. A `dir` listing left the Visual Progress bar animating indefinitely. The
recognition engine no longer claims ownership from a bare product-name
mention, no longer rematches arbitrary records under an established claim,
structurally clears a still-running bar after two consecutive ordinary
records, and no longer reads slashed dates as meters.
4. Typed commands never appeared in the Timeline because nothing imported the
packaged `winTerm.Shell` module. Bare PowerShell profile commandlines are
now rewritten at connection creation to import it, gated by the new
per-profile setting `"shellIntegration.autoInject"` (default `true`).

## Next steps

1. Install `v1.3.0-alpha1` locally and exercise the Command Timeline.
2. Fix anything the alpha testing surfaces.
3. Cut `v1.3.0-beta1` on channel `beta`. The beta may be listed on the winTerm
1. Re-test the four fixes locally on a fresh build.
2. Cut `v1.3.0-beta1` on channel `beta`. The beta may be listed on the winTerm
website alongside the stable v1.2.0 download.
4. Promote to a stable `v1.3.0` only after beta testing, which is the point at
3. Promote to a stable `v1.3.0` only after beta testing, which is the point at
which Latest, WinGet, and the website stable slot move.

## Validation state
Expand Down
29 changes: 27 additions & 2 deletions docs/powershell-integration.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# PowerShell integration

The packaged module is `ShellAssets\powershell\winTerm.Shell\winTerm.Shell.psd1`, version `1.0.2`. It supports PowerShell 7 and Windows PowerShell 5.1 with the same syntax.
The packaged module is `ShellAssets\powershell\winTerm.Shell\winTerm.Shell.psd1`; its module version tracks the winTerm release. It supports PowerShell 7 and Windows PowerShell 5.1 with the same syntax.

An explicit winTerm profile launcher must set these process-local variables before importing the module:
A winTerm launcher must set these process-local variables before importing the module:

```powershell
$env:WINTERM_SESSION_ID = '<opaque-session-id>'
Expand All @@ -12,6 +12,31 @@ Import-Module '<package-path>\ShellAssets\powershell\winTerm.Shell\winTerm.Shell

The module does not add this block to `$PROFILE`. A launcher must preserve normal PowerShell execution policy; the module neither uses nor recommends `-ExecutionPolicy Bypass`. If policy prevents importing a module, PowerShell must still launch and diagnostics should report the failure and recommend a user-reviewed policy or installation remedy.

## Automatic integration for bare PowerShell profiles

winTerm performs the launcher steps automatically when a profile's commandline
is a bare PowerShell invocation, so the Command Timeline and shell-lifecycle
progress work out of the box for the stock Windows PowerShell profile. The
rules are deliberately narrow and are implemented in
`src/winterm/Shell/AutoIntegration.h`:

- Only `powershell.exe` and `pwsh.exe` are recognized, by executable basename.
- The only arguments tolerated on the original commandline are `-NoLogo` and
`-NoExit`. Any other argument — including `-Command`, `-File`,
`-EncodedCommand`, `-NoProfile`, or `-ExecutionPolicy` — means the user has
customized the invocation, and it launches unchanged.
- The rewrite appends `-NoExit -Command` with a fragment that sets the two
session variables and imports the packaged module with
`-ErrorAction SilentlyContinue`. Execution policy is never altered, and an
import failure leaves a working shell without integration.
- A commandline that already mentions the module is not rewritten again, so a
restarted connection stays stable.

The per-profile setting `"shellIntegration.autoInject"` (default `true`)
disables the rewrite when set to `false`. Because `-Command` is present on the
rewritten invocation, PowerShell suppresses its startup banner; this is the
standard behavior of every launcher-based shell integration.

## Prompt and marks

On an eligible session, the module captures the current `prompt` function, sends prompt/CWD marks around its output, and calls the original script block. A second import detects its own wrapper instead of nesting it. Removing `winTerm.Shell` restores the captured prompt when the wrapper is still active. This preserves common profile customizations, including prompt frameworks loaded before the module.
Expand Down
10 changes: 7 additions & 3 deletions scripts/winterm/test-command-timeline.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,12 @@ if ($overlayStart -lt 0 -or $rendererNoticeStart -le $overlayStart) {
$overlayXaml = $termControlXaml.Substring($overlayStart, $rendererNoticeStart - $overlayStart)
Assert-Contains -Content $overlayXaml -Values @(
'x:Name="CommandTimelineHandle"',
'Margin="8,0,0,0"',
'Width="6"',
'PointerEntered="_CommandTimelineHandlePointerEntered"',
'PointerExited="_CommandTimelineHandlePointerExited"',
'x:Name="CommandTimelineList"',
'SelectionMode="Single"'
) -Failure 'The overlay handle, bounded list, or selection presentation is incomplete.'
) -Failure 'The overlay auto-hiding handle, bounded list, or selection presentation is incomplete.'
if ($overlayXaml.Contains('SwapChainPanel') -or $overlayXaml.Contains('ColumnDefinition') -or $overlayXaml.Contains('Storyboard')) {
throw 'The Timeline overlay must not resize the terminal or add an independent animation loop.'
}
Expand All @@ -207,11 +209,13 @@ Assert-Contains -Content $termControlSource -Values @(
'GetTSFHandle().HasActiveComposition()',
'_tryHandleCommandTimelineKey(vkey, modifiers, keyDown)',
'_tryHandleCommandTimelineWheel(point.Position(), delta)',
'_commandTimelineOpen && !_isPointOverCommandTimeline(point.Position())',
'_updateCommandTimelineHandleVisual()',
'TextTrimming::CharacterEllipsis',
'_commandTimelineWheelSettleTimer.Stop()',
'CommandTimelineList().Items().Clear()',
'Focus(FocusState::Programmatic)'
) -Failure 'Timeline input isolation, IME precedence, snapping, or close cleanup is incomplete.'
) -Failure 'Timeline input isolation, IME precedence, light dismiss, or close cleanup is incomplete.'
if ($termControlSource.IndexOf('_TryHandleKeyBinding(vkey, scanCode, modifiers)', [StringComparison]::Ordinal) -gt
$termControlSource.IndexOf('_tryHandleCommandTimelineKey(vkey, modifiers, keyDown)', [StringComparison]::Ordinal)) {
throw 'User-defined key bindings must retain precedence over bare Timeline navigation.'
Expand Down
48 changes: 47 additions & 1 deletion src/cascadia/TerminalApp/TerminalPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include "TerminalSettingsCache.h"
#include "VisualProgressWindowCoordinator.h"
#include "../../winterm/Design/DesignTokens.h"
#include "../../winterm/Shell/AutoIntegration.h"

#include "LaunchPositionRequest.g.cpp"
#include "RenameWindowRequestedArgs.g.cpp"
Expand Down Expand Up @@ -1537,6 +1538,43 @@ namespace winrt::TerminalApp::implementation
// - the terminal settings
// Return value:
// - the desired connection
// Rewrites a bare PowerShell profile commandline so the packaged
// winTerm.Shell module provides shell integration marks for that session.
// Anything the eligibility rules do not positively recognize launches
// unchanged; a missing packaged module disables the rewrite entirely.
static std::optional<std::wstring> _buildAutoIntegratedShellCommandline(const std::wstring_view commandline)
{
static const auto moduleManifestPath = []() -> std::wstring {
try
{
const std::filesystem::path root{ wil::GetModuleFileNameW<std::wstring>(nullptr) };
auto candidate = root.parent_path() / L"ShellAssets" / L"powershell" / L"winTerm.Shell" / L"winTerm.Shell.psd1";
std::error_code ec;
if (std::filesystem::exists(candidate, ec))
{
return candidate.wstring();
}
}
CATCH_LOG();
return {};
}();

if (moduleManifestPath.empty())
{
return std::nullopt;
}

GUID sessionId{};
if (FAILED(CoCreateGuid(&sessionId)))
{
return std::nullopt;
}

return winTerm::Shell::BuildAutoIntegratedPowerShellCommandline(commandline,
moduleManifestPath,
::Microsoft::Console::Utils::GuidToString(sessionId));
}

TerminalConnection::ITerminalConnection TerminalPage::_CreateConnectionFromSettings(Profile profile,
IControlSettings settings,
const bool inheritCursor)
Expand Down Expand Up @@ -1601,7 +1639,15 @@ namespace winrt::TerminalApp::implementation
// restored the CWD to its original value.
auto newWorkingDirectory{ _evaluatePathForCwd(settings.StartingDirectory()) };
connection = TerminalConnection::ConptyConnection{};
valueSet = TerminalConnection::ConptyConnection::CreateSettings(settings.Commandline(),
auto commandline = settings.Commandline();
if (profile.AutoInjectShellIntegration())
{
if (const auto integrated = _buildAutoIntegratedShellCommandline(commandline))
{
commandline = winrt::hstring{ *integrated };
}
}
valueSet = TerminalConnection::ConptyConnection::CreateSettings(commandline,
newWorkingDirectory,
settings.StartingTitle(),
settingsInternal->ReloadEnvironmentVariables(),
Expand Down
54 changes: 53 additions & 1 deletion src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2003,6 +2003,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation
const auto point = args.GetCurrentPoint(*this);
const auto type = ptr.PointerDeviceType();

// A press on the terminal area light-dismisses the Command Timeline.
// The press still continues into the terminal below, so the click
// also does whatever it would normally do there. Presses on the
// overlay itself never reach this handler.
if (_commandTimelineOpen && !_isPointOverCommandTimeline(point.Position()))
{
_closeCommandTimeline(false);
}

// GH#19908: _focused can be true even when the search box has
// keyboard focus, because GotFocus bubbles from the search box
// child and _GotFocusHandler sets _focused=true. If the user
Expand Down Expand Up @@ -2771,6 +2780,48 @@ namespace winrt::Microsoft::Terminal::Control::implementation
ToggleCommandTimeline();
}

// The handle rests as a thin strip on the terminal's left edge and only
// widens while it is hovered, focused, or the overlay is open, so it
// does not cover terminal content in normal use.
void TermControl::_updateCommandTimelineHandleVisual()
{
static constexpr double restingWidth{ 6.0 };
static constexpr double expandedWidth{ 20.0 };
const auto expanded = _commandTimelineOpen ||
_commandTimelineHandlePointerOver ||
_commandTimelineHandleFocused;
CommandTimelineHandle().Width(expanded ? expandedWidth : restingWidth);
CommandTimelineHandleIcon().Visibility(expanded ? Visibility::Visible : Visibility::Collapsed);
}

void TermControl::_CommandTimelineHandlePointerEntered(const IInspectable& /*sender*/,
const Input::PointerRoutedEventArgs& /*args*/)
{
_commandTimelineHandlePointerOver = true;
_updateCommandTimelineHandleVisual();
}

void TermControl::_CommandTimelineHandlePointerExited(const IInspectable& /*sender*/,
const Input::PointerRoutedEventArgs& /*args*/)
{
_commandTimelineHandlePointerOver = false;
_updateCommandTimelineHandleVisual();
}

void TermControl::_CommandTimelineHandleGotFocus(const IInspectable& /*sender*/,
const RoutedEventArgs& /*args*/)
{
_commandTimelineHandleFocused = true;
_updateCommandTimelineHandleVisual();
}

void TermControl::_CommandTimelineHandleLostFocus(const IInspectable& /*sender*/,
const RoutedEventArgs& /*args*/)
{
_commandTimelineHandleFocused = false;
_updateCommandTimelineHandleVisual();
}

void TermControl::_CommandTimelineSelectionChanged(const IInspectable& /*sender*/,
const Controls::SelectionChangedEventArgs& /*args*/)
{
Expand Down Expand Up @@ -3042,6 +3093,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
Windows::UI::Xaml::Automation::AutomationProperties::SetName(CommandTimelineHandle(), RS_(L"CommandTimelineOpen"));
Controls::ToolTipService::SetToolTip(CommandTimelineHandle(), box_value(RS_(L"CommandTimelineOpen")));
CommandTimelineHandleIcon().Glyph(L"\xE76C");
_updateCommandTimelineHandleVisual();

if (returnFocus && !_IsClosing())
{
Expand Down Expand Up @@ -3534,10 +3586,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
_commandTimelineOpen = true;
CommandTimelineOverlay().Visibility(Visibility::Visible);
CommandTimelineHandle().Margin({ 8, 0, 0, 0 });
Windows::UI::Xaml::Automation::AutomationProperties::SetName(CommandTimelineHandle(), RS_(L"CommandTimelineClose"));
Controls::ToolTipService::SetToolTip(CommandTimelineHandle(), box_value(RS_(L"CommandTimelineClose")));
CommandTimelineHandleIcon().Glyph(L"\xE76B");
_updateCommandTimelineHandleVisual();

const auto width = std::clamp(ActualWidth() * 0.42, 180.0, 360.0);
CommandTimelineOverlay().Width(std::max(1.0, std::min(width, ActualWidth())));
Expand Down
Loading