@@ -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