neoagent 3.0.1-beta.2 → 3.0.1-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +19 -0
- package/README.md +16 -0
- package/docs/benchmarking.md +102 -0
- package/docs/billing.md +224 -0
- package/docs/configuration.md +22 -0
- package/docs/index.md +1 -0
- package/flutter_app/lib/main.dart +4 -1
- package/flutter_app/lib/main_app_shell.dart +316 -0
- package/flutter_app/lib/main_billing.dart +1465 -0
- package/flutter_app/lib/main_chat.dart +1568 -231
- package/flutter_app/lib/main_controller.dart +263 -26
- package/flutter_app/lib/main_devices.dart +1 -1
- package/flutter_app/lib/main_install.dart +1147 -0
- package/flutter_app/lib/main_navigation.dart +12 -0
- package/flutter_app/lib/main_operations.dart +134 -9
- package/flutter_app/lib/main_settings.dart +148 -52
- package/flutter_app/lib/main_shared.dart +1 -0
- package/flutter_app/lib/src/backend_client.dart +69 -1
- package/landing/index.html +2 -2
- package/lib/manager.js +156 -0
- package/lib/schema_migrations.js +84 -0
- package/package.json +10 -4
- package/server/admin/access.js +12 -7
- package/server/admin/admin.css +78 -0
- package/server/admin/admin.js +511 -23
- package/server/admin/billing.js +415 -0
- package/server/admin/index.html +120 -13
- package/server/admin/users.js +15 -15
- package/server/db/database.js +13 -21
- package/server/db/sessions_db.js +8 -0
- package/server/http/middleware.js +4 -4
- package/server/http/routes.js +9 -0
- package/server/http/static.js +4 -2
- package/server/middleware/requireBilling.js +12 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +89120 -85431
- package/server/routes/admin.js +524 -29
- package/server/routes/agents.js +203 -21
- package/server/routes/billing.js +127 -0
- package/server/routes/billing_webhook.js +43 -0
- package/server/routes/memory.js +1 -4
- package/server/routes/settings.js +1 -2
- package/server/routes/skills.js +1 -1
- package/server/routes/triggers.js +2 -2
- package/server/services/account/sessions.js +1 -9
- package/server/services/ai/loop/agent_engine_core.js +42 -0
- package/server/services/ai/loop/blank_recovery.js +36 -0
- package/server/services/ai/loop/completion_judge.js +42 -0
- package/server/services/ai/loop/conversation_loop.js +248 -34
- package/server/services/ai/loop/progress_monitor.js +6 -6
- package/server/services/ai/loopPolicy.js +37 -44
- package/server/services/ai/messagingFallback.js +25 -1
- package/server/services/ai/models.js +18 -0
- package/server/services/ai/preModelCompaction.js +25 -5
- package/server/services/ai/rate_limits.js +28 -4
- package/server/services/ai/systemPrompt.js +23 -5
- package/server/services/ai/taskAnalysis.js +2 -0
- package/server/services/ai/tools.js +231 -20
- package/server/services/billing/billing_email.js +106 -0
- package/server/services/billing/config.js +17 -0
- package/server/services/billing/plans.js +121 -0
- package/server/services/billing/stripe_client.js +21 -0
- package/server/services/billing/subscriptions.js +462 -0
- package/server/services/billing/trial_guard.js +111 -0
- package/server/services/browser/contentExtractor.js +629 -0
- package/server/services/browser/controller.js +4 -8
- package/server/services/desktop/gateway.js +7 -4
- package/server/services/integrations/google/calendar.js +30 -2
- package/server/services/integrations/secrets.js +7 -4
- package/server/services/manager.js +11 -0
- package/server/services/messaging/automation.js +1 -1
- package/server/services/messaging/telnyx.js +12 -11
- package/server/services/messaging/whatsapp.js +1 -1
- package/server/services/recordings/manager.js +13 -7
- package/server/services/runtime/backends/local-vm.js +40 -22
- package/server/services/tasks/runtime.js +103 -15
- package/server/services/tasks/schedule_utils.js +5 -5
- package/server/services/voice/runtimeManager.js +19 -10
- package/server/services/wearable/gateway.js +8 -5
- package/server/services/websocket.js +26 -8
- package/server/services/workspace/manager.js +37 -3
|
@@ -11,6 +11,7 @@ class ChatPanel extends StatefulWidget {
|
|
|
11
11
|
|
|
12
12
|
class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
13
13
|
static const double _autoScrollBottomThreshold = 120;
|
|
14
|
+
static const double _olderHistoryLoadThreshold = 180;
|
|
14
15
|
static const int _autoScrollSettlePasses = 4;
|
|
15
16
|
|
|
16
17
|
late final TextEditingController _composerController;
|
|
@@ -21,12 +22,19 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
21
22
|
String _lastScrollContentSignature = '';
|
|
22
23
|
bool _stickToBottom = true;
|
|
23
24
|
bool _ignoreScrollUpdates = false;
|
|
25
|
+
bool _loadingOlderHistory = false;
|
|
24
26
|
int _scrollGeneration = 0;
|
|
27
|
+
// Opacity-hide the list while the initial batch of messages settles to the
|
|
28
|
+
// bottom, so the user never sees the layout jitter across settle passes.
|
|
29
|
+
bool _awaitingInitialScrollSettle = false;
|
|
30
|
+
int _visibleMessageCountAtLastSettle = 0;
|
|
25
31
|
bool _isSendingChatMessage = false;
|
|
26
32
|
bool _isDictating = false;
|
|
27
33
|
bool _isTranscribing = false;
|
|
28
34
|
LiveVoiceCapture? _dictationCapture;
|
|
29
35
|
final List<Uint8List> _dictationChunks = [];
|
|
36
|
+
Timer? _connectionBannerTimer;
|
|
37
|
+
bool _showConnectionBanner = false;
|
|
30
38
|
|
|
31
39
|
@override
|
|
32
40
|
void initState() {
|
|
@@ -36,6 +44,7 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
36
44
|
_composerController.addListener(_handleComposerLayoutChanged);
|
|
37
45
|
_scrollController.addListener(_handleScrollPositionChanged);
|
|
38
46
|
widget.controller.addListener(_consumeQueuedDraft);
|
|
47
|
+
widget.controller.addListener(_updateConnectionBanner);
|
|
39
48
|
_consumeQueuedDraft();
|
|
40
49
|
_scheduleScrollToBottom(force: true);
|
|
41
50
|
}
|
|
@@ -45,24 +54,49 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
45
54
|
super.didUpdateWidget(oldWidget);
|
|
46
55
|
if (oldWidget.controller != widget.controller) {
|
|
47
56
|
oldWidget.controller.removeListener(_consumeQueuedDraft);
|
|
57
|
+
oldWidget.controller.removeListener(_updateConnectionBanner);
|
|
48
58
|
widget.controller.addListener(_consumeQueuedDraft);
|
|
59
|
+
widget.controller.addListener(_updateConnectionBanner);
|
|
49
60
|
_appliedSharedPayloadSignature = null;
|
|
50
61
|
_lastScrollContentSignature = '';
|
|
51
62
|
_stickToBottom = true;
|
|
63
|
+
_awaitingInitialScrollSettle = false;
|
|
64
|
+
_visibleMessageCountAtLastSettle = 0;
|
|
52
65
|
_consumeQueuedDraft();
|
|
53
66
|
_scheduleScrollToBottom(force: true);
|
|
54
67
|
}
|
|
55
68
|
}
|
|
56
69
|
|
|
70
|
+
void _updateConnectionBanner() {
|
|
71
|
+
final connected = widget.controller.socketConnected;
|
|
72
|
+
if (connected) {
|
|
73
|
+
_connectionBannerTimer?.cancel();
|
|
74
|
+
_connectionBannerTimer = null;
|
|
75
|
+
if (_showConnectionBanner) {
|
|
76
|
+
setState(() => _showConnectionBanner = false);
|
|
77
|
+
}
|
|
78
|
+
} else if (!_showConnectionBanner && !widget.controller.isBooting) {
|
|
79
|
+
// ??= keeps the first timer alive; repeated disconnect events don't reset the 2s clock.
|
|
80
|
+
_connectionBannerTimer ??= Timer(const Duration(seconds: 2), () {
|
|
81
|
+
if (mounted && !widget.controller.socketConnected) {
|
|
82
|
+
setState(() => _showConnectionBanner = true);
|
|
83
|
+
}
|
|
84
|
+
_connectionBannerTimer = null;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
57
89
|
@override
|
|
58
90
|
void dispose() {
|
|
59
91
|
WidgetsBinding.instance.removeObserver(this);
|
|
60
92
|
widget.controller.removeListener(_consumeQueuedDraft);
|
|
93
|
+
widget.controller.removeListener(_updateConnectionBanner);
|
|
61
94
|
_composerController.removeListener(_handleComposerLayoutChanged);
|
|
62
95
|
_scrollController.removeListener(_handleScrollPositionChanged);
|
|
63
96
|
_composerController.dispose();
|
|
64
97
|
_scrollController.dispose();
|
|
65
98
|
_dictationCapture?.dispose();
|
|
99
|
+
_connectionBannerTimer?.cancel();
|
|
66
100
|
super.dispose();
|
|
67
101
|
}
|
|
68
102
|
|
|
@@ -267,8 +301,18 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
267
301
|
return pos.pixels >= pos.maxScrollExtent - _autoScrollBottomThreshold;
|
|
268
302
|
}
|
|
269
303
|
|
|
304
|
+
bool get _isNearTop {
|
|
305
|
+
if (!_scrollController.hasClients) return false;
|
|
306
|
+
final pos = _scrollController.position;
|
|
307
|
+
if (!pos.hasContentDimensions) return false;
|
|
308
|
+
return pos.pixels <= _olderHistoryLoadThreshold;
|
|
309
|
+
}
|
|
310
|
+
|
|
270
311
|
void _handleScrollPositionChanged() {
|
|
271
312
|
if (_ignoreScrollUpdates || !_scrollController.hasClients) return;
|
|
313
|
+
if (_isNearTop) {
|
|
314
|
+
unawaited(_maybeLoadOlderHistory());
|
|
315
|
+
}
|
|
272
316
|
final nearBottom = _isNearBottom;
|
|
273
317
|
if (_stickToBottom && !nearBottom) {
|
|
274
318
|
_scrollGeneration++;
|
|
@@ -278,6 +322,46 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
278
322
|
}
|
|
279
323
|
}
|
|
280
324
|
|
|
325
|
+
Future<void> _maybeLoadOlderHistory() async {
|
|
326
|
+
final controller = widget.controller;
|
|
327
|
+
if (_loadingOlderHistory ||
|
|
328
|
+
controller.isLoadingOlderChatHistory ||
|
|
329
|
+
!controller.chatHistoryHasMore ||
|
|
330
|
+
!_isNearTop) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
_loadingOlderHistory = true;
|
|
334
|
+
final hasClients = _scrollController.hasClients;
|
|
335
|
+
final previousPixels = hasClients ? _scrollController.position.pixels : 0.0;
|
|
336
|
+
final previousMaxExtent = hasClients
|
|
337
|
+
? _scrollController.position.maxScrollExtent
|
|
338
|
+
: 0.0;
|
|
339
|
+
final loaded = await controller.loadOlderChatHistory();
|
|
340
|
+
if (!mounted || !loaded) {
|
|
341
|
+
_loadingOlderHistory = false;
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
345
|
+
if (!mounted || !_scrollController.hasClients) {
|
|
346
|
+
_loadingOlderHistory = false;
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
final position = _scrollController.position;
|
|
350
|
+
final extentDelta = position.maxScrollExtent - previousMaxExtent;
|
|
351
|
+
final targetOffset = (previousPixels + extentDelta).clamp(
|
|
352
|
+
0.0,
|
|
353
|
+
position.maxScrollExtent,
|
|
354
|
+
);
|
|
355
|
+
_ignoreScrollUpdates = true;
|
|
356
|
+
_scrollController.jumpTo(targetOffset.toDouble());
|
|
357
|
+
_ignoreScrollUpdates = false;
|
|
358
|
+
_loadingOlderHistory = false;
|
|
359
|
+
if (_isNearTop) {
|
|
360
|
+
unawaited(_maybeLoadOlderHistory());
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
281
365
|
void _openModelPicker() {
|
|
282
366
|
final controller = widget.controller;
|
|
283
367
|
if (controller.hasLiveRun) return;
|
|
@@ -368,6 +452,12 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
368
452
|
settle(remainingPasses - 1);
|
|
369
453
|
}
|
|
370
454
|
});
|
|
455
|
+
} else if (_awaitingInitialScrollSettle) {
|
|
456
|
+
setState(() {
|
|
457
|
+
_awaitingInitialScrollSettle = false;
|
|
458
|
+
_visibleMessageCountAtLastSettle =
|
|
459
|
+
widget.controller.visibleChatMessages.length;
|
|
460
|
+
});
|
|
371
461
|
}
|
|
372
462
|
});
|
|
373
463
|
}
|
|
@@ -384,6 +474,15 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
384
474
|
final isInitialContent = _lastScrollContentSignature.isEmpty;
|
|
385
475
|
final shouldFollow = _stickToBottom || isInitialContent;
|
|
386
476
|
_lastScrollContentSignature = signature;
|
|
477
|
+
|
|
478
|
+
if (messages.isEmpty) {
|
|
479
|
+
// Agent switch resets the settle tracker so the next batch triggers hiding.
|
|
480
|
+
_visibleMessageCountAtLastSettle = 0;
|
|
481
|
+
} else if (_visibleMessageCountAtLastSettle == 0 && !_awaitingInitialScrollSettle) {
|
|
482
|
+
// Messages just went from 0 → N: hide until the scroll position settles.
|
|
483
|
+
_awaitingInitialScrollSettle = true;
|
|
484
|
+
}
|
|
485
|
+
|
|
387
486
|
if (shouldFollow) {
|
|
388
487
|
_scheduleScrollToBottom(force: isInitialContent);
|
|
389
488
|
}
|
|
@@ -396,6 +495,18 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
396
495
|
_maybeFollowChatContent(messages, controller);
|
|
397
496
|
|
|
398
497
|
final threadChildren = <Widget>[
|
|
498
|
+
if (controller.isLoadingOlderChatHistory) ...<Widget>[
|
|
499
|
+
const Padding(
|
|
500
|
+
padding: EdgeInsets.only(bottom: 16),
|
|
501
|
+
child: Center(
|
|
502
|
+
child: SizedBox(
|
|
503
|
+
width: 20,
|
|
504
|
+
height: 20,
|
|
505
|
+
child: CircularProgressIndicator(strokeWidth: 2),
|
|
506
|
+
),
|
|
507
|
+
),
|
|
508
|
+
),
|
|
509
|
+
],
|
|
399
510
|
if (controller.usageAndLimits case final usage?
|
|
400
511
|
when usage.hasLimits) ...<Widget>[
|
|
401
512
|
_RateLimitStatusCard(usage: usage),
|
|
@@ -471,26 +582,29 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
471
582
|
Expanded(
|
|
472
583
|
child: Stack(
|
|
473
584
|
children: <Widget>[
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
child:
|
|
488
|
-
|
|
489
|
-
|
|
585
|
+
Opacity(
|
|
586
|
+
opacity: _awaitingInitialScrollSettle ? 0.0 : 1.0,
|
|
587
|
+
child: SelectionArea(
|
|
588
|
+
child: ListView(
|
|
589
|
+
controller: _scrollController,
|
|
590
|
+
padding: EdgeInsets.fromLTRB(
|
|
591
|
+
sidePadding,
|
|
592
|
+
30,
|
|
593
|
+
sidePadding,
|
|
594
|
+
18,
|
|
595
|
+
),
|
|
596
|
+
children: <Widget>[
|
|
597
|
+
Center(
|
|
598
|
+
child: ConstrainedBox(
|
|
599
|
+
constraints: const BoxConstraints(maxWidth: 860),
|
|
600
|
+
child: Column(
|
|
601
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
602
|
+
children: threadChildren,
|
|
603
|
+
),
|
|
490
604
|
),
|
|
491
605
|
),
|
|
492
|
-
|
|
493
|
-
|
|
606
|
+
],
|
|
607
|
+
),
|
|
494
608
|
),
|
|
495
609
|
),
|
|
496
610
|
if (!_stickToBottom)
|
|
@@ -518,6 +632,20 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
518
632
|
constraints: const BoxConstraints(maxWidth: 860),
|
|
519
633
|
child: Column(
|
|
520
634
|
children: <Widget>[
|
|
635
|
+
if (_showConnectionBanner)
|
|
636
|
+
Padding(
|
|
637
|
+
padding: const EdgeInsets.only(bottom: 8),
|
|
638
|
+
child: _ConnectionReconnectingBanner(
|
|
639
|
+
hasNetwork: controller.hasNetworkConnection,
|
|
640
|
+
),
|
|
641
|
+
),
|
|
642
|
+
if (controller.pendingApproval != null)
|
|
643
|
+
Padding(
|
|
644
|
+
padding: const EdgeInsets.only(bottom: 8),
|
|
645
|
+
child: _PendingApprovalBanner(
|
|
646
|
+
approval: controller.pendingApproval!,
|
|
647
|
+
),
|
|
648
|
+
),
|
|
521
649
|
if (_pendingSharedAttachments.isNotEmpty)
|
|
522
650
|
Padding(
|
|
523
651
|
padding: const EdgeInsets.only(bottom: 10),
|
|
@@ -1414,6 +1542,10 @@ class _MessagingPanelState extends State<MessagingPanel> {
|
|
|
1414
1542
|
),
|
|
1415
1543
|
const SizedBox(height: 22),
|
|
1416
1544
|
],
|
|
1545
|
+
if (controller.ignoredChats.isNotEmpty) ...[
|
|
1546
|
+
const SizedBox(height: 18),
|
|
1547
|
+
_IgnoredChatsPanel(controller: controller),
|
|
1548
|
+
],
|
|
1417
1549
|
_MessagingActivityPanel(messages: controller.messagingMessages),
|
|
1418
1550
|
],
|
|
1419
1551
|
);
|
|
@@ -1839,6 +1971,101 @@ class _MessagingGroupHeader extends StatelessWidget {
|
|
|
1839
1971
|
}
|
|
1840
1972
|
}
|
|
1841
1973
|
|
|
1974
|
+
class _IgnoredChatsPanel extends StatelessWidget {
|
|
1975
|
+
const _IgnoredChatsPanel({required this.controller});
|
|
1976
|
+
|
|
1977
|
+
final NeoAgentController controller;
|
|
1978
|
+
|
|
1979
|
+
@override
|
|
1980
|
+
Widget build(BuildContext context) {
|
|
1981
|
+
final ignored = controller.ignoredChats;
|
|
1982
|
+
return Container(
|
|
1983
|
+
padding: const EdgeInsets.all(18),
|
|
1984
|
+
decoration: BoxDecoration(
|
|
1985
|
+
color: _bgCard,
|
|
1986
|
+
borderRadius: BorderRadius.circular(8),
|
|
1987
|
+
border: Border.all(color: _borderLight),
|
|
1988
|
+
),
|
|
1989
|
+
child: Column(
|
|
1990
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1991
|
+
children: [
|
|
1992
|
+
Row(
|
|
1993
|
+
children: [
|
|
1994
|
+
Expanded(
|
|
1995
|
+
child: Column(
|
|
1996
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1997
|
+
children: [
|
|
1998
|
+
Text(
|
|
1999
|
+
'Ignored Channels',
|
|
2000
|
+
style: TextStyle(
|
|
2001
|
+
color: _textPrimary,
|
|
2002
|
+
fontSize: 18,
|
|
2003
|
+
fontWeight: FontWeight.w800,
|
|
2004
|
+
),
|
|
2005
|
+
),
|
|
2006
|
+
const SizedBox(height: 3),
|
|
2007
|
+
Text(
|
|
2008
|
+
'These channels are permanently silenced. To receive messages from them, add them manually to the access policy for the relevant platform.',
|
|
2009
|
+
style: TextStyle(color: _textSecondary, fontSize: 13, height: 1.4),
|
|
2010
|
+
),
|
|
2011
|
+
],
|
|
2012
|
+
),
|
|
2013
|
+
),
|
|
2014
|
+
_StatusPill(label: '${ignored.length}', color: _textMuted),
|
|
2015
|
+
],
|
|
2016
|
+
),
|
|
2017
|
+
const SizedBox(height: 14),
|
|
2018
|
+
for (final key in ignored)
|
|
2019
|
+
Builder(builder: (context) {
|
|
2020
|
+
final sep = key.indexOf(':');
|
|
2021
|
+
final platform = sep > 0 ? key.substring(0, sep) : key;
|
|
2022
|
+
final chatId = sep > 0 ? key.substring(sep + 1) : '';
|
|
2023
|
+
return Container(
|
|
2024
|
+
margin: const EdgeInsets.only(bottom: 8),
|
|
2025
|
+
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
|
2026
|
+
decoration: BoxDecoration(
|
|
2027
|
+
color: _bgSecondary,
|
|
2028
|
+
borderRadius: BorderRadius.circular(8),
|
|
2029
|
+
border: Border.all(color: _borderLight),
|
|
2030
|
+
),
|
|
2031
|
+
child: Row(
|
|
2032
|
+
children: [
|
|
2033
|
+
Icon(Icons.block_rounded, size: 16, color: _textMuted),
|
|
2034
|
+
const SizedBox(width: 10),
|
|
2035
|
+
Expanded(
|
|
2036
|
+
child: Column(
|
|
2037
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2038
|
+
children: [
|
|
2039
|
+
Text(
|
|
2040
|
+
platform.toUpperCase(),
|
|
2041
|
+
style: TextStyle(
|
|
2042
|
+
color: _textMuted,
|
|
2043
|
+
fontSize: 10,
|
|
2044
|
+
fontWeight: FontWeight.w700,
|
|
2045
|
+
letterSpacing: 0.6,
|
|
2046
|
+
),
|
|
2047
|
+
),
|
|
2048
|
+
Text(
|
|
2049
|
+
chatId.isNotEmpty ? chatId : platform,
|
|
2050
|
+
style: TextStyle(color: _textPrimary, fontSize: 13),
|
|
2051
|
+
),
|
|
2052
|
+
],
|
|
2053
|
+
),
|
|
2054
|
+
),
|
|
2055
|
+
TextButton(
|
|
2056
|
+
onPressed: () => controller.removeIgnoredChat(key),
|
|
2057
|
+
child: Text('Remove'),
|
|
2058
|
+
),
|
|
2059
|
+
],
|
|
2060
|
+
),
|
|
2061
|
+
);
|
|
2062
|
+
}),
|
|
2063
|
+
],
|
|
2064
|
+
),
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
|
|
1842
2069
|
class _MessagingActivityPanel extends StatelessWidget {
|
|
1843
2070
|
const _MessagingActivityPanel({required this.messages});
|
|
1844
2071
|
|
|
@@ -1967,6 +2194,156 @@ class _MessagingActivityItem extends StatelessWidget {
|
|
|
1967
2194
|
}
|
|
1968
2195
|
}
|
|
1969
2196
|
|
|
2197
|
+
// ─── Connection reconnecting banner (#65) ─────────────────────────────────
|
|
2198
|
+
|
|
2199
|
+
class _ConnectionReconnectingBanner extends StatelessWidget {
|
|
2200
|
+
const _ConnectionReconnectingBanner({required this.hasNetwork});
|
|
2201
|
+
|
|
2202
|
+
final bool hasNetwork;
|
|
2203
|
+
|
|
2204
|
+
@override
|
|
2205
|
+
Widget build(BuildContext context) {
|
|
2206
|
+
final msg = hasNetwork ? 'Reconnecting to server…' : 'No network connection';
|
|
2207
|
+
return Container(
|
|
2208
|
+
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
|
2209
|
+
decoration: BoxDecoration(
|
|
2210
|
+
color: _warning.withValues(alpha: 0.12),
|
|
2211
|
+
borderRadius: BorderRadius.circular(12),
|
|
2212
|
+
border: Border.all(color: _warning.withValues(alpha: 0.32)),
|
|
2213
|
+
),
|
|
2214
|
+
child: Row(
|
|
2215
|
+
children: <Widget>[
|
|
2216
|
+
if (hasNetwork)
|
|
2217
|
+
SizedBox.square(
|
|
2218
|
+
dimension: 14,
|
|
2219
|
+
child: CircularProgressIndicator(strokeWidth: 2, color: _warning),
|
|
2220
|
+
)
|
|
2221
|
+
else
|
|
2222
|
+
Icon(Icons.wifi_off_outlined, size: 16, color: _warning),
|
|
2223
|
+
const SizedBox(width: 10),
|
|
2224
|
+
Expanded(
|
|
2225
|
+
child: Text(
|
|
2226
|
+
msg,
|
|
2227
|
+
style: TextStyle(
|
|
2228
|
+
color: _warning,
|
|
2229
|
+
fontSize: 13,
|
|
2230
|
+
fontWeight: FontWeight.w600,
|
|
2231
|
+
),
|
|
2232
|
+
),
|
|
2233
|
+
),
|
|
2234
|
+
],
|
|
2235
|
+
),
|
|
2236
|
+
);
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
// ─── Pending approval banner (#62) ────────────────────────────────────────
|
|
2241
|
+
|
|
2242
|
+
class _PendingApprovalBanner extends StatefulWidget {
|
|
2243
|
+
const _PendingApprovalBanner({required this.approval});
|
|
2244
|
+
|
|
2245
|
+
final ToolApprovalRequest approval;
|
|
2246
|
+
|
|
2247
|
+
@override
|
|
2248
|
+
State<_PendingApprovalBanner> createState() => _PendingApprovalBannerState();
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
class _PendingApprovalBannerState extends State<_PendingApprovalBanner> {
|
|
2252
|
+
late Timer _timer;
|
|
2253
|
+
int _secondsRemaining = 30;
|
|
2254
|
+
|
|
2255
|
+
@override
|
|
2256
|
+
void initState() {
|
|
2257
|
+
super.initState();
|
|
2258
|
+
_secondsRemaining = _remaining();
|
|
2259
|
+
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
|
2260
|
+
if (mounted) setState(() => _secondsRemaining = _remaining());
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
@override
|
|
2265
|
+
void didUpdateWidget(covariant _PendingApprovalBanner oldWidget) {
|
|
2266
|
+
super.didUpdateWidget(oldWidget);
|
|
2267
|
+
if (oldWidget.approval.approvalId != widget.approval.approvalId) {
|
|
2268
|
+
_timer.cancel();
|
|
2269
|
+
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
|
2270
|
+
if (mounted) setState(() => _secondsRemaining = _remaining());
|
|
2271
|
+
});
|
|
2272
|
+
setState(() => _secondsRemaining = _remaining());
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
int _remaining() {
|
|
2277
|
+
final expiry = widget.approval.expiresAt;
|
|
2278
|
+
if (expiry == null) return 30;
|
|
2279
|
+
return expiry.difference(DateTime.now()).inSeconds.clamp(0, 300);
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
@override
|
|
2283
|
+
void dispose() {
|
|
2284
|
+
_timer.cancel();
|
|
2285
|
+
super.dispose();
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
@override
|
|
2289
|
+
Widget build(BuildContext context) {
|
|
2290
|
+
final toolName = widget.approval.toolName.ifEmpty('tool');
|
|
2291
|
+
return Container(
|
|
2292
|
+
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
|
2293
|
+
decoration: BoxDecoration(
|
|
2294
|
+
color: _accent.withValues(alpha: 0.10),
|
|
2295
|
+
borderRadius: BorderRadius.circular(12),
|
|
2296
|
+
border: Border.all(color: _accent.withValues(alpha: 0.32)),
|
|
2297
|
+
),
|
|
2298
|
+
child: Row(
|
|
2299
|
+
children: <Widget>[
|
|
2300
|
+
_PulseHalo(
|
|
2301
|
+
color: _accent,
|
|
2302
|
+
animate: true,
|
|
2303
|
+
child: Container(
|
|
2304
|
+
width: 22,
|
|
2305
|
+
height: 22,
|
|
2306
|
+
decoration: BoxDecoration(
|
|
2307
|
+
color: _accent.withValues(alpha: 0.16),
|
|
2308
|
+
shape: BoxShape.circle,
|
|
2309
|
+
),
|
|
2310
|
+
child: Icon(
|
|
2311
|
+
Icons.security_outlined,
|
|
2312
|
+
size: 13,
|
|
2313
|
+
color: _accent,
|
|
2314
|
+
),
|
|
2315
|
+
),
|
|
2316
|
+
),
|
|
2317
|
+
const SizedBox(width: 10),
|
|
2318
|
+
Expanded(
|
|
2319
|
+
child: RichText(
|
|
2320
|
+
text: TextSpan(
|
|
2321
|
+
style: TextStyle(color: _textPrimary, fontSize: 13),
|
|
2322
|
+
children: <TextSpan>[
|
|
2323
|
+
const TextSpan(
|
|
2324
|
+
text: 'Waiting for approval: ',
|
|
2325
|
+
style: TextStyle(fontWeight: FontWeight.w600),
|
|
2326
|
+
),
|
|
2327
|
+
TextSpan(
|
|
2328
|
+
text: toolName,
|
|
2329
|
+
style: TextStyle(color: _accent),
|
|
2330
|
+
),
|
|
2331
|
+
TextSpan(
|
|
2332
|
+
text: ' ${_secondsRemaining}s',
|
|
2333
|
+
style: TextStyle(color: _textSecondary),
|
|
2334
|
+
),
|
|
2335
|
+
],
|
|
2336
|
+
),
|
|
2337
|
+
),
|
|
2338
|
+
),
|
|
2339
|
+
],
|
|
2340
|
+
),
|
|
2341
|
+
);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
// ─── Runs page ─────────────────────────────────────────────────────────────
|
|
2346
|
+
|
|
1970
2347
|
class RunsPanel extends StatefulWidget {
|
|
1971
2348
|
const RunsPanel({super.key, required this.controller, this.embedded = false});
|
|
1972
2349
|
|
|
@@ -1984,6 +2361,7 @@ class _RunsPanelState extends State<RunsPanel> {
|
|
|
1984
2361
|
RunDetailSnapshot? _detail;
|
|
1985
2362
|
bool _loadingDetail = false;
|
|
1986
2363
|
String? _detailError;
|
|
2364
|
+
String? _selectedGraphNodeId;
|
|
1987
2365
|
|
|
1988
2366
|
@override
|
|
1989
2367
|
void initState() {
|
|
@@ -2160,127 +2538,155 @@ class _RunsPanelState extends State<RunsPanel> {
|
|
|
2160
2538
|
orElse: () => null,
|
|
2161
2539
|
);
|
|
2162
2540
|
final detail = _detail?.run.id == selected?.id ? _detail : null;
|
|
2541
|
+
final padding = widget.embedded ? EdgeInsets.zero : _pagePadding(context);
|
|
2542
|
+
|
|
2543
|
+
Widget header = const SizedBox.shrink();
|
|
2544
|
+
if (!widget.embedded) {
|
|
2545
|
+
header = _PageTitle(
|
|
2546
|
+
title: 'Runs',
|
|
2547
|
+
subtitle: 'Explore run flows, tool steps, and responses.',
|
|
2548
|
+
trailing: OutlinedButton.icon(
|
|
2549
|
+
onPressed: _refreshRuns,
|
|
2550
|
+
icon: const Icon(Icons.refresh),
|
|
2551
|
+
label: const Text('Refresh'),
|
|
2552
|
+
),
|
|
2553
|
+
);
|
|
2554
|
+
}
|
|
2163
2555
|
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
title: 'Runs',
|
|
2170
|
-
subtitle:
|
|
2171
|
-
'Inspect recent runs, failures, tool steps, and final responses.',
|
|
2172
|
-
trailing: OutlinedButton.icon(
|
|
2173
|
-
onPressed: _refreshRuns,
|
|
2174
|
-
icon: Icon(Icons.refresh),
|
|
2175
|
-
label: Text('Refresh'),
|
|
2176
|
-
),
|
|
2177
|
-
)
|
|
2178
|
-
else
|
|
2179
|
-
Align(
|
|
2180
|
-
alignment: Alignment.centerRight,
|
|
2181
|
-
child: Padding(
|
|
2182
|
-
padding: const EdgeInsets.only(bottom: 12),
|
|
2183
|
-
child: OutlinedButton.icon(
|
|
2184
|
-
onPressed: _refreshRuns,
|
|
2185
|
-
icon: const Icon(Icons.refresh),
|
|
2186
|
-
label: const Text('Refresh'),
|
|
2187
|
-
),
|
|
2188
|
-
),
|
|
2189
|
-
),
|
|
2190
|
-
if (controller.errorMessage != null) ...<Widget>[
|
|
2191
|
-
_InlineError(message: controller.errorMessage!),
|
|
2192
|
-
const SizedBox(height: 16),
|
|
2193
|
-
],
|
|
2194
|
-
if (controller.activeRun != null ||
|
|
2195
|
-
controller.toolEvents.isNotEmpty) ...<Widget>[
|
|
2196
|
-
_RunStatusPanel(
|
|
2197
|
-
run: controller.activeRun,
|
|
2198
|
-
tools: controller.toolEvents,
|
|
2199
|
-
),
|
|
2200
|
-
const SizedBox(height: 16),
|
|
2201
|
-
],
|
|
2202
|
-
if (controller.recentRuns.isEmpty)
|
|
2556
|
+
if (controller.recentRuns.isEmpty) {
|
|
2557
|
+
return ListView(
|
|
2558
|
+
padding: padding,
|
|
2559
|
+
children: <Widget>[
|
|
2560
|
+
header,
|
|
2203
2561
|
const _EmptyCard(
|
|
2204
2562
|
title: 'No runs yet',
|
|
2205
2563
|
subtitle:
|
|
2206
2564
|
'Send a task from chat and its execution history will show up here.',
|
|
2207
|
-
)
|
|
2208
|
-
else ...<Widget>[
|
|
2209
|
-
_RunsMetricsStrip(
|
|
2210
|
-
runs: filteredRuns,
|
|
2211
|
-
totalLoaded: controller.recentRuns.length,
|
|
2212
|
-
),
|
|
2213
|
-
const SizedBox(height: 16),
|
|
2214
|
-
_RunsFilterBar(
|
|
2215
|
-
searchController: _searchController,
|
|
2216
|
-
statusFilter: _statusFilter,
|
|
2217
|
-
onStatusChanged: _setStatusFilter,
|
|
2218
2565
|
),
|
|
2219
|
-
const SizedBox(height: 16),
|
|
2220
|
-
if (filteredRuns.isEmpty)
|
|
2221
|
-
const _EmptyCard(
|
|
2222
|
-
title: 'No matching runs',
|
|
2223
|
-
subtitle:
|
|
2224
|
-
'Try clearing the search or switching the status filter.',
|
|
2225
|
-
)
|
|
2226
|
-
else
|
|
2227
|
-
LayoutBuilder(
|
|
2228
|
-
builder: (context, constraints) {
|
|
2229
|
-
final wide = constraints.maxWidth >= 1120;
|
|
2230
|
-
final historyPane = _RunsHistoryPane(
|
|
2231
|
-
runs: filteredRuns,
|
|
2232
|
-
selectedRunId: _selectedRunId,
|
|
2233
|
-
onSelect: _selectRun,
|
|
2234
|
-
);
|
|
2235
|
-
final detailPane = _RunDetailWorkspace(
|
|
2236
|
-
run: selected,
|
|
2237
|
-
detail: detail,
|
|
2238
|
-
errorMessage: _detailError,
|
|
2239
|
-
loading: _loadingDetail,
|
|
2240
|
-
onDelete: _deleteSelectedRun,
|
|
2241
|
-
onCopyResponse: _copyResponse,
|
|
2242
|
-
);
|
|
2243
|
-
if (!wide) {
|
|
2244
|
-
return Column(
|
|
2245
|
-
children: <Widget>[
|
|
2246
|
-
detailPane,
|
|
2247
|
-
const SizedBox(height: 16),
|
|
2248
|
-
historyPane,
|
|
2249
|
-
],
|
|
2250
|
-
);
|
|
2251
|
-
}
|
|
2252
|
-
return Row(
|
|
2253
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2254
|
-
children: <Widget>[
|
|
2255
|
-
SizedBox(width: 360, child: historyPane),
|
|
2256
|
-
const SizedBox(width: 16),
|
|
2257
|
-
Expanded(child: detailPane),
|
|
2258
|
-
],
|
|
2259
|
-
);
|
|
2260
|
-
},
|
|
2261
|
-
),
|
|
2262
2566
|
],
|
|
2263
|
-
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
final selectorRail = _RunSelectorRail(
|
|
2571
|
+
controller: controller,
|
|
2572
|
+
filteredRuns: filteredRuns,
|
|
2573
|
+
selectedRunId: _selectedRunId,
|
|
2574
|
+
searchController: _searchController,
|
|
2575
|
+
statusFilter: _statusFilter,
|
|
2576
|
+
onStatusChanged: _setStatusFilter,
|
|
2577
|
+
onSelect: (id) {
|
|
2578
|
+
_selectRun(id);
|
|
2579
|
+
setState(() => _selectedGraphNodeId = null);
|
|
2580
|
+
},
|
|
2581
|
+
onRefresh: _refreshRuns,
|
|
2264
2582
|
);
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
2583
|
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
required this.onLogout,
|
|
2277
|
-
});
|
|
2584
|
+
final graphCanvas = _RunFlowGraphCanvas(
|
|
2585
|
+
run: selected,
|
|
2586
|
+
detail: detail,
|
|
2587
|
+
loading: _loadingDetail,
|
|
2588
|
+
errorMessage: _detailError,
|
|
2589
|
+
selectedNodeId: _selectedGraphNodeId,
|
|
2590
|
+
onNodeSelected: (id) => setState(() => _selectedGraphNodeId = id),
|
|
2591
|
+
);
|
|
2278
2592
|
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2593
|
+
final detailPanel = _RunNodeDetailPanel(
|
|
2594
|
+
run: selected,
|
|
2595
|
+
detail: detail,
|
|
2596
|
+
nodeId: _selectedGraphNodeId,
|
|
2597
|
+
loading: _loadingDetail,
|
|
2598
|
+
onDelete: _deleteSelectedRun,
|
|
2599
|
+
onCopyResponse: _copyResponse,
|
|
2600
|
+
);
|
|
2601
|
+
|
|
2602
|
+
return LayoutBuilder(
|
|
2603
|
+
builder: (context, constraints) {
|
|
2604
|
+
final wide = constraints.maxWidth >= 1100;
|
|
2605
|
+
final medium = constraints.maxWidth >= 760;
|
|
2606
|
+
|
|
2607
|
+
final body = wide
|
|
2608
|
+
? Row(
|
|
2609
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2610
|
+
children: <Widget>[
|
|
2611
|
+
SizedBox(width: 260, child: selectorRail),
|
|
2612
|
+
const SizedBox(width: 12),
|
|
2613
|
+
Expanded(child: graphCanvas),
|
|
2614
|
+
const SizedBox(width: 12),
|
|
2615
|
+
SizedBox(width: 310, child: detailPanel),
|
|
2616
|
+
],
|
|
2617
|
+
)
|
|
2618
|
+
: medium
|
|
2619
|
+
? Row(
|
|
2620
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2621
|
+
children: <Widget>[
|
|
2622
|
+
SizedBox(width: 220, child: selectorRail),
|
|
2623
|
+
const SizedBox(width: 12),
|
|
2624
|
+
Expanded(
|
|
2625
|
+
child: Column(
|
|
2626
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
2627
|
+
children: <Widget>[
|
|
2628
|
+
graphCanvas,
|
|
2629
|
+
const SizedBox(height: 12),
|
|
2630
|
+
detailPanel,
|
|
2631
|
+
],
|
|
2632
|
+
),
|
|
2633
|
+
),
|
|
2634
|
+
],
|
|
2635
|
+
)
|
|
2636
|
+
: Column(
|
|
2637
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
2638
|
+
children: <Widget>[
|
|
2639
|
+
graphCanvas,
|
|
2640
|
+
const SizedBox(height: 12),
|
|
2641
|
+
selectorRail,
|
|
2642
|
+
const SizedBox(height: 12),
|
|
2643
|
+
detailPanel,
|
|
2644
|
+
],
|
|
2645
|
+
);
|
|
2646
|
+
|
|
2647
|
+
return SingleChildScrollView(
|
|
2648
|
+
padding: padding,
|
|
2649
|
+
child: Column(
|
|
2650
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
2651
|
+
children: <Widget>[
|
|
2652
|
+
header,
|
|
2653
|
+
if (controller.errorMessage != null) ...<Widget>[
|
|
2654
|
+
_InlineError(message: controller.errorMessage!),
|
|
2655
|
+
const SizedBox(height: 12),
|
|
2656
|
+
],
|
|
2657
|
+
if (controller.activeRun != null ||
|
|
2658
|
+
controller.toolEvents.isNotEmpty) ...<Widget>[
|
|
2659
|
+
_RunStatusPanel(
|
|
2660
|
+
run: controller.activeRun,
|
|
2661
|
+
tools: controller.toolEvents,
|
|
2662
|
+
),
|
|
2663
|
+
const SizedBox(height: 12),
|
|
2664
|
+
],
|
|
2665
|
+
body,
|
|
2666
|
+
],
|
|
2667
|
+
),
|
|
2668
|
+
);
|
|
2669
|
+
},
|
|
2670
|
+
);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
class _MessagingCard extends StatelessWidget {
|
|
2675
|
+
const _MessagingCard({
|
|
2676
|
+
required this.platform,
|
|
2677
|
+
required this.status,
|
|
2678
|
+
required this.accessCatalog,
|
|
2679
|
+
required this.controller,
|
|
2680
|
+
required this.onConnect,
|
|
2681
|
+
required this.onDisconnect,
|
|
2682
|
+
required this.onLogout,
|
|
2683
|
+
});
|
|
2684
|
+
|
|
2685
|
+
final MessagingPlatformDescriptor platform;
|
|
2686
|
+
final MessagingPlatformStatus? status;
|
|
2687
|
+
final MessagingAccessCatalog accessCatalog;
|
|
2688
|
+
final NeoAgentController controller;
|
|
2689
|
+
final Future<void> Function() onConnect;
|
|
2284
2690
|
final Future<void> Function() onDisconnect;
|
|
2285
2691
|
final Future<void> Function() onLogout;
|
|
2286
2692
|
|
|
@@ -3645,113 +4051,105 @@ class _RunHeroCard extends StatelessWidget {
|
|
|
3645
4051
|
|
|
3646
4052
|
@override
|
|
3647
4053
|
Widget build(BuildContext context) {
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
spacing: 10,
|
|
3675
|
-
runSpacing: 10,
|
|
3676
|
-
children: <Widget>[
|
|
3677
|
-
_StatusPill(
|
|
3678
|
-
label: run.statusLabel,
|
|
3679
|
-
color: run.statusColor,
|
|
3680
|
-
),
|
|
3681
|
-
_MetaPill(
|
|
3682
|
-
label: run.triggerLabel,
|
|
3683
|
-
icon: Icons.bolt_outlined,
|
|
3684
|
-
),
|
|
3685
|
-
_MetaPill(
|
|
3686
|
-
label: run.modelLabel,
|
|
3687
|
-
icon: Icons.memory_outlined,
|
|
3688
|
-
),
|
|
3689
|
-
if (run.deliverableType.trim().isNotEmpty)
|
|
3690
|
-
_MetaPill(
|
|
3691
|
-
label: run.deliverableType.replaceAll('_', ' '),
|
|
3692
|
-
icon: Icons.inventory_2_outlined,
|
|
3693
|
-
),
|
|
3694
|
-
],
|
|
3695
|
-
),
|
|
3696
|
-
const SizedBox(height: 16),
|
|
3697
|
-
Text(
|
|
3698
|
-
run.title,
|
|
3699
|
-
style: TextStyle(
|
|
3700
|
-
fontSize: 24,
|
|
3701
|
-
fontWeight: FontWeight.w800,
|
|
3702
|
-
height: 1.15,
|
|
3703
|
-
),
|
|
3704
|
-
),
|
|
3705
|
-
const SizedBox(height: 10),
|
|
3706
|
-
Wrap(
|
|
3707
|
-
spacing: 10,
|
|
3708
|
-
runSpacing: 10,
|
|
3709
|
-
children: <Widget>[
|
|
3710
|
-
_MetaPill(
|
|
3711
|
-
label: 'Started ${run.createdAtLabel}',
|
|
3712
|
-
icon: Icons.schedule_outlined,
|
|
3713
|
-
),
|
|
3714
|
-
_MetaPill(
|
|
3715
|
-
label: run.durationLabel,
|
|
3716
|
-
icon: Icons.timer_outlined,
|
|
3717
|
-
),
|
|
3718
|
-
_MetaPill(
|
|
3719
|
-
label: '${run.totalTokensLabel} tokens',
|
|
3720
|
-
icon: Icons.toll_outlined,
|
|
3721
|
-
),
|
|
3722
|
-
_MetaPill(
|
|
3723
|
-
label: run.id.length <= 12
|
|
3724
|
-
? run.id
|
|
3725
|
-
: '${run.id.substring(0, 12)}…',
|
|
3726
|
-
icon: Icons.tag_outlined,
|
|
3727
|
-
),
|
|
3728
|
-
],
|
|
3729
|
-
),
|
|
3730
|
-
],
|
|
4054
|
+
final statusColor = run.statusColor;
|
|
4055
|
+
return Card(
|
|
4056
|
+
child: Padding(
|
|
4057
|
+
padding: const EdgeInsets.all(14),
|
|
4058
|
+
child: Column(
|
|
4059
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
4060
|
+
children: <Widget>[
|
|
4061
|
+
Row(
|
|
4062
|
+
crossAxisAlignment: CrossAxisAlignment.center,
|
|
4063
|
+
children: <Widget>[
|
|
4064
|
+
Container(
|
|
4065
|
+
width: 8,
|
|
4066
|
+
height: 8,
|
|
4067
|
+
decoration: BoxDecoration(
|
|
4068
|
+
color: statusColor,
|
|
4069
|
+
shape: BoxShape.circle,
|
|
4070
|
+
),
|
|
4071
|
+
),
|
|
4072
|
+
const SizedBox(width: 7),
|
|
4073
|
+
Text(
|
|
4074
|
+
run.statusLabel,
|
|
4075
|
+
style: TextStyle(
|
|
4076
|
+
color: statusColor,
|
|
4077
|
+
fontWeight: FontWeight.w600,
|
|
4078
|
+
fontSize: 12,
|
|
4079
|
+
),
|
|
3731
4080
|
),
|
|
4081
|
+
const Spacer(),
|
|
4082
|
+
IconButton(
|
|
4083
|
+
tooltip: 'Delete run',
|
|
4084
|
+
icon: const Icon(Icons.delete_outline, size: 18),
|
|
4085
|
+
onPressed: onDelete,
|
|
4086
|
+
visualDensity: VisualDensity.compact,
|
|
4087
|
+
color: _textSecondary,
|
|
4088
|
+
),
|
|
4089
|
+
],
|
|
4090
|
+
),
|
|
4091
|
+
const SizedBox(height: 8),
|
|
4092
|
+
Text(
|
|
4093
|
+
run.title,
|
|
4094
|
+
style: const TextStyle(
|
|
4095
|
+
fontSize: 15,
|
|
4096
|
+
fontWeight: FontWeight.w700,
|
|
4097
|
+
height: 1.3,
|
|
3732
4098
|
),
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
4099
|
+
maxLines: 3,
|
|
4100
|
+
overflow: TextOverflow.ellipsis,
|
|
4101
|
+
),
|
|
4102
|
+
const SizedBox(height: 10),
|
|
4103
|
+
Wrap(
|
|
4104
|
+
spacing: 6,
|
|
4105
|
+
runSpacing: 6,
|
|
4106
|
+
children: <Widget>[
|
|
4107
|
+
_MetaPill(
|
|
4108
|
+
label: run.triggerLabel,
|
|
4109
|
+
icon: Icons.bolt_outlined,
|
|
4110
|
+
),
|
|
4111
|
+
_MetaPill(
|
|
4112
|
+
label: run.modelLabel,
|
|
4113
|
+
icon: Icons.memory_outlined,
|
|
4114
|
+
),
|
|
4115
|
+
_MetaPill(
|
|
4116
|
+
label: run.createdAtLabel,
|
|
4117
|
+
icon: Icons.schedule_outlined,
|
|
4118
|
+
),
|
|
4119
|
+
_MetaPill(
|
|
4120
|
+
label: run.durationLabel,
|
|
4121
|
+
icon: Icons.timer_outlined,
|
|
4122
|
+
),
|
|
4123
|
+
if (run.totalTokensLabel.isNotEmpty)
|
|
4124
|
+
_MetaPill(
|
|
4125
|
+
label: '${run.totalTokensLabel} tok',
|
|
4126
|
+
icon: Icons.toll_outlined,
|
|
4127
|
+
),
|
|
4128
|
+
if (run.deliverableType.trim().isNotEmpty)
|
|
4129
|
+
_MetaPill(
|
|
4130
|
+
label: run.deliverableType.replaceAll('_', ' '),
|
|
4131
|
+
icon: Icons.inventory_2_outlined,
|
|
4132
|
+
),
|
|
4133
|
+
],
|
|
4134
|
+
),
|
|
4135
|
+
if (run.error.trim().isNotEmpty) ...<Widget>[
|
|
4136
|
+
const SizedBox(height: 10),
|
|
4137
|
+
Container(
|
|
4138
|
+
width: double.infinity,
|
|
4139
|
+
padding: const EdgeInsets.all(10),
|
|
4140
|
+
decoration: BoxDecoration(
|
|
4141
|
+
color: const Color(0x19EF4444),
|
|
4142
|
+
borderRadius: BorderRadius.circular(10),
|
|
4143
|
+
border: Border.all(color: const Color(0x4CEF4444)),
|
|
4144
|
+
),
|
|
4145
|
+
child: Text(
|
|
4146
|
+
run.error,
|
|
4147
|
+
style: TextStyle(fontSize: 12, height: 1.45),
|
|
4148
|
+
),
|
|
3738
4149
|
),
|
|
3739
4150
|
],
|
|
3740
|
-
),
|
|
3741
|
-
if (run.error.trim().isNotEmpty) ...<Widget>[
|
|
3742
|
-
const SizedBox(height: 16),
|
|
3743
|
-
Container(
|
|
3744
|
-
width: double.infinity,
|
|
3745
|
-
padding: const EdgeInsets.all(14),
|
|
3746
|
-
decoration: BoxDecoration(
|
|
3747
|
-
color: const Color(0x19EF4444),
|
|
3748
|
-
borderRadius: BorderRadius.circular(14),
|
|
3749
|
-
border: Border.all(color: const Color(0x4CEF4444)),
|
|
3750
|
-
),
|
|
3751
|
-
child: Text(run.error, style: TextStyle(height: 1.45)),
|
|
3752
|
-
),
|
|
3753
4151
|
],
|
|
3754
|
-
|
|
4152
|
+
),
|
|
3755
4153
|
),
|
|
3756
4154
|
);
|
|
3757
4155
|
}
|
|
@@ -4203,6 +4601,945 @@ class _RunDetailBlock extends StatelessWidget {
|
|
|
4203
4601
|
}
|
|
4204
4602
|
}
|
|
4205
4603
|
|
|
4604
|
+
// ─── Run flow graph (#66) ─────────────────────────────────────────────────
|
|
4605
|
+
|
|
4606
|
+
// ── Data model ──────────────────────────────────────────────────────────────
|
|
4607
|
+
|
|
4608
|
+
class _FlowNode {
|
|
4609
|
+
_FlowNode({
|
|
4610
|
+
required this.id,
|
|
4611
|
+
required this.label,
|
|
4612
|
+
required this.nodeType,
|
|
4613
|
+
required this.status,
|
|
4614
|
+
this.step,
|
|
4615
|
+
});
|
|
4616
|
+
|
|
4617
|
+
final String id;
|
|
4618
|
+
final String label;
|
|
4619
|
+
final String nodeType;
|
|
4620
|
+
final String status;
|
|
4621
|
+
final RunStepItem? step;
|
|
4622
|
+
double x = 0;
|
|
4623
|
+
double y = 0;
|
|
4624
|
+
int lane = 0;
|
|
4625
|
+
|
|
4626
|
+
static const double w = 156.0;
|
|
4627
|
+
static const double h = 64.0;
|
|
4628
|
+
static const double hGap = 34.0;
|
|
4629
|
+
static const double vGap = 80.0;
|
|
4630
|
+
|
|
4631
|
+
Color get nodeColor {
|
|
4632
|
+
if (status == 'failed') return _danger;
|
|
4633
|
+
switch (nodeType) {
|
|
4634
|
+
case 'start':
|
|
4635
|
+
return _accent;
|
|
4636
|
+
case 'end':
|
|
4637
|
+
return _success;
|
|
4638
|
+
case 'fail':
|
|
4639
|
+
return _danger;
|
|
4640
|
+
case 'model':
|
|
4641
|
+
return _info;
|
|
4642
|
+
case 'plan':
|
|
4643
|
+
return _warning;
|
|
4644
|
+
case 'subagent':
|
|
4645
|
+
return _accentHover;
|
|
4646
|
+
case 'verify':
|
|
4647
|
+
return const Color(0xFF8B5CF6);
|
|
4648
|
+
default:
|
|
4649
|
+
return _success;
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
|
|
4653
|
+
IconData get nodeIcon {
|
|
4654
|
+
switch (nodeType) {
|
|
4655
|
+
case 'start':
|
|
4656
|
+
return Icons.play_circle_outline;
|
|
4657
|
+
case 'end':
|
|
4658
|
+
return Icons.check_circle_outline;
|
|
4659
|
+
case 'fail':
|
|
4660
|
+
return Icons.error_outline;
|
|
4661
|
+
case 'model':
|
|
4662
|
+
return Icons.psychology_outlined;
|
|
4663
|
+
case 'plan':
|
|
4664
|
+
return Icons.list_alt_outlined;
|
|
4665
|
+
case 'subagent':
|
|
4666
|
+
return Icons.account_tree_outlined;
|
|
4667
|
+
case 'verify':
|
|
4668
|
+
return Icons.fact_check_outlined;
|
|
4669
|
+
case 'note':
|
|
4670
|
+
return Icons.notes_outlined;
|
|
4671
|
+
default:
|
|
4672
|
+
return Icons.build_circle_outlined;
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4677
|
+
List<_FlowNode> _buildRunFlowNodes(RunDetailSnapshot detail) {
|
|
4678
|
+
final nodes = <_FlowNode>[];
|
|
4679
|
+
|
|
4680
|
+
nodes.add(
|
|
4681
|
+
_FlowNode(
|
|
4682
|
+
id: '__start__',
|
|
4683
|
+
label: 'Start',
|
|
4684
|
+
nodeType: 'start',
|
|
4685
|
+
status: 'completed',
|
|
4686
|
+
),
|
|
4687
|
+
);
|
|
4688
|
+
|
|
4689
|
+
int subLaneCounter = 0;
|
|
4690
|
+
for (final step in detail.steps) {
|
|
4691
|
+
final lower = '${step.type} ${step.toolName}'.toLowerCase();
|
|
4692
|
+
String nodeType;
|
|
4693
|
+
int nodeLane = 0;
|
|
4694
|
+
if (lower.contains('model') ||
|
|
4695
|
+
lower.contains('think') ||
|
|
4696
|
+
lower.contains('llm')) {
|
|
4697
|
+
nodeType = 'model';
|
|
4698
|
+
} else if (lower.contains('plan')) {
|
|
4699
|
+
nodeType = 'plan';
|
|
4700
|
+
} else if (lower.contains('subagent') ||
|
|
4701
|
+
lower.contains('helper') ||
|
|
4702
|
+
lower.contains('delegat')) {
|
|
4703
|
+
nodeType = 'subagent';
|
|
4704
|
+
nodeLane = ++subLaneCounter;
|
|
4705
|
+
} else if (lower.contains('verif')) {
|
|
4706
|
+
nodeType = 'verify';
|
|
4707
|
+
} else if (lower.contains('note') || lower.contains('analysis')) {
|
|
4708
|
+
nodeType = 'note';
|
|
4709
|
+
} else {
|
|
4710
|
+
nodeType = 'tool';
|
|
4711
|
+
}
|
|
4712
|
+
nodes.add(
|
|
4713
|
+
_FlowNode(
|
|
4714
|
+
id: step.id,
|
|
4715
|
+
label: step.label,
|
|
4716
|
+
nodeType: nodeType,
|
|
4717
|
+
status: step.status,
|
|
4718
|
+
step: step,
|
|
4719
|
+
)..lane = nodeLane,
|
|
4720
|
+
);
|
|
4721
|
+
}
|
|
4722
|
+
|
|
4723
|
+
nodes.add(
|
|
4724
|
+
_FlowNode(
|
|
4725
|
+
id: '__end__',
|
|
4726
|
+
label: detail.run.isFailure ? 'Failed' : 'Done',
|
|
4727
|
+
nodeType: detail.run.isFailure ? 'fail' : 'end',
|
|
4728
|
+
status: detail.run.isFailure ? 'failed' : 'completed',
|
|
4729
|
+
),
|
|
4730
|
+
);
|
|
4731
|
+
|
|
4732
|
+
// Layout positions
|
|
4733
|
+
final mainNodes = nodes.where((n) => n.lane == 0).toList();
|
|
4734
|
+
double cx = 0;
|
|
4735
|
+
for (final n in mainNodes) {
|
|
4736
|
+
n.x = cx;
|
|
4737
|
+
n.y = 0;
|
|
4738
|
+
cx += _FlowNode.w + _FlowNode.hGap;
|
|
4739
|
+
}
|
|
4740
|
+
|
|
4741
|
+
final subLanes = <int, List<_FlowNode>>{};
|
|
4742
|
+
for (final n in nodes.where((n) => n.lane > 0)) {
|
|
4743
|
+
subLanes.putIfAbsent(n.lane, () => []).add(n);
|
|
4744
|
+
}
|
|
4745
|
+
int laneRow = 1;
|
|
4746
|
+
for (final laneNodes in subLanes.values) {
|
|
4747
|
+
double subCx = _FlowNode.w + _FlowNode.hGap;
|
|
4748
|
+
for (final n in laneNodes) {
|
|
4749
|
+
n.x = subCx;
|
|
4750
|
+
n.y = laneRow * (_FlowNode.h + _FlowNode.vGap);
|
|
4751
|
+
subCx += _FlowNode.w + _FlowNode.hGap;
|
|
4752
|
+
}
|
|
4753
|
+
laneRow++;
|
|
4754
|
+
}
|
|
4755
|
+
|
|
4756
|
+
return nodes;
|
|
4757
|
+
}
|
|
4758
|
+
|
|
4759
|
+
List<(String, String, bool)> _buildRunFlowEdges(List<_FlowNode> nodes) {
|
|
4760
|
+
final edges = <(String, String, bool)>[];
|
|
4761
|
+
final mainLane = nodes.where((n) => n.lane == 0).toList();
|
|
4762
|
+
for (int i = 0; i + 1 < mainLane.length; i++) {
|
|
4763
|
+
edges.add((mainLane[i].id, mainLane[i + 1].id, false));
|
|
4764
|
+
}
|
|
4765
|
+
// Delegation nodes: connect from start to their first node
|
|
4766
|
+
for (final n in nodes.where((n) => n.lane > 0)) {
|
|
4767
|
+
edges.add(('__start__', n.id, true));
|
|
4768
|
+
}
|
|
4769
|
+
return edges;
|
|
4770
|
+
}
|
|
4771
|
+
|
|
4772
|
+
// ── Edge painter ──────────────────────────────────────────────────────────────
|
|
4773
|
+
|
|
4774
|
+
class _RunGraphEdgePainter extends CustomPainter {
|
|
4775
|
+
const _RunGraphEdgePainter({required this.nodeMap, required this.edges});
|
|
4776
|
+
|
|
4777
|
+
final Map<String, _FlowNode> nodeMap;
|
|
4778
|
+
final List<(String, String, bool)> edges;
|
|
4779
|
+
|
|
4780
|
+
@override
|
|
4781
|
+
void paint(Canvas canvas, Size size) {
|
|
4782
|
+
for (final (fromId, toId, isDelegation) in edges) {
|
|
4783
|
+
final from = nodeMap[fromId];
|
|
4784
|
+
final to = nodeMap[toId];
|
|
4785
|
+
if (from == null || to == null) continue;
|
|
4786
|
+
|
|
4787
|
+
final color = isDelegation
|
|
4788
|
+
? _accentHover.withValues(alpha: 0.55)
|
|
4789
|
+
: _border.withValues(alpha: 0.8);
|
|
4790
|
+
|
|
4791
|
+
final paint = Paint()
|
|
4792
|
+
..color = color
|
|
4793
|
+
..style = PaintingStyle.stroke
|
|
4794
|
+
..strokeWidth = 1.8
|
|
4795
|
+
..strokeCap = StrokeCap.round;
|
|
4796
|
+
|
|
4797
|
+
final start = Offset(
|
|
4798
|
+
from.x + _FlowNode.w,
|
|
4799
|
+
from.y + _FlowNode.h / 2,
|
|
4800
|
+
);
|
|
4801
|
+
final end = Offset(to.x, to.y + _FlowNode.h / 2);
|
|
4802
|
+
|
|
4803
|
+
if (from.y != to.y) {
|
|
4804
|
+
final mid = (start.dx + end.dx) / 2;
|
|
4805
|
+
final path = Path()
|
|
4806
|
+
..moveTo(start.dx, start.dy)
|
|
4807
|
+
..cubicTo(mid, start.dy, mid, end.dy, end.dx, end.dy);
|
|
4808
|
+
canvas.drawPath(path, paint);
|
|
4809
|
+
} else {
|
|
4810
|
+
canvas.drawLine(start, end, paint);
|
|
4811
|
+
}
|
|
4812
|
+
|
|
4813
|
+
// Arrowhead
|
|
4814
|
+
final arrowPaint = Paint()
|
|
4815
|
+
..color = color
|
|
4816
|
+
..style = PaintingStyle.fill;
|
|
4817
|
+
const arrowSize = 6.0;
|
|
4818
|
+
final arrow = Path()
|
|
4819
|
+
..moveTo(end.dx, end.dy)
|
|
4820
|
+
..lineTo(end.dx - arrowSize, end.dy - arrowSize * 0.5)
|
|
4821
|
+
..lineTo(end.dx - arrowSize, end.dy + arrowSize * 0.5)
|
|
4822
|
+
..close();
|
|
4823
|
+
canvas.drawPath(arrow, arrowPaint);
|
|
4824
|
+
}
|
|
4825
|
+
}
|
|
4826
|
+
|
|
4827
|
+
@override
|
|
4828
|
+
bool shouldRepaint(_RunGraphEdgePainter old) =>
|
|
4829
|
+
old.nodeMap != nodeMap || old.edges != edges;
|
|
4830
|
+
}
|
|
4831
|
+
|
|
4832
|
+
// ── Individual node widget ────────────────────────────────────────────────────
|
|
4833
|
+
|
|
4834
|
+
class _FlowNodeWidget extends StatelessWidget {
|
|
4835
|
+
const _FlowNodeWidget({
|
|
4836
|
+
required this.node,
|
|
4837
|
+
required this.selected,
|
|
4838
|
+
required this.onTap,
|
|
4839
|
+
});
|
|
4840
|
+
|
|
4841
|
+
final _FlowNode node;
|
|
4842
|
+
final bool selected;
|
|
4843
|
+
final VoidCallback onTap;
|
|
4844
|
+
|
|
4845
|
+
@override
|
|
4846
|
+
Widget build(BuildContext context) {
|
|
4847
|
+
final color = node.nodeColor;
|
|
4848
|
+
final isSpecial =
|
|
4849
|
+
node.nodeType == 'start' ||
|
|
4850
|
+
node.nodeType == 'end' ||
|
|
4851
|
+
node.nodeType == 'fail';
|
|
4852
|
+
return GestureDetector(
|
|
4853
|
+
onTap: onTap,
|
|
4854
|
+
child: Container(
|
|
4855
|
+
width: _FlowNode.w,
|
|
4856
|
+
height: _FlowNode.h,
|
|
4857
|
+
decoration: BoxDecoration(
|
|
4858
|
+
color: selected
|
|
4859
|
+
? color.withValues(alpha: 0.16)
|
|
4860
|
+
: isSpecial
|
|
4861
|
+
? color.withValues(alpha: 0.10)
|
|
4862
|
+
: _bgCard,
|
|
4863
|
+
borderRadius: BorderRadius.circular(18),
|
|
4864
|
+
border: Border.all(
|
|
4865
|
+
color: selected ? color : color.withValues(alpha: 0.30),
|
|
4866
|
+
width: selected ? 2.0 : 1.2,
|
|
4867
|
+
),
|
|
4868
|
+
boxShadow: <BoxShadow>[
|
|
4869
|
+
BoxShadow(
|
|
4870
|
+
color: color.withValues(alpha: selected ? 0.22 : 0.07),
|
|
4871
|
+
blurRadius: selected ? 14 : 6,
|
|
4872
|
+
offset: const Offset(0, 3),
|
|
4873
|
+
),
|
|
4874
|
+
],
|
|
4875
|
+
),
|
|
4876
|
+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
|
4877
|
+
child: Column(
|
|
4878
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
4879
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
4880
|
+
children: <Widget>[
|
|
4881
|
+
Row(
|
|
4882
|
+
children: <Widget>[
|
|
4883
|
+
Icon(node.nodeIcon, size: 13, color: color),
|
|
4884
|
+
const SizedBox(width: 6),
|
|
4885
|
+
Expanded(
|
|
4886
|
+
child: Text(
|
|
4887
|
+
node.label,
|
|
4888
|
+
maxLines: 1,
|
|
4889
|
+
overflow: TextOverflow.ellipsis,
|
|
4890
|
+
style: TextStyle(
|
|
4891
|
+
fontSize: 12,
|
|
4892
|
+
fontWeight: FontWeight.w700,
|
|
4893
|
+
color: _textPrimary,
|
|
4894
|
+
),
|
|
4895
|
+
),
|
|
4896
|
+
),
|
|
4897
|
+
],
|
|
4898
|
+
),
|
|
4899
|
+
const SizedBox(height: 4),
|
|
4900
|
+
Row(
|
|
4901
|
+
children: <Widget>[
|
|
4902
|
+
Container(
|
|
4903
|
+
width: 5,
|
|
4904
|
+
height: 5,
|
|
4905
|
+
decoration: BoxDecoration(
|
|
4906
|
+
shape: BoxShape.circle,
|
|
4907
|
+
color: color,
|
|
4908
|
+
),
|
|
4909
|
+
),
|
|
4910
|
+
const SizedBox(width: 5),
|
|
4911
|
+
Text(
|
|
4912
|
+
isSpecial ? node.nodeType : node.status,
|
|
4913
|
+
style: TextStyle(
|
|
4914
|
+
color: _textSecondary,
|
|
4915
|
+
fontSize: 11,
|
|
4916
|
+
),
|
|
4917
|
+
),
|
|
4918
|
+
],
|
|
4919
|
+
),
|
|
4920
|
+
],
|
|
4921
|
+
),
|
|
4922
|
+
),
|
|
4923
|
+
);
|
|
4924
|
+
}
|
|
4925
|
+
}
|
|
4926
|
+
|
|
4927
|
+
// ── Flow graph canvas ────────────────────────────────────────────────────────
|
|
4928
|
+
|
|
4929
|
+
class _RunFlowGraphCanvas extends StatefulWidget {
|
|
4930
|
+
const _RunFlowGraphCanvas({
|
|
4931
|
+
required this.run,
|
|
4932
|
+
required this.detail,
|
|
4933
|
+
required this.loading,
|
|
4934
|
+
required this.errorMessage,
|
|
4935
|
+
required this.selectedNodeId,
|
|
4936
|
+
required this.onNodeSelected,
|
|
4937
|
+
});
|
|
4938
|
+
|
|
4939
|
+
final RunSummary? run;
|
|
4940
|
+
final RunDetailSnapshot? detail;
|
|
4941
|
+
final bool loading;
|
|
4942
|
+
final String? errorMessage;
|
|
4943
|
+
final String? selectedNodeId;
|
|
4944
|
+
final ValueChanged<String?> onNodeSelected;
|
|
4945
|
+
|
|
4946
|
+
@override
|
|
4947
|
+
State<_RunFlowGraphCanvas> createState() => _RunFlowGraphCanvasState();
|
|
4948
|
+
}
|
|
4949
|
+
|
|
4950
|
+
class _RunFlowGraphCanvasState extends State<_RunFlowGraphCanvas> {
|
|
4951
|
+
final TransformationController _transform = TransformationController();
|
|
4952
|
+
String? _lastRunId;
|
|
4953
|
+
|
|
4954
|
+
@override
|
|
4955
|
+
void didUpdateWidget(covariant _RunFlowGraphCanvas old) {
|
|
4956
|
+
super.didUpdateWidget(old);
|
|
4957
|
+
final currentId = widget.detail?.run.id ?? widget.run?.id;
|
|
4958
|
+
if (currentId != _lastRunId && currentId != null) {
|
|
4959
|
+
_lastRunId = currentId;
|
|
4960
|
+
_transform.value = Matrix4.identity();
|
|
4961
|
+
}
|
|
4962
|
+
}
|
|
4963
|
+
|
|
4964
|
+
@override
|
|
4965
|
+
void dispose() {
|
|
4966
|
+
_transform.dispose();
|
|
4967
|
+
super.dispose();
|
|
4968
|
+
}
|
|
4969
|
+
|
|
4970
|
+
@override
|
|
4971
|
+
Widget build(BuildContext context) {
|
|
4972
|
+
if (widget.run == null) {
|
|
4973
|
+
return const _EmptyCard(
|
|
4974
|
+
title: 'Select a run',
|
|
4975
|
+
subtitle: 'Pick a run from the list on the left to explore its flow.',
|
|
4976
|
+
);
|
|
4977
|
+
}
|
|
4978
|
+
|
|
4979
|
+
if (widget.loading && widget.detail == null) {
|
|
4980
|
+
return Card(
|
|
4981
|
+
child: Padding(
|
|
4982
|
+
padding: const EdgeInsets.all(40),
|
|
4983
|
+
child: Row(
|
|
4984
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
4985
|
+
children: <Widget>[
|
|
4986
|
+
const SizedBox.square(
|
|
4987
|
+
dimension: 20,
|
|
4988
|
+
child: CircularProgressIndicator(strokeWidth: 2),
|
|
4989
|
+
),
|
|
4990
|
+
const SizedBox(width: 14),
|
|
4991
|
+
Text(
|
|
4992
|
+
'Loading run flow…',
|
|
4993
|
+
style: TextStyle(color: _textSecondary),
|
|
4994
|
+
),
|
|
4995
|
+
],
|
|
4996
|
+
),
|
|
4997
|
+
),
|
|
4998
|
+
);
|
|
4999
|
+
}
|
|
5000
|
+
|
|
5001
|
+
if (widget.errorMessage != null) {
|
|
5002
|
+
return _InlineError(message: widget.errorMessage!);
|
|
5003
|
+
}
|
|
5004
|
+
|
|
5005
|
+
final detail = widget.detail;
|
|
5006
|
+
if (detail == null) {
|
|
5007
|
+
return const _EmptyCard(
|
|
5008
|
+
title: 'No detail available',
|
|
5009
|
+
subtitle: 'This run has no recorded step data.',
|
|
5010
|
+
);
|
|
5011
|
+
}
|
|
5012
|
+
|
|
5013
|
+
final nodes = _buildRunFlowNodes(detail);
|
|
5014
|
+
final edges = _buildRunFlowEdges(nodes);
|
|
5015
|
+
final nodeMap = <String, _FlowNode>{for (final n in nodes) n.id: n};
|
|
5016
|
+
|
|
5017
|
+
final canvasW =
|
|
5018
|
+
nodes.fold(0.0, (m, n) => math.max(m, n.x + _FlowNode.w)) + 48;
|
|
5019
|
+
final canvasH =
|
|
5020
|
+
nodes.fold(0.0, (m, n) => math.max(m, n.y + _FlowNode.h)) + 48;
|
|
5021
|
+
|
|
5022
|
+
final stepCount = nodes.length - 2; // subtract start + end virtuals
|
|
5023
|
+
|
|
5024
|
+
return Card(
|
|
5025
|
+
child: Column(
|
|
5026
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
5027
|
+
children: <Widget>[
|
|
5028
|
+
Padding(
|
|
5029
|
+
padding: const EdgeInsets.fromLTRB(16, 14, 16, 0),
|
|
5030
|
+
child: Row(
|
|
5031
|
+
children: <Widget>[
|
|
5032
|
+
Expanded(
|
|
5033
|
+
child: Text(
|
|
5034
|
+
'$stepCount step${stepCount == 1 ? '' : 's'} · tap a node to inspect',
|
|
5035
|
+
style: TextStyle(
|
|
5036
|
+
color: _textSecondary,
|
|
5037
|
+
fontSize: 12.5,
|
|
5038
|
+
),
|
|
5039
|
+
),
|
|
5040
|
+
),
|
|
5041
|
+
if (widget.loading)
|
|
5042
|
+
const Padding(
|
|
5043
|
+
padding: EdgeInsets.only(right: 8),
|
|
5044
|
+
child: SizedBox.square(
|
|
5045
|
+
dimension: 14,
|
|
5046
|
+
child: CircularProgressIndicator(strokeWidth: 2),
|
|
5047
|
+
),
|
|
5048
|
+
),
|
|
5049
|
+
TextButton.icon(
|
|
5050
|
+
onPressed: () => _transform.value = Matrix4.identity(),
|
|
5051
|
+
style: TextButton.styleFrom(
|
|
5052
|
+
visualDensity: VisualDensity.compact,
|
|
5053
|
+
padding: const EdgeInsets.symmetric(
|
|
5054
|
+
horizontal: 10,
|
|
5055
|
+
vertical: 6,
|
|
5056
|
+
),
|
|
5057
|
+
),
|
|
5058
|
+
icon: const Icon(Icons.center_focus_strong_outlined, size: 15),
|
|
5059
|
+
label: const Text('Reset view', style: TextStyle(fontSize: 12)),
|
|
5060
|
+
),
|
|
5061
|
+
],
|
|
5062
|
+
),
|
|
5063
|
+
),
|
|
5064
|
+
const SizedBox(height: 8),
|
|
5065
|
+
SizedBox(
|
|
5066
|
+
height: math.max(220.0, math.min(canvasH + 24, 380.0)),
|
|
5067
|
+
child: ClipRect(
|
|
5068
|
+
child: InteractiveViewer(
|
|
5069
|
+
transformationController: _transform,
|
|
5070
|
+
constrained: false,
|
|
5071
|
+
minScale: 0.2,
|
|
5072
|
+
maxScale: 3.0,
|
|
5073
|
+
boundaryMargin: const EdgeInsets.all(64),
|
|
5074
|
+
child: Padding(
|
|
5075
|
+
padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
|
|
5076
|
+
child: SizedBox(
|
|
5077
|
+
width: canvasW,
|
|
5078
|
+
height: canvasH,
|
|
5079
|
+
child: Stack(
|
|
5080
|
+
children: <Widget>[
|
|
5081
|
+
Positioned.fill(
|
|
5082
|
+
child: CustomPaint(
|
|
5083
|
+
painter: _RunGraphEdgePainter(
|
|
5084
|
+
nodeMap: nodeMap,
|
|
5085
|
+
edges: edges,
|
|
5086
|
+
),
|
|
5087
|
+
),
|
|
5088
|
+
),
|
|
5089
|
+
...nodes.map(
|
|
5090
|
+
(node) => Positioned(
|
|
5091
|
+
left: node.x,
|
|
5092
|
+
top: node.y,
|
|
5093
|
+
child: _FlowNodeWidget(
|
|
5094
|
+
node: node,
|
|
5095
|
+
selected: node.id == widget.selectedNodeId,
|
|
5096
|
+
onTap: () => widget.onNodeSelected(
|
|
5097
|
+
node.id == widget.selectedNodeId ? null : node.id,
|
|
5098
|
+
),
|
|
5099
|
+
),
|
|
5100
|
+
),
|
|
5101
|
+
),
|
|
5102
|
+
],
|
|
5103
|
+
),
|
|
5104
|
+
),
|
|
5105
|
+
),
|
|
5106
|
+
),
|
|
5107
|
+
),
|
|
5108
|
+
),
|
|
5109
|
+
const SizedBox(height: 8),
|
|
5110
|
+
Padding(
|
|
5111
|
+
padding: const EdgeInsets.fromLTRB(16, 0, 16, 14),
|
|
5112
|
+
child: Wrap(
|
|
5113
|
+
spacing: 12,
|
|
5114
|
+
runSpacing: 8,
|
|
5115
|
+
children: <Widget>[
|
|
5116
|
+
_GraphLegendChip(color: _info, label: 'Model turn'),
|
|
5117
|
+
_GraphLegendChip(color: _success, label: 'Tool'),
|
|
5118
|
+
_GraphLegendChip(color: _warning, label: 'Plan'),
|
|
5119
|
+
_GraphLegendChip(color: _accentHover, label: 'Sub-agent'),
|
|
5120
|
+
_GraphLegendChip(color: _danger, label: 'Failed'),
|
|
5121
|
+
],
|
|
5122
|
+
),
|
|
5123
|
+
),
|
|
5124
|
+
],
|
|
5125
|
+
),
|
|
5126
|
+
);
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
|
|
5130
|
+
class _GraphLegendChip extends StatelessWidget {
|
|
5131
|
+
const _GraphLegendChip({required this.color, required this.label});
|
|
5132
|
+
|
|
5133
|
+
final Color color;
|
|
5134
|
+
final String label;
|
|
5135
|
+
|
|
5136
|
+
@override
|
|
5137
|
+
Widget build(BuildContext context) {
|
|
5138
|
+
return Row(
|
|
5139
|
+
mainAxisSize: MainAxisSize.min,
|
|
5140
|
+
children: <Widget>[
|
|
5141
|
+
Container(
|
|
5142
|
+
width: 8,
|
|
5143
|
+
height: 8,
|
|
5144
|
+
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
|
5145
|
+
),
|
|
5146
|
+
const SizedBox(width: 5),
|
|
5147
|
+
Text(label, style: TextStyle(fontSize: 11, color: _textSecondary)),
|
|
5148
|
+
],
|
|
5149
|
+
);
|
|
5150
|
+
}
|
|
5151
|
+
}
|
|
5152
|
+
|
|
5153
|
+
// ── Run selector rail ─────────────────────────────────────────────────────────
|
|
5154
|
+
|
|
5155
|
+
class _RunSelectorRail extends StatelessWidget {
|
|
5156
|
+
const _RunSelectorRail({
|
|
5157
|
+
required this.controller,
|
|
5158
|
+
required this.filteredRuns,
|
|
5159
|
+
required this.selectedRunId,
|
|
5160
|
+
required this.searchController,
|
|
5161
|
+
required this.statusFilter,
|
|
5162
|
+
required this.onStatusChanged,
|
|
5163
|
+
required this.onSelect,
|
|
5164
|
+
required this.onRefresh,
|
|
5165
|
+
});
|
|
5166
|
+
|
|
5167
|
+
final NeoAgentController controller;
|
|
5168
|
+
final List<RunSummary> filteredRuns;
|
|
5169
|
+
final String? selectedRunId;
|
|
5170
|
+
final TextEditingController searchController;
|
|
5171
|
+
final String statusFilter;
|
|
5172
|
+
final ValueChanged<String> onStatusChanged;
|
|
5173
|
+
final ValueChanged<String> onSelect;
|
|
5174
|
+
final VoidCallback onRefresh;
|
|
5175
|
+
|
|
5176
|
+
@override
|
|
5177
|
+
Widget build(BuildContext context) {
|
|
5178
|
+
const filters = <String>['all', 'running', 'completed', 'failed'];
|
|
5179
|
+
final totalLoaded = controller.recentRuns.length;
|
|
5180
|
+
|
|
5181
|
+
return Card(
|
|
5182
|
+
child: Padding(
|
|
5183
|
+
padding: const EdgeInsets.all(14),
|
|
5184
|
+
child: Column(
|
|
5185
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
5186
|
+
children: <Widget>[
|
|
5187
|
+
Row(
|
|
5188
|
+
children: <Widget>[
|
|
5189
|
+
Expanded(
|
|
5190
|
+
child: Text(
|
|
5191
|
+
'${filteredRuns.length}${filteredRuns.length != totalLoaded ? '/$totalLoaded' : ''} runs',
|
|
5192
|
+
style: const TextStyle(
|
|
5193
|
+
fontWeight: FontWeight.w700,
|
|
5194
|
+
fontSize: 13,
|
|
5195
|
+
),
|
|
5196
|
+
),
|
|
5197
|
+
),
|
|
5198
|
+
IconButton(
|
|
5199
|
+
tooltip: 'Refresh',
|
|
5200
|
+
icon: const Icon(Icons.refresh, size: 18),
|
|
5201
|
+
onPressed: onRefresh,
|
|
5202
|
+
visualDensity: VisualDensity.compact,
|
|
5203
|
+
),
|
|
5204
|
+
],
|
|
5205
|
+
),
|
|
5206
|
+
const SizedBox(height: 8),
|
|
5207
|
+
TextField(
|
|
5208
|
+
controller: searchController,
|
|
5209
|
+
decoration: InputDecoration(
|
|
5210
|
+
isDense: true,
|
|
5211
|
+
hintText: 'Search runs…',
|
|
5212
|
+
prefixIcon: const Icon(Icons.search, size: 18),
|
|
5213
|
+
suffixIcon: searchController.text.trim().isEmpty
|
|
5214
|
+
? null
|
|
5215
|
+
: IconButton(
|
|
5216
|
+
icon: const Icon(Icons.close, size: 16),
|
|
5217
|
+
onPressed: searchController.clear,
|
|
5218
|
+
visualDensity: VisualDensity.compact,
|
|
5219
|
+
),
|
|
5220
|
+
contentPadding: const EdgeInsets.symmetric(
|
|
5221
|
+
horizontal: 10,
|
|
5222
|
+
vertical: 8,
|
|
5223
|
+
),
|
|
5224
|
+
),
|
|
5225
|
+
),
|
|
5226
|
+
const SizedBox(height: 8),
|
|
5227
|
+
Wrap(
|
|
5228
|
+
spacing: 6,
|
|
5229
|
+
runSpacing: 6,
|
|
5230
|
+
children: filters.map((f) {
|
|
5231
|
+
return ChoiceChip(
|
|
5232
|
+
label: Text(
|
|
5233
|
+
_titleCase(f),
|
|
5234
|
+
style: const TextStyle(fontSize: 11),
|
|
5235
|
+
),
|
|
5236
|
+
selected: statusFilter == f,
|
|
5237
|
+
selectedColor: _accentMuted,
|
|
5238
|
+
backgroundColor: _bgSecondary,
|
|
5239
|
+
side: BorderSide(color: _border),
|
|
5240
|
+
onSelected: (_) => onStatusChanged(f),
|
|
5241
|
+
visualDensity: VisualDensity.compact,
|
|
5242
|
+
);
|
|
5243
|
+
}).toList(),
|
|
5244
|
+
),
|
|
5245
|
+
const SizedBox(height: 10),
|
|
5246
|
+
if (filteredRuns.isEmpty)
|
|
5247
|
+
Padding(
|
|
5248
|
+
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
5249
|
+
child: Text(
|
|
5250
|
+
'No matching runs',
|
|
5251
|
+
style: TextStyle(color: _textSecondary, fontSize: 12),
|
|
5252
|
+
),
|
|
5253
|
+
)
|
|
5254
|
+
else
|
|
5255
|
+
...<Widget>[
|
|
5256
|
+
ListView.separated(
|
|
5257
|
+
shrinkWrap: true,
|
|
5258
|
+
physics: const NeverScrollableScrollPhysics(),
|
|
5259
|
+
itemCount: math.min(filteredRuns.length, 40),
|
|
5260
|
+
separatorBuilder: (_, __) => const SizedBox(height: 6),
|
|
5261
|
+
itemBuilder: (context, index) {
|
|
5262
|
+
final run = filteredRuns[index];
|
|
5263
|
+
final isSelected = run.id == selectedRunId;
|
|
5264
|
+
return _RunSelectorRow(
|
|
5265
|
+
run: run,
|
|
5266
|
+
selected: isSelected,
|
|
5267
|
+
onTap: () => onSelect(run.id),
|
|
5268
|
+
);
|
|
5269
|
+
},
|
|
5270
|
+
),
|
|
5271
|
+
if (filteredRuns.length > 40)
|
|
5272
|
+
Padding(
|
|
5273
|
+
padding: const EdgeInsets.only(top: 8),
|
|
5274
|
+
child: Text(
|
|
5275
|
+
'Showing 40 of ${filteredRuns.length} — use search to narrow results',
|
|
5276
|
+
style: TextStyle(color: _textSecondary, fontSize: 11),
|
|
5277
|
+
),
|
|
5278
|
+
),
|
|
5279
|
+
],
|
|
5280
|
+
],
|
|
5281
|
+
),
|
|
5282
|
+
),
|
|
5283
|
+
);
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
|
|
5287
|
+
class _RunSelectorRow extends StatelessWidget {
|
|
5288
|
+
const _RunSelectorRow({
|
|
5289
|
+
required this.run,
|
|
5290
|
+
required this.selected,
|
|
5291
|
+
required this.onTap,
|
|
5292
|
+
});
|
|
5293
|
+
|
|
5294
|
+
final RunSummary run;
|
|
5295
|
+
final bool selected;
|
|
5296
|
+
final VoidCallback onTap;
|
|
5297
|
+
|
|
5298
|
+
@override
|
|
5299
|
+
Widget build(BuildContext context) {
|
|
5300
|
+
final color = run.statusColor;
|
|
5301
|
+
return InkWell(
|
|
5302
|
+
borderRadius: BorderRadius.circular(12),
|
|
5303
|
+
onTap: onTap,
|
|
5304
|
+
child: Container(
|
|
5305
|
+
padding: const EdgeInsets.all(10),
|
|
5306
|
+
decoration: BoxDecoration(
|
|
5307
|
+
color: selected ? _accentMuted : _bgSecondary,
|
|
5308
|
+
borderRadius: BorderRadius.circular(12),
|
|
5309
|
+
border: Border.all(color: selected ? _accent : _border),
|
|
5310
|
+
),
|
|
5311
|
+
child: Row(
|
|
5312
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
5313
|
+
children: <Widget>[
|
|
5314
|
+
Container(
|
|
5315
|
+
width: 8,
|
|
5316
|
+
height: 8,
|
|
5317
|
+
margin: const EdgeInsets.only(top: 4),
|
|
5318
|
+
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
|
5319
|
+
),
|
|
5320
|
+
const SizedBox(width: 8),
|
|
5321
|
+
Expanded(
|
|
5322
|
+
child: Column(
|
|
5323
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
5324
|
+
children: <Widget>[
|
|
5325
|
+
Text(
|
|
5326
|
+
run.title,
|
|
5327
|
+
maxLines: 2,
|
|
5328
|
+
overflow: TextOverflow.ellipsis,
|
|
5329
|
+
style: const TextStyle(
|
|
5330
|
+
fontWeight: FontWeight.w600,
|
|
5331
|
+
fontSize: 12,
|
|
5332
|
+
height: 1.3,
|
|
5333
|
+
),
|
|
5334
|
+
),
|
|
5335
|
+
const SizedBox(height: 3),
|
|
5336
|
+
Text(
|
|
5337
|
+
'${run.createdAtLabel} · ${run.durationLabel}',
|
|
5338
|
+
style: TextStyle(
|
|
5339
|
+
color: _textSecondary,
|
|
5340
|
+
fontSize: 11,
|
|
5341
|
+
),
|
|
5342
|
+
),
|
|
5343
|
+
],
|
|
5344
|
+
),
|
|
5345
|
+
),
|
|
5346
|
+
],
|
|
5347
|
+
),
|
|
5348
|
+
),
|
|
5349
|
+
);
|
|
5350
|
+
}
|
|
5351
|
+
}
|
|
5352
|
+
|
|
5353
|
+
// ── Node detail panel ─────────────────────────────────────────────────────────
|
|
5354
|
+
|
|
5355
|
+
class _RunNodeDetailPanel extends StatelessWidget {
|
|
5356
|
+
const _RunNodeDetailPanel({
|
|
5357
|
+
required this.run,
|
|
5358
|
+
required this.detail,
|
|
5359
|
+
required this.nodeId,
|
|
5360
|
+
required this.loading,
|
|
5361
|
+
required this.onDelete,
|
|
5362
|
+
required this.onCopyResponse,
|
|
5363
|
+
});
|
|
5364
|
+
|
|
5365
|
+
final RunSummary? run;
|
|
5366
|
+
final RunDetailSnapshot? detail;
|
|
5367
|
+
final String? nodeId;
|
|
5368
|
+
final bool loading;
|
|
5369
|
+
final Future<void> Function() onDelete;
|
|
5370
|
+
final Future<void> Function(String) onCopyResponse;
|
|
5371
|
+
|
|
5372
|
+
RunStepItem? get _selectedStep {
|
|
5373
|
+
if (nodeId == null || detail == null) return null;
|
|
5374
|
+
return detail!.steps.cast<RunStepItem?>().firstWhere(
|
|
5375
|
+
(s) => s?.id == nodeId,
|
|
5376
|
+
orElse: () => null,
|
|
5377
|
+
);
|
|
5378
|
+
}
|
|
5379
|
+
|
|
5380
|
+
@override
|
|
5381
|
+
Widget build(BuildContext context) {
|
|
5382
|
+
final r = run;
|
|
5383
|
+
final d = detail;
|
|
5384
|
+
final step = _selectedStep;
|
|
5385
|
+
|
|
5386
|
+
if (r == null) return const SizedBox.shrink();
|
|
5387
|
+
|
|
5388
|
+
return Column(
|
|
5389
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
5390
|
+
children: <Widget>[
|
|
5391
|
+
_RunHeroCard(run: r, onDelete: onDelete),
|
|
5392
|
+
const SizedBox(height: 12),
|
|
5393
|
+
if (step != null) ...<Widget>[
|
|
5394
|
+
_RunSelectedStepCard(step: step),
|
|
5395
|
+
const SizedBox(height: 12),
|
|
5396
|
+
] else if (d != null) ...<Widget>[
|
|
5397
|
+
_RunResponseCard(
|
|
5398
|
+
response: d.response,
|
|
5399
|
+
onCopy: () => onCopyResponse(d.response),
|
|
5400
|
+
),
|
|
5401
|
+
if (d.run.deliverableType.trim().isNotEmpty) ...<Widget>[
|
|
5402
|
+
const SizedBox(height: 12),
|
|
5403
|
+
_DeliverableSummaryCard(run: d.run),
|
|
5404
|
+
],
|
|
5405
|
+
const SizedBox(height: 12),
|
|
5406
|
+
Wrap(
|
|
5407
|
+
spacing: 10,
|
|
5408
|
+
runSpacing: 10,
|
|
5409
|
+
children: <Widget>[
|
|
5410
|
+
_RunMetricCard(
|
|
5411
|
+
title: 'Steps',
|
|
5412
|
+
value: '${d.steps.length}',
|
|
5413
|
+
helper: 'Recorded',
|
|
5414
|
+
color: _info,
|
|
5415
|
+
),
|
|
5416
|
+
_RunMetricCard(
|
|
5417
|
+
title: 'Done',
|
|
5418
|
+
value: '${d.completedTools}',
|
|
5419
|
+
helper: 'Successful',
|
|
5420
|
+
color: _success,
|
|
5421
|
+
),
|
|
5422
|
+
_RunMetricCard(
|
|
5423
|
+
title: 'Failed',
|
|
5424
|
+
value: '${d.failedTools}',
|
|
5425
|
+
helper: 'Errors',
|
|
5426
|
+
color: _danger,
|
|
5427
|
+
),
|
|
5428
|
+
],
|
|
5429
|
+
),
|
|
5430
|
+
] else if (loading)
|
|
5431
|
+
const Card(
|
|
5432
|
+
child: Padding(
|
|
5433
|
+
padding: EdgeInsets.all(20),
|
|
5434
|
+
child: Center(child: CircularProgressIndicator()),
|
|
5435
|
+
),
|
|
5436
|
+
),
|
|
5437
|
+
],
|
|
5438
|
+
);
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
5441
|
+
|
|
5442
|
+
class _RunSelectedStepCard extends StatelessWidget {
|
|
5443
|
+
const _RunSelectedStepCard({required this.step});
|
|
5444
|
+
|
|
5445
|
+
final RunStepItem step;
|
|
5446
|
+
|
|
5447
|
+
@override
|
|
5448
|
+
Widget build(BuildContext context) {
|
|
5449
|
+
final color = step.statusColor;
|
|
5450
|
+
return Card(
|
|
5451
|
+
child: Padding(
|
|
5452
|
+
padding: const EdgeInsets.all(16),
|
|
5453
|
+
child: Column(
|
|
5454
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
5455
|
+
children: <Widget>[
|
|
5456
|
+
Row(
|
|
5457
|
+
children: <Widget>[
|
|
5458
|
+
Container(
|
|
5459
|
+
padding: const EdgeInsets.all(8),
|
|
5460
|
+
decoration: BoxDecoration(
|
|
5461
|
+
color: color.withValues(alpha: 0.14),
|
|
5462
|
+
borderRadius: BorderRadius.circular(10),
|
|
5463
|
+
),
|
|
5464
|
+
child: Icon(Icons.build_circle_outlined, size: 18, color: color),
|
|
5465
|
+
),
|
|
5466
|
+
const SizedBox(width: 10),
|
|
5467
|
+
Expanded(
|
|
5468
|
+
child: Column(
|
|
5469
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
5470
|
+
children: <Widget>[
|
|
5471
|
+
Text(
|
|
5472
|
+
step.label,
|
|
5473
|
+
style: const TextStyle(
|
|
5474
|
+
fontWeight: FontWeight.w700,
|
|
5475
|
+
fontSize: 14,
|
|
5476
|
+
),
|
|
5477
|
+
),
|
|
5478
|
+
const SizedBox(height: 2),
|
|
5479
|
+
Text(
|
|
5480
|
+
step.typeLabel,
|
|
5481
|
+
style: TextStyle(
|
|
5482
|
+
color: _textSecondary,
|
|
5483
|
+
fontSize: 12,
|
|
5484
|
+
),
|
|
5485
|
+
),
|
|
5486
|
+
],
|
|
5487
|
+
),
|
|
5488
|
+
),
|
|
5489
|
+
_StatusPill(label: step.statusLabel, color: color),
|
|
5490
|
+
],
|
|
5491
|
+
),
|
|
5492
|
+
const SizedBox(height: 10),
|
|
5493
|
+
Wrap(
|
|
5494
|
+
spacing: 8,
|
|
5495
|
+
runSpacing: 8,
|
|
5496
|
+
children: <Widget>[
|
|
5497
|
+
if (step.startedAt != null)
|
|
5498
|
+
_MetaPill(
|
|
5499
|
+
label: step.startedAtLabel!,
|
|
5500
|
+
icon: Icons.schedule_outlined,
|
|
5501
|
+
),
|
|
5502
|
+
if (step.durationLabel != null)
|
|
5503
|
+
_MetaPill(
|
|
5504
|
+
label: step.durationLabel!,
|
|
5505
|
+
icon: Icons.timer_outlined,
|
|
5506
|
+
),
|
|
5507
|
+
if (step.tokensUsed > 0)
|
|
5508
|
+
_MetaPill(
|
|
5509
|
+
label: '${_formatNumber(step.tokensUsed)} tokens',
|
|
5510
|
+
icon: Icons.toll_outlined,
|
|
5511
|
+
),
|
|
5512
|
+
],
|
|
5513
|
+
),
|
|
5514
|
+
if (step.description.trim().isNotEmpty &&
|
|
5515
|
+
step.description.trim() != step.summary.trim()) ...<Widget>[
|
|
5516
|
+
const SizedBox(height: 10),
|
|
5517
|
+
_RunDetailBlock(label: 'Description', value: step.description),
|
|
5518
|
+
],
|
|
5519
|
+
if (step.inputSummary.trim().isNotEmpty) ...<Widget>[
|
|
5520
|
+
const SizedBox(height: 8),
|
|
5521
|
+
_RunDetailBlock(label: 'Input', value: step.inputSummary),
|
|
5522
|
+
],
|
|
5523
|
+
if (step.error.trim().isNotEmpty) ...<Widget>[
|
|
5524
|
+
const SizedBox(height: 8),
|
|
5525
|
+
_RunDetailBlock(label: 'Error', value: step.error, monospace: true),
|
|
5526
|
+
] else if (step.result.trim().isNotEmpty) ...<Widget>[
|
|
5527
|
+
const SizedBox(height: 8),
|
|
5528
|
+
_RunDetailBlock(
|
|
5529
|
+
label: 'Result',
|
|
5530
|
+
value: _truncateRunText(step.result),
|
|
5531
|
+
monospace: true,
|
|
5532
|
+
),
|
|
5533
|
+
],
|
|
5534
|
+
],
|
|
5535
|
+
),
|
|
5536
|
+
),
|
|
5537
|
+
);
|
|
5538
|
+
}
|
|
5539
|
+
}
|
|
5540
|
+
|
|
5541
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
5542
|
+
|
|
4206
5543
|
Future<void> openMessagingConfig(
|
|
4207
5544
|
BuildContext context,
|
|
4208
5545
|
NeoAgentController controller,
|