claude_session_viewer/lib/models/log_entry.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

297 lines
8.7 KiB
Dart

import 'content_block.dart';
import 'token_usage.dart';
class LogEntry {
final String? uuid;
final String? parentUuid;
final String? sessionId;
final DateTime? timestamp;
final String? cwd;
final String? version;
final String? gitBranch;
final String type;
final bool isSidechain;
final Map<String, dynamic> raw;
LogEntry({
this.uuid,
this.parentUuid,
this.sessionId,
this.timestamp,
this.cwd,
this.version,
this.gitBranch,
required this.type,
this.isSidechain = false,
required this.raw,
});
factory LogEntry.fromJson(Map<String, dynamic> json) {
final type = json['type'] as String? ?? 'unknown';
switch (type) {
case 'user':
return UserEntry.fromJson(json);
case 'assistant':
return AssistantEntry.fromJson(json);
case 'system':
return SystemEntry.fromJson(json);
case 'progress':
return ProgressEntry.fromJson(json);
case 'file-history-snapshot':
return FileSnapshotEntry.fromJson(json);
default:
return _baseFromJson(json, type);
}
}
static LogEntry _baseFromJson(Map<String, dynamic> json, String type) {
return LogEntry(
uuid: json['uuid'] as String?,
parentUuid: json['parentUuid'] as String?,
sessionId: json['sessionId'] as String?,
timestamp: _parseTimestamp(json['timestamp']),
cwd: json['cwd'] as String?,
version: json['version'] as String?,
gitBranch: json['gitBranch'] as String?,
type: type,
isSidechain: json['isSidechain'] as bool? ?? false,
raw: json,
);
}
static DateTime? _parseTimestamp(dynamic ts) {
if (ts is String) return DateTime.tryParse(ts);
return null;
}
}
class UserEntry extends LogEntry {
final dynamic content;
UserEntry({
required this.content,
required super.uuid,
required super.parentUuid,
required super.sessionId,
required super.timestamp,
required super.cwd,
required super.version,
required super.gitBranch,
required super.isSidechain,
required super.raw,
}) : super(type: 'user');
String get promptText {
if (content is String) return content;
return '';
}
bool get isToolResult => content is List;
List<ToolResultData> get toolResults {
if (content is! List) return [];
return (content as List)
.where((c) => c is Map<String, dynamic> && c['type'] == 'tool_result')
.map((c) => ToolResultData.fromJson(c as Map<String, dynamic>))
.toList();
}
factory UserEntry.fromJson(Map<String, dynamic> json) {
final message = json['message'] as Map<String, dynamic>? ?? {};
return UserEntry(
content: message['content'],
uuid: json['uuid'] as String?,
parentUuid: json['parentUuid'] as String?,
sessionId: json['sessionId'] as String?,
timestamp: LogEntry._parseTimestamp(json['timestamp']),
cwd: json['cwd'] as String?,
version: json['version'] as String?,
gitBranch: json['gitBranch'] as String?,
isSidechain: json['isSidechain'] as bool? ?? false,
raw: json,
);
}
}
class AssistantEntry extends LogEntry {
final String? model;
final String? messageId;
final List<ContentBlock> contentBlocks;
final TokenUsage? usage;
final String? stopReason;
AssistantEntry({
this.model,
this.messageId,
required this.contentBlocks,
this.usage,
this.stopReason,
required super.uuid,
required super.parentUuid,
required super.sessionId,
required super.timestamp,
required super.cwd,
required super.version,
required super.gitBranch,
required super.isSidechain,
required super.raw,
}) : super(type: 'assistant');
List<TextBlock> get textBlocks =>
contentBlocks.whereType<TextBlock>().toList();
List<ThinkingBlock> get thinkingBlocks =>
contentBlocks.whereType<ThinkingBlock>().toList();
List<ToolUseBlock> get toolUseBlocks =>
contentBlocks.whereType<ToolUseBlock>().toList();
bool get hasText => textBlocks.isNotEmpty;
bool get hasThinking => thinkingBlocks.isNotEmpty;
bool get hasToolUse => toolUseBlocks.isNotEmpty;
factory AssistantEntry.fromJson(Map<String, dynamic> json) {
final message = json['message'] as Map<String, dynamic>? ?? {};
final contentList = message['content'] as List<dynamic>? ?? [];
final usageJson = message['usage'] as Map<String, dynamic>?;
return AssistantEntry(
model: message['model'] as String?,
messageId: message['id'] as String?,
contentBlocks: contentList
.whereType<Map<String, dynamic>>()
.map((c) => ContentBlock.fromJson(c))
.toList(),
usage: usageJson != null ? TokenUsage.fromJson(usageJson) : null,
stopReason: message['stop_reason'] as String?,
uuid: json['uuid'] as String?,
parentUuid: json['parentUuid'] as String?,
sessionId: json['sessionId'] as String?,
timestamp: LogEntry._parseTimestamp(json['timestamp']),
cwd: json['cwd'] as String?,
version: json['version'] as String?,
gitBranch: json['gitBranch'] as String?,
isSidechain: json['isSidechain'] as bool? ?? false,
raw: json,
);
}
}
class SystemEntry extends LogEntry {
final String? subtype;
final int? durationMs;
final String? slug;
SystemEntry({
this.subtype,
this.durationMs,
this.slug,
required super.uuid,
required super.parentUuid,
required super.sessionId,
required super.timestamp,
required super.cwd,
required super.version,
required super.gitBranch,
required super.isSidechain,
required super.raw,
}) : super(type: 'system');
factory SystemEntry.fromJson(Map<String, dynamic> json) {
return SystemEntry(
subtype: json['subtype'] as String?,
durationMs: json['durationMs'] as int?,
slug: json['slug'] as String?,
uuid: json['uuid'] as String?,
parentUuid: json['parentUuid'] as String?,
sessionId: json['sessionId'] as String?,
timestamp: LogEntry._parseTimestamp(json['timestamp']),
cwd: json['cwd'] as String?,
version: json['version'] as String?,
gitBranch: json['gitBranch'] as String?,
isSidechain: json['isSidechain'] as bool? ?? false,
raw: json,
);
}
}
class ProgressEntry extends LogEntry {
final String? slug;
final String? toolUseID;
final String? parentToolUseID;
final Map<String, dynamic> data;
ProgressEntry({
this.slug,
this.toolUseID,
this.parentToolUseID,
required this.data,
required super.uuid,
required super.parentUuid,
required super.sessionId,
required super.timestamp,
required super.cwd,
required super.version,
required super.gitBranch,
required super.isSidechain,
required super.raw,
}) : super(type: 'progress');
Map<String, dynamic>? get progressMessage =>
data['message'] as Map<String, dynamic>?;
factory ProgressEntry.fromJson(Map<String, dynamic> json) {
return ProgressEntry(
slug: json['slug'] as String?,
toolUseID: json['toolUseID'] as String?,
parentToolUseID: json['parentToolUseID'] as String?,
data: (json['data'] as Map<String, dynamic>?) ?? {},
uuid: json['uuid'] as String?,
parentUuid: json['parentUuid'] as String?,
sessionId: json['sessionId'] as String?,
timestamp: LogEntry._parseTimestamp(json['timestamp']),
cwd: json['cwd'] as String?,
version: json['version'] as String?,
gitBranch: json['gitBranch'] as String?,
isSidechain: json['isSidechain'] as bool? ?? false,
raw: json,
);
}
}
class FileSnapshotEntry extends LogEntry {
final String? messageId;
final Map<String, dynamic> snapshot;
final bool isSnapshotUpdate;
FileSnapshotEntry({
this.messageId,
required this.snapshot,
this.isSnapshotUpdate = false,
required super.uuid,
required super.parentUuid,
required super.sessionId,
required super.timestamp,
required super.cwd,
required super.version,
required super.gitBranch,
required super.isSidechain,
required super.raw,
}) : super(type: 'file-history-snapshot');
factory FileSnapshotEntry.fromJson(Map<String, dynamic> json) {
return FileSnapshotEntry(
messageId: json['messageId'] as String?,
snapshot: (json['snapshot'] as Map<String, dynamic>?) ?? {},
isSnapshotUpdate: json['isSnapshotUpdate'] as bool? ?? false,
uuid: json['uuid'] as String?,
parentUuid: json['parentUuid'] as String?,
sessionId: json['sessionId'] as String?,
timestamp: LogEntry._parseTimestamp(json['timestamp']),
cwd: json['cwd'] as String?,
version: json['version'] as String?,
gitBranch: json['gitBranch'] as String?,
isSidechain: json['isSidechain'] as bool? ?? false,
raw: json,
);
}
}