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() {