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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,57 @@
# Changelog

## 1.2.4 - 2026-08-04

### Added

- Added pane-local Command Timeline search. `/` or Tab moves focus to the filter
box, typing filters the current pane's commands, Up and Down walk the filtered
results, and Enter loads the selected command without executing it. Neither
the focus keys nor the query text ever reach the PTY.
- Added the `commandTimeline.enabled` and `commandTimeline.historyLimit` global
settings, exposed under Settings → Appearance → Command timeline. Defaults are
`true` and `500`; the history limit accepts 50 through 5000 per pane.
- Added four distinct empty states so an unsupported shell is never reported as
simply having run no commands: waiting for shell integration, command timeline
unavailable, no commands yet, and no matching commands.
- Added bounded per-pane history with oldest-first eviction, plus deterministic
coverage for filtered navigation, wheel accumulation over the filtered
projection, surrogate-safe query truncation, eviction, and a 5000-entry
worst-case search.

### Changed

- Search is a literal, case-insensitive substring match over each pane's bounded
in-memory command text only. There is no regex, no fuzzy matching, no output
search, and no terminal-buffer rescan.
- Queries are capped at 256 UTF-16 code units and truncated without leaving a
lone surrogate. A query is never persisted: closing the overlay releases the
query, the filtered projection, and every materialized row.
- The filtered projection keeps stable `CommandId` identity. A command that
still matches 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.
- Escape now clears a non-empty query first and only closes the overlay once the
query is already empty.
- Lowering `commandTimeline.historyLimit` evicts oldest-first immediately on
panes that already exist. Raising it never resurrects an evicted command, and
sequence IDs are never reused.
- Disabling `commandTimeline.enabled` hides the left-side handle and closes an
overlay that is already open; the toggle shortcut no longer opens it.
- Advanced engineering application and PowerShell module versions to `1.2.4`,
package/file versions to `1.2.4.0`, and the intended checkpoint tag to
`v1.2.4`; workspace, docking, shell, theme, update-manifest, package identity,
and signing-policy versions remain unchanged.

### Checkpoint status

- `v1.2.4` is an engineering checkpoint for Command Timeline Phase 4, not a
public GitHub Release. GitHub Latest and README public downloads remain on
v1.2.0. There is still no persistent history, no output cache, and no
telemetry.
- Builds on Command Timeline Phase 3, squash-merged to `main` as `5fd2172`
through pull request #29.

## 1.2.3 - 2026-08-03

### Added
Expand Down
12 changes: 12 additions & 0 deletions doc/cascadia/profiles.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2378,6 +2378,18 @@
"description": "When set to true, a selection is immediately copied to your clipboard upon creation. When set to false, the selection persists and awaits further action.",
"type": "boolean"
},
"commandTimeline.enabled": {
"default": true,
"description": "When set to true, the Command Timeline overlay and its left-side handle are available for each pane. When set to false, the handle is hidden and the toggle shortcut does not open the overlay.",
"type": "boolean"
},
"commandTimeline.historyLimit": {
"default": 500,
"description": "The maximum number of commands each pane keeps in its in-memory Command Timeline. Lowering this evicts the oldest commands immediately. History is never written to disk.",
"maximum": 5000,
"minimum": 50,
"type": "integer"
},
"focusFollowMouse": {
"default": false,
"description": "When set to true, the terminal will focus the pane on mouse hover.",
Expand Down
91 changes: 47 additions & 44 deletions docs/current-progress.md
Original file line number Diff line number Diff line change
@@ -1,68 +1,71 @@
# Current development progress

Last updated: 2026-08-03
Last updated: 2026-08-04

## Repository state

- Branch: `feature/command-timeline-v1.3.0`
- Starting commit: `395f9becd` (`main` after Command Timeline Phase 2)
- Branch: `feature/command-timeline-v1.3.0-phase4`
- Base branch: `main`. Command Timeline Phase 3 (tag `v1.2.3`) was squash-merged
to `main` as `5fd2172` through pull request #29.
- Microsoft Terminal upstream revision:
`1cea42d433253d95c4487a3037db48197b5e72f4`
- Engineering application and PowerShell module version: `1.2.3`
- Engineering package version: `1.2.3.0`
- Intended checkpoint tag: `v1.2.3`
- Engineering application and PowerShell module version: `1.2.4`
- Engineering package version: `1.2.4.0`
- Intended checkpoint tag: `v1.2.4`
- Final Command Timeline release target: `v1.3.0`
- Current public Latest: `v1.2.0`
- Supported target: Windows 11 x64

`v1.2.3` is a development checkpoint, not a distributable release. The README
`v1.2.4` is a development checkpoint, not a distributable release. The README
and GitHub Latest continue to identify v1.2.0 as the public Visual Progress
release. Checkpoint tags v1.2.1 through v1.2.4 run quick validation only and are
explicitly excluded from full build, installer packaging, asset publication,
and GitHub Release jobs.

## Implemented in the working tree

- Retained the Phase 1 pane-owned index and the Phase 2 navigation model,
overlay, wheel accumulation, and accessibility surface unchanged as the only
history data source and presentation path.
- Added a pane-owned pure C++ `CommandTimelineActionModel`. It decides whether
a load, copy, or jump is possible from the stable selected `CommandId`, and
never resolves output, reads the clipboard, or produces a payload containing
a carriage return.
- Added Enter and single-click load onto the focused pane's input line. The
payload is filtered for control codes only, `CarriageReturnNewline` is
deliberately not applied, no carriage return is appended, and `SendInput`
targets this pane's connection, so the load can never execute, never reads
the Windows clipboard, and is never forwarded by input broadcast.
- Added multi-line and large-load protection: a multi-line command is refused
when the shell has not enabled bracketed paste, and a load above 1024
characters requires a confirming Enter. Escape cancels a pending confirmation
before it closes the overlay.
- Added Space to jump the viewport to the selected command's 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.
- Added on-demand output resolution through
`Terminal::ResolveCommandTimelineOutput`. Output is read from the buffer only
for an explicit copy action and is never cached, indexed, or retained.
- Added `loadedCommandId` plus execution-generation tracking. `CommandStart`
retires the loaded state, `IsCurrentGeneration` detects a late completion from
a retired command, and `ReconcileLoadedInput` releases loaded-input state when
the loaded command is evicted.
- Advanced authoritative engineering version surfaces to `1.2.3`/`1.2.3.0` and
added the v1.2.3 root changelog entry.
- Kept Phase 4 out of scope: there is no search box, no filtering, and no
public `commandTimeline.*` settings yet.
- Retained the Phase 1 index, the Phase 2 overlay and navigation model, and the
Phase 3 load/copy/jump entry actions unchanged in behavior.
- Added pane-local search over each pane's bounded in-memory command text. The
match is a literal case-insensitive substring search; there is no regex, no
fuzzy matching, no output search, and no terminal-buffer rescan.
- Added a 256 UTF-16 code-unit query cap that truncates without leaving a lone
surrogate, enforced in the model and mirrored by `MaxLength` on the filter box.
- Reworked the navigation model to walk a filtered projection 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.
- Added `/` and Tab to focus the filter box. Both are consumed before the PTY,
and filter-box text never reaches the shell. Escape now clears a non-empty
query before it closes the overlay.
- Added the `commandTimeline.enabled` and `commandTimeline.historyLimit` global
settings with defaults `true` and `500`, a 50–5000 clamped 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.
- Added 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.
- Added four distinct empty states so an unsupported shell is never reported as
simply having run no commands.
- Made list item position and set size reflect the filtered result count, and
kept localized accessible names, non-color status, High Contrast theme
resources, and the Reduced Motion-safe no-animation path.
- Advanced authoritative engineering version surfaces to `1.2.4`/`1.2.4.0` and
added the v1.2.4 root changelog entry.
- Kept `v1.3.0-alpha` out of scope: no persistent history, no output cache, no
telemetry, and no public release work.

## Validation state

Phase 3 validation requires the focused Command Timeline model/control tests,
the extended `test-command-timeline.ps1` source and privacy boundaries, version
and branding verification, release/CI classification guards, shell integration
checks, repository Smoke validation, the smallest affected native projects, and
GitHub quick PR validation. Record exact results in the Draft PR and final task
report; do not treat this document as evidence for a command that did not run.
Phase 4 validation requires the focused Command Timeline model/control tests,
the Settings Model Command Timeline tests, the extended
`test-command-timeline.ps1` source, search, settings, and privacy boundaries,
version and branding verification, release/CI classification guards, repository
Smoke validation, the smallest affected native projects, and GitHub quick PR
validation. Record exact results in the Draft PR and final task report; do not
treat this document as evidence for a command that did not run.

The annotated `v1.2.3` checkpoint tag must point to the final commit that passes
The annotated `v1.2.4` checkpoint tag must point to the final commit that passes
those gates. Its tag workflow must run checkpoint quick validation only and
must not create a GitHub Release or update Latest.
182 changes: 182 additions & 0 deletions docs/development/command-timeline-phase4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Command Timeline Phase 4 — search, settings, and bounded history

Phase 4 completes the in-memory Command Timeline feature surface: pane-local
search and filtering, the two public settings, trustworthy shell degradation
states, and bounded history with oldest-first eviction.

Engineering checkpoint: `1.2.4` / `1.2.4.0`, tag `v1.2.4`. This is not a public
release; GitHub Latest stays on v1.2.0.

## Scope

| In scope | Out of scope |
| --- | --- |
| Pane-local literal search | Regex or fuzzy search |
| Filtered projection with stable identity | Output search or indexing |
| `commandTimeline.enabled` / `.historyLimit` | Persistent history |
| Settings UI under Appearance | Output cache |
| Four distinct empty states | Telemetry |
| Bounded history, oldest-first eviction | `v1.3.0-alpha` work |

## Search semantics

`CommandTimelineQueryMatches` is a case-insensitive literal substring search
implemented with `std::search` and `towlower`. It is deliberately not a regex
and deliberately not fuzzy, and `test-command-timeline.ps1` fails the build if
`std::regex`, `regex_search`, or a fuzzy matcher appears in the model.

Filtering reads `entry.cachedCommandText` only — the same bounded 4096-character
cache Phase 1 established. Output is never consulted, and no terminal-buffer
scan is triggered. `_rebuildFilter` walks the existing in-memory index; a new
command is reconciled through the same incremental path that already existed.

### Query bounds

`NormalizeCommandTimelineQuery` caps the query at
`MaxCommandTimelineQueryLength` (256 UTF-16 code units). If the cut would land
between a high and low surrogate, the orphaned lead unit is dropped, so the
result is never a lone surrogate. The XAML `TextBox` also carries
`MaxLength="256"`, and the control writes the normalized value back into the box
when truncation shortens it.

## Filtered projection

The navigation model keeps `_filtered`, a vector of indices into the caller's
entries span, and navigates over *positions within `_filtered`* rather than over
raw entry indices. An empty query fills `_filtered` with every index, so the
unfiltered case walks exactly the same code path.

Selection reconciliation:

| Situation | Result |
| --- | --- |
| Selected command still matches | Stays selected |
| Selected command stops matching | Nearest surviving match (`_nearestPosition`) |
| Following latest, new command matches | New command becomes the selection |
| 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 that the newest command is itself in the
projection, which is what stops a non-matching new command from pulling the
selection anywhere.

Every action still resolves through `viewState.selectedCommandId`, so filtered
navigation, hover, click, wheel, copy, load, jump, and the context menu all act
on the same stable command.

Only `visibleCapacity` rows are ever materialized, whatever the size of the
history behind them.

## Settings

| Setting | Default | Range | Effect |
| --- | ---: | --- | --- |
| `commandTimeline.enabled` | `true` | — | Overlay and left-side handle |
| `commandTimeline.historyLimit` | `500` | 50–5000, integer | Per-pane history |

Plumbing, in order: `MTSMSettings.h` (`MTSM_GLOBAL_SETTINGS`) →
`GlobalAppSettings.idl` → `ControlProperties.h` → `IControlSettings.idl` →
`TerminalSettings.cpp` → `ControlCore`. Defaults live in `defaults.json`; the
JSON schema in `doc/cascadia/profiles.schema.json` carries type, default,
`minimum`, and `maximum`.

An absent setting is not written back on serialization, so an existing settings
file needs no migration. An out-of-range value is accepted by the parser and
clamped by `ClampCommandTimelineHistoryLimit`, so the runtime value is always
within 50–5000 rather than failing the whole settings load.

`UpdateSettings` applies the limit to panes that already exist, and the index
constructor applies it to new panes.

Disabling the feature hides the handle and closes an open overlay
(`_applyCommandTimelineEnabledSetting`), and `ToggleCommandTimeline` refuses to
open while disabled.

## History limit and eviction

`CommandTimelineIndex::_applyHistoryLimit` erases from the front until the
history fits, and runs on entry creation, on bootstrap, and on
`SetHistoryLimit`. Consequences, all covered by tests:

- Lowering the limit evicts immediately, oldest first.
- Raising the limit never resurrects an evicted entry.
- `_nextSequence` only ever increases, so sequence IDs are never reused.
- `ReconcileLoadedInput` releases loaded-input state when the loaded command is
evicted.

## Shell degradation

`CommandTimelineEmptyState` distinguishes four cases so an unsupported shell is
never presented as an empty history:

| State | Condition | Message |
| --- | --- | --- |
| `WaitingForShell` | Capability `Unknown`, no entries | Waiting for shell integration |
| `ShellUnsupported` | Capability `Limited` | Command timeline unavailable |
| `NoCommands` | Capability `Full`, no entries | No commands yet |
| `NoMatchingCommands` | Query non-empty, entries exist, no matches | No matching commands |

No prompt parser, no heuristic output detection, and no ConPTY, VT parser,
TextBuffer, renderer, or shell protocol change.

## Input isolation

| Key | Timeline focus | Filter-box focus |
| --- | --- | --- |
| `/` | Focus filter box | Types `/` |
| Tab | Focus filter box | Types/moves per text box |
| Up / Down | Move selection | Move selection |
| Left / Right | Page edges | Caret editing |
| Enter | Load, never execute | Load, never execute |
| Escape | Cancel confirmation → clear query → close | Clear query → close |
| Ctrl+C | Copy selected command | Text box copy |

`/` and Tab are consumed by `_tryHandleCommandTimelineKey`, so neither reaches
the PTY. Filter-box text never reaches the PTY because the `TextBox` owns the
input; `_CommandTimelineSearchKeyDown` claims only Up, Down, Enter, and Escape
and leaves everything else — including IME/TSF composition — to the text box.
`_commandTimelineConsumedKeys` still de-duplicates key-down/key-up so a consumed
key never leaks on release.

`Ctrl+Tab` and user-defined key bindings keep precedence because
`_TryHandleKeyBinding` runs before the Timeline handler.

## Accessibility

- The filter box has a localized accessible name and placeholder.
- List item `PositionInSet` and `SizeOfSet` use the **filtered** result count,
so assistive technology announces a position within the matches.
- Empty states are localized resources, and status is never conveyed by color
alone.
- The overlay uses `{ThemeResource}` brushes for High Contrast and adds no
storyboard or continuous animation.
- Geometry is in device-independent pixels; the overlay changes no terminal
rows/columns, pane size, PTY size, swap-chain size, or padding.

## Performance evidence

`SearchStressAtMaximumHistoryLimit` builds a full 5000-entry history, then:

- Filters with a query matching all 5000 — projection reports 5000 matches while
materializing exactly `visibleCapacity` (20) rows.
- Runs 25 passes of narrow → no-result → broad filtering, asserting the
projection and the materialized row count return to their expected values each
pass, so repeated filtering does not accumulate.
- Asserts cached command text stays within
`5000 * DefaultMaxCachedCommandText`.
- Closes and asserts the filtered projection is released.

Measured result is recorded in the pull request rather than described as
"performs well".

## Validation

- `scripts/winterm/test-command-timeline.ps1` — extended with Phase 4 guards for
search literalness, query bounds, settings defaults/range/schema, Settings UI
presence, and the filter reading only cached command text.
- `src/cascadia/UnitTests_Control/CommandTimelineTests.cpp` — the `Search*`,
`HistoryLimit*`, and `ShellDegradation*` tests.
- `src/cascadia/UnitTests_SettingsModel/WinTermCommandTimelineTests.cpp` —
settings defaults, JSON round-trip, runtime clamping, and out-of-range
handling.
Loading