neoagent 3.2.1-beta.2 → 3.2.1-beta.4

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.
Files changed (145) hide show
  1. package/extensions/chrome-browser/background.mjs +318 -88
  2. package/extensions/chrome-browser/http.mjs +136 -0
  3. package/extensions/chrome-browser/protocol.mjs +511 -89
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +29 -20
  6. package/flutter_app/lib/main_models.dart +3 -0
  7. package/flutter_app/lib/main_operations.dart +334 -321
  8. package/flutter_app/lib/main_settings.dart +4 -3
  9. package/flutter_app/lib/main_shared.dart +14 -11
  10. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  11. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  12. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  13. package/lib/manager.js +106 -89
  14. package/lib/schema_migrations.js +67 -13
  15. package/package.json +20 -13
  16. package/runtime/paths.js +49 -5
  17. package/server/guest_agent.js +52 -30
  18. package/server/http/middleware.js +24 -0
  19. package/server/http/routes.js +4 -6
  20. package/server/public/.last_build_id +1 -1
  21. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  22. package/server/public/flutter_bootstrap.js +1 -1
  23. package/server/public/main.dart.js +30803 -30805
  24. package/server/routes/admin.js +1 -1
  25. package/server/routes/android.js +30 -34
  26. package/server/routes/browser.js +23 -15
  27. package/server/routes/desktop.js +18 -1
  28. package/server/routes/integrations.js +5 -1
  29. package/server/routes/memory.js +1 -0
  30. package/server/routes/settings.js +16 -5
  31. package/server/routes/social_reach.js +12 -3
  32. package/server/routes/social_video.js +4 -0
  33. package/server/services/ai/compaction.js +7 -2
  34. package/server/services/ai/history.js +44 -5
  35. package/server/services/ai/integrated_tools/http_request.js +8 -0
  36. package/server/services/ai/loop/agent_engine_core.js +347 -162
  37. package/server/services/ai/loop/callbacks.js +1 -0
  38. package/server/services/ai/loop/completion_judge.js +12 -0
  39. package/server/services/ai/loop/conversation_loop.js +348 -242
  40. package/server/services/ai/loop/messaging_delivery.js +129 -57
  41. package/server/services/ai/loop/model_call_guard.js +91 -0
  42. package/server/services/ai/loop/model_io.js +8 -41
  43. package/server/services/ai/loop/tool_dispatch.js +19 -8
  44. package/server/services/ai/loopPolicy.js +24 -19
  45. package/server/services/ai/model_discovery.js +227 -0
  46. package/server/services/ai/model_identity.js +71 -0
  47. package/server/services/ai/models.js +67 -162
  48. package/server/services/ai/providerRetry.js +17 -59
  49. package/server/services/ai/provider_selector.js +111 -0
  50. package/server/services/ai/providers/anthropic.js +2 -2
  51. package/server/services/ai/providers/claudeCode.js +15 -28
  52. package/server/services/ai/providers/githubCopilot.js +36 -16
  53. package/server/services/ai/providers/google.js +23 -5
  54. package/server/services/ai/providers/grok.js +4 -3
  55. package/server/services/ai/providers/grokOauth.js +13 -22
  56. package/server/services/ai/providers/nvidia.js +10 -5
  57. package/server/services/ai/providers/ollama.js +102 -82
  58. package/server/services/ai/providers/ollama_stream.js +142 -0
  59. package/server/services/ai/providers/openai.js +39 -5
  60. package/server/services/ai/providers/openaiCodex.js +11 -4
  61. package/server/services/ai/providers/openrouter.js +29 -7
  62. package/server/services/ai/providers/provider_error.js +36 -0
  63. package/server/services/ai/settings.js +26 -2
  64. package/server/services/ai/taskAnalysis.js +5 -1
  65. package/server/services/ai/terminal_reply.js +45 -0
  66. package/server/services/ai/toolEvidence.js +58 -29
  67. package/server/services/ai/tools.js +124 -111
  68. package/server/services/android/controller.js +770 -237
  69. package/server/services/android/process.js +140 -0
  70. package/server/services/android/sdk_download.js +143 -0
  71. package/server/services/android/uia.js +6 -5
  72. package/server/services/artifacts/store.js +24 -0
  73. package/server/services/browser/controller.js +736 -385
  74. package/server/services/browser/extension/gateway.js +40 -16
  75. package/server/services/browser/extension/protocol.js +12 -1
  76. package/server/services/browser/extension/provider.js +59 -47
  77. package/server/services/browser/extension/registry.js +155 -34
  78. package/server/services/cli/executor.js +62 -9
  79. package/server/services/desktop/gateway.js +41 -4
  80. package/server/services/desktop/protocol.js +3 -0
  81. package/server/services/desktop/provider.js +39 -42
  82. package/server/services/desktop/registry.js +137 -52
  83. package/server/services/integrations/figma/provider.js +78 -12
  84. package/server/services/integrations/github/common.js +11 -6
  85. package/server/services/integrations/github/provider.js +52 -53
  86. package/server/services/integrations/google/provider.js +55 -19
  87. package/server/services/integrations/home_assistant/network.js +17 -20
  88. package/server/services/integrations/home_assistant/provider.js +7 -5
  89. package/server/services/integrations/home_assistant/tools.js +17 -5
  90. package/server/services/integrations/http.js +51 -0
  91. package/server/services/integrations/manager.js +158 -53
  92. package/server/services/integrations/microsoft/provider.js +80 -13
  93. package/server/services/integrations/neoarchive/provider.js +55 -29
  94. package/server/services/integrations/neorecall/client.js +17 -10
  95. package/server/services/integrations/neorecall/provider.js +20 -11
  96. package/server/services/integrations/notion/provider.js +16 -13
  97. package/server/services/integrations/oauth_provider.js +115 -51
  98. package/server/services/integrations/slack/provider.js +98 -9
  99. package/server/services/integrations/spotify/provider.js +67 -71
  100. package/server/services/integrations/trello/provider.js +21 -7
  101. package/server/services/integrations/weather/provider.js +18 -12
  102. package/server/services/integrations/whatsapp/provider.js +76 -16
  103. package/server/services/manager.js +87 -1
  104. package/server/services/memory/embedding_index.js +20 -8
  105. package/server/services/memory/embeddings.js +151 -90
  106. package/server/services/memory/ingestion.js +50 -9
  107. package/server/services/memory/ingestion_documents.js +13 -3
  108. package/server/services/memory/manager.js +52 -19
  109. package/server/services/messaging/automation.js +84 -9
  110. package/server/services/messaging/http_platforms.js +33 -13
  111. package/server/services/messaging/inbound_queue.js +78 -24
  112. package/server/services/messaging/inbound_store.js +224 -0
  113. package/server/services/messaging/manager.js +326 -51
  114. package/server/services/messaging/typing_keepalive.js +5 -2
  115. package/server/services/messaging/whatsapp.js +22 -14
  116. package/server/services/network/http.js +210 -0
  117. package/server/services/network/safe_request.js +307 -0
  118. package/server/services/runtime/backends/local-vm.js +214 -66
  119. package/server/services/runtime/manager.js +17 -12
  120. package/server/services/social_reach/channels/github.js +10 -4
  121. package/server/services/social_reach/channels/reddit.js +4 -4
  122. package/server/services/social_reach/channels/rss.js +2 -2
  123. package/server/services/social_reach/channels/social_video.js +12 -7
  124. package/server/services/social_reach/channels/v2ex.js +21 -8
  125. package/server/services/social_reach/channels/x.js +2 -2
  126. package/server/services/social_reach/channels/xueqiu.js +5 -5
  127. package/server/services/social_reach/service.js +9 -6
  128. package/server/services/social_reach/utils.js +65 -14
  129. package/server/services/social_video/service.js +160 -50
  130. package/server/services/tasks/integration_runtime.js +18 -8
  131. package/server/services/tasks/runtime.js +39 -4
  132. package/server/services/voice/agentBridge.js +17 -4
  133. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  134. package/server/services/voice/liveSession.js +31 -0
  135. package/server/services/voice/openaiSpeech.js +33 -8
  136. package/server/services/voice/providers.js +233 -151
  137. package/server/services/voice/runtimeManager.js +118 -20
  138. package/server/services/voice/turnRunner.js +6 -0
  139. package/server/services/wearable/firmware_manifest.js +51 -13
  140. package/server/services/wearable/service.js +1 -0
  141. package/server/utils/abort.js +96 -0
  142. package/server/utils/cloud-security.js +110 -3
  143. package/server/utils/files.js +31 -0
  144. package/server/utils/image_payload.js +95 -0
  145. package/server/utils/retry.js +107 -0
@@ -436,7 +436,9 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
436
436
  double? prevExtent;
437
437
  void settleInitial(int framesLeft) {
438
438
  WidgetsBinding.instance.addPostFrameCallback((_) {
439
- if (!mounted || generation != _scrollGeneration || !_scrollController.hasClients) {
439
+ if (!mounted ||
440
+ generation != _scrollGeneration ||
441
+ !_scrollController.hasClients) {
440
442
  return;
441
443
  }
442
444
  final pos = _scrollController.position;
@@ -463,6 +465,7 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
463
465
  settleInitial(framesLeft - 1);
464
466
  });
465
467
  }
468
+
466
469
  settleInitial(120);
467
470
  return;
468
471
  }
@@ -511,7 +514,8 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
511
514
  if (messages.isEmpty) {
512
515
  // Agent switch resets the settle tracker so the next batch triggers hiding.
513
516
  _visibleMessageCountAtLastSettle = 0;
514
- } else if (_visibleMessageCountAtLastSettle == 0 && !_awaitingInitialScrollSettle) {
517
+ } else if (_visibleMessageCountAtLastSettle == 0 &&
518
+ !_awaitingInitialScrollSettle) {
515
519
  // Messages just went from 0 → N: hide until the scroll position settles.
516
520
  _awaitingInitialScrollSettle = true;
517
521
  }
@@ -2039,7 +2043,11 @@ class _IgnoredChatsPanel extends StatelessWidget {
2039
2043
  const SizedBox(height: 3),
2040
2044
  Text(
2041
2045
  'These channels are permanently silenced. To receive messages from them, add them manually to the access policy for the relevant platform.',
2042
- style: TextStyle(color: _textSecondary, fontSize: 13, height: 1.4),
2046
+ style: TextStyle(
2047
+ color: _textSecondary,
2048
+ fontSize: 13,
2049
+ height: 1.4,
2050
+ ),
2043
2051
  ),
2044
2052
  ],
2045
2053
  ),
@@ -2049,50 +2057,58 @@ class _IgnoredChatsPanel extends StatelessWidget {
2049
2057
  ),
2050
2058
  const SizedBox(height: 14),
2051
2059
  for (final key in ignored)
2052
- Builder(builder: (context) {
2053
- final sep = key.indexOf(':');
2054
- final platform = sep > 0 ? key.substring(0, sep) : key;
2055
- final chatId = sep > 0 ? key.substring(sep + 1) : '';
2056
- return Container(
2057
- margin: const EdgeInsets.only(bottom: 8),
2058
- padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
2059
- decoration: BoxDecoration(
2060
- color: _bgSecondary,
2061
- borderRadius: BorderRadius.circular(8),
2062
- border: Border.all(color: _borderLight),
2063
- ),
2064
- child: Row(
2065
- children: [
2066
- Icon(Icons.block_rounded, size: 16, color: _textMuted),
2067
- const SizedBox(width: 10),
2068
- Expanded(
2069
- child: Column(
2070
- crossAxisAlignment: CrossAxisAlignment.start,
2071
- children: [
2072
- Text(
2073
- platform.toUpperCase(),
2074
- style: TextStyle(
2075
- color: _textMuted,
2076
- fontSize: 10,
2077
- fontWeight: FontWeight.w700,
2078
- letterSpacing: 0.6,
2060
+ Builder(
2061
+ builder: (context) {
2062
+ final sep = key.indexOf(':');
2063
+ final platform = sep > 0 ? key.substring(0, sep) : key;
2064
+ final chatId = sep > 0 ? key.substring(sep + 1) : '';
2065
+ return Container(
2066
+ margin: const EdgeInsets.only(bottom: 8),
2067
+ padding: const EdgeInsets.symmetric(
2068
+ horizontal: 14,
2069
+ vertical: 10,
2070
+ ),
2071
+ decoration: BoxDecoration(
2072
+ color: _bgSecondary,
2073
+ borderRadius: BorderRadius.circular(8),
2074
+ border: Border.all(color: _borderLight),
2075
+ ),
2076
+ child: Row(
2077
+ children: [
2078
+ Icon(Icons.block_rounded, size: 16, color: _textMuted),
2079
+ const SizedBox(width: 10),
2080
+ Expanded(
2081
+ child: Column(
2082
+ crossAxisAlignment: CrossAxisAlignment.start,
2083
+ children: [
2084
+ Text(
2085
+ platform.toUpperCase(),
2086
+ style: TextStyle(
2087
+ color: _textMuted,
2088
+ fontSize: 10,
2089
+ fontWeight: FontWeight.w700,
2090
+ letterSpacing: 0.6,
2091
+ ),
2079
2092
  ),
2080
- ),
2081
- Text(
2082
- chatId.isNotEmpty ? chatId : platform,
2083
- style: TextStyle(color: _textPrimary, fontSize: 13),
2084
- ),
2085
- ],
2093
+ Text(
2094
+ chatId.isNotEmpty ? chatId : platform,
2095
+ style: TextStyle(
2096
+ color: _textPrimary,
2097
+ fontSize: 13,
2098
+ ),
2099
+ ),
2100
+ ],
2101
+ ),
2086
2102
  ),
2087
- ),
2088
- TextButton(
2089
- onPressed: () => controller.removeIgnoredChat(key),
2090
- child: Text('Remove'),
2091
- ),
2092
- ],
2093
- ),
2094
- );
2095
- }),
2103
+ TextButton(
2104
+ onPressed: () => controller.removeIgnoredChat(key),
2105
+ child: Text('Remove'),
2106
+ ),
2107
+ ],
2108
+ ),
2109
+ );
2110
+ },
2111
+ ),
2096
2112
  ],
2097
2113
  ),
2098
2114
  );
@@ -2236,7 +2252,9 @@ class _ConnectionReconnectingBanner extends StatelessWidget {
2236
2252
 
2237
2253
  @override
2238
2254
  Widget build(BuildContext context) {
2239
- final msg = hasNetwork ? 'Reconnecting to server…' : 'No network connection';
2255
+ final msg = hasNetwork
2256
+ ? 'Reconnecting to server…'
2257
+ : 'No network connection';
2240
2258
  return Container(
2241
2259
  padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
2242
2260
  decoration: BoxDecoration(
@@ -2308,7 +2326,6 @@ class _PendingApprovalBannerState extends State<_PendingApprovalBanner> {
2308
2326
 
2309
2327
  int _remaining() {
2310
2328
  final expiry = widget.approval.expiresAt;
2311
- if (expiry == null) return 30;
2312
2329
  return expiry.difference(DateTime.now()).inSeconds.clamp(0, 300);
2313
2330
  }
2314
2331
 
@@ -2340,11 +2357,7 @@ class _PendingApprovalBannerState extends State<_PendingApprovalBanner> {
2340
2357
  color: _accent.withValues(alpha: 0.16),
2341
2358
  shape: BoxShape.circle,
2342
2359
  ),
2343
- child: Icon(
2344
- Icons.security_outlined,
2345
- size: 13,
2346
- color: _accent,
2347
- ),
2360
+ child: Icon(Icons.security_outlined, size: 13, color: _accent),
2348
2361
  ),
2349
2362
  ),
2350
2363
  const SizedBox(width: 10),
@@ -3685,61 +3698,6 @@ class _MessagingMiniPill extends StatelessWidget {
3685
3698
  }
3686
3699
  }
3687
3700
 
3688
- class _RunsMetricsStrip extends StatelessWidget {
3689
- const _RunsMetricsStrip({required this.runs, required this.totalLoaded});
3690
-
3691
- final List<RunSummary> runs;
3692
- final int totalLoaded;
3693
-
3694
- @override
3695
- Widget build(BuildContext context) {
3696
- final running = runs.where((run) => run.status == 'running').length;
3697
- final failed = runs.where((run) => run.isFailure).length;
3698
- final completed = runs.where((run) => run.status == 'completed').length;
3699
- final tokens = runs.fold<int>(0, (sum, run) => sum + run.totalTokens);
3700
-
3701
- return Wrap(
3702
- spacing: 12,
3703
- runSpacing: 12,
3704
- children: <Widget>[
3705
- _RunMetricCard(
3706
- title: 'Showing',
3707
- value: '${runs.length}',
3708
- helper: totalLoaded == runs.length
3709
- ? 'Recent runs loaded'
3710
- : 'Filtered from $totalLoaded loaded runs',
3711
- color: _info,
3712
- ),
3713
- _RunMetricCard(
3714
- title: 'Completed',
3715
- value: '$completed',
3716
- helper: 'Finished successfully',
3717
- color: _success,
3718
- ),
3719
- _RunMetricCard(
3720
- title: 'Failed',
3721
- value: '$failed',
3722
- helper: 'Need attention',
3723
- color: _danger,
3724
- ),
3725
- _RunMetricCard(
3726
- title: 'Tokens',
3727
- value: _formatNumber(tokens),
3728
- helper: 'Across visible runs',
3729
- color: _accentHover,
3730
- ),
3731
- if (running > 0)
3732
- _RunMetricCard(
3733
- title: 'Running',
3734
- value: '$running',
3735
- helper: 'Still in progress',
3736
- color: _warning,
3737
- ),
3738
- ],
3739
- );
3740
- }
3741
- }
3742
-
3743
3701
  class _RunMetricCard extends StatelessWidget {
3744
3702
  const _RunMetricCard({
3745
3703
  required this.title,
@@ -3787,323 +3745,6 @@ class _RunMetricCard extends StatelessWidget {
3787
3745
  }
3788
3746
  }
3789
3747
 
3790
- class _RunsFilterBar extends StatelessWidget {
3791
- const _RunsFilterBar({
3792
- required this.searchController,
3793
- required this.statusFilter,
3794
- required this.onStatusChanged,
3795
- });
3796
-
3797
- final TextEditingController searchController;
3798
- final String statusFilter;
3799
- final ValueChanged<String> onStatusChanged;
3800
-
3801
- @override
3802
- Widget build(BuildContext context) {
3803
- const filters = <String>['all', 'running', 'completed', 'failed'];
3804
- return Card(
3805
- child: Padding(
3806
- padding: const EdgeInsets.all(18),
3807
- child: Column(
3808
- crossAxisAlignment: CrossAxisAlignment.start,
3809
- children: <Widget>[
3810
- const _SectionTitle('Filter Runs'),
3811
- const SizedBox(height: 12),
3812
- TextField(
3813
- controller: searchController,
3814
- decoration: InputDecoration(
3815
- prefixIcon: Icon(Icons.search),
3816
- hintText: 'Search title, model, trigger, error, or run id',
3817
- suffixIcon: searchController.text.trim().isEmpty
3818
- ? null
3819
- : IconButton(
3820
- tooltip: 'Clear search',
3821
- onPressed: searchController.clear,
3822
- icon: Icon(Icons.close),
3823
- ),
3824
- ),
3825
- ),
3826
- const SizedBox(height: 14),
3827
- Wrap(
3828
- spacing: 10,
3829
- runSpacing: 10,
3830
- children: filters.map((filter) {
3831
- return FilterChip(
3832
- label: Text(_titleCase(filter)),
3833
- selected: statusFilter == filter,
3834
- selectedColor: _accentMuted,
3835
- checkmarkColor: _accent,
3836
- backgroundColor: _bgSecondary,
3837
- side: BorderSide(color: _border),
3838
- onSelected: (_) => onStatusChanged(filter),
3839
- );
3840
- }).toList(),
3841
- ),
3842
- ],
3843
- ),
3844
- ),
3845
- );
3846
- }
3847
- }
3848
-
3849
- class _RunsHistoryPane extends StatelessWidget {
3850
- const _RunsHistoryPane({
3851
- required this.runs,
3852
- required this.selectedRunId,
3853
- required this.onSelect,
3854
- });
3855
-
3856
- final List<RunSummary> runs;
3857
- final String? selectedRunId;
3858
- final ValueChanged<String> onSelect;
3859
-
3860
- @override
3861
- Widget build(BuildContext context) {
3862
- return Card(
3863
- child: Padding(
3864
- padding: const EdgeInsets.all(18),
3865
- child: Column(
3866
- crossAxisAlignment: CrossAxisAlignment.start,
3867
- children: <Widget>[
3868
- Row(
3869
- children: <Widget>[
3870
- Expanded(child: _SectionTitle('Run History')),
3871
- Text(
3872
- '${runs.length} items',
3873
- style: TextStyle(color: _textSecondary),
3874
- ),
3875
- ],
3876
- ),
3877
- const SizedBox(height: 12),
3878
- ...runs.map((run) {
3879
- return Padding(
3880
- padding: const EdgeInsets.only(bottom: 10),
3881
- child: _RunHistoryRow(
3882
- run: run,
3883
- selected: run.id == selectedRunId,
3884
- onTap: () => onSelect(run.id),
3885
- ),
3886
- );
3887
- }),
3888
- ],
3889
- ),
3890
- ),
3891
- );
3892
- }
3893
- }
3894
-
3895
- class _RunHistoryRow extends StatelessWidget {
3896
- const _RunHistoryRow({
3897
- required this.run,
3898
- required this.selected,
3899
- required this.onTap,
3900
- });
3901
-
3902
- final RunSummary run;
3903
- final bool selected;
3904
- final VoidCallback onTap;
3905
-
3906
- @override
3907
- Widget build(BuildContext context) {
3908
- return InkWell(
3909
- borderRadius: BorderRadius.circular(16),
3910
- onTap: onTap,
3911
- child: Container(
3912
- padding: const EdgeInsets.all(14),
3913
- decoration: BoxDecoration(
3914
- color: selected ? _accentMuted : _bgSecondary,
3915
- borderRadius: BorderRadius.circular(16),
3916
- border: Border.all(color: selected ? _accent : _border),
3917
- ),
3918
- child: Row(
3919
- crossAxisAlignment: CrossAxisAlignment.start,
3920
- children: <Widget>[
3921
- Container(
3922
- width: 12,
3923
- height: 12,
3924
- margin: const EdgeInsets.only(top: 5),
3925
- decoration: BoxDecoration(
3926
- color: run.statusColor,
3927
- shape: BoxShape.circle,
3928
- ),
3929
- ),
3930
- const SizedBox(width: 12),
3931
- Expanded(
3932
- child: Column(
3933
- crossAxisAlignment: CrossAxisAlignment.start,
3934
- children: <Widget>[
3935
- Text(
3936
- run.title,
3937
- maxLines: 2,
3938
- overflow: TextOverflow.ellipsis,
3939
- style: TextStyle(fontWeight: FontWeight.w700, height: 1.2),
3940
- ),
3941
- const SizedBox(height: 6),
3942
- Text(
3943
- '${run.triggerLabel} • ${run.createdAtLabel}${run.durationLabel == 'In progress' ? '' : ' • ${run.durationLabel}'}',
3944
- style: TextStyle(color: _textSecondary, fontSize: 12),
3945
- ),
3946
- const SizedBox(height: 4),
3947
- Text(
3948
- '${run.modelLabel} • ${run.totalTokensLabel} tokens',
3949
- style: TextStyle(color: _textSecondary, fontSize: 12),
3950
- ),
3951
- if (run.deliverableType.trim().isNotEmpty) ...<Widget>[
3952
- const SizedBox(height: 4),
3953
- Text(
3954
- 'Deliverable • ${run.deliverableType.replaceAll('_', ' ')}',
3955
- style: TextStyle(color: _accent, fontSize: 12),
3956
- ),
3957
- ],
3958
- if (run.error.trim().isNotEmpty) ...<Widget>[
3959
- const SizedBox(height: 8),
3960
- Text(
3961
- run.error,
3962
- maxLines: 2,
3963
- overflow: TextOverflow.ellipsis,
3964
- style: TextStyle(
3965
- color: _danger,
3966
- fontSize: 12,
3967
- height: 1.4,
3968
- ),
3969
- ),
3970
- ],
3971
- ],
3972
- ),
3973
- ),
3974
- const SizedBox(width: 10),
3975
- Column(
3976
- crossAxisAlignment: CrossAxisAlignment.end,
3977
- children: <Widget>[
3978
- _StatusPill(label: run.statusLabel, color: run.statusColor),
3979
- const SizedBox(height: 12),
3980
- Icon(
3981
- Icons.chevron_right,
3982
- color: selected ? _textPrimary : _textSecondary,
3983
- ),
3984
- ],
3985
- ),
3986
- ],
3987
- ),
3988
- ),
3989
- );
3990
- }
3991
- }
3992
-
3993
- class _RunDetailWorkspace extends StatelessWidget {
3994
- const _RunDetailWorkspace({
3995
- required this.run,
3996
- required this.detail,
3997
- required this.errorMessage,
3998
- required this.loading,
3999
- required this.onDelete,
4000
- required this.onCopyResponse,
4001
- });
4002
-
4003
- final RunSummary? run;
4004
- final RunDetailSnapshot? detail;
4005
- final String? errorMessage;
4006
- final bool loading;
4007
- final Future<void> Function() onDelete;
4008
- final Future<void> Function(String response) onCopyResponse;
4009
-
4010
- @override
4011
- Widget build(BuildContext context) {
4012
- if (run == null) {
4013
- return const _EmptyCard(
4014
- title: 'Select a run',
4015
- subtitle: 'Pick a run from the history list to inspect its steps.',
4016
- );
4017
- }
4018
-
4019
- final selectedRun = run!;
4020
- final snapshot = detail;
4021
- return Column(
4022
- children: <Widget>[
4023
- _RunHeroCard(run: selectedRun, onDelete: onDelete),
4024
- const SizedBox(height: 16),
4025
- if (loading && snapshot == null)
4026
- Card(
4027
- child: Padding(
4028
- padding: EdgeInsets.all(24),
4029
- child: Row(
4030
- children: <Widget>[
4031
- SizedBox.square(
4032
- dimension: 20,
4033
- child: CircularProgressIndicator(strokeWidth: 2),
4034
- ),
4035
- SizedBox(width: 12),
4036
- Text(
4037
- 'Loading run detail...',
4038
- style: TextStyle(color: _textSecondary),
4039
- ),
4040
- ],
4041
- ),
4042
- ),
4043
- )
4044
- else if (errorMessage case final message?) ...<Widget>[
4045
- _InlineError(message: message),
4046
- const SizedBox(height: 16),
4047
- ] else if (snapshot != null) ...<Widget>[
4048
- Wrap(
4049
- spacing: 12,
4050
- runSpacing: 12,
4051
- children: <Widget>[
4052
- _RunMetricCard(
4053
- title: 'Steps',
4054
- value: '${snapshot.steps.length}',
4055
- helper: 'Recorded events',
4056
- color: _info,
4057
- ),
4058
- _RunMetricCard(
4059
- title: 'Completed tools',
4060
- value: '${snapshot.completedTools}',
4061
- helper: 'Successful tool calls',
4062
- color: _success,
4063
- ),
4064
- _RunMetricCard(
4065
- title: 'Failures',
4066
- value: '${snapshot.failedTools}',
4067
- helper: 'Tool errors',
4068
- color: _danger,
4069
- ),
4070
- _RunMetricCard(
4071
- title: 'Helpers',
4072
- value: '${snapshot.helperCount}',
4073
- helper: 'Subagents or helpers',
4074
- color: _accentHover,
4075
- ),
4076
- _RunMetricCard(
4077
- title: 'Trace events',
4078
- value: '${snapshot.events.length}',
4079
- helper: 'Structured run timeline',
4080
- color: _warning,
4081
- ),
4082
- ],
4083
- ),
4084
- const SizedBox(height: 16),
4085
- _RunResponseCard(
4086
- response: snapshot.response,
4087
- onCopy: () => onCopyResponse(snapshot.response),
4088
- ),
4089
- if (snapshot.run.deliverableType.trim().isNotEmpty) ...<Widget>[
4090
- const SizedBox(height: 16),
4091
- _DeliverableSummaryCard(run: snapshot.run),
4092
- ],
4093
- const SizedBox(height: 16),
4094
- _RunTimelineCard(steps: snapshot.steps, loading: loading),
4095
- const SizedBox(height: 16),
4096
- _RunEventTimelineCard(events: snapshot.events, loading: loading),
4097
- ] else
4098
- const _EmptyCard(
4099
- title: 'No detail available',
4100
- subtitle: 'This run does not have step detail yet.',
4101
- ),
4102
- ],
4103
- );
4104
- }
4105
- }
4106
-
4107
3748
  class _RunHeroCard extends StatelessWidget {
4108
3749
  const _RunHeroCard({required this.run, required this.onDelete});
4109
3750
 
@@ -4165,22 +3806,13 @@ class _RunHeroCard extends StatelessWidget {
4165
3806
  spacing: 6,
4166
3807
  runSpacing: 6,
4167
3808
  children: <Widget>[
4168
- _MetaPill(
4169
- label: run.triggerLabel,
4170
- icon: Icons.bolt_outlined,
4171
- ),
4172
- _MetaPill(
4173
- label: run.modelLabel,
4174
- icon: Icons.memory_outlined,
4175
- ),
3809
+ _MetaPill(label: run.triggerLabel, icon: Icons.bolt_outlined),
3810
+ _MetaPill(label: run.modelLabel, icon: Icons.memory_outlined),
4176
3811
  _MetaPill(
4177
3812
  label: run.createdAtLabel,
4178
3813
  icon: Icons.schedule_outlined,
4179
3814
  ),
4180
- _MetaPill(
4181
- label: run.durationLabel,
4182
- icon: Icons.timer_outlined,
4183
- ),
3815
+ _MetaPill(label: run.durationLabel, icon: Icons.timer_outlined),
4184
3816
  if (run.totalTokensLabel.isNotEmpty)
4185
3817
  _MetaPill(
4186
3818
  label: '${run.totalTokensLabel} tok',
@@ -4357,259 +3989,6 @@ class _DeliverableSummaryCard extends StatelessWidget {
4357
3989
  }
4358
3990
  }
4359
3991
 
4360
- class _RunTimelineCard extends StatelessWidget {
4361
- const _RunTimelineCard({required this.steps, required this.loading});
4362
-
4363
- final List<RunStepItem> steps;
4364
- final bool loading;
4365
-
4366
- @override
4367
- Widget build(BuildContext context) {
4368
- return Card(
4369
- child: Padding(
4370
- padding: const EdgeInsets.all(18),
4371
- child: Column(
4372
- crossAxisAlignment: CrossAxisAlignment.start,
4373
- children: <Widget>[
4374
- Row(
4375
- children: <Widget>[
4376
- Expanded(child: _SectionTitle('Step Timeline')),
4377
- if (loading)
4378
- const SizedBox.square(
4379
- dimension: 16,
4380
- child: CircularProgressIndicator(strokeWidth: 2),
4381
- ),
4382
- ],
4383
- ),
4384
- const SizedBox(height: 12),
4385
- if (steps.isEmpty)
4386
- Text(
4387
- 'No run steps recorded yet.',
4388
- style: TextStyle(color: _textSecondary),
4389
- )
4390
- else
4391
- ...steps.map((step) {
4392
- return Padding(
4393
- padding: const EdgeInsets.only(bottom: 10),
4394
- child: _RunStepCard(step: step),
4395
- );
4396
- }),
4397
- ],
4398
- ),
4399
- ),
4400
- );
4401
- }
4402
- }
4403
-
4404
- class _RunEventTimelineCard extends StatelessWidget {
4405
- const _RunEventTimelineCard({required this.events, required this.loading});
4406
-
4407
- final List<RunEventItem> events;
4408
- final bool loading;
4409
-
4410
- @override
4411
- Widget build(BuildContext context) {
4412
- return Card(
4413
- child: Padding(
4414
- padding: const EdgeInsets.all(18),
4415
- child: Column(
4416
- crossAxisAlignment: CrossAxisAlignment.start,
4417
- children: <Widget>[
4418
- Row(
4419
- children: <Widget>[
4420
- Expanded(child: _SectionTitle('Execution Trace')),
4421
- if (loading)
4422
- const SizedBox.square(
4423
- dimension: 16,
4424
- child: CircularProgressIndicator(strokeWidth: 2),
4425
- ),
4426
- ],
4427
- ),
4428
- const SizedBox(height: 12),
4429
- if (events.isEmpty)
4430
- Text(
4431
- 'No structured run events recorded yet.',
4432
- style: TextStyle(color: _textSecondary),
4433
- )
4434
- else
4435
- ...events.map((event) {
4436
- return Padding(
4437
- padding: const EdgeInsets.only(bottom: 10),
4438
- child: _RunEventCard(event: event),
4439
- );
4440
- }),
4441
- ],
4442
- ),
4443
- ),
4444
- );
4445
- }
4446
- }
4447
-
4448
- class _RunEventCard extends StatelessWidget {
4449
- const _RunEventCard({required this.event});
4450
-
4451
- final RunEventItem event;
4452
-
4453
- @override
4454
- Widget build(BuildContext context) {
4455
- final accent = event.isFailure ? _danger : _info;
4456
- return Container(
4457
- padding: const EdgeInsets.all(14),
4458
- decoration: BoxDecoration(
4459
- color: _bgSecondary,
4460
- borderRadius: BorderRadius.circular(16),
4461
- border: Border.all(color: accent.withValues(alpha: 0.24)),
4462
- ),
4463
- child: Column(
4464
- crossAxisAlignment: CrossAxisAlignment.start,
4465
- children: <Widget>[
4466
- Row(
4467
- children: <Widget>[
4468
- Container(
4469
- width: 10,
4470
- height: 10,
4471
- decoration: BoxDecoration(
4472
- color: accent,
4473
- shape: BoxShape.circle,
4474
- ),
4475
- ),
4476
- const SizedBox(width: 10),
4477
- Expanded(
4478
- child: Text(
4479
- event.title,
4480
- style: const TextStyle(fontWeight: FontWeight.w700),
4481
- ),
4482
- ),
4483
- if (event.createdAtLabel.isNotEmpty)
4484
- Text(
4485
- event.createdAtLabel,
4486
- style: TextStyle(color: _textSecondary, fontSize: 12),
4487
- ),
4488
- ],
4489
- ),
4490
- if (event.detail.trim().isNotEmpty) ...<Widget>[
4491
- const SizedBox(height: 8),
4492
- Text(
4493
- event.detail,
4494
- style: TextStyle(color: _textSecondary, height: 1.4),
4495
- ),
4496
- ],
4497
- ],
4498
- ),
4499
- );
4500
- }
4501
- }
4502
-
4503
- class _RunStepCard extends StatelessWidget {
4504
- const _RunStepCard({required this.step});
4505
-
4506
- final RunStepItem step;
4507
-
4508
- @override
4509
- Widget build(BuildContext context) {
4510
- final theme = Theme.of(context);
4511
- return Container(
4512
- decoration: BoxDecoration(
4513
- color: _bgSecondary,
4514
- borderRadius: BorderRadius.circular(16),
4515
- border: Border.all(color: _border),
4516
- ),
4517
- child: Theme(
4518
- data: theme.copyWith(dividerColor: Colors.transparent),
4519
- child: ExpansionTile(
4520
- tilePadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
4521
- childrenPadding: const EdgeInsets.fromLTRB(14, 0, 14, 14),
4522
- initiallyExpanded:
4523
- step.status == 'failed' || step.status == 'running',
4524
- leading: Container(
4525
- width: 34,
4526
- height: 34,
4527
- decoration: BoxDecoration(
4528
- color: step.statusColor.withValues(alpha: 0.16),
4529
- shape: BoxShape.circle,
4530
- ),
4531
- child: Center(
4532
- child: Text(
4533
- '${step.displayIndex}',
4534
- style: TextStyle(
4535
- color: step.statusColor,
4536
- fontWeight: FontWeight.w800,
4537
- ),
4538
- ),
4539
- ),
4540
- ),
4541
- title: Column(
4542
- crossAxisAlignment: CrossAxisAlignment.start,
4543
- children: <Widget>[
4544
- Text(step.label, style: TextStyle(fontWeight: FontWeight.w700)),
4545
- const SizedBox(height: 6),
4546
- Text(
4547
- step.summary,
4548
- maxLines: 2,
4549
- overflow: TextOverflow.ellipsis,
4550
- style: TextStyle(
4551
- color: _textSecondary,
4552
- fontSize: 12,
4553
- height: 1.45,
4554
- ),
4555
- ),
4556
- const SizedBox(height: 8),
4557
- Wrap(
4558
- spacing: 8,
4559
- runSpacing: 8,
4560
- children: <Widget>[
4561
- _StatusPill(label: step.statusLabel, color: step.statusColor),
4562
- _MetaPill(label: step.typeLabel, icon: Icons.layers_outlined),
4563
- if (step.startedAt != null)
4564
- _MetaPill(
4565
- label: step.startedAtLabel!,
4566
- icon: Icons.schedule_outlined,
4567
- ),
4568
- if (step.durationLabel != null)
4569
- _MetaPill(
4570
- label: step.durationLabel!,
4571
- icon: Icons.timer_outlined,
4572
- ),
4573
- if (step.tokensUsed > 0)
4574
- _MetaPill(
4575
- label: '${_formatNumber(step.tokensUsed)} tokens',
4576
- icon: Icons.toll_outlined,
4577
- ),
4578
- ],
4579
- ),
4580
- ],
4581
- ),
4582
- children: <Widget>[
4583
- if (step.description.trim().isNotEmpty &&
4584
- step.description.trim() != step.summary.trim())
4585
- _RunDetailBlock(label: 'Description', value: step.description),
4586
- if (step.inputSummary.trim().isNotEmpty)
4587
- _RunDetailBlock(label: 'Input summary', value: step.inputSummary),
4588
- if (step.toolInput.trim().isNotEmpty)
4589
- _RunDetailBlock(
4590
- label: 'Tool input',
4591
- value: _truncateRunText(step.toolInput),
4592
- monospace: true,
4593
- ),
4594
- if (step.error.trim().isNotEmpty)
4595
- _RunDetailBlock(
4596
- label: 'Error',
4597
- value: step.error,
4598
- monospace: true,
4599
- )
4600
- else if (step.result.trim().isNotEmpty)
4601
- _RunDetailBlock(
4602
- label: 'Result',
4603
- value: _truncateRunText(step.result),
4604
- monospace: true,
4605
- ),
4606
- ],
4607
- ),
4608
- ),
4609
- );
4610
- }
4611
- }
4612
-
4613
3992
  class _RunDetailBlock extends StatelessWidget {
4614
3993
  const _RunDetailBlock({
4615
3994
  required this.label,
@@ -4855,10 +4234,7 @@ class _RunGraphEdgePainter extends CustomPainter {
4855
4234
  ..strokeWidth = 1.8
4856
4235
  ..strokeCap = StrokeCap.round;
4857
4236
 
4858
- final start = Offset(
4859
- from.x + _FlowNode.w,
4860
- from.y + _FlowNode.h / 2,
4861
- );
4237
+ final start = Offset(from.x + _FlowNode.w, from.y + _FlowNode.h / 2);
4862
4238
  final end = Offset(to.x, to.y + _FlowNode.h / 2);
4863
4239
 
4864
4240
  if (from.y != to.y) {
@@ -4971,10 +4347,7 @@ class _FlowNodeWidget extends StatelessWidget {
4971
4347
  const SizedBox(width: 5),
4972
4348
  Text(
4973
4349
  isSpecial ? node.nodeType : node.status,
4974
- style: TextStyle(
4975
- color: _textSecondary,
4976
- fontSize: 11,
4977
- ),
4350
+ style: TextStyle(color: _textSecondary, fontSize: 11),
4978
4351
  ),
4979
4352
  ],
4980
4353
  ),
@@ -5093,10 +4466,7 @@ class _RunFlowGraphCanvasState extends State<_RunFlowGraphCanvas> {
5093
4466
  Expanded(
5094
4467
  child: Text(
5095
4468
  '$stepCount step${stepCount == 1 ? '' : 's'} · tap a node to inspect',
5096
- style: TextStyle(
5097
- color: _textSecondary,
5098
- fontSize: 12.5,
5099
- ),
4469
+ style: TextStyle(color: _textSecondary, fontSize: 12.5),
5100
4470
  ),
5101
4471
  ),
5102
4472
  if (widget.loading)
@@ -5116,8 +4486,14 @@ class _RunFlowGraphCanvasState extends State<_RunFlowGraphCanvas> {
5116
4486
  vertical: 6,
5117
4487
  ),
5118
4488
  ),
5119
- icon: const Icon(Icons.center_focus_strong_outlined, size: 15),
5120
- label: const Text('Reset view', style: TextStyle(fontSize: 12)),
4489
+ icon: const Icon(
4490
+ Icons.center_focus_strong_outlined,
4491
+ size: 15,
4492
+ ),
4493
+ label: const Text(
4494
+ 'Reset view',
4495
+ style: TextStyle(fontSize: 12),
4496
+ ),
5121
4497
  ),
5122
4498
  ],
5123
4499
  ),
@@ -5155,7 +4531,9 @@ class _RunFlowGraphCanvasState extends State<_RunFlowGraphCanvas> {
5155
4531
  node: node,
5156
4532
  selected: node.id == widget.selectedNodeId,
5157
4533
  onTap: () => widget.onNodeSelected(
5158
- node.id == widget.selectedNodeId ? null : node.id,
4534
+ node.id == widget.selectedNodeId
4535
+ ? null
4536
+ : node.id,
5159
4537
  ),
5160
4538
  ),
5161
4539
  ),
@@ -5312,32 +4690,31 @@ class _RunSelectorRail extends StatelessWidget {
5312
4690
  style: TextStyle(color: _textSecondary, fontSize: 12),
5313
4691
  ),
5314
4692
  )
5315
- else
5316
- ...<Widget>[
5317
- ListView.separated(
5318
- shrinkWrap: true,
5319
- physics: const NeverScrollableScrollPhysics(),
5320
- itemCount: math.min(filteredRuns.length, 40),
5321
- separatorBuilder: (_, __) => const SizedBox(height: 6),
5322
- itemBuilder: (context, index) {
5323
- final run = filteredRuns[index];
5324
- final isSelected = run.id == selectedRunId;
5325
- return _RunSelectorRow(
5326
- run: run,
5327
- selected: isSelected,
5328
- onTap: () => onSelect(run.id),
5329
- );
5330
- },
5331
- ),
5332
- if (filteredRuns.length > 40)
5333
- Padding(
5334
- padding: const EdgeInsets.only(top: 8),
5335
- child: Text(
5336
- 'Showing 40 of ${filteredRuns.length} — use search to narrow results',
5337
- style: TextStyle(color: _textSecondary, fontSize: 11),
5338
- ),
4693
+ else ...<Widget>[
4694
+ ListView.separated(
4695
+ shrinkWrap: true,
4696
+ physics: const NeverScrollableScrollPhysics(),
4697
+ itemCount: math.min(filteredRuns.length, 40),
4698
+ separatorBuilder: (_, __) => const SizedBox(height: 6),
4699
+ itemBuilder: (context, index) {
4700
+ final run = filteredRuns[index];
4701
+ final isSelected = run.id == selectedRunId;
4702
+ return _RunSelectorRow(
4703
+ run: run,
4704
+ selected: isSelected,
4705
+ onTap: () => onSelect(run.id),
4706
+ );
4707
+ },
4708
+ ),
4709
+ if (filteredRuns.length > 40)
4710
+ Padding(
4711
+ padding: const EdgeInsets.only(top: 8),
4712
+ child: Text(
4713
+ 'Showing 40 of ${filteredRuns.length} — use search to narrow results',
4714
+ style: TextStyle(color: _textSecondary, fontSize: 11),
5339
4715
  ),
5340
- ],
4716
+ ),
4717
+ ],
5341
4718
  ],
5342
4719
  ),
5343
4720
  ),
@@ -5396,10 +4773,7 @@ class _RunSelectorRow extends StatelessWidget {
5396
4773
  const SizedBox(height: 3),
5397
4774
  Text(
5398
4775
  '${run.createdAtLabel} · ${run.durationLabel}',
5399
- style: TextStyle(
5400
- color: _textSecondary,
5401
- fontSize: 11,
5402
- ),
4776
+ style: TextStyle(color: _textSecondary, fontSize: 11),
5403
4777
  ),
5404
4778
  ],
5405
4779
  ),
@@ -5522,7 +4896,11 @@ class _RunSelectedStepCard extends StatelessWidget {
5522
4896
  color: color.withValues(alpha: 0.14),
5523
4897
  borderRadius: BorderRadius.circular(10),
5524
4898
  ),
5525
- child: Icon(Icons.build_circle_outlined, size: 18, color: color),
4899
+ child: Icon(
4900
+ Icons.build_circle_outlined,
4901
+ size: 18,
4902
+ color: color,
4903
+ ),
5526
4904
  ),
5527
4905
  const SizedBox(width: 10),
5528
4906
  Expanded(
@@ -5539,10 +4917,7 @@ class _RunSelectedStepCard extends StatelessWidget {
5539
4917
  const SizedBox(height: 2),
5540
4918
  Text(
5541
4919
  step.typeLabel,
5542
- style: TextStyle(
5543
- color: _textSecondary,
5544
- fontSize: 12,
5545
- ),
4920
+ style: TextStyle(color: _textSecondary, fontSize: 12),
5546
4921
  ),
5547
4922
  ],
5548
4923
  ),
@@ -5583,7 +4958,11 @@ class _RunSelectedStepCard extends StatelessWidget {
5583
4958
  ],
5584
4959
  if (step.error.trim().isNotEmpty) ...<Widget>[
5585
4960
  const SizedBox(height: 8),
5586
- _RunDetailBlock(label: 'Error', value: step.error, monospace: true),
4961
+ _RunDetailBlock(
4962
+ label: 'Error',
4963
+ value: step.error,
4964
+ monospace: true,
4965
+ ),
5587
4966
  ] else if (step.result.trim().isNotEmpty) ...<Widget>[
5588
4967
  const SizedBox(height: 8),
5589
4968
  _RunDetailBlock(