claude_session_viewer/lib/widgets/navigation/sidebar.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

189 lines
5.6 KiB
Dart

import 'package:flutter/material.dart';
import '../../theme/app_theme.dart';
enum SidebarScreen { home, timeline, agents, toolbelt, tokens }
class Sidebar extends StatelessWidget {
final SidebarScreen selected;
final ValueChanged<SidebarScreen> onSelect;
final bool hasSession;
const Sidebar({
super.key,
required this.selected,
required this.onSelect,
required this.hasSession,
});
@override
Widget build(BuildContext context) {
return Container(
width: 220,
decoration: const BoxDecoration(
color: AppColors.surface,
border: Border(
right: BorderSide(color: AppColors.surfaceBorder, width: 1),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 40),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: AppColors.assistant.withAlpha(30),
borderRadius: BorderRadius.circular(6),
),
child: const Icon(
Icons.terminal,
size: 16,
color: AppColors.assistant,
),
),
const SizedBox(width: 10),
const Text(
'Session Viewer',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
],
),
),
const SizedBox(height: 8),
_SidebarItem(
icon: Icons.folder_open,
label: 'Open Session',
isSelected: selected == SidebarScreen.home,
onTap: () => onSelect(SidebarScreen.home),
),
if (hasSession) ...[
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'SESSION',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: AppColors.textMuted,
letterSpacing: 1.2,
),
),
),
_SidebarItem(
icon: Icons.timeline,
label: 'Timeline',
isSelected: selected == SidebarScreen.timeline,
onTap: () => onSelect(SidebarScreen.timeline),
),
_SidebarItem(
icon: Icons.smart_toy_outlined,
label: 'Agents',
isSelected: selected == SidebarScreen.agents,
onTap: () => onSelect(SidebarScreen.agents),
),
_SidebarItem(
icon: Icons.build_outlined,
label: 'Toolbelt',
isSelected: selected == SidebarScreen.toolbelt,
onTap: () => onSelect(SidebarScreen.toolbelt),
),
_SidebarItem(
icon: Icons.data_usage,
label: 'Token Usage',
isSelected: selected == SidebarScreen.tokens,
onTap: () => onSelect(SidebarScreen.tokens),
),
],
const Spacer(),
Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Claude Session Viewer v0.1',
style: TextStyle(
fontSize: 10,
color: AppColors.textMuted.withAlpha(128),
),
),
),
],
),
);
}
}
class _SidebarItem extends StatefulWidget {
final IconData icon;
final String label;
final bool isSelected;
final VoidCallback onTap;
const _SidebarItem({
required this.icon,
required this.label,
required this.isSelected,
required this.onTap,
});
@override
State<_SidebarItem> createState() => _SidebarItemState();
}
class _SidebarItemState extends State<_SidebarItem> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: GestureDetector(
onTap: widget.onTap,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: widget.isSelected
? AppColors.assistant.withAlpha(20)
: _hovered
? AppColors.surfaceLight
: Colors.transparent,
borderRadius: BorderRadius.circular(6),
),
child: Row(
children: [
Icon(
widget.icon,
size: 18,
color: widget.isSelected
? AppColors.assistant
: AppColors.textSecondary,
),
const SizedBox(width: 10),
Text(
widget.label,
style: TextStyle(
fontSize: 13,
fontWeight:
widget.isSelected ? FontWeight.w600 : FontWeight.w400,
color: widget.isSelected
? AppColors.textPrimary
: AppColors.textSecondary,
),
),
],
),
),
),
);
}
}