claude_session_viewer/lib/screens/agents/agents_screen.dart
Mathias Beaulieu-Duncan 364877d376 Initial commit: Claude Code session viewer (Flutter macOS)
A desktop app that parses Claude Code .jsonl session logs and provides
a rich UI for exploring conversations, tool usage, subagents, and token
consumption. Features include project browser with auto-discovery of
~/.claude/projects, conversation timeline with inline subagent expansion,
agents overview, toolbelt chart, and token usage dashboard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:17:23 -04:00

339 lines
9.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../models/agent_info.dart';
import '../../providers/session_provider.dart';
import '../../theme/app_theme.dart';
import '../../widgets/common/expandable_card.dart';
class AgentsScreen extends StatelessWidget {
const AgentsScreen({super.key});
@override
Widget build(BuildContext context) {
final provider = context.watch<SessionProvider>();
final session = provider.session;
if (session == null) {
return const Center(
child: Text('No session loaded',
style: TextStyle(color: AppColors.textMuted)),
);
}
return Scaffold(
backgroundColor: AppColors.background,
body: ListView(
padding: const EdgeInsets.all(24),
children: [
const Text(
'Agents Overview',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 8),
Text(
'${session.agents.length} agent(s) used in this session',
style: const TextStyle(fontSize: 13, color: AppColors.textSecondary),
),
const SizedBox(height: 24),
// Summary cards
Wrap(
spacing: 12,
runSpacing: 12,
children: [
_SummaryCard(
label: 'Total Agents',
value: '${session.agents.length}',
icon: Icons.smart_toy_outlined,
color: AppColors.agent,
),
_SummaryCard(
label: 'Total Tools Used',
value: '${session.toolsByName.length}',
icon: Icons.build_outlined,
color: AppColors.tool,
),
_SummaryCard(
label: 'Total Tokens',
value: _formatTokens(session.totalUsage.totalTokens),
icon: Icons.data_usage,
color: AppColors.assistant,
),
],
),
const SizedBox(height: 24),
// Agent cards
for (final agent in session.agents) ...[
_AgentCard(agent: agent),
const SizedBox(height: 12),
],
],
),
);
}
String _formatTokens(int tokens) {
if (tokens >= 1000000) return '${(tokens / 1000000).toStringAsFixed(1)}M';
if (tokens >= 1000) return '${(tokens / 1000).toStringAsFixed(1)}K';
return '$tokens';
}
}
class _SummaryCard extends StatelessWidget {
final String label;
final String value;
final IconData icon;
final Color color;
const _SummaryCard({
required this.label,
required this.value,
required this.icon,
required this.color,
});
@override
Widget build(BuildContext context) {
return Container(
width: 200,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.surfaceBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 20, color: color),
const SizedBox(height: 12),
Text(
value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(fontSize: 12, color: AppColors.textMuted),
),
],
),
);
}
}
class _AgentCard extends StatelessWidget {
final AgentInfo agent;
const _AgentCard({required this.agent});
@override
Widget build(BuildContext context) {
final isMain = agent.id == 'main';
final color = isMain ? AppColors.assistant : AppColors.agent;
return ExpandableCard(
initiallyExpanded: isMain,
backgroundColor: isMain ? AppColors.assistantBg : AppColors.agentBg,
borderColor: color.withAlpha(40),
header: Row(
children: [
Icon(
isMain ? Icons.smart_toy : Icons.smart_toy_outlined,
size: 16,
color: color,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
agent.name,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: color,
),
),
if (agent.subagentType != null)
Text(
agent.subagentType!,
style: const TextStyle(
fontSize: 11, color: AppColors.textMuted),
),
],
),
),
if (agent.model != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
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: [
// Stats row
Row(
children: [
_StatChip('Messages', '${agent.messageCount}'),
const SizedBox(width: 8),
_StatChip('Tool Calls', '${agent.toolsUsed.length}'),
const SizedBox(width: 8),
_StatChip(
'Input Tokens',
_formatTokens(agent.aggregatedUsage.inputTokens),
),
const SizedBox(width: 8),
_StatChip(
'Output Tokens',
_formatTokens(agent.aggregatedUsage.outputTokens),
),
],
),
const SizedBox(height: 12),
// Description
if (agent.description != null) ...[
const Text(
'DESCRIPTION',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: AppColors.textMuted,
letterSpacing: 1,
),
),
const SizedBox(height: 4),
Text(
agent.description!,
style: const TextStyle(
fontSize: 12, color: AppColors.textSecondary),
),
const SizedBox(height: 12),
],
// Prompt
if (agent.prompt != null) ...[
ExpandableCard(
backgroundColor: AppColors.surface,
borderColor: AppColors.surfaceBorder,
header: const Text(
'Agent Prompt',
style: TextStyle(fontSize: 11, color: AppColors.textMuted),
),
child: SelectableText(
agent.prompt!,
style: const TextStyle(
fontSize: 12,
color: AppColors.textSecondary,
height: 1.5,
),
),
),
const SizedBox(height: 8),
],
// Toolbelt
if (agent.uniqueToolNames.isNotEmpty) ...[
const Text(
'TOOLBELT',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: AppColors.textMuted,
letterSpacing: 1,
),
),
const SizedBox(height: 6),
Wrap(
spacing: 6,
runSpacing: 6,
children: agent.toolUsageCounts.entries.map((e) {
return Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColors.toolBg,
borderRadius: BorderRadius.circular(4),
border: Border.all(color: AppColors.tool.withAlpha(40)),
),
child: Text(
'${e.key} (${e.value})',
style: const TextStyle(
fontSize: 11,
color: AppColors.tool,
fontFamily: 'JetBrains Mono',
),
),
);
}).toList(),
),
],
],
),
);
}
String _formatTokens(int tokens) {
if (tokens >= 1000000) return '${(tokens / 1000000).toStringAsFixed(1)}M';
if (tokens >= 1000) return '${(tokens / 1000).toStringAsFixed(1)}K';
return '$tokens';
}
}
class _StatChip extends StatelessWidget {
final String label;
final String value;
const _StatChip(this.label, this.value);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColors.surfaceLight,
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'$label: ',
style: const TextStyle(fontSize: 11, color: AppColors.textMuted),
),
Text(
value,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
],
),
);
}
}