Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4d465da
Initial plan
Copilot Dec 8, 2025
46ebe3f
Fix critical nullable reference warnings in AstVisitorExplainer
Copilot Dec 8, 2025
4d3cda8
Extract magic strings into Constants class for improved maintainability
Copilot Dec 8, 2025
b3695ae
Add repository pattern for improved testability and separation of con…
Copilot Dec 8, 2025
88a9516
Add InMemoryHelpRepository with comprehensive unit tests
Copilot Dec 8, 2025
5ebb7d8
Address code review feedback: fix inconsistent empty list handling
Copilot Dec 8, 2025
e18fe25
Update package references to latest stable versions in project files
JosKWRubicon Dec 10, 2025
adc73b3
Refactor GitHub Actions workflow update for dotnet version handling
JosKWRubicon Dec 10, 2025
005a27d
Refactor code to improve null handling and make properties nullable w…
JosKWRubicon Dec 12, 2025
0e230a9
Update VSCode extension recommendations for improved development expe…
JosKWRubicon Dec 13, 2025
a87b779
Update code tours
JosKWRubicon Dec 13, 2025
8c627ad
Add code tour for AI explanation feature
JosKWRubicon Dec 13, 2025
ac2efdf
Add Copilot instructions for Explain PowerShell repository
JosKWRubicon Dec 13, 2025
b136b9a
Add Invoke-AiExplanation function for AI analysis integration
JosKWRubicon Dec 13, 2025
94bf734
Refactor AI Explanation integration tests to use Invoke-AiExplanation…
JosKWRubicon Dec 13, 2025
86d250e
Fix links in README for explainshell.com and CodeTour extension
JosKWRubicon Dec 13, 2025
e105b03
Replace Newtonsoft.Json with System.Text.Json for request deserializa…
JosKWRubicon Dec 13, 2025
2179e1d
Log unhandled AST node types and their counts in GetAnalysisResult
JosKWRubicon Dec 13, 2025
ce8b3af
Implement cancellation and cleanup for AI explanation requests to pre…
JosKWRubicon Dec 13, 2025
be8f39b
Add in-memory help repository seeding for unit tests
JosKWRubicon Dec 13, 2025
8ee37a9
Enhance unit tests for return and throw statements, adding behavior v…
JosKWRubicon Dec 13, 2025
2bd2352
Add explanations for return and throw statements with documentation l…
JosKWRubicon Dec 13, 2025
0c3d2c2
Add HTML synopsis scraper and related functions for documentation ext…
JosKWRubicon Dec 13, 2025
df46893
Refactor logical operators for consistency in AstVisitorExplainer
JosKWRubicon Dec 13, 2025
4d9f7e6
Add trap statement explanation and related tests
JosKWRubicon Dec 13, 2025
117da9f
Update documentation links to use the new Microsoft Learn URLs
JosKWRubicon Dec 13, 2025
81fe417
Add tests for tree generation and update syntax analyzer methods to r…
JosKWRubicon Dec 13, 2025
231259e
Implement switch statement explanation and related tests
JosKWRubicon Dec 13, 2025
ccbb566
fix failing tests
JosKWRubicon Dec 13, 2025
ed1d480
Vastly speed up test suite
JosKWRubicon Dec 13, 2025
85806b5
Refactor test execution scripts and update launch configurations
JosKWRubicon Dec 13, 2025
d9d67ca
Add unit tests for SyntaxAnalyzerFunction and refactor dependencies
JosKWRubicon Dec 13, 2025
de3c8f7
Add UpdateProfile switch to bootstrap script for conditional profile …
JosKWRubicon Dec 13, 2025
b9b95f4
Refactor GenerateTree method for improved clarity and performance
JosKWRubicon Dec 13, 2025
ed6ba30
Implement SyntaxAnalyzerClient and related classes for improved code …
JosKWRubicon Dec 13, 2025
8a65db6
Update tour descriptions and line references for improved clarity in …
JosKWRubicon Dec 13, 2025
707dac2
Refactor launch.json and tasks.json for consistency; update launchSet…
JosKWRubicon Dec 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Copilot instructions for Explain PowerShell

These instructions are for GitHub Copilot Chat/Edits when working in this repository.

## Repo overview (what this project is)
- A PowerShell oneliner explainer.
- Backend: Azure Functions (.NET) in `explainpowershell.analysisservice/`.
- Frontend: Blazor in `explainpowershell.frontend/`.
- Shared models: `explainpowershell.models/`.
- Tests: Pester tests in `explainpowershell.analysisservice.tests/`.
- Infra: Bicep in `explainpowershell.azureinfra/`.

## Architecture & flow
- The primary explanation is AST-based:
- `SyntaxAnalyzer` parses PowerShell into an AST and produces a list of `Explanation` nodes.
- The AI feature is additive and optional:
- The AST analysis returns immediately; the AI call is a separate endpoint invoked after the tree is available.
- Frontend triggers AI in the background so the UI remains responsive.

## Editing guidelines (preferred behavior)
- Keep in mind this project is open source and intended to be cross platform.
- Follow existing code style and patterns.
- Favor readability and maintainability.
- Prefer small, surgical changes; avoid unrelated refactors.
- Preserve existing public APIs and JSON shapes unless explicitly requested.
- Keep AI functionality optional and non-blocking.
- If AI configuration is missing, the app should still work (AI can silently no-op).
- Use existing patterns in the codebase (logging, DI, options, error handling).
- Don’t add new external dependencies unless necessary and justified.

## C# conventions
- Prefer async/await end-to-end.
- Handle nullability deliberately; avoid introducing new nullable warnings.
- Use `System.Text.Json` where the project already does; don’t mix serializers in the same flow unless required.

## Unit tests
- Aim for high coverage on new features.
- Focus on behavior verification over implementation details.
- When adding tests, follow existing patterns in `explainpowershell.analysisservice.tests/`.

## Building
- On fresh clones, run all code generators before building: `Get-ChildItem -Path $PSScriptRoot/explainpowershell.analysisservice/ -Recurse -Filter *_code_generator.ps1 | ForEach-Object { & $_.FullName }`

## PowerShell / Pester conventions
- Keep tests deterministic and fast; avoid relying on external services unless explicitly an integration test.
- When adding tests, follow the existing Pester structure and naming.
- Before adding Pester tests, consider if the behavior can be verified in C# unit tests first.

## Running locally
- For running Pester integration tests locally successfully it is necessary to run `.\bootstrap.ps1` from the repo root, it sets up the required data in Azurite, and calls code generators.
- For general debuging, running `.\bootstrap.ps1` once is also recommended. If Azurite is present and has helpldata, it is not necessary to run it again.
- You can load helper methods to test the functionapp locally by importing the following scripts in your PowerShell session:
```powershell
. C:\Users\JosKoelewijn\GitNoOneDrive\explainpowershell/explainpowershell.analysisservice.tests/Invoke-SyntaxAnalyzer.ps1
. C:\Users\JosKoelewijn\GitNoOneDrive\explainpowershell/explainpowershell.analysisservice.tests/Invoke-AiExplanation.ps1
. C:\Users\JosKoelewijn\GitNoOneDrive\explainpowershell/explainpowershell.analysisservice.tests/Get-HelpDatabaseData.ps1
. C:\Users\JosKoelewijn\GitNoOneDrive\explainpowershell/explainpowershell.analysisservice.tests/Get-MetaData.ps1
```

## How to validate changes
- Prefer the repo task: run the VS Code task named `run tests` (Pester).
- If you need a build check, use the VS Code `build` task.

## Documentation
- When adding developer-facing features, also update or add a CodeTour in `.tours/` when it improves onboarding.
60 changes: 15 additions & 45 deletions .tours/high-level-tour-of-the-application.tour
Original file line number Diff line number Diff line change
Expand Up @@ -5,87 +5,57 @@
{
"file": "explainpowershell.frontend/Pages/Index.razor",
"description": "Welcome at the high-level tour of Explain PowerShell!\n\nWe will follow the journey of one user request trough the application and this is where that begins; the text input field where you can enter your PowerShell oneliner.\n\nIf the user presses Enter or clicks the `Explain` button, the oneliner is sent from this frontend to the backend api, the SyntaxAnalyzer endpoint.\n\nLet's see what happens there, and we will come back once we have an explanation to display here.",
"line": 12,
"selection": {
"start": {
"line": 16,
"character": 34
},
"end": {
"line": 22,
"character": 23
}
}
"line": 15
},
{
"file": "explainpowershell.analysisservice/SyntaxAnalyzer.cs",
"description": "This is where the PowerShell oneliner is sent to, the SyntaxAnalyzer endpoint, an Azure FunctionApp. \n\nWe use PowerShell's own parsing engine to parse the oneliner that was sent, the parser creates a so called Abstract Syntax Tree (AST), a logical representation of the oneliner in a convenient tree format that we can then 'walk' in an automated fashion. ",
"line": 24
"line": 25
},
{
"file": "explainpowershell.analysisservice/SyntaxAnalyzer.cs",
"description": "The AST is analyzed here, via the AstVisitorExplainer. It basically looks at all the logical elements of the oneliner and generates an explanation for each of them.\n\nWe will have a brief look there as well, to get the basic idea.",
"line": 53
"line": 68
},
{
"file": "explainpowershell.analysisservice/AstVisitorExplainer.cs",
"file": "explainpowershell.analysisservice/AstVisitorExplainer_statements.cs",
"description": "This is an example of how an 'if' statement explanation is generated. When the AST contains an 'if' statement, this method is called, and an explanation for it is added to the list of explanations. ",
"line": 577
"line": 178
},
{
"file": "explainpowershell.analysisservice/AstVisitorExplainer.cs",
"file": "explainpowershell.analysisservice/AstVisitorExplainer_statements.cs",
"description": "The `Explanation` type you see here, is defined in the `Models` project, which is used both by the Backend api as well as the Blazor Frontend. \n\nLet's have a quick look there.",
"line": 580
"line": 181
},
{
"file": "explainpowershell.models/Explanation.cs",
"description": "This is how the `Explanation` type is defined. Even though we will send this type through the wire from the backend api to the frontend as json, because we use this same type on both ends, we can safely reserialize this data from json back to an `Explanation` object in the Frontend. \n\nThis is a great advantage of Blazor + C# api projects, you can have shared models. In JavaScript framework + c# backend api, you have to define the model twice. Which is errorprone. Ok back to our api.",
"line": 2
"line": 3
},
{
"file": "explainpowershell.analysisservice/AstVisitorExplainer.cs",
"file": "explainpowershell.analysisservice/AstVisitorExplainer_helpers.cs",
"description": "Once we are done going through all the elements in the AST, this method gets called, and we return all explanations and a little metadata.",
"line": 24,
"selection": {
"start": {
"line": 397,
"character": 55
},
"end": {
"line": 397,
"character": 58
}
}
"line": 29
},
{
"file": "explainpowershell.analysisservice/SyntaxAnalyzer.cs",
"description": "if there were any parse errors, we get the message for that here.",
"line": 61
"line": 79
},
{
"file": "explainpowershell.analysisservice/SyntaxAnalyzer.cs",
"description": "We send our list of explanations, and any parse error messages back to the frontend.",
"line": 65
"line": 88
},
{
"file": "explainpowershell.frontend/Pages/Index.razor",
"file": "explainpowershell.frontend/Pages/Index.razor.cs",
"description": "This is where we re-create the AST tree a little bit, and generate our own tree, to display everything in a nice tree view, ordered logically. ",
"line": 250,
"selection": {
"start": {
"line": 29,
"character": 29
},
"end": {
"line": 29,
"character": 38
}
}
"line": 152
},
{
"file": "explainpowershell.frontend/Pages/Index.razor",
"description": "Here is where we display all the tree items. This is basically a foreach, with an ItemTemplate that is filled in for each item in the tree.\n\nThis is how the end user gets to see the explanation that was generated for them.\n\nThis is the end of the high-level tour",
"line": 29
"line": 52
}
]
}
86 changes: 86 additions & 0 deletions .tours/tour-of-the-ai-explanation-feature.tour
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{
"$schema": "https://aka.ms/codetour-schema",
"title": "Tour of the AI explanation feature",
"steps": [
{
"file": "explainpowershell.analysisservice/SyntaxAnalyzer.cs",
"description": "The AI explanation is intentionally *not* generated during the main AST analysis call.\n\nThis endpoint parses the PowerShell input into an AST and walks it with `AstVisitorExplainer` to produce the structured explanation nodes (the data that becomes the tree you see in the UI).\n\nThat AST-based result is returned quickly so the UI can render immediately.",
"line": 25
},
{
"file": "explainpowershell.analysisservice/SyntaxAnalyzer.cs",
"description": "Key design choice: the AST endpoint always sets `AiExplanation` to an empty string.\n\nThe AI summary is fetched via a separate endpoint *after* the AST explanation tree is available. This keeps the main analysis deterministic and avoids coupling UI responsiveness to an external AI call.",
"line": 82
},
{
"file": "explainpowershell.frontend/Pages/Index.razor.cs",
"description": "Frontend starts by calling the AST analysis endpoint (`SyntaxAnalyzer`).\n\nThis request returns the expanded code plus a flat list of explanation nodes (each has an `Id` and optional `ParentId`).",
"line": 120
},
{
"file": "explainpowershell.frontend/Pages/Index.razor.cs",
"description": "Once the AST analysis result comes back, the frontend builds the explanation tree (based on `Id` / `ParentId`) for display in the `MudTreeView`.\n\nAt this point, the user already has a useful explanation from the AST visitor.",
"line": 152
},
{
"file": "explainpowershell.frontend/Pages/Index.razor.cs",
"description": "Immediately after the tree is available, the AI request is kicked off *in the background*.\n\nNotice the fire-and-forget pattern (`_ = ...`) so the AST UI is not blocked while the AI endpoint runs.",
"line": 157
},
{
"file": "explainpowershell.frontend/Pages/Index.razor.cs",
"description": "`LoadAiExplanationAsync` constructs a payload containing:\n- the original PowerShell code\n- the full AST analysis result\n\nThen it POSTs to the backend `AiExplanation` endpoint.\n\nIf the call fails, the UI silently continues without the AI summary (AI is treated as optional).",
"line": 160
},
{
"file": "explainpowershell.frontend/Clients/SyntaxAnalyzerClient.cs",
"description": "This is the actual HTTP call that requests the AI explanation.\n\nIt sends both the code and the AST result so the backend can build a prompt that is grounded in the already-produced explanation nodes and help metadata.",
"line": 69
},
{
"file": "explainpowershell.frontend/Pages/Index.razor",
"description": "The UI has a dedicated 'AI explanation' card.\n\nIt's only shown when either:\n- `AiExplanationLoading` is true (spinner), or\n- a non-empty `AiExplanation` has arrived.\n\nThis makes the feature feel additive: the AST explanation tree appears first, then the AI summary appears when ready.",
"line": 34
},
{
"file": "explainpowershell.analysisservice/AiExplanationFunction.cs",
"description": "Backend entrypoint for the AI feature: an Azure Function named `AiExplanation`.\n\nIt accepts an `AiExplanationRequest` that includes both the PowerShell code and the `AnalysisResult` produced earlier by the AST endpoint.",
"line": 23
},
{
"file": "explainpowershell.analysisservice/AiExplanationFunction.cs",
"description": "After validating/deserializing the request, this function delegates the real work to `IAiExplanationService.GenerateAsync(...)`.\n\nThis separation keeps the HTTP handler thin and makes the behavior easier to test.",
"line": 63
},
{
"file": "explainpowershell.analysisservice/Program.cs",
"description": "The AI feature is wired up through DI.\n\nA `ChatClient` is registered only when configuration is present (`Endpoint`, `ApiKey`, `DeploymentName`) and `Enabled` is true. If not configured, the factory returns `null` and AI remains disabled.",
"line": 36
},
{
"file": "explainpowershell.analysisservice/Services/AiExplanationOptions.cs",
"description": "AI behavior is controlled via `AiExplanationOptions`:\n- the system prompt (how the AI should behave)\n- an example prompt/response (few-shot guidance)\n- payload size guardrails (`MaxPayloadCharacters`)\n- request timeout (`RequestTimeoutSeconds`)\n\nThese settings are loaded from the `AiExplanation` config section.",
"line": 6
},
{
"file": "explainpowershell.analysisservice/Services/AiExplanationService.cs",
"description": "First guardrail: if DI did not create a `ChatClient`, the service returns `(null, null)` and logs that AI is unavailable.\n\nThis is what makes the feature optional without breaking the main AST explanation flow.",
"line": 30
},
{
"file": "explainpowershell.analysisservice/Services/AiExplanationService.cs",
"description": "Prompt grounding & size safety: the service builds a ‘slim’ version of the analysis result and serializes it to JSON for the prompt.\n\nIf the JSON is too large, it progressively reduces details (e.g., trims help fields, limits explanation count) so the request stays under `MaxPayloadCharacters`.",
"line": 57
},
{
"file": "explainpowershell.analysisservice/Services/AiExplanationService.cs",
"description": "Finally, the service builds a chat message list and calls `CompleteChatAsync`.\n\nThe response is reduced to a best-effort single sentence (per the prompt), and the model name is returned too so the UI can display what model produced the summary.",
"line": 92
},
{
"file": "explainpowershell.analysisservice.tests/Invoke-AiExplanation.Tests.ps1",
"description": "The AI endpoint has Pester integration tests.\n\nNotably, there’s coverage for:\n- accepting a valid `AnalysisResult` payload\n- handling very large payloads (50KB+) gracefully (exercise the payload reduction path)\n- an end-to-end workflow: call SyntaxAnalyzer first, then call AiExplanation with that output.",
"line": 3
}
]
}
2 changes: 1 addition & 1 deletion .tours/tour-of-the-azure-bootstrapper.tour
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{
"file": "azuredeploymentbootstrapper.ps1",
"description": "Welcome to the Azure bootstrapper tour.\n\nHere we will have a look at how you can get your own copy of explain powershell running in Azure, and this is basically how the actual www.explainpowershell.com site is set up in Azure too, excluding DNS, CDN and application insights.\n\nTo be able to use this, you need a GitHub account and an Azure subscription. A 30-day free subscription will do just fine.\nThe script assumes you have forked the explain powershell repo to your own github.\n\nYou will be asked to authenticate, so the script can set up everything.\n\nA few things are stored as GitHub Secrets, so they can be used from the GitHub Actions.\n\nAfter the resource group in Azure is created and the secrets are in place, you can run the `Deploy Azure Infra` GitHub Action. This action will deploy you copy of explain powershell to Azure.",
"line": 13
"line": 1
},
{
"file": ".github/workflows/deploy_azure_infra.yml",
Expand Down
15 changes: 10 additions & 5 deletions .tours/tour-of-the-help-collector.tour
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@
},
{
"file": ".github/workflows/add_module_help.yml",
"description": "A little bit above here, the requested module is installed on the GitHub action runner and here, the `helpcollector.ps1` script is run. This script will get all the relevant help information from the module. \n\nThe output is saved to json, and a check is done to see if there actually is any data in the file. ",
"line": 46
"description": "If the module is not installed on the machine running this pipeline, try to install it.",
"line": 25
},
{
"file": ".github/workflows/add_module_help.yml",
"description": "After the module is installed, the `helpcollector.ps1` script is run. This script will get all the relevant help information from the module. \n\nThe output is saved to json, and a check is done to see if there actually is any data in the file. ",
"line": 39
},
{
"file": ".github/workflows/add_module_help.yml",
"description": "Here the `helpwriter.ps1` script is started, and is given the path to the cached json output from the previous step. This basically writes the information to an Azure Storage Table.\n\nThe steps below are just to notify the requester of the module if everything succeeded or not. We will have a look over at the scripts now, to see what they do.",
"line": 68
"line": 74
},
{
"file": "explainpowershell.helpcollector/helpcollector.ps1",
Expand All @@ -24,8 +29,8 @@
},
{
"file": "explainpowershell.helpcollector/helpwriter.ps1",
"description": "We store the help data in an Azure Storage Table. These are tables with two very important columns: `PartitionKey` and `RowKey`. Data from a Table like this, is retrieved based on these two columns. If a want a certain row, I ask for `(PartitionKey='x' RowKey='y')`, if my data is at x, y so to speak. So if we store data, the string that is in the PartitionKey and the RowKey, needs to be unique and easily searchable. That's why we convert the name of the command to lower case. When we search for a command later, we can convert that commandname to lower too, and be sure we always find the right command, even when the user MIsTyped-ThecOmmand. ",
"line": 48
"description": "We store the help data in an Azure Storage Table. These are tables with two very important columns: `PartitionKey` and `RowKey`. Data from a Table like this, is retrieved based on these two columns. If we want a certain row, we ask for `(PartitionKey='x' RowKey='y')`, if our data is at x, y so to speak. So if we store data, the string that is in the PartitionKey and the RowKey, needs to be unique and easily searchable. That's why we convert the name of the command to lower case. When we search for a command later, we can convert that commandname to lower too, and be sure we always find the right command, even when the user MIsTyped-ThecOmmand. ",
"line": 117
},
{
"file": "explainpowershell.helpcollector/BulkHelpCollector.ps1",
Expand Down
11 changes: 4 additions & 7 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
{
"recommendations": [
"azurite.azurite",
"ms-azuretools.vscode-azurefunctions",
"ms-dotnettools.blazorwasm-companion",
"ms-dotnettools.csharp",
"ms-vscode.powershell",
"ms-dotnettools.vscode-dotnet-runtime",
"ms-dotnettools.blazorwasm-companion",
"vsls-contrib.codetour",
"derivitec-ltd.vscode-dotnet-adapter",
"ms-vscode.test-adapter-converter",
"hbenl.vscode-test-explorer",
"azurite.azurite"
"ms-vscode.powershell",
"vsls-contrib.codetour"
]
}
Loading