neoagent 3.0.1-beta.23 → 3.0.1-beta.25

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 (49) hide show
  1. package/.env.example +0 -8
  2. package/flutter_app/lib/main.dart +1 -0
  3. package/flutter_app/lib/main_app_shell.dart +28 -13
  4. package/flutter_app/lib/main_chat.dart +28 -0
  5. package/flutter_app/lib/main_controller.dart +145 -12
  6. package/flutter_app/lib/main_devices.dart +292 -206
  7. package/flutter_app/lib/main_models.dart +90 -0
  8. package/flutter_app/lib/main_navigation.dart +13 -1
  9. package/flutter_app/lib/main_settings.dart +369 -255
  10. package/flutter_app/lib/main_shared.dart +1 -0
  11. package/flutter_app/lib/main_timeline.dart +230 -0
  12. package/flutter_app/lib/src/backend_client.dart +64 -21
  13. package/flutter_app/lib/src/desktop_companion_actions.dart +132 -21
  14. package/flutter_app/lib/src/desktop_companion_io.dart +65 -1
  15. package/flutter_app/lib/src/desktop_companion_stub.dart +12 -0
  16. package/flutter_app/lib/src/desktop_ocr_bridge.dart +2 -0
  17. package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +125 -0
  18. package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +30 -0
  19. package/flutter_app/lib/src/desktop_passive_history.dart +332 -0
  20. package/flutter_app/lib/src/recording_bridge_io.dart +80 -72
  21. package/flutter_app/lib/src/recording_bridge_web.dart +127 -114
  22. package/flutter_app/lib/src/recording_chunk_queue.dart +149 -0
  23. package/flutter_app/lib/src/recording_chunk_queue_io.dart +182 -0
  24. package/flutter_app/lib/src/recording_payloads.dart +9 -0
  25. package/flutter_app/macos/Runner/AppDelegate.swift +25 -0
  26. package/flutter_app/windows/runner/flutter_window.cpp +75 -0
  27. package/package.json +1 -1
  28. package/server/admin/admin.js +0 -70
  29. package/server/admin/index.html +0 -4
  30. package/server/db/database.js +89 -1
  31. package/server/http/routes.js +1 -0
  32. package/server/public/.last_build_id +1 -1
  33. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  34. package/server/public/flutter_bootstrap.js +1 -1
  35. package/server/public/main.dart.js +72780 -72297
  36. package/server/routes/admin.js +0 -43
  37. package/server/routes/recordings.js +96 -6
  38. package/server/routes/screenHistory.js +140 -2
  39. package/server/routes/timeline.js +43 -0
  40. package/server/services/ai/loop/agent_engine_core.js +8 -1
  41. package/server/services/ai/loop/conversation_loop.js +29 -0
  42. package/server/services/desktop/auth.js +3 -0
  43. package/server/services/desktop/registry.js +50 -2
  44. package/server/services/manager.js +12 -42
  45. package/server/services/recordings/manager.js +60 -27
  46. package/server/services/tasks/runtime.js +104 -0
  47. package/server/services/timeline/service.js +558 -0
  48. package/server/services/desktop/screenRecorder.js +0 -292
  49. package/server/services/desktop/screen_recorder_support.js +0 -46
package/.env.example CHANGED
@@ -253,14 +253,6 @@ OLLAMA_URL=http://localhost:11434
253
253
  # Connected integration memory refresh cadence in milliseconds.
254
254
  NEOAGENT_MEMORY_INGESTION_INTERVAL_MS=600000
255
255
 
256
- # Local screen OCR capture. This captures the backend host's macOS desktop,
257
- # so enable it only when that host is the configured user's desktop.
258
- NEOAGENT_SCREEN_RECORDER_ENABLED=false
259
- # Existing NeoAgent user ID that owns all captured screen history. Required when enabled.
260
- NEOAGENT_SCREEN_RECORDER_USER_ID=
261
- NEOAGENT_SCREEN_RECORDER_INTERVAL_MS=10000
262
- NEOAGENT_SCREEN_RECORDER_RETENTION_DAYS=7
263
-
264
256
  ########################################
265
257
  # Messaging and voice integrations
266
258
  ########################################
@@ -61,6 +61,7 @@ part 'main_models.dart';
61
61
  part 'main_shared.dart';
62
62
  part 'main_voice_assistant.dart';
63
63
  part 'main_navigation.dart';
64
+ part 'main_timeline.dart';
64
65
  part 'main_runtime.dart';
65
66
  part 'main_controller.dart';
66
67
  part 'main_devices.dart';
@@ -205,9 +205,15 @@ class _BackendSetupViewState extends State<BackendSetupView> {
205
205
  const SizedBox(height: 10),
206
206
  Center(
207
207
  child: TextButton.icon(
208
- onPressed: () => setState(() => _localInstall = true),
209
- icon: const Icon(Icons.download_outlined, size: 16),
210
- label: const Text('Install NeoAgent on this machine'),
208
+ onPressed: () =>
209
+ setState(() => _localInstall = true),
210
+ icon: const Icon(
211
+ Icons.download_outlined,
212
+ size: 16,
213
+ ),
214
+ label: const Text(
215
+ 'Install NeoAgent on this machine',
216
+ ),
211
217
  ),
212
218
  ),
213
219
  ],
@@ -268,7 +274,9 @@ class _LocalInstallWidgetState extends State<_LocalInstallWidget> {
268
274
  if (!mounted) return;
269
275
  setState(() => _phase = _LocalInstallPhase.checking);
270
276
 
271
- final r = await Process.run(Platform.isWindows ? 'where' : 'which', ['node']);
277
+ final r = await Process.run(Platform.isWindows ? 'where' : 'which', [
278
+ 'node',
279
+ ]);
272
280
  if (r.exitCode == 0) {
273
281
  _nodePath = (r.stdout as String).trim().split('\n').first.trim();
274
282
  } else if (!Platform.isWindows) {
@@ -315,7 +323,8 @@ class _LocalInstallWidgetState extends State<_LocalInstallWidget> {
315
323
  if (!_ensureEnvFile()) {
316
324
  setState(() {
317
325
  _phase = _LocalInstallPhase.failed;
318
- _errorMsg = 'Could not create ~/.neoagent/runtime/.env — check directory permissions.';
326
+ _errorMsg =
327
+ 'Could not create ~/.neoagent/runtime/.env — check directory permissions.';
319
328
  });
320
329
  return;
321
330
  }
@@ -324,11 +333,10 @@ class _LocalInstallWidgetState extends State<_LocalInstallWidget> {
324
333
  _log.clear();
325
334
  });
326
335
 
327
- final process = await Process.start(
328
- _nodePath!,
329
- ['bin/neoagent.js', 'install'],
330
- workingDirectory: _installDir,
331
- );
336
+ final process = await Process.start(_nodePath!, [
337
+ 'bin/neoagent.js',
338
+ 'install',
339
+ ], workingDirectory: _installDir);
332
340
  _proc = process;
333
341
 
334
342
  void onChunk(String data) {
@@ -349,7 +357,8 @@ class _LocalInstallWidgetState extends State<_LocalInstallWidget> {
349
357
  if (!mounted) return;
350
358
  setState(() {
351
359
  _phase = exit == 0 ? _LocalInstallPhase.done : _LocalInstallPhase.failed;
352
- if (exit != 0) _errorMsg = 'Installation failed (exit $exit). Check the log above.';
360
+ if (exit != 0)
361
+ _errorMsg = 'Installation failed (exit $exit). Check the log above.';
353
362
  });
354
363
  }
355
364
 
@@ -388,7 +397,10 @@ class _LocalInstallWidgetState extends State<_LocalInstallWidget> {
388
397
  const SizedBox(height: 16),
389
398
  const _BrandLockup(logoSize: 60),
390
399
  const SizedBox(height: 22),
391
- Text('INSTALL LOCALLY', style: _sectionEyebrowStyle()),
400
+ Text(
401
+ 'INSTALL LOCALLY',
402
+ style: _sectionEyebrowStyle(),
403
+ ),
392
404
  const SizedBox(height: 10),
393
405
  Text(
394
406
  'Install NeoAgent as a background service',
@@ -459,7 +471,8 @@ class _LocalInstallWidgetState extends State<_LocalInstallWidget> {
459
471
  final proc = _proc;
460
472
  proc?.kill(ProcessSignal.sigterm);
461
473
  await proc?.exitCode;
462
- if (mounted) setState(() => _phase = _LocalInstallPhase.ready);
474
+ if (mounted)
475
+ setState(() => _phase = _LocalInstallPhase.ready);
463
476
  },
464
477
  child: const Text('Cancel'),
465
478
  ),
@@ -2496,6 +2509,8 @@ class _SectionBody extends StatelessWidget {
2496
2509
  switch (controller.selectedSection) {
2497
2510
  case AppSection.chat:
2498
2511
  return ChatPanel(controller: controller);
2512
+ case AppSection.timeline:
2513
+ return TimelinePanel(controller: controller);
2499
2514
  case AppSection.voiceAssistant:
2500
2515
  return VoiceAssistantPanel(controller: controller);
2501
2516
  case AppSection.devices:
@@ -2453,6 +2453,34 @@ class _RunsPanelState extends State<RunsPanel> {
2453
2453
  }
2454
2454
 
2455
2455
  void _syncSelection() {
2456
+ final requestedRunId = widget.controller.requestedRunFocusId?.trim();
2457
+ if (requestedRunId != null && requestedRunId.isNotEmpty) {
2458
+ final requestedRunExists = widget.controller.recentRuns.any(
2459
+ (run) => run.id == requestedRunId,
2460
+ );
2461
+ if (_filteredRuns.any((run) => run.id == requestedRunId)) {
2462
+ widget.controller.clearRequestedRunFocus(requestedRunId);
2463
+ if (_selectedRunId != requestedRunId) {
2464
+ unawaited(_selectRun(requestedRunId));
2465
+ }
2466
+ return;
2467
+ }
2468
+ if (requestedRunExists) {
2469
+ if (_statusFilter != 'all') {
2470
+ if (mounted) {
2471
+ setState(() {
2472
+ _statusFilter = 'all';
2473
+ });
2474
+ } else {
2475
+ _statusFilter = 'all';
2476
+ }
2477
+ }
2478
+ if (_searchController.text.isNotEmpty) {
2479
+ _searchController.clear();
2480
+ return;
2481
+ }
2482
+ }
2483
+ }
2456
2484
  final runs = _filteredRuns;
2457
2485
  if (runs.isEmpty) {
2458
2486
  _selectedRunId = null;
@@ -47,7 +47,8 @@ class NeoAgentController extends ChangeNotifier {
47
47
  StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
48
48
  bool _connectivityPluginAvailable = true;
49
49
  static const int _maxVisibleLogs = 400;
50
- static const int _maxToolEvents = 500; // separate list from _maxVisibleLogs (chat diagnostics)
50
+ static const int _maxToolEvents =
51
+ 500; // separate list from _maxVisibleLogs (chat diagnostics)
51
52
 
52
53
  static const String _configuredBackendUrl = String.fromEnvironment(
53
54
  'NEOAGENT_BACKEND_URL',
@@ -153,6 +154,7 @@ class NeoAgentController extends ChangeNotifier {
153
154
  List<ModelMeta> supportedModels = const <ModelMeta>[];
154
155
  List<AiProviderMeta> aiProviders = const <AiProviderMeta>[];
155
156
  List<RunSummary> recentRuns = const <RunSummary>[];
157
+ List<TimelineEventItem> timelineItems = const <TimelineEventItem>[];
156
158
  TokenUsageSnapshot? tokenUsage;
157
159
  Map<String, dynamic>? billingSubscription;
158
160
  List<Map<String, dynamic>> billingPlans = const <Map<String, dynamic>>[];
@@ -212,6 +214,7 @@ class NeoAgentController extends ChangeNotifier {
212
214
  String? _chatHistoryBeforeCreatedAt;
213
215
  String? _chatHistoryBeforeSource;
214
216
  String? _chatHistoryBeforeId;
217
+ String? _requestedRunFocusId;
215
218
 
216
219
  ActiveRunState? activeRun;
217
220
  List<ToolEventItem> toolEvents = const <ToolEventItem>[];
@@ -240,14 +243,23 @@ class NeoAgentController extends ChangeNotifier {
240
243
  bool _desktopKeepRunningOnClose = true;
241
244
  bool _desktopAutoShowFloatingToolbar = true;
242
245
  bool _desktopAssistantHotkeyEnabled = true;
246
+ bool isRefreshingTimeline = false;
247
+ Set<String> selectedTimelineSources = <String>{'screen', 'tasks', 'runs'};
243
248
 
244
249
  bool get desktopCompanionEnabled => _desktopCompanion.enabled;
250
+ bool get desktopPassiveHistoryEnabled =>
251
+ _desktopCompanion.passiveHistoryEnabled;
245
252
  bool get isLauncherMode => appMode == NeoAgentAppMode.launcher;
246
253
  bool get desktopCompanionConnected => _desktopCompanion.connected;
247
254
  bool get desktopCompanionConnecting => _desktopCompanion.connecting;
248
255
  bool get desktopCompanionPaused => _desktopCompanion.paused;
249
256
  String get desktopCompanionLabel => _desktopCompanion.label;
250
257
  String? get desktopCompanionErrorMessage => _desktopCompanion.errorMessage;
258
+ String? get desktopPassiveHistoryLastUploadedAt =>
259
+ _desktopCompanion.passiveHistoryLastUploadedAt;
260
+ String? get desktopPassiveHistoryLastError =>
261
+ _desktopCompanion.passiveHistoryLastError;
262
+ String? get requestedRunFocusId => _requestedRunFocusId;
251
263
  Map<String, Object?> get desktopCompanionStatus => _desktopCompanion.status;
252
264
 
253
265
  bool get hasLiveRun => isSendingMessage && activeRun != null;
@@ -828,7 +840,9 @@ class NeoAgentController extends ChangeNotifier {
828
840
 
829
841
  Future<void> bootstrap() async {
830
842
  _prefs = await SharedPreferences.getInstance();
831
- _ignoredChats.addAll(_prefs?.getStringList('messaging.ignored_chats') ?? <String>[]);
843
+ _ignoredChats.addAll(
844
+ _prefs?.getStringList('messaging.ignored_chats') ?? <String>[],
845
+ );
832
846
  await _desktopCompanion.bootstrap(_prefs!);
833
847
  final configured = _configuredBackendUrl.trim();
834
848
  final savedBackendUrl = _prefs?.getString('backend_url')?.trim() ?? '';
@@ -1671,6 +1685,8 @@ class NeoAgentController extends ChangeNotifier {
1671
1685
  supportedModels = const <ModelMeta>[];
1672
1686
  aiProviders = const <AiProviderMeta>[];
1673
1687
  recentRuns = const <RunSummary>[];
1688
+ timelineItems = const <TimelineEventItem>[];
1689
+ isRefreshingTimeline = false;
1674
1690
  tokenUsage = null;
1675
1691
  updateStatus = const UpdateStatusSnapshot();
1676
1692
  _serverLogs = const <LogEntry>[];
@@ -1794,6 +1810,9 @@ class NeoAgentController extends ChangeNotifier {
1794
1810
  if (section == AppSection.devices) {
1795
1811
  unawaited(refreshDevices());
1796
1812
  }
1813
+ if (section == AppSection.timeline) {
1814
+ unawaited(refreshTimeline());
1815
+ }
1797
1816
  if (section == AppSection.accountSettings) {
1798
1817
  unawaited(refreshAccountSettings());
1799
1818
  }
@@ -1803,6 +1822,22 @@ class NeoAgentController extends ChangeNotifier {
1803
1822
  notifyListeners();
1804
1823
  }
1805
1824
 
1825
+ Future<void> openRunDetails(String runId) async {
1826
+ final normalized = runId.trim();
1827
+ if (normalized.isEmpty) {
1828
+ return;
1829
+ }
1830
+ _requestedRunFocusId = normalized;
1831
+ setSelectedSection(AppSection.runs);
1832
+ await refreshRunsOnly();
1833
+ }
1834
+
1835
+ void clearRequestedRunFocus(String runId) {
1836
+ if (_requestedRunFocusId == runId) {
1837
+ _requestedRunFocusId = null;
1838
+ }
1839
+ }
1840
+
1806
1841
  void _ensureSelectedAgent() {
1807
1842
  if (agentProfiles.isEmpty) {
1808
1843
  selectedAgentId = null;
@@ -2058,15 +2093,23 @@ class NeoAgentController extends ChangeNotifier {
2058
2093
  final key = '${notice.platform}:${notice.chatId ?? notice.sender ?? ''}';
2059
2094
  _ignoredChats.add(key);
2060
2095
  _blockedSenderQueue.removeWhere(
2061
- (n) => n.platform == notice.platform && (n.chatId == notice.chatId || n.sender == notice.sender),
2096
+ (n) =>
2097
+ n.platform == notice.platform &&
2098
+ (n.chatId == notice.chatId || n.sender == notice.sender),
2099
+ );
2100
+ await _prefs?.setStringList(
2101
+ 'messaging.ignored_chats',
2102
+ _ignoredChats.toList(),
2062
2103
  );
2063
- await _prefs?.setStringList('messaging.ignored_chats', _ignoredChats.toList());
2064
2104
  notifyListeners();
2065
2105
  }
2066
2106
 
2067
2107
  Future<void> removeIgnoredChat(String key) async {
2068
2108
  _ignoredChats.remove(key);
2069
- await _prefs?.setStringList('messaging.ignored_chats', _ignoredChats.toList());
2109
+ await _prefs?.setStringList(
2110
+ 'messaging.ignored_chats',
2111
+ _ignoredChats.toList(),
2112
+ );
2070
2113
  notifyListeners();
2071
2114
  }
2072
2115
 
@@ -2080,7 +2123,8 @@ class NeoAgentController extends ChangeNotifier {
2080
2123
  }
2081
2124
 
2082
2125
  void _enqueueBlockedSenderNotice(BlockedSenderNotice notice) {
2083
- final ignoreKey = '${notice.platform}:${notice.chatId ?? notice.sender ?? ''}';
2126
+ final ignoreKey =
2127
+ '${notice.platform}:${notice.chatId ?? notice.sender ?? ''}';
2084
2128
  if (_ignoredChats.contains(ignoreKey)) return;
2085
2129
  final exists = _blockedSenderQueue.any((item) => item.id == notice.id);
2086
2130
  if (!exists) {
@@ -2178,6 +2222,15 @@ class NeoAgentController extends ChangeNotifier {
2178
2222
  _backendClient.fetchRuns(backendUrl, agentId: agentId),
2179
2223
  const <String, dynamic>{'runs': <dynamic>[]},
2180
2224
  );
2225
+ final timelineFuture = _softRefreshLoad<Map<String, dynamic>>(
2226
+ 'timeline',
2227
+ _backendClient.fetchTimeline(
2228
+ backendUrl,
2229
+ sources: selectedTimelineSources,
2230
+ limit: 50,
2231
+ ),
2232
+ const <String, dynamic>{'items': <dynamic>[]},
2233
+ );
2181
2234
  final versionFuture = _softRefreshLoad<Map<String, dynamic>>(
2182
2235
  'version',
2183
2236
  _backendClient.fetchVersion(backendUrl),
@@ -2301,6 +2354,7 @@ class NeoAgentController extends ChangeNotifier {
2301
2354
  final providersResponse = await providersFuture;
2302
2355
  final settingsResponse = await settingsFuture;
2303
2356
  final runsResponse = await runsFuture;
2357
+ final timelineResponse = await timelineFuture;
2304
2358
  final versionResponse = await versionFuture;
2305
2359
  final tokenResponse = await tokenFuture;
2306
2360
  final rateLimitResponse = await rateLimitFuture;
@@ -2348,6 +2402,12 @@ class NeoAgentController extends ChangeNotifier {
2348
2402
  RunSummary.fromJson,
2349
2403
  fallbackToMapValues: true,
2350
2404
  );
2405
+ timelineItems = _decodeModelList(
2406
+ 'timeline',
2407
+ timelineResponse['items'],
2408
+ TimelineEventItem.fromJson,
2409
+ fallbackToMapValues: true,
2410
+ );
2351
2411
  versionInfo = versionResponse;
2352
2412
  backendHealthStatus = healthResponse;
2353
2413
  tokenUsage = TokenUsageSnapshot.fromJson(tokenResponse);
@@ -2541,6 +2601,62 @@ class NeoAgentController extends ChangeNotifier {
2541
2601
  } catch (_) {}
2542
2602
  }
2543
2603
 
2604
+ Future<void> refreshTimeline({
2605
+ Set<String>? sources,
2606
+ bool notify = true,
2607
+ }) async {
2608
+ if (!isAuthenticated) {
2609
+ return;
2610
+ }
2611
+ if (sources != null) {
2612
+ selectedTimelineSources = sources
2613
+ .map((value) => value.trim().toLowerCase())
2614
+ .where((value) => value.isNotEmpty)
2615
+ .toSet();
2616
+ }
2617
+ isRefreshingTimeline = true;
2618
+ if (notify) {
2619
+ notifyListeners();
2620
+ }
2621
+ try {
2622
+ final response = await _backendClient.fetchTimeline(
2623
+ backendUrl,
2624
+ sources: selectedTimelineSources,
2625
+ limit: 50,
2626
+ );
2627
+ timelineItems = _decodeModelList(
2628
+ 'timeline',
2629
+ response['items'],
2630
+ TimelineEventItem.fromJson,
2631
+ fallbackToMapValues: true,
2632
+ );
2633
+ } catch (error) {
2634
+ errorMessage = _friendlyErrorMessage(error);
2635
+ } finally {
2636
+ isRefreshingTimeline = false;
2637
+ if (notify) {
2638
+ notifyListeners();
2639
+ }
2640
+ }
2641
+ }
2642
+
2643
+ Future<void> toggleTimelineSource(String sourceKind) async {
2644
+ final normalized = sourceKind.trim().toLowerCase();
2645
+ if (normalized.isEmpty) {
2646
+ return;
2647
+ }
2648
+ final next = <String>{...selectedTimelineSources};
2649
+ if (next.contains(normalized)) {
2650
+ if (next.length == 1) {
2651
+ return;
2652
+ }
2653
+ next.remove(normalized);
2654
+ } else {
2655
+ next.add(normalized);
2656
+ }
2657
+ await refreshTimeline(sources: next);
2658
+ }
2659
+
2544
2660
  Future<void> refreshMessaging() async {
2545
2661
  try {
2546
2662
  final statuses = await _backendClient.fetchMessagingStatus(
@@ -3875,6 +3991,19 @@ class NeoAgentController extends ChangeNotifier {
3875
3991
  }
3876
3992
  }
3877
3993
 
3994
+ Future<void> setDesktopPassiveHistoryEnabled(bool value) async {
3995
+ final prefs = _prefs;
3996
+ if (prefs == null) {
3997
+ return;
3998
+ }
3999
+ try {
4000
+ await _desktopCompanion.setPassiveHistoryEnabled(value, prefs);
4001
+ } catch (error) {
4002
+ errorMessage = _friendlyErrorMessage(error);
4003
+ notifyListeners();
4004
+ }
4005
+ }
4006
+
3878
4007
  Future<void> rotateDesktopCompanionIdentity() async {
3879
4008
  final prefs = _prefs;
3880
4009
  if (prefs == null) {
@@ -4992,12 +5121,13 @@ class NeoAgentController extends ChangeNotifier {
4992
5121
  _backendClient.getBillingPlans(backendUrl),
4993
5122
  _backendClient.getBillingInvoices(backendUrl),
4994
5123
  ]);
4995
- billingSubscription =
4996
- results[0]['subscription'] as Map<String, dynamic>?;
4997
- billingPlans = _asDynList(results[1]['plans'])
4998
- .cast<Map<String, dynamic>>();
4999
- billingInvoices = _asDynList(results[2]['invoices'])
5000
- .cast<Map<String, dynamic>>();
5124
+ billingSubscription = results[0]['subscription'] as Map<String, dynamic>?;
5125
+ billingPlans = _asDynList(
5126
+ results[1]['plans'],
5127
+ ).cast<Map<String, dynamic>>();
5128
+ billingInvoices = _asDynList(
5129
+ results[2]['invoices'],
5130
+ ).cast<Map<String, dynamic>>();
5001
5131
  } catch (_) {
5002
5132
  // retain previous data on error
5003
5133
  } finally {
@@ -7001,6 +7131,9 @@ class NeoAgentController extends ChangeNotifier {
7001
7131
  }
7002
7132
  unawaited(_refreshRecordingSessionById(sessionId));
7003
7133
  });
7134
+ socket.on('timeline:updated', (dynamic _) {
7135
+ unawaited(refreshTimeline());
7136
+ });
7004
7137
  socket.on('voice:session_ready', (dynamic data) {
7005
7138
  final payload = _jsonMap(data);
7006
7139
  voiceAssistantLiveState = voiceAssistantLiveState.copyWith(