neoagent 3.2.1-beta.1 → 3.2.1-beta.10

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 (172) hide show
  1. package/extensions/chrome-browser/background.mjs +318 -88
  2. package/extensions/chrome-browser/http.mjs +136 -0
  3. package/extensions/chrome-browser/protocol.mjs +654 -90
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +111 -20
  6. package/flutter_app/lib/main_integrations.dart +607 -8
  7. package/flutter_app/lib/main_models.dart +3 -0
  8. package/flutter_app/lib/main_operations.dart +334 -321
  9. package/flutter_app/lib/main_security.dart +266 -112
  10. package/flutter_app/lib/main_settings.dart +4 -3
  11. package/flutter_app/lib/main_shared.dart +14 -11
  12. package/flutter_app/lib/src/backend_client.dart +78 -0
  13. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  14. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  15. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  16. package/landing/index.html +3 -1
  17. package/lib/manager.js +106 -89
  18. package/lib/schema_migrations.js +115 -13
  19. package/package.json +30 -15
  20. package/runtime/paths.js +49 -5
  21. package/server/db/database.js +2 -2
  22. package/server/guest-agent.cli.package.json +13 -0
  23. package/server/guest_agent.js +85 -40
  24. package/server/http/middleware.js +24 -0
  25. package/server/http/routes.js +11 -6
  26. package/server/public/.last_build_id +1 -1
  27. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  28. package/server/public/flutter_bootstrap.js +2 -2
  29. package/server/public/main.dart.js +73083 -72209
  30. package/server/routes/admin.js +1 -1
  31. package/server/routes/android.js +30 -34
  32. package/server/routes/browser.js +23 -15
  33. package/server/routes/desktop.js +18 -1
  34. package/server/routes/integrations.js +107 -1
  35. package/server/routes/memory.js +1 -0
  36. package/server/routes/settings.js +16 -5
  37. package/server/routes/social_reach.js +12 -3
  38. package/server/routes/social_video.js +4 -0
  39. package/server/services/agents/manager.js +1 -1
  40. package/server/services/ai/capabilityHealth.js +62 -96
  41. package/server/services/ai/compaction.js +7 -2
  42. package/server/services/ai/history.js +45 -6
  43. package/server/services/ai/integrated_tools/http_request.js +8 -0
  44. package/server/services/ai/loop/agent_engine_core.js +390 -162
  45. package/server/services/ai/loop/blank_recovery.js +5 -4
  46. package/server/services/ai/loop/callbacks.js +1 -0
  47. package/server/services/ai/loop/completion_judge.js +121 -5
  48. package/server/services/ai/loop/conversation_loop.js +510 -332
  49. package/server/services/ai/loop/messaging_delivery.js +129 -57
  50. package/server/services/ai/loop/model_call_guard.js +91 -0
  51. package/server/services/ai/loop/model_io.js +20 -45
  52. package/server/services/ai/loop/progress_classification.js +2 -0
  53. package/server/services/ai/loop/tool_dispatch.js +19 -8
  54. package/server/services/ai/loopPolicy.js +48 -21
  55. package/server/services/ai/messagingFallback.js +17 -17
  56. package/server/services/ai/model_discovery.js +227 -0
  57. package/server/services/ai/model_failure_cache.js +108 -0
  58. package/server/services/ai/model_identity.js +71 -0
  59. package/server/services/ai/models.js +68 -163
  60. package/server/services/ai/providerRetry.js +17 -59
  61. package/server/services/ai/provider_selector.js +166 -0
  62. package/server/services/ai/providers/anthropic.js +2 -2
  63. package/server/services/ai/providers/claudeCode.js +21 -33
  64. package/server/services/ai/providers/githubCopilot.js +41 -20
  65. package/server/services/ai/providers/google.js +135 -97
  66. package/server/services/ai/providers/grok.js +4 -3
  67. package/server/services/ai/providers/grokOauth.js +19 -27
  68. package/server/services/ai/providers/nvidia.js +10 -5
  69. package/server/services/ai/providers/ollama.js +111 -84
  70. package/server/services/ai/providers/ollama_stream.js +142 -0
  71. package/server/services/ai/providers/openai.js +39 -5
  72. package/server/services/ai/providers/openaiCodex.js +11 -4
  73. package/server/services/ai/providers/openrouter.js +29 -7
  74. package/server/services/ai/providers/provider_error.js +36 -0
  75. package/server/services/ai/settings.js +26 -2
  76. package/server/services/ai/systemPrompt.js +19 -12
  77. package/server/services/ai/taskAnalysis.js +58 -10
  78. package/server/services/ai/terminal_reply.js +18 -0
  79. package/server/services/ai/toolEvidence.js +350 -29
  80. package/server/services/ai/tools.js +190 -111
  81. package/server/services/android/controller.js +770 -237
  82. package/server/services/android/process.js +140 -0
  83. package/server/services/android/sdk_download.js +143 -0
  84. package/server/services/android/uia.js +6 -5
  85. package/server/services/artifacts/store.js +24 -0
  86. package/server/services/browser/controller.js +843 -385
  87. package/server/services/browser/extension/gateway.js +40 -16
  88. package/server/services/browser/extension/protocol.js +15 -1
  89. package/server/services/browser/extension/provider.js +71 -47
  90. package/server/services/browser/extension/registry.js +155 -34
  91. package/server/services/cli/executor.js +62 -9
  92. package/server/services/credentials/bitwarden_cli.js +322 -0
  93. package/server/services/credentials/broker.js +594 -0
  94. package/server/services/desktop/gateway.js +41 -4
  95. package/server/services/desktop/protocol.js +3 -0
  96. package/server/services/desktop/provider.js +39 -42
  97. package/server/services/desktop/registry.js +137 -52
  98. package/server/services/integrations/bitwarden/constants.js +14 -0
  99. package/server/services/integrations/bitwarden/provider.js +197 -0
  100. package/server/services/integrations/bitwarden/snapshot.js +65 -0
  101. package/server/services/integrations/figma/provider.js +78 -12
  102. package/server/services/integrations/github/common.js +11 -6
  103. package/server/services/integrations/github/provider.js +52 -53
  104. package/server/services/integrations/google/provider.js +55 -19
  105. package/server/services/integrations/home_assistant/network.js +17 -20
  106. package/server/services/integrations/home_assistant/provider.js +7 -5
  107. package/server/services/integrations/home_assistant/tools.js +17 -5
  108. package/server/services/integrations/http.js +51 -0
  109. package/server/services/integrations/manager.js +159 -53
  110. package/server/services/integrations/microsoft/provider.js +80 -13
  111. package/server/services/integrations/neoarchive/provider.js +55 -29
  112. package/server/services/integrations/neorecall/client.js +17 -10
  113. package/server/services/integrations/neorecall/provider.js +20 -11
  114. package/server/services/integrations/notion/provider.js +16 -13
  115. package/server/services/integrations/oauth_provider.js +115 -51
  116. package/server/services/integrations/registry.js +2 -0
  117. package/server/services/integrations/slack/provider.js +98 -9
  118. package/server/services/integrations/spotify/provider.js +67 -71
  119. package/server/services/integrations/trello/provider.js +21 -7
  120. package/server/services/integrations/weather/provider.js +18 -12
  121. package/server/services/integrations/whatsapp/provider.js +76 -16
  122. package/server/services/manager.js +110 -1
  123. package/server/services/memory/embedding_index.js +20 -8
  124. package/server/services/memory/embeddings.js +151 -90
  125. package/server/services/memory/ingestion.js +50 -9
  126. package/server/services/memory/ingestion_documents.js +13 -3
  127. package/server/services/memory/manager.js +52 -19
  128. package/server/services/messaging/automation.js +85 -10
  129. package/server/services/messaging/formatting_guides.js +7 -4
  130. package/server/services/messaging/http_platforms.js +33 -13
  131. package/server/services/messaging/inbound_queue.js +78 -24
  132. package/server/services/messaging/inbound_store.js +224 -0
  133. package/server/services/messaging/manager.js +326 -51
  134. package/server/services/messaging/typing_keepalive.js +5 -2
  135. package/server/services/messaging/whatsapp.js +22 -14
  136. package/server/services/network/http.js +210 -0
  137. package/server/services/network/safe_request.js +307 -0
  138. package/server/services/runtime/backends/local-vm.js +227 -67
  139. package/server/services/runtime/docker-vm-manager.js +9 -0
  140. package/server/services/runtime/guest_bootstrap.js +30 -4
  141. package/server/services/runtime/guest_image.js +43 -12
  142. package/server/services/runtime/manager.js +77 -23
  143. package/server/services/runtime/validation.js +7 -6
  144. package/server/services/security/tool_categories.js +6 -0
  145. package/server/services/social_reach/channels/github.js +10 -4
  146. package/server/services/social_reach/channels/reddit.js +4 -4
  147. package/server/services/social_reach/channels/rss.js +2 -2
  148. package/server/services/social_reach/channels/social_video.js +12 -7
  149. package/server/services/social_reach/channels/v2ex.js +21 -8
  150. package/server/services/social_reach/channels/x.js +2 -2
  151. package/server/services/social_reach/channels/xueqiu.js +5 -5
  152. package/server/services/social_reach/service.js +9 -6
  153. package/server/services/social_reach/utils.js +65 -14
  154. package/server/services/social_video/service.js +160 -50
  155. package/server/services/tasks/integration_runtime.js +18 -8
  156. package/server/services/tasks/runtime.js +39 -4
  157. package/server/services/voice/agentBridge.js +17 -4
  158. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  159. package/server/services/voice/liveSession.js +31 -0
  160. package/server/services/voice/message.js +1 -1
  161. package/server/services/voice/openaiSpeech.js +33 -8
  162. package/server/services/voice/providers.js +233 -151
  163. package/server/services/voice/runtime.js +2 -2
  164. package/server/services/voice/runtimeManager.js +118 -20
  165. package/server/services/voice/turnRunner.js +6 -0
  166. package/server/services/wearable/firmware_manifest.js +51 -13
  167. package/server/services/wearable/service.js +1 -0
  168. package/server/utils/abort.js +96 -0
  169. package/server/utils/cloud-security.js +110 -3
  170. package/server/utils/files.js +31 -0
  171. package/server/utils/image_payload.js +95 -0
  172. package/server/utils/retry.js +107 -0
@@ -230,6 +230,9 @@ void _openOfficialIntegrationSetupDialog(
230
230
  String providerId,
231
231
  ) {
232
232
  switch (providerId) {
233
+ case 'bitwarden':
234
+ _showBitwardenSetupDialog(context, controller);
235
+ return;
233
236
  case 'home_assistant':
234
237
  _showHomeAssistantSetupDialog(context, controller);
235
238
  return;
@@ -245,6 +248,587 @@ void _openOfficialIntegrationSetupDialog(
245
248
  }
246
249
  }
247
250
 
251
+ Future<void> _showBitwardenBindingDialog(
252
+ BuildContext context,
253
+ NeoAgentController controller, {
254
+ required int connectionId,
255
+ }) async {
256
+ List<Map<String, dynamic>> items;
257
+ try {
258
+ items = await controller.fetchBitwardenItems();
259
+ } catch (error) {
260
+ if (context.mounted) {
261
+ ScaffoldMessenger.of(context).showSnackBar(
262
+ SnackBar(content: Text(controller.errorMessage ?? error.toString())),
263
+ );
264
+ }
265
+ return;
266
+ }
267
+ if (items.isEmpty || !context.mounted) return;
268
+ String itemId = items.first['id']?.toString() ?? '';
269
+ var usageType = 'browser';
270
+ var saving = false;
271
+ var errorText = '';
272
+ final aliasController = TextEditingController();
273
+ final originController = TextEditingController(
274
+ text: ((items.first['origins'] as List?)?.firstOrNull)?.toString() ?? '',
275
+ );
276
+ final pathController = TextEditingController(text: '/');
277
+ final headerController = TextEditingController(text: 'X-API-Key');
278
+ var authType = 'bearer';
279
+ var secretField = 'login.password';
280
+
281
+ await showDialog<void>(
282
+ context: context,
283
+ barrierDismissible: false,
284
+ builder: (dialogContext) => StatefulBuilder(
285
+ builder: (dialogContext, setState) {
286
+ final selected = items.firstWhere(
287
+ (item) => item['id']?.toString() == itemId,
288
+ orElse: () => items.first,
289
+ );
290
+ final customFields = (selected['fields'] as List? ?? const [])
291
+ .whereType<Map>()
292
+ .map(
293
+ (field) => (
294
+ field['id']?.toString().isNotEmpty == true
295
+ ? field['id'].toString()
296
+ : field['name'].toString(),
297
+ field['name']?.toString() ?? 'Custom field',
298
+ ),
299
+ )
300
+ .toList();
301
+ final secretOptions = <(String, String)>[
302
+ ('login.password', 'Login password'),
303
+ ...customFields,
304
+ ];
305
+ if (!secretOptions.any((entry) => entry.$1 == secretField)) {
306
+ secretField = secretOptions.first.$1;
307
+ }
308
+ return AlertDialog(
309
+ title: const Text('Add credential binding'),
310
+ content: SizedBox(
311
+ width: 520,
312
+ child: SingleChildScrollView(
313
+ child: Column(
314
+ mainAxisSize: MainAxisSize.min,
315
+ children: <Widget>[
316
+ DropdownButtonFormField<String>(
317
+ initialValue: itemId,
318
+ decoration: const InputDecoration(
319
+ labelText: 'Bitwarden item',
320
+ border: OutlineInputBorder(),
321
+ ),
322
+ items: items
323
+ .map(
324
+ (item) => DropdownMenuItem(
325
+ value: item['id']?.toString(),
326
+ child: Text(item['name']?.toString() ?? 'Untitled'),
327
+ ),
328
+ )
329
+ .toList(),
330
+ onChanged: saving
331
+ ? null
332
+ : (value) => setState(() {
333
+ itemId = value ?? itemId;
334
+ final item = items.firstWhere(
335
+ (candidate) =>
336
+ candidate['id']?.toString() == itemId,
337
+ );
338
+ final origins = item['origins'] as List?;
339
+ if (origins?.isNotEmpty == true) {
340
+ originController.text = origins!.first.toString();
341
+ }
342
+ }),
343
+ ),
344
+ const SizedBox(height: 12),
345
+ TextField(
346
+ controller: aliasController,
347
+ decoration: const InputDecoration(
348
+ labelText: 'Agent-visible name',
349
+ hintText: 'Work account',
350
+ border: OutlineInputBorder(),
351
+ ),
352
+ ),
353
+ const SizedBox(height: 12),
354
+ DropdownButtonFormField<String>(
355
+ initialValue: usageType,
356
+ decoration: const InputDecoration(
357
+ labelText: 'Use for',
358
+ border: OutlineInputBorder(),
359
+ ),
360
+ items: const [
361
+ DropdownMenuItem(
362
+ value: 'browser',
363
+ child: Text('Browser login'),
364
+ ),
365
+ DropdownMenuItem(
366
+ value: 'http',
367
+ child: Text('API request'),
368
+ ),
369
+ ],
370
+ onChanged: saving
371
+ ? null
372
+ : (value) =>
373
+ setState(() => usageType = value ?? usageType),
374
+ ),
375
+ const SizedBox(height: 12),
376
+ TextField(
377
+ controller: originController,
378
+ keyboardType: TextInputType.url,
379
+ decoration: InputDecoration(
380
+ labelText: usageType == 'browser'
381
+ ? 'Allowed HTTPS origin'
382
+ : 'API HTTPS origin',
383
+ border: const OutlineInputBorder(),
384
+ ),
385
+ ),
386
+ if (usageType == 'http') ...[
387
+ const SizedBox(height: 12),
388
+ TextField(
389
+ controller: pathController,
390
+ decoration: const InputDecoration(
391
+ labelText: 'Allowed path prefix',
392
+ border: OutlineInputBorder(),
393
+ ),
394
+ ),
395
+ const SizedBox(height: 12),
396
+ DropdownButtonFormField<String>(
397
+ initialValue: authType,
398
+ decoration: const InputDecoration(
399
+ labelText: 'Authentication',
400
+ border: OutlineInputBorder(),
401
+ ),
402
+ items: const [
403
+ DropdownMenuItem(
404
+ value: 'bearer',
405
+ child: Text('Bearer token'),
406
+ ),
407
+ DropdownMenuItem(
408
+ value: 'basic',
409
+ child: Text('Basic authentication'),
410
+ ),
411
+ DropdownMenuItem(
412
+ value: 'header',
413
+ child: Text('Custom header'),
414
+ ),
415
+ ],
416
+ onChanged: saving
417
+ ? null
418
+ : (value) =>
419
+ setState(() => authType = value ?? authType),
420
+ ),
421
+ const SizedBox(height: 12),
422
+ DropdownButtonFormField<String>(
423
+ initialValue: secretField,
424
+ decoration: const InputDecoration(
425
+ labelText: 'Secret field',
426
+ border: OutlineInputBorder(),
427
+ ),
428
+ items: secretOptions
429
+ .map(
430
+ (entry) => DropdownMenuItem(
431
+ value: entry.$1,
432
+ child: Text(entry.$2),
433
+ ),
434
+ )
435
+ .toList(),
436
+ onChanged: saving
437
+ ? null
438
+ : (value) => setState(
439
+ () => secretField = value ?? secretField,
440
+ ),
441
+ ),
442
+ if (authType == 'header') ...[
443
+ const SizedBox(height: 12),
444
+ TextField(
445
+ controller: headerController,
446
+ decoration: const InputDecoration(
447
+ labelText: 'Header name',
448
+ border: OutlineInputBorder(),
449
+ ),
450
+ ),
451
+ ],
452
+ ],
453
+ if (errorText.isNotEmpty) ...[
454
+ const SizedBox(height: 12),
455
+ Text(errorText, style: TextStyle(color: _danger)),
456
+ ],
457
+ ],
458
+ ),
459
+ ),
460
+ ),
461
+ actions: [
462
+ TextButton(
463
+ onPressed: saving
464
+ ? null
465
+ : () => Navigator.of(dialogContext).pop(),
466
+ child: const Text('Cancel'),
467
+ ),
468
+ FilledButton(
469
+ onPressed: saving
470
+ ? null
471
+ : () async {
472
+ setState(() {
473
+ saving = true;
474
+ errorText = '';
475
+ });
476
+ try {
477
+ await controller.createCredentialBinding(
478
+ <String, dynamic>{
479
+ 'connectionId': connectionId,
480
+ 'alias': aliasController.text.trim(),
481
+ 'usageType': usageType,
482
+ 'itemId': itemId,
483
+ if (usageType == 'browser')
484
+ 'origins': <String>[originController.text.trim()],
485
+ if (usageType == 'http') ...<String, dynamic>{
486
+ 'origin': originController.text.trim(),
487
+ 'pathPrefix': pathController.text.trim(),
488
+ 'methods': const [
489
+ 'GET',
490
+ 'POST',
491
+ 'PUT',
492
+ 'PATCH',
493
+ 'DELETE',
494
+ ],
495
+ 'authType': authType,
496
+ 'secretField': secretField,
497
+ if (authType == 'basic')
498
+ 'usernameField': 'login.username',
499
+ if (authType == 'header')
500
+ 'headerName': headerController.text.trim(),
501
+ },
502
+ },
503
+ );
504
+ if (dialogContext.mounted) {
505
+ Navigator.of(dialogContext).pop();
506
+ }
507
+ } catch (_) {
508
+ setState(() {
509
+ errorText =
510
+ controller.errorMessage ??
511
+ 'Could not create binding.';
512
+ saving = false;
513
+ });
514
+ }
515
+ },
516
+ child: Text(saving ? 'Adding...' : 'Add binding'),
517
+ ),
518
+ ],
519
+ );
520
+ },
521
+ ),
522
+ );
523
+ aliasController.dispose();
524
+ originController.dispose();
525
+ pathController.dispose();
526
+ headerController.dispose();
527
+ }
528
+
529
+ Future<void> _showBitwardenSetupDialog(
530
+ BuildContext context,
531
+ NeoAgentController controller,
532
+ ) async {
533
+ Map<String, dynamic> config;
534
+ List<Map<String, dynamic>> bindings;
535
+ try {
536
+ config = await controller.getOfficialIntegrationConfig('bitwarden');
537
+ bindings = await controller.fetchCredentialBindings();
538
+ } catch (error) {
539
+ if (context.mounted) {
540
+ ScaffoldMessenger.of(context).showSnackBar(
541
+ SnackBar(content: Text(controller.errorMessage ?? error.toString())),
542
+ );
543
+ }
544
+ return;
545
+ }
546
+ var unlocked = config['unlocked'] == true;
547
+ var busy = false;
548
+ var errorText = '';
549
+ final serverController = TextEditingController(
550
+ text: config['serverUrl']?.toString() ?? 'https://vault.bitwarden.com',
551
+ );
552
+ final emailController = TextEditingController(
553
+ text: config['email']?.toString() ?? '',
554
+ );
555
+ final clientIdController = TextEditingController();
556
+ final clientSecretController = TextEditingController();
557
+ final masterPasswordController = TextEditingController();
558
+ final timeoutController = TextEditingController(
559
+ text: (config['idleTimeoutMinutes'] ?? 30).toString(),
560
+ );
561
+ if (!context.mounted) return;
562
+ await showDialog<void>(
563
+ context: context,
564
+ barrierDismissible: false,
565
+ builder: (dialogContext) => StatefulBuilder(
566
+ builder: (dialogContext, setState) => AlertDialog(
567
+ title: const Text('Bitwarden credential broker'),
568
+ content: SizedBox(
569
+ width: 600,
570
+ child: SingleChildScrollView(
571
+ child: Column(
572
+ mainAxisSize: MainAxisSize.min,
573
+ crossAxisAlignment: CrossAxisAlignment.start,
574
+ children: [
575
+ Text(
576
+ 'NeoAgent receives only opaque binding IDs. Your master password, vault session, usernames, passwords, and API tokens stay outside the AI context.',
577
+ style: TextStyle(color: _textSecondary),
578
+ ),
579
+ const SizedBox(height: 16),
580
+ TextField(
581
+ controller: serverController,
582
+ decoration: const InputDecoration(
583
+ labelText: 'Bitwarden server',
584
+ border: OutlineInputBorder(),
585
+ ),
586
+ ),
587
+ const SizedBox(height: 12),
588
+ TextField(
589
+ controller: emailController,
590
+ keyboardType: TextInputType.emailAddress,
591
+ decoration: const InputDecoration(
592
+ labelText: 'Account email',
593
+ border: OutlineInputBorder(),
594
+ ),
595
+ ),
596
+ const SizedBox(height: 12),
597
+ TextField(
598
+ controller: clientIdController,
599
+ decoration: InputDecoration(
600
+ labelText: config['hasClientId'] == true
601
+ ? 'Replacement personal API client ID'
602
+ : 'Personal API client ID',
603
+ border: const OutlineInputBorder(),
604
+ ),
605
+ ),
606
+ const SizedBox(height: 12),
607
+ TextField(
608
+ controller: clientSecretController,
609
+ obscureText: true,
610
+ decoration: InputDecoration(
611
+ labelText: config['hasClientSecret'] == true
612
+ ? 'Replacement personal API client secret'
613
+ : 'Personal API client secret',
614
+ border: const OutlineInputBorder(),
615
+ ),
616
+ ),
617
+ const SizedBox(height: 20),
618
+ Text(
619
+ unlocked ? 'Vault unlocked' : 'Vault locked',
620
+ style: TextStyle(color: unlocked ? _success : _textSecondary),
621
+ ),
622
+ const SizedBox(height: 8),
623
+ Row(
624
+ children: [
625
+ Expanded(
626
+ child: TextField(
627
+ controller: masterPasswordController,
628
+ obscureText: true,
629
+ enabled: !busy && !unlocked,
630
+ decoration: const InputDecoration(
631
+ labelText: 'Master password',
632
+ border: OutlineInputBorder(),
633
+ ),
634
+ ),
635
+ ),
636
+ const SizedBox(width: 8),
637
+ SizedBox(
638
+ width: 92,
639
+ child: TextField(
640
+ controller: timeoutController,
641
+ keyboardType: TextInputType.number,
642
+ enabled: !busy && !unlocked,
643
+ decoration: const InputDecoration(
644
+ labelText: 'Minutes',
645
+ border: OutlineInputBorder(),
646
+ ),
647
+ ),
648
+ ),
649
+ const SizedBox(width: 8),
650
+ FilledButton(
651
+ onPressed:
652
+ busy || (!unlocked && config['configured'] != true)
653
+ ? null
654
+ : () async {
655
+ setState(() => busy = true);
656
+ try {
657
+ if (unlocked) {
658
+ await controller.lockBitwarden();
659
+ unlocked = false;
660
+ } else {
661
+ await controller.unlockBitwarden(
662
+ masterPasswordController.text,
663
+ idleTimeoutMinutes:
664
+ int.tryParse(timeoutController.text) ??
665
+ 30,
666
+ );
667
+ masterPasswordController.clear();
668
+ unlocked = true;
669
+ }
670
+ setState(() {});
671
+ } catch (_) {
672
+ setState(
673
+ () => errorText =
674
+ controller.errorMessage ??
675
+ 'Vault operation failed.',
676
+ );
677
+ } finally {
678
+ setState(() => busy = false);
679
+ }
680
+ },
681
+ child: Text(unlocked ? 'Lock' : 'Unlock'),
682
+ ),
683
+ ],
684
+ ),
685
+ const SizedBox(height: 20),
686
+ Row(
687
+ children: [
688
+ const Expanded(
689
+ child: Text(
690
+ 'Credential bindings',
691
+ style: TextStyle(fontWeight: FontWeight.w600),
692
+ ),
693
+ ),
694
+ FilledButton.tonalIcon(
695
+ onPressed:
696
+ !unlocked || config['connectionId'] == null || busy
697
+ ? null
698
+ : () async {
699
+ await _showBitwardenBindingDialog(
700
+ dialogContext,
701
+ controller,
702
+ connectionId: (config['connectionId'] as num)
703
+ .toInt(),
704
+ );
705
+ bindings = await controller
706
+ .fetchCredentialBindings();
707
+ setState(() {});
708
+ },
709
+ icon: const Icon(Icons.add_rounded),
710
+ label: const Text('Add'),
711
+ ),
712
+ ],
713
+ ),
714
+ if (bindings.isEmpty)
715
+ Padding(
716
+ padding: const EdgeInsets.only(top: 8),
717
+ child: Text(
718
+ 'No bindings yet.',
719
+ style: TextStyle(color: _textSecondary),
720
+ ),
721
+ )
722
+ else
723
+ ...bindings.map(
724
+ (binding) => ListTile(
725
+ contentPadding: EdgeInsets.zero,
726
+ title: Text(binding['alias']?.toString() ?? 'Credential'),
727
+ subtitle: Text(binding['usageType']?.toString() ?? ''),
728
+ trailing: IconButton(
729
+ tooltip: 'Delete binding',
730
+ icon: const Icon(Icons.delete_outline_rounded),
731
+ onPressed: busy
732
+ ? null
733
+ : () async {
734
+ await controller.deleteCredentialBinding(
735
+ binding['id'].toString(),
736
+ );
737
+ bindings = await controller
738
+ .fetchCredentialBindings();
739
+ setState(() {});
740
+ },
741
+ ),
742
+ ),
743
+ ),
744
+ if (errorText.isNotEmpty)
745
+ Padding(
746
+ padding: const EdgeInsets.only(top: 12),
747
+ child: Text(errorText, style: TextStyle(color: _danger)),
748
+ ),
749
+ ],
750
+ ),
751
+ ),
752
+ ),
753
+ actions: [
754
+ if (config['configured'] == true)
755
+ TextButton(
756
+ onPressed: busy
757
+ ? null
758
+ : () async {
759
+ setState(() => busy = true);
760
+ try {
761
+ await controller.clearOfficialIntegrationConfig(
762
+ 'bitwarden',
763
+ );
764
+ if (dialogContext.mounted) {
765
+ Navigator.of(dialogContext).pop();
766
+ }
767
+ } catch (_) {
768
+ setState(() {
769
+ errorText =
770
+ controller.errorMessage ??
771
+ 'Could not disconnect Bitwarden.';
772
+ busy = false;
773
+ });
774
+ }
775
+ },
776
+ child: const Text('Disconnect'),
777
+ ),
778
+ TextButton(
779
+ onPressed: busy ? null : () => Navigator.of(dialogContext).pop(),
780
+ child: const Text('Close'),
781
+ ),
782
+ FilledButton(
783
+ onPressed: busy
784
+ ? null
785
+ : () async {
786
+ setState(() {
787
+ busy = true;
788
+ errorText = '';
789
+ });
790
+ try {
791
+ await controller.saveOfficialIntegrationConfig(
792
+ 'bitwarden',
793
+ config: <String, dynamic>{
794
+ 'serverUrl': serverController.text.trim(),
795
+ 'email': emailController.text.trim(),
796
+ if (clientIdController.text.trim().isNotEmpty)
797
+ 'clientId': clientIdController.text.trim(),
798
+ if (clientSecretController.text.isNotEmpty)
799
+ 'clientSecret': clientSecretController.text,
800
+ 'idleTimeoutMinutes':
801
+ int.tryParse(timeoutController.text) ?? 30,
802
+ },
803
+ );
804
+ config = await controller.getOfficialIntegrationConfig(
805
+ 'bitwarden',
806
+ );
807
+ setState(() {});
808
+ } catch (_) {
809
+ setState(
810
+ () => errorText =
811
+ controller.errorMessage ??
812
+ 'Could not save Bitwarden setup.',
813
+ );
814
+ } finally {
815
+ setState(() => busy = false);
816
+ }
817
+ },
818
+ child: Text(busy ? 'Saving...' : 'Save setup'),
819
+ ),
820
+ ],
821
+ ),
822
+ ),
823
+ );
824
+ serverController.dispose();
825
+ emailController.dispose();
826
+ clientIdController.dispose();
827
+ clientSecretController.dispose();
828
+ masterPasswordController.dispose();
829
+ timeoutController.dispose();
830
+ }
831
+
248
832
  Future<void> _showNeoRecallSetupDialog(
249
833
  BuildContext context,
250
834
  NeoAgentController controller,
@@ -305,7 +889,8 @@ Future<void> _showNeoRecallSetupDialog(
305
889
  if (dialogContext.mounted) Navigator.of(dialogContext).pop();
306
890
  } catch (_) {
307
891
  setState(() {
308
- errorText = controller.errorMessage ?? 'Could not save NeoRecall setup.';
892
+ errorText =
893
+ controller.errorMessage ?? 'Could not save NeoRecall setup.';
309
894
  busy = false;
310
895
  });
311
896
  }
@@ -380,11 +965,13 @@ Future<void> _showNeoRecallSetupDialog(
380
965
  ),
381
966
  actions: <Widget>[
382
967
  TextButton(
383
- onPressed: () => Navigator.of(context).pop(false),
968
+ onPressed: () =>
969
+ Navigator.of(context).pop(false),
384
970
  child: const Text('Cancel'),
385
971
  ),
386
972
  FilledButton(
387
- onPressed: () => Navigator.of(context).pop(true),
973
+ onPressed: () =>
974
+ Navigator.of(context).pop(true),
388
975
  child: const Text('Disconnect'),
389
976
  ),
390
977
  ],
@@ -397,11 +984,17 @@ Future<void> _showNeoRecallSetupDialog(
397
984
  errorText = '';
398
985
  });
399
986
  try {
400
- await controller.clearOfficialIntegrationConfig('neorecall');
401
- if (dialogContext.mounted) Navigator.of(dialogContext).pop();
987
+ await controller.clearOfficialIntegrationConfig(
988
+ 'neorecall',
989
+ );
990
+ if (dialogContext.mounted) {
991
+ Navigator.of(dialogContext).pop();
992
+ }
402
993
  } catch (_) {
403
994
  setState(() {
404
- errorText = controller.errorMessage ?? 'Could not disconnect NeoRecall.';
995
+ errorText =
996
+ controller.errorMessage ??
997
+ 'Could not disconnect NeoRecall.';
405
998
  busy = false;
406
999
  });
407
1000
  }
@@ -413,11 +1006,15 @@ Future<void> _showNeoRecallSetupDialog(
413
1006
  child: const Text('Close'),
414
1007
  ),
415
1008
  TextButton(
416
- onPressed: busy ? null : () => save(setState, dialogContext, connect: false),
1009
+ onPressed: busy
1010
+ ? null
1011
+ : () => save(setState, dialogContext, connect: false),
417
1012
  child: const Text('Save Only'),
418
1013
  ),
419
1014
  FilledButton(
420
- onPressed: busy ? null : () => save(setState, dialogContext, connect: true),
1015
+ onPressed: busy
1016
+ ? null
1017
+ : () => save(setState, dialogContext, connect: true),
421
1018
  child: Text(
422
1019
  busy
423
1020
  ? 'Working...'
@@ -1544,6 +2141,7 @@ class _OfficialIntegrationIcon extends StatelessWidget {
1544
2141
  'neorecall' => const Color(0xFFD98AA6),
1545
2142
  'google' => const Color(0xFF4285F4),
1546
2143
  'home_assistant' => const Color(0xFF41BDF5),
2144
+ 'password' => const Color(0xFF175DDC),
1547
2145
  'trello' => const Color(0xFF0C66E4),
1548
2146
  _ => _accent,
1549
2147
  };
@@ -1552,6 +2150,7 @@ class _OfficialIntegrationIcon extends StatelessWidget {
1552
2150
  'neorecall' => 'R',
1553
2151
  'google' => 'G',
1554
2152
  'home_assistant' => 'H',
2153
+ 'password' => 'B',
1555
2154
  'trello' => 'T',
1556
2155
  _ => item.label.isNotEmpty ? item.label[0] : '?',
1557
2156
  };
@@ -1852,6 +1852,7 @@ class AgentProfile {
1852
1852
  class ModelMeta {
1853
1853
  const ModelMeta({
1854
1854
  required this.id,
1855
+ required this.modelId,
1855
1856
  required this.label,
1856
1857
  required this.provider,
1857
1858
  required this.purpose,
@@ -1864,6 +1865,7 @@ class ModelMeta {
1864
1865
  factory ModelMeta.fromJson(Map<dynamic, dynamic> json) {
1865
1866
  return ModelMeta(
1866
1867
  id: json['id']?.toString() ?? '',
1868
+ modelId: json['modelId']?.toString() ?? json['id']?.toString() ?? '',
1867
1869
  label: json['label']?.toString() ?? '',
1868
1870
  provider: json['provider']?.toString() ?? '',
1869
1871
  purpose: json['purpose']?.toString() ?? '',
@@ -1875,6 +1877,7 @@ class ModelMeta {
1875
1877
  }
1876
1878
 
1877
1879
  final String id;
1880
+ final String modelId;
1878
1881
  final String label;
1879
1882
  final String provider;
1880
1883
  final String purpose;