21 files changed
+402
-107
android/app/build.gradle.kts
+4
@@ -63,6 +63,10 @@ android {
63
signingConfigs.getByName("debug")
64
isMinifyEnabled = true
65
isShrinkResources = true
66
+ proguardFiles(
67
+ getDefaultProguardFile("proguard-android-optimize.txt"),
68
+ "proguard-rules.pro"
69
+ )
70
}
71
}
72
}
android/app/proguard-rules.pro
new
+4
@@ -0,0 +1,4 @@
1
+# Proguard rules for R8 / Proguard
2
+-dontwarn com.google.auto.value.extension.memoized.Memoized
3
+-dontwarn com.google.mediapipe.proto.CalculatorProfileProto$CalculatorProfile
4
+-dontwarn com.google.mediapipe.proto.GraphTemplateProto$CalculatorGraphTemplate
lib/features/chat/views/components/chat_bubble.dart
+97
-14
@@ -9,6 +9,7 @@ import 'message_action_bar.dart';
9
import 'processing_indicator.dart';
10
import 'typing_indicator.dart';
11
import 'reasoning_widget.dart';
12
+import '../../data/tools/tool_definition.dart';
13
import '../../data/tools/tool_event.dart';
14
15
class ChatBubble extends StatelessWidget {
@@ -134,7 +135,12 @@ class _UserBubble extends StatelessWidget {
135
constraints: BoxConstraints(
136
maxWidth: MediaQuery.of(context).size.width * 0.75,
137
),
137
- margin: const EdgeInsetsDirectional.only(start: 48, end: 8, top: 4, bottom: 2),
138
+ margin: const EdgeInsetsDirectional.only(
139
+ start: 48,
140
+ end: 8,
141
+ top: 4,
142
+ bottom: 2,
143
+ ),
144
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
145
decoration: BoxDecoration(
146
color: isDark ? const Color(0xFF3B82F6) : const Color(0xFF2563EB),
@@ -161,7 +167,11 @@ class _UserBubble extends StatelessWidget {
167
data: message.content,
168
selectable: true,
169
styleSheet: MarkdownStyleSheet(
164
- p: TextStyle(color: Colors.white, fontSize: 15, height: 1.4),
170
+ p: TextStyle(
171
+ color: Colors.white,
172
+ fontSize: 15,
173
+ height: 1.4,
174
+ ),
175
),
176
shrinkWrap: true,
177
),
@@ -368,7 +378,9 @@ class _MarkdownContent extends StatelessWidget {
378
),
379
),
380
),
371
- blockquotePadding: EdgeInsetsDirectional.only(start: 16).resolve(Directionality.of(context)),
381
+ blockquotePadding: EdgeInsetsDirectional.only(
382
+ start: 16,
383
+ ).resolve(Directionality.of(context)),
384
listBullet: TextStyle(color: isDark ? Colors.white : Colors.black),
385
tableHead: TextStyle(
386
color: isDark ? Colors.white : Colors.black,
@@ -463,7 +475,12 @@ class _ToolBubble extends StatelessWidget {
475
constraints: BoxConstraints(
476
maxWidth: MediaQuery.of(context).size.width * 0.8,
477
),
466
- margin: const EdgeInsetsDirectional.only(start: 8, end: 48, top: 4, bottom: 4),
478
+ margin: const EdgeInsetsDirectional.only(
479
+ start: 8,
480
+ end: 48,
481
+ top: 4,
482
+ bottom: 4,
483
+ ),
484
padding: const EdgeInsets.all(12),
485
decoration: BoxDecoration(
486
color: isDark ? const Color(0xFF1F2937) : const Color(0xFFEEF2FF),
@@ -539,19 +556,34 @@ class _ToolTimeline extends StatelessWidget {
556
padding: const EdgeInsets.fromLTRB(10, 8, 10, 4),
557
child: Row(
558
children: [
542
- Icon(Icons.troubleshoot, size: 14, color: isDark ? const Color(0xFF94A3B8) : const Color(0xFF64748B)),
559
+ Icon(
560
+ Icons.troubleshoot,
561
+ size: 14,
562
+ color: isDark
563
+ ? const Color(0xFF94A3B8)
564
+ : const Color(0xFF64748B),
565
+ ),
566
const SizedBox(width: 4),
544
- Text('Tools', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: isDark ? const Color(0xFF94A3B8) : const Color(0xFF64748B))),
567
+ Text(
568
+ 'Tools',
569
+ style: TextStyle(
570
+ fontSize: 11,
571
+ fontWeight: FontWeight.w600,
572
+ color: isDark
573
+ ? const Color(0xFF94A3B8)
574
+ : const Color(0xFF64748B),
575
+ ),
576
+ ),
577
],
578
),
579
),
548
- ...events.map((event) => _buildEventRow(event, isDark)),
580
+ ...events.map((event) => _buildEventRow(context, event, isDark)),
581
],
582
),
583
);
584
}
585
554
- Widget _buildEventRow(ToolEvent event, bool isDark) {
586
+ Widget _buildEventRow(BuildContext context, ToolEvent event, bool isDark) {
587
final iconData = switch (event.status) {
588
ToolEventStatus.requested => Icons.hourglass_empty,
589
ToolEventStatus.approved => Icons.check_circle_outline,
@@ -562,8 +594,10 @@ class _ToolTimeline extends StatelessWidget {
594
};
595
final iconColor = switch (event.status) {
596
ToolEventStatus.completed => const Color(0xFF22C55E),
565
- ToolEventStatus.failed || ToolEventStatus.rejected => const Color(0xFFEF4444),
566
- ToolEventStatus.running || ToolEventStatus.requested => const Color(0xFFF59E0B),
597
+ ToolEventStatus.failed ||
598
+ ToolEventStatus.rejected => const Color(0xFFEF4444),
599
+ ToolEventStatus.running ||
600
+ ToolEventStatus.requested => const Color(0xFFF59E0B),
601
ToolEventStatus.approved => const Color(0xFF3B82F6),
602
};
603
final label = switch (event.status) {
@@ -574,6 +608,8 @@ class _ToolTimeline extends StatelessWidget {
608
ToolEventStatus.completed => 'Done',
609
ToolEventStatus.failed => 'Failed',
610
};
611
+ final l10n = AppLocalizations.of(context)!;
612
+ final isMcpTool = event.providerType == ToolProviderType.mcp;
613
614
return Padding(
615
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
@@ -604,9 +640,22 @@ class _ToolTimeline extends StatelessWidget {
640
),
641
),
642
const SizedBox(width: 4),
643
+ if (isMcpTool) ...[
644
+ _InlineToolBadge(label: l10n.beta_label, isDark: isDark),
645
+ const SizedBox(width: 4),
646
+ _InlineToolBadge(
647
+ label: l10n.experimental_label,
648
+ isDark: isDark,
649
+ ),
650
+ const SizedBox(width: 4),
651
+ ],
652
Text(
653
label,
609
- style: TextStyle(fontSize: 10, color: iconColor, fontWeight: FontWeight.w500),
654
+ style: TextStyle(
655
+ fontSize: 10,
656
+ color: iconColor,
657
+ fontWeight: FontWeight.w500,
658
+ ),
659
),
660
],
661
),
@@ -617,7 +666,9 @@ class _ToolTimeline extends StatelessWidget {
666
_formatArgs(event.arguments!),
667
style: TextStyle(
668
fontSize: 10,
620
- color: isDark ? const Color(0xFF94A3B8) : const Color(0xFF64748B),
669
+ color: isDark
670
+ ? const Color(0xFF94A3B8)
671
+ : const Color(0xFF64748B),
672
fontFamily: 'monospace',
673
),
674
maxLines: 3,
@@ -631,7 +682,9 @@ class _ToolTimeline extends StatelessWidget {
682
'→ ${event.result}',
683
style: TextStyle(
684
fontSize: 10,
634
- color: isDark ? const Color(0xFF86EFAC) : const Color(0xFF166534),
685
+ color: isDark
686
+ ? const Color(0xFF86EFAC)
687
+ : const Color(0xFF166534),
688
fontFamily: 'monospace',
689
),
690
maxLines: 2,
@@ -659,7 +712,9 @@ class _ToolTimeline extends StatelessWidget {
712
'${event.durationMs}ms',
713
style: TextStyle(
714
fontSize: 9,
662
- color: isDark ? const Color(0xFF64748B) : const Color(0xFF94A3B8),
715
+ color: isDark
716
+ ? const Color(0xFF64748B)
717
+ : const Color(0xFF94A3B8),
718
),
719
),
720
),
@@ -676,6 +731,34 @@ class _ToolTimeline extends StatelessWidget {
731
}
732
}
733
734
+class _InlineToolBadge extends StatelessWidget {
735
+ const _InlineToolBadge({required this.label, required this.isDark});
736
+
737
+ final String label;
738
+ final bool isDark;
739
+
740
+ @override
741
+ Widget build(BuildContext context) {
742
+ return Container(
743
+ padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
744
+ decoration: BoxDecoration(
745
+ color: Colors.amber.withValues(alpha: isDark ? 0.18 : 0.14),
746
+ borderRadius: BorderRadius.circular(999),
747
+ border: Border.all(color: Colors.amber.withValues(alpha: 0.45)),
748
+ ),
749
+ child: Text(
750
+ label.toUpperCase(),
751
+ style: const TextStyle(
752
+ color: Color(0xFFB45309),
753
+ fontSize: 8,
754
+ fontWeight: FontWeight.w700,
755
+ letterSpacing: 0.35,
756
+ ),
757
+ ),
758
+ );
759
+ }
760
+}
761
+
762
class _AttachmentList extends StatelessWidget {
763
const _AttachmentList({required this.paths, required this.isUser});
764
lib/features/chat/views/components/chat_settings_sheet.dart
+113
-36
@@ -41,7 +41,8 @@ class _ChatSettingsSheetState extends ConsumerState<ChatSettingsSheet> {
41
final maxTokens = activeConv?.maxTokens ?? settings.maxTokens;
42
final contextLength = activeConv?.contextLength ?? settings.contextLength;
43
44
- final hasOverrides = activeConv?.temperature != null ||
44
+ final hasOverrides =
45
+ activeConv?.temperature != null ||
46
activeConv?.topP != null ||
47
activeConv?.maxTokens != null ||
48
activeConv?.contextLength != null;
@@ -116,7 +117,10 @@ class _ChatSettingsSheetState extends ConsumerState<ChatSettingsSheet> {
117
),
118
child: Row(
119
children: [
119
- const HugeIcon(icon: HugeIcons.strokeRoundedSlidersHorizontal, size: 16),
120
+ const HugeIcon(
121
+ icon: HugeIcons.strokeRoundedSlidersHorizontal,
122
+ size: 16,
123
+ ),
124
const SizedBox(width: 8),
125
Text(l10n.parameters_tab),
126
],
@@ -133,9 +137,14 @@ class _ChatSettingsSheetState extends ConsumerState<ChatSettingsSheet> {
137
),
138
child: Row(
139
children: [
136
- const HugeIcon(icon: HugeIcons.strokeRoundedPuzzle, size: 16),
140
+ const HugeIcon(
141
+ icon: HugeIcons.strokeRoundedPuzzle,
142
+ size: 16,
143
+ ),
144
const SizedBox(width: 8),
145
Text(l10n.mcp_tab),
146
+ const SizedBox(width: 6),
147
+ _McpBadge(label: l10n.beta_label, isDark: isDark),
148
],
149
),
150
),
@@ -190,7 +199,8 @@ class _ChatSettingsSheetState extends ConsumerState<ChatSettingsSheet> {
199
label: l10n.max_tokens,
200
value: maxTokens,
201
description: l10n.max_tokens_desc,
193
- onChanged: (v) => _updateParam(ref, conversationId, maxTokens: v),
202
+ onChanged: (v) =>
203
+ _updateParam(ref, conversationId, maxTokens: v),
204
isDark: isDark,
205
),
206
),
@@ -200,7 +210,8 @@ class _ChatSettingsSheetState extends ConsumerState<ChatSettingsSheet> {
210
label: l10n.context_length,
211
value: contextLength,
212
description: l10n.context_length_desc,
203
- onChanged: (v) => _updateParam(ref, conversationId, contextLength: v),
213
+ onChanged: (v) =>
214
+ _updateParam(ref, conversationId, contextLength: v),
215
isDark: isDark,
216
),
217
),
@@ -235,12 +246,19 @@ class _ChatSettingsSheetState extends ConsumerState<ChatSettingsSheet> {
246
),
247
child: Row(
248
children: [
238
- const Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 20),
249
+ const Icon(
250
+ Icons.warning_amber_rounded,
251
+ color: Colors.orange,
252
+ size: 20,
253
+ ),
254
const SizedBox(width: 12),
255
Expanded(
256
child: Text(
257
l10n.mcp_disabled_warning,
243
- style: const TextStyle(fontSize: 13, color: Colors.orange),
258
+ style: const TextStyle(
259
+ fontSize: 13,
260
+ color: Colors.orange,
261
+ ),
262
),
263
),
264
],
@@ -250,7 +268,9 @@ class _ChatSettingsSheetState extends ConsumerState<ChatSettingsSheet> {
268
value: mcpConfig.enabled,
269
onChanged: isGloballyEnabled
270
? (v) {
253
- final convId = ref.read(conv.activeConversationProvider)?.id;
271
+ final convId = ref
272
+ .read(conv.activeConversationProvider)
273
+ ?.id;
274
if (convId != null) {
275
ref
276
.read(chatMcpConfigProvider.notifier)
@@ -260,7 +280,16 @@ class _ChatSettingsSheetState extends ConsumerState<ChatSettingsSheet> {
280
}
281
}
282
: null,
263
- label: Text(l10n.mcp_enable_chat),
283
+ label: Wrap(
284
+ spacing: 6,
285
+ runSpacing: 4,
286
+ crossAxisAlignment: WrapCrossAlignment.center,
287
+ children: [
288
+ Text(l10n.mcp_enable_chat),
289
+ _McpBadge(label: l10n.beta_label, isDark: isDark),
290
+ _McpBadge(label: l10n.experimental_label, isDark: isDark),
291
+ ],
292
+ ),
293
),
294
const SizedBox(height: 16),
295
if (mcpConfig.enabled && isGloballyEnabled) ...[
@@ -268,13 +297,18 @@ class _ChatSettingsSheetState extends ConsumerState<ChatSettingsSheet> {
297
value: mcpConfig.autoExecuteTools,
298
onChanged: (v) =>
299
ref.read(chatMcpConfigProvider.notifier).toggleAutoExecute(),
271
- label: Text(l10n.auto_execute_tools),
300
+ label: Wrap(
301
+ spacing: 6,
302
+ runSpacing: 4,
303
+ crossAxisAlignment: WrapCrossAlignment.center,
304
+ children: [
305
+ Text(l10n.auto_execute_tools),
306
+ _McpBadge(label: l10n.experimental_label, isDark: isDark),
307
+ ],
308
+ ),
309
),
310
const SizedBox(height: 24),
274
- Text(
275
- l10n.add_ephemeral_mcp,
276
- style: theme.textTheme.list,
277
- ),
311
+ Text(l10n.add_ephemeral_mcp, style: theme.textTheme.list),
312
const SizedBox(height: 12),
313
Row(
314
children: [
@@ -302,10 +336,7 @@ class _ChatSettingsSheetState extends ConsumerState<ChatSettingsSheet> {
336
),
337
const SizedBox(height: 24),
338
if (mcpConfig.integrations.isNotEmpty) ...[
305
- Text(
306
- l10n.active_integrations,
307
- style: theme.textTheme.list,
308
- ),
339
+ Text(l10n.active_integrations, style: theme.textTheme.list),
340
const SizedBox(height: 12),
341
ConstrainedBox(
342
constraints: const BoxConstraints(maxHeight: 200),
@@ -317,15 +348,22 @@ class _ChatSettingsSheetState extends ConsumerState<ChatSettingsSheet> {
348
return Container(
349
margin: const EdgeInsets.only(bottom: 8),
350
decoration: BoxDecoration(
320
- color: isDark ? const Color(0xFF1E1E1E) : const Color(0xFFF9FAFB),
351
+ color: isDark
352
+ ? const Color(0xFF1E1E1E)
353
+ : const Color(0xFFF9FAFB),
354
borderRadius: BorderRadius.circular(12),
355
border: Border.all(
323
- color: isDark ? const Color(0xFF2A2A2A) : const Color(0xFFE5E7EB),
356
+ color: isDark
357
+ ? const Color(0xFF2A2A2A)
358
+ : const Color(0xFFE5E7EB),
359
),
360
),
361
child: ListTile(
362
dense: true,
328
- leading: const HugeIcon(icon: HugeIcons.strokeRoundedPuzzle, size: 18),
363
+ leading: const HugeIcon(
364
+ icon: HugeIcons.strokeRoundedPuzzle,
365
+ size: 18,
366
+ ),
367
title: Text(
368
integration.serverLabel ?? integration.pluginId ?? '',
369
style: const TextStyle(fontWeight: FontWeight.w600),
@@ -335,7 +373,11 @@ class _ChatSettingsSheetState extends ConsumerState<ChatSettingsSheet> {
373
style: const TextStyle(fontSize: 11),
374
),
375
trailing: ShadIconButton.ghost(
338
- icon: const Icon(Icons.delete_outline, size: 18, color: Colors.red),
376
+ icon: const Icon(
377
+ Icons.delete_outline,
378
+ size: 18,
379
+ color: Colors.red,
380
+ ),
381
onPressed: () => ref
382
.read(chatMcpConfigProvider.notifier)
383
.removeIntegration(index),
@@ -387,23 +429,55 @@ class _ChatSettingsSheetState extends ConsumerState<ChatSettingsSheet> {
429
return;
430
}
431
390
- ref.read(conv.conversationsProvider.notifier).updateChatParams(
391
- conversationId,
392
- temperature: temperature,
393
- topP: topP,
394
- maxTokens: maxTokens,
395
- contextLength: contextLength,
396
- );
432
+ ref
433
+ .read(conv.conversationsProvider.notifier)
434
+ .updateChatParams(
435
+ conversationId,
436
+ temperature: temperature,
437
+ topP: topP,
438
+ maxTokens: maxTokens,
439
+ contextLength: contextLength,
440
+ );
441
}
442
443
void _resetToDefaults(WidgetRef ref, String? conversationId) {
444
if (conversationId == null) return;
401
- ref.read(conv.conversationsProvider.notifier).updateChatParams(
402
- conversationId,
403
- clearTemperature: true,
404
- clearTopP: true,
405
- clearMaxTokens: true,
406
- clearContextLength: true,
445
+ ref
446
+ .read(conv.conversationsProvider.notifier)
447
+ .updateChatParams(
448
+ conversationId,
449
+ clearTemperature: true,
450
+ clearTopP: true,
451
+ clearMaxTokens: true,
452
+ clearContextLength: true,
453
+ );
454
+ }
455
+}
456
+
457
+class _McpBadge extends StatelessWidget {
458
+ const _McpBadge({required this.label, required this.isDark});
459
+
460
+ final String label;
461
+ final bool isDark;
462
+
463
+ @override
464
+ Widget build(BuildContext context) {
465
+ return Container(
466
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
467
+ decoration: BoxDecoration(
468
+ color: Colors.amber.withValues(alpha: isDark ? 0.18 : 0.14),
469
+ borderRadius: BorderRadius.circular(999),
470
+ border: Border.all(color: Colors.amber.withValues(alpha: 0.45)),
471
+ ),
472
+ child: Text(
473
+ label.toUpperCase(),
474
+ style: const TextStyle(
475
+ color: Color(0xFFB45309),
476
+ fontSize: 9,
477
+ fontWeight: FontWeight.w700,
478
+ letterSpacing: 0.4,
479
+ ),
480
+ ),
481
);
482
}
483
}
@@ -516,7 +590,10 @@ class _ParamInput extends StatelessWidget {
590
}
591
}
592
519
-void showChatSettingsSheet(BuildContext context, {String initialTab = 'parameters'}) {
593
+void showChatSettingsSheet(
594
+ BuildContext context, {
595
+ String initialTab = 'parameters',
596
+}) {
597
showModalBottomSheet(
598
context: context,
599
isScrollControlled: true,
lib/features/settings/views/settings_screen.dart
+100
-51
@@ -147,6 +147,10 @@ class SettingsViews extends ConsumerWidget {
147
),
148
_ToggleSetting(
149
label: l10n.enable_mcp,
150
+ badges: [
151
+ _FeatureBadge(label: l10n.beta_label, isDark: isDark),
152
+ _FeatureBadge(label: l10n.experimental_label, isDark: isDark),
153
+ ],
154
value: settings.mcpEnabled,
155
onChanged: (v) =>
156
ref.read(settingsProvider.notifier).setMcpEnabled(v),
@@ -155,6 +159,7 @@ class SettingsViews extends ConsumerWidget {
159
if (settings.mcpEnabled)
160
_ToggleSetting(
161
label: l10n.new_chat_mcp_default,
162
+ badges: [_FeatureBadge(label: l10n.beta_label, isDark: isDark)],
163
value: settings.newChatMcpEnabled,
164
onChanged: (v) =>
165
ref.read(settingsProvider.notifier).setNewChatMcpEnabled(v),
@@ -356,8 +361,7 @@ class _LanguageSetting extends StatelessWidget {
361
isExpanded: true,
362
padding: const EdgeInsets.symmetric(horizontal: 12),
363
borderRadius: BorderRadius.circular(8),
359
- dropdownColor:
360
- isDark ? const Color(0xFF1F1F1F) : Colors.white,
364
+ dropdownColor: isDark ? const Color(0xFF1F1F1F) : Colors.white,
365
items: [
366
DropdownMenuItem(
367
value: null,
@@ -381,49 +385,51 @@ class _LanguageSetting extends StatelessWidget {
385
],
386
),
387
),
384
- ..._localeItems.map((item) => DropdownMenuItem(
385
- value: item.$1,
386
- child: Row(
387
- children: [
388
- Container(
389
- width: 24,
390
- height: 16,
391
- decoration: BoxDecoration(
392
- borderRadius: BorderRadius.circular(2),
393
- border: Border.all(
394
- color: isDark
395
- ? const Color(0xFF444444)
396
- : const Color(0xFFDDDDDD),
397
- width: 0.5,
388
+ ..._localeItems.map(
389
+ (item) => DropdownMenuItem(
390
+ value: item.$1,
391
+ child: Row(
392
+ children: [
393
+ Container(
394
+ width: 24,
395
+ height: 16,
396
+ decoration: BoxDecoration(
397
+ borderRadius: BorderRadius.circular(2),
398
+ border: Border.all(
399
+ color: isDark
400
+ ? const Color(0xFF444444)
401
+ : const Color(0xFFDDDDDD),
402
+ width: 0.5,
403
+ ),
404
),
399
- ),
400
- child: ClipRRect(
401
- borderRadius: BorderRadius.circular(2),
402
- child: Image.asset(
403
- item.$3,
404
- width: 24,
405
- height: 16,
406
- fit: BoxFit.cover,
407
- errorBuilder: (context, error, stackTrace) {
408
- return Text(
409
- item.$4,
410
- style: const TextStyle(fontSize: 12),
411
- );
412
- },
405
+ child: ClipRRect(
406
+ borderRadius: BorderRadius.circular(2),
407
+ child: Image.asset(
408
+ item.$3,
409
+ width: 24,
410
+ height: 16,
411
+ fit: BoxFit.cover,
412
+ errorBuilder: (context, error, stackTrace) {
413
+ return Text(
414
+ item.$4,
415
+ style: const TextStyle(fontSize: 12),
416
+ );
417
+ },
418
+ ),
419
),
420
),
415
- ),
416
- const SizedBox(width: 10),
417
- Text(
418
- item.$2,
419
- style: TextStyle(
420
- fontSize: 14,
421
- color: isDark ? Colors.white : Colors.black,
421
+ const SizedBox(width: 10),
422
+ Text(
423
+ item.$2,
424
+ style: TextStyle(
425
+ fontSize: 14,
426
+ color: isDark ? Colors.white : Colors.black,
427
+ ),
428
),
423
- ),
424
- ],
429
+ ],
430
+ ),
431
),
426
- )),
432
+ ),
433
],
434
onChanged: onChanged,
435
),
@@ -645,22 +651,32 @@ class _ToggleSetting extends StatelessWidget {
651
required this.value,
652
required this.onChanged,
653
required this.isDark,
654
+ this.badges = const [],
655
});
656
657
final String label;
658
final bool value;
659
final ValueChanged<bool> onChanged;
660
final bool isDark;
661
+ final List<Widget> badges;
662
663
@override
664
Widget build(BuildContext context) {
665
return SwitchListTile(
658
- title: Text(
659
- label,
660
- style: TextStyle(
661
- fontSize: 14,
662
- color: isDark ? Colors.white : Colors.black,
663
- ),
666
+ title: Wrap(
667
+ spacing: 6,
668
+ runSpacing: 4,
669
+ crossAxisAlignment: WrapCrossAlignment.center,
670
+ children: [
671
+ Text(
672
+ label,
673
+ style: TextStyle(
674
+ fontSize: 14,
675
+ color: isDark ? Colors.white : Colors.black,
676
+ ),
677
+ ),
678
+ ...badges,
679
+ ],
680
),
681
value: value,
682
onChanged: onChanged,
@@ -670,6 +686,34 @@ class _ToggleSetting extends StatelessWidget {
686
}
687
}
688
689
+class _FeatureBadge extends StatelessWidget {
690
+ const _FeatureBadge({required this.label, required this.isDark});
691
+
692
+ final String label;
693
+ final bool isDark;
694
+
695
+ @override
696
+ Widget build(BuildContext context) {
697
+ return Container(
698
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
699
+ decoration: BoxDecoration(
700
+ color: Colors.amber.withValues(alpha: isDark ? 0.18 : 0.14),
701
+ borderRadius: BorderRadius.circular(999),
702
+ border: Border.all(color: Colors.amber.withValues(alpha: 0.45)),
703
+ ),
704
+ child: Text(
705
+ label.toUpperCase(),
706
+ style: const TextStyle(
707
+ color: Color(0xFFB45309),
708
+ fontSize: 9,
709
+ fontWeight: FontWeight.w700,
710
+ letterSpacing: 0.4,
711
+ ),
712
+ ),
713
+ );
714
+ }
715
+}
716
+
717
class _ThemeToggle extends StatelessWidget {
718
const _ThemeToggle({required this.current, required this.ref});
719
final ThemeMode current;
@@ -1024,9 +1068,9 @@ class _DangerousAction extends StatelessWidget {
1068
onPressed: () {
1069
Navigator.pop(ctx);
1070
onConfirm();
1027
- ScaffoldMessenger.of(
1028
- context,
1029
- ).showSnackBar(SnackBar(content: Text(l10n.label_completed(label))));
1071
+ ScaffoldMessenger.of(context).showSnackBar(
1072
+ SnackBar(content: Text(l10n.label_completed(label))),
1073
+ );
1074
},
1075
style: TextButton.styleFrom(foregroundColor: Colors.red),
1076
child: Text(dlgL10n.confirm),
@@ -1400,10 +1444,15 @@ class _OnDeviceOnDeviceEngineStatus extends ConsumerWidget {
1444
Expanded(
1445
child: Text(
1446
engineState.status == OnDeviceEngineStatus.loaded
1403
- ? l10n.model_loaded(engineState.loadedModelId ?? "unknown", engineState.backend?.name ?? "CPU")
1447
+ ? l10n.model_loaded(
1448
+ engineState.loadedModelId ?? "unknown",
1449
+ engineState.backend?.name ?? "CPU",
1450
+ )
1451
: engineState.status == OnDeviceEngineStatus.loading
1452
? l10n.loading
1406
- : l10n.error_with_message(engineState.error ?? l10n.unknown_error),
1453
+ : l10n.error_with_message(
1454
+ engineState.error ?? l10n.unknown_error,
1455
+ ),
1456
style: TextStyle(
1457
fontSize: 13,
1458
color: engineState.status == OnDeviceEngineStatus.loaded
lib/l10n/app_ar.arb
+4
@@ -421,6 +421,10 @@
421
422
"auto_execute_tools": "تشغيل الأدوات تلقائياً",
423
424
+ "beta_label": "تجريبي",
425
+
426
+ "experimental_label": "اختباري",
427
+
428
"add_ephemeral_mcp": "إضافة خادم MCP مؤقت",
429
430
"mcp_label_placeholder": "التسمية",
lib/l10n/app_bn.arb
+3
-1
@@ -23,6 +23,8 @@
23
"auto_execute_tools": "স্বয়ংক্রিয়ভাবে টুলস নির্বাহ করুন",
24
"auto_generate_titles": "স্বয়ংক্রিয় শিরোনাম তৈরি করুন",
25
"available": "উপলব্ধ",
26
+ "beta_label": "বেটা",
27
+ "experimental_label": "পরীক্ষামূলক",
28
"available_models": "উপলব্ধ মডেলসমূহ",
29
"available_ram": "উপলব্ধ র্যাম (RAM)",
30
"backend_cpu_desc": "সব ডিভাইসে কাজ করে। সবচেয়ে বেশি সামঞ্জস্যপূর্ণ।",
@@ -496,4 +498,4 @@
498
"voice_ryan": "Ryan (US)",
499
"waiting_selection": "নির্বাচনের জন্য অপেক্ষা করা হচ্ছে",
500
"yesterday_section": "গতকাল"
499
-}
\ No newline at end of file
501
+}
lib/l10n/app_en.arb
+10
@@ -1147,6 +1147,16 @@
1147
"description": "Toggle for auto-executing tools"
1148
},
1149
1150
+ "beta_label": "Beta",
1151
+ "@beta_label": {
1152
+ "description": "Badge label for beta features"
1153
+ },
1154
+
1155
+ "experimental_label": "Experimental",
1156
+ "@experimental_label": {
1157
+ "description": "Badge label for experimental features"
1158
+ },
1159
+
1160
"add_ephemeral_mcp": "Add Ephemeral MCP Server",
1161
"@add_ephemeral_mcp": {
1162
"description": "Section label for adding temp MCP server"
lib/l10n/app_es.arb
+3
-1
@@ -23,6 +23,8 @@
23
"auto_execute_tools": "Ejecutar herramientas automáticamente",
24
"auto_generate_titles": "Autogenerar títulos",
25
"available": "Disponible",
26
+ "beta_label": "Beta",
27
+ "experimental_label": "Experimental",
28
"available_models": "Modelos disponibles",
29
"available_ram": "RAM disponible",
30
"backend_cpu_desc": "Funciona en todos los dispositivos. Más compatible.",
@@ -496,4 +498,4 @@
498
"voice_ryan": "Ryan (US)",
499
"waiting_selection": "ESPERANDO SELECCIÓN",
500
"yesterday_section": "AYER"
499
-}
\ No newline at end of file
501
+}
lib/l10n/app_hi.arb
+3
-1
@@ -23,6 +23,8 @@
23
"auto_execute_tools": "स्वचालित रूप से उपकरण निष्पादित करें",
24
"auto_generate_titles": "स्वचालित रूप से शीर्षक उत्पन्न करें",
25
"available": "उपलब्ध",
26
+ "beta_label": "बीटा",
27
+ "experimental_label": "प्रायोगिक",
28
"available_models": "उपलब्ध मॉडल",
29
"available_ram": "उपलब्ध रैम (RAM)",
30
"backend_cpu_desc": "सभी उपकरणों पर काम करता है। सबसे अनुकूल।",
@@ -496,4 +498,4 @@
498
"voice_ryan": "Ryan (US)",
499
"waiting_selection": "चयन की प्रतीक्षा की जा रही है",
500
"yesterday_section": "कल"
499
-}
\ No newline at end of file
501
+}
lib/l10n/app_it.arb
+3
-1
@@ -211,6 +211,8 @@
211
"mcp_disabled_warning": "MCP è disabilitato a livello globale. Abilitalo nelle Impostazioni per utilizzare queste funzionalità.",
212
"mcp_enable_chat": "Abilita MCP per questa chat",
213
"auto_execute_tools": "Esecuzione automatica degli strumenti",
214
+ "beta_label": "Beta",
215
+ "experimental_label": "Sperimentale",
216
"add_ephemeral_mcp": "Aggiungi server MCP effimero",
217
"mcp_label_placeholder": "Etichetta",
218
"mcp_url_placeholder": "URL (https://...)",
@@ -496,4 +498,4 @@
498
"select_model_hint": "Seleziona un modello per iniziare a chattare",
499
"test_notification_title": "Notifica di prova",
500
"test_notification_body": "Questa è una notifica di prova per l'avanzamento del download del modello."
499
-}
\ No newline at end of file
501
+}
lib/l10n/app_localizations.dart
+12
@@ -1368,6 +1368,18 @@ abstract class AppLocalizations {
1368
/// **'Auto-execute tools'**
1369
String get auto_execute_tools;
1370
1371
+ /// Badge label for beta features
1372
+ ///
1373
+ /// In en, this message translates to:
1374
+ /// **'Beta'**
1375
+ String get beta_label;
1376
+
1377
+ /// Badge label for experimental features
1378
+ ///
1379
+ /// In en, this message translates to:
1380
+ /// **'Experimental'**
1381
+ String get experimental_label;
1382
+
1383
/// Section label for adding temp MCP server
1384
///
1385
/// In en, this message translates to:
lib/l10n/app_localizations_ar.dart
+6
@@ -674,6 +674,12 @@ class AppLocalizationsAr extends AppLocalizations {
674
@override
675
String get auto_execute_tools => 'تشغيل الأدوات تلقائياً';
676
677
+ @override
678
+ String get beta_label => 'تجريبي';
679
+
680
+ @override
681
+ String get experimental_label => 'اختباري';
682
+
683
@override
684
String get add_ephemeral_mcp => 'إضافة خادم MCP مؤقت';
685
lib/l10n/app_localizations_bn.dart
+6
@@ -677,6 +677,12 @@ class AppLocalizationsBn extends AppLocalizations {
677
@override
678
String get auto_execute_tools => 'স্বয়ংক্রিয়ভাবে টুলস নির্বাহ করুন';
679
680
+ @override
681
+ String get beta_label => 'বেটা';
682
+
683
+ @override
684
+ String get experimental_label => 'পরীক্ষামূলক';
685
+
686
@override
687
String get add_ephemeral_mcp => 'সাময়িক MCP সার্ভার যুক্ত করুন';
688
lib/l10n/app_localizations_en.dart
+6
@@ -675,6 +675,12 @@ class AppLocalizationsEn extends AppLocalizations {
675
@override
676
String get auto_execute_tools => 'Auto-execute tools';
677
678
+ @override
679
+ String get beta_label => 'Beta';
680
+
681
+ @override
682
+ String get experimental_label => 'Experimental';
683
+
684
@override
685
String get add_ephemeral_mcp => 'Add Ephemeral MCP Server';
686
lib/l10n/app_localizations_es.dart
+6
@@ -677,6 +677,12 @@ class AppLocalizationsEs extends AppLocalizations {
677
@override
678
String get auto_execute_tools => 'Ejecutar herramientas automáticamente';
679
680
+ @override
681
+ String get beta_label => 'Beta';
682
+
683
+ @override
684
+ String get experimental_label => 'Experimental';
685
+
686
@override
687
String get add_ephemeral_mcp => 'Añadir servidor MCP efímero';
688
lib/l10n/app_localizations_hi.dart
+6
@@ -677,6 +677,12 @@ class AppLocalizationsHi extends AppLocalizations {
677
@override
678
String get auto_execute_tools => 'स्वचालित रूप से उपकरण निष्पादित करें';
679
680
+ @override
681
+ String get beta_label => 'बीटा';
682
+
683
+ @override
684
+ String get experimental_label => 'प्रायोगिक';
685
+
686
@override
687
String get add_ephemeral_mcp => 'अस्थायी MCP सर्वर जोड़ें';
688
lib/l10n/app_localizations_it.dart
+6
@@ -678,6 +678,12 @@ class AppLocalizationsIt extends AppLocalizations {
678
@override
679
String get auto_execute_tools => 'Esecuzione automatica degli strumenti';
680
681
+ @override
682
+ String get beta_label => 'Beta';
683
+
684
+ @override
685
+ String get experimental_label => 'Sperimentale';
686
+
687
@override
688
String get add_ephemeral_mcp => 'Aggiungi server MCP effimero';
689
lib/l10n/app_localizations_zh.dart
+6
@@ -666,6 +666,12 @@ class AppLocalizationsZh extends AppLocalizations {
666
@override
667
String get auto_execute_tools => '自动执行工具';
668
669
+ @override
670
+ String get beta_label => 'Beta';
671
+
672
+ @override
673
+ String get experimental_label => '实验性';
674
+
675
@override
676
String get add_ephemeral_mcp => '添加临时 MCP 服务器';
677
lib/l10n/app_zh.arb
+3
-1
@@ -23,6 +23,8 @@
23
"auto_execute_tools": "自动执行工具",
24
"auto_generate_titles": "自动生成标题",
25
"available": "可用",
26
+ "beta_label": "Beta",
27
+ "experimental_label": "实验性",
28
"available_models": "可用模型",
29
"available_ram": "可用内存",
30
"backend_cpu_desc": "适用于所有设备。兼容性最好。",
@@ -496,4 +498,4 @@
498
"voice_ryan": "Ryan (美音)",
499
"waiting_selection": "等待选择",
500
"yesterday_section": "昨天"
499
-}
\ No newline at end of file
501
+}
pubspec.yaml
+1
-1
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
16
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17
# In Windows, build-name is used as the major, minor, and patch parts
18
# of the product and file versions while build-number is used as the build suffix.
19
-version: 1.3.0+30
19
+version: 1.3.1+31
20
21
environment:
22
sdk: ^3.11.0