neoagent 3.1.1-beta.8 → 3.1.1-beta.9

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.
@@ -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,82 @@ 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(
38
+ 0,
39
+ items.length - 1,
40
+ );
41
+ if (nextIndex == selectedIndex) {
42
+ return;
43
+ }
44
+ setPopupState(() {
45
+ selectedIndex = nextIndex;
46
+ });
47
+ _selectEvent(items[nextIndex]);
48
+ }
49
+
50
+ return _TimelineDetailPane(
51
+ items: items,
52
+ selectedEvent: selectedEvent,
53
+ selectedIndex: selectedIndex,
54
+ onSelectPrevious: selectedIndex > 0 ? () => selectOffset(-1) : null,
55
+ onSelectNext: selectedIndex < items.length - 1
56
+ ? () => selectOffset(1)
57
+ : null,
58
+ onOpenRun: selectedEvent.runId.isNotEmpty
59
+ ? () => unawaited(
60
+ widget.controller.openRunDetails(selectedEvent.runId),
61
+ )
62
+ : null,
63
+ onClose: () => Navigator.of(context).pop(),
64
+ );
65
+ }
66
+
67
+ final compact = MediaQuery.sizeOf(context).width < 760;
68
+
69
+ if (compact) {
70
+ await showModalBottomSheet<void>(
71
+ context: context,
72
+ isScrollControlled: true,
73
+ useSafeArea: true,
74
+ backgroundColor: Colors.transparent,
75
+ builder: (context) {
76
+ return StatefulBuilder(
77
+ builder: (context, setSheetState) {
78
+ return FractionallySizedBox(
79
+ heightFactor: 0.9,
80
+ child: Padding(
81
+ padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
82
+ child: buildDetail(setSheetState),
83
+ ),
84
+ );
85
+ },
86
+ );
87
+ },
88
+ );
89
+ return;
90
+ }
91
+
92
+ await showDialog<void>(
65
93
  context: context,
66
- isScrollControlled: true,
67
- useSafeArea: true,
68
- backgroundColor: Colors.transparent,
94
+ barrierColor: Colors.black.withValues(alpha: 0.5),
69
95
  builder: (context) {
70
96
  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,
97
+ builder: (context, setDialogState) {
98
+ return Dialog(
99
+ backgroundColor: Colors.transparent,
100
+ insetPadding: const EdgeInsets.symmetric(
101
+ horizontal: 40,
102
+ vertical: 40,
103
+ ),
104
+ child: ConstrainedBox(
105
+ constraints: const BoxConstraints(
106
+ maxWidth: 640,
107
+ maxHeight: 780,
106
108
  ),
109
+ child: buildDetail(setDialogState),
107
110
  ),
108
111
  );
109
112
  },
@@ -116,22 +119,59 @@ class _TimelinePanelState extends State<TimelinePanel> {
116
119
  Widget build(BuildContext context) {
117
120
  final items = _sortedTimelineEvents(widget.controller.timelineItems);
118
121
  final groups = _groupTimelineEvents(items);
119
- final selectedEvent = _resolveSelectedEvent(items);
120
- final selectedIndex = selectedEvent == null
121
- ? -1
122
- : items.indexWhere((item) => item.id == selectedEvent.id);
122
+ final controller = widget.controller;
123
123
 
124
124
  return Padding(
125
125
  padding: _pagePadding(context),
126
126
  child: Column(
127
127
  crossAxisAlignment: CrossAxisAlignment.start,
128
128
  children: <Widget>[
129
- _TimelineHeroHeader(
130
- items: items,
131
- selectedEvent: selectedEvent,
132
- controller: widget.controller,
129
+ _PageTitle(
130
+ title: 'Timeline',
131
+ subtitle:
132
+ 'Emails, AI actions, recordings, tasks and run activity in one chronological feed.',
133
+ trailing: Wrap(
134
+ spacing: 10,
135
+ runSpacing: 10,
136
+ children: <Widget>[
137
+ for (final filter in const <({String id, String label})>[
138
+ (id: 'screen', label: 'Screen'),
139
+ (id: 'tasks', label: 'Tasks'),
140
+ (id: 'runs', label: 'Runs'),
141
+ ])
142
+ FilterChip(
143
+ selected: controller.selectedTimelineSources.contains(
144
+ filter.id,
145
+ ),
146
+ label: Text(filter.label),
147
+ onSelected: (_) =>
148
+ controller.toggleTimelineSource(filter.id),
149
+ avatar: Icon(
150
+ _timelineLaneIcon(filter.id),
151
+ size: 16,
152
+ color:
153
+ controller.selectedTimelineSources.contains(
154
+ filter.id,
155
+ )
156
+ ? _bgSecondary
157
+ : _sourceColorForKind(filter.id),
158
+ ),
159
+ ),
160
+ OutlinedButton.icon(
161
+ onPressed: controller.isRefreshingTimeline
162
+ ? null
163
+ : controller.refreshTimeline,
164
+ icon: controller.isRefreshingTimeline
165
+ ? const SizedBox.square(
166
+ dimension: 14,
167
+ child: CircularProgressIndicator(strokeWidth: 2),
168
+ )
169
+ : const Icon(Icons.sync_outlined),
170
+ label: const Text('Refresh'),
171
+ ),
172
+ ],
173
+ ),
133
174
  ),
134
- const SizedBox(height: 18),
135
175
  Expanded(
136
176
  child: items.isEmpty
137
177
  ? Card(
@@ -145,66 +185,11 @@ class _TimelinePanelState extends State<TimelinePanel> {
145
185
  ),
146
186
  ),
147
187
  )
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
- },
188
+ : _TimelineFeedPane(
189
+ groups: groups,
190
+ selectedEventId: _selectedEventId,
191
+ onSelectEvent: (item) =>
192
+ unawaited(_showEventDetailsPopup(items, item)),
208
193
  ),
209
194
  ),
210
195
  ],
@@ -213,179 +198,6 @@ class _TimelinePanelState extends State<TimelinePanel> {
213
198
  }
214
199
  }
215
200
 
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
201
  class _TimelineFeedPane extends StatelessWidget {
390
202
  const _TimelineFeedPane({
391
203
  required this.groups,
@@ -751,6 +563,7 @@ class _TimelineDetailPane extends StatelessWidget {
751
563
  this.onSelectPrevious,
752
564
  this.onSelectNext,
753
565
  this.onOpenRun,
566
+ this.onClose,
754
567
  });
755
568
 
756
569
  final List<TimelineEventItem> items;
@@ -759,6 +572,7 @@ class _TimelineDetailPane extends StatelessWidget {
759
572
  final VoidCallback? onSelectPrevious;
760
573
  final VoidCallback? onSelectNext;
761
574
  final VoidCallback? onOpenRun;
575
+ final VoidCallback? onClose;
762
576
 
763
577
  @override
764
578
  Widget build(BuildContext context) {
@@ -814,6 +628,12 @@ class _TimelineDetailPane extends StatelessWidget {
814
628
  onPressed: onSelectNext,
815
629
  icon: const Icon(Icons.chevron_right_rounded),
816
630
  ),
631
+ if (onClose != null)
632
+ IconButton(
633
+ tooltip: 'Close',
634
+ onPressed: onClose,
635
+ icon: const Icon(Icons.close_rounded),
636
+ ),
817
637
  ],
818
638
  ),
819
639
  ),
@@ -1310,19 +1130,6 @@ bool _isSameDay(DateTime a, DateTime b) {
1310
1130
  return a.year == b.year && a.month == b.month && a.day == b.day;
1311
1131
  }
1312
1132
 
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
1133
  String _formatTimelineDate(DateTime value) {
1327
1134
  return '${_monthShort(value.month)} ${value.day}, ${value.year}';
1328
1135
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "3.1.1-beta.8",
3
+ "version": "3.1.1-beta.9",
4
4
  "description": "Self-hosted AI agent for long-running tasks, automation, messaging, device control, and local memory",
5
5
  "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
@@ -1 +1 @@
1
- 70da65bf673b2dfca39064bfac839c64
1
+ 42ed3501c09c6e3ffc03212056c51751
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"83675ed27633283e7fc296c8bca22e841224c0
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "145521641" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "648964245" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });