neoagent 2.4.1-beta.27 → 2.4.1-beta.29

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.
@@ -189,8 +189,8 @@ class OfficialIntegrationsTab extends StatelessWidget {
189
189
  ? item.env.summary
190
190
  : item.hasExpiredAccounts
191
191
  ? item.id == 'google_workspace'
192
- ? 'One or more accounts expired. Reconnect to restore access. If this keeps happening, your Google Cloud OAuth app may be in Testing mode — publish it to Production in Google Cloud Console to get long-lived tokens.'
193
- : 'One or more accounts expired. Reconnect the affected account to restore tool access.'
192
+ ? 'One or more accounts expired. Reconnect to restore access. If this keeps happening, your Google Cloud OAuth app may be in Testing mode — publish it to Production in Google Cloud Console to get long-lived tokens.'
193
+ : 'One or more accounts expired. Reconnect the affected account to restore tool access.'
194
194
  : !item.supportsMultipleAccounts && item.isConnected
195
195
  ? 'This integration currently supports one connected account per agent. Re-open setup to replace it.'
196
196
  : item.isConnected
@@ -228,12 +228,255 @@ void _openOfficialIntegrationSetupDialog(
228
228
  String providerId,
229
229
  ) {
230
230
  switch (providerId) {
231
+ case 'home_assistant':
232
+ _showHomeAssistantSetupDialog(context, controller);
233
+ return;
231
234
  case 'trello':
232
235
  _showTrelloSetupDialog(context, controller);
233
236
  return;
234
237
  }
235
238
  }
236
239
 
240
+ Future<void> _showHomeAssistantSetupDialog(
241
+ BuildContext context,
242
+ NeoAgentController controller,
243
+ ) async {
244
+ Map<String, dynamic> existing;
245
+ try {
246
+ existing = await controller.getOfficialIntegrationConfig('home_assistant');
247
+ } catch (error) {
248
+ if (context.mounted) {
249
+ ScaffoldMessenger.of(context).showSnackBar(
250
+ SnackBar(content: Text(controller.errorMessage ?? error.toString())),
251
+ );
252
+ }
253
+ return;
254
+ }
255
+
256
+ final savedBaseUrl = existing['baseUrl']?.toString() ?? '';
257
+ final hasToken = existing['hasToken'] == true;
258
+ final accountCount = (existing['accountCount'] as num?)?.toInt() ?? 0;
259
+ final hasConnectedAccount =
260
+ existing['hasConnectedAccount'] == true || accountCount > 0;
261
+ var formError = '';
262
+ var saving = false;
263
+
264
+ final baseUrlController = TextEditingController(text: savedBaseUrl);
265
+ final tokenController = TextEditingController();
266
+
267
+ if (!context.mounted) return;
268
+ await showDialog<void>(
269
+ context: context,
270
+ barrierDismissible: false,
271
+ builder: (dialogContext) {
272
+ return StatefulBuilder(
273
+ builder: (dialogContext, setState) {
274
+ return AlertDialog(
275
+ title: const Text('Home Assistant Setup'),
276
+ content: SizedBox(
277
+ width: 520,
278
+ child: Column(
279
+ mainAxisSize: MainAxisSize.min,
280
+ crossAxisAlignment: CrossAxisAlignment.start,
281
+ children: <Widget>[
282
+ Text(
283
+ 'Connect a public HTTPS Home Assistant endpoint with a Long-Lived Access Token. Local, loopback, and private network addresses are blocked by the server.',
284
+ style: TextStyle(color: _textSecondary),
285
+ ),
286
+ const SizedBox(height: 16),
287
+ _IntegrationSetupStatusItem(
288
+ label: 'Endpoint',
289
+ status: savedBaseUrl.trim().isNotEmpty
290
+ ? 'Configured'
291
+ : 'Not configured',
292
+ isConnected: savedBaseUrl.trim().isNotEmpty,
293
+ ),
294
+ const SizedBox(height: 12),
295
+ _IntegrationSetupStatusItem(
296
+ label: 'Connected Instance',
297
+ status: hasConnectedAccount
298
+ ? '$accountCount ${accountCount == 1 ? 'instance' : 'instances'} connected'
299
+ : 'Not connected',
300
+ isConnected: hasConnectedAccount,
301
+ ),
302
+ const SizedBox(height: 12),
303
+ TextField(
304
+ controller: baseUrlController,
305
+ onChanged: (_) => setState(() {}),
306
+ keyboardType: TextInputType.url,
307
+ decoration: const InputDecoration(
308
+ labelText: 'Home Assistant URL',
309
+ hintText: 'https://your-instance.example.com',
310
+ border: OutlineInputBorder(),
311
+ ),
312
+ ),
313
+ const SizedBox(height: 12),
314
+ TextField(
315
+ controller: tokenController,
316
+ onChanged: (_) => setState(() {}),
317
+ obscureText: true,
318
+ decoration: InputDecoration(
319
+ labelText: hasToken
320
+ ? 'Paste replacement Long-Lived Access Token'
321
+ : 'Long-Lived Access Token',
322
+ border: const OutlineInputBorder(),
323
+ ),
324
+ ),
325
+ if (hasToken) ...<Widget>[
326
+ const SizedBox(height: 8),
327
+ Text(
328
+ 'Leave the token empty to keep the currently stored token.',
329
+ style: TextStyle(color: _textSecondary, fontSize: 12),
330
+ ),
331
+ ],
332
+ if (formError.isNotEmpty) ...<Widget>[
333
+ const SizedBox(height: 12),
334
+ Container(
335
+ padding: const EdgeInsets.all(8),
336
+ decoration: BoxDecoration(
337
+ color: _danger.withValues(alpha: 0.1),
338
+ borderRadius: BorderRadius.circular(4),
339
+ border: Border.all(
340
+ color: _danger.withValues(alpha: 0.3),
341
+ ),
342
+ ),
343
+ child: Text(
344
+ formError,
345
+ style: TextStyle(color: _danger, fontSize: 12),
346
+ ),
347
+ ),
348
+ ],
349
+ ],
350
+ ),
351
+ ),
352
+ actions: <Widget>[
353
+ if (savedBaseUrl.trim().isNotEmpty || hasToken)
354
+ TextButton(
355
+ onPressed: saving
356
+ ? null
357
+ : () async {
358
+ final shouldClear =
359
+ await showDialog<bool>(
360
+ context: dialogContext,
361
+ builder: (context) {
362
+ return AlertDialog(
363
+ title: const Text(
364
+ 'Disconnect Home Assistant?',
365
+ ),
366
+ content: const Text(
367
+ 'This removes the Home Assistant setup and connected instance for this agent.',
368
+ ),
369
+ actions: [
370
+ TextButton(
371
+ onPressed: () =>
372
+ Navigator.of(context).pop(false),
373
+ child: const Text('Cancel'),
374
+ ),
375
+ FilledButton(
376
+ onPressed: () =>
377
+ Navigator.of(context).pop(true),
378
+ child: const Text('Disconnect'),
379
+ ),
380
+ ],
381
+ );
382
+ },
383
+ ) ??
384
+ false;
385
+ if (!shouldClear) {
386
+ return;
387
+ }
388
+ setState(() {
389
+ formError = '';
390
+ saving = true;
391
+ });
392
+ try {
393
+ await controller.clearOfficialIntegrationConfig(
394
+ 'home_assistant',
395
+ );
396
+ if (dialogContext.mounted) {
397
+ Navigator.of(dialogContext).pop();
398
+ }
399
+ } catch (_) {
400
+ setState(() {
401
+ formError =
402
+ controller.errorMessage ??
403
+ 'Could not disconnect Home Assistant.';
404
+ saving = false;
405
+ });
406
+ }
407
+ },
408
+ child: const Text('Disconnect'),
409
+ ),
410
+ TextButton(
411
+ onPressed: saving
412
+ ? null
413
+ : () => Navigator.of(dialogContext).pop(),
414
+ child: const Text('Close'),
415
+ ),
416
+ FilledButton(
417
+ onPressed: saving
418
+ ? null
419
+ : () async {
420
+ setState(() {
421
+ formError = '';
422
+ saving = true;
423
+ });
424
+ try {
425
+ final baseUrl = baseUrlController.text.trim();
426
+ final token = tokenController.text.trim();
427
+ if (baseUrl.isEmpty) {
428
+ setState(() {
429
+ formError = 'Home Assistant URL is required.';
430
+ saving = false;
431
+ });
432
+ return;
433
+ }
434
+ if (token.isEmpty && !hasToken) {
435
+ setState(() {
436
+ formError =
437
+ 'Home Assistant Long-Lived Access Token is required.';
438
+ saving = false;
439
+ });
440
+ return;
441
+ }
442
+ await controller.saveOfficialIntegrationConfig(
443
+ 'home_assistant',
444
+ config: <String, dynamic>{
445
+ 'baseUrl': baseUrl,
446
+ if (token.isNotEmpty) 'token': token,
447
+ },
448
+ );
449
+ if (dialogContext.mounted) {
450
+ Navigator.of(dialogContext).pop();
451
+ }
452
+ } catch (_) {
453
+ setState(() {
454
+ formError =
455
+ controller.errorMessage ??
456
+ 'Could not save Home Assistant setup.';
457
+ saving = false;
458
+ });
459
+ }
460
+ },
461
+ child: Text(
462
+ saving
463
+ ? 'Saving...'
464
+ : hasConnectedAccount
465
+ ? 'Update Instance'
466
+ : 'Connect Instance',
467
+ ),
468
+ ),
469
+ ],
470
+ );
471
+ },
472
+ );
473
+ },
474
+ );
475
+
476
+ baseUrlController.dispose();
477
+ tokenController.dispose();
478
+ }
479
+
237
480
  Future<void> _showTrelloSetupDialog(
238
481
  BuildContext context,
239
482
  NeoAgentController controller,
@@ -283,13 +526,13 @@ Future<void> _showTrelloSetupDialog(
283
526
  style: TextStyle(color: _textSecondary),
284
527
  ),
285
528
  const SizedBox(height: 16),
286
- _TrelloStatusItem(
529
+ _IntegrationSetupStatusItem(
287
530
  label: 'API Key',
288
531
  status: apiKeyConfigured ? 'Configured' : 'Not configured',
289
532
  isConnected: apiKeyConfigured,
290
533
  ),
291
534
  const SizedBox(height: 12),
292
- _TrelloStatusItem(
535
+ _IntegrationSetupStatusItem(
293
536
  label: 'Connected Account',
294
537
  status: hasConnectedAccount
295
538
  ? '$accountCount ${accountCount == 1 ? 'connected account' : 'connected accounts'}'
@@ -337,7 +580,9 @@ Future<void> _showTrelloSetupDialog(
337
580
  decoration: BoxDecoration(
338
581
  color: _danger.withValues(alpha: 0.1),
339
582
  borderRadius: BorderRadius.circular(4),
340
- border: Border.all(color: _danger.withValues(alpha: 0.3)),
583
+ border: Border.all(
584
+ color: _danger.withValues(alpha: 0.3),
585
+ ),
341
586
  ),
342
587
  child: Text(
343
588
  formError,
@@ -532,8 +777,8 @@ Future<void> _showTrelloSetupDialog(
532
777
  tokenInputController.dispose();
533
778
  }
534
779
 
535
- class _TrelloStatusItem extends StatelessWidget {
536
- const _TrelloStatusItem({
780
+ class _IntegrationSetupStatusItem extends StatelessWidget {
781
+ const _IntegrationSetupStatusItem({
537
782
  required this.label,
538
783
  required this.status,
539
784
  required this.isConnected,
@@ -886,11 +1131,13 @@ class _OfficialIntegrationIcon extends StatelessWidget {
886
1131
  Widget build(BuildContext context) {
887
1132
  final color = switch (item.icon) {
888
1133
  'google' => const Color(0xFF4285F4),
1134
+ 'home_assistant' => const Color(0xFF41BDF5),
889
1135
  'trello' => const Color(0xFF0C66E4),
890
1136
  _ => _accent,
891
1137
  };
892
1138
  final label = switch (item.icon) {
893
1139
  'google' => 'G',
1140
+ 'home_assistant' => 'H',
894
1141
  'trello' => 'T',
895
1142
  _ => item.label.isNotEmpty ? item.label[0] : '?',
896
1143
  };
@@ -1345,8 +1345,8 @@ class _LogoBadgeState extends State<_LogoBadge> {
1345
1345
  ),
1346
1346
  child: Image.asset(
1347
1347
  isDark
1348
- ? 'assets/branding/app_icon_512.png'
1349
- : 'assets/branding/app_icon_light_512.png',
1348
+ ? 'assets/branding/app_icon_1024.png'
1349
+ : 'assets/branding/app_icon_light_1024.png',
1350
1350
  width: widget.size,
1351
1351
  height: widget.size,
1352
1352
  filterQuality: FilterQuality.high,
@@ -695,6 +695,41 @@ class BackendClient {
695
695
  return getMap(baseUrl, '/api/desktop/devices');
696
696
  }
697
697
 
698
+ Future<Map<String, dynamic>> fetchWorkspaceDirectory(
699
+ String baseUrl, {
700
+ String path = '.',
701
+ }) async {
702
+ return getMap(
703
+ baseUrl,
704
+ '/api/workspace/files?path=${Uri.encodeQueryComponent(path)}',
705
+ );
706
+ }
707
+
708
+ Future<Map<String, dynamic>> fetchWorkspaceFile(
709
+ String baseUrl, {
710
+ required String path,
711
+ }) async {
712
+ return getMap(
713
+ baseUrl,
714
+ '/api/workspace/files/content?path=${Uri.encodeQueryComponent(path)}',
715
+ );
716
+ }
717
+
718
+ Future<Map<String, dynamic>> saveWorkspaceFile(
719
+ String baseUrl, {
720
+ required String path,
721
+ required String content,
722
+ }) async {
723
+ return putMap(baseUrl, '/api/workspace/files/content', <String, dynamic>{
724
+ 'path': path,
725
+ 'content': content,
726
+ });
727
+ }
728
+
729
+ String workspaceDownloadPath(String path) {
730
+ return '/api/workspace/files/download?path=${Uri.encodeQueryComponent(path)}';
731
+ }
732
+
698
733
  Future<Map<String, dynamic>> selectDesktopDevice(
699
734
  String baseUrl, {
700
735
  required String deviceId,
@@ -56,8 +56,10 @@ flutter:
56
56
  - web/icons/Icon-192.png
57
57
  - assets/branding/app_icon_256.png
58
58
  - assets/branding/app_icon_512.png
59
+ - assets/branding/app_icon_1024.png
59
60
  - assets/branding/app_icon_light_256.png
60
61
  - assets/branding/app_icon_light_512.png
62
+ - assets/branding/app_icon_light_1024.png
61
63
  - assets/branding/tray_icon_template.png
62
64
  - assets/branding/tray_icon_light_template.png
63
65
  - assets/branding/onboarding_intro.mp4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.4.1-beta.27",
3
+ "version": "2.4.1-beta.29",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
@@ -18,6 +18,7 @@ const routeRegistry = [
18
18
  { basePath: '/api/skills', modulePath: '../routes/skills' },
19
19
  { basePath: '/api/store', modulePath: '../routes/store' },
20
20
  { basePath: '/api/artifacts', modulePath: '../routes/artifacts' },
21
+ { basePath: '/api/workspace', modulePath: '../routes/workspace' },
21
22
  { basePath: '/api/memory', modulePath: '../routes/memory' },
22
23
  { basePath: '/api/tasks', modulePath: '../routes/tasks' },
23
24
  { basePath: '/api/widgets', modulePath: '../routes/widgets' },
@@ -1 +1 @@
1
- 77eaad1940c3818c9ff1374401de40eb
1
+ 7ba231f310e272fc741c3f39d24d2198
@@ -1 +1 @@
1
-
1
+
@@ -1 +1 @@
1
- "DQwHIGFzc2V0cy9icmFuZGluZy9hcHBfaWNvbl8yNTYucG5nDAENAQcFYXNzZXQHIGFzc2V0cy9icmFuZGluZy9hcHBfaWNvbl8yNTYucG5nByBhc3NldHMvYnJhbmRpbmcvYXBwX2ljb25fNTEyLnBuZwwBDQEHBWFzc2V0ByBhc3NldHMvYnJhbmRpbmcvYXBwX2ljb25fNTEyLnBuZwcmYXNzZXRzL2JyYW5kaW5nL2FwcF9pY29uX2xpZ2h0XzI1Ni5wbmcMAQ0BBwVhc3NldAcmYXNzZXRzL2JyYW5kaW5nL2FwcF9pY29uX2xpZ2h0XzI1Ni5wbmcHJmFzc2V0cy9icmFuZGluZy9hcHBfaWNvbl9saWdodF81MTIucG5nDAENAQcFYXNzZXQHJmFzc2V0cy9icmFuZGluZy9hcHBfaWNvbl9saWdodF81MTIucG5nByRhc3NldHMvYnJhbmRpbmcvb25ib2FyZGluZ19pbnRyby5tcDQMAQ0BBwVhc3NldAckYXNzZXRzL2JyYW5kaW5nL29uYm9hcmRpbmdfaW50cm8ubXA0Byxhc3NldHMvYnJhbmRpbmcvdHJheV9pY29uX2xpZ2h0X3RlbXBsYXRlLnBuZwwBDQEHBWFzc2V0Byxhc3NldHMvYnJhbmRpbmcvdHJheV9pY29uX2xpZ2h0X3RlbXBsYXRlLnBuZwcmYXNzZXRzL2JyYW5kaW5nL3RyYXlfaWNvbl90ZW1wbGF0ZS5wbmcMAQ0BBwVhc3NldAcmYXNzZXRzL2JyYW5kaW5nL3RyYXlfaWNvbl90ZW1wbGF0ZS5wbmcHMnBhY2thZ2VzL2N1cGVydGlub19pY29ucy9hc3NldHMvQ3VwZXJ0aW5vSWNvbnMudHRmDAENAQcFYXNzZXQHMnBhY2thZ2VzL2N1cGVydGlub19pY29ucy9hc3NldHMvQ3VwZXJ0aW5vSWNvbnMudHRmByxwYWNrYWdlcy9taXhwYW5lbF9mbHV0dGVyL2Fzc2V0cy9taXhwYW5lbC5qcwwBDQEHBWFzc2V0ByxwYWNrYWdlcy9taXhwYW5lbF9mbHV0dGVyL2Fzc2V0cy9taXhwYW5lbC5qcwc3cGFja2FnZXMvcmVjb3JkX3dlYi9hc3NldHMvanMvcmVjb3JkLmZpeHdlYm1kdXJhdGlvbi5qcwwBDQEHBWFzc2V0BzdwYWNrYWdlcy9yZWNvcmRfd2ViL2Fzc2V0cy9qcy9yZWNvcmQuZml4d2VibWR1cmF0aW9uLmpzBy9wYWNrYWdlcy9yZWNvcmRfd2ViL2Fzc2V0cy9qcy9yZWNvcmQud29ya2xldC5qcwwBDQEHBWFzc2V0By9wYWNrYWdlcy9yZWNvcmRfd2ViL2Fzc2V0cy9qcy9yZWNvcmQud29ya2xldC5qcwcWd2ViL2ljb25zL0ljb24tMTkyLnBuZwwBDQEHBWFzc2V0BxZ3ZWIvaWNvbnMvSWNvbi0xOTIucG5n"
1
+ "DQ4HIWFzc2V0cy9icmFuZGluZy9hcHBfaWNvbl8xMDI0LnBuZwwBDQEHBWFzc2V0ByFhc3NldHMvYnJhbmRpbmcvYXBwX2ljb25fMTAyNC5wbmcHIGFzc2V0cy9icmFuZGluZy9hcHBfaWNvbl8yNTYucG5nDAENAQcFYXNzZXQHIGFzc2V0cy9icmFuZGluZy9hcHBfaWNvbl8yNTYucG5nByBhc3NldHMvYnJhbmRpbmcvYXBwX2ljb25fNTEyLnBuZwwBDQEHBWFzc2V0ByBhc3NldHMvYnJhbmRpbmcvYXBwX2ljb25fNTEyLnBuZwcnYXNzZXRzL2JyYW5kaW5nL2FwcF9pY29uX2xpZ2h0XzEwMjQucG5nDAENAQcFYXNzZXQHJ2Fzc2V0cy9icmFuZGluZy9hcHBfaWNvbl9saWdodF8xMDI0LnBuZwcmYXNzZXRzL2JyYW5kaW5nL2FwcF9pY29uX2xpZ2h0XzI1Ni5wbmcMAQ0BBwVhc3NldAcmYXNzZXRzL2JyYW5kaW5nL2FwcF9pY29uX2xpZ2h0XzI1Ni5wbmcHJmFzc2V0cy9icmFuZGluZy9hcHBfaWNvbl9saWdodF81MTIucG5nDAENAQcFYXNzZXQHJmFzc2V0cy9icmFuZGluZy9hcHBfaWNvbl9saWdodF81MTIucG5nByRhc3NldHMvYnJhbmRpbmcvb25ib2FyZGluZ19pbnRyby5tcDQMAQ0BBwVhc3NldAckYXNzZXRzL2JyYW5kaW5nL29uYm9hcmRpbmdfaW50cm8ubXA0Byxhc3NldHMvYnJhbmRpbmcvdHJheV9pY29uX2xpZ2h0X3RlbXBsYXRlLnBuZwwBDQEHBWFzc2V0Byxhc3NldHMvYnJhbmRpbmcvdHJheV9pY29uX2xpZ2h0X3RlbXBsYXRlLnBuZwcmYXNzZXRzL2JyYW5kaW5nL3RyYXlfaWNvbl90ZW1wbGF0ZS5wbmcMAQ0BBwVhc3NldAcmYXNzZXRzL2JyYW5kaW5nL3RyYXlfaWNvbl90ZW1wbGF0ZS5wbmcHMnBhY2thZ2VzL2N1cGVydGlub19pY29ucy9hc3NldHMvQ3VwZXJ0aW5vSWNvbnMudHRmDAENAQcFYXNzZXQHMnBhY2thZ2VzL2N1cGVydGlub19pY29ucy9hc3NldHMvQ3VwZXJ0aW5vSWNvbnMudHRmByxwYWNrYWdlcy9taXhwYW5lbF9mbHV0dGVyL2Fzc2V0cy9taXhwYW5lbC5qcwwBDQEHBWFzc2V0ByxwYWNrYWdlcy9taXhwYW5lbF9mbHV0dGVyL2Fzc2V0cy9taXhwYW5lbC5qcwc3cGFja2FnZXMvcmVjb3JkX3dlYi9hc3NldHMvanMvcmVjb3JkLmZpeHdlYm1kdXJhdGlvbi5qcwwBDQEHBWFzc2V0BzdwYWNrYWdlcy9yZWNvcmRfd2ViL2Fzc2V0cy9qcy9yZWNvcmQuZml4d2VibWR1cmF0aW9uLmpzBy9wYWNrYWdlcy9yZWNvcmRfd2ViL2Fzc2V0cy9qcy9yZWNvcmQud29ya2xldC5qcwwBDQEHBWFzc2V0By9wYWNrYWdlcy9yZWNvcmRfd2ViL2Fzc2V0cy9qcy9yZWNvcmQud29ya2xldC5qcwcWd2ViL2ljb25zL0ljb24tMTkyLnBuZwwBDQEHBWFzc2V0BxZ3ZWIvaWNvbnMvSWNvbi0xOTIucG5n"
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"c416acfeb8126e097f758c664aaa3da929e27d
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "1085140529" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "515105998" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });