Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,15 @@ Inside the agent pane, type `/` to see available commands. Type `/help` at any t
| `/restart` | Restart the agent with a clean session |
| `/stop` | Cancel the in-flight prompt |
| `/sessions` | Open agent management (same as <kbd>Ctrl+Shift+/</kbd>) |
| `/agent [id]` | Pick the agent source for this tab. In a WSL pane, the picker includes agents installed on Windows and in that pane's WSL distro; it never offers other distros. |
| `/model [id]` | Pick the model for this pane; bare `/model` opens a picker, `/model <id>` switches directly |

Profiles use the global Windows-hosted agent by default. In a profile's
**General** settings, **Agent pane agent** can instead select an ACP agent
installed in that profile's WSL distro. The picker only lists the Windows host
and that one distro. An explicit profile selection is strict: if that agent
cannot start, the pane reports the failure without switching to another agent.

### Agent Management

<p align="center">
Expand Down
8 changes: 8 additions & 0 deletions doc/cascadia/profiles.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3195,6 +3195,14 @@
"null"
]
},
"agentPaneBackend": {
"description": "Selects an optional profile-specific agent pane agent. An empty value uses the global Windows-hosted agent. WSL values identify an agent installed in the profile's own distro.",
"type": [
"string",
"null"
],
"pattern": "^(|host:[^:]+|wsl:[^:]+:[^:]+)$"
},
"suppressApplicationTitle": {
"description": "When set to true, tabTitle overrides the default title of the tab and any title change messages from the application will be suppressed. When set to false, tabTitle behaves as normal.",
"type": "boolean",
Expand Down
9 changes: 8 additions & 1 deletion src/cascadia/TerminalApp/AgentPaneContent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,15 @@ namespace winrt::TerminalApp::implementation
void AgentPaneContent::UpdateAgentStatus(const winrt::hstring& name,
const winrt::hstring& version,
const winrt::hstring& model,
const winrt::hstring& state)
const winrt::hstring& state,
const winrt::hstring& backend)
{
const bool nameChanged = _agentName != name;
_agentName = name;
_agentVersion = version;
_agentModel = model;
_agentState = state;
_agentBackend = backend;
_refreshLabel();
if (nameChanged)
{
Expand Down Expand Up @@ -231,6 +233,11 @@ namespace winrt::TerminalApp::implementation
else
{
text = std::wstring{ _agentName };
if (!_agentBackend.empty())
{
text += L" \u00B7 ";
text += _agentBackend;
}
if (!_agentVersion.empty())
{
text += L" ";
Expand Down
4 changes: 3 additions & 1 deletion src/cascadia/TerminalApp/AgentPaneContent.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ namespace winrt::TerminalApp::implementation
void UpdateAgentStatus(const winrt::hstring& name,
const winrt::hstring& version,
const winrt::hstring& model,
const winrt::hstring& state);
const winrt::hstring& state,
const winrt::hstring& backend);

void SetSessionsView(bool active);
// Whether the agent pane is currently displaying its sessions view
Expand Down Expand Up @@ -137,6 +138,7 @@ namespace winrt::TerminalApp::implementation
winrt::hstring _agentVersion{};
winrt::hstring _agentModel{};
winrt::hstring _agentState{};
winrt::hstring _agentBackend{};

// When true, the bar replaces "<agent> <version>" with "Agent sessions"
// and hides the agent logo. Driven by TerminalPage::OnAgentStateChanged
Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalApp/AgentPaneContent.idl
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace TerminalApp

// Pushed in from outside when wta emits an `agent_status` event.
// state is one of "connecting" | "connected" | "failed" | "disconnected".
void UpdateAgentStatus(String name, String version, String model, String state);
void UpdateAgentStatus(String name, String version, String model, String state, String backend);

// Toggle the bar's "agent name + version" header into a "Agent sessions"
// label (and hide the agent logo) while wta's session management view is
Expand Down
12 changes: 11 additions & 1 deletion src/cascadia/TerminalApp/Tab.h
Original file line number Diff line number Diff line change
Expand Up @@ -138,20 +138,28 @@ namespace winrt::TerminalApp::implementation
const winrt::hstring& AgentIdOverride() const noexcept { return _agentIdOverride; }
const winrt::hstring& AgentModelOverride() const noexcept { return _agentModelOverride; }
const winrt::hstring& AgentCustomCommandOverride() const noexcept { return _agentCustomCommandOverride; }
const winrt::hstring& AgentSourceOverride() const noexcept { return _agentSourceOverride; }
const winrt::hstring& AgentWslDistroOverride() const noexcept { return _agentWslDistroOverride; }
bool HasAgentOverride() const noexcept { return !_agentIdOverride.empty(); }
void SetAgentOverride(const winrt::hstring& agentId,
const winrt::hstring& model,
const winrt::hstring& customCommand)
const winrt::hstring& customCommand,
const winrt::hstring& source = L"host",
const winrt::hstring& wslDistro = {})
{
_agentIdOverride = agentId;
_agentModelOverride = model;
_agentCustomCommandOverride = customCommand;
_agentSourceOverride = source;
_agentWslDistroOverride = wslDistro;
}
void ClearAgentOverride() noexcept
{
_agentIdOverride = {};
_agentModelOverride = {};
_agentCustomCommandOverride = {};
_agentSourceOverride = {};
_agentWslDistroOverride = {};
}

// Stable per-tab identifier (GUID string). Survives tab reordering
Expand Down Expand Up @@ -243,6 +251,8 @@ namespace winrt::TerminalApp::implementation
winrt::hstring _agentIdOverride{};
winrt::hstring _agentModelOverride{};
winrt::hstring _agentCustomCommandOverride{};
winrt::hstring _agentSourceOverride{};
winrt::hstring _agentWslDistroOverride{};

winrt::Microsoft::Terminal::Settings::Model::IconStyle _lastIconStyle;
winrt::hstring _lastIconPath{};
Expand Down
147 changes: 132 additions & 15 deletions src/cascadia/TerminalApp/TerminalPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "../WinRTUtils/inc/WtExeUtils.h"
#include "../inc/AgentRegistry.h"
#include "../inc/AgentPolicy.h"
#include "../inc/AgentPaneBackend.h"
#include "../TerminalSettingsAppAdapterLib/TerminalSettings.h"
#include "AgentPaneContent.h"
#include "AgentPaneDragStash.h"
Expand Down Expand Up @@ -1391,25 +1392,32 @@ namespace winrt::TerminalApp::implementation
TerminalPage::AgentSettingsSnapshot TerminalPage::_CaptureAgentSettingsSnapshot() const
{
const auto& globals = _settings.GlobalSettings();
return AgentSettingsSnapshot{
AgentSettingsSnapshot snapshot{
std::wstring{ globals.AcpAgent() },
std::wstring{ globals.AcpModel() },
std::wstring{ globals.AcpCustomCommand() },
std::wstring{ globals.DelegateAgent() },
std::wstring{ globals.DelegateModel() },
std::wstring{ globals.DelegateCustomCommand() },
};
for (const auto& profile : _settings.AllProfiles())
{
snapshot.profileBackends.emplace_back(
profile.Guid(),
std::wstring{ profile.AgentPaneBackend() });
}
return snapshot;
}

bool TerminalPage::_AgentSettingsChanged(const AgentSettingsSnapshot& a, const AgentSettingsSnapshot& b)
{
// Only the agent-CLI *identity* (which binary + agent-id) forces a
// master respawn. acp-model and the delegate-* fields are hot-updated
// over the protocol by _EmitAgentRuntimeConfigIfChanged and must NOT
// trigger a teardown/rebuild here — that was the bug where changing
// the delegate agent restarted the whole agent pane connection.
// Agent identity changes rebuild helpers. acp-model and delegate-*
// fields are hot-updated over the protocol and must not trigger a
// teardown. A profile backend is part of identity because it changes
// both the agent id and the execution source for that profile.
return a.acpAgent != b.acpAgent ||
a.acpCustomCommand != b.acpCustomCommand;
a.acpCustomCommand != b.acpCustomCommand ||
a.profileBackends != b.profileBackends;
}

TerminalPage::AgentRuntimeConfigSnapshot TerminalPage::_CaptureAgentRuntimeConfig() const
Expand Down Expand Up @@ -1878,13 +1886,58 @@ namespace winrt::TerminalApp::implementation
winrt::hstring effectiveAgentId;
winrt::hstring effectiveModel;
winrt::hstring agentCliPath;
winrt::hstring effectiveAgentSource{ L"host" };
winrt::hstring effectiveAgentWslDistro;
const bool hasAgentOverride = tab->HasAgentOverride();
bool hasProfileBackend = false;
if (hasAgentOverride)
{
effectiveAgentId = tab->AgentIdOverride();
effectiveModel = tab->AgentModelOverride();
effectiveAgentSource = tab->AgentSourceOverride();
effectiveAgentWslDistro = tab->AgentWslDistroOverride();
agentCliPath = _ResolveAgentCliPathForId(effectiveAgentId, effectiveModel, tab->AgentCustomCommandOverride());
}
else if (const auto profile = tab->GetFocusedProfile())
{
const auto configured = std::wstring_view{ profile.AgentPaneBackend() };
hasProfileBackend = !configured.empty();
if (const auto backend = ::Microsoft::Terminal::Settings::Model::AgentPaneBackend::Parse(configured))
{
effectiveAgentId = winrt::hstring{ backend->agentId };
effectiveModel = {};
Comment thread
DDKinger marked this conversation as resolved.
effectiveAgentSource = backend->source == ::Microsoft::Terminal::Settings::Model::AgentPaneBackendSource::Wsl ?
winrt::hstring{ L"wsl" } :
winrt::hstring{ L"host" };
effectiveAgentWslDistro = winrt::hstring{ backend->wslDistro };
namespace Registry = ::Microsoft::Terminal::Settings::Model::AgentRegistry;
const auto allowedAgents = Registry::FilteredAcpAgents();
const auto knownAndAllowed = std::any_of(
allowedAgents.begin(),
allowedAgents.end(),
[&](const auto& agent) {
return agent.id == std::wstring_view{ effectiveAgentId };
});
if (knownAndAllowed)
{
agentCliPath = _ResolveAgentCliPathForId(effectiveAgentId, effectiveModel, {});
}
if (backend->source == ::Microsoft::Terminal::Settings::Model::AgentPaneBackendSource::Wsl)
{
const auto shellName = tab->GetActiveTerminalControl().ShellName();
const auto expectedShell = winrt::hstring{ L"wsl:" + backend->wslDistro };
if (!shellName.empty() && shellName != expectedShell)
{
_agentPaneLog("_AutoCreateHiddenAgentPaneShared: profile WSL backend does not match active shell");
agentCliPath = {};
}
Comment thread
DDKinger marked this conversation as resolved.
}
}
else if (hasProfileBackend)
{
_agentPaneLog("_AutoCreateHiddenAgentPaneShared: invalid profile agentPaneBackend");
}
}
// `_ResolveAgentCliPathForId` returns empty to signal "fall back"
// (the override id is unknown / blocked by GPO, or a custom override
// has no usable command line). Honor that contract here instead of
Expand All @@ -1894,10 +1947,12 @@ namespace winrt::TerminalApp::implementation
// to the global/default agent (the same resolution as the
// no-override case). The GPO all-agents-blocked case is still caught
// by the policy check below.
if (!hasAgentOverride || agentCliPath.empty())
if (!hasAgentOverride && !hasProfileBackend)
{
effectiveAgentId = globals.EffectiveAcpAgent();
effectiveModel = globals.AcpModel();
effectiveAgentSource = L"host";
effectiveAgentWslDistro = {};
agentCliPath = _ResolveEffectiveAgentCliPath(globals, [this]() { return _DetectAgentCli(); });
// When the global selection is absent/blocked,
// _ResolveEffectiveAgentCliPath falls back to auto-detection and
Expand All @@ -1915,6 +1970,12 @@ namespace winrt::TerminalApp::implementation
}
}

if ((hasAgentOverride || hasProfileBackend) && agentCliPath.empty())
{
_agentPaneLog("_AutoCreateHiddenAgentPaneShared: explicit agent selection cannot be launched");
return false;
}

// GPO `AllowedAgents` enforcement — mirror the legacy path so a
// managed environment that blocks all agents doesn't get a
// working pane via the shared master.
Expand Down Expand Up @@ -2006,6 +2067,8 @@ namespace winrt::TerminalApp::implementation
// master spawns/reuses the right agent CLI for THIS tab.
appendHelperFlagValue(L"--agent", agentCliPath);
appendHelperFlagValue(L"--agent-id", effectiveAgentId);
appendHelperFlagValue(L"--agent-source", effectiveAgentSource);
appendHelperFlagValue(L"--agent-wsl-distro", effectiveAgentWslDistro);
{
namespace Reg = ::Microsoft::Terminal::Settings::Model::AgentRegistry;
std::wstring allowedIds;
Expand Down Expand Up @@ -2126,6 +2189,10 @@ namespace winrt::TerminalApp::implementation
startingDirectory = winrt::hstring{ homePath };
}
}
if (effectiveAgentSource == L"wsl" && !startingDirectory.empty())
{
appendHelperFlagValue(L"--agent-source-cwd", startingDirectory);
}

NewTerminalArgs args;
args.Commandline(winrt::hstring{ helperCmd });
Expand Down Expand Up @@ -2942,8 +3009,6 @@ namespace winrt::TerminalApp::implementation
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
};

const auto& globals = _settings.GlobalSettings();

// Surface GPO policy / no-wta failures up-front so the user gets a
// teaching tip instead of a silent no-op.
const auto wtaPath = _DetectWtaPath();
Expand All @@ -2959,8 +3024,8 @@ namespace winrt::TerminalApp::implementation
}
return;
}
if (const auto agentCliPath = _ResolveEffectiveAgentCliPath(globals, [this]() { return _DetectAgentCli(); });
agentCliPath.empty() && AgentPolicy::IsAllowedAgentsPolicyConfigured())
if (AgentPolicy::IsAllowedAgentsPolicyConfigured() &&
::Microsoft::Terminal::Settings::Model::AgentRegistry::FilteredAcpAgents().empty())
{
_agentPaneLog("EARLY RETURN: all agents blocked by GPO policy");
if (auto tip{ FindName(L"WindowIdToast").try_as<MUX::Controls::TeachingTip>() })
Expand Down Expand Up @@ -4412,6 +4477,7 @@ namespace winrt::TerminalApp::implementation
const auto version = pickStr("version");
const auto model = pickStr("model");
const auto state = pickStr("state");
const auto backend = pickStr("backend");

_agentPaneLog("OnAgentStatusChanged: payload=" + winrt::to_string(eventJson).substr(0, 600));

Expand Down Expand Up @@ -4515,7 +4581,7 @@ namespace winrt::TerminalApp::implementation
const auto update = [&](const winrt::com_ptr<Tab>& tabImpl) {
if (const auto content = tabImpl->FindAgentPaneContent())
{
content.UpdateAgentStatus(name, version, model, state);
content.UpdateAgentStatus(name, version, model, state, backend);
}
};
if (!tabId.empty())
Expand Down Expand Up @@ -4829,10 +4895,55 @@ namespace winrt::TerminalApp::implementation

const auto tab = _FindTabByStableId(winrt::to_hstring(params["tab_id"].asString()));
const auto agentId = winrt::to_hstring(params["agent_id"].asString());
const auto source = params.isMember("agent_source") && params["agent_source"].isString() ?
winrt::to_hstring(params["agent_source"].asString()) :
winrt::hstring{ L"host" };
const auto wslDistro = params.isMember("wsl_distro") && params["wsl_distro"].isString() ?
winrt::to_hstring(params["wsl_distro"].asString()) :
winrt::hstring{};
if (!tab || agentId.empty())
{
return;
}
if (source != L"host" && source != L"wsl")
{
_agentPaneLog("OnAgentSwitchRequested: unknown agent source");
return;
}
if (source == L"wsl")
{
if (wslDistro.empty())
{
_agentPaneLog("OnAgentSwitchRequested: WSL source missing distro");
return;
}

auto effectivePane = tab->GetActivePane();
if (effectivePane && effectivePane->IsAgentPane())
{
if (const auto rootPane = tab->GetRootPane())
{
rootPane->WalkTree([&](const auto& pane) {
if (pane->IsSourceOfAgentPane())
{
effectivePane = pane;
}
});
}
}
winrt::Microsoft::Terminal::Control::TermControl control{ nullptr };
if (effectivePane)
{
control = effectivePane->GetTerminalControl();
}
std::wstring expectedShell{ L"wsl:" };
expectedShell.append(std::wstring_view{ wslDistro });
if (!control || control.ShellName() != expectedShell)
{
_agentPaneLog("OnAgentSwitchRequested: WSL source does not match the tab working pane");
return;
}
}

namespace Reg = ::Microsoft::Terminal::Settings::Model::AgentRegistry;
bool allowed = false;
Expand All @@ -4853,12 +4964,18 @@ namespace winrt::TerminalApp::implementation
const auto currentId = tab->HasAgentOverride() ?
tab->AgentIdOverride() :
_settings.GlobalSettings().EffectiveAcpAgent();
if (currentId == agentId)
const auto currentSource = tab->HasAgentOverride() ?
tab->AgentSourceOverride() :
winrt::hstring{ L"host" };
const auto currentWslDistro = tab->HasAgentOverride() ?
tab->AgentWslDistroOverride() :
winrt::hstring{};
if (currentId == agentId && currentSource == source && currentWslDistro == wslDistro)
{
return;
}

tab->SetAgentOverride(agentId, winrt::hstring{}, winrt::hstring{});
tab->SetAgentOverride(agentId, winrt::hstring{}, winrt::hstring{}, source, wslDistro);
_RebuildAgentPaneForTab(tab);
}

Expand Down
Loading
Loading