From be623582706fd015d1851019063f71a183ac94a6 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Thu, 23 Oct 2025 22:37:06 +0530 Subject: [PATCH 1/6] feat(chat): strip reasoning when copying Remove internal reasoning from copied message text to avoidleaking implementation details or developer-only when a user content from the chat- In chat_pagecopyMessage, cleaning steps to: -
...
think> and ... tags - trim leftover whitespace before writing to the clipboard - In assistant_message_widget._buildSegmentedContent, remove an unused hasMediaAbove calculation and a conditional spacer that added extra top padding before reasoning tiles. This simplifies rendering logic and avoids relying on removed spacing behavior. --- lib/features/chat/views/chat_page.dart | 28 ++++++++++++++++++- .../widgets/assistant_message_widget.dart | 9 ------ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index a157b4f..67c7e16 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -969,7 +969,33 @@ class _ChatPageState extends ConsumerState { } void _copyMessage(String content) { - Clipboard.setData(ClipboardData(text: content)); + // Strip reasoning details from the copied content + String cleanedContent = content; + + // Remove
blocks + cleanedContent = cleanedContent.replaceAll( + RegExp( + r']*>[\s\S]*?<\/details>', + multiLine: true, + dotAll: true, + ), + '', + ); + + // Remove raw reasoning tags + cleanedContent = cleanedContent.replaceAll( + RegExp(r'[\s\S]*?<\/think>', multiLine: true, dotAll: true), + '', + ); + cleanedContent = cleanedContent.replaceAll( + RegExp(r'[\s\S]*?<\/reasoning>', multiLine: true, dotAll: true), + '', + ); + + // Clean up any extra whitespace + cleanedContent = cleanedContent.trim(); + + Clipboard.setData(ClipboardData(text: cleanedContent)); } void _regenerateMessage(dynamic message) async { diff --git a/lib/features/chat/widgets/assistant_message_widget.dart b/lib/features/chat/widgets/assistant_message_widget.dart index 6bd4ea9..0359fba 100644 --- a/lib/features/chat/widgets/assistant_message_widget.dart +++ b/lib/features/chat/widgets/assistant_message_widget.dart @@ -435,10 +435,6 @@ class _AssistantMessageWidgetState extends ConsumerState Widget _buildSegmentedContent() { final children = []; - // Determine if media (attachments or generated images) is rendered above. - final hasMediaAbove = - (widget.message.attachmentIds?.isNotEmpty ?? false) || - (widget.message.files?.isNotEmpty ?? false); bool firstToolSpacerAdded = false; int idx = 0; for (final seg in _segments) { @@ -450,11 +446,6 @@ class _AssistantMessageWidgetState extends ConsumerState } children.add(_buildToolCallTile(seg.toolCall!)); } else if (seg.isReasoning && seg.reasoning != null) { - // If a reasoning tile is the very first content and sits at the top, - // add a small spacer above it for breathing room. - if (children.isEmpty && !hasMediaAbove) { - children.add(const SizedBox(height: Spacing.sm)); - } children.add(_buildReasoningTile(seg.reasoning!, idx)); } else if ((seg.text ?? '').trim().isNotEmpty) { children.add(_buildEnhancedMarkdownContent(seg.text!)); From 5d898aaca04ac5f93e617ab5342d8309a685da76 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Thu, 23 Oct 2025 23:19:41 +0530 Subject: [PATCH 2/6] refactor: improve composer layout and constrain height the modern chat input composer layout to better handle vertical space and alignment. Change the row crossAxisAlignment from center to end so controls align to the input bottom. Wrap the animated container with a ConstrainedBox that limits the composer max height to 25% of the screen, preventing excessive growth on tall displays. Reorder and clean up the AnimatedContainer/decoration block, extracting the composer radius constant and preserving styling (background color, border color/width). Move the text field and inline mic into the same row structure under the constrained container and retain the isActive flag and padding. These changes prevent layout overflow, ensure a consistent border radius, and improve visual alignment of controls. --- .../chat/widgets/modern_chat_input.dart | 69 ++++++++++--------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/lib/features/chat/widgets/modern_chat_input.dart b/lib/features/chat/widgets/modern_chat_input.dart index d1a1632..e002d47 100644 --- a/lib/features/chat/widgets/modern_chat_input.dart +++ b/lib/features/chat/widgets/modern_chat_input.dart @@ -786,7 +786,7 @@ class _ModernChatInputState extends ConsumerState Spacing.sm, ), child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, children: [ _buildOverflowButton( tooltip: AppLocalizations.of(context)!.more, @@ -796,42 +796,47 @@ class _ModernChatInputState extends ConsumerState ), const SizedBox(width: Spacing.sm), Expanded( - child: AnimatedContainer( - duration: const Duration(milliseconds: 180), - curve: Curves.easeOutCubic, - padding: const EdgeInsets.symmetric(horizontal: Spacing.md), - constraints: const BoxConstraints( - minHeight: TouchTarget.input, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.25, ), - decoration: BoxDecoration( - color: composerSurface.withValues( - alpha: brightness == Brightness.dark ? 0.9 : 0.2, + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + curve: Curves.easeOutCubic, + padding: const EdgeInsets.symmetric(horizontal: Spacing.md), + constraints: const BoxConstraints( + minHeight: TouchTarget.input, ), - borderRadius: BorderRadius.circular(AppBorderRadius.round), - border: Border.all( - color: outlineColor.withValues( - alpha: brightness == Brightness.dark ? 0.32 : 0.2, + decoration: BoxDecoration( + color: composerSurface.withValues( + alpha: brightness == Brightness.dark ? 0.9 : 0.2, ), - width: BorderWidth.micro, - ), - ), - child: Row( - children: [ - Expanded( - child: _buildComposerTextField( - brightness: brightness, - sendOnEnter: sendOnEnter, - placeholderBase: placeholderBase, - placeholderFocused: placeholderFocused, - contentPadding: const EdgeInsets.symmetric( - vertical: Spacing.xs, - ), - isActive: isActive, + borderRadius: BorderRadius.circular(_composerRadius), + border: Border.all( + color: outlineColor.withValues( + alpha: brightness == Brightness.dark ? 0.32 : 0.2, ), + width: BorderWidth.micro, ), - if (!_hasText && voiceAvailable && !isGenerating) - _buildInlineMicIcon(voiceAvailable), - ], + ), + child: Row( + children: [ + Expanded( + child: _buildComposerTextField( + brightness: brightness, + sendOnEnter: sendOnEnter, + placeholderBase: placeholderBase, + placeholderFocused: placeholderFocused, + contentPadding: const EdgeInsets.symmetric( + vertical: Spacing.xs, + ), + isActive: isActive, + ), + ), + if (!_hasText && voiceAvailable && !isGenerating) + _buildInlineMicIcon(voiceAvailable), + ], + ), ), ), ), From 625631c096d9a2b21dee7d8aa154edca8ee02da9 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Thu, 23 Oct 2025 23:27:11 +0530 Subject: [PATCH 3/6] feat: add scr color tokens and use them for drawers Introduce scrimMedium and scrimStrong color tokens to the shared theme color tokens and propagate them through constructors, copyWith, and lerp so scrim values interpolate and can be overridden. Define scrim tokens as black with different alpha values per theme mode (lighter alpha in light mode stronger in dark mode) to create a consistent darkening effect for overlays. Refactor ChatPage to use the new scrim tokens for drawer scrims and format a RegExp call for readability. This replaces previous use of overlay tokens for platform-specific scrims to provide clearer semantics and better visual control for modal/drawer backdrops. --- lib/features/chat/views/chat_page.dart | 10 +++++++--- lib/shared/theme/color_tokens.dart | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 67c7e16..1bc15db 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -988,7 +988,11 @@ class _ChatPageState extends ConsumerState { '', ); cleanedContent = cleanedContent.replaceAll( - RegExp(r'[\s\S]*?<\/reasoning>', multiLine: true, dotAll: true), + RegExp( + r'[\s\S]*?<\/reasoning>', + multiLine: true, + dotAll: true, + ), '', ); @@ -1274,8 +1278,8 @@ class _ChatPageState extends ConsumerState { final maxFraction = isTablet ? 0.42 : 0.84; final edgeFraction = isTablet ? 0.36 : 0.50; // large phone edge final scrim = Platform.isIOS - ? context.colorTokens.overlayMedium - : context.colorTokens.overlayStrong; + ? context.colorTokens.scrimMedium + : context.colorTokens.scrimStrong; return ResponsiveDrawerLayout( maxFraction: maxFraction, diff --git a/lib/shared/theme/color_tokens.dart b/lib/shared/theme/color_tokens.dart index 2e4f3ff..ce5d660 100644 --- a/lib/shared/theme/color_tokens.dart +++ b/lib/shared/theme/color_tokens.dart @@ -39,6 +39,8 @@ class AppColorTokens extends ThemeExtension { required this.overlayWeak, required this.overlayMedium, required this.overlayStrong, + required this.scrimMedium, + required this.scrimStrong, required this.codeBackground, required this.codeBorder, required this.codeText, @@ -84,6 +86,10 @@ class AppColorTokens extends ThemeExtension { final Color overlayMedium; final Color overlayStrong; + // Scrim tokens (for drawer/modal overlays) + final Color scrimMedium; + final Color scrimStrong; + // Markdown/code tokens final Color codeBackground; final Color codeBorder; @@ -194,6 +200,14 @@ class AppColorTokens extends ThemeExtension { alpha: isLight ? 0.32 : 0.36, ); + // Scrim tokens use black to create darkening effect in both modes + final Color scrimMedium = Colors.black.withValues( + alpha: isLight ? 0.2 : 0.5, + ); + final Color scrimStrong = Colors.black.withValues( + alpha: isLight ? 0.32 : 0.6, + ); + final Color codeBackground = mix(variant.muted, neutralTone00, 0.5); final Color codeBorder = mix(variant.border, neutralTone40, 0.6); final Color codeText = _ensureContrast( @@ -232,6 +246,8 @@ class AppColorTokens extends ThemeExtension { overlayWeak: overlayWeak, overlayMedium: overlayMedium, overlayStrong: overlayStrong, + scrimMedium: scrimMedium, + scrimStrong: scrimStrong, codeBackground: codeBackground, codeBorder: codeBorder, codeText: codeText, @@ -269,6 +285,8 @@ class AppColorTokens extends ThemeExtension { Color? overlayWeak, Color? overlayMedium, Color? overlayStrong, + Color? scrimMedium, + Color? scrimStrong, Color? codeBackground, Color? codeBorder, Color? codeText, @@ -303,6 +321,8 @@ class AppColorTokens extends ThemeExtension { overlayWeak: overlayWeak ?? this.overlayWeak, overlayMedium: overlayMedium ?? this.overlayMedium, overlayStrong: overlayStrong ?? this.overlayStrong, + scrimMedium: scrimMedium ?? this.scrimMedium, + scrimStrong: scrimStrong ?? this.scrimStrong, codeBackground: codeBackground ?? this.codeBackground, codeBorder: codeBorder ?? this.codeBorder, codeText: codeText ?? this.codeText, @@ -364,6 +384,8 @@ class AppColorTokens extends ThemeExtension { overlayWeak: Color.lerp(overlayWeak, other.overlayWeak, t)!, overlayMedium: Color.lerp(overlayMedium, other.overlayMedium, t)!, overlayStrong: Color.lerp(overlayStrong, other.overlayStrong, t)!, + scrimMedium: Color.lerp(scrimMedium, other.scrimMedium, t)!, + scrimStrong: Color.lerp(scrimStrong, other.scrimStrong, t)!, codeBackground: Color.lerp(codeBackground, other.codeBackground, t)!, codeBorder: Color.lerp(codeBorder, other.codeBorder, t)!, codeText: Color.lerp(codeText, other.codeText, t)!, From c1eae6608d0b014ebd56ed4e4c889a2a5724f33f Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Thu, 23 Oct 2025 23:38:32 +0530 Subject: [PATCH 4/6] feat: pass button BuildContext to conversation menu to position it --- .../navigation/widgets/chats_drawer.dart | 47 ++++--- .../utils/conversation_context_menu.dart | 119 +++++++++--------- 2 files changed, 86 insertions(+), 80 deletions(-) diff --git a/lib/features/navigation/widgets/chats_drawer.dart b/lib/features/navigation/widgets/chats_drawer.dart index 849b660..33d36c9 100644 --- a/lib/features/navigation/widgets/chats_drawer.dart +++ b/lib/features/navigation/widgets/chats_drawer.dart @@ -1254,9 +1254,9 @@ class _ChatsDrawerState extends ConsumerState { ? null : () => _selectConversation(context, conv.id), onLongPress: null, - onMorePressed: () { + onMorePressed: (buttonContext) { showConversationContextMenu( - context: context, + context: buttonContext, ref: ref, conversation: conv, ); @@ -1609,7 +1609,7 @@ class _ConversationTileContent extends StatelessWidget { final bool pinned; final bool selected; final bool isLoading; - final VoidCallback? onMorePressed; + final void Function(BuildContext)? onMorePressed; final Widget? leading; const _ConversationTileContent({ @@ -1664,22 +1664,29 @@ class _ConversationTileContent extends StatelessWidget { } else if (onMorePressed != null) { trailing.addAll([ const SizedBox(width: Spacing.sm), - IconButton( - iconSize: IconSize.sm, - visualDensity: const VisualDensity(horizontal: -2, vertical: -2), - padding: EdgeInsets.zero, - constraints: const BoxConstraints( - minWidth: TouchTarget.listItem, - minHeight: TouchTarget.listItem, - ), - icon: Icon( - Platform.isIOS - ? CupertinoIcons.ellipsis - : Icons.more_vert_rounded, - color: theme.iconSecondary, - ), - onPressed: onMorePressed, - tooltip: AppLocalizations.of(context)!.more, + Builder( + builder: (buttonContext) { + return IconButton( + iconSize: IconSize.sm, + visualDensity: const VisualDensity( + horizontal: -2, + vertical: -2, + ), + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: TouchTarget.listItem, + minHeight: TouchTarget.listItem, + ), + icon: Icon( + Platform.isIOS + ? CupertinoIcons.ellipsis + : Icons.more_vert_rounded, + color: theme.iconSecondary, + ), + onPressed: () => onMorePressed!(buttonContext), + tooltip: AppLocalizations.of(context)!.more, + ); + }, ), ]); } @@ -1720,7 +1727,7 @@ class _ConversationTile extends StatelessWidget { final Widget? leading; final VoidCallback? onTap; final VoidCallback? onLongPress; - final VoidCallback? onMorePressed; + final void Function(BuildContext)? onMorePressed; const _ConversationTile({ required this.title, diff --git a/lib/shared/utils/conversation_context_menu.dart b/lib/shared/utils/conversation_context_menu.dart index 0a27832..ca911da 100644 --- a/lib/shared/utils/conversation_context_menu.dart +++ b/lib/shared/utils/conversation_context_menu.dart @@ -3,9 +3,6 @@ import 'dart:io' show Platform; import 'package:conduit/core/providers/app_providers.dart'; import 'package:conduit/l10n/app_localizations.dart'; import 'package:conduit/shared/theme/theme_extensions.dart'; -import 'package:conduit/shared/widgets/conduit_components.dart'; -import 'package:conduit/shared/widgets/modal_safe_area.dart'; -import 'package:conduit/shared/widgets/sheet_handle.dart'; import 'package:conduit/shared/widgets/themed_dialogs.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -35,74 +32,76 @@ class ConduitContextMenuAction { Future showConduitContextMenu({ required BuildContext context, required List actions, + Offset? position, }) async { if (actions.isEmpty) return; final theme = context.conduitTheme; + final RenderBox? overlay = + Overlay.of(context).context.findRenderObject() as RenderBox?; - await showModalBottomSheet( + if (overlay == null) return; + + // Determine menu position + final Offset menuPosition = position ?? _getDefaultMenuPosition(context); + + final result = await showMenu( context: context, - backgroundColor: Colors.transparent, - builder: (sheetContext) { - Future handleAction(ConduitContextMenuAction action) async { - action.onBeforeClose?.call(); - Navigator.of(sheetContext).pop(); - await Future.microtask(action.onSelected); - } - - List buildActionTiles() { - return actions - .map( - (action) => ConduitListItem( - isCompact: true, - leading: Icon( - Platform.isIOS ? action.cupertinoIcon : action.materialIcon, - color: action.destructive ? Colors.red : theme.iconPrimary, - size: IconSize.modal, - ), - title: Text( - action.label, - style: AppTypography.standard.copyWith( - color: action.destructive ? Colors.red : theme.textPrimary, - fontWeight: FontWeight.w500, - ), - ), - onTap: () => handleAction(action), - ), - ) - .toList(); - } - - final actionTiles = buildActionTiles(); - - return ModalSheetSafeArea( + position: RelativeRect.fromLTRB( + menuPosition.dx, + menuPosition.dy, + overlay.size.width - menuPosition.dx, + overlay.size.height - menuPosition.dy, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + ), + color: theme.surfaceBackground, + elevation: 8, + items: actions.map((action) { + return PopupMenuItem( + value: action, padding: const EdgeInsets.symmetric( - horizontal: Spacing.screenPadding, - vertical: Spacing.screenPadding, + horizontal: Spacing.md, + vertical: Spacing.xs, ), - child: Container( - decoration: BoxDecoration( - color: theme.surfaceBackground, - borderRadius: BorderRadius.circular(AppBorderRadius.lg), - boxShadow: ConduitShadows.modal(context), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: Spacing.sm), - const SheetHandle(), - const SizedBox(height: Spacing.sm), - for (var i = 0; i < actionTiles.length; i++) ...[ - if (i != 0) const ConduitDivider(isCompact: true), - actionTiles[i], - ], - const SizedBox(height: Spacing.sm), - ], - ), + child: Row( + children: [ + Icon( + Platform.isIOS ? action.cupertinoIcon : action.materialIcon, + color: action.destructive ? Colors.red : theme.iconPrimary, + size: IconSize.sm, + ), + const SizedBox(width: Spacing.md), + Expanded( + child: Text( + action.label, + style: AppTypography.standard.copyWith( + color: action.destructive ? Colors.red : theme.textPrimary, + fontWeight: FontWeight.w500, + ), + ), + ), + ], ), ); - }, + }).toList(), ); + + if (result != null) { + result.onBeforeClose?.call(); + await Future.microtask(result.onSelected); + } +} + +Offset _getDefaultMenuPosition(BuildContext context) { + final RenderBox? renderBox = context.findRenderObject() as RenderBox?; + if (renderBox == null) { + return Offset.zero; + } + final position = renderBox.localToGlobal(Offset.zero); + final size = renderBox.size; + return Offset(position.dx + size.width, position.dy); } Future showConversationContextMenu({ From 9c2cf347a7a6ea880ae8ffdfe2e4ac18ebed4bc4 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Fri, 24 Oct 2025 00:31:29 +0530 Subject: [PATCH 5/6] feat(chat): inline action panel with context menu edit flow --- .../chat/widgets/user_message_bubble.dart | 207 +++++++++++------- .../utils/conversation_context_menu.dart | 14 +- 2 files changed, 141 insertions(+), 80 deletions(-) diff --git a/lib/features/chat/widgets/user_message_bubble.dart b/lib/features/chat/widgets/user_message_bubble.dart index 8304cde..896d35c 100644 --- a/lib/features/chat/widgets/user_message_bubble.dart +++ b/lib/features/chat/widgets/user_message_bubble.dart @@ -7,10 +7,11 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'dart:io' show Platform; import 'package:conduit/l10n/app_localizations.dart'; -import 'package:conduit/shared/widgets/chat_action_button.dart'; +import 'package:flutter/services.dart'; import '../../../core/providers/app_providers.dart'; import '../providers/chat_providers.dart'; import '../../../shared/services/tasks/task_queue.dart'; +import '../../../shared/utils/conversation_context_menu.dart'; import '../../tools/providers/tools_providers.dart'; class UserMessageBubble extends ConsumerStatefulWidget { @@ -41,28 +42,15 @@ class UserMessageBubble extends ConsumerStatefulWidget { ConsumerState createState() => _UserMessageBubbleState(); } -class _UserMessageBubbleState extends ConsumerState - with TickerProviderStateMixin { - bool _showActions = false; - late AnimationController _fadeController; - late AnimationController _slideController; - // press state handled by shared ChatActionButton - +class _UserMessageBubbleState extends ConsumerState { bool _isEditing = false; late final TextEditingController _editController; final FocusNode _editFocusNode = FocusNode(); + final GlobalKey _bubbleKey = GlobalKey(); @override void initState() { super.initState(); - _fadeController = AnimationController( - duration: AnimationDuration.microInteraction, - vsync: this, - ); - _slideController = AnimationController( - duration: AnimationDuration.messageSlide, - vsync: this, - ); _editController = TextEditingController( text: widget.message?.content ?? '', ); @@ -417,25 +405,56 @@ class _UserMessageBubbleState extends ConsumerState @override void dispose() { - _fadeController.dispose(); - _slideController.dispose(); _editController.dispose(); _editFocusNode.dispose(); super.dispose(); } - void _toggleActions() { - setState(() { - _showActions = !_showActions; - }); + Future _showMessageMenu(BuildContext context) async { + // Don't show menu while editing - use the visible Save/Cancel buttons instead + if (_isEditing) return; - if (_showActions) { - _fadeController.forward(); - _slideController.forward(); - } else { - _fadeController.reverse(); - _slideController.reverse(); + final l10n = AppLocalizations.of(context)!; + HapticFeedback.selectionClick(); + + // Get the position of the bubble to show menu below it + Offset? menuPosition; + final RenderBox? renderBox = + _bubbleKey.currentContext?.findRenderObject() as RenderBox?; + if (renderBox != null) { + final position = renderBox.localToGlobal(Offset.zero); + final size = renderBox.size; + // Position menu at bottom-right of the bubble + menuPosition = Offset( + position.dx + size.width, + position.dy + size.height, + ); } + + await showConduitContextMenu( + context: context, + position: menuPosition, + actions: [ + ConduitContextMenuAction( + cupertinoIcon: CupertinoIcons.pencil, + materialIcon: Icons.edit_outlined, + label: l10n.edit, + onBeforeClose: () => HapticFeedback.selectionClick(), + onSelected: () async => _startInlineEdit(), + ), + ConduitContextMenuAction( + cupertinoIcon: CupertinoIcons.doc_on_clipboard, + materialIcon: Icons.content_copy, + label: l10n.copy, + onBeforeClose: () => HapticFeedback.selectionClick(), + onSelected: () async { + if (widget.onCopy != null) { + widget.onCopy!(); + } + }, + ), + ], + ); } @override @@ -458,7 +477,7 @@ class _UserMessageBubbleState extends ConsumerState ); return GestureDetector( - onLongPress: () => _toggleActions(), + onLongPress: () => _showMessageMenu(context), behavior: HitTestBehavior.translucent, child: Container( width: double.infinity, @@ -490,6 +509,7 @@ class _UserMessageBubbleState extends ConsumerState maxWidth: MediaQuery.of(context).size.width * 0.82, ), child: Container( + key: _bubbleKey, padding: const EdgeInsets.symmetric( horizontal: Spacing.chatBubblePadding, vertical: Spacing.sm, @@ -584,12 +604,11 @@ class _UserMessageBubbleState extends ConsumerState ), ], ), - if (hasText) const SizedBox(height: Spacing.xs), - // Action buttons below the message - if (_showActions) ...[ + // Edit action buttons - show Save/Cancel when editing + if (_isEditing) ...[ const SizedBox(height: Spacing.sm), - _buildUserActionButtons(), + _buildEditActionButtons(), ], ], ), @@ -597,59 +616,100 @@ class _UserMessageBubbleState extends ConsumerState ); } - // Assistant-only message renderer removed. + Widget _buildEditActionButtons() { + final l10n = AppLocalizations.of(context)!; + final theme = context.conduitTheme; - // Markdown rendering and typing indicator helpers removed. - - // Removed unused assistant action buttons builder. - - Widget _buildActionButton({ - required IconData icon, - required String label, - VoidCallback? onTap, - }) { - return ChatActionButton(icon: icon, label: label, onTap: onTap); - } - - Widget _buildUserActionButtons() { - return Wrap( - spacing: Spacing.sm, - runSpacing: Spacing.sm, + return Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - if (_isEditing) ...[ - _buildActionButton( - icon: Platform.isIOS ? CupertinoIcons.check_mark : Icons.check, - label: AppLocalizations.of(context)!.save, - onTap: _saveInlineEdit, - ), - _buildActionButton( - icon: Platform.isIOS ? CupertinoIcons.xmark : Icons.close, - label: AppLocalizations.of(context)!.cancel, + // Cancel button + Material( + color: Colors.transparent, + child: InkWell( onTap: _cancelInlineEdit, + borderRadius: BorderRadius.circular(AppBorderRadius.small), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.xs, + ), + decoration: BoxDecoration( + color: theme.surfaceContainer, + borderRadius: BorderRadius.circular(AppBorderRadius.small), + border: Border.all( + color: theme.cardBorder, + width: BorderWidth.thin, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Platform.isIOS ? CupertinoIcons.xmark : Icons.close, + size: IconSize.xs, + color: theme.textSecondary, + ), + const SizedBox(width: Spacing.xs), + Text( + l10n.cancel, + style: AppTypography.standard.copyWith( + color: theme.textSecondary, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), ), - ] else ...[ - _buildActionButton( - icon: Platform.isIOS ? CupertinoIcons.pencil : Icons.edit_outlined, - label: AppLocalizations.of(context)!.edit, - onTap: widget.onEdit ?? _startInlineEdit, + ), + const SizedBox(width: Spacing.sm), + // Save button + Material( + color: Colors.transparent, + child: InkWell( + onTap: _saveInlineEdit, + borderRadius: BorderRadius.circular(AppBorderRadius.small), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.xs, + ), + decoration: BoxDecoration( + color: theme.buttonPrimary, + borderRadius: BorderRadius.circular(AppBorderRadius.small), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Platform.isIOS ? CupertinoIcons.check_mark : Icons.check, + size: IconSize.xs, + color: theme.buttonPrimaryText, + ), + const SizedBox(width: Spacing.xs), + Text( + l10n.save, + style: AppTypography.standard.copyWith( + color: theme.buttonPrimaryText, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), ), - _buildActionButton( - icon: Platform.isIOS - ? CupertinoIcons.doc_on_clipboard - : Icons.content_copy, - label: AppLocalizations.of(context)!.copy, - onTap: widget.onCopy, - ), - ], + ), ], ); } + // Assistant-only message renderer removed. + void _startInlineEdit() { if (_isEditing) return; setState(() { _isEditing = true; - _showActions = true; // ensure actions visible for Save/Cancel _editController.text = widget.message.content ?? ''; }); // Request focus after frame to show keyboard @@ -664,7 +724,6 @@ class _UserMessageBubbleState extends ConsumerState if (!_isEditing) return; setState(() { _isEditing = false; - // keep actions panel open; user can close with long-press _editController.text = widget.message.content ?? ''; }); _editFocusNode.unfocus(); diff --git a/lib/shared/utils/conversation_context_menu.dart b/lib/shared/utils/conversation_context_menu.dart index ca911da..893a1e1 100644 --- a/lib/shared/utils/conversation_context_menu.dart +++ b/lib/shared/utils/conversation_context_menu.dart @@ -54,31 +54,33 @@ Future showConduitContextMenu({ overlay.size.height - menuPosition.dy, ), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), + borderRadius: BorderRadius.circular(AppBorderRadius.small), ), color: theme.surfaceBackground, - elevation: 8, + elevation: 4, items: actions.map((action) { return PopupMenuItem( value: action, padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.xs, + horizontal: Spacing.sm, + vertical: Spacing.xxs, ), + height: 36, child: Row( children: [ Icon( Platform.isIOS ? action.cupertinoIcon : action.materialIcon, color: action.destructive ? Colors.red : theme.iconPrimary, - size: IconSize.sm, + size: IconSize.xs, ), - const SizedBox(width: Spacing.md), + const SizedBox(width: Spacing.sm), Expanded( child: Text( action.label, style: AppTypography.standard.copyWith( color: action.destructive ? Colors.red : theme.textPrimary, fontWeight: FontWeight.w500, + fontSize: 14, ), ), ), From 15299ecd82c24a76c5f48c408d1017d2587527de Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Fri, 24 Oct 2025 00:39:43 +0530 Subject: [PATCH 6/6] feat(chat): dismiss after send to recover screen space --- lib/features/chat/widgets/modern_chat_input.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/features/chat/widgets/modern_chat_input.dart b/lib/features/chat/widgets/modern_chat_input.dart index e002d47..452c62f 100644 --- a/lib/features/chat/widgets/modern_chat_input.dart +++ b/lib/features/chat/widgets/modern_chat_input.dart @@ -210,7 +210,14 @@ class _ModernChatInputState extends ConsumerState PlatformUtils.lightHaptic(); widget.onSendMessage(text); _controller.clear(); - // Keep focus and keyboard open; do not collapse automatically + + // Dismiss keyboard after sending to recover screen space + _focusNode.unfocus(); + try { + SystemChannels.textInput.invokeMethod('TextInput.hide'); + } catch (_) { + // Silently handle if keyboard dismissal fails + } } void _insertNewline() {