Skip to content

Commit 739a805

Browse files
sharpninjaCopilot
andcommitted
feat: add Settings tab to Android, rename filter words to phrases, add Revert button
- Create PhoneSettingsView for Android with mobile-friendly layout - Add Settings tab to PhoneMainView bottom tabs - Rename 'filter words' to 'filter phrases' across all labels, docs, and logs - Add RevertFilterWordsCommand to restore last-saved values - Add Revert button to both Desktop and Android settings views Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1c7898c commit 739a805

9 files changed

Lines changed: 165 additions & 28 deletions

File tree

docs/todo.yaml

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,64 @@ Architecture:
77
description:
88
- McpServer.UI.Core contains shared ViewModels (RunSyncViewModel, SyncStatusViewModel were removed but other VMs may exist). Evaluate whether McpServerManager.Core ViewModels can be refactored to reuse or extend the ViewModels provided by McpServer.UI.Core to reduce duplication and improve maintainability.
99
- This is especially relevant after the /mcp/ to /mcpserver/ route rename and sync endpoint removal — the UI.Core layer has been updated accordingly.
10+
- id: RT-REFACTOR
11+
title: Observable ConnectionState refactor
12+
estimate: 8h
13+
done: false
14+
description:
15+
- Full refactor to replace push-based data propagation with observable ConnectionState singleton.
16+
- 'STEP 1 (001): Create ConnectionState ObservableObject singleton in McpServerManager.Core/Services/ with properties: ActiveMcpBaseUrl, ApiKey, BearerToken, WorkspacePath, WorkspaceKey, WorkspaceDisplayName, IsConnected.'
17+
- 'STEP 2 (002): Migrate WorkspacePath - remove duplicates from VoiceConversationViewModel and McpVoiceConversationService; all consumers read from ConnectionState directly.'
18+
- 'STEP 3 (003): Migrate voice service auth credentials - remove cached _baseUrl/_apiKey/_bearerToken from McpVoiceConversationService; read from ConnectionState per-request.'
19+
- 'STEP 4 (004): Replace platform callbacks (SaveWorkspaceKey/LoadWorkspaceKey, GetEditorText) with IWorkspacePreferenceStore and IEditorTextProvider interfaces.'
20+
- 'STEP 5 (005): Replace GlobalStatusChanged event pattern with observable StatusState or ConnectionState properties; final audit pass for remaining push-based propagation.'
21+
technical-details:
22+
- Use [ObservableProperty] source generators from CommunityToolkit.Mvvm for ConnectionState
23+
- Must be thread-safe for cross-thread property change notifications
24+
- 'Auth credentials currently duplicated in 5+ objects: MainWindowViewModel, _mcpClient, _mcpPromptClient, McpVoiceConversationService, VoiceConversationViewModel'
25+
- 'WorkspacePath currently lives in 4 objects: _mcpClient, _mcpPromptClient, VoiceConversationViewModel, McpVoiceConversationService'
26+
- 'CRITICAL sync risk: voice service never gets updated with new credentials after creation'
27+
- The voice VM lazy-init race condition is a direct result of push-based propagation
28+
- McpServerClient.WorkspacePath should also read from ConnectionState
29+
implementation-tasks:
30+
- task: '[001] Create ConnectionState.cs in Core/Services/'
31+
done: false
32+
- task: '[001] Register ConnectionState as singleton in service collection'
33+
done: false
34+
- task: '[001] Inject ConnectionState into MainWindowViewModel constructor'
35+
done: false
36+
- task: '[002] Remove _workspacePath field from VoiceConversationViewModel'
37+
done: false
38+
- task: '[002] Remove WorkspacePath property from McpVoiceConversationService'
39+
done: false
40+
- task: '[002] McpServerClient reads WorkspacePath from ConnectionState'
41+
done: false
42+
- task: '[002] Remove all workspace path push sites from MainWindowViewModel'
43+
done: false
44+
- task: '[003] Remove _baseUrl, _apiKey, _bearerToken fields from McpVoiceConversationService'
45+
done: false
46+
- task: '[003] Inject ConnectionState into McpVoiceConversationService'
47+
done: false
48+
- task: '[003] Read auth from ConnectionState per-request in HTTP client creation'
49+
done: false
50+
- task: '[003] Evaluate McpServerClient auth migration (may keep current pattern)'
51+
done: false
52+
- task: '[004] Create IWorkspacePreferenceStore interface'
53+
done: false
54+
- task: '[004] Implement AndroidWorkspacePreferenceStore using SharedPreferences'
55+
done: false
56+
- task: '[004] Create IEditorTextProvider interface'
57+
done: false
58+
- task: '[004] Wire ConnectionState.WorkspaceKey PropertyChanged to auto-save'
59+
done: false
60+
- task: '[004] Remove SaveWorkspaceKey/LoadWorkspaceKey callbacks from MainWindowViewModel'
61+
done: false
62+
- task: '[005] Create StatusState observable or add status properties to ConnectionState'
63+
done: false
64+
- task: '[005] Replace GlobalStatusChanged event subscriptions with PropertyChanged'
65+
done: false
66+
- task: '[005] Final audit pass for remaining push patterns'
67+
done: false
1068
Features:
1169
high-priority:
1270
- id: settings-tab-speech-filter-words
@@ -23,16 +81,16 @@ Features:
2381
done-summary: Settings tab with configurable speech filter word list implemented and committed in 5e42d8f. SpeechFilterService, SettingsViewModel, SettingsView.axaml all created and integrated. Build verified.
2482
implementation-tasks:
2583
- task: Create SpeechFilterService in Core with persistence and ShouldFilter method
26-
done: false
84+
done: true
2785
- task: Create SettingsViewModel in Core with filter word list binding
28-
done: false
86+
done: true
2987
- task: Create SettingsView.axaml and code-behind in Desktop
30-
done: false
88+
done: true
3189
- task: Add Settings tab to MainWindow.axaml
32-
done: false
90+
done: true
3391
- task: Wire SettingsViewModel in MainWindowViewModel
34-
done: false
92+
done: true
3593
- task: Integrate SpeechFilterService into Android SimplifiedVoiceView TTS flow
36-
done: false
94+
done: true
3795
- task: Build and verify no compilation errors
38-
done: false
96+
done: true

src/McpServerManager.Android/Views/PhoneMainView.axaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252
<TabItem Header="Session Log">
5353
<views:PhoneSessionLogView/>
5454
</TabItem>
55+
<TabItem Header="Settings">
56+
<views:PhoneSettingsView DataContext="{Binding SettingsViewModel}"/>
57+
</TabItem>
5558
</TabControl>
5659

5760
<!-- Status Bar -->
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<UserControl xmlns="https://github.com/avaloniaui"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
xmlns:vm="using:McpServerManager.Core.ViewModels"
4+
x:Class="McpServerManager.Android.Views.PhoneSettingsView"
5+
x:DataType="vm:SettingsViewModel">
6+
7+
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="8">
8+
9+
<!-- Speech Filter Section Header -->
10+
<StackPanel Grid.Row="0" Spacing="4" Margin="0,8,0,8">
11+
<TextBlock Text="Speech Filter Phrases" FontSize="22" FontWeight="SemiBold"/>
12+
<TextBlock Text="Lines containing any of these phrases (case-insensitive) will be excluded from text-to-speech. One phrase per line."
13+
TextWrapping="Wrap" Opacity="0.7" FontSize="18"/>
14+
</StackPanel>
15+
16+
<!-- Filter Words Editor -->
17+
<TextBox Grid.Row="2"
18+
Text="{Binding SpeechFilterWords, Mode=TwoWay}"
19+
AcceptsReturn="True"
20+
TextWrapping="Wrap"
21+
Watermark="Enter filter phrases, one per line..."
22+
FontSize="20"
23+
MinHeight="200"
24+
VerticalContentAlignment="Top"/>
25+
26+
<!-- Save Button + Status -->
27+
<StackPanel Grid.Row="3" Orientation="Horizontal" Spacing="8" Margin="0,8,0,4">
28+
<Button Content="Save" Command="{Binding SaveFilterWordsCommand}" Padding="14,8" FontSize="21"/>
29+
<Button Content="Revert" Command="{Binding RevertFilterWordsCommand}" Padding="14,8" FontSize="21"/>
30+
<TextBlock Text="{Binding StatusMessage}" VerticalAlignment="Center" Opacity="0.7" FontSize="19"/>
31+
</StackPanel>
32+
33+
</Grid>
34+
35+
</UserControl>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Avalonia.Controls;
2+
3+
namespace McpServerManager.Android.Views;
4+
5+
public partial class PhoneSettingsView : UserControl
6+
{
7+
public PhoneSettingsView()
8+
{
9+
InitializeComponent();
10+
}
11+
}

src/McpServerManager.Android/Views/SimplifiedVoiceView.axaml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,18 @@
3737
IsVisible="{Binding IsAssistant}">
3838
<StackPanel>
3939
<md:MarkdownScrollViewer Markdown="{Binding Text}" Margin="0"/>
40-
<TextBlock Text="{Binding TimingText}"
41-
IsVisible="{Binding HasTiming}"
42-
FontSize="13" Opacity="0.5" Margin="2,4,2,0"
43-
FontStyle="Italic" TextWrapping="Wrap"/>
40+
<StackPanel Orientation="Horizontal"
41+
IsVisible="{Binding HasTiming}"
42+
Margin="2,4,2,0" Spacing="4">
43+
<TextBlock Text="{Binding TimingText}"
44+
FontSize="13" Opacity="0.5"
45+
FontStyle="Italic" TextWrapping="Wrap"
46+
VerticalAlignment="Center"/>
47+
<PathIcon Data="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4M12.5,7H11V13L16.2,16.2L17,14.9L12.5,12.2V7Z"
48+
Width="14" Height="14" Opacity="0.5"
49+
IsVisible="{Binding IsStreaming}"
50+
VerticalAlignment="Center"/>
51+
</StackPanel>
4452
</StackPanel>
4553
</Border>
4654
</Panel>

src/McpServerManager.Android/Views/SimplifiedVoiceView.axaml.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,19 @@ public string TimingText
6767
/// <summary>Elapsed time from request submission to final response.</summary>
6868
public TimeSpan? FinalResponseDuration { get; set; }
6969

70+
private bool _isStreaming;
71+
/// <summary>True while response is still streaming in.</summary>
72+
public bool IsStreaming
73+
{
74+
get => _isStreaming;
75+
set
76+
{
77+
if (_isStreaming == value) return;
78+
_isStreaming = value;
79+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsStreaming)));
80+
}
81+
}
82+
7083
public bool HasTiming => !string.IsNullOrEmpty(_timingText);
7184

7285
public event PropertyChangedEventHandler? PropertyChanged;
@@ -91,8 +104,8 @@ public void UpdateTiming(TimeSpan? elapsed = null)
91104
? FormatDuration(FirstResponseDuration.Value) : "—";
92105
var current = elapsed ?? FinalResponseDuration;
93106
var total = current.HasValue ? FormatDuration(current.Value) : "—";
94-
var suffix = FinalResponseDuration.HasValue ? "" : " ⏳";
95-
TimingText = $"{ts} · first: {first} · total: {total}{suffix}";
107+
IsStreaming = !FinalResponseDuration.HasValue;
108+
TimingText = $"{ts} · first: {first} · total: {total}";
96109
}
97110

98111
/// <summary>Sets timing text from captured durations (finalized).</summary>

src/McpServerManager.Core/Services/SpeechFilterService.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
namespace McpServerManager.Core.Services;
88

99
/// <summary>
10-
/// Manages a user-configurable list of filter words for text-to-speech output.
11-
/// Lines containing any filter word (case-insensitive substring match) are excluded from TTS.
10+
/// Manages a user-configurable list of filter phrases for text-to-speech output.
11+
/// Lines containing any filter phrase (case-insensitive substring match) are excluded from TTS.
1212
/// </summary>
1313
public sealed class SpeechFilterService
1414
{
@@ -38,7 +38,7 @@ private static string GetFilePath()
3838
}
3939

4040
/// <summary>
41-
/// Gets or sets the raw filter text (one word/phrase per line).
41+
/// Gets or sets the raw filter text (one phrase per line).
4242
/// Setting this value persists the list to disk.
4343
/// </summary>
4444
public string FilterText
@@ -62,7 +62,7 @@ public string FilterText
6262

6363
/// <summary>
6464
/// Returns true if the given line should be excluded from TTS because it contains
65-
/// one or more filter words (case-insensitive substring match).
65+
/// one or more filter phrases (case-insensitive substring match).
6666
/// </summary>
6767
public bool ShouldFilter(string line)
6868
{
@@ -81,7 +81,7 @@ public bool ShouldFilter(string line)
8181
return false;
8282
}
8383

84-
/// <summary>Returns a snapshot of the current filter words.</summary>
84+
/// <summary>Returns a snapshot of the current filter phrases.</summary>
8585
public IReadOnlyList<string> GetFilterWords()
8686
{
8787
lock (_lock)
@@ -102,12 +102,12 @@ private void Load()
102102
{
103103
_filterWords = ParseLines(text);
104104
}
105-
Logger.LogDebug("Loaded {Count} speech filter words", _filterWords.Count);
105+
Logger.LogDebug("Loaded {Count} speech filter phrases", _filterWords.Count);
106106
}
107107
}
108108
catch (Exception ex)
109109
{
110-
Logger.LogWarning(ex, "Failed to load speech filter words");
110+
Logger.LogWarning(ex, "Failed to load speech filter phrases");
111111
}
112112
}
113113

@@ -120,11 +120,11 @@ private void Save()
120120
{
121121
File.WriteAllText(path, string.Join(Environment.NewLine, _filterWords));
122122
}
123-
Logger.LogDebug("Saved {Count} speech filter words", _filterWords.Count);
123+
Logger.LogDebug("Saved {Count} speech filter phrases", _filterWords.Count);
124124
}
125125
catch (Exception ex)
126126
{
127-
Logger.LogWarning(ex, "Failed to save speech filter words");
127+
Logger.LogWarning(ex, "Failed to save speech filter phrases");
128128
}
129129
}
130130

src/McpServerManager.Core/ViewModels/SettingsViewModel.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace McpServerManager.Core.ViewModels;
66

7-
/// <summary>ViewModel for the Settings tab. Exposes speech filter word configuration.</summary>
7+
/// <summary>ViewModel for the Settings tab. Exposes speech filter phrase configuration.</summary>
88
public partial class SettingsViewModel : ViewModelBase
99
{
1010
private readonly SpeechFilterService _speechFilter = SpeechFilterService.Instance;
@@ -20,11 +20,19 @@ public SettingsViewModel()
2020
_speechFilterWords = _speechFilter.FilterText;
2121
}
2222

23-
/// <summary>Saves the current filter word list to disk.</summary>
23+
/// <summary>Saves the current filter phrase list to disk.</summary>
2424
[RelayCommand]
2525
private void SaveFilterWords()
2626
{
2727
_speechFilter.FilterText = SpeechFilterWords;
28-
StatusMessage = $"Saved {_speechFilter.GetFilterWords().Count} filter word(s).";
28+
StatusMessage = $"Saved {_speechFilter.GetFilterWords().Count} filter phrase(s).";
29+
}
30+
31+
/// <summary>Reverts the editor text to the last saved values.</summary>
32+
[RelayCommand]
33+
private void RevertFilterWords()
34+
{
35+
SpeechFilterWords = _speechFilter.FilterText;
36+
StatusMessage = "Reverted to saved values.";
2937
}
3038
}

src/McpServerManager.Desktop/Views/SettingsView.axaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414

1515
<!-- Speech Filter Section Header -->
1616
<StackPanel Grid.Row="1" Spacing="4" Margin="0,0,0,8">
17-
<TextBlock Text="Speech Filter Words" FontSize="16" FontWeight="SemiBold"/>
18-
<TextBlock Text="Lines containing any of these words (case-insensitive) will be excluded from text-to-speech. One word or phrase per line."
17+
<TextBlock Text="Speech Filter Phrases" FontSize="16" FontWeight="SemiBold"/>
18+
<TextBlock Text="Lines containing any of these phrases (case-insensitive) will be excluded from text-to-speech. One phrase per line."
1919
TextWrapping="Wrap" Opacity="0.7" FontSize="13"/>
2020
</StackPanel>
2121

@@ -24,7 +24,7 @@
2424
Text="{Binding SpeechFilterWords, Mode=TwoWay}"
2525
AcceptsReturn="True"
2626
TextWrapping="Wrap"
27-
Watermark="Enter filter words, one per line..."
27+
Watermark="Enter filter phrases, one per line..."
2828
FontFamily="Cascadia Code,Consolas,Menlo,monospace"
2929
FontSize="14"
3030
MinHeight="200"
@@ -33,6 +33,7 @@
3333
<!-- Save Button + Status -->
3434
<StackPanel Grid.Row="3" Orientation="Horizontal" Spacing="8" Margin="0,8,0,0">
3535
<Button Content="Save" Command="{Binding SaveFilterWordsCommand}" Padding="12,6"/>
36+
<Button Content="Revert" Command="{Binding RevertFilterWordsCommand}" Padding="12,6"/>
3637
<TextBlock Text="{Binding StatusMessage}" VerticalAlignment="Center" Opacity="0.7"/>
3738
</StackPanel>
3839

0 commit comments

Comments
 (0)