neoagent 2.4.1-beta.13 → 2.4.1-beta.15

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.
@@ -129,6 +129,7 @@ class RunsAndLogsPanel extends StatefulWidget {
129
129
  class _RunsAndLogsPanelState extends State<RunsAndLogsPanel>
130
130
  with SingleTickerProviderStateMixin {
131
131
  late final TabController _tabController;
132
+ bool _syncingFromController = false;
132
133
 
133
134
  @override
134
135
  void initState() {
@@ -137,7 +138,7 @@ class _RunsAndLogsPanelState extends State<RunsAndLogsPanel>
137
138
  length: _RunsPageTab.values.length,
138
139
  vsync: this,
139
140
  initialIndex: _tabForSection(widget.controller.selectedSection).index,
140
- );
141
+ )..addListener(_handleTabChanged);
141
142
  }
142
143
 
143
144
  @override
@@ -149,17 +150,34 @@ class _RunsAndLogsPanelState extends State<RunsAndLogsPanel>
149
150
  selectedSection == AppSection.logs)) {
150
151
  final targetIndex = _tabForSection(selectedSection).index;
151
152
  if (_tabController.index != targetIndex) {
153
+ _syncingFromController = true;
152
154
  _tabController.index = targetIndex;
155
+ _syncingFromController = false;
153
156
  }
154
157
  }
155
158
  }
156
159
 
157
160
  @override
158
161
  void dispose() {
162
+ _tabController.removeListener(_handleTabChanged);
159
163
  _tabController.dispose();
160
164
  super.dispose();
161
165
  }
162
166
 
167
+ void _handleTabChanged() {
168
+ if (_syncingFromController || _tabController.indexIsChanging) {
169
+ return;
170
+ }
171
+ _selectSectionForTabIndex(_tabController.index);
172
+ }
173
+
174
+ void _selectSectionForTabIndex(int index) {
175
+ final section = _sectionForTab(_RunsPageTab.values[index]);
176
+ if (widget.controller.selectedSection != section) {
177
+ widget.controller.setSelectedSection(section);
178
+ }
179
+ }
180
+
163
181
  _RunsPageTab _tabForSection(AppSection section) {
164
182
  switch (section) {
165
183
  case AppSection.logs:
@@ -169,6 +187,15 @@ class _RunsAndLogsPanelState extends State<RunsAndLogsPanel>
169
187
  }
170
188
  }
171
189
 
190
+ AppSection _sectionForTab(_RunsPageTab tab) {
191
+ switch (tab) {
192
+ case _RunsPageTab.logs:
193
+ return AppSection.logs;
194
+ case _RunsPageTab.runs:
195
+ return AppSection.runs;
196
+ }
197
+ }
198
+
172
199
  @override
173
200
  Widget build(BuildContext context) {
174
201
  final controller = widget.controller;
@@ -193,6 +220,7 @@ class _RunsAndLogsPanelState extends State<RunsAndLogsPanel>
193
220
  dividerColor: _border,
194
221
  indicatorSize: TabBarIndicatorSize.tab,
195
222
  labelStyle: const TextStyle(fontWeight: FontWeight.w700),
223
+ onTap: _selectSectionForTabIndex,
196
224
  tabs: <Widget>[
197
225
  Tab(text: 'Runs (${controller.recentRuns.length})'),
198
226
  Tab(text: 'Logs (${controller.logs.length})'),
@@ -1644,6 +1644,17 @@ class BackendClient {
1644
1644
  await deleteMap(baseUrl, '/api/recordings/$sessionId');
1645
1645
  }
1646
1646
 
1647
+ Future<Map<String, dynamic>> transcribeAudio(
1648
+ String baseUrl, {
1649
+ required String audioBase64,
1650
+ String mimeType = 'audio/pcm;rate=16000;channels=1',
1651
+ }) {
1652
+ return postMap(baseUrl, '/api/voice-assistant/transcribe', <String, dynamic>{
1653
+ 'audioBase64': audioBase64,
1654
+ 'mimeType': mimeType,
1655
+ });
1656
+ }
1657
+
1647
1658
  Future<Map<String, dynamic>> runVoiceAssistantTurn(
1648
1659
  String baseUrl, {
1649
1660
  required String sessionId,
@@ -25,6 +25,7 @@ class DesktopCompanionManager extends ChangeNotifier {
25
25
  final DesktopCompanionActions _actions;
26
26
  WebSocket? _socket;
27
27
  Timer? _reconnectTimer;
28
+ Timer? _connectionWatchdogTimer;
28
29
  Timer? _streamTimer;
29
30
  bool _streamCaptureInFlight = false;
30
31
  // Set true while a click / drag / scroll / typeText / pressKey command is
@@ -94,6 +95,7 @@ class DesktopCompanionManager extends ChangeNotifier {
94
95
  await disconnect();
95
96
  return;
96
97
  }
98
+ _ensureConnectionWatchdog();
97
99
  await _ensureConnected();
98
100
  }
99
101
 
@@ -138,6 +140,8 @@ class DesktopCompanionManager extends ChangeNotifier {
138
140
  Future<void> disconnect() async {
139
141
  _reconnectTimer?.cancel();
140
142
  _reconnectTimer = null;
143
+ _connectionWatchdogTimer?.cancel();
144
+ _connectionWatchdogTimer = null;
141
145
  _stopStreaming();
142
146
  _connecting = false;
143
147
  _connected = false;
@@ -224,6 +228,7 @@ class DesktopCompanionManager extends ChangeNotifier {
224
228
  uri.toString(),
225
229
  headers: <String, Object>{'Cookie': _sessionCookie},
226
230
  );
231
+ socket.pingInterval = const Duration(seconds: 25);
227
232
  _socket = socket;
228
233
  socket.listen(
229
234
  _handleMessage,
@@ -470,6 +475,8 @@ class DesktopCompanionManager extends ChangeNotifier {
470
475
  void dispose() {
471
476
  _reconnectTimer?.cancel();
472
477
  _reconnectTimer = null;
478
+ _connectionWatchdogTimer?.cancel();
479
+ _connectionWatchdogTimer = null;
473
480
  _stopStreaming();
474
481
  _connecting = false;
475
482
  _connected = false;
@@ -486,12 +493,28 @@ class DesktopCompanionManager extends ChangeNotifier {
486
493
 
487
494
  void _scheduleReconnect() {
488
495
  if (!_enabled || !_authenticated || _sessionCookie.isEmpty) return;
496
+ _ensureConnectionWatchdog();
489
497
  _reconnectTimer?.cancel();
490
498
  _reconnectTimer = Timer(const Duration(seconds: 5), () {
491
499
  unawaited(_ensureConnected());
492
500
  });
493
501
  }
494
502
 
503
+ void _ensureConnectionWatchdog() {
504
+ if (!_enabled || !_authenticated || _sessionCookie.isEmpty) return;
505
+ if (_connectionWatchdogTimer != null) return;
506
+ _connectionWatchdogTimer = Timer.periodic(const Duration(seconds: 30), (_) {
507
+ if (!_enabled || !_authenticated || _sessionCookie.isEmpty) {
508
+ _connectionWatchdogTimer?.cancel();
509
+ _connectionWatchdogTimer = null;
510
+ return;
511
+ }
512
+ if (!_connected && !_connecting) {
513
+ unawaited(_ensureConnected());
514
+ }
515
+ });
516
+ }
517
+
495
518
  Future<void> _sendEvent(String event, Map<String, Object?> payload) async {
496
519
  final socket = _socket;
497
520
  if (socket == null || !_connected) return;
@@ -15,6 +15,9 @@ class StreamRenderer extends StatefulWidget {
15
15
  this.onSwipe,
16
16
  this.onType,
17
17
  this.onHover,
18
+ this.onFirstFrame,
19
+ this.onFrameTimeout,
20
+ this.firstFrameTimeout = const Duration(seconds: 8),
18
21
  this.fit = BoxFit.contain,
19
22
  this.alignment = Alignment.center,
20
23
  });
@@ -27,6 +30,9 @@ class StreamRenderer extends StatefulWidget {
27
30
  final void Function(double x1, double y1, double x2, double y2)? onSwipe;
28
31
  final void Function(String text)? onType;
29
32
  final void Function(double x, double y)? onHover;
33
+ final VoidCallback? onFirstFrame;
34
+ final VoidCallback? onFrameTimeout;
35
+ final Duration firstFrameTimeout;
30
36
  final BoxFit fit;
31
37
  final Alignment alignment;
32
38
 
@@ -43,6 +49,8 @@ class _StreamRendererState extends State<StreamRenderer> {
43
49
  Offset? _dragEnd;
44
50
 
45
51
  Timer? _hoverThrottleTimer;
52
+ Timer? _firstFrameTimer;
53
+ bool _firstFrameTimedOut = false;
46
54
  Offset? _pendingHoverOffset;
47
55
  DateTime _lastHoverTime = DateTime.fromMillisecondsSinceEpoch(0);
48
56
 
@@ -54,6 +62,7 @@ class _StreamRendererState extends State<StreamRenderer> {
54
62
  'deviceId': widget.deviceId,
55
63
  'platform': widget.platform,
56
64
  });
65
+ _startFirstFrameTimer();
57
66
  }
58
67
 
59
68
  @override
@@ -65,7 +74,9 @@ class _StreamRendererState extends State<StreamRenderer> {
65
74
  return;
66
75
  }
67
76
  _frameSize = null;
77
+ _frame = null;
68
78
  _detachImageListener();
79
+ _firstFrameTimer?.cancel();
69
80
  oldWidget.socket.off('stream:frame', _onFrame);
70
81
  oldWidget.socket.emit('stream:unsubscribe', <String, Object?>{
71
82
  'deviceId': oldWidget.deviceId,
@@ -76,6 +87,24 @@ class _StreamRendererState extends State<StreamRenderer> {
76
87
  'deviceId': widget.deviceId,
77
88
  'platform': widget.platform,
78
89
  });
90
+ _startFirstFrameTimer();
91
+ }
92
+
93
+ void _startFirstFrameTimer() {
94
+ _firstFrameTimer?.cancel();
95
+ _firstFrameTimedOut = false;
96
+ if (widget.onFrameTimeout == null ||
97
+ widget.firstFrameTimeout <= Duration.zero ||
98
+ _frame != null) {
99
+ return;
100
+ }
101
+ _firstFrameTimer = Timer(widget.firstFrameTimeout, () {
102
+ if (!mounted || _frame != null) {
103
+ return;
104
+ }
105
+ _firstFrameTimedOut = true;
106
+ widget.onFrameTimeout?.call();
107
+ });
79
108
  }
80
109
 
81
110
  void _onFrame(dynamic data) {
@@ -102,6 +131,14 @@ class _StreamRendererState extends State<StreamRenderer> {
102
131
  _ => null,
103
132
  };
104
133
  if (frame == null || frame.isEmpty || !mounted) return;
134
+ final hadFrame = _frame != null;
135
+ if (!hadFrame) {
136
+ _firstFrameTimer?.cancel();
137
+ _firstFrameTimer = null;
138
+ if (!_firstFrameTimedOut) {
139
+ widget.onFirstFrame?.call();
140
+ }
141
+ }
105
142
  if (_frameSize == null) {
106
143
  _resolveFrameSize(frame);
107
144
  }
@@ -146,7 +183,8 @@ class _StreamRendererState extends State<StreamRenderer> {
146
183
  return MouseRegion(
147
184
  onHover: widget.onHover == null
148
185
  ? null
149
- : (event) => _handleHoverEvent(event.localPosition, constraints.biggest),
186
+ : (event) =>
187
+ _handleHoverEvent(event.localPosition, constraints.biggest),
150
188
  child: GestureDetector(
151
189
  behavior: HitTestBehavior.opaque,
152
190
  onTapDown: widget.onTap == null
@@ -197,7 +235,7 @@ class _StreamRendererState extends State<StreamRenderer> {
197
235
  final now = DateTime.now();
198
236
  final elapsed = now.difference(_lastHoverTime);
199
237
  const throttleDuration = Duration(milliseconds: 70);
200
-
238
+
201
239
  if (elapsed >= throttleDuration) {
202
240
  _sendPendingHover(boxSize);
203
241
  } else {
@@ -213,7 +251,7 @@ class _StreamRendererState extends State<StreamRenderer> {
213
251
  if (offset == null) return;
214
252
  _pendingHoverOffset = null;
215
253
  _lastHoverTime = DateTime.now();
216
-
254
+
217
255
  final point = _mapToRemote(offset, boxSize);
218
256
  if (point != null) {
219
257
  widget.onHover?.call(point.dx, point.dy);
@@ -275,6 +313,7 @@ class _StreamRendererState extends State<StreamRenderer> {
275
313
  @override
276
314
  void dispose() {
277
315
  _hoverThrottleTimer?.cancel();
316
+ _firstFrameTimer?.cancel();
278
317
  widget.socket.emit('stream:unsubscribe', <String, Object?>{
279
318
  'deviceId': widget.deviceId,
280
319
  'platform': widget.platform,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.4.1-beta.13",
3
+ "version": "2.4.1-beta.15",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
@@ -1 +1 @@
1
- da05ba4590c63eb13c0254be7a8b0840
1
+ 8dab82a7512518b6f2bb17abf16b00fe
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"4c525dac5ebe5971c5708ef73558ed8edcf4a3
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "2422850760" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "617137012" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });