neoagent 3.1.1-beta.8 → 3.2.0

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 (150) hide show
  1. package/.env.example +2 -2
  2. package/README.md +5 -1
  3. package/docs/architecture.md +1 -1
  4. package/docs/benchmarking.md +68 -87
  5. package/docs/capabilities.md +1 -1
  6. package/docs/clients-and-devices.md +3 -4
  7. package/docs/configuration.md +1 -1
  8. package/docs/devices.md +1 -2
  9. package/docs/health.md +24 -0
  10. package/docs/index.md +2 -2
  11. package/docs/integrations-architecture.md +6 -0
  12. package/docs/integrations.md +15 -1
  13. package/docs/models.md +1 -1
  14. package/docs/persistence.md +1 -1
  15. package/docs/why-neoagent.md +1 -1
  16. package/flutter_app/README.md +0 -5
  17. package/flutter_app/android/app/src/main/AndroidManifest.xml +0 -14
  18. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +0 -98
  19. package/flutter_app/lib/main.dart +0 -6
  20. package/flutter_app/lib/main_app_shell.dart +0 -2
  21. package/flutter_app/lib/main_controller.dart +2 -867
  22. package/flutter_app/lib/main_devices.dart +0 -73
  23. package/flutter_app/lib/main_integrations.dart +339 -187
  24. package/flutter_app/lib/main_launcher.dart +2 -212
  25. package/flutter_app/lib/main_models.dart +0 -259
  26. package/flutter_app/lib/main_navigation.dart +2 -14
  27. package/flutter_app/lib/main_operations.dart +322 -364
  28. package/flutter_app/lib/main_runtime.dart +9 -208
  29. package/flutter_app/lib/main_settings.dart +17 -199
  30. package/flutter_app/lib/main_shared.dart +20 -591
  31. package/flutter_app/lib/main_timeline.dart +149 -438
  32. package/flutter_app/lib/main_voice_assistant.dart +0 -5
  33. package/flutter_app/lib/src/backend_client.dart +0 -102
  34. package/flutter_app/lib/src/desktop_companion_io.dart +1 -65
  35. package/flutter_app/lib/src/desktop_companion_stub.dart +0 -12
  36. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +0 -4
  37. package/flutter_app/linux/flutter/generated_plugins.cmake +0 -1
  38. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +0 -2
  39. package/flutter_app/macos/Podfile.lock +56 -5
  40. package/flutter_app/pubspec.lock +0 -7
  41. package/flutter_app/pubspec.yaml +0 -2
  42. package/flutter_app/web/index.html +174 -0
  43. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +0 -3
  44. package/flutter_app/windows/flutter/generated_plugins.cmake +0 -1
  45. package/landing/index.html +0 -2
  46. package/lib/manager.js +1 -1
  47. package/lib/schema_migrations.js +75 -0
  48. package/package.json +1 -4
  49. package/server/db/database.js +1 -133
  50. package/server/http/middleware.js +2 -16
  51. package/server/http/routes.js +0 -2
  52. package/server/public/.last_build_id +1 -1
  53. package/server/public/assets/NOTICES +0 -25
  54. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  55. package/server/public/flutter_bootstrap.js +1 -1
  56. package/server/public/index.html +174 -0
  57. package/server/public/main.dart.js +88124 -90164
  58. package/server/routes/auth.js +11 -2
  59. package/server/routes/settings.js +0 -5
  60. package/server/routes/voice_assistant.js +1 -128
  61. package/server/services/account/erasure.js +0 -3
  62. package/server/services/ai/deliverables/artifact_helpers.js +1 -1
  63. package/server/services/ai/loop/conversation_loop.js +6 -2
  64. package/server/services/ai/loop/tool_dispatch.js +0 -3
  65. package/server/services/ai/messagingFallback.js +1 -1
  66. package/server/services/ai/settings.js +0 -6
  67. package/server/services/ai/toolEvidence.js +1 -4
  68. package/server/services/ai/tools.js +0 -234
  69. package/server/services/desktop/auth.js +0 -3
  70. package/server/services/desktop/registry.js +2 -50
  71. package/server/services/integrations/neoarchive/provider.js +616 -0
  72. package/server/services/integrations/neorecall/client.js +140 -0
  73. package/server/services/integrations/neorecall/constants.js +87 -0
  74. package/server/services/integrations/neorecall/provider.js +163 -0
  75. package/server/services/integrations/registry.js +4 -2
  76. package/server/services/manager.js +0 -22
  77. package/server/services/memory/manager.js +44 -19
  78. package/server/services/memory/retrieval_reasoning.js +36 -14
  79. package/server/services/messaging/whatsapp.js +18 -0
  80. package/server/services/security/tool_categories.js +0 -5
  81. package/server/services/social_video/service.js +2 -20
  82. package/server/services/tasks/adapters/index.js +1 -1
  83. package/server/services/tasks/integration_runtime.js +3 -67
  84. package/server/services/tasks/runtime.js +1 -6
  85. package/server/services/tasks/task_repository.js +6 -13
  86. package/server/services/timeline/service.js +0 -224
  87. package/server/services/voice/message.js +0 -24
  88. package/server/services/voice/providers.js +42 -1
  89. package/server/services/voice/runtimeManager.js +9 -5
  90. package/server/services/voice/turnRunner.js +0 -22
  91. package/server/services/wearable/service.js +0 -5
  92. package/docs/recordings-and-health.md +0 -41
  93. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +0 -21
  94. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +0 -586
  95. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +0 -78
  96. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +0 -104
  97. package/flutter_app/lib/main_recordings.dart +0 -920
  98. package/flutter_app/lib/src/desktop_ocr_bridge.dart +0 -2
  99. package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +0 -125
  100. package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +0 -30
  101. package/flutter_app/lib/src/desktop_passive_history.dart +0 -332
  102. package/flutter_app/lib/src/recording_bridge.dart +0 -232
  103. package/flutter_app/lib/src/recording_bridge_io.dart +0 -1027
  104. package/flutter_app/lib/src/recording_bridge_stub.dart +0 -120
  105. package/flutter_app/lib/src/recording_bridge_web.dart +0 -702
  106. package/flutter_app/lib/src/recording_chunk_queue.dart +0 -149
  107. package/flutter_app/lib/src/recording_chunk_queue_io.dart +0 -182
  108. package/flutter_app/lib/src/recording_payloads.dart +0 -95
  109. package/flutter_app/third_party/desktop_audio_capture/LICENSE +0 -21
  110. package/flutter_app/third_party/desktop_audio_capture/README.md +0 -262
  111. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +0 -65
  112. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +0 -153
  113. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +0 -110
  114. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +0 -461
  115. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +0 -91
  116. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +0 -106
  117. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +0 -219
  118. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +0 -336
  119. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +0 -101
  120. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +0 -692
  121. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +0 -35
  122. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +0 -36
  123. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +0 -32
  124. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +0 -32
  125. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +0 -878
  126. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +0 -27
  127. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +0 -1172
  128. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +0 -655
  129. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +0 -12
  130. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +0 -30
  131. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +0 -87
  132. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +0 -105
  133. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +0 -80
  134. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +0 -31
  135. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +0 -12
  136. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +0 -23
  137. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +0 -25
  138. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +0 -1117
  139. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +0 -115
  140. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +0 -777
  141. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +0 -87
  142. package/server/routes/recordings.js +0 -335
  143. package/server/routes/screenHistory.js +0 -190
  144. package/server/services/ai/recordingInsights.js +0 -105
  145. package/server/services/integrations/neomail/provider.js +0 -993
  146. package/server/services/recordings/deepgram.js +0 -58
  147. package/server/services/recordings/manager.js +0 -1273
  148. package/server/services/tasks/adapters/neomail_email_received.js +0 -46
  149. package/server/services/voice/screenshotContext.js +0 -73
  150. package/server/services/voice/turnCoordinator.js +0 -51
@@ -21,37 +21,7 @@ class _TimelinePanelState extends State<TimelinePanel> {
21
21
  });
22
22
  }
23
23
 
24
- void _moveSelection(List<TimelineEventItem> items, int offset) {
25
- if (items.isEmpty) {
26
- return;
27
- }
28
- final currentIndex = items.indexWhere(
29
- (item) => item.id == _selectedEventId,
30
- );
31
- final baseIndex = currentIndex == -1
32
- ? (offset > 0 ? 0 : items.length - 1)
33
- : currentIndex;
34
- final nextIndex = (baseIndex + offset).clamp(0, items.length - 1);
35
- setState(() {
36
- _selectedEventId = items[nextIndex].id;
37
- });
38
- }
39
-
40
- TimelineEventItem? _resolveSelectedEvent(List<TimelineEventItem> items) {
41
- if (items.isEmpty) {
42
- _selectedEventId = null;
43
- return null;
44
- }
45
- for (final item in items) {
46
- if (item.id == _selectedEventId) {
47
- return item;
48
- }
49
- }
50
- _selectedEventId = items.first.id;
51
- return items.first;
52
- }
53
-
54
- Future<void> _showMobileEventDetails(
24
+ Future<void> _showEventDetailsPopup(
55
25
  List<TimelineEventItem> items,
56
26
  TimelineEventItem initialEvent,
57
27
  ) async {
@@ -61,49 +31,79 @@ class _TimelinePanelState extends State<TimelinePanel> {
61
31
  selectedIndex = 0;
62
32
  }
63
33
 
64
- await showModalBottomSheet<void>(
34
+ Widget buildDetail(StateSetter setPopupState) {
35
+ final selectedEvent = items[selectedIndex];
36
+ void selectOffset(int offset) {
37
+ final nextIndex = (selectedIndex + offset).clamp(0, items.length - 1);
38
+ if (nextIndex == selectedIndex) {
39
+ return;
40
+ }
41
+ setPopupState(() {
42
+ selectedIndex = nextIndex;
43
+ });
44
+ _selectEvent(items[nextIndex]);
45
+ }
46
+
47
+ return _TimelineDetailPane(
48
+ items: items,
49
+ selectedEvent: selectedEvent,
50
+ selectedIndex: selectedIndex,
51
+ onSelectPrevious: selectedIndex > 0 ? () => selectOffset(-1) : null,
52
+ onSelectNext: selectedIndex < items.length - 1
53
+ ? () => selectOffset(1)
54
+ : null,
55
+ onOpenRun: selectedEvent.runId.isNotEmpty
56
+ ? () => unawaited(
57
+ widget.controller.openRunDetails(selectedEvent.runId),
58
+ )
59
+ : null,
60
+ onClose: () => Navigator.of(context).pop(),
61
+ );
62
+ }
63
+
64
+ final compact = MediaQuery.sizeOf(context).width < 760;
65
+
66
+ if (compact) {
67
+ await showModalBottomSheet<void>(
68
+ context: context,
69
+ isScrollControlled: true,
70
+ useSafeArea: true,
71
+ backgroundColor: Colors.transparent,
72
+ builder: (context) {
73
+ return StatefulBuilder(
74
+ builder: (context, setSheetState) {
75
+ return FractionallySizedBox(
76
+ heightFactor: 0.9,
77
+ child: Padding(
78
+ padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
79
+ child: buildDetail(setSheetState),
80
+ ),
81
+ );
82
+ },
83
+ );
84
+ },
85
+ );
86
+ return;
87
+ }
88
+
89
+ await showDialog<void>(
65
90
  context: context,
66
- isScrollControlled: true,
67
- useSafeArea: true,
68
- backgroundColor: Colors.transparent,
91
+ barrierColor: Colors.black.withValues(alpha: 0.5),
69
92
  builder: (context) {
70
93
  return StatefulBuilder(
71
- builder: (context, setSheetState) {
72
- final selectedEvent = items[selectedIndex];
73
- void selectOffset(int offset) {
74
- final nextIndex = (selectedIndex + offset).clamp(
75
- 0,
76
- items.length - 1,
77
- );
78
- if (nextIndex == selectedIndex) {
79
- return;
80
- }
81
- setSheetState(() {
82
- selectedIndex = nextIndex;
83
- });
84
- _selectEvent(items[nextIndex]);
85
- }
86
-
87
- return FractionallySizedBox(
88
- heightFactor: 0.9,
89
- child: Padding(
90
- padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
91
- child: _TimelineDetailPane(
92
- items: items,
93
- selectedEvent: selectedEvent,
94
- selectedIndex: selectedIndex,
95
- onSelectPrevious: selectedIndex > 0
96
- ? () => selectOffset(-1)
97
- : null,
98
- onSelectNext: selectedIndex < items.length - 1
99
- ? () => selectOffset(1)
100
- : null,
101
- onOpenRun: selectedEvent.runId.isNotEmpty
102
- ? () => unawaited(
103
- widget.controller.openRunDetails(selectedEvent.runId),
104
- )
105
- : null,
94
+ builder: (context, setDialogState) {
95
+ return Dialog(
96
+ backgroundColor: Colors.transparent,
97
+ insetPadding: const EdgeInsets.symmetric(
98
+ horizontal: 40,
99
+ vertical: 40,
100
+ ),
101
+ child: ConstrainedBox(
102
+ constraints: const BoxConstraints(
103
+ maxWidth: 640,
104
+ maxHeight: 780,
106
105
  ),
106
+ child: buildDetail(setDialogState),
107
107
  ),
108
108
  );
109
109
  },
@@ -116,22 +116,56 @@ class _TimelinePanelState extends State<TimelinePanel> {
116
116
  Widget build(BuildContext context) {
117
117
  final items = _sortedTimelineEvents(widget.controller.timelineItems);
118
118
  final groups = _groupTimelineEvents(items);
119
- final selectedEvent = _resolveSelectedEvent(items);
120
- final selectedIndex = selectedEvent == null
121
- ? -1
122
- : items.indexWhere((item) => item.id == selectedEvent.id);
119
+ final controller = widget.controller;
123
120
 
124
121
  return Padding(
125
122
  padding: _pagePadding(context),
126
123
  child: Column(
127
124
  crossAxisAlignment: CrossAxisAlignment.start,
128
125
  children: <Widget>[
129
- _TimelineHeroHeader(
130
- items: items,
131
- selectedEvent: selectedEvent,
132
- controller: widget.controller,
126
+ _PageTitle(
127
+ title: 'Timeline',
128
+ subtitle:
129
+ 'Emails, AI actions, tasks and run activity in one chronological feed.',
130
+ trailing: Wrap(
131
+ spacing: 10,
132
+ runSpacing: 10,
133
+ children: <Widget>[
134
+ for (final filter in const <({String id, String label})>[
135
+ (id: 'tasks', label: 'Tasks'),
136
+ (id: 'runs', label: 'Runs'),
137
+ ])
138
+ FilterChip(
139
+ selected: controller.selectedTimelineSources.contains(
140
+ filter.id,
141
+ ),
142
+ label: Text(filter.label),
143
+ onSelected: (_) =>
144
+ controller.toggleTimelineSource(filter.id),
145
+ avatar: Icon(
146
+ _timelineLaneIcon(filter.id),
147
+ size: 16,
148
+ color:
149
+ controller.selectedTimelineSources.contains(filter.id)
150
+ ? _bgSecondary
151
+ : _sourceColorForKind(filter.id),
152
+ ),
153
+ ),
154
+ OutlinedButton.icon(
155
+ onPressed: controller.isRefreshingTimeline
156
+ ? null
157
+ : controller.refreshTimeline,
158
+ icon: controller.isRefreshingTimeline
159
+ ? const SizedBox.square(
160
+ dimension: 14,
161
+ child: CircularProgressIndicator(strokeWidth: 2),
162
+ )
163
+ : const Icon(Icons.sync_outlined),
164
+ label: const Text('Refresh'),
165
+ ),
166
+ ],
167
+ ),
133
168
  ),
134
- const SizedBox(height: 18),
135
169
  Expanded(
136
170
  child: items.isEmpty
137
171
  ? Card(
@@ -145,66 +179,11 @@ class _TimelinePanelState extends State<TimelinePanel> {
145
179
  ),
146
180
  ),
147
181
  )
148
- : LayoutBuilder(
149
- builder: (context, constraints) {
150
- final isWide = constraints.maxWidth >= 1180;
151
- final isCompact = constraints.maxWidth < 760;
152
- final feedPane = _TimelineFeedPane(
153
- groups: groups,
154
- selectedEventId: selectedEvent?.id,
155
- onSelectEvent: isCompact
156
- ? (item) => unawaited(
157
- _showMobileEventDetails(items, item),
158
- )
159
- : _selectEvent,
160
- );
161
- final detailPane = _TimelineDetailPane(
162
- items: items,
163
- selectedEvent: selectedEvent,
164
- selectedIndex: selectedIndex,
165
- onSelectPrevious: selectedIndex > 0
166
- ? () => _moveSelection(items, -1)
167
- : null,
168
- onSelectNext:
169
- selectedIndex >= 0 &&
170
- selectedIndex < items.length - 1
171
- ? () => _moveSelection(items, 1)
172
- : null,
173
- onOpenRun:
174
- selectedEvent != null &&
175
- selectedEvent.runId.isNotEmpty
176
- ? () => unawaited(
177
- widget.controller.openRunDetails(
178
- selectedEvent.runId,
179
- ),
180
- )
181
- : null,
182
- );
183
-
184
- if (isWide) {
185
- return Row(
186
- crossAxisAlignment: CrossAxisAlignment.stretch,
187
- children: <Widget>[
188
- Expanded(flex: 10, child: feedPane),
189
- const SizedBox(width: 16),
190
- SizedBox(width: 420, child: detailPane),
191
- ],
192
- );
193
- }
194
-
195
- if (isCompact) {
196
- return feedPane;
197
- }
198
-
199
- return Column(
200
- crossAxisAlignment: CrossAxisAlignment.stretch,
201
- children: <Widget>[
202
- Expanded(flex: 11, child: feedPane),
203
- const SizedBox(height: 16),
204
- Expanded(flex: 8, child: detailPane),
205
- ],
206
- );
207
- },
182
+ : _TimelineFeedPane(
183
+ groups: groups,
184
+ selectedEventId: _selectedEventId,
185
+ onSelectEvent: (item) =>
186
+ unawaited(_showEventDetailsPopup(items, item)),
208
187
  ),
209
188
  ),
210
189
  ],
@@ -213,179 +192,6 @@ class _TimelinePanelState extends State<TimelinePanel> {
213
192
  }
214
193
  }
215
194
 
216
- class _TimelineHeroHeader extends StatelessWidget {
217
- const _TimelineHeroHeader({
218
- required this.items,
219
- required this.selectedEvent,
220
- required this.controller,
221
- });
222
-
223
- final List<TimelineEventItem> items;
224
- final TimelineEventItem? selectedEvent;
225
- final NeoAgentController controller;
226
-
227
- @override
228
- Widget build(BuildContext context) {
229
- final compact = MediaQuery.sizeOf(context).width < 760;
230
- final focusedDay =
231
- selectedEvent?.occurredAt ??
232
- (items.isEmpty ? null : items.first.occurredAt);
233
-
234
- return Container(
235
- width: double.infinity,
236
- padding: EdgeInsets.all(compact ? 16 : 22),
237
- decoration: BoxDecoration(
238
- gradient: LinearGradient(
239
- colors: <Color>[
240
- _bgSecondary.withValues(alpha: 0.96),
241
- _bgPrimary.withValues(alpha: 0.9),
242
- ],
243
- begin: Alignment.topLeft,
244
- end: Alignment.bottomRight,
245
- ),
246
- borderRadius: BorderRadius.circular(compact ? 20 : 28),
247
- border: Border.all(color: _borderLight),
248
- boxShadow: <BoxShadow>[
249
- BoxShadow(
250
- color: Colors.black.withValues(alpha: 0.12),
251
- blurRadius: 26,
252
- offset: const Offset(0, 16),
253
- ),
254
- ],
255
- ),
256
- child: Column(
257
- crossAxisAlignment: CrossAxisAlignment.start,
258
- children: <Widget>[
259
- LayoutBuilder(
260
- builder: (context, constraints) {
261
- final isWide = constraints.maxWidth >= 940;
262
- final heading = Column(
263
- crossAxisAlignment: CrossAxisAlignment.start,
264
- children: <Widget>[
265
- Text(
266
- 'ACTIVITY FEED',
267
- style: TextStyle(
268
- color: _accentHover,
269
- fontSize: compact ? 11 : 13,
270
- fontWeight: FontWeight.w700,
271
- letterSpacing: compact ? 2.4 : 4.2,
272
- ),
273
- ),
274
- SizedBox(height: compact ? 8 : 14),
275
- Text(
276
- 'Timeline',
277
- style: TextStyle(
278
- fontSize: compact ? 28 : 40,
279
- fontWeight: FontWeight.w800,
280
- height: 1,
281
- ),
282
- ),
283
- SizedBox(height: compact ? 8 : 14),
284
- Text(
285
- 'Emails, AI actions, recordings, tasks and run activity in one chronological feed.',
286
- style: TextStyle(
287
- color: _textSecondary,
288
- fontSize: compact ? 13.5 : 16.5,
289
- height: compact ? 1.28 : 1.35,
290
- ),
291
- ),
292
- ],
293
- );
294
- final summary = Wrap(
295
- spacing: 10,
296
- runSpacing: 10,
297
- alignment: isWide ? WrapAlignment.end : WrapAlignment.start,
298
- children: <Widget>[
299
- if (items.isNotEmpty)
300
- _TimelineStatPill(
301
- icon: Icons.event_note_rounded,
302
- label: '${items.length} events',
303
- ),
304
- if (items.length > 1)
305
- _TimelineStatPill(
306
- icon: Icons.schedule_outlined,
307
- label: _formatTimelineRange(items.first, items.last),
308
- ),
309
- if (focusedDay != null)
310
- _TimelineStatPill(
311
- icon: Icons.calendar_today_outlined,
312
- label: _formatTimelineDate(focusedDay.toLocal()),
313
- ),
314
- ],
315
- );
316
-
317
- if (isWide) {
318
- return Row(
319
- crossAxisAlignment: CrossAxisAlignment.start,
320
- children: <Widget>[
321
- Expanded(child: heading),
322
- const SizedBox(width: 20),
323
- ConstrainedBox(
324
- constraints: const BoxConstraints(maxWidth: 440),
325
- child: Align(
326
- alignment: Alignment.topRight,
327
- child: summary,
328
- ),
329
- ),
330
- ],
331
- );
332
- }
333
-
334
- return Column(
335
- crossAxisAlignment: CrossAxisAlignment.start,
336
- children: <Widget>[
337
- heading,
338
- const SizedBox(height: 18),
339
- summary,
340
- ],
341
- );
342
- },
343
- ),
344
- const SizedBox(height: 18),
345
- Wrap(
346
- spacing: 10,
347
- runSpacing: 10,
348
- children: <Widget>[
349
- for (final filter in const <({String id, String label})>[
350
- (id: 'screen', label: 'Screen'),
351
- (id: 'tasks', label: 'Tasks'),
352
- (id: 'runs', label: 'Runs'),
353
- ])
354
- FilterChip(
355
- selected: controller.selectedTimelineSources.contains(
356
- filter.id,
357
- ),
358
- label: Text(filter.label),
359
- onSelected: (_) => controller.toggleTimelineSource(filter.id),
360
- avatar: Icon(
361
- _timelineLaneIcon(filter.id),
362
- size: 16,
363
- color:
364
- controller.selectedTimelineSources.contains(filter.id)
365
- ? _bgSecondary
366
- : _sourceColorForKind(filter.id),
367
- ),
368
- ),
369
- OutlinedButton.icon(
370
- onPressed: controller.isRefreshingTimeline
371
- ? null
372
- : controller.refreshTimeline,
373
- icon: controller.isRefreshingTimeline
374
- ? const SizedBox.square(
375
- dimension: 14,
376
- child: CircularProgressIndicator(strokeWidth: 2),
377
- )
378
- : const Icon(Icons.sync_outlined),
379
- label: const Text('Refresh'),
380
- ),
381
- ],
382
- ),
383
- ],
384
- ),
385
- );
386
- }
387
- }
388
-
389
195
  class _TimelineFeedPane extends StatelessWidget {
390
196
  const _TimelineFeedPane({
391
197
  required this.groups,
@@ -751,6 +557,7 @@ class _TimelineDetailPane extends StatelessWidget {
751
557
  this.onSelectPrevious,
752
558
  this.onSelectNext,
753
559
  this.onOpenRun,
560
+ this.onClose,
754
561
  });
755
562
 
756
563
  final List<TimelineEventItem> items;
@@ -759,6 +566,7 @@ class _TimelineDetailPane extends StatelessWidget {
759
566
  final VoidCallback? onSelectPrevious;
760
567
  final VoidCallback? onSelectNext;
761
568
  final VoidCallback? onOpenRun;
569
+ final VoidCallback? onClose;
762
570
 
763
571
  @override
764
572
  Widget build(BuildContext context) {
@@ -814,6 +622,12 @@ class _TimelineDetailPane extends StatelessWidget {
814
622
  onPressed: onSelectNext,
815
623
  icon: const Icon(Icons.chevron_right_rounded),
816
624
  ),
625
+ if (onClose != null)
626
+ IconButton(
627
+ tooltip: 'Close',
628
+ onPressed: onClose,
629
+ icon: const Icon(Icons.close_rounded),
630
+ ),
817
631
  ],
818
632
  ),
819
633
  ),
@@ -1095,20 +909,7 @@ List<_TimelineDayGroup> _groupTimelineEvents(List<TimelineEventItem> items) {
1095
909
 
1096
910
  List<_TimelineChip> _timelineEventChips(TimelineEventItem item) {
1097
911
  final chips = <_TimelineChip>[];
1098
- if (item.sourceKind == 'screen') {
1099
- final duration = _timelineSpanDuration(item);
1100
- if (duration != null) {
1101
- chips.add(_TimelineChip(label: duration, color: _accent));
1102
- }
1103
- if (item.appName.trim().isNotEmpty) {
1104
- chips.add(_TimelineChip(label: item.appName.trim(), color: _info));
1105
- }
1106
- if (item.windowTitle.trim().isNotEmpty) {
1107
- chips.add(
1108
- _TimelineChip(label: item.windowTitle.trim(), color: _textSecondary),
1109
- );
1110
- }
1111
- } else if (item.sourceKind == 'tasks') {
912
+ if (item.sourceKind == 'tasks') {
1112
913
  chips.add(
1113
914
  _TimelineChip(
1114
915
  label: _titleCase(item.eventKind.replaceAll('_', ' ')),
@@ -1130,7 +931,7 @@ List<_TimelineChip> _timelineEventChips(TimelineEventItem item) {
1130
931
  chips.add(_TimelineChip(label: 'Run linked', color: _accentAlt));
1131
932
  }
1132
933
  }
1133
- if (item.deviceLabel.trim().isNotEmpty && item.sourceKind != 'screen') {
934
+ if (item.deviceLabel.trim().isNotEmpty) {
1134
935
  chips.add(_TimelineChip(label: item.deviceLabel.trim(), color: _info));
1135
936
  }
1136
937
  return chips.take(4).toList(growable: false);
@@ -1155,26 +956,7 @@ List<_TimelineDetailCell> _timelineDetailCells(TimelineEventItem item) {
1155
956
  ),
1156
957
  ];
1157
958
 
1158
- if (item.sourceKind == 'screen') {
1159
- cells.add(
1160
- _TimelineDetailCell(
1161
- label: 'DURATION',
1162
- value: _timelineSpanDuration(item) ?? item.screenSpanLabel,
1163
- ),
1164
- );
1165
- cells.add(
1166
- _TimelineDetailCell(
1167
- label: 'APP',
1168
- value: item.appName.ifEmpty('Unknown app'),
1169
- ),
1170
- );
1171
- cells.add(
1172
- _TimelineDetailCell(
1173
- label: 'DEVICE',
1174
- value: item.deviceLabel.ifEmpty('Desktop'),
1175
- ),
1176
- );
1177
- } else if (item.sourceKind == 'tasks') {
959
+ if (item.sourceKind == 'tasks') {
1178
960
  cells.add(
1179
961
  _TimelineDetailCell(
1180
962
  label: 'TASK',
@@ -1229,12 +1011,6 @@ List<_TimelineDetailCell> _timelineDetailCells(TimelineEventItem item) {
1229
1011
 
1230
1012
  String _timelineCardDescription(TimelineEventItem item) {
1231
1013
  switch (item.sourceKind) {
1232
- case 'screen':
1233
- final preview = item.previewText.trim();
1234
- if (preview.isNotEmpty) {
1235
- return preview;
1236
- }
1237
- return '${item.deviceLabel.ifEmpty('Desktop')} · ${item.appName.ifEmpty('Unknown app')}';
1238
1014
  case 'tasks':
1239
1015
  case 'runs':
1240
1016
  return item.summary.trim().ifEmpty(
@@ -1246,46 +1022,15 @@ String _timelineCardDescription(TimelineEventItem item) {
1246
1022
  }
1247
1023
 
1248
1024
  String _timelineDetailDescription(TimelineEventItem item) {
1249
- final body = item.sourceKind == 'screen'
1250
- ? item.previewText.trim()
1251
- : item.summary.trim();
1025
+ final body = item.summary.trim();
1252
1026
  if (body.isNotEmpty) {
1253
1027
  return body;
1254
1028
  }
1255
- if (item.sourceKind == 'screen') {
1256
- return '${item.appName.ifEmpty('Unknown app')} on ${item.deviceLabel.ifEmpty('Desktop')}';
1257
- }
1258
1029
  return _titleCase(item.eventKind.replaceAll('_', ' '));
1259
1030
  }
1260
1031
 
1261
- String? _timelineSpanDuration(TimelineEventItem item) {
1262
- final start = item.startedAt;
1263
- final end = item.endedAt;
1264
- if (start == null || end == null) {
1265
- return null;
1266
- }
1267
- final span = end.difference(start);
1268
- if (span.inSeconds < 60) {
1269
- return '${span.inSeconds}s';
1270
- }
1271
- if (span.inMinutes < 60) {
1272
- final seconds = span.inSeconds % 60;
1273
- if (seconds == 0) {
1274
- return '${span.inMinutes}m';
1275
- }
1276
- return '${span.inMinutes}m ${seconds}s';
1277
- }
1278
- final minutes = span.inMinutes % 60;
1279
- if (minutes == 0) {
1280
- return '${span.inHours}h';
1281
- }
1282
- return '${span.inHours}h ${minutes}m';
1283
- }
1284
-
1285
1032
  Color _sourceColorForKind(String kind) {
1286
1033
  switch (kind) {
1287
- case 'screen':
1288
- return _accent;
1289
1034
  case 'tasks':
1290
1035
  return _warning;
1291
1036
  case 'runs':
@@ -1310,19 +1055,6 @@ bool _isSameDay(DateTime a, DateTime b) {
1310
1055
  return a.year == b.year && a.month == b.month && a.day == b.day;
1311
1056
  }
1312
1057
 
1313
- String _formatTimelineRange(TimelineEventItem first, TimelineEventItem last) {
1314
- final start = last.occurredAt.toLocal();
1315
- final end = first.occurredAt.toLocal();
1316
- final startDate = _formatTimelineDate(start);
1317
- final endDate = _formatTimelineDate(end);
1318
- final startTime = _formatTimelineTime(start);
1319
- final endTime = _formatTimelineTime(end);
1320
- if (_isSameDay(start, end)) {
1321
- return '$startDate · $startTime - $endTime';
1322
- }
1323
- return '$startDate -> $endDate';
1324
- }
1325
-
1326
1058
  String _formatTimelineDate(DateTime value) {
1327
1059
  return '${_monthShort(value.month)} ${value.day}, ${value.year}';
1328
1060
  }
@@ -1359,8 +1091,6 @@ String _weekdayShort(int weekday) {
1359
1091
 
1360
1092
  IconData _timelineLaneIcon(String sourceKind) {
1361
1093
  switch (sourceKind) {
1362
- case 'screen':
1363
- return Icons.desktop_windows_outlined;
1364
1094
  case 'tasks':
1365
1095
  return Icons.task_alt_outlined;
1366
1096
  case 'runs':
@@ -1376,47 +1106,28 @@ List<Widget> _timelineDetailBody(
1376
1106
  ) {
1377
1107
  final content = <Widget>[];
1378
1108
 
1379
- if (item.sourceKind == 'screen') {
1380
- content.addAll(<Widget>[
1381
- _TimelineMetaLine(
1382
- icon: Icons.computer_outlined,
1383
- text:
1384
- '${item.deviceLabel.ifEmpty('Desktop')} · ${item.appName.ifEmpty('Unknown app')}',
1385
- ),
1386
- if (item.windowTitle.isNotEmpty)
1387
- _TimelineMetaLine(
1388
- icon: Icons.web_asset_outlined,
1389
- text: item.windowTitle,
1390
- ),
1391
- _TimelineMetaLine(
1392
- icon: Icons.schedule_outlined,
1393
- text: item.screenSpanLabel,
1394
- ),
1395
- ]);
1396
- } else {
1109
+ content.add(
1110
+ _TimelineMetaLine(
1111
+ icon: item.sourceKind == 'tasks'
1112
+ ? Icons.task_alt_outlined
1113
+ : Icons.monitor_heart_outlined,
1114
+ text: _titleCase(item.eventKind.replaceAll('_', ' ')),
1115
+ ),
1116
+ );
1117
+ if (item.runId.isNotEmpty && onOpenRun != null) {
1397
1118
  content.add(
1398
- _TimelineMetaLine(
1399
- icon: item.sourceKind == 'tasks'
1400
- ? Icons.task_alt_outlined
1401
- : Icons.monitor_heart_outlined,
1402
- text: _titleCase(item.eventKind.replaceAll('_', ' ')),
1403
- ),
1404
- );
1405
- if (item.runId.isNotEmpty && onOpenRun != null) {
1406
- content.add(
1407
- Padding(
1408
- padding: const EdgeInsets.only(top: 8),
1409
- child: Align(
1410
- alignment: Alignment.centerLeft,
1411
- child: TextButton.icon(
1412
- onPressed: onOpenRun,
1413
- icon: const Icon(Icons.open_in_new_rounded, size: 16),
1414
- label: const Text('Open run'),
1415
- ),
1119
+ Padding(
1120
+ padding: const EdgeInsets.only(top: 8),
1121
+ child: Align(
1122
+ alignment: Alignment.centerLeft,
1123
+ child: TextButton.icon(
1124
+ onPressed: onOpenRun,
1125
+ icon: const Icon(Icons.open_in_new_rounded, size: 16),
1126
+ label: const Text('Open run'),
1416
1127
  ),
1417
1128
  ),
1418
- );
1419
- }
1129
+ ),
1130
+ );
1420
1131
  }
1421
1132
  return content;
1422
1133
  }