Skip to content
Open
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
9 changes: 8 additions & 1 deletion doc/release-check-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,14 @@ Net effect: UT shrinks the manual matrix to "did the wiring and UI connect", not

### Shell integration and detection

- [ ] `C295` `[new]` `[E2E]` **Detected Autofix clicks remain isolated between tabs:** With errors detected in two tabs, clicking one tab's diagnostics button submits exactly one prompt for that tab and preserves the other tab's pending opt-in. _(E2E: `Feature.AutofixRouting`.)_
- [ ] `C299` `[new]` `[E2E]` **Detected Autofix clicks remain isolated between tabs:** With errors detected in two tabs, clicking one tab's diagnostics button submits exactly one prompt for that tab and preserves the other tab's pending opt-in. _(E2E: `Feature.AutofixRouting`.)_
- [ ] `C300` `[new]` `[E2E]` **Pane context captures the completed marked command:** A single context request returns the source pane metadata together with the completed command and its error output. _(#838; E2E: `Feature.PaneContext`.)_
- [ ] `C301` `[new]` `[E2E]` **Pane context falls back to the newest unmarked output:** Shells without command marks return a bounded recent buffer tail with an explicit fallback reason. _(#838; E2E: `Feature.PaneContext`.)_
- [ ] `C302` `[new]` `[E2E]` **Explicit pane context stays isolated from the focused tab and split:** Explicit context requests read the requested pane even while a different tab or split is focused. _(#838; E2E: `Feature.PaneContext`.)_
- [ ] `C303` `[new]` `[E2E]` **Missing and closed pane context fails without active-pane fallback:** Stale or unknown pane IDs fail instead of leaking another pane's context. _(#838; E2E: `Feature.PaneContext`.)_
- [ ] `C304` `[new]` `[E2E]` **Pane context metadata-only requests omit terminal content:** A zero line or character budget returns only pane metadata. _(#838; E2E: `Feature.PaneContext`.)_
- [ ] `C305` `[new]` `[E2E]` **Pane context bounds preserve Unicode and truthful truncation:** Marked-command and buffer-tail captures honor their limits without splitting Unicode characters or hiding truncation. _(#838; E2E: `Feature.PaneContext`.)_
- [ ] `C306` `[new]` `[E2E]` **Focused agent pane context resolves to its source terminal:** Default context requests use the agent pane's source shell, while explicit agent-pane requests fail. _(#838; E2E: `Feature.PaneContext`.)_

- [ ] `C087` `[E2E]` **PowerShell shell integration installed:** Supported PowerShell profiles emit command-finished events, including non-zero marks for PowerShell-level failures on Windows PowerShell 5.1.
- [ ] `C219` `[new]` `[E2E]` **Bash / WSL shell integration installed:** Supported bash and WSL-bash profiles emit command-finished events, and the injected `PROMPT_COMMAND` is safe under `set -u` (no errors in strict-mode shells). _(#340.)_
Expand Down
168 changes: 168 additions & 0 deletions src/cascadia/TerminalApp/TerminalPage.Protocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,174 @@ namespace winrt::TerminalApp::implementation
co_return result;
}

// Keep UI-owned references in the caller's apartment; only immutable text
// and limits cross into the background operation.
static IAsyncOperation<Protocol::PaneContext> _buildBoundedPaneContext(
hstring text,
int32_t maxLines,
int32_t maxCharacters,
bool lastCommand)
{
co_await winrt::resume_background();

const auto utf8 = winrt::to_string(text);
const auto bounded = lastCommand
? ProtocolParsing::BuildBoundedCommand(utf8, maxLines, maxCharacters)
: ProtocolParsing::BuildBoundedBufferTail(utf8, maxLines, maxCharacters);
Protocol::PaneContext result{};
result.Content = winrt::to_hstring(bounded.content);
result.LineCount = bounded.lineCount;
result.Truncated = bounded.truncated;
co_return result;
}

IAsyncOperation<Protocol::PaneContext> TerminalPage::GetProtocolPaneContext(
winrt::guid sourceSessionId,
bool hasExplicitSource,
int32_t maxLines,
int32_t maxCharacters)
{
auto strong = get_strong();
co_await wil::resume_foreground(Dispatcher());

Protocol::PaneContext result{};
std::shared_ptr<Pane> targetPane;
uint32_t targetTabIndex = 0;

if (hasExplicitSource)
{
for (uint32_t tabIndex = 0; tabIndex < _tabs.Size() && !targetPane; ++tabIndex)
{
const auto tabImpl = _GetTabImpl(_tabs.GetAt(tabIndex));
const auto rootPane = tabImpl ? tabImpl->GetRootPane() : nullptr;
if (rootPane)
{
targetPane = rootPane->FindPaneBySessionId(sourceSessionId);
if (targetPane)
{
targetTabIndex = tabIndex;
}
}
}
}
else if (const auto focusedTabIndex = _GetFocusedTabIndex())
{
targetTabIndex = focusedTabIndex.value();
if (const auto tabImpl = _GetTabImpl(_tabs.GetAt(targetTabIndex)))
{
targetPane = tabImpl->GetActivePane();
if (targetPane && targetPane->IsAgentPane())
{
if (const auto rootPane = tabImpl->GetRootPane())
{
rootPane->WalkTree([&](const auto& pane) {
if (pane->IsSourceOfAgentPane())
{
targetPane = pane;
}
});
}
}
}
}

const auto sessionId = targetPane ? _getSessionIdFromPane(targetPane) : winrt::guid{};
if (!targetPane || sessionId == winrt::guid{} || targetPane->IsAgentPane())
{
co_return result;
}

Protocol::PaneInfo paneInfo{};
paneInfo.SessionId = sessionId;
paneInfo.TabId = targetTabIndex;
paneInfo.IsAgentPane = false;
paneInfo.Pid = _getPidFromPane(targetPane);

if (const auto tabImpl = _GetTabImpl(_tabs.GetAt(targetTabIndex)))
{
const auto activePane = tabImpl->GetActivePane();
paneInfo.IsActive = activePane && activePane->IsAgentPane()
? targetPane->IsSourceOfAgentPane()
: activePane == targetPane;
}

if (const auto termContent = targetPane->GetContent().try_as<TerminalApp::TerminalPaneContent>())
{
paneInfo.Title = termContent.Title();
const auto profile = termContent.GetProfile();
paneInfo.Profile = profile ? profile.Name() : L"";
}

const auto termControl = targetPane->GetTerminalControl();
if (!termControl)
{
co_return result;
}

paneInfo.Rows = termControl.ViewHeight();
paneInfo.Columns = 0;
paneInfo.Cwd = termControl.WorkingDirectory();
paneInfo.Shell = termControl.ShellName();
paneInfo.ShellVersion = termControl.ShellVersion();
result.Pane = paneInfo;

if (maxLines == 0 || maxCharacters == 0)
{
result.OutputSource = L"metadata_only";
co_return result;
}

hstring bufferTail;
try
{
const auto lastCommand = termControl.ReadLastPromptBounded(maxLines + 1, maxCharacters + 1);
if (!lastCommand.empty())
{
const auto bounded = co_await _buildBoundedPaneContext(
lastCommand,
maxLines,
maxCharacters,
true);
result.Content = bounded.Content;
result.OutputSource = L"last_command";
result.LineCount = bounded.LineCount;
result.Truncated = bounded.Truncated;
result.HasMarks = true;
co_return result;
}

result.OutputSource = L"buffer_tail";
result.FallbackReason = L"marks_unavailable";
bufferTail = termControl.ReadBufferTail(maxLines + 1, maxCharacters + maxLines + 2);
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
result.OutputSource = L"buffer_tail";
result.FallbackReason = L"last_command_error";
try
{
bufferTail = termControl.ReadBufferTail(maxLines + 1, maxCharacters + maxLines + 2);
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
result.Pane = {};
co_return result;
}
}

const auto bounded = co_await _buildBoundedPaneContext(
bufferTail,
maxLines,
maxCharacters,
false);
result.Content = bounded.Content;
result.LineCount = bounded.LineCount;
result.Truncated = bounded.Truncated;
co_return result;
}

IAsyncOperation<Windows::Foundation::Collections::IVector<Protocol::TabInfo>> TerminalPage::GetProtocolTabs()
{
auto strong = get_strong();
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalApp/TerminalPage.h
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ namespace winrt::TerminalApp::implementation
Windows::Foundation::IAsyncOperation<Windows::Foundation::Collections::IVector<Microsoft::Terminal::Protocol::TabInfo>> GetProtocolTabs();
Windows::Foundation::IAsyncOperation<Windows::Foundation::Collections::IVector<Microsoft::Terminal::Protocol::PaneInfo>> GetProtocolPanes(uint32_t tabIdFilter);
Windows::Foundation::IAsyncOperation<Microsoft::Terminal::Protocol::PaneOutput> ReadProtocolPaneOutput(winrt::guid sessionId, hstring source, int32_t maxLines);
Windows::Foundation::IAsyncOperation<Microsoft::Terminal::Protocol::PaneContext> GetProtocolPaneContext(winrt::guid sourceSessionId, bool hasExplicitSource, int32_t maxLines, int32_t maxCharacters);
Windows::Foundation::IAsyncOperation<Microsoft::Terminal::Protocol::ProcessStatus> GetProtocolProcessStatus(winrt::guid sessionId);
Windows::Foundation::IAsyncOperation<Microsoft::Terminal::Protocol::SessionVariable> GetProtocolSessionVariable(winrt::guid sessionId, hstring name);
Windows::Foundation::IAsyncOperation<bool> SetProtocolSessionVariable(winrt::guid sessionId, hstring name, hstring value);
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalApp/TerminalPage.idl
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ namespace TerminalApp
Windows.Foundation.IAsyncOperation<Windows.Foundation.Collections.IVector<Microsoft.Terminal.Protocol.TabInfo> > GetProtocolTabs();
Windows.Foundation.IAsyncOperation<Windows.Foundation.Collections.IVector<Microsoft.Terminal.Protocol.PaneInfo> > GetProtocolPanes(UInt32 tabIdFilter);
Windows.Foundation.IAsyncOperation<Microsoft.Terminal.Protocol.PaneOutput> ReadProtocolPaneOutput(Guid sessionId, String source, Int32 maxLines);
Windows.Foundation.IAsyncOperation<Microsoft.Terminal.Protocol.PaneContext> GetProtocolPaneContext(Guid sourceSessionId, Boolean hasExplicitSource, Int32 maxLines, Int32 maxCharacters);
Windows.Foundation.IAsyncOperation<Microsoft.Terminal.Protocol.ProcessStatus> GetProtocolProcessStatus(Guid sessionId);
Windows.Foundation.IAsyncOperation<Microsoft.Terminal.Protocol.SessionVariable> GetProtocolSessionVariable(Guid sessionId, String name);
Windows.Foundation.IAsyncOperation<Boolean> SetProtocolSessionVariable(Guid sessionId, String name, String value);
Expand Down
126 changes: 126 additions & 0 deletions src/cascadia/TerminalControl/ControlCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <dsound.h>

#include <DefaultSettings.h>
#include <til/unicode.h>
#include <unicode.hpp>

#include "EventArgs.h"
Expand Down Expand Up @@ -2453,6 +2454,68 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return hstring{ str };
}

hstring ControlCore::ReadBufferTail(const int32_t maxLogicalLines, const int32_t maxCharacters) const
{
THROW_HR_IF(E_INVALIDARG, maxLogicalLines <= 0 || maxCharacters <= 0);

const auto lock = _terminal->LockForReading();
const auto& textBuffer = _terminal->GetTextBuffer();
const auto lastRow = textBuffer.GetLastNonSpaceCharacter().y;

std::vector<std::wstring> chunks;
auto remainingCharacters = static_cast<size_t>(maxCharacters);
int32_t logicalLines = 1;

for (auto rowIndex = lastRow;; --rowIndex)
{
const auto& row = textBuffer.GetRowByOffset(rowIndex);
const auto rowText = row.GetText();
const auto strEnd = rowText.find_last_not_of(UNICODE_SPACE);

std::wstring chunk;
if (strEnd != decltype(rowText)::npos)
{
chunk.assign(rowText.substr(0, strEnd + 1));
}
if (!row.WasWrapForced())
{
chunk.append(L"\r\n");
}

auto start = chunk.size();
size_t selectedCharacters = 0;
while (start > 0 && selectedCharacters < remainingCharacters)
{
start = til::utf16_iterate_prev(chunk, start);
++selectedCharacters;
}
chunks.emplace_back(chunk.substr(start));
remainingCharacters -= selectedCharacters;

if (remainingCharacters == 0 || rowIndex == 0)
{
break;
}

const auto previousRow = rowIndex - 1;
if (!textBuffer.GetRowByOffset(previousRow).WasWrapForced())
{
if (logicalLines >= maxLogicalLines)
{
break;
}
++logicalLines;
}
}

std::wstring result;
for (auto it = chunks.rbegin(); it != chunks.rend(); ++it)
{
result.append(*it);
}
return hstring{ result };
}

// Returns the most recent *finished* shell prompt — the command typed
// at an OSC 133;B mark plus its output, sliced exactly between the
// command-start and command-end markers. Used by external agents
Expand Down Expand Up @@ -2517,6 +2580,69 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return {};
}

hstring ControlCore::ReadLastPromptBounded(const int32_t maxLogicalLines, const int32_t maxCharacters) const
{
THROW_HR_IF(E_INVALIDARG, maxLogicalLines <= 0 || maxCharacters <= 0);

const auto lock = _terminal->LockForReading();
const auto& marks = _terminal->GetMarkExtents();
const auto& textBuffer = _terminal->GetTextBuffer();

for (auto it = marks.rbegin(); it != marks.rend(); ++it)
{
if (!it->HasCommand() || !it->data.exitCode.has_value())
{
continue;
}

const auto startPoint = it->end;
const auto endPoint = it->outputEnd.value_or(*it->commandEnd);
std::wstring result;
auto remainingCharacters = static_cast<size_t>(maxCharacters);
int32_t logicalLines = 1;

for (auto rowIndex = startPoint.y; rowIndex <= endPoint.y && remainingCharacters > 0; ++rowIndex)
{
const auto& row = textBuffer.GetRowByOffset(rowIndex);
auto rowBegin = rowIndex == startPoint.y ? startPoint.x : 0;
auto rowEnd = rowIndex == endPoint.y ? endPoint.x : row.GetReadableColumnCount();
rowBegin = row.AdjustToGlyphStart(rowBegin);
rowEnd = row.AdjustToGlyphEnd(rowEnd);
const auto rowText = row.GetText(rowBegin, rowEnd);

size_t textEnd = 0;
size_t selectedCharacters = 0;
while (textEnd < rowText.size() && selectedCharacters < remainingCharacters)
{
textEnd = til::utf16_iterate_next(rowText, textEnd);
++selectedCharacters;
}
result.append(rowText.substr(0, textEnd));
remainingCharacters -= selectedCharacters;

if (textEnd < rowText.size() || rowIndex == endPoint.y)
{
break;
}

if (!row.WasWrapForced())
{
if (logicalLines >= maxLogicalLines || remainingCharacters == 0)
{
break;
}
result.push_back(L'\n');
--remainingCharacters;
++logicalLines;
}
}

return hstring{ result };
}

return {};
}

// Get all of our recent commands. This will only really work if the user has enabled shell integration.
Control::CommandHistoryContext ControlCore::CommandHistory() const
{
Expand Down
2 changes: 2 additions & 0 deletions src/cascadia/TerminalControl/ControlCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void SetReadOnlyMode(const bool readOnlyState);

hstring ReadEntireBuffer() const;
hstring ReadBufferTail(int32_t maxLogicalLines, int32_t maxCharacters) const;
hstring ReadLastPrompt() const;
hstring ReadLastPromptBounded(int32_t maxLogicalLines, int32_t maxCharacters) const;
Control::CommandHistoryContext CommandHistory() const;
bool QuickFixesAvailable() const noexcept;
void UpdateQuickFixes(const Windows::Foundation::Collections::IVector<hstring>& quickFixes);
Expand Down
2 changes: 2 additions & 0 deletions src/cascadia/TerminalControl/ControlCore.idl
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,9 @@ namespace Microsoft.Terminal.Control
void EnablePainting();

String ReadEntireBuffer();
String ReadBufferTail(Int32 maxLogicalLines, Int32 maxCharacters);
String ReadLastPrompt();
String ReadLastPromptBounded(Int32 maxLogicalLines, Int32 maxCharacters);
CommandHistoryContext CommandHistory();
Boolean QuickFixesAvailable { get; };

Expand Down
8 changes: 8 additions & 0 deletions src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3828,10 +3828,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
return _core.ReadEntireBuffer();
}
hstring TermControl::ReadBufferTail(const int32_t maxLogicalLines, const int32_t maxCharacters) const
{
return _core.ReadBufferTail(maxLogicalLines, maxCharacters);
}
hstring TermControl::ReadLastPrompt() const
{
return _core.ReadLastPrompt();
}
hstring TermControl::ReadLastPromptBounded(const int32_t maxLogicalLines, const int32_t maxCharacters) const
{
return _core.ReadLastPromptBounded(maxLogicalLines, maxCharacters);
}
Control::CommandHistoryContext TermControl::CommandHistory() const
{
return _core.CommandHistory();
Expand Down
Loading
Loading