Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions cli/azd/cmd/copilot.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/internal/agent/consent"
agentcopilot "github.com/azure/azure-dev/cli/azd/internal/agent/copilot"
"github.com/azure/azure-dev/cli/azd/pkg/config"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/output"
Expand All @@ -24,8 +25,10 @@ import (
func copilotActions(root *actions.ActionDescriptor) *actions.ActionDescriptor {
group := root.Add("copilot", &actions.ActionDescriptorOptions{
Command: &cobra.Command{
Use: "copilot",
Short: fmt.Sprintf("Manage Copilot agent settings. %s", output.WithWarningFormat("(Alpha)")),
Use: "copilot",
Short: fmt.Sprintf(
"Manage %s agent settings. %s",
agentcopilot.DisplayTitle, output.WithWarningFormat("(Preview)")),
},
GroupingOptions: actions.CommandGroupOptions{
RootLevelHelp: actions.CmdGroupAlpha,
Expand Down
36 changes: 23 additions & 13 deletions cli/azd/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ func (i *initAction) Run(ctx context.Context) (*actions.ActionResult, error) {

if i.featuresManager.IsEnabled(agentcopilot.FeatureCopilot) {
followUp += fmt.Sprintf("\n\n%s Run %s to deploy project to the cloud.",
color.HiMagentaString("Next steps:"),
output.WithHintFormat("(→) NEXT STEPS:"),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Future nit - think it's worth collecting patterns like (→) in output to help us evolve our whole UX easier as we kick the tires with things like TUIs?

output.WithHighLightFormat("azd up"))
}

Expand Down Expand Up @@ -409,7 +409,11 @@ func (i *initAction) initAppWithAgent(ctx context.Context, azdCtx *azdcontext.Az
if dirty {
defaultNo := false
confirm := uxlib.NewConfirm(&uxlib.ConfirmOptions{
Message: "Your working directory has uncommitted changes. Continue initializing?",
Message: "Your working directory has uncommitted changes. Continue initializing?",
HelpMessage: fmt.Sprintf(
"%s may create or modify files in your working directory. "+
"Consider committing or stashing your changes first to avoid losing work.",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe drop the "losing work" blurb because there are so many other reasons you might want to do this (easily seeing all the changes in a diff, trying again with a higher reasoning effort, ...)?

Suggested change
"Consider committing or stashing your changes first to avoid losing work.",
"Consider committing or stashing your changes first.",

agentcopilot.DisplayTitle),
DefaultValue: &defaultNo,
})
result, promptErr := confirm.Ask(ctx)
Expand All @@ -422,12 +426,14 @@ func (i *initAction) initAppWithAgent(ctx context.Context, azdCtx *azdcontext.Az
}
}

// Show alpha warning
// Show preview notice
i.console.MessageUxItem(ctx, &ux.MessageTitle{
Title: fmt.Sprintf("Agentic mode init is in preview. The agent will scan your repository and "+
"attempt to make an azd-ready template to init.\nYou can always change permissions later "+
"by running %s. Mistakes may occur in agent mode.\n\n"+
"To learn more, go to %s",
Title: fmt.Sprintf(
"%s will scan your repository and help generate an azd compatible project to get you started. "+
"This experience is currently in preview.\n\n"+
"You can always change permissions later by running %s.\n\n"+
"To learn more, go to %s",
agentcopilot.DisplayTitle,
output.WithHighLightFormat("azd copilot consent"),
output.WithLinkFormat("https://aka.ms/azd-feature-stages")),
})
Expand Down Expand Up @@ -486,7 +492,9 @@ func (i *initAction) initAppWithAgent(ctx context.Context, azdCtx *azdcontext.Az
timeDisplay := agent.FormatSessionTime(session.StartedAt)
defaultYes := true
confirm := uxlib.NewConfirm(&uxlib.ConfirmOptions{
Message: fmt.Sprintf("Resume previous session from %s?", timeDisplay),
Message: fmt.Sprintf("Resume previous session from %s?", timeDisplay),
HelpMessage: "Resuming continues where you left off. " +
"Choosing no starts a fresh session.",
Comment on lines +496 to +497
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Future idea - Might be worth very briefly summarizing what's in that session? A session from 30 seconds ago is obvious, but if I got interrupted last Friday I might not remember what that includes.

DefaultValue: &defaultYes,
})
if result, err := confirm.Ask(ctx); err == nil && result != nil && *result {
Expand Down Expand Up @@ -561,7 +569,7 @@ func promptInitType(
options := []string{
"Scan current directory", // This now covers minimal project creation too
"Select a template",
fmt.Sprintf("Use agent mode %s", color.YellowString("(Alpha)")),
fmt.Sprintf("Set up with %s %s", agentcopilot.DisplayTitle, color.YellowString("(Preview)")),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit - Might be worth getting slightly more descriptive than Set up for folks who are hesitant about letting an agent run wild? Maybe

Suggested change
fmt.Sprintf("Set up with %s %s", agentcopilot.DisplayTitle, color.YellowString("(Preview)")),
fmt.Sprintf("Create a project with %s %s", agentcopilot.DisplayTitle, color.YellowString("(Preview)")),

}

selection, err := console.Select(ctx, input.ConsoleOptions{
Expand Down Expand Up @@ -594,8 +602,8 @@ func promptInitType(
return initUnknown, fmt.Errorf("failed to save config: %w", err)
}

console.Message(ctx, "\nThe azd agent feature has been enabled to support this new experience."+
" To turn off in the future run `azd config unset alpha.llm`.")
console.Message(ctx, fmt.Sprintf("\n%s has been enabled to support this new experience."+
" To turn off in the future run `azd config unset alpha.llm`.", agentcopilot.DisplayTitle))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit - worth adding something like agentcopilot.ConfigKeyErrorHandlingFix for the flag (mostly so it's easier to remove these once we leave preview)?


err = azdConfig.Set(agentcopilot.ConfigKeyModelType, "copilot")
if err != nil {
Expand All @@ -607,8 +615,10 @@ func promptInitType(
return initUnknown, fmt.Errorf("failed to save config: %w", err)
}

console.Message(ctx, fmt.Sprintf("\nGitHub Copilot has been enabled to support this new experience."+
" To turn off in the future run `azd config unset %s`.", agentcopilot.ConfigKeyModelType))
console.Message(ctx, fmt.Sprintf(
"\n%s has been enabled to support this new experience."+
" To turn off in the future run `azd config unset %s`.",
agentcopilot.DisplayTitle, agentcopilot.ConfigKeyModelType))
}

return initWithAgent, nil
Expand Down
15 changes: 9 additions & 6 deletions cli/azd/cmd/middleware/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/tools/maven"
"github.com/azure/azure-dev/cli/azd/pkg/tools/pack"
uxlib "github.com/azure/azure-dev/cli/azd/pkg/ux"
"github.com/fatih/color"
"go.opentelemetry.io/otel/codes"
)

Expand Down Expand Up @@ -262,7 +261,8 @@ func (e *ErrorMiddleware) Run(ctx context.Context, next NextFn) (*actions.Action
troubleshootPrompt := e.buildTroubleshootingPrompt(originalError)

previousError = originalError
e.console.Message(ctx, color.MagentaString("Preparing Copilot to troubleshoot error..."))
e.console.Message(ctx, output.WithHintFormat(
"Preparing %s to troubleshoot error...", agentcopilot.DisplayTitle))
agentResult, err := azdAgent.SendMessage(ctx, troubleshootPrompt)

if err != nil {
Expand Down Expand Up @@ -337,7 +337,8 @@ func (e *ErrorMiddleware) promptTroubleshootConsent(ctx context.Context) (bool,
// Check for saved "always allow" preference
if val, ok := userConfig.GetString(agentcopilot.ConfigKeyErrorHandlingFix); ok && val == "allow" {
e.console.Message(ctx, output.WithWarningFormat(
"Agent troubleshooting is set to always allow. To change, run %s.\n",
"%s troubleshooting is set to always allow. To change, run %s.\n",
agentcopilot.DisplayTitle,
output.WithHighLightFormat(
fmt.Sprintf("azd config unset %s", agentcopilot.ConfigKeyErrorHandlingFix)),
))
Expand All @@ -347,7 +348,8 @@ func (e *ErrorMiddleware) promptTroubleshootConsent(ctx context.Context) (bool,
// Check for saved "always skip" preference
if val, ok := userConfig.GetString(agentcopilot.ConfigKeyErrorHandlingTroubleshootSkip); ok && val == "allow" {
e.console.Message(ctx, output.WithWarningFormat(
"Agent troubleshooting is set to always skip. To change, run %s.\n",
"%s troubleshooting is set to always skip. To change, run %s.\n",
agentcopilot.DisplayTitle,
output.WithHighLightFormat(
fmt.Sprintf("azd config unset %s", agentcopilot.ConfigKeyErrorHandlingTroubleshootSkip)),
))
Expand All @@ -362,10 +364,11 @@ func (e *ErrorMiddleware) promptTroubleshootConsent(ctx context.Context) (bool,
}

selector := uxlib.NewSelect(&uxlib.SelectOptions{
Message: "Would you like the agent to troubleshoot this error?",
Message: fmt.Sprintf("Would you like %s to troubleshoot this error?", agentcopilot.DisplayTitle),
HelpMessage: fmt.Sprintf(
"The agent will explain the error and offer to fix it. "+
"%s will explain the error and offer to fix it. "+
"Edit permissions anytime by running %s.",
agentcopilot.DisplayTitle,
output.WithHighLightFormat("azd copilot consent")),
Choices: choices,
EnableFiltering: new(false),
Expand Down
4 changes: 2 additions & 2 deletions cli/azd/cmd/testdata/TestFigSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1568,7 +1568,7 @@ const completionSpec: Fig.Spec = {
},
{
name: ['copilot'],
description: 'Manage Copilot agent settings. (Alpha)',
description: 'Manage GitHub Copilot agent settings. (Preview)',
subcommands: [
{
name: ['consent'],
Expand Down Expand Up @@ -3409,7 +3409,7 @@ const completionSpec: Fig.Spec = {
},
{
name: ['copilot'],
description: 'Manage Copilot agent settings. (Alpha)',
description: 'Manage GitHub Copilot agent settings. (Preview)',
subcommands: [
{
name: ['consent'],
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/cmd/testdata/TestUsage-azd-copilot.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

Manage Copilot agent settings. (Alpha)
Manage GitHub Copilot agent settings. (Preview)

Usage
azd copilot [command]
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/cmd/testdata/TestUsage-azd.snap
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Commands
template : Find and view template details.

Enabled alpha commands
copilot : Manage Copilot agent settings. (Alpha)
copilot : Manage GitHub Copilot agent settings. (Preview)
mcp : Manage Model Context Protocol (MCP) server. (Alpha)

Enabled extensions commands
Expand Down
6 changes: 3 additions & 3 deletions cli/azd/internal/agent/copilot/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,13 @@ func (c *CopilotCLI) ensureInstalled(ctx context.Context) error {
return fmt.Errorf("creating copilot CLI directory: %w", err)
}

c.console.ShowSpinner(ctx, "Downloading Copilot CLI", input.Step)
c.console.ShowSpinner(ctx, "Downloading GitHub Copilot CLI", input.Step)
err := downloadCopilotCLI(ctx, c.transporter, cliVersion, cliPath)
if err != nil {
c.console.StopSpinner(ctx, "Downloading Copilot CLI", input.StepFailed)
c.console.StopSpinner(ctx, "Downloading GitHub Copilot CLI", input.StepFailed)
return fmt.Errorf("downloading copilot CLI: %w", err)
}
c.console.StopSpinner(ctx, "Downloading Copilot CLI", input.StepDone)
c.console.StopSpinner(ctx, "Downloading GitHub Copilot CLI", input.StepDone)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One minor thing that bugs me is telling people we're downloading this when they already probably have it installed. Feels confusing if you don't know about the layering of the Copilot SDK. Is it worth changing this to something more like Downloading Github Copilot SDK or Downloading Github Copilot SDK resources?

c.console.Message(ctx, "")
}

Expand Down
4 changes: 4 additions & 0 deletions cli/azd/internal/agent/copilot/config_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ package copilot
// Config key constants for the copilot.* namespace in azd user configuration.
// All keys are built from shared prefix constants so renaming any level requires a single change.
const (
// DisplayTitle is the user-facing brand name for the agent experience.
// Change this single constant to rebrand across all UI text.
DisplayTitle = "GitHub Copilot"

// ConfigRoot is the root namespace for all Copilot agent configuration keys.
ConfigRoot = "copilot"

Expand Down
8 changes: 4 additions & 4 deletions cli/azd/internal/agent/copilot/copilot_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ func (m *CopilotClientManager) Start(ctx context.Context) error {
if err := m.client.Start(ctx); err != nil {
log.Printf("[copilot-client] Start failed: %v", err)
return fmt.Errorf(
"failed to start Copilot agent runtime: %w",
err,
"failed to start %s agent runtime: %w",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we want still want agent runtime in there?

Suggested change
"failed to start %s agent runtime: %w",
"failed to start %s: %w",

DisplayTitle, err,
)
}
log.Printf("[copilot-client] Started successfully (state=%s)", m.client.State())
Expand All @@ -95,7 +95,7 @@ func (m *CopilotClientManager) Client() *copilot.Client {
func (m *CopilotClientManager) GetAuthStatus(ctx context.Context) (*copilot.GetAuthStatusResponse, error) {
status, err := m.client.GetAuthStatus(ctx)
if err != nil {
return nil, fmt.Errorf("failed to check Copilot auth status: %w", err)
return nil, fmt.Errorf("failed to check %s auth status: %w", DisplayTitle, err)
}
return status, nil
}
Expand All @@ -104,7 +104,7 @@ func (m *CopilotClientManager) GetAuthStatus(ctx context.Context) (*copilot.GetA
func (m *CopilotClientManager) ListModels(ctx context.Context) ([]copilot.ModelInfo, error) {
models, err := m.client.ListModels(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list Copilot models: %w", err)
return nil, fmt.Errorf("failed to list %s models: %w", DisplayTitle, err)
}
return models, nil
}
Expand Down
5 changes: 3 additions & 2 deletions cli/azd/internal/agent/copilot/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ func IsFeatureEnabled(alphaManager *alpha.FeatureManager) error {
panic("alphaManager cannot be nil")
}
if !alphaManager.IsEnabled(FeatureCopilot) {
return fmt.Errorf("the Copilot agent feature is not enabled. Please enable it using the command: \"%s\"",
alpha.GetEnableCommand(FeatureCopilot))
return fmt.Errorf(
"the %s feature is not enabled. Please enable it using the command: \"%s\"",
DisplayTitle, alpha.GetEnableCommand(FeatureCopilot))
}
return nil
}
13 changes: 12 additions & 1 deletion cli/azd/internal/agent/copilot/session_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,18 @@ func (b *SessionConfigBuilder) Build(

Do not respond to requests unrelated to application development, Azure services, or deployment.
For unrelated requests, briefly explain that you are focused on Azure application development
and suggest the user use a general-purpose assistant for other topics.`
and suggest the user use a general-purpose assistant for other topics.

When prompting the user to choose Azure subscriptions, regions, or resources, follow these guidelines:
- Use short, focused prompts (e.g., "Which subscription would you like to use?") paired with
well-formatted choices via the ask_user tool with the choices field.
- Do NOT embed long inline lists of options inside a text message with an open-ended question.
- Keep prompt messages concise — move details into choices, not the question text.
- Format Azure subscriptions as: <Subscription Name> (<subscription-id>)
Example: "My Dev Subscription (a1b2c3d4-e5f6-7890-abcd-ef1234567890)"
- Format Azure regions as: <Full Region Name> (<region-short-name>)
Example: "East US 2 (eastus2)"
- Always use actual entity names and identifiers from Azure APIs, never placeholders.`

if msg, ok := userConfig.GetString(ConfigKeySystemMessage); ok && msg != "" {
systemContent += "\n\n" + msg
Expand Down
40 changes: 26 additions & 14 deletions cli/azd/internal/agent/copilot_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,10 @@ func (a *CopilotAgent) createUserInputHandler(ctx context.Context) copilot.UserI

selected := choices[*idx].Value
if selected == freeformValue {
prompt := uxlib.NewPrompt(&uxlib.PromptOptions{Message: question})
prompt := uxlib.NewPrompt(&uxlib.PromptOptions{
Message: question,
IgnoreHintKeys: true,
})
answer, err := prompt.Ask(ctx)
fmt.Println()
if err != nil {
Expand All @@ -779,7 +782,12 @@ func (a *CopilotAgent) createUserInputHandler(ctx context.Context) copilot.UserI
return copilot.UserInputResponse{Answer: selected}, nil
}

prompt := uxlib.NewPrompt(&uxlib.PromptOptions{Message: question})
// TODO: IgnoreHintKeys should not be needed — Prompt should auto-suppress
// hint key handling when no HelpMessage is provided.
prompt := uxlib.NewPrompt(&uxlib.PromptOptions{
Message: question,
IgnoreHintKeys: true,
})
answer, err := prompt.Ask(ctx)
fmt.Println()
if err != nil {
Expand Down Expand Up @@ -850,11 +858,14 @@ func (a *CopilotAgent) ensureAuthenticated(ctx context.Context) error {

// Not authenticated — prompt to sign in
a.console.Message(ctx, "")
a.console.Message(ctx, output.WithWarningFormat("Not authenticated with GitHub Copilot"))
a.console.Message(ctx, output.WithWarningFormat("Not authenticated with %s", agentcopilot.DisplayTitle))
a.console.Message(ctx, "")

confirm := uxlib.NewConfirm(&uxlib.ConfirmOptions{
Message: "Sign in to GitHub Copilot? (opens browser)",
Message: fmt.Sprintf("Sign in to %s? (opens browser)", agentcopilot.DisplayTitle),
HelpMessage: fmt.Sprintf(
"%s requires GitHub authentication to access AI models and agent capabilities.",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit - maybe drop Github auth here because it ends up being called Github Copilot auth/sign in below?

Suggested change
"%s requires GitHub authentication to access AI models and agent capabilities.",
"%s requires authentication to access AI models and agent capabilities.",

agentcopilot.DisplayTitle),
DefaultValue: uxlib.Ptr(true),
})

Expand All @@ -864,18 +875,18 @@ func (a *CopilotAgent) ensureAuthenticated(ctx context.Context) error {
}

if shouldLogin == nil || !*shouldLogin {
return fmt.Errorf("GitHub Copilot authentication is required to continue")
return fmt.Errorf("%s authentication is required to continue", agentcopilot.DisplayTitle)
}

a.console.Message(ctx, "")
if err := a.cli.Login(ctx); err != nil {
return fmt.Errorf("GitHub Copilot sign-in failed: %w", err)
return fmt.Errorf("%s sign-in failed: %w", agentcopilot.DisplayTitle, err)
}

// Verify auth succeeded
authStatus, err = a.clientManager.GetAuthStatus(ctx)
if err != nil || !authStatus.IsAuthenticated {
return fmt.Errorf("GitHub Copilot authentication was not completed")
return fmt.Errorf("%s authentication was not completed", agentcopilot.DisplayTitle)
}

a.console.Message(ctx, "")
Expand All @@ -889,7 +900,7 @@ func (a *CopilotAgent) ensurePlugins(ctx context.Context) {
if _, err := exec.LookPath("copilot"); err != nil {
log.Printf("[copilot] 'copilot' CLI not found in PATH — skipping plugin management")
a.console.Message(ctx, output.WithWarningFormat(
"The Copilot CLI is not installed. Some features may be limited.\n"+
"The GitHub Copilot CLI is not installed. Some features may be limited.\n"+
"Install it with: npm install -g @github/copilot"))
return
}
Expand Down Expand Up @@ -933,12 +944,13 @@ func (a *CopilotAgent) promptPluginInstall(ctx context.Context, plugin pluginSpe

a.console.Message(ctx, "")
confirm := uxlib.NewConfirm(&uxlib.ConfirmOptions{
Message: fmt.Sprintf("The %s plugin is not installed. Would you like to install it?", plugin.Name),
HelpMessage: fmt.Sprintf(
"The %s plugin provides Azure-specific skills for infrastructure generation, "+
"project validation, and deployment guidance. Without it, the agent will have "+
"limited Azure capabilities. The plugin is installed globally at ~/.copilot/installed-plugins/.",
plugin.Name),
Message: fmt.Sprintf(
"%s works better with the %s plugin. Would you like to install it?",
agentcopilot.DisplayTitle, plugin.Name),
HelpMessage: "The Azure plugin provides:\n" +
"• Azure MCP server that contains additional tools for Azure\n" +
"• Skills that streamline and provide better results for creating, " +
"validating, and deploying applications to Azure",
DefaultValue: &defaultYes,
})

Expand Down
8 changes: 0 additions & 8 deletions cli/azd/pkg/output/colors.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,6 @@ func WithBackticks(s string) string {
return fmt.Sprintf("`%s`", s)
}

func AzdLabel() string {
return "[azd]"
}

func AzdAgentLabel() string {
return color.HiMagentaString(fmt.Sprintf("🤖 %s Agent", AzdLabel()))
}

// WithMarkdown converts markdown to terminal-friendly colorized output using glamour.
// This provides rich markdown rendering including bold, italic, code blocks, headers, etc.
func WithMarkdown(markdownText string) string {
Expand Down
Loading
Loading