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>
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user