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>
117 lines
3.0 KiB
Dart
117 lines
3.0 KiB
Dart
class ContentBlock {
|
|
final String type;
|
|
final Map<String, dynamic> raw;
|
|
|
|
ContentBlock({required this.type, required this.raw});
|
|
|
|
factory ContentBlock.fromJson(Map<String, dynamic> json) {
|
|
final type = json['type'] as String? ?? 'unknown';
|
|
switch (type) {
|
|
case 'text':
|
|
return TextBlock.fromJson(json);
|
|
case 'thinking':
|
|
return ThinkingBlock.fromJson(json);
|
|
case 'tool_use':
|
|
return ToolUseBlock.fromJson(json);
|
|
default:
|
|
return ContentBlock(type: type, raw: json);
|
|
}
|
|
}
|
|
}
|
|
|
|
class TextBlock extends ContentBlock {
|
|
final String text;
|
|
|
|
TextBlock({required this.text, required Map<String, dynamic> raw})
|
|
: super(type: 'text', raw: raw);
|
|
|
|
factory TextBlock.fromJson(Map<String, dynamic> json) {
|
|
return TextBlock(
|
|
text: json['text'] as String? ?? '',
|
|
raw: json,
|
|
);
|
|
}
|
|
}
|
|
|
|
class ThinkingBlock extends ContentBlock {
|
|
final String thinking;
|
|
final String? signature;
|
|
|
|
ThinkingBlock({
|
|
required this.thinking,
|
|
this.signature,
|
|
required Map<String, dynamic> raw,
|
|
}) : super(type: 'thinking', raw: raw);
|
|
|
|
factory ThinkingBlock.fromJson(Map<String, dynamic> json) {
|
|
return ThinkingBlock(
|
|
thinking: json['thinking'] as String? ?? '',
|
|
signature: json['signature'] as String?,
|
|
raw: json,
|
|
);
|
|
}
|
|
}
|
|
|
|
class ToolUseBlock extends ContentBlock {
|
|
final String id;
|
|
final String name;
|
|
final Map<String, dynamic> input;
|
|
ToolResultData? linkedResult;
|
|
|
|
ToolUseBlock({
|
|
required this.id,
|
|
required this.name,
|
|
required this.input,
|
|
this.linkedResult,
|
|
required Map<String, dynamic> raw,
|
|
}) : super(type: 'tool_use', raw: raw);
|
|
|
|
factory ToolUseBlock.fromJson(Map<String, dynamic> json) {
|
|
return ToolUseBlock(
|
|
id: json['id'] as String? ?? '',
|
|
name: json['name'] as String? ?? '',
|
|
input: (json['input'] as Map<String, dynamic>?) ?? {},
|
|
raw: json,
|
|
);
|
|
}
|
|
|
|
bool get isAgentCall => name == 'Agent';
|
|
String? get subagentType => isAgentCall ? input['subagent_type'] as String? : null;
|
|
String? get agentDescription => isAgentCall ? input['description'] as String? : null;
|
|
String? get agentPrompt => isAgentCall ? input['prompt'] as String? : null;
|
|
}
|
|
|
|
class ToolResultData {
|
|
final String toolUseId;
|
|
final dynamic content;
|
|
final bool isError;
|
|
final Map<String, dynamic> raw;
|
|
|
|
ToolResultData({
|
|
required this.toolUseId,
|
|
this.content,
|
|
this.isError = false,
|
|
required this.raw,
|
|
});
|
|
|
|
String get textContent {
|
|
if (content is String) return content;
|
|
if (content is List) {
|
|
return (content as List)
|
|
.where((c) => c is Map && c['type'] == 'text')
|
|
.map((c) => c['text'] as String? ?? '')
|
|
.join('\n');
|
|
}
|
|
return content?.toString() ?? '';
|
|
}
|
|
|
|
factory ToolResultData.fromJson(Map<String, dynamic> json) {
|
|
return ToolResultData(
|
|
toolUseId: json['tool_use_id'] as String? ?? '',
|
|
content: json['content'],
|
|
isError: json['is_error'] as bool? ?? false,
|
|
raw: json,
|
|
);
|
|
}
|
|
}
|