neoagent 2.3.1-beta.62 → 2.3.1-beta.64

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 (45) hide show
  1. package/.env.example +9 -0
  2. package/docs/configuration.md +11 -0
  3. package/flutter_app/lib/main.dart +2 -0
  4. package/flutter_app/lib/main_account_settings.dart +50 -22
  5. package/flutter_app/lib/main_admin.dart +24 -10
  6. package/flutter_app/lib/main_app_shell.dart +10 -9
  7. package/flutter_app/lib/main_chat.dart +433 -309
  8. package/flutter_app/lib/main_controller.dart +164 -6
  9. package/flutter_app/lib/main_integrations.dart +15 -7
  10. package/flutter_app/lib/main_models.dart +116 -7
  11. package/flutter_app/lib/main_navigation.dart +27 -18
  12. package/flutter_app/lib/main_operations.dart +162 -91
  13. package/flutter_app/lib/main_runtime.dart +22 -0
  14. package/flutter_app/lib/main_settings.dart +287 -75
  15. package/flutter_app/lib/main_shared.dart +165 -6
  16. package/flutter_app/lib/main_unified.dart +388 -0
  17. package/flutter_app/lib/src/analytics_service.dart +294 -0
  18. package/flutter_app/lib/src/backend_client.dart +4 -0
  19. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +2 -0
  20. package/flutter_app/pubspec.lock +8 -0
  21. package/flutter_app/pubspec.yaml +1 -0
  22. package/flutter_app/web/index.html +1 -0
  23. package/package.json +1 -1
  24. package/server/config/analytics.js +30 -0
  25. package/server/db/database.js +52 -0
  26. package/server/http/routes.js +1 -0
  27. package/server/public/.last_build_id +1 -1
  28. package/server/public/assets/AssetManifest.bin +1 -1
  29. package/server/public/assets/AssetManifest.bin.json +1 -1
  30. package/server/public/assets/NOTICES +183 -0
  31. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  32. package/server/public/assets/packages/mixpanel_flutter/assets/mixpanel.js +3 -0
  33. package/server/public/flutter_bootstrap.js +1 -1
  34. package/server/public/index.html +1 -0
  35. package/server/public/main.dart.js +83685 -82046
  36. package/server/routes/agents.js +2 -1
  37. package/server/routes/memory.js +75 -3
  38. package/server/routes/runtime.js +14 -0
  39. package/server/routes/widgets.js +4 -4
  40. package/server/services/ai/engine.js +86 -0
  41. package/server/services/ai/runEvents.js +100 -0
  42. package/server/services/memory/manager.js +242 -26
  43. package/server/services/websocket.js +3 -1
  44. package/server/services/widgets/focus_widget.js +126 -0
  45. package/server/services/widgets/service.js +130 -2
@@ -0,0 +1,388 @@
1
+ part of 'main.dart';
2
+
3
+ enum _ToolsPageTab { integrations, mcp, skills }
4
+
5
+ enum _RunsPageTab { runs, logs }
6
+
7
+ enum _SettingsWorkspaceSection { app, account, security }
8
+
9
+ class ToolsPanel extends StatefulWidget {
10
+ const ToolsPanel({super.key, required this.controller});
11
+
12
+ final NeoAgentController controller;
13
+
14
+ @override
15
+ State<ToolsPanel> createState() => _ToolsPanelState();
16
+ }
17
+
18
+ class _ToolsPanelState extends State<ToolsPanel>
19
+ with SingleTickerProviderStateMixin {
20
+ late final TabController _tabController;
21
+
22
+ @override
23
+ void initState() {
24
+ super.initState();
25
+ _tabController = TabController(
26
+ length: _ToolsPageTab.values.length,
27
+ vsync: this,
28
+ initialIndex: _tabForSection(widget.controller.selectedSection).index,
29
+ );
30
+ }
31
+
32
+ @override
33
+ void didUpdateWidget(covariant ToolsPanel oldWidget) {
34
+ super.didUpdateWidget(oldWidget);
35
+ final selectedSection = widget.controller.selectedSection;
36
+ if (selectedSection != oldWidget.controller.selectedSection &&
37
+ (selectedSection == AppSection.integrations ||
38
+ selectedSection == AppSection.mcp ||
39
+ selectedSection == AppSection.skills)) {
40
+ final targetIndex = _tabForSection(selectedSection).index;
41
+ if (_tabController.index != targetIndex) {
42
+ _tabController.index = targetIndex;
43
+ }
44
+ }
45
+ }
46
+
47
+ @override
48
+ void dispose() {
49
+ _tabController.dispose();
50
+ super.dispose();
51
+ }
52
+
53
+ _ToolsPageTab _tabForSection(AppSection section) {
54
+ switch (section) {
55
+ case AppSection.mcp:
56
+ return _ToolsPageTab.mcp;
57
+ case AppSection.skills:
58
+ return _ToolsPageTab.skills;
59
+ default:
60
+ return _ToolsPageTab.integrations;
61
+ }
62
+ }
63
+
64
+ @override
65
+ Widget build(BuildContext context) {
66
+ final controller = widget.controller;
67
+ final visibleIntegrations = controller.officialIntegrations
68
+ .where(
69
+ (item) =>
70
+ item.env.configured ||
71
+ item.env.setupMode == 'user' ||
72
+ item.isConnected,
73
+ )
74
+ .length;
75
+ return Padding(
76
+ padding: _pagePadding(context),
77
+ child: Column(
78
+ children: <Widget>[
79
+ const _PageTitle(
80
+ title: 'Tools',
81
+ subtitle:
82
+ 'Manage official integrations, MCP servers, and reusable skills in one place.',
83
+ ),
84
+ const SizedBox(height: 12),
85
+ Container(
86
+ decoration: BoxDecoration(
87
+ color: _bgSecondary,
88
+ borderRadius: BorderRadius.circular(14),
89
+ border: Border.all(color: _border),
90
+ ),
91
+ child: TabBar(
92
+ controller: _tabController,
93
+ dividerColor: Colors.transparent,
94
+ indicatorSize: TabBarIndicatorSize.tab,
95
+ labelStyle: const TextStyle(fontWeight: FontWeight.w700),
96
+ tabs: <Widget>[
97
+ Tab(text: 'Integrations ($visibleIntegrations)'),
98
+ Tab(text: 'MCP (${controller.mcpServers.length})'),
99
+ Tab(text: 'Skills (${controller.skills.length})'),
100
+ ],
101
+ ),
102
+ ),
103
+ const SizedBox(height: 12),
104
+ Expanded(
105
+ child: TabBarView(
106
+ controller: _tabController,
107
+ children: <Widget>[
108
+ IntegrationsPanel(controller: controller, embedded: true),
109
+ McpPanel(controller: controller, embedded: true),
110
+ SkillsPanel(controller: controller, embedded: true),
111
+ ],
112
+ ),
113
+ ),
114
+ ],
115
+ ),
116
+ );
117
+ }
118
+ }
119
+
120
+ class RunsAndLogsPanel extends StatefulWidget {
121
+ const RunsAndLogsPanel({super.key, required this.controller});
122
+
123
+ final NeoAgentController controller;
124
+
125
+ @override
126
+ State<RunsAndLogsPanel> createState() => _RunsAndLogsPanelState();
127
+ }
128
+
129
+ class _RunsAndLogsPanelState extends State<RunsAndLogsPanel>
130
+ with SingleTickerProviderStateMixin {
131
+ late final TabController _tabController;
132
+
133
+ @override
134
+ void initState() {
135
+ super.initState();
136
+ _tabController = TabController(
137
+ length: _RunsPageTab.values.length,
138
+ vsync: this,
139
+ initialIndex: _tabForSection(widget.controller.selectedSection).index,
140
+ );
141
+ }
142
+
143
+ @override
144
+ void didUpdateWidget(covariant RunsAndLogsPanel oldWidget) {
145
+ super.didUpdateWidget(oldWidget);
146
+ final selectedSection = widget.controller.selectedSection;
147
+ if (selectedSection != oldWidget.controller.selectedSection &&
148
+ (selectedSection == AppSection.runs ||
149
+ selectedSection == AppSection.logs)) {
150
+ final targetIndex = _tabForSection(selectedSection).index;
151
+ if (_tabController.index != targetIndex) {
152
+ _tabController.index = targetIndex;
153
+ }
154
+ }
155
+ }
156
+
157
+ @override
158
+ void dispose() {
159
+ _tabController.dispose();
160
+ super.dispose();
161
+ }
162
+
163
+ _RunsPageTab _tabForSection(AppSection section) {
164
+ switch (section) {
165
+ case AppSection.logs:
166
+ return _RunsPageTab.logs;
167
+ default:
168
+ return _RunsPageTab.runs;
169
+ }
170
+ }
171
+
172
+ @override
173
+ Widget build(BuildContext context) {
174
+ final controller = widget.controller;
175
+ return Padding(
176
+ padding: _pagePadding(context),
177
+ child: Column(
178
+ children: <Widget>[
179
+ const _PageTitle(
180
+ title: 'Runs & Logs',
181
+ subtitle:
182
+ 'Inspect execution history, failures, tool traces, and diagnostics from one workspace.',
183
+ ),
184
+ const SizedBox(height: 12),
185
+ Container(
186
+ decoration: BoxDecoration(
187
+ color: _bgSecondary,
188
+ borderRadius: BorderRadius.circular(14),
189
+ border: Border.all(color: _border),
190
+ ),
191
+ child: TabBar(
192
+ controller: _tabController,
193
+ dividerColor: Colors.transparent,
194
+ indicatorSize: TabBarIndicatorSize.tab,
195
+ labelStyle: const TextStyle(fontWeight: FontWeight.w700),
196
+ tabs: <Widget>[
197
+ Tab(text: 'Runs (${controller.recentRuns.length})'),
198
+ Tab(text: 'Logs (${controller.logs.length})'),
199
+ ],
200
+ ),
201
+ ),
202
+ const SizedBox(height: 12),
203
+ Expanded(
204
+ child: TabBarView(
205
+ controller: _tabController,
206
+ children: <Widget>[
207
+ RunsPanel(controller: controller, embedded: true),
208
+ LogsPanel(controller: controller, embedded: true),
209
+ ],
210
+ ),
211
+ ),
212
+ ],
213
+ ),
214
+ );
215
+ }
216
+ }
217
+
218
+ class SettingsWorkspacePanel extends StatefulWidget {
219
+ const SettingsWorkspacePanel({super.key, required this.controller});
220
+
221
+ final NeoAgentController controller;
222
+
223
+ @override
224
+ State<SettingsWorkspacePanel> createState() => _SettingsWorkspacePanelState();
225
+ }
226
+
227
+ class _SettingsWorkspacePanelState extends State<SettingsWorkspacePanel> {
228
+ late _SettingsWorkspaceSection _selectedSection;
229
+
230
+ @override
231
+ void initState() {
232
+ super.initState();
233
+ _selectedSection =
234
+ widget.controller.selectedSection == AppSection.accountSettings
235
+ ? _SettingsWorkspaceSection.account
236
+ : _SettingsWorkspaceSection.app;
237
+ }
238
+
239
+ @override
240
+ void didUpdateWidget(covariant SettingsWorkspacePanel oldWidget) {
241
+ super.didUpdateWidget(oldWidget);
242
+ if (widget.controller.selectedSection == AppSection.settings &&
243
+ _selectedSection != _SettingsWorkspaceSection.app) {
244
+ _selectedSection = _SettingsWorkspaceSection.app;
245
+ }
246
+ if (widget.controller.selectedSection == AppSection.accountSettings &&
247
+ _selectedSection == _SettingsWorkspaceSection.app) {
248
+ _selectedSection = _SettingsWorkspaceSection.account;
249
+ }
250
+ }
251
+
252
+ @override
253
+ Widget build(BuildContext context) {
254
+ final compact = MediaQuery.sizeOf(context).width < 960;
255
+ return Padding(
256
+ padding: _pagePadding(context),
257
+ child: Column(
258
+ children: <Widget>[
259
+ const _PageTitle(
260
+ title: 'Settings',
261
+ subtitle:
262
+ 'Workspace configuration and account security in one place.',
263
+ ),
264
+ const SizedBox(height: 12),
265
+ Expanded(
266
+ child: Card(
267
+ child: Padding(
268
+ padding: const EdgeInsets.all(20),
269
+ child: compact
270
+ ? Column(
271
+ crossAxisAlignment: CrossAxisAlignment.start,
272
+ children: <Widget>[
273
+ _SettingsWorkspaceNav(
274
+ selected: _selectedSection,
275
+ compact: true,
276
+ onSelected: _selectSection,
277
+ ),
278
+ const SizedBox(height: 16),
279
+ Expanded(child: _buildContent()),
280
+ ],
281
+ )
282
+ : Row(
283
+ crossAxisAlignment: CrossAxisAlignment.start,
284
+ children: <Widget>[
285
+ SizedBox(
286
+ width: 220,
287
+ child: _SettingsWorkspaceNav(
288
+ selected: _selectedSection,
289
+ compact: false,
290
+ onSelected: _selectSection,
291
+ ),
292
+ ),
293
+ const SizedBox(width: 24),
294
+ Expanded(child: _buildContent()),
295
+ ],
296
+ ),
297
+ ),
298
+ ),
299
+ ),
300
+ ],
301
+ ),
302
+ );
303
+ }
304
+
305
+ void _selectSection(_SettingsWorkspaceSection section) {
306
+ setState(() => _selectedSection = section);
307
+ if (section == _SettingsWorkspaceSection.app) {
308
+ widget.controller.setSelectedSection(AppSection.settings);
309
+ return;
310
+ }
311
+ widget.controller.setSelectedSection(AppSection.accountSettings);
312
+ }
313
+
314
+ Widget _buildContent() {
315
+ switch (_selectedSection) {
316
+ case _SettingsWorkspaceSection.app:
317
+ return SettingsPanel(controller: widget.controller, embedded: true);
318
+ case _SettingsWorkspaceSection.account:
319
+ return AccountSettingsPanel(
320
+ controller: widget.controller,
321
+ embedded: true,
322
+ initialTab: AccountSettingsTab.account,
323
+ );
324
+ case _SettingsWorkspaceSection.security:
325
+ return AccountSettingsPanel(
326
+ controller: widget.controller,
327
+ embedded: true,
328
+ initialTab: AccountSettingsTab.security,
329
+ );
330
+ }
331
+ }
332
+ }
333
+
334
+ class _SettingsWorkspaceNav extends StatelessWidget {
335
+ const _SettingsWorkspaceNav({
336
+ required this.selected,
337
+ required this.compact,
338
+ required this.onSelected,
339
+ });
340
+
341
+ final _SettingsWorkspaceSection selected;
342
+ final bool compact;
343
+ final ValueChanged<_SettingsWorkspaceSection> onSelected;
344
+
345
+ @override
346
+ Widget build(BuildContext context) {
347
+ final items = <Widget>[
348
+ _navButton(
349
+ section: _SettingsWorkspaceSection.app,
350
+ icon: Icons.tune,
351
+ label: 'App Settings',
352
+ ),
353
+ _navButton(
354
+ section: _SettingsWorkspaceSection.account,
355
+ icon: Icons.person_outline,
356
+ label: 'Account',
357
+ ),
358
+ _navButton(
359
+ section: _SettingsWorkspaceSection.security,
360
+ icon: Icons.security_outlined,
361
+ label: 'Security',
362
+ ),
363
+ ];
364
+ return compact
365
+ ? Wrap(spacing: 8, runSpacing: 8, children: items)
366
+ : Column(
367
+ crossAxisAlignment: CrossAxisAlignment.stretch,
368
+ children: items,
369
+ );
370
+ }
371
+
372
+ Widget _navButton({
373
+ required _SettingsWorkspaceSection section,
374
+ required IconData icon,
375
+ required String label,
376
+ }) {
377
+ final button = _SidebarButton(
378
+ label: label,
379
+ icon: icon,
380
+ active: selected == section,
381
+ onTap: () => onSelected(section),
382
+ );
383
+ if (compact) {
384
+ return button;
385
+ }
386
+ return Padding(padding: const EdgeInsets.only(bottom: 8), child: button);
387
+ }
388
+ }
@@ -0,0 +1,294 @@
1
+ import 'package:mixpanel_flutter/mixpanel_flutter.dart';
2
+
3
+ class AppAnalytics {
4
+ Mixpanel? _mixpanel;
5
+ bool _initialized = false;
6
+ bool _enabled = false;
7
+ String? _currentToken;
8
+ bool _consentGranted = false;
9
+ final List<_QueuedEvent> _queue = <_QueuedEvent>[];
10
+
11
+ bool get enabled => _enabled && _consentGranted;
12
+ bool get consentGranted => _consentGranted;
13
+ bool get isConfigured => (_currentToken ?? '').isNotEmpty;
14
+
15
+ Future<void> initialize({
16
+ required String? token,
17
+ required bool consentGranted,
18
+ }) async {
19
+ final normalizedToken = token?.trim() ?? '';
20
+ final shouldEnable = normalizedToken.isNotEmpty;
21
+ if (_initialized &&
22
+ normalizedToken == _currentToken &&
23
+ shouldEnable == _enabled &&
24
+ consentGranted == _consentGranted) {
25
+ return;
26
+ }
27
+
28
+ _consentGranted = consentGranted;
29
+ _initialized = false;
30
+ _enabled = false;
31
+ _currentToken = normalizedToken.isEmpty ? null : normalizedToken;
32
+ _mixpanel = null;
33
+
34
+ if (!shouldEnable) {
35
+ _queue.clear();
36
+ _initialized = true;
37
+ return;
38
+ }
39
+
40
+ if (!consentGranted) {
41
+ _queue.clear();
42
+ _initialized = true;
43
+ return;
44
+ }
45
+
46
+ try {
47
+ _mixpanel = await Mixpanel.init(
48
+ normalizedToken,
49
+ trackAutomaticEvents: false,
50
+ );
51
+ _enabled = true;
52
+ _initialized = true;
53
+ await _flushQueue();
54
+ } catch (_) {
55
+ _queue.clear();
56
+ _mixpanel = null;
57
+ _enabled = false;
58
+ _initialized = true;
59
+ }
60
+ }
61
+
62
+ Future<void> setConsentGranted(bool consentGranted) async {
63
+ if (_consentGranted == consentGranted && _initialized) {
64
+ return;
65
+ }
66
+
67
+ _consentGranted = consentGranted;
68
+ if (!consentGranted) {
69
+ _mixpanel = null;
70
+ _enabled = false;
71
+ _initialized = true;
72
+ _queue.clear();
73
+ return;
74
+ }
75
+
76
+ await initialize(token: _currentToken, consentGranted: true);
77
+ }
78
+
79
+ Future<void> track(
80
+ String eventName, {
81
+ Map<String, Object?> properties = const <String, Object?>{},
82
+ }) async {
83
+ final event = _QueuedEvent(
84
+ eventName: eventName,
85
+ properties: _cleanProperties(properties),
86
+ );
87
+
88
+ if (!_initialized) {
89
+ _queue.add(event);
90
+ return;
91
+ }
92
+
93
+ if (!enabled || _mixpanel == null) {
94
+ return;
95
+ }
96
+
97
+ await _sendEvent(event);
98
+ }
99
+
100
+ Future<void> trackAppOpened({
101
+ required String appMode,
102
+ required String platform,
103
+ required String backendMode,
104
+ required String selectedSection,
105
+ required String deploymentProfile,
106
+ required bool authenticated,
107
+ }) {
108
+ return track(
109
+ 'app_opened',
110
+ properties: <String, Object?>{
111
+ 'app_mode': appMode,
112
+ 'platform': platform,
113
+ 'backend_mode': backendMode,
114
+ 'selected_section': selectedSection,
115
+ 'deployment_profile': deploymentProfile,
116
+ 'authenticated': authenticated,
117
+ },
118
+ );
119
+ }
120
+
121
+ Future<void> trackBackendUrlSaved({
122
+ required String backendMode,
123
+ }) {
124
+ return track(
125
+ 'backend_url_saved',
126
+ properties: <String, Object?>{
127
+ 'backend_mode': backendMode,
128
+ },
129
+ );
130
+ }
131
+
132
+ Future<void> trackSectionChanged({
133
+ required String section,
134
+ required String previousSection,
135
+ }) {
136
+ return track(
137
+ 'section_changed',
138
+ properties: <String, Object?>{
139
+ 'section': section,
140
+ 'previous_section': previousSection,
141
+ },
142
+ );
143
+ }
144
+
145
+ Future<void> trackChatMessageSent({
146
+ required int length,
147
+ required bool steeringLiveRun,
148
+ }) {
149
+ return track(
150
+ 'chat_message_sent',
151
+ properties: <String, Object?>{
152
+ 'length': length,
153
+ 'steering_live_run': steeringLiveRun,
154
+ },
155
+ );
156
+ }
157
+
158
+ Future<void> trackRecordingStarted({
159
+ required String kind,
160
+ }) {
161
+ return track(
162
+ 'recording_started',
163
+ properties: <String, Object?>{
164
+ 'kind': kind,
165
+ },
166
+ );
167
+ }
168
+
169
+ Future<void> trackRecordingStopped({
170
+ required String kind,
171
+ required String stopReason,
172
+ }) {
173
+ return track(
174
+ 'recording_stopped',
175
+ properties: <String, Object?>{
176
+ 'kind': kind,
177
+ 'stop_reason': stopReason,
178
+ },
179
+ );
180
+ }
181
+
182
+ Future<void> trackAppUpdateCheck({
183
+ required bool silent,
184
+ }) {
185
+ return track(
186
+ 'app_update_check',
187
+ properties: <String, Object?>{
188
+ 'silent': silent,
189
+ },
190
+ );
191
+ }
192
+
193
+ Future<void> trackTaskRunRequested({
194
+ required int taskId,
195
+ }) {
196
+ return track(
197
+ 'task_run_requested',
198
+ properties: <String, Object?>{
199
+ 'task_id': taskId,
200
+ },
201
+ );
202
+ }
203
+
204
+ Future<void> trackWidgetRefreshRequested({
205
+ required bool all,
206
+ }) {
207
+ return track(
208
+ 'widget_refresh_requested',
209
+ properties: <String, Object?>{
210
+ 'all': all,
211
+ },
212
+ );
213
+ }
214
+
215
+ Future<void> trackAppUpdateTriggered() {
216
+ return track('app_update_triggered');
217
+ }
218
+
219
+ Future<void> trackSignedIn({
220
+ required String authMethod,
221
+ required bool isRegistration,
222
+ }) {
223
+ return track(
224
+ 'signed_in',
225
+ properties: <String, Object?>{
226
+ 'auth_method': authMethod,
227
+ 'is_registration': isRegistration,
228
+ },
229
+ );
230
+ }
231
+
232
+ Future<void> trackSignedOut() {
233
+ return track('signed_out');
234
+ }
235
+
236
+ Future<void> trackOnboardingDismissed() {
237
+ return track('onboarding_dismissed');
238
+ }
239
+
240
+ Future<void> dispose() async {
241
+ try {
242
+ await _flushQueue();
243
+ } catch (_) {}
244
+ _queue.clear();
245
+ _mixpanel = null;
246
+ _initialized = false;
247
+ _enabled = false;
248
+ _currentToken = null;
249
+ _consentGranted = false;
250
+ }
251
+
252
+ Future<void> _flushQueue() async {
253
+ if (!enabled || _mixpanel == null || _queue.isEmpty) {
254
+ return;
255
+ }
256
+
257
+ final pending = List<_QueuedEvent>.from(_queue);
258
+ _queue.clear();
259
+ for (final event in pending) {
260
+ await _sendEvent(event);
261
+ }
262
+ }
263
+
264
+ Future<void> _sendEvent(_QueuedEvent event) async {
265
+ final mixpanel = _mixpanel;
266
+ if (mixpanel == null) {
267
+ return;
268
+ }
269
+
270
+ try {
271
+ await mixpanel.track(event.eventName, properties: event.properties);
272
+ } catch (_) {}
273
+ }
274
+
275
+ Map<String, Object?> _cleanProperties(Map<String, Object?> properties) {
276
+ final cleaned = <String, Object?>{};
277
+ for (final entry in properties.entries) {
278
+ final value = entry.value;
279
+ if (value == null) continue;
280
+ cleaned[entry.key] = value;
281
+ }
282
+ return cleaned;
283
+ }
284
+ }
285
+
286
+ class _QueuedEvent {
287
+ const _QueuedEvent({
288
+ required this.eventName,
289
+ required this.properties,
290
+ });
291
+
292
+ final String eventName;
293
+ final Map<String, Object?> properties;
294
+ }
@@ -463,6 +463,10 @@ class BackendClient {
463
463
  return getMap(baseUrl, '/api/version');
464
464
  }
465
465
 
466
+ Future<Map<String, dynamic>> fetchRuntimeConfig(String baseUrl) async {
467
+ return getMap(baseUrl, '/api/runtime/config', allowUnauthorized: true);
468
+ }
469
+
466
470
  Future<Map<String, dynamic>> fetchBrowserStatus(String baseUrl) async {
467
471
  return getMap(baseUrl, '/api/browser/status');
468
472
  }
@@ -11,6 +11,7 @@ import desktop_audio_capture
11
11
  import flutter_secure_storage_macos
12
12
  import geolocator_apple
13
13
  import hotkey_manager_macos
14
+ import mixpanel_flutter
14
15
  import mobile_scanner
15
16
  import package_info_plus
16
17
  import path_provider_foundation
@@ -30,6 +31,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
30
31
  FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
31
32
  GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
32
33
  HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
34
+ MixpanelFlutterPlugin.register(with: registry.registrar(forPlugin: "MixpanelFlutterPlugin"))
33
35
  MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
34
36
  FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
35
37
  PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))