Repository: microsoft/terminal (language: cpp)

Issue:
Scroll to previous/next prompt
Another suggestion: how hard would it be to account for navigation in the output buffer by command? 

For example, lets say you have the following terminal output:

```
> pwd
/some/path

> dir
// lots of output

> whoami
some-user
```

I would like to be able to skip past the output of a command and move to the input prompt of the next one. For example, if my cursor were at the bottom, pressing some keystroke would move me to the second last line. Pressing it again moves me to the input prompt where I typed dir, allowing me to easily skip past the many lines of output. This could let me then go down to review the output of the dir command at my leisure, especially useful in the situation where it scrolls on for many lines.

_Originally posted by @Neurrone in https://github.com/microsoft/terminal/pull/5804#issuecomment-632831843_

# Description of the new feature/enhancement

In macOS' terminal, pressing <kbd>cmd+up/down</kbd> scrolls the terminal to the previous/next prompt. As mentioned above, this would be a useful way to navigate the buffer.

# Proposed technical implementation details (optional)
I genuinely don't know how this is done. Maybe you could cache the position where user input occurred? Then navigate through those? Even then, when using a text editor like vim, Terminal should scroll up to the previous prompt, instead of the previous line.


Discussion / hints:
I believe this is achieved with a set of OSC escape sequences that "mark up" the prompt and command output. The original concept came from _Final Term_, which had also sorts interactive integration with command output (you can read a blog post on some of the ideas [here](https://worldwidemann.com/final-term-a-modern-terminal-emulator/)). _iTerm2_ copied some of those features and as well as adding their own extensions (see [here](https://iterm2.com/documentation-shell-integration.html)).

Edit: I should also mention that Mintty has its own way of doing this, but it's horrible abuse of the mode system, so I wouldn't want to recommend that approach.
There was some minor discussion about this in #1527. Yanking triage to throw this on the backlog.
Visual Studio Code has an implementation here:

[code.visualstudio.com/updates/v1_22#_command-tracking](https://code.visualstudio.com/updates/v1_22#_command-tracking)
[scrollToPreviousCommand](https://github.com/microsoft/vscode/blob/eb87968766f00edb208bb31fda677a9670472856/src/vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon.ts#L68)
It seems to only guess. Why no integration instead? Such as a VT seq extension?
I believe they do actually use a VT sequence. Originally it was the Final Term `OSC 133` sequence, which quite a few other terminals are using now, but they recently decided to switch to their own proprietary sequence for some reason. 🤷‍♂️

See here:
https://github.com/microsoft/vscode/blob/39cc1e1c42b2e53e83b1846c2857ca194848cc1d/src/vs/workbench/contrib/terminal/browser/xterm/shellIntegrationAddon.ts#L50-L52

Fix patch:
diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp
index 6db40713c9a..5b035f38ebe 100644
--- a/src/cascadia/TerminalApp/AppActionHandlers.cpp
+++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp
@@ -278,6 +278,55 @@ namespace winrt::TerminalApp::implementation
         args.Handled(true);
     }
 
+    void TerminalPage::_HandleScrollToMark(const IInspectable& /*sender*/,
+                                           const ActionEventArgs& args)
+    {
+        if (const auto& realArgs = args.ActionArgs().try_as<ScrollToMarkArgs>())
+        {
+            _ApplyToActiveControls([&realArgs](auto& control) {
+                control.ScrollToMark(realArgs.Direction());
+            });
+        }
+        args.Handled(true);
+    }
+    void TerminalPage::_HandleAddMark(const IInspectable& /*sender*/,
+                                      const ActionEventArgs& args)
+    {
+        if (const auto& realArgs = args.ActionArgs().try_as<AddMarkArgs>())
+        {
+            _ApplyToActiveControls([realArgs](auto& control) {
+                Control::ScrollMark mark;
+                if (realArgs.Color())
+                {
+                    mark.Color.Color = realArgs.Color().Value();
+                    mark.Color.HasValue = true;
+                }
+                else
+                {
+                    mark.Color.HasValue = false;
+                }
+                control.AddMark(mark);
+            });
+        }
+        args.Handled(true);
+    }
+    void TerminalPage::_HandleClearMark(const IInspectable& /*sender*/,
+                                        const ActionEventArgs& args)
+    {
+        _ApplyToActiveControls([](auto& control) {
+            control.ClearMark();
+        });
+        args.Handled(true);
+    }
+    void TerminalPage::_HandleClearAllMarks(const IInspectable& /*sender*/,
+                                            const ActionEventArgs& args)
+    {
+        _ApplyToActiveControls([](auto& control) {
+            control.ClearAllMarks();
+        });
+        args.Handled(true);
+    }
+
     void TerminalPage::_HandleFindMatch(const IInspectable& /*sender*/,
                                         const ActionEventArgs& args)
     {
diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp
index 9d98115c912..ec470a31dd6 100644
--- a/src/cascadia/TerminalControl/ControlCore.cpp
+++ b/src/cascadia/TerminalControl/ControlCore.cpp
@@ -37,6 +37,28 @@ constexpr const auto UpdatePatternLocationsInterval = std::chrono::milliseconds(
 
 namespace winrt::Microsoft::Terminal::Control::implementation
 {
+    static winrt::Microsoft::Terminal::Core::OptionalColor OptionalFromColor(const til::color& c)
+    {
+        Core::OptionalColor result;
+        result.Color = c;
+        result.HasValue = true;
+        return result;
+    }
+    static winrt::Microsoft::Terminal::Core::OptionalColor OptionalFromColor(const std::optional<til::color>& c)
+    {
+        Core::OptionalColor result;
+        if (c.has_value())
+        {
+            result.Color = *c;
+            result.HasValue = true;
+        }
+        else
+        {
+            result.HasValue = false;
+        }
+        return result;
+    }
+
     // Helper static function to ensure that all ambiguous-width glyphs are reported as narrow.
     // See microsoft/terminal#2066 for more info.
     static bool _IsGlyphWideForceNarrowFallback(const std::wstring_view /* glyph */)
@@ -1196,6 +1218,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
                                                      const int viewHeight,
                                                      const int bufferSize)
     {
+        if (!_initializedTerminal)
+        {
+            return;
+        }
         // Clear the regex pattern tree so the renderer does not try to render them while scrolling
         // We're **NOT** taking the lock here unlike _scrollbarChangeHandler because
         // we are already under lock (since this usually happens as a result of writing).
@@ -1854,4 +1880,140 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         // transparency, or our acrylic, or our image.
         return Opacity() < 1.0f || UseAcrylic() || !_settings->BackgroundImage().empty() || _settings->UseBackgroundImageForWindow();
     }
+
+    Windows::Foundation::Collections::IVector<Control::ScrollMark> ControlCore::ScrollMarks() const
+    {
+        auto internalMarks{ _terminal->GetScrollMarks() };
+        auto v = winrt::single_threaded_observable_vector<Control::ScrollMark>();
+        for (const auto& mark : internalMarks)
+        {
+            Control::ScrollMark m{};
+
+            // sneaky: always evaluate the color of the mark to a real value
+            // before shoving it into the optional. If the mark doesn't have a
+            // specific color set, we'll use the value from the color table
+            // that's appropriate for this category of mark. If we do have a
+            // color set, then great we'll use that. The TermControl can then
+            // always use the value in the Mark regardless if it was actually
+            // set or not.
+            m.Color = OptionalFromColor(_terminal->GetColorForMark(mark));
+            m.Start = mark.start.to_core_point();
+            m.End = mark.end.to_core_point();
+
+            v.Append(m);
+        }
+
+        return v;
+    }
+
+    void ControlCore::AddMark(const Control::ScrollMark& mark)
+    {
+        ::Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark m{};
+
+        if (mark.Color.HasValue)
+        {
+            m.color = til::color{ mark.Color.Color };
+        }
+
+        if (HasSelection())
+        {
+            m.start = til::point{ _terminal->GetSelectionAnchor() };
+            m.end = til::point{ _terminal->GetSelectionEnd() };
+        }
+        else
+        {
+            m.start = m.end = til::point{ _terminal->GetTextBuffer().GetCursor().GetPosition() };
+        }
+
+        // The version of this that only accepts a ScrollMark will automatically
+        // set the start & end to the cursor position.
+        _terminal->AddMark(m, m.start, m.end);
+    }
+    void ControlCore::ClearMark() { _terminal->ClearMark(); }
+    void ControlCore::ClearAllMarks() { _terminal->ClearAllMarks(); }
+
+    void ControlCore::ScrollToMark(const Control::ScrollToMarkDirection& direction)
+    {
+        const auto currentOffset = ScrollOffset();
+        const auto& marks{ _terminal->GetScrollMarks() };
+
+        std::optional<DispatchTypes::ScrollMark> tgt;
+
+        switch (direction)
+        {
+        case ScrollToMarkDirection::Last:
+        {
+            int highest = currentOffset;
+            for (const auto& mark : marks)
+            {
+                const auto newY = mark.start.y;
+                if (newY > highest)
+                {
+                    tgt = mark;
+                    highest = newY;
+                }
+            }
+            break;
+        }
+        case ScrollToMarkDirection::First:
+        {
+            int lowest = currentOffset;
+            for (const auto& mark : marks)
+            {
+                const auto newY = mark.start.y;
+                if (newY < lowest)
+                {
+                    tgt = mark;
+                    lowest = newY;
+                }
+            }
+            break;
+        }
+        case ScrollToMarkDirection::Next:
+        {
+            int minDistance = INT_MAX;
+            for (const auto& mark : marks)
+            {
+                const auto delta = mark.start.y - currentOffset;
+                if (delta > 0 && delta < minDistance)
+                {
+                    tgt = mark;
+                    minDistance = delta;
+                }
+            }
+            break;
+        }
+        case ScrollToMarkDirection::Previous:
+        default:
+        {
+            int minDistance = INT_MAX;
+            for (const auto& mark : marks)
+            {
+                const auto delta = currentOffset - mark.start.y;
+                if (delta > 0 && delta < minDistance)
+                {
+                    tgt = mark;
+                    minDistance = delta;
+                }
+            }
+            break;
+        }
+        }
+
+        if (tgt.has_value())
+        {
+            UserScrollViewport(tgt->start.y);
+        }
+        else
+        {
+            if (direction == ScrollToMarkDirection::Last || direction == ScrollToMarkDirection::Next)
+            {
+                UserScrollViewport(BufferHeight());
+            }
+            else if (direction == ScrollToMarkDirection::First || direction == ScrollToMarkDirection::Previous)
+            {
+                UserScrollViewport(0);
+            }
+        }
+    }
 }
diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h
index b3c8afa8a98..5f20493db17 100644
--- a/src/cascadia/TerminalControl/ControlCore.h
+++ b/src/cascadia/TerminalControl/ControlCore.h
@@ -119,6 +119,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         int BufferHeight() const;
 
         bool BracketedPasteEnabled() const noexcept;
+
+        Windows::Foundation::Collections::IVector<Control::ScrollMark> ScrollMarks() const;
+        void AddMark(const Control::ScrollMark& mark);
+        void ClearMark();
+        void ClearAllMarks();
+        void ScrollToMark(const Control::ScrollToMarkDirection& direction);
+
 #pragma endregion
 
 #pragma region ITerminalInput
diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl
index ac47a729554..13bd83c8550 100644
--- a/src/cascadia/TerminalControl/ControlCore.idl
+++ b/src/cascadia/TerminalControl/ControlCore.idl
@@ -22,7 +22,6 @@ namespace Microsoft.Terminal.Control
         IsRightButtonDown = 0x4
     };
 
-
     enum ClearBufferType
     {
         Screen,
diff --git a/src/cascadia/TerminalControl/IControlSettings.idl b/src/cascadia/TerminalControl/IControlSettings.idl
index 2fb8c598519..66ea5a33625 100644
--- a/src/cascadia/TerminalControl/IControlSettings.idl
+++ b/src/cascadia/TerminalControl/IControlSettings.idl
@@ -56,6 +56,7 @@ namespace Microsoft.Terminal.Control
         // Experimental Settings
         Boolean ForceFullRepaintRendering { get; };
         Boolean SoftwareRendering { get; };
+        Boolean ShowMarks { get; };
         Boolean UseBackgroundImageForWindow { get; };
     };
 }
diff --git a/src/cascadia/TerminalControl/ICoreState.idl b/src/cascadia/TerminalControl/ICoreState.idl
index fd11bda8f14..4d4d2e2e4ed 100644
--- a/src/cascadia/TerminalControl/ICoreState.idl
+++ b/src/cascadia/TerminalControl/ICoreState.idl
@@ -3,6 +3,32 @@
 
 namespace Microsoft.Terminal.Control
 {
+    enum MarkCategory
+    {
+        Prompt = 0,
+        Error = 1,
+        Warning = 2,
+        Info = 3
+    };
+
+    struct ScrollMark
+    {
+        // There are other members of DispatchTypes::ScrollMark, but these are
+        // all we need to expose up and set downwards currently. Additional
+        // members can be bubbled as necessary.
+        Microsoft.Terminal.Core.Point Start;
+        Microsoft.Terminal.Core.Point End; // exclusive
+        Microsoft.Terminal.Core.OptionalColor Color;
+    };
+
+    enum ScrollToMarkDirection
+    {
+        Previous,
+        Next,
+        First,
+        Last
+    };
+
     // These are properties of the TerminalCore that should be queryable by the
     // rest of the app.
     interface ICoreState
@@ -27,5 +53,11 @@ namespace Microsoft.Terminal.Control
 
         UInt64 OwningHwnd;
 
+        void AddMark(ScrollMark mark);
+        void ClearMark();
+        void ClearAllMarks();
+        void ScrollToMark(ScrollToMarkDirection direction);
+        IVector<ScrollMark> ScrollMarks { get; };
+
     };
 }
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index 5fe54d6accf..53a8f9f7785 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -124,20 +124,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
             [weakThis = get_weak()](const auto& update) {
                 if (auto control{ weakThis.get() }; !control->_IsClosing())
                 {
-                    control->_isInternalScrollBarUpdate = true;
-
-                    auto scrollBar = control->ScrollBar();
-                    if (update.newValue)
-                    {
-                        scrollBar.Value(*update.newValue);
-                    }
-                    scrollBar.Maximum(update.newMaximum);
-                    scrollBar.Minimum(update.newMinimum);
-                    scrollBar.ViewportSize(update.newViewportSize);
-                    // scroll one full screen worth at a time when the scroll bar is clicked
-                    scrollBar.LargeChange(std::max(update.newViewportSize - 1, 0.));
-
-                    control->_isInternalScrollBarUpdate = false;
+                    control->_throttledUpdateScrollbar(update);
                 }
             });
 
@@ -148,6 +135,57 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         _ApplyUISettings();
     }
 
+    void TermControl::_throttledUpdateScrollbar(const ScrollBarUpdate& update)
+    {
+        // Assumptions:
+        // * we're already not closing
+        // * caller already checked weak ptr to make sure we're still alive
+
+        _isInternalScrollBarUpdate = true;
+
+        auto scrollBar = ScrollBar();
+        if (update.newValue)
+        {
+            scrollBar.Value(*update.newValue);
+        }
+        scrollBar.Maximum(update.newMaximum);
+        scrollBar.Minimum(update.newMinimum);
+        scrollBar.ViewportSize(update.newViewportSize);
+        // scroll one full screen worth at a time when the scroll bar is clicked
+        scrollBar.LargeChange(std::max(update.newViewportSize - 1, 0.));
+
+        _isInternalScrollBarUpdate = false;
+
+        if (_showMarksInScrollbar)
+        {
+            // Update scrollbar marks
+            ScrollBarCanvas().Children().Clear();
+            const auto marks{ _core.ScrollMarks() };
+            const auto fullHeight{ ScrollBarCanvas().ActualHeight() };
+            const auto totalBufferRows{ update.newMaximum + update.newViewportSize };
+
+            for (const auto m : marks)
+            {
+                Windows::UI::Xaml::Shapes::Rectangle r;
+                Media::SolidColorBrush brush{};
+                // Sneaky: technically, a mark doesn't need to have a color set,
+                // it might want to just use the color from the palette for that
+                // kind of mark. Fortunately, ControlCore is kind enough to
+                // pre-evaluate that for us, and shove the real value into the
+                // Color member, regardless if the mark has a literal value set.
+                brush.Color(static_cast<til::color>(m.Color.Color));
+                r.Fill(brush);
+                r.Width(16.0f / 3.0f); // pip width - 1/3rd of the scrollbar width.
+                r.Height(2);
+                const auto markRow = m.Start.Y;
+                const auto fractionalHeight = markRow / totalBufferRows;
+                const auto relativePos = fractionalHeight * fullHeight;
+                ScrollBarCanvas().Children().Append(r);
+                Windows::UI::Xaml::Controls::Canvas::SetTop(r, relativePos);
+            }
+        }
+    }
+
     // Method Description:
     // - Loads the search box from the xaml UI and focuses it.
     void TermControl::CreateSearchBoxControl()
@@ -404,6 +442,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation
                                                              newMargin.Right,
                                                              newMargin.Bottom });
         }
+
+        _showMarksInScrollbar = settings.ShowMarks();
+        // Clear out all the current marks
+        ScrollBarCanvas().Children().Clear();
+        // When we hot reload the settings, the core will send us a scrollbar
+        // update. If we enabled scrollbar marks, then great, when we handle
+        // that message, we'll redraw them.
     }
 
     // Method Description:
@@ -2863,4 +2908,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         return _core.OwningHwnd();
     }
 
+    void TermControl::AddMark(const Control::ScrollMark& mark)
+    {
+        _core.AddMark(mark);
+    }
+    void TermControl::ClearMark() { _core.ClearMark(); }
+    void TermControl::ClearAllMarks() { _core.ClearAllMarks(); }
+    void TermControl::ScrollToMark(const Control::ScrollToMarkDirection& direction) { _core.ScrollToMark(direction); }
+
+    Windows::Foundation::Collections::IVector<Control::ScrollMark> TermControl::ScrollMarks() const
+    {
+        return _core.ScrollMarks();
+    }
+
 }
diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h
index 8816472e016..de091b67484 100644
--- a/src/cascadia/TerminalControl/TermControl.h
+++ b/src/cascadia/TerminalControl/TermControl.h
@@ -67,6 +67,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation
 
         uint64_t OwningHwnd();
         void OwningHwnd(uint64_t owner);
+
+        Windows::Foundation::Collections::IVector<Control::ScrollMark> ScrollMarks() const;
+        void AddMark(const Control::ScrollMark& mark);
+        void ClearMark();
+        void ClearAllMarks();
+        void ScrollToMark(const Control::ScrollToMarkDirection& direction);
+
 #pragma endregion
 
         void ScrollViewport(int viewTop);
@@ -195,6 +202,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         std::optional<Windows::UI::Xaml::DispatcherTimer> _blinkTimer;
 
         winrt::Windows::UI::Xaml::Controls::SwapChainPanel::LayoutUpdated_revoker _layoutUpdatedRevoker;
+        bool _showMarksInScrollbar{ false };
 
         inline bool _IsClosing() const noexcept
         {
@@ -287,6 +295,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         void _coreRaisedNotice(const IInspectable& s, const Control::NoticeEventArgs& args);
         void _coreWarningBell(const IInspectable& sender, const IInspectable& args);
         void _coreFoundMatch(const IInspectable& sender, const Control::FoundResultsArgs& args);
+        void _throttledUpdateScrollbar(const ScrollBarUpdate& update);
     };
 }
 
diff --git a/src/cascadia/TerminalControl/TermControl.xaml b/src/cascadia/TerminalControl/TermControl.xaml
index 94afdb9c438..dcb24c353b3 100644
--- a/src/cascadia/TerminalControl/TermControl.xaml
+++ b/src/cascadia/TerminalControl/TermControl.xaml
@@ -1243,6 +1243,30 @@
                        Style="{StaticResource ForkedScrollbarTemplate}"
                        ValueChanged="_ScrollbarChangeHandler"
                        ViewportSize="10" />
+
+            <Grid x:Name="ScrollMarksGrid"
+                  Grid.Column="1"
+                  Width="{StaticResource ScrollBarSize}"
+                  HorizontalAlignment="Right"
+                  VerticalAlignment="Stretch">
+
+                <Grid.RowDefinitions>
+                    <RowDefinition Height="Auto" />
+                    <RowDefinition Height="*" />
+                    <RowDefinition Height="Auto" />
+                </Grid.RowDefinitions>
+
+                <Border Grid.Row="0"
+                        Height="{StaticResource ScrollBarSize}" />
+                <Canvas x:Name="ScrollBarCanvas"
+                        Grid.Row="1"
+                        Width="{StaticResource ScrollBarSize}"
+                        HorizontalAlignment="Right"
+                        VerticalAlignment="Stretch" />
+                <Border Grid.Row="2"
+                        Height="{StaticResource ScrollBarSize}" />
+            </Grid>
+
         </Grid>
 
         <local:TSFInputControl x:Name="TSFInputControl"
diff --git a/src/cascadia/TerminalControl/pch.h b/src/cascadia/TerminalControl/pch.h
index 7d710c5b5c7..4958dac941d 100644
--- a/src/cascadia/TerminalControl/pch.h
+++ b/src/cascadia/TerminalControl/pch.h
@@ -45,6 +45,7 @@
 #include <winrt/Windows.UI.Xaml.Input.h>
 #include <winrt/Windows.UI.Xaml.Interop.h>
 #include <winrt/Windows.ui.xaml.markup.h>
+#include <winrt/Windows.ui.xaml.shapes.h>
 #include <winrt/Windows.ApplicationModel.DataTransfer.h>
 #include <winrt/Windows.Storage.h>
 
diff --git a/src/cascadia/TerminalCore/ICoreAppearance.idl b/src/cascadia/TerminalCore/ICoreAppearance.idl
index 131410be780..5e5ed3ff721 100644
--- a/src/cascadia/TerminalCore/ICoreAppearance.idl
+++ b/src/cascadia/TerminalCore/ICoreAppearance.idl
@@ -24,6 +24,15 @@ namespace Microsoft.Terminal.Core
         UInt8 A;
     };
 
+    // Yes, this is also just an IReference<Color>. However, IReference has some
+    // weird ownership semantics that just make it a pain for something as
+    // simple as "maybe this color doesn't have a value set".
+    struct OptionalColor
+    {
+        Boolean HasValue;
+        Microsoft.Terminal.Core.Color Color;
+    };
+
     // TerminalCore declares its own Color struct to avoid depending on
     // Windows.UI. Windows.Foundation.Point also exists, but it's composed of
     // floating-point coordinates, when we almost always need integer coordinates.
diff --git a/src/cascadia/TerminalCore/ICoreSettings.idl b/src/cascadia/TerminalCore/ICoreSettings.idl
index 5b05a72c05e..1f3c78aa09b 100644
--- a/src/cascadia/TerminalCore/ICoreSettings.idl
+++ b/src/cascadia/TerminalCore/ICoreSettings.idl
@@ -26,6 +26,9 @@ namespace Microsoft.Terminal.Core
 
         Windows.Foundation.IReference<Microsoft.Terminal.Core.Color> TabColor;
         Windows.Foundation.IReference<Microsoft.Terminal.Core.Color> StartingTabColor;
+
+        Boolean AutoMarkPrompts;
+
     };
 
 }
diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp
index bc4b01e608d..cead8dd5d87 100644
--- a/src/cascadia/TerminalCore/Terminal.cpp
+++ b/src/cascadia/TerminalCore/Terminal.cpp
@@ -49,7 +49,8 @@ Terminal::Terminal() :
     _selection{ std::nullopt },
     _taskbarState{ 0 },
     _taskbarProgress{ 0 },
-    _trimBlockSelection{ false }
+    _trimBlockSelection{ false },
+    _autoMarkPrompts{ false }
 {
     auto passAlongInput = [&](std::deque<std::unique_ptr<IInputEvent>>& inEventsToWrite) {
         if (!_pfnWriteInput)
@@ -121,6 +122,7 @@ void Terminal::UpdateSettings(ICoreSettings settings)
     _suppressApplicationTitle = settings.SuppressApplicationTitle();
     _startingTitle = settings.StartingTitle();
     _trimBlockSelection = settings.TrimBlockSelection();
+    _autoMarkPrompts = settings.AutoMarkPrompts();
 
     _terminalInput->ForceDisableWin32InputMode(settings.ForceVTInput());
 
@@ -211,6 +213,11 @@ void Terminal::UpdateAppearance(const ICoreAppearance& appearance)
     }
 
     _defaultCursorShape = cursorShape;
+
+    // Tell the control that the scrollbar has somehow changed. Used as a
+    // workaround to force the control to redraw any scrollbar marks whose color
+    // may have changed.
+    _NotifyScrollEvent();
 }
 
 void Terminal::SetCursorStyle(const DispatchTypes::CursorStyle cursorStyle)
@@ -750,6 +757,22 @@ bool Terminal::SendCharEvent(const wchar_t ch, const WORD scanCode, const Contro
         vkey = _VirtualKeyFromCharacter(ch);
     }
 
+    // GH#1527: When the user has auto mark prompts enabled, we're going to try
+    // and heuristically detect if this was the line the prompt was on.
+    // * If the key was an Enter keypress (Terminal.app also marks ^C keypresses
+    //   as prompts. That's omitted for now.)
+    // * AND we're not in the alt buffer
+    //
+    // Then treat this line like it's a prompt mark.
+    if (_autoMarkPrompts && vkey == VK_RETURN && !_inAltBuffer())
+    {
+        DispatchTypes::ScrollMark mark;
+        mark.category = DispatchTypes::MarkCategory::Prompt;
+        // Don't set the color - we'll automatically use the DEFAULT_FOREGROUND
+        // color for any MarkCategory::Prompt marks without one set.
+        AddMark(mark);
+    }
+
     // Unfortunately, the UI doesn't give us both a character down and a
     // character up event, only a character received event. So fake sending both
     // to the terminal input translator. Unless it's in win32-input-mode, it'll
@@ -1198,6 +1221,17 @@ void Terminal::_AdjustCursorPosition(const til::point proposedPosition)
 
     if (rowsPushedOffTopOfBuffer != 0)
     {
+        if (_scrollMarks.size() > 0)
+        {
+            for (auto& mark : _scrollMarks)
+            {
+                mark.start.y -= rowsPushedOffTopOfBuffer;
+            }
+            _scrollMarks.erase(std::remove_if(_scrollMarks.begin(),
+                                              _scrollMarks.end(),
+                                              [](const VirtualTerminal::DispatchTypes::ScrollMark& m) { return m.start.y < 0; }),
+                               _scrollMarks.end());
+        }
         // We have to report the delta here because we might have circled the text buffer.
         // That didn't change the viewport and therefore the TriggerScroll(void)
         // method can't detect the delta on its own.
@@ -1450,6 +1484,11 @@ void Terminal::ApplyScheme(const Scheme& colorScheme)
     _renderSettings.SetColorTableEntry(TextColor::CURSOR_COLOR, til::color{ colorScheme.CursorColor });
 
     _renderSettings.MakeAdjustedColorArray();
+
+    // Tell the control that the scrollbar has somehow changed. Used as a
+    // workaround to force the control to redraw any scrollbar marks whose color
+    // may have changed.
+    _NotifyScrollEvent();
 }
 
 bool Terminal::_inAltBuffer() const noexcept
@@ -1476,3 +1515,101 @@ void Terminal::_updateUrlDetection()
         ClearPatternTree();
     }
 }
+
+void Terminal::AddMark(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark,
+                       const til::point& start,
+                       const til::point& end)
+{
+    if (_inAltBuffer())
+    {
+        return;
+    }
+
+    DispatchTypes::ScrollMark m = mark;
+    m.start = start;
+    m.end = end;
+
+    _scrollMarks.push_back(m);
+
+    // Tell the control that the scrollbar has somehow changed. Used as a
+    // workaround to force the control to redraw any scrollbar marks
+    _NotifyScrollEvent();
+}
+
+void Terminal::ClearMark()
+{
+    // Look for one where the cursor is, or where the selection is if we have
+    // one. Any mark that intersects the cursor/selection, on either side
+    // (inclusive), will get cleared.
+    const til::point cursor{ _activeBuffer().GetCursor().GetPosition() };
+    til::point start{ cursor };
+    til::point end{ cursor };
+
+    if (IsSelectionActive())
+    {
+        start = til::point{ GetSelectionAnchor() };
+        end = til::point{ GetSelectionEnd() };
+    }
+
+    _scrollMarks.erase(std::remove_if(_scrollMarks.begin(),
+                                      _scrollMarks.end(),
+                                      [&start, &end](const auto& m) {
+                                          return (m.start >= start && m.start <= end) ||
+                                                 (m.end >= start && m.end <= end);
+                                      }),
+                       _scrollMarks.end());
+
+    // Tell the control that the scrollbar has somehow changed. Used as a
+    // workaround to force the control to redraw any scrollbar marks
+    _NotifyScrollEvent();
+}
+void Terminal::ClearAllMarks()
+{
+    _scrollMarks.clear();
+    // Tell the control that the scrollbar has somehow changed. Used as a
+    // workaround to force the control to redraw any scrollbar marks
+    _NotifyScrollEvent();
+}
+
+const std::vector<Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark>& Terminal::GetScrollMarks() const
+{
+    // TODO: GH#11000 - when the marks are stored per-buffer, get rid of this.
+    // We want to return _no_ marks when we're in the alt buffer, to effectively
+    // hide them. We need to return a reference, so we can't just ctor an empty
+    // list here just for when we're in the alt buffer.
+    static std::vector<DispatchTypes::ScrollMark> _altBufferMarks{};
+    return _inAltBuffer() ? _altBufferMarks : _scrollMarks;
+}
+
+til::color Terminal::GetColorForMark(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark) const
+{
+    if (mark.color.has_value())
+    {
+        return *mark.color;
+    }
+
+    switch (mark.category)
+    {
+    case Microsoft::Console::VirtualTerminal::DispatchTypes::MarkCategory::Prompt:
+    {
+        return _renderSettings.GetColorAlias(ColorAlias::DefaultForeground);
+    }
+    case Microsoft::Console::VirtualTerminal::DispatchTypes::MarkCategory::Error:
+    {
+        return _renderSettings.GetColorTableEntry(TextColor::BRIGHT_RED);
+    }
+    case Microsoft::Console::VirtualTerminal::DispatchTypes::MarkCategory::Warning:
+    {
+        return _renderSettings.GetColorTableEntry(TextColor::BRIGHT_YELLOW);
+    }
+    case Microsoft::Console::VirtualTerminal::DispatchTypes::MarkCategory::Success:
+    {
+        return _renderSettings.GetColorTableEntry(TextColor::BRIGHT_GREEN);
+    }
+    default:
+    case Microsoft::Console::VirtualTerminal::DispatchTypes::MarkCategory::Info:
+    {
+        return _renderSettings.GetColorAlias(ColorAlias::DefaultForeground);
+    }
+    }
+}
diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp
index 509f2c326c6..ba4c43d34cd 100644
--- a/src/cascadia/TerminalCore/Terminal.hpp
+++ b/src/cascadia/TerminalCore/Terminal.hpp
@@ -102,6 +102,11 @@ class Microsoft::Terminal::Core::Terminal final :
     RenderSettings& GetRenderSettings() noexcept { return _renderSettings; };
     const RenderSettings& GetRenderSettings() const noexcept { return _renderSettings; };
 
+    const std::vector<Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark>& GetScrollMarks() const;
+    void AddMark(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark,
+                 const til::point& start,
+                 const til::point& end);
+
 #pragma region ITerminalApi
     // These methods are defined in TerminalApi.cpp
     void PrintString(const std::wstring_view string) override;
@@ -129,11 +134,18 @@ class Microsoft::Terminal::Core::Terminal final :
     void ShowWindow(bool showOrHide) override;
     void UseAlternateScreenBuffer() override;
     void UseMainScreenBuffer() override;
+
+    void AddMark(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark) override;
+
     bool IsConsolePty() const override;
     bool IsVtInputEnabled() const override;
     void NotifyAccessibilityChange(const til::rect& changedRect) override;
 #pragma endregion
 
+    void ClearMark();
+    void ClearAllMarks();
+    til::color GetColorForMark(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark) const;
+
 #pragma region ITerminalInput
     // These methods are defined in Terminal.cpp
     bool SendKeyEvent(const WORD vkey, const WORD scanCode, const Microsoft::Terminal::Core::ControlKeyStates states, const bool keyDown) override;
@@ -290,6 +302,7 @@ class Microsoft::Terminal::Core::Terminal final :
     bool _suppressApplicationTitle;
     bool _bracketedPasteMode;
     bool _trimBlockSelection;
+    bool _autoMarkPrompts;
 
     size_t _taskbarState;
     size_t _taskbarProgress;
@@ -356,6 +369,8 @@ class Microsoft::Terminal::Core::Terminal final :
     };
     std::optional<KeyEventCodes> _lastKeyEventCodes;
 
+    std::vector<Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark> _scrollMarks;
+
     static WORD _ScanCodeFromVirtualKey(const WORD vkey) noexcept;
     static WORD _VirtualKeyFromScanCode(const WORD scanCode) noexcept;
     static WORD _VirtualKeyFromCharacter(const wchar_t ch) noexcept;
diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp
index 06df2d809ce..01894b0522d 100644
--- a/src/cascadia/TerminalCore/TerminalApi.cpp
+++ b/src/cascadia/TerminalCore/TerminalApi.cpp
@@ -298,6 +298,12 @@ void Terminal::UseMainScreenBuffer()
     CATCH_LOG();
 }
 
+void Terminal::AddMark(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark)
+{
+    const til::point cursorPos{ _activeBuffer().GetCursor().GetPosition() };
+    AddMark(mark, cursorPos, cursorPos);
+}
+
 // Method Description:
 // - Reacts to a client asking us to show or hide the window.
 // Arguments:
diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
index 148d77fb376..1b49cc3dd4c 100644
--- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
+++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
@@ -39,6 +39,10 @@ static constexpr std::string_view ScrollUpKey{ "scrollUp" };
 static constexpr std::string_view ScrollUpPageKey{ "scrollUpPage" };
 static constexpr std::string_view ScrollToTopKey{ "scrollToTop" };
 static constexpr std::string_view ScrollToBottomKey{ "scrollToBottom" };
+static constexpr std::string_view ScrollToMarkKey{ "scrollToMark" };
+static constexpr std::string_view AddMarkKey{ "addMark" };
+static constexpr std::string_view ClearMarkKey{ "clearMark" };
+static constexpr std::string_view ClearAllMarksKey{ "clearAllMarks" };
 static constexpr std::string_view SendInputKey{ "sendInput" };
 static constexpr std::string_view SetColorSchemeKey{ "setColorScheme" };
 static constexpr std::string_view SetTabColorKey{ "setTabColor" };
@@ -354,6 +358,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
                 { ShortcutAction::ScrollUpPage, RS_(L"ScrollUpPageCommandKey") },
                 { ShortcutAction::ScrollToTop, RS_(L"ScrollToTopCommandKey") },
                 { ShortcutAction::ScrollToBottom, RS_(L"ScrollToBottomCommandKey") },
+                { ShortcutAction::ScrollToMark, RS_(L"ScrollToPreviousMarkCommandKey") },
+                { ShortcutAction::AddMark, RS_(L"AddMarkCommandKey") },
+                { ShortcutAction::ClearMark, RS_(L"ClearMarkCommandKey") },
+                { ShortcutAction::ClearAllMarks, RS_(L"ClearAllMarksCommandKey") },
                 { ShortcutAction::SendInput, L"" },
                 { ShortcutAction::SetColorScheme, L"" },
                 { ShortcutAction::SetTabColor, RS_(L"ResetTabColorCommandKey") },
diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp
index 42898ee1668..3b3c56543a1 100644
--- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp
+++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp
@@ -29,6 +29,8 @@
 #include "CloseTabsAfterArgs.g.cpp"
 #include "CloseTabArgs.g.cpp"
 #include "MoveTabArgs.g.cpp"
+#include "ScrollToMarkArgs.g.cpp"
+#include "AddMarkArgs.g.cpp"
 #include "FindMatchArgs.g.cpp"
 #include "ToggleCommandPaletteArgs.g.cpp"
 #include "NewWindowArgs.g.cpp"
@@ -611,6 +613,38 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
         return RS_(L"ScrollDownCommandKey");
     }
 
+    winrt::hstring ScrollToMarkArgs::GenerateName() const
+    {
+        switch (Direction())
+        {
+        case Microsoft::Terminal::Control::ScrollToMarkDirection::Last:
+            return winrt::hstring{ RS_(L"ScrollToLastMarkCommandKey") };
+        case Microsoft::Terminal::Control::ScrollToMarkDirection::First:
+            return winrt::hstring{ RS_(L"ScrollToFirstMarkCommandKey") };
+        case Microsoft::Terminal::Control::ScrollToMarkDirection::Next:
+            return winrt::hstring{ RS_(L"ScrollToNextMarkCommandKey") };
+        case Microsoft::Terminal::Control::ScrollToMarkDirection::Previous:
+        default:
+            return winrt::hstring{ RS_(L"ScrollToPreviousMarkCommandKey") };
+        }
+        return winrt::hstring{ RS_(L"ScrollToPreviousMarkCommandKey") };
+    }
+
+    winrt::hstring AddMarkArgs::GenerateName() const
+    {
+        if (Color())
+        {
+            return winrt::hstring{
+                fmt::format(std::wstring_view(RS_(L"AddMarkWithColorCommandKey")),
+                            til::color{ Color().Value() }.ToHexString(true))
+            };
+        }
+        else
+        {
+            return RS_(L"AddMarkCommandKey");
+        }
+    }
+
     winrt::hstring MoveTabArgs::GenerateName() const
     {
         winrt::hstring directionString;
diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h
index 05bd831a3ae..03f69252561 100644
--- a/src/cascadia/TerminalSettingsModel/ActionArgs.h
+++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h
@@ -30,6 +30,8 @@
 #include "CloseTabArgs.g.h"
 #include "ScrollUpArgs.g.h"
 #include "ScrollDownArgs.g.h"
+#include "ScrollToMarkArgs.g.h"
+#include "AddMarkArgs.g.h"
 #include "MoveTabArgs.g.h"
 #include "ToggleCommandPaletteArgs.g.h"
 #include "FindMatchArgs.g.h"
@@ -180,6 +182,14 @@ private:
 #define SCROLL_DOWN_ARGS(X) \
     X(Windows::Foundation::IReference<uint32_t>, RowsToScroll, "rowsToScroll", false, nullptr)
 
+////////////////////////////////////////////////////////////////////////////////
+#define SCROLL_TO_MARK_ARGS(X) \
+    X(Microsoft::Terminal::Control::ScrollToMarkDirection, Direction, "direction", false, Microsoft::Terminal::Control::ScrollToMarkDirection::Previous)
+
+////////////////////////////////////////////////////////////////////////////////
+#define ADD_MARK_ARGS(X) \
+    X(Windows::Foundation::IReference<Microsoft::Terminal::Core::Color>, Color, "color", false, nullptr)
+
 ////////////////////////////////////////////////////////////////////////////////
 #define TOGGLE_COMMAND_PALETTE_ARGS(X) \
     X(CommandPaletteLaunchMode, LaunchMode, "launchMode", false, CommandPaletteLaunchMode::Action)
@@ -612,6 +622,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
 
     ACTION_ARGS_STRUCT(ScrollDownArgs, SCROLL_DOWN_ARGS);
 
+    ACTION_ARGS_STRUCT(ScrollToMarkArgs, SCROLL_TO_MARK_ARGS);
+
+    ACTION_ARGS_STRUCT(AddMarkArgs, ADD_MARK_ARGS);
+
     ACTION_ARGS_STRUCT(ToggleCommandPaletteArgs, TOGGLE_COMMAND_PALETTE_ARGS);
 
     ACTION_ARGS_STRUCT(FindMatchArgs, FIND_MATCH_ARGS);
diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl
index ddc05c28dc9..77c817315b6 100644
--- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl
+++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl
@@ -290,6 +290,17 @@ namespace Microsoft.Terminal.Settings.Model
         Windows.Foundation.IReference<UInt32> RowsToScroll { get; };
     };
 
+
+    [default_interface] runtimeclass ScrollToMarkArgs : IActionArgs
+    {
+        Microsoft.Terminal.Control.ScrollToMarkDirection Direction { get; };
+    };
+
+    [default_interface] runtimeclass AddMarkArgs : IActionArgs
+    {
+        Windows.Foundation.IReference<Microsoft.Terminal.Core.Color> Color { get; };
+    };
+
     [default_interface] runtimeclass ToggleCommandPaletteArgs : IActionArgs
     {
         CommandPaletteLaunchMode LaunchMode { get; };
diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
index d6a23dc203d..c9bd6bd2076 100644
--- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
+++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
@@ -47,6 +47,10 @@
     ON_ALL_ACTIONS(ScrollDownPage)         \
     ON_ALL_ACTIONS(ScrollToTop)            \
     ON_ALL_ACTIONS(ScrollToBottom)         \
+    ON_ALL_ACTIONS(ScrollToMark)           \
+    ON_ALL_ACTIONS(AddMark)                \
+    ON_ALL_ACTIONS(ClearMark)              \
+    ON_ALL_ACTIONS(ClearAllMarks)          \
     ON_ALL_ACTIONS(ResizePane)             \
     ON_ALL_ACTIONS(MoveFocus)              \
     ON_ALL_ACTIONS(MovePane)               \
@@ -119,6 +123,8 @@
     ON_ALL_ACTIONS_WITH_ARGS(ResizePane)           \
     ON_ALL_ACTIONS_WITH_ARGS(ScrollDown)           \
     ON_ALL_ACTIONS_WITH_ARGS(ScrollUp)             \
+    ON_ALL_ACTIONS_WITH_ARGS(ScrollToMark)         \
+    ON_ALL_ACTIONS_WITH_ARGS(AddMark)              \
     ON_ALL_ACTIONS_WITH_ARGS(SendInput)            \
     ON_ALL_ACTIONS_WITH_ARGS(SetColorScheme)       \
     ON_ALL_ACTIONS_WITH_ARGS(SetTabColor)          \
diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h
index 5c90f6e5fb3..e425bdfd877 100644
--- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h
+++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h
@@ -78,7 +78,9 @@ Author(s):
     X(bool, UseAtlasEngine, "experimental.useAtlasEngine", false)                                                                                              \
     X(Windows::Foundation::Collections::IVector<winrt::hstring>, BellSound, "bellSound", nullptr)                                                              \
     X(bool, Elevate, "elevate", false)                                                                                                                         \
-    X(bool, VtPassthrough, "experimental.connection.passthroughMode", false)
+    X(bool, VtPassthrough, "experimental.connection.passthroughMode", false)                                                                                   \
+    X(bool, AutoMarkPrompts, "experimental.autoMarkPrompts", false)                                                                                            \
+    X(bool, ShowMarks, "experimental.showMarksOnScrollbar", false)
 
 // Intentionally omitted Profile settings:
 // * Name
diff --git a/src/cascadia/TerminalSettingsModel/Profile.idl b/src/cascadia/TerminalSettingsModel/Profile.idl
index f8f3dc8d1cd..76357b65898 100644
--- a/src/cascadia/TerminalSettingsModel/Profile.idl
+++ b/src/cascadia/TerminalSettingsModel/Profile.idl
@@ -84,5 +84,7 @@ namespace Microsoft.Terminal.Settings.Model
         INHERITABLE_PROFILE_SETTING(Windows.Foundation.Collections.IVector<String>, BellSound);
 
         INHERITABLE_PROFILE_SETTING(Boolean, Elevate);
+        INHERITABLE_PROFILE_SETTING(Boolean, AutoMarkPrompts);
+        INHERITABLE_PROFILE_SETTING(Boolean, ShowMarks);
     }
 }
diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
index ec3868faa5c..9b7e6fbe661 100644
--- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
@@ -368,6 +368,31 @@
   <data name="ScrollToBottomCommandKey" xml:space="preserve">
     <value>Scroll to the bottom of history</value>
   </data>
+  <data name="ScrollToPreviousMarkCommandKey" xml:space="preserve">
+    <value>Scroll to the the previous mark</value>
+  </data>
+  <data name="ScrollToNextMarkCommandKey" xml:space="preserve">
+    <value>Scroll to the next mark</value>
+  </data>
+  <data name="ScrollToFirstMarkCommandKey" xml:space="preserve">
+    <value>Scroll to the first mark</value>
+  </data>
+  <data name="ScrollToLastMarkCommandKey" xml:space="preserve">
+    <value>Scroll to the last mark</value>
+  </data>
+  <data name="AddMarkCommandKey" xml:space="preserve">
+    <value>Add a scroll mark</value>
+  </data>
+  <data name="AddMarkWithColorCommandKey" xml:space="preserve">
+    <value>Add a scroll mark, color:{0}</value>
+    <comment>{Locked="color"}. "color" is a literal parameter name that shouldn't be translated. {0} will be replaced with a color in hexadecimal format, like `#123456`</comment>
+  </data>
+  <data name="ClearMarkCommandKey" xml:space="preserve">
+    <value>Clear mark</value>
+  </data>
+  <data name="ClearAllMarksCommandKey" xml:space="preserve">
+    <value>Clear all marks</value>
+  </data>
   <data name="SendInputCommandKey" xml:space="preserve">
     <value>Send Input: "{0}"</value>
     <comment>{0} will be replaced with a string of input as defined by the user</comment>
@@ -536,4 +561,4 @@
   <data name="ToggleBlockSelectionCommandKey" xml:space="preserve">
     <value>Toggle block selection</value>
   </data>
-</root>
\ No newline at end of file
+</root>
diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp
index e8f0aa3d2ce..0ba57857303 100644
--- a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp
+++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp
@@ -281,6 +281,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
         }
 
         _Elevate = profile.Elevate();
+        _AutoMarkPrompts = Feature_ScrollbarMarks::IsEnabled() && profile.AutoMarkPrompts();
+        _ShowMarks = Feature_ScrollbarMarks::IsEnabled() && profile.ShowMarks();
     }
 
     // Method Description:
diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.h b/src/cascadia/TerminalSettingsModel/TerminalSettings.h
index 3b5ca2d0852..9786af8aa03 100644
--- a/src/cascadia/TerminalSettingsModel/TerminalSettings.h
+++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.h
@@ -158,6 +158,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
 
         INHERITABLE_SETTING(Model::TerminalSettings, bool, Elevate, false);
 
+        INHERITABLE_SETTING(Model::TerminalSettings, bool, AutoMarkPrompts, false);
+        INHERITABLE_SETTING(Model::TerminalSettings, bool, ShowMarks, false);
+
     private:
         std::optional<std::array<Microsoft::Terminal::Core::Color, COLOR_TABLE_SIZE>> _ColorTable;
         gsl::span<Microsoft::Terminal::Core::Color> _getColorTableImpl();
diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h
index 65df15cdbd5..9dd273ee3c9 100644
--- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h
+++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h
@@ -546,3 +546,14 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage)
         pair_type{ "setAsDefault", ValueType::SetAsDefault },
     };
 };
+
+// Possible ScrollToMarkDirection values
+JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Control::ScrollToMarkDirection)
+{
+    JSON_MAPPINGS(4) = {
+        pair_type{ "previous", ValueType::Previous },
+        pair_type{ "next", ValueType::Next },
+        pair_type{ "first", ValueType::First },
+        pair_type{ "last", ValueType::Last },
+    };
+};
diff --git a/src/cascadia/inc/ControlProperties.h b/src/cascadia/inc/ControlProperties.h
index f9c880e83db..6d3b42a4dbd 100644
--- a/src/cascadia/inc/ControlProperties.h
+++ b/src/cascadia/inc/ControlProperties.h
@@ -46,7 +46,8 @@
     X(bool, ForceVTInput, false)                                                                                  \
     X(winrt::hstring, StartingTitle)                                                                              \
     X(bool, DetectURLs, true)                                                                                     \
-    X(bool, VtPassthrough, false)
+    X(bool, VtPassthrough, false)                                                                                 \
+    X(bool, AutoMarkPrompts)
 
 // --------------------------- Control Settings ---------------------------
 //  All of these settings are defined in IControlSettings.
@@ -68,5 +69,6 @@
     X(winrt::Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode, winrt::Microsoft::Terminal::Control::TextAntialiasingMode::Grayscale) \
     X(bool, ForceFullRepaintRendering, false)                                                                                                            \
     X(bool, SoftwareRendering, false)                                                                                                                    \
+    X(bool, UseAtlasEngine, false)                                                                                                                       \
     X(bool, UseBackgroundImageForWindow, false)                                                                                                          \
-    X(bool, UseAtlasEngine, false)
+    X(bool, ShowMarks, false)
diff --git a/src/cascadia/ut_app/TerminalApp.Unit.Tests.manifest b/src/cascadia/ut_app/TerminalApp.Unit.Tests.manifest
index 8e1afe241f0..8a61b53a59d 100644
--- a/src/cascadia/ut_app/TerminalApp.Unit.Tests.manifest
+++ b/src/cascadia/ut_app/TerminalApp.Unit.Tests.manifest
@@ -27,6 +27,7 @@
     <windowsSettings>
       <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
       <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
+      <heapType xmlns="http://schemas.microsoft.com/SMI/2020/WindowsSettings">SegmentHeap</heapType>
     </windowsSettings>
   </application>
 </assembly>
diff --git a/src/features.xml b/src/features.xml
index 985297e10f4..cbe55f3a5ad 100644
--- a/src/features.xml
+++ b/src/features.xml
@@ -116,4 +116,16 @@
             <brandingToken>Preview</brandingToken>
         </alwaysDisabledBrandingTokens>
     </feature>
+
+    <feature>
+        <name>Feature_ScrollbarMarks</name>
+        <description>Enables the experimental scrollbar marks feature.</description>
+        <stage>AlwaysDisabled</stage>
+        <!-- Did it this way instead of "release tokens" to ensure it won't go into Windows Inbox either... -->
+        <alwaysEnabledBrandingTokens>
+            <brandingToken>Dev</brandingToken>
+            <brandingToken>Preview</brandingToken>
+        </alwaysEnabledBrandingTokens>
+    </feature>
+
 </featureStaging>
diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp
index e91241336d3..900869af2a8 100644
--- a/src/host/outputStream.cpp
+++ b/src/host/outputStream.cpp
@@ -481,3 +481,8 @@ void ConhostInternalGetSet::NotifyAccessibilityChange(const til::rect& changedRe
             changedRect.bottom - 1);
     }
 }
+
+void ConhostInternalGetSet::AddMark(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& /*mark*/)
+{
+    // Not implemented for conhost.
+}
diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp
index 9a3bc2a0bdd..2acd85ae295 100644
--- a/src/host/outputStream.hpp
+++ b/src/host/outputStream.hpp
@@ -74,6 +74,8 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal::
 
     void NotifyAccessibilityChange(const til::rect& changedRect) override;
 
+    void AddMark(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark) override;
+
 private:
     Microsoft::Console::IIoProvider& _io;
 };
diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp
index 2ae3b67a6ab..afa1de0f797 100644
--- a/src/terminal/adapter/DispatchTypes.hpp
+++ b/src/terminal/adapter/DispatchTypes.hpp
@@ -496,4 +496,22 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
     constexpr VTInt s_sDECCOLMSetColumns = 132;
     constexpr VTInt s_sDECCOLMResetColumns = 80;
 
+    enum class MarkCategory : size_t
+    {
+        Prompt = 0,
+        Error = 1,
+        Warning = 2,
+        Success = 3,
+        Info = 4
+    };
+
+    struct ScrollMark
+    {
+        std::optional<til::color> color;
+        til::point start;
+        til::point end; // exclusive
+        MarkCategory category{ MarkCategory::Info };
+        // Other things we may want to think about in the future are listed in
+        // GH#11000
+    };
 }
diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp
index bf6cdbbb934..17c7374c309 100644
--- a/src/terminal/adapter/ITermDispatch.hpp
+++ b/src/terminal/adapter/ITermDispatch.hpp
@@ -135,6 +135,8 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
 
     virtual bool DoConEmuAction(const std::wstring_view string) = 0;
 
+    virtual bool DoITerm2Action(const std::wstring_view string) = 0;
+
     virtual StringHandler DownloadDRCS(const VTInt fontNumber,
                                        const VTParameter startChar,
                                        const DispatchTypes::DrcsEraseControl eraseControl,
diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp
index 277a07fafdc..32910133560 100644
--- a/src/terminal/adapter/ITerminalApi.hpp
+++ b/src/terminal/adapter/ITerminalApi.hpp
@@ -76,5 +76,7 @@ namespace Microsoft::Console::VirtualTerminal
         virtual bool IsConsolePty() const = 0;
 
         virtual void NotifyAccessibilityChange(const til::rect& changedRect) = 0;
+
+        virtual void AddMark(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark) = 0;
     };
 }
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index 0531b4aef66..a94b4f0c6a4 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -2465,6 +2465,50 @@ bool AdaptDispatch::DoConEmuAction(const std::wstring_view string)
     return false;
 }
 
+// Method Description:
+// - Performs a iTerm2 action
+// - Ascribes to the ITermDispatch interface
+// - Currently, the actions we support are:
+//   * `OSC1337;SetMark`: mark a line as a prompt line
+// - Not actually used in conhost
+// Arguments:
+// - string: contains the parameters that define which action we do
+// Return Value:
+// - false in conhost, true for the SetMark action, otherwise false.
+bool AdaptDispatch::DoITerm2Action(const std::wstring_view string)
+{
+    // This is not implemented in conhost.
+    if (_api.IsConsolePty())
+    {
+        // Flush the frame manually, to make sure marks end up on the right line, like the alt buffer sequence.
+        _renderer.TriggerFlush(false);
+        return false;
+    }
+
+    if constexpr (!Feature_ScrollbarMarks::IsEnabled())
+    {
+        return false;
+    }
+
+    const auto parts = Utils::SplitString(string, L';');
+
+    if (parts.size() < 1)
+    {
+        return false;
+    }
+
+    const auto action = til::at(parts, 0);
+
+    if (action == L"SetMark")
+    {
+        DispatchTypes::ScrollMark mark;
+        mark.category = DispatchTypes::MarkCategory::Prompt;
+        _api.AddMark(mark);
+        return true;
+    }
+    return false;
+}
+
 // Method Description:
 // - DECDLD - Downloads one or more characters of a dynamically redefinable
 //   character set (DRCS) with a specified pixel pattern. The pixel array is
diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp
index 41e91885e25..ca6a221fc76 100644
--- a/src/terminal/adapter/adaptDispatch.hpp
+++ b/src/terminal/adapter/adaptDispatch.hpp
@@ -130,6 +130,8 @@ namespace Microsoft::Console::VirtualTerminal
 
         bool DoConEmuAction(const std::wstring_view string) override;
 
+        bool DoITerm2Action(const std::wstring_view string) override;
+
         StringHandler DownloadDRCS(const VTInt fontNumber,
                                    const VTParameter startChar,
                                    const DispatchTypes::DrcsEraseControl eraseControl,
diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp
index 6da351861f2..49c690aabbd 100644
--- a/src/terminal/adapter/termDispatch.hpp
+++ b/src/terminal/adapter/termDispatch.hpp
@@ -128,6 +128,8 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
 
     bool DoConEmuAction(const std::wstring_view /*string*/) override { return false; }
 
+    bool DoITerm2Action(const std::wstring_view /*string*/) override { return false; }
+
     StringHandler DownloadDRCS(const VTInt /*fontNumber*/,
                                const VTParameter /*startChar*/,
                                const DispatchTypes::DrcsEraseControl /*eraseControl*/,
diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp
index 8475f181542..64454ca0367 100644
--- a/src/terminal/adapter/ut_adapter/adapterTest.cpp
+++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp
@@ -237,6 +237,11 @@ class TestGetSet final : public ITerminalApi
         Log::Comment(L"NotifyAccessibilityChange MOCK called...");
     }
 
+    void AddMark(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& /*mark*/) override
+    {
+        Log::Comment(L"AddMark MOCK called...");
+    }
+
     void PrepData()
     {
         PrepData(CursorDirection::UP); // if called like this, the cursor direction doesn't matter.
diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp
index 5e0a9e14f3d..08972476e7e 100644
--- a/src/terminal/parser/OutputStateMachineEngine.cpp
+++ b/src/terminal/parser/OutputStateMachineEngine.cpp
@@ -837,6 +837,11 @@ bool OutputStateMachineEngine::ActionOscDispatch(const wchar_t /*wch*/,
         success = _dispatch->DoConEmuAction(string);
         break;
     }
+    case OscActionCodes::ITerm2Action:
+    {
+        success = _dispatch->DoITerm2Action(string);
+        break;
+    }
     default:
         // If no functions to call, overall dispatch was a failure.
         success = false;
diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp
index 21055d09bc1..f37843e2495 100644
--- a/src/terminal/parser/OutputStateMachineEngine.hpp
+++ b/src/terminal/parser/OutputStateMachineEngine.hpp
@@ -187,7 +187,8 @@ namespace Microsoft::Console::VirtualTerminal
             SetClipboard = 52,
             ResetForegroundColor = 110, // Not implemented
             ResetBackgroundColor = 111, // Not implemented
-            ResetCursorColor = 112
+            ResetCursorColor = 112,
+            ITerm2Action = 1337,
         };
 
         bool _GetOscTitle(const std::wstring_view string,


Test patch: