diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/Core/AssistIconLoader.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Core/AssistIconLoader.cs index a9082538..9bc3d8c5 100644 --- a/ast-visual-studio-extension/CxExtension/CxAssist/Core/AssistIconLoader.cs +++ b/ast-visual-studio-extension/CxExtension/CxAssist/Core/AssistIconLoader.cs @@ -264,7 +264,7 @@ public static ImageSource LoadSeverityIcon(SeverityLevel severity) if (!_popupIconsLogged) { _popupIconsLogged = true; - System.Diagnostics.Debug.WriteLine($"[{CxAssistConstants.LogCategory}] {string.Format(CxAssistConstants.ICONS_LOADED_FOR_THEME, currentTheme)}"); + CxAssistOutputPane.WriteToOutputPane(string.Format(CxAssistConstants.ICONS_LOADED_FOR_THEME, currentTheme)); } return img; } diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/Core/CopilotIntegration.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Core/CopilotIntegration.cs index 700fcfa0..44e80a71 100644 --- a/ast-visual-studio-extension/CxExtension/CxAssist/Core/CopilotIntegration.cs +++ b/ast-visual-studio-extension/CxExtension/CxAssist/Core/CopilotIntegration.cs @@ -2,11 +2,16 @@ using System.Diagnostics; using System.Windows; using System.Windows.Threading; -using Microsoft.VisualStudio.Shell; +using ast_visual_studio_extension.CxExtension.Utils; using EnvDTE; using EnvDTE80; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.Imaging; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; using System.Windows.Automation; using Process = System.Diagnostics.Process; +using System.Linq; namespace ast_visual_studio_extension.CxExtension.CxAssist.Core { @@ -48,13 +53,13 @@ internal static class CopilotIntegration private static class Timing { /// Delay after opening Copilot to allow UI to fully render. - public const int CopilotOpenDelayMs = 1200; + public const int CopilotOpenDelayMs = 900; /// Delay after starting a new thread for UI to settle. - public const int NewThreadDelayMs = 500; + public const int NewThreadDelayMs = 400; /// Delay before paste/submit to ensure input field has focus. - public const int PasteDelayMs = 400; + public const int PasteDelayMs = 350; /// Brief pause between paste and Enter to let VS process clipboard. public const int PasteSettleMs = 100; @@ -66,9 +71,13 @@ private static class Timing private static class AutomationProperties { public static readonly string[] ModePickerNames = { - "Chat Mode Picker", "Chat mode", + // VS 2026 name (primary) + "Chat mode", + // VS 2022 and fallback names + "Chat Mode Picker", "Agent Mode Picker", "Agent mode", "Agent", - "Mode" + "Mode", "Copilot mode", "Chat mode picker", + "Mode picker", "Pick a mode" }; public const string AgentOptionName = "Agent"; } @@ -145,6 +154,108 @@ public static IntegrationResult Fail(string msg, Exception ex = null) => // ==================== Public API ==================== + /// + /// Shows a non-modal main-window info bar (fallback: status bar). No blocking dialogs. + /// + /// When true and not an error, uses the warning (yellow) info bar style. + public static void ShowAssistNotification(string message, bool isError = false, bool useWarningSeverity = false) + { + try + { + ThreadHelper.ThrowIfNotOnUIThread(); + var pkg = ServiceProvider.GlobalProvider?.GetService(typeof(AsyncPackage)) as AsyncPackage; + if (pkg != null) + { + if (isError) + CxUtils.DisplayMessageInInfoBar(pkg, message, KnownMonikers.StatusError, autoDismiss: true); + else if (useWarningSeverity) + CxUtils.DisplayMessageInInfoBar(pkg, message, KnownMonikers.StatusWarning, autoDismiss: true); + else + CxUtils.DisplayMessageInInfoBar(pkg, message, KnownMonikers.StatusInformation, autoDismiss: true); + return; + } + + var dte = GetDte(); + if (dte?.StatusBar != null) + dte.StatusBar.Text = message; + } + catch (Exception ex) + { + Log("ShowAssistNotification failed: " + ex.Message); + } + } + + /// True when GitHub Copilot chat commands are registered (extension present). + public static bool CheckCopilotInstalled() + { + try + { + ThreadHelper.ThrowIfNotOnUIThread(); + var dte = GetDte(); + if (dte?.Commands == null) return false; + + foreach (string cmdId in OpenChatCommands) + { + try + { + var cmd = dte.Commands.Item(cmdId); + if (cmd != null) return true; + } + catch + { + } + } + } + catch + { + } + return false; + } + + /// Legacy name; use . + public static bool IsCopilotAvailable() => CheckCopilotInstalled(); + + /// + /// Starts a new Copilot chat thread via DTE (best-effort). + /// + public static bool OpenCopilotThread() + { + ThreadHelper.ThrowIfNotOnUIThread(); + return TryExecuteDteCommands(NewThreadCommands); + } + + /// + /// Whether Copilot Chat appears to be in Agent mode (VS 2022 vs newer UIs differ; heuristics apply for major version 19+). + /// VS 2026: Mode detection is unreliable via UI Automation, so we assume Agent mode is active. + /// + public static bool IsAgentMode() + { + try + { + ThreadHelper.ThrowIfNotOnUIThread(); + var vsProcess = Process.GetCurrentProcess(); + AutomationElement vsWindow = AutomationElement.FromHandle(vsProcess.MainWindowHandle); + + if (vsWindow == null) + { + Log("IsAgentMode: Could not get VS main window"); + return false; + } + + // Attempt UI Automation detection + // NOTE: VS 2026 doesn't reliably expose current mode through standard UI Automation patterns. + // In Ask mode, detection will return false (correct behavior). + // In Agent mode, detection may or may not work depending on whether the UI exposes the mode state. + bool detected = IsAgentModeAlreadyActive(vsWindow); + return detected; + } + catch (Exception ex) + { + Log("IsAgentMode failed: " + ex.Message); + return false; + } + } + /// /// Opens Copilot Chat, starts a new thread, pastes the prompt, and sends it. /// Returns true if the clipboard was set (even if full automation failed). @@ -182,37 +293,32 @@ public static IntegrationResult SendPromptToCopilotDetailed(string prompt, strin } Log("Prompt copied to clipboard"); + // Capture the code document window before Copilot steals focus (for editor info bar above the file). + IVsWindowFrame assistDocumentFrame = TryCaptureAssistDocumentFrame(); + // Step 2: Pre-check if Copilot is available (aligned with JetBrains CopilotIntegration.isCopilotAvailable) - if (!IsCopilotAvailable()) + if (!CheckCopilotInstalled()) { Log("Copilot not available (pre-check), prompt copied to clipboard"); - MessageBox.Show( - CxAssistConstants.CopilotOpenInstructionsMessage, - CxAssistConstants.DisplayName, - MessageBoxButton.OK, - MessageBoxImage.Information); + ShowCopilotNotInstalledMessage(assistDocumentFrame); return IntegrationResult.CopilotNotAvailable( - CxAssistConstants.CopilotOpenInstructionsMessage); + CxAssistConstants.CopilotNotInstalledInfoBarMessage); } // Step 3: Open Copilot Chat - bool opened = TryOpenCopilotChat(); + bool opened = OpenCopilotChat(); if (!opened) { Log("Copilot Chat failed to open - Copilot may not be installed"); - MessageBox.Show( - CxAssistConstants.CopilotOpenInstructionsMessage, - CxAssistConstants.DisplayName, - MessageBoxButton.OK, - MessageBoxImage.Information); + ShowCopilotChatOpenFailedMessage(assistDocumentFrame); return IntegrationResult.CopilotNotAvailable( - CxAssistConstants.CopilotOpenInstructionsMessage); + CxAssistConstants.CopilotChatOpenFailedInfoBarMessage); } Log("Copilot Chat opened, scheduling automation sequence"); // Step 4: Schedule the automation sequence after UI renders - ScheduleAutomatedPromptEntry(prompt); + ScheduleAutomatedPromptEntry(prompt, assistDocumentFrame); return IntegrationResult.PartialSuccess( "Copilot Chat opened, automation in progress..."); @@ -223,11 +329,8 @@ public static IntegrationResult SendPromptToCopilotDetailed(string prompt, strin try { CopyToClipboard(prompt); - MessageBox.Show( - clipboardFallbackMessage ?? CxAssistConstants.CopilotGenericFallbackMessage, - CxAssistConstants.DisplayName, - MessageBoxButton.OK, - MessageBoxImage.Information); + ShowAssistNotification( + clipboardFallbackMessage ?? CxAssistConstants.CopilotGenericFallbackMessage); return IntegrationResult.PartialSuccess(clipboardFallbackMessage); } catch @@ -244,73 +347,206 @@ public static IntegrationResult SendPromptToCopilotDetailed(string prompt, strin /// DispatcherTimer steps. Each step yields to the UI thread so that /// Copilot Chat can render and process events between operations. /// - /// Step 1 (after CopilotOpenDelayMs): Start new thread via DTE. - /// Step 2 (after NewThreadDelayMs): Switch to Agent mode via UI Automation. - /// Step 3 (after AgentModeDelayMs): Re-focus Copilot Chat, paste prompt, submit. + /// Agent mode: new thread → paste → Enter (submit). + /// Non-agent: new thread (awaited via timer chain) → paste only → info bar (no modal). + /// + /// + /// Resolves the active document while the editor still has selection context. + /// + private static IVsWindowFrame TryCaptureAssistDocumentFrame() + { + try + { + ThreadHelper.ThrowIfNotOnUIThread(); + var mon = Package.GetGlobalService(typeof(SVsShellMonitorSelection)) as IVsMonitorSelection; + if (mon != null + && ErrorHandler.Succeeded(mon.GetCurrentElementValue((uint)VSConstants.VSSELELEMID.SEID_DocumentFrame, out object frameObj)) + && frameObj is IVsWindowFrame frame) + { + return frame; + } + } + catch (Exception ex) + { + Log("TryCaptureAssistDocumentFrame: " + ex.Message); + } + + return null; + } + + private static void ShowCopilotNotAgentModeUserMessage(IVsWindowFrame assistDocumentFrame) + { + ThreadHelper.ThrowIfNotOnUIThread(); + ShowAssistNotification( + CxAssistConstants.CopilotNotAgentModeInfoBarMessage, + isError: false, + useWarningSeverity: true); + } + + /// + /// Shows Copilot not installed warning in the info bar. + /// + private static void ShowCopilotNotInstalledMessage(IVsWindowFrame assistDocumentFrame) + { + ThreadHelper.ThrowIfNotOnUIThread(); + ShowAssistNotification( + CxAssistConstants.CopilotNotInstalledInfoBarMessage, + isError: false, + useWarningSeverity: true); + } + + /// + /// Shows Copilot Chat failed to open warning in the info bar. + /// + private static void ShowCopilotChatOpenFailedMessage(IVsWindowFrame assistDocumentFrame) + { + ThreadHelper.ThrowIfNotOnUIThread(); + ShowAssistNotification( + CxAssistConstants.CopilotChatOpenFailedInfoBarMessage, + isError: false, + useWarningSeverity: true); + } + + /// + /// Shows Copilot prompt preparation failed error in the info bar (as warning with error fallback). /// - private static void ScheduleAutomatedPromptEntry(string prompt) + private static void ShowCopilotPromptPrepareFailedMessage(IVsWindowFrame assistDocumentFrame) + { + ThreadHelper.ThrowIfNotOnUIThread(); + ShowAssistNotification( + CxAssistConstants.CopilotPromptPrepareFailedInfoBarMessage, + isError: true); + } + + /// + /// Shows VS 2026 paste-only workflow message in the info bar. + /// Used when mode detection is unavailable and prompt is pasted without auto-submit. + /// + private static void ShowCopilotPasteOnlyVs2026Message(IVsWindowFrame assistDocumentFrame) + { + ThreadHelper.ThrowIfNotOnUIThread(); + ShowAssistNotification( + CxAssistConstants.CopilotPasteOnlyVs2026InfoBarMessage, + isError: false, + useWarningSeverity: true); + } + + private static void ScheduleAutomatedPromptEntry(string prompt, IVsWindowFrame assistDocumentFrame) { - // New flow: after Copilot opens, require the user to have Agent mode active. - // If Agent mode is not active, show an informational popup and do not - // attempt to switch modes automatically. The prompt is already copied - // to the clipboard as a guaranteed fallback. ScheduleOnIdle(Timing.CopilotOpenDelayMs, () => { try { - var vsProcess = Process.GetCurrentProcess(); - AutomationElement vsWindow = AutomationElement.FromHandle(vsProcess.MainWindowHandle); + int vsMajor = GetVisualStudioMajorVersion(); - bool agentActive = false; - if (vsWindow != null) + // VS 2026+: Mode detection is unreliable (Chat mode button doesn't expose selection state) + // Skip all automation and paste-only workflow with user message + if (vsMajor >= 18) { - agentActive = IsAgentModeAlreadyActive(vsWindow); + Log("VS 2026+ detected — using paste-only workflow (mode detection unavailable)"); + ScheduleOnIdle(Timing.NewThreadDelayMs, () => + { + bool threadOk = OpenCopilotThread(); + if (!threadOk) + ShowCopilotChatOpenFailedMessage(assistDocumentFrame); + else + { + try + { + var vsProc = Process.GetCurrentProcess(); + AutomationElement wnd = AutomationElement.FromHandle(vsProc.MainWindowHandle); + if (wnd != null) + FocusCopilotInput(wnd); + } + catch (Exception exFocus) + { + Log("UI Automation: focus before paste: " + exFocus.Message); + } + } + + int delayBeforePaste = threadOk ? Timing.NewThreadDelayMs : Timing.PasteDelayMs; + ScheduleOnIdle(delayBeforePaste, () => + { + bool inserted = InsertPromptWithoutSubmitting(); + if (!threadOk) + return; + if (!inserted) + ShowCopilotPromptPrepareFailedMessage(assistDocumentFrame); + else + ShowCopilotPasteOnlyVs2026Message(assistDocumentFrame); + }); + }); + return; } - if (!agentActive) + // VS 2022 and earlier: Attempt to detect Agent mode + bool agentMode = IsAgentMode(); + + if (agentMode) { - Log("Agent mode not active - prompting user to enable Agent mode and use clipboard fallback"); - MessageBox.Show( - "Please select 'Agent' mode in GitHub Copilot Chat. The prompt has been copied to your clipboard — open Copilot Chat, select Agent mode, then paste the prompt to continue.", - CxAssistConstants.DisplayName, - MessageBoxButton.OK, - MessageBoxImage.Information); + Log("Agent mode detected — auto-submitting prompt"); + ScheduleOnIdle(Timing.NewThreadDelayMs, () => + { + bool newThreadStarted = OpenCopilotThread(); + Log(newThreadStarted + ? "New thread started via DTE command" + : "DTE new-thread commands not available, continuing with current thread"); + + if (newThreadStarted) + { + try + { + var vsProc = Process.GetCurrentProcess(); + AutomationElement wnd = AutomationElement.FromHandle(vsProc.MainWindowHandle); + if (wnd != null) + { + bool focused = FocusCopilotInput(wnd); + Log("UI Automation: Focused Copilot input after new thread: " + focused); + } + } + catch (Exception exFocus) + { + Log("UI Automation: error focusing input after new thread: " + exFocus.Message); + } + } + + int delayAfterThread = newThreadStarted ? Timing.NewThreadDelayMs : Timing.PasteDelayMs; + ScheduleOnIdle(delayAfterThread, PerformPasteAndSubmit); + }); return; } - // Agent mode is active — proceed to start a new thread and paste/submit + Log("Agent mode not detected — pasting prompt without auto-submit"); ScheduleOnIdle(Timing.NewThreadDelayMs, () => { - bool newThreadStarted = TryStartNewThread(); - Log(newThreadStarted - ? "New thread started via DTE command" - : "DTE new-thread commands not available, continuing with current thread"); - - // If a new thread was started, try focusing the Copilot input - if (newThreadStarted) + bool threadOk = OpenCopilotThread(); + if (!threadOk) + ShowCopilotChatOpenFailedMessage(assistDocumentFrame); + else { try { var vsProc = Process.GetCurrentProcess(); AutomationElement wnd = AutomationElement.FromHandle(vsProc.MainWindowHandle); if (wnd != null) - { - bool focused = FocusCopilotInput(wnd); - Log("UI Automation: Focused Copilot input after new thread: " + focused); - } + FocusCopilotInput(wnd); } catch (Exception exFocus) { - Log("UI Automation: error focusing input after new thread: " + exFocus.Message); + Log("UI Automation: focus before non-agent paste: " + exFocus.Message); } } - int agentDelay = newThreadStarted ? Timing.NewThreadDelayMs : Timing.PasteDelayMs; - - // Paste + submit - ScheduleOnIdle(agentDelay, () => + int delayBeforePaste = threadOk ? Timing.NewThreadDelayMs : Timing.PasteDelayMs; + ScheduleOnIdle(delayBeforePaste, () => { - PerformPasteAndSubmit(); + bool inserted = InsertPromptWithoutSubmitting(); + if (!threadOk) + return; + if (!inserted) + ShowCopilotPromptPrepareFailedMessage(assistDocumentFrame); + else + ShowCopilotNotAgentModeUserMessage(assistDocumentFrame); }); }); } @@ -343,11 +579,7 @@ private static void PerformPasteAndSubmit() catch (Exception ex) { CxAssistErrorHandler.LogAndSwallow(ex, "CopilotIntegration.PerformPasteAndSubmit"); - MessageBox.Show( - CxAssistConstants.CopilotPasteFailedMessage, - CxAssistConstants.DisplayName, - MessageBoxButton.OK, - MessageBoxImage.Information); + ShowAssistNotification(CxAssistConstants.CopilotPromptPrepareFailedInfoBarMessage, isError: true); } } @@ -372,16 +604,31 @@ private static void ScheduleOnIdle(int delayMs, Action action) catch (Exception ex) { CxAssistErrorHandler.LogAndSwallow(ex, "CopilotIntegration.ScheduleOnIdle"); - MessageBox.Show( - CxAssistConstants.CopilotPasteFailedMessage, - CxAssistConstants.DisplayName, - MessageBoxButton.OK, - MessageBoxImage.Information); + ShowAssistNotification(CxAssistConstants.CopilotPromptPrepareFailedInfoBarMessage, isError: true); } }; timer.Start(); } + /// + /// Pastes the prompt from the clipboard into Copilot input without sending (no Enter). + /// + private static bool InsertPromptWithoutSubmitting() + { + ThreadHelper.ThrowIfNotOnUIThread(); + try + { + TryExecuteDteCommands(OpenChatCommands); + System.Windows.Forms.SendKeys.SendWait("^v"); + return true; + } + catch (Exception ex) + { + CxAssistErrorHandler.LogAndSwallow(ex, "CopilotIntegration.InsertPromptWithoutSubmitting"); + return false; + } + } + // ==================== SendKeys ==================== /// @@ -398,13 +645,9 @@ private static void PasteAndSubmitViaSendKeys() // ==================== Opening Copilot Chat ==================== /// - /// Attempts to open GitHub Copilot Chat using multiple strategies: - /// - /// DTE ExecuteCommand with known command IDs - /// Keyboard shortcut simulation (Ctrl+\ then C) - /// + /// Opens the Copilot Chat tool window (not necessarily a new thread), using DTE commands or Ctrl+\, C. /// - private static bool TryOpenCopilotChat() + private static bool OpenCopilotChat() { ThreadHelper.ThrowIfNotOnUIThread(); @@ -430,47 +673,8 @@ private static bool TryOpenCopilotChat() return false; } - /// - /// Attempts to start a new chat thread via DTE commands. - /// - private static bool TryStartNewThread() - { - ThreadHelper.ThrowIfNotOnUIThread(); - return TryExecuteDteCommands(NewThreadCommands); - } - // ==================== Availability Check ==================== - - /// - /// Checks if GitHub Copilot is available before attempting to open it. - /// Aligned with JetBrains CopilotIntegration.isCopilotAvailable: checks known - /// command IDs via DTE.Commands to see if any are registered. - /// - public static bool IsCopilotAvailable() - { - try - { - ThreadHelper.ThrowIfNotOnUIThread(); - var dte = GetDte(); - if (dte?.Commands == null) return false; - - foreach (string cmdId in OpenChatCommands) - { - try - { - var cmd = dte.Commands.Item(cmdId); - if (cmd != null) return true; - } - catch - { - } - } - } - catch - { - } - return false; - } + // See and . // ==================== DTE Helpers ==================== @@ -517,9 +721,17 @@ private static int GetVisualStudioMajorVersion() var dte = GetDte(); if (dte?.Version != null) { + Log($"GetVisualStudioMajorVersion: DTE.Version = '{dte.Version}'"); var parts = dte.Version.Split('.'); if (parts.Length > 0 && int.TryParse(parts[0], out int major)) + { + Log($"GetVisualStudioMajorVersion: Parsed major version = {major}"); return major; + } + } + else + { + Log($"GetVisualStudioMajorVersion: DTE or DTE.Version is null"); } } catch (Exception ex) @@ -531,78 +743,6 @@ private static int GetVisualStudioMajorVersion() // ==================== Agent Mode Switching ==================== - /// - /// Switches Copilot Chat to Agent mode using direct UI Automation (no keyboard navigation). - /// Searches the entire VS main window for the mode picker button by known names, - /// then opens the dropdown and selects Agent. - /// - /// If Agent mode is already active, returns true without attempting to switch. - /// - private static bool TrySwitchToAgentMode() - { - try - { - ThreadHelper.ThrowIfNotOnUIThread(); - Log("Switching to Agent mode via UI Automation..."); - - var vsProcess = Process.GetCurrentProcess(); - AutomationElement vsWindow = AutomationElement.FromHandle(vsProcess.MainWindowHandle); - if (vsWindow == null) - { - Log("UI Automation: Could not get VS main window"); - return false; - } - - if (IsAgentModeAlreadyActive(vsWindow)) - { - Log("UI Automation: Agent mode is already active, skipping switch"); - return true; - } - - AutomationElement modePicker = FindModePickerButton(vsWindow); - if (modePicker == null) - { - Log("UI Automation: Mode Picker button not found"); - ListAvailableElements(vsWindow); - return false; - } - - Log("UI Automation: Found Mode Picker, attempting to open dropdown..."); - - if (modePicker.TryGetCurrentPattern(InvokePattern.Pattern, out object pattern)) - { - Log("UI Automation: Using InvokePattern to open dropdown"); - ((InvokePattern)pattern).Invoke(); - System.Threading.Thread.Sleep(700); - } - else - { - Log("UI Automation: InvokePattern not supported, using mouse click"); - if (!ClickElement(modePicker)) - { - Log("UI Automation: Failed to click Mode Picker button"); - return false; - } - System.Threading.Thread.Sleep(700); - } - - if (SelectAgentDirectly(vsWindow)) - { - Log("UI Automation: Agent mode selected successfully"); - return true; - } - - Log("UI Automation: Failed to select Agent option"); - System.Windows.Forms.SendKeys.SendWait("{ESC}"); - return false; - } - catch (Exception ex) - { - Log("UI Automation error: " + ex.Message); - return false; - } - } - /// /// Finds the Mode Picker button by searching the VS window for known names. /// @@ -816,8 +956,74 @@ private static bool IsAgentModeAlreadyActive(AutomationElement root) if (modePicker == null) return false; + // VS 2026: Search entire Copilot Chat pane for "Agent" text indicator + // The current mode is displayed somewhere in the Copilot Chat window, not necessarily at the button + try + { + // Find the Copilot Chat pane (parent of the mode picker) + AutomationElement copilotPane = modePicker; + for (int i = 0; i < 10; i++) // Walk up the tree + { + var parent = TreeWalker.ControlViewWalker.GetParent(copilotPane); + if (parent == null) break; + + string parentName = parent.Current.Name ?? ""; + if (parentName.IndexOf("Copilot", StringComparison.OrdinalIgnoreCase) >= 0 || + parentName.IndexOf("Chat", StringComparison.OrdinalIgnoreCase) >= 0) + { + copilotPane = parent; + break; + } + copilotPane = parent; + } + + // Search pane for "Agent" text + var allInPane = copilotPane.FindAll(TreeScope.Descendants, System.Windows.Automation.Condition.TrueCondition); + for (int i = 0; i < allInPane.Count; i++) + { + try + { + string name = allInPane[i].Current.Name ?? ""; + // Look for standalone "Agent" or "Agent mode" indicator + if (name.Equals("Agent", StringComparison.OrdinalIgnoreCase) || + name.IndexOf("Agent mode", StringComparison.OrdinalIgnoreCase) >= 0) + { + Log("UI Automation: Found Agent mode indicator: '" + name + "'"); + return true; + } + } + catch { } + } + } + catch (Exception ex) + { + Log("UI Automation: Copilot pane search failed: " + ex.Message); + } + + // Fallback: Check button's direct children + try + { + var children = modePicker.FindAll(TreeScope.Children, System.Windows.Automation.Condition.TrueCondition); + foreach (AutomationElement child in children) + { + try + { + string childName = child.Current.Name ?? ""; + if (childName.IndexOf("agent", StringComparison.OrdinalIgnoreCase) >= 0) + { + Log("UI Automation: Found 'Agent' in button child: " + childName); + return true; + } + } + catch { } + } + } + catch (Exception ex) + { + Log("UI Automation: Child search failed: " + ex.Message); + } + string modePickerName = modePicker.Current.Name ?? ""; - bool isAgentActive = modePickerName.IndexOf("agent", StringComparison.OrdinalIgnoreCase) >= 0; // Prefer a direct read of the selected value via common patterns @@ -846,7 +1052,8 @@ private static bool IsAgentModeAlreadyActive(AutomationElement root) var all = root.FindAll(TreeScope.Descendants, System.Windows.Automation.Condition.TrueCondition); int vsMajor = GetVisualStudioMajorVersion(); - bool isNewVs = vsMajor >= 19; // treat 19+ as VS2026 or newer + // VS 17 = VS2022; VS 18+ = newer shell (2025/2026) with different Copilot mode UI. + bool isNewVs = vsMajor >= 18; for (int i = 0; i < all.Count; i++) { @@ -915,228 +1122,6 @@ private static bool IsAgentModeAlreadyActive(AutomationElement root) } } - /// - /// Selects Agent by typing "Agent" to filter the dropdown, then clicking - /// the result. Falls back to arrow key navigation if typing doesn't work. - /// - private static bool SelectAgentDirectly(AutomationElement root) - { - try - { - Log("UI Automation: Typing 'Agent' to search/filter dropdown..."); - System.Windows.Forms.SendKeys.SendWait("Agent"); - System.Threading.Thread.Sleep(800); - - Log("UI Automation: Searching for Agent option after typing..."); - AutomationElement agentElement = FindAgentElement(root); - - if (agentElement != null) - { - Log("UI Automation: Found Agent element, clicking it..."); - - if (ClickElement(agentElement)) - { - Log("UI Automation: Agent selected via click"); - System.Threading.Thread.Sleep(400); - return true; - } - - if (agentElement.TryGetCurrentPattern(InvokePattern.Pattern, out object invoke)) - { - ((InvokePattern)invoke).Invoke(); - Log("UI Automation: Agent selected via InvokePattern"); - System.Threading.Thread.Sleep(400); - return true; - } - - if (agentElement.TryGetCurrentPattern(SelectionItemPattern.Pattern, out object selectPattern)) - { - ((SelectionItemPattern)selectPattern).Select(); - Log("UI Automation: Agent selected via SelectionItemPattern"); - System.Threading.Thread.Sleep(400); - return true; - } - } - - Log("UI Automation: Agent element not found after typing, trying arrow key navigation..."); - return SelectAgentViaArrowKeys(); - } - catch (Exception ex) - { - Log("UI Automation error in SelectAgentDirectly: " + ex.Message); - return false; - } - } - - /// - /// Selects Agent mode by pressing Down arrow repeatedly to navigate - /// through dropdown options, then pressing Enter to confirm. - /// Fallback when typing doesn't filter the dropdown. - /// - private static bool SelectAgentViaArrowKeys() - { - try - { - Log("UI Automation: Attempting arrow key navigation..."); - - for (int i = 0; i < 5; i++) - { - System.Windows.Forms.SendKeys.SendWait("{DOWN}"); - System.Threading.Thread.Sleep(200); - - var vsProcess = Process.GetCurrentProcess(); - AutomationElement vsWindow = AutomationElement.FromHandle(vsProcess.MainWindowHandle); - if (vsWindow != null && IsAgentModeAlreadyActive(vsWindow)) - { - Log("UI Automation: Agent mode now active (after " + (i + 1) + " Down presses)"); - System.Windows.Forms.SendKeys.SendWait("{ENTER}"); - System.Threading.Thread.Sleep(400); - return true; - } - } - - Log("UI Automation: Arrow key navigation did not activate Agent mode"); - System.Windows.Forms.SendKeys.SendWait("{ESC}"); - return false; - } - catch (Exception ex) - { - Log("UI Automation error in SelectAgentViaArrowKeys: " + ex.Message); - return false; - } - } - - /// - /// Searches for the "Agent" mode option (MenuItem or ListItem with name "Agent"). - /// Excludes items like "Search agents" that contain "agent" but aren't the mode option. - /// - private static AutomationElement FindAgentElement(AutomationElement root) - { - try - { - var allElements = root.FindAll(TreeScope.Descendants, - System.Windows.Automation.Condition.TrueCondition); - - foreach (AutomationElement el in allElements) - { - try - { - string name = el.Current.Name ?? ""; - string nameLower = name.ToLowerInvariant(); - - if (nameLower == "agent" || nameLower.StartsWith("agent ")) - { - string ctType = el.Current.ControlType.ProgrammaticName ?? ""; - - if (ctType.Contains("MenuItem") || ctType.Contains("ListItem")) - { - Log("UI Automation: Found Agent mode option [" + ctType + "]: '" + name + "'"); - return el; - } - } - } - catch - { - } - } - - foreach (AutomationElement el in allElements) - { - try - { - string name = el.Current.Name ?? ""; - if (string.Equals(name, "agent", StringComparison.OrdinalIgnoreCase)) - { - Log("UI Automation: Found Agent element (second pass): '" + name + "'"); - return el; - } - } - catch - { - } - } - } - catch (Exception ex) - { - Log("UI Automation error in FindAgentElement: " + ex.Message); - } - - return null; - } - - /// - /// Clicks an element using mouse coordinates from its bounding rectangle. - /// - private static bool ClickElement(AutomationElement element) - { - try - { - var rect = element.Current.BoundingRectangle; - if (rect.IsEmpty || rect.Width == 0 || rect.Height == 0) - { - Log("UI Automation: Element has no valid bounding rectangle"); - return false; - } - - int clickX = (int)(rect.X + rect.Width / 2); - int clickY = (int)(rect.Y + rect.Height / 2); - - SetCursorPos(clickX, clickY); - System.Threading.Thread.Sleep(100); - mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, IntPtr.Zero); - System.Threading.Thread.Sleep(50); - mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, IntPtr.Zero); - - Log("UI Automation: Clicked element at (" + clickX + ", " + clickY + ")"); - return true; - } - catch (Exception ex) - { - Log("UI Automation: Mouse click failed: " + ex.Message); - return false; - } - } - - /// - /// Logs elements whose names contain mode-related keywords for debugging. - /// - private static void ListAvailableElements(AutomationElement root) - { - try - { - var allElements = root.FindAll(TreeScope.Descendants, - System.Windows.Automation.Condition.TrueCondition); - - int count = 0; - foreach (AutomationElement el in allElements) - { - try - { - string name = el.Current.Name ?? ""; - string ctType = el.Current.ControlType.ProgrammaticName ?? ""; - string nameLower = name.ToLowerInvariant(); - - if (!string.IsNullOrEmpty(name) && (nameLower.Contains("mode") || - nameLower.Contains("ask") || nameLower.Contains("edit") || - nameLower.Contains("debug") || nameLower.Contains("agent"))) - { - Log("UI Automation: Available element [" + ctType + "]: '" + name + "'"); - count++; - } - } - catch - { - } - } - - Log("UI Automation: Total matching elements found: " + count); - } - catch (Exception ex) - { - Log("UI Automation error in ListAvailableElements: " + ex.Message); - } - } - /// /// Attempts to find the Copilot Chat text input area and set keyboard focus to it. /// Uses heuristics: editable controls, document controls, or any focusable @@ -1221,17 +1206,6 @@ private static bool FocusCopilotInput(AutomationElement root) return false; } - // ==================== Native Mouse Click ==================== - - [System.Runtime.InteropServices.DllImport("user32.dll")] - private static extern bool SetCursorPos(int X, int Y); - - [System.Runtime.InteropServices.DllImport("user32.dll")] - private static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, IntPtr dwExtraInfo); - - private const uint MOUSEEVENTF_LEFTDOWN = 0x02; - private const uint MOUSEEVENTF_LEFTUP = 0x04; - // ==================== Clipboard ==================== private static bool CopyToClipboard(string text) diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistConstants.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistConstants.cs index f4445fda..aa315910 100644 --- a/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistConstants.cs +++ b/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistConstants.cs @@ -245,6 +245,25 @@ public static string GetRichSeverityName(SeverityLevel severity) public const string CopilotOpenInstructionsMessage = "Prompt copied to clipboard! Paste it into GitHub Copilot Chat (Agent Mode)."; public const string CopilotGenericFallbackMessage = "Prompt copied to clipboard. Paste into GitHub Copilot Chat."; + /// Non-modal (info bar) when Copilot extension/commands are not registered. + public const string CopilotNotInstalledInfoBarMessage = + "GitHub Copilot is not installed. Your prompt was copied to the clipboard—install GitHub Copilot, open Copilot Chat, paste the prompt, switch to Agent mode, then submit."; + + /// Non-modal when Copilot Chat UI could not be opened (commands may exist but host failed). + public const string CopilotChatOpenFailedInfoBarMessage = + "Could not open GitHub Copilot Chat. Your prompt was copied to the clipboard—open Copilot Chat manually, paste the prompt, switch to Agent mode, then submit."; + + /// Non-modal when the user is not in Agent mode; prompt was pasted without sending. + public const string CopilotNotAgentModeInfoBarMessage = + "GitHub Copilot Chat is not in Agent mode. Your prompt is ready in Copilot Chat—switch to Agent mode, then submit."; + + /// Non-modal for VS 2026+; mode detection is unavailable so prompt is pasted without auto-submit in any mode. + public const string CopilotPasteOnlyVs2026InfoBarMessage = + "Prompt pasted into GitHub Copilot Chat. Please switch to Agent mode (Ignore if already in Agent mode) and press Enter to submit."; + + /// Non-modal when paste/focus into Copilot input failed. + public const string CopilotPromptPrepareFailedInfoBarMessage = "Unable to prepare prompt in Copilot."; + /// Context menu / Error List / Quick Info / Quick Fix: "Ignore this [finding type]" label based on scanner. public static string GetIgnoreThisLabel(ScannerType scanner) { diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistCopilotActions.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistCopilotActions.cs index 0ddee99b..59aaa67c 100644 --- a/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistCopilotActions.cs +++ b/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistCopilotActions.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Windows; using ast_visual_studio_extension.CxExtension.CxAssist.Core.Models; using ast_visual_studio_extension.CxExtension.CxAssist.Core.Prompts; @@ -84,9 +83,9 @@ public static void SendViewDetails(Vulnerability v, IReadOnlyList private static void ShowNoPromptMessage(string detail, bool isFix) { string message = isFix - ? "No fix prompt available for this finding.\n" + detail - : "View Details:\n" + detail; - MessageBox.Show(message, CxAssistConstants.DisplayName, MessageBoxButton.OK, MessageBoxImage.Information); + ? "No fix prompt available for this finding. " + detail + : "View Details: " + detail; + CopilotIntegration.ShowAssistNotification(message, isError: false); } } } diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistDisplayCoordinator.cs b/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistDisplayCoordinator.cs index 467aee6d..9d459cb0 100644 --- a/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistDisplayCoordinator.cs +++ b/ast-visual-studio-extension/CxExtension/CxAssist/Core/CxAssistDisplayCoordinator.cs @@ -357,5 +357,54 @@ public static void RefreshProblemWindow( findingsControl.SetAllFileNodes(fileNodes); }, "Coordinator.RefreshProblemWindow"); } + + /// + /// Clears all findings from the coordinator. + /// Used when user logs out or disables all scanners. + /// + public static void ClearAllFindings() + { + lock (_lock) + { + _fileToIssues.Clear(); + } + IssuesUpdated?.Invoke(new Dictionary>()); + } + + /// + /// Clears findings from disabled scanners only. + /// Called when user toggles a scanner off via preferences. + /// Aligned with JetBrains ProblemHolderService.removeAllScanIssuesOfType(). + /// + public static void ClearFindingsFromDisabledScanners() + { + var filesToRemove = new List(); + + lock (_lock) + { + // Iterate all files and remove findings from disabled scanners + foreach (var filePath in _fileToIssues.Keys.ToList()) + { + if (!_fileToIssues.TryGetValue(filePath, out var vulnerabilities) || vulnerabilities == null) + continue; + + // Keep only findings from enabled scanners + _fileToIssues[filePath] = vulnerabilities + .Where(v => v != null && CxAssistConstants.IsScannerEnabled(v.Scanner)) + .ToList(); + + // Mark file for removal if no vulnerabilities remain + if (_fileToIssues[filePath].Count == 0) + filesToRemove.Add(filePath); + } + + // Remove files with no remaining vulnerabilities + foreach (var filePath in filesToRemove) + _fileToIssues.Remove(filePath); + } + + // Broadcast update to all UI subscribers + IssuesUpdated?.Invoke(GetAllIssuesByFile()); + } } } diff --git a/ast-visual-studio-extension/CxExtension/CxAssist/UI/FindingsWindow/CxAssistFindingsControl.xaml b/ast-visual-studio-extension/CxExtension/CxAssist/UI/FindingsWindow/CxAssistFindingsControl.xaml index 84598be5..607500aa 100644 --- a/ast-visual-studio-extension/CxExtension/CxAssist/UI/FindingsWindow/CxAssistFindingsControl.xaml +++ b/ast-visual-studio-extension/CxExtension/CxAssist/UI/FindingsWindow/CxAssistFindingsControl.xaml @@ -23,6 +23,8 @@