Skip to content

Per-tab AI agents via a multi-agent wta-master (id-based, GPO-aware)#296

Open
frarteaga wants to merge 17 commits into
microsoft:mainfrom
frarteaga:feature/per-tab-agent
Open

Per-tab AI agents via a multi-agent wta-master (id-based, GPO-aware)#296
frarteaga wants to merge 17 commits into
microsoft:mainfrom
frarteaga:feature/per-tab-agent

Conversation

@frarteaga

Copy link
Copy Markdown

Summary of the Pull Request

Adds per-tab AI agent support: each tab can run its own agent simultaneously (e.g. Gemini in one tab, Claude in another) in the same window. Reworks wta-master from a single-agent multiplexer into a lazy multi-agent broker, and adds a runtime per-tab agent override in TerminalApp selectable from the agent-bar chip.

References and Relevant Issues

Closes #295

Implements the "per-pane different agent" direction described in doc/specs/Multi-window-agent-pane.md §9.

Detailed Description of the Pull Request / Additional comments

Rust — wta-master becomes a lazy multi-agent broker (tools/wta/src/master/mod.rs, session_registry.rs, protocol/acp/client.rs, main.rs):

  • Helpers declare their agent (agent_cmd / agent_id) in the ACP initialize handshake via _meta.wta (WtaMeta extended).
  • The master keeps a pool keyed by the agent command line and lazily spawns/reuses one agent CLI per distinct command line (get_or_spawn_agent + per-key OnceCell), each with its own cached initialize response and resolved cli_source.
  • HelperHandler resolves its agent during initialize and forwards all later requests to that CLI. Session routing is unchanged (keyed by SessionId).
  • A dead agent CLI is reaped from the pool without bringing the master down, so sibling tabs on other agents keep working. --agent/--agent-id remain the fallback default for helpers that don't declare one.
  • Side benefit: per-session cli_source is now stamped per agent (the F2 list shows each row's real CLI instead of one process-wide value).

C++ — per-tab override + chip flyout (src/cascadia/TerminalApp/*):

  • Tab gains a runtime-only agent override (id + model + custom command); empty id = follow the global default.
  • Agent-pane spawn resolves the effective agent per tab (_ResolveAgentCliPathForId) and passes it to that tab's helper.
  • Clicking the agent-bar chip opens a flyout of GPO-allowed agents (AgentSwitchRequested); TerminalPage records the override and rebuilds only that tab's pane (_RebuildAgentPaneForTab, no master restart).
  • _RebuildAgentStack (global-agent change) now skips overridden tabs and no longer restarts the shared master; OnAgentStatusChanged no longer persists an overridden tab's agent to the global default.

Runtime-only by design: new tabs start on the global default (no settings.json/schema changes).

Validation Steps Performed

  • cargo build + cargo test for tools/wta559 tests pass, including a new _meta.wta agent-identity round-trip test.
  • Clean full build of OpenConsole.slnx (Debug x64) — 0 errors; new AgentSwitchRequested symbol linked into TerminalApp.dll.
  • Ran the packaged dev build end-to-end:
    • Master log shows the new declared-agent handshake + lazy spawn for claude (agent_cmd=npx -y @zed-industries/claude-code-acp, cli_source=Claude) and gemini (agent_cmd=gemini --experimental-acp, agent_id=gemini).
    • The agent-bar updated live from "Claude Code v0.16.2" to "Gemini CLI v0.46.0" when the tab's agent changed; _RebuildAgentStack ran without crashing.
    • (Two-tab simultaneity via the chip flyout confirmed manually.)

PR Checklist

frarteaga and others added 2 commits June 15, 2026 14:51
wta-master previously spawned exactly one agent CLI at startup and
multiplexed every helper onto it, so all tabs/windows shared a single
agent. Rework it into a pool keyed by the agent command line:

- Helpers declare their agent (agent_cmd/agent_id) in the `initialize`
  handshake via `_meta.wta` (WtaMeta gains agent_cmd/agent_id).
- The master lazily spawns/reuses one agent CLI per distinct command
  line (`get_or_spawn_agent` + per-key `OnceCell`), each with its own
  cached initialize response and resolved `cli_source`.
- HelperHandler resolves its agent during `initialize` and forwards all
  subsequent requests to that CLI; session routing is unchanged
  (keyed by SessionId, unique per agent in practice).
- A dead agent CLI is reaped from the pool without killing the master,
  so sibling tabs on other agents keep working.

`--agent`/`--agent-id` remain as the fallback default for helpers that
don't declare one. Fixes the per-session cli_source stamping (each row
now reflects its real CLI) as a side effect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Let each tab run its own agent (e.g. Gemini in one tab, Claude in
another) on top of the multi-agent master:

- Tab gains a runtime-only agent override (agent id + model + custom
  command); empty id means "follow the global default".
- Agent-pane spawn resolves the effective agent per tab
  (`_ResolveAgentCliPathForId` / override-or-global) and passes the
  tab's agent to its helper, which declares it to the master.
- Clicking the agent-bar chip opens a flyout of GPO-allowed agents
  (`AgentSwitchRequested` event); TerminalPage records the per-tab
  override and rebuilds just that tab's pane (`_RebuildAgentPaneForTab`,
  no master restart).
- `_RebuildAgentStack` (global-agent change) now skips tabs with an
  override and no longer restarts the shared master — fresh helpers
  declare the new agent and the multi-agent master spawns/reuses it.
- `OnAgentStatusChanged` no longer persists a tab's agent back to the
  global default when that tab has an override.

Runtime-only by design: new tabs start on the global default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 15, 2026 19:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds multi-agent support for Windows Terminal Agent (WTA) by letting each tab declare its desired agent during ACP initialize, enabling the master to lazily spawn/reuse one agent CLI per distinct command line and providing a per-tab agent switch UI.

Changes:

  • Extend _meta.wta to carry per-tab agent_cmd and agent_id, and round-trip them through helper/master.
  • Replace single cached agent connection in the master with a per-command agent-CLI pool (lazy spawn + reuse).
  • Add per-tab agent override + UI flyout to switch agents without restarting the shared master.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tools/wta/src/session_registry.rs Adds agent_cmd/agent_id to WTA meta and round-trip test coverage.
tools/wta/src/protocol/acp/client.rs Helper forwards per-tab agent identity in ACP initialize meta.
tools/wta/src/master/mod.rs Master becomes a multi-agent broker with a keyed agent-CLI pool and per-helper binding.
tools/wta/src/main.rs Plumbs helper CLI’s agent identity into the pipe client startup.
src/cascadia/TerminalApp/TerminalPage.h Declares _RebuildAgentPaneForTab for scoped per-tab rebuilds.
src/cascadia/TerminalApp/TerminalPage.cpp Implements per-tab agent selection, rebuild behavior, and avoids persisting overrides globally.
src/cascadia/TerminalApp/Tab.h Adds runtime-only per-tab agent override state and accessors.
src/cascadia/TerminalApp/AgentPaneContent.idl Adds AgentSwitchRequested event to request per-tab agent switching.
src/cascadia/TerminalApp/AgentPaneContent.h Declares event + tap handler for agent picker flyout.
src/cascadia/TerminalApp/AgentPaneContent.cpp Implements agent picker flyout and wires it to the agent-bar chip.

Comment thread tools/wta/src/master/mod.rs Outdated
Comment thread tools/wta/src/master/mod.rs Outdated
Comment thread src/cascadia/TerminalApp/AgentPaneContent.cpp Outdated
Comment thread tools/wta/src/master/mod.rs
Comment thread tools/wta/src/master/mod.rs
@frarteaga

Copy link
Copy Markdown
Author

Contributor License Agreement

@microsoft-github-policy-service agree

frarteaga and others added 2 commits June 15, 2026 19:02
Security finding on PR microsoft#296: the multi-agent master spawned whatever
`agent_cmd` string a helper sent over the named pipe, making it an
arbitrary-process-spawn surface for any same-user process.

- Helpers now declare identity (agent_id + model) in `_meta.wta`, not a
  command line. `WtaMeta` gains `model`; `agent_cmd` stays on the wire
  for diagnostics but is never executed.
- New `resolve_agent_selection` rebuilds the command from the id via
  `build_acp_command`, only for KNOWN ids that pass the host's
  GPO-filtered allowlist (`--allowed-agent-ids`); anything else (unknown,
  custom, blocked, empty) falls back to the trusted `--agent` default.
  The agent-CLI pool is keyed by the reconstructed command, never by
  pipe input.
- Defense in depth: the master pipe is created with a tightened SDDL
  (protected DACL = SYSTEM + current user, medium-IL no-write-up label)
  plus reject_remote_clients, at both create sites; falls back to the
  default ACL if the descriptor can't be built.

Also fixes a reaper leak: the child/IO reapers were spawned before
`initialize`, so an init failure leaked the agent subprocess + I/O task
and stranded the pool slot (repeated respawns). The child is now owned
across init and killed on failure; the I/O reaper (which must drive
init) runs first and reaps the slot. The I/O loop result is logged
(clean vs error) instead of discarded.

Adds unit tests for the selection policy (id reconstruction, GPO
allowlist, fallback, "agent_cmd is never executed", pool-key dedupe) and
the WtaMeta model round-trip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…agent chip

- `_BuildSharedWtaExtraArgs` now emits `--allowed-agent-ids` (the
  GPO-filtered `FilteredAcpAgents()` set) so the multi-agent master can
  validate per-tab agent selections declared over the pipe. Pairs with
  the master-side agent_id-only resolution that no longer trusts a
  command string off the pipe.
- The agent-bar chip is now a focusable Button instead of a tapped
  Border, so the per-tab agent picker opens on keyboard (Enter/Space)
  and pointer activation alike and is reachable via Tab; the flyout
  build moves into `_showAgentPicker()`. Adds a localized automation
  name (AgentPane_SwitchAgentAutomationName) for screen readers.

Addresses Copilot review feedback on PR microsoft#296.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

This comment has been minimized.

The check-spelling bot flagged four tokens introduced by this PR:

- `GPO` — a real recurring acronym (Group Policy Object); added to the
  check-spelling expect list. The per-tab agent feature uses it in a
  test name and many comments, so it can't be reworded away.
- `allowset` → renamed the test helper to `allow_set` (idiomatic
  snake_case), avoiding a one-off junk dictionary entry.
- `chokepoint` → reworded to "choke point".
- `chromeless` → reworded the XAML comment to "has no chrome".

No behavior change; the 41 wta master tests still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 16, 2026 01:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (1)

src/cascadia/TerminalApp/TerminalPage.cpp:1

  • _ResolveAgentCliPathForId is documented to return empty to signal “fall back”, but the caller currently does not implement a fallback when policy is not configured. In that case, a tab override that yields an empty command line (blocked/invalid/no usable command) will proceed with agentCliPath empty, which can propagate an empty --agent value into helper launch. Fix by explicitly falling back to the global/default resolution when tab->HasAgentOverride() but agentCliPath.empty().

Comment thread src/cascadia/TerminalApp/TerminalPage.cpp
Comment thread src/cascadia/TerminalApp/TerminalPage.cpp
Comment thread tools/wta/src/protocol/acp/client.rs
Comment thread tools/wta/src/protocol/acp/client.rs Outdated
Comment thread tools/wta/src/master/mod.rs
@github-actions

This comment has been minimized.

Reconciles ~94 commits of main's WTA evolution (since 2026-05-29) with the
per-tab agent feature. All conflicts were in the 4 wta files; resolved by
keeping the multi-agent broker + security work as the base and folding in
main's platform improvements:

- session_registry.rs / client.rs: WtaMeta carries BOTH per-tab identity
  (agent_id + model) AND main's owner_tab_id (microsoft#266 pane<->session binding);
  helper initialize keeps the _meta.wta inject and adopts main's ACP
  telemetry (microsoft#272).
- master/mod.rs: kept the lazy multi-agent broker (agents pool, agent_id-only
  resolution + GPO allowlist + pipe SDDL) and folded in main's helper_meta
  crash-recovery + restart_agent_pane (microsoft#145/microsoft#266), session/new timeout-to-
  master + telemetry (microsoft#268/microsoft#272), WT connection_state->PaneClosed bridging
  (microsoft#208), and stderr-at-debug. Dropped main's single-agent eager spawn /
  shutdown channel / cached_init_resp (superseded by the per-agent reaper).
- Cargo.toml: union of windows-sys features.
- app.rs: thread agent_cmd/agent_id through main's pipe-mode reconnect caller.

Verified: cargo test 729 pass (incl. the agent_cmd-never-spawned security
test + main's recovery tests); TerminalApp C++ builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

This comment has been minimized.

main advanced 5 more commits while the first merge was being resolved.
Only master/mod.rs conflicted: main's microsoft#288 added a hookless Class-B
session watcher + a Class-B liveness poll in run_master_loop, right where
the multi-agent broker sits — accepted both (independent features) ahead
of the WT-event subscriber.

Also tracked main's microsoft#257/microsoft#280 Claude ACP adapter migration
(@zed-industries/claude-code-acp -> @agentclientprotocol/claude-agent-acp)
in the two tests that hard-code the adapter command.

Verified: cargo test 824 pass; TerminalApp C++ builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 16, 2026 04:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Comment thread tools/wta/src/master/mod.rs Outdated
Comment thread tools/wta/src/master/mod.rs Outdated
Comment thread tools/wta/src/session_registry.rs
Comment thread tools/wta/src/protocol/acp/client.rs
Comment thread tools/wta/src/master/mod.rs Fixed
@github-actions

This comment has been minimized.

frarteaga and others added 2 commits June 16, 2026 08:16
…arison

`resolve_agent_selection` decided whether a helper-requested agent id was
"known" via `lookup_profile_by_id(id).id != DEFAULT_PROFILE.id`. That
conflates the default agent with the unknown-id fallback: the day
`DEFAULT_PROFILE.id` becomes a real, selectable agent id, a helper
requesting that id would be treated as unknown — forced onto the default
command and silently dropping any requested-model folding.

Add `agent_registry::is_known_id`, a membership test against KNOWN_AGENTS
that is decoupled from DEFAULT_PROFILE, and use it in
`resolve_agent_selection`. Apply the same predicate to the preflight
custom/unknown check in main.rs, which had the identical latent coupling
(`lookup_profile_by_id(id).id == "unknown"`).

Tests: unit-test is_known_id membership semantics, plus a master-side
regression test asserting every KNOWN_AGENTS id is honored (rebuilt
command + own id stamp) rather than falling back.

Addresses PR review: microsoft#296 (comment)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… logging, docs

- master: collapse an empty / all-blank --allowed-agent-ids to None (not
  Some(empty_set)). With clap value_delimiter=',', `--allowed-agent-ids ""`
  parses to [""]; treating that as Some({}) made resolve_agent_selection
  refuse every id (a silent block-all). Extracted normalize_allowed_agent_ids
  + regression test.
- master: log agent stderr at debug, not warn. Agent stderr routinely
  carries prompt/file content and adapter chatter; warn was noisy and an
  information-leak in release.
- master: fix HelperHandler.agent doc — the binding is resolved from
  _meta.wta.agent_id (+ model), not the no-longer-trusted agent_cmd.
- session_registry: extract_wta_meta drops empty / whitespace-only string
  fields to None so empty values round-trip as None (WtaMeta::is_empty stays
  correct and no empty _meta.wta is kept alive). Mirrors the helper's
  injection-side filter. + regression test.
- client/main/app: drop the unused agent_cmd param from
  run_acp_client_over_pipe (per-tab identity travels as agent_id over the
  wire); removes the `let _ = &agent_cmd` dead-store suppression.
- TerminalPage: when a per-tab agent override resolves to an empty command
  line (unknown/blocked id or empty custom command), fall back to the
  global/default agent per _ResolveAgentCliPathForId's contract instead of
  launching the helper with no --agent and a stale --agent-id.
- spelling: accept the lowercase "gpo" token (expect.txt).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 16, 2026 15:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Comment thread tools/wta/src/master/mod.rs Outdated
Comment thread src/cascadia/TerminalApp/TerminalPage.cpp
Comment thread tools/wta/src/session_registry.rs Outdated
@frarteaga frarteaga changed the title Per-tab AI agent: multi-agent wta-master + per-tab override Per-tab AI agents via a multi-agent wta-master (id-based, GPO-aware) Jun 16, 2026
@frarteaga frarteaga requested a review from Copilot June 16, 2026 15:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Comment thread tools/wta/src/master/mod.rs Outdated
Comment thread tools/wta/src/protocol/acp/client.rs
Comment thread src/cascadia/TerminalApp/AgentPaneContent.cpp
@frarteaga frarteaga marked this pull request as draft June 16, 2026 20:39
frarteaga and others added 3 commits June 16, 2026 17:20
`buf` is a Vec<u8> (alignment 1) but TOKEN_USER contains a pointer and
needs pointer alignment, so creating a reference into the buffer was UB.
Copy the header out with std::ptr::read_unaligned; the Sid pointer still
points into `buf`, which outlives the conversion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
inject_wta_meta now mirrors extract_wta_meta: whitespace-only fields are
filtered out and the wta namespace is omitted entirely when no field
survives, so we never emit empty-string metadata over the wire. Adds a
regression test covering all-blank and partially-blank inputs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Trim and ASCII-lowercase the agent_id before forwarding, and only pass
through ids that agent_registry::is_known_id accepts, matching how the
master canonicalizes. Unknown or malformed ids are dropped rather than
declared.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
frarteaga and others added 2 commits June 16, 2026 17:20
In _AutoCreateHiddenAgentPaneShared, when the effective agent id is empty
but a CLI path was resolved, fall back to _DetectAgentCli() so the helper
is launched with a concrete --agent-id instead of an empty one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_showAgentPicker now skips FilteredAcpAgents entries whose id is empty so
the menu never renders a dead item that selects nothing, mirroring the
empty-id filtering already done in TerminalPage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@frarteaga frarteaga marked this pull request as ready for review June 16, 2026 23:35
Copilot AI review requested due to automatic review settings June 16, 2026 23:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.

Comment thread src/cascadia/TerminalApp/AgentPaneContent.cpp Outdated
Comment thread src/cascadia/TerminalApp/AgentPaneContent.cpp Outdated
Comment thread src/cascadia/TerminalApp/AgentPaneContent.cpp Outdated
Comment thread tools/wta/src/master/mod.rs
Comment thread tools/wta/src/master/mod.rs
…wlist, docs)

AgentPaneContent: match current agent by id (not display name) so the
checkmark tracks correctly even when two agents share a display name.
Replace MenuFlyoutItem+Accept-icon with RadioMenuFlyoutItem (MUXC) so
screen readers see IsChecked semantics; items are grouped under
GroupName("agents") for mutual-exclusion UIA semantics.

wta/master: normalize_allowed_agent_ids now calls is_known_id to drop
custom / unrecognised ids before building the allowlist — unknown ids
would be silently ignored by the dispatcher anyway, so admitting them
into the set leaks information and wastes the set capacity.
Document the pool eviction policy on the agents field (warm-for-life,
reaped on crash, no idle-timeout).

Build: restore Host.Proxy before Settings Model/Editor, add NuGet
packages.config restore step, set CL_MPCount=1 for RAM-constrained
machines (FRANK11 ~7.4 GB).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread tools/wta/src/master/mod.rs Fixed
Comment thread tools/wta/src/master/mod.rs Fixed
@github-actions

This comment has been minimized.

Copilot AI review requested due to automatic review settings June 17, 2026 14:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.

Comment on lines +1997 to +2009
fn normalize_allowed_agent_ids(raw: &[String]) -> Option<std::collections::HashSet<String>> {
let set: std::collections::HashSet<String> = raw
.iter()
.map(|s| s.trim().to_ascii_lowercase())
.filter(|s| !s.is_empty())
.filter(|s| crate::agent_registry::is_known_id(s))
.collect();
if set.is_empty() {
None
} else {
Some(set)
}
}
Comment on lines +71 to +75
self.pane_session_id.is_none()
&& self.agent_cmd.is_none()
&& self.agent_id.is_none()
&& self.model.is_none()
&& self.owner_tab_id.is_none()
Comment thread _build_msix_x64.cmd Outdated
Comment on lines +3 to +6
set MSBUILD="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"
set SOLUTION_DIR=%CD%\
set COMMON=/p:Platform=x64 /p:Configuration=Release /p:WindowsTerminalBranding=Dev /p:SolutionDir=%SOLUTION_DIR% /m /nologo
set CL_MPCount=1
set COMMON=/p:Platform=x64 /p:Configuration=Release /p:WindowsTerminalBranding=Dev /p:SolutionDir=%SOLUTION_DIR% /m:1 /nologo
Comment on lines +95 to +107
for (const auto& a : Reg::FilteredAcpAgents())
{
const std::wstring_view dn{ a.displayName };
if (dn.size() == _agentName.size() &&
std::equal(dn.begin(), dn.end(), _agentName.begin(),
[](wchar_t x, wchar_t y) {
return std::towlower(x) == std::towlower(y);
}))
{
currentAgentId = winrt::hstring{ a.id };
break;
}
}
Comment on lines +111 to +112
for (const auto& agent : Reg::FilteredAcpAgents())
{
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Per-tab AI agent: run a different agent (e.g. Gemini and Claude) in each tab

3 participants