A WPF (and beta WinUI3) user control that provides an AI chat assistant interface with support for multiple AI providers (GitHub Copilot, Google Gemini, Anthropic, OpenAI, GitHub Models, and custom or local providers like ollama that support OpenAI API endpoints). It is meant to be a very quick way to bolt on an AI agent to any application you have. You can provide context and direct action buttons inline with the AI responses. It includes both a standalone chat control and an expandable sidebar version. It has integrated support for discovery of local existing GitHub Copilot tokens (ie from vscode) and also integrates a login flow for users that don't already have it present.
The included Expandable control allows it to easily collapse down to 40 pixels when not used. It has built in configuration UI for easy setup.
You can optionally provide a "reference text/document" which can be a longer document the user is working on and allow the user to control how much(if any) of it to send along.
| In this screenshot it is the left panel | Config Pane | Collapsed View |
|---|---|---|
![]() |
![]() |
![]() |
- PilotAiAssistControl
Install-Package PilotAiAssistControlWPF
xmlns:pia="clr-namespace:PilotAIAssistantControl;assembly=PilotAIAssistantControl"Create a class inheriting from AIOptions to configure the AI behavior:
public class RegexAIOptions : AIOptions {
public override string GetSystemPrompt() =>
"You are a C# Regex expert assistant. " +
"Provide regex patterns inside Markdown code blocks (```regex ... ```). " +
"Explain how the pattern works briefly.";
}Option A: Expandable Panel (Recommended for sidebars)
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="AiColumn" Width="Auto"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<pia:UCAIExpandable x:Name="ucAiExpandable"
TargetColumn="{Binding ElementName=AiColumn}"
DefaultSize="400"
MinExpandedSize="200"
CollapsedSize="40"/>
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch"/>
<!-- Your main content in Grid.Column="2" -->
</Grid>Option B: Direct Control (For fixed layouts)
<pia:UCAI x:Name="ucAi" Width="400"/>Important: You must call Configure() followed by ImportData() for the control to fully initialize. Always call ImportData() even if you have no data to restore (pass null).
private void Window_Loaded(object sender, RoutedEventArgs e) {
var options = new RegexAIOptions();
// For expandable version:
ucAiExpandable.AiControl.Configure(options);
ucAiExpandable.AiControl.ImportData(null); // Required even with no saved data
// Or for direct control:
ucAi.Configure(options);
ucAi.ImportData(null);
}- .NET 6.0+ or .NET Framework 4.7.2+
- WPF application
- Newtonsoft.Json (for settings serialization)
The control supports saving and restoring the user's AI provider configuration. You must call ImportData even if you are not storing any settings (just pass null) as the control does not initialize until you do. You can call ExportData/ImportData to save/restore settings. It will return a Serializable friendly object from ExportData and expects that object back for importing. You can append this into your own serializable config if so desired.
Security: User tokens can be saved in this data for their AI providers depending on the provider. If the token is saved it is protected with the microsoft DPAPI which locks it to the current user account. You should not need to do anything the control will handle it all.
Here is a simple example:
// Save settings (e.g., on window closing)
private void Window_Closing(object sender, CancelEventArgs e) {
AIUserConfig data = ucAiExpandable.AiControl.ExportData();
string json = JsonConvert.SerializeObject(data);
File.WriteAllText("ai_settings.json", json);
}
// Restore settings (on load)
private void Window_Loaded(object sender, RoutedEventArgs e) {
var options = new RegexAIOptions();
ucAiExpandable.AiControl.Configure(options);
AIUserConfig? savedData = null;
if (File.Exists("ai_settings.json")) {
savedData = JsonConvert.DeserializeObject<AIUserConfig>(
File.ReadAllText("ai_settings.json"));
}
ucAiExpandable.AiControl.ImportData(savedData);
}The AIOptions class controls how the AI chat behaves. Override properties and methods to customize:
| Member | Description |
|---|---|
string GetSystemPrompt() |
Required. Returns the system prompt that defines the AI's behavior and context. |
IAIModelProvider[] Providers |
Array of available AI providers. Defaults to built-in providers (GitHub Copilot, OpenAI, etc.). |
REFERENCE_TEXT_REPLACE_ACTION ReplaceAction |
Controls how reference text updates are handled in chat history. Default: ChangeOldToPlaceholder. |
stirng GetCurrentReferenceText() |
Returns the current reference text to send with queries (e.g., document content). |
string ReferenceTextHeader |
Header shown to the AI for reference text blocks. Default: "Reference Text". |
string ReferenceTextDisplayName |
User-facing name for reference text. Default: "Reference Text". |
int DefaultMaxReferenceTextCharsToSend |
Max characters of reference text to include. Default: 5000. |
bool AllowUserToSetMaxReferenceTextCharsToSend |
Show UI to let user adjust max chars. Default: true. |
String HintForUserInput |
Placeholder text for the chat input box. Default: "Ask your question here...". |
String FormatUserQuestion(string) |
Transform user questions before sending to AI. |
IEnumerable<CodeblockAction> CodeblockActions |
Custom actions shown on code blocks in AI responses. |
void HandleDebugMessage(string) |
Receive debug messages from the AI service. |
It may be non-obvious but if you want to disable the "reference text" functionality you should set ReplaceAction = ReferenceTextDisabled this will disable it completely and hide those UI Components. See below for more details on options.
When using reference text that changes during the conversation we likely don't want to include all the past versions to minimize the context window. The enum value here controls how we handle that while trying to minimize any confusion on the ai model's side as to what happened in the conversation.
| Action | Behavior |
|---|---|
ReferenceTextDisabled |
Disables reference text feature entirely. |
ChangeOldToPlaceholder |
Replaces old reference text with a placeholder note. (Recommended) |
LeaveOldInplace |
Keeps old versions in history (uses more tokens). |
UpdateInPlace |
Updates reference text where it first appeared. |
DeleteOld |
Removes old reference text completely. |
When the AI responds with code blocks (markdown fenced code), you can add action buttons that conditionally appear below each block. This lets users quickly apply, copy, or process the AI's code suggestions.
GenericCodeblockAction.ClipboardAction- A pre-built "π Copy" sample action that copies the code to clipboard.
YOu can create your own custom actions either implementing the ICodeblockAction interface directly or using the GenericCodeblockAction helper base class to create custom actions:
new GenericCodeblockAction("π Use as Pattern", async (block) => {
// block.Code contains the code block content
// block.Language contains the language identifier (e.g., "regex", "csharp")
_mainWindow.txtPattern.Text = block.Code;
return true; // Return true on success, false on failure
}) {
Tooltip = "Use this code block as the regex pattern",
FeedbackOnAction = "β Applied!" // Shown briefly after clicking
}The block parameter passed to your action implements ICodeBlock:
| Property | Type | Description |
|---|---|---|
Code |
string |
The content of the code block (without the fences). |
Language |
string |
The language identifier from the fence (e.g., regex, csharp, json). |
To show actions only for specific code block types, set IsVisibleDel. It will get the entire ICodeBlock object to determine if it wants to offer its action:
new GenericCodeblockAction("π Use as Pattern", async (block) => {
_mainWindow.txtPattern.Text = block.Code;
return true;
}) {
Tooltip = "Apply this regex pattern",
FeedbackOnAction = "β Applied!",
IsVisibleDel = (block) => block.Language == "regex" // Only show for regex blocks
}For more control, implement the CodeblockAction interface directly:
public interface ICodeblockAction {
Task<bool> DoAction(ICodeBlock block); // Execute the action
string DisplayName { get; } // Button text (e.g., "π Copy")
string Tooltip => DisplayName; // Hover tooltip
string FeedbackOnAction => "β Done!"; // Shown after action completes
bool IsVisible(ICodeBlock block) => true; // Control visibility per block
}public class RegexAIOptions : AIOptions {
private readonly MainWindow _mainWindow;
public RegexAIOptions(MainWindow mainWindow) {
_mainWindow = mainWindow;
}
public override string GetSystemPrompt() =>
"You are a C# Regex expert assistant. The user has questions about their regex patterns and target text. " +
"Provide regex patterns inside Markdown code blocks (```regex ... ```). " +
"Explain how the pattern works briefly. " +
"If the language supports named capture groups, use these by default.";
public override string GetCurrentReferenceText() =>
_mainWindow.txtTargetText.Text;
public override string FormatUserQuestion(string userQuestion) =>
$"Current pattern:\n```regex\n{_mainWindow.txtPattern.Text}\n```\n\nMy question: {userQuestion}";
public override string ReferenceTextHeader => "Users current target text";
public override string ReferenceTextDisplayName => "Target Text";
public override string HintForUserInput => "Ask about a pattern or matching...";
public override int DefaultMaxReferenceTextCharsToSend => 5000;
public override IEnumerable<CodeblockAction> CodeblockActions => [
GenericCodeblockAction.ClipboardAction,
new GenericCodeblockAction("π Use as Pattern", async (block) => {
_mainWindow.txtPattern.Text = block.Code;
return true;
}) {
Tooltip = "Use this code block as the regex pattern",
FeedbackOnAction = "β Applied!"
}
];
public override void HandleDebugMessage(string msg) =>
System.Diagnostics.Debug.WriteLine(msg);
}| Property | Type | Description |
|---|---|---|
TargetColumn |
ColumnDefinition |
Grid column to resize on expand/collapse (horizontal mode). |
TargetRow |
RowDefinition |
Grid row to resize on expand/collapse (vertical mode). |
DefaultSize |
double |
Initial expanded size. Default: 400. |
MinExpandedSize |
double |
Minimum size when expanded. Default: 200. |
CollapsedSize |
double |
Size when collapsed. Default: 40. |
IsExpanded |
bool |
Current expansion state (two-way bindable). |
Header |
object |
Custom header content for the expander. |
AiControl |
UCAI |
Access to the inner AI chat control. |
ExpanderControl |
Expander |
Access to the inner Expander control. |
The control doesn't have built-in UI for switching between multiple AI "agents" (different system prompts), but you can easily implement this yourself by calling Configure() with different AIOptions instances. If the "Providers" property of the AIOptions is the same between AiOption instances then the user's provider settings are preserved across agent switches.
Define multiple agents with different system prompts:
// Agent 1: Generates regex patterns based on user requirements
public class RegexGeneratorAgent : AIOptions {
public override string GetSystemPrompt() =>
"You are a C# Regex expert. Help users create regex patterns. " +
"Provide patterns in ```regex code blocks.";
public override string HintForUserInput => "Describe what you want to match...";
public override string ReferenceTextHeader => "Users current target text";
public override string GetCurrentReferenceText() => MainWindow.Instance.txtTest.Text;
public override string FormatUserQuestion(string userQuestion) =>
$"Current pattern:\n```regex\n{MainWindow.Instance.txtRegex.Text}\n```\n\n{userQuestion}";
}
// Agent 2: Explains existing regex patterns
public class RegexExplainerAgent : AIOptions {
public override string GetSystemPrompt() =>
"You are a regex expert. Explain how regex patterns work. " +
"Be concise - a few sentences for simple patterns, a paragraph for complex ones. " +
"Don't give examples unless asked.";
public override string HintForUserInput => "Provide a regex pattern to explain...";
public override string FormatUserQuestion(string userQuestion) =>
$"Please explain this regex:\n```regex\n{userQuestion}\n```";
// This agent doesn't need reference text
public override REFERENCE_TEXT_REPLACE_ACTION ReplaceAction =>
REFERENCE_TEXT_REPLACE_ACTION.ReferenceTextDisabled;
}Add a ComboBox above the AI pane to switch agents:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ComboBox x:Name="agentCombo" SelectionChanged="AgentCombo_SelectionChanged">
<ComboBoxItem Content="Regex Generator" IsSelected="True"/>
<ComboBoxItem Content="Regex Explainer"/>
</ComboBox>
<pia:UCAIExpandable Grid.Row="1" x:Name="ucAi"
TargetColumn="{Binding ElementName=AiColumn}"/>
</Grid>Handle the switch in code-behind:
private RegexGeneratorAgent _generatorAgent = new();
private RegexExplainerAgent _explainerAgent = new();
private void Window_Loaded(object sender, RoutedEventArgs e) {
ucAi.AiControl.Configure(_generatorAgent);
ucAi.AiControl.ImportData(LoadSavedSettings());
}
private void AgentCombo_SelectionChanged(object sender, SelectionChangedEventArgs e) {
if (!IsLoaded) return;
AIOptions newAgent = agentCombo.SelectedIndex == 0 ? _generatorAgent : _explainerAgent;
ucAi.Configure(newAgent); // Clears chat, keeps provider settings
}You can also trigger agent switches programmatically and send a message:
private async void ExplainRegex_Click(object sender, RoutedEventArgs e) {
agentCombo.SelectedIndex = 1; // Switch to explainer agent
await ucAi.SendMessage($"`{txtRegex.Text}`"); // Send the current pattern
}The control uses Micrsoft's Microsoft.SemanticKernel AI backend to talk with different AI model providers. Most providers can work with the standard OpenAI style protocol so can be added using the custom endpoint provider in our settings. Additional providers that work through Microsoft.SemanticKernel can be added pretty easily, look at AiModelProvider.cs for examples. GithubCopilotProvider.cs shows the most complex provider using a custom login flow and auto token discovery.
There is also a WinUi3 beta support version of this control. It largely works but is not as polished. There is not a good Markdown engine with regex highlighting for one for WinUI3 but we are using the older community toolkit which does give us limited highlight support. The WinUI3 version is not as well tested and may have other bugs.


