diff --git a/README.md b/README.md
index 3e6980d..982b315 100644
--- a/README.md
+++ b/README.md
@@ -1,189 +1,109 @@
-# ide-visualstudio-plugin
+# Conviso Platform for Visual Studio
-Visual Studio 2022 extension for Conviso Platform integration.
+Analyze code, review security vulnerabilities, receive AI-powered remediation guidance, and interact with Conviso AI without leaving Visual Studio.
-## What this repository is
+## 🔐 What can you do with Conviso Platform for Visual Studio?
-This repository is the Visual Studio port of the existing Conviso IDE plugin ecosystem.
-It is no longer just a blank scaffold: the extension already exposes commands, tool windows, settings persistence, GraphQL-backed views, and broker-backed chat actions.
+- **Analyze code and receive AI-powered security recommendations.**
+ Select code in the editor to identify potential security issues, understand their risks, and receive practical remediation guidance.
-What is still true is that the port has not been validated end to end on a Windows + Visual Studio environment yet. Because of that, the main risk today is not missing code structure, but unclear operator guidance and unknown runtime gaps.
+- **Apply AI-assisted code fixes.**
+ When an AI response includes a suggested code block, apply it directly to the current editor selection after reviewing and confirming the change.
-## Current scope
+- **Review vulnerabilities from Conviso Platform.**
+ Filter vulnerabilities by asset for the configured company, inspect their details, request an AI-generated fix, and update vulnerability status from Visual Studio.
-Implemented in the repository:
+- **Track projects, security requirements, and activities.**
+ Browse projects and their requirements, inspect related activities, and update project or activity status.
-- VSIX project and solution structure for Visual Studio 2022
-- `AsyncPackage` bootstrap and command registration
-- `Conviso Settings` tool window
-- `Conviso Chat` tool window with:
- - broker connection
- - free-form chat
- - attach selection
- - analyze selected code
- - check similar issues across the workspace
- - apply suggested fix from the latest assistant response
- - mark response as helpful
-- `Conviso Vulnerabilities` tool window with:
- - company filter
- - asset filter
- - vulnerability list
- - vulnerability details
- - generate-fix action
- - vulnerability status update
-- `Conviso Requirements` tool window with:
- - projects list
- - requirement list per project
- - activities list per requirement
- - project status update
- - activity status update
-- `Conviso Pipeline Breaks` tool window with list and details
-- GraphQL diagnostics routed to the Visual Studio ActivityLog
-- Secrets stored with Windows user protection through the settings service
+- **Investigate pipeline breaks.**
+ Review security gate executions and inspect their status, source, asset, trigger, and failure reasons.
-Known gaps and risks:
+- **Chat with Conviso AI using IDE context.**
+ Attach a code selection, analyze selected code, include workspace files when checking for similar issues, apply a suggested fix, and mark useful responses.
-- no validated Windows test pass yet
-- build and installation flow has not been documented until now
-- broker compatibility still depends on the target environment
-- local AST scanner parity with the VS Code extension is not present
-- there is no automated validation in this repository for the VSIX runtime behavior
+> The current Visual Studio extension does not include a standalone local repository scanner or a separate Repository Vulnerabilities view.
-## Quick start
+---
-### Prerequisites
+## 🔑 Getting access
-- Windows
-- Visual Studio 2022
-- access to a Conviso API token
-- access to a broker endpoint and API key when chat features are used
+You need a Conviso Platform account and a valid Platform API token.
-### Install and open
+If you do not have an account, sign up or start a free trial:
-For end users and QA:
+👉 https://www.convisoappsec.com/
-1. Install the provided `.vsix` package.
-2. Open Visual Studio 2022.
-3. Open a repository or project with editable source files.
+---
-For maintainers preparing a test build:
+## 🛠️ Installation and configuration
-1. Open `Conviso.Platform.VisualStudio.sln` in Visual Studio 2022.
-2. Restore packages if Visual Studio prompts for it.
-3. Build the solution in `Debug` or `Release`.
-4. Start the extension in the Experimental Instance with `F5`, or distribute the generated `.vsix` on Windows.
+### Install the extension
-Notes:
+1. Go to **Extensions > Manage Extensions > Online**.
+2. Search for **Conviso Platform**.
+3. Install.
-- The project targets `.NET Framework 4.7.2`.
-- This repository can be edited on macOS, but the VSIX cannot be validated end to end outside Windows + Visual Studio 2022.
+### Configure Conviso Platform
-## How to use the extension
+1. In Visual Studio, open **Tools > Conviso Settings**.
+2. On the **Platform API** tab, enter your **API Token**.
+3. Open the **Scope** tab and choose the company to use.
+4. Select **Save Settings**.
-After installation, open the commands from the Visual Studio `Tools` menu:
+You can use **Test API** and **Test Chat** to validate the credentials. The API token is stored using Windows user-level data protection.
-- `Conviso Chat`
-- `Conviso Vulnerabilities`
-- `Conviso Requirements`
-- `Conviso Pipeline Breaks`
-- `Conviso Settings`
-- `Analyze + Suggest Fix`
-- `Attach Selection to Chat`
-- `Check Similar Issues`
+---
-### 1. Configure settings first
+## 🚀 How to use
-Open `Tools > Conviso Settings` and fill:
+Open extension features from Visual Studio's **Tools** menu:
-- `API Base URL`
-- `API Token`
-- `Company ID`
-- `Requirements Scope ID`
-- `Broker Endpoint`
-- `Broker API Key`
+- **Conviso Chat** — opens **AI Autonomous AppSec** for security questions, code analysis, workspace similarity checks, and AI-assisted fixes.
+- **Conviso Vulnerabilities** — reviews and manages vulnerabilities from Conviso Platform.
+- **Conviso Requirements** — browses projects, requirements, and related activities.
+- **Conviso Pipeline Breaks** — investigates security gate executions and their failure reasons.
+- **Conviso Settings** — configures the API token and company.
-Useful behavior already implemented:
+The **Tools** menu also provides direct editor actions:
-- `Load Companies` fetches accessible companies from the API
-- `Use Selected Company` copies the selected company into `Company ID` and `Requirements Scope ID`
-- `Save Settings` persists text values and stores secrets with Windows user protection
-- `Test API` performs a GraphQL round-trip
-- `Test Broker` attempts a WebSocket connection
-- `Use Defaults` restores the default API base URL and broker endpoint
+- **Analyze + Suggest Fix**
+- **Attach Selection to Chat**
+- **Check Similar Issues**
-### 2. Use chat features
+### Analyze selected code
-Open `Tools > Conviso Chat`.
+1. Open a source file and select the relevant code.
+2. Choose **Tools > Analyze + Suggest Fix**, or open **Conviso Chat** and use **Analyze + Suggest Fix**.
+3. Review the response in **AI Autonomous AppSec**.
+4. If the response contains a fenced code block, select the destination code and choose **Apply Suggested Fix**.
-The chat window supports:
+The extension always asks for confirmation before replacing the current selection.
-- `Connect Chat`
-- `Send Message`
-- `Attach Selection`
-- `Analyze + Suggest Fix`
-- `Check Similar Issues`
-- `Apply Suggested Fix`
-- `Mark Helpful`
+### Use selection and workspace context
-Expected workflow:
+- **Attach Selection** adds the current code selection to subsequent chat messages.
+- **Clear** removes the attached selection.
+- **Check Similar Issues** collects supported workspace files and asks Conviso AI to identify similar patterns.
+- **Clear Chat** removes the current transcript.
-1. Open a file and select the code you want to analyze.
-2. Use `Attach Selection` if you want the selected code added as context to the chat session.
-3. Use `Analyze + Suggest Fix` to send the current selection for analysis.
-4. Use `Check Similar Issues` to scan the workspace for similar patterns.
-5. If the assistant returns a fenced code block, use `Apply Suggested Fix` to replace the current selection after confirmation.
+### Manage vulnerabilities
-Important behavior:
+1. Open **Tools > Conviso Vulnerabilities**.
+2. Optionally filter by asset.
+3. Select a vulnerability to inspect its title, description, severity, status, and asset.
+4. Use **Generate Fix** to request an AI-assisted remediation result.
+5. Enter a permitted status and use **Update Status** to update the finding in Conviso Platform.
-- `Analyze + Suggest Fix`, `Attach Selection to Chat`, and `Check Similar Issues` are also exposed as top-level menu commands and will open the chat window automatically.
-- `Apply Suggested Fix` only works when there is a current editor selection and the assistant response contains a fenced code block.
+### Review requirements and pipeline breaks
-### 3. Use data views
+- In **Conviso Requirements**, select a project to view its details and requirements, then select a requirement to inspect its activities. Project and activity status can be updated from their detail panels.
+- In **Conviso Pipeline Breaks**, select an execution to inspect its status, date, trigger, source, asset, and severity-based failure reasons.
-`Conviso Vulnerabilities`
+---
-- filter by company and asset
-- refresh the list
-- inspect details
-- generate a fix suggestion
-- update vulnerability status
+## 💡 Pro tip
-`Conviso Requirements`
+Use Conviso Platform throughout development: attach the smallest relevant code selection for focused AI guidance, check for similar patterns across the workspace, and review Platform findings before committing your changes.
-- load projects
-- inspect project details
-- inspect requirements for a selected project
-- inspect activities for a selected requirement
-- update project status
-- update activity status
-
-`Conviso Pipeline Breaks`
-
-- load the list of pipeline break executions
-- inspect execution details for the selected row
-
-## Recommended validation flow
-
-Use the checklist in [docs/manual-validation.md](/Users/welias/welias_files/dev/ide-visualstudio-plugin/docs/manual-validation.md) before calling this port production-ready.
-
-## Troubleshooting
-
-- If API requests fail, inspect the Visual Studio ActivityLog. GraphQL request and response details are logged there.
-- If company discovery fails, verify `API Base URL` and `API Token` first.
-- If chat does not connect, verify the broker endpoint resolves to `/ws` and that the broker API key is valid.
-- If `Apply Suggested Fix` is disabled or fails, confirm that:
- - the assistant returned a fenced code block
- - the target file is open
- - the intended replacement region is selected
-
-## Repository layout
-
-- `src/Conviso.Platform.VisualStudio`: Visual Studio VSIX project
-- `docs/architecture.md`: port architecture and runtime boundaries
-- `docs/manual-validation.md`: manual test checklist for Windows validation
-
-## References
-
-- `../platform-ide-plugins/docs/ide-adapters.md`
-- `../platform-ide-plugins/docs/protocol.md`
-- `../ide-vscode-plugin/docs/architecture.md`
+Build secure software without leaving **Visual Studio** using **Conviso Platform**.
diff --git a/docs/README-internal.md b/docs/README-internal.md
new file mode 100644
index 0000000..bcc8b21
--- /dev/null
+++ b/docs/README-internal.md
@@ -0,0 +1,186 @@
+# ide-visualstudio-plugin
+
+Visual Studio 2022 extension for Conviso Platform integration.
+
+## What this repository is
+
+This repository is the Visual Studio port of the existing Conviso IDE plugin ecosystem.
+It is no longer just a blank scaffold: the extension already exposes commands, tool windows, settings persistence, GraphQL-backed views, and broker-backed chat actions.
+
+What is still true is that the port has not been validated end to end on a Windows + Visual Studio environment yet. Because of that, the main risk today is not missing code structure, but unclear operator guidance and unknown runtime gaps.
+
+## Current scope
+
+Implemented in the repository:
+
+- VSIX project and solution structure for Visual Studio 2022
+- `AsyncPackage` bootstrap and command registration
+- `Conviso Settings` tool window
+- `Conviso Chat` tool window with:
+ - broker connection
+ - free-form chat
+ - attach selection
+ - analyze selected code
+ - check similar issues across the workspace
+ - apply suggested fix from the latest assistant response
+ - mark response as helpful
+- `Conviso Vulnerabilities` tool window with:
+ - globally configured company
+ - asset filter
+ - vulnerability list
+ - vulnerability details
+ - generate-fix action
+ - vulnerability status update
+- `Conviso Requirements` tool window with:
+ - projects list
+ - requirement list per project
+ - activities list per requirement
+ - project status update
+ - activity status update
+- `Conviso Pipeline Breaks` tool window with list and details
+- GraphQL diagnostics routed to the Visual Studio ActivityLog
+- Secrets stored with Windows user protection through the settings service
+
+Known gaps and risks:
+
+- no validated Windows test pass yet
+- build and installation flow has not been documented until now
+- broker compatibility still depends on the target environment
+- local AST scanner parity with the VS Code extension is not present
+- there is no automated validation in this repository for the VSIX runtime behavior
+
+## Quick start
+
+### Prerequisites
+
+- Windows
+- Visual Studio 2022
+- access to a Conviso API token
+- access to a broker endpoint and API key when chat features are used
+
+### Install and open
+
+For end users and QA:
+
+1. Install the provided `.vsix` package.
+2. Open Visual Studio 2022.
+3. Open a repository or project with editable source files.
+
+For maintainers preparing a test build:
+
+1. Open `Conviso.Platform.VisualStudio.sln` in Visual Studio 2022.
+2. Restore packages if Visual Studio prompts for it.
+3. Build the solution in `Debug` or `Release`.
+4. Start the extension in the Experimental Instance with `F5`, or distribute the generated `.vsix` on Windows.
+
+Notes:
+
+- The project targets `.NET Framework 4.7.2`.
+- This repository can be edited on macOS, but the VSIX cannot be validated end to end outside Windows + Visual Studio 2022.
+
+## How to use the extension
+
+After installation, open the commands from the Visual Studio `Tools` menu:
+
+- `Conviso Chat`
+- `Conviso Vulnerabilities`
+- `Conviso Requirements`
+- `Conviso Pipeline Breaks`
+- `Conviso Settings`
+- `Analyze + Suggest Fix`
+- `Attach Selection to Chat`
+- `Check Similar Issues`
+
+### 1. Configure settings first
+
+Open `Tools > Conviso Settings` and fill:
+
+- `API Token`
+- `Company` on the `Scope` tab
+
+Useful behavior already implemented:
+
+- opening `Scope` fetches accessible companies from the API
+- the first accessible company is selected automatically on first configuration
+- changing `Company` persists the global company immediately
+- `Save Settings` stores the API token with Windows user protection
+- `Test API` performs a GraphQL round-trip
+- `Test Broker` attempts a WebSocket connection
+
+### 2. Use chat features
+
+Open `Tools > Conviso Chat`.
+
+The chat window supports:
+
+- `Send Message`
+- `Attach Selection`
+- `Analyze + Suggest Fix`
+- `Check Similar Issues`
+- `Apply Suggested Fix`
+- `Mark Helpful`
+
+The broker connection is established automatically when a chat action needs it. A centered `Thinking...` indicator uses a permanently reserved line and is empty when no response is pending.
+
+Expected workflow:
+
+1. Open a file and select the code you want to analyze.
+2. Use `Attach Selection` if you want the selected code added as context to the chat session.
+3. Use `Analyze + Suggest Fix` to send the current selection for analysis.
+4. Use `Check Similar Issues` to scan the workspace for similar patterns.
+5. If the assistant returns a fenced code block, use `Apply Suggested Fix` to replace the current selection after confirmation.
+
+Important behavior:
+
+- `Analyze + Suggest Fix`, `Attach Selection to Chat`, and `Check Similar Issues` are also exposed as top-level menu commands and will open the chat window automatically.
+- `Apply Suggested Fix` only works when there is a current editor selection and the assistant response contains a fenced code block.
+
+### 3. Use data views
+
+`Conviso Vulnerabilities`
+
+- use the globally configured company and filter by asset
+- refresh the list
+- inspect details
+- generate a fix suggestion
+- update vulnerability status
+
+`Conviso Requirements`
+
+- load projects
+- inspect project details
+- inspect requirements for a selected project
+- inspect activities for a selected requirement
+- update project status
+- update activity status
+
+`Conviso Pipeline Breaks`
+
+- load the list of pipeline break executions
+- inspect execution details for the selected row
+
+## Recommended validation flow
+
+Use the checklist in `docs/manual-validation.md` before calling this port production-ready.
+
+## Troubleshooting
+
+- If API requests fail, inspect the Visual Studio ActivityLog. GraphQL request and response details are logged there.
+- If company discovery fails, verify `API Base URL` and `API Token` first.
+- If chat does not connect, verify the broker endpoint resolves to `/ws` and that the broker API key is valid.
+- If `Apply Suggested Fix` is disabled or fails, confirm that:
+ - the assistant returned a fenced code block
+ - the target file is open
+ - the intended replacement region is selected
+
+## Repository layout
+
+- `src/Conviso.Platform.VisualStudio`: Visual Studio VSIX project
+- `docs/architecture.md`: port architecture and runtime boundaries
+- `docs/manual-validation.md`: manual test checklist for Windows validation
+
+## References
+
+- `../platform-ide-plugins/docs/ide-adapters.md`
+- `../platform-ide-plugins/docs/protocol.md`
+- `../ide-vscode-plugin/docs/architecture.md`
diff --git a/docs/manual-validation.md b/docs/manual-validation.md
index 6bb9c02..cb882ed 100644
--- a/docs/manual-validation.md
+++ b/docs/manual-validation.md
@@ -48,50 +48,44 @@ Expected result:
1. Open `Tools > Conviso Settings`.
2. Verify all fields render:
- - API Base URL
- API Token
- - Company ID
- - Requirements Scope ID
- - Accessible companies
- - Broker Endpoint
- - Broker API Key
-3. Click `Use Defaults`.
-4. Enter valid API and broker credentials.
-5. Click `Save Settings`.
-6. Click `Test API`.
-7. Click `Load Companies`.
-8. Select a company and click `Use Selected Company`.
-9. Click `Save Settings` again.
-10. Click `Test Broker`.
+ - Company
+3. Enter a valid API token.
+4. Click `Save Settings`.
+5. Click `Test API`.
+6. Open the `Scope` tab.
+7. Confirm that accessible companies load automatically.
+8. If this is the first configuration, confirm that the first company is selected automatically.
+9. Select another company, close the settings window, reopen it, and return to `Scope`.
+10. Click `Test Chat`.
Expected result:
- status messages change coherently after each action
- accessible companies load for a valid token
-- selected company fills both `Company ID` and `Requirements Scope ID`
+- the selected company is saved immediately and restored when settings are reopened
- API test succeeds or returns a clear permission error
-- broker test succeeds or returns a clear connection error
+- chat test succeeds or returns a clear connection error
## Chat window
1. Open `Tools > Conviso Chat`.
-2. Click `Connect Chat`.
-3. Send a plain message.
+2. Send a plain message without connecting manually.
+3. Confirm that `Thinking...` appears centered between the transcript and message input while the response is pending, then changes to an empty reserved line.
4. Open a code file, select a snippet, and click `Attach Selection`.
5. Click `Analyze + Suggest Fix`.
6. Click `Check Similar Issues`.
7. If a response contains a fenced code block, select the target code and click `Apply Suggested Fix`.
8. Click `Mark Helpful`.
-9. Click `Disconnect`.
Expected result:
-- connection status changes to connected
+- the chat connects automatically when an action needs the broker
- transcript shows user and assistant messages
+- the transient `Thinking...` indicator is empty when no response is pending without shifting the input
- attach-selection updates the context summary
- analyze and similarity actions append responses instead of failing silently
- apply-fix asks for confirmation and replaces only the current selection
-- disconnect updates the status and transcript
## Menu command shortcuts
@@ -108,18 +102,22 @@ Expected result:
## Vulnerabilities window
1. Open `Tools > Conviso Vulnerabilities`.
-2. Confirm the company filter loads.
-3. Change the company filter.
-4. Change the asset filter.
-5. Click `Refresh`.
-6. Select a vulnerability row.
-7. Review the details panel.
-8. Click `Generate Fix`.
-9. Enter a new status and click `Update Status`.
+2. Confirm that data uses the company configured in `Conviso Settings > Scope`.
+3. Type part of a vulnerability title in `Filter by title...`.
+4. Confirm results change on each keystroke without a loading request.
+5. Clear the text filter and confirm all loaded vulnerabilities return.
+6. Change the asset filter.
+7. Click `Refresh`.
+8. Select a vulnerability row.
+9. Review the details panel.
+10. Click `Generate Fix`.
+11. Enter a new status and click `Update Status`.
Expected result:
- list loads without UI crashes
+- the title is the first column and horizontal scrolling is available when columns overflow
+- title filtering is case-insensitive, supports no-match results, and does not discard loaded items
- filters do not break the view
- selecting a row loads details
- generate-fix returns a meaningful result or a clear backend error
@@ -129,17 +127,22 @@ Expected result:
1. Open `Tools > Conviso Requirements`.
2. Click `Refresh`.
-3. Select a project.
-4. Verify project details load.
-5. Select a requirement.
-6. Verify requirement details and activities load.
-7. Select an activity.
-8. Change project status and click `Update Project`.
-9. Change activity status and click `Update Activity`.
+3. Filter projects by label, then clear the filter.
+4. Select a project.
+5. Verify project details load.
+6. Filter requirements by label, then clear the filter.
+7. Select a requirement.
+8. Verify requirement details and activities load.
+9. Filter activities by title, then clear the filter.
+10. Select an activity.
+11. Change project status and click `Update Project`.
+12. Change activity status and click `Update Activity`.
Expected result:
- projects load
+- project, requirement, and activity lists show horizontal scrollbars when their columns overflow
+- each text filter updates only its local loaded list and preserves the full list when cleared
- selecting a project populates requirements
- selecting a requirement populates activities
- detail sidebars update correctly
@@ -149,11 +152,14 @@ Expected result:
1. Open `Tools > Conviso Pipeline Breaks`.
2. Click `Refresh`.
-3. Select an item.
+3. Filter the list by part of an asset name, then clear the filter.
+4. Select an item.
Expected result:
- list loads
+- the list shows a horizontal scrollbar when its columns overflow
+- asset filtering updates immediately without an additional API request
- selecting a row loads execution details
- empty-state or setup-state messages remain readable
diff --git a/src/Conviso.Platform.VisualStudio/AssemblyInfo.cs b/src/Conviso.Platform.VisualStudio/AssemblyInfo.cs
index 0f26709..21bed72 100644
--- a/src/Conviso.Platform.VisualStudio/AssemblyInfo.cs
+++ b/src/Conviso.Platform.VisualStudio/AssemblyInfo.cs
@@ -5,6 +5,6 @@
[assembly: AssemblyDescription("Conviso Platform Visual Studio extension")]
[assembly: AssemblyCompany("Conviso")]
[assembly: AssemblyProduct("Conviso Platform Visual Studio")]
-[assembly: AssemblyVersion("0.1.4.0")]
-[assembly: AssemblyFileVersion("0.1.4.0")]
+[assembly: AssemblyVersion("0.2.1.0")]
+[assembly: AssemblyFileVersion("0.2.1.0")]
[assembly: ComVisible(false)]
diff --git a/src/Conviso.Platform.VisualStudio/Commands/AnalyzeSecurityAndSuggestFixCommand.cs b/src/Conviso.Platform.VisualStudio/Commands/AnalyzeSecurityAndSuggestFixCommand.cs
index 85a47a4..cb99148 100644
--- a/src/Conviso.Platform.VisualStudio/Commands/AnalyzeSecurityAndSuggestFixCommand.cs
+++ b/src/Conviso.Platform.VisualStudio/Commands/AnalyzeSecurityAndSuggestFixCommand.cs
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel.Design;
+using System.Threading;
using System.Threading.Tasks;
using Conviso.Platform.VisualStudio.ToolWindows;
using Microsoft.VisualStudio.Shell;
@@ -9,6 +10,7 @@ namespace Conviso.Platform.VisualStudio.Commands
internal sealed class AnalyzeSecurityAndSuggestFixCommand
{
private readonly AsyncPackage package;
+ private int isExecuting;
private AnalyzeSecurityAndSuggestFixCommand(AsyncPackage package, OleMenuCommandService commandService)
{
@@ -25,12 +27,32 @@ public static async Task InitializeAsync(AsyncPackage package)
private void Execute(object? sender, EventArgs e)
{
- ThreadHelper.JoinableTaskFactory.RunAsync(async delegate
+ if (Interlocked.Exchange(ref isExecuting, 1) != 0)
{
- ToolWindowPane window = await package.ShowToolWindowAsync(typeof(ChatToolWindow), 0, true, package.DisposalToken);
- if (window?.Content is ChatToolWindowControl control)
+ return;
+ }
+
+ _ = ThreadHelper.JoinableTaskFactory.RunAsync(async delegate
+ {
+ try
+ {
+ ToolWindowPane window = await package.ShowToolWindowAsync(typeof(ChatToolWindow), 0, true, package.DisposalToken);
+ if (window?.Content is ChatToolWindowControl control)
+ {
+ await control.RunAnalyzeSecurityAndSuggestFixAsync();
+ }
+ }
+ catch (OperationCanceledException) when (package.DisposalToken.IsCancellationRequested)
+ {
+
+ }
+ catch (Exception error)
+ {
+ Infrastructure.DiagnosticsLogger.LogError("Analyze + Suggest Fix failed: " + error);
+ }
+ finally
{
- await control.RunAnalyzeSecurityAndSuggestFixAsync();
+ Volatile.Write(ref isExecuting, 0);
}
});
}
diff --git a/src/Conviso.Platform.VisualStudio/Commands/AttachSelectionToChatCommand.cs b/src/Conviso.Platform.VisualStudio/Commands/AttachSelectionToChatCommand.cs
index 3a50a92..56050d3 100644
--- a/src/Conviso.Platform.VisualStudio/Commands/AttachSelectionToChatCommand.cs
+++ b/src/Conviso.Platform.VisualStudio/Commands/AttachSelectionToChatCommand.cs
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel.Design;
+using System.Threading;
using System.Threading.Tasks;
using Conviso.Platform.VisualStudio.ToolWindows;
using Microsoft.VisualStudio.Shell;
@@ -9,6 +10,7 @@ namespace Conviso.Platform.VisualStudio.Commands
internal sealed class AttachSelectionToChatCommand
{
private readonly AsyncPackage package;
+ private int isExecuting;
private AttachSelectionToChatCommand(AsyncPackage package, OleMenuCommandService commandService)
{
@@ -25,12 +27,32 @@ public static async Task InitializeAsync(AsyncPackage package)
private void Execute(object? sender, EventArgs e)
{
- ThreadHelper.JoinableTaskFactory.RunAsync(async delegate
+ if (Interlocked.Exchange(ref isExecuting, 1) != 0)
{
- ToolWindowPane window = await package.ShowToolWindowAsync(typeof(ChatToolWindow), 0, true, package.DisposalToken);
- if (window?.Content is ChatToolWindowControl control)
+ return;
+ }
+
+ _ = ThreadHelper.JoinableTaskFactory.RunAsync(async delegate
+ {
+ try
+ {
+ ToolWindowPane window = await package.ShowToolWindowAsync(typeof(ChatToolWindow), 0, true, package.DisposalToken);
+ if (window?.Content is ChatToolWindowControl control)
+ {
+ await control.RunAttachSelectionAsync();
+ }
+ }
+ catch (OperationCanceledException) when (package.DisposalToken.IsCancellationRequested)
+ {
+
+ }
+ catch (Exception error)
+ {
+ Infrastructure.DiagnosticsLogger.LogError("Attach Selection failed: " + error);
+ }
+ finally
{
- await control.RunAttachSelectionAsync();
+ Volatile.Write(ref isExecuting, 0);
}
});
}
diff --git a/src/Conviso.Platform.VisualStudio/Commands/CheckSimilarIssuesCommand.cs b/src/Conviso.Platform.VisualStudio/Commands/CheckSimilarIssuesCommand.cs
index 0be9bce..5e94ee8 100644
--- a/src/Conviso.Platform.VisualStudio/Commands/CheckSimilarIssuesCommand.cs
+++ b/src/Conviso.Platform.VisualStudio/Commands/CheckSimilarIssuesCommand.cs
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel.Design;
+using System.Threading;
using System.Threading.Tasks;
using Conviso.Platform.VisualStudio.ToolWindows;
using Microsoft.VisualStudio.Shell;
@@ -9,6 +10,7 @@ namespace Conviso.Platform.VisualStudio.Commands
internal sealed class CheckSimilarIssuesCommand
{
private readonly AsyncPackage package;
+ private int isExecuting;
private CheckSimilarIssuesCommand(AsyncPackage package, OleMenuCommandService commandService)
{
@@ -25,12 +27,32 @@ public static async Task InitializeAsync(AsyncPackage package)
private void Execute(object? sender, EventArgs e)
{
- ThreadHelper.JoinableTaskFactory.RunAsync(async delegate
+ if (Interlocked.Exchange(ref isExecuting, 1) != 0)
{
- ToolWindowPane window = await package.ShowToolWindowAsync(typeof(ChatToolWindow), 0, true, package.DisposalToken);
- if (window?.Content is ChatToolWindowControl control)
+ return;
+ }
+
+ _ = ThreadHelper.JoinableTaskFactory.RunAsync(async delegate
+ {
+ try
+ {
+ ToolWindowPane window = await package.ShowToolWindowAsync(typeof(ChatToolWindow), 0, true, package.DisposalToken);
+ if (window?.Content is ChatToolWindowControl control)
+ {
+ await control.RunCheckSimilarIssuesAsync();
+ }
+ }
+ catch (OperationCanceledException) when (package.DisposalToken.IsCancellationRequested)
+ {
+
+ }
+ catch (Exception error)
+ {
+ Infrastructure.DiagnosticsLogger.LogError("Check Similar Issues failed: " + error);
+ }
+ finally
{
- await control.RunCheckSimilarIssuesAsync();
+ Volatile.Write(ref isExecuting, 0);
}
});
}
diff --git a/src/Conviso.Platform.VisualStudio/Commands/OpenToolWindowCommandBase.cs b/src/Conviso.Platform.VisualStudio/Commands/OpenToolWindowCommandBase.cs
index b532c4b..616403b 100644
--- a/src/Conviso.Platform.VisualStudio/Commands/OpenToolWindowCommandBase.cs
+++ b/src/Conviso.Platform.VisualStudio/Commands/OpenToolWindowCommandBase.cs
@@ -32,10 +32,21 @@ private void Execute(object? sender, EventArgs e)
{
ThreadHelper.JoinableTaskFactory.RunAsync(async delegate
{
- ToolWindowPane window = await package.ShowToolWindowAsync(typeof(TToolWindow), 0, true, package.DisposalToken);
- if (window == null)
+ try
{
- throw new InvalidOperationException($"Unable to open tool window {typeof(TToolWindow).Name}.");
+ ToolWindowPane window = await package.ShowToolWindowAsync(typeof(TToolWindow), 0, true, package.DisposalToken);
+ if (window == null)
+ {
+ throw new InvalidOperationException($"Unable to open tool window {typeof(TToolWindow).Name}.");
+ }
+ }
+ catch (OperationCanceledException) when (package.DisposalToken.IsCancellationRequested)
+ {
+
+ }
+ catch (Exception error)
+ {
+ Infrastructure.DiagnosticsLogger.LogError($"Unable to open {typeof(TToolWindow).Name}: {error}");
}
});
}
diff --git a/src/Conviso.Platform.VisualStudio/Configuration/ConvisoOptions.cs b/src/Conviso.Platform.VisualStudio/Configuration/ConvisoOptions.cs
index 210b00d..6a14dbb 100644
--- a/src/Conviso.Platform.VisualStudio/Configuration/ConvisoOptions.cs
+++ b/src/Conviso.Platform.VisualStudio/Configuration/ConvisoOptions.cs
@@ -3,12 +3,11 @@ namespace Conviso.Platform.VisualStudio.Configuration
internal static class ConvisoOptions
{
public const string DefaultApiBaseUrl = "https://api.convisoappsec.com";
- public const string DefaultBrokerEndpoint = "wss://conviso.docker.localhost/ws";
+ public const string DefaultBrokerEndpoint = "wss://securecode.convisoappsec.com/ws";
public const string CollectionPath = "ConvisoPlatform";
public const string ApiBaseUrlKey = "Conviso.ApiBaseUrl";
public const string CompanyIdKey = "Conviso.CompanyId";
- public const string RequirementsScopeIdKey = "Conviso.RequirementsScopeId";
public const string BrokerEndpointKey = "Conviso.BrokerEndpoint";
public const string ApiTokenKey = "Conviso.ApiToken";
public const string BrokerApiKeyKey = "Conviso.BrokerApiKey";
diff --git a/src/Conviso.Platform.VisualStudio/Conviso.Platform.VisualStudio.csproj b/src/Conviso.Platform.VisualStudio/Conviso.Platform.VisualStudio.csproj
index 566de14..8031ede 100644
--- a/src/Conviso.Platform.VisualStudio/Conviso.Platform.VisualStudio.csproj
+++ b/src/Conviso.Platform.VisualStudio/Conviso.Platform.VisualStudio.csproj
@@ -24,9 +24,20 @@
-
+
+
+
+
+
+
+
+
+
@@ -36,6 +47,9 @@
Designer
+
+ true
+
diff --git a/src/Conviso.Platform.VisualStudio/Conviso.Platform.VisualStudioPackage.vsct b/src/Conviso.Platform.VisualStudio/Conviso.Platform.VisualStudioPackage.vsct
index 4dcc7a0..b768838 100644
--- a/src/Conviso.Platform.VisualStudio/Conviso.Platform.VisualStudioPackage.vsct
+++ b/src/Conviso.Platform.VisualStudio/Conviso.Platform.VisualStudioPackage.vsct
@@ -6,7 +6,7 @@
-
+
diff --git a/src/Conviso.Platform.VisualStudio/ConvisoPlatformPackage.cs b/src/Conviso.Platform.VisualStudio/ConvisoPlatformPackage.cs
index 6ab3dd7..3fb0342 100644
--- a/src/Conviso.Platform.VisualStudio/ConvisoPlatformPackage.cs
+++ b/src/Conviso.Platform.VisualStudio/ConvisoPlatformPackage.cs
@@ -17,6 +17,7 @@ namespace Conviso.Platform.VisualStudio
[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
[InstalledProductRegistration("Conviso Platform", "Conviso Platform integration for Visual Studio", "0.1.4")]
[ProvideMenuResource("Menus.ctmenu", 1)]
+ [ProvideBindingPath]
[ProvideToolWindow(typeof(ChatToolWindow))]
[ProvideToolWindow(typeof(VulnerabilitiesToolWindow))]
[ProvideToolWindow(typeof(RequirementsToolWindow))]
diff --git a/src/Conviso.Platform.VisualStudio/Infrastructure/AsyncDelegateCommand.cs b/src/Conviso.Platform.VisualStudio/Infrastructure/AsyncDelegateCommand.cs
index dcc2988..9656c2c 100644
--- a/src/Conviso.Platform.VisualStudio/Infrastructure/AsyncDelegateCommand.cs
+++ b/src/Conviso.Platform.VisualStudio/Infrastructure/AsyncDelegateCommand.cs
@@ -36,6 +36,12 @@ public async void Execute(object? parameter)
RaiseCanExecuteChanged();
await execute();
}
+ catch (Exception error)
+ {
+ // ICommand.Execute is async void by contract. Never let an exception
+ // escape to WPF's dispatcher, because Visual Studio hosts us in-proc.
+ DiagnosticsLogger.LogError("Command failed: " + error);
+ }
finally
{
isExecuting = false;
diff --git a/src/Conviso.Platform.VisualStudio/Models/ChatTranscriptItem.cs b/src/Conviso.Platform.VisualStudio/Models/ChatTranscriptItem.cs
index ccbeb12..2298913 100644
--- a/src/Conviso.Platform.VisualStudio/Models/ChatTranscriptItem.cs
+++ b/src/Conviso.Platform.VisualStudio/Models/ChatTranscriptItem.cs
@@ -1,15 +1,35 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
namespace Conviso.Platform.VisualStudio.Models
{
- public sealed class ChatTranscriptItem
+ public sealed class ChatTranscriptItem : INotifyPropertyChanged
{
+ private string content;
+
public ChatTranscriptItem(string role, string content)
{
Role = role;
- Content = content;
+ this.content = content;
}
+ public event PropertyChangedEventHandler? PropertyChanged;
+
public string Role { get; }
- public string Content { get; }
+ public string Content
+ {
+ get => content;
+ set
+ {
+ if (content == value)
+ {
+ return;
+ }
+
+ content = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Content)));
+ }
+ }
}
}
diff --git a/src/Conviso.Platform.VisualStudio/Resources/pluginIcon.png b/src/Conviso.Platform.VisualStudio/Resources/pluginIcon.png
new file mode 100644
index 0000000..8e1a9b4
Binary files /dev/null and b/src/Conviso.Platform.VisualStudio/Resources/pluginIcon.png differ
diff --git a/src/Conviso.Platform.VisualStudio/Services/Broker/BrokerClient.cs b/src/Conviso.Platform.VisualStudio/Services/Broker/BrokerClient.cs
index 414a26a..e30a051 100644
--- a/src/Conviso.Platform.VisualStudio/Services/Broker/BrokerClient.cs
+++ b/src/Conviso.Platform.VisualStudio/Services/Broker/BrokerClient.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
@@ -14,6 +15,9 @@ internal sealed class BrokerClient : IBrokerClient
private ClientWebSocket? socket;
private CancellationTokenSource? receiveLoopCancellation;
private TaskCompletionSource? authenticationCompletionSource;
+ private readonly object exclusiveRequestsLock = new object();
+ private readonly HashSet exclusiveRequestIds = new HashSet(StringComparer.Ordinal);
+ private event Action? InternalEventReceived;
public event Action? EventReceived;
@@ -35,7 +39,11 @@ public async Task ConnectAsync(BrokerConnectionOptions options, CancellationToke
await socket.ConnectAsync(new Uri(endpoint), cancellationToken);
receiveLoopCancellation = new CancellationTokenSource();
- _ = Task.Run(() => ReceiveLoopAsync(socket, receiveLoopCancellation.Token), receiveLoopCancellation.Token);
+ ClientWebSocket activeSocket = socket;
+ CancellationToken receiveToken = receiveLoopCancellation.Token;
+ _ = Task.Run(
+ () => RunReceiveLoopSafelyAsync(activeSocket, receiveToken),
+ receiveToken);
string authRequestId = CreateRequestId("auth");
await SendMessageAsync(
@@ -111,6 +119,7 @@ public async Task RequestAutoFixAsync(string findingId, Cancellat
string requestId = CreateRequestId("req");
var responseCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ BrokerEvent? completedEvent = null;
void HandleEvent(BrokerEvent brokerEvent)
{
@@ -119,13 +128,14 @@ void HandleEvent(BrokerEvent brokerEvent)
return;
}
- if (brokerEvent.Type == "autofix_complete" || brokerEvent.Type == "autofix_suggested")
+ if (brokerEvent.Type == "analysis_complete")
{
+ completedEvent = brokerEvent;
responseCompletionSource.TrySetResult(brokerEvent);
return;
}
- if (brokerEvent.Type == "autofix_error" || brokerEvent.Type == "error")
+ if (brokerEvent.Type == "analysis_error" || brokerEvent.Type == "error")
{
responseCompletionSource.TrySetException(new InvalidOperationException(
string.IsNullOrWhiteSpace(brokerEvent.Content)
@@ -134,7 +144,11 @@ void HandleEvent(BrokerEvent brokerEvent)
}
}
- EventReceived += HandleEvent;
+ InternalEventReceived += HandleEvent;
+ lock (exclusiveRequestsLock)
+ {
+ exclusiveRequestIds.Add(requestId);
+ }
try
{
@@ -142,11 +156,16 @@ await SendMessageAsync(
socket,
new
{
- type = "request_autofix",
+ type = "analyze_code",
request_id = requestId,
payload = new
{
- finding_id = findingId,
+ code = string.Join(
+ "\n",
+ "Generate a secure remediation for the following Conviso Platform vulnerability.",
+ "Explain the risk and provide the corrected code in a fenced code block when possible.",
+ "Vulnerability ID: " + findingId),
+ language = "text",
},
},
cancellationToken);
@@ -163,7 +182,13 @@ await SendMessageAsync(
try
{
BrokerEvent response = await responseCompletionSource.Task;
- return ExtractAutoFixResult(response.RawPayload);
+ AutoFixResult result = ExtractAutoFixResult(response.RawPayload);
+ if (string.IsNullOrWhiteSpace(result.PrUrl) && string.IsNullOrWhiteSpace(result.Summary))
+ {
+ result.Summary = response.Content;
+ }
+
+ return result;
}
catch (TaskCanceledException) when (timeoutCancellation.IsCancellationRequested)
{
@@ -173,7 +198,26 @@ await SendMessageAsync(
}
finally
{
- EventReceived -= HandleEvent;
+ lock (exclusiveRequestsLock)
+ {
+ exclusiveRequestIds.Remove(requestId);
+ }
+
+ InternalEventReceived -= HandleEvent;
+
+ // Autofix chunks stay isolated to avoid flooding the chat UI, but
+ // the completed response is published once so users can review it.
+ if (completedEvent != null)
+ {
+ try
+ {
+ EventReceived?.Invoke(completedEvent);
+ }
+ catch
+ {
+ // A UI subscriber must not turn a completed fix into a failed request.
+ }
+ }
}
}
@@ -264,18 +308,34 @@ public async Task DisconnectAsync(CancellationToken cancellationToken)
receiveLoopCancellation?.Dispose();
receiveLoopCancellation = null;
- if (socket == null)
+ ClientWebSocket? activeSocket = socket;
+ socket = null;
+ if (activeSocket == null)
{
return;
}
- if (socket.State == WebSocketState.Open)
+ try
{
- await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "disconnect", cancellationToken);
+ if (activeSocket.State == WebSocketState.Open)
+ {
+ await activeSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "disconnect", cancellationToken);
+ }
+ }
+ catch (WebSocketException) when (activeSocket.State == WebSocketState.Aborted ||
+ activeSocket.State == WebSocketState.Closed)
+ {
+ // An aborted socket is already disconnected; cleanup is enough.
+ }
+ catch (InvalidOperationException) when (activeSocket.State == WebSocketState.Aborted ||
+ activeSocket.State == WebSocketState.Closed)
+ {
+ // CloseAsync cannot be used after the receive loop aborts the socket.
+ }
+ finally
+ {
+ activeSocket.Dispose();
}
-
- socket.Dispose();
- socket = null;
}
private async Task ReceiveLoopAsync(ClientWebSocket activeSocket, CancellationToken cancellationToken)
@@ -305,6 +365,23 @@ private async Task ReceiveLoopAsync(ClientWebSocket activeSocket, CancellationTo
}
}
+ private async Task RunReceiveLoopSafelyAsync(ClientWebSocket activeSocket, CancellationToken cancellationToken)
+ {
+ try
+ {
+ await ReceiveLoopAsync(activeSocket, cancellationToken);
+ }
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+ {
+
+ }
+ catch (Exception error)
+ {
+ authenticationCompletionSource?.TrySetException(error);
+ Infrastructure.DiagnosticsLogger.LogError("Chat receive loop stopped: " + error);
+ }
+ }
+
private void ProcessIncomingMessage(string raw)
{
BrokerEvent brokerEvent = ParseEvent(raw);
@@ -338,7 +415,18 @@ private void ProcessIncomingMessage(string raw)
return;
}
- EventReceived?.Invoke(brokerEvent);
+ InternalEventReceived?.Invoke(brokerEvent);
+
+ bool isExclusiveRequest;
+ lock (exclusiveRequestsLock)
+ {
+ isExclusiveRequest = exclusiveRequestIds.Contains(brokerEvent.RequestId);
+ }
+
+ if (!isExclusiveRequest)
+ {
+ EventReceived?.Invoke(brokerEvent);
+ }
}
private static BrokerEvent ParseEvent(string raw)
diff --git a/src/Conviso.Platform.VisualStudio/Services/Editor/EditorContextService.cs b/src/Conviso.Platform.VisualStudio/Services/Editor/EditorContextService.cs
index a591f30..be740e7 100644
--- a/src/Conviso.Platform.VisualStudio/Services/Editor/EditorContextService.cs
+++ b/src/Conviso.Platform.VisualStudio/Services/Editor/EditorContextService.cs
@@ -14,6 +14,7 @@ internal sealed class EditorContextService : IEditorContextService
{
private const int MaxWorkspaceFiles = 20;
private const int MaxWorkspaceCharacters = 50000;
+ private const int MaxEditorContextCharacters = 12000;
private readonly AsyncPackage package;
public EditorContextService(AsyncPackage package)
@@ -50,21 +51,35 @@ public EditorContextService(AsyncPackage package)
return null;
}
- EditPoint startPoint = textDocument.StartPoint.CreateEditPoint();
- string documentText = startPoint.GetText(textDocument.EndPoint);
-
string selectionText = string.Empty;
- if (textDocument.Selection != null && !textDocument.Selection.IsEmpty)
+ string documentText = string.Empty;
+ try
{
- selectionText = textDocument.Selection.Text ?? string.Empty;
+ TextSelection? selection = textDocument.Selection;
+ if (selection != null && !selection.IsEmpty)
+ {
+ selectionText = Truncate(selection.Text?.Trim() ?? string.Empty, MaxEditorContextCharacters);
+ }
+ else
+ {
+ EditPoint startPoint = textDocument.StartPoint.CreateEditPoint();
+ documentText = Truncate(
+ startPoint.GetText(textDocument.EndPoint).Trim(),
+ MaxEditorContextCharacters);
+ }
+ }
+ catch (Exception error)
+ {
+ Infrastructure.DiagnosticsLogger.LogError("Unable to capture active editor context: " + error);
+ return null;
}
return new EditorContextSnapshot
{
FilePath = filePath,
Language = language,
- SelectionText = selectionText.Trim(),
- DocumentText = documentText.Trim(),
+ SelectionText = selectionText,
+ DocumentText = documentText,
};
}
@@ -92,6 +107,17 @@ public EditorContextService(AsyncPackage package)
}
string extension = Path.GetExtension(reference.FilePath ?? string.Empty);
+ return await Task.Run(
+ () => CollectWorkspaceContext(rootPath, extension, reference.FilePath ?? string.Empty, cancellationToken),
+ cancellationToken);
+ }
+
+ private static WorkspaceContextSnapshot CollectWorkspaceContext(
+ string rootPath,
+ string extension,
+ string referenceFilePath,
+ CancellationToken cancellationToken)
+ {
var result = new WorkspaceContextSnapshot
{
RootPath = rootPath,
@@ -102,7 +128,7 @@ public EditorContextService(AsyncPackage package)
{
cancellationToken.ThrowIfCancellationRequested();
- if (string.Equals(filePath, reference.FilePath, StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(filePath, referenceFilePath, StringComparison.OrdinalIgnoreCase))
{
continue;
}
diff --git a/src/Conviso.Platform.VisualStudio/Services/Platform/GraphQlDocuments.cs b/src/Conviso.Platform.VisualStudio/Services/Platform/GraphQlDocuments.cs
index 1b95a4c..1d2b3cd 100644
--- a/src/Conviso.Platform.VisualStudio/Services/Platform/GraphQlDocuments.cs
+++ b/src/Conviso.Platform.VisualStudio/Services/Platform/GraphQlDocuments.cs
@@ -32,13 +32,11 @@ query Issues(
$pagination: PaginationInput!
$filters: IssuesFiltersInput
$companyId: ID!
- $sortOptions: [IssueSortOptionInput!]
) {
issues(
pagination: $pagination
filters: $filters
companyId: $companyId
- sortOptions: $sortOptions
) {
collection {
id
diff --git a/src/Conviso.Platform.VisualStudio/Services/Platform/PlatformApiClient.cs b/src/Conviso.Platform.VisualStudio/Services/Platform/PlatformApiClient.cs
index 90294fc..6a99fd4 100644
--- a/src/Conviso.Platform.VisualStudio/Services/Platform/PlatformApiClient.cs
+++ b/src/Conviso.Platform.VisualStudio/Services/Platform/PlatformApiClient.cs
@@ -21,9 +21,8 @@ public PlatformApiClient(ISettingsService settingsService)
public async Task QueryAsync(string graphqlDocument, string variablesJson, CancellationToken cancellationToken)
{
- string apiBaseUrl = settingsService.GetString(ConvisoOptions.ApiBaseUrlKey, ConvisoOptions.DefaultApiBaseUrl);
string apiToken = settingsService.GetSecret(ConvisoOptions.ApiTokenKey, string.Empty);
- string endpoint = $"{apiBaseUrl.TrimEnd('/')}/graphql";
+ string endpoint = ConvisoOptions.DefaultApiBaseUrl + "/graphql";
using var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
request.Headers.Add("Accept", "application/json");
diff --git a/src/Conviso.Platform.VisualStudio/Services/Platform/PlatformFacade.cs b/src/Conviso.Platform.VisualStudio/Services/Platform/PlatformFacade.cs
index 7281421..54b6b92 100644
--- a/src/Conviso.Platform.VisualStudio/Services/Platform/PlatformFacade.cs
+++ b/src/Conviso.Platform.VisualStudio/Services/Platform/PlatformFacade.cs
@@ -23,7 +23,7 @@ public async Task> GetAccessibleCompanies
{
string json = await apiClient.QueryAsync(
GraphQlDocuments.SetupCompanies,
- SerializeVariables(new { page = 1, limit = 100 }),
+ SerializeVariables(new { page = 1, limit = 200 }),
cancellationToken);
using JsonDocument document = JsonDocument.Parse(json);
@@ -351,18 +351,10 @@ public async Task> GetVulnerabilitiesAsync(s
pagination = new
{
page = 1,
- limit = 20,
+ perPage = 20,
},
filters,
companyId = effectiveCompanyId,
- sortOptions = new[]
- {
- new
- {
- field = "updatedAt",
- direction = "DESC",
- },
- },
});
string json = await apiClient.QueryAsync(GraphQlDocuments.IssuesList, variables, cancellationToken);
diff --git a/src/Conviso.Platform.VisualStudio/ToolWindows/ChatToolWindow.cs b/src/Conviso.Platform.VisualStudio/ToolWindows/ChatToolWindow.cs
index 538c2ba..61d8c13 100644
--- a/src/Conviso.Platform.VisualStudio/ToolWindows/ChatToolWindow.cs
+++ b/src/Conviso.Platform.VisualStudio/ToolWindows/ChatToolWindow.cs
@@ -6,7 +6,7 @@ internal sealed class ChatToolWindow : ToolWindowPane
{
public ChatToolWindow() : base(null)
{
- Caption = "Conviso Platform";
+ Caption = "AI Autonomous AppSec";
Content = new ChatToolWindowControl(ConvisoPlatformPackage.Instance!.ToolWindowContext);
}
}
diff --git a/src/Conviso.Platform.VisualStudio/ToolWindows/ChatToolWindowControl.xaml b/src/Conviso.Platform.VisualStudio/ToolWindows/ChatToolWindowControl.xaml
index d8a26a5..42d877c 100644
--- a/src/Conviso.Platform.VisualStudio/ToolWindows/ChatToolWindowControl.xaml
+++ b/src/Conviso.Platform.VisualStudio/ToolWindows/ChatToolWindowControl.xaml
@@ -1,76 +1,172 @@
-
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:vs="clr-namespace:Microsoft.VisualStudio.Shell;assembly=Microsoft.VisualStudio.Shell.15.0"
+ Background="{DynamicResource {x:Static vs:VsBrushes.ToolWindowBackgroundKey}}"
+ Foreground="{DynamicResource {x:Static vs:VsBrushes.ToolWindowTextKey}}">
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+
-
-
-
+
+
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Margin="0,0,0,12">
+
+
+
+
+
+
+
+ Command="{Binding SendCommand}">
+
+
+
diff --git a/src/Conviso.Platform.VisualStudio/ToolWindows/ChatToolWindowControl.xaml.cs b/src/Conviso.Platform.VisualStudio/ToolWindows/ChatToolWindowControl.xaml.cs
index c69dd1d..5bd7b2c 100644
--- a/src/Conviso.Platform.VisualStudio/ToolWindows/ChatToolWindowControl.xaml.cs
+++ b/src/Conviso.Platform.VisualStudio/ToolWindows/ChatToolWindowControl.xaml.cs
@@ -1,13 +1,22 @@
using System.Threading.Tasks;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Windows;
using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Threading;
using Conviso.Platform.VisualStudio.Infrastructure;
+using Conviso.Platform.VisualStudio.Models;
using Conviso.Platform.VisualStudio.ViewModels;
namespace Conviso.Platform.VisualStudio.ToolWindows
{
public partial class ChatToolWindowControl : UserControl
{
+ private const double MinimumContentHeight = 480;
+ private const double VerticalContentMargin = 24;
private readonly ChatToolWindowViewModel viewModel;
+ private readonly DispatcherTimer transcriptScrollTimer;
public ChatToolWindowControl(ToolWindowContext context)
{
@@ -18,6 +27,89 @@ public ChatToolWindowControl(ToolWindowContext context)
context.EditorContextService,
context.PatchService);
DataContext = viewModel;
+ transcriptScrollTimer = new DispatcherTimer(DispatcherPriority.Background)
+ {
+ Interval = System.TimeSpan.FromMilliseconds(75),
+ };
+ transcriptScrollTimer.Tick += OnTranscriptScrollTimerTick;
+ viewModel.Transcript.CollectionChanged += OnTranscriptChanged;
+ }
+
+ private void OnTranscriptChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (e.OldItems != null)
+ {
+ foreach (ChatTranscriptItem item in e.OldItems)
+ {
+ item.PropertyChanged -= OnTranscriptItemPropertyChanged;
+ }
+ }
+
+ if (e.NewItems != null)
+ {
+ foreach (ChatTranscriptItem item in e.NewItems)
+ {
+ item.PropertyChanged += OnTranscriptItemPropertyChanged;
+ }
+ }
+
+ ScheduleTranscriptScroll();
+
+ viewModel.ClearChatCommand.RaiseCanExecuteChanged();
+ }
+
+ private void ScrollTranscriptToEnd()
+ {
+ FindVisualChild(TranscriptList)?.ScrollToEnd();
+ }
+
+ private void OnTranscriptItemPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(ChatTranscriptItem.Content))
+ {
+ ScheduleTranscriptScroll();
+ }
+ }
+
+ private void ScheduleTranscriptScroll()
+ {
+ transcriptScrollTimer.Stop();
+ transcriptScrollTimer.Start();
+ }
+
+ private void OnTranscriptScrollTimerTick(object sender, System.EventArgs e)
+ {
+ transcriptScrollTimer.Stop();
+ ScrollTranscriptToEnd();
+ }
+
+ private void OnWindowScrollViewerSizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ // Above the minimum, the star-sized transcript follows the viewport.
+ // Below it, the content remains fixed and the outer viewer starts scrolling.
+ double availableContentHeight = System.Math.Max(0, e.NewSize.Height - VerticalContentMargin);
+ LayoutRoot.Height = System.Math.Max(MinimumContentHeight, availableContentHeight);
+ }
+
+ private static T? FindVisualChild(DependencyObject parent) where T : DependencyObject
+ {
+ int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
+ for (int index = 0; index < childrenCount; index++)
+ {
+ DependencyObject child = VisualTreeHelper.GetChild(parent, index);
+ if (child is T match)
+ {
+ return match;
+ }
+
+ T? nestedMatch = FindVisualChild(child);
+ if (nestedMatch != null)
+ {
+ return nestedMatch;
+ }
+ }
+
+ return null;
}
public Task RunAnalyzeSecurityAndSuggestFixAsync()
diff --git a/src/Conviso.Platform.VisualStudio/ToolWindows/PipelineBreaksToolWindowControl.xaml b/src/Conviso.Platform.VisualStudio/ToolWindows/PipelineBreaksToolWindowControl.xaml
index c6ca3d4..f72c801 100644
--- a/src/Conviso.Platform.VisualStudio/ToolWindows/PipelineBreaksToolWindowControl.xaml
+++ b/src/Conviso.Platform.VisualStudio/ToolWindows/PipelineBreaksToolWindowControl.xaml
@@ -1,40 +1,49 @@
-
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:vs="clr-namespace:Microsoft.VisualStudio.Shell;assembly=Microsoft.VisualStudio.Shell.15.0"
+ Background="{DynamicResource {x:Static vs:VsBrushes.ToolWindowBackgroundKey}}"
+ Foreground="{DynamicResource {x:Static vs:VsBrushes.ToolWindowTextKey}}">
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
+
-
+
+
@@ -46,6 +55,7 @@
-
+
+
diff --git a/src/Conviso.Platform.VisualStudio/ToolWindows/PipelineBreaksToolWindowControl.xaml.cs b/src/Conviso.Platform.VisualStudio/ToolWindows/PipelineBreaksToolWindowControl.xaml.cs
index 2108820..fac8ef9 100644
--- a/src/Conviso.Platform.VisualStudio/ToolWindows/PipelineBreaksToolWindowControl.xaml.cs
+++ b/src/Conviso.Platform.VisualStudio/ToolWindows/PipelineBreaksToolWindowControl.xaml.cs
@@ -6,6 +6,8 @@ namespace Conviso.Platform.VisualStudio.ToolWindows
{
public partial class PipelineBreaksToolWindowControl : UserControl
{
+ private const double MinimumContentHeight = 520;
+ private const double VerticalContentMargin = 24;
private readonly PipelineBreaksToolWindowViewModel viewModel;
public PipelineBreaksToolWindowControl(ToolWindowContext context)
@@ -13,7 +15,20 @@ public PipelineBreaksToolWindowControl(ToolWindowContext context)
InitializeComponent();
viewModel = new PipelineBreaksToolWindowViewModel(context.PlatformFacade);
DataContext = viewModel;
- Loaded += async (_, __) => await viewModel.RefreshAsync();
+ Loaded += OnLoaded;
+ }
+
+ private async void OnLoaded(object sender, System.Windows.RoutedEventArgs e)
+ {
+ Loaded -= OnLoaded;
+ try { await viewModel.RefreshAsync(); }
+ catch (System.Exception error) { DiagnosticsLogger.LogError("Unable to load pipeline breaks: " + error); }
+ }
+
+ private void OnWindowScrollViewerSizeChanged(object sender, System.Windows.SizeChangedEventArgs e)
+ {
+ double availableContentHeight = System.Math.Max(0, e.NewSize.Height - VerticalContentMargin);
+ LayoutRoot.Height = System.Math.Max(MinimumContentHeight, availableContentHeight);
}
}
}
diff --git a/src/Conviso.Platform.VisualStudio/ToolWindows/RequirementsToolWindowControl.xaml b/src/Conviso.Platform.VisualStudio/ToolWindows/RequirementsToolWindowControl.xaml
index 56d52a9..c63bc7c 100644
--- a/src/Conviso.Platform.VisualStudio/ToolWindows/RequirementsToolWindowControl.xaml
+++ b/src/Conviso.Platform.VisualStudio/ToolWindows/RequirementsToolWindowControl.xaml
@@ -1,21 +1,20 @@
-
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:vs="clr-namespace:Microsoft.VisualStudio.Shell;assembly=Microsoft.VisualStudio.Shell.15.0"
+ Background="{DynamicResource {x:Static vs:VsBrushes.ToolWindowBackgroundKey}}"
+ Foreground="{DynamicResource {x:Static vs:VsBrushes.ToolWindowTextKey}}">
+
+
-
-
-
-
-
-
+
-
+
@@ -25,17 +24,28 @@
-
+
+
+
+
+
+
+
+
-
+
-
+
+
@@ -46,36 +56,58 @@
-
+
+
+
+
+
+
+
+
-
+
-
+
+
-
+
+
+
+
+
+
+
+
-
+
-
+
+
-
+
@@ -90,12 +122,14 @@
-
+
-
+
@@ -106,7 +140,7 @@
-
+
@@ -117,7 +151,9 @@
-
+
diff --git a/src/Conviso.Platform.VisualStudio/ToolWindows/RequirementsToolWindowControl.xaml.cs b/src/Conviso.Platform.VisualStudio/ToolWindows/RequirementsToolWindowControl.xaml.cs
index 0accca1..6498705 100644
--- a/src/Conviso.Platform.VisualStudio/ToolWindows/RequirementsToolWindowControl.xaml.cs
+++ b/src/Conviso.Platform.VisualStudio/ToolWindows/RequirementsToolWindowControl.xaml.cs
@@ -13,7 +13,14 @@ public RequirementsToolWindowControl(ToolWindowContext context)
InitializeComponent();
viewModel = new RequirementsToolWindowViewModel(context.PlatformFacade);
DataContext = viewModel;
- Loaded += async (_, __) => await viewModel.RefreshAsync();
+ Loaded += OnLoaded;
+ }
+
+ private async void OnLoaded(object sender, System.Windows.RoutedEventArgs e)
+ {
+ Loaded -= OnLoaded;
+ try { await viewModel.RefreshAsync(); }
+ catch (System.Exception error) { DiagnosticsLogger.LogError("Unable to load requirements: " + error); }
}
}
}
diff --git a/src/Conviso.Platform.VisualStudio/ToolWindows/SettingsToolWindowControl.xaml b/src/Conviso.Platform.VisualStudio/ToolWindows/SettingsToolWindowControl.xaml
index 6ded019..d9daa43 100644
--- a/src/Conviso.Platform.VisualStudio/ToolWindows/SettingsToolWindowControl.xaml
+++ b/src/Conviso.Platform.VisualStudio/ToolWindows/SettingsToolWindowControl.xaml
@@ -1,64 +1,25 @@
-
-
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:vs="clr-namespace:Microsoft.VisualStudio.Shell;assembly=Microsoft.VisualStudio.Shell.15.0"
+ Background="{DynamicResource {x:Static vs:VsBrushes.ToolWindowBackgroundKey}}"
+ Foreground="{DynamicResource {x:Static vs:VsBrushes.ToolWindowTextKey}}">
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Conviso.Platform.VisualStudio/ToolWindows/SettingsToolWindowControl.xaml.cs b/src/Conviso.Platform.VisualStudio/ToolWindows/SettingsToolWindowControl.xaml.cs
index 0b0c0e2..dc0455d 100644
--- a/src/Conviso.Platform.VisualStudio/ToolWindows/SettingsToolWindowControl.xaml.cs
+++ b/src/Conviso.Platform.VisualStudio/ToolWindows/SettingsToolWindowControl.xaml.cs
@@ -6,10 +6,31 @@ namespace Conviso.Platform.VisualStudio.ToolWindows
{
public partial class SettingsToolWindowControl : UserControl
{
+ private readonly SettingsToolWindowViewModel viewModel;
+
public SettingsToolWindowControl(ToolWindowContext context)
{
InitializeComponent();
- DataContext = new SettingsToolWindowViewModel(context.SettingsService, context.PlatformFacade);
+ viewModel = new SettingsToolWindowViewModel(context.SettingsService, context.PlatformFacade);
+ DataContext = viewModel;
+ ApiTokenPasswordBox.Password = viewModel.ApiToken;
+ SettingsTabs.SelectionChanged += OnSettingsTabSelectionChanged;
+ }
+
+ private void OnApiTokenPasswordChanged(object sender, System.Windows.RoutedEventArgs e)
+ {
+ if (DataContext is SettingsToolWindowViewModel currentViewModel)
+ {
+ currentViewModel.ApiToken = ApiTokenPasswordBox.Password;
+ }
+ }
+
+ private async void OnSettingsTabSelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (e.Source == SettingsTabs && ScopeTab.IsSelected)
+ {
+ await viewModel.LoadCompaniesAsync();
+ }
}
}
}
diff --git a/src/Conviso.Platform.VisualStudio/ToolWindows/ToolWindowStyles.xaml b/src/Conviso.Platform.VisualStudio/ToolWindows/ToolWindowStyles.xaml
new file mode 100644
index 0000000..b4fc3c1
--- /dev/null
+++ b/src/Conviso.Platform.VisualStudio/ToolWindows/ToolWindowStyles.xaml
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Conviso.Platform.VisualStudio/ToolWindows/VulnerabilitiesToolWindowControl.xaml b/src/Conviso.Platform.VisualStudio/ToolWindows/VulnerabilitiesToolWindowControl.xaml
index 512935d..37e2792 100644
--- a/src/Conviso.Platform.VisualStudio/ToolWindows/VulnerabilitiesToolWindowControl.xaml
+++ b/src/Conviso.Platform.VisualStudio/ToolWindows/VulnerabilitiesToolWindowControl.xaml
@@ -1,54 +1,76 @@
-
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:vs="clr-namespace:Microsoft.VisualStudio.Shell;assembly=Microsoft.VisualStudio.Shell.15.0"
+ Background="{DynamicResource {x:Static vs:VsBrushes.ToolWindowBackgroundKey}}"
+ Foreground="{DynamicResource {x:Static vs:VsBrushes.ToolWindowTextKey}}">
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
-
+
-
-
+
+
-
+
+
+
diff --git a/src/Conviso.Platform.VisualStudio/ToolWindows/VulnerabilitiesToolWindowControl.xaml.cs b/src/Conviso.Platform.VisualStudio/ToolWindows/VulnerabilitiesToolWindowControl.xaml.cs
index 6ee0d3a..b5f8ced 100644
--- a/src/Conviso.Platform.VisualStudio/ToolWindows/VulnerabilitiesToolWindowControl.xaml.cs
+++ b/src/Conviso.Platform.VisualStudio/ToolWindows/VulnerabilitiesToolWindowControl.xaml.cs
@@ -6,6 +6,8 @@ namespace Conviso.Platform.VisualStudio.ToolWindows
{
public partial class VulnerabilitiesToolWindowControl : UserControl
{
+ private const double MinimumContentHeight = 600;
+ private const double VerticalContentMargin = 24;
private readonly VulnerabilitiesToolWindowViewModel viewModel;
public VulnerabilitiesToolWindowControl(ToolWindowContext context)
@@ -13,7 +15,20 @@ public VulnerabilitiesToolWindowControl(ToolWindowContext context)
InitializeComponent();
viewModel = new VulnerabilitiesToolWindowViewModel(context.PlatformFacade, context.SettingsService, context.BrokerClient);
DataContext = viewModel;
- Loaded += async (_, __) => await viewModel.RefreshAsync();
+ Loaded += OnLoaded;
+ }
+
+ private async void OnLoaded(object sender, System.Windows.RoutedEventArgs e)
+ {
+ Loaded -= OnLoaded;
+ try { await viewModel.InitializeAsync(); }
+ catch (System.Exception error) { DiagnosticsLogger.LogError("Unable to load vulnerabilities: " + error); }
+ }
+
+ private void OnWindowScrollViewerSizeChanged(object sender, System.Windows.SizeChangedEventArgs e)
+ {
+ double availableContentHeight = System.Math.Max(0, e.NewSize.Height - VerticalContentMargin);
+ LayoutRoot.Height = System.Math.Max(MinimumContentHeight, availableContentHeight);
}
}
}
diff --git a/src/Conviso.Platform.VisualStudio/ViewModels/ChatToolWindowViewModel.cs b/src/Conviso.Platform.VisualStudio/ViewModels/ChatToolWindowViewModel.cs
index 3b32fba..d6f7db7 100644
--- a/src/Conviso.Platform.VisualStudio/ViewModels/ChatToolWindowViewModel.cs
+++ b/src/Conviso.Platform.VisualStudio/ViewModels/ChatToolWindowViewModel.cs
@@ -21,12 +21,14 @@ internal sealed class ChatToolWindowViewModel : ObservableObject
private readonly IBrokerClient brokerClient;
private readonly IEditorContextService editorContextService;
private readonly IPatchService patchService;
+ private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1, 1);
private readonly Dictionary requestExtractorIdMap = new Dictionary();
private EditorContextSnapshot? attachedContext;
private string? latestCompletedRequestId;
private string status = "Ready";
private string message = string.Empty;
private string attachedContextSummary = "No attached selection.";
+ private string thinkingStatus = string.Empty;
public ChatToolWindowViewModel(
ISettingsService settingsService,
@@ -39,15 +41,14 @@ public ChatToolWindowViewModel(
this.editorContextService = editorContextService;
this.patchService = patchService;
Transcript = new ObservableCollection();
- ConnectCommand = new AsyncDelegateCommand(ConnectAsync);
SendCommand = new AsyncDelegateCommand(SendAsync, () => !string.IsNullOrWhiteSpace(Message));
- DisconnectCommand = new AsyncDelegateCommand(DisconnectAsync);
AttachSelectionCommand = new AsyncDelegateCommand(AttachSelectionAsync);
AnalyzeSelectionCommand = new AsyncDelegateCommand(AnalyzeSecurityAndSuggestFixAsync);
CheckSimilarIssuesCommand = new AsyncDelegateCommand(CheckSimilarIssuesAsync);
ApplySuggestedFixCommand = new AsyncDelegateCommand(ApplySuggestedFixAsync, CanApplySuggestedFix);
MarkResponseHelpfulCommand = new AsyncDelegateCommand(MarkResponseHelpfulAsync, CanMarkResponseHelpful);
ClearAttachedContextCommand = new AsyncDelegateCommand(ClearAttachedContextAsync, () => attachedContext != null);
+ ClearChatCommand = new AsyncDelegateCommand(ClearChatAsync, () => Transcript.Count > 0);
brokerClient.EventReceived += OnBrokerEventReceived;
}
@@ -69,12 +70,14 @@ public string Message
}
}
- public AsyncDelegateCommand ConnectCommand { get; }
+ public string ThinkingStatus
+ {
+ get => thinkingStatus;
+ set => SetProperty(ref thinkingStatus, value);
+ }
public AsyncDelegateCommand SendCommand { get; }
- public AsyncDelegateCommand DisconnectCommand { get; }
-
public AsyncDelegateCommand AttachSelectionCommand { get; }
public AsyncDelegateCommand AnalyzeSelectionCommand { get; }
@@ -87,6 +90,8 @@ public string Message
public AsyncDelegateCommand ClearAttachedContextCommand { get; }
+ public AsyncDelegateCommand ClearChatCommand { get; }
+
public ObservableCollection Transcript { get; }
public string AttachedContextSummary
@@ -95,19 +100,6 @@ public string AttachedContextSummary
set => SetProperty(ref attachedContextSummary, value);
}
- private async Task ConnectAsync()
- {
- try
- {
- await EnsureConnectedAsync();
- }
- catch (System.Exception error)
- {
- Status = "Connection failed";
- Transcript.Add(new ChatTranscriptItem("system", "Connection failed: " + error.Message));
- }
- }
-
private async Task SendAsync()
{
string currentMessage = Message.Trim();
@@ -118,6 +110,7 @@ private async Task SendAsync()
try
{
+ ThinkingStatus = "Thinking...";
await EnsureConnectedAsync();
Transcript.Add(new ChatTranscriptItem("user", currentMessage));
var context = attachedContext ?? await editorContextService.GetActiveContextAsync(CancellationToken.None);
@@ -129,6 +122,7 @@ private async Task SendAsync()
}
catch (System.Exception error)
{
+ ThinkingStatus = string.Empty;
Status = "Send failed";
Transcript.Add(new ChatTranscriptItem("system", "Failed to send: " + error.Message));
}
@@ -174,6 +168,7 @@ public async Task AnalyzeSecurityAndSuggestFixAsync()
string userMessage = "Analyze the selected code and suggest a fix.";
string request = context.SelectionText;
+ ThinkingStatus = "Thinking...";
await EnsureConnectedAsync();
Transcript.Add(new ChatTranscriptItem("user", userMessage));
await brokerClient.SendChatMessageAsync(new ChatMessage("user", request, context.Language), CancellationToken.None);
@@ -181,6 +176,7 @@ public async Task AnalyzeSecurityAndSuggestFixAsync()
}
catch (System.Exception error)
{
+ ThinkingStatus = string.Empty;
Status = "Analyze failed";
Transcript.Add(new ChatTranscriptItem("system", "Failed to analyze selection: " + error.Message));
}
@@ -210,6 +206,7 @@ public async Task CheckSimilarIssuesAsync()
string request = BuildSimilarIssuesMessage(context, workspaceContext!);
string userMessage = "Check whether the selected issue appears elsewhere in the workspace (" + workspaceFilesCount + " files scanned).";
+ ThinkingStatus = "Thinking...";
await EnsureConnectedAsync();
Transcript.Add(new ChatTranscriptItem("user", userMessage));
await brokerClient.SendChatMessageAsync(new ChatMessage("user", request, context.Language), CancellationToken.None);
@@ -217,6 +214,7 @@ public async Task CheckSimilarIssuesAsync()
}
catch (System.Exception error)
{
+ ThinkingStatus = string.Empty;
Status = "Similarity scan failed";
Transcript.Add(new ChatTranscriptItem("system", "Failed to check similar issues: " + error.Message));
}
@@ -294,6 +292,7 @@ public async Task MarkResponseHelpfulAsync()
return;
}
+ await EnsureConnectedAsync();
await brokerClient.UpdateExtractorAcceptedAsync(extractorId, CancellationToken.None);
Status = "Response marked as helpful";
Transcript.Add(new ChatTranscriptItem("system", "Assistant response marked as helpful."));
@@ -316,23 +315,16 @@ private Task ClearAttachedContextAsync()
return Task.CompletedTask;
}
- private async Task DisconnectAsync()
+ private Task ClearChatAsync()
{
- try
- {
- await brokerClient.DisconnectAsync(CancellationToken.None);
- Status = "Disconnected";
- Transcript.Add(new ChatTranscriptItem("system", "Disconnected from broker."));
- ApplySuggestedFixCommand.RaiseCanExecuteChanged();
- MarkResponseHelpfulCommand.RaiseCanExecuteChanged();
- }
- catch (System.Exception error)
- {
- Status = "Disconnect failed";
- Transcript.Add(new ChatTranscriptItem("system", "Disconnect failed: " + error.Message));
- ApplySuggestedFixCommand.RaiseCanExecuteChanged();
- MarkResponseHelpfulCommand.RaiseCanExecuteChanged();
- }
+ Transcript.Clear();
+ requestExtractorIdMap.Clear();
+ latestCompletedRequestId = null;
+ Status = "Chat cleared";
+ ClearChatCommand.RaiseCanExecuteChanged();
+ ApplySuggestedFixCommand.RaiseCanExecuteChanged();
+ MarkResponseHelpfulCommand.RaiseCanExecuteChanged();
+ return Task.CompletedTask;
}
private void OnBrokerEventReceived(BrokerEvent brokerEvent)
@@ -341,11 +333,11 @@ void UpdateUi()
{
if (brokerEvent.Type == "analysis_chunk")
{
+ ThinkingStatus = string.Empty;
var last = Transcript.LastOrDefault();
if (last != null && last.Role == "assistant")
{
- Transcript.Remove(last);
- Transcript.Add(new ChatTranscriptItem("assistant", last.Content + brokerEvent.Content));
+ last.Content += brokerEvent.Content;
}
else
{
@@ -360,6 +352,7 @@ void UpdateUi()
if (brokerEvent.Type == "analysis_complete")
{
+ ThinkingStatus = string.Empty;
latestCompletedRequestId = brokerEvent.RequestId;
TrackExtractorId(brokerEvent);
if (!string.IsNullOrWhiteSpace(brokerEvent.Content))
@@ -379,6 +372,7 @@ void UpdateUi()
if (brokerEvent.Type == "analysis_error" || brokerEvent.Type == "error")
{
+ ThinkingStatus = string.Empty;
Transcript.Add(new ChatTranscriptItem("system", brokerEvent.Content));
Status = "Error from broker";
ApplySuggestedFixCommand.RaiseCanExecuteChanged();
@@ -386,6 +380,7 @@ void UpdateUi()
return;
}
+ ThinkingStatus = string.Empty;
Transcript.Add(new ChatTranscriptItem("assistant", brokerEvent.Content));
Status = "Message received";
ApplySuggestedFixCommand.RaiseCanExecuteChanged();
@@ -398,10 +393,17 @@ void UpdateUi()
return;
}
- ThreadHelper.JoinableTaskFactory.RunAsync(async delegate
+ _ = ThreadHelper.JoinableTaskFactory.RunAsync(async delegate
{
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
- UpdateUi();
+ try
+ {
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ UpdateUi();
+ }
+ catch (System.Exception error)
+ {
+ DiagnosticsLogger.LogError("Unable to update chat UI from broker event: " + error);
+ }
});
}
@@ -468,6 +470,7 @@ private async Task TryAcceptLatestResponseAsync(string successMessage)
return;
}
+ await EnsureConnectedAsync();
await brokerClient.UpdateExtractorAcceptedAsync(extractorId, CancellationToken.None);
Transcript.Add(new ChatTranscriptItem("system", successMessage));
}
@@ -528,16 +531,26 @@ private async Task EnsureConnectedAsync()
return;
}
- Status = "Connecting...";
- await brokerClient.ConnectAsync(
- new BrokerConnectionOptions
+ await connectionLock.WaitAsync();
+ try
+ {
+ if (brokerClient.IsConnected)
{
- Endpoint = settingsService.GetString(ConvisoOptions.BrokerEndpointKey, ConvisoOptions.DefaultBrokerEndpoint),
- ApiKey = settingsService.GetSecret(ConvisoOptions.BrokerApiKeyKey, string.Empty),
- },
- CancellationToken.None);
- Status = "Connected";
- Transcript.Add(new ChatTranscriptItem("system", "Connected to broker."));
+ return;
+ }
+
+ await brokerClient.ConnectAsync(
+ new BrokerConnectionOptions
+ {
+ Endpoint = ConvisoOptions.DefaultBrokerEndpoint,
+ ApiKey = settingsService.GetSecret(ConvisoOptions.ApiTokenKey, string.Empty),
+ },
+ CancellationToken.None);
+ }
+ finally
+ {
+ connectionLock.Release();
+ }
}
private static string BuildBrokerMessage(string message, EditorContextSnapshot? context)
diff --git a/src/Conviso.Platform.VisualStudio/ViewModels/PipelineBreaksToolWindowViewModel.cs b/src/Conviso.Platform.VisualStudio/ViewModels/PipelineBreaksToolWindowViewModel.cs
index deb22d2..722a59b 100644
--- a/src/Conviso.Platform.VisualStudio/ViewModels/PipelineBreaksToolWindowViewModel.cs
+++ b/src/Conviso.Platform.VisualStudio/ViewModels/PipelineBreaksToolWindowViewModel.cs
@@ -1,6 +1,8 @@
using System.Collections.ObjectModel;
+using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
+using System.Windows.Data;
using Conviso.Platform.VisualStudio.Infrastructure;
using Conviso.Platform.VisualStudio.Models;
using Conviso.Platform.VisualStudio.Services.Platform;
@@ -19,22 +21,43 @@ internal sealed class PipelineBreaksToolWindowViewModel : ObservableObject
private string detailAssetName = string.Empty;
private string detailReasonText = string.Empty;
private string detailCommandStatus = "Select a pipeline break";
+ private string itemFilter = string.Empty;
public PipelineBreaksToolWindowViewModel(IPlatformFacade platformFacade)
{
this.platformFacade = platformFacade;
Items = new ObservableCollection();
+ ItemsView = CollectionViewSource.GetDefaultView(Items);
+ ItemsView.Filter = FilterItem;
RefreshCommand = new AsyncDelegateCommand(RefreshAsync);
}
public ObservableCollection Items { get; }
+ public ICollectionView ItemsView { get; }
+
public string Status
{
get => status;
set => SetProperty(ref status, value);
}
+ public string ItemFilter
+ {
+ get => itemFilter;
+ set
+ {
+ if (SetProperty(ref itemFilter, value))
+ {
+ ItemsView.Refresh();
+ if (SelectedItem != null && !ItemsView.Contains(SelectedItem))
+ {
+ SelectedItem = null;
+ }
+ }
+ }
+ }
+
public PipelineBreakSummary? SelectedItem
{
get => selectedItem;
@@ -69,12 +92,19 @@ public async Task RefreshAsync()
Items.Clear();
SelectedItem = null;
ResetDetails();
- foreach (PipelineBreakSummary item in await platformFacade.GetPipelineBreaksAsync(CancellationToken.None))
+ try
{
- Items.Add(item);
+ foreach (PipelineBreakSummary item in await platformFacade.GetPipelineBreaksAsync(CancellationToken.None))
+ {
+ Items.Add(item);
+ }
+ Status = $"Loaded {Items.Count} item(s)";
+ }
+ catch (System.Exception error)
+ {
+ Status = "Unable to load pipeline breaks: " + error.Message;
+ DiagnosticsLogger.LogError("Unable to load pipeline breaks: " + error);
}
-
- Status = $"Loaded {Items.Count} item(s)";
}
private async Task LoadDetailsAsync(PipelineBreakSummary? item)
@@ -86,7 +116,17 @@ private async Task LoadDetailsAsync(PipelineBreakSummary? item)
}
DetailCommandStatus = "Loading pipeline break details...";
- PipelineBreakDetails details = await platformFacade.GetPipelineBreakDetailsAsync(item.Id, CancellationToken.None);
+ PipelineBreakDetails details;
+ try
+ {
+ details = await platformFacade.GetPipelineBreakDetailsAsync(item.Id, CancellationToken.None);
+ }
+ catch (System.Exception error)
+ {
+ DetailCommandStatus = "Unable to load details: " + error.Message;
+ DiagnosticsLogger.LogError("Unable to load pipeline break details: " + error);
+ return;
+ }
DetailStatus = details.Status;
DetailExecutionDate = details.ExecutionDate;
DetailTriggeredBy = details.TriggeredBy;
@@ -106,5 +146,15 @@ private void ResetDetails()
DetailReasonText = string.Empty;
DetailCommandStatus = "Select a pipeline break";
}
+
+ private bool FilterItem(object item)
+ {
+ if (item is not PipelineBreakSummary pipelineBreak || string.IsNullOrWhiteSpace(ItemFilter))
+ {
+ return true;
+ }
+
+ return (pipelineBreak.AssetName ?? string.Empty).IndexOf(ItemFilter.Trim(), System.StringComparison.OrdinalIgnoreCase) >= 0;
+ }
}
}
diff --git a/src/Conviso.Platform.VisualStudio/ViewModels/RequirementsToolWindowViewModel.cs b/src/Conviso.Platform.VisualStudio/ViewModels/RequirementsToolWindowViewModel.cs
index 70899fd..507db93 100644
--- a/src/Conviso.Platform.VisualStudio/ViewModels/RequirementsToolWindowViewModel.cs
+++ b/src/Conviso.Platform.VisualStudio/ViewModels/RequirementsToolWindowViewModel.cs
@@ -1,6 +1,8 @@
using System.Collections.ObjectModel;
+using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
+using System.Windows.Data;
using Conviso.Platform.VisualStudio.Infrastructure;
using Conviso.Platform.VisualStudio.Models;
using Conviso.Platform.VisualStudio.Services.Platform;
@@ -35,6 +37,9 @@ internal sealed class RequirementsToolWindowViewModel : ObservableObject
private string activityUpdatedAt = string.Empty;
private string newActivityStatus = string.Empty;
private string activityCommandStatus = "Select an activity";
+ private string projectFilter = string.Empty;
+ private string requirementFilter = string.Empty;
+ private string activityFilter = string.Empty;
public RequirementsToolWindowViewModel(IPlatformFacade platformFacade)
{
@@ -42,6 +47,12 @@ public RequirementsToolWindowViewModel(IPlatformFacade platformFacade)
Projects = new ObservableCollection();
Requirements = new ObservableCollection();
Activities = new ObservableCollection();
+ ProjectsView = CollectionViewSource.GetDefaultView(Projects);
+ RequirementsView = CollectionViewSource.GetDefaultView(Requirements);
+ ActivitiesView = CollectionViewSource.GetDefaultView(Activities);
+ ProjectsView.Filter = FilterProject;
+ RequirementsView.Filter = FilterRequirement;
+ ActivitiesView.Filter = FilterActivity;
RefreshCommand = new AsyncDelegateCommand(RefreshAsync);
UpdateProjectStatusCommand = new AsyncDelegateCommand(UpdateProjectStatusAsync, () => selectedProject != null && !string.IsNullOrWhiteSpace(NewProjectStatus));
UpdateActivityStatusCommand = new AsyncDelegateCommand(UpdateActivityStatusAsync, () => selectedActivity != null && !string.IsNullOrWhiteSpace(NewActivityStatus));
@@ -53,6 +64,12 @@ public RequirementsToolWindowViewModel(IPlatformFacade platformFacade)
public ObservableCollection Activities { get; }
+ public ICollectionView ProjectsView { get; }
+
+ public ICollectionView RequirementsView { get; }
+
+ public ICollectionView ActivitiesView { get; }
+
public string Status
{
get => status;
@@ -66,7 +83,7 @@ public ProjectSummary? SelectedProject
{
if (SetProperty(ref selectedProject, value))
{
- _ = LoadProjectAsync(value);
+ _ = LoadProjectSafelyAsync(value);
}
}
}
@@ -78,7 +95,7 @@ public RequirementSummary? SelectedRequirement
{
if (SetProperty(ref selectedRequirement, value))
{
- _ = LoadRequirementAsync(value);
+ _ = LoadRequirementSafelyAsync(value);
}
}
}
@@ -163,6 +180,81 @@ public string NewActivityStatus
public AsyncDelegateCommand UpdateActivityStatusCommand { get; }
+ private async Task LoadProjectSafelyAsync(ProjectSummary? project)
+ {
+ try
+ {
+ await LoadProjectAsync(project);
+ }
+ catch (System.Exception error)
+ {
+ ProjectCommandStatus = "Unable to load project: " + error.Message;
+ Status = ProjectCommandStatus;
+ DiagnosticsLogger.LogError("Unable to load project: " + error);
+ }
+ }
+
+ public string ProjectFilter
+ {
+ get => projectFilter;
+ set
+ {
+ if (SetProperty(ref projectFilter, value))
+ {
+ ProjectsView.Refresh();
+ if (SelectedProject != null && !ProjectsView.Contains(SelectedProject))
+ {
+ SelectedProject = null;
+ }
+ }
+ }
+ }
+
+ public string RequirementFilter
+ {
+ get => requirementFilter;
+ set
+ {
+ if (SetProperty(ref requirementFilter, value))
+ {
+ RequirementsView.Refresh();
+ if (SelectedRequirement != null && !RequirementsView.Contains(SelectedRequirement))
+ {
+ SelectedRequirement = null;
+ }
+ }
+ }
+ }
+
+ public string ActivityFilter
+ {
+ get => activityFilter;
+ set
+ {
+ if (SetProperty(ref activityFilter, value))
+ {
+ ActivitiesView.Refresh();
+ if (SelectedActivity != null && !ActivitiesView.Contains(SelectedActivity))
+ {
+ SelectedActivity = null;
+ }
+ }
+ }
+ }
+
+ private async Task LoadRequirementSafelyAsync(RequirementSummary? requirement)
+ {
+ try
+ {
+ await LoadRequirementAsync(requirement);
+ }
+ catch (System.Exception error)
+ {
+ Status = "Unable to load requirement: " + error.Message;
+ DiagnosticsLogger.LogError("Unable to load requirement: " + error);
+ }
+ }
+
public async Task RefreshAsync()
{
Status = "Loading projects...";
@@ -176,12 +268,20 @@ public async Task RefreshAsync()
ResetRequirementDetails();
ResetActivityDetails();
- foreach (ProjectSummary item in await platformFacade.GetProjectsAsync(CancellationToken.None))
+ try
{
- Projects.Add(item);
- }
+ foreach (ProjectSummary item in await platformFacade.GetProjectsAsync(CancellationToken.None))
+ {
+ Projects.Add(item);
+ }
- Status = $"Loaded {Projects.Count} project(s)";
+ Status = $"Loaded {Projects.Count} project(s)";
+ }
+ catch (System.Exception error)
+ {
+ Status = "Unable to load projects: " + error.Message;
+ DiagnosticsLogger.LogError("Unable to load projects: " + error);
+ }
}
private async Task LoadProjectAsync(ProjectSummary? project)
@@ -201,7 +301,17 @@ private async Task LoadProjectAsync(ProjectSummary? project)
}
ProjectCommandStatus = "Loading project details...";
- ProjectDetails details = await platformFacade.GetProjectDetailsAsync(project.Id, CancellationToken.None);
+ ProjectDetails details;
+ try
+ {
+ details = await platformFacade.GetProjectDetailsAsync(project.Id, CancellationToken.None);
+ }
+ catch (System.Exception error)
+ {
+ ProjectCommandStatus = "Unable to load project: " + error.Message;
+ DiagnosticsLogger.LogError("Unable to load project details: " + error);
+ return;
+ }
ProjectLabel = details.Label;
ProjectStatus = details.Status;
ProjectType = details.ProjectTypeLabel;
@@ -328,5 +438,26 @@ private void ResetActivityDetails()
NewActivityStatus = string.Empty;
ActivityCommandStatus = "Select an activity";
}
+
+ private bool FilterProject(object item)
+ {
+ return item is not ProjectSummary project ||
+ string.IsNullOrWhiteSpace(ProjectFilter) ||
+ (project.Label ?? string.Empty).IndexOf(ProjectFilter.Trim(), System.StringComparison.OrdinalIgnoreCase) >= 0;
+ }
+
+ private bool FilterRequirement(object item)
+ {
+ return item is not RequirementSummary requirement ||
+ string.IsNullOrWhiteSpace(RequirementFilter) ||
+ (requirement.Label ?? string.Empty).IndexOf(RequirementFilter.Trim(), System.StringComparison.OrdinalIgnoreCase) >= 0;
+ }
+
+ private bool FilterActivity(object item)
+ {
+ return item is not ProjectActivitySummary activity ||
+ string.IsNullOrWhiteSpace(ActivityFilter) ||
+ (activity.Title ?? string.Empty).IndexOf(ActivityFilter.Trim(), System.StringComparison.OrdinalIgnoreCase) >= 0;
+ }
}
}
diff --git a/src/Conviso.Platform.VisualStudio/ViewModels/SettingsToolWindowViewModel.cs b/src/Conviso.Platform.VisualStudio/ViewModels/SettingsToolWindowViewModel.cs
index 0ada4d2..f533f23 100644
--- a/src/Conviso.Platform.VisualStudio/ViewModels/SettingsToolWindowViewModel.cs
+++ b/src/Conviso.Platform.VisualStudio/ViewModels/SettingsToolWindowViewModel.cs
@@ -19,36 +19,29 @@ internal sealed class SettingsToolWindowViewModel : ObservableObject
private readonly IPlatformFacade platformFacade;
private string apiBaseUrl;
private string apiToken;
- private string companyId;
- private string requirementsScopeId;
private string brokerEndpoint;
private string brokerApiKey;
private AccessibleCompanyOption? selectedCompany;
+ private bool isLoadingCompanies;
+ private bool isInitializingCompanySelection;
private string status = "Ready";
public SettingsToolWindowViewModel(ISettingsService settingsService, IPlatformFacade platformFacade)
{
this.settingsService = settingsService;
this.platformFacade = platformFacade;
- apiBaseUrl = settingsService.GetString(ConvisoOptions.ApiBaseUrlKey, ConvisoOptions.DefaultApiBaseUrl);
+ apiBaseUrl = ConvisoOptions.DefaultApiBaseUrl;
apiToken = settingsService.GetSecret(ConvisoOptions.ApiTokenKey, string.Empty);
- companyId = settingsService.GetString(ConvisoOptions.CompanyIdKey, string.Empty);
- requirementsScopeId = settingsService.GetString(ConvisoOptions.RequirementsScopeIdKey, string.Empty);
- brokerEndpoint = settingsService.GetString(ConvisoOptions.BrokerEndpointKey, ConvisoOptions.DefaultBrokerEndpoint);
- brokerApiKey = settingsService.GetSecret(ConvisoOptions.BrokerApiKeyKey, string.Empty);
+ brokerEndpoint = ConvisoOptions.DefaultBrokerEndpoint;
+ brokerApiKey = apiToken;
Companies = new ObservableCollection();
SaveCommand = new AsyncDelegateCommand(SaveAsync);
TestApiCommand = new AsyncDelegateCommand(TestApiAsync);
TestBrokerCommand = new AsyncDelegateCommand(TestBrokerAsync);
- ResetDefaultsCommand = new AsyncDelegateCommand(ResetDefaultsAsync);
- LoadCompaniesCommand = new AsyncDelegateCommand(LoadCompaniesAsync);
- ApplySelectedCompanyCommand = new AsyncDelegateCommand(ApplySelectedCompanyAsync, () => SelectedCompany != null);
}
public string ApiBaseUrl { get => apiBaseUrl; set => SetProperty(ref apiBaseUrl, value); }
public string ApiToken { get => apiToken; set => SetProperty(ref apiToken, value); }
- public string CompanyId { get => companyId; set => SetProperty(ref companyId, value); }
- public string RequirementsScopeId { get => requirementsScopeId; set => SetProperty(ref requirementsScopeId, value); }
public string BrokerEndpoint { get => brokerEndpoint; set => SetProperty(ref brokerEndpoint, value); }
public string BrokerApiKey { get => brokerApiKey; set => SetProperty(ref brokerApiKey, value); }
public string Status { get => status; set => SetProperty(ref status, value); }
@@ -62,7 +55,13 @@ public AccessibleCompanyOption? SelectedCompany
{
if (SetProperty(ref selectedCompany, value))
{
- ApplySelectedCompanyCommand.RaiseCanExecuteChanged();
+ if (!isInitializingCompanySelection &&
+ value != null &&
+ !string.IsNullOrWhiteSpace(value.Id))
+ {
+ settingsService.SetString(ConvisoOptions.CompanyIdKey, value.Id);
+ Status = $"Company {value.DisplayLabel} selected and saved.";
+ }
}
}
}
@@ -73,44 +72,25 @@ public AccessibleCompanyOption? SelectedCompany
public AsyncDelegateCommand TestBrokerCommand { get; }
- public AsyncDelegateCommand ResetDefaultsCommand { get; }
-
- public AsyncDelegateCommand LoadCompaniesCommand { get; }
-
- public AsyncDelegateCommand ApplySelectedCompanyCommand { get; }
-
private Task SaveAsync()
{
- string normalizedApiBaseUrl = NormalizeApiBaseUrl(ApiBaseUrl);
- string normalizedBrokerEndpoint = NormalizeBrokerEndpoint(BrokerEndpoint);
- string normalizedCompanyId = CompanyId.Trim();
- string normalizedRequirementsScopeId = string.IsNullOrWhiteSpace(RequirementsScopeId)
- ? normalizedCompanyId
- : RequirementsScopeId.Trim();
+ string normalizedApiBaseUrl = ConvisoOptions.DefaultApiBaseUrl;
+ string normalizedBrokerEndpoint = ConvisoOptions.DefaultBrokerEndpoint;
string normalizedApiToken = ApiToken.Trim();
- string normalizedBrokerApiKey = string.IsNullOrWhiteSpace(BrokerApiKey)
- ? normalizedApiToken
- : BrokerApiKey.Trim();
+ string normalizedBrokerApiKey = normalizedApiToken;
ApiBaseUrl = normalizedApiBaseUrl;
BrokerEndpoint = normalizedBrokerEndpoint;
- CompanyId = normalizedCompanyId;
- RequirementsScopeId = normalizedRequirementsScopeId;
BrokerApiKey = normalizedBrokerApiKey;
- settingsService.SetString(ConvisoOptions.ApiBaseUrlKey, normalizedApiBaseUrl);
settingsService.SetSecret(ConvisoOptions.ApiTokenKey, normalizedApiToken);
- settingsService.SetString(ConvisoOptions.CompanyIdKey, normalizedCompanyId);
- settingsService.SetString(ConvisoOptions.RequirementsScopeIdKey, normalizedRequirementsScopeId);
- settingsService.SetString(ConvisoOptions.BrokerEndpointKey, normalizedBrokerEndpoint);
- settingsService.SetSecret(ConvisoOptions.BrokerApiKeyKey, normalizedBrokerApiKey);
Status = "Settings saved. Secrets are stored with Windows user protection.";
return Task.CompletedTask;
}
private async Task TestApiAsync()
{
- string normalizedApiBaseUrl = NormalizeApiBaseUrl(ApiBaseUrl);
+ string normalizedApiBaseUrl = ConvisoOptions.DefaultApiBaseUrl;
string normalizedApiToken = ApiToken.Trim();
using var httpClient = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Post, normalizedApiBaseUrl.TrimEnd('/') + "/graphql");
@@ -156,8 +136,8 @@ private async Task TestBrokerAsync()
await brokerClient.ConnectAsync(
new BrokerConnectionOptions
{
- Endpoint = NormalizeBrokerEndpoint(BrokerEndpoint),
- ApiKey = BrokerApiKey.Trim(),
+ Endpoint = ConvisoOptions.DefaultBrokerEndpoint,
+ ApiKey = ApiToken.Trim(),
},
CancellationToken.None);
Status = "Broker connection successful.";
@@ -172,56 +152,62 @@ await brokerClient.ConnectAsync(
}
}
- private Task ResetDefaultsAsync()
+ public async Task LoadCompaniesAsync()
{
- ApiBaseUrl = ConvisoOptions.DefaultApiBaseUrl;
- BrokerEndpoint = ConvisoOptions.DefaultBrokerEndpoint;
- if (string.IsNullOrWhiteSpace(RequirementsScopeId))
+ if (isLoadingCompanies)
{
- RequirementsScopeId = CompanyId.Trim();
+ return;
}
- Status = "Defaults restored for API and broker endpoint.";
- return Task.CompletedTask;
- }
+ string savedApiToken = settingsService.GetSecret(ConvisoOptions.ApiTokenKey, string.Empty);
+ if (string.IsNullOrWhiteSpace(savedApiToken))
+ {
+ Companies.Clear();
+ SelectedCompany = null;
+ Status = "Save an API token before selecting a company.";
+ return;
+ }
- private async Task LoadCompaniesAsync()
- {
try
{
- await SaveAsync();
+ isLoadingCompanies = true;
Status = "Loading accessible companies...";
var companies = await platformFacade.GetAccessibleCompaniesAsync(CancellationToken.None);
Companies.Clear();
- foreach (AccessibleCompanyOption company in companies)
+ foreach (AccessibleCompanyOption company in companies.OrderBy(item => item.Label))
{
Companies.Add(company);
}
- string currentCompanyId = CompanyId.Trim();
- SelectedCompany = Companies.FirstOrDefault(item => item.Id == currentCompanyId) ?? Companies.FirstOrDefault();
+ string currentCompanyId = settingsService.GetString(ConvisoOptions.CompanyIdKey, string.Empty);
+ AccessibleCompanyOption? companyToSelect =
+ Companies.FirstOrDefault(item => item.Id == currentCompanyId) ??
+ (string.IsNullOrWhiteSpace(currentCompanyId) ? Companies.FirstOrDefault() : null);
+
+ isInitializingCompanySelection = true;
+ SelectedCompany = companyToSelect;
+ isInitializingCompanySelection = false;
+
+ if (companyToSelect != null && string.IsNullOrWhiteSpace(currentCompanyId))
+ {
+ settingsService.SetString(ConvisoOptions.CompanyIdKey, companyToSelect.Id);
+ }
+
Status = Companies.Count == 0
? "No accessible companies returned for this API key."
- : $"Loaded {Companies.Count} accessible compan{(Companies.Count == 1 ? "y" : "ies")}.";
+ : companyToSelect == null
+ ? "The saved company is no longer accessible. Select another company."
+ : $"Company {companyToSelect.DisplayLabel} selected.";
}
catch (Exception error)
{
Status = "Company discovery failed: " + error.Message;
}
- }
-
- private Task ApplySelectedCompanyAsync()
- {
- if (SelectedCompany == null)
+ finally
{
- Status = "Select a company first.";
- return Task.CompletedTask;
+ isInitializingCompanySelection = false;
+ isLoadingCompanies = false;
}
-
- CompanyId = SelectedCompany.Id;
- RequirementsScopeId = SelectedCompany.Id;
- Status = $"Selected company {SelectedCompany.DisplayLabel}. Save settings to persist it.";
- return Task.CompletedTask;
}
private static string NormalizeApiBaseUrl(string value)
diff --git a/src/Conviso.Platform.VisualStudio/ViewModels/VulnerabilitiesToolWindowViewModel.cs b/src/Conviso.Platform.VisualStudio/ViewModels/VulnerabilitiesToolWindowViewModel.cs
index b4cf2cc..50003ab 100644
--- a/src/Conviso.Platform.VisualStudio/ViewModels/VulnerabilitiesToolWindowViewModel.cs
+++ b/src/Conviso.Platform.VisualStudio/ViewModels/VulnerabilitiesToolWindowViewModel.cs
@@ -1,7 +1,9 @@
using System.Collections.ObjectModel;
+using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using System.Windows.Data;
using Conviso.Platform.VisualStudio.Configuration;
using Conviso.Platform.VisualStudio.Infrastructure;
using Conviso.Platform.VisualStudio.Models;
@@ -15,10 +17,11 @@ internal sealed class VulnerabilitiesToolWindowViewModel : ObservableObject
private readonly IPlatformFacade platformFacade;
private readonly ISettingsService settingsService;
private readonly IBrokerClient brokerClient;
- private AccessibleCompanyOption? selectedCompany;
+ private string companyId = string.Empty;
private AssetOption? selectedAsset;
private VulnerabilitySummary? selectedItem;
private string status = "Ready";
+ private string itemFilter = string.Empty;
public VulnerabilitiesToolWindowViewModel(
IPlatformFacade platformFacade,
@@ -29,16 +32,16 @@ public VulnerabilitiesToolWindowViewModel(
this.settingsService = settingsService;
this.brokerClient = brokerClient;
Items = new ObservableCollection();
- Companies = new ObservableCollection();
+ ItemsView = CollectionViewSource.GetDefaultView(Items);
+ ItemsView.Filter = FilterItem;
Assets = new ObservableCollection();
Details = new VulnerabilityDetailsViewModel(platformFacade, settingsService, brokerClient);
RefreshCommand = new AsyncDelegateCommand(RefreshAsync);
- _ = LoadFilterOptionsAsync();
}
public ObservableCollection Items { get; }
- public ObservableCollection Companies { get; }
+ public ICollectionView ItemsView { get; }
public ObservableCollection Assets { get; }
@@ -57,20 +60,7 @@ public VulnerabilitySummary? SelectedItem
{
if (SetProperty(ref selectedItem, value))
{
- _ = Details.LoadAsync(value);
- }
- }
- }
-
- public AccessibleCompanyOption? SelectedCompany
- {
- get => selectedCompany;
- set
- {
- if (SetProperty(ref selectedCompany, value))
- {
- PersistSelectedCompany(value);
- _ = LoadAssetsForSelectedCompanyAsync();
+ _ = LoadDetailsSafelyAsync(value);
}
}
}
@@ -87,42 +77,70 @@ public async Task RefreshAsync()
{
Status = "Loading vulnerabilities...";
Items.Clear();
- foreach (var item in await platformFacade.GetVulnerabilitiesAsync(
- SelectedCompany?.Id,
- string.IsNullOrWhiteSpace(SelectedAsset?.Id) ? null : SelectedAsset?.Id,
- CancellationToken.None))
+ try
{
- Items.Add(item);
+ string currentCompanyId = settingsService.GetString(ConvisoOptions.CompanyIdKey, string.Empty);
+ if (companyId != currentCompanyId)
+ {
+ companyId = currentCompanyId;
+ await LoadAssetsForSelectedCompanyAsync();
+ }
+
+ foreach (var item in await platformFacade.GetVulnerabilitiesAsync(
+ companyId,
+ string.IsNullOrWhiteSpace(SelectedAsset?.Id) ? null : SelectedAsset?.Id,
+ CancellationToken.None))
+ {
+ Items.Add(item);
+ }
+
+ Status = $"Loaded {Items.Count} item(s)";
+ }
+ catch (System.Exception error)
+ {
+ Status = "Unable to load vulnerabilities: " + error.Message;
+ DiagnosticsLogger.LogError("Unable to load vulnerabilities: " + error);
}
+ }
- Status = $"Loaded {Items.Count} item(s)";
+ public Task InitializeAsync()
+ {
+ return LoadFilterOptionsAsync();
}
- private async Task LoadFilterOptionsAsync()
+ public string ItemFilter
{
- try
+ get => itemFilter;
+ set
{
- var companies = await platformFacade.GetAccessibleCompaniesAsync(CancellationToken.None);
- Companies.Clear();
- foreach (AccessibleCompanyOption company in companies.OrderBy(item => item.Label))
+ if (SetProperty(ref itemFilter, value))
{
- Companies.Add(company);
+ ItemsView.Refresh();
+ if (SelectedItem != null && !ItemsView.Contains(SelectedItem))
+ {
+ SelectedItem = null;
+ }
}
+ }
+ }
- string currentCompanyId = settingsService.GetString(ConvisoOptions.CompanyIdKey, string.Empty);
- SelectedCompany = Companies.FirstOrDefault(item => item.Id == currentCompanyId) ?? Companies.FirstOrDefault();
+ private async Task LoadDetailsSafelyAsync(VulnerabilitySummary? item)
+ {
+ try
+ {
+ await Details.LoadAsync(item);
}
- catch
+ catch (System.Exception error)
{
- string currentCompanyId = settingsService.GetString(ConvisoOptions.CompanyIdKey, string.Empty);
- if (!string.IsNullOrWhiteSpace(currentCompanyId))
- {
- Companies.Clear();
- Companies.Add(new AccessibleCompanyOption { Id = currentCompanyId, Label = currentCompanyId });
- SelectedCompany = Companies[0];
- }
+ Status = "Unable to load vulnerability details: " + error.Message;
+ DiagnosticsLogger.LogError("Unable to load vulnerability details: " + error);
}
+ }
+ private async Task LoadFilterOptionsAsync()
+ {
+ companyId = settingsService.GetString(ConvisoOptions.CompanyIdKey, string.Empty);
+ await LoadAssetsForSelectedCompanyAsync();
await RefreshAsync();
}
@@ -132,14 +150,14 @@ private async Task LoadAssetsForSelectedCompanyAsync()
Assets.Add(new AssetOption { Id = string.Empty, Name = "All assets" });
SelectedAsset = Assets[0];
- if (SelectedCompany == null)
+ if (string.IsNullOrWhiteSpace(companyId))
{
return;
}
try
{
- var assets = await platformFacade.GetAssetsAsync(SelectedCompany.Id, CancellationToken.None);
+ var assets = await platformFacade.GetAssetsAsync(companyId, CancellationToken.None);
foreach (AssetOption asset in assets.OrderBy(item => item.Name))
{
Assets.Add(asset);
@@ -151,15 +169,15 @@ private async Task LoadAssetsForSelectedCompanyAsync()
}
}
- private void PersistSelectedCompany(AccessibleCompanyOption? company)
+ private bool FilterItem(object item)
{
- if (company == null || string.IsNullOrWhiteSpace(company.Id))
+ if (item is not VulnerabilitySummary vulnerability || string.IsNullOrWhiteSpace(ItemFilter))
{
- return;
+ return true;
}
- settingsService.SetString(ConvisoOptions.CompanyIdKey, company.Id);
- settingsService.SetString(ConvisoOptions.RequirementsScopeIdKey, company.Id);
+ return (vulnerability.Title ?? string.Empty).IndexOf(ItemFilter.Trim(), System.StringComparison.OrdinalIgnoreCase) >= 0;
}
+
}
}
diff --git a/src/Conviso.Platform.VisualStudio/ViewModels/VulnerabilityDetailsViewModel.cs b/src/Conviso.Platform.VisualStudio/ViewModels/VulnerabilityDetailsViewModel.cs
index 760ece5..72a6cf3 100644
--- a/src/Conviso.Platform.VisualStudio/ViewModels/VulnerabilityDetailsViewModel.cs
+++ b/src/Conviso.Platform.VisualStudio/ViewModels/VulnerabilityDetailsViewModel.cs
@@ -112,22 +112,38 @@ private async Task GenerateFixAsync()
return;
}
- CommandStatus = "Connecting to broker...";
- await EnsureBrokerConnectedAsync();
+ try
+ {
+ CommandStatus = "Connecting to AI service...";
+ await EnsureBrokerConnectedAsync();
+
+ CommandStatus = "Generating fix guidance...";
+ AutoFixResult result = await brokerClient.RequestAutoFixAsync(selectedSummary.Id, CancellationToken.None);
- CommandStatus = "Submitting autofix request...";
- AutoFixResult result = await brokerClient.RequestAutoFixAsync(selectedSummary.Id, CancellationToken.None);
+ if (!string.IsNullOrWhiteSpace(result.PrUrl))
+ {
+ Process.Start(new ProcessStartInfo(result.PrUrl) { UseShellExecute = true });
+ CommandStatus = "Fix generated. Pull request opened in browser.";
+ return;
+ }
- if (!string.IsNullOrWhiteSpace(result.PrUrl))
+ CommandStatus = string.IsNullOrWhiteSpace(result.Summary)
+ ? "Fix request completed without guidance."
+ : TruncateStatus(result.Summary!, 2000);
+ }
+ catch (System.Exception error)
{
- Process.Start(new ProcessStartInfo(result.PrUrl) { UseShellExecute = true });
- CommandStatus = "Fix generated. Pull request opened in browser.";
- return;
+ CommandStatus = "Generate Fix failed: " + error.Message;
+ DiagnosticsLogger.LogError("Generate Fix failed: " + error);
}
+ }
- CommandStatus = string.IsNullOrWhiteSpace(result.Summary)
- ? "AutoFix request sent. Track execution status in Conviso Platform."
- : result.Summary!;
+ private static string TruncateStatus(string value, int maxLength)
+ {
+ string normalized = value.Trim();
+ return normalized.Length <= maxLength
+ ? normalized
+ : normalized.Substring(0, maxLength) + "\n\n[response truncated]";
}
private async Task EnsureBrokerConnectedAsync()
@@ -140,8 +156,8 @@ private async Task EnsureBrokerConnectedAsync()
await brokerClient.ConnectAsync(
new BrokerConnectionOptions
{
- Endpoint = settingsService.GetString(ConvisoOptions.BrokerEndpointKey, ConvisoOptions.DefaultBrokerEndpoint),
- ApiKey = settingsService.GetSecret(ConvisoOptions.BrokerApiKeyKey, string.Empty),
+ Endpoint = ConvisoOptions.DefaultBrokerEndpoint,
+ ApiKey = settingsService.GetSecret(ConvisoOptions.ApiTokenKey, string.Empty),
},
CancellationToken.None);
}
diff --git a/src/Conviso.Platform.VisualStudio/source.extension.vsixmanifest b/src/Conviso.Platform.VisualStudio/source.extension.vsixmanifest
index c5db6ff..ccee98f 100644
--- a/src/Conviso.Platform.VisualStudio/source.extension.vsixmanifest
+++ b/src/Conviso.Platform.VisualStudio/source.extension.vsixmanifest
@@ -1,9 +1,10 @@
-
+
Conviso Platform
Conviso Platform integration for Visual Studio.
+ Resources\pluginIcon.png