fix: bound the shell-integration launch fallback to one traversal per command - #41
Merged
Merged
Conversation
… 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.
12 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Any command without a real progress source keeps the Visual Progress rainbow/comet animation looping until the command exits. For long-running processes that is effectively forever:
k9s,vim,less,top,htop(Alternate Screen TUIs)tail -f,kubectl port-forward, and anything else that intentionally stays runningRoot cause
OSC 133
CommandExecutedmakesProgressStateMachine::ApplyShellLifecyclestore aShellIntegration / Indeterminate / Runningfallback snapshot, and nothing bounds it: it stays the active presentation untilCommandFinished. The renderer faithfully animates that snapshot withIterationBehavior::Forever, so the launch animation never stops while the command lives.Implementation
The Shell Integration fallback becomes a bounded one-shot launch indication, implemented as command-generation-scoped state — no command names, process names, or regex allow/deny lists anywhere.
State machine (
src/winterm/VisualProgress/VisualProgressModel.h, renderer-independent, fully unit-testable):ProgressSnapshotgainslaunchGeneration, non-zero only for the launch fallback.ProgressStateMachinetracks a monotonic shell launch generation, an expired flag, and the last observed lifecycle state. Every lifecycle transition (andReset/Close/disable) advances the generation, which strands stale completion callbacks.CommandExecutedobservation of the same still-running command (alternate-screen churn, pane rehydration, reconnect re-broadcast) is idempotent: it neither re-arms a consumed launch nor opens a new generation.ExpireShellLaunch(generation)clears the stored fallback only when the captured generation is still current and the stored shell state is still the running launch. It clears the fallback even while a provider or explicit source owns the presentation, so a later ownership release cannot resurrect it. Expiration publishes Hidden with Running status — Hidden/Cancelled would raise a fake interruption announcement through the accessibility policy, and Success would be a lie.Renderer (
src/winterm/VisualProgress/RainbowArcRenderer.h):CompositionPropertySet, wrapped in aCompositionScopedBatchwhose generation-guarded, weak-referenced completion invokes the newLaunchExpiredCallback. Duration is the existingIndeterminateCycleDuration(1,800 ms) — no duplicated raw duration, no timer, no polling loop, no CPU frame loop.IterationCount(1): it makes exactly one traversal and parks off-track, transparent. Explicit OSC 9;4 and provider indeterminate presentations keepForever.Pane (
src/cascadia/TerminalApp/Pane.cpp|h):RainbowArcRenderer::TryCreate._ExpireVisualProgressShellLaunchruns on the UI thread, asks the state machine to expire (its own lock, never held across UI calls), and queues the resulting snapshot through the existing one-element mailbox.ControlCore: unchanged. Recognition, providers, and the ownership precedence (explicit OSC 9;4 → provider → shell fallback → hidden) are untouched.
Lifecycle / state machine
Provider/explicit interplay: if a provider or OSC 9;4 takes over mid-launch and the clock later completes, the stored fallback is silently expired; when that owner clears,
_fallbackSnapshot()resolves to Hidden(Running) instead of resurrecting the launch.Test coverage
48/48 TAEF tests pass (
SettingsModel.Unit.Tests.dll /name:*WinTermVisualProgressTests*), including 4 new methods:ShellLaunchFallbackIsOneShotPerCommand— launch starts onCommandExecuted; re-broadcast idempotence; expiry publishes Hidden(Running), never Success; no replay after expiry; the later real result still presents.ShellLaunchExpirationIgnoresStaleGenerations— short command finishing first is untouched by a late completion; a stale generation can never hide a newer command; generation 0 invalid; expiry is exactly-once.ExpiredShellLaunchDoesNotResurrectAfterOwnershipClears— provider takeover, background expiry, provider clear → Hidden; same for explicit OSC 9;4; a provider can still start genuinely new work afterwards.ShellLaunchInvalidationOnResetDisableAndClose—Reset()(pane close/detach),SetEnabled(false)(settings disable, including across re-enable), andClose()all strand pending completions.scripts/winterm/test-visual-progress.ps1 -SourceOnlypasses with new assertions locking in: the model policy (ExpireShellLaunch, generation guard, Hidden(Running), idempotent re-broadcast), the renderer clock (sharedIndeterminateCycleDuration, no raw1800literal, one-shot comet iteration), the Pane wiring, the new smoke fixtures, and the new documentation section.scripts/winterm/invoke-visual-progress-smoke.ps1gains dependency-free fixtures: a long-running command one-shot launch (synthetic output ticks with no133;Dfor several traversal periods) and an alternate-screen enter/leave/re-enter sequence during a running command, plus manual-check guidance for FastAPI/uvicorn, Spring Boot (Maven/Gradle provider handoff), k9s/vim/top/htop, and short commands.Also run: the full
ut_appTAEF suite (51/51 passed) after theTerminalAppLibbuild compile-verified thePane.cppandRainbowArcRenderer.hchanges, and the smoke fixture script end-to-end with-DelayMilliseconds 0.Not run, and why:
UnitTests_Controlwas neither rebuilt nor run — no TerminalControl source changed (ControlCore is untouched by this fix). Live-app animation timing (the actual 1.8 s compositor batch completion and the unparented property-set clock) cannot be asserted from unit tests because the compositor is not exercisable headlessly — which is exactly why the generation/expiration policy is isolated in the renderer-independent state machine and tested there directly; the compositor-side behavior is covered by the manual steps below.Manual verification steps
.\scripts\winterm\invoke-visual-progress-smoke.ps1— watch the "long-running command one-shot launch" fixture: one traversal, overlay disappears, output continues, and the result presentation still runs at133;D.python -m http.server, uvicorn, a Node dev server, orGet-Content -Waiton a growing file): one launch traversal, then no overlay while output continues; Ctrl+C still shows the normal cancelled/error/success presentation.k9sorvim: launch plays at most once; entering/leaving the alternate screen, resizing, and switching tabs do not replay it.mvn spring-boot:run(or any Maven/Gradle build): real provider progress presents as before; after the app becomes a plain long-running server, no fallback animation returns.dir) behave exactly as before; completion supersedes the launch immediately.Risks and rollback
SamePresentation) is extended, not altered, for existing sources.launchGenerationlives only in the in-process snapshot struct).