Skip to content

Commit bfdc4da

Browse files
sharpninjaCopilot
andcommitted
CQRS server profile handlers + clear API key feature
- Add SaveServerProfileRequest/Handler for persisting profile edits - Add DeleteServerProfileRequest/Handler for removing profiles - Add ClearServerApiKeyRequest/Handler to clear stored API keys - Refactor SettingsPageViewModel to dispatch all commands via IRequestDispatcher - Add API key status display and Clear API Key button to SettingsPage - Register all three handlers in MauiProgram.cs DI - Add ServerProfileHandlerTests with 7 test cases (save, delete, clear key) - Compact mobile chat UI: remove icon label, shrink session tabs/buttons - Reduce toolbar to Connect/Disconnect only (session controls in card) - Default port changed to 5243 (Linux/Docker) - Update PortPickerViewModelTests to match new default Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5ceb870 commit bfdc4da

14 files changed

Lines changed: 374 additions & 56 deletions
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using RemoteAgent.App.Logic;
2+
using RemoteAgent.App.Logic.Cqrs;
3+
using RemoteAgent.App.Requests;
4+
5+
namespace RemoteAgent.App.Handlers;
6+
7+
public sealed class ClearServerApiKeyHandler(IServerProfileStore profileStore)
8+
: IRequestHandler<ClearServerApiKeyRequest, CommandResult>
9+
{
10+
public Task<CommandResult> HandleAsync(ClearServerApiKeyRequest request, CancellationToken ct = default)
11+
{
12+
var workspace = request.Workspace;
13+
var profile = workspace.SelectedProfile;
14+
if (profile == null)
15+
return Task.FromResult(CommandResult.Fail("No server selected."));
16+
17+
profile.ApiKey = "";
18+
profileStore.Upsert(profile);
19+
workspace.HasApiKey = false;
20+
21+
return Task.FromResult(CommandResult.Ok());
22+
}
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using RemoteAgent.App.Logic;
2+
using RemoteAgent.App.Logic.Cqrs;
3+
using RemoteAgent.App.Requests;
4+
5+
namespace RemoteAgent.App.Handlers;
6+
7+
public sealed class DeleteServerProfileHandler(IServerProfileStore profileStore)
8+
: IRequestHandler<DeleteServerProfileRequest, CommandResult>
9+
{
10+
public Task<CommandResult> HandleAsync(DeleteServerProfileRequest request, CancellationToken ct = default)
11+
{
12+
var workspace = request.Workspace;
13+
var profile = workspace.SelectedProfile;
14+
if (profile == null)
15+
return Task.FromResult(CommandResult.Fail("No server selected."));
16+
17+
profileStore.Delete(profile.Host, profile.Port);
18+
workspace.SelectedProfile = null;
19+
workspace.RefreshProfiles();
20+
21+
return Task.FromResult(CommandResult.Ok());
22+
}
23+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using RemoteAgent.App.Logic;
2+
using RemoteAgent.App.Logic.Cqrs;
3+
using RemoteAgent.App.Requests;
4+
5+
namespace RemoteAgent.App.Handlers;
6+
7+
public sealed class SaveServerProfileHandler(IServerProfileStore profileStore)
8+
: IRequestHandler<SaveServerProfileRequest, CommandResult>
9+
{
10+
public Task<CommandResult> HandleAsync(SaveServerProfileRequest request, CancellationToken ct = default)
11+
{
12+
var workspace = request.Workspace;
13+
var profile = workspace.SelectedProfile;
14+
if (profile == null)
15+
return Task.FromResult(CommandResult.Fail("No server selected."));
16+
17+
profile.DisplayName = workspace.EditDisplayName;
18+
profile.PerRequestContext = workspace.EditPerRequestContext;
19+
profile.DefaultSessionContext = workspace.EditDefaultSessionContext;
20+
profileStore.Upsert(profile);
21+
workspace.RefreshProfiles();
22+
23+
return Task.FromResult(CommandResult.Ok());
24+
}
25+
}

src/RemoteAgent.App/MainPage.xaml

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@
2727
<ContentPage.ToolbarItems>
2828
<ToolbarItem Text="Connect" Command="{Binding ConnectCommand}" />
2929
<ToolbarItem Text="Disconnect" Command="{Binding DisconnectCommand}" />
30-
<ToolbarItem Text="New Session" Command="{Binding NewSessionCommand}" />
31-
<ToolbarItem Text="Terminate" Command="{Binding TerminateCurrentSessionCommand}" />
32-
<ToolbarItem Text="Template" Command="{Binding UsePromptTemplateCommand}" />
3330
</ContentPage.ToolbarItems>
3431

3532
<ContentPage.Resources>
@@ -85,14 +82,14 @@
8582
<Border.StrokeShape>
8683
<RoundRectangle CornerRadius="12"/>
8784
</Border.StrokeShape>
88-
<VerticalStackLayout Spacing="6">
89-
<HorizontalStackLayout Spacing="8" VerticalOptions="Center">
90-
<Label Text="Sessions" FontAttributes="Bold" VerticalOptions="Center" />
91-
<Button x:Name="NewSessionBtn" Text="+ New" Command="{Binding NewSessionCommand}" Padding="12,4" />
92-
<Button Text="Terminate" Command="{Binding TerminateCurrentSessionCommand}" Padding="12,4" />
85+
<VerticalStackLayout Spacing="4">
86+
<HorizontalStackLayout Spacing="6" VerticalOptions="Center">
87+
<Label Text="Sessions" FontAttributes="Bold" FontSize="13" VerticalOptions="Center" />
88+
<Button x:Name="NewSessionBtn" Text="+ New" Command="{Binding NewSessionCommand}" Padding="8,2" FontSize="12" />
89+
<Button Text="End" Command="{Binding TerminateCurrentSessionCommand}" Padding="8,2" FontSize="12" />
9390
</HorizontalStackLayout>
9491
<CollectionView x:Name="SessionTabsView"
95-
HeightRequest="44"
92+
HeightRequest="34"
9693
ItemsSource="{Binding Sessions}"
9794
SelectedItem="{Binding CurrentSession, Mode=TwoWay}"
9895
SelectionMode="Single">
@@ -101,15 +98,16 @@
10198
</CollectionView.ItemsLayout>
10299
<CollectionView.ItemTemplate>
103100
<DataTemplate>
104-
<HorizontalStackLayout Spacing="6">
105-
<Border Padding="10,6" StrokeThickness="1" Stroke="{StaticResource OutlineBrush}" BackgroundColor="{StaticResource SurfaceBrush}">
106-
<Border.StrokeShape><RoundRectangle CornerRadius="8"/></Border.StrokeShape>
107-
<Label Text="{Binding Title}" MaxLines="1" LineBreakMode="TailTruncation" />
101+
<HorizontalStackLayout Spacing="2">
102+
<Border Padding="8,4" StrokeThickness="1" Stroke="{StaticResource OutlineBrush}" BackgroundColor="{StaticResource SurfaceBrush}">
103+
<Border.StrokeShape><RoundRectangle CornerRadius="6"/></Border.StrokeShape>
104+
<Label Text="{Binding Title}" FontSize="12" MaxLines="1" LineBreakMode="TailTruncation" />
108105
</Border>
109-
<Button Text="X"
110-
WidthRequest="34"
111-
HeightRequest="34"
106+
<Button Text="" FontSize="10"
107+
WidthRequest="24"
108+
HeightRequest="24"
112109
Padding="0"
110+
CornerRadius="12"
113111
Command="{Binding BindingContext.TerminateSessionCommand, Source={x:Reference ThisPage}}"
114112
CommandParameter="{Binding .}" />
115113
</HorizontalStackLayout>
@@ -259,22 +257,21 @@
259257
</CollectionView.ItemTemplate>
260258
</CollectionView>
261259

262-
<!-- Input: M3 outlined field row, 48dp touch -->
263-
<Border Grid.Row="3" Stroke="{StaticResource OutlineBrush}" StrokeThickness="1" Padding="12,8" Margin="0,8,0,0">
264-
<Border.StrokeShape>
265-
<RoundRectangle CornerRadius="8"/>
266-
</Border.StrokeShape>
267-
<HorizontalStackLayout Spacing="8" VerticalOptions="Center">
268-
<Label Style="{StaticResource IconLabel}" Text="{StaticResource IconMessage}"
269-
TextColor="{StaticResource OnSurfaceVariantBrush}" VerticalOptions="Center" />
270-
<Editor x:Name="MessageEditor"
260+
<!-- Input: M3 outlined field row -->
261+
<Border Grid.Row="3" Stroke="{StaticResource OutlineBrush}" StrokeThickness="1" Padding="8,4" Margin="0,8,0,0">
262+
<Border.StrokeShape>
263+
<RoundRectangle CornerRadius="8"/>
264+
</Border.StrokeShape>
265+
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="6">
266+
<Editor Grid.Column="0" x:Name="MessageEditor"
271267
Placeholder="Message to agent..."
272268
Text="{Binding PendingMessage, Mode=TwoWay}"
273269
AutoSize="TextChanges"
274-
MinimumHeightRequest="72"
270+
MinimumHeightRequest="40"
275271
PlaceholderColor="{StaticResource OnSurfaceVariantBrush}"
276-
VerticalOptions="Center" />
277-
<HorizontalStackLayout Spacing="4" VerticalOptions="Center">
272+
VerticalOptions="Center"
273+
HorizontalOptions="Fill" />
274+
<HorizontalStackLayout Grid.Column="1" Spacing="4" VerticalOptions="Center">
278275
<Button x:Name="AttachBtn" Command="{Binding AttachCommand}" Padding="8" MinimumHeightRequest="36" MinimumWidthRequest="36">
279276
<Button.ImageSource>
280277
<FontImageSource FontFamily="fa-solid-900" Glyph="{StaticResource IconPaperclip}" Color="{StaticResource OnPrimaryBrush}" Size="16"/>
@@ -286,7 +283,7 @@
286283
</Button.ImageSource>
287284
</Button>
288285
</HorizontalStackLayout>
289-
</HorizontalStackLayout>
286+
</Grid>
290287
</Border>
291288

292289
<!-- Status: M3 label small, OnSurfaceVariant -->

src/RemoteAgent.App/MauiProgram.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ public static MauiApp CreateMauiApp()
5353
builder.Services.AddTransient<IRequestHandler<SaveMcpServerRequest, CommandResult>, SaveMcpServerHandler>();
5454
builder.Services.AddTransient<IRequestHandler<DeleteMcpServerRequest, CommandResult>, DeleteMcpServerHandler>();
5555
builder.Services.AddTransient<IRequestHandler<ScanQrCodeRequest, CommandResult>, ScanQrCodeHandler>();
56+
builder.Services.AddTransient<IRequestHandler<SaveServerProfileRequest, CommandResult>, SaveServerProfileHandler>();
57+
builder.Services.AddTransient<IRequestHandler<DeleteServerProfileRequest, CommandResult>, DeleteServerProfileHandler>();
58+
builder.Services.AddTransient<IRequestHandler<ClearServerApiKeyRequest, CommandResult>, ClearServerApiKeyHandler>();
5659

5760
builder.Services.AddSingleton<MainPage>();
5861
builder.Services.AddSingleton<MainPageViewModel>();
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using RemoteAgent.App.Logic.Cqrs;
2+
using RemoteAgent.App.ViewModels;
3+
4+
namespace RemoteAgent.App.Requests;
5+
6+
public sealed record ClearServerApiKeyRequest(
7+
Guid CorrelationId,
8+
SettingsPageViewModel Workspace) : IRequest<CommandResult>
9+
{
10+
public override string ToString() =>
11+
$"ClearServerApiKeyRequest {{ CorrelationId = {CorrelationId} }}";
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using RemoteAgent.App.Logic.Cqrs;
2+
using RemoteAgent.App.ViewModels;
3+
4+
namespace RemoteAgent.App.Requests;
5+
6+
public sealed record DeleteServerProfileRequest(
7+
Guid CorrelationId,
8+
SettingsPageViewModel Workspace) : IRequest<CommandResult>
9+
{
10+
public override string ToString() =>
11+
$"DeleteServerProfileRequest {{ CorrelationId = {CorrelationId} }}";
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using RemoteAgent.App.Logic.Cqrs;
2+
using RemoteAgent.App.ViewModels;
3+
4+
namespace RemoteAgent.App.Requests;
5+
6+
public sealed record SaveServerProfileRequest(
7+
Guid CorrelationId,
8+
SettingsPageViewModel Workspace) : IRequest<CommandResult>
9+
{
10+
public override string ToString() =>
11+
$"SaveServerProfileRequest {{ CorrelationId = {CorrelationId} }}";
12+
}

src/RemoteAgent.App/SettingsPage.xaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
<?xml version="1.0" encoding="utf-8" ?>
22
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
33
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
4+
xmlns:converters="clr-namespace:RemoteAgent.App.Converters"
45
x:Class="RemoteAgent.App.SettingsPage"
56
Title="Settings"
67
BackgroundColor="{StaticResource SurfaceBrush}">
8+
<ContentPage.Resources>
9+
<converters:InverseBoolConverter x:Key="InverseBool" />
10+
</ContentPage.Resources>
711
<ScrollView>
812
<VerticalStackLayout Padding="16" Spacing="12">
913
<Label Text="Settings" FontSize="24" FontAttributes="Bold" Margin="4" />
@@ -60,6 +64,16 @@
6064
Placeholder="Context seeded when a new session starts"
6165
AutoSize="TextChanges" MinimumHeightRequest="64" Margin="4" />
6266

67+
<Label Text="API Key" FontSize="12" TextColor="{StaticResource OnSurfaceVariantBrush}" Margin="4" />
68+
<HorizontalStackLayout Spacing="8" Margin="4">
69+
<Label Text="✓ API key stored" FontSize="13" TextColor="Green" VerticalOptions="Center"
70+
IsVisible="{Binding HasApiKey}" />
71+
<Label Text="No API key" FontSize="13" TextColor="{StaticResource OnSurfaceVariantBrush}" VerticalOptions="Center"
72+
IsVisible="{Binding HasApiKey, Converter={StaticResource InverseBool}}" />
73+
<Button Text="Clear API Key" Command="{Binding ClearApiKeyCommand}"
74+
TextColor="{StaticResource ErrorBrush}" Padding="8,4" FontSize="12" />
75+
</HorizontalStackLayout>
76+
6377
<HorizontalStackLayout Spacing="8" Margin="4">
6478
<Button Text="Save" Command="{Binding SaveCommand}" />
6579
<Button Text="Delete Server" Command="{Binding DeleteCommand}"

src/RemoteAgent.App/ViewModels/MainPageViewModel.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ public sealed class MainPageViewModel : INotifyPropertyChanged, ISessionCommandB
1616
private const string PrefServerPort = "ServerPort";
1717
private const string PrefPerRequestContext = "PerRequestContext";
1818
private const string PrefApiKey = "ApiKey";
19-
private const string DefaultPort = "5244";
19+
private const string DefaultPort = "5243";
2020

21-
/// <summary>Well-known ports offered in the port picker (Windows service = 5244, Linux/Docker = 5243).</summary>
22-
public static readonly IReadOnlyList<string> AvailablePorts = ["5244", "5243"];
21+
/// <summary>Well-known ports offered in the port picker (Linux/Docker = 5243, Windows service = 5244).</summary>
22+
public static readonly IReadOnlyList<string> AvailablePorts = ["5243", "5244"];
2323

2424
private readonly ISessionStore _sessionStore;
2525
private readonly IAgentGatewayClient _gateway;

0 commit comments

Comments
 (0)