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.
- package/.env.example +2 -2
- package/README.md +5 -1
- package/docs/architecture.md +1 -1
- package/docs/benchmarking.md +68 -87
- package/docs/capabilities.md +1 -1
- package/docs/clients-and-devices.md +3 -4
- package/docs/configuration.md +1 -1
- package/docs/devices.md +1 -2
- package/docs/health.md +24 -0
- package/docs/index.md +2 -2
- package/docs/integrations-architecture.md +6 -0
- package/docs/integrations.md +15 -1
- package/docs/models.md +1 -1
- package/docs/persistence.md +1 -1
- package/docs/why-neoagent.md +1 -1
- package/flutter_app/README.md +0 -5
- package/flutter_app/android/app/src/main/AndroidManifest.xml +0 -14
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +0 -98
- package/flutter_app/lib/main.dart +0 -6
- package/flutter_app/lib/main_app_shell.dart +0 -2
- package/flutter_app/lib/main_controller.dart +2 -867
- package/flutter_app/lib/main_devices.dart +0 -73
- package/flutter_app/lib/main_integrations.dart +339 -187
- package/flutter_app/lib/main_launcher.dart +2 -212
- package/flutter_app/lib/main_models.dart +0 -259
- package/flutter_app/lib/main_navigation.dart +2 -14
- package/flutter_app/lib/main_operations.dart +322 -364
- package/flutter_app/lib/main_runtime.dart +9 -208
- package/flutter_app/lib/main_settings.dart +17 -199
- package/flutter_app/lib/main_shared.dart +20 -591
- package/flutter_app/lib/main_timeline.dart +149 -438
- package/flutter_app/lib/main_voice_assistant.dart +0 -5
- package/flutter_app/lib/src/backend_client.dart +0 -102
- package/flutter_app/lib/src/desktop_companion_io.dart +1 -65
- package/flutter_app/lib/src/desktop_companion_stub.dart +0 -12
- package/flutter_app/linux/flutter/generated_plugin_registrant.cc +0 -4
- package/flutter_app/linux/flutter/generated_plugins.cmake +0 -1
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +0 -2
- package/flutter_app/macos/Podfile.lock +56 -5
- package/flutter_app/pubspec.lock +0 -7
- package/flutter_app/pubspec.yaml +0 -2
- package/flutter_app/web/index.html +174 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.cc +0 -3
- package/flutter_app/windows/flutter/generated_plugins.cmake +0 -1
- package/landing/index.html +0 -2
- package/lib/manager.js +1 -1
- package/lib/schema_migrations.js +75 -0
- package/package.json +1 -4
- package/server/db/database.js +1 -133
- package/server/http/middleware.js +2 -16
- package/server/http/routes.js +0 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +0 -25
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/index.html +174 -0
- package/server/public/main.dart.js +88124 -90164
- package/server/routes/auth.js +11 -2
- package/server/routes/settings.js +0 -5
- package/server/routes/voice_assistant.js +1 -128
- package/server/services/account/erasure.js +0 -3
- package/server/services/ai/deliverables/artifact_helpers.js +1 -1
- package/server/services/ai/loop/conversation_loop.js +6 -2
- package/server/services/ai/loop/tool_dispatch.js +0 -3
- package/server/services/ai/messagingFallback.js +1 -1
- package/server/services/ai/settings.js +0 -6
- package/server/services/ai/toolEvidence.js +1 -4
- package/server/services/ai/tools.js +0 -234
- package/server/services/desktop/auth.js +0 -3
- package/server/services/desktop/registry.js +2 -50
- package/server/services/integrations/neoarchive/provider.js +616 -0
- package/server/services/integrations/neorecall/client.js +140 -0
- package/server/services/integrations/neorecall/constants.js +87 -0
- package/server/services/integrations/neorecall/provider.js +163 -0
- package/server/services/integrations/registry.js +4 -2
- package/server/services/manager.js +0 -22
- package/server/services/memory/manager.js +44 -19
- package/server/services/memory/retrieval_reasoning.js +36 -14
- package/server/services/messaging/whatsapp.js +18 -0
- package/server/services/security/tool_categories.js +0 -5
- package/server/services/social_video/service.js +2 -20
- package/server/services/tasks/adapters/index.js +1 -1
- package/server/services/tasks/integration_runtime.js +3 -67
- package/server/services/tasks/runtime.js +1 -6
- package/server/services/tasks/task_repository.js +6 -13
- package/server/services/timeline/service.js +0 -224
- package/server/services/voice/message.js +0 -24
- package/server/services/voice/providers.js +42 -1
- package/server/services/voice/runtimeManager.js +9 -5
- package/server/services/voice/turnRunner.js +0 -22
- package/server/services/wearable/service.js +0 -5
- package/docs/recordings-and-health.md +0 -41
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +0 -21
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +0 -586
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +0 -78
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +0 -104
- package/flutter_app/lib/main_recordings.dart +0 -920
- package/flutter_app/lib/src/desktop_ocr_bridge.dart +0 -2
- package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +0 -125
- package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +0 -30
- package/flutter_app/lib/src/desktop_passive_history.dart +0 -332
- package/flutter_app/lib/src/recording_bridge.dart +0 -232
- package/flutter_app/lib/src/recording_bridge_io.dart +0 -1027
- package/flutter_app/lib/src/recording_bridge_stub.dart +0 -120
- package/flutter_app/lib/src/recording_bridge_web.dart +0 -702
- package/flutter_app/lib/src/recording_chunk_queue.dart +0 -149
- package/flutter_app/lib/src/recording_chunk_queue_io.dart +0 -182
- package/flutter_app/lib/src/recording_payloads.dart +0 -95
- package/flutter_app/third_party/desktop_audio_capture/LICENSE +0 -21
- package/flutter_app/third_party/desktop_audio_capture/README.md +0 -262
- package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +0 -65
- package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +0 -153
- package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +0 -110
- package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +0 -461
- package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +0 -91
- package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +0 -106
- package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +0 -219
- package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +0 -336
- package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +0 -101
- package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +0 -692
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +0 -35
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +0 -36
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +0 -32
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +0 -32
- package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +0 -878
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +0 -27
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +0 -1172
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +0 -655
- package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +0 -12
- package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +0 -30
- package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +0 -87
- package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +0 -105
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +0 -80
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +0 -31
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +0 -12
- package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +0 -23
- package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +0 -25
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +0 -1117
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +0 -115
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +0 -777
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +0 -87
- package/server/routes/recordings.js +0 -335
- package/server/routes/screenHistory.js +0 -190
- package/server/services/ai/recordingInsights.js +0 -105
- package/server/services/integrations/neomail/provider.js +0 -993
- package/server/services/recordings/deepgram.js +0 -58
- package/server/services/recordings/manager.js +0 -1273
- package/server/services/tasks/adapters/neomail_email_received.js +0 -46
- package/server/services/voice/screenshotContext.js +0 -73
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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 == '
|
|
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
|
|
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 == '
|
|
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.
|
|
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
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
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
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
}
|