diff --git a/app/lib/ai/tools/confirm_gate.dart b/app/lib/ai/tools/confirm_gate.dart index fb55e6c..f65fa37 100644 --- a/app/lib/ai/tools/confirm_gate.dart +++ b/app/lib/ai/tools/confirm_gate.dart @@ -27,21 +27,26 @@ class ConfirmGate { final theme = Theme.of(ctx); return AlertDialog( title: const Text('이 작업을 수행할까요?'), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(tool.description, style: theme.textTheme.bodyMedium), - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(8), + // SingleChildScrollView 로 감싸 좁은 모바일 화면에서 description 이 + // 길거나 summary 가 multi-line 일 때 잘리지 않고 스크롤되게 한다. + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(tool.description, style: theme.textTheme.bodyMedium), + const SizedBox(height: 12), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: Text(summary), ), - child: Text(summary), - ), - ], + ], + ), ), actions: [ TextButton( diff --git a/app/lib/ui/screens/chat_screen.dart b/app/lib/ui/screens/chat_screen.dart index 636e2dd..804312a 100644 --- a/app/lib/ui/screens/chat_screen.dart +++ b/app/lib/ui/screens/chat_screen.dart @@ -55,7 +55,7 @@ class _ChatScreenState extends ConsumerState { actions: [ IconButton( icon: const Icon(Icons.refresh), - tooltip: '새 대화', + tooltip: '새 대화 (이전 기록 비우기)', onPressed: () { ref.read(chatSessionControllerProvider.notifier).clear(); }, @@ -150,6 +150,19 @@ class _ChatScreenState extends ConsumerState { } } +/// Human-friendly Korean labels for the 6 tools registered in +/// `ToolRegistry.defaults()`. Falls back to the raw tool name for any +/// future tool that hasn't been mapped yet — better to show the raw id +/// than to silently drop the message. +const Map _kToolKoreanLabels = { + 'search_catalog': '카탈로그 검색', + 'query_protocol': '프로토콜 상세', + 'list_active_habits': '활성 습관 조회', + 'get_streak': '스트릭 조회', + 'add_habit': '습관 추가', + 'log_tracker_entry': '체크 기록', +}; + class _MessageBubble extends StatelessWidget { final ChatMessage message; final bool streaming; @@ -168,14 +181,29 @@ class _MessageBubble extends StatelessWidget { text: m.text, ); case ModelChatMessage m: + // Streaming cursor uses primary so it stays discoverable in both + // light and dark themes (default onSurface low-contrasted with the + // surfaceContainerHighest bubble in dark mode). return _bubble( context, align: Alignment.centerLeft, color: theme.colorScheme.surfaceContainerHighest, textColor: theme.colorScheme.onSurface, - text: m.text + (streaming ? '▍' : ''), + richText: streaming + ? TextSpan( + children: [ + TextSpan(text: m.text), + TextSpan( + text: '▍', + style: TextStyle(color: theme.colorScheme.primary), + ), + ], + ) + : null, + text: m.text, ); case ToolCallChatMessage m: + final label = _kToolKoreanLabels[m.name] ?? m.name; return Align( alignment: Alignment.center, child: Container( @@ -187,8 +215,9 @@ class _MessageBubble extends StatelessWidget { border: Border.all(color: theme.colorScheme.outlineVariant), ), child: Text( - '🛠 ${m.name} → ${_toolResultLabel(m.result)}', + '🛠 $label → ${_toolResultLabel(m.result)}', style: theme.textTheme.bodySmall, + textAlign: TextAlign.center, ), ), ); @@ -212,6 +241,7 @@ class _MessageBubble extends StatelessWidget { required Color color, required Color textColor, required String text, + TextSpan? richText, }) { return Align( alignment: align, @@ -225,7 +255,9 @@ class _MessageBubble extends StatelessWidget { color: color, borderRadius: BorderRadius.circular(12), ), - child: Text(text, style: TextStyle(color: textColor)), + child: richText != null + ? Text.rich(richText, style: TextStyle(color: textColor)) + : Text(text, style: TextStyle(color: textColor)), ), ); }