Files
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

560 lines
18 KiB
Dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gpt_markdown/gpt_markdown.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../../shared/l10n/l10n.dart';
import '../../../shared/theme/theme.dart';
import 'observer_models.dart';
/// Renders a single [TranscriptItem] in the agent activity transcript.
class TranscriptItemWidget extends StatelessWidget {
final TranscriptItem item;
const TranscriptItemWidget({super.key, required this.item});
@override
Widget build(BuildContext context) {
return switch (item) {
final MessageItem i => _MessageItemWidget(item: i),
final ThoughtItem i => _ThoughtItemWidget(item: i),
final LifecycleItem i => _LifecycleItemWidget(item: i),
final MetadataItem i => _MetadataItemWidget(item: i),
final ToolItem i => _ToolItemWidget(item: i),
};
}
}
class _MessageItemWidget extends StatelessWidget {
final MessageItem item;
const _MessageItemWidget({required this.item});
@override
Widget build(BuildContext context) {
final isAssistant = item.role == 'assistant';
final badgeColor = isAssistant
? context.colors.primary
: context.colors.onSurfaceVariant;
final badgeLabel = isAssistant
? context.l10n.transcriptAssistant
: context.l10n.transcriptUser;
return Padding(
padding: const EdgeInsets.symmetric(vertical: Grid.half),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: Grid.xxs,
vertical: Grid.quarter,
),
decoration: BoxDecoration(
color: badgeColor.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
),
child: Text(
badgeLabel,
style: context.textTheme.labelSmall?.copyWith(
color: badgeColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: Grid.half),
if (item.text.isNotEmpty)
GptMarkdown(
item.text,
style: context.textTheme.bodyMedium?.copyWith(
color: context.colors.onSurface,
),
),
],
),
);
}
}
class _ThoughtItemWidget extends HookWidget {
final ThoughtItem item;
const _ThoughtItemWidget({required this.item});
@override
Widget build(BuildContext context) {
final expanded = useState(item.text.length <= 200);
useEffect(() {
expanded.value = item.text.length <= 200;
return null;
}, [item.id]);
return Padding(
padding: const EdgeInsets.symmetric(vertical: Grid.half),
child: GestureDetector(
onTap: () => expanded.value = !expanded.value,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(Grid.xxs),
decoration: BoxDecoration(
color: context.colors.surfaceContainerHighest.withValues(
alpha: 0.5,
),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
LucideIcons.brain,
size: 14,
color: context.colors.onSurfaceVariant,
),
const SizedBox(width: Grid.half),
Expanded(
child: Text(
_localizedTranscriptLabel(context, item.title),
style: context.textTheme.labelMedium?.copyWith(
color: context.colors.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
),
Icon(
expanded.value
? LucideIcons.chevronUp
: LucideIcons.chevronDown,
size: 14,
color: context.colors.onSurfaceVariant,
),
],
),
if (expanded.value && item.text.isNotEmpty) ...[
const SizedBox(height: Grid.half),
GptMarkdown(
item.text,
style: context.textTheme.bodySmall?.copyWith(
color: context.colors.onSurfaceVariant,
),
),
],
],
),
),
),
);
}
}
class _LifecycleItemWidget extends StatelessWidget {
final LifecycleItem item;
const _LifecycleItemWidget({required this.item});
@override
Widget build(BuildContext context) {
final localizedText = _localizedTranscriptText(context, item.text);
return Padding(
padding: const EdgeInsets.symmetric(vertical: Grid.half),
child: Center(
child: Text(
'${_localizedTranscriptLabel(context, item.title)}'
'${localizedText.isNotEmpty ? ' \u2014 $localizedText' : ''}',
style: context.textTheme.labelSmall?.copyWith(
color: context.colors.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
),
);
}
}
class _MetadataItemWidget extends HookWidget {
final MetadataItem item;
const _MetadataItemWidget({required this.item});
@override
Widget build(BuildContext context) {
final expanded = useState(false);
useEffect(() {
expanded.value = false;
return null;
}, [item.id]);
return Padding(
padding: const EdgeInsets.symmetric(vertical: Grid.half),
child: GestureDetector(
onTap: () => expanded.value = !expanded.value,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(Grid.xxs),
decoration: BoxDecoration(
color: context.colors.surfaceContainerHighest.withValues(
alpha: 0.3,
),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
LucideIcons.fileText,
size: 14,
color: context.colors.onSurfaceVariant,
),
const SizedBox(width: Grid.half),
Expanded(
child: Text(
'${_localizedTranscriptLabel(context, item.title)} '
'(${context.l10n.transcriptSections(item.sections.length)})',
style: context.textTheme.labelMedium?.copyWith(
color: context.colors.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
),
Icon(
expanded.value
? LucideIcons.chevronUp
: LucideIcons.chevronDown,
size: 14,
color: context.colors.onSurfaceVariant,
),
],
),
if (expanded.value)
for (final section in item.sections) ...[
const SizedBox(height: Grid.xxs),
Text(
_localizedTranscriptLabel(context, section.title),
style: context.textTheme.labelSmall?.copyWith(
color: context.colors.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
),
if (section.body.isNotEmpty) ...[
const SizedBox(height: Grid.quarter),
Text(
section.body.length > 500
? '${section.body.substring(0, 500)}\u2026'
: section.body,
style: context.textTheme.bodySmall?.copyWith(
color: context.colors.onSurfaceVariant,
),
),
],
],
],
),
),
),
);
}
}
class _ToolItemWidget extends HookWidget {
final ToolItem item;
const _ToolItemWidget({required this.item});
@override
Widget build(BuildContext context) {
final argsExpanded = useState(false);
final resultExpanded = useState(false);
useEffect(() {
argsExpanded.value = false;
resultExpanded.value = false;
return null;
}, [item.id]);
final (statusColor, statusLabel, statusIcon) = _toolStatusDisplay(
item.status,
item.isError,
context,
);
final displayName = _formatToolName(item.toolName);
return Padding(
padding: const EdgeInsets.symmetric(vertical: Grid.half),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(Grid.xxs),
decoration: BoxDecoration(
color: context.colors.surfaceContainerHighest.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(8),
border: item.isError
? Border.all(color: context.colors.error.withValues(alpha: 0.3))
: null,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header: tool name + status badge
Row(
children: [
Icon(LucideIcons.wrench, size: 14, color: statusColor),
const SizedBox(width: Grid.half),
Expanded(
child: Text(
displayName,
style: context.textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: Grid.xxs,
vertical: Grid.quarter,
),
decoration: BoxDecoration(
color: statusColor.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(statusIcon, size: 10, color: statusColor),
const SizedBox(width: Grid.quarter),
Text(
statusLabel,
style: context.textTheme.labelSmall?.copyWith(
color: statusColor,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
// Args section
if (item.args.isNotEmpty) ...[
const SizedBox(height: Grid.half),
GestureDetector(
onTap: () => argsExpanded.value = !argsExpanded.value,
child: Row(
children: [
Text(
context.l10n.transcriptArguments,
style: context.textTheme.labelSmall?.copyWith(
color: context.colors.onSurfaceVariant,
),
),
const SizedBox(width: Grid.half),
Icon(
argsExpanded.value
? LucideIcons.chevronUp
: LucideIcons.chevronDown,
size: 12,
color: context.colors.onSurfaceVariant,
),
],
),
),
if (argsExpanded.value) ...[
const SizedBox(height: Grid.quarter),
_CodeBlock(text: _prettyJson(item.args)),
],
],
// Result section
if (item.result.isNotEmpty) ...[
const SizedBox(height: Grid.half),
GestureDetector(
onTap: () => resultExpanded.value = !resultExpanded.value,
child: Row(
children: [
Text(
context.l10n.transcriptResult,
style: context.textTheme.labelSmall?.copyWith(
color: item.isError
? context.colors.error
: context.colors.onSurfaceVariant,
),
),
const SizedBox(width: Grid.half),
Icon(
resultExpanded.value
? LucideIcons.chevronUp
: LucideIcons.chevronDown,
size: 12,
color: item.isError
? context.colors.error
: context.colors.onSurfaceVariant,
),
],
),
),
if (resultExpanded.value) ...[
const SizedBox(height: Grid.quarter),
_CodeBlock(
text: item.result.length > 2000
? '${item.result.substring(0, 2000)}\n\n'
'${context.l10n.transcriptTruncated}'
: item.result,
isError: item.isError,
),
],
],
],
),
),
);
}
}
(Color, String, IconData) _toolStatusDisplay(
ToolStatus status,
bool isError,
BuildContext context,
) {
if (isError || status == ToolStatus.failed) {
return (
context.colors.error,
context.l10n.transcriptError,
LucideIcons.circleX,
);
}
if (status == ToolStatus.completed) {
return (
context.appColors.success,
context.l10n.transcriptDone,
LucideIcons.circleCheck,
);
}
if (status == ToolStatus.pending) {
return (
context.colors.onSurfaceVariant,
context.l10n.transcriptPending,
LucideIcons.circleDot,
);
}
return (
context.appColors.warning,
context.l10n.transcriptRunning,
LucideIcons.clock3,
);
}
String _formatToolName(String toolName) {
return toolName
.split('_')
.map(
(part) =>
part.isEmpty ? '' : '${part[0].toUpperCase()}${part.substring(1)}',
)
.join(' ');
}
String _localizedTranscriptLabel(BuildContext context, String label) {
final l10n = context.l10n;
return switch (label) {
'Assistant' => l10n.transcriptAssistant,
'Base' => l10n.transcriptBase,
'Buzz event' => l10n.transcriptBuzzEvent,
'Channel Canvas' => l10n.transcriptChannelCanvas,
'Core Memory' => l10n.transcriptCoreMemory,
'Plan' => l10n.transcriptPlan,
'Prompt' => l10n.prompt,
'Prompt context' => l10n.transcriptPromptContext,
'Session ready' => l10n.transcriptSessionReady,
'System' => l10n.transcriptSystem,
'System prompt' => l10n.transcriptSystemPrompt,
'Team Instructions' => l10n.transcriptTeamInstructions,
'Thinking' => l10n.transcriptThinking,
'Tool call' => l10n.transcriptToolCall,
'Turn started' => l10n.transcriptTurnStarted,
'User' => l10n.transcriptUser,
'Wire parse error' => l10n.transcriptWireParseError,
_ => label,
};
}
String _localizedTranscriptText(BuildContext context, String text) {
final l10n = context.l10n;
if (text == 'Heartbeat or internal turn.') {
return l10n.transcriptInternalTurn;
}
if (text == 'Using existing ACP session.') {
return l10n.transcriptUsingExistingSession;
}
if (text.startsWith('Triggered by ') && text.endsWith('.')) {
return l10n.transcriptTriggeredBy(
text.substring('Triggered by '.length, text.length - 1),
);
}
if (text.startsWith('Created session ') && text.endsWith('.')) {
return l10n.transcriptCreatedSession(
text.substring('Created session '.length, text.length - 1),
);
}
if (text.startsWith('Using session ') && text.endsWith('.')) {
return l10n.transcriptUsingSession(
text.substring('Using session '.length, text.length - 1),
);
}
return text;
}
String _prettyJson(Map<String, dynamic> value) {
try {
return const JsonEncoder.withIndent(' ').convert(value);
} catch (_) {
return value.toString();
}
}
class _CodeBlock extends StatelessWidget {
final String text;
final bool isError;
const _CodeBlock({required this.text, this.isError = false});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(Grid.xxs),
decoration: BoxDecoration(
color: isError
? context.colors.error.withValues(alpha: 0.06)
: context.colors.surface,
borderRadius: BorderRadius.circular(6),
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Text(
text,
softWrap: false,
style: context.textTheme.bodySmall?.copyWith(
fontFamily: 'monospace',
fontSize: 11,
color: isError
? context.colors.error
: context.colors.onSurfaceVariant,
),
),
),
);
}
}