Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
42fee12
Extract cmdline builder with proper quoting and add unit tests
yeelam-gordon May 21, 2026
a5be1e8
feat: pass agent config as JSON-encoded --agent-config argument
yeelam-gordon May 21, 2026
221c122
fix: correct misleading comments about JsonCpp usage
yeelam-gordon May 21, 2026
ff5e4e2
chore: trigger re-review
yeelam-gordon May 21, 2026
ef17c67
fix: address Copilot review round 2
yeelam-gordon May 21, 2026
ad520e5
chore: request re-review
yeelam-gordon May 21, 2026
92e663d
fix: make QuoteArgForCommandLine non-throwing (address round 3)
yeelam-gordon May 21, 2026
e519a19
chore: trigger review
yeelam-gordon May 21, 2026
9213afe
fix: address round 4 Copilot review comments
yeelam-gordon May 21, 2026
dfdae64
chore: trigger review
yeelam-gordon May 21, 2026
b44b2e3
fix: quote wta path with QuoteProgramPath in all launch paths
yeelam-gordon May 21, 2026
7b7bcc7
chore: trigger review
yeelam-gordon May 21, 2026
183e02d
fix: make BuildAgentConfigArg return std::optional
yeelam-gordon May 21, 2026
59b62da
chore: trigger review
yeelam-gordon May 21, 2026
9cac907
fix: address round 7 - logging and cross-platform guard
yeelam-gordon May 21, 2026
aefece2
chore: trigger review
yeelam-gordon May 21, 2026
a7460ee
chore: trigger copilot review
yeelam-gordon May 21, 2026
e204334
chore: trigger review after resolving threads
yeelam-gordon May 21, 2026
0f8228d
chore: trigger copilot review
yeelam-gordon May 21, 2026
6d3eaf9
fix: use ../inc/ prefix for QuoteArgForCommandLine.h include
yeelam-gordon May 21, 2026
3b5d2dd
fix: address round 8 - noexcept safety and overlay dedup
yeelam-gordon May 21, 2026
b1dd16c
fix: resolve check-spelling alerts in cmdline tests
yeelam-gordon May 21, 2026
6cf87a5
fix: destructure AgentConfig in overlay blocks
yeelam-gordon May 21, 2026
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
3 changes: 3 additions & 0 deletions .github/actions/spelling/expect/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@ FACESIZE
FAILIFTHERE
fastlink
fcharset
ffi
fgbg
FGCOLOR
FGHIJ
Expand Down Expand Up @@ -670,6 +671,7 @@ GMEM
Goldmine
gonce
goutput
gpt
GPUs
GREENSCROLL
Grehan
Expand Down Expand Up @@ -1340,6 +1342,7 @@ PUCHAR
pvar
pwch
PWDDMCONSOLECONTEXT
pwned
pws
pwstr
pwsz
Expand Down
197 changes: 89 additions & 108 deletions src/cascadia/TerminalApp/TerminalPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include "TerminalSettingsCache.h"
#include "TerminalProtocolPipeServer.h"
#include "WtaProcessLauncher.h"
#include "../inc/QuoteArgForCommandLine.h"

#include "LaunchPositionRequest.g.cpp"
#include "RenameWindowRequestedArgs.g.cpp"
Expand Down Expand Up @@ -1163,37 +1164,37 @@ namespace winrt::TerminalApp::implementation
const auto& globals = _settings.GlobalSettings();
const auto agentCliPath = _ResolveEffectiveAgentCliPath(globals, [this]() { return _DetectAgentCli(); });

// Helper: escape and quote an argument for the command line.
auto quoteArg = [](std::wstring_view arg) -> std::wstring {
std::wstring escaped{ arg };
for (size_t pos = 0; (pos = escaped.find(L'"', pos)) != std::wstring::npos; pos += 2)
{
escaped.replace(pos, 1, L"\\\"");
}
return L"\"" + escaped + L"\"";
};

// Build: wta delegate --agent <agent> --delegate-agent <delegate> "<prompt>"
std::wstring cmdline = quoteArg(wtaPath) + L" delegate";
using Microsoft::Terminal::CommandLine::QuoteArgForCommandLine;
using Microsoft::Terminal::CommandLine::QuoteProgramPath;

if (!agentCliPath.empty())
// Build: wta delegate --agent-config <json> --cwd <cwd> "<prompt>"
auto quotedPath = QuoteProgramPath(wtaPath);
if (!quotedPath)
{
Comment thread
yeelam-gordon marked this conversation as resolved.
cmdline += L" --agent " + quoteArg(std::wstring_view{ agentCliPath });
_agentPaneLog("ABORT: wta path contains invalid characters, cannot delegate");
return; // Invalid WTA path (contains NUL or quote) — cannot launch.
}
std::wstring cmdline = *quotedPath + L" delegate";

Comment thread
yeelam-gordon marked this conversation as resolved.
const auto delegateAgent = _ResolveEffectiveDelegateAgent(globals);
if (!delegateAgent.empty())
const auto delegateModel = globals.DelegateModel();

// Pass agent config fields as JSON (same mechanism as the agent pane).
if (auto agentArg = Microsoft::Terminal::CommandLine::BuildAgentConfigArg(
std::wstring_view{ agentCliPath },
std::wstring_view{} /* agentId — not needed for delegate */,
std::wstring_view{ delegateAgent },
std::wstring_view{ delegateModel },
std::wstring_view{} /* acpModel */))
{
cmdline += L" --delegate-agent " + quoteArg(std::wstring_view{ delegateAgent });
cmdline += *agentArg;
}
const auto delegateModel = globals.DelegateModel();
if (!delegateModel.empty())
else
{
cmdline += L" --delegate-model " + quoteArg(std::wstring_view{ delegateModel });
_agentPaneLog("ABORT: failed to build agent config for delegate");
return;
}



// Pass CWD from the active pane.
winrt::hstring activeCwd;
if (const auto& activeControl = _GetActiveControl())
Expand All @@ -1210,16 +1211,24 @@ namespace winrt::TerminalApp::implementation
}
if (!activeCwd.empty())
{
cmdline += L" --cwd " + quoteArg(std::wstring_view{ activeCwd });
if (auto q = QuoteArgForCommandLine(std::wstring_view{ activeCwd }))
{
cmdline += L" --cwd " + *q;
}
Comment thread
yeelam-gordon marked this conversation as resolved.
else
{
_agentPaneLog("WARNING: delegate CWD contains invalid characters, omitting --cwd");
}
}

// Append the prompt as a positional argument.
std::wstring escapedPrompt{ prompt };
for (size_t pos = 0; (pos = escapedPrompt.find(L'"', pos)) != std::wstring::npos; pos += 2)
// Append the prompt as a positional argument (required by wta delegate).
auto quotedPrompt = QuoteArgForCommandLine(std::wstring_view{ prompt });
if (!quotedPrompt)
{
Comment thread
yeelam-gordon marked this conversation as resolved.
escapedPrompt.replace(pos, 1, L"\"\"");
_agentPaneLog("ABORT: delegate prompt contains invalid characters");
return; // Prompt contains embedded NUL — cannot safely launch.
}
Comment thread
yeelam-gordon marked this conversation as resolved.
cmdline += fmt::format(FMT_COMPILE(L" \"{}\""), escapedPrompt);
cmdline += L" " + *quotedPrompt;

_agentPaneLog("launching: " + winrt::to_string(winrt::hstring{ cmdline }));

Expand Down Expand Up @@ -1720,7 +1729,13 @@ namespace winrt::TerminalApp::implementation
}

const auto& globals = _settings.GlobalSettings();
std::wstring cmdline = std::wstring{ wtaPath };
auto quotedWtaPath = Microsoft::Terminal::CommandLine::QuoteProgramPath(wtaPath);
if (!quotedWtaPath)
{
_agentPaneLog("ABORT: wta path contains invalid characters");
return;
}
std::wstring cmdline = *quotedWtaPath;

// Tell wta which tab owns its pane up-front (passed as a hidden CLI
// arg). wta seeds app_state.tab_id from this before any ACP work
Expand All @@ -1730,59 +1745,40 @@ namespace winrt::TerminalApp::implementation
// _NotifyAgentTabChanged → tab_changed events.
if (const auto stableId = tab->StableId(); !stableId.empty())
{
cmdline += fmt::format(FMT_COMPILE(L" --owner-tab-id \"{}\""), std::wstring_view{ stableId });
if (auto q = Microsoft::Terminal::CommandLine::QuoteArgForCommandLine(std::wstring_view{ stableId }))
{
cmdline += L" --owner-tab-id " + *q;
}
Comment thread
yeelam-gordon marked this conversation as resolved.
else
{
_agentPaneLog("ABORT: owner-tab-id contains invalid characters");
return;
}
}

const auto agentCliPath = _ResolveEffectiveAgentCliPath(globals, [this]() { return _DetectAgentCli(); });
if (!agentCliPath.empty())
{
std::wstring s{ agentCliPath };
for (size_t pos = 0; (pos = s.find(L'"', pos)) != std::wstring::npos; pos += 2)
s.replace(pos, 1, L"\"\"");
cmdline += fmt::format(FMT_COMPILE(L" --agent \"{}\""), s);
}

// See `_OpenOrReuseAgentPane` for the rationale: wta needs the
// canonical `acpAgent` setting value (not the expanded command
// line) to drive the session-management view's CLI filter.
if (const auto acpAgent = globals.AcpAgent(); !acpAgent.empty())
{
std::wstring s{ acpAgent };
for (size_t pos = 0; (pos = s.find(L'"', pos)) != std::wstring::npos; pos += 2)
s.replace(pos, 1, L"\"\"");
cmdline += fmt::format(FMT_COMPILE(L" --agent-id \"{}\""), s);
}

const auto acpAgent = globals.AcpAgent();
const auto delegateAgent = _ResolveEffectiveDelegateAgent(globals);
if (!delegateAgent.empty())
{
std::wstring s{ delegateAgent };
for (size_t pos = 0; (pos = s.find(L'"', pos)) != std::wstring::npos; pos += 2)
s.replace(pos, 1, L"\"\"");
cmdline += fmt::format(FMT_COMPILE(L" --delegate-agent \"{}\""), s);
}

const auto delegateModel = globals.DelegateModel();
if (!delegateModel.empty())
const auto acpModel = globals.AcpModel();

// Pass all agent-related config as a single JSON-encoded argument.
// This eliminates per-field hand-rolled escaping — BuildAgentConfigArg
// performs RFC 8259 JSON encoding internally, and only one correctly-
// quoted argument boundary is needed (via QuoteArgForCommandLine).
if (auto agentArg = Microsoft::Terminal::CommandLine::BuildAgentConfigArg(
std::wstring_view{ agentCliPath },
std::wstring_view{ acpAgent },
std::wstring_view{ delegateAgent },
std::wstring_view{ delegateModel },
std::wstring_view{ acpModel }))
{
std::wstring s{ delegateModel };
for (size_t pos = 0; (pos = s.find(L'"', pos)) != std::wstring::npos; pos += 2)
s.replace(pos, 1, L"\"\"");
cmdline += fmt::format(FMT_COMPILE(L" --delegate-model \"{}\""), s);
cmdline += *agentArg;
}

// Pass the ACP model selection out-of-band so it works for adapter
// launches (claude/codex via npx) where --model can't be put on the
// adapter cmdline. wta sends this via ACP setSessionModel after
// handshake; for copilot/gemini it's redundant (their --model is
// already on the agent cmdline) but harmless.
const auto acpModel = globals.AcpModel();
if (!acpModel.empty())
else
{
std::wstring s{ acpModel };
for (size_t pos = 0; (pos = s.find(L'"', pos)) != std::wstring::npos; pos += 2)
s.replace(pos, 1, L"\"\"");
cmdline += fmt::format(FMT_COMPILE(L" --acp-model \"{}\""), s);
_agentPaneLog("ABORT: failed to build agent config");
return;
}

if (!globals.AutoFixEnabled())
Expand Down Expand Up @@ -2118,48 +2114,33 @@ namespace winrt::TerminalApp::implementation
// Build the wta command line (single-process TUI mode, no subcommand).
if (const auto wtaPath = _DetectWtaPath(); !wtaPath.empty())
{
cmdline = std::wstring{ wtaPath };

const auto agentCliPath = _ResolveEffectiveAgentCliPath(globals, [this]() { return _DetectAgentCli(); });
if (!agentCliPath.empty())
auto quotedWtaPath = Microsoft::Terminal::CommandLine::QuoteProgramPath(wtaPath);
if (!quotedWtaPath)
{
std::wstring agentStr{ agentCliPath };
for (size_t pos = 0; (pos = agentStr.find(L'"', pos)) != std::wstring::npos; pos += 2)
agentStr.replace(pos, 1, L"\"\"");
cmdline += fmt::format(FMT_COMPILE(L" --agent \"{}\""), agentStr);
}

// Tell wta which `acpAgent` setting value produced this launch
// — wta needs the canonical id ("copilot" / "claude" / "codex"
// / "gemini" / "custom:…") to drive the session-management
// view's CLI filter, and parsing it back out of the expanded
// `--agent` command line is fragile (adapter launches expand
// to "npx -y …" and lose the agent's name). Passing it through
// here keeps a single source of truth.
if (const auto acpAgent = globals.AcpAgent(); !acpAgent.empty())
{
std::wstring idStr{ acpAgent };
for (size_t pos = 0; (pos = idStr.find(L'"', pos)) != std::wstring::npos; pos += 2)
idStr.replace(pos, 1, L"\"\"");
cmdline += fmt::format(FMT_COMPILE(L" --agent-id \"{}\""), idStr);
_agentPaneLog("ABORT: wta path contains invalid characters");
return;
}
cmdline = *quotedWtaPath;

const auto agentCliPath = _ResolveEffectiveAgentCliPath(globals, [this]() { return _DetectAgentCli(); });
const auto acpAgent = globals.AcpAgent();
const auto delegateAgent = _ResolveEffectiveDelegateAgent(globals);
if (!delegateAgent.empty())
const auto delegateModel = globals.DelegateModel();

// Pass all agent-related config as a single JSON-encoded argument.
if (auto agentArg = Microsoft::Terminal::CommandLine::BuildAgentConfigArg(
std::wstring_view{ agentCliPath },
std::wstring_view{ acpAgent },
std::wstring_view{ delegateAgent },
std::wstring_view{ delegateModel },
std::wstring_view{} /* acpModel — not used in this path */))
{
std::wstring delegateStr{ delegateAgent };
for (size_t pos = 0; (pos = delegateStr.find(L'"', pos)) != std::wstring::npos; pos += 2)
delegateStr.replace(pos, 1, L"\"\"");
cmdline += fmt::format(FMT_COMPILE(L" --delegate-agent \"{}\""), delegateStr);
cmdline += *agentArg;
}

const auto delegateModel = globals.DelegateModel();
if (!delegateModel.empty())
else
{
std::wstring modelStr{ delegateModel };
for (size_t pos = 0; (pos = modelStr.find(L'"', pos)) != std::wstring::npos; pos += 2)
modelStr.replace(pos, 1, L"\"\"");
cmdline += fmt::format(FMT_COMPILE(L" --delegate-model \"{}\""), modelStr);
_agentPaneLog("ABORT: failed to build agent config");
return;
}

if (!globals.AutoFixEnabled())
Expand Down
18 changes: 9 additions & 9 deletions src/cascadia/TerminalSettingsEditor/AIAgentsViewModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "../inc/AgentRegistry.h"
#include "../inc/AgentHooksStatus.h"
#include "../inc/WtaProcess.h"
#include "../inc/QuoteArgForCommandLine.h"

#include <json/json.h>

Expand Down Expand Up @@ -966,17 +967,16 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
std::string stdoutText;
if (!wtaPath.empty())
{
// Quote-escape internal `"` per Windows CRT rules.
std::wstring escaped = agentCmdline;
for (size_t pos = 0; (pos = escaped.find(L'"', pos)) != std::wstring::npos; pos += 2)
// Use correct CommandLineToArgvW quoting for the agent argument.
auto quoted = ::Microsoft::Terminal::CommandLine::QuoteArgForCommandLine(std::wstring_view{ agentCmdline });
if (quoted)
{
escaped.replace(pos, 1, L"\"\"");
const std::wstring args = L"probe-models --agent " + *quoted;
// 40s ceiling matches probe.rs's internal limits (npx
// initialize 25s + new_session 10s + slack). Cached
// adapters return in <2s.
stdoutText = ::Microsoft::Terminal::WtaProcess::RunWtaCaptureStdout(wtaPath, args, 40'000);
}
const std::wstring args = L"probe-models --agent \"" + escaped + L"\"";
// 40s ceiling matches probe.rs's internal limits (npx
// initialize 25s + new_session 10s + slack). Cached
// adapters return in <2s.
stdoutText = ::Microsoft::Terminal::WtaProcess::RunWtaCaptureStdout(wtaPath, args, 40'000);
}

std::vector<Model::AcpModelInfo> parsed;
Expand Down
Loading
Loading