neoagent 2.3.1-beta.86 → 2.3.1-beta.88

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 (52) hide show
  1. package/docs/capabilities.md +2 -0
  2. package/flutter_app/android/app/src/main/AndroidManifest.xml +14 -0
  3. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +84 -0
  4. package/flutter_app/lib/features/notifications/notification_interceptor.dart +11 -45
  5. package/flutter_app/lib/main.dart +1 -0
  6. package/flutter_app/lib/main_app_shell.dart +23 -14
  7. package/flutter_app/lib/main_chat.dart +267 -7
  8. package/flutter_app/lib/main_controller.dart +151 -13
  9. package/flutter_app/lib/main_models.dart +69 -0
  10. package/flutter_app/lib/main_operations.dart +248 -0
  11. package/flutter_app/lib/main_runtime.dart +11 -2
  12. package/flutter_app/lib/main_settings.dart +182 -183
  13. package/flutter_app/lib/main_shared.dart +99 -16
  14. package/flutter_app/lib/src/app_launch_bridge.dart +39 -10
  15. package/flutter_app/lib/src/backend_client.dart +28 -0
  16. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +2 -0
  17. package/flutter_app/pubspec.lock +24 -0
  18. package/flutter_app/pubspec.yaml +1 -0
  19. package/package.json +1 -1
  20. package/server/db/database.js +42 -4
  21. package/server/guest_agent.js +8 -1
  22. package/server/http/routes.js +1 -0
  23. package/server/public/.last_build_id +1 -1
  24. package/server/public/assets/NOTICES +70 -44
  25. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  26. package/server/public/flutter_bootstrap.js +1 -1
  27. package/server/public/main.dart.js +70947 -70038
  28. package/server/routes/browser.js +1 -1
  29. package/server/routes/memory.js +90 -0
  30. package/server/routes/social_video.js +66 -0
  31. package/server/services/ai/systemPrompt.js +1 -0
  32. package/server/services/ai/toolResult.js +20 -0
  33. package/server/services/ai/tools.js +30 -0
  34. package/server/services/android/android_bootstrap_worker.js +1 -0
  35. package/server/services/android/controller.js +244 -76
  36. package/server/services/browser/controller.js +24 -8
  37. package/server/services/manager.js +15 -0
  38. package/server/services/memory/llm_transfer.js +217 -0
  39. package/server/services/runtime/backends/local-vm.js +29 -5
  40. package/server/services/social_video/adapters/base.js +26 -0
  41. package/server/services/social_video/adapters/index.js +27 -0
  42. package/server/services/social_video/adapters/instagram.js +17 -0
  43. package/server/services/social_video/adapters/tiktok.js +17 -0
  44. package/server/services/social_video/adapters/x.js +17 -0
  45. package/server/services/social_video/adapters/youtube.js +17 -0
  46. package/server/services/social_video/captions.js +187 -0
  47. package/server/services/social_video/frame.js +42 -0
  48. package/server/services/social_video/index.js +7 -0
  49. package/server/services/social_video/metadata.js +63 -0
  50. package/server/services/social_video/result.js +63 -0
  51. package/server/services/social_video/service.js +650 -0
  52. package/server/services/social_video/url.js +83 -0
@@ -186,6 +186,8 @@ class NeoAgentController extends ChangeNotifier {
186
186
  <String, RunDetailSnapshot>{};
187
187
  String? _selectedWidgetId;
188
188
  String? _pendingChatDraft;
189
+ List<SharedChatAttachment> _pendingSharedChatAttachments =
190
+ const <SharedChatAttachment>[];
189
191
 
190
192
  ActiveRunState? activeRun;
191
193
  List<ToolEventItem> toolEvents = const <ToolEventItem>[];
@@ -997,7 +999,7 @@ class NeoAgentController extends ChangeNotifier {
997
999
  unawaited(_analytics.trackAppUpdateCheck(silent: silent));
998
1000
  if (!appUpdaterConfigured) {
999
1001
  appUpdateErrorMessage = kIsWeb
1000
- ? 'Client app update checks are unavailable in the web app.'
1002
+ ? null
1001
1003
  : 'App updates are not configured for this build.';
1002
1004
  if (!silent) {
1003
1005
  notifyListeners();
@@ -2443,6 +2445,31 @@ class NeoAgentController extends ChangeNotifier {
2443
2445
  );
2444
2446
  notifyListeners();
2445
2447
  } catch (_) {}
2448
+
2449
+ Future<String> fetchMemoryTransferPrompt() async {
2450
+ final response = await _backendClient.fetchMemoryTransferPrompt(
2451
+ backendUrl,
2452
+ agentId: _scopedAgentId,
2453
+ );
2454
+ return response['prompt']?.toString() ?? '';
2455
+ }
2456
+
2457
+ Future<MemoryTransferImportResult> importMemoryTransfer(
2458
+ String text, {
2459
+ bool applyBehaviorNotes = true,
2460
+ bool applyCoreMemory = true,
2461
+ }) async {
2462
+ final response = await _backendClient.importMemoryTransfer(
2463
+ backendUrl,
2464
+ text: text,
2465
+ applyBehaviorNotes: applyBehaviorNotes,
2466
+ applyCoreMemory: applyCoreMemory,
2467
+ agentId: _scopedAgentId,
2468
+ );
2469
+ final result = MemoryTransferImportResult.fromJson(response);
2470
+ await refreshMemory();
2471
+ return result;
2472
+ }
2446
2473
  }
2447
2474
 
2448
2475
  Future<void> refreshMessaging() async {
@@ -2550,6 +2577,31 @@ class NeoAgentController extends ChangeNotifier {
2550
2577
  notifyListeners();
2551
2578
  }
2552
2579
 
2580
+ Future<String> fetchMemoryTransferPrompt() async {
2581
+ final response = await _backendClient.fetchMemoryTransferPrompt(
2582
+ backendUrl,
2583
+ agentId: _scopedAgentId,
2584
+ );
2585
+ return response['prompt']?.toString() ?? '';
2586
+ }
2587
+
2588
+ Future<MemoryTransferImportResult> importMemoryTransfer(
2589
+ String text, {
2590
+ bool applyBehaviorNotes = true,
2591
+ bool applyCoreMemory = true,
2592
+ }) async {
2593
+ final response = await _backendClient.importMemoryTransfer(
2594
+ backendUrl,
2595
+ text: text,
2596
+ applyBehaviorNotes: applyBehaviorNotes,
2597
+ applyCoreMemory: applyCoreMemory,
2598
+ agentId: _scopedAgentId,
2599
+ );
2600
+ final result = MemoryTransferImportResult.fromJson(response);
2601
+ await refreshMemory();
2602
+ return result;
2603
+ }
2604
+
2553
2605
  Future<void> refreshTasks() async {
2554
2606
  taskItems = _decodeModelList(
2555
2607
  'tasks',
@@ -2808,6 +2860,7 @@ class NeoAgentController extends ChangeNotifier {
2808
2860
  () => _backendClient.launchBrowser(backendUrl),
2809
2861
  browser: true,
2810
2862
  );
2863
+ browserScreenshotPath = null;
2811
2864
  }
2812
2865
 
2813
2866
  Future<void> navigateBrowserRuntime({
@@ -2919,6 +2972,8 @@ class NeoAgentController extends ChangeNotifier {
2919
2972
  () => _backendClient.closeBrowser(backendUrl),
2920
2973
  browser: true,
2921
2974
  );
2975
+ browserScreenshotPath = null;
2976
+ notifyListeners();
2922
2977
  }
2923
2978
 
2924
2979
  Future<void> startAndroidRuntime() async {
@@ -4408,10 +4463,21 @@ class NeoAgentController extends ChangeNotifier {
4408
4463
  }
4409
4464
  }
4410
4465
 
4411
- Future<void> sendMessage(String task) async {
4466
+ Future<void> sendMessage(
4467
+ String task, {
4468
+ List<SharedChatAttachment> sharedAttachments =
4469
+ const <SharedChatAttachment>[],
4470
+ }) async {
4412
4471
  final trimmed = task.trim();
4472
+ final normalizedAttachments = sharedAttachments
4473
+ .where((item) => item.isValid)
4474
+ .toList(growable: false);
4475
+ final outgoingTask = _taskWithSharedAttachments(
4476
+ trimmed,
4477
+ normalizedAttachments,
4478
+ );
4413
4479
  final canSteerLiveRun = hasLiveRun && _socket != null && socketConnected;
4414
- if (trimmed.isEmpty || (isSendingMessage && !canSteerLiveRun)) {
4480
+ if (outgoingTask.isEmpty || (isSendingMessage && !canSteerLiveRun)) {
4415
4481
  return;
4416
4482
  }
4417
4483
  unawaited(
@@ -4424,9 +4490,20 @@ class NeoAgentController extends ChangeNotifier {
4424
4490
  final optimistic = ChatEntry(
4425
4491
  id: '',
4426
4492
  role: 'user',
4427
- content: trimmed,
4493
+ content: trimmed.isNotEmpty
4494
+ ? trimmed
4495
+ : (normalizedAttachments.isNotEmpty
4496
+ ? 'Sent shared attachments from mobile app.'
4497
+ : outgoingTask),
4428
4498
  platform: 'flutter',
4429
4499
  createdAt: DateTime.now(),
4500
+ metadata: normalizedAttachments.isEmpty
4501
+ ? const <String, dynamic>{}
4502
+ : <String, dynamic>{
4503
+ 'sharedAttachments': normalizedAttachments
4504
+ .map((item) => item.toJson())
4505
+ .toList(growable: false),
4506
+ },
4430
4507
  );
4431
4508
  chatMessages = <ChatEntry>[...chatMessages, optimistic];
4432
4509
  errorMessage = null;
@@ -4434,14 +4511,14 @@ class NeoAgentController extends ChangeNotifier {
4434
4511
  isSendingMessage = true;
4435
4512
  toolEvents = const <ToolEventItem>[];
4436
4513
  streamingAssistant = '';
4437
- activeRun = ActiveRunState.pending(trimmed);
4514
+ activeRun = ActiveRunState.pending(outgoingTask);
4438
4515
  }
4439
4516
  notifyListeners();
4440
4517
 
4441
4518
  try {
4442
4519
  if (_socket != null && socketConnected) {
4443
4520
  _socket!.emit('agent:run', <String, dynamic>{
4444
- 'task': trimmed,
4521
+ 'task': outgoingTask,
4445
4522
  'agentId': _scopedAgentId,
4446
4523
  'options': <String, dynamic>{'agentId': _scopedAgentId},
4447
4524
  });
@@ -4450,7 +4527,7 @@ class NeoAgentController extends ChangeNotifier {
4450
4527
 
4451
4528
  final response = await _backendClient.runTask(
4452
4529
  backendUrl,
4453
- trimmed,
4530
+ outgoingTask,
4454
4531
  agentId: _scopedAgentId,
4455
4532
  );
4456
4533
  final content = response['content']?.toString().trim();
@@ -4644,9 +4721,7 @@ class NeoAgentController extends ChangeNotifier {
4644
4721
  }
4645
4722
  }
4646
4723
 
4647
- Future<bool> updateAccountDisplayName({
4648
- required String displayName,
4649
- }) async {
4724
+ Future<bool> updateAccountDisplayName({required String displayName}) async {
4650
4725
  isSavingAccountSettings = true;
4651
4726
  errorMessage = null;
4652
4727
  notifyListeners();
@@ -5594,6 +5669,7 @@ class NeoAgentController extends ChangeNotifier {
5594
5669
  return;
5595
5670
  }
5596
5671
  _pendingChatDraft = normalized;
5672
+ _pendingSharedChatAttachments = const <SharedChatAttachment>[];
5597
5673
  if (!_isMobilePlatform) {
5598
5674
  setSelectedSection(AppSection.chat);
5599
5675
  } else {
@@ -5601,15 +5677,77 @@ class NeoAgentController extends ChangeNotifier {
5601
5677
  }
5602
5678
  }
5603
5679
 
5680
+ void queueSharedChatPayload({
5681
+ String? text,
5682
+ String? subject,
5683
+ List<Map<String, dynamic>> files = const <Map<String, dynamic>>[],
5684
+ }) {
5685
+ final attachments = files
5686
+ .map(SharedChatAttachment.fromJson)
5687
+ .where((item) => item.isValid)
5688
+ .toList(growable: false);
5689
+ final textPart = (text ?? '').toString().trim();
5690
+ final subjectPart = (subject ?? '').toString().trim();
5691
+ final combined = <String>[
5692
+ subjectPart,
5693
+ textPart,
5694
+ ].where((part) => part.isNotEmpty).join('\n').trim();
5695
+
5696
+ _pendingChatDraft = combined;
5697
+ _pendingSharedChatAttachments = attachments;
5698
+ setSelectedSection(AppSection.chat);
5699
+ }
5700
+
5701
+ bool get hasPendingSharedChatPayload =>
5702
+ (_pendingChatDraft?.trim().isNotEmpty ?? false) ||
5703
+ _pendingSharedChatAttachments.isNotEmpty;
5704
+
5604
5705
  bool get _isMobilePlatform =>
5605
5706
  !kIsWeb &&
5606
5707
  (defaultTargetPlatform == TargetPlatform.android ||
5607
5708
  defaultTargetPlatform == TargetPlatform.iOS);
5608
5709
 
5609
- String? takePendingChatDraft() {
5610
- final draft = _pendingChatDraft;
5710
+ String? peekPendingChatDraft() {
5711
+ final draft = _pendingChatDraft?.trim() ?? '';
5712
+ return draft.isEmpty ? null : draft;
5713
+ }
5714
+
5715
+ List<SharedChatAttachment> peekPendingSharedChatAttachments() {
5716
+ return List<SharedChatAttachment>.unmodifiable(
5717
+ _pendingSharedChatAttachments,
5718
+ );
5719
+ }
5720
+
5721
+ void clearPendingSharedChatPayload() {
5611
5722
  _pendingChatDraft = null;
5612
- return draft;
5723
+ _pendingSharedChatAttachments = const <SharedChatAttachment>[];
5724
+ }
5725
+
5726
+ String _taskWithSharedAttachments(
5727
+ String task,
5728
+ List<SharedChatAttachment> attachments,
5729
+ ) {
5730
+ final base = task.trim();
5731
+ if (attachments.isEmpty) {
5732
+ return base;
5733
+ }
5734
+ final lines = attachments
5735
+ .map((item) {
5736
+ final type = item.mimeType.trim().isEmpty
5737
+ ? 'unknown'
5738
+ : item.mimeType.trim();
5739
+ return '- ${item.name} ($type) [local uri: ${item.uri}]';
5740
+ })
5741
+ .join('\n');
5742
+ final attachmentBlock = [
5743
+ 'Shared attachments from mobile app:',
5744
+ lines,
5745
+ 'Use these for context. If the local URI is not directly accessible from the server, ask me to upload the file.',
5746
+ ].join('\n');
5747
+ if (base.isEmpty) {
5748
+ return attachmentBlock;
5749
+ }
5750
+ return '$base\n\n$attachmentBlock';
5613
5751
  }
5614
5752
 
5615
5753
  void selectWidget(String? widgetId) {
@@ -1321,6 +1321,32 @@ class VoiceAssistantTurnResult {
1321
1321
  final String? ttsError;
1322
1322
  }
1323
1323
 
1324
+ class MemoryTransferImportResult {
1325
+ const MemoryTransferImportResult({
1326
+ required this.importedCount,
1327
+ required this.skippedCount,
1328
+ required this.coreUpdatedCount,
1329
+ required this.behaviorNotesUpdated,
1330
+ required this.warnings,
1331
+ });
1332
+
1333
+ factory MemoryTransferImportResult.fromJson(Map<dynamic, dynamic> json) {
1334
+ return MemoryTransferImportResult(
1335
+ importedCount: _asInt(json['importedCount']),
1336
+ skippedCount: _asInt(json['skippedCount']),
1337
+ coreUpdatedCount: _asInt(json['coreUpdatedCount']),
1338
+ behaviorNotesUpdated: json['behaviorNotesUpdated'] == true,
1339
+ warnings: _jsonStringList(json['warnings']),
1340
+ );
1341
+ }
1342
+
1343
+ final int importedCount;
1344
+ final int skippedCount;
1345
+ final int coreUpdatedCount;
1346
+ final bool behaviorNotesUpdated;
1347
+ final List<String> warnings;
1348
+ }
1349
+
1324
1350
  class LiveVoiceBufferedChunk {
1325
1351
  LiveVoiceBufferedChunk({
1326
1352
  required this.sequence,
@@ -1864,6 +1890,49 @@ class ChatEntry {
1864
1890
  }
1865
1891
  }
1866
1892
 
1893
+ class SharedChatAttachment {
1894
+ const SharedChatAttachment({
1895
+ required this.uri,
1896
+ required this.name,
1897
+ required this.mimeType,
1898
+ this.sizeBytes,
1899
+ this.source = 'share_intent',
1900
+ });
1901
+
1902
+ factory SharedChatAttachment.fromJson(Map<dynamic, dynamic> json) {
1903
+ return SharedChatAttachment(
1904
+ uri: json['uri']?.toString() ?? '',
1905
+ name: json['name']?.toString() ?? 'Attachment',
1906
+ mimeType:
1907
+ json['mimeType']?.toString().ifEmpty('application/octet-stream') ??
1908
+ 'application/octet-stream',
1909
+ sizeBytes: json['sizeBytes'] is num
1910
+ ? (json['sizeBytes'] as num).toInt()
1911
+ : null,
1912
+ source:
1913
+ json['source']?.toString().ifEmpty('share_intent') ?? 'share_intent',
1914
+ );
1915
+ }
1916
+
1917
+ final String uri;
1918
+ final String name;
1919
+ final String mimeType;
1920
+ final int? sizeBytes;
1921
+ final String source;
1922
+
1923
+ Map<String, dynamic> toJson() {
1924
+ return <String, dynamic>{
1925
+ 'uri': uri,
1926
+ 'name': name,
1927
+ 'mimeType': mimeType,
1928
+ if (sizeBytes != null) 'sizeBytes': sizeBytes,
1929
+ 'source': source,
1930
+ };
1931
+ }
1932
+
1933
+ bool get isValid => uri.trim().isNotEmpty;
1934
+ }
1935
+
1867
1936
  class AgentProfile {
1868
1937
  const AgentProfile({
1869
1938
  required this.id,
@@ -1242,21 +1242,157 @@ class MemoryPanel extends StatefulWidget {
1242
1242
 
1243
1243
  class _MemoryPanelState extends State<MemoryPanel> {
1244
1244
  late final TextEditingController _searchController;
1245
+ late final TextEditingController _llmPromptController;
1246
+ late final TextEditingController _llmImportController;
1245
1247
  final Set<String> _selectedMemoryIds = <String>{};
1246
1248
  bool _bulkActionInFlight = false;
1249
+ bool _llmPromptLoading = false;
1250
+ bool _llmImporting = false;
1251
+ bool _llmApplyBehaviorNotes = true;
1252
+ bool _llmApplyCoreMemory = true;
1247
1253
 
1248
1254
  @override
1249
1255
  void initState() {
1250
1256
  super.initState();
1251
1257
  _searchController = TextEditingController();
1258
+ _llmPromptController = TextEditingController();
1259
+ _llmImportController = TextEditingController();
1252
1260
  }
1253
1261
 
1254
1262
  @override
1255
1263
  void dispose() {
1256
1264
  _searchController.dispose();
1265
+ _llmPromptController.dispose();
1266
+ _llmImportController.dispose();
1257
1267
  super.dispose();
1258
1268
  }
1259
1269
 
1270
+ Future<void> _loadLlmPrompt(NeoAgentController controller) async {
1271
+ if (_llmPromptLoading) {
1272
+ return;
1273
+ }
1274
+ setState(() {
1275
+ _llmPromptLoading = true;
1276
+ });
1277
+ try {
1278
+ final prompt = await controller.fetchMemoryTransferPrompt();
1279
+ if (!mounted) {
1280
+ return;
1281
+ }
1282
+ setState(() {
1283
+ _llmPromptController.text = prompt;
1284
+ });
1285
+ } catch (error) {
1286
+ if (!mounted) {
1287
+ return;
1288
+ }
1289
+ ScaffoldMessenger.of(context).showSnackBar(
1290
+ SnackBar(content: Text('Failed to generate prompt: $error')),
1291
+ );
1292
+ } finally {
1293
+ if (mounted) {
1294
+ setState(() {
1295
+ _llmPromptLoading = false;
1296
+ });
1297
+ }
1298
+ }
1299
+ }
1300
+
1301
+ Future<void> _copyLlmPrompt() async {
1302
+ final prompt = _llmPromptController.text.trim();
1303
+ if (prompt.isEmpty) {
1304
+ return;
1305
+ }
1306
+ await Clipboard.setData(ClipboardData(text: prompt));
1307
+ if (!mounted) {
1308
+ return;
1309
+ }
1310
+ ScaffoldMessenger.of(
1311
+ context,
1312
+ ).showSnackBar(const SnackBar(content: Text('Prompt copied.')));
1313
+ }
1314
+
1315
+ Future<void> _importLlmMemories(NeoAgentController controller) async {
1316
+ if (_llmImporting) {
1317
+ return;
1318
+ }
1319
+ final text = _llmImportController.text.trim();
1320
+ if (text.isEmpty) {
1321
+ return;
1322
+ }
1323
+ final confirmImport = await showDialog<bool>(
1324
+ context: context,
1325
+ builder: (context) {
1326
+ final applyTargets = <String>[
1327
+ if (_llmApplyBehaviorNotes) 'behavior notes',
1328
+ if (_llmApplyCoreMemory) 'core memory',
1329
+ 'memories',
1330
+ ];
1331
+ final targetLabel = applyTargets.join(', ');
1332
+ return AlertDialog(
1333
+ backgroundColor: _bgCard,
1334
+ title: Text('Import memory transfer?'),
1335
+ content: Text(
1336
+ 'This will import the response into $targetLabel.',
1337
+ ),
1338
+ actions: <Widget>[
1339
+ TextButton(
1340
+ onPressed: () => Navigator.of(context).pop(false),
1341
+ child: Text('Cancel'),
1342
+ ),
1343
+ FilledButton(
1344
+ onPressed: () => Navigator.of(context).pop(true),
1345
+ child: Text('Import'),
1346
+ ),
1347
+ ],
1348
+ );
1349
+ },
1350
+ );
1351
+ if (confirmImport != true) {
1352
+ return;
1353
+ }
1354
+ setState(() {
1355
+ _llmImporting = true;
1356
+ });
1357
+ try {
1358
+ final result = await controller.importMemoryTransfer(
1359
+ text,
1360
+ applyBehaviorNotes: _llmApplyBehaviorNotes,
1361
+ applyCoreMemory: _llmApplyCoreMemory,
1362
+ );
1363
+ if (!mounted) {
1364
+ return;
1365
+ }
1366
+ _llmImportController.clear();
1367
+ final warningText = result.warnings.isEmpty
1368
+ ? ''
1369
+ : ' ${result.warnings.join(' ')}';
1370
+ ScaffoldMessenger.of(context).showSnackBar(
1371
+ SnackBar(
1372
+ content: Text(
1373
+ 'Imported ${result.importedCount} memories, '
1374
+ '${result.coreUpdatedCount} core entries.'
1375
+ '${result.behaviorNotesUpdated ? ' Behavior notes updated.' : ''}'
1376
+ '$warningText',
1377
+ ),
1378
+ ),
1379
+ );
1380
+ } catch (error) {
1381
+ if (!mounted) {
1382
+ return;
1383
+ }
1384
+ ScaffoldMessenger.of(context).showSnackBar(
1385
+ SnackBar(content: Text('Import failed: $error')),
1386
+ );
1387
+ } finally {
1388
+ if (mounted) {
1389
+ setState(() {
1390
+ _llmImporting = false;
1391
+ });
1392
+ }
1393
+ }
1394
+ }
1395
+
1260
1396
  List<MemoryItem> get _visibleMemories {
1261
1397
  final controller = widget.controller;
1262
1398
  return controller.memoryRecallResults.isNotEmpty
@@ -1473,6 +1609,118 @@ class _MemoryPanelState extends State<MemoryPanel> {
1473
1609
  ),
1474
1610
  ),
1475
1611
  const SizedBox(height: 16),
1612
+ Card(
1613
+ child: Theme(
1614
+ data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
1615
+ child: ExpansionTile(
1616
+ initiallyExpanded: false,
1617
+ tilePadding: const EdgeInsets.symmetric(
1618
+ horizontal: 18,
1619
+ vertical: 6,
1620
+ ),
1621
+ childrenPadding: const EdgeInsets.fromLTRB(18, 0, 18, 18),
1622
+ leading: Icon(Icons.swap_horiz_outlined, color: _textSecondary),
1623
+ title: const _SectionTitle('LLM Memory Transfer'),
1624
+ subtitle: Text(
1625
+ 'Export/import memories with another AI in one shot.',
1626
+ style: TextStyle(color: _textSecondary),
1627
+ ),
1628
+ children: <Widget>[
1629
+ Text(
1630
+ 'Generate a prompt to use in another AI, then paste the response here to import memories.',
1631
+ style: TextStyle(color: _textSecondary),
1632
+ ),
1633
+ const SizedBox(height: 12),
1634
+ Wrap(
1635
+ spacing: 10,
1636
+ runSpacing: 10,
1637
+ children: <Widget>[
1638
+ FilledButton.icon(
1639
+ onPressed: _llmPromptLoading
1640
+ ? null
1641
+ : () => _loadLlmPrompt(controller),
1642
+ icon: Icon(Icons.auto_awesome_outlined),
1643
+ label: Text(
1644
+ _llmPromptLoading ? 'Generating...' : 'Generate Prompt',
1645
+ ),
1646
+ ),
1647
+ OutlinedButton.icon(
1648
+ onPressed: _llmPromptController.text.trim().isEmpty
1649
+ ? null
1650
+ : _copyLlmPrompt,
1651
+ icon: Icon(Icons.copy_all_outlined),
1652
+ label: Text('Copy Prompt'),
1653
+ ),
1654
+ ],
1655
+ ),
1656
+ const SizedBox(height: 12),
1657
+ TextField(
1658
+ controller: _llmPromptController,
1659
+ minLines: 6,
1660
+ maxLines: 10,
1661
+ readOnly: true,
1662
+ decoration: const InputDecoration(
1663
+ labelText: 'Prompt to paste into another AI',
1664
+ ),
1665
+ ),
1666
+ const SizedBox(height: 12),
1667
+ SwitchListTile.adaptive(
1668
+ contentPadding: EdgeInsets.zero,
1669
+ value: _llmApplyBehaviorNotes,
1670
+ onChanged: _llmImporting
1671
+ ? null
1672
+ : (value) {
1673
+ setState(() {
1674
+ _llmApplyBehaviorNotes = value;
1675
+ });
1676
+ },
1677
+ title: Text('Apply behavior notes'),
1678
+ subtitle: Text(
1679
+ 'Overwrite assistant behavior notes from the import.',
1680
+ ),
1681
+ ),
1682
+ SwitchListTile.adaptive(
1683
+ contentPadding: EdgeInsets.zero,
1684
+ value: _llmApplyCoreMemory,
1685
+ onChanged: _llmImporting
1686
+ ? null
1687
+ : (value) {
1688
+ setState(() {
1689
+ _llmApplyCoreMemory = value;
1690
+ });
1691
+ },
1692
+ title: Text('Apply core memory'),
1693
+ subtitle: Text(
1694
+ 'Update core memory key/value entries from the import.',
1695
+ ),
1696
+ ),
1697
+ const SizedBox(height: 16),
1698
+ Text(
1699
+ 'Paste the response from the other AI below, then import.',
1700
+ style: TextStyle(color: _textSecondary),
1701
+ ),
1702
+ const SizedBox(height: 12),
1703
+ TextField(
1704
+ controller: _llmImportController,
1705
+ minLines: 6,
1706
+ maxLines: 12,
1707
+ decoration: const InputDecoration(
1708
+ labelText: 'LLM memory export response',
1709
+ ),
1710
+ ),
1711
+ const SizedBox(height: 12),
1712
+ FilledButton.icon(
1713
+ onPressed: _llmImporting
1714
+ ? null
1715
+ : () => _importLlmMemories(controller),
1716
+ icon: Icon(Icons.file_download_outlined),
1717
+ label: Text(_llmImporting ? 'Importing...' : 'Import'),
1718
+ ),
1719
+ ],
1720
+ ),
1721
+ ),
1722
+ ),
1723
+ const SizedBox(height: 16),
1476
1724
  Card(
1477
1725
  child: Padding(
1478
1726
  padding: const EdgeInsets.all(18),
@@ -14,7 +14,7 @@ class _NeoAgentAppState extends State<NeoAgentApp>
14
14
  late final NeoAgentController _controller;
15
15
  late final WebAppUpdateMonitor _webAppUpdateMonitor;
16
16
  final AppLaunchBridge _appLaunchBridge = AppLaunchBridge();
17
- StreamSubscription<String>? _appLaunchSubscription;
17
+ StreamSubscription<AppLaunchRequest>? _appLaunchSubscription;
18
18
  StreamSubscription<String>? _widgetOpenSubscription;
19
19
  GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
20
20
  String? _navigatorScopeSignature;
@@ -93,9 +93,18 @@ class _NeoAgentAppState extends State<NeoAgentApp>
93
93
  unawaited(_syncDesktopShell());
94
94
  }
95
95
 
96
- void _handleAppLaunchRequest(String action) {
96
+ void _handleAppLaunchRequest(AppLaunchRequest request) {
97
+ final action = request.action;
97
98
  if (action == AppLaunchBridge.voiceAssistantAction) {
98
99
  _controller.openVoiceAssistantSurface();
100
+ return;
101
+ }
102
+ if (action == AppLaunchBridge.shareToChatAction) {
103
+ _controller.queueSharedChatPayload(
104
+ text: request.text,
105
+ subject: request.subject,
106
+ files: request.files,
107
+ );
99
108
  }
100
109
  }
101
110