import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:provider/provider.dart'; import '../../../models/agent_info.dart'; import '../../../models/content_block.dart'; import '../../../models/log_entry.dart'; import '../../../providers/session_provider.dart'; import '../../../theme/app_theme.dart'; import '../../../widgets/common/expandable_card.dart'; import '../../../widgets/common/json_tree_view.dart'; class AssistantMessageCard extends StatelessWidget { final AssistantEntry entry; final int index; const AssistantMessageCard({ super.key, required this.entry, required this.index, }); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: AppColors.assistantBg, borderRadius: BorderRadius.circular(8), border: Border.all(color: AppColors.assistant.withAlpha(40)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Header Padding( padding: const EdgeInsets.fromLTRB(14, 10, 14, 8), child: Row( children: [ Container( width: 22, height: 22, decoration: BoxDecoration( color: AppColors.assistant.withAlpha(40), borderRadius: BorderRadius.circular(4), ), child: const Icon(Icons.smart_toy, size: 14, color: AppColors.assistant), ), const SizedBox(width: 8), Text( entry.model ?? 'Assistant', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: AppColors.assistant, ), ), const SizedBox(width: 8), Text( '#$index', style: const TextStyle( fontSize: 11, color: AppColors.textMuted), ), const Spacer(), if (entry.usage != null) ...[ _TokenBadge( label: 'in', value: entry.usage!.inputTokens, color: AppColors.user, ), const SizedBox(width: 6), _TokenBadge( label: 'out', value: entry.usage!.outputTokens, color: AppColors.assistant, ), if (entry.usage!.cacheReadInputTokens > 0) ...[ const SizedBox(width: 6), _TokenBadge( label: 'cache', value: entry.usage!.cacheReadInputTokens, color: AppColors.tool, ), ], const SizedBox(width: 12), ], if (entry.timestamp != null) Text( _formatTime(entry.timestamp!), style: const TextStyle( fontSize: 11, color: AppColors.textMuted), ), ], ), ), // Thinking blocks if (entry.hasThinking) Padding( padding: const EdgeInsets.fromLTRB(14, 0, 14, 6), child: ExpandableCard( backgroundColor: AppColors.surfaceLight, borderColor: AppColors.surfaceBorder, header: Row( children: [ const Icon(Icons.psychology, size: 14, color: AppColors.agent), const SizedBox(width: 6), const Text( 'Thinking', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: AppColors.agent, ), ), const SizedBox(width: 8), Text( entry.thinkingBlocks.first.thinking.isEmpty ? '(encrypted)' : '${entry.thinkingBlocks.first.thinking.length} chars', style: const TextStyle( fontSize: 11, color: AppColors.textMuted), ), ], ), child: entry.thinkingBlocks.first.thinking.isEmpty ? const Text( 'Thinking content is encrypted and not available.', style: TextStyle( fontSize: 12, color: AppColors.textMuted, fontStyle: FontStyle.italic, ), ) : SelectableText( entry.thinkingBlocks.first.thinking, style: const TextStyle( fontFamily: 'JetBrains Mono', fontSize: 12, color: AppColors.textSecondary, height: 1.5, ), ), ), ), // Text blocks for (final block in entry.textBlocks) Padding( padding: const EdgeInsets.fromLTRB(14, 0, 14, 8), child: MarkdownBody( data: block.text, selectable: true, styleSheet: MarkdownStyleSheet( p: const TextStyle( fontSize: 13, color: AppColors.textPrimary, height: 1.5, ), code: const TextStyle( fontFamily: 'JetBrains Mono', fontSize: 12, color: AppColors.tool, backgroundColor: AppColors.surfaceLight, ), codeblockDecoration: BoxDecoration( color: AppColors.surfaceLight, borderRadius: BorderRadius.circular(6), border: Border.all(color: AppColors.surfaceBorder), ), codeblockPadding: const EdgeInsets.all(12), h1: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.textPrimary), h2: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: AppColors.textPrimary), h3: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: AppColors.textPrimary), listBullet: const TextStyle( fontSize: 13, color: AppColors.textSecondary), blockquoteDecoration: BoxDecoration( border: Border( left: BorderSide( color: AppColors.assistant.withAlpha(80), width: 3), ), ), ), ), ), // Tool use blocks if (entry.hasToolUse) Padding( padding: const EdgeInsets.fromLTRB(14, 0, 14, 8), child: Column( children: entry.toolUseBlocks .map((block) => Padding( padding: const EdgeInsets.only(bottom: 6), child: _ToolUseCard(block: block), )) .toList(), ), ), // Raw JSON expandable Padding( padding: const EdgeInsets.fromLTRB(14, 0, 14, 8), child: ExpandableCard( backgroundColor: AppColors.surface, borderColor: AppColors.surfaceBorder, header: const Row( children: [ Icon(Icons.data_object, size: 14, color: AppColors.textMuted), SizedBox(width: 6), Text( 'Raw JSON', style: TextStyle(fontSize: 11, color: AppColors.textMuted), ), ], ), child: JsonTreeView(data: entry.raw), ), ), ], ), ); } String _formatTime(DateTime dt) { return '${dt.hour.toString().padLeft(2, '0')}:' '${dt.minute.toString().padLeft(2, '0')}:' '${dt.second.toString().padLeft(2, '0')}'; } } // ─── Tool use card (handles both regular tools and Agent calls) ─── class _ToolUseCard extends StatelessWidget { final ToolUseBlock block; const _ToolUseCard({required this.block}); @override Widget build(BuildContext context) { final isAgent = block.isAgentCall; final color = isAgent ? AppColors.agent : AppColors.tool; final bgColor = isAgent ? AppColors.agentBg : AppColors.toolBg; final hasResult = block.linkedResult != null; final isError = block.linkedResult?.isError ?? false; // Look up subagent info if this is an Agent call AgentInfo? subagent; if (isAgent) { final session = context.read().session; subagent = session?.findAgentForToolUse(block.id); } return ExpandableCard( backgroundColor: bgColor, borderColor: color.withAlpha(40), header: Row( children: [ Icon( isAgent ? Icons.smart_toy_outlined : Icons.build_outlined, size: 14, color: color, ), const SizedBox(width: 6), Text( block.name, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: color, ), ), if (isAgent && block.subagentType != null) ...[ const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), decoration: BoxDecoration( color: color.withAlpha(20), borderRadius: BorderRadius.circular(3), ), child: Text( block.subagentType!, style: TextStyle(fontSize: 10, color: color), ), ), ], if (isAgent && block.agentDescription != null) ...[ const SizedBox(width: 8), Expanded( child: Text( block.agentDescription!, style: const TextStyle( fontSize: 11, color: AppColors.textSecondary), overflow: TextOverflow.ellipsis, ), ), ] else const Spacer(), // Subagent stats if (subagent != null) ...[ Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), decoration: BoxDecoration( color: AppColors.surfaceLight, borderRadius: BorderRadius.circular(3), ), child: Text( '${subagent.messages.where((m) => m.type == 'assistant').length} turns', style: const TextStyle(fontSize: 10, color: AppColors.textMuted), ), ), const SizedBox(width: 4), if (subagent.toolsUsed.isNotEmpty) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), decoration: BoxDecoration( color: AppColors.surfaceLight, borderRadius: BorderRadius.circular(3), ), child: Text( '${subagent.toolsUsed.length} tools', style: const TextStyle(fontSize: 10, color: AppColors.textMuted), ), ), const SizedBox(width: 4), ], if (hasResult) Icon( isError ? Icons.error_outline : Icons.check_circle_outline, size: 14, color: isError ? AppColors.error : AppColors.assistant, ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Arguments const Text( 'ARGUMENTS', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w600, color: AppColors.textMuted, letterSpacing: 1, ), ), const SizedBox(height: 4), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(6), ), child: JsonTreeView( data: block.input, initiallyExpanded: true, ), ), // Subagent conversation (inline nested timeline) if (subagent != null) ...[ const SizedBox(height: 12), _SubagentTimeline(agent: subagent), ], // Result if (hasResult) ...[ const SizedBox(height: 12), Text( isError ? 'ERROR' : 'RESULT', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w600, color: isError ? AppColors.error : AppColors.textMuted, letterSpacing: 1, ), ), const SizedBox(height: 4), Container( padding: const EdgeInsets.all(8), constraints: const BoxConstraints(maxHeight: 400), decoration: BoxDecoration( color: isError ? AppColors.errorBg : AppColors.surface, borderRadius: BorderRadius.circular(6), border: isError ? Border.all(color: AppColors.error.withAlpha(40)) : null, ), child: SingleChildScrollView( child: SelectableText( block.linkedResult!.textContent, style: TextStyle( fontFamily: 'JetBrains Mono', fontSize: 12, color: isError ? AppColors.error : AppColors.textSecondary, height: 1.5, ), ), ), ), ], // Raw tool_use JSON const SizedBox(height: 12), ExpandableCard( backgroundColor: AppColors.surface, borderColor: AppColors.surfaceBorder, header: const Row( children: [ Icon(Icons.data_object, size: 12, color: AppColors.textMuted), SizedBox(width: 4), Text( 'Raw Tool JSON', style: TextStyle(fontSize: 10, color: AppColors.textMuted), ), ], ), child: JsonTreeView(data: block.raw), ), ], ), ); } } // ─── Inline subagent conversation ──────────────────────────── class _SubagentTimeline extends StatelessWidget { final AgentInfo agent; const _SubagentTimeline({required this.agent}); @override Widget build(BuildContext context) { // Filter to user and assistant messages only final messages = agent.messages .where((m) => m.type == 'user' || m.type == 'assistant') .toList(); if (messages.isEmpty) { return const Text( 'No subagent conversation data available.', style: TextStyle(fontSize: 12, color: AppColors.textMuted, fontStyle: FontStyle.italic), ); } return ExpandableCard( backgroundColor: AppColors.agentBg, borderColor: AppColors.agent.withAlpha(30), header: Row( children: [ const Icon(Icons.account_tree_outlined, size: 14, color: AppColors.agent), const SizedBox(width: 6), Text( 'Subagent Conversation (${messages.length} messages)', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: AppColors.agent, ), ), if (agent.model != null) ...[ const Spacer(), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), decoration: BoxDecoration( color: AppColors.surfaceLight, borderRadius: BorderRadius.circular(3), ), child: Text( agent.model!, style: const TextStyle( fontSize: 10, color: AppColors.textSecondary, fontFamily: 'JetBrains Mono', ), ), ), ], ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Toolbelt summary if (agent.uniqueToolNames.isNotEmpty) ...[ Wrap( spacing: 4, runSpacing: 4, children: agent.toolUsageCounts.entries.map((e) { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: AppColors.tool.withAlpha(15), borderRadius: BorderRadius.circular(3), border: Border.all(color: AppColors.tool.withAlpha(30)), ), child: Text( '${e.key} (${e.value})', style: const TextStyle( fontSize: 10, color: AppColors.tool, fontFamily: 'JetBrains Mono', ), ), ); }).toList(), ), const SizedBox(height: 10), ], // Messages for (int i = 0; i < messages.length; i++) ...[ _SubagentMessageTile(entry: messages[i], index: i), if (i < messages.length - 1) const SizedBox(height: 4), ], ], ), ); } } class _SubagentMessageTile extends StatelessWidget { final LogEntry entry; final int index; const _SubagentMessageTile({required this.entry, required this.index}); @override Widget build(BuildContext context) { if (entry is UserEntry) { return _buildUserTile(entry as UserEntry); } if (entry is AssistantEntry) { return _buildAssistantTile(entry as AssistantEntry); } return const SizedBox.shrink(); } Widget _buildUserTile(UserEntry user) { if (user.isToolResult) return const SizedBox.shrink(); return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: AppColors.userBg, borderRadius: BorderRadius.circular(6), border: Border.all(color: AppColors.user.withAlpha(25)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon(Icons.person, size: 12, color: AppColors.user), const SizedBox(width: 6), Expanded( child: SelectableText( user.promptText, style: const TextStyle(fontSize: 12, color: AppColors.textPrimary, height: 1.4), ), ), ], ), ); } Widget _buildAssistantTile(AssistantEntry assistant) { return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(6), border: Border.all(color: AppColors.surfaceBorder), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Header row Row( children: [ const Icon(Icons.smart_toy, size: 12, color: AppColors.assistant), const SizedBox(width: 6), Text( assistant.model ?? 'assistant', style: const TextStyle(fontSize: 10, color: AppColors.assistant, fontWeight: FontWeight.w600), ), const Spacer(), if (assistant.usage != null) Text( 'in:${_fmt(assistant.usage!.inputTokens)} out:${_fmt(assistant.usage!.outputTokens)}', style: const TextStyle(fontSize: 9, color: AppColors.textMuted, fontFamily: 'JetBrains Mono'), ), ], ), // Text content for (final block in assistant.textBlocks) ...[ const SizedBox(height: 6), SelectableText( block.text, style: const TextStyle(fontSize: 12, color: AppColors.textPrimary, height: 1.4), ), ], // Tool uses for (final tool in assistant.toolUseBlocks) ...[ const SizedBox(height: 6), ExpandableCard( backgroundColor: AppColors.toolBg, borderColor: AppColors.tool.withAlpha(30), header: Row( children: [ const Icon(Icons.build_outlined, size: 12, color: AppColors.tool), const SizedBox(width: 4), Text( tool.name, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.tool), ), const Spacer(), if (tool.linkedResult != null) Icon( tool.linkedResult!.isError ? Icons.error_outline : Icons.check_circle_outline, size: 12, color: tool.linkedResult!.isError ? AppColors.error : AppColors.assistant, ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ JsonTreeView(data: tool.input, initiallyExpanded: true), if (tool.linkedResult != null) ...[ const SizedBox(height: 8), Container( constraints: const BoxConstraints(maxHeight: 200), child: SingleChildScrollView( child: SelectableText( tool.linkedResult!.textContent, style: TextStyle( fontFamily: 'JetBrains Mono', fontSize: 11, color: tool.linkedResult!.isError ? AppColors.error : AppColors.textSecondary, ), ), ), ), ], ], ), ), ], ], ), ); } String _fmt(int v) { if (v >= 1000) return '${(v / 1000).toStringAsFixed(1)}K'; return '$v'; } } // ─── Token badge ───────────────────────────────────────────── class _TokenBadge extends StatelessWidget { final String label; final int value; final Color color; const _TokenBadge({ required this.label, required this.value, required this.color, }); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: color.withAlpha(15), borderRadius: BorderRadius.circular(3), ), child: Text( '$label: ${_format(value)}', style: TextStyle( fontSize: 10, color: color.withAlpha(200), fontFamily: 'JetBrains Mono', ), ), ); } String _format(int v) { if (v >= 1000) return '${(v / 1000).toStringAsFixed(1)}K'; return '$v'; } }