Skip to content

Commit caf90c2

Browse files
shanselmanCopilot
andcommitted
fix(tray): harden assistant bridge controls
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 45e028c commit caf90c2

9 files changed

Lines changed: 707 additions & 47 deletions

File tree

src/OpenClaw.Tray.WinUI/Pages/VoiceSettingsPage.xaml

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,28 +27,30 @@
2727
<ColumnDefinition Width="Auto"/>
2828
</Grid.ColumnDefinitions>
2929
<StackPanel Spacing="4">
30-
<TextBlock Text="Assistant" Style="{StaticResource BodyStrongTextBlockStyle}"/>
31-
<TextBlock Text="Always-listening assistant service and latest conversation turns."
32-
Style="{StaticResource CaptionTextBlockStyle}"
33-
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
34-
TextWrapping="Wrap"/>
30+
<TextBlock x:Uid="VoiceSettingsPage_AssistantHeader" Text="Assistant" Style="{StaticResource BodyStrongTextBlockStyle}"/>
31+
<TextBlock x:Uid="VoiceSettingsPage_AssistantDescription"
32+
Text="Always-listening assistant service and latest conversation turns."
33+
Style="{StaticResource CaptionTextBlockStyle}"
34+
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
35+
TextWrapping="Wrap"/>
3536
</StackPanel>
3637
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8" VerticalAlignment="Top">
37-
<Button x:Name="AssistantRefreshButton" Click="OnAssistantRefreshClick" Padding="10,8"
38+
<Button x:Uid="VoiceSettingsPage_AssistantRefreshButton"
39+
x:Name="AssistantRefreshButton" Click="OnAssistantRefreshClick" Padding="10,8"
3840
ToolTipService.ToolTip="Refresh assistant status">
3941
<FontIcon Glyph="&#xE72C;" FontSize="14"/>
4042
</Button>
4143
<Button x:Name="AssistantStartButton" Click="OnAssistantStartClick"
4244
Style="{StaticResource AccentButtonStyle}">
4345
<StackPanel Orientation="Horizontal" Spacing="6">
4446
<FontIcon Glyph="&#xE768;" FontSize="14"/>
45-
<TextBlock Text="Start"/>
47+
<TextBlock x:Uid="VoiceSettingsPage_AssistantStartButtonText" Text="Start"/>
4648
</StackPanel>
4749
</Button>
4850
<Button x:Name="AssistantStopButton" Click="OnAssistantStopClick">
4951
<StackPanel Orientation="Horizontal" Spacing="6">
5052
<FontIcon Glyph="&#xE71A;" FontSize="14"/>
51-
<TextBlock Text="Stop"/>
53+
<TextBlock x:Uid="VoiceSettingsPage_AssistantStopButtonText" Text="Stop"/>
5254
</StackPanel>
5355
</Button>
5456
</StackPanel>
@@ -62,7 +64,8 @@
6264
<RowDefinition Height="Auto"/>
6365
<RowDefinition Height="Auto"/>
6466
</Grid.RowDefinitions>
65-
<TextBlock x:Name="AssistantStatusText"
67+
<TextBlock x:Uid="VoiceSettingsPage_AssistantStatusChecking"
68+
x:Name="AssistantStatusText"
6669
Text="Checking assistant status..."
6770
Style="{StaticResource BodyStrongTextBlockStyle}"/>
6871
<TextBlock x:Name="AssistantDetailText" Grid.Row="1"
@@ -77,7 +80,7 @@
7780
</Border>
7881

7982
<StackPanel Spacing="8">
80-
<TextBlock Text="Recent turns" Style="{StaticResource BodyStrongTextBlockStyle}"/>
83+
<TextBlock x:Uid="VoiceSettingsPage_AssistantRecentTurnsHeader" Text="Recent turns" Style="{StaticResource BodyStrongTextBlockStyle}"/>
8184
<StackPanel x:Name="AssistantTurnsPanel" Spacing="8"/>
8285
</StackPanel>
8386
</StackPanel>

src/OpenClaw.Tray.WinUI/Pages/VoiceSettingsPage.xaml.cs

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public sealed partial class VoiceSettingsPage : Page
1919
private VoiceService? _voiceService;
2020
private AssistantBridgeService? _assistantBridgeService;
2121
private CancellationTokenSource? _assistantRequestCts;
22+
private int _assistantOperationVersion;
2223
private bool _suppressEvents = true; // suppress until Initialize/LoadSettings runs
2324
// Per-asset CTS so a Piper download doesn't cancel an in-flight Whisper
2425
// download (and vice versa). Each download type owns its own token.
@@ -88,15 +89,15 @@ private void OnAssistantStartClick(object sender, RoutedEventArgs e) =>
8889
private async Task OnAssistantStartClickAsync()
8990
{
9091
var bridge = EnsureAssistantBridge();
91-
SetAssistantBusy(true);
92-
AssistantStatusText.Text = "Starting assistant...";
93-
AssistantDetailText.Text = "Starting always-listening mode with local assistant routing and turn storage.";
92+
var busyVersion = BeginAssistantOperation();
93+
AssistantStatusText.Text = L("VoiceSettingsPage_AssistantStarting");
94+
AssistantDetailText.Text = L("VoiceSettingsPage_AssistantStartingDetail");
9495
try
9596
{
9697
var result = await bridge.StartListenServiceAsync(NewAssistantRequestToken());
9798
if (!result.Success)
9899
{
99-
AssistantStatusText.Text = "Assistant did not start";
100+
AssistantStatusText.Text = L("VoiceSettingsPage_AssistantStartFailed");
100101
AssistantDetailText.Text = result.ErrorMessage;
101102
return;
102103
}
@@ -108,7 +109,7 @@ private async Task OnAssistantStartClickAsync()
108109
}
109110
finally
110111
{
111-
SetAssistantBusy(false);
112+
EndAssistantOperation(busyVersion);
112113
}
113114
}
114115

@@ -121,15 +122,15 @@ private void OnAssistantStopClick(object sender, RoutedEventArgs e) =>
121122
private async Task OnAssistantStopClickAsync()
122123
{
123124
var bridge = EnsureAssistantBridge();
124-
SetAssistantBusy(true);
125-
AssistantStatusText.Text = "Stopping assistant...";
125+
var busyVersion = BeginAssistantOperation();
126+
AssistantStatusText.Text = L("VoiceSettingsPage_AssistantStopping");
126127
AssistantDetailText.Text = "";
127128
try
128129
{
129130
var result = await bridge.StopListenServiceAsync(NewAssistantRequestToken());
130131
if (!result.Success)
131132
{
132-
AssistantStatusText.Text = "Assistant did not stop";
133+
AssistantStatusText.Text = L("VoiceSettingsPage_AssistantStopFailed");
133134
AssistantDetailText.Text = result.ErrorMessage;
134135
return;
135136
}
@@ -141,14 +142,14 @@ private async Task OnAssistantStopClickAsync()
141142
}
142143
finally
143144
{
144-
SetAssistantBusy(false);
145+
EndAssistantOperation(busyVersion);
145146
}
146147
}
147148

148149
private async Task RefreshAssistantAsync()
149150
{
150151
var bridge = EnsureAssistantBridge();
151-
SetAssistantBusy(true);
152+
var busyVersion = BeginAssistantOperation();
152153
try
153154
{
154155
var snapshot = await bridge.GetStatusAsync(NewAssistantRequestToken());
@@ -159,7 +160,7 @@ private async Task RefreshAssistantAsync()
159160
}
160161
finally
161162
{
162-
SetAssistantBusy(false);
163+
EndAssistantOperation(busyVersion);
163164
}
164165
}
165166

@@ -188,17 +189,30 @@ private void SetAssistantBusy(bool busy)
188189
AssistantStopButton.IsEnabled = !busy;
189190
}
190191

192+
private int BeginAssistantOperation()
193+
{
194+
var version = Interlocked.Increment(ref _assistantOperationVersion);
195+
SetAssistantBusy(true);
196+
return version;
197+
}
198+
199+
private void EndAssistantOperation(int version)
200+
{
201+
if (Volatile.Read(ref _assistantOperationVersion) == version)
202+
SetAssistantBusy(false);
203+
}
204+
191205
private void RenderAssistantSnapshot(AssistantBridgeSnapshot snapshot)
192206
{
193207
if (!snapshot.IsAvailable)
194208
{
195-
AssistantStatusText.Text = "Assistant bridge unavailable";
209+
AssistantStatusText.Text = L("VoiceSettingsPage_AssistantBridgeUnavailable");
196210
AssistantDetailText.Text = snapshot.ErrorMessage;
197211
AssistantLastRefreshText.Text = "";
198212
AssistantTurnsPanel.Children.Clear();
199213
AssistantTurnsPanel.Children.Add(new TextBlock
200214
{
201-
Text = "No assistant turns loaded.",
215+
Text = L("VoiceSettingsPage_AssistantNoTurnsLoaded"),
202216
Style = (Style)Application.Current.Resources["CaptionTextBlockStyle"],
203217
Foreground = (Brush)Application.Current.Resources["TextFillColorSecondaryBrush"],
204218
TextWrapping = TextWrapping.Wrap
@@ -207,32 +221,33 @@ private void RenderAssistantSnapshot(AssistantBridgeSnapshot snapshot)
207221
}
208222

209223
var listen = snapshot.ListenService;
210-
var status = string.IsNullOrWhiteSpace(listen.Status) ? "unknown" : listen.Status;
224+
var unknown = L("VoiceSettingsPage_AssistantUnknown");
225+
var status = string.IsNullOrWhiteSpace(listen.Status) ? unknown : listen.Status;
211226
AssistantStatusText.Text = listen.IsRunning
212-
? $"Assistant listening (PID {listen.Pid?.ToString() ?? "unknown"})"
227+
? Lf("VoiceSettingsPage_AssistantListeningFormat", listen.Pid?.ToString(CultureInfo.CurrentCulture) ?? unknown)
213228
: listen.IsStopped
214-
? "Assistant stopped"
215-
: $"Assistant {status}";
229+
? L("VoiceSettingsPage_AssistantStopped")
230+
: Lf("VoiceSettingsPage_AssistantStatusFormat", status);
216231

217232
var details = new[]
218233
{
219-
string.IsNullOrWhiteSpace(listen.Transcriber) ? "" : $"Transcriber: {listen.Transcriber}",
220-
string.IsNullOrWhiteSpace(snapshot.PreferredInputDevice) ? "" : $"Mic: {snapshot.PreferredInputDevice}",
221-
string.IsNullOrWhiteSpace(snapshot.PreferredOutputDevice) ? "" : $"Speaker: {snapshot.PreferredOutputDevice}",
222-
listen.AllowCloud ? "Cloud routing: on" : "Cloud routing: off",
223-
listen.SpeakAloud ? "Speech output: on" : "Speech output: off"
234+
string.IsNullOrWhiteSpace(listen.Transcriber) ? "" : Lf("VoiceSettingsPage_AssistantTranscriberFormat", listen.Transcriber),
235+
string.IsNullOrWhiteSpace(snapshot.PreferredInputDevice) ? "" : Lf("VoiceSettingsPage_AssistantMicFormat", snapshot.PreferredInputDevice),
236+
string.IsNullOrWhiteSpace(snapshot.PreferredOutputDevice) ? "" : Lf("VoiceSettingsPage_AssistantSpeakerFormat", snapshot.PreferredOutputDevice),
237+
Lf("VoiceSettingsPage_AssistantCloudRoutingFormat", listen.AllowCloud ? L("VoiceSettingsPage_AssistantOn") : L("VoiceSettingsPage_AssistantOff")),
238+
Lf("VoiceSettingsPage_AssistantSpeechOutputFormat", listen.SpeakAloud ? L("VoiceSettingsPage_AssistantOn") : L("VoiceSettingsPage_AssistantOff"))
224239
}.Where(s => !string.IsNullOrWhiteSpace(s));
225240
AssistantDetailText.Text = string.Join(" | ", details);
226241
AssistantLastRefreshText.Text = string.IsNullOrWhiteSpace(snapshot.GeneratedAt)
227242
? ""
228-
: $"Last bridge update: {snapshot.GeneratedAt}";
243+
: Lf("VoiceSettingsPage_AssistantLastBridgeUpdateFormat", snapshot.GeneratedAt);
229244

230245
AssistantTurnsPanel.Children.Clear();
231246
if (snapshot.RecentTurns.Count == 0)
232247
{
233248
AssistantTurnsPanel.Children.Add(new TextBlock
234249
{
235-
Text = "No conversation turns yet.",
250+
Text = L("VoiceSettingsPage_AssistantNoConversationTurns"),
236251
Style = (Style)Application.Current.Resources["CaptionTextBlockStyle"],
237252
Foreground = (Brush)Application.Current.Resources["TextFillColorSecondaryBrush"],
238253
TextWrapping = TextWrapping.Wrap
@@ -247,26 +262,29 @@ private void RenderAssistantSnapshot(AssistantBridgeSnapshot snapshot)
247262
private static Border CreateAssistantTurnView(AssistantTurnSnapshot turn)
248263
{
249264
var panel = new StackPanel { Spacing = 6 };
250-
var source = string.IsNullOrWhiteSpace(turn.Source) ? "assistant" : turn.Source;
265+
var source = string.IsNullOrWhiteSpace(turn.Source) ? L("VoiceSettingsPage_AssistantSourceDefault") : turn.Source;
266+
var stage = string.IsNullOrWhiteSpace(turn.Stage) ? L("VoiceSettingsPage_AssistantUnknown") : turn.Stage;
251267
var model = string.IsNullOrWhiteSpace(turn.Provider)
252268
? turn.ModelProfile
253269
: $"{turn.Provider} {turn.ModelProfile}".Trim();
254-
var latency = turn.TotalMs is int ms ? $" | {ms} ms" : "";
270+
var metadata = turn.TotalMs is int ms
271+
? Lf("VoiceSettingsPage_AssistantTurnMetadataWithLatencyFormat", source, stage, ms.ToString(CultureInfo.CurrentCulture))
272+
: Lf("VoiceSettingsPage_AssistantTurnMetadataFormat", source, stage);
255273
panel.Children.Add(new TextBlock
256274
{
257-
Text = $"{source} | {turn.Stage}{latency}".Trim(' ', '|'),
275+
Text = metadata,
258276
Style = (Style)Application.Current.Resources["CaptionTextBlockStyle"],
259277
Foreground = (Brush)Application.Current.Resources["TextFillColorSecondaryBrush"],
260278
TextWrapping = TextWrapping.Wrap
261279
});
262280
panel.Children.Add(new TextBlock
263281
{
264-
Text = $"You: {TrimForDisplay(turn.InputText)}",
282+
Text = Lf("VoiceSettingsPage_AssistantUserTurnFormat", TrimForDisplay(turn.InputText)),
265283
TextWrapping = TextWrapping.Wrap
266284
});
267285
panel.Children.Add(new TextBlock
268286
{
269-
Text = $"OpenClaw: {TrimForDisplay(turn.ResponseText)}",
287+
Text = Lf("VoiceSettingsPage_AssistantResponseTurnFormat", TrimForDisplay(turn.ResponseText)),
270288
TextWrapping = TextWrapping.Wrap
271289
});
272290
if (!string.IsNullOrWhiteSpace(model))
@@ -292,7 +310,7 @@ private static Border CreateAssistantTurnView(AssistantTurnSnapshot turn)
292310
private static string TrimForDisplay(string value)
293311
{
294312
if (string.IsNullOrWhiteSpace(value))
295-
return "(empty)";
313+
return L("VoiceSettingsPage_AssistantEmptyValue");
296314
value = value.Trim();
297315
return value.Length <= 220 ? value : value[..217] + "...";
298316
}

0 commit comments

Comments
 (0)