From baf6f8a8731207f4bb24229c8af9653417b3618a Mon Sep 17 00:00:00 2001 From: HelloThisWorld Date: Wed, 5 Aug 2026 20:20:08 +0800 Subject: [PATCH 1/2] fix: bound the shell-integration launch fallback to one traversal per command OSC 133 CommandExecuted published a ShellIntegration/Indeterminate/Running fallback that stayed active until CommandFinished, so any long-running command (k9s, vim, top, FastAPI/uvicorn, Spring Boot, Node dev servers, tail -f, kubectl port-forward) looped the rainbow comet animation for its whole lifetime. The fallback is now a bounded one-shot launch indication scoped by a shell command generation, never by command or process names. CommandExecuted opens a new launch generation and publishes the launch snapshot carrying it; a repeated observation of the same still-running command (alternate screen churn, pane rehydration, reconnect re-broadcast) is idempotent. The renderer bounds the launch with a launch clock: one one-shot compositor animation on a private property set using the existing 1,800 ms IndeterminateCycleDuration (no timer, no polling loop, no CPU frame loop) whose generation-guarded completion asks the state machine to expire the fallback. Expiration clears the stored shell snapshot even while a provider or explicit OSC 9;4 source owns the bar, so a later ownership release cannot resurrect it, and publishes Hidden with Running status so accessibility announces neither a fake success nor a fake cancellation. The launch comet itself plays a single traversal and parks off-track; explicit and provider indeterminate presentations keep the continuous traversal. CommandFinished supersedes the launch immediately and strands in-flight completions, preserving every terminal presentation. Four new TAEF tests cover the one-shot, stale-generation, ownership resurrection, and reset/disable/close invalidation semantics; the source suite locks in the policy, the clock sharing the traversal constant, and the Pane wiring; the smoke fixture gains a long-running one-shot launch and an alternate-screen sequence plus manual guidance for FastAPI, Spring Boot, and k9s. Phase 2 documentation now describes the fallback as a bounded launch indication, and the changelog records the fix under Unreleased. --- CHANGELOG.md | 23 +++ docs/development/visual-progress-phase2.md | 53 ++++- .../winterm/invoke-visual-progress-smoke.ps1 | 47 +++++ scripts/winterm/test-visual-progress.ps1 | 53 +++++ src/cascadia/TerminalApp/Pane.cpp | 30 +++ src/cascadia/TerminalApp/Pane.h | 1 + .../WinTermVisualProgressTests.cpp | 167 ++++++++++++++++ .../VisualProgress/RainbowArcRenderer.h | 183 +++++++++++++++++- .../VisualProgress/VisualProgressModel.h | 94 ++++++++- 9 files changed, 639 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 258a2fc3c..763e1f8bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## Unreleased + +### Fixed + +- Long-running commands no longer animate the Visual Progress rainbow bar + forever. The OSC 133 Shell Integration fallback published at + `CommandExecuted` used to stay in an indeterminate Running state until + `CommandFinished`, so development servers (FastAPI/uvicorn, Spring Boot, + Node.js), Alternate Screen applications (k9s, vim, top, htop), `tail -f`, + `kubectl port-forward`, and similar intentionally long-running processes + looped the comet animation for their whole lifetime. The fallback is now a + bounded one-shot launch indication: the comet makes one traversal (the + existing 1,800 ms cycle, driven by a one-shot compositor batch rather than + a timer or polling loop) and then the overlay hides and releases its + animation resources. The expiration is command-generation scoped, so stale + completions cannot affect a newer command, re-entering the Alternate Screen + or rehydrating a pane cannot replay a consumed launch, and an expired + fallback cannot resurrect after a CLI provider or explicit OSC 9;4 owner + clears. Explicit progress, recognized providers, short-lived commands, and + the success/error/cancelled result presentations keep their existing + behavior. + + ## 1.3.0-beta2 - 2026-08-04 Second beta of the Command Timeline release, fixing a failure-reporting diff --git a/docs/development/visual-progress-phase2.md b/docs/development/visual-progress-phase2.md index ab8ddd2ab..b6f992aef 100644 --- a/docs/development/visual-progress-phase2.md +++ b/docs/development/visual-progress-phase2.md @@ -32,6 +32,55 @@ Progress ownership follows this order: Generic heuristic recognition cannot override explicit progress or an owned built-in provider. Clearing explicit progress reveals the current valid provider or shell fallback. A new semantic prompt resets provider ownership and bounded parser state for that pane. +## Bounded shell launch fallback + +The generic OSC 133 running state is a bounded launch indication, not a report +of the whole execution. When `CommandExecuted` starts a command that has no +explicit progress and no recognized provider, the indeterminate comet makes one +traversal; the overlay then hides and releases its continuous animation +resources. The fallback is not intended to represent the entire execution +duration: a development server (FastAPI/uvicorn, Spring Boot, Node.js), a TUI +such as k9s, vim, or top, `tail -f`, `kubectl port-forward`, and every other +intentionally long-running process keeps printing output with no permanent +decorative animation. Explicit OSC 9;4 progress and recognized CLI providers +are unaffected: they remain active for their real progress lifecycle, and +determinate values, provider stages, success, error, and cancelled +presentations keep their existing semantics. + +The one-shot is command-generation-scoped state in the progress state machine, +never a command-name or process-name heuristic: + +- Every shell lifecycle transition advances a monotonic launch generation. + `CommandExecuted` opens a new generation and publishes the launch fallback + carrying it. A repeated `CommandExecuted` observation of the same + still-running command (alternate-screen churn, pane rehydration, reconnect + re-broadcast) neither restarts a consumed launch nor opens a new generation, + so entering or leaving the Alternate Screen, resizing, focus and visibility + changes, tab switches, zoom, and renderer recreation cannot replay it. +- The renderer bounds the launch with a launch clock: one one-shot compositor + animation on a private property set whose duration is the existing + 1,800-millisecond indeterminate traversal. There is no timer, no polling + loop, and no CPU frame loop. The clock is decoupled from the visible comet, + so focus, geometry, and performance transitions cannot cut it short, and it + keeps running while a provider or explicit source owns the bar. +- When the clock completes, the state machine expires the fallback only if the + captured generation is still current and the stored shell state is still the + running launch; stale completions from an earlier command are inert. The + expiration publishes a silent Hidden snapshot with Running status, so + accessibility announces neither a fake success nor a fake cancellation, and + a later provider or explicit-progress clear cannot resurrect the expired + fallback. +- `CommandFinished` supersedes the launch immediately. Short commands keep + their existing completion, error, and cancellation presentations, and a + long-running command may still present its real result long after the + launch expired. + +Reduced Motion, High Contrast, and degraded static tiers keep the same logical +one-shot lifecycle: the static launch presentation is bounded by the same +clock without requiring continuous animation. Only the solid XAML fallback +tier, where the compositor is unavailable, retains the pre-existing behavior +of ending the fallback at the next shell lifecycle transition. + ## Rainbow Arc Weld renderer The WinTerm-owned renderer is under `src/winterm/VisualProgress/`. Its renderer-independent state and constants are separated from the small Pane integration boundary so timing, status transitions, degradation, and resource budgets can be tested without XAML. @@ -54,7 +103,7 @@ The centralized geometry uses a 10-DIP horizontal inset, an 8-DIP bottom inset, The rainbow is a coherent red-to-orange-to-yellow-to-green-to-cyan-to-blue-to-violet-to-magenta gradient. Its cached brush moves on a 2,000-millisecond cycle and is never rebuilt per frame. Determinate updates normally interpolate for 220 milliseconds. A real regression uses an intentional 240-millisecond phase-reset transition; the renderer does not invent a monotonic value. Zero percent keeps the welding head inside the track, while the fill remains clipped and the bloom drawing space remains available at 100 percent. -Indeterminate progress uses a welding-head comet with a continuous tail covering 25 percent of the track. It traverses the track in 1,800 milliseconds, fades cleanly at the right edge, and reappears at the left without showing a fabricated percentage. +Indeterminate progress uses a welding-head comet with a continuous tail covering 25 percent of the track. It traverses the track in 1,800 milliseconds, fades cleanly at the right edge, and reappears at the left without showing a fabricated percentage. Explicit and provider indeterminate progress repeat the traversal continuously; the Shell Integration launch fallback runs it once and parks off-track, transparent, until the launch clock publishes the Hidden snapshot that ends the launch. ### Status presentations @@ -213,7 +262,7 @@ winTerm pane: .\scripts\winterm\invoke-visual-progress-smoke.ps1 ``` -The script uses only PowerShell output and OSC sequences. It requires no Docker, Node, Python, Maven, Gradle, provider CLI, network access, or download. It exercises determinate values at 0, 1, 50, 99, and 100 percent; a real regression; indeterminate, waiting, success, error, cancellation, and clear; and sanitized carriage-return samples for every built-in provider and the generic fallback. +The script uses only PowerShell output and OSC sequences. It requires no Docker, Node, Python, Maven, Gradle, provider CLI, network access, or download. It exercises determinate values at 0, 1, 50, 99, and 100 percent; a real regression; indeterminate, waiting, success, error, cancellation, and clear; the one-shot launch fallback for a long-running command, including alternate-screen entry and re-entry that must not replay it; and sanitized carriage-return samples for every built-in provider and the generic fallback. For replacement testing, first run with `visualProgress.replaceRecognizedOutput` false and confirm every synthetic line diff --git a/scripts/winterm/invoke-visual-progress-smoke.ps1 b/scripts/winterm/invoke-visual-progress-smoke.ps1 index 0404b757a..4cd4cd2a3 100644 --- a/scripts/winterm/invoke-visual-progress-smoke.ps1 +++ b/scripts/winterm/invoke-visual-progress-smoke.ps1 @@ -34,6 +34,17 @@ function Send-Osc Wait-DemoStep } +function Send-ControlSequence +{ + param( + [Parameter(Mandatory)] + [string]$Payload + ) + + [Console]::Write("$([char]27)$Payload") + Wait-DemoStep +} + function Write-TransientFrame { param( @@ -180,6 +191,16 @@ Visual Progress manual checks: - Minimize, switch tabs, or deactivate the window to verify animation pauses or simplifies. - Rerun after disabling Windows animations to verify the static Reduced Motion fallback. - Rerun with a Windows contrast theme to verify the solid High Contrast fallback. +- For real long-running processes (a FastAPI/uvicorn dev server, a Spring Boot service, a Node.js dev + server, `tail -f`, or `kubectl port-forward`), the launch animation plays one traversal and then + disappears; later log output must not bring it back, and stopping the process still shows the + normal terminal result presentation. +- Real Maven/Gradle progress (for example a Spring Boot build) stays owned by the provider; once only + the long-running application remains, the expired launch fallback must not reappear. +- For Alternate Screen applications (k9s, vim, top, htop), the launch animation appears at most once; + entering, leaving, and re-entering the Alternate Screen, resizing, or switching tabs must not + replay it, and exiting still produces the normal final state when available. +- A short command keeps its current behavior: completion supersedes the launch animation immediately. '@ foreach ($payload in @('9;4;1;0', '9;4;1;1', '9;4;1;50', '9;4;1;99', '9;4;1;100')) @@ -228,6 +249,32 @@ Visual Progress manual checks: Send-Osc '133;A' Send-Osc '9;4;0' + Write-Host 'Visual Progress fixture: long-running command one-shot launch' + Write-Host ' Expected: the launch animation plays one traversal, then the overlay disappears while output continues.' + Send-Osc '133;B' + Send-Osc '133;C' + for ($tick = 1; $tick -le 4; $tick++) + { + Write-Host "synthetic long-running service output tick $tick (no progress overlay expected after the first traversal)" + Wait-DemoStep + } + Write-Host 'Visual Progress fixture: the long-running command still reports its real result' + Send-Osc '133;D;0' + Send-Osc '133;A' + + Write-Host 'Visual Progress fixture: alternate screen during a long-running command' + Write-Host ' Expected: entering, leaving, and re-entering the alternate screen does not replay the launch animation.' + Send-Osc '133;B' + Send-Osc '133;C' + Send-ControlSequence '[?1049h' + Write-Host 'synthetic alternate-screen application frame' + Send-ControlSequence '[?1049l' + Send-ControlSequence '[?1049h' + Write-Host 'synthetic alternate-screen application frame after re-entry' + Send-ControlSequence '[?1049l' + Send-Osc '133;D;0' + Send-Osc '133;A' + Invoke-ProviderFixtures Invoke-BoundedSoak diff --git a/scripts/winterm/test-visual-progress.ps1 b/scripts/winterm/test-visual-progress.ps1 index e806449ef..5145a6ba3 100644 --- a/scripts/winterm/test-visual-progress.ps1 +++ b/scripts/winterm/test-visual-progress.ps1 @@ -248,6 +248,20 @@ try } Assert-NotMatches $model 'std::wstring(?!_view)|std::string(?!_view)|winrt::hstring' 'Normalized progress state text-retention boundary' + foreach ($required in @( + 'uint64_t launchGeneration{}', + 'launchGeneration == other.launchGeneration', + 'std::optional ExpireShellLaunch(const uint64_t generation) noexcept', + 'generation != _shellLaunchGeneration || _shellLaunchExpired', + '_beginShellLaunchScope', + '_resetShellLaunchScope', + 'HiddenSnapshot(ProgressStatus::Running)' + )) + { + Assert-Contains $model $required 'Bounded one-shot shell launch fallback policy' + } + Assert-Matches $model '(?s)case ShellLifecycleState::CommandExecuted:.*?if \(_shellLifecycle == ShellLifecycleState::CommandExecuted\)\s*\{\s*break;\s*\}' 'Idempotent CommandExecuted re-broadcast boundary' + $providers = @( 'DockerPull', 'DockerBuildKit', @@ -477,6 +491,24 @@ try } Assert-NotMatches $renderer '_coreWindow\s*\.\s*(?:VisibilityChanged|Activated)\s*\(' 'Authoritative XAML-Islands host-window lifecycle boundary' + foreach ($required in @( + 'using LaunchExpiredCallback = std::function', + '_synchronizeLaunchClock', + '_startLaunchClock', + '_completeLaunchClock', + '_clearLaunchClock', + '_launchClockProperties.StartAnimation(L"Progress", _launchClockAnimation)', + '_launchClockAnimation.Duration(_timeSpan(RainbowArcVisualConstants::IndeterminateCycleDuration))', + '_cometTailAnimation.IterationCount(1)', + '_cometHeadAnimation.IterationCount(1)', + '_cometHeadOpacityAnimation.IterationCount(1)' + )) + { + Assert-Contains $renderer $required 'Bounded one-shot shell launch clock' + } + Assert-Matches $renderer '(?s)const auto oneShotLaunch = _snapshot\.source == ProgressSource::ShellIntegration &&\s*_snapshot\.launchGeneration != 0;' 'One-shot launch comet source discrimination' + Assert-NotContains $renderer '1800' 'Launch timeout shares the indeterminate traversal constant instead of a raw duration' + $paneVisualStart = $source.PaneCpp.IndexOf('void Pane::_SetVisualProgressEnabled', [System.StringComparison]::Ordinal) $paneVisualEnd = $source.PaneCpp.IndexOf('void Pane::_UpdatePaneHeader', $paneVisualStart, [System.StringComparison]::Ordinal) if ($paneVisualStart -lt 0 -or $paneVisualEnd -le $paneVisualStart) @@ -750,6 +782,9 @@ try Assert-Contains $taskbarProgress 'ApplyTaskbar' 'OSC 9;4 taskbar-state progress remains active' Assert-NotContains $taskbarProgress '_visualProgressRecognizeCliProgress' 'OSC 9;4 independence from CLI recognition setting' + Assert-Matches $pane '(?s)RainbowArcRenderer::TryCreate\(.*?_OnVisualProgressRendererFault\(\);.*?\[weakThis\]\(const uint64_t launchGeneration\).*?_ExpireVisualProgressShellLaunch\(launchGeneration\);' 'Weak one-shot launch expiration callback registration' + Assert-Matches $pane '(?s)void Pane::_ExpireVisualProgressShellLaunch.*?ExpireShellLaunch\(launchGeneration\).*?_QueueVisualProgressUpdate' 'Launch expiration routed through the state machine and UI mailbox' + $terminalPage = $source.TerminalPageCpp $registerStart = $terminalPage.IndexOf('void TerminalPage::_RegisterTabEvents', [System.StringComparison]::Ordinal) $registerEnd = $terminalPage.IndexOf('void TerminalPage::_UnZoomIfNeeded', $registerStart, [System.StringComparison]::Ordinal) @@ -1162,6 +1197,10 @@ try 'SuppressDuplicateState', 'ProviderProgressPrecedesShellLifecycle', 'StandardProgressPrecedesProviderAndFallsBack', + 'ShellLaunchFallbackIsOneShotPerCommand', + 'ShellLaunchExpirationIgnoresStaleGenerations', + 'ExpiredShellLaunchDoesNotResurrectAfterOwnershipClears', + 'ShellLaunchInvalidationOnResetDisableAndClose', 'ProviderStatePackingContainsOnlyStructuralFields', 'RecognitionClassifiesProvidersAndGenericFallback', 'RecognitionHandlesFragmentationAndMalformedInput', @@ -1278,6 +1317,14 @@ try '[ValidateRange(0, 10000)]', 'SoakIterations', 'synthetic summary; must remain visible', + 'long-running command one-shot launch', + 'alternate screen during a long-running command', + "Send-ControlSequence '[?1049h'", + "Send-ControlSequence '[?1049l'", + 'launch animation plays one traversal', + 'expired launch fallback must not reappear', + 'appears at most once', + 'completion supersedes the launch animation immediately', 'No files or external commands were used.' )) { @@ -1317,6 +1364,12 @@ try 'generic output is never suppressible', 'alternate-screen output', 'lifecycle-generation changes', + '## Bounded shell launch fallback', + 'bounded launch indication', + 'not intended to represent the entire execution', + 'launch clock', + 'command-generation-scoped state', + 'cannot resurrect the expired', 'does not upload or persist terminal content', 'WINTERM_DISABLE_VISUAL_PROGRESS=1', 'invoke-visual-progress-smoke.ps1', diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 26a425a2d..13d413c3f 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -2572,6 +2572,12 @@ void Pane::_CreateVisualProgressOverlay() { pane->_OnVisualProgressRendererFault(); } + }, + [weakThis](const uint64_t launchGeneration) { + if (const auto pane = weakThis.lock()) + { + pane->_ExpireVisualProgressShellLaunch(launchGeneration); + } }); const auto rendererReady = _visualProgressRenderer && !_visualProgressRenderer->Faulted(); _visualProgressRendererReady.store(rendererReady, std::memory_order_release); @@ -2687,6 +2693,30 @@ void Pane::_UpdateVisualProgressFromProvider() } } +// Invoked on the UI thread when the renderer's one-shot launch clock +// completes. The state machine decides under its own lock whether the +// captured generation still identifies the current command; the resulting +// snapshot is queued through the mailbox like every other progress update, +// so no state-machine lock is ever held while renderer or XAML code runs. +void Pane::_ExpireVisualProgressShellLaunch(const uint64_t launchGeneration) noexcept +{ + if (!_visualProgressEnabled.load(std::memory_order_acquire)) + { + return; + } + try + { + if (const auto snapshot = _visualProgressState.ExpireShellLaunch(launchGeneration)) + { + _QueueVisualProgressUpdate(*snapshot); + } + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + } +} + void Pane::_ConfigureVisualProgressRecognition() noexcept { try diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 50a97f6a5..6a93d86e1 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -370,6 +370,7 @@ class Pane : public std::enable_shared_from_this void _UpdateVisualProgressFromTaskbar(); void _UpdateVisualProgressFromShellIntegration(); void _UpdateVisualProgressFromProvider(); + void _ExpireVisualProgressShellLaunch(uint64_t launchGeneration) noexcept; void _ConfigureVisualProgressRecognition() noexcept; void _QueueVisualProgressUpdate(const winTerm::VisualProgress::ProgressSnapshot& snapshot); void _ScheduleVisualProgressUpdate(); diff --git a/src/cascadia/UnitTests_SettingsModel/WinTermVisualProgressTests.cpp b/src/cascadia/UnitTests_SettingsModel/WinTermVisualProgressTests.cpp index 45c5c8d3b..b7baab637 100644 --- a/src/cascadia/UnitTests_SettingsModel/WinTermVisualProgressTests.cpp +++ b/src/cascadia/UnitTests_SettingsModel/WinTermVisualProgressTests.cpp @@ -54,6 +54,10 @@ namespace SettingsModelUnitTests TEST_METHOD(SparkPoolsEnforcePaneAndGlobalCaps); TEST_METHOD(BackgroundAndHiddenPanesDoNotRequestSparkWork); TEST_METHOD(CommandCompletionClearsAtNextPrompt); + TEST_METHOD(ShellLaunchFallbackIsOneShotPerCommand); + TEST_METHOD(ShellLaunchExpirationIgnoresStaleGenerations); + TEST_METHOD(ExpiredShellLaunchDoesNotResurrectAfterOwnershipClears); + TEST_METHOD(ShellLaunchInvalidationOnResetDisableAndClose); TEST_METHOD(EmergencyOverridePrecedesSetting); TEST_METHOD(DisabledFeatureIgnoresEvents); TEST_METHOD(MultiplePanesRemainIndependent); @@ -1888,6 +1892,169 @@ namespace SettingsModelUnitTests VERIFY_IS_FALSE(state.Current().visible); } + void WinTermVisualProgressTests::ShellLaunchFallbackIsOneShotPerCommand() + { + ProgressStateMachine state; + state.SetEnabled(true); + state.ApplyShellLifecycle(ShellLifecycleState::CommandStart, -1); + + const auto launch = state.ApplyShellLifecycle(ShellLifecycleState::CommandExecuted, -1); + VERIFY_IS_TRUE(launch.has_value()); + VERIFY_ARE_EQUAL(static_cast(ProgressSource::ShellIntegration), static_cast(launch->source)); + VERIFY_ARE_EQUAL(static_cast(ProgressMode::Indeterminate), static_cast(launch->mode)); + VERIFY_ARE_EQUAL(static_cast(ProgressStatus::Running), static_cast(launch->status)); + VERIFY_IS_TRUE(launch->launchGeneration != 0); + + // Alternate-screen churn, rehydration, and reconnect re-broadcasts + // re-observe the same still-running command. The launch generation + // must not advance, so the one-shot cannot be re-armed. + VERIFY_IS_FALSE(state.ApplyShellLifecycle(ShellLifecycleState::CommandExecuted, -1).has_value()); + VERIFY_ARE_EQUAL(launch->launchGeneration, state.Current().launchGeneration); + + // One traversal completed while the command keeps running: the + // fallback expires to Hidden -- never to Success -- and the Running + // status keeps the hidden snapshot free of cancellation semantics. + const auto expired = state.ExpireShellLaunch(launch->launchGeneration); + VERIFY_IS_TRUE(expired.has_value()); + VERIFY_IS_FALSE(expired->visible); + VERIFY_ARE_EQUAL(static_cast(ProgressMode::Hidden), static_cast(expired->mode)); + VERIFY_ARE_EQUAL(static_cast(ProgressStatus::Running), static_cast(expired->status)); + + // Long-running output and alternate-screen transitions after the + // expiration re-observe the same command and must not replay it, and + // a duplicate completion is inert. + VERIFY_IS_FALSE(state.ApplyShellLifecycle(ShellLifecycleState::CommandExecuted, -1).has_value()); + VERIFY_IS_FALSE(state.Current().visible); + VERIFY_IS_FALSE(state.ExpireShellLaunch(launch->launchGeneration).has_value()); + + // The eventual real result still presents with unchanged semantics. + const auto finished = state.ApplyShellLifecycle(ShellLifecycleState::CommandFinished, 0); + VERIFY_IS_TRUE(finished.has_value()); + VERIFY_ARE_EQUAL(static_cast(ProgressStatus::Success), static_cast(finished->status)); + VERIFY_ARE_EQUAL(uint8_t{ 100 }, finished->value); + VERIFY_IS_FALSE(state.ApplyShellLifecycle(ShellLifecycleState::Prompt, -1)->visible); + } + + void WinTermVisualProgressTests::ShellLaunchExpirationIgnoresStaleGenerations() + { + ProgressStateMachine state; + state.SetEnabled(true); + + const auto first = state.ApplyShellLifecycle(ShellLifecycleState::CommandExecuted, -1); + const auto firstGeneration = first->launchGeneration; + + // A short command finishes before the traversal ends: completion + // supersedes the launch immediately and the late expiration is inert. + const auto finished = state.ApplyShellLifecycle(ShellLifecycleState::CommandFinished, 1); + VERIFY_ARE_EQUAL(static_cast(ProgressStatus::Error), static_cast(finished->status)); + VERIFY_IS_FALSE(state.ExpireShellLaunch(firstGeneration).has_value()); + VERIFY_ARE_EQUAL(static_cast(ProgressStatus::Error), static_cast(state.Current().status)); + + // The next command opens a new generation and allows one new launch. + state.ApplyShellLifecycle(ShellLifecycleState::Prompt, -1); + state.ApplyShellLifecycle(ShellLifecycleState::CommandStart, -1); + const auto second = state.ApplyShellLifecycle(ShellLifecycleState::CommandExecuted, -1); + VERIFY_IS_TRUE(second.has_value()); + VERIFY_IS_TRUE(second->launchGeneration != 0); + VERIFY_ARE_NOT_EQUAL(firstGeneration, second->launchGeneration); + + // A stale completion captured for the earlier command can never hide + // the newer command's launch, and zero is never a valid generation. + VERIFY_IS_FALSE(state.ExpireShellLaunch(firstGeneration).has_value()); + VERIFY_IS_FALSE(state.ExpireShellLaunch(0).has_value()); + VERIFY_IS_TRUE(state.Current().visible); + VERIFY_ARE_EQUAL(static_cast(ProgressStatus::Running), static_cast(state.Current().status)); + + // The current generation expires exactly once. + VERIFY_IS_TRUE(state.ExpireShellLaunch(second->launchGeneration).has_value()); + VERIFY_IS_FALSE(state.ExpireShellLaunch(second->launchGeneration).has_value()); + } + + void WinTermVisualProgressTests::ExpiredShellLaunchDoesNotResurrectAfterOwnershipClears() + { + ProgressStateMachine state; + state.SetEnabled(true); + const auto launch = state.ApplyShellLifecycle(ShellLifecycleState::CommandExecuted, -1); + + // A recognized provider takes ownership while the launch is active. + const ProviderProgress provider{ + ProgressProvider::Maven, + ProgressMode::Determinate, + ProgressStatus::Running, + 40, + ProviderConfidence::High, + true, + false, + false, + 2, + 1, + }; + const auto owned = state.ApplyProvider(provider); + VERIFY_IS_TRUE(owned.has_value()); + VERIFY_ARE_EQUAL(static_cast(ProgressSource::Provider), static_cast(owned->source)); + + // The one-shot duration elapses in the background. Nothing visible + // changes, but the stored fallback must be expired -- not merely left + // unpainted -- so the later ownership release cannot resurrect it. + VERIFY_IS_FALSE(state.ExpireShellLaunch(launch->launchGeneration).has_value()); + const auto afterProvider = state.ResetProvider(); + VERIFY_IS_TRUE(afterProvider.has_value()); + VERIFY_IS_FALSE(afterProvider->visible); + VERIFY_ARE_EQUAL(static_cast(ProgressMode::Hidden), static_cast(afterProvider->mode)); + VERIFY_ARE_EQUAL(static_cast(ProgressStatus::Running), static_cast(afterProvider->status)); + + // The same rule holds when explicit OSC 9;4 progress owned the bar + // while the launch expired underneath it. + state.ApplyShellLifecycle(ShellLifecycleState::Prompt, -1); + state.ApplyShellLifecycle(ShellLifecycleState::CommandStart, -1); + const auto second = state.ApplyShellLifecycle(ShellLifecycleState::CommandExecuted, -1); + const auto explicitProgress = state.ApplyTaskbar(3, 0); + VERIFY_ARE_EQUAL(static_cast(ProgressSource::Taskbar), static_cast(explicitProgress->source)); + VERIFY_IS_FALSE(state.ExpireShellLaunch(second->launchGeneration).has_value()); + const auto afterExplicit = state.ApplyTaskbar(0, 0); + VERIFY_IS_TRUE(afterExplicit.has_value()); + VERIFY_IS_FALSE(afterExplicit->visible); + VERIFY_ARE_EQUAL(static_cast(ProgressStatus::Running), static_cast(afterExplicit->status)); + + // A provider may still begin genuinely new work after the launch + // expired; only the expired shell fallback stays retired. + const auto reowned = state.ApplyProvider(provider); + VERIFY_ARE_EQUAL(static_cast(ProgressSource::Provider), static_cast(reowned->source)); + const auto cleared = state.ResetProvider(); + VERIFY_IS_FALSE(cleared->visible); + VERIFY_ARE_EQUAL(static_cast(ProgressStatus::Running), static_cast(cleared->status)); + } + + void WinTermVisualProgressTests::ShellLaunchInvalidationOnResetDisableAndClose() + { + // Pane close and content detach reset the state machine; a pending + // completion captured before the reset must be stranded. + ProgressStateMachine detached; + detached.SetEnabled(true); + const auto detachedLaunch = detached.ApplyShellLifecycle(ShellLifecycleState::CommandExecuted, -1); + detached.Reset(); + VERIFY_IS_FALSE(detached.ExpireShellLaunch(detachedLaunch->launchGeneration).has_value()); + VERIFY_IS_FALSE(detached.Current().visible); + + // Disabling Visual Progress strands pending completions, including + // across a later re-enable. + ProgressStateMachine disabled; + disabled.SetEnabled(true); + const auto disabledLaunch = disabled.ApplyShellLifecycle(ShellLifecycleState::CommandExecuted, -1); + disabled.SetEnabled(false); + VERIFY_IS_FALSE(disabled.ExpireShellLaunch(disabledLaunch->launchGeneration).has_value()); + disabled.SetEnabled(true); + VERIFY_IS_FALSE(disabled.ExpireShellLaunch(disabledLaunch->launchGeneration).has_value()); + VERIFY_IS_FALSE(disabled.Current().visible); + + // Close strands pending completions permanently. + ProgressStateMachine closed; + closed.SetEnabled(true); + const auto closedLaunch = closed.ApplyShellLifecycle(ShellLifecycleState::CommandExecuted, -1); + closed.Close(); + VERIFY_IS_FALSE(closed.ExpireShellLaunch(closedLaunch->launchGeneration).has_value()); + } + void WinTermVisualProgressTests::EmergencyOverridePrecedesSetting() { VERIFY_IS_TRUE(IsFeatureEnabled(true, L"")); diff --git a/src/winterm/VisualProgress/RainbowArcRenderer.h b/src/winterm/VisualProgress/RainbowArcRenderer.h index 0830c6fb2..59675f0a3 100644 --- a/src/winterm/VisualProgress/RainbowArcRenderer.h +++ b/src/winterm/VisualProgress/RainbowArcRenderer.h @@ -52,10 +52,16 @@ namespace winTerm::VisualProgress { public: using FaultCallback = std::function; + // Reports that the one-shot Shell Integration launch presentation has + // run for one full traversal. The argument is the launch generation + // captured when that presentation started; the progress state machine + // uses it to discard stale completions. + using LaunchExpiredCallback = std::function; static std::shared_ptr TryCreate( const winrt::Windows::UI::Xaml::Controls::Grid& host, - FaultCallback faultCallback = {}) noexcept + FaultCallback faultCallback = {}, + LaunchExpiredCallback launchExpiredCallback = {}) noexcept { if (!host) { @@ -64,7 +70,7 @@ namespace winTerm::VisualProgress try { - auto renderer = std::shared_ptr{ new RainbowArcRenderer{ std::move(faultCallback) } }; + auto renderer = std::shared_ptr{ new RainbowArcRenderer{ std::move(faultCallback), std::move(launchExpiredCallback) } }; if (!renderer->_initialize(host)) { return nullptr; @@ -106,6 +112,7 @@ namespace winTerm::VisualProgress _applyPerformanceDecision(_evaluatePerformancePolicy()); auto plan = _renderState.Apply(snapshot, _environment, now); _applyWithDegradation(plan, true); + _synchronizeLaunchClock(); } void SetPaneActive(const bool active) noexcept @@ -220,6 +227,7 @@ namespace winTerm::VisualProgress } _closed = true; _faultCallback = {}; + _launchExpiredCallback = {}; try { @@ -227,6 +235,7 @@ namespace winTerm::VisualProgress _stopAllAnimations(); _releaseAllSparks(); _clearTerminalBatch(); + _clearLaunchClock(); _renderState.Close(); if (_host) @@ -306,9 +315,10 @@ namespace winTerm::VisualProgress bool ambient{}; }; - explicit RainbowArcRenderer(FaultCallback faultCallback) noexcept : + RainbowArcRenderer(FaultCallback faultCallback, LaunchExpiredCallback launchExpiredCallback) noexcept : _sparkPool{ _sharedSparkBudget }, - _faultCallback{ std::move(faultCallback) } + _faultCallback{ std::move(faultCallback) }, + _launchExpiredCallback{ std::move(launchExpiredCallback) } { // Visible is a safe presentation default. Focus deliberately // starts false so no continuous work or sparks begin before the @@ -1410,22 +1420,40 @@ namespace winTerm::VisualProgress _cometTail.Brush(_cometBrush); _cometTail.Offset({ startX, 0.0f, 0.0f }); + // The Shell Integration launch fallback is a bounded one-shot: its + // comet makes a single traversal and parks off-track, transparent. + // Every other indeterminate owner (explicit OSC 9;4, providers) + // keeps the continuous traversal. The launch clock, not this + // visual, publishes the Hidden snapshot that ends the launch. + const auto oneShotLaunch = _snapshot.source == ProgressSource::ShellIntegration && + _snapshot.launchGeneration != 0; + const auto iterationBehavior = oneShotLaunch ? + WUC::AnimationIterationBehavior::Count : + WUC::AnimationIterationBehavior::Forever; + _cometTailAnimation.InsertKeyFrame(0.0f, { startX, 0.0f, 0.0f }); _cometTailAnimation.InsertKeyFrame(1.0f, { endX, 0.0f, 0.0f }); _cometTailAnimation.Duration(_timeSpan(RainbowArcVisualConstants::IndeterminateCycleDuration)); - _cometTailAnimation.IterationBehavior(WUC::AnimationIterationBehavior::Forever); + _cometTailAnimation.IterationBehavior(iterationBehavior); _cometHeadAnimation.InsertKeyFrame(0.0f, { RainbowArcVisualConstants::HorizontalInset, _headY, 0.0f }); _cometHeadAnimation.InsertKeyFrame(1.0f, { RainbowArcVisualConstants::HorizontalInset + _trackWidth + tailWidth, _headY, 0.0f }); _cometHeadAnimation.Duration(_timeSpan(RainbowArcVisualConstants::IndeterminateCycleDuration)); - _cometHeadAnimation.IterationBehavior(WUC::AnimationIterationBehavior::Forever); + _cometHeadAnimation.IterationBehavior(iterationBehavior); _cometHeadOpacityAnimation.InsertKeyFrame(0.0f, 0.0f); _cometHeadOpacityAnimation.InsertKeyFrame(0.06f, 1.0f); _cometHeadOpacityAnimation.InsertKeyFrame(0.91f, 1.0f); _cometHeadOpacityAnimation.InsertKeyFrame(1.0f, 0.0f); _cometHeadOpacityAnimation.Duration(_timeSpan(RainbowArcVisualConstants::IndeterminateCycleDuration)); - _cometHeadOpacityAnimation.IterationBehavior(WUC::AnimationIterationBehavior::Forever); + _cometHeadOpacityAnimation.IterationBehavior(iterationBehavior); + + if (oneShotLaunch) + { + _cometTailAnimation.IterationCount(1); + _cometHeadAnimation.IterationCount(1); + _cometHeadOpacityAnimation.IterationCount(1); + } if (!_indeterminateRunning) { @@ -1982,6 +2010,136 @@ namespace winTerm::VisualProgress _terminalBatch = nullptr; } + // The launch clock bounds the Shell Integration launch fallback to one + // comet traversal of wall time. It is a single one-shot composition + // animation on a private property set: no timer, no polling loop, and + // no coupling to the visible comet, whose animations stop and restart + // with focus, geometry, and performance changes. It keeps running when + // a provider or explicit source takes the presentation over, so the + // stored fallback still expires in the background; the state machine's + // generation checks decide whether a completion still matters. + void _synchronizeLaunchClock() noexcept + { + const auto launch = _snapshot.visible && + _snapshot.source == ProgressSource::ShellIntegration && + _snapshot.mode == ProgressMode::Indeterminate && + _snapshot.status == ProgressStatus::Running && + _snapshot.launchGeneration != 0; + if (launch) + { + if (_activeLaunchGeneration != _snapshot.launchGeneration) + { + _startLaunchClock(_snapshot.launchGeneration); + } + return; + } + + // Shell terminal presentations and hidden snapshots end the launch + // scope; any queued completion is already stale by generation. + if (_snapshot.source == ProgressSource::ShellIntegration || _snapshot.mode == ProgressMode::Hidden) + { + _clearLaunchClock(); + } + } + + void _startLaunchClock(const uint64_t launchGeneration) noexcept + { + _clearLaunchClock(); + if (_closed || _faulted || !_compositor || !_launchExpiredCallback) + { + return; + } + + try + { + if (!_launchClockProperties) + { + _launchClockProperties = _compositor.CreatePropertySet(); + _launchClockProperties.InsertScalar(L"Progress", 0.0f); + } + if (!_launchClockAnimation) + { + _launchClockAnimation = _compositor.CreateScalarKeyFrameAnimation(); + _launchClockAnimation.InsertKeyFrame(1.0f, 1.0f); + _launchClockAnimation.Duration(_timeSpan(RainbowArcVisualConstants::IndeterminateCycleDuration)); + _launchClockAnimation.IterationBehavior(WUC::AnimationIterationBehavior::Count); + _launchClockAnimation.IterationCount(1); + } + + _launchBatch = _compositor.CreateScopedBatch(WUC::CompositionBatchTypes::Animation); + _launchClockProperties.StartAnimation(L"Progress", _launchClockAnimation); + _launchBatch.End(); + _activeLaunchGeneration = launchGeneration; + + const auto weak = weak_from_this(); + _launchBatchToken = _launchBatch.Completed([weak, launchGeneration](auto&&, auto&&) { + if (const auto self = weak.lock()) + { + self->_completeLaunchClock(launchGeneration); + } + }); + _launchBatchSubscribed = true; + } + catch (...) + { + // The clock only bounds a decorative fallback. Without it the + // fallback keeps its pre-existing lifetime, ending at the next + // shell lifecycle transition. + _clearLaunchClock(); + } + } + + void _completeLaunchClock(const uint64_t launchGeneration) noexcept + { + if (_closed || !_launchBatchSubscribed || _activeLaunchGeneration != launchGeneration) + { + return; + } + _clearLaunchClock(); + + const auto callback = _launchExpiredCallback; + if (callback) + { + try + { + callback(launchGeneration); + } + catch (...) + { + // Expiration is advisory and must remain fail-open. + } + } + } + + void _clearLaunchClock() noexcept + { + // Invalidate first so a queued completion from a replaced batch is + // stale even when its replacement carries the same generation. + _activeLaunchGeneration = 0; + try + { + if (_launchBatchSubscribed && _launchBatch) + { + _launchBatch.Completed(_launchBatchToken); + } + } + catch (...) + { + } + _launchBatchSubscribed = false; + _launchBatch = nullptr; + try + { + if (_launchClockProperties) + { + _launchClockProperties.StopAnimation(L"Progress"); + } + } + catch (...) + { + } + } + void _stopStatusAnimations() noexcept { try @@ -2209,6 +2367,9 @@ namespace winTerm::VisualProgress void _releaseCompositionHandles() noexcept { _clearTerminalBatch(); + _clearLaunchClock(); + _launchClockProperties = nullptr; + _launchClockAnimation = nullptr; _root = nullptr; _trackVisual = nullptr; _trackGeometry = nullptr; @@ -2347,6 +2508,14 @@ namespace winTerm::VisualProgress bool _terminalBatchSubscribed{}; bool _terminalPresentationCompleted{}; + LaunchExpiredCallback _launchExpiredCallback; + WUC::CompositionPropertySet _launchClockProperties{ nullptr }; + WUC::ScalarKeyFrameAnimation _launchClockAnimation{ nullptr }; + WUC::CompositionScopedBatch _launchBatch{ nullptr }; + winrt::event_token _launchBatchToken{}; + uint64_t _activeLaunchGeneration{}; + bool _launchBatchSubscribed{}; + WUVM::UISettings _uiSettings{ nullptr }; WUVM::AccessibilitySettings _accessibilitySettings{ nullptr }; winrt::event_token _loadedToken{}; diff --git a/src/winterm/VisualProgress/VisualProgressModel.h b/src/winterm/VisualProgress/VisualProgressModel.h index 0079ad430..20eaec6fb 100644 --- a/src/winterm/VisualProgress/VisualProgressModel.h +++ b/src/winterm/VisualProgress/VisualProgressModel.h @@ -134,6 +134,12 @@ namespace winTerm::VisualProgress bool transient{}; bool suppressible{}; uint16_t stage{}; + // Identifies the OSC 133 command a Shell Integration launch fallback + // belongs to. Zero for every other source and shell state. The + // renderer captures it when the launch presentation starts and hands + // it back through ExpireShellLaunch, which makes stale one-shot + // completions inert. + uint64_t launchGeneration{}; bool SamePresentation(const ProgressSnapshot& other) const noexcept { @@ -146,7 +152,8 @@ namespace winTerm::VisualProgress confidence == other.confidence && transient == other.transient && suppressible == other.suppressible && - stage == other.stage; + stage == other.stage && + launchGeneration == other.launchGeneration; } }; @@ -173,6 +180,7 @@ namespace winTerm::VisualProgress _explicitSnapshot = {}; _providerSnapshot.reset(); _shellSnapshot.reset(); + _resetShellLaunchScope(); return _emit(HiddenSnapshot(ProgressStatus::Cancelled)); } return std::nullopt; @@ -254,7 +262,7 @@ namespace winTerm::VisualProgress } _providerSnapshot.reset(); - return _explicitActive ? std::nullopt : _emit(_shellSnapshot.value_or(HiddenSnapshot())); + return _explicitActive ? std::nullopt : _emit(_fallbackSnapshot()); } std::optional ApplyShellLifecycle(const ShellLifecycleState state, const int64_t exitCode) noexcept @@ -270,18 +278,35 @@ namespace winTerm::VisualProgress case ShellLifecycleState::Prompt: _providerSnapshot.reset(); _shellSnapshot.reset(); + _beginShellLaunchScope(); break; case ShellLifecycleState::CommandStart: // OSC 133;B: the user is composing input at an interactive // prompt. Nothing is executing, so an idle prompt must never // animate a bar; only CommandExecuted starts one. _shellSnapshot.reset(); + _beginShellLaunchScope(); break; case ShellLifecycleState::CommandExecuted: + // The launch fallback is a bounded one-shot per command. A + // repeated CommandExecuted observation (pane rehydration, + // alternate-screen churn, reconnect re-broadcast) belongs to + // the same still-running command, so it must neither restart + // a consumed launch nor open a new launch generation. + if (_shellLifecycle == ShellLifecycleState::CommandExecuted) + { + break; + } + _beginShellLaunchScope(); _shellSnapshot = ProgressSnapshot{ ProgressMode::Indeterminate, ProgressStatus::Running, 0, true, ProgressSource::ShellIntegration, 0 }; + _shellSnapshot->launchGeneration = _shellLaunchGeneration; break; case ShellLifecycleState::CommandFinished: _providerSnapshot.reset(); + // Completion supersedes the launch immediately and advances + // the launch scope, so a one-shot completion still in flight + // can never disturb this terminal presentation. + _beginShellLaunchScope(); _shellSnapshot = ProgressSnapshot{ ProgressMode::Determinate, exitCode > 0 ? ProgressStatus::Error : ProgressStatus::Success, @@ -296,6 +321,36 @@ namespace winTerm::VisualProgress return std::nullopt; } + _shellLifecycle = state; + return _explicitActive ? std::nullopt : _emit(_fallbackSnapshot()); + } + + // Called when the renderer's one-shot launch presentation has run for + // one full traversal. The captured generation makes stale completions + // inert: a callback from an earlier command can never hide progress + // belonging to a newer command. Expiration clears the stored fallback + // even while a provider or explicit source owns the presentation, so + // a later ownership release cannot resurrect an expired launch. + std::optional ExpireShellLaunch(const uint64_t generation) noexcept + { + std::scoped_lock lock{ _mutex }; + if (!_enabled || _closed || generation == 0 || + generation != _shellLaunchGeneration || _shellLaunchExpired) + { + return std::nullopt; + } + if (!_shellSnapshot || + _shellSnapshot->source != ProgressSource::ShellIntegration || + _shellSnapshot->mode != ProgressMode::Indeterminate || + _shellSnapshot->status != ProgressStatus::Running) + { + // The launch fallback was already replaced; success, error, + // and cancelled presentations are never expired. + return std::nullopt; + } + + _shellLaunchExpired = true; + _shellSnapshot.reset(); return _explicitActive ? std::nullopt : _emit(_fallbackSnapshot()); } @@ -306,6 +361,7 @@ namespace winTerm::VisualProgress _explicitSnapshot = {}; _providerSnapshot.reset(); _shellSnapshot.reset(); + _resetShellLaunchScope(); return _emit(HiddenSnapshot()); } @@ -322,6 +378,7 @@ namespace winTerm::VisualProgress _explicitSnapshot = {}; _providerSnapshot.reset(); _shellSnapshot.reset(); + _resetShellLaunchScope(); return _emit(HiddenSnapshot(ProgressStatus::Cancelled)); } @@ -343,7 +400,35 @@ namespace winTerm::VisualProgress { return *_providerSnapshot; } - return _shellSnapshot.value_or(HiddenSnapshot()); + if (_shellSnapshot) + { + return *_shellSnapshot; + } + // An expired launch hides the bar while its command keeps + // running. Running status keeps that hidden snapshot silent; + // Cancelled would raise a fake interruption announcement through + // the accessibility policy even though nothing was interrupted. + if (_shellLifecycle == ShellLifecycleState::CommandExecuted && _shellLaunchExpired) + { + return HiddenSnapshot(ProgressStatus::Running); + } + return HiddenSnapshot(); + } + + // Every shell lifecycle transition opens a new launch scope: the + // generation advance strands completion callbacks captured for an + // earlier scope, and the cleared flag re-arms the one-shot for the + // next CommandExecuted. + void _beginShellLaunchScope() noexcept + { + ++_shellLaunchGeneration; + _shellLaunchExpired = false; + } + + void _resetShellLaunchScope() noexcept + { + _beginShellLaunchScope(); + _shellLifecycle = ShellLifecycleState::None; } uint8_t _meaningfulValue(const uint8_t value) const noexcept @@ -370,7 +455,10 @@ namespace winTerm::VisualProgress bool _enabled{}; bool _closed{}; bool _explicitActive{}; + bool _shellLaunchExpired{}; + ShellLifecycleState _shellLifecycle{ ShellLifecycleState::None }; uint64_t _sequence{}; + uint64_t _shellLaunchGeneration{}; ProgressSnapshot _current{}; ProgressSnapshot _explicitSnapshot{}; std::optional _providerSnapshot; From feb2687e5d088e0a2accf2100dbda0e2fe65fa56 Mon Sep 17 00:00:00 2001 From: HelloThisWorld Date: Wed, 5 Aug 2026 21:06:41 +0800 Subject: [PATCH 2/2] release: prepare winTerm 1.3.0-beta3 prerelease Third beta of the Command Timeline release, carrying the beta2 field-report fix: the Visual Progress Shell Integration fallback is now a bounded one-shot launch indication, so long-running commands (k9s, vim, top, FastAPI/uvicorn, Spring Boot, Node dev servers, tail -f, kubectl port-forward) no longer animate the rainbow bar forever. Application version 1.3.0-beta3, package/file version 1.3.0.6, PowerShell module 1.3.0 with prerelease suffix beta3, channel beta, tag v1.3.0-beta3. Published as a GitHub prerelease: Latest and WinGet keep pointing at v1.2.0, and the beta is listed on the winTerm website next to the stable download. --- CHANGELOG.md | 12 +- README.md | 8 +- docs/current-progress.md | 53 +++-- docs/releases/1.3.0-beta3.md | 189 ++++++++++++++++++ scripts/winterm/package-shell-assets.ps1 | 4 +- scripts/winterm/test-visual-progress.ps1 | 34 ++-- scripts/winterm/test.ps1 | 2 +- scripts/winterm/verify-branding.ps1 | 2 +- scripts/winterm/verify-version.ps1 | 16 +- .../winTerm.Shell/winTerm.Shell.psd1 | 2 +- shell/shared/version.json | 4 +- .../Package-winTerm.appxmanifest | 2 +- .../WindowsTerminal/WindowsTerminal.rc | 8 +- src/cascadia/wt/wt.rc | 8 +- .../winterm-shim/winterm-shim.rc | 8 +- src/winterm/Branding/ReleaseMetadata.h | 2 +- src/winterm/Branding/version.json | 8 +- .../Workspaces/Model/WorkspaceDescriptor.h | 2 +- .../Persistence/WorkspaceSerializer.cpp | 2 +- 19 files changed, 293 insertions(+), 73 deletions(-) create mode 100644 docs/releases/1.3.0-beta3.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 763e1f8bb..6f972fdd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog -## Unreleased +## 1.3.0-beta3 - 2026-08-05 + +Third beta of the Command Timeline release, fixing the beta2 field report +that long-running commands animate the Visual Progress bar forever. Like the +earlier betas, this is published as a GitHub prerelease; GitHub Latest and +WinGet continue to point at v1.2.0 until a stable 1.3.0. ### Fixed @@ -22,6 +27,11 @@ the success/error/cancelled result presentations keep their existing behavior. +### Changed + +- Advanced the beta: application version `1.3.0-beta3`, package/file version + `1.3.0.6`, PowerShell module `1.3.0` with prerelease suffix `beta3`. + ## 1.3.0-beta2 - 2026-08-04 diff --git a/README.md b/README.md index 666fe75d7..a640cff0c 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ notes, and checksums together. A **beta** channel is also available for early access to the Command Timeline: -[v1.3.0-beta2](https://github.com/HelloThisWorld/winTerm/releases/tag/v1.3.0-beta2) +[v1.3.0-beta3](https://github.com/HelloThisWorld/winTerm/releases/tag/v1.3.0-beta3) is published as a GitHub prerelease with the same asset layout. Betas are for testing; the stable release above stays the recommended download. @@ -35,7 +35,7 @@ application downloads are: - `winTerm--setup-x64.exe` — current-user or all-users installation; - `winTerm--portable-x64.zip` — extract and run without installation. -The current source version is `1.3.0-beta2`; +The current source version is `1.3.0-beta3`; the latest stable release is `1.2.0`. See the [latest official Release](https://github.com/HelloThisWorld/winTerm/releases/latest) for the complete published asset list and checksums. @@ -104,8 +104,8 @@ Use PowerShell 7 and the Microsoft Terminal toolchain described in .\scripts\winterm\build.ps1 -Configuration Release -Platform x64 -IncludeTests .\scripts\winterm\test.ps1 -Suite Relevant -Configuration Release -Platform x64 .\scripts\winterm\build-unpackaged.ps1 -Configuration Release -Platform x64 -.\scripts\winterm\build-installer.ps1 -Version 1.3.0-beta2 -Platform x64 -.\scripts\winterm\build-portable.ps1 -Version 1.3.0-beta2 -Platform x64 +.\scripts\winterm\build-installer.ps1 -Version 1.3.0-beta3 -Platform x64 +.\scripts\winterm\build-portable.ps1 -Version 1.3.0-beta3 -Platform x64 ``` The unpackaged generator uses an unsigned MSIX only as an upstream build diff --git a/docs/current-progress.md b/docs/current-progress.md index c5e8d432c..9448828f9 100644 --- a/docs/current-progress.md +++ b/docs/current-progress.md @@ -1,30 +1,33 @@ # Current development progress -Last updated: 2026-08-04 +Last updated: 2026-08-05 ## Repository state -- Branch: `release/1.3.0-beta2` -- Base branch: `main` at `a6ec9a415` (prompt exit code fix, pull request - #39) +- Branch: `release/v1.3.0-beta3` +- Base: `fix/visual-progress-shell-fallback-one-shot` at `baf6f8a87` + (one-shot launch fallback fix, pull request #41), based on `main` at + `39193206c` (1.3.0-beta2 release metadata, pull request #40) - Microsoft Terminal upstream revision: `1cea42d433253d95c4487a3037db48197b5e72f4` -- Application version: `1.3.0-beta2` -- Package/file version: `1.3.0.5` -- PowerShell module version: `1.3.0` with prerelease suffix `beta2` +- Application version: `1.3.0-beta3` +- Package/file version: `1.3.0.6` +- PowerShell module version: `1.3.0` with prerelease suffix `beta3` - Release channel: `beta` -- Release tag: `v1.3.0-beta2` +- Release tag: `v1.3.0-beta3` - Current public Latest: `v1.2.0`, the stable Visual Progress release - Supported target: Windows 11 x64 -`v1.3.0-beta2` is the second beta of the Command Timeline release. It fixes -a beta1 defect found while producing the website screenshots: the PowerShell -prompt wrapper reset `$?` before reading it, so every Timeline entry reported -Succeeded and a failure mark could never appear. The release workflow marks -any non-stable channel with `--prerelease` and `--latest=false`, so -`/releases/latest` keeps resolving to v1.2.0. Like beta1, the beta is listed -on the winTerm website next to the stable v1.2.0 download; it is still -skipped by the WinGet workflow. +`v1.3.0-beta3` is the third beta of the Command Timeline release. It fixes +the beta2 field report that any long-running command — k9s, vim, top, +FastAPI/uvicorn, Spring Boot, Node dev servers, `tail -f`, +`kubectl port-forward` — kept the Visual Progress rainbow animation looping +for its whole lifetime. The OSC 133 Shell Integration fallback is now a +bounded one-shot launch indication scoped by a shell command generation. The +release workflow marks any non-stable channel with `--prerelease` and +`--latest=false`, so `/releases/latest` keeps resolving to v1.2.0. Like the +earlier betas, it is listed on the winTerm website next to the stable v1.2.0 +download and is skipped by the WinGet workflow. ## Command Timeline status @@ -51,6 +54,24 @@ 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`. +## Beta2 findings + +Field testing the published beta2 build with long-running processes surfaced +one defect, fixed through pull request #41 and carried by this release +branch: + +1. The Shell Integration fallback animated forever for any command that + intentionally stays running (Alternate Screen TUIs, development servers, + `tail -f`). The fallback is now a bounded one-shot launch indication: the + comet plays one 1,800 ms traversal driven by a one-shot compositor batch + (no timer, no polling loop), then the overlay hides with a silent + Hidden/Running snapshot. The expiration is command-generation scoped, so + stale completions cannot affect a newer command, alternate-screen churn + cannot replay a consumed launch, and an expired fallback cannot resurrect + after a provider or explicit OSC 9;4 owner clears. Explicit progress, + recognized providers, short commands, and the result presentations are + unchanged. + ## Beta1 findings Producing the sanitized website screenshots against the published beta1 diff --git a/docs/releases/1.3.0-beta3.md b/docs/releases/1.3.0-beta3.md new file mode 100644 index 000000000..bda2f87ad --- /dev/null +++ b/docs/releases/1.3.0-beta3.md @@ -0,0 +1,189 @@ +# winTerm 1.3.0-beta3 + +**This is a beta prerelease.** The current stable release remains +[v1.2.0](https://github.com/HelloThisWorld/winTerm/releases/tag/v1.2.0), and +`/releases/latest` continues to point there. The beta is listed on the winTerm +website next to the stable download and is published as a GitHub prerelease; +it is not submitted to WinGet. + +winTerm 1.3.0-beta3 is the third beta of the **Command Timeline** release. +It fixes one Visual Progress defect reported from beta2 field testing and is +otherwise identical to beta2. + +## Fixed since 1.3.0-beta2 + +- **Long-running commands no longer animate the progress bar forever.** Any + command without a real progress source used to keep the Visual Progress + rainbow animation looping until it exited — effectively forever for + Alternate Screen applications (k9s, vim, top, htop), development servers + (FastAPI/uvicorn, Spring Boot, Node.js), `tail -f`, and + `kubectl port-forward`. The OSC 133 launch fallback is now a bounded + one-shot launch indication: the comet makes one traversal, then the overlay + disappears and releases its animation resources while the command keeps + printing output normally. Entering or leaving the Alternate Screen, + resizing, or switching tabs does not replay it, and the eventual real + result still shows the normal success, error, or cancelled presentation. + Explicit OSC 9;4 progress, recognized CLI providers (Docker, pip, Git, + curl, wget, npm/pnpm/yarn, nvm, Maven, Gradle), and short-lived commands + keep their existing behavior, and no command-name or process-name matching + is involved. Screen readers hear neither a fake completion nor a fake + cancellation when the launch indication ends. + +## What is new since v1.2.0: Command Timeline + +The Command Timeline is a per-pane, in-memory list of the commands that pane +has run. Open it with `Ctrl+Tab`, or with the thin auto-hiding handle on the +left edge of the terminal; click the terminal, press `Escape`, or press +`Ctrl+Tab` again to close it. + +- **Per-pane history.** Each pane keeps its own list, built from OSC 133 + shell integration only. winTerm never guesses where a prompt or output + begins, and `cmd.exe` — which does not report command boundaries — shows + `Command timeline unavailable` rather than untrustworthy results. +- **Works out of the box.** A profile whose commandline is a bare + `powershell.exe` or `pwsh.exe` (optionally with `-NoLogo`/`-NoExit`) + automatically imports the packaged `winTerm.Shell` integration module at + startup. Any customized commandline launches unchanged; the per-profile + setting `"shellIntegration.autoInject": false` turns the rewrite off. +- **Load without executing.** `Enter` or a single click puts the selected + command on the input line and stops there. No carriage return is ever sent, + the Windows clipboard is never read, and input broadcast never forwards the + load to another pane. +- **Filtering.** Press `/` or `Tab` and type to narrow the list. Matching is + a plain, case-insensitive substring match over command text only — no + wildcards, no regular expressions, no fuzzy matching, and command output is + never searched. +- **Copy and jump.** Right-click an entry to copy the command, copy its + output, or scroll to where its output begins. Output is read from the + terminal buffer only at that moment and is never cached. +- **Trustworthy status.** A command shows ● Running while it executes and + resolves to ✓ Succeeded or ✕ Failed when the shell reports its exit code. + `Unknown` appears only when the shell genuinely did not report a result, + and explains itself on hover. Status is never conveyed by color alone. +- **Readable rows.** Hover any row for the full command text; the selection + moves without rebuilding rows, so the list stays steady under the arrow + keys. + +## Keyboard + +| Shortcut | Behavior | +| --- | --- | +| `Ctrl+Tab` | Toggle the Command Timeline for the focused pane | +| `Ctrl+T` | Next tab | +| `Ctrl+Shift+T` | Previous tab | +| `Ctrl+Alt+T` | Open new tab | + +While the Timeline is open: + +| Key | Behavior | +| --- | --- | +| `/` or `Tab` | Move focus to the filter box | +| `Up` / `Down` | Move the selection by one command | +| `Left` / `Right` | Select the first / last command on the current page | +| `Enter` | Load the selected command; never runs it | +| `Space` | Scroll the terminal to that command's output | +| `Ctrl+C` | Copy the selected command text | +| `Escape` | Clear the filter, or close the Timeline if the filter is empty | + +Your own key bindings take precedence over every default above. Keys the +Timeline consumes are not sent to the shell, and filter text never reaches +the shell. + +## Settings + +```json +{ + "commandTimeline.enabled": true, + "commandTimeline.historyLimit": 500 +} +``` + +Per profile: + +```json +{ + "shellIntegration.autoInject": true +} +``` + +- **Show command timeline** defaults on. Turning it off hides the handle, + closes an open overlay, and stops `Ctrl+Tab` from opening it. +- **Commands remembered per pane** defaults to 500 and accepts 50 through + 5000. An out-of-range value is clamped rather than failing the settings + load. + +An existing settings file needs no migration. Visual Progress settings are +unchanged; the bounded launch behavior needs no new setting. + +## Changes accumulated through the prerelease series + +- alpha2: auto-injected shell integration, the auto-hiding handle, + terminal-click light dismiss, and the recognition-engine fixes for stuck + progress bars. +- alpha3: correct FinalTerm mark emission (no stray characters, exact command + capture), no progress bar at an idle prompt, no phantom Timeline row, no + selection jitter, and silent degradation when antivirus blocks an optional + module component. +- alpha4: the antivirus write-then-execute false positive removed at its + source, ✓/✕ results restored through the Enter-keypress lifecycle + notification, and full-command tooltips. +- beta2: real exit codes restored to the finished mark, so failures resolve + to ✕ Failed instead of a false ✓ Succeeded. +- beta3: the Shell Integration launch fallback is a bounded one-shot, so + long-running commands no longer animate the progress bar forever. + +## Privacy + +- Command text lives only in memory, only in the pane that ran it, and only + while that pane is open. Nothing is written to disk; there is no command + history file or database. +- Command output is never cached, indexed, or searched. +- Filter text is never saved and never leaves the pane. +- No telemetry is written, and no command, output, path, or filter text is + logged. +- The clipboard is written only by an explicit copy action, and is never + read. + +See the [privacy policy](https://github.com/HelloThisWorld/winTerm/blob/main/PRIVACY.md). + +## Unchanged in this release + +Workspace schema (2), docking model (1), shell protocol (1), theme schema +(1), update manifest schema (1), package identity, and signing policy are all +unchanged from v1.2.0. Visual Progress keeps its v1.2.0 semantics for +explicit progress, recognized providers, determinate values, and the +success/error/cancelled presentations; only the generic launch fallback is +now bounded as described above. + +## Known limitations + +- The Timeline requires OSC 133 shell integration; it stays unavailable under + `cmd.exe`. +- A multi-line command is refused when the shell has not enabled bracketed + paste, because unbracketed line breaks would be read as pressing Enter. +- Loading a command longer than 1024 characters asks for a confirming + `Enter`. +- Command history does not survive closing a pane, and is intentionally never + persisted. + +## Installation + +The Setup EXE supports current-user and all-users installation; the Portable +ZIP can be extracted to a writable directory and run without installation. +Installing the beta over v1.2.0 or an earlier prerelease is supported; the +file version is `1.3.0.6`. Package identity stays separate from Microsoft +Terminal, so it does not disturb an existing Windows Terminal installation. + +Publisher: `helloThisWorld`. Tag: `v1.3.0-beta3`. + +## Signing + +The winTerm 1.3.0-beta3 Setup EXE is not Authenticode-signed. Windows may +display Unknown Publisher or a SmartScreen warning. Download only from the +official GitHub Release and verify the file against `SHA256SUMS.txt` before +running it. + +## Policies + +- [Code signing policy](https://github.com/HelloThisWorld/winTerm/blob/main/CODE_SIGNING_POLICY.md) +- [Privacy policy](https://github.com/HelloThisWorld/winTerm/blob/main/PRIVACY.md) diff --git a/scripts/winterm/package-shell-assets.ps1 b/scripts/winterm/package-shell-assets.ps1 index e566a3187..87025cd16 100644 --- a/scripts/winterm/package-shell-assets.ps1 +++ b/scripts/winterm/package-shell-assets.ps1 @@ -33,8 +33,8 @@ foreach ($relativePath in $sourceAssets) $version = Get-Content -LiteralPath (Join-Path $repositoryRoot 'shell\shared\version.json') -Raw | ConvertFrom-Json if ($version.moduleVersion -ne '1.3.0' -or - $version.modulePrerelease -ne 'beta2' -or - $version.applicationVersion -ne '1.3.0-beta2' -or + $version.modulePrerelease -ne 'beta3' -or + $version.applicationVersion -ne '1.3.0-beta3' -or $version.protocolVersion -ne 1) { throw 'The winTerm Shell asset version metadata is invalid.' diff --git a/scripts/winterm/test-visual-progress.ps1 b/scripts/winterm/test-visual-progress.ps1 index 5145a6ba3..890af4b60 100644 --- a/scripts/winterm/test-visual-progress.ps1 +++ b/scripts/winterm/test-visual-progress.ps1 @@ -1431,12 +1431,12 @@ try $version = $source.VersionMetadata | ConvertFrom-Json $expectedVersionValues = [ordered]@{ - applicationVersion = '1.3.0-beta2' - packageVersion = '1.3.0.5' + applicationVersion = '1.3.0-beta3' + packageVersion = '1.3.0.6' moduleVersion = '1.3.0' - modulePrerelease = 'beta2' + modulePrerelease = 'beta3' channel = 'beta' - tag = 'v1.3.0-beta2' + tag = 'v1.3.0-beta3' workspaceSchemaVersion = 2 dockingModelVersion = 1 shellProtocolVersion = 1 @@ -1451,39 +1451,39 @@ try } } $shellVersion = $source.ShellVersion | ConvertFrom-Json - if ($shellVersion.applicationVersion -ne '1.3.0-beta2' -or $shellVersion.moduleVersion -ne '1.3.0' -or $shellVersion.protocolVersion -ne 1) + if ($shellVersion.applicationVersion -ne '1.3.0-beta3' -or $shellVersion.moduleVersion -ne '1.3.0' -or $shellVersion.protocolVersion -ne 1) { - throw 'Shell version metadata does not match winTerm release 1.3.0-beta2 with protocol version 1.' + throw 'Shell version metadata does not match winTerm release 1.3.0-beta3 with protocol version 1.' } foreach ($surface in @( - @{ Content = $source.ReleaseMetadata; Value = 'ApplicationVersion{ L"1.3.0-beta2" }'; Description = 'About release metadata' }, - @{ Content = $source.PackageManifest; Value = 'Version="1.3.0.5"'; Description = 'MSIX package manifest' }, - @{ Content = $source.HostResource; Value = 'FILEVERSION 1,3,0,5'; Description = 'Terminal host file version' }, - @{ Content = $source.HostResource; Value = '"ProductVersion", "1.3.0-beta2\0"'; Description = 'Terminal host display version' }, - @{ Content = $source.ShimResource; Value = 'FILEVERSION 1,3,0,5'; Description = 'Shim file version' }, - @{ Content = $source.ShimResource; Value = '"ProductVersion", "1.3.0-beta2\0"'; Description = 'Shim display version' }, + @{ Content = $source.ReleaseMetadata; Value = 'ApplicationVersion{ L"1.3.0-beta3" }'; Description = 'About release metadata' }, + @{ Content = $source.PackageManifest; Value = 'Version="1.3.0.6"'; Description = 'MSIX package manifest' }, + @{ Content = $source.HostResource; Value = 'FILEVERSION 1,3,0,6'; Description = 'Terminal host file version' }, + @{ Content = $source.HostResource; Value = '"ProductVersion", "1.3.0-beta3\0"'; Description = 'Terminal host display version' }, + @{ Content = $source.ShimResource; Value = 'FILEVERSION 1,3,0,6'; Description = 'Shim file version' }, + @{ Content = $source.ShimResource; Value = '"ProductVersion", "1.3.0-beta3\0"'; Description = 'Shim display version' }, @{ Content = $source.CustomProps; Value = '1'; Description = 'Executable major version' }, @{ Content = $source.CustomProps; Value = '3'; Description = 'Executable minor version' }, @{ Content = $source.ShellModuleManifest; Value = "ModuleVersion = '1.3.0'"; Description = 'PowerShell module manifest' }, @{ Content = $source.ShellModule; Value = "`$script:WinTermModuleVersion = '1.3.0'"; Description = 'PowerShell module runtime' }, @{ Content = $source.PackageShellAssets; Value = "'shell\shared\version.json'"; Description = 'Canonical shell version metadata packaging' }, - @{ Content = $source.WorkspaceSerializer; Value = '"1.3.0-beta2"'; Description = 'Workspace application-version fallback' } + @{ Content = $source.WorkspaceSerializer; Value = '"1.3.0-beta3"'; Description = 'Workspace application-version fallback' } )) { Assert-Contains $surface.Content $surface.Value $surface.Description } foreach ($required in @( - "applicationVersion -eq '1.3.0-beta2'", - "packageVersion -eq '1.3.0.5'", + "applicationVersion -eq '1.3.0-beta3'", + "packageVersion -eq '1.3.0.6'", "moduleVersion -eq '1.3.0'", - "tag -eq 'v1.3.0-beta2'", + "tag -eq 'v1.3.0-beta3'", "Workspace Schema version remains 2", "Docking Model version remains 1", "Shell Protocol version remains 1", "Theme Schema remains at version 1" )) { - Assert-Contains $source.VerifyVersion $required 'Authoritative v1.3.0-beta2 version validation surface' + Assert-Contains $source.VerifyVersion $required 'Authoritative v1.3.0-beta3 version validation surface' } $testBinary = Join-Path $root "bin\$Platform\$Configuration\UnitTests_SettingsModel\SettingsModel.Unit.Tests.dll" diff --git a/scripts/winterm/test.ps1 b/scripts/winterm/test.ps1 index fcc8887fc..edf9d94a4 100644 --- a/scripts/winterm/test.ps1 +++ b/scripts/winterm/test.ps1 @@ -293,7 +293,7 @@ function Test-ShellExperienceFoundations $manifest = Import-PowerShellDataFile -LiteralPath $moduleManifest if ($manifest.ModuleVersion -ne '1.3.0' -or - $manifest.PrivateData.PSData.Prerelease -ne 'beta2' -or + $manifest.PrivateData.PSData.Prerelease -ne 'beta3' -or $manifest.PowerShellVersion -ne '5.1') { throw 'The winTerm PowerShell module manifest does not declare the supported version boundary.' diff --git a/scripts/winterm/verify-branding.ps1 b/scripts/winterm/verify-branding.ps1 index a4e504562..46aaa77da 100644 --- a/scripts/winterm/verify-branding.ps1 +++ b/scripts/winterm/verify-branding.ps1 @@ -126,7 +126,7 @@ function Test-Manifest Test-Requirement -Condition ($null -ne $identity -and $identity.Name -eq 'HelloThisWorld.winTerm') -Message "$Path uses package identity HelloThisWorld.winTerm" Test-Requirement -Condition ($null -ne $identity -and $identity.Name -notmatch '^Microsoft\.') -Message "$Path does not use a Microsoft package name" Test-Requirement -Condition ($null -ne $identity -and $identity.Publisher -ceq $ExpectedPublisher) -Message "$Path uses the expected non-Microsoft publisher" - Test-Requirement -Condition ($null -ne $identity -and $identity.Version -eq '1.3.0.5') -Message "$Path uses package version 1.3.0.5" + Test-Requirement -Condition ($null -ne $identity -and $identity.Version -eq '1.3.0.6') -Message "$Path uses package version 1.3.0.6" Test-Requirement -Condition ($null -ne $properties -and $properties.DisplayName -eq 'winTerm') -Message "$Path package display name is winTerm" Test-Requirement -Condition ($null -ne $application -and $application.Id -eq 'winTerm') -Message "$Path application ID is winTerm" Test-Requirement -Condition ($null -ne $visualElements -and $visualElements.DisplayName -eq 'winTerm') -Message "$Path application display name is winTerm" diff --git a/scripts/winterm/verify-version.ps1 b/scripts/winterm/verify-version.ps1 index 28ce5f4a2..179c92e95 100644 --- a/scripts/winterm/verify-version.ps1 +++ b/scripts/winterm/verify-version.ps1 @@ -49,10 +49,10 @@ try $versionPath = Join-Path $repositoryRoot 'src\winterm\Branding\version.json' $version = Get-Content -LiteralPath $versionPath -Raw | ConvertFrom-Json - Assert-Condition ($version.applicationVersion -eq '1.3.0-beta2') 'Application version is 1.3.0-beta2' - Assert-Condition ($version.packageVersion -eq '1.3.0.5') 'Package version is 1.3.0.5' + Assert-Condition ($version.applicationVersion -eq '1.3.0-beta3') 'Application version is 1.3.0-beta3' + Assert-Condition ($version.packageVersion -eq '1.3.0.6') 'Package version is 1.3.0.6' Assert-Condition ($version.moduleVersion -eq '1.3.0') 'PowerShell module version is 1.3.0' - Assert-Condition ($version.modulePrerelease -eq 'beta2') 'PowerShell module prerelease suffix is beta2' + Assert-Condition ($version.modulePrerelease -eq 'beta3') 'PowerShell module prerelease suffix is beta3' # The release workflow treats any channel other than 'stable' as a # prerelease: it marks the GitHub Release --prerelease and --latest=false. @@ -66,7 +66,7 @@ try Assert-Condition ($version.packageVersion -match '^\d+\.\d+\.\d+\.\d+$') 'Package version stays a four-part numeric version' Assert-Condition ($version.moduleVersion -match '^\d+\.\d+\.\d+$') 'PowerShell module version stays numeric' - Assert-Condition ($version.tag -eq 'v1.3.0-beta2') 'Release tag is v1.3.0-beta2' + Assert-Condition ($version.tag -eq 'v1.3.0-beta3') 'Release tag is v1.3.0-beta3' Assert-Condition ($version.workspaceSchemaVersion -eq 2) 'Workspace Schema version remains 2' Assert-Condition ($version.dockingModelVersion -eq 1) 'Docking Model version remains 1' Assert-Condition ($version.shellProtocolVersion -eq 1) 'Shell Protocol version remains 1' @@ -98,7 +98,7 @@ try Assert-Condition ($shellVersion.protocolVersion -eq $version.shellProtocolVersion) 'Shell asset protocol version matches release metadata' $releaseHeader = Get-Text 'src\winterm\Branding\ReleaseMetadata.h' - Assert-Condition ($releaseHeader.Contains('ApplicationVersion{ L"1.3.0-beta2" }')) 'About metadata application version is 1.3.0-beta2' + Assert-Condition ($releaseHeader.Contains('ApplicationVersion{ L"1.3.0-beta3" }')) 'About metadata application version is 1.3.0-beta3' Assert-Condition ($releaseHeader.Contains('ReleaseChannel{ L"Beta" }')) 'About metadata channel is Beta' Assert-Condition ($releaseHeader.Contains($version.microsoftTerminalUpstreamRevision)) 'About metadata contains the Microsoft Terminal upstream revision' Assert-Condition ($releaseHeader.Contains('WorkspaceSchemaVersion{ 2 }')) 'About metadata contains Workspace Schema version 2' @@ -129,10 +129,10 @@ try Assert-Condition ((Get-Text 'src\winterm\Workspaces\Model\WorkspaceDescriptor.h').Contains('WorkspaceSchemaVersion{ 2 }')) 'Workspace model remains at Schema version 2' Assert-Condition ((Get-Text 'src\winterm\Workspaces\Model\WorkspaceDescriptor.h').Contains('DockingModelVersion{ 1 }')) 'Workspace model remains at Docking version 1' - Assert-Condition ((Get-Text 'src\winterm\Workspaces\Model\WorkspaceDescriptor.h').Contains('applicationVersion{ "1.3.0-beta2" }')) 'Workspace model application-version fallback is 1.3.0-beta2' + Assert-Condition ((Get-Text 'src\winterm\Workspaces\Model\WorkspaceDescriptor.h').Contains('applicationVersion{ "1.3.0-beta3" }')) 'Workspace model application-version fallback is 1.3.0-beta3' Assert-Condition ((Get-Text 'src\winterm\Shell\Protocol\ShellIntegrationProtocol.h').Contains('ShellProtocolVersion{ 1 }')) 'Shell protocol remains at version 1' Assert-Condition ((Get-Text 'src\winterm\Appearance\Themes\ThemeDescriptor.h').Contains('CurrentThemeSchemaVersion{ 1 }')) 'Theme Schema remains at version 1' - Assert-Condition ((Get-Text 'src\winterm\Workspaces\Persistence\WorkspaceSerializer.cpp').Contains('"1.3.0-beta2"')) 'Workspace serializer application-version fallback is 1.3.0-beta2' + Assert-Condition ((Get-Text 'src\winterm\Workspaces\Persistence\WorkspaceSerializer.cpp').Contains('"1.3.0-beta3"')) 'Workspace serializer application-version fallback is 1.3.0-beta3' $releaseWorkflow = Get-Text '.github\workflows\release.yml' Assert-Condition ($releaseWorkflow.Contains("- 'v*'")) 'Release workflow accepts version tags through a generic guarded trigger' @@ -148,7 +148,7 @@ try if ($RequireTag) { $tag = (& git describe --tags --exact-match 2>$null).Trim() - Assert-Condition ($LASTEXITCODE -eq 0 -and $tag -eq $version.tag) 'Checked-out commit is exactly tagged v1.3.0-beta2' + Assert-Condition ($LASTEXITCODE -eq 0 -and $tag -eq $version.tag) 'Checked-out commit is exactly tagged v1.3.0-beta3' } Write-Host 'winTerm version consistency verification passed.' -ForegroundColor Green diff --git a/shell/powershell/winTerm.Shell/winTerm.Shell.psd1 b/shell/powershell/winTerm.Shell/winTerm.Shell.psd1 index 7daf5384a..5428bd543 100644 --- a/shell/powershell/winTerm.Shell/winTerm.Shell.psd1 +++ b/shell/powershell/winTerm.Shell/winTerm.Shell.psd1 @@ -12,7 +12,7 @@ AliasesToExport = @() PrivateData = @{ PSData = @{ - Prerelease = 'beta2' + Prerelease = 'beta3' Tags = @('winTerm', 'terminal', 'shell-integration') ProjectUri = 'https://github.com/HelloThisWorld/winTerm' LicenseUri = 'https://github.com/HelloThisWorld/winTerm/blob/main/LICENSE' diff --git a/shell/shared/version.json b/shell/shared/version.json index dd69cabf8..fe072a0ba 100644 --- a/shell/shared/version.json +++ b/shell/shared/version.json @@ -1,6 +1,6 @@ { - "applicationVersion": "1.3.0-beta2", + "applicationVersion": "1.3.0-beta3", "moduleVersion": "1.3.0", - "modulePrerelease": "beta2", + "modulePrerelease": "beta3", "protocolVersion": 1 } diff --git a/src/cascadia/CascadiaPackage/Package-winTerm.appxmanifest b/src/cascadia/CascadiaPackage/Package-winTerm.appxmanifest index 5b56ab425..35c602499 100644 --- a/src/cascadia/CascadiaPackage/Package-winTerm.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-winTerm.appxmanifest @@ -18,7 +18,7 @@ + Version="1.3.0.6" /> winTerm diff --git a/src/cascadia/WindowsTerminal/WindowsTerminal.rc b/src/cascadia/WindowsTerminal/WindowsTerminal.rc index 16da2fc07..fd81c8855 100644 --- a/src/cascadia/WindowsTerminal/WindowsTerminal.rc +++ b/src/cascadia/WindowsTerminal/WindowsTerminal.rc @@ -83,8 +83,8 @@ IDI_APPICON_HC_WHITE ICON "..\\..\\..\\res\\terminal\\imag #if defined(WT_BRANDING_WINTERM) 1 VERSIONINFO - FILEVERSION 1,3,0,5 - PRODUCTVERSION 1,3,0,5 + FILEVERSION 1,3,0,6 + PRODUCTVERSION 1,3,0,6 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS VS_FF_DEBUG @@ -101,12 +101,12 @@ BEGIN BEGIN VALUE "CompanyName", "helloThisWorld\0" VALUE "FileDescription", "winTerm Terminal Host\0" - VALUE "FileVersion", "1.3.0.5\0" + VALUE "FileVersion", "1.3.0.6\0" VALUE "InternalName", "WindowsTerminal\0" VALUE "LegalCopyright", "Copyright (c) winTerm contributors. Portions copyright Microsoft Corporation.\0" VALUE "OriginalFilename", "WindowsTerminal.exe\0" VALUE "ProductName", "winTerm\0" - VALUE "ProductVersion", "1.3.0-beta2\0" + VALUE "ProductVersion", "1.3.0-beta3\0" END END BLOCK "VarFileInfo" diff --git a/src/cascadia/wt/wt.rc b/src/cascadia/wt/wt.rc index 507b4b34a..9ca4e210d 100644 --- a/src/cascadia/wt/wt.rc +++ b/src/cascadia/wt/wt.rc @@ -58,8 +58,8 @@ IDI_APPICON ICON "..\\..\\..\\res\\terminal.ico" #if defined(WT_BRANDING_WINTERM) 1 VERSIONINFO - FILEVERSION 1,3,0,5 - PRODUCTVERSION 1,3,0,5 + FILEVERSION 1,3,0,6 + PRODUCTVERSION 1,3,0,6 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS VS_FF_DEBUG @@ -76,12 +76,12 @@ BEGIN BEGIN VALUE "CompanyName", "helloThisWorld\0" VALUE "FileDescription", "winTerm Launcher\0" - VALUE "FileVersion", "1.3.0.5\0" + VALUE "FileVersion", "1.3.0.6\0" VALUE "InternalName", "winTerm\0" VALUE "LegalCopyright", "Copyright (c) winTerm contributors. Portions copyright Microsoft Corporation.\0" VALUE "OriginalFilename", "winTerm.exe\0" VALUE "ProductName", "winTerm\0" - VALUE "ProductVersion", "1.3.0-beta2\0" + VALUE "ProductVersion", "1.3.0-beta3\0" END END BLOCK "VarFileInfo" diff --git a/src/winterm-tools/winterm-shim/winterm-shim.rc b/src/winterm-tools/winterm-shim/winterm-shim.rc index 8174833b9..d221c720a 100644 --- a/src/winterm-tools/winterm-shim/winterm-shim.rc +++ b/src/winterm-tools/winterm-shim/winterm-shim.rc @@ -4,8 +4,8 @@ #include 1 VERSIONINFO - FILEVERSION 1,3,0,5 - PRODUCTVERSION 1,3,0,5 + FILEVERSION 1,3,0,6 + PRODUCTVERSION 1,3,0,6 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS VS_FF_DEBUG @@ -22,12 +22,12 @@ BEGIN BEGIN VALUE "CompanyName", "helloThisWorld\0" VALUE "FileDescription", "winTerm Shell Integration Helper\0" - VALUE "FileVersion", "1.3.0.5\0" + VALUE "FileVersion", "1.3.0.6\0" VALUE "InternalName", "winterm-shim\0" VALUE "LegalCopyright", "Copyright (c) winTerm contributors.\0" VALUE "OriginalFilename", "winterm-shim.exe\0" VALUE "ProductName", "winTerm\0" - VALUE "ProductVersion", "1.3.0-beta2\0" + VALUE "ProductVersion", "1.3.0-beta3\0" END END BLOCK "VarFileInfo" diff --git a/src/winterm/Branding/ReleaseMetadata.h b/src/winterm/Branding/ReleaseMetadata.h index a93ea91f7..d4681f444 100644 --- a/src/winterm/Branding/ReleaseMetadata.h +++ b/src/winterm/Branding/ReleaseMetadata.h @@ -25,7 +25,7 @@ namespace winTerm::Branding { inline constexpr std::wstring_view Publisher{ L"helloThisWorld" }; - inline constexpr std::wstring_view ApplicationVersion{ L"1.3.0-beta2" }; + inline constexpr std::wstring_view ApplicationVersion{ L"1.3.0-beta3" }; inline constexpr std::wstring_view ReleaseChannel{ L"Beta" }; inline constexpr std::wstring_view CommitSha{ WINTERM_BUILD_COMMIT_SHA }; inline constexpr std::wstring_view BuildTimestamp{ WINTERM_BUILD_TIMESTAMP }; diff --git a/src/winterm/Branding/version.json b/src/winterm/Branding/version.json index 81ed8bb17..99082cd9f 100644 --- a/src/winterm/Branding/version.json +++ b/src/winterm/Branding/version.json @@ -1,10 +1,10 @@ { - "applicationVersion": "1.3.0-beta2", - "packageVersion": "1.3.0.5", + "applicationVersion": "1.3.0-beta3", + "packageVersion": "1.3.0.6", "moduleVersion": "1.3.0", - "modulePrerelease": "beta2", + "modulePrerelease": "beta3", "channel": "beta", - "tag": "v1.3.0-beta2", + "tag": "v1.3.0-beta3", "workspaceSchemaVersion": 2, "dockingModelVersion": 1, "shellProtocolVersion": 1, diff --git a/src/winterm/Workspaces/Model/WorkspaceDescriptor.h b/src/winterm/Workspaces/Model/WorkspaceDescriptor.h index 22f6dc151..68686acbf 100644 --- a/src/winterm/Workspaces/Model/WorkspaceDescriptor.h +++ b/src/winterm/Workspaces/Model/WorkspaceDescriptor.h @@ -219,7 +219,7 @@ namespace winTerm::Workspaces std::string createdAt; std::string updatedAt; WorkspaceSource source{ WorkspaceSource::User }; - std::string applicationVersion{ "1.3.0-beta2" }; + std::string applicationVersion{ "1.3.0-beta3" }; uint32_t protocolVersion{ 1 }; uint32_t dockingModelVersion{ DockingModelVersion }; WorkspaceStartupBehavior startupBehavior; diff --git a/src/winterm/Workspaces/Persistence/WorkspaceSerializer.cpp b/src/winterm/Workspaces/Persistence/WorkspaceSerializer.cpp index 19fe75cf5..99ab22803 100644 --- a/src/winterm/Workspaces/Persistence/WorkspaceSerializer.cpp +++ b/src/winterm/Workspaces/Persistence/WorkspaceSerializer.cpp @@ -618,7 +618,7 @@ WorkspaceDescriptor WorkspaceSerializer::FromJson(const Json::Value& json, const throw std::runtime_error("The workspace source is not supported."); } workspace.source = *source; - workspace.applicationVersion = StringOrDefault(json, "applicationVersion", "1.3.0-beta2"); + workspace.applicationVersion = StringOrDefault(json, "applicationVersion", "1.3.0-beta3"); workspace.protocolVersion = UIntOrDefault(json, "protocolVersion", 1); workspace.dockingModelVersion = UIntOrDefault(json, "dockingModelVersion", DockingModelVersion); if (const auto& startup = json["startupBehavior"]; !startup.isNull())