Skip to content
Open
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
114 changes: 70 additions & 44 deletions cli/azd/cmd/middleware/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,45 +168,56 @@ func (e *ErrorMiddleware) Run(ctx context.Context, next NextFn) (*actions.Action
}

if troubleshootScope != "" {
var troubleshootPrompt string
switch troubleshootScope {
case "explain":
troubleshootPrompt = fmt.Sprintf(
`Steps to follow:
troubleshootPrompt := fmt.Sprintf(
`Steps to follow:
1. Use available tools including azd_error_troubleshooting tool to identify this error.
2. Provide a concise explanation of the error and its root cause in 3-5 sentences.
DO NOT return JSON. Use plain text with minimal markdown formatting.
2. Provide a concise explanation using these two sections with bold markdown titles:
**What's happening**: Explain what the error is in 1-2 sentences.
**Why it's happening**: Explain the root cause in 1-3 sentences.
DO NOT return JSON. Use plain text with minimal markdown formatting beyond the section titles.
Do not provide fix steps. Do not perform any file changes.
Error details: %s`, errorInput)
case "guide":
troubleshootPrompt = fmt.Sprintf(
`Steps to follow:

agentOutput, err := azdAgent.SendMessage(ctx, troubleshootPrompt)

if err != nil {
e.displayAgentResponse(ctx, agentOutput, AIDisclaimer)
span.SetStatus(codes.Error, "agent.send_message.failed")
return nil, err
}

e.displayAgentResponse(ctx, agentOutput, AIDisclaimer)

// Ask user if they want step-by-step fix guidance
wantGuide, err := e.promptForFixGuidance(ctx)
if err != nil {
span.SetStatus(codes.Error, "agent.guidance.prompt.failed")
return nil, fmt.Errorf("prompting for fix guidance: %w", err)
}

if !wantGuide {
span.SetStatus(codes.Ok, "agent.troubleshoot.explain_only")
return actionResult, originalError
}

// Generate step-by-step fix guidance
guidePrompt := fmt.Sprintf(
`Steps to follow:
1. Use available tools including azd_error_troubleshooting tool to identify this error.
2. Provide only the actionable fix steps as a short numbered list (max 5 steps).
Each step should be one sentence. Include exact commands if applicable.
DO NOT return JSON. Do not explain the error. Do not perform any file changes.
Error details: %s`, errorInput)
Comment on lines +204 to 210
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

The guide prompt tells the agent to use azd_error_troubleshooting again even though the immediately preceding explanation step likely already invoked the tool in the same conversation. This can increase latency/cost and may re-introduce verbosity; consider instructing the agent to reuse prior context/tool results and only call the troubleshooting tool again if needed.

Copilot uses AI. Check for mistakes.
case "summarize":
troubleshootPrompt = fmt.Sprintf(
`Steps to follow:
1. Use available tools including azd_error_troubleshooting tool to identify this error.
2. Provide a brief summary with two sections:
**Error**: 1-2 sentence explanation of the root cause.
**Fix**: Numbered list of up to 3 actionable steps (one sentence each).
Keep the total response under 10 lines.
DO NOT return JSON. Do not perform any file changes.
Error details: %s`, errorInput)
}

agentOutput, err := azdAgent.SendMessage(ctx, troubleshootPrompt)
guideOutput, err := azdAgent.SendMessage(ctx, guidePrompt)

if err != nil {
e.displayAgentResponse(ctx, agentOutput, AIDisclaimer)
e.displayAgentResponse(ctx, guideOutput, AIDisclaimer)
span.SetStatus(codes.Error, "agent.send_message.failed")
return nil, err
}

e.displayAgentResponse(ctx, agentOutput, AIDisclaimer)
e.displayAgentResponse(ctx, guideOutput, AIDisclaimer)
}

// Ask user if they want to let AI fix the error
Expand Down Expand Up @@ -427,7 +438,7 @@ func promptForErrorHandlingConsent(

// promptTroubleshootingWithConsent combines consent and scope selection into a single prompt.
// Checks if a saved preference exists (e.g. mcp.errorHandling.troubleshooting.explain).
// Returns the scope ("explain", "fix", "summary") or "" if skipped.
// Returns the scope ("explain") or "" if skipped.
Comment on lines 439 to +441
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

This function-level comment is now inaccurate: promptTroubleshootingWithConsent no longer checks for saved scope preferences like "...troubleshooting.explain" (it only honors the saved skip preference). Please update the comment to reflect the new behavior to avoid future confusion.

Copilot uses AI. Check for mistakes.
func (e *ErrorMiddleware) promptTroubleshootingWithConsent(ctx context.Context) (string, error) {
const configPrefix = "mcp.errorHandling.troubleshooting"

Expand All @@ -436,30 +447,18 @@ func (e *ErrorMiddleware) promptTroubleshootingWithConsent(ctx context.Context)
return "", fmt.Errorf("failed to load user config: %w", err)
}

// Check for saved "always" preferences
scopes := []string{"explain", "guide", "summarize", "skip"}
for _, scope := range scopes {
if val, ok := userConfig.GetString(configPrefix + "." + scope); ok && val == "allow" {
e.console.Message(ctx, output.WithWarningFormat(
"Troubleshooting scope is set to always %q. To change, run %s.\n",
scope,
output.WithHighLightFormat(fmt.Sprintf("azd config unset %s.%s", configPrefix, scope)),
))
if scope == "skip" {
return "", nil
}
return scope, nil
}
// Check for saved "always skip" preference
if val, ok := userConfig.GetString(configPrefix + ".skip"); ok && val == "allow" {
e.console.Message(ctx, output.WithWarningFormat(
"Troubleshooting is set to always skip. To change, run %s.\n",
output.WithHighLightFormat(fmt.Sprintf("azd config unset %s.skip", configPrefix)),
))
return "", nil
}

Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

promptTroubleshootingWithConsent no longer auto-returns saved scopes like "guide"/"summarize" from config, but there are existing unit tests in cli/azd/cmd/middleware/error_test.go that assert those saved scopes are returned. Unless the tests are updated/removed, CI will fail (the function will now attempt to prompt interactively). Please update the tests to reflect the new supported scopes and add coverage for promptForFixGuidance / the two-step flow.

Suggested change
// Check for saved troubleshooting scope (e.g. "explain") and honor it without prompting.
if val, ok := userConfig.GetString(configPrefix + ".explain"); ok && val == "allow" {
return "explain", nil
}

Copilot uses AI. Check for mistakes.
choices := []*uxlib.SelectChoice{
{Value: "explain", Label: "Explain the error"},
{Value: "guide", Label: "Provide step-by-step fix guidance"},
{Value: "summarize", Label: "Summary (explanation + guidance)"},
{Value: "skip", Label: "Skip"},
{Value: "always.explain", Label: "Always explain the error"},
{Value: "always.guide", Label: "Always provide fix guidance"},
{Value: "always.summarize", Label: "Always provide summary"},
{Value: "always.skip", Label: "Always skip"},
}
Comment on lines 459 to 463
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

With the remaining option now being "Explain the error" (and fix guidance coming in a separate follow-up), the surrounding prompt text still saying "Generate troubleshooting steps" can be misleading. Consider updating the prompt/help copy to reflect the new two-step explanation→guidance flow so users know what to expect.

Copilot uses AI. Check for mistakes.

Expand Down Expand Up @@ -507,6 +506,33 @@ func (e *ErrorMiddleware) promptTroubleshootingWithConsent(ctx context.Context)
return selected, nil
}

// promptForFixGuidance asks the user if they want step-by-step fix guidance after seeing the error explanation.
// Returns true if the user wants guidance, false if they want to exit.
func (e *ErrorMiddleware) promptForFixGuidance(ctx context.Context) (bool, error) {
choices := []*uxlib.SelectChoice{
{Value: "yes", Label: "Yes"},
{Value: "no", Label: "No, I know what to do (exit agent mode)"},
}

selector := uxlib.NewSelect(&uxlib.SelectOptions{
Message: "Do you want to generate step-by-step fix guidance?",
Choices: choices,
EnableFiltering: uxlib.Ptr(false),
DisplayCount: len(choices),
})

choiceIndex, err := selector.Ask(ctx)
if err != nil {
return false, err
}

if choiceIndex == nil || *choiceIndex < 0 || *choiceIndex >= len(choices) {
return false, fmt.Errorf("invalid choice selected")
}

return choices[*choiceIndex].Value == "yes", nil
}

// extractSuggestedSolutions extracts solutions from the LLM response.
// It expects a JSON response with the structure: {"analysis": "...", "solutions": ["...", "...", "..."]}
// The response may be wrapped in a "text" field by the agent framework:
Expand Down
Loading