Skip to content

feat: complete Command Timeline Phase 4 checkpoint - #30

Merged
HelloThisWorld merged 4 commits into
mainfrom
feature/command-timeline-v1.3.0-phase4
Aug 3, 2026
Merged

feat: complete Command Timeline Phase 4 checkpoint#30
HelloThisWorld merged 4 commits into
mainfrom
feature/command-timeline-v1.3.0-phase4

Conversation

@HelloThisWorld

Copy link
Copy Markdown
Owner

Summary

Command Timeline Phase 4 — search, settings, and bounded history. This
completes the in-memory Command Timeline feature surface.

Engineering checkpoint 1.2.4 / 1.2.4.0, intended tag v1.2.4. Not a public
release
— GitHub Latest and README downloads stay on v1.2.0.

Phase 3 dependency — satisfied. Phase 4 depends on the Phase 3
load/copy/jump actions: Enter-loads-without-executing and the filtered
copy/jump/context-menu actions are Phase 3 surfaces that Phase 4 makes work
under a filter. Phase 3 (#29,
tag v1.2.3) was squash-merged to main as 5fd2172 while this work was
in progress, so this PR targets main directly.

Because the squash made the Phase 3 commit a non-ancestor of main, main was
merged into this branch (trees were identical, so the merge only re-parents
Phase 4). Verified: the diff against main contains zero Phase 3 additions
and only the Phase 4 version bump 1.2.3 → 1.2.4.

Related issues

No tracking issue.

Detailed changes

Search semantics

CommandTimelineQueryMatches is a case-insensitive literal substring search
(std::search + towlower). Deliberately not a regex and not fuzzy —
test-command-timeline.ps1 now fails the build if std::regex, regex_search,
or a fuzzy matcher appears in the model (comments stripped before the check).

Filtering reads entry.cachedCommandText only — the bounded 4096-character
Phase 1 cache. Output is never consulted and the terminal buffer is never
rescanned; a new command is reconciled through the existing incremental index.

Query bounds: capped at 256 UTF-16 code units by
NormalizeCommandTimelineQuery, truncating without leaving a lone surrogate.
The TextBox carries MaxLength="256", and the control writes the normalized
value back when truncation shortens it. A query is never persisted.

Filtered projection and stable identity

The navigation model now walks _filtered (indices into the entries span) by
position, not by raw entry index. An empty query fills _filtered with every
index, so the unfiltered case takes the same path.

Situation Result
Selected command still matches Stays selected
Selected command stops matching Nearest surviving match
Following latest, new command matches New command selected
Following latest, new command does not match Selection unchanged
Browsing older history, new command arrives Selection unchanged
No results Selected CommandId retained, nothing projected

Following-latest additionally requires the newest command to be in the
projection, which is what stops a non-matching new command from moving the
selection. Every action still resolves through viewState.selectedCommandId.

Only visibleCapacity rows are materialized, whatever the history size.

Settings

Setting Default Range Effect
commandTimeline.enabled true Overlay + left-side handle
commandTimeline.historyLimit 500 50–5000 Per-pane history

Plumbed MTSMSettings.hGlobalAppSettings.idlControlProperties.h
IControlSettings.idlTerminalSettings.cppControlCore. Defaults in
defaults.json; JSON schema carries type, default, minimum, maximum.

An absent setting is not serialized back, so existing settings files need no
migration
. An out-of-range value is accepted by the parser and clamped by
ClampCommandTimelineHistoryLimit, so the runtime value is always 50–5000
rather than failing the settings load. Settings UI lives under
Settings → Appearance → Command timeline.

UpdateSettings applies the limit to panes that already exist; the index
constructor applies it to new panes. Disabling hides the handle, closes an open
overlay, and makes ToggleCommandTimeline refuse to open.

Bounded history

_applyHistoryLimit erases oldest-first on entry creation, bootstrap, and
SetHistoryLimit. Lowering evicts immediately; raising never resurrects;
_nextSequence only increases so sequence IDs are never reused;
ReconcileLoadedInput releases loaded-input state for an evicted command.

Shell degradation

Four distinct states so an unsupported shell is never shown as an empty history:
WaitingForShell, ShellUnsupported, NoCommands, NoMatchingCommands. No
prompt parser, no heuristic detection, no ConPTY/VT/TextBuffer/renderer/shell
protocol change.

Input isolation

/ and Tab are consumed by _tryHandleCommandTimelineKey and never reach the
PTY. Filter text never reaches the shell — the TextBox owns it, and
_CommandTimelineSearchKeyDown claims only Up/Down/Enter/Escape, leaving
Left/Right caret editing and IME/TSF composition alone. Escape now clears a
non-empty query before closing. Ctrl+Tab and user key bindings keep precedence
(_TryHandleKeyBinding runs first).

Accessibility

Localized accessible name and placeholder on the filter box; PositionInSet /
SizeOfSet use the filtered count; localized empty states; status never by
color alone; {ThemeResource} brushes for High Contrast; no storyboard or
continuous animation; DIP geometry; overlay changes no rows/columns, pane size,
PTY size, swap-chain size, or padding.

Validation performed

Environment: Windows 11 Pro 26100, MSVC 14.44.35207, Windows SDK 10.0.26100.0.

Native builds — ran, passed (0 Error(s) each)

Project Result
UnitTests_Control\Control.UnitTests.vcxproj 0 errors
UnitTests_SettingsModel\SettingsModel.UnitTests.vcxproj 0 errors
TerminalSettingsEditor\Microsoft.Terminal.Settings.Editor.vcxproj 0 errors, 47 warnings (pre-existing), 3m09s

All via /p:Configuration=Release /p:Platform=x64 /p:SolutionDir=D:\work\winTerm\.
Transitively built TerminalCore, TerminalControlLib, and
Microsoft.Terminal.Settings.ModelLib.

Unit tests — ran, passed

Suite Result
CommandTimelineTests (Control) Total=43, Passed=43, Failed=0
WinTermCommandTimelineTests (SettingsModel) Total=4, Passed=4, Failed=0
Full Control suite Total=73, Passed=73, Failed=0

Full SettingsModel suite — 65 pre-existing failures, none from this change

Build Total Passed Failed
Baseline (Phase 3 settings model, my tests removed) 231 166 65
This change 235 170 65

Verified by reverting MTSMSettings.h, GlobalAppSettings.idl,
TerminalSettings.cpp, and the test-project entry to their Phase 3 state,
rebuilding, and re-running. The failure count is identical; this change adds 4
tests and all 4 pass. The failures are host-process crashes (0xC0000409)
spread across 9 upstream classes (DeserializationTests, MediaResourceTests,
SerializationTests, CommandTests, KeyBindingsTests, NewTabMenuTests,
ProfileTests, ThemeTests, TerminalSettingsTests) including tests with no
connection to global settings. Reverting only defaults.json and rebuilding
also left the count at exactly 65. These are environmental on this host; the
repository's own suites (test.ps1 -Suite Smoke, test-visual-progress.ps1)
do not run the full SettingsModel suite.

Performance evidence — ran, measured

SearchStressAtMaximumHistoryLimit builds a full 5000-entry history, then:
filters with a query matching all 5000 (projection reports 5000, materializes
exactly 20 rows = visibleCapacity); runs 25 passes of narrow → no-result →
broad filtering asserting projection and materialized row counts return to
expected values each pass; asserts cached text stays within
5000 * 4096; closes and asserts the projection is released.

Measured wall clock: 1,267 ms for the whole test including TAEF host
startup. Row materialization stays at 20 regardless of the 5000-entry history.

Repository scripts — ran, passed

Command Result
test-command-timeline.ps1 PASS (extended with Phase 4 guards)
verify-version.ps1 PASS — 1.2.4 / 1.2.4.0 / v1.2.4, Latest still 1.2.0
verify-branding.ps1 -ExpectedPublisher 'CN=helloThisWorld' PASS
test-release-workflow.ps1 PASS
test-ci-classification.ps1 PASS
test-privacy.ps1 PASS
test-visual-progress.ps1 PASS
test.ps1 -Suite Smoke -Configuration Release -Platform x64 PASS

Static gates — ran, passed

  • clang-format over only the changed line ranges: identical modulo line
    endings for every touched C++ file (two real diffs found and fixed).
  • git diff --check: clean.
  • PowerShell AST parse of all 8 touched scripts: clean.
  • XML parse of TermControl.xaml, GlobalAppearance.xaml, both Resources.resw,
    Package-winTerm.appxmanifest, SettingsModel.UnitTests.vcxproj: clean.
  • Strict JSON parse of version.json (both) and profiles.schema.json: clean.
  • JSONC parse of defaults.json: clean, 32 // comment lines preserved.

Not run, with reason

  • test-shell-integration.ps1 -Shell Allblocked by local antivirus/AMSI,
    not by this change. Fails parsing
    shell/powershell/winTerm.Shell/Public/Compatibility.ps1 with "This script
    contains malicious content and has been blocked by your antivirus software."

    That file is byte-identical to main and untouched here. Needs CI or a host
    without the AMSI block.
  • Full Release build, installer, Portable packaging, delivery workflow, and the
    full unrelated upstream test suite — deliberately out of scope for a
    checkpoint.

No screenshot was needed or created.

Checklist

  • The change is focused and does not include unrelated formatting.
  • Tests were added or updated where appropriate.
  • All tests claimed above actually ran and passed.
  • User-facing behavior and limitations are documented in this repository.
  • This source/docs commit updates the root CHANGELOG.md.
  • The Wiki Development-Changes.md ledger has been pushed with the final source SHA, link, summary, and checkpoint/release.
  • Documentation screenshots reuse a suitable existing sanitized winterm-site asset, or no screenshot was added.
  • Version or schema changes include compatibility and migration notes.
  • Package identity, winterm.exe, and Microsoft Terminal coexistence remain isolated.
  • No command text, terminal output, clipboard content, credentials, or private paths are logged.
  • New source and script files contain the appropriate MIT license header.
  • I did not include generated build output, secrets, certificates, or local absolute paths.

Source ↔ Wiki mapping

Source commit Wiki commit Wiki pages
c142699 a3003ad Development-Changes, Changelog, Home

Boundary

No persistent history, output cache, output search or indexing, regex or fuzzy
search, command analytics, telemetry, automatic execution, command replay, CMD
prompt parser, heuristic prompt/output detection, ConPTY/VT/TextBuffer/renderer
change, workspace or shell protocol schema change, installer, Portable
packaging, GitHub Release, Latest update, winterm-site change, or screenshot.

v1.3.0-alpha is not started. The interfaces it can consume directly are
CommandTimelineNavigationModel::SetQuery / Query / FilteredCount,
CommandTimelineActionModel, CommandTimelineEmptyState,
ClampCommandTimelineHistoryLimit, CommandTimelineIndex::SetHistoryLimit, and
ControlCore::FilterCommandTimeline / PrepareCommandTimelineAction.

Adds load, copy, and jump actions to the pane-owned Command Timeline
overlay introduced in Phase 2, without adding persistence, an output
cache, or any path that can execute a command.

- Add a pure C++ CommandTimelineActionModel that decides load, copy, and
  jump eligibility from the stable selected CommandId, tracks the loaded
  command, and advances an execution generation so a completion from a
  retired command is detectable and discarded.
- Load the selected command onto the focused pane input with Enter or a
  single click. The payload is filtered for control codes only,
  CarriageReturnNewline is deliberately not applied, no carriage return
  is appended, and SendInput targets this pane connection, so the load
  never executes, never reads the Windows clipboard, and is never
  forwarded by input broadcast.
- Refuse a multi-line load when the shell has not enabled bracketed
  paste, and require a confirming Enter above 1024 characters. Escape
  cancels a pending confirmation before it closes the overlay.
- Add Space to jump the viewport to the selected command native mark and
  a per-entry context menu with copy command, copy output, and jump to
  output. Ctrl+C copies the selected command while the Timeline owns the
  keyboard.
- Resolve output on demand through Terminal::ResolveCommandTimelineOutput
  for an explicit copy action only. Output is never cached, indexed, or
  retained.
- Release loaded-input state when the loaded command is evicted, and keep
  every action bound to the stable CommandId rather than the XAML row.
- Advance engineering version surfaces to 1.2.3/1.2.3.0 for checkpoint
  tag v1.2.3, and extend test-command-timeline.ps1 with guards that fail
  if the load path gains a submission character or a clipboard read.

Public Latest remains v1.2.0. Search, filtering, and public
commandTimeline settings remain Phase 4 scope.
Completes the in-memory Command Timeline feature surface: pane-local search,
the two public settings, trustworthy shell degradation, and bounded history.
No persistence, no output cache, and no telemetry are introduced.

- Add pane-local search over each pane's bounded in-memory command text. The
  match is a literal case-insensitive substring search built on std::search and
  towlower; there is no regex, no fuzzy matching, no output search, and no
  terminal-buffer rescan.
- Cap queries at 256 UTF-16 code units and truncate without leaving a lone
  surrogate, enforced in NormalizeCommandTimelineQuery and mirrored by
  MaxLength on the filter box. A query is never persisted.
- Rework the navigation model to walk a filtered projection of indices while
  keeping stable CommandId identity. A still-matching command stays selected, a
  command that stops matching hands selection to the nearest surviving match,
  and a new command only takes the selection when it matches and the view was
  already following the latest command.
- Route slash and Tab to the filter box and consume them before the PTY. Filter
  text never reaches the shell, Left/Right keep normal caret editing, and
  IME/TSF composition is left to the text box. Escape now clears a non-empty
  query before it closes the overlay.
- Add the commandTimeline.enabled and commandTimeline.historyLimit global
  settings with defaults true and 500, a clamped 50-5000 range, JSON schema
  entries, and a Settings UI section under Appearance. An absent setting is not
  serialized back, so existing settings files need no migration.
- Add bounded per-pane history with oldest-first eviction that applies to panes
  that already exist and to new panes. Raising the limit never resurrects an
  evicted command and sequence IDs are never reused.
- Add four distinct empty states so an unsupported shell is never reported as
  simply having run no commands.
- Report list item position and set size against the filtered result count, and
  keep localized accessible names, non-color status, High Contrast theme
  resources, and the Reduced Motion-safe no-animation path.
- Advance engineering version surfaces to 1.2.4/1.2.4.0 for checkpoint tag
  v1.2.4, and extend test-command-timeline.ps1 with guards for search
  literalness, query bounds, settings defaults/range/schema, and the filter
  reading only cached command text.

Public Latest remains v1.2.0. This builds on the Phase 3 branch and does not
begin v1.3.0-alpha.
Phase 3 was squash-merged to main as 5fd2172. The trees are identical, so this
merge only re-parents Phase 4 onto main and leaves the pull request diff as
Phase 4 changes alone.

# Conflicts:
#	CHANGELOG.md
#	docs/current-progress.md
#	docs/user/command-timeline.md
#	scripts/winterm/package-shell-assets.ps1
#	scripts/winterm/test-command-timeline.ps1
#	scripts/winterm/test-visual-progress.ps1
#	scripts/winterm/test.ps1
#	scripts/winterm/verify-branding.ps1
#	scripts/winterm/verify-version.ps1
#	shell/powershell/winTerm.Shell/winTerm.Shell.psd1
#	shell/powershell/winTerm.Shell/winTerm.Shell.psm1
#	shell/shared/version.json
#	src/cascadia/CascadiaPackage/Package-winTerm.appxmanifest
#	src/cascadia/TerminalControl/ControlCore.cpp
#	src/cascadia/TerminalControl/ControlCore.h
#	src/cascadia/TerminalControl/Resources/en-US/Resources.resw
#	src/cascadia/TerminalControl/TermControl.cpp
#	src/cascadia/TerminalControl/TermControl.h
#	src/cascadia/UnitTests_Control/CommandTimelineTests.cpp
#	src/cascadia/WindowsTerminal/WindowsTerminal.rc
#	src/cascadia/wt/wt.rc
#	src/winterm-tools/winterm-shim/winterm-shim.rc
#	src/winterm/Branding/ReleaseMetadata.h
#	src/winterm/Branding/version.json
#	src/winterm/Workspaces/Model/WorkspaceDescriptor.h
#	src/winterm/Workspaces/Persistence/WorkspaceSerializer.cpp
Phase 3 was squash-merged to main as 5fd2172 through pull request #29 while
Phase 4 was in progress, so the Phase 4 base branch is main rather than the
Phase 3 feature branch.
@HelloThisWorld
HelloThisWorld marked this pull request as ready for review August 3, 2026 17:25
@HelloThisWorld
HelloThisWorld merged commit ac760ea into main Aug 3, 2026
9 checks passed
@HelloThisWorld
HelloThisWorld deleted the feature/command-timeline-v1.3.0-phase4 branch August 3, 2026 17:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant