neoagent 2.3.1-beta.85 → 2.3.1-beta.87
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.
- package/docs/capabilities.md +2 -0
- package/flutter_app/android/app/src/main/AndroidManifest.xml +14 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +84 -0
- package/flutter_app/lib/main_chat.dart +156 -2
- package/flutter_app/lib/main_controller.dart +137 -10
- package/flutter_app/lib/main_models.dart +69 -0
- package/flutter_app/lib/main_operations.dart +248 -0
- package/flutter_app/lib/main_runtime.dart +11 -2
- package/flutter_app/lib/main_settings.dart +173 -176
- package/flutter_app/lib/main_shared.dart +78 -0
- package/flutter_app/lib/src/app_launch_bridge.dart +39 -10
- package/flutter_app/lib/src/backend_client.dart +28 -0
- package/package.json +1 -1
- package/server/guest-agent.android.package.json +13 -0
- package/server/guest-agent.browser.package.json +14 -0
- package/server/guest_agent.js +61 -44
- package/server/http/routes.js +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +69936 -69277
- package/server/routes/android.js +2 -11
- package/server/routes/browser.js +2 -2
- package/server/routes/memory.js +90 -0
- package/server/routes/social_video.js +62 -0
- package/server/services/ai/capabilityHealth.js +6 -14
- package/server/services/ai/systemPrompt.js +1 -0
- package/server/services/ai/toolResult.js +20 -0
- package/server/services/ai/tools.js +29 -0
- package/server/services/android/android_bootstrap_worker.js +2 -2
- package/server/services/android/controller.js +528 -132
- package/server/services/browser/controller.js +51 -68
- package/server/services/manager.js +15 -0
- package/server/services/memory/llm_transfer.js +217 -0
- package/server/services/runtime/backends/local-vm.js +16 -3
- package/server/services/runtime/guest_bootstrap.js +224 -56
- package/server/services/runtime/manager.js +53 -15
- package/server/services/runtime/qemu.js +149 -24
- package/server/services/runtime/settings.js +9 -14
- package/server/services/runtime/validation.js +10 -11
- package/server/services/social_video/adapters/base.js +26 -0
- package/server/services/social_video/adapters/index.js +27 -0
- package/server/services/social_video/adapters/instagram.js +17 -0
- package/server/services/social_video/adapters/tiktok.js +17 -0
- package/server/services/social_video/adapters/x.js +17 -0
- package/server/services/social_video/adapters/youtube.js +17 -0
- package/server/services/social_video/captions.js +187 -0
- package/server/services/social_video/frame.js +42 -0
- package/server/services/social_video/index.js +7 -0
- package/server/services/social_video/metadata.js +63 -0
- package/server/services/social_video/result.js +63 -0
- package/server/services/social_video/service.js +576 -0
- package/server/services/social_video/url.js +83 -0
- package/server/utils/deployment.js +4 -4
- package/server/guest-agent.package.json +0 -15
package/docs/capabilities.md
CHANGED
|
@@ -33,6 +33,7 @@ Recording sessions support:
|
|
|
33
33
|
- Retry transcription and delete transcript segment actions.
|
|
34
34
|
- Full session deletion with storage cleanup.
|
|
35
35
|
- Agent tools for listing, opening, and searching transcripts: `recordings_list`, `recordings_get`, and `recordings_search`.
|
|
36
|
+
- A keyless social video extractor for public YouTube, TikTok, Instagram, and X URLs through `social_video_extract`, with metadata-first fetch, captions-first transcript strategy, STT fallback, and representative frame extraction.
|
|
36
37
|
|
|
37
38
|
Transcription uses Deepgram when `DEEPGRAM_API_KEY` is configured. The default speech model is `nova-3`, and the default language mode is `multi`. When `auto_recording_insights` is enabled in AI settings, NeoAgent can generate structured recording insights such as a summary, action items, and events.
|
|
38
39
|
|
|
@@ -102,6 +103,7 @@ NeoAgent's agent tool surface includes more than basic chat:
|
|
|
102
103
|
| Output | Generate markdown tables and Mermaid graphs |
|
|
103
104
|
| Images | Generate images with Grok and analyze local image files with a vision-capable model |
|
|
104
105
|
| Recordings | List, inspect, and search recording transcripts |
|
|
106
|
+
| Social video | Extract title, description, transcript, and a representative frame from public social video URLs |
|
|
105
107
|
| Health | Read synced mobile health metrics |
|
|
106
108
|
|
|
107
109
|
Generated binary or text artifacts can be promoted into user-scoped artifact storage under `~/.neoagent/data/artifacts` and served through authenticated `/api/artifacts/:id/content` URLs.
|
|
@@ -56,6 +56,20 @@
|
|
|
56
56
|
<action android:name="android.intent.action.MAIN"/>
|
|
57
57
|
<category android:name="android.intent.category.LAUNCHER"/>
|
|
58
58
|
</intent-filter>
|
|
59
|
+
<intent-filter>
|
|
60
|
+
<action android:name="android.intent.action.SEND" />
|
|
61
|
+
<category android:name="android.intent.category.DEFAULT" />
|
|
62
|
+
<data android:mimeType="text/*" />
|
|
63
|
+
<data android:mimeType="video/*" />
|
|
64
|
+
<data android:mimeType="image/*" />
|
|
65
|
+
<data android:mimeType="audio/*" />
|
|
66
|
+
<data android:mimeType="application/*" />
|
|
67
|
+
</intent-filter>
|
|
68
|
+
<intent-filter>
|
|
69
|
+
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
|
70
|
+
<category android:name="android.intent.category.DEFAULT" />
|
|
71
|
+
<data android:mimeType="*/*" />
|
|
72
|
+
</intent-filter>
|
|
59
73
|
</activity>
|
|
60
74
|
<provider
|
|
61
75
|
android:name="androidx.core.content.FileProvider"
|
|
@@ -8,6 +8,7 @@ import android.net.Uri
|
|
|
8
8
|
import android.os.BatteryManager
|
|
9
9
|
import android.os.Build
|
|
10
10
|
import android.provider.Settings
|
|
11
|
+
import android.provider.OpenableColumns
|
|
11
12
|
import android.view.KeyEvent
|
|
12
13
|
import androidx.activity.result.ActivityResultLauncher
|
|
13
14
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
@@ -55,6 +56,7 @@ class MainActivity : FlutterFragmentActivity() {
|
|
|
55
56
|
private var widgetEventSink: EventChannel.EventSink? = null
|
|
56
57
|
private var appLaunchEventSink: EventChannel.EventSink? = null
|
|
57
58
|
private var pendingAppLaunchAction: String? = null
|
|
59
|
+
private var pendingSharePayload: Map<String, Any?>? = null
|
|
58
60
|
|
|
59
61
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
|
60
62
|
super.configureFlutterEngine(flutterEngine)
|
|
@@ -590,6 +592,7 @@ class MainActivity : FlutterFragmentActivity() {
|
|
|
590
592
|
|
|
591
593
|
captureWidgetIntent(intent)
|
|
592
594
|
captureAppLaunchIntent(intent)
|
|
595
|
+
captureShareIntent(intent)
|
|
593
596
|
}
|
|
594
597
|
|
|
595
598
|
override fun onNewIntent(intent: Intent) {
|
|
@@ -597,6 +600,7 @@ class MainActivity : FlutterFragmentActivity() {
|
|
|
597
600
|
setIntent(intent)
|
|
598
601
|
captureWidgetIntent(intent)
|
|
599
602
|
captureAppLaunchIntent(intent)
|
|
603
|
+
captureShareIntent(intent)
|
|
600
604
|
}
|
|
601
605
|
|
|
602
606
|
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
|
@@ -723,11 +727,91 @@ class MainActivity : FlutterFragmentActivity() {
|
|
|
723
727
|
|
|
724
728
|
private fun emitPendingAppLaunchIntent() {
|
|
725
729
|
val sink = appLaunchEventSink ?: return
|
|
730
|
+
val sharePayload = pendingSharePayload
|
|
731
|
+
if (sharePayload != null) {
|
|
732
|
+
pendingSharePayload = null
|
|
733
|
+
sink.success(sharePayload)
|
|
734
|
+
return
|
|
735
|
+
}
|
|
726
736
|
val action = pendingAppLaunchAction ?: return
|
|
727
737
|
pendingAppLaunchAction = null
|
|
728
738
|
sink.success(mapOf("action" to action))
|
|
729
739
|
}
|
|
730
740
|
|
|
741
|
+
private fun captureShareIntent(intent: Intent?) {
|
|
742
|
+
if (intent == null) {
|
|
743
|
+
return
|
|
744
|
+
}
|
|
745
|
+
if (intent.action != Intent.ACTION_SEND && intent.action != Intent.ACTION_SEND_MULTIPLE) {
|
|
746
|
+
return
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)?.trim().orEmpty()
|
|
750
|
+
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)?.trim().orEmpty()
|
|
751
|
+
val fileUris = mutableListOf<Uri>()
|
|
752
|
+
|
|
753
|
+
@Suppress("DEPRECATION")
|
|
754
|
+
val single = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
|
|
755
|
+
if (single != null) {
|
|
756
|
+
fileUris.add(single)
|
|
757
|
+
}
|
|
758
|
+
@Suppress("DEPRECATION")
|
|
759
|
+
val multiple = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
|
|
760
|
+
if (multiple != null) {
|
|
761
|
+
fileUris.addAll(multiple)
|
|
762
|
+
}
|
|
763
|
+
val clip = intent.clipData
|
|
764
|
+
if (clip != null) {
|
|
765
|
+
for (index in 0 until clip.itemCount) {
|
|
766
|
+
val uri = clip.getItemAt(index)?.uri ?: continue
|
|
767
|
+
fileUris.add(uri)
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
val uniqueUris = linkedSetOf<String>()
|
|
772
|
+
val files = mutableListOf<Map<String, Any?>>()
|
|
773
|
+
for (uri in fileUris) {
|
|
774
|
+
val key = uri.toString()
|
|
775
|
+
if (!uniqueUris.add(key)) continue
|
|
776
|
+
files.add(buildSharedFileDescriptor(uri))
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
pendingSharePayload = mapOf(
|
|
780
|
+
"action" to "share_to_chat",
|
|
781
|
+
"text" to sharedText,
|
|
782
|
+
"subject" to subject,
|
|
783
|
+
"files" to files,
|
|
784
|
+
)
|
|
785
|
+
emitPendingAppLaunchIntent()
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
private fun buildSharedFileDescriptor(uri: Uri): Map<String, Any?> {
|
|
789
|
+
var name: String? = null
|
|
790
|
+
var sizeBytes: Long? = null
|
|
791
|
+
val type = contentResolver.getType(uri)?.trim().orEmpty()
|
|
792
|
+
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
|
793
|
+
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
|
794
|
+
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
|
|
795
|
+
if (cursor.moveToFirst()) {
|
|
796
|
+
if (nameIndex >= 0) {
|
|
797
|
+
name = cursor.getString(nameIndex)?.trim()
|
|
798
|
+
}
|
|
799
|
+
if (sizeIndex >= 0 && !cursor.isNull(sizeIndex)) {
|
|
800
|
+
sizeBytes = cursor.getLong(sizeIndex)
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
val fallbackName = uri.lastPathSegment?.substringAfterLast('/')?.trim().orEmpty()
|
|
805
|
+
val resolvedName = (name ?: fallbackName).ifBlank { "Attachment" }
|
|
806
|
+
return mapOf(
|
|
807
|
+
"uri" to uri.toString(),
|
|
808
|
+
"name" to resolvedName,
|
|
809
|
+
"mimeType" to if (type.isBlank()) "application/octet-stream" else type,
|
|
810
|
+
"sizeBytes" to sizeBytes,
|
|
811
|
+
"source" to "android_share_intent",
|
|
812
|
+
)
|
|
813
|
+
}
|
|
814
|
+
|
|
731
815
|
private fun buildVolumeState(audioManager: AudioManager): Map<String, Any> {
|
|
732
816
|
val minVolume =
|
|
733
817
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
@@ -12,6 +12,8 @@ class ChatPanel extends StatefulWidget {
|
|
|
12
12
|
class _ChatPanelState extends State<ChatPanel> {
|
|
13
13
|
late final TextEditingController _composerController;
|
|
14
14
|
final ScrollController _scrollController = ScrollController();
|
|
15
|
+
List<SharedChatAttachment> _pendingSharedAttachments =
|
|
16
|
+
const <SharedChatAttachment>[];
|
|
15
17
|
int _lastMessageCount = 0;
|
|
16
18
|
int _lastToolCount = 0;
|
|
17
19
|
String _lastStream = '';
|
|
@@ -35,12 +37,23 @@ class _ChatPanelState extends State<ChatPanel> {
|
|
|
35
37
|
|
|
36
38
|
void _consumeQueuedDraft() {
|
|
37
39
|
final draft = widget.controller.takePendingChatDraft();
|
|
40
|
+
final attachments = widget.controller.takePendingSharedChatAttachments();
|
|
38
41
|
if (draft == null || draft.isEmpty) {
|
|
42
|
+
if (attachments.isNotEmpty) {
|
|
43
|
+
setState(() {
|
|
44
|
+
_pendingSharedAttachments = attachments;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
39
47
|
return;
|
|
40
48
|
}
|
|
41
49
|
_composerController
|
|
42
50
|
..text = draft
|
|
43
51
|
..selection = TextSelection.collapsed(offset: draft.length);
|
|
52
|
+
if (attachments.isNotEmpty) {
|
|
53
|
+
setState(() {
|
|
54
|
+
_pendingSharedAttachments = attachments;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
void _scrollToBottom() {
|
|
@@ -151,6 +164,29 @@ class _ChatPanelState extends State<ChatPanel> {
|
|
|
151
164
|
),
|
|
152
165
|
child: Column(
|
|
153
166
|
children: <Widget>[
|
|
167
|
+
if (_pendingSharedAttachments.isNotEmpty)
|
|
168
|
+
Padding(
|
|
169
|
+
padding: const EdgeInsets.only(bottom: 10),
|
|
170
|
+
child: _SharedAttachmentTray(
|
|
171
|
+
attachments: _pendingSharedAttachments,
|
|
172
|
+
onRemoveAt: (index) {
|
|
173
|
+
setState(() {
|
|
174
|
+
_pendingSharedAttachments = _pendingSharedAttachments
|
|
175
|
+
.asMap()
|
|
176
|
+
.entries
|
|
177
|
+
.where((entry) => entry.key != index)
|
|
178
|
+
.map((entry) => entry.value)
|
|
179
|
+
.toList(growable: false);
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
onClear: () {
|
|
183
|
+
setState(() {
|
|
184
|
+
_pendingSharedAttachments =
|
|
185
|
+
const <SharedChatAttachment>[];
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
),
|
|
189
|
+
),
|
|
154
190
|
Container(
|
|
155
191
|
padding: const EdgeInsets.fromLTRB(16, 4, 4, 4),
|
|
156
192
|
decoration: BoxDecoration(
|
|
@@ -199,7 +235,8 @@ class _ChatPanelState extends State<ChatPanel> {
|
|
|
199
235
|
? null
|
|
200
236
|
: () async {
|
|
201
237
|
final task = _composerController.text;
|
|
202
|
-
if (task.trim().isEmpty
|
|
238
|
+
if ((task.trim().isEmpty &&
|
|
239
|
+
_pendingSharedAttachments.isEmpty) ||
|
|
203
240
|
_isSendingChatMessage) {
|
|
204
241
|
return;
|
|
205
242
|
}
|
|
@@ -207,8 +244,17 @@ class _ChatPanelState extends State<ChatPanel> {
|
|
|
207
244
|
_isSendingChatMessage = true;
|
|
208
245
|
});
|
|
209
246
|
_composerController.clear();
|
|
247
|
+
final outgoingAttachments =
|
|
248
|
+
_pendingSharedAttachments;
|
|
249
|
+
setState(() {
|
|
250
|
+
_pendingSharedAttachments =
|
|
251
|
+
const <SharedChatAttachment>[];
|
|
252
|
+
});
|
|
210
253
|
try {
|
|
211
|
-
await controller.sendMessage(
|
|
254
|
+
await controller.sendMessage(
|
|
255
|
+
task,
|
|
256
|
+
sharedAttachments: outgoingAttachments,
|
|
257
|
+
);
|
|
212
258
|
} finally {
|
|
213
259
|
if (mounted) {
|
|
214
260
|
setState(() {
|
|
@@ -268,6 +314,114 @@ class _ChatPanelState extends State<ChatPanel> {
|
|
|
268
314
|
}
|
|
269
315
|
}
|
|
270
316
|
|
|
317
|
+
class _SharedAttachmentTray extends StatelessWidget {
|
|
318
|
+
const _SharedAttachmentTray({
|
|
319
|
+
required this.attachments,
|
|
320
|
+
required this.onRemoveAt,
|
|
321
|
+
required this.onClear,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
final List<SharedChatAttachment> attachments;
|
|
325
|
+
final void Function(int index) onRemoveAt;
|
|
326
|
+
final VoidCallback onClear;
|
|
327
|
+
|
|
328
|
+
IconData _iconForMime(String mime) {
|
|
329
|
+
final normalized = mime.toLowerCase();
|
|
330
|
+
if (normalized.startsWith('image/')) return Icons.image_outlined;
|
|
331
|
+
if (normalized.startsWith('video/')) return Icons.videocam_outlined;
|
|
332
|
+
if (normalized.startsWith('audio/')) return Icons.audiotrack_outlined;
|
|
333
|
+
if (normalized.contains('pdf')) return Icons.picture_as_pdf_outlined;
|
|
334
|
+
return Icons.attach_file_rounded;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
@override
|
|
338
|
+
Widget build(BuildContext context) {
|
|
339
|
+
return Container(
|
|
340
|
+
width: double.infinity,
|
|
341
|
+
padding: const EdgeInsets.all(10),
|
|
342
|
+
decoration: BoxDecoration(
|
|
343
|
+
color: _bgSecondary,
|
|
344
|
+
borderRadius: BorderRadius.circular(12),
|
|
345
|
+
border: Border.all(color: _borderLight),
|
|
346
|
+
),
|
|
347
|
+
child: Column(
|
|
348
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
349
|
+
children: <Widget>[
|
|
350
|
+
Row(
|
|
351
|
+
children: <Widget>[
|
|
352
|
+
Icon(Icons.forward_to_inbox_outlined, size: 15, color: _info),
|
|
353
|
+
const SizedBox(width: 6),
|
|
354
|
+
Expanded(
|
|
355
|
+
child: Text(
|
|
356
|
+
'Shared from another app',
|
|
357
|
+
style: TextStyle(
|
|
358
|
+
color: _textPrimary,
|
|
359
|
+
fontSize: 12,
|
|
360
|
+
fontWeight: FontWeight.w700,
|
|
361
|
+
),
|
|
362
|
+
),
|
|
363
|
+
),
|
|
364
|
+
TextButton(onPressed: onClear, child: const Text('Clear')),
|
|
365
|
+
],
|
|
366
|
+
),
|
|
367
|
+
const SizedBox(height: 6),
|
|
368
|
+
Wrap(
|
|
369
|
+
spacing: 8,
|
|
370
|
+
runSpacing: 8,
|
|
371
|
+
children: attachments
|
|
372
|
+
.asMap()
|
|
373
|
+
.entries
|
|
374
|
+
.map((entry) {
|
|
375
|
+
final item = entry.value;
|
|
376
|
+
return Container(
|
|
377
|
+
padding: const EdgeInsets.symmetric(
|
|
378
|
+
horizontal: 10,
|
|
379
|
+
vertical: 7,
|
|
380
|
+
),
|
|
381
|
+
decoration: BoxDecoration(
|
|
382
|
+
color: _bgCard,
|
|
383
|
+
borderRadius: BorderRadius.circular(999),
|
|
384
|
+
border: Border.all(color: _border),
|
|
385
|
+
),
|
|
386
|
+
child: Row(
|
|
387
|
+
mainAxisSize: MainAxisSize.min,
|
|
388
|
+
children: <Widget>[
|
|
389
|
+
Icon(
|
|
390
|
+
_iconForMime(item.mimeType),
|
|
391
|
+
size: 14,
|
|
392
|
+
color: _textSecondary,
|
|
393
|
+
),
|
|
394
|
+
const SizedBox(width: 6),
|
|
395
|
+
ConstrainedBox(
|
|
396
|
+
constraints: const BoxConstraints(maxWidth: 170),
|
|
397
|
+
child: Text(
|
|
398
|
+
item.name,
|
|
399
|
+
maxLines: 1,
|
|
400
|
+
overflow: TextOverflow.ellipsis,
|
|
401
|
+
style: TextStyle(color: _textPrimary, fontSize: 12),
|
|
402
|
+
),
|
|
403
|
+
),
|
|
404
|
+
const SizedBox(width: 6),
|
|
405
|
+
GestureDetector(
|
|
406
|
+
onTap: () => onRemoveAt(entry.key),
|
|
407
|
+
child: Icon(
|
|
408
|
+
Icons.close_rounded,
|
|
409
|
+
size: 14,
|
|
410
|
+
color: _textMuted,
|
|
411
|
+
),
|
|
412
|
+
),
|
|
413
|
+
],
|
|
414
|
+
),
|
|
415
|
+
);
|
|
416
|
+
})
|
|
417
|
+
.toList(growable: false),
|
|
418
|
+
),
|
|
419
|
+
],
|
|
420
|
+
),
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
271
425
|
class _TypingIndicatorBubble extends StatefulWidget {
|
|
272
426
|
const _TypingIndicatorBubble();
|
|
273
427
|
|
|
@@ -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
|
-
?
|
|
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',
|
|
@@ -4408,10 +4460,21 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
4408
4460
|
}
|
|
4409
4461
|
}
|
|
4410
4462
|
|
|
4411
|
-
Future<void> sendMessage(
|
|
4463
|
+
Future<void> sendMessage(
|
|
4464
|
+
String task, {
|
|
4465
|
+
List<SharedChatAttachment> sharedAttachments =
|
|
4466
|
+
const <SharedChatAttachment>[],
|
|
4467
|
+
}) async {
|
|
4412
4468
|
final trimmed = task.trim();
|
|
4469
|
+
final normalizedAttachments = sharedAttachments
|
|
4470
|
+
.where((item) => item.isValid)
|
|
4471
|
+
.toList(growable: false);
|
|
4472
|
+
final outgoingTask = _taskWithSharedAttachments(
|
|
4473
|
+
trimmed,
|
|
4474
|
+
normalizedAttachments,
|
|
4475
|
+
);
|
|
4413
4476
|
final canSteerLiveRun = hasLiveRun && _socket != null && socketConnected;
|
|
4414
|
-
if (
|
|
4477
|
+
if (outgoingTask.isEmpty || (isSendingMessage && !canSteerLiveRun)) {
|
|
4415
4478
|
return;
|
|
4416
4479
|
}
|
|
4417
4480
|
unawaited(
|
|
@@ -4424,9 +4487,20 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
4424
4487
|
final optimistic = ChatEntry(
|
|
4425
4488
|
id: '',
|
|
4426
4489
|
role: 'user',
|
|
4427
|
-
content: trimmed
|
|
4490
|
+
content: trimmed.isNotEmpty
|
|
4491
|
+
? trimmed
|
|
4492
|
+
: (normalizedAttachments.isNotEmpty
|
|
4493
|
+
? 'Sent shared attachments from mobile app.'
|
|
4494
|
+
: outgoingTask),
|
|
4428
4495
|
platform: 'flutter',
|
|
4429
4496
|
createdAt: DateTime.now(),
|
|
4497
|
+
metadata: normalizedAttachments.isEmpty
|
|
4498
|
+
? const <String, dynamic>{}
|
|
4499
|
+
: <String, dynamic>{
|
|
4500
|
+
'sharedAttachments': normalizedAttachments
|
|
4501
|
+
.map((item) => item.toJson())
|
|
4502
|
+
.toList(growable: false),
|
|
4503
|
+
},
|
|
4430
4504
|
);
|
|
4431
4505
|
chatMessages = <ChatEntry>[...chatMessages, optimistic];
|
|
4432
4506
|
errorMessage = null;
|
|
@@ -4434,14 +4508,14 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
4434
4508
|
isSendingMessage = true;
|
|
4435
4509
|
toolEvents = const <ToolEventItem>[];
|
|
4436
4510
|
streamingAssistant = '';
|
|
4437
|
-
activeRun = ActiveRunState.pending(
|
|
4511
|
+
activeRun = ActiveRunState.pending(outgoingTask);
|
|
4438
4512
|
}
|
|
4439
4513
|
notifyListeners();
|
|
4440
4514
|
|
|
4441
4515
|
try {
|
|
4442
4516
|
if (_socket != null && socketConnected) {
|
|
4443
4517
|
_socket!.emit('agent:run', <String, dynamic>{
|
|
4444
|
-
'task':
|
|
4518
|
+
'task': outgoingTask,
|
|
4445
4519
|
'agentId': _scopedAgentId,
|
|
4446
4520
|
'options': <String, dynamic>{'agentId': _scopedAgentId},
|
|
4447
4521
|
});
|
|
@@ -4450,7 +4524,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
4450
4524
|
|
|
4451
4525
|
final response = await _backendClient.runTask(
|
|
4452
4526
|
backendUrl,
|
|
4453
|
-
|
|
4527
|
+
outgoingTask,
|
|
4454
4528
|
agentId: _scopedAgentId,
|
|
4455
4529
|
);
|
|
4456
4530
|
final content = response['content']?.toString().trim();
|
|
@@ -4644,9 +4718,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
4644
4718
|
}
|
|
4645
4719
|
}
|
|
4646
4720
|
|
|
4647
|
-
Future<bool> updateAccountDisplayName({
|
|
4648
|
-
required String displayName,
|
|
4649
|
-
}) async {
|
|
4721
|
+
Future<bool> updateAccountDisplayName({required String displayName}) async {
|
|
4650
4722
|
isSavingAccountSettings = true;
|
|
4651
4723
|
errorMessage = null;
|
|
4652
4724
|
notifyListeners();
|
|
@@ -5594,6 +5666,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
5594
5666
|
return;
|
|
5595
5667
|
}
|
|
5596
5668
|
_pendingChatDraft = normalized;
|
|
5669
|
+
_pendingSharedChatAttachments = const <SharedChatAttachment>[];
|
|
5597
5670
|
if (!_isMobilePlatform) {
|
|
5598
5671
|
setSelectedSection(AppSection.chat);
|
|
5599
5672
|
} else {
|
|
@@ -5601,6 +5674,27 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
5601
5674
|
}
|
|
5602
5675
|
}
|
|
5603
5676
|
|
|
5677
|
+
void queueSharedChatPayload({
|
|
5678
|
+
String? text,
|
|
5679
|
+
String? subject,
|
|
5680
|
+
List<Map<String, dynamic>> files = const <Map<String, dynamic>>[],
|
|
5681
|
+
}) {
|
|
5682
|
+
final attachments = files
|
|
5683
|
+
.map(SharedChatAttachment.fromJson)
|
|
5684
|
+
.where((item) => item.isValid)
|
|
5685
|
+
.toList(growable: false);
|
|
5686
|
+
final textPart = (text ?? '').toString().trim();
|
|
5687
|
+
final subjectPart = (subject ?? '').toString().trim();
|
|
5688
|
+
final combined = <String>[
|
|
5689
|
+
subjectPart,
|
|
5690
|
+
textPart,
|
|
5691
|
+
].where((part) => part.isNotEmpty).join('\n').trim();
|
|
5692
|
+
|
|
5693
|
+
_pendingChatDraft = combined;
|
|
5694
|
+
_pendingSharedChatAttachments = attachments;
|
|
5695
|
+
setSelectedSection(AppSection.chat);
|
|
5696
|
+
}
|
|
5697
|
+
|
|
5604
5698
|
bool get _isMobilePlatform =>
|
|
5605
5699
|
!kIsWeb &&
|
|
5606
5700
|
(defaultTargetPlatform == TargetPlatform.android ||
|
|
@@ -5612,6 +5706,39 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
5612
5706
|
return draft;
|
|
5613
5707
|
}
|
|
5614
5708
|
|
|
5709
|
+
List<SharedChatAttachment> takePendingSharedChatAttachments() {
|
|
5710
|
+
final pending = _pendingSharedChatAttachments;
|
|
5711
|
+
_pendingSharedChatAttachments = const <SharedChatAttachment>[];
|
|
5712
|
+
return pending;
|
|
5713
|
+
}
|
|
5714
|
+
|
|
5715
|
+
String _taskWithSharedAttachments(
|
|
5716
|
+
String task,
|
|
5717
|
+
List<SharedChatAttachment> attachments,
|
|
5718
|
+
) {
|
|
5719
|
+
final base = task.trim();
|
|
5720
|
+
if (attachments.isEmpty) {
|
|
5721
|
+
return base;
|
|
5722
|
+
}
|
|
5723
|
+
final lines = attachments
|
|
5724
|
+
.map((item) {
|
|
5725
|
+
final type = item.mimeType.trim().isEmpty
|
|
5726
|
+
? 'unknown'
|
|
5727
|
+
: item.mimeType.trim();
|
|
5728
|
+
return '- ${item.name} ($type) [local uri: ${item.uri}]';
|
|
5729
|
+
})
|
|
5730
|
+
.join('\n');
|
|
5731
|
+
final attachmentBlock = [
|
|
5732
|
+
'Shared attachments from mobile app:',
|
|
5733
|
+
lines,
|
|
5734
|
+
'Use these for context. If the local URI is not directly accessible from the server, ask me to upload the file.',
|
|
5735
|
+
].join('\n');
|
|
5736
|
+
if (base.isEmpty) {
|
|
5737
|
+
return attachmentBlock;
|
|
5738
|
+
}
|
|
5739
|
+
return '$base\n\n$attachmentBlock';
|
|
5740
|
+
}
|
|
5741
|
+
|
|
5615
5742
|
void selectWidget(String? widgetId) {
|
|
5616
5743
|
if (widgetId != null && !widgets.any((widget) => widget.id == widgetId)) {
|
|
5617
5744
|
return;
|
|
@@ -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,
|