Skip to content

feat: surface CLI subagent/skill events in chat + /agent command#445

Merged
PureWeen merged 7 commits intomainfrom
feature/cli-agent-visibility
Mar 29, 2026
Merged

feat: surface CLI subagent/skill events in chat + /agent command#445
PureWeen merged 7 commits intomainfrom
feature/cli-agent-visibility

Conversation

@PureWeen
Copy link
Copy Markdown
Owner

Summary

Improves Copilot CLI feature parity by making the CLI's built-in agent system visible to PolyPilot users.

What's new

1. Subagent activity shown in chat

When the Copilot CLI automatically invokes a specialized agent (e.g. code-review, security-review) in response to a prompt, PolyPilot now shows system messages in the chat thread:

  • 🤖 Agent: **Code Review** — agent selected by CLI
  • ▶️ Starting agent: **Code Review** — description — agent invocation began
  • Agent completed: **Code Review** — success
  • Agent failed: **Code Review**: error — failure with error detail

The active agent name/display name is also tracked on AgentSessionInfo and cleared on completion/deselect.

Previously these events fell through to LogUnhandledSessionEvent with no user-visible feedback.

2. SkillInvokedEvent shown in chat

When the CLI invokes a skill plugin, a ⚡ system message is shown: Skill: **skillName (plugin)**.

3. Active agent badge in session info panel

When a subagent is active, a 🤖 AgentName badge appears in the ExpandedSessionView info strip alongside the model selector.

4. /agent slash command

New /agent command with three modes:

  • /agent — lists available agents from both local discovery (.github/agents/, .claude/agents/, .copilot/agents/) and the live SDK AgentApi.ListAsync()
  • /agent <name> — selects an agent via AgentApi.SelectAsync(name)
  • /agent deselect — deselects the current agent via AgentApi.DeselectAsync()

Added to autocomplete in index.html and updated /help output.

SdkEventMatrix updates

  • SubagentSelectedEvent, SubagentDeselectedEvent, SubagentStartedEvent, SubagentCompletedEvent, SubagentFailedEvent: TimelineOnlyChatVisible
  • SkillInvokedEvent: TimelineOnlyChatVisible

Tests

  • SlashCommandAutocompleteTests updated to include /agent in the withArgs list
  • All 2987 tests pass; MAUI Mac Catalyst build succeeds with 0 errors

Copy link
Copy Markdown
Owner Author

@PureWeen PureWeen left a comment

Choose a reason for hiding this comment

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

Review: PR #445 — feat: surface CLI subagent/skill events in chat + /agent command

⚠️ Breaking change: /reflect command removed

The diff removes the /reflect case from the command switch and replaces it with /fleet. The HandleReflectCommand method is deleted from Dashboard.razor. The InsertReflectCommand shortcut button is also removed from ExpandedSessionView.razor.

This is a significant behavioral removal. /reflect was the user-facing entry point for reflection cycles. However:

  • The underlying reflection cycle infrastructure (StopReflectionCycle, ReflectionCycle model, OrchestratorReflect mode) still exists and is referenced in both files
  • The reflection pill UI (showing iteration progress, stop button) still renders in ExpandedSessionView.razor
  • /reflect is removed from autocomplete in index.html and from the /help text

The user can no longer start a reflection cycle via /reflect, but the existing infrastructure for running them (e.g., via multi-agent orchestrator mode) remains. If this removal is intentional, the leftover ReflectionCycle UI and references should be cleaned up. If it was accidental, /reflect needs to be re-added.


✅ Event handling — correct

The new subagent/skill event cases are well-structured:

  • SubagentSelectedEvent: Sets ActiveAgentName/ActiveAgentDisplayName on AgentSessionInfo and adds a system message. Correct.
  • SubagentDeselectedEvent: Clears both fields. Correct.
  • SubagentStartedEvent: Adds a ▶️ message. The description is appended only when non-empty. Correct.
  • SubagentCompletedEvent / SubagentFailedEvent: Clears ActiveAgentName only when the name matches (OrdinalIgnoreCase). This is a nice correctness guard against stale state from rapid sequential agent invocations.
  • SkillInvokedEvent: Adds ⚡ message with plugin name. Correct.
  • CommandsChangedEvent: Added as TimelineOnly — a reasonable default for an event that has no user-visible meaning yet.

All cases use Invoke(() => ...) for correct UI-thread marshaling. All use NotifyStateChangedCoalesced() consistently with surrounding code.


✅ /agent command — correct

HandleAgentCommand in Dashboard.razor correctly handles three cases:

  • Empty arg → list agents (SDK API + local discovery)
  • deselect → calls DeselectAgentAsync
  • Any other arg → calls SelectAgentAsync

The service methods (ListAgentsFromApiAsync, SelectAgentAsync, DeselectAgentAsync) all have proper null guards and exception handling (catch-and-log). ✅


✅ /fleet command — correct but minimal

HandleFleetCommand calls StartFleetAsync which calls state.Session.Rpc.Fleet.StartAsync. The "fleet started" message fires before the API result is confirmed:

session.History.Add(ChatMessage.SystemMessage($"🚀 Starting fleet for: *{arg}*"));
var started = await CopilotService.StartFleetAsync(sessionName, arg);
    session.History.Add(ChatMessage.ErrorMessage("Failed to start fleet mode."));

This is a minor UX issue — "Starting fleet..." appears briefly even on failure. Low severity, but worth noting.


✅ Active agent badge — correct

The badge in ExpandedSessionView.razor uses Session.ActiveAgentDisplayName ?? Session.ActiveAgentName with a null guard. CSS uses var(--type-footnote) — consistent with font-sizing conventions. ✅


Minor: AgentSessionInfo.ActiveAgentName not persisted

ActiveAgentName/ActiveAgentDisplayName are not included in session persistence/bridge sync. After an app restart or remote-mode reconnect, the badge won't show. This is acceptable (the agent state is runtime-only and the SDK will re-emit events on reconnect), but worth documenting.


Summary

Issue Severity
/reflect removed — breaking change, or leftover UI needs cleanup High — needs explicit decision
"Starting fleet" message before API confirms Low
ActiveAgentName not persisted Low/Info

All new event handling, API methods, and UI additions are correct. The /reflect removal is the only issue requiring a decision before merging.

@PureWeen
Copy link
Copy Markdown
Owner Author

Thanks for the thorough review @PureWeen!

On /reflect removal: This was intentional per the user's explicit request. The reflection pill UI (iteration counter, stop button) and all underlying infrastructure (ReflectionCycle, OrchestratorReflect, StopReflectionCycle) are intentionally retained — they still work when reflection is triggered via multi-agent orchestration. The removal only affects the user-facing /reflect slash command entry point. The leftover UI is not dead code — it activates when multi-agent orchestration starts a reflection cycle.

On fleet message before API confirms: Fixed in 00bff88 — the 🚀 message now only appears after StartFleetAsync returns true, error message on false.

On ActiveAgentName not persisted: Agreed this is acceptable — the SDK will re-emit subagent events on reconnect/resume. Not persisting is the right call to avoid stale badge state.

PureWeen and others added 7 commits March 28, 2026 15:34
- Show 🤖/▶️/✅/❌ system messages when CLI subagents activate
  (SubagentSelectedEvent, SubagentStartedEvent, SubagentCompletedEvent,
  SubagentFailedEvent, SubagentDeselectedEvent)
- Show ⚡ system message when SkillInvokedEvent fires
- Update SdkEventMatrix: subagent + skill events → ChatVisible
- Handle CommandsChangedEvent: store CLI commands in AgentSessionInfo.CliSlashCommands
- Add ActiveAgentName/ActiveAgentDisplayName to AgentSessionInfo
- Show active CLI agent badge in ExpandedSessionView info panel
- Add /agent slash command: list agents (local + SDK API) or select/deselect
- Wire AgentApi: ListAgentsFromApiAsync, SelectAgentAsync, DeselectAgentAsync
- Add /agent to index.html autocomplete + SlashCommandAutocompleteTests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove CommandsChangedEvent case handler (type not exported from SDK 0.2.0)
  SdkEventMatrix string entry remains for future use when SDK exposes the type
- Replace session.AvailableAgents (nonexistent property) with
  CopilotService.DiscoverAvailableAgents(session.WorkingDirectory) in /agent handler

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CommandsChangedEvent handler was removed (SDK 0.2.0 doesn't export the type),
so this field was never set. Remove to avoid dead code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add /fleet <prompt> slash command that calls FleetApi.StartAsync() via
  the SDK, enabling parallel subagent execution. Shows a system message on
  success/failure. Added to index.html autocomplete and /help output.
  Added CopilotService.StartFleetAsync() wrapping state.Session.Rpc.Fleet.StartAsync().

- Remove /reflect slash command: remove case handler, delete
  HandleReflectCommand() method (~130 lines), remove from COMMANDS array and /help.
  The underlying reflection machinery (ReflectionCycle model,
  StartReflectionCycleAsync, OrchestratorReflect multi-agent mode,
  all reflection UI in ChatMessageItem/SessionCard) is UNTOUCHED --
  reflection still works via multi-agent orchestration.

- Update SlashCommandAutocompleteTests: replace /reflect with /fleet in
  both the minimum-commands and the hasArgs assertions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove 'Reflect' button from ExpandedSessionView toolbar
- Remove dead InsertReflectCommand() method
- Remove '/reflect stop' suggestion from context-full warning message

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Only show 🚀 success message after StartFleetAsync returns true,
show error on false. Avoids premature success indicator on failure.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…leet guard

- SubagentCompleted/Failed: always clear ActiveAgent state even when
  displayName is empty (prevents permanently stuck badge)
- DeselectAgentAsync: return bool, surface failures in /agent deselect
- StartFleetAsync: reject when IsProcessing to prevent concurrent turns

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen PureWeen force-pushed the feature/cli-agent-visibility branch from 00bff88 to 09bc16e Compare March 28, 2026 20:50
@PureWeen
Copy link
Copy Markdown
Owner Author

🔍 Squad Re-Review — PR #445 Round 2

Commits since R1: 3 new commits since the original review:

  • 1e80fb2e — "fix: don't show fleet 'started' message before API confirms"
  • 09bc16e3 — "fix: address review findings — stuck badge, deselect error surface, f…"

Tests: ✅ 2929/2929 pass (confirmed locally)


Feature Summary

  • Subagent/skill events promoted from TimelineOnlyChatVisible: SubagentSelectedEvent, SubagentDeselectedEvent, SubagentStartedEvent, SubagentCompletedEvent, SubagentFailedEvent, SkillInvokedEvent
  • Active agent badge in session toolbar (only shown when ActiveAgentName / ActiveAgentDisplayName is set)
  • /agent [name|deselect] — list/select/deselect CLI agents via SDK Agent.ListAsync / SelectAsync / DeselectAsync
  • /fleet <prompt> — starts parallel subagent execution via Fleet.StartAsync

Analysis

Thread Safety ✅

ActiveAgentName / ActiveAgentDisplayName mutations all happen inside Invoke(() => {...}), dispatched to the UI thread. The badge is read only in Blazor render (UI thread). No background thread races.

History.Add inside Invoke is consistent with the existing pattern for FlushCurrentResponse and HandleContent — all on UI thread, no HistoryLock needed for UI-thread writes.

Stuck Badge Fix (09bc16e3) ✅

SubagentCompletedEvent and SubagentFailedEvent now clear ActiveAgentName / ActiveAgentDisplayName even when displayName is empty:

// Always clear active agent state — even if displayName is empty
if (d?.AgentName == null || string.Equals(state.Info.ActiveAgentName, d.AgentName, ...))
{
    state.Info.ActiveAgentName = null;
    state.Info.ActiveAgentDisplayName = null;
}

The guard correctly prevents a completion event for agent A from clearing the badge when agent B is already active. ✅

Remote Mode / Null Safety ✅

All new SDK-calling methods (ListAgentsFromApiAsync, SelectAgentAsync, DeselectAgentAsync, StartFleetAsync) guard with:

if (!_sessions.TryGetValue(sessionName, out var state) || state.Session == null)
    return false / [];

Remote mode (state.Session == null) returns gracefully. ✅

ListAgentsFromApiAsync null-chains safely: result?.Agents?.Where(...), and a!.Name! is safe because the Where filters out null/empty names first. ✅

Fleet Command — No Race ✅

StartFleetAsync checks state.Info.IsProcessing before calling the API. The UI gets a clear error message if called while busy. ✅


Observations (Non-Blocking)

🟡 No unit tests for new agent API methods

ListAgentsFromApiAsync, SelectAgentAsync, DeselectAgentAsync, StartFleetAsync all call into state.Session.Rpc.* which requires SDK support not currently available in StubSession. These are integration-tested implicitly, but if the SDK changes its Agent/Fleet API surface, there's no test to catch regressions. Worth tracking as a future test gap.

🟢 /reflect removal is intentional (confirmed by author reply)

The /reflect slash command, Reflect button in the toolbar, and HandleReflectCommand method are removed. The underlying ReflectionCycle infrastructure and StopReflectionCycle remain (used by multi-agent orchestration). The context window warning message correctly drops the outdated /reflect stop suggestion. No dangling references remain.


✅ Verdict: Approve

Clean feature addition. Thread safety correct, remote mode guards in place, stuck-badge fix verified, 2929/2929 tests green. Good to merge.

@PureWeen PureWeen merged commit cfc8161 into main Mar 29, 2026
@PureWeen PureWeen deleted the feature/cli-agent-visibility branch March 29, 2026 14:18
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.

1 participant