Skip to content

Migrate to Copilot SDK for azd agent implementation#6883

Merged
wbreza merged 85 commits intomainfrom
copilot-sdk-phase1
Mar 16, 2026
Merged

Migrate to Copilot SDK for azd agent implementation#6883
wbreza merged 85 commits intomainfrom
copilot-sdk-phase1

Conversation

@wbreza
Copy link
Copy Markdown
Contributor

@wbreza wbreza commented Feb 25, 2026

Replace langchaingo with GitHub Copilot SDK for agent mode

Resolves #6871 #6872 #6873 #6874 #6875 | Epic #6870

Replaces langchaingo agent orchestration with GitHub Copilot SDK (copilot-sdk/go v0.1.32).

Agent API

agent, _ := factory.Create(ctx, agent.WithMode(agent.AgentModePlan))
initResult, _ := agent.Initialize(ctx)
result, _ := agent.SendMessage(ctx, prompt)
agent.Stop()

Screenshots

Startup
image

Mid Flow
image

Completed
image

Features

  • Copilot SDK client lifecycle, session create/resume, permission hooks
  • Initialize() with model/reasoning prompts, dynamic model list
  • SelectSession() UX picker, resume via WithSessionID()
  • AgentDisplay: 15+ event types, reasoning window, intent spinner, nested subagents
  • Azure plugin auto-install, skills + MCP servers from plugin
  • Usage metrics: tokens, billing rate, premium requests, duration
  • Init flow: single prompt with azure-prepare/azure-validate skills

Config Namespace Migration

  • All config keys moved from ai.agent.* to copilot.* namespace
  • Composable constants in internal/agent/copilot/config_keys.go with ConfigRoot prefix
  • mcp.consent to copilot.consent, mcp.errorHandling.* to copilot.errorHandling.*
  • Clean break - no backward compatibility for old keys

Package Restructure

  • Deleted entire pkg/llm/ package (azure_openai, ollama, github_copilot, model_factory, manager, langchaingo deps)
  • Created internal/agent/copilot/ subpackage (copilot_client, session_config, feature flag, config keys)
  • FeatureLlm to FeatureCopilot (alpha key string stays "llm" for existing user config compat)
  • Consent commands moved: azd mcp consent to azd copilot consent
  • WithSystemMessage AgentOption for system prompt overrides

Error Middleware Streamlining

  • Replaced 4-prompt, 5-agent-call orchestration with single consent prompt + single agent interaction in plan mode
  • Troubleshooting workflow in embedded Go text templates
  • Agent-driven flow: diagnose, explain, ask user via ask_user tool, fix or show steps
  • "Always allow" and "always skip" preference persistence via copilot.errorHandling.* config keys
  • Retry-after-fix prompt before re-running failed command

UX Rendering Fixes

  • Fix WaitForIdle hang: SessionIdle events arriving before AssistantMessage are now deferred via pendingIdle flag and flushed when the message arrives, preventing indefinite hangs
  • Fix ticker vs Pause() race: Added renderGuard sync.RWMutex so Pause() write-locks to block ticker renders and wait for any in-flight render to complete, closing the TOCTOU gap that caused spinner to render over consent prompts
  • Fix consent grant persistence: PromptAndGrantConsent no longer returns config save errors as tool denials - user approval is honored immediately, persistence errors are logged but don't block execution
  • Add consent error logging: Silent error swallowing in checkUnifiedRules and permission handler now logs failures for [consent] diagnostics

Agent Display Improvements

  • Contextual verbs: Read, Edit, Create, Search, Find, Ran, Fetched, Queried instead of generic "Ran toolname with"
  • Colored diff stats: Edit shows (+3 -1) with green/red ANSI, Create shows (+25) in green
  • Tree-style sub-detail: Shell commands show description on main line, actual command on indented sub-line; MCP tools show args as tree
  • Error display: Failed tool calls show error message on red sub-line
  • Skill/subagent display: Skills show plugin name and version, subagents show description tree and completion summary
  • Consistent whitespace: All section transitions use printSeparated for uniform blank-line spacing
  • Spinner matches completion: In-progress spinner uses same contextual verb format; description/intent preferred over raw command
  • AssistantMessage rendered in real-time by AgentDisplay (was silently cached, never printed)
  • Guard against empty/whitespace-only assistant messages and reasoning causing extra blank lines

Copilot CLI Distribution

  • On-demand download following the Bicep tool pattern (internal/agent/copilot/cli.go)
  • Downloads version-pinned CLI binary from npm registry on first agent use (CLI 1.0.2 / SDK v0.1.32)
  • Caches at ~/.azd/bin/copilot-cli-{version}[.exe], override via AZD_COPILOT_CLI_PATH
  • Implements tools.ExternalTool interface (Name, InstallUrl, CheckInstalled)
  • Thread-safe via sync.Once, progress spinner during download, 200MB decompression limit
  • Deleted fragile npm node_modules path scanning and SDK bundler (~106MB binary size reduction)

CI Pipeline Cleanup

  • Removed ghCopilot build tag, GhCopilotClientId/GhCopilotIntegrationId from ci-build.ps1 and 5 pipeline YAMLs
  • Removed langchaingo from go.mod

Testing

  • 80+ unit tests covering display helpers, consent persistence, usage metrics
  • Consent persistence tests verify approve-once to approve-always to auto-approve flow
  • Consent reload test verifies rules survive config file round-trip
  • E2E test, golangci-lint 0 issues

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds the GitHub Copilot SDK (v0.1.25) as a foundational dependency and creates new agent infrastructure that will eventually replace the existing langchaingo-based implementation. This is Phase 1 of a 3-phase migration, where all new code coexists alongside existing code without any deletions or modifications to current functionality.

Changes:

  • Adds GitHub Copilot SDK dependency and creates wrapper types (CopilotClientManager, SessionConfigBuilder) that bridge azd config to SDK types
  • Implements CopilotAgent and CopilotAgentFactory that satisfy the existing Agent interface for seamless future switchover
  • Creates event handling infrastructure (SessionEventLogger, SessionFileLogger) that maps SDK events to azd's existing UX patterns
  • Adds 8 new config options (ai.agent.*) for customizing agent behavior (model, tools, MCP servers, system message)

Reviewed changes

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

Show a summary per file
File Description
go.mod / go.sum Adds copilot-sdk/go v0.1.25 and transitive dependency jsonschema-go v0.4.2 (both marked indirect)
resources/config_options.yaml Adds 8 new config keys for Copilot SDK agent customization
pkg/llm/copilot_client.go Manager wrapping SDK client lifecycle with azd-specific error messages
pkg/llm/copilot_client_test.go Tests for client manager instantiation (2 cases)
pkg/llm/session_config.go Bridge converting azd config → SDK SessionConfig with MCP server merging
pkg/llm/session_config_test.go Tests for config reading, tool control, MCP merging (8 cases)
internal/agent/copilot_agent.go Agent implementation using SDK Session.SendAndWait, reuses existing UX patterns
internal/agent/copilot_agent_factory.go Factory creating agents with SDK client, session, hooks, event handlers
internal/agent/logging/session_event_handler.go Event handlers mapping SDK events to thought channel + file logging
internal/agent/logging/session_event_handler_test.go Tests for event handling, tool input extraction, composite handler (10 cases)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@microsoft-github-policy-service microsoft-github-policy-service bot added the no-recent-activity identity issues with no activity label Mar 6, 2026
@wbreza wbreza force-pushed the copilot-sdk-phase1 branch from d606a7b to b3ff2be Compare March 9, 2026 18:15
@microsoft-github-policy-service microsoft-github-policy-service bot removed the no-recent-activity identity issues with no activity label Mar 9, 2026
@wbreza wbreza force-pushed the copilot-sdk-phase1 branch from 0c5b9fd to f4f6bd0 Compare March 11, 2026 00:26
@wbreza wbreza marked this pull request as ready for review March 11, 2026 00:37
Copy link
Copy Markdown
Member

@spboyer spboyer left a comment

Choose a reason for hiding this comment

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

Combined Code Review — GPT-5.4 + Claude Opus 4.6

Reviewed independently by two models, findings deduplicated and combined below.

Summary

Severity Count Details
🔴 Critical/Bug 7 3x nil deref in error middleware, 1x error swallowed, 2x security (fail-open consent, blanket permissions), 1x WaitForIdle hang
🟡 Warning 5 Always-on content logging, swallowed errors, cleanup ordering, singleton lifecycle, config fallback
🔵 Suggestion 1 Ticker/canvas race condition

Cross-Model Consensus (flagged by BOTH models independently)

These are the highest-confidence findings:

  • Nil deref on agentResult.Content in error middleware (3 locations)
  • Original error silently swallowed when user declines fix
  • Blanket SDK permission approval bypasses safety boundary
  • Consent check fails open on error

Dead Config Keys (flagged by GPT-5.4)

ai.agent.mode in config_options.yaml is never read (factory hard-codes "interactive"), and ai.agent.copilot.logLevel is never read (container constructs NewCopilotClientManager(nil) which defaults to "debug"). Users can set these keys, but they have no runtime effect. Either wire them into construction or remove from config_options.yaml until they work.

@wbreza wbreza changed the title Phase 1: Add Copilot SDK foundation alongside existing langchaingo agent Migrate to Copilot SDK for azd agent implementation Mar 11, 2026
@wbreza wbreza force-pushed the copilot-sdk-phase1 branch from c961cd9 to 5e41265 Compare March 11, 2026 19:35
@vhvb1989
Copy link
Copy Markdown
Member

Heads up: orphaned ghCopilot build tag infrastructure

This PR removes the ghCopilot build-gated registration files (github_copilot_registration.go and github_copilot_registration_stub.go), but several related pieces are still in place and are now effectively dead code:

Still present (untouched by this PR):

  • cli/azd/pkg/llm/github_copilot.go — Still has //go:build ghCopilot, still imports langchaingo, still defines the full GitHubCopilotModelProvider with device code auth flow and the clientID/copilotIntegrationID ldflags. Since the registration call site is deleted, this provider is compiled but never wired into IoC.
  • cli/azd/ci-build.ps1 — Still passes -tags ghCopilot and the ldflags (-X ...clientID, -X ...copilotIntegrationID) when GitHubCopilotClientId is provided.
  • CI pipeline YAML filesbuild-cli.yml, cross-build-cli.yml, and build-and-test.yml still pass ghCopilotClientId / ghCopilotIntegrationId parameters through.

The build won't break (the old code compiles but is just unused), but it's worth tracking cleanup of these artifacts — either in this PR or as a follow-up:

  1. Remove pkg/llm/github_copilot.go (and its langchaingo dependency)
  2. Remove the ghCopilot tag logic from ci-build.ps1
  3. Clean up the ghCopilotClientId/ghCopilotIntegrationId params from CI YAML files

Just flagging since the list is fresh — totally fine if this is planned for a subsequent iteration.

Copy link
Copy Markdown
Member

@vhvb1989 vhvb1989 left a comment

Choose a reason for hiding this comment

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

No blockers from my side. The migration to the Copilot SDK looks solid overall. Left a couple of comments for consideration:

  1. Silent plugin installensurePlugins() installs the Azure plugin system-wide without user awareness. Worth surfacing a message or prompting.
  2. Old config migration — Users with ai.agent.model.type set to "github-copilot" will get a hard failure. A small auto-migration or friendly error would help early adopters.
  3. Orphaned ghCopilot build taggithub_copilot.go, ci-build.ps1, and CI YAML params still reference the old build tag. Dead code that can be cleaned up in a follow-up.

None of these are blocking — just flagging for author consideration.

wbreza and others added 14 commits March 14, 2026 01:37
Bug fixes:
- Fix nil pointer dereferences in error.go when agentResult is nil
- Fix session.error not unblocking WaitForIdle (signal idleCh on error)
- Fix consent check fails-open: deny on error instead of allow

Security:
- PreToolUse consent check now denies on error with logged reason
- OnPermissionRequest remains approve-all (CLI-level coarse permissions,
  fine-grained control via PreToolUse hooks)

Code quality:
- Deterministic cleanup order: changed from map to ordered slice with
  reverse teardown (session events -> file logger -> client)
- Log warning on config load failure
- Simplified MCP server unmarshaling

Config:
- Added skills.directories and skills.disabled to config_options.yaml
- Fixed tools.available/excluded type from object to array

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
AgentMode:
- New AgentMode type with constants: AgentModeInteractive, AgentModeAutopilot,
  AgentModePlan. WithMode() now takes AgentMode instead of string.

Removed logging (redundant with Copilot CLI logs at ~/.copilot/logs/):
- thought_logger.go — old langchaingo callback handler
- file_logger.go — old langchaingo callback handler
- chained_handler.go — old langchaingo callback handler
- session_event_handler.go — SessionEventLogger, SessionFileLogger,
  CompositeEventHandler all unused after AgentDisplay consolidation
- session_event_handler_test.go

Kept: logging/util.go with TruncateString (used by display.go)

Net: 902 lines deleted.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When the user declines the agent fix, the code returned 'err' which
was nil (consent check succeeded), silently swallowing the original
command failure. Now returns originalError to preserve the error.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Deleted (all replaced by Copilot CLI built-in tools):
- tools/dev/ — command executor (shell tool)
- tools/io/ — 12 file/directory tools + tests
- tools/common/ — AnnotatedTool interface, ToLangChainTools, ToolLoader
- tools/loader.go — composite tool loader
- tools/mcp/tool_adapter.go — MCP-to-langchaingo adapter
- tools/mcp/sampling_handler.go — MCP sampling handler
- tools/mcp/elicitation_handler.go — MCP elicitation handler
- tools/mcp/loader.go — MCP tool loader
- consent/consent_wrapper_tool.go — langchaingo tool wrapper

Cleaned:
- Removed WrapTool/WrapTools from ConsentManager interface and impl
- Removed common package import from consent
- Kept tools/mcp/embed.go with McpJson embed (still used by factory)

Net: 7,025 lines deleted.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Updated test expectations after MCP tool migration to Copilot SDK skills.
Tests now verify error_troubleshooting, provision_common_error, and
validate_azure_yaml (the 3 remaining MCP tools).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- intPtr() -> new() for pointer creation
- strings.Split -> strings.SplitSeq for range iteration
- strings.HasPrefix+TrimPrefix -> strings.CutPrefix
- floatPtr/strPtr helpers replaced with new() in tests

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

- Migrate all config keys from ai.agent.* to copilot.* namespace
- Move copilot_client.go and session_config.go to internal/agent/copilot/
- Delete entire pkg/llm/ package (azure_openai, ollama, github_copilot, model_factory, manager)
- Move consent commands from azd mcp consent to azd copilot consent
- Streamline error middleware: single consent prompt + agent-driven troubleshooting
- Troubleshooting prompts in embedded Go text templates
- AgentDisplay: render AssistantMessage in real-time, red x for failed tools
- Remove Content from AgentResult, delete dead feedback package
- Adopt SDK bundler for CLI binary embedding, remove npm path scanning
- Clean up CI pipelines: remove ghCopilot build tag and ldflags
- Add WithSystemMessage AgentOption
- Add composable config key constants with ConfigRoot prefix
- Remove langchaingo dependency

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add CopilotCLI managed tool (internal/agent/copilot/cli.go) following Bicep pattern
- Download platform-specific CLI from npm registry on first use
- Cache at ~/.azd/bin/copilot-cli-{version}, override via AZD_COPILOT_CLI_PATH
- Implement tools.ExternalTool interface (Name, InstallUrl, CheckInstalled)
- Integrate with CopilotClientManager (resolves CLI at Start time)
- Remove SDK bundler (zcopilot_* files, go tool bundler CI step, tool dep)
- Binary size reduced ~106MB (no longer embedded)
- Fix cspell: add agentcopilot to word list, reword comment

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix WaitForIdle hang when SessionIdle fires before AssistantMessage
- Fix ticker vs Pause() TOCTOU race causing spinner to render over consent prompts
- Fix consent grant errors silently denying tool execution
- Add error logging for consent rule load/save failures
- Improve tool completion display with contextual verbs and diff stats
- Add tree-style sub-detail for shell commands and MCP tool args
- Add colored diff stats (green +N / red -N) for edit/create tools
- Show plugin version in skill invocation display
- Normalize whitespace spacing via printSeparated for all section transitions
- Show error messages on tool call failures
- Add comprehensive unit tests for display helpers and consent persistence

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@wbreza wbreza force-pushed the copilot-sdk-phase1 branch from 4e90ac2 to e500827 Compare March 14, 2026 08:39
wbreza and others added 3 commits March 14, 2026 01:47
Includes copilot CLI plugin management (ListPlugins, InstallPlugin),
session time formatting, consent command migration, and azdcontext updates.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On WSL with Windows filesystem mounts (/mnt/c/...), fsnotify's
filepath.Walk + inotify watch setup can hang or be extremely slow.
Moving NewWatcher() to a background goroutine prevents it from
blocking SendMessage. The watcher results are still collected at
cleanup via mutex-protected access.

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

- Check GitHub Copilot auth status before session creation
- Prompt to sign in via copilot login (OAuth device flow) if not authenticated
- Add CopilotCLI.Login() wrapper for interactive copilot login command
- Fix spinner showing 'Running Ran tool' — use tool name for spinner, verb for completion
- Fix E2E test: add OnPermissionRequest: ApproveAll to session config
- Add ErrToolExecutionSkipped to excluded errors list in error mapping test
- Add unit tests for Login success and error cases

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@weikanglim weikanglim left a comment

Choose a reason for hiding this comment

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

Overall this looks good! I'm really happy that we're removing the langchain dependencies in favor of copilot-sdk.

I left a comment about namespacing session state just to make it easier for us to work with longer term.

@wbreza wbreza merged commit 890d937 into main Mar 16, 2026
26 checks passed
@wbreza wbreza deleted the copilot-sdk-phase1 branch March 16, 2026 23:37
jongio pushed a commit to jongio/azure-dev that referenced this pull request Mar 27, 2026
* Add Copilot SDK foundation alongside existing langchaingo agent

Add the GitHub Copilot SDK (github.com/github/copilot-sdk/go) as a new
dependency and create the foundational types for a Copilot SDK-based agent
implementation. All new code coexists alongside the existing langchaingo
agent — no existing code is modified or deleted.

New files:
- pkg/llm/copilot_client.go: CopilotClientManager wrapping copilot.Client
  lifecycle (Start, Stop, GetAuthStatus, ListModels)
- pkg/llm/session_config.go: SessionConfigBuilder that reads ai.agent.*
  config keys and produces copilot.SessionConfig, including MCP server
  merging (built-in + user-configured) and tool control
- internal/agent/copilot_agent.go: CopilotAgent implementing the Agent
  interface backed by copilot.Session with SendAndWait
- internal/agent/copilot_agent_factory.go: CopilotAgentFactory that
  creates CopilotAgent instances with SDK client, session, permission
  hooks, MCP servers, and event handlers
- internal/agent/logging/session_event_handler.go: SessionEventLogger,
  SessionFileLogger, and CompositeEventHandler for SDK SessionEvent
  streaming to UX thought channel and daily log files

Config additions (resources/config_options.yaml):
- ai.agent.model: Default model for Copilot SDK sessions
- ai.agent.mode: Agent mode (autopilot/interactive/plan)
- ai.agent.mcp.servers: Additional MCP servers
- ai.agent.tools.available/excluded: Tool allow/deny lists
- ai.agent.systemMessage: Custom system prompt append
- ai.agent.copilot.logLevel: SDK log level

Resolves Azure#6871, Azure#6872, Azure#6873, Azure#6874, Azure#6875

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

* Add 'copilot' as default agent provider type

Register CopilotProvider as a named 'copilot' model provider in the IoC
container. This makes 'copilot' the default agent type when ai.agent.model.type
is not explicitly configured.

Changes:
- Add LlmTypeCopilot constant and CopilotProvider (copilot_provider.go)
- Default GetDefaultModel() to 'copilot' when no model type is set
- Register 'copilot' provider in container.go
- Update init.go to set 'copilot' instead of 'github-copilot'
- Update error message to list copilot as supported type

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

* Add diagnostic logging to Copilot SDK agent pipeline

Add [copilot] and [copilot-event] prefixed log statements throughout
the Copilot SDK agent pipeline for troubleshooting:

- CopilotClientManager: Start/stop state transitions
- CopilotAgentFactory: MCP server count, session config details,
  PreToolUse/PostToolUse/ErrorOccurred hook invocations
- CopilotAgent.SendMessage: prompt size, response size, errors
- SessionEventLogger: every event type received, plus detail for
  assistant.message, tool.execution_start, and assistant.reasoning

Run with AZD_DEBUG=true to see log output.

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

* Wire CopilotAgentFactory into AgentFactory for automatic delegation

AgentFactory.Create() now checks the configured model type. When it's
'copilot' (the new default), it delegates to CopilotAgentFactory which
creates a CopilotAgent backed by the Copilot SDK session. No call site
changes needed — existing code calling AgentFactory.Create() gets the
Copilot SDK agent automatically.

Changes:
- AgentFactory now takes CopilotAgentFactory as a dependency
- Create() checks model type and delegates to CopilotAgentFactory
- Register CopilotAgentFactory, CopilotClientManager, and
  SessionConfigBuilder in IoC container (cmd/container.go)

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

* Enable SDK debug logging to diagnose CLI process startup failure

Set SDK LogLevel to 'debug' by default to surface the command and args
the SDK uses when spawning the copilot CLI process. This will help
diagnose the 'exit status 1' error during client.Start().

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

* Update copilot-sdk to v0.1.26-preview.0

CLI v0.0.419-0 now supports --headless and --stdio flags required by the
SDK. Updated Go SDK from v0.1.25 to v0.1.26-preview.0 for latest
compatibility.

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

* Auto-discover native Copilot CLI binary from @github/copilot-sdk npm package

The copilot CLI shim in PATH (from @github/copilot npm package) doesn't
support --headless --stdio flags required by the Go SDK. However, the
@github/copilot-sdk npm package bundles a newer native binary at
node_modules/@github/copilot-{platform}/copilot[.exe] that does.

CopilotClientManager now auto-discovers this binary with resolution order:
1. COPILOT_CLI_PATH environment variable
2. Native binary from @github/copilot-sdk npm package (platform-specific)
3. Falls back to 'copilot' in PATH (SDK default)

Also adds a passing e2e test (TestCopilotSDK_E2E) that validates the full
SDK lifecycle: client start → auth check → list models → create session →
send prompt → receive response → cleanup. Pure native binary, no Node.js.

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

* Fix: explicitly allow tools in PreToolUse hook

The empty PreToolUseHookOutput{} was interpreted as deny by the SDK,
blocking all tool calls. Set PermissionDecision to 'allow' explicitly.

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

* Fix: add OnPermissionRequest handler to approve tool permissions

The SDK has two separate permission mechanisms:
1. PreToolUse hooks (lifecycle interception) - already set to 'allow'
2. OnPermissionRequest handler (CLI permission prompts) - was NOT set

Without OnPermissionRequest, the CLI's permission requests go unanswered
and default to deny, blocking all tool calls. Use the SDK's built-in
PermissionHandler.ApproveAll to approve all requests.

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

* Fix: increase SendAndWait timeout to 10 minutes

The SDK defaults to 60s timeout when the context has no deadline,
which is too short for agent init tasks (discovery, IaC generation,
Dockerfile creation, etc.). Set a 10-minute timeout per message.

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

* Use Send + idle event instead of SendAndWait to avoid timeout

Replace SendAndWait (which imposes a 60s default timeout) with Send
(non-blocking) + explicit wait for session.idle event. The agent task
runs until the SDK signals completion or the parent context is cancelled.
No artificial timeout.

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

* Integrate azd consent system and required plugin auto-install

Two major changes to CopilotAgentFactory:

1. Required plugin auto-install:
   - Runs 'copilot plugin install microsoft/GitHub-Copilot-for-Azure:plugin'
     before starting each session (idempotent, non-interactive)
   - Uses the resolved CLI binary path from CopilotClientManager
   - Logs warnings on install failure but doesn't block session creation

2. Wire azd consent system into SDK permission handlers:
   - OnPermissionRequest: delegates to ConsentManager.CheckConsent()
     for CLI-level permission requests. If consent requires prompting,
     uses ConsentChecker to show azd's interactive consent UX with
     scoped persistence (session/project/global).
   - OnPreToolUse: checks ConsentManager before each tool execution.
     If no rule exists, prompts via ConsentChecker.PromptAndGrantConsent()
     which stores the user's choice at their selected scope.
   - Replaces the previous PermissionHandler.ApproveAll with proper
     consent-gated approval flow.

Also:
- CopilotAgentFactory now takes ConsentManager as a dependency
- CopilotClientManager exposes CLIPath() for plugin install commands

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

* Fix mage namespace discovery for dev:install target

The Dev struct needs to be declared as 'type Dev mg.Namespace' for mage
to discover it as a namespace. Also adds github.com/magefile/mage to
go.mod as required by the magefile's mg import.

This fixes 'mage dev:install' and 'mage dev:uninstall' targets.

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

* Fix OTel schema version conflict causing panic on startup

The copilot-sdk pulled in otel core v1.42.0 (schema 1.39.0) but otel/sdk
remained at v1.38.0 (schema 1.37.0). This mismatch caused a panic in
resource.New() at startup. Upgraded all OTel SDK packages to v1.42.0 to
match the core version.

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

* Update copilot-sdk to v0.1.32

Updated from v0.1.26-preview.0 to v0.1.32. Adapted to API change where
PermissionRequest.Kind is now a typed PermissionRequestKind instead of
plain string.

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

* Fix OTel semconv schema mismatch: update to v1.40.0

OTel SDK v1.42.0 uses semconv/v1.40.0 internally for resource.Default(),
but our resource.go imported semconv/v1.39.0. The schema URL mismatch
(1.40.0 vs 1.39.0) caused a panic on resource.Merge(). Updated import
to match the SDK's semconv version.

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

* Fix: approve CLI permission requests, use consent only in PreToolUse

The OnPermissionRequest and OnPreToolUse handlers were conflicting:
OnPermissionRequest was calling CheckToolConsent (which only checks
rules, doesn't prompt) and falling through to 'denied' when no rules
existed. Meanwhile OnPreToolUse correctly called PromptAndGrantConsent.

Fix: OnPermissionRequest now approves all CLI-level permission requests
(file access, shell, URLs). Fine-grained per-tool consent with user
prompting is handled exclusively by the OnPreToolUse hook.

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

* Add AgentDisplay for consolidated Copilot SDK UX rendering

Replace the thought-channel indirection with AgentDisplay — a direct
event-driven UX renderer that subscribes to session.On() and handles
all SDK event types using existing azd UX components.

New: internal/agent/display.go
- AgentDisplay manages Canvas + Spinner + VisualElement per SendMessage
- Handles 15+ event types: turn lifecycle, tool execution with elapsed
  timer, reasoning/thinking display, streaming message deltas, errors,
  warnings, skill invocations, subagent delegation
- WaitForIdle() blocks until session.idle with final content capture
- Thread-safe state management for concurrent event handling

Changes:
- CopilotAgent.SendMessage() creates AgentDisplay per turn, subscribes
  it to session events, and uses WaitForIdle() for completion
- CopilotAgentFactory no longer creates thought channel or
  SessionEventLogger — file logger remains for audit trail
- Export TruncateString from logging package for shared use

Removes:
- Thought channel and WithCopilotThoughtChannel option
- renderThoughts() goroutine (replaced by AgentDisplay)
- SessionEventLogger dependency in factory (UX moved to display)

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

* Fix reasoning display and show relative paths in tool summaries

Two fixes to AgentDisplay:

1. Reasoning: accumulate reasoning_delta chunks into a rolling buffer
   instead of replacing with each delta. Also handle streaming_delta
   events with phase='thinking' for models that emit reasoning that
   way. Canvas.Update() called after each reasoning update for
   immediate display.

2. Paths: extractToolInputSummary now converts absolute paths to
   relative (via filepath.Rel to cwd) for cleaner display. Paths
   that escape cwd are shown absolute.

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

* Show full reasoning in scrolling window with flush on transitions

Reasoning display now works as a scrolling window:
- VisualElement shows last ~5 lines of reasoning below the spinner,
  updating live as delta chunks stream in
- Full reasoning accumulates in a buffer (no truncation)
- On tool start or turn end, the complete reasoning is printed as a
  persistent dimmed block above the canvas, then the buffer resets

Removed latestThought field — reasoning state is now entirely managed
via reasoningBuf. Removed AssistantMessageDelta handler (message deltas
don't need UX rendering — final content comes via AssistantMessage).

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

* UX: add blank line before spinner and change text to 'Working...'

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

* Check if plugin is installed before install/update

ensurePlugins now lists installed plugins first. If a required plugin
is already installed, it runs 'copilot plugin update' instead of a
full install. New plugins get 'copilot plugin install'.

Also restructured requiredPlugins as pluginSpec with Source (install
path) and Name (installed name for update command).

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

* Simplify init to single prompt + wire OnUserInputRequest handler

Two changes:

1. Wire OnUserInputRequest in CopilotAgentFactory:
   Enables the agent's built-in ask_user tool. When the agent asks a
   question, the handler renders it using azd's UX components:
   - Choices → ux.NewSelect() with azd styling
   - Freeform → ux.NewPrompt() for text input
   This lets the agent ask clarifying questions during execution
   (architecture choices, service selection, config options).

2. Simplify initAppWithAgent() from 6-step loop to single prompt:
   Replaces 6 hardcoded steps + inter-step feedback loops + post-
   completion summary aggregation with a single prompt that delegates
   to azure-prepare and azure-validate skills from the Azure plugin.
   The skills handle all orchestration internally and can ask the user
   questions via ask_user when needed.

   Removed: initStep struct, step definitions, collectAndApplyFeedback(),
   postCompletionSummary(), feedback import (~140 lines).

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

* UX: use intent as spinner text, move reasoning above spinner, set interactive mode

Three UX improvements:

1. Spinner text: uses assistant.intent events (short task descriptions
   like 'Analyzing project structure') instead of static 'Working...'
   Truncated to 80 chars to stay concise.

2. Reasoning display: moved above the spinner instead of below. Layout
   is now: blank line → reasoning (last 5 lines, gray) → blank line →
   spinner. More natural reading order.

3. Mode: SendMessage now explicitly sets Mode to 'interactive' so the
   agent asks for approval before executing tools.

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

* Strip markdown from ask_user prompts, render reasoning with markdown, fix skill triggers

Three changes:

1. ask_user: Strip markdown formatting (bold, italic, backticks, headings)
   from question text and choice labels before rendering in azd UX prompts.
   Choices still return the original value to the agent.

2. flushReasoning: Render accumulated reasoning with output.WithMarkdown()
   instead of raw gray text, giving proper formatting for code blocks,
   lists, and other markdown content.

3. init prompt: Use natural trigger phrases ('prepare this application
   for deployment to Azure', 'validate that everything is ready') that
   match the azure-prepare and azure-validate skill description triggers,
   instead of referencing skill names directly.

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

* Support AllowFreeform in ask_user with 'Other' choice option

When the agent sends choices with AllowFreeform=true, append an
'Other (type your own answer)' option to the Select list. If selected,
follow up with a freeform Prompt and return WasFreeform: true.

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

* Explicitly invoke @azure-prepare and @azure-validate skills in init prompt

Use @skill-name syntax and numbered steps to ensure both skills are
invoked in order. Previous natural language triggers may not have
reliably activated the skills.

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

* Specify 'azd' recipe for azure-prepare and azure-validate skills

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

* Store Copilot session files in .azure/copilot relative to cwd

Sets SessionConfig.ConfigDir to .azure/copilot in the current working
directory so session state is project-local instead of global ~/.copilot.

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

* Fix skill discovery: revert ConfigDir, use qualified skill names

ConfigDir override to .azure/copilot broke plugin discovery — the CLI
loads plugins from ConfigDir/installed-plugins/ which was empty. Reverted
to default ~/.copilot so installed plugins (and their skills) are found.
Set WorkingDirectory instead for tool operations.

Updated init prompt to use fully qualified azure:azure-prepare and
azure:azure-validate skill names (plugin:skill-name format).

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

* Pass installed plugins via --plugin-dir for headless mode discovery

The CLI in --headless --stdio mode (used by the SDK) doesn't auto-discover
installed plugins from ~/.copilot/installed-plugins/. This meant skills
from the Azure plugin were never loaded.

Fix: discoverInstalledPluginDirs() scans ~/.copilot/installed-plugins/
for plugin directories (verified by presence of skills/ or .claude-plugin/)
and passes each via --plugin-dir CLIArgs to the SDK client.

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

* Scope plugin discovery to Azure plugin only

Only load the microsoft/GitHub-Copilot-for-Azure plugin via --plugin-dir.
Checks both _direct and marketplace install paths. Other plugins will be
handled separately later.

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

* Use SkillDirectories instead of --plugin-dir for skill loading

The --plugin-dir CLI flag isn't supported by all copilot binary builds.
Instead, pass the Azure plugin's skills directory via SessionConfig.
SkillDirectories, which is sent via JSON-RPC createSession and works
reliably in headless mode.

discoverAzurePluginSkillDirs() finds the skills/ directory from the
installed Azure plugin and adds it to SkillDirectories alongside any
user-configured skill directories.

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

* Persist intent text in spinner instead of resetting to 'Thinking...'

Track lastIntent and reuse it when the spinner resets after turn start
or tool completion. Only falls back to 'Thinking...' if no intent has
been received yet.

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

* Add blank line after spinner

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

* Show relative paths for skill directories in tool summaries

toRelativePath now tries both cwd and ~/.copilot/installed-plugins/
as base directories. Paths under the plugins root (skill files) are
shown relative instead of absolute.

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

* Capture intent from report_intent tool calls for spinner text

The agent uses a report_intent tool (not assistant.intent events) to
signal what it's working on. Extract the intent text from the tool's
arguments and use it as the spinner text. The report_intent tool is
suppressed from UX display (no 'Ran report_intent' completion line).

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

* Show nested subagent tool calls with rich display

Subagent events now show richer UX:
- started: '◆ {DisplayName} — {Description}'
- completed: '✔ {DisplayName} completed' with summary
- failed: '✖ {DisplayName} failed: {error}'
- deselected: resets subagent state

Tool calls inside a subagent are indented with 2 spaces to show
nesting visually:
  ◆ Azure Prepare — Prepare apps for deployment
    ✔ Ran read_file with path: main.go
    ✔ Ran write_file with path: azure.yaml
  ✔ Azure Prepare completed

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

* UX polish: blank lines before skill/subagent, suppress internal tools

- Add blank line before 'Using skill:' and 'Delegating to:' lines
- Suppress tool call display for report_intent, ask_user, task, and
  skill: prefixed tools — these are internal/UX tools that shouldn't
  show as 'Ran X' completion lines

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

* Prompt for reasoning effort and model on first agent run

First run (no ai.agent.reasoningEffort config):
- Prompt for reasoning effort (low/medium/high) with cost guidance
- Prompt for model selection (default or specific model)
- Save both to azd config

Subsequent runs:
- Show info note with current model and reasoning level
- Point to 'azd config set' for changes

Also:
- Wire ai.agent.reasoningEffort to SessionConfig.ReasoningEffort
- Add reasoningEffort to config_options.yaml

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

* Fix: only signal idle when final content exists

Multiple session.idle events can arrive during a session (after
permission prompts, between tool calls). The first idle was consumed
by WaitForIdle before the assistant message arrived, causing the
display to clear with no summary shown.

Fix: only signal idleCh when finalContent has been set by an
assistant.message event. Early idle events are ignored.

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

* Load Azure plugin MCP servers into session config

Read .mcp.json from the installed Azure plugin and merge its MCP servers
(azure, foundry-mcp, context7) into SessionConfig.MCPServers alongside
built-in and user-configured servers.

Merge order: built-in → Azure plugin → user config (last wins).

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

* Suppress 'skill' tool from display, add blank line after Using skill

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

* Fix agent exit: reset finalContent per turn, add completion logging

Root cause: assistant.message from an earlier turn set finalContent,
then a session.idle between tool calls found hasContent=true and
signaled WaitForIdle prematurely — before the actual final message.

Fixes:
- Reset finalContent on every assistant.turn_start so only the last
  turn's message is considered
- Handle session.task_complete and session.shutdown as additional
  completion signals
- Add debug logging to assistant.message, session.idle, and
  WaitForIdle for future troubleshooting

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

* Add scoped system message and empty directory handling

System message (append mode):
- Scopes the agent to Azure application development only
- Rejects unrelated requests with a focused explanation
- User-configured ai.agent.systemMessage appended after default

Init prompt:
- Agent now checks if cwd is empty/has no code first
- If empty, asks user what type of Azure app to build
- Then proceeds with azure-prepare and azure-validate skills

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

* Consistent whitespace and improved config display

Whitespace rules:
- Skills, subagents, reasoning, errors, warnings: blank line before
  and after (via printSeparated, no double blanks)
- Tool completions: stack without blank lines (via printLine)
- lastPrintedBlank flag prevents duplicate blank lines

Config display:
- Split into multiple lines for readability
- Show model and reasoning on separate bullet points
- azd commands shown in highlight format

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

* Add blank lines before/after user prompts, fix extra blank in config display

- User prompts (ask_user): blank line before and after Select/Prompt
- Config display: remove leading blank line (was doubling up with
  prior output), remove trailing newline from alpha warning title

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

* Log MCP server details and skill dirs for debugging

Before session creation, log each configured MCP server (name, type,
command/url) and skill directory. Also capture session.info events
with AllowedTools list and skill.invoked events in the file logger.

Visible with AZD_DEBUG=true or in ~/.azd/logs/azd-agent-*.log.

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

* Print MCP servers and skill dirs to console output for debugging

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

* Print available tools to console from session events

Captures AllowedTools from the first session event that carries them
and prints the full list to console output for debugging.

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

* Fix MCP server config: use type 'local' and add tools wildcard

The SDK expects type='local' (not 'stdio') for local MCP servers,
and requires a 'tools' field listing which tools to expose. Without
tools=['*'], no MCP tools were made available to the agent.

Fixes:
- convertServerConfig: use type='local', add tools=['*']
- loadAzurePluginMCPServers: normalize plugin configs — set type
  to 'local' for command-based servers, add tools=['*'] if missing

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

* Add diagnostic prompt to list all tools including MCP server tools

Temporary: agent prints all available tools grouped by built-in, MCP
server tools, and skills before proceeding with init. Will remove once
MCP server integration is verified working.

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

* Dump first 30 session events with details for MCP debugging

Temporary: prints event type + key fields (allowedTools, tools, message,
infoType, name) for the first 30 events to diagnose MCP server loading.

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

* Remove diagnostic prompt, rely on event dump for tool debugging

The system message was causing the model to ignore the diagnostic
'list tools' request. Event dump from the factory provides better
programmatic visibility.

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

* Debugging: disable system message, add diagnostic tools list prompt, remove event dump

Temporary changes to diagnose MCP server tool availability:
- Comment out system message (was blocking diagnostic requests)
- Add explicit 'list all tools' prompt before init task
- Remove event dump from factory (wasn't showing useful data)

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

* Add post-init Q&A loop for follow-up questions

After the init summary, prompt the user 'Any questions?' in a loop.
User can ask follow-up questions (sent to the same agent session with
full context). Press Enter with no input to finish and exit.

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

* Add session resume support for cancelled/crashed agent sessions

On azd init with agent mode, checks for previous sessions in the
current directory via client.ListSessions(). If found, prompts user
to resume a previous session or start fresh.

Resume uses client.ResumeSession() which restores full conversation
history with the same MCP servers, skills, permissions, and hooks.

Changes:
- CopilotAgentFactory: add ListSessions() and Resume() methods
- init.go: add session picker before agent creation, add
  CopilotAgentFactory to initAction struct

Spec at docs/specs/copilot-agent-ux/session-resume.md

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

* Polish session resume UX: numbers, local time, truncated labels

- Show numbered choices in session picker (DisplayNumbers: true)
- Convert timestamps to local time (Today 3:04 PM, Yesterday, Jan 2)
- Truncate labels to ~120 chars total
- Shorter prompt: 'Previous sessions found:'

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

* Fix session labels: collapse newlines, enforce 120 char max

Summaries from sessions can contain newlines and markdown. Use
strings.Fields() to collapse all whitespace into single spaces,
then truncate to fit within 120 chars total.

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

* Enable line numbers and filtering on all Select prompts

Added DisplayNumbers and EnableFiltering to reasoning effort, model
selection, and session picker prompts.

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

* Dynamic model list from SDK with billing and reasoning metadata

Fetch models via ListModels() instead of hardcoding. Each option shows:
  'Claude Sonnet 4.5 (high) (1x)'
  — name, default reasoning effort, billing multiplier

Also:
- Remove trailing ':' from prompt messages (UX components add them)
- Add ListModels() to CopilotAgentFactory

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

* Show session usage metrics at end of init

Accumulate usage from assistant.usage and session.usage_info events:
input/output tokens, cost multiplier, premium requests, API duration,
and model used.

Display at session end:
  Session usage:
  • Model:            claude-sonnet-4.5
  • Input tokens:     45.2K
  • Output tokens:    12.8K
  • Total tokens:     58.0K
  • Cost:             1.0x premium
  • Premium requests: 15
  • API duration:     2m 34s

Token counts formatted as K/M for readability.

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

* Remove post-init Q&A loop

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

* Consolidate agent into self-contained CopilotAgent with factory

Major refactor: CopilotAgent is now a self-contained agent that
encapsulates initialization, session management, display, and usage.
CopilotAgentFactory creates agents with dependencies wired via IoC.

New API:
  agent, _ := factory.Create(ctx, agent.WithMode('interactive'))
  initResult, _ := agent.Initialize(ctx)
  selected, _ := agent.SelectSession(ctx)
  result, _ := agent.SendMessage(ctx, prompt, agent.WithSessionID(...))
  // result.Content, result.SessionID, result.Usage
  agent.Stop()

New types (types.go):
  AgentResult{Content, SessionID, Usage}
  InitResult{Model, ReasoningEffort, IsFirstRun}
  UsageMetrics with Format() method
  AgentOption: WithModel, WithReasoningEffort, WithMode, WithDebug
  SendOption: WithSessionID
  InitOption: WithForcePrompt

Agent methods:
  Initialize() — config prompts (first run), plugin install, client start
  SelectSession() — UX picker for session resume
  ListSessions() — raw session listing
  SendMessage() / SendMessageWithRetry() — returns AgentResult
  Stop() — cleanup

Deleted (old langchaingo agent):
  agent.go, agent_factory.go, conversational_agent.go, prompts/

Simplified:
  init.go — ~40 lines instead of ~150
  container.go — removed old AgentFactory, ModelFactory registrations
  error.go — updated to use CopilotAgentFactory

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

* Fix spacing: add blank line after each prompt, remove leading blanks

Each prompt component (Select, Prompt) now adds a blank line after
its Ask() call. Callers manage spacing before prompts. Removed the
leading blank line from SelectSession (was doubling up with the
trailing blank from the previous prompt).

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

* Add unit tests for types.go and display.go pure functions

New test files:
- types_test.go: UsageMetrics.Format(), TotalTokens(), formatTokenCount,
  stripMarkdown, formatSessionTime (40 test cases)
- display_test.go: extractToolInputSummary, extractIntentFromArgs,
  toRelativePath, GetUsageMetrics accumulation

All pure functions tested without SDK mocking.

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

* Fix billing display and CI checks

- Rename Cost to BillingRate (per-request multiplier, not cumulative)
- Show premium requests from SDK (omit if not reported)
- Handle session.shutdown for TotalPremiumRequests
- Remove manual API call counter
- Fix all lint issues (errorlint, gosec, staticcheck, unused)
- Fix formatting, remove unused code (github_copilot_registration files,
  discoverInstalledPluginDirs)
- All CI checks pass: gofmt, golangci-lint, tests

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

* Address PR review feedback

- Fix config_options.yaml: tools.available/excluded type 'object' -> 'array'
- Add missing config docs: ai.agent.skills.directories, ai.agent.skills.disabled
- Log warning on userConfigManager.Load() failure instead of silently swallowing
- Simplify redundant MCP server unmarshaling (removed type probe, single path)

Other review items (r3, r6, r8, r9) referenced old code that was deleted
in the agent consolidation commit.

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

* Address second round PR review feedback

Bug fixes:
- Fix nil pointer dereferences in error.go when agentResult is nil
- Fix session.error not unblocking WaitForIdle (signal idleCh on error)
- Fix consent check fails-open: deny on error instead of allow

Security:
- PreToolUse consent check now denies on error with logged reason
- OnPermissionRequest remains approve-all (CLI-level coarse permissions,
  fine-grained control via PreToolUse hooks)

Code quality:
- Deterministic cleanup order: changed from map to ordered slice with
  reverse teardown (session events -> file logger -> client)
- Log warning on config load failure
- Simplified MCP server unmarshaling

Config:
- Added skills.directories and skills.disabled to config_options.yaml
- Fixed tools.available/excluded type from object to array

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

* Add AgentMode enum, remove redundant logging infrastructure

AgentMode:
- New AgentMode type with constants: AgentModeInteractive, AgentModeAutopilot,
  AgentModePlan. WithMode() now takes AgentMode instead of string.

Removed logging (redundant with Copilot CLI logs at ~/.copilot/logs/):
- thought_logger.go — old langchaingo callback handler
- file_logger.go — old langchaingo callback handler
- chained_handler.go — old langchaingo callback handler
- session_event_handler.go — SessionEventLogger, SessionFileLogger,
  CompositeEventHandler all unused after AgentDisplay consolidation
- session_event_handler_test.go

Kept: logging/util.go with TruncateString (used by display.go)

Net: 902 lines deleted.

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

* Fix: return originalError when user declines fix in error middleware

When the user declines the agent fix, the code returned 'err' which
was nil (consent check succeeded), silently swallowing the original
command failure. Now returns originalError to preserve the error.

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

* Document permission handler intent and relationship to PreToolUse

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

* Remove docs/specs/copilot-agent-ux (deleted by reviewer)

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

* Delete dead agent tools and unused consent wrapper

Deleted (all replaced by Copilot CLI built-in tools):
- tools/dev/ — command executor (shell tool)
- tools/io/ — 12 file/directory tools + tests
- tools/common/ — AnnotatedTool interface, ToLangChainTools, ToolLoader
- tools/loader.go — composite tool loader
- tools/mcp/tool_adapter.go — MCP-to-langchaingo adapter
- tools/mcp/sampling_handler.go — MCP sampling handler
- tools/mcp/elicitation_handler.go — MCP elicitation handler
- tools/mcp/loader.go — MCP tool loader
- consent/consent_wrapper_tool.go — langchaingo tool wrapper

Cleaned:
- Removed WrapTool/WrapTools from ConsentManager interface and impl
- Removed common package import from consent
- Kept tools/mcp/embed.go with McpJson embed (still used by factory)

Net: 7,025 lines deleted.

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

* Remove unused ExecutingTool and related global state from consent

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

* Fix MCP functional tests for remaining tools

Updated test expectations after MCP tool migration to Copilot SDK skills.
Tests now verify error_troubleshooting, provision_common_error, and
validate_azure_yaml (the 3 remaining MCP tools).

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

* Fix init.go: remove stray backtick, highlight azd up command

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

* Apply go fix for Go 1.26 compatibility

- intPtr() -> new() for pointer creation
- strings.Split -> strings.SplitSeq for range iteration
- strings.HasPrefix+TrimPrefix -> strings.CutPrefix
- floatPtr/strPtr helpers replaced with new() in tests

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

* Remove unused intPtr, floatPtr, strPtr helpers (inlined by go fix)

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

* refactor: migrate to copilot.* namespace, delete pkg/llm, streamline error middleware

- Migrate all config keys from ai.agent.* to copilot.* namespace
- Move copilot_client.go and session_config.go to internal/agent/copilot/
- Delete entire pkg/llm/ package (azure_openai, ollama, github_copilot, model_factory, manager)
- Move consent commands from azd mcp consent to azd copilot consent
- Streamline error middleware: single consent prompt + agent-driven troubleshooting
- Troubleshooting prompts in embedded Go text templates
- AgentDisplay: render AssistantMessage in real-time, red x for failed tools
- Remove Content from AgentResult, delete dead feedback package
- Adopt SDK bundler for CLI binary embedding, remove npm path scanning
- Clean up CI pipelines: remove ghCopilot build tag and ldflags
- Add WithSystemMessage AgentOption
- Add composable config key constants with ConfigRoot prefix
- Remove langchaingo dependency

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

* feat: on-demand Copilot CLI download, replace SDK bundler

- Add CopilotCLI managed tool (internal/agent/copilot/cli.go) following Bicep pattern
- Download platform-specific CLI from npm registry on first use
- Cache at ~/.azd/bin/copilot-cli-{version}, override via AZD_COPILOT_CLI_PATH
- Implement tools.ExternalTool interface (Name, InstallUrl, CheckInstalled)
- Integrate with CopilotClientManager (resolves CLI at Start time)
- Remove SDK bundler (zcopilot_* files, go tool bundler CI step, tool dep)
- Binary size reduced ~106MB (no longer embedded)
- Fix cspell: add agentcopilot to word list, reword comment

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

* fix: copilot display deadlocks, consent persistence, and UX improvements

- Fix WaitForIdle hang when SessionIdle fires before AssistantMessage
- Fix ticker vs Pause() TOCTOU race causing spinner to render over consent prompts
- Fix consent grant errors silently denying tool execution
- Add error logging for consent rule load/save failures
- Improve tool completion display with contextual verbs and diff stats
- Add tree-style sub-detail for shell commands and MCP tool args
- Add colored diff stats (green +N / red -N) for edit/create tools
- Show plugin version in skill invocation display
- Normalize whitespace spacing via printSeparated for all section transitions
- Show error messages on tool call failures
- Add comprehensive unit tests for display helpers and consent persistence

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

* chore: commit pending branch changes for CI

Includes copilot CLI plugin management (ListPlugins, InstallPlugin),
session time formatting, consent command migration, and azdcontext updates.

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

* fix: move file watcher to background goroutine to prevent WSL hang

On WSL with Windows filesystem mounts (/mnt/c/...), fsnotify's
filepath.Walk + inotify watch setup can hang or be extremely slow.
Moving NewWatcher() to a background goroutine prevents it from
blocking SendMessage. The watcher results are still collected at
cleanup via mutex-protected access.

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

* feat: add auth check with interactive login, fix spinner verb, fix E2E test

- Check GitHub Copilot auth status before session creation
- Prompt to sign in via copilot login (OAuth device flow) if not authenticated
- Add CopilotCLI.Login() wrapper for interactive copilot login command
- Fix spinner showing 'Running Ran tool' — use tool name for spinner, verb for completion
- Fix E2E test: add OnPermissionRequest: ApproveAll to session config
- Add ErrToolExecutionSkipped to excluded errors list in error mapping test
- Add unit tests for Login success and error cases

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jongio pushed a commit to jongio/azure-dev that referenced this pull request Mar 27, 2026
* Add Copilot SDK foundation alongside existing langchaingo agent

Add the GitHub Copilot SDK (github.com/github/copilot-sdk/go) as a new
dependency and create the foundational types for a Copilot SDK-based agent
implementation. All new code coexists alongside the existing langchaingo
agent — no existing code is modified or deleted.

New files:
- pkg/llm/copilot_client.go: CopilotClientManager wrapping copilot.Client
  lifecycle (Start, Stop, GetAuthStatus, ListModels)
- pkg/llm/session_config.go: SessionConfigBuilder that reads ai.agent.*
  config keys and produces copilot.SessionConfig, including MCP server
  merging (built-in + user-configured) and tool control
- internal/agent/copilot_agent.go: CopilotAgent implementing the Agent
  interface backed by copilot.Session with SendAndWait
- internal/agent/copilot_agent_factory.go: CopilotAgentFactory that
  creates CopilotAgent instances with SDK client, session, permission
  hooks, MCP servers, and event handlers
- internal/agent/logging/session_event_handler.go: SessionEventLogger,
  SessionFileLogger, and CompositeEventHandler for SDK SessionEvent
  streaming to UX thought channel and daily log files

Config additions (resources/config_options.yaml):
- ai.agent.model: Default model for Copilot SDK sessions
- ai.agent.mode: Agent mode (autopilot/interactive/plan)
- ai.agent.mcp.servers: Additional MCP servers
- ai.agent.tools.available/excluded: Tool allow/deny lists
- ai.agent.systemMessage: Custom system prompt append
- ai.agent.copilot.logLevel: SDK log level

Resolves Azure#6871, Azure#6872, Azure#6873, Azure#6874, Azure#6875

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

* Add 'copilot' as default agent provider type

Register CopilotProvider as a named 'copilot' model provider in the IoC
container. This makes 'copilot' the default agent type when ai.agent.model.type
is not explicitly configured.

Changes:
- Add LlmTypeCopilot constant and CopilotProvider (copilot_provider.go)
- Default GetDefaultModel() to 'copilot' when no model type is set
- Register 'copilot' provider in container.go
- Update init.go to set 'copilot' instead of 'github-copilot'
- Update error message to list copilot as supported type

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

* Add diagnostic logging to Copilot SDK agent pipeline

Add [copilot] and [copilot-event] prefixed log statements throughout
the Copilot SDK agent pipeline for troubleshooting:

- CopilotClientManager: Start/stop state transitions
- CopilotAgentFactory: MCP server count, session config details,
  PreToolUse/PostToolUse/ErrorOccurred hook invocations
- CopilotAgent.SendMessage: prompt size, response size, errors
- SessionEventLogger: every event type received, plus detail for
  assistant.message, tool.execution_start, and assistant.reasoning

Run with AZD_DEBUG=true to see log output.

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

* Wire CopilotAgentFactory into AgentFactory for automatic delegation

AgentFactory.Create() now checks the configured model type. When it's
'copilot' (the new default), it delegates to CopilotAgentFactory which
creates a CopilotAgent backed by the Copilot SDK session. No call site
changes needed — existing code calling AgentFactory.Create() gets the
Copilot SDK agent automatically.

Changes:
- AgentFactory now takes CopilotAgentFactory as a dependency
- Create() checks model type and delegates to CopilotAgentFactory
- Register CopilotAgentFactory, CopilotClientManager, and
  SessionConfigBuilder in IoC container (cmd/container.go)

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

* Enable SDK debug logging to diagnose CLI process startup failure

Set SDK LogLevel to 'debug' by default to surface the command and args
the SDK uses when spawning the copilot CLI process. This will help
diagnose the 'exit status 1' error during client.Start().

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

* Update copilot-sdk to v0.1.26-preview.0

CLI v0.0.419-0 now supports --headless and --stdio flags required by the
SDK. Updated Go SDK from v0.1.25 to v0.1.26-preview.0 for latest
compatibility.

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

* Auto-discover native Copilot CLI binary from @github/copilot-sdk npm package

The copilot CLI shim in PATH (from @github/copilot npm package) doesn't
support --headless --stdio flags required by the Go SDK. However, the
@github/copilot-sdk npm package bundles a newer native binary at
node_modules/@github/copilot-{platform}/copilot[.exe] that does.

CopilotClientManager now auto-discovers this binary with resolution order:
1. COPILOT_CLI_PATH environment variable
2. Native binary from @github/copilot-sdk npm package (platform-specific)
3. Falls back to 'copilot' in PATH (SDK default)

Also adds a passing e2e test (TestCopilotSDK_E2E) that validates the full
SDK lifecycle: client start → auth check → list models → create session →
send prompt → receive response → cleanup. Pure native binary, no Node.js.

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

* Fix: explicitly allow tools in PreToolUse hook

The empty PreToolUseHookOutput{} was interpreted as deny by the SDK,
blocking all tool calls. Set PermissionDecision to 'allow' explicitly.

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

* Fix: add OnPermissionRequest handler to approve tool permissions

The SDK has two separate permission mechanisms:
1. PreToolUse hooks (lifecycle interception) - already set to 'allow'
2. OnPermissionRequest handler (CLI permission prompts) - was NOT set

Without OnPermissionRequest, the CLI's permission requests go unanswered
and default to deny, blocking all tool calls. Use the SDK's built-in
PermissionHandler.ApproveAll to approve all requests.

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

* Fix: increase SendAndWait timeout to 10 minutes

The SDK defaults to 60s timeout when the context has no deadline,
which is too short for agent init tasks (discovery, IaC generation,
Dockerfile creation, etc.). Set a 10-minute timeout per message.

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

* Use Send + idle event instead of SendAndWait to avoid timeout

Replace SendAndWait (which imposes a 60s default timeout) with Send
(non-blocking) + explicit wait for session.idle event. The agent task
runs until the SDK signals completion or the parent context is cancelled.
No artificial timeout.

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

* Integrate azd consent system and required plugin auto-install

Two major changes to CopilotAgentFactory:

1. Required plugin auto-install:
   - Runs 'copilot plugin install microsoft/GitHub-Copilot-for-Azure:plugin'
     before starting each session (idempotent, non-interactive)
   - Uses the resolved CLI binary path from CopilotClientManager
   - Logs warnings on install failure but doesn't block session creation

2. Wire azd consent system into SDK permission handlers:
   - OnPermissionRequest: delegates to ConsentManager.CheckConsent()
     for CLI-level permission requests. If consent requires prompting,
     uses ConsentChecker to show azd's interactive consent UX with
     scoped persistence (session/project/global).
   - OnPreToolUse: checks ConsentManager before each tool execution.
     If no rule exists, prompts via ConsentChecker.PromptAndGrantConsent()
     which stores the user's choice at their selected scope.
   - Replaces the previous PermissionHandler.ApproveAll with proper
     consent-gated approval flow.

Also:
- CopilotAgentFactory now takes ConsentManager as a dependency
- CopilotClientManager exposes CLIPath() for plugin install commands

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

* Fix mage namespace discovery for dev:install target

The Dev struct needs to be declared as 'type Dev mg.Namespace' for mage
to discover it as a namespace. Also adds github.com/magefile/mage to
go.mod as required by the magefile's mg import.

This fixes 'mage dev:install' and 'mage dev:uninstall' targets.

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

* Fix OTel schema version conflict causing panic on startup

The copilot-sdk pulled in otel core v1.42.0 (schema 1.39.0) but otel/sdk
remained at v1.38.0 (schema 1.37.0). This mismatch caused a panic in
resource.New() at startup. Upgraded all OTel SDK packages to v1.42.0 to
match the core version.

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

* Update copilot-sdk to v0.1.32

Updated from v0.1.26-preview.0 to v0.1.32. Adapted to API change where
PermissionRequest.Kind is now a typed PermissionRequestKind instead of
plain string.

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

* Fix OTel semconv schema mismatch: update to v1.40.0

OTel SDK v1.42.0 uses semconv/v1.40.0 internally for resource.Default(),
but our resource.go imported semconv/v1.39.0. The schema URL mismatch
(1.40.0 vs 1.39.0) caused a panic on resource.Merge(). Updated import
to match the SDK's semconv version.

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

* Fix: approve CLI permission requests, use consent only in PreToolUse

The OnPermissionRequest and OnPreToolUse handlers were conflicting:
OnPermissionRequest was calling CheckToolConsent (which only checks
rules, doesn't prompt) and falling through to 'denied' when no rules
existed. Meanwhile OnPreToolUse correctly called PromptAndGrantConsent.

Fix: OnPermissionRequest now approves all CLI-level permission requests
(file access, shell, URLs). Fine-grained per-tool consent with user
prompting is handled exclusively by the OnPreToolUse hook.

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

* Add AgentDisplay for consolidated Copilot SDK UX rendering

Replace the thought-channel indirection with AgentDisplay — a direct
event-driven UX renderer that subscribes to session.On() and handles
all SDK event types using existing azd UX components.

New: internal/agent/display.go
- AgentDisplay manages Canvas + Spinner + VisualElement per SendMessage
- Handles 15+ event types: turn lifecycle, tool execution with elapsed
  timer, reasoning/thinking display, streaming message deltas, errors,
  warnings, skill invocations, subagent delegation
- WaitForIdle() blocks until session.idle with final content capture
- Thread-safe state management for concurrent event handling

Changes:
- CopilotAgent.SendMessage() creates AgentDisplay per turn, subscribes
  it to session events, and uses WaitForIdle() for completion
- CopilotAgentFactory no longer creates thought channel or
  SessionEventLogger — file logger remains for audit trail
- Export TruncateString from logging package for shared use

Removes:
- Thought channel and WithCopilotThoughtChannel option
- renderThoughts() goroutine (replaced by AgentDisplay)
- SessionEventLogger dependency in factory (UX moved to display)

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

* Fix reasoning display and show relative paths in tool summaries

Two fixes to AgentDisplay:

1. Reasoning: accumulate reasoning_delta chunks into a rolling buffer
   instead of replacing with each delta. Also handle streaming_delta
   events with phase='thinking' for models that emit reasoning that
   way. Canvas.Update() called after each reasoning update for
   immediate display.

2. Paths: extractToolInputSummary now converts absolute paths to
   relative (via filepath.Rel to cwd) for cleaner display. Paths
   that escape cwd are shown absolute.

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

* Show full reasoning in scrolling window with flush on transitions

Reasoning display now works as a scrolling window:
- VisualElement shows last ~5 lines of reasoning below the spinner,
  updating live as delta chunks stream in
- Full reasoning accumulates in a buffer (no truncation)
- On tool start or turn end, the complete reasoning is printed as a
  persistent dimmed block above the canvas, then the buffer resets

Removed latestThought field — reasoning state is now entirely managed
via reasoningBuf. Removed AssistantMessageDelta handler (message deltas
don't need UX rendering — final content comes via AssistantMessage).

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

* UX: add blank line before spinner and change text to 'Working...'

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

* Check if plugin is installed before install/update

ensurePlugins now lists installed plugins first. If a required plugin
is already installed, it runs 'copilot plugin update' instead of a
full install. New plugins get 'copilot plugin install'.

Also restructured requiredPlugins as pluginSpec with Source (install
path) and Name (installed name for update command).

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

* Simplify init to single prompt + wire OnUserInputRequest handler

Two changes:

1. Wire OnUserInputRequest in CopilotAgentFactory:
   Enables the agent's built-in ask_user tool. When the agent asks a
   question, the handler renders it using azd's UX components:
   - Choices → ux.NewSelect() with azd styling
   - Freeform → ux.NewPrompt() for text input
   This lets the agent ask clarifying questions during execution
   (architecture choices, service selection, config options).

2. Simplify initAppWithAgent() from 6-step loop to single prompt:
   Replaces 6 hardcoded steps + inter-step feedback loops + post-
   completion summary aggregation with a single prompt that delegates
   to azure-prepare and azure-validate skills from the Azure plugin.
   The skills handle all orchestration internally and can ask the user
   questions via ask_user when needed.

   Removed: initStep struct, step definitions, collectAndApplyFeedback(),
   postCompletionSummary(), feedback import (~140 lines).

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

* UX: use intent as spinner text, move reasoning above spinner, set interactive mode

Three UX improvements:

1. Spinner text: uses assistant.intent events (short task descriptions
   like 'Analyzing project structure') instead of static 'Working...'
   Truncated to 80 chars to stay concise.

2. Reasoning display: moved above the spinner instead of below. Layout
   is now: blank line → reasoning (last 5 lines, gray) → blank line →
   spinner. More natural reading order.

3. Mode: SendMessage now explicitly sets Mode to 'interactive' so the
   agent asks for approval before executing tools.

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

* Strip markdown from ask_user prompts, render reasoning with markdown, fix skill triggers

Three changes:

1. ask_user: Strip markdown formatting (bold, italic, backticks, headings)
   from question text and choice labels before rendering in azd UX prompts.
   Choices still return the original value to the agent.

2. flushReasoning: Render accumulated reasoning with output.WithMarkdown()
   instead of raw gray text, giving proper formatting for code blocks,
   lists, and other markdown content.

3. init prompt: Use natural trigger phrases ('prepare this application
   for deployment to Azure', 'validate that everything is ready') that
   match the azure-prepare and azure-validate skill description triggers,
   instead of referencing skill names directly.

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

* Support AllowFreeform in ask_user with 'Other' choice option

When the agent sends choices with AllowFreeform=true, append an
'Other (type your own answer)' option to the Select list. If selected,
follow up with a freeform Prompt and return WasFreeform: true.

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

* Explicitly invoke @azure-prepare and @azure-validate skills in init prompt

Use @skill-name syntax and numbered steps to ensure both skills are
invoked in order. Previous natural language triggers may not have
reliably activated the skills.

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

* Specify 'azd' recipe for azure-prepare and azure-validate skills

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

* Store Copilot session files in .azure/copilot relative to cwd

Sets SessionConfig.ConfigDir to .azure/copilot in the current working
directory so session state is project-local instead of global ~/.copilot.

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

* Fix skill discovery: revert ConfigDir, use qualified skill names

ConfigDir override to .azure/copilot broke plugin discovery — the CLI
loads plugins from ConfigDir/installed-plugins/ which was empty. Reverted
to default ~/.copilot so installed plugins (and their skills) are found.
Set WorkingDirectory instead for tool operations.

Updated init prompt to use fully qualified azure:azure-prepare and
azure:azure-validate skill names (plugin:skill-name format).

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

* Pass installed plugins via --plugin-dir for headless mode discovery

The CLI in --headless --stdio mode (used by the SDK) doesn't auto-discover
installed plugins from ~/.copilot/installed-plugins/. This meant skills
from the Azure plugin were never loaded.

Fix: discoverInstalledPluginDirs() scans ~/.copilot/installed-plugins/
for plugin directories (verified by presence of skills/ or .claude-plugin/)
and passes each via --plugin-dir CLIArgs to the SDK client.

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

* Scope plugin discovery to Azure plugin only

Only load the microsoft/GitHub-Copilot-for-Azure plugin via --plugin-dir.
Checks both _direct and marketplace install paths. Other plugins will be
handled separately later.

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

* Use SkillDirectories instead of --plugin-dir for skill loading

The --plugin-dir CLI flag isn't supported by all copilot binary builds.
Instead, pass the Azure plugin's skills directory via SessionConfig.
SkillDirectories, which is sent via JSON-RPC createSession and works
reliably in headless mode.

discoverAzurePluginSkillDirs() finds the skills/ directory from the
installed Azure plugin and adds it to SkillDirectories alongside any
user-configured skill directories.

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

* Persist intent text in spinner instead of resetting to 'Thinking...'

Track lastIntent and reuse it when the spinner resets after turn start
or tool completion. Only falls back to 'Thinking...' if no intent has
been received yet.

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

* Add blank line after spinner

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

* Show relative paths for skill directories in tool summaries

toRelativePath now tries both cwd and ~/.copilot/installed-plugins/
as base directories. Paths under the plugins root (skill files) are
shown relative instead of absolute.

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

* Capture intent from report_intent tool calls for spinner text

The agent uses a report_intent tool (not assistant.intent events) to
signal what it's working on. Extract the intent text from the tool's
arguments and use it as the spinner text. The report_intent tool is
suppressed from UX display (no 'Ran report_intent' completion line).

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

* Show nested subagent tool calls with rich display

Subagent events now show richer UX:
- started: '◆ {DisplayName} — {Description}'
- completed: '✔ {DisplayName} completed' with summary
- failed: '✖ {DisplayName} failed: {error}'
- deselected: resets subagent state

Tool calls inside a subagent are indented with 2 spaces to show
nesting visually:
  ◆ Azure Prepare — Prepare apps for deployment
    ✔ Ran read_file with path: main.go
    ✔ Ran write_file with path: azure.yaml
  ✔ Azure Prepare completed

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

* UX polish: blank lines before skill/subagent, suppress internal tools

- Add blank line before 'Using skill:' and 'Delegating to:' lines
- Suppress tool call display for report_intent, ask_user, task, and
  skill: prefixed tools — these are internal/UX tools that shouldn't
  show as 'Ran X' completion lines

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

* Prompt for reasoning effort and model on first agent run

First run (no ai.agent.reasoningEffort config):
- Prompt for reasoning effort (low/medium/high) with cost guidance
- Prompt for model selection (default or specific model)
- Save both to azd config

Subsequent runs:
- Show info note with current model and reasoning level
- Point to 'azd config set' for changes

Also:
- Wire ai.agent.reasoningEffort to SessionConfig.ReasoningEffort
- Add reasoningEffort to config_options.yaml

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

* Fix: only signal idle when final content exists

Multiple session.idle events can arrive during a session (after
permission prompts, between tool calls). The first idle was consumed
by WaitForIdle before the assistant message arrived, causing the
display to clear with no summary shown.

Fix: only signal idleCh when finalContent has been set by an
assistant.message event. Early idle events are ignored.

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

* Load Azure plugin MCP servers into session config

Read .mcp.json from the installed Azure plugin and merge its MCP servers
(azure, foundry-mcp, context7) into SessionConfig.MCPServers alongside
built-in and user-configured servers.

Merge order: built-in → Azure plugin → user config (last wins).

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

* Suppress 'skill' tool from display, add blank line after Using skill

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

* Fix agent exit: reset finalContent per turn, add completion logging

Root cause: assistant.message from an earlier turn set finalContent,
then a session.idle between tool calls found hasContent=true and
signaled WaitForIdle prematurely — before the actual final message.

Fixes:
- Reset finalContent on every assistant.turn_start so only the last
  turn's message is considered
- Handle session.task_complete and session.shutdown as additional
  completion signals
- Add debug logging to assistant.message, session.idle, and
  WaitForIdle for future troubleshooting

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

* Add scoped system message and empty directory handling

System message (append mode):
- Scopes the agent to Azure application development only
- Rejects unrelated requests with a focused explanation
- User-configured ai.agent.systemMessage appended after default

Init prompt:
- Agent now checks if cwd is empty/has no code first
- If empty, asks user what type of Azure app to build
- Then proceeds with azure-prepare and azure-validate skills

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

* Consistent whitespace and improved config display

Whitespace rules:
- Skills, subagents, reasoning, errors, warnings: blank line before
  and after (via printSeparated, no double blanks)
- Tool completions: stack without blank lines (via printLine)
- lastPrintedBlank flag prevents duplicate blank lines

Config display:
- Split into multiple lines for readability
- Show model and reasoning on separate bullet points
- azd commands shown in highlight format

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

* Add blank lines before/after user prompts, fix extra blank in config display

- User prompts (ask_user): blank line before and after Select/Prompt
- Config display: remove leading blank line (was doubling up with
  prior output), remove trailing newline from alpha warning title

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

* Log MCP server details and skill dirs for debugging

Before session creation, log each configured MCP server (name, type,
command/url) and skill directory. Also capture session.info events
with AllowedTools list and skill.invoked events in the file logger.

Visible with AZD_DEBUG=true or in ~/.azd/logs/azd-agent-*.log.

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

* Print MCP servers and skill dirs to console output for debugging

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

* Print available tools to console from session events

Captures AllowedTools from the first session event that carries them
and prints the full list to console output for debugging.

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

* Fix MCP server config: use type 'local' and add tools wildcard

The SDK expects type='local' (not 'stdio') for local MCP servers,
and requires a 'tools' field listing which tools to expose. Without
tools=['*'], no MCP tools were made available to the agent.

Fixes:
- convertServerConfig: use type='local', add tools=['*']
- loadAzurePluginMCPServers: normalize plugin configs — set type
  to 'local' for command-based servers, add tools=['*'] if missing

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

* Add diagnostic prompt to list all tools including MCP server tools

Temporary: agent prints all available tools grouped by built-in, MCP
server tools, and skills before proceeding with init. Will remove once
MCP server integration is verified working.

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

* Dump first 30 session events with details for MCP debugging

Temporary: prints event type + key fields (allowedTools, tools, message,
infoType, name) for the first 30 events to diagnose MCP server loading.

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

* Remove diagnostic prompt, rely on event dump for tool debugging

The system message was causing the model to ignore the diagnostic
'list tools' request. Event dump from the factory provides better
programmatic visibility.

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

* Debugging: disable system message, add diagnostic tools list prompt, remove event dump

Temporary changes to diagnose MCP server tool availability:
- Comment out system message (was blocking diagnostic requests)
- Add explicit 'list all tools' prompt before init task
- Remove event dump from factory (wasn't showing useful data)

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

* Add post-init Q&A loop for follow-up questions

After the init summary, prompt the user 'Any questions?' in a loop.
User can ask follow-up questions (sent to the same agent session with
full context). Press Enter with no input to finish and exit.

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

* Add session resume support for cancelled/crashed agent sessions

On azd init with agent mode, checks for previous sessions in the
current directory via client.ListSessions(). If found, prompts user
to resume a previous session or start fresh.

Resume uses client.ResumeSession() which restores full conversation
history with the same MCP servers, skills, permissions, and hooks.

Changes:
- CopilotAgentFactory: add ListSessions() and Resume() methods
- init.go: add session picker before agent creation, add
  CopilotAgentFactory to initAction struct

Spec at docs/specs/copilot-agent-ux/session-resume.md

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

* Polish session resume UX: numbers, local time, truncated labels

- Show numbered choices in session picker (DisplayNumbers: true)
- Convert timestamps to local time (Today 3:04 PM, Yesterday, Jan 2)
- Truncate labels to ~120 chars total
- Shorter prompt: 'Previous sessions found:'

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

* Fix session labels: collapse newlines, enforce 120 char max

Summaries from sessions can contain newlines and markdown. Use
strings.Fields() to collapse all whitespace into single spaces,
then truncate to fit within 120 chars total.

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

* Enable line numbers and filtering on all Select prompts

Added DisplayNumbers and EnableFiltering to reasoning effort, model
selection, and session picker prompts.

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

* Dynamic model list from SDK with billing and reasoning metadata

Fetch models via ListModels() instead of hardcoding. Each option shows:
  'Claude Sonnet 4.5 (high) (1x)'
  — name, default reasoning effort, billing multiplier

Also:
- Remove trailing ':' from prompt messages (UX components add them)
- Add ListModels() to CopilotAgentFactory

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

* Show session usage metrics at end of init

Accumulate usage from assistant.usage and session.usage_info events:
input/output tokens, cost multiplier, premium requests, API duration,
and model used.

Display at session end:
  Session usage:
  • Model:            claude-sonnet-4.5
  • Input tokens:     45.2K
  • Output tokens:    12.8K
  • Total tokens:     58.0K
  • Cost:             1.0x premium
  • Premium requests: 15
  • API duration:     2m 34s

Token counts formatted as K/M for readability.

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

* Remove post-init Q&A loop

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

* Consolidate agent into self-contained CopilotAgent with factory

Major refactor: CopilotAgent is now a self-contained agent that
encapsulates initialization, session management, display, and usage.
CopilotAgentFactory creates agents with dependencies wired via IoC.

New API:
  agent, _ := factory.Create(ctx, agent.WithMode('interactive'))
  initResult, _ := agent.Initialize(ctx)
  selected, _ := agent.SelectSession(ctx)
  result, _ := agent.SendMessage(ctx, prompt, agent.WithSessionID(...))
  // result.Content, result.SessionID, result.Usage
  agent.Stop()

New types (types.go):
  AgentResult{Content, SessionID, Usage}
  InitResult{Model, ReasoningEffort, IsFirstRun}
  UsageMetrics with Format() method
  AgentOption: WithModel, WithReasoningEffort, WithMode, WithDebug
  SendOption: WithSessionID
  InitOption: WithForcePrompt

Agent methods:
  Initialize() — config prompts (first run), plugin install, client start
  SelectSession() — UX picker for session resume
  ListSessions() — raw session listing
  SendMessage() / SendMessageWithRetry() — returns AgentResult
  Stop() — cleanup

Deleted (old langchaingo agent):
  agent.go, agent_factory.go, conversational_agent.go, prompts/

Simplified:
  init.go — ~40 lines instead of ~150
  container.go — removed old AgentFactory, ModelFactory registrations
  error.go — updated to use CopilotAgentFactory

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

* Fix spacing: add blank line after each prompt, remove leading blanks

Each prompt component (Select, Prompt) now adds a blank line after
its Ask() call. Callers manage spacing before prompts. Removed the
leading blank line from SelectSession (was doubling up with the
trailing blank from the previous prompt).

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

* Add unit tests for types.go and display.go pure functions

New test files:
- types_test.go: UsageMetrics.Format(), TotalTokens(), formatTokenCount,
  stripMarkdown, formatSessionTime (40 test cases)
- display_test.go: extractToolInputSummary, extractIntentFromArgs,
  toRelativePath, GetUsageMetrics accumulation

All pure functions tested without SDK mocking.

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

* Fix billing display and CI checks

- Rename Cost to BillingRate (per-request multiplier, not cumulative)
- Show premium requests from SDK (omit if not reported)
- Handle session.shutdown for TotalPremiumRequests
- Remove manual API call counter
- Fix all lint issues (errorlint, gosec, staticcheck, unused)
- Fix formatting, remove unused code (github_copilot_registration files,
  discoverInstalledPluginDirs)
- All CI checks pass: gofmt, golangci-lint, tests

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

* Address PR review feedback

- Fix config_options.yaml: tools.available/excluded type 'object' -> 'array'
- Add missing config docs: ai.agent.skills.directories, ai.agent.skills.disabled
- Log warning on userConfigManager.Load() failure instead of silently swallowing
- Simplify redundant MCP server unmarshaling (removed type probe, single path)

Other review items (r3, r6, r8, r9) referenced old code that was deleted
in the agent consolidation commit.

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

* Address second round PR review feedback

Bug fixes:
- Fix nil pointer dereferences in error.go when agentResult is nil
- Fix session.error not unblocking WaitForIdle (signal idleCh on error)
- Fix consent check fails-open: deny on error instead of allow

Security:
- PreToolUse consent check now denies on error with logged reason
- OnPermissionRequest remains approve-all (CLI-level coarse permissions,
  fine-grained control via PreToolUse hooks)

Code quality:
- Deterministic cleanup order: changed from map to ordered slice with
  reverse teardown (session events -> file logger -> client)
- Log warning on config load failure
- Simplified MCP server unmarshaling

Config:
- Added skills.directories and skills.disabled to config_options.yaml
- Fixed tools.available/excluded type from object to array

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

* Add AgentMode enum, remove redundant logging infrastructure

AgentMode:
- New AgentMode type with constants: AgentModeInteractive, AgentModeAutopilot,
  AgentModePlan. WithMode() now takes AgentMode instead of string.

Removed logging (redundant with Copilot CLI logs at ~/.copilot/logs/):
- thought_logger.go — old langchaingo callback handler
- file_logger.go — old langchaingo callback handler
- chained_handler.go — old langchaingo callback handler
- session_event_handler.go — SessionEventLogger, SessionFileLogger,
  CompositeEventHandler all unused after AgentDisplay consolidation
- session_event_handler_test.go

Kept: logging/util.go with TruncateString (used by display.go)

Net: 902 lines deleted.

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

* Fix: return originalError when user declines fix in error middleware

When the user declines the agent fix, the code returned 'err' which
was nil (consent check succeeded), silently swallowing the original
command failure. Now returns originalError to preserve the error.

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

* Document permission handler intent and relationship to PreToolUse

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

* Remove docs/specs/copilot-agent-ux (deleted by reviewer)

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

* Delete dead agent tools and unused consent wrapper

Deleted (all replaced by Copilot CLI built-in tools):
- tools/dev/ — command executor (shell tool)
- tools/io/ — 12 file/directory tools + tests
- tools/common/ — AnnotatedTool interface, ToLangChainTools, ToolLoader
- tools/loader.go — composite tool loader
- tools/mcp/tool_adapter.go — MCP-to-langchaingo adapter
- tools/mcp/sampling_handler.go — MCP sampling handler
- tools/mcp/elicitation_handler.go — MCP elicitation handler
- tools/mcp/loader.go — MCP tool loader
- consent/consent_wrapper_tool.go — langchaingo tool wrapper

Cleaned:
- Removed WrapTool/WrapTools from ConsentManager interface and impl
- Removed common package import from consent
- Kept tools/mcp/embed.go with McpJson embed (still used by factory)

Net: 7,025 lines deleted.

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

* Remove unused ExecutingTool and related global state from consent

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

* Fix MCP functional tests for remaining tools

Updated test expectations after MCP tool migration to Copilot SDK skills.
Tests now verify error_troubleshooting, provision_common_error, and
validate_azure_yaml (the 3 remaining MCP tools).

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

* Fix init.go: remove stray backtick, highlight azd up command

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

* Apply go fix for Go 1.26 compatibility

- intPtr() -> new() for pointer creation
- strings.Split -> strings.SplitSeq for range iteration
- strings.HasPrefix+TrimPrefix -> strings.CutPrefix
- floatPtr/strPtr helpers replaced with new() in tests

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

* Remove unused intPtr, floatPtr, strPtr helpers (inlined by go fix)

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

* refactor: migrate to copilot.* namespace, delete pkg/llm, streamline error middleware

- Migrate all config keys from ai.agent.* to copilot.* namespace
- Move copilot_client.go and session_config.go to internal/agent/copilot/
- Delete entire pkg/llm/ package (azure_openai, ollama, github_copilot, model_factory, manager)
- Move consent commands from azd mcp consent to azd copilot consent
- Streamline error middleware: single consent prompt + agent-driven troubleshooting
- Troubleshooting prompts in embedded Go text templates
- AgentDisplay: render AssistantMessage in real-time, red x for failed tools
- Remove Content from AgentResult, delete dead feedback package
- Adopt SDK bundler for CLI binary embedding, remove npm path scanning
- Clean up CI pipelines: remove ghCopilot build tag and ldflags
- Add WithSystemMessage AgentOption
- Add composable config key constants with ConfigRoot prefix
- Remove langchaingo dependency

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

* feat: on-demand Copilot CLI download, replace SDK bundler

- Add CopilotCLI managed tool (internal/agent/copilot/cli.go) following Bicep pattern
- Download platform-specific CLI from npm registry on first use
- Cache at ~/.azd/bin/copilot-cli-{version}, override via AZD_COPILOT_CLI_PATH
- Implement tools.ExternalTool interface (Name, InstallUrl, CheckInstalled)
- Integrate with CopilotClientManager (resolves CLI at Start time)
- Remove SDK bundler (zcopilot_* files, go tool bundler CI step, tool dep)
- Binary size reduced ~106MB (no longer embedded)
- Fix cspell: add agentcopilot to word list, reword comment

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

* fix: copilot display deadlocks, consent persistence, and UX improvements

- Fix WaitForIdle hang when SessionIdle fires before AssistantMessage
- Fix ticker vs Pause() TOCTOU race causing spinner to render over consent prompts
- Fix consent grant errors silently denying tool execution
- Add error logging for consent rule load/save failures
- Improve tool completion display with contextual verbs and diff stats
- Add tree-style sub-detail for shell commands and MCP tool args
- Add colored diff stats (green +N / red -N) for edit/create tools
- Show plugin version in skill invocation display
- Normalize whitespace spacing via printSeparated for all section transitions
- Show error messages on tool call failures
- Add comprehensive unit tests for display helpers and consent persistence

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

* chore: commit pending branch changes for CI

Includes copilot CLI plugin management (ListPlugins, InstallPlugin),
session time formatting, consent command migration, and azdcontext updates.

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

* fix: move file watcher to background goroutine to prevent WSL hang

On WSL with Windows filesystem mounts (/mnt/c/...), fsnotify's
filepath.Walk + inotify watch setup can hang or be extremely slow.
Moving NewWatcher() to a background goroutine prevents it from
blocking SendMessage. The watcher results are still collected at
cleanup via mutex-protected access.

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

* feat: add auth check with interactive login, fix spinner verb, fix E2E test

- Check GitHub Copilot auth status before session creation
- Prompt to sign in via copilot login (OAuth device flow) if not authenticated
- Add CopilotCLI.Login() wrapper for interactive copilot login command
- Fix spinner showing 'Running Ran tool' — use tool name for spinner, verb for completion
- Fix E2E test: add OnPermissionRequest: ApproveAll to session config
- Add ErrToolExecutionSkipped to excluded errors list in error mapping test
- Add unit tests for Login success and error cases

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jongio pushed a commit to jongio/azure-dev that referenced this pull request Mar 27, 2026
* Add Copilot SDK foundation alongside existing langchaingo agent

Add the GitHub Copilot SDK (github.com/github/copilot-sdk/go) as a new
dependency and create the foundational types for a Copilot SDK-based agent
implementation. All new code coexists alongside the existing langchaingo
agent — no existing code is modified or deleted.

New files:
- pkg/llm/copilot_client.go: CopilotClientManager wrapping copilot.Client
  lifecycle (Start, Stop, GetAuthStatus, ListModels)
- pkg/llm/session_config.go: SessionConfigBuilder that reads ai.agent.*
  config keys and produces copilot.SessionConfig, including MCP server
  merging (built-in + user-configured) and tool control
- internal/agent/copilot_agent.go: CopilotAgent implementing the Agent
  interface backed by copilot.Session with SendAndWait
- internal/agent/copilot_agent_factory.go: CopilotAgentFactory that
  creates CopilotAgent instances with SDK client, session, permission
  hooks, MCP servers, and event handlers
- internal/agent/logging/session_event_handler.go: SessionEventLogger,
  SessionFileLogger, and CompositeEventHandler for SDK SessionEvent
  streaming to UX thought channel and daily log files

Config additions (resources/config_options.yaml):
- ai.agent.model: Default model for Copilot SDK sessions
- ai.agent.mode: Agent mode (autopilot/interactive/plan)
- ai.agent.mcp.servers: Additional MCP servers
- ai.agent.tools.available/excluded: Tool allow/deny lists
- ai.agent.systemMessage: Custom system prompt append
- ai.agent.copilot.logLevel: SDK log level

Resolves Azure#6871, Azure#6872, Azure#6873, Azure#6874, Azure#6875

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

* Add 'copilot' as default agent provider type

Register CopilotProvider as a named 'copilot' model provider in the IoC
container. This makes 'copilot' the default agent type when ai.agent.model.type
is not explicitly configured.

Changes:
- Add LlmTypeCopilot constant and CopilotProvider (copilot_provider.go)
- Default GetDefaultModel() to 'copilot' when no model type is set
- Register 'copilot' provider in container.go
- Update init.go to set 'copilot' instead of 'github-copilot'
- Update error message to list copilot as supported type

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

* Add diagnostic logging to Copilot SDK agent pipeline

Add [copilot] and [copilot-event] prefixed log statements throughout
the Copilot SDK agent pipeline for troubleshooting:

- CopilotClientManager: Start/stop state transitions
- CopilotAgentFactory: MCP server count, session config details,
  PreToolUse/PostToolUse/ErrorOccurred hook invocations
- CopilotAgent.SendMessage: prompt size, response size, errors
- SessionEventLogger: every event type received, plus detail for
  assistant.message, tool.execution_start, and assistant.reasoning

Run with AZD_DEBUG=true to see log output.

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

* Wire CopilotAgentFactory into AgentFactory for automatic delegation

AgentFactory.Create() now checks the configured model type. When it's
'copilot' (the new default), it delegates to CopilotAgentFactory which
creates a CopilotAgent backed by the Copilot SDK session. No call site
changes needed — existing code calling AgentFactory.Create() gets the
Copilot SDK agent automatically.

Changes:
- AgentFactory now takes CopilotAgentFactory as a dependency
- Create() checks model type and delegates to CopilotAgentFactory
- Register CopilotAgentFactory, CopilotClientManager, and
  SessionConfigBuilder in IoC container (cmd/container.go)

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

* Enable SDK debug logging to diagnose CLI process startup failure

Set SDK LogLevel to 'debug' by default to surface the command and args
the SDK uses when spawning the copilot CLI process. This will help
diagnose the 'exit status 1' error during client.Start().

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

* Update copilot-sdk to v0.1.26-preview.0

CLI v0.0.419-0 now supports --headless and --stdio flags required by the
SDK. Updated Go SDK from v0.1.25 to v0.1.26-preview.0 for latest
compatibility.

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

* Auto-discover native Copilot CLI binary from @github/copilot-sdk npm package

The copilot CLI shim in PATH (from @github/copilot npm package) doesn't
support --headless --stdio flags required by the Go SDK. However, the
@github/copilot-sdk npm package bundles a newer native binary at
node_modules/@github/copilot-{platform}/copilot[.exe] that does.

CopilotClientManager now auto-discovers this binary with resolution order:
1. COPILOT_CLI_PATH environment variable
2. Native binary from @github/copilot-sdk npm package (platform-specific)
3. Falls back to 'copilot' in PATH (SDK default)

Also adds a passing e2e test (TestCopilotSDK_E2E) that validates the full
SDK lifecycle: client start → auth check → list models → create session →
send prompt → receive response → cleanup. Pure native binary, no Node.js.

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

* Fix: explicitly allow tools in PreToolUse hook

The empty PreToolUseHookOutput{} was interpreted as deny by the SDK,
blocking all tool calls. Set PermissionDecision to 'allow' explicitly.

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

* Fix: add OnPermissionRequest handler to approve tool permissions

The SDK has two separate permission mechanisms:
1. PreToolUse hooks (lifecycle interception) - already set to 'allow'
2. OnPermissionRequest handler (CLI permission prompts) - was NOT set

Without OnPermissionRequest, the CLI's permission requests go unanswered
and default to deny, blocking all tool calls. Use the SDK's built-in
PermissionHandler.ApproveAll to approve all requests.

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

* Fix: increase SendAndWait timeout to 10 minutes

The SDK defaults to 60s timeout when the context has no deadline,
which is too short for agent init tasks (discovery, IaC generation,
Dockerfile creation, etc.). Set a 10-minute timeout per message.

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

* Use Send + idle event instead of SendAndWait to avoid timeout

Replace SendAndWait (which imposes a 60s default timeout) with Send
(non-blocking) + explicit wait for session.idle event. The agent task
runs until the SDK signals completion or the parent context is cancelled.
No artificial timeout.

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

* Integrate azd consent system and required plugin auto-install

Two major changes to CopilotAgentFactory:

1. Required plugin auto-install:
   - Runs 'copilot plugin install microsoft/GitHub-Copilot-for-Azure:plugin'
     before starting each session (idempotent, non-interactive)
   - Uses the resolved CLI binary path from CopilotClientManager
   - Logs warnings on install failure but doesn't block session creation

2. Wire azd consent system into SDK permission handlers:
   - OnPermissionRequest: delegates to ConsentManager.CheckConsent()
     for CLI-level permission requests. If consent requires prompting,
     uses ConsentChecker to show azd's interactive consent UX with
     scoped persistence (session/project/global).
   - OnPreToolUse: checks ConsentManager before each tool execution.
     If no rule exists, prompts via ConsentChecker.PromptAndGrantConsent()
     which stores the user's choice at their selected scope.
   - Replaces the previous PermissionHandler.ApproveAll with proper
     consent-gated approval flow.

Also:
- CopilotAgentFactory now takes ConsentManager as a dependency
- CopilotClientManager exposes CLIPath() for plugin install commands

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

* Fix mage namespace discovery for dev:install target

The Dev struct needs to be declared as 'type Dev mg.Namespace' for mage
to discover it as a namespace. Also adds github.com/magefile/mage to
go.mod as required by the magefile's mg import.

This fixes 'mage dev:install' and 'mage dev:uninstall' targets.

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

* Fix OTel schema version conflict causing panic on startup

The copilot-sdk pulled in otel core v1.42.0 (schema 1.39.0) but otel/sdk
remained at v1.38.0 (schema 1.37.0). This mismatch caused a panic in
resource.New() at startup. Upgraded all OTel SDK packages to v1.42.0 to
match the core version.

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

* Update copilot-sdk to v0.1.32

Updated from v0.1.26-preview.0 to v0.1.32. Adapted to API change where
PermissionRequest.Kind is now a typed PermissionRequestKind instead of
plain string.

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

* Fix OTel semconv schema mismatch: update to v1.40.0

OTel SDK v1.42.0 uses semconv/v1.40.0 internally for resource.Default(),
but our resource.go imported semconv/v1.39.0. The schema URL mismatch
(1.40.0 vs 1.39.0) caused a panic on resource.Merge(). Updated import
to match the SDK's semconv version.

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

* Fix: approve CLI permission requests, use consent only in PreToolUse

The OnPermissionRequest and OnPreToolUse handlers were conflicting:
OnPermissionRequest was calling CheckToolConsent (which only checks
rules, doesn't prompt) and falling through to 'denied' when no rules
existed. Meanwhile OnPreToolUse correctly called PromptAndGrantConsent.

Fix: OnPermissionRequest now approves all CLI-level permission requests
(file access, shell, URLs). Fine-grained per-tool consent with user
prompting is handled exclusively by the OnPreToolUse hook.

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

* Add AgentDisplay for consolidated Copilot SDK UX rendering

Replace the thought-channel indirection with AgentDisplay — a direct
event-driven UX renderer that subscribes to session.On() and handles
all SDK event types using existing azd UX components.

New: internal/agent/display.go
- AgentDisplay manages Canvas + Spinner + VisualElement per SendMessage
- Handles 15+ event types: turn lifecycle, tool execution with elapsed
  timer, reasoning/thinking display, streaming message deltas, errors,
  warnings, skill invocations, subagent delegation
- WaitForIdle() blocks until session.idle with final content capture
- Thread-safe state management for concurrent event handling

Changes:
- CopilotAgent.SendMessage() creates AgentDisplay per turn, subscribes
  it to session events, and uses WaitForIdle() for completion
- CopilotAgentFactory no longer creates thought channel or
  SessionEventLogger — file logger remains for audit trail
- Export TruncateString from logging package for shared use

Removes:
- Thought channel and WithCopilotThoughtChannel option
- renderThoughts() goroutine (replaced by AgentDisplay)
- SessionEventLogger dependency in factory (UX moved to display)

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

* Fix reasoning display and show relative paths in tool summaries

Two fixes to AgentDisplay:

1. Reasoning: accumulate reasoning_delta chunks into a rolling buffer
   instead of replacing with each delta. Also handle streaming_delta
   events with phase='thinking' for models that emit reasoning that
   way. Canvas.Update() called after each reasoning update for
   immediate display.

2. Paths: extractToolInputSummary now converts absolute paths to
   relative (via filepath.Rel to cwd) for cleaner display. Paths
   that escape cwd are shown absolute.

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

* Show full reasoning in scrolling window with flush on transitions

Reasoning display now works as a scrolling window:
- VisualElement shows last ~5 lines of reasoning below the spinner,
  updating live as delta chunks stream in
- Full reasoning accumulates in a buffer (no truncation)
- On tool start or turn end, the complete reasoning is printed as a
  persistent dimmed block above the canvas, then the buffer resets

Removed latestThought field — reasoning state is now entirely managed
via reasoningBuf. Removed AssistantMessageDelta handler (message deltas
don't need UX rendering — final content comes via AssistantMessage).

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

* UX: add blank line before spinner and change text to 'Working...'

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

* Check if plugin is installed before install/update

ensurePlugins now lists installed plugins first. If a required plugin
is already installed, it runs 'copilot plugin update' instead of a
full install. New plugins get 'copilot plugin install'.

Also restructured requiredPlugins as pluginSpec with Source (install
path) and Name (installed name for update command).

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

* Simplify init to single prompt + wire OnUserInputRequest handler

Two changes:

1. Wire OnUserInputRequest in CopilotAgentFactory:
   Enables the agent's built-in ask_user tool. When the agent asks a
   question, the handler renders it using azd's UX components:
   - Choices → ux.NewSelect() with azd styling
   - Freeform → ux.NewPrompt() for text input
   This lets the agent ask clarifying questions during execution
   (architecture choices, service selection, config options).

2. Simplify initAppWithAgent() from 6-step loop to single prompt:
   Replaces 6 hardcoded steps + inter-step feedback loops + post-
   completion summary aggregation with a single prompt that delegates
   to azure-prepare and azure-validate skills from the Azure plugin.
   The skills handle all orchestration internally and can ask the user
   questions via ask_user when needed.

   Removed: initStep struct, step definitions, collectAndApplyFeedback(),
   postCompletionSummary(), feedback import (~140 lines).

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

* UX: use intent as spinner text, move reasoning above spinner, set interactive mode

Three UX improvements:

1. Spinner text: uses assistant.intent events (short task descriptions
   like 'Analyzing project structure') instead of static 'Working...'
   Truncated to 80 chars to stay concise.

2. Reasoning display: moved above the spinner instead of below. Layout
   is now: blank line → reasoning (last 5 lines, gray) → blank line →
   spinner. More natural reading order.

3. Mode: SendMessage now explicitly sets Mode to 'interactive' so the
   agent asks for approval before executing tools.

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

* Strip markdown from ask_user prompts, render reasoning with markdown, fix skill triggers

Three changes:

1. ask_user: Strip markdown formatting (bold, italic, backticks, headings)
   from question text and choice labels before rendering in azd UX prompts.
   Choices still return the original value to the agent.

2. flushReasoning: Render accumulated reasoning with output.WithMarkdown()
   instead of raw gray text, giving proper formatting for code blocks,
   lists, and other markdown content.

3. init prompt: Use natural trigger phrases ('prepare this application
   for deployment to Azure', 'validate that everything is ready') that
   match the azure-prepare and azure-validate skill description triggers,
   instead of referencing skill names directly.

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

* Support AllowFreeform in ask_user with 'Other' choice option

When the agent sends choices with AllowFreeform=true, append an
'Other (type your own answer)' option to the Select list. If selected,
follow up with a freeform Prompt and return WasFreeform: true.

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

* Explicitly invoke @azure-prepare and @azure-validate skills in init prompt

Use @skill-name syntax and numbered steps to ensure both skills are
invoked in order. Previous natural language triggers may not have
reliably activated the skills.

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

* Specify 'azd' recipe for azure-prepare and azure-validate skills

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

* Store Copilot session files in .azure/copilot relative to cwd

Sets SessionConfig.ConfigDir to .azure/copilot in the current working
directory so session state is project-local instead of global ~/.copilot.

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

* Fix skill discovery: revert ConfigDir, use qualified skill names

ConfigDir override to .azure/copilot broke plugin discovery — the CLI
loads plugins from ConfigDir/installed-plugins/ which was empty. Reverted
to default ~/.copilot so installed plugins (and their skills) are found.
Set WorkingDirectory instead for tool operations.

Updated init prompt to use fully qualified azure:azure-prepare and
azure:azure-validate skill names (plugin:skill-name format).

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

* Pass installed plugins via --plugin-dir for headless mode discovery

The CLI in --headless --stdio mode (used by the SDK) doesn't auto-discover
installed plugins from ~/.copilot/installed-plugins/. This meant skills
from the Azure plugin were never loaded.

Fix: discoverInstalledPluginDirs() scans ~/.copilot/installed-plugins/
for plugin directories (verified by presence of skills/ or .claude-plugin/)
and passes each via --plugin-dir CLIArgs to the SDK client.

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

* Scope plugin discovery to Azure plugin only

Only load the microsoft/GitHub-Copilot-for-Azure plugin via --plugin-dir.
Checks both _direct and marketplace install paths. Other plugins will be
handled separately later.

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

* Use SkillDirectories instead of --plugin-dir for skill loading

The --plugin-dir CLI flag isn't supported by all copilot binary builds.
Instead, pass the Azure plugin's skills directory via SessionConfig.
SkillDirectories, which is sent via JSON-RPC createSession and works
reliably in headless mode.

discoverAzurePluginSkillDirs() finds the skills/ directory from the
installed Azure plugin and adds it to SkillDirectories alongside any
user-configured skill directories.

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

* Persist intent text in spinner instead of resetting to 'Thinking...'

Track lastIntent and reuse it when the spinner resets after turn start
or tool completion. Only falls back to 'Thinking...' if no intent has
been received yet.

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

* Add blank line after spinner

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

* Show relative paths for skill directories in tool summaries

toRelativePath now tries both cwd and ~/.copilot/installed-plugins/
as base directories. Paths under the plugins root (skill files) are
shown relative instead of absolute.

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

* Capture intent from report_intent tool calls for spinner text

The agent uses a report_intent tool (not assistant.intent events) to
signal what it's working on. Extract the intent text from the tool's
arguments and use it as the spinner text. The report_intent tool is
suppressed from UX display (no 'Ran report_intent' completion line).

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

* Show nested subagent tool calls with rich display

Subagent events now show richer UX:
- started: '◆ {DisplayName} — {Description}'
- completed: '✔ {DisplayName} completed' with summary
- failed: '✖ {DisplayName} failed: {error}'
- deselected: resets subagent state

Tool calls inside a subagent are indented with 2 spaces to show
nesting visually:
  ◆ Azure Prepare — Prepare apps for deployment
    ✔ Ran read_file with path: main.go
    ✔ Ran write_file with path: azure.yaml
  ✔ Azure Prepare completed

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

* UX polish: blank lines before skill/subagent, suppress internal tools

- Add blank line before 'Using skill:' and 'Delegating to:' lines
- Suppress tool call display for report_intent, ask_user, task, and
  skill: prefixed tools — these are internal/UX tools that shouldn't
  show as 'Ran X' completion lines

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

* Prompt for reasoning effort and model on first agent run

First run (no ai.agent.reasoningEffort config):
- Prompt for reasoning effort (low/medium/high) with cost guidance
- Prompt for model selection (default or specific model)
- Save both to azd config

Subsequent runs:
- Show info note with current model and reasoning level
- Point to 'azd config set' for changes

Also:
- Wire ai.agent.reasoningEffort to SessionConfig.ReasoningEffort
- Add reasoningEffort to config_options.yaml

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

* Fix: only signal idle when final content exists

Multiple session.idle events can arrive during a session (after
permission prompts, between tool calls). The first idle was consumed
by WaitForIdle before the assistant message arrived, causing the
display to clear with no summary shown.

Fix: only signal idleCh when finalContent has been set by an
assistant.message event. Early idle events are ignored.

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

* Load Azure plugin MCP servers into session config

Read .mcp.json from the installed Azure plugin and merge its MCP servers
(azure, foundry-mcp, context7) into SessionConfig.MCPServers alongside
built-in and user-configured servers.

Merge order: built-in → Azure plugin → user config (last wins).

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

* Suppress 'skill' tool from display, add blank line after Using skill

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

* Fix agent exit: reset finalContent per turn, add completion logging

Root cause: assistant.message from an earlier turn set finalContent,
then a session.idle between tool calls found hasContent=true and
signaled WaitForIdle prematurely — before the actual final message.

Fixes:
- Reset finalContent on every assistant.turn_start so only the last
  turn's message is considered
- Handle session.task_complete and session.shutdown as additional
  completion signals
- Add debug logging to assistant.message, session.idle, and
  WaitForIdle for future troubleshooting

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

* Add scoped system message and empty directory handling

System message (append mode):
- Scopes the agent to Azure application development only
- Rejects unrelated requests with a focused explanation
- User-configured ai.agent.systemMessage appended after default

Init prompt:
- Agent now checks if cwd is empty/has no code first
- If empty, asks user what type of Azure app to build
- Then proceeds with azure-prepare and azure-validate skills

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

* Consistent whitespace and improved config display

Whitespace rules:
- Skills, subagents, reasoning, errors, warnings: blank line before
  and after (via printSeparated, no double blanks)
- Tool completions: stack without blank lines (via printLine)
- lastPrintedBlank flag prevents duplicate blank lines

Config display:
- Split into multiple lines for readability
- Show model and reasoning on separate bullet points
- azd commands shown in highlight format

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

* Add blank lines before/after user prompts, fix extra blank in config display

- User prompts (ask_user): blank line before and after Select/Prompt
- Config display: remove leading blank line (was doubling up with
  prior output), remove trailing newline from alpha warning title

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

* Log MCP server details and skill dirs for debugging

Before session creation, log each configured MCP server (name, type,
command/url) and skill directory. Also capture session.info events
with AllowedTools list and skill.invoked events in the file logger.

Visible with AZD_DEBUG=true or in ~/.azd/logs/azd-agent-*.log.

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

* Print MCP servers and skill dirs to console output for debugging

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

* Print available tools to console from session events

Captures AllowedTools from the first session event that carries them
and prints the full list to console output for debugging.

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

* Fix MCP server config: use type 'local' and add tools wildcard

The SDK expects type='local' (not 'stdio') for local MCP servers,
and requires a 'tools' field listing which tools to expose. Without
tools=['*'], no MCP tools were made available to the agent.

Fixes:
- convertServerConfig: use type='local', add tools=['*']
- loadAzurePluginMCPServers: normalize plugin configs — set type
  to 'local' for command-based servers, add tools=['*'] if missing

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

* Add diagnostic prompt to list all tools including MCP server tools

Temporary: agent prints all available tools grouped by built-in, MCP
server tools, and skills before proceeding with init. Will remove once
MCP server integration is verified working.

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

* Dump first 30 session events with details for MCP debugging

Temporary: prints event type + key fields (allowedTools, tools, message,
infoType, name) for the first 30 events to diagnose MCP server loading.

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

* Remove diagnostic prompt, rely on event dump for tool debugging

The system message was causing the model to ignore the diagnostic
'list tools' request. Event dump from the factory provides better
programmatic visibility.

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

* Debugging: disable system message, add diagnostic tools list prompt, remove event dump

Temporary changes to diagnose MCP server tool availability:
- Comment out system message (was blocking diagnostic requests)
- Add explicit 'list all tools' prompt before init task
- Remove event dump from factory (wasn't showing useful data)

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

* Add post-init Q&A loop for follow-up questions

After the init summary, prompt the user 'Any questions?' in a loop.
User can ask follow-up questions (sent to the same agent session with
full context). Press Enter with no input to finish and exit.

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

* Add session resume support for cancelled/crashed agent sessions

On azd init with agent mode, checks for previous sessions in the
current directory via client.ListSessions(). If found, prompts user
to resume a previous session or start fresh.

Resume uses client.ResumeSession() which restores full conversation
history with the same MCP servers, skills, permissions, and hooks.

Changes:
- CopilotAgentFactory: add ListSessions() and Resume() methods
- init.go: add session picker before agent creation, add
  CopilotAgentFactory to initAction struct

Spec at docs/specs/copilot-agent-ux/session-resume.md

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

* Polish session resume UX: numbers, local time, truncated labels

- Show numbered choices in session picker (DisplayNumbers: true)
- Convert timestamps to local time (Today 3:04 PM, Yesterday, Jan 2)
- Truncate labels to ~120 chars total
- Shorter prompt: 'Previous sessions found:'

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

* Fix session labels: collapse newlines, enforce 120 char max

Summaries from sessions can contain newlines and markdown. Use
strings.Fields() to collapse all whitespace into single spaces,
then truncate to fit within 120 chars total.

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

* Enable line numbers and filtering on all Select prompts

Added DisplayNumbers and EnableFiltering to reasoning effort, model
selection, and session picker prompts.

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

* Dynamic model list from SDK with billing and reasoning metadata

Fetch models via ListModels() instead of hardcoding. Each option shows:
  'Claude Sonnet 4.5 (high) (1x)'
  — name, default reasoning effort, billing multiplier

Also:
- Remove trailing ':' from prompt messages (UX components add them)
- Add ListModels() to CopilotAgentFactory

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

* Show session usage metrics at end of init

Accumulate usage from assistant.usage and session.usage_info events:
input/output tokens, cost multiplier, premium requests, API duration,
and model used.

Display at session end:
  Session usage:
  • Model:            claude-sonnet-4.5
  • Input tokens:     45.2K
  • Output tokens:    12.8K
  • Total tokens:     58.0K
  • Cost:             1.0x premium
  • Premium requests: 15
  • API duration:     2m 34s

Token counts formatted as K/M for readability.

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

* Remove post-init Q&A loop

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

* Consolidate agent into self-contained CopilotAgent with factory

Major refactor: CopilotAgent is now a self-contained agent that
encapsulates initialization, session management, display, and usage.
CopilotAgentFactory creates agents with dependencies wired via IoC.

New API:
  agent, _ := factory.Create(ctx, agent.WithMode('interactive'))
  initResult, _ := agent.Initialize(ctx)
  selected, _ := agent.SelectSession(ctx)
  result, _ := agent.SendMessage(ctx, prompt, agent.WithSessionID(...))
  // result.Content, result.SessionID, result.Usage
  agent.Stop()

New types (types.go):
  AgentResult{Content, SessionID, Usage}
  InitResult{Model, ReasoningEffort, IsFirstRun}
  UsageMetrics with Format() method
  AgentOption: WithModel, WithReasoningEffort, WithMode, WithDebug
  SendOption: WithSessionID
  InitOption: WithForcePrompt

Agent methods:
  Initialize() — config prompts (first run), plugin install, client start
  SelectSession() — UX picker for session resume
  ListSessions() — raw session listing
  SendMessage() / SendMessageWithRetry() — returns AgentResult
  Stop() — cleanup

Deleted (old langchaingo agent):
  agent.go, agent_factory.go, conversational_agent.go, prompts/

Simplified:
  init.go — ~40 lines instead of ~150
  container.go — removed old AgentFactory, ModelFactory registrations
  error.go — updated to use CopilotAgentFactory

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

* Fix spacing: add blank line after each prompt, remove leading blanks

Each prompt component (Select, Prompt) now adds a blank line after
its Ask() call. Callers manage spacing before prompts. Removed the
leading blank line from SelectSession (was doubling up with the
trailing blank from the previous prompt).

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

* Add unit tests for types.go and display.go pure functions

New test files:
- types_test.go: UsageMetrics.Format(), TotalTokens(), formatTokenCount,
  stripMarkdown, formatSessionTime (40 test cases)
- display_test.go: extractToolInputSummary, extractIntentFromArgs,
  toRelativePath, GetUsageMetrics accumulation

All pure functions tested without SDK mocking.

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

* Fix billing display and CI checks

- Rename Cost to BillingRate (per-request multiplier, not cumulative)
- Show premium requests from SDK (omit if not reported)
- Handle session.shutdown for TotalPremiumRequests
- Remove manual API call counter
- Fix all lint issues (errorlint, gosec, staticcheck, unused)
- Fix formatting, remove unused code (github_copilot_registration files,
  discoverInstalledPluginDirs)
- All CI checks pass: gofmt, golangci-lint, tests

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

* Address PR review feedback

- Fix config_options.yaml: tools.available/excluded type 'object' -> 'array'
- Add missing config docs: ai.agent.skills.directories, ai.agent.skills.disabled
- Log warning on userConfigManager.Load() failure instead of silently swallowing
- Simplify redundant MCP server unmarshaling (removed type probe, single path)

Other review items (r3, r6, r8, r9) referenced old code that was deleted
in the agent consolidation commit.

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

* Address second round PR review feedback

Bug fixes:
- Fix nil pointer dereferences in error.go when agentResult is nil
- Fix session.error not unblocking WaitForIdle (signal idleCh on error)
- Fix consent check fails-open: deny on error instead of allow

Security:
- PreToolUse consent check now denies on error with logged reason
- OnPermissionRequest remains approve-all (CLI-level coarse permissions,
  fine-grained control via PreToolUse hooks)

Code quality:
- Deterministic cleanup order: changed from map to ordered slice with
  reverse teardown (session events -> file logger -> client)
- Log warning on config load failure
- Simplified MCP server unmarshaling

Config:
- Added skills.directories and skills.disabled to config_options.yaml
- Fixed tools.available/excluded type from object to array

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

* Add AgentMode enum, remove redundant logging infrastructure

AgentMode:
- New AgentMode type with constants: AgentModeInteractive, AgentModeAutopilot,
  AgentModePlan. WithMode() now takes AgentMode instead of string.

Removed logging (redundant with Copilot CLI logs at ~/.copilot/logs/):
- thought_logger.go — old langchaingo callback handler
- file_logger.go — old langchaingo callback handler
- chained_handler.go — old langchaingo callback handler
- session_event_handler.go — SessionEventLogger, SessionFileLogger,
  CompositeEventHandler all unused after AgentDisplay consolidation
- session_event_handler_test.go

Kept: logging/util.go with TruncateString (used by display.go)

Net: 902 lines deleted.

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

* Fix: return originalError when user declines fix in error middleware

When the user declines the agent fix, the code returned 'err' which
was nil (consent check succeeded), silently swallowing the original
command failure. Now returns originalError to preserve the error.

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

* Document permission handler intent and relationship to PreToolUse

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

* Remove docs/specs/copilot-agent-ux (deleted by reviewer)

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

* Delete dead agent tools and unused consent wrapper

Deleted (all replaced by Copilot CLI built-in tools):
- tools/dev/ — command executor (shell tool)
- tools/io/ — 12 file/directory tools + tests
- tools/common/ — AnnotatedTool interface, ToLangChainTools, ToolLoader
- tools/loader.go — composite tool loader
- tools/mcp/tool_adapter.go — MCP-to-langchaingo adapter
- tools/mcp/sampling_handler.go — MCP sampling handler
- tools/mcp/elicitation_handler.go — MCP elicitation handler
- tools/mcp/loader.go — MCP tool loader
- consent/consent_wrapper_tool.go — langchaingo tool wrapper

Cleaned:
- Removed WrapTool/WrapTools from ConsentManager interface and impl
- Removed common package import from consent
- Kept tools/mcp/embed.go with McpJson embed (still used by factory)

Net: 7,025 lines deleted.

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

* Remove unused ExecutingTool and related global state from consent

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

* Fix MCP functional tests for remaining tools

Updated test expectations after MCP tool migration to Copilot SDK skills.
Tests now verify error_troubleshooting, provision_common_error, and
validate_azure_yaml (the 3 remaining MCP tools).

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

* Fix init.go: remove stray backtick, highlight azd up command

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

* Apply go fix for Go 1.26 compatibility

- intPtr() -> new() for pointer creation
- strings.Split -> strings.SplitSeq for range iteration
- strings.HasPrefix+TrimPrefix -> strings.CutPrefix
- floatPtr/strPtr helpers replaced with new() in tests

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

* Remove unused intPtr, floatPtr, strPtr helpers (inlined by go fix)

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

* refactor: migrate to copilot.* namespace, delete pkg/llm, streamline error middleware

- Migrate all config keys from ai.agent.* to copilot.* namespace
- Move copilot_client.go and session_config.go to internal/agent/copilot/
- Delete entire pkg/llm/ package (azure_openai, ollama, github_copilot, model_factory, manager)
- Move consent commands from azd mcp consent to azd copilot consent
- Streamline error middleware: single consent prompt + agent-driven troubleshooting
- Troubleshooting prompts in embedded Go text templates
- AgentDisplay: render AssistantMessage in real-time, red x for failed tools
- Remove Content from AgentResult, delete dead feedback package
- Adopt SDK bundler for CLI binary embedding, remove npm path scanning
- Clean up CI pipelines: remove ghCopilot build tag and ldflags
- Add WithSystemMessage AgentOption
- Add composable config key constants with ConfigRoot prefix
- Remove langchaingo dependency

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

* feat: on-demand Copilot CLI download, replace SDK bundler

- Add CopilotCLI managed tool (internal/agent/copilot/cli.go) following Bicep pattern
- Download platform-specific CLI from npm registry on first use
- Cache at ~/.azd/bin/copilot-cli-{version}, override via AZD_COPILOT_CLI_PATH
- Implement tools.ExternalTool interface (Name, InstallUrl, CheckInstalled)
- Integrate with CopilotClientManager (resolves CLI at Start time)
- Remove SDK bundler (zcopilot_* files, go tool bundler CI step, tool dep)
- Binary size reduced ~106MB (no longer embedded)
- Fix cspell: add agentcopilot to word list, reword comment

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

* fix: copilot display deadlocks, consent persistence, and UX improvements

- Fix WaitForIdle hang when SessionIdle fires before AssistantMessage
- Fix ticker vs Pause() TOCTOU race causing spinner to render over consent prompts
- Fix consent grant errors silently denying tool execution
- Add error logging for consent rule load/save failures
- Improve tool completion display with contextual verbs and diff stats
- Add tree-style sub-detail for shell commands and MCP tool args
- Add colored diff stats (green +N / red -N) for edit/create tools
- Show plugin version in skill invocation display
- Normalize whitespace spacing via printSeparated for all section transitions
- Show error messages on tool call failures
- Add comprehensive unit tests for display helpers and consent persistence

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

* chore: commit pending branch changes for CI

Includes copilot CLI plugin management (ListPlugins, InstallPlugin),
session time formatting, consent command migration, and azdcontext updates.

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

* fix: move file watcher to background goroutine to prevent WSL hang

On WSL with Windows filesystem mounts (/mnt/c/...), fsnotify's
filepath.Walk + inotify watch setup can hang or be extremely slow.
Moving NewWatcher() to a background goroutine prevents it from
blocking SendMessage. The watcher results are still collected at
cleanup via mutex-protected access.

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

* feat: add auth check with interactive login, fix spinner verb, fix E2E test

- Check GitHub Copilot auth status before session creation
- Prompt to sign in via copilot login (OAuth device flow) if not authenticated
- Add CopilotCLI.Login() wrapper for interactive copilot login command
- Fix spinner showing 'Running Ran tool' — use tool name for spinner, verb for completion
- Fix E2E test: add OnPermissionRequest: ApproveAll to session config
- Add ErrToolExecutionSkipped to excluded errors list in error mapping test
- Add unit tests for Login success and error cases

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jongio pushed a commit to jongio/azure-dev that referenced this pull request Mar 27, 2026
* Add Copilot SDK foundation alongside existing langchaingo agent

Add the GitHub Copilot SDK (github.com/github/copilot-sdk/go) as a new
dependency and create the foundational types for a Copilot SDK-based agent
implementation. All new code coexists alongside the existing langchaingo
agent — no existing code is modified or deleted.

New files:
- pkg/llm/copilot_client.go: CopilotClientManager wrapping copilot.Client
  lifecycle (Start, Stop, GetAuthStatus, ListModels)
- pkg/llm/session_config.go: SessionConfigBuilder that reads ai.agent.*
  config keys and produces copilot.SessionConfig, including MCP server
  merging (built-in + user-configured) and tool control
- internal/agent/copilot_agent.go: CopilotAgent implementing the Agent
  interface backed by copilot.Session with SendAndWait
- internal/agent/copilot_agent_factory.go: CopilotAgentFactory that
  creates CopilotAgent instances with SDK client, session, permission
  hooks, MCP servers, and event handlers
- internal/agent/logging/session_event_handler.go: SessionEventLogger,
  SessionFileLogger, and CompositeEventHandler for SDK SessionEvent
  streaming to UX thought channel and daily log files

Config additions (resources/config_options.yaml):
- ai.agent.model: Default model for Copilot SDK sessions
- ai.agent.mode: Agent mode (autopilot/interactive/plan)
- ai.agent.mcp.servers: Additional MCP servers
- ai.agent.tools.available/excluded: Tool allow/deny lists
- ai.agent.systemMessage: Custom system prompt append
- ai.agent.copilot.logLevel: SDK log level

Resolves Azure#6871, Azure#6872, Azure#6873, Azure#6874, Azure#6875

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

* Add 'copilot' as default agent provider type

Register CopilotProvider as a named 'copilot' model provider in the IoC
container. This makes 'copilot' the default agent type when ai.agent.model.type
is not explicitly configured.

Changes:
- Add LlmTypeCopilot constant and CopilotProvider (copilot_provider.go)
- Default GetDefaultModel() to 'copilot' when no model type is set
- Register 'copilot' provider in container.go
- Update init.go to set 'copilot' instead of 'github-copilot'
- Update error message to list copilot as supported type

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

* Add diagnostic logging to Copilot SDK agent pipeline

Add [copilot] and [copilot-event] prefixed log statements throughout
the Copilot SDK agent pipeline for troubleshooting:

- CopilotClientManager: Start/stop state transitions
- CopilotAgentFactory: MCP server count, session config details,
  PreToolUse/PostToolUse/ErrorOccurred hook invocations
- CopilotAgent.SendMessage: prompt size, response size, errors
- SessionEventLogger: every event type received, plus detail for
  assistant.message, tool.execution_start, and assistant.reasoning

Run with AZD_DEBUG=true to see log output.

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

* Wire CopilotAgentFactory into AgentFactory for automatic delegation

AgentFactory.Create() now checks the configured model type. When it's
'copilot' (the new default), it delegates to CopilotAgentFactory which
creates a CopilotAgent backed by the Copilot SDK session. No call site
changes needed — existing code calling AgentFactory.Create() gets the
Copilot SDK agent automatically.

Changes:
- AgentFactory now takes CopilotAgentFactory as a dependency
- Create() checks model type and delegates to CopilotAgentFactory
- Register CopilotAgentFactory, CopilotClientManager, and
  SessionConfigBuilder in IoC container (cmd/container.go)

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

* Enable SDK debug logging to diagnose CLI process startup failure

Set SDK LogLevel to 'debug' by default to surface the command and args
the SDK uses when spawning the copilot CLI process. This will help
diagnose the 'exit status 1' error during client.Start().

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

* Update copilot-sdk to v0.1.26-preview.0

CLI v0.0.419-0 now supports --headless and --stdio flags required by the
SDK. Updated Go SDK from v0.1.25 to v0.1.26-preview.0 for latest
compatibility.

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

* Auto-discover native Copilot CLI binary from @github/copilot-sdk npm package

The copilot CLI shim in PATH (from @github/copilot npm package) doesn't
support --headless --stdio flags required by the Go SDK. However, the
@github/copilot-sdk npm package bundles a newer native binary at
node_modules/@github/copilot-{platform}/copilot[.exe] that does.

CopilotClientManager now auto-discovers this binary with resolution order:
1. COPILOT_CLI_PATH environment variable
2. Native binary from @github/copilot-sdk npm package (platform-specific)
3. Falls back to 'copilot' in PATH (SDK default)

Also adds a passing e2e test (TestCopilotSDK_E2E) that validates the full
SDK lifecycle: client start → auth check → list models → create session →
send prompt → receive response → cleanup. Pure native binary, no Node.js.

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

* Fix: explicitly allow tools in PreToolUse hook

The empty PreToolUseHookOutput{} was interpreted as deny by the SDK,
blocking all tool calls. Set PermissionDecision to 'allow' explicitly.

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

* Fix: add OnPermissionRequest handler to approve tool permissions

The SDK has two separate permission mechanisms:
1. PreToolUse hooks (lifecycle interception) - already set to 'allow'
2. OnPermissionRequest handler (CLI permission prompts) - was NOT set

Without OnPermissionRequest, the CLI's permission requests go unanswered
and default to deny, blocking all tool calls. Use the SDK's built-in
PermissionHandler.ApproveAll to approve all requests.

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

* Fix: increase SendAndWait timeout to 10 minutes

The SDK defaults to 60s timeout when the context has no deadline,
which is too short for agent init tasks (discovery, IaC generation,
Dockerfile creation, etc.). Set a 10-minute timeout per message.

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

* Use Send + idle event instead of SendAndWait to avoid timeout

Replace SendAndWait (which imposes a 60s default timeout) with Send
(non-blocking) + explicit wait for session.idle event. The agent task
runs until the SDK signals completion or the parent context is cancelled.
No artificial timeout.

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

* Integrate azd consent system and required plugin auto-install

Two major changes to CopilotAgentFactory:

1. Required plugin auto-install:
   - Runs 'copilot plugin install microsoft/GitHub-Copilot-for-Azure:plugin'
     before starting each session (idempotent, non-interactive)
   - Uses the resolved CLI binary path from CopilotClientManager
   - Logs warnings on install failure but doesn't block session creation

2. Wire azd consent system into SDK permission handlers:
   - OnPermissionRequest: delegates to ConsentManager.CheckConsent()
     for CLI-level permission requests. If consent requires prompting,
     uses ConsentChecker to show azd's interactive consent UX with
     scoped persistence (session/project/global).
   - OnPreToolUse: checks ConsentManager before each tool execution.
     If no rule exists, prompts via ConsentChecker.PromptAndGrantConsent()
     which stores the user's choice at their selected scope.
   - Replaces the previous PermissionHandler.ApproveAll with proper
     consent-gated approval flow.

Also:
- CopilotAgentFactory now takes ConsentManager as a dependency
- CopilotClientManager exposes CLIPath() for plugin install commands

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

* Fix mage namespace discovery for dev:install target

The Dev struct needs to be declared as 'type Dev mg.Namespace' for mage
to discover it as a namespace. Also adds github.com/magefile/mage to
go.mod as required by the magefile's mg import.

This fixes 'mage dev:install' and 'mage dev:uninstall' targets.

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

* Fix OTel schema version conflict causing panic on startup

The copilot-sdk pulled in otel core v1.42.0 (schema 1.39.0) but otel/sdk
remained at v1.38.0 (schema 1.37.0). This mismatch caused a panic in
resource.New() at startup. Upgraded all OTel SDK packages to v1.42.0 to
match the core version.

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

* Update copilot-sdk to v0.1.32

Updated from v0.1.26-preview.0 to v0.1.32. Adapted to API change where
PermissionRequest.Kind is now a typed PermissionRequestKind instead of
plain string.

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

* Fix OTel semconv schema mismatch: update to v1.40.0

OTel SDK v1.42.0 uses semconv/v1.40.0 internally for resource.Default(),
but our resource.go imported semconv/v1.39.0. The schema URL mismatch
(1.40.0 vs 1.39.0) caused a panic on resource.Merge(). Updated import
to match the SDK's semconv version.

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

* Fix: approve CLI permission requests, use consent only in PreToolUse

The OnPermissionRequest and OnPreToolUse handlers were conflicting:
OnPermissionRequest was calling CheckToolConsent (which only checks
rules, doesn't prompt) and falling through to 'denied' when no rules
existed. Meanwhile OnPreToolUse correctly called PromptAndGrantConsent.

Fix: OnPermissionRequest now approves all CLI-level permission requests
(file access, shell, URLs). Fine-grained per-tool consent with user
prompting is handled exclusively by the OnPreToolUse hook.

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

* Add AgentDisplay for consolidated Copilot SDK UX rendering

Replace the thought-channel indirection with AgentDisplay — a direct
event-driven UX renderer that subscribes to session.On() and handles
all SDK event types using existing azd UX components.

New: internal/agent/display.go
- AgentDisplay manages Canvas + Spinner + VisualElement per SendMessage
- Handles 15+ event types: turn lifecycle, tool execution with elapsed
  timer, reasoning/thinking display, streaming message deltas, errors,
  warnings, skill invocations, subagent delegation
- WaitForIdle() blocks until session.idle with final content capture
- Thread-safe state management for concurrent event handling

Changes:
- CopilotAgent.SendMessage() creates AgentDisplay per turn, subscribes
  it to session events, and uses WaitForIdle() for completion
- CopilotAgentFactory no longer creates thought channel or
  SessionEventLogger — file logger remains for audit trail
- Export TruncateString from logging package for shared use

Removes:
- Thought channel and WithCopilotThoughtChannel option
- renderThoughts() goroutine (replaced by AgentDisplay)
- SessionEventLogger dependency in factory (UX moved to display)

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

* Fix reasoning display and show relative paths in tool summaries

Two fixes to AgentDisplay:

1. Reasoning: accumulate reasoning_delta chunks into a rolling buffer
   instead of replacing with each delta. Also handle streaming_delta
   events with phase='thinking' for models that emit reasoning that
   way. Canvas.Update() called after each reasoning update for
   immediate display.

2. Paths: extractToolInputSummary now converts absolute paths to
   relative (via filepath.Rel to cwd) for cleaner display. Paths
   that escape cwd are shown absolute.

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

* Show full reasoning in scrolling window with flush on transitions

Reasoning display now works as a scrolling window:
- VisualElement shows last ~5 lines of reasoning below the spinner,
  updating live as delta chunks stream in
- Full reasoning accumulates in a buffer (no truncation)
- On tool start or turn end, the complete reasoning is printed as a
  persistent dimmed block above the canvas, then the buffer resets

Removed latestThought field — reasoning state is now entirely managed
via reasoningBuf. Removed AssistantMessageDelta handler (message deltas
don't need UX rendering — final content comes via AssistantMessage).

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

* UX: add blank line before spinner and change text to 'Working...'

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

* Check if plugin is installed before install/update

ensurePlugins now lists installed plugins first. If a required plugin
is already installed, it runs 'copilot plugin update' instead of a
full install. New plugins get 'copilot plugin install'.

Also restructured requiredPlugins as pluginSpec with Source (install
path) and Name (installed name for update command).

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

* Simplify init to single prompt + wire OnUserInputRequest handler

Two changes:

1. Wire OnUserInputRequest in CopilotAgentFactory:
   Enables the agent's built-in ask_user tool. When the agent asks a
   question, the handler renders it using azd's UX components:
   - Choices → ux.NewSelect() with azd styling
   - Freeform → ux.NewPrompt() for text input
   This lets the agent ask clarifying questions during execution
   (architecture choices, service selection, config options).

2. Simplify initAppWithAgent() from 6-step loop to single prompt:
   Replaces 6 hardcoded steps + inter-step feedback loops + post-
   completion summary aggregation with a single prompt that delegates
   to azure-prepare and azure-validate skills from the Azure plugin.
   The skills handle all orchestration internally and can ask the user
   questions via ask_user when needed.

   Removed: initStep struct, step definitions, collectAndApplyFeedback(),
   postCompletionSummary(), feedback import (~140 lines).

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

* UX: use intent as spinner text, move reasoning above spinner, set interactive mode

Three UX improvements:

1. Spinner text: uses assistant.intent events (short task descriptions
   like 'Analyzing project structure') instead of static 'Working...'
   Truncated to 80 chars to stay concise.

2. Reasoning display: moved above the spinner instead of below. Layout
   is now: blank line → reasoning (last 5 lines, gray) → blank line →
   spinner. More natural reading order.

3. Mode: SendMessage now explicitly sets Mode to 'interactive' so the
   agent asks for approval before executing tools.

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

* Strip markdown from ask_user prompts, render reasoning with markdown, fix skill triggers

Three changes:

1. ask_user: Strip markdown formatting (bold, italic, backticks, headings)
   from question text and choice labels before rendering in azd UX prompts.
   Choices still return the original value to the agent.

2. flushReasoning: Render accumulated reasoning with output.WithMarkdown()
   instead of raw gray text, giving proper formatting for code blocks,
   lists, and other markdown content.

3. init prompt: Use natural trigger phrases ('prepare this application
   for deployment to Azure', 'validate that everything is ready') that
   match the azure-prepare and azure-validate skill description triggers,
   instead of referencing skill names directly.

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

* Support AllowFreeform in ask_user with 'Other' choice option

When the agent sends choices with AllowFreeform=true, append an
'Other (type your own answer)' option to the Select list. If selected,
follow up with a freeform Prompt and return WasFreeform: true.

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

* Explicitly invoke @azure-prepare and @azure-validate skills in init prompt

Use @skill-name syntax and numbered steps to ensure both skills are
invoked in order. Previous natural language triggers may not have
reliably activated the skills.

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

* Specify 'azd' recipe for azure-prepare and azure-validate skills

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

* Store Copilot session files in .azure/copilot relative to cwd

Sets SessionConfig.ConfigDir to .azure/copilot in the current working
directory so session state is project-local instead of global ~/.copilot.

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

* Fix skill discovery: revert ConfigDir, use qualified skill names

ConfigDir override to .azure/copilot broke plugin discovery — the CLI
loads plugins from ConfigDir/installed-plugins/ which was empty. Reverted
to default ~/.copilot so installed plugins (and their skills) are found.
Set WorkingDirectory instead for tool operations.

Updated init prompt to use fully qualified azure:azure-prepare and
azure:azure-validate skill names (plugin:skill-name format).

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

* Pass installed plugins via --plugin-dir for headless mode discovery

The CLI in --headless --stdio mode (used by the SDK) doesn't auto-discover
installed plugins from ~/.copilot/installed-plugins/. This meant skills
from the Azure plugin were never loaded.

Fix: discoverInstalledPluginDirs() scans ~/.copilot/installed-plugins/
for plugin directories (verified by presence of skills/ or .claude-plugin/)
and passes each via --plugin-dir CLIArgs to the SDK client.

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

* Scope plugin discovery to Azure plugin only

Only load the microsoft/GitHub-Copilot-for-Azure plugin via --plugin-dir.
Checks both _direct and marketplace install paths. Other plugins will be
handled separately later.

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

* Use SkillDirectories instead of --plugin-dir for skill loading

The --plugin-dir CLI flag isn't supported by all copilot binary builds.
Instead, pass the Azure plugin's skills directory via SessionConfig.
SkillDirectories, which is sent via JSON-RPC createSession and works
reliably in headless mode.

discoverAzurePluginSkillDirs() finds the skills/ directory from the
installed Azure plugin and adds it to SkillDirectories alongside any
user-configured skill directories.

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

* Persist intent text in spinner instead of resetting to 'Thinking...'

Track lastIntent and reuse it when the spinner resets after turn start
or tool completion. Only falls back to 'Thinking...' if no intent has
been received yet.

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

* Add blank line after spinner

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

* Show relative paths for skill directories in tool summaries

toRelativePath now tries both cwd and ~/.copilot/installed-plugins/
as base directories. Paths under the plugins root (skill files) are
shown relative instead of absolute.

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

* Capture intent from report_intent tool calls for spinner text

The agent uses a report_intent tool (not assistant.intent events) to
signal what it's working on. Extract the intent text from the tool's
arguments and use it as the spinner text. The report_intent tool is
suppressed from UX display (no 'Ran report_intent' completion line).

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

* Show nested subagent tool calls with rich display

Subagent events now show richer UX:
- started: '◆ {DisplayName} — {Description}'
- completed: '✔ {DisplayName} completed' with summary
- failed: '✖ {DisplayName} failed: {error}'
- deselected: resets subagent state

Tool calls inside a subagent are indented with 2 spaces to show
nesting visually:
  ◆ Azure Prepare — Prepare apps for deployment
    ✔ Ran read_file with path: main.go
    ✔ Ran write_file with path: azure.yaml
  ✔ Azure Prepare completed

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

* UX polish: blank lines before skill/subagent, suppress internal tools

- Add blank line before 'Using skill:' and 'Delegating to:' lines
- Suppress tool call display for report_intent, ask_user, task, and
  skill: prefixed tools — these are internal/UX tools that shouldn't
  show as 'Ran X' completion lines

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

* Prompt for reasoning effort and model on first agent run

First run (no ai.agent.reasoningEffort config):
- Prompt for reasoning effort (low/medium/high) with cost guidance
- Prompt for model selection (default or specific model)
- Save both to azd config

Subsequent runs:
- Show info note with current model and reasoning level
- Point to 'azd config set' for changes

Also:
- Wire ai.agent.reasoningEffort to SessionConfig.ReasoningEffort
- Add reasoningEffort to config_options.yaml

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

* Fix: only signal idle when final content exists

Multiple session.idle events can arrive during a session (after
permission prompts, between tool calls). The first idle was consumed
by WaitForIdle before the assistant message arrived, causing the
display to clear with no summary shown.

Fix: only signal idleCh when finalContent has been set by an
assistant.message event. Early idle events are ignored.

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

* Load Azure plugin MCP servers into session config

Read .mcp.json from the installed Azure plugin and merge its MCP servers
(azure, foundry-mcp, context7) into SessionConfig.MCPServers alongside
built-in and user-configured servers.

Merge order: built-in → Azure plugin → user config (last wins).

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

* Suppress 'skill' tool from display, add blank line after Using skill

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

* Fix agent exit: reset finalContent per turn, add completion logging

Root cause: assistant.message from an earlier turn set finalContent,
then a session.idle between tool calls found hasContent=true and
signaled WaitForIdle prematurely — before the actual final message.

Fixes:
- Reset finalContent on every assistant.turn_start so only the last
  turn's message is considered
- Handle session.task_complete and session.shutdown as additional
  completion signals
- Add debug logging to assistant.message, session.idle, and
  WaitForIdle for future troubleshooting

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

* Add scoped system message and empty directory handling

System message (append mode):
- Scopes the agent to Azure application development only
- Rejects unrelated requests with a focused explanation
- User-configured ai.agent.systemMessage appended after default

Init prompt:
- Agent now checks if cwd is empty/has no code first
- If empty, asks user what type of Azure app to build
- Then proceeds with azure-prepare and azure-validate skills

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

* Consistent whitespace and improved config display

Whitespace rules:
- Skills, subagents, reasoning, errors, warnings: blank line before
  and after (via printSeparated, no double blanks)
- Tool completions: stack without blank lines (via printLine)
- lastPrintedBlank flag prevents duplicate blank lines

Config display:
- Split into multiple lines for readability
- Show model and reasoning on separate bullet points
- azd commands shown in highlight format

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

* Add blank lines before/after user prompts, fix extra blank in config display

- User prompts (ask_user): blank line before and after Select/Prompt
- Config display: remove leading blank line (was doubling up with
  prior output), remove trailing newline from alpha warning title

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

* Log MCP server details and skill dirs for debugging

Before session creation, log each configured MCP server (name, type,
command/url) and skill directory. Also capture session.info events
with AllowedTools list and skill.invoked events in the file logger.

Visible with AZD_DEBUG=true or in ~/.azd/logs/azd-agent-*.log.

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

* Print MCP servers and skill dirs to console output for debugging

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

* Print available tools to console from session events

Captures AllowedTools from the first session event that carries them
and prints the full list to console output for debugging.

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

* Fix MCP server config: use type 'local' and add tools wildcard

The SDK expects type='local' (not 'stdio') for local MCP servers,
and requires a 'tools' field listing which tools to expose. Without
tools=['*'], no MCP tools were made available to the agent.

Fixes:
- convertServerConfig: use type='local', add tools=['*']
- loadAzurePluginMCPServers: normalize plugin configs — set type
  to 'local' for command-based servers, add tools=['*'] if missing

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

* Add diagnostic prompt to list all tools including MCP server tools

Temporary: agent prints all available tools grouped by built-in, MCP
server tools, and skills before proceeding with init. Will remove once
MCP server integration is verified working.

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

* Dump first 30 session events with details for MCP debugging

Temporary: prints event type + key fields (allowedTools, tools, message,
infoType, name) for the first 30 events to diagnose MCP server loading.

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

* Remove diagnostic prompt, rely on event dump for tool debugging

The system message was causing the model to ignore the diagnostic
'list tools' request. Event dump from the factory provides better
programmatic visibility.

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

* Debugging: disable system message, add diagnostic tools list prompt, remove event dump

Temporary changes to diagnose MCP server tool availability:
- Comment out system message (was blocking diagnostic requests)
- Add explicit 'list all tools' prompt before init task
- Remove event dump from factory (wasn't showing useful data)

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

* Add post-init Q&A loop for follow-up questions

After the init summary, prompt the user 'Any questions?' in a loop.
User can ask follow-up questions (sent to the same agent session with
full context). Press Enter with no input to finish and exit.

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

* Add session resume support for cancelled/crashed agent sessions

On azd init with agent mode, checks for previous sessions in the
current directory via client.ListSessions(). If found, prompts user
to resume a previous session or start fresh.

Resume uses client.ResumeSession() which restores full conversation
history with the same MCP servers, skills, permissions, and hooks.

Changes:
- CopilotAgentFactory: add ListSessions() and Resume() methods
- init.go: add session picker before agent creation, add
  CopilotAgentFactory to initAction struct

Spec at docs/specs/copilot-agent-ux/session-resume.md

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

* Polish session resume UX: numbers, local time, truncated labels

- Show numbered choices in session picker (DisplayNumbers: true)
- Convert timestamps to local time (Today 3:04 PM, Yesterday, Jan 2)
- Truncate labels to ~120 chars total
- Shorter prompt: 'Previous sessions found:'

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

* Fix session labels: collapse newlines, enforce 120 char max

Summaries from sessions can contain newlines and markdown. Use
strings.Fields() to collapse all whitespace into single spaces,
then truncate to fit within 120 chars total.

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

* Enable line numbers and filtering on all Select prompts

Added DisplayNumbers and EnableFiltering to reasoning effort, model
selection, and session picker prompts.

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

* Dynamic model list from SDK with billing and reasoning metadata

Fetch models via ListModels() instead of hardcoding. Each option shows:
  'Claude Sonnet 4.5 (high) (1x)'
  — name, default reasoning effort, billing multiplier

Also:
- Remove trailing ':' from prompt messages (UX components add them)
- Add ListModels() to CopilotAgentFactory

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

* Show session usage metrics at end of init

Accumulate usage from assistant.usage and session.usage_info events:
input/output tokens, cost multiplier, premium requests, API duration,
and model used.

Display at session end:
  Session usage:
  • Model:            claude-sonnet-4.5
  • Input tokens:     45.2K
  • Output tokens:    12.8K
  • Total tokens:     58.0K
  • Cost:             1.0x premium
  • Premium requests: 15
  • API duration:     2m 34s

Token counts formatted as K/M for readability.

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

* Remove post-init Q&A loop

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

* Consolidate agent into self-contained CopilotAgent with factory

Major refactor: CopilotAgent is now a self-contained agent that
encapsulates initialization, session management, display, and usage.
CopilotAgentFactory creates agents with dependencies wired via IoC.

New API:
  agent, _ := factory.Create(ctx, agent.WithMode('interactive'))
  initResult, _ := agent.Initialize(ctx)
  selected, _ := agent.SelectSession(ctx)
  result, _ := agent.SendMessage(ctx, prompt, agent.WithSessionID(...))
  // result.Content, result.SessionID, result.Usage
  agent.Stop()

New types (types.go):
  AgentResult{Content, SessionID, Usage}
  InitResult{Model, ReasoningEffort, IsFirstRun}
  UsageMetrics with Format() method
  AgentOption: WithModel, WithReasoningEffort, WithMode, WithDebug
  SendOption: WithSessionID
  InitOption: WithForcePrompt

Agent methods:
  Initialize() — config prompts (first run), plugin install, client start
  SelectSession() — UX picker for session resume
  ListSessions() — raw session listing
  SendMessage() / SendMessageWithRetry() — returns AgentResult
  Stop() — cleanup

Deleted (old langchaingo agent):
  agent.go, agent_factory.go, conversational_agent.go, prompts/

Simplified:
  init.go — ~40 lines instead of ~150
  container.go — removed old AgentFactory, ModelFactory registrations
  error.go — updated to use CopilotAgentFactory

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

* Fix spacing: add blank line after each prompt, remove leading blanks

Each prompt component (Select, Prompt) now adds a blank line after
its Ask() call. Callers manage spacing before prompts. Removed the
leading blank line from SelectSession (was doubling up with the
trailing blank from the previous prompt).

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

* Add unit tests for types.go and display.go pure functions

New test files:
- types_test.go: UsageMetrics.Format(), TotalTokens(), formatTokenCount,
  stripMarkdown, formatSessionTime (40 test cases)
- display_test.go: extractToolInputSummary, extractIntentFromArgs,
  toRelativePath, GetUsageMetrics accumulation

All pure functions tested without SDK mocking.

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

* Fix billing display and CI checks

- Rename Cost to BillingRate (per-request multiplier, not cumulative)
- Show premium requests from SDK (omit if not reported)
- Handle session.shutdown for TotalPremiumRequests
- Remove manual API call counter
- Fix all lint issues (errorlint, gosec, staticcheck, unused)
- Fix formatting, remove unused code (github_copilot_registration files,
  discoverInstalledPluginDirs)
- All CI checks pass: gofmt, golangci-lint, tests

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

* Address PR review feedback

- Fix config_options.yaml: tools.available/excluded type 'object' -> 'array'
- Add missing config docs: ai.agent.skills.directories, ai.agent.skills.disabled
- Log warning on userConfigManager.Load() failure instead of silently swallowing
- Simplify redundant MCP server unmarshaling (removed type probe, single path)

Other review items (r3, r6, r8, r9) referenced old code that was deleted
in the agent consolidation commit.

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

* Address second round PR review feedback

Bug fixes:
- Fix nil pointer dereferences in error.go when agentResult is nil
- Fix session.error not unblocking WaitForIdle (signal idleCh on error)
- Fix consent check fails-open: deny on error instead of allow

Security:
- PreToolUse consent check now denies on error with logged reason
- OnPermissionRequest remains approve-all (CLI-level coarse permissions,
  fine-grained control via PreToolUse hooks)

Code quality:
- Deterministic cleanup order: changed from map to ordered slice with
  reverse teardown (session events -> file logger -> client)
- Log warning on config load failure
- Simplified MCP server unmarshaling

Config:
- Added skills.directories and skills.disabled to config_options.yaml
- Fixed tools.available/excluded type from object to array

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

* Add AgentMode enum, remove redundant logging infrastructure

AgentMode:
- New AgentMode type with constants: AgentModeInteractive, AgentModeAutopilot,
  AgentModePlan. WithMode() now takes AgentMode instead of string.

Removed logging (redundant with Copilot CLI logs at ~/.copilot/logs/):
- thought_logger.go — old langchaingo callback handler
- file_logger.go — old langchaingo callback handler
- chained_handler.go — old langchaingo callback handler
- session_event_handler.go — SessionEventLogger, SessionFileLogger,
  CompositeEventHandler all unused after AgentDisplay consolidation
- session_event_handler_test.go

Kept: logging/util.go with TruncateString (used by display.go)

Net: 902 lines deleted.

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

* Fix: return originalError when user declines fix in error middleware

When the user declines the agent fix, the code returned 'err' which
was nil (consent check succeeded), silently swallowing the original
command failure. Now returns originalError to preserve the error.

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

* Document permission handler intent and relationship to PreToolUse

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

* Remove docs/specs/copilot-agent-ux (deleted by reviewer)

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

* Delete dead agent tools and unused consent wrapper

Deleted (all replaced by Copilot CLI built-in tools):
- tools/dev/ — command executor (shell tool)
- tools/io/ — 12 file/directory tools + tests
- tools/common/ — AnnotatedTool interface, ToLangChainTools, ToolLoader
- tools/loader.go — composite tool loader
- tools/mcp/tool_adapter.go — MCP-to-langchaingo adapter
- tools/mcp/sampling_handler.go — MCP sampling handler
- tools/mcp/elicitation_handler.go — MCP elicitation handler
- tools/mcp/loader.go — MCP tool loader
- consent/consent_wrapper_tool.go — langchaingo tool wrapper

Cleaned:
- Removed WrapTool/WrapTools from ConsentManager interface and impl
- Removed common package import from consent
- Kept tools/mcp/embed.go with McpJson embed (still used by factory)

Net: 7,025 lines deleted.

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

* Remove unused ExecutingTool and related global state from consent

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

* Fix MCP functional tests for remaining tools

Updated test expectations after MCP tool migration to Copilot SDK skills.
Tests now verify error_troubleshooting, provision_common_error, and
validate_azure_yaml (the 3 remaining MCP tools).

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

* Fix init.go: remove stray backtick, highlight azd up command

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

* Apply go fix for Go 1.26 compatibility

- intPtr() -> new() for pointer creation
- strings.Split -> strings.SplitSeq for range iteration
- strings.HasPrefix+TrimPrefix -> strings.CutPrefix
- floatPtr/strPtr helpers replaced with new() in tests

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

* Remove unused intPtr, floatPtr, strPtr helpers (inlined by go fix)

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

* refactor: migrate to copilot.* namespace, delete pkg/llm, streamline error middleware

- Migrate all config keys from ai.agent.* to copilot.* namespace
- Move copilot_client.go and session_config.go to internal/agent/copilot/
- Delete entire pkg/llm/ package (azure_openai, ollama, github_copilot, model_factory, manager)
- Move consent commands from azd mcp consent to azd copilot consent
- Streamline error middleware: single consent prompt + agent-driven troubleshooting
- Troubleshooting prompts in embedded Go text templates
- AgentDisplay: render AssistantMessage in real-time, red x for failed tools
- Remove Content from AgentResult, delete dead feedback package
- Adopt SDK bundler for CLI binary embedding, remove npm path scanning
- Clean up CI pipelines: remove ghCopilot build tag and ldflags
- Add WithSystemMessage AgentOption
- Add composable config key constants with ConfigRoot prefix
- Remove langchaingo dependency

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

* feat: on-demand Copilot CLI download, replace SDK bundler

- Add CopilotCLI managed tool (internal/agent/copilot/cli.go) following Bicep pattern
- Download platform-specific CLI from npm registry on first use
- Cache at ~/.azd/bin/copilot-cli-{version}, override via AZD_COPILOT_CLI_PATH
- Implement tools.ExternalTool interface (Name, InstallUrl, CheckInstalled)
- Integrate with CopilotClientManager (resolves CLI at Start time)
- Remove SDK bundler (zcopilot_* files, go tool bundler CI step, tool dep)
- Binary size reduced ~106MB (no longer embedded)
- Fix cspell: add agentcopilot to word list, reword comment

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

* fix: copilot display deadlocks, consent persistence, and UX improvements

- Fix WaitForIdle hang when SessionIdle fires before AssistantMessage
- Fix ticker vs Pause() TOCTOU race causing spinner to render over consent prompts
- Fix consent grant errors silently denying tool execution
- Add error logging for consent rule load/save failures
- Improve tool completion display with contextual verbs and diff stats
- Add tree-style sub-detail for shell commands and MCP tool args
- Add colored diff stats (green +N / red -N) for edit/create tools
- Show plugin version in skill invocation display
- Normalize whitespace spacing via printSeparated for all section transitions
- Show error messages on tool call failures
- Add comprehensive unit tests for display helpers and consent persistence

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

* chore: commit pending branch changes for CI

Includes copilot CLI plugin management (ListPlugins, InstallPlugin),
session time formatting, consent command migration, and azdcontext updates.

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

* fix: move file watcher to background goroutine to prevent WSL hang

On WSL with Windows filesystem mounts (/mnt/c/...), fsnotify's
filepath.Walk + inotify watch setup can hang or be extremely slow.
Moving NewWatcher() to a background goroutine prevents it from
blocking SendMessage. The watcher results are still collected at
cleanup via mutex-protected access.

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

* feat: add auth check with interactive login, fix spinner verb, fix E2E test

- Check GitHub Copilot auth status before session creation
- Prompt to sign in via copilot login (OAuth device flow) if not authenticated
- Add CopilotCLI.Login() wrapper for interactive copilot login command
- Fix spinner showing 'Running Ran tool' — use tool name for spinner, verb for completion
- Fix E2E test: add OnPermissionRequest: ApproveAll to session config
- Add ErrToolExecutionSkipped to excluded errors list in error mapping test
- Add unit tests for Login success and error cases

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jongio pushed a commit to jongio/azure-dev that referenced this pull request Mar 27, 2026
* Add Copilot SDK foundation alongside existing langchaingo agent

Add the GitHub Copilot SDK (github.com/github/copilot-sdk/go) as a new
dependency and create the foundational types for a Copilot SDK-based agent
implementation. All new code coexists alongside the existing langchaingo
agent — no existing code is modified or deleted.

New files:
- pkg/llm/copilot_client.go: CopilotClientManager wrapping copilot.Client
  lifecycle (Start, Stop, GetAuthStatus, ListModels)
- pkg/llm/session_config.go: SessionConfigBuilder that reads ai.agent.*
  config keys and produces copilot.SessionConfig, including MCP server
  merging (built-in + user-configured) and tool control
- internal/agent/copilot_agent.go: CopilotAgent implementing the Agent
  interface backed by copilot.Session with SendAndWait
- internal/agent/copilot_agent_factory.go: CopilotAgentFactory that
  creates CopilotAgent instances with SDK client, session, permission
  hooks, MCP servers, and event handlers
- internal/agent/logging/session_event_handler.go: SessionEventLogger,
  SessionFileLogger, and CompositeEventHandler for SDK SessionEvent
  streaming to UX thought channel and daily log files

Config additions (resources/config_options.yaml):
- ai.agent.model: Default model for Copilot SDK sessions
- ai.agent.mode: Agent mode (autopilot/interactive/plan)
- ai.agent.mcp.servers: Additional MCP servers
- ai.agent.tools.available/excluded: Tool allow/deny lists
- ai.agent.systemMessage: Custom system prompt append
- ai.agent.copilot.logLevel: SDK log level

Resolves Azure#6871, Azure#6872, Azure#6873, Azure#6874, Azure#6875

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

* Add 'copilot' as default agent provider type

Register CopilotProvider as a named 'copilot' model provider in the IoC
container. This makes 'copilot' the default agent type when ai.agent.model.type
is not explicitly configured.

Changes:
- Add LlmTypeCopilot constant and CopilotProvider (copilot_provider.go)
- Default GetDefaultModel() to 'copilot' when no model type is set
- Register 'copilot' provider in container.go
- Update init.go to set 'copilot' instead of 'github-copilot'
- Update error message to list copilot as supported type

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

* Add diagnostic logging to Copilot SDK agent pipeline

Add [copilot] and [copilot-event] prefixed log statements throughout
the Copilot SDK agent pipeline for troubleshooting:

- CopilotClientManager: Start/stop state transitions
- CopilotAgentFactory: MCP server count, session config details,
  PreToolUse/PostToolUse/ErrorOccurred hook invocations
- CopilotAgent.SendMessage: prompt size, response size, errors
- SessionEventLogger: every event type received, plus detail for
  assistant.message, tool.execution_start, and assistant.reasoning

Run with AZD_DEBUG=true to see log output.

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

* Wire CopilotAgentFactory into AgentFactory for automatic delegation

AgentFactory.Create() now checks the configured model type. When it's
'copilot' (the new default), it delegates to CopilotAgentFactory which
creates a CopilotAgent backed by the Copilot SDK session. No call site
changes needed — existing code calling AgentFactory.Create() gets the
Copilot SDK agent automatically.

Changes:
- AgentFactory now takes CopilotAgentFactory as a dependency
- Create() checks model type and delegates to CopilotAgentFactory
- Register CopilotAgentFactory, CopilotClientManager, and
  SessionConfigBuilder in IoC container (cmd/container.go)

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

* Enable SDK debug logging to diagnose CLI process startup failure

Set SDK LogLevel to 'debug' by default to surface the command and args
the SDK uses when spawning the copilot CLI process. This will help
diagnose the 'exit status 1' error during client.Start().

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

* Update copilot-sdk to v0.1.26-preview.0

CLI v0.0.419-0 now supports --headless and --stdio flags required by the
SDK. Updated Go SDK from v0.1.25 to v0.1.26-preview.0 for latest
compatibility.

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

* Auto-discover native Copilot CLI binary from @github/copilot-sdk npm package

The copilot CLI shim in PATH (from @github/copilot npm package) doesn't
support --headless --stdio flags required by the Go SDK. However, the
@github/copilot-sdk npm package bundles a newer native binary at
node_modules/@github/copilot-{platform}/copilot[.exe] that does.

CopilotClientManager now auto-discovers this binary with resolution order:
1. COPILOT_CLI_PATH environment variable
2. Native binary from @github/copilot-sdk npm package (platform-specific)
3. Falls back to 'copilot' in PATH (SDK default)

Also adds a passing e2e test (TestCopilotSDK_E2E) that validates the full
SDK lifecycle: client start → auth check → list models → create session →
send prompt → receive response → cleanup. Pure native binary, no Node.js.

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

* Fix: explicitly allow tools in PreToolUse hook

The empty PreToolUseHookOutput{} was interpreted as deny by the SDK,
blocking all tool calls. Set PermissionDecision to 'allow' explicitly.

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

* Fix: add OnPermissionRequest handler to approve tool permissions

The SDK has two separate permission mechanisms:
1. PreToolUse hooks (lifecycle interception) - already set to 'allow'
2. OnPermissionRequest handler (CLI permission prompts) - was NOT set

Without OnPermissionRequest, the CLI's permission requests go unanswered
and default to deny, blocking all tool calls. Use the SDK's built-in
PermissionHandler.ApproveAll to approve all requests.

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

* Fix: increase SendAndWait timeout to 10 minutes

The SDK defaults to 60s timeout when the context has no deadline,
which is too short for agent init tasks (discovery, IaC generation,
Dockerfile creation, etc.). Set a 10-minute timeout per message.

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

* Use Send + idle event instead of SendAndWait to avoid timeout

Replace SendAndWait (which imposes a 60s default timeout) with Send
(non-blocking) + explicit wait for session.idle event. The agent task
runs until the SDK signals completion or the parent context is cancelled.
No artificial timeout.

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

* Integrate azd consent system and required plugin auto-install

Two major changes to CopilotAgentFactory:

1. Required plugin auto-install:
   - Runs 'copilot plugin install microsoft/GitHub-Copilot-for-Azure:plugin'
     before starting each session (idempotent, non-interactive)
   - Uses the resolved CLI binary path from CopilotClientManager
   - Logs warnings on install failure but doesn't block session creation

2. Wire azd consent system into SDK permission handlers:
   - OnPermissionRequest: delegates to ConsentManager.CheckConsent()
     for CLI-level permission requests. If consent requires prompting,
     uses ConsentChecker to show azd's interactive consent UX with
     scoped persistence (session/project/global).
   - OnPreToolUse: checks ConsentManager before each tool execution.
     If no rule exists, prompts via ConsentChecker.PromptAndGrantConsent()
     which stores the user's choice at their selected scope.
   - Replaces the previous PermissionHandler.ApproveAll with proper
     consent-gated approval flow.

Also:
- CopilotAgentFactory now takes ConsentManager as a dependency
- CopilotClientManager exposes CLIPath() for plugin install commands

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

* Fix mage namespace discovery for dev:install target

The Dev struct needs to be declared as 'type Dev mg.Namespace' for mage
to discover it as a namespace. Also adds github.com/magefile/mage to
go.mod as required by the magefile's mg import.

This fixes 'mage dev:install' and 'mage dev:uninstall' targets.

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

* Fix OTel schema version conflict causing panic on startup

The copilot-sdk pulled in otel core v1.42.0 (schema 1.39.0) but otel/sdk
remained at v1.38.0 (schema 1.37.0). This mismatch caused a panic in
resource.New() at startup. Upgraded all OTel SDK packages to v1.42.0 to
match the core version.

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

* Update copilot-sdk to v0.1.32

Updated from v0.1.26-preview.0 to v0.1.32. Adapted to API change where
PermissionRequest.Kind is now a typed PermissionRequestKind instead of
plain string.

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

* Fix OTel semconv schema mismatch: update to v1.40.0

OTel SDK v1.42.0 uses semconv/v1.40.0 internally for resource.Default(),
but our resource.go imported semconv/v1.39.0. The schema URL mismatch
(1.40.0 vs 1.39.0) caused a panic on resource.Merge(). Updated import
to match the SDK's semconv version.

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

* Fix: approve CLI permission requests, use consent only in PreToolUse

The OnPermissionRequest and OnPreToolUse handlers were conflicting:
OnPermissionRequest was calling CheckToolConsent (which only checks
rules, doesn't prompt) and falling through to 'denied' when no rules
existed. Meanwhile OnPreToolUse correctly called PromptAndGrantConsent.

Fix: OnPermissionRequest now approves all CLI-level permission requests
(file access, shell, URLs). Fine-grained per-tool consent with user
prompting is handled exclusively by the OnPreToolUse hook.

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

* Add AgentDisplay for consolidated Copilot SDK UX rendering

Replace the thought-channel indirection with AgentDisplay — a direct
event-driven UX renderer that subscribes to session.On() and handles
all SDK event types using existing azd UX components.

New: internal/agent/display.go
- AgentDisplay manages Canvas + Spinner + VisualElement per SendMessage
- Handles 15+ event types: turn lifecycle, tool execution with elapsed
  timer, reasoning/thinking display, streaming message deltas, errors,
  warnings, skill invocations, subagent delegation
- WaitForIdle() blocks until session.idle with final content capture
- Thread-safe state management for concurrent event handling

Changes:
- CopilotAgent.SendMessage() creates AgentDisplay per turn, subscribes
  it to session events, and uses WaitForIdle() for completion
- CopilotAgentFactory no longer creates thought channel or
  SessionEventLogger — file logger remains for audit trail
- Export TruncateString from logging package for shared use

Removes:
- Thought channel and WithCopilotThoughtChannel option
- renderThoughts() goroutine (replaced by AgentDisplay)
- SessionEventLogger dependency in factory (UX moved to display)

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

* Fix reasoning display and show relative paths in tool summaries

Two fixes to AgentDisplay:

1. Reasoning: accumulate reasoning_delta chunks into a rolling buffer
   instead of replacing with each delta. Also handle streaming_delta
   events with phase='thinking' for models that emit reasoning that
   way. Canvas.Update() called after each reasoning update for
   immediate display.

2. Paths: extractToolInputSummary now converts absolute paths to
   relative (via filepath.Rel to cwd) for cleaner display. Paths
   that escape cwd are shown absolute.

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

* Show full reasoning in scrolling window with flush on transitions

Reasoning display now works as a scrolling window:
- VisualElement shows last ~5 lines of reasoning below the spinner,
  updating live as delta chunks stream in
- Full reasoning accumulates in a buffer (no truncation)
- On tool start or turn end, the complete reasoning is printed as a
  persistent dimmed block above the canvas, then the buffer resets

Removed latestThought field — reasoning state is now entirely managed
via reasoningBuf. Removed AssistantMessageDelta handler (message deltas
don't need UX rendering — final content comes via AssistantMessage).

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

* UX: add blank line before spinner and change text to 'Working...'

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

* Check if plugin is installed before install/update

ensurePlugins now lists installed plugins first. If a required plugin
is already installed, it runs 'copilot plugin update' instead of a
full install. New plugins get 'copilot plugin install'.

Also restructured requiredPlugins as pluginSpec with Source (install
path) and Name (installed name for update command).

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

* Simplify init to single prompt + wire OnUserInputRequest handler

Two changes:

1. Wire OnUserInputRequest in CopilotAgentFactory:
   Enables the agent's built-in ask_user tool. When the agent asks a
   question, the handler renders it using azd's UX components:
   - Choices → ux.NewSelect() with azd styling
   - Freeform → ux.NewPrompt() for text input
   This lets the agent ask clarifying questions during execution
   (architecture choices, service selection, config options).

2. Simplify initAppWithAgent() from 6-step loop to single prompt:
   Replaces 6 hardcoded steps + inter-step feedback loops + post-
   completion summary aggregation with a single prompt that delegates
   to azure-prepare and azure-validate skills from the Azure plugin.
   The skills handle all orchestration internally and can ask the user
   questions via ask_user when needed.

   Removed: initStep struct, step definitions, collectAndApplyFeedback(),
   postCompletionSummary(), feedback import (~140 lines).

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

* UX: use intent as spinner text, move reasoning above spinner, set interactive mode

Three UX improvements:

1. Spinner text: uses assistant.intent events (short task descriptions
   like 'Analyzing project structure') instead of static 'Working...'
   Truncated to 80 chars to stay concise.

2. Reasoning display: moved above the spinner instead of below. Layout
   is now: blank line → reasoning (last 5 lines, gray) → blank line →
   spinner. More natural reading order.

3. Mode: SendMessage now explicitly sets Mode to 'interactive' so the
   agent asks for approval before executing tools.

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

* Strip markdown from ask_user prompts, render reasoning with markdown, fix skill triggers

Three changes:

1. ask_user: Strip markdown formatting (bold, italic, backticks, headings)
   from question text and choice labels before rendering in azd UX prompts.
   Choices still return the original value to the agent.

2. flushReasoning: Render accumulated reasoning with output.WithMarkdown()
   instead of raw gray text, giving proper formatting for code blocks,
   lists, and other markdown content.

3. init prompt: Use natural trigger phrases ('prepare this application
   for deployment to Azure', 'validate that everything is ready') that
   match the azure-prepare and azure-validate skill description triggers,
   instead of referencing skill names directly.

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

* Support AllowFreeform in ask_user with 'Other' choice option

When the agent sends choices with AllowFreeform=true, append an
'Other (type your own answer)' option to the Select list. If selected,
follow up with a freeform Prompt and return WasFreeform: true.

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

* Explicitly invoke @azure-prepare and @azure-validate skills in init prompt

Use @skill-name syntax and numbered steps to ensure both skills are
invoked in order. Previous natural language triggers may not have
reliably activated the skills.

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

* Specify 'azd' recipe for azure-prepare and azure-validate skills

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

* Store Copilot session files in .azure/copilot relative to cwd

Sets SessionConfig.ConfigDir to .azure/copilot in the current working
directory so session state is project-local instead of global ~/.copilot.

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

* Fix skill discovery: revert ConfigDir, use qualified skill names

ConfigDir override to .azure/copilot broke plugin discovery — the CLI
loads plugins from ConfigDir/installed-plugins/ which was empty. Reverted
to default ~/.copilot so installed plugins (and their skills) are found.
Set WorkingDirectory instead for tool operations.

Updated init prompt to use fully qualified azure:azure-prepare and
azure:azure-validate skill names (plugin:skill-name format).

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

* Pass installed plugins via --plugin-dir for headless mode discovery

The CLI in --headless --stdio mode (used by the SDK) doesn't auto-discover
installed plugins from ~/.copilot/installed-plugins/. This meant skills
from the Azure plugin were never loaded.

Fix: discoverInstalledPluginDirs() scans ~/.copilot/installed-plugins/
for plugin directories (verified by presence of skills/ or .claude-plugin/)
and passes each via --plugin-dir CLIArgs to the SDK client.

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

* Scope plugin discovery to Azure plugin only

Only load the microsoft/GitHub-Copilot-for-Azure plugin via --plugin-dir.
Checks both _direct and marketplace install paths. Other plugins will be
handled separately later.

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

* Use SkillDirectories instead of --plugin-dir for skill loading

The --plugin-dir CLI flag isn't supported by all copilot binary builds.
Instead, pass the Azure plugin's skills directory via SessionConfig.
SkillDirectories, which is sent via JSON-RPC createSession and works
reliably in headless mode.

discoverAzurePluginSkillDirs() finds the skills/ directory from the
installed Azure plugin and adds it to SkillDirectories alongside any
user-configured skill directories.

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

* Persist intent text in spinner instead of resetting to 'Thinking...'

Track lastIntent and reuse it when the spinner resets after turn start
or tool completion. Only falls back to 'Thinking...' if no intent has
been received yet.

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

* Add blank line after spinner

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

* Show relative paths for skill directories in tool summaries

toRelativePath now tries both cwd and ~/.copilot/installed-plugins/
as base directories. Paths under the plugins root (skill files) are
shown relative instead of absolute.

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

* Capture intent from report_intent tool calls for spinner text

The agent uses a report_intent tool (not assistant.intent events) to
signal what it's working on. Extract the intent text from the tool's
arguments and use it as the spinner text. The report_intent tool is
suppressed from UX display (no 'Ran report_intent' completion line).

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

* Show nested subagent tool calls with rich display

Subagent events now show richer UX:
- started: '◆ {DisplayName} — {Description}'
- completed: '✔ {DisplayName} completed' with summary
- failed: '✖ {DisplayName} failed: {error}'
- deselected: resets subagent state

Tool calls inside a subagent are indented with 2 spaces to show
nesting visually:
  ◆ Azure Prepare — Prepare apps for deployment
    ✔ Ran read_file with path: main.go
    ✔ Ran write_file with path: azure.yaml
  ✔ Azure Prepare completed

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

* UX polish: blank lines before skill/subagent, suppress internal tools

- Add blank line before 'Using skill:' and 'Delegating to:' lines
- Suppress tool call display for report_intent, ask_user, task, and
  skill: prefixed tools — these are internal/UX tools that shouldn't
  show as 'Ran X' completion lines

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

* Prompt for reasoning effort and model on first agent run

First run (no ai.agent.reasoningEffort config):
- Prompt for reasoning effort (low/medium/high) with cost guidance
- Prompt for model selection (default or specific model)
- Save both to azd config

Subsequent runs:
- Show info note with current model and reasoning level
- Point to 'azd config set' for changes

Also:
- Wire ai.agent.reasoningEffort to SessionConfig.ReasoningEffort
- Add reasoningEffort to config_options.yaml

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

* Fix: only signal idle when final content exists

Multiple session.idle events can arrive during a session (after
permission prompts, between tool calls). The first idle was consumed
by WaitForIdle before the assistant message arrived, causing the
display to clear with no summary shown.

Fix: only signal idleCh when finalContent has been set by an
assistant.message event. Early idle events are ignored.

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

* Load Azure plugin MCP servers into session config

Read .mcp.json from the installed Azure plugin and merge its MCP servers
(azure, foundry-mcp, context7) into SessionConfig.MCPServers alongside
built-in and user-configured servers.

Merge order: built-in → Azure plugin → user config (last wins).

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

* Suppress 'skill' tool from display, add blank line after Using skill

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

* Fix agent exit: reset finalContent per turn, add completion logging

Root cause: assistant.message from an earlier turn set finalContent,
then a session.idle between tool calls found hasContent=true and
signaled WaitForIdle prematurely — before the actual final message.

Fixes:
- Reset finalContent on every assistant.turn_start so only the last
  turn's message is considered
- Handle session.task_complete and session.shutdown as additional
  completion signals
- Add debug logging to assistant.message, session.idle, and
  WaitForIdle for future troubleshooting

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

* Add scoped system message and empty directory handling

System message (append mode):
- Scopes the agent to Azure application development only
- Rejects unrelated requests with a focused explanation
- User-configured ai.agent.systemMessage appended after default

Init prompt:
- Agent now checks if cwd is empty/has no code first
- If empty, asks user what type of Azure app to build
- Then proceeds with azure-prepare and azure-validate skills

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

* Consistent whitespace and improved config display

Whitespace rules:
- Skills, subagents, reasoning, errors, warnings: blank line before
  and after (via printSeparated, no double blanks)
- Tool completions: stack without blank lines (via printLine)
- lastPrintedBlank flag prevents duplicate blank lines

Config display:
- Split into multiple lines for readability
- Show model and reasoning on separate bullet points
- azd commands shown in highlight format

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

* Add blank lines before/after user prompts, fix extra blank in config display

- User prompts (ask_user): blank line before and after Select/Prompt
- Config display: remove leading blank line (was doubling up with
  prior output), remove trailing newline from alpha warning title

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

* Log MCP server details and skill dirs for debugging

Before session creation, log each configured MCP server (name, type,
command/url) and skill directory. Also capture session.info events
with AllowedTools list and skill.invoked events in the file logger.

Visible with AZD_DEBUG=true or in ~/.azd/logs/azd-agent-*.log.

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

* Print MCP servers and skill dirs to console output for debugging

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

* Print available tools to console from session events

Captures AllowedTools from the first session event that carries them
and prints the full list to console output for debugging.

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

* Fix MCP server config: use type 'local' and add tools wildcard

The SDK expects type='local' (not 'stdio') for local MCP servers,
and requires a 'tools' field listing which tools to expose. Without
tools=['*'], no MCP tools were made available to the agent.

Fixes:
- convertServerConfig: use type='local', add tools=['*']
- loadAzurePluginMCPServers: normalize plugin configs — set type
  to 'local' for command-based servers, add tools=['*'] if missing

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

* Add diagnostic prompt to list all tools including MCP server tools

Temporary: agent prints all available tools grouped by built-in, MCP
server tools, and skills before proceeding with init. Will remove once
MCP server integration is verified working.

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

* Dump first 30 session events with details for MCP debugging

Temporary: prints event type + key fields (allowedTools, tools, message,
infoType, name) for the first 30 events to diagnose MCP server loading.

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

* Remove diagnostic prompt, rely on event dump for tool debugging

The system message was causing the model to ignore the diagnostic
'list tools' request. Event dump from the factory provides better
programmatic visibility.

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

* Debugging: disable system message, add diagnostic tools list prompt, remove event dump

Temporary changes to diagnose MCP server tool availability:
- Comment out system message (was blocking diagnostic requests)
- Add explicit 'list all tools' prompt before init task
- Remove event dump from factory (wasn't showing useful data)

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

* Add post-init Q&A loop for follow-up questions

After the init summary, prompt the user 'Any questions?' in a loop.
User can ask follow-up questions (sent to the same agent session with
full context). Press Enter with no input to finish and exit.

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

* Add session resume support for cancelled/crashed agent sessions

On azd init with agent mode, checks for previous sessions in the
current directory via client.ListSessions(). If found, prompts user
to resume a previous session or start fresh.

Resume uses client.ResumeSession() which restores full conversation
history with the same MCP servers, skills, permissions, and hooks.

Changes:
- CopilotAgentFactory: add ListSessions() and Resume() methods
- init.go: add session picker before agent creation, add
  CopilotAgentFactory to initAction struct

Spec at docs/specs/copilot-agent-ux/session-resume.md

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

* Polish session resume UX: numbers, local time, truncated labels

- Show numbered choices in session picker (DisplayNumbers: true)
- Convert timestamps to local time (Today 3:04 PM, Yesterday, Jan 2)
- Truncate labels to ~120 chars total
- Shorter prompt: 'Previous sessions found:'

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

* Fix session labels: collapse newlines, enforce 120 char max

Summaries from sessions can contain newlines and markdown. Use
strings.Fields() to collapse all whitespace into single spaces,
then truncate to fit within 120 chars total.

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

* Enable line numbers and filtering on all Select prompts

Added DisplayNumbers and EnableFiltering to reasoning effort, model
selection, and session picker prompts.

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

* Dynamic model list from SDK with billing and reasoning metadata

Fetch models via ListModels() instead of hardcoding. Each option shows:
  'Claude Sonnet 4.5 (high) (1x)'
  — name, default reasoning effort, billing multiplier

Also:
- Remove trailing ':' from prompt messages (UX components add them)
- Add ListModels() to CopilotAgentFactory

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

* Show session usage metrics at end of init

Accumulate usage from assistant.usage and session.usage_info events:
input/output tokens, cost multiplier, premium requests, API duration,
and model used.

Display at session end:
  Session usage:
  • Model:            claude-sonnet-4.5
  • Input tokens:     45.2K
  • Output tokens:    12.8K
  • Total tokens:     58.0K
  • Cost:             1.0x premium
  • Premium requests: 15
  • API duration:     2m 34s

Token counts formatted as K/M for readability.

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

* Remove post-init Q&A loop

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

* Consolidate agent into self-contained CopilotAgent with factory

Major refactor: CopilotAgent is now a self-contained agent that
encapsulates initialization, session management, display, and usage.
CopilotAgentFactory creates agents with dependencies wired via IoC.

New API:
  agent, _ := factory.Create(ctx, agent.WithMode('interactive'))
  initResult, _ := agent.Initialize(ctx)
  selected, _ := agent.SelectSession(ctx)
  result, _ := agent.SendMessage(ctx, prompt, agent.WithSessionID(...))
  // result.Content, result.SessionID, result.Usage
  agent.Stop()

New types (types.go):
  AgentResult{Content, SessionID, Usage}
  InitResult{Model, ReasoningEffort, IsFirstRun}
  UsageMetrics with Format() method
  AgentOption: WithModel, WithReasoningEffort, WithMode, WithDebug
  SendOption: WithSessionID
  InitOption: WithForcePrompt

Agent methods:
  Initialize() — config prompts (first run), plugin install, client start
  SelectSession() — UX picker for session resume
  ListSessions() — raw session listing
  SendMessage() / SendMessageWithRetry() — returns AgentResult
  Stop() — cleanup

Deleted (old langchaingo agent):
  agent.go, agent_factory.go, conversational_agent.go, prompts/

Simplified:
  init.go — ~40 lines instead of ~150
  container.go — removed old AgentFactory, ModelFactory registrations
  error.go — updated to use CopilotAgentFactory

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

* Fix spacing: add blank line after each prompt, remove leading blanks

Each prompt component (Select, Prompt) now adds a blank line after
its Ask() call. Callers manage spacing before prompts. Removed the
leading blank line from SelectSession (was doubling up with the
trailing blank from the previous prompt).

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

* Add unit tests for types.go and display.go pure functions

New test files:
- types_test.go: UsageMetrics.Format(), TotalTokens(), formatTokenCount,
  stripMarkdown, formatSessionTime (40 test cases)
- display_test.go: extractToolInputSummary, extractIntentFromArgs,
  toRelativePath, GetUsageMetrics accumulation

All pure functions tested without SDK mocking.

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

* Fix billing display and CI checks

- Rename Cost to BillingRate (per-request multiplier, not cumulative)
- Show premium requests from SDK (omit if not reported)
- Handle session.shutdown for TotalPremiumRequests
- Remove manual API call counter
- Fix all lint issues (errorlint, gosec, staticcheck, unused)
- Fix formatting, remove unused code (github_copilot_registration files,
  discoverInstalledPluginDirs)
- All CI checks pass: gofmt, golangci-lint, tests

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

* Address PR review feedback

- Fix config_options.yaml: tools.available/excluded type 'object' -> 'array'
- Add missing config docs: ai.agent.skills.directories, ai.agent.skills.disabled
- Log warning on userConfigManager.Load() failure instead of silently swallowing
- Simplify redundant MCP server unmarshaling (removed type probe, single path)

Other review items (r3, r6, r8, r9) referenced old code that was deleted
in the agent consolidation commit.

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

* Address second round PR review feedback

Bug fixes:
- Fix nil pointer dereferences in error.go when agentResult is nil
- Fix session.error not unblocking WaitForIdle (signal idleCh on error)
- Fix consent check fails-open: deny on error instead of allow

Security:
- PreToolUse consent check now denies on error with logged reason
- OnPermissionRequest remains approve-all (CLI-level coarse permissions,
  fine-grained control via PreToolUse hooks)

Code quality:
- Deterministic cleanup order: changed from map to ordered slice with
  reverse teardown (session events -> file logger -> client)
- Log warning on config load failure
- Simplified MCP server unmarshaling

Config:
- Added skills.directories and skills.disabled to config_options.yaml
- Fixed tools.available/excluded type from object to array

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

* Add AgentMode enum, remove redundant logging infrastructure

AgentMode:
- New AgentMode type with constants: AgentModeInteractive, AgentModeAutopilot,
  AgentModePlan. WithMode() now takes AgentMode instead of string.

Removed logging (redundant with Copilot CLI logs at ~/.copilot/logs/):
- thought_logger.go — old langchaingo callback handler
- file_logger.go — old langchaingo callback handler
- chained_handler.go — old langchaingo callback handler
- session_event_handler.go — SessionEventLogger, SessionFileLogger,
  CompositeEventHandler all unused after AgentDisplay consolidation
- session_event_handler_test.go

Kept: logging/util.go with TruncateString (used by display.go)

Net: 902 lines deleted.

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

* Fix: return originalError when user declines fix in error middleware

When the user declines the agent fix, the code returned 'err' which
was nil (consent check succeeded), silently swallowing the original
command failure. Now returns originalError to preserve the error.

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

* Document permission handler intent and relationship to PreToolUse

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

* Remove docs/specs/copilot-agent-ux (deleted by reviewer)

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

* Delete dead agent tools and unused consent wrapper

Deleted (all replaced by Copilot CLI built-in tools):
- tools/dev/ — command executor (shell tool)
- tools/io/ — 12 file/directory tools + tests
- tools/common/ — AnnotatedTool interface, ToLangChainTools, ToolLoader
- tools/loader.go — composite tool loader
- tools/mcp/tool_adapter.go — MCP-to-langchaingo adapter
- tools/mcp/sampling_handler.go — MCP sampling handler
- tools/mcp/elicitation_handler.go — MCP elicitation handler
- tools/mcp/loader.go — MCP tool loader
- consent/consent_wrapper_tool.go — langchaingo tool wrapper

Cleaned:
- Removed WrapTool/WrapTools from ConsentManager interface and impl
- Removed common package import from consent
- Kept tools/mcp/embed.go with McpJson embed (still used by factory)

Net: 7,025 lines deleted.

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

* Remove unused ExecutingTool and related global state from consent

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

* Fix MCP functional tests for remaining tools

Updated test expectations after MCP tool migration to Copilot SDK skills.
Tests now verify error_troubleshooting, provision_common_error, and
validate_azure_yaml (the 3 remaining MCP tools).

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

* Fix init.go: remove stray backtick, highlight azd up command

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

* Apply go fix for Go 1.26 compatibility

- intPtr() -> new() for pointer creation
- strings.Split -> strings.SplitSeq for range iteration
- strings.HasPrefix+TrimPrefix -> strings.CutPrefix
- floatPtr/strPtr helpers replaced with new() in tests

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

* Remove unused intPtr, floatPtr, strPtr helpers (inlined by go fix)

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

* refactor: migrate to copilot.* namespace, delete pkg/llm, streamline error middleware

- Migrate all config keys from ai.agent.* to copilot.* namespace
- Move copilot_client.go and session_config.go to internal/agent/copilot/
- Delete entire pkg/llm/ package (azure_openai, ollama, github_copilot, model_factory, manager)
- Move consent commands from azd mcp consent to azd copilot consent
- Streamline error middleware: single consent prompt + agent-driven troubleshooting
- Troubleshooting prompts in embedded Go text templates
- AgentDisplay: render AssistantMessage in real-time, red x for failed tools
- Remove Content from AgentResult, delete dead feedback package
- Adopt SDK bundler for CLI binary embedding, remove npm path scanning
- Clean up CI pipelines: remove ghCopilot build tag and ldflags
- Add WithSystemMessage AgentOption
- Add composable config key constants with ConfigRoot prefix
- Remove langchaingo dependency

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

* feat: on-demand Copilot CLI download, replace SDK bundler

- Add CopilotCLI managed tool (internal/agent/copilot/cli.go) following Bicep pattern
- Download platform-specific CLI from npm registry on first use
- Cache at ~/.azd/bin/copilot-cli-{version}, override via AZD_COPILOT_CLI_PATH
- Implement tools.ExternalTool interface (Name, InstallUrl, CheckInstalled)
- Integrate with CopilotClientManager (resolves CLI at Start time)
- Remove SDK bundler (zcopilot_* files, go tool bundler CI step, tool dep)
- Binary size reduced ~106MB (no longer embedded)
- Fix cspell: add agentcopilot to word list, reword comment

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

* fix: copilot display deadlocks, consent persistence, and UX improvements

- Fix WaitForIdle hang when SessionIdle fires before AssistantMessage
- Fix ticker vs Pause() TOCTOU race causing spinner to render over consent prompts
- Fix consent grant errors silently denying tool execution
- Add error logging for consent rule load/save failures
- Improve tool completion display with contextual verbs and diff stats
- Add tree-style sub-detail for shell commands and MCP tool args
- Add colored diff stats (green +N / red -N) for edit/create tools
- Show plugin version in skill invocation display
- Normalize whitespace spacing via printSeparated for all section transitions
- Show error messages on tool call failures
- Add comprehensive unit tests for display helpers and consent persistence

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

* chore: commit pending branch changes for CI

Includes copilot CLI plugin management (ListPlugins, InstallPlugin),
session time formatting, consent command migration, and azdcontext updates.

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

* fix: move file watcher to background goroutine to prevent WSL hang

On WSL with Windows filesystem mounts (/mnt/c/...), fsnotify's
filepath.Walk + inotify watch setup can hang or be extremely slow.
Moving NewWatcher() to a background goroutine prevents it from
blocking SendMessage. The watcher results are still collected at
cleanup via mutex-protected access.

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

* feat: add auth check with interactive login, fix spinner verb, fix E2E test

- Check GitHub Copilot auth status before session creation
- Prompt to sign in via copilot login (OAuth device flow) if not authenticated
- Add CopilotCLI.Login() wrapper for interactive copilot login command
- Fix spinner showing 'Running Ran tool' — use tool name for spinner, verb for completion
- Fix E2E test: add OnPermissionRequest: ApproveAll to session config
- Add ErrToolExecutionSkipped to excluded errors list in error mapping test
- Add unit tests for Login success and error cases

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

Phase 1: Add Copilot SDK Foundation (new code alongside existing)

8 participants