@tduncan / Cortana / commits / 2e69dd8

feat: add beta and experimental badges to MCP tool events in chat bubbles

abdulmominsakib committed May 27, 2026 at 00:01 UTC 2e69dd87d34199ddb9aa0e00357c2b4456509b9f
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