diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index 729892aac7..1d32e78f67 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -385,6 +385,13 @@ "group": "inline" } ], + "editor/context": [ + { + "command": "fx-extension.teamsAgentTroubleshootSelectedText", + "group": "navigation", + "when": "(fx-extension.isChatParticipantUIEntriesEnabled && (resourceFilename =~ /.*Teams Toolkit\\.log$/ || (activeOutputChannel =~ /^extension-output-TeamsDevApp\\.ms-teams-vscode-extension.*/ && view == 'workbench.panel.output')))" + } + ], "editor/title/run": [ { "command": "fx-extension.selectAndDebug", @@ -547,6 +554,14 @@ { "command": "fx-extension.openOfficeDevHelpFeedbackLink", "when": "false" + }, + { + "command": "fx-extension.teamsAgentTroubleshootSelectedText", + "when": "false" + }, + { + "command": "fx-extension.teamsAgentTroubleshootError", + "when": "false" } ] }, @@ -907,6 +922,18 @@ "title": "%teamstoolkit.commandsTreeViewProvider.syncManifest%", "category": "Teams", "enablement": "fx-extension.isSyncManifestEnabled" + }, + { + "command": "fx-extension.teamsAgentTroubleshootSelectedText", + "title": "Troubleshoot selection with @teamsapp", + "enablement": "editor.hasSelection && fx-extension.isChatParticipantUIEntriesEnabled", + "category": "Teams" + }, + { + "command": "fx-extension.teamsAgentTroubleshootError", + "title": "Troubleshoot error with @teamsapp", + "enablement": "fx-extension.isChatParticipantUIEntriesEnabled", + "category": "Teams" } ], "taskDefinitions": [ diff --git a/packages/vscode-extension/src/error/common.ts b/packages/vscode-extension/src/error/common.ts index ec7ad5fb6c..8a29d40036 100644 --- a/packages/vscode-extension/src/error/common.ts +++ b/packages/vscode-extension/src/error/common.ts @@ -2,7 +2,12 @@ // Licensed under the MIT license. import { UserError, SystemError, FxError, Result, err, ok } from "@microsoft/teamsfx-api"; -import { isUserCancelError, ConcurrentError } from "@microsoft/teamsfx-core"; +import { + isUserCancelError, + ConcurrentError, + featureFlagManager, + FeatureFlags as CoreFeatureFlags, +} from "@microsoft/teamsfx-core"; import { Uri, commands, window } from "vscode"; import { RecommendedOperations, @@ -14,7 +19,11 @@ import { ExtTelemetry } from "../telemetry/extTelemetry"; import { anonymizeFilePaths } from "../utils/fileSystemUtils"; import { localize } from "../utils/localizeUtils"; import { isTestToolEnabledProject } from "../utils/projectChecker"; -import { TelemetryEvent, TelemetryProperty } from "../telemetry/extTelemetryEvents"; +import { + TelemetryEvent, + TelemetryProperty, + TelemetryTriggerFrom, +} from "../telemetry/extTelemetryEvents"; import VsCodeLogInstance from "../commonlib/log"; import { ExtensionSource, ExtensionErrors } from "./error"; @@ -34,6 +43,20 @@ export async function showError(e: UserError | SystemError) { workspaceUri?.fsPath && isTestToolEnabledProject(workspaceUri.fsPath); + const shouldRecommendTeamsAgent = featureFlagManager.getBooleanValue( + CoreFeatureFlags.ChatParticipantUIEntries + ); + const troubleshootErrorWithTeamsAgentButton = { + title: "Troubleshoot error with @teamsapp", // Localize + run: async () => { + await commands.executeCommand( + "fx-extension.teamsAgentTroubleshootError", + TelemetryTriggerFrom.Notification, + e + ); + }, + }; + if (recommendTestTool) { const recommendTestToolMessage = openTestToolMessage(); const recommendTestToolDisplayMessage = openTestToolDisplayMessage(); @@ -59,6 +82,9 @@ export async function showError(e: UserError | SystemError) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions VsCodeLogInstance.debug(`Call stack: ${e.stack || e.innerError?.stack || ""}`); const buttons = recommendTestTool ? [runTestTool, help] : [help]; + if (shouldRecommendTeamsAgent) { + buttons.push(troubleshootErrorWithTeamsAgentButton); + } const button = await window.showErrorMessage( `[${errorCode}]: ${notificationMessage}`, ...buttons @@ -95,6 +121,9 @@ export async function showError(e: UserError | SystemError) { const buttons = recommendTestTool ? [runTestTool, issue, similarIssues] : [issue, similarIssues]; + if (shouldRecommendTeamsAgent) { + buttons.push(troubleshootErrorWithTeamsAgentButton); + } const button = await window.showErrorMessage( `[${errorCode}]: ${notificationMessage}`, ...buttons @@ -104,7 +133,13 @@ export async function showError(e: UserError | SystemError) { if (!(e instanceof ConcurrentError)) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions VsCodeLogInstance.debug(`Call stack: ${e.stack || e.innerError?.stack || ""}`); - const buttons = recommendTestTool ? [runTestTool] : []; + const buttons: { + title: string; + run: () => void; + }[] = recommendTestTool ? [runTestTool] : []; + if (shouldRecommendTeamsAgent) { + buttons.push(troubleshootErrorWithTeamsAgentButton); + } const button = await window.showErrorMessage( `[${errorCode}]: ${notificationMessage}`, ...buttons diff --git a/packages/vscode-extension/src/extension.ts b/packages/vscode-extension/src/extension.ts index df8b151330..a33de62994 100644 --- a/packages/vscode-extension/src/extension.ts +++ b/packages/vscode-extension/src/extension.ts @@ -458,6 +458,18 @@ function registerActivateCommands(context: vscode.ExtensionContext) { Correlator.run(copilotChatHandlers.invokeTeamsAgent, args) ); context.subscriptions.push(invokeTeamsAgent); + + const troubleshootSelectedText = vscode.commands.registerCommand( + "fx-extension.teamsAgentTroubleshootSelectedText", + (...args) => Correlator.run(copilotChatHandlers.troubleshootSelectedText, args) + ); + context.subscriptions.push(troubleshootSelectedText); + + const troubleshootError = vscode.commands.registerCommand( + "fx-extension.teamsAgentTroubleshootError", + (...args) => Correlator.run(copilotChatHandlers.troubleshootError, args) + ); + context.subscriptions.push(troubleshootError); } /** diff --git a/packages/vscode-extension/src/handlers/copilotChatHandlers.ts b/packages/vscode-extension/src/handlers/copilotChatHandlers.ts index 5a2d60c464..ba5a91adfc 100644 --- a/packages/vscode-extension/src/handlers/copilotChatHandlers.ts +++ b/packages/vscode-extension/src/handlers/copilotChatHandlers.ts @@ -3,7 +3,7 @@ import * as util from "util"; import * as vscode from "vscode"; -import { FxError, Result, SystemError, err, ok } from "@microsoft/teamsfx-api"; +import { FxError, Result, SystemError, UserError, err, ok } from "@microsoft/teamsfx-api"; import { assembleError, globalStateGet, globalStateUpdate } from "@microsoft/teamsfx-core"; import { UserCancelError, sleep } from "@microsoft/vscode-ui"; import VsCodeLogInstance from "../commonlib/log"; @@ -24,6 +24,12 @@ import { VS_CODE_UI } from "../qm/vsc_ui"; const githubCopilotChatExtensionId = "github.copilot-chat"; const teamsAgentLink = "https://aka.ms/install-teamsapp"; +enum errorNames { + NoActiveTextEditor = "NoActiveTextEditor", + CannotVerifyGithubCopilotChat = "CannotVerifyGithubCopilotChat", + openCopilotError = "openCopilotError", +} + function githubCopilotInstalled(): boolean { const extension = vscode.extensions.getExtension(githubCopilotChatExtensionId); return !!extension; @@ -42,7 +48,7 @@ async function openGithubCopilotChat(query: string): Promise> { - const eventName = TelemetryEvent.InvokeTeamsAgent; - const triggerFromProperty = getTriggerFromProperty(args); - ExtTelemetry.sendTelemetryEvent(TelemetryEvent.InvokeTeamsAgentStart, triggerFromProperty); - - const query = - triggerFromProperty["trigger-from"] === TelemetryTriggerFrom.TreeView || - triggerFromProperty["trigger-from"] === TelemetryTriggerFrom.CommandPalette - ? "@teamsapp Use this GitHub Copilot extension to ask questions about Teams app and agent development." - : "@teamsapp Write your own query message to find relevant templates or samples to build your Teams app and agent as per your description. E.g. @teamsapp create an AI assistant bot that can complete common tasks."; - let res: Result; - +async function invoke( + query: string, + eventName: string, + triggerFromProperty: { [key: string]: TelemetryTriggerFrom } +): Promise> { const skipRemindInstallTeamsAgent = await globalStateGet( GlobalKey.DoNotRemindInstallTeamsAgent, false @@ -181,7 +180,7 @@ export async function invokeTeamsAgent(args?: any[]): Promise> { + const eventName = TelemetryEvent.InvokeTeamsAgent; + const triggerFromProperty = getTriggerFromProperty(args); + ExtTelemetry.sendTelemetryEvent(TelemetryEvent.InvokeTeamsAgentStart, triggerFromProperty); + + const query = + triggerFromProperty["trigger-from"] === TelemetryTriggerFrom.TreeView || + triggerFromProperty["trigger-from"] === TelemetryTriggerFrom.CommandPalette + ? "@teamsapp Use this GitHub Copilot extension to ask questions about Teams app and agent development." + : "@teamsapp Write your own query message to find relevant templates or samples to build your Teams app and agent as per your description. E.g. @teamsapp create an AI assistant bot that can complete common tasks."; + const res = await invoke(query, eventName, triggerFromProperty); + if (res.isErr()) { ExtTelemetry.sendTelemetryErrorEvent(eventName, res.error, triggerFromProperty); } else { @@ -240,3 +258,98 @@ export async function invokeTeamsAgent(args?: any[]): Promise> { + const eventName = TelemetryEvent.TroubleshootSelectedText; + const triggerFromProperty = getTriggerFromProperty([TelemetryTriggerFrom.EditorContextMenu]); + ExtTelemetry.sendTelemetryEvent( + TelemetryEvent.TroubleshootSelectedTextStart, + triggerFromProperty + ); + + const editor = vscode.window.activeTextEditor; + let selectedText = ""; + if (editor) { + const selection = editor.selection; + selectedText = editor.document.getText(selection); + } else { + return err( + new UserError( + eventName, + errorNames.NoActiveTextEditor, + "No active text. Please select some text for troubleshooting." // TODO: localize. + ) + ); + } + + const query = `@teamsapp I'm encountering the following error in my code. +\`\`\` +{ + Error message: ${selectedText} +} +\`\`\` +Can you help me diagnose the issue and suggest possible solutions? +`; + const res = await invoke(query, eventName, triggerFromProperty); + + if (res.isErr()) { + ExtTelemetry.sendTelemetryErrorEvent(eventName, res.error, triggerFromProperty); + } else { + ExtTelemetry.sendTelemetryEvent(eventName, { + [TelemetryProperty.Success]: TelemetrySuccess.Yes, + ...triggerFromProperty, + }); + } + return res; +} + +/** + * Invokes teams agent for troubleshooting current error. + * @param args + * @returns Result + */ +export async function troubleshootError(args?: any[]): Promise> { + const eventName = TelemetryEvent.TroubleshootErrorFromNotification; + if (!args || args.length !== 2) { + // should never happen + return ok(null); + } + + const currentError = args[1] as FxError; + const errorCode = `${currentError.source}.${currentError.name}`; + const triggerFromProperty = getTriggerFromProperty(args); + const telemtryProperties = { + ...triggerFromProperty, + [TelemetryProperty.ErrorCode]: errorCode, + }; + ExtTelemetry.sendTelemetryEvent( + TelemetryEvent.TroubleshootErrorFromNotificationStart, + telemtryProperties + ); + + const query = `@teamsapp I'm encountering the following error in Teams Toolkit. + \`\`\` + { + Error code: ${errorCode} + Error message: ${currentError.message} + } + \`\`\` + Can you help me diagnose the issue and suggest possible solutions? + `; + const res = await invoke(query, eventName, triggerFromProperty); + + if (res.isErr()) { + ExtTelemetry.sendTelemetryErrorEvent(eventName, res.error, telemtryProperties); + } else { + ExtTelemetry.sendTelemetryEvent(eventName, { + [TelemetryProperty.Success]: TelemetrySuccess.Yes, + ...telemtryProperties, + }); + } + return res; +} diff --git a/packages/vscode-extension/src/telemetry/extTelemetryEvents.ts b/packages/vscode-extension/src/telemetry/extTelemetryEvents.ts index 7dd6b0797b..21b99d2dbd 100644 --- a/packages/vscode-extension/src/telemetry/extTelemetryEvents.ts +++ b/packages/vscode-extension/src/telemetry/extTelemetryEvents.ts @@ -278,8 +278,13 @@ export enum TelemetryEvent { FindSimilarIssues = "find-similar-issues", + // Teams Github Copilot UI InvokeTeamsAgentStart = "invoke-teams-agent-start", InvokeTeamsAgent = "invoke-teams-agent", + TroubleshootSelectedTextStart = "troubleshoot-selected-text-start", + TroubleshootSelectedText = "troubleshoot-selected-text", + TroubleshootErrorFromNotificationStart = "troubleshoot-error-from-notification-start", + TroubleshootErrorFromNotification = "troubleshoot-error-from-notification", // Copilot Chat CopilotChatStart = "copilot-chat-start", @@ -459,6 +464,7 @@ export enum TelemetryTriggerFrom { SampleDetailPage = "SampleDetailPage", CopilotChat = "CopilotChat", CreateAppQuestionFlow = "CreateAppQuestionFlow", + EditorContextMenu = "EditorContextMenu", Other = "Other", Auto = "Auto", Unknow = "Unknow", diff --git a/packages/vscode-extension/src/utils/telemetryUtils.ts b/packages/vscode-extension/src/utils/telemetryUtils.ts index 83c6174d7c..70d0584131 100644 --- a/packages/vscode-extension/src/utils/telemetryUtils.ts +++ b/packages/vscode-extension/src/utils/telemetryUtils.ts @@ -73,6 +73,8 @@ export function getTriggerFromProperty(args?: any[]) { return { [TelemetryProperty.TriggerFrom]: TelemetryTriggerFrom.Other }; case TelemetryTriggerFrom.CreateAppQuestionFlow: return { [TelemetryProperty.TriggerFrom]: TelemetryTriggerFrom.CreateAppQuestionFlow }; + case TelemetryTriggerFrom.EditorContextMenu: + return { [TelemetryProperty.TriggerFrom]: TelemetryTriggerFrom.EditorContextMenu }; default: return { [TelemetryProperty.TriggerFrom]: TelemetryTriggerFrom.Unknow }; } diff --git a/packages/vscode-extension/test/error/common.test.ts b/packages/vscode-extension/test/error/common.test.ts index 1f8f016030..3092254460 100644 --- a/packages/vscode-extension/test/error/common.test.ts +++ b/packages/vscode-extension/test/error/common.test.ts @@ -10,6 +10,7 @@ import { SystemError, UserError } from "@microsoft/teamsfx-api"; import { showError } from "../../src/error/common"; import { TelemetryEvent } from "../../src/telemetry/extTelemetryEvents"; import { RecommendedOperations } from "../../src/debug/common/debugConstants"; +import { featureFlagManager } from "@microsoft/teamsfx-core"; describe("common", () => { const sandbox = sinon.createSandbox(); @@ -121,6 +122,7 @@ describe("common", () => { }); it(`showError - ${type} - recommend test tool`, async () => { + sandbox.stub(featureFlagManager, "getBooleanValue").returns(false); sandbox.stub(localizeUtils, "localize").returns(""); const showErrorMessageStub = sandbox.stub(vscode.window, "showErrorMessage"); sandbox.stub(projectChecker, "isTestToolEnabledProject").returns(true); @@ -130,5 +132,17 @@ describe("common", () => { await showError(error); chai.assert.equal(showErrorMessageStub.firstCall.args.length, buttonNum + 1); }); + + it(`showError - ${type} - recommend troubleshoot`, async () => { + sandbox.stub(featureFlagManager, "getBooleanValue").returns(true); + sandbox.stub(localizeUtils, "localize").returns(""); + const showErrorMessageStub = sandbox.stub(vscode.window, "showErrorMessage"); + sandbox.stub(projectChecker, "isTestToolEnabledProject").returns(false); + sandbox.stub(globalVariables, "workspaceUri").value(vscode.Uri.file("path")); + sandbox.stub(vscode.commands, "executeCommand"); + const error = buildError(); + await showError(error); + chai.assert.equal(showErrorMessageStub.firstCall.args.length, buttonNum + 1); + }); }); }); diff --git a/packages/vscode-extension/test/handlers/copilotChatHandlers.test.ts b/packages/vscode-extension/test/handlers/copilotChatHandlers.test.ts index 44bf9c2697..89197ed72e 100644 --- a/packages/vscode-extension/test/handlers/copilotChatHandlers.test.ts +++ b/packages/vscode-extension/test/handlers/copilotChatHandlers.test.ts @@ -288,4 +288,82 @@ describe("invokeTeamsAgent", async () => { chai.assert.isTrue(openUrlStub.notCalled); }); }); + + describe("troubleshootSelectedText", async () => { + it("can invoke teams agent", async () => { + sandbox.stub(vscode.window, "activeTextEditor").value({ + selection: "current select", + document: { + getText: (selection: vscode.Selection) => "current select", + }, + } as any); + sandbox.stub(globalState, "globalStateGet").resolves(true); + sandbox.stub(vscode.extensions, "getExtension").returns({ name: "github.copilot" } as any); + sandbox.stub(vscode.commands, "executeCommand").resolves(); + const res = await handlers.troubleshootSelectedText(); + if (res.isErr()) { + console.log(res.error); + } + chai.assert.isTrue(res.isOk()); + }); + + it("no active text", async () => { + sandbox.stub(vscode.window, "activeTextEditor").value(undefined); + const res = await handlers.troubleshootSelectedText(); + chai.assert.isTrue(res.isErr()); + }); + + it("error", async () => { + sandbox.stub(vscode.window, "activeTextEditor").value({ + selection: "current select", + document: { + getText: (selection: vscode.Selection) => "current select", + }, + } as any); + sandbox.stub(globalState, "globalStateGet").resolves(true); + sandbox.stub(versionUtils, "isVSCodeInsiderVersion").returns(true); + sandbox.stub(vscode.extensions, "getExtension").onFirstCall().returns(undefined); + const res = await handlers.troubleshootSelectedText(); + chai.assert.isTrue(res.isErr()); + if (res.isErr()) { + console.log(res.error); + } + }); + }); + + describe("troubleshootError", async () => { + it("can invoke teams agent", async () => { + sandbox.stub(globalState, "globalStateGet").resolves(true); + sandbox.stub(vscode.extensions, "getExtension").returns({ name: "github.copilot" } as any); + sandbox.stub(vscode.commands, "executeCommand").resolves(); + + const currentError = new SystemError("test", "test", "test", "test"); + const res = await handlers.troubleshootError(["triggerFrom", currentError]); + chai.assert.isTrue(res.isOk()); + }); + + it("missing args", async () => { + const res = await handlers.troubleshootError([]); + const calledCommand = sandbox.stub(vscode.commands, "executeCommand").resolves(); + chai.assert.isTrue(res.isOk()); + chai.assert.isFalse(calledCommand.calledOnce); + }); + + it("error", async () => { + sandbox.stub(globalState, "globalStateGet").resolves(true); + sandbox.stub(versionUtils, "isVSCodeInsiderVersion").returns(true); + sandbox.stub(vscode.extensions, "getExtension").onFirstCall().returns(undefined); + sandbox.stub(vscode.commands, "executeCommand").callsFake(async (command: string) => { + if (command === "workbench.extensions.installExtension") { + throw new Error("Install Error"); + } else { + return {}; + } + }); + + const currentError = new SystemError("test", "test", "test", "test"); + const res = await handlers.troubleshootError(["triggerFrom", currentError]); + chai.assert.isTrue(res.isErr()); + }); + }); });