neoagent 2.4.1-beta.9 → 2.4.1

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 (234) hide show
  1. package/.env.example +33 -3
  2. package/LICENSE +111 -56
  3. package/README.md +8 -3
  4. package/docs/configuration.md +8 -0
  5. package/docs/getting-started.md +9 -3
  6. package/docs/index.md +4 -0
  7. package/extensions/chrome-browser/background.mjs +36 -4
  8. package/extensions/chrome-browser/icons/icon128.png +0 -0
  9. package/extensions/chrome-browser/icons/icon16.png +0 -0
  10. package/extensions/chrome-browser/icons/icon48.png +0 -0
  11. package/extensions/chrome-browser/icons/logo.svg +39 -8
  12. package/extensions/chrome-browser/manifest.json +3 -3
  13. package/extensions/chrome-browser/popup.html +5 -1
  14. package/extensions/chrome-browser/popup.js +13 -0
  15. package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
  16. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  17. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  18. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  19. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  20. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  21. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  22. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  23. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  24. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  25. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  26. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  27. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  28. package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
  29. package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
  30. package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
  31. package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
  32. package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
  33. package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
  34. package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
  35. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  36. package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
  37. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  38. package/flutter_app/lib/features/location/location_service.dart +3 -0
  39. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
  40. package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
  41. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
  42. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
  43. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
  44. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
  45. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
  46. package/flutter_app/lib/main.dart +2 -0
  47. package/flutter_app/lib/main_account_settings.dart +10 -34
  48. package/flutter_app/lib/main_admin.dart +14 -1
  49. package/flutter_app/lib/main_app_shell.dart +377 -340
  50. package/flutter_app/lib/main_chat.dart +707 -227
  51. package/flutter_app/lib/main_controller.dart +197 -42
  52. package/flutter_app/lib/main_devices.dart +436 -4
  53. package/flutter_app/lib/main_integrations.dart +255 -6
  54. package/flutter_app/lib/main_launcher.dart +1 -1
  55. package/flutter_app/lib/main_model_picker.dart +685 -0
  56. package/flutter_app/lib/main_models.dart +212 -0
  57. package/flutter_app/lib/main_navigation.dart +1 -9
  58. package/flutter_app/lib/main_operations.dart +1417 -614
  59. package/flutter_app/lib/main_settings.dart +673 -871
  60. package/flutter_app/lib/main_shared.dart +971 -443
  61. package/flutter_app/lib/main_spacing.dart +4 -4
  62. package/flutter_app/lib/main_theme.dart +22 -14
  63. package/flutter_app/lib/main_unified.dart +5 -72
  64. package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
  65. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
  66. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
  67. package/flutter_app/lib/src/backend_client.dart +46 -0
  68. package/flutter_app/lib/src/desktop_companion_actions.dart +30 -7
  69. package/flutter_app/lib/src/desktop_companion_io.dart +71 -1
  70. package/flutter_app/lib/src/security/password_strength.dart +84 -0
  71. package/flutter_app/lib/src/stream_renderer.dart +42 -3
  72. package/flutter_app/lib/src/theme/palette.dart +76 -34
  73. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  74. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  75. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  76. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  77. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  78. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  79. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  80. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  81. package/flutter_app/pubspec.lock +2 -2
  82. package/flutter_app/pubspec.yaml +7 -1
  83. package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
  84. package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
  85. package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
  86. package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
  87. package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
  88. package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
  89. package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
  90. package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
  91. package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
  92. package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
  93. package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
  94. package/flutter_app/tool/generate_desktop_branding.py +154 -152
  95. package/flutter_app/web/favicon.png +0 -0
  96. package/flutter_app/web/favicon.svg +40 -9
  97. package/flutter_app/web/favicon_light.svg +43 -0
  98. package/flutter_app/web/icons/Icon-192-light.png +0 -0
  99. package/flutter_app/web/icons/Icon-192.png +0 -0
  100. package/flutter_app/web/icons/Icon-512-light.png +0 -0
  101. package/flutter_app/web/icons/Icon-512.png +0 -0
  102. package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
  103. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  104. package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
  105. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  106. package/flutter_app/windows/runner/main.cpp +7 -1
  107. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  108. package/lib/manager.js +445 -83
  109. package/package.json +17 -3
  110. package/runtime/paths.js +3 -1
  111. package/server/admin/access.js +198 -0
  112. package/server/admin/admin.css +268 -0
  113. package/server/admin/admin.js +348 -0
  114. package/server/admin/analytics.js +128 -0
  115. package/server/admin/index.html +1015 -0
  116. package/server/admin/login.html +290 -0
  117. package/server/admin/logo.svg +43 -0
  118. package/server/admin/sql.js +134 -0
  119. package/server/admin/users.js +147 -0
  120. package/server/config/origins.js +3 -1
  121. package/server/db/database.js +92 -0
  122. package/server/db/ftsQuery.js +27 -0
  123. package/server/http/routes.js +1 -0
  124. package/server/http/static.js +23 -6
  125. package/server/index.js +1 -1
  126. package/server/middleware/adminAuth.js +48 -0
  127. package/server/middleware/auth.js +1 -40
  128. package/server/public/.last_build_id +1 -1
  129. package/server/public/app_icon.png +0 -0
  130. package/server/public/assets/AssetManifest.bin +1 -1
  131. package/server/public/assets/AssetManifest.bin.json +1 -1
  132. package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
  133. package/server/public/assets/assets/branding/app_icon_256.png +0 -0
  134. package/server/public/assets/assets/branding/app_icon_512.png +0 -0
  135. package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
  136. package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
  137. package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
  138. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  139. package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
  140. package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
  141. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  142. package/server/public/assets/web/icons/Icon-192.png +0 -0
  143. package/server/public/favicon.png +0 -0
  144. package/server/public/favicon.svg +40 -9
  145. package/server/public/favicon_light.svg +43 -0
  146. package/server/public/flutter_bootstrap.js +2 -2
  147. package/server/public/icons/Icon-192-light.png +0 -0
  148. package/server/public/icons/Icon-192.png +0 -0
  149. package/server/public/icons/Icon-512-light.png +0 -0
  150. package/server/public/icons/Icon-512.png +0 -0
  151. package/server/public/icons/Icon-maskable-192-light.png +0 -0
  152. package/server/public/icons/Icon-maskable-192.png +0 -0
  153. package/server/public/icons/Icon-maskable-512-light.png +0 -0
  154. package/server/public/icons/Icon-maskable-512.png +0 -0
  155. package/server/public/main.dart.js +88537 -85936
  156. package/server/routes/admin.js +632 -0
  157. package/server/routes/agent_profiles.js +3 -0
  158. package/server/routes/agents.js +1 -1
  159. package/server/routes/auth.js +3 -1
  160. package/server/routes/mcp.js +29 -13
  161. package/server/routes/memory.js +23 -1
  162. package/server/routes/messaging.js +2 -0
  163. package/server/routes/screenHistory.js +14 -8
  164. package/server/routes/settings.js +10 -2
  165. package/server/routes/stream.js +1 -1
  166. package/server/routes/triggers.js +4 -4
  167. package/server/routes/voice_assistant.js +36 -1
  168. package/server/routes/workspace.js +86 -0
  169. package/server/services/account/admin_two_factor.js +132 -0
  170. package/server/services/account/password_policy.js +6 -1
  171. package/server/services/ai/completion.js +44 -0
  172. package/server/services/ai/engine.js +321 -500
  173. package/server/services/ai/imageAnalysis.js +9 -5
  174. package/server/services/ai/logFormat.js +46 -0
  175. package/server/services/ai/loopPolicy.js +11 -0
  176. package/server/services/ai/messagingFallback.js +228 -0
  177. package/server/services/ai/models.js +194 -55
  178. package/server/services/ai/providerRetry.js +169 -0
  179. package/server/services/ai/providers/anthropic.js +15 -4
  180. package/server/services/ai/providers/google.js +38 -11
  181. package/server/services/ai/providers/grok.js +19 -68
  182. package/server/services/ai/providers/grokOauth.js +141 -0
  183. package/server/services/ai/providers/nvidia.js +154 -0
  184. package/server/services/ai/providers/ollama.js +76 -39
  185. package/server/services/ai/providers/openai.js +20 -31
  186. package/server/services/ai/providers/openaiCodex.js +6 -2
  187. package/server/services/ai/providers/openaiCompatible.js +70 -0
  188. package/server/services/ai/providers/openrouter.js +162 -0
  189. package/server/services/ai/settings.js +30 -0
  190. package/server/services/ai/systemPrompt.js +51 -29
  191. package/server/services/ai/taskAnalysis.js +60 -1
  192. package/server/services/ai/toolEvidence.js +207 -0
  193. package/server/services/ai/toolSelector.js +8 -1
  194. package/server/services/ai/tools.js +78 -13
  195. package/server/services/browser/extension/registry.js +94 -9
  196. package/server/services/desktop/registry.js +104 -1
  197. package/server/services/desktop/screenRecorder.js +208 -98
  198. package/server/services/desktop/screen_recorder_support.js +46 -0
  199. package/server/services/integrations/google/provider.js +13 -0
  200. package/server/services/integrations/home_assistant/constants.js +88 -0
  201. package/server/services/integrations/home_assistant/network.js +207 -0
  202. package/server/services/integrations/home_assistant/provider.js +249 -0
  203. package/server/services/integrations/home_assistant/snapshot.js +98 -0
  204. package/server/services/integrations/home_assistant/tools.js +101 -0
  205. package/server/services/integrations/manager.js +5 -0
  206. package/server/services/integrations/registry.js +2 -0
  207. package/server/services/integrations/trello/provider.js +8 -2
  208. package/server/services/manager.js +51 -53
  209. package/server/services/mcp/client.js +208 -268
  210. package/server/services/mcp/client_support.js +172 -0
  211. package/server/services/mcp/recovery.js +116 -0
  212. package/server/services/mcp/tool_operations.js +123 -0
  213. package/server/services/memory/ingestion.js +185 -370
  214. package/server/services/memory/ingestion_coverage.js +129 -0
  215. package/server/services/memory/ingestion_documents.js +123 -0
  216. package/server/services/memory/ingestion_support.js +171 -0
  217. package/server/services/memory/intelligence.js +181 -0
  218. package/server/services/memory/manager.js +476 -32
  219. package/server/services/messaging/automation.js +71 -134
  220. package/server/services/messaging/formatting_guides.js +26 -1
  221. package/server/services/messaging/inbound_queue.js +111 -0
  222. package/server/services/messaging/manager.js +25 -8
  223. package/server/services/messaging/typing_keepalive.js +110 -0
  224. package/server/services/streaming/android-stream.js +293 -40
  225. package/server/services/tasks/integration_runtime.js +4 -1
  226. package/server/services/tasks/runtime.js +415 -56
  227. package/server/services/tasks/task_repository.js +48 -6
  228. package/server/services/websocket.js +8 -2
  229. package/server/services/widgets/service.js +17 -1
  230. package/server/services/workspace/manager.js +116 -0
  231. package/server/utils/logger.js +44 -6
  232. package/server/utils/security.js +3 -0
  233. package/server/utils/version.js +29 -19
  234. package/server/services/memory/openhuman_uplift.test.js +0 -98
@@ -1,5 +1,7 @@
1
1
  part of 'main.dart';
2
2
 
3
+ // ignore_for_file: unused_element
4
+
3
5
  class _OptimizedScreenshotPayload {
4
6
  const _OptimizedScreenshotPayload({
5
7
  required this.bytes,
@@ -26,6 +28,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
26
28
  late final TextEditingController _androidLaunchController;
27
29
  late final TextEditingController _desktopLaunchController;
28
30
  late final TextEditingController _textEntryController;
31
+ late final TextEditingController _workspaceEditorController;
29
32
  Timer? _surfaceFrameTimer;
30
33
  _DeviceSurface _surface = _DeviceSurface.browser;
31
34
  _DeviceSurface? _runningSurface;
@@ -39,6 +42,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
39
42
  );
40
43
  _desktopLaunchController = TextEditingController();
41
44
  _textEntryController = TextEditingController();
45
+ _workspaceEditorController = TextEditingController();
42
46
  WidgetsBinding.instance.addPostFrameCallback((_) {
43
47
  if (!mounted) {
44
48
  return;
@@ -57,6 +61,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
57
61
  _androidLaunchController,
58
62
  _desktopLaunchController,
59
63
  _textEntryController,
64
+ _workspaceEditorController,
60
65
  ]) {
61
66
  controller.dispose();
62
67
  }
@@ -66,6 +71,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
66
71
 
67
72
  bool get _isBrowser => _surface == _DeviceSurface.browser;
68
73
  bool get _isDesktop => _surface == _DeviceSurface.desktop;
74
+ bool get _isFiles => _surface == _DeviceSurface.files;
69
75
 
70
76
  bool get _isCurrentSurfaceBusy =>
71
77
  widget.controller.isRunningDeviceAction &&
@@ -180,6 +186,13 @@ class _DevicesPanelState extends State<DevicesPanel> {
180
186
  return;
181
187
  }
182
188
 
189
+ if (_isFiles) {
190
+ if (controller.workspaceEntries.isEmpty) {
191
+ await controller.refreshWorkspaceFiles();
192
+ }
193
+ return;
194
+ }
195
+
183
196
  if (_androidOnline && (controller.androidScreenshotPath ?? '').isEmpty) {
184
197
  await controller.screenshotAndroidRuntime();
185
198
  }
@@ -201,6 +214,9 @@ class _DevicesPanelState extends State<DevicesPanel> {
201
214
  await widget.controller.refreshDesktopFrameRuntime();
202
215
  return;
203
216
  }
217
+ if (_isFiles) {
218
+ return;
219
+ }
204
220
  if (_androidStarting) {
205
221
  await widget.controller.refreshDevices();
206
222
  if (_androidOnline &&
@@ -258,6 +274,11 @@ class _DevicesPanelState extends State<DevicesPanel> {
258
274
  return;
259
275
  }
260
276
 
277
+ if (_isFiles) {
278
+ await controller.refreshWorkspaceFiles();
279
+ return;
280
+ }
281
+
261
282
  if (!_androidOnline) {
262
283
  await controller.startAndroidRuntime();
263
284
  await widget.controller.refreshDevices();
@@ -299,6 +320,9 @@ class _DevicesPanelState extends State<DevicesPanel> {
299
320
  }
300
321
  return;
301
322
  }
323
+ if (_isFiles) {
324
+ return;
325
+ }
302
326
  if (!_androidOnline) {
303
327
  return;
304
328
  }
@@ -314,6 +338,8 @@ class _DevicesPanelState extends State<DevicesPanel> {
314
338
  await widget.controller.typeBrowserTextRuntime(text, pressEnter: true);
315
339
  } else if (_isDesktop) {
316
340
  await widget.controller.typeDesktopRuntime(text, pressEnter: true);
341
+ } else if (_isFiles) {
342
+ return;
317
343
  } else {
318
344
  await widget.controller.typeAndroidRuntime(<String, dynamic>{
319
345
  'text': text,
@@ -513,6 +539,18 @@ class _DevicesPanelState extends State<DevicesPanel> {
513
539
  browserExtensionActive: usingExtension,
514
540
  browserFallbackLabel: browserFallbackLabel,
515
541
  ),
542
+ if (_isFiles) ...<Widget>[
543
+ const SizedBox(height: 14),
544
+ _WorkspaceExplorer(
545
+ controller: controller,
546
+ editorController: _workspaceEditorController,
547
+ ),
548
+ const SizedBox(height: 14),
549
+ _SurfaceSwitcher(
550
+ surface: _surface,
551
+ onSelect: _selectSurface,
552
+ ),
553
+ ] else ...<Widget>[
516
554
  if (_isBrowser && prefersExtension) ...<Widget>[
517
555
  const SizedBox(height: 14),
518
556
  if (_browserExtensionDevices.isNotEmpty) ...<Widget>[
@@ -706,8 +744,8 @@ class _DevicesPanelState extends State<DevicesPanel> {
706
744
  onAction: _runQuickAction,
707
745
  ),
708
746
  if (kIsWeb) ...<Widget>[
709
- const SizedBox(height: 14),
710
- AndroidApkDropZone(
747
+ const SizedBox(height: 12),
748
+ _AndroidActionsBox(
711
749
  enabled: _androidOnline,
712
750
  busy: _isCurrentSurfaceBusy,
713
751
  onInstall: ({required filename, required bytes}) {
@@ -740,6 +778,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
740
778
  surface: _surface,
741
779
  onSelect: _selectSurface,
742
780
  ),
781
+ ],
743
782
  ],
744
783
  ),
745
784
  ),
@@ -751,13 +790,14 @@ class _DevicesPanelState extends State<DevicesPanel> {
751
790
  }
752
791
  }
753
792
 
754
- enum _DeviceSurface { browser, android, desktop }
793
+ enum _DeviceSurface { browser, android, desktop, files }
755
794
 
756
795
  extension _DeviceSurfaceX on _DeviceSurface {
757
796
  String get label => switch (this) {
758
797
  _DeviceSurface.browser => 'Browser',
759
798
  _DeviceSurface.android => 'Phone',
760
799
  _DeviceSurface.desktop => 'Desktop',
800
+ _DeviceSurface.files => 'Files',
761
801
  };
762
802
 
763
803
  String get helper => switch (this) {
@@ -765,12 +805,14 @@ extension _DeviceSurfaceX on _DeviceSurface {
765
805
  _DeviceSurface.android => 'Tap to touch. Drag to swipe.',
766
806
  _DeviceSurface.desktop =>
767
807
  'Tap to click. Drag to drag windows or selections.',
808
+ _DeviceSurface.files => 'Browse files created by the agent.',
768
809
  };
769
810
 
770
811
  IconData get icon => switch (this) {
771
812
  _DeviceSurface.browser => Icons.language_outlined,
772
813
  _DeviceSurface.android => Icons.smartphone_outlined,
773
814
  _DeviceSurface.desktop => Icons.computer_outlined,
815
+ _DeviceSurface.files => Icons.folder_outlined,
774
816
  };
775
817
  }
776
818
 
@@ -846,6 +888,7 @@ class _DeviceSurfaceHeader extends StatelessWidget {
846
888
  selectedDesktop?['label']?.toString().trim().isNotEmpty == true
847
889
  ? selectedDesktop!['label'].toString()
848
890
  : 'Desktop Companion',
891
+ _DeviceSurface.files => 'File Explorer',
849
892
  };
850
893
  final subtitle = switch (surface) {
851
894
  _DeviceSurface.browser =>
@@ -887,6 +930,8 @@ class _DeviceSurfaceHeader extends StatelessWidget {
887
930
  ? 'One desktop companion is online. Open the surface to fetch the latest frame.'
888
931
  : 'No desktop companion is online. Enable Companion Mode on a signed-in desktop app.'
889
932
  : '${selectedDesktop['platform'] ?? 'desktop'} · ${selectedDesktop['hostname'] ?? 'unknown host'}',
933
+ _DeviceSurface.files =>
934
+ 'Secure per-user workspace inside the isolated VM.',
890
935
  };
891
936
  final statusLabel = surface == _DeviceSurface.browser
892
937
  ? browserExtensionPreferred && selectedExtension == null
@@ -904,6 +949,8 @@ class _DeviceSurfaceHeader extends StatelessWidget {
904
949
  : (selectedDesktop['online'] == true
905
950
  ? 'Live'
906
951
  : 'Offline'))
952
+ : surface == _DeviceSurface.files
953
+ ? 'Isolated'
907
954
  : (androidOnline
908
955
  ? 'Live'
909
956
  : androidStarting
@@ -915,6 +962,8 @@ class _DeviceSurfaceHeader extends StatelessWidget {
915
962
  ? (selectedDesktop?['paused'] == true
916
963
  ? _warning
917
964
  : (selectedDesktop?['online'] == true ? _success : _warning))
965
+ : surface == _DeviceSurface.files
966
+ ? _success
918
967
  : (androidOnline
919
968
  ? _success
920
969
  : (androidStarting ? _accent : _warning));
@@ -1021,16 +1070,19 @@ class _DeviceLaunchBar extends StatelessWidget {
1021
1070
  _DeviceSurface.android => _packageOrUrlHint,
1022
1071
  _DeviceSurface.desktop =>
1023
1072
  'Launch an app on the selected desktop (optional)',
1073
+ _DeviceSurface.files => 'Workspace path',
1024
1074
  };
1025
1075
  final buttonLabel = switch (surface) {
1026
1076
  _DeviceSurface.browser => 'Open',
1027
1077
  _DeviceSurface.android => starting ? 'Starting...' : 'Launch',
1028
1078
  _DeviceSurface.desktop => 'Refresh',
1079
+ _DeviceSurface.files => 'Refresh',
1029
1080
  };
1030
1081
  final sleepLabel = switch (surface) {
1031
1082
  _DeviceSurface.browser => 'Sleep Browser',
1032
1083
  _DeviceSurface.android => 'Sleep Phone',
1033
1084
  _DeviceSurface.desktop => 'Pause Desktop',
1085
+ _DeviceSurface.files => 'Close',
1034
1086
  };
1035
1087
  final narrow = MediaQuery.sizeOf(context).width < 720;
1036
1088
 
@@ -1044,6 +1096,8 @@ class _DeviceLaunchBar extends StatelessWidget {
1044
1096
  ? Icons.travel_explore
1045
1097
  : surface == _DeviceSurface.desktop
1046
1098
  ? Icons.apps_outlined
1099
+ : surface == _DeviceSurface.files
1100
+ ? Icons.folder_outlined
1047
1101
  : Icons.open_in_new,
1048
1102
  ),
1049
1103
  ),
@@ -1056,6 +1110,8 @@ class _DeviceLaunchBar extends StatelessWidget {
1056
1110
  ? Icons.arrow_forward
1057
1111
  : surface == _DeviceSurface.desktop
1058
1112
  ? Icons.desktop_windows_outlined
1113
+ : surface == _DeviceSurface.files
1114
+ ? Icons.refresh_rounded
1059
1115
  : Icons.play_arrow,
1060
1116
  ),
1061
1117
  label: Text(buttonLabel),
@@ -1113,6 +1169,7 @@ class _DeviceTypeDock extends StatelessWidget {
1113
1169
  _DeviceSurface.browser => 'Type into the currently focused field',
1114
1170
  _DeviceSurface.android => 'Type into the current phone field',
1115
1171
  _DeviceSurface.desktop => 'Type into the focused desktop field',
1172
+ _DeviceSurface.files => 'Edit the selected file',
1116
1173
  };
1117
1174
  final narrow = MediaQuery.sizeOf(context).width < 720;
1118
1175
 
@@ -1186,6 +1243,9 @@ class _DeviceQuickActions extends StatelessWidget {
1186
1243
  _DeviceSurface.android => const <MapEntry<String, IconData>>[
1187
1244
  MapEntry<String, IconData>('surface_refresh', Icons.refresh_rounded),
1188
1245
  ],
1246
+ _DeviceSurface.files => const <MapEntry<String, IconData>>[
1247
+ MapEntry<String, IconData>('surface_refresh', Icons.refresh_rounded),
1248
+ ],
1189
1249
  };
1190
1250
 
1191
1251
  return Wrap(
@@ -1264,6 +1324,77 @@ class _AndroidNavDock extends StatelessWidget {
1264
1324
  }
1265
1325
  }
1266
1326
 
1327
+ /// Tiny pill shown in the top-right corner of the preview to indicate no audio.
1328
+ class _MutedBadge extends StatelessWidget {
1329
+ const _MutedBadge();
1330
+
1331
+ @override
1332
+ Widget build(BuildContext context) {
1333
+ return Container(
1334
+ padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 4),
1335
+ decoration: BoxDecoration(
1336
+ color: Colors.black54,
1337
+ borderRadius: BorderRadius.circular(8),
1338
+ ),
1339
+ child: const Icon(Icons.volume_off_rounded, size: 11, color: Colors.white),
1340
+ );
1341
+ }
1342
+ }
1343
+
1344
+ /// Compact expandable actions box shown beneath the Android nav dock.
1345
+ /// Starts with APK install; more actions can be added as tiles.
1346
+ class _AndroidActionsBox extends StatelessWidget {
1347
+ const _AndroidActionsBox({
1348
+ required this.enabled,
1349
+ required this.busy,
1350
+ required this.onInstall,
1351
+ });
1352
+
1353
+ final bool enabled;
1354
+ final bool busy;
1355
+ final AndroidApkInstallCallback onInstall;
1356
+
1357
+ @override
1358
+ Widget build(BuildContext context) {
1359
+ return Container(
1360
+ width: double.infinity,
1361
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
1362
+ decoration: BoxDecoration(
1363
+ color: _bgSecondary,
1364
+ borderRadius: BorderRadius.circular(18),
1365
+ border: Border.all(color: _borderLight),
1366
+ ),
1367
+ child: Column(
1368
+ crossAxisAlignment: CrossAxisAlignment.start,
1369
+ mainAxisSize: MainAxisSize.min,
1370
+ children: <Widget>[
1371
+ Text(
1372
+ 'ACTIONS',
1373
+ style: TextStyle(
1374
+ fontSize: 10,
1375
+ fontWeight: FontWeight.w700,
1376
+ letterSpacing: 0.8,
1377
+ color: _textSecondary,
1378
+ ),
1379
+ ),
1380
+ const SizedBox(height: 8),
1381
+ Wrap(
1382
+ spacing: 8,
1383
+ runSpacing: 8,
1384
+ children: <Widget>[
1385
+ AndroidApkTile(
1386
+ enabled: enabled,
1387
+ busy: busy,
1388
+ onInstall: onInstall,
1389
+ ),
1390
+ ],
1391
+ ),
1392
+ ],
1393
+ ),
1394
+ );
1395
+ }
1396
+ }
1397
+
1267
1398
  class _SurfaceSwitcher extends StatelessWidget {
1268
1399
  const _SurfaceSwitcher({required this.surface, required this.onSelect});
1269
1400
 
@@ -1466,6 +1597,42 @@ class _InteractiveSurfacePreviewState
1466
1597
  } catch (_) {}
1467
1598
  }
1468
1599
 
1600
+ void _handleStreamFirstFrame(String streamKey) {
1601
+ if (!mounted || _requestedStreamKey != streamKey) {
1602
+ return;
1603
+ }
1604
+ if (_streamFailedKey == streamKey) {
1605
+ setState(() => _streamFailedKey = null);
1606
+ }
1607
+ }
1608
+
1609
+ void _handleStreamFrameTimeout(String streamKey) {
1610
+ if (!mounted || _requestedStreamKey != streamKey) {
1611
+ return;
1612
+ }
1613
+ setState(() => _streamFailedKey = streamKey);
1614
+ unawaited(_stopActiveStream());
1615
+ if ((widget.screenshotPath ?? '').isEmpty) {
1616
+ unawaited(_refreshStaticFrame());
1617
+ }
1618
+ }
1619
+
1620
+ Future<void> _refreshStaticFrame() async {
1621
+ switch (widget.surface) {
1622
+ case _DeviceSurface.browser:
1623
+ await widget.controller.screenshotBrowserRuntime();
1624
+ break;
1625
+ case _DeviceSurface.android:
1626
+ await widget.controller.screenshotAndroidRuntime();
1627
+ break;
1628
+ case _DeviceSurface.desktop:
1629
+ await widget.controller.screenshotDesktopRuntime();
1630
+ break;
1631
+ case _DeviceSurface.files:
1632
+ break;
1633
+ }
1634
+ }
1635
+
1469
1636
  void _detachImageListener() {
1470
1637
  if (_imageStream != null && _imageListener != null) {
1471
1638
  _imageStream!.removeListener(_imageListener!);
@@ -1571,6 +1738,7 @@ class _InteractiveSurfacePreviewState
1571
1738
  _DeviceSurface.browser => 16 / 10,
1572
1739
  _DeviceSurface.android => 10 / 16,
1573
1740
  _DeviceSurface.desktop => 16 / 10,
1741
+ _DeviceSurface.files => 16 / 10,
1574
1742
  };
1575
1743
 
1576
1744
  return Container(
@@ -1610,6 +1778,7 @@ class _InteractiveSurfacePreviewState
1610
1778
  streamDeviceId != null &&
1611
1779
  streamDeviceId.isNotEmpty &&
1612
1780
  streamKey != _streamFailedKey) {
1781
+ final activeStreamKey = streamKey!;
1613
1782
  return Stack(
1614
1783
  fit: StackFit.expand,
1615
1784
  children: <Widget>[
@@ -1619,6 +1788,10 @@ class _InteractiveSurfacePreviewState
1619
1788
  deviceId: streamDeviceId,
1620
1789
  platform: streamPlatform,
1621
1790
  remoteResolution: _pixelSize,
1791
+ onFirstFrame: () =>
1792
+ _handleStreamFirstFrame(activeStreamKey),
1793
+ onFrameTimeout: () =>
1794
+ _handleStreamFrameTimeout(activeStreamKey),
1622
1795
  onTap: widget.busy
1623
1796
  ? null
1624
1797
  : (x, y) => unawaited(
@@ -1636,6 +1809,14 @@ class _InteractiveSurfacePreviewState
1636
1809
  ? null
1637
1810
  : (x, y) => widget.onHover?.call(Offset(x, y)),
1638
1811
  ),
1812
+ const Positioned(
1813
+ top: 8,
1814
+ right: 8,
1815
+ child: Opacity(
1816
+ opacity: 0.45,
1817
+ child: _MutedBadge(),
1818
+ ),
1819
+ ),
1639
1820
  Positioned(
1640
1821
  left: 12,
1641
1822
  right: 12,
@@ -1743,6 +1924,14 @@ class _InteractiveSurfacePreviewState
1743
1924
  fit: BoxFit.contain,
1744
1925
  gaplessPlayback: true,
1745
1926
  ),
1927
+ const Positioned(
1928
+ top: 8,
1929
+ right: 8,
1930
+ child: Opacity(
1931
+ opacity: 0.45,
1932
+ child: _MutedBadge(),
1933
+ ),
1934
+ ),
1746
1935
  Positioned(
1747
1936
  left: 12,
1748
1937
  right: 12,
@@ -1815,6 +2004,7 @@ class _EmptySurfaceState extends StatelessWidget {
1815
2004
  connectRequired
1816
2005
  ? 'Select Desktop'
1817
2006
  : (busy ? 'Refreshing Desktop...' : 'Refresh Desktop'),
2007
+ _DeviceSurface.files => busy ? 'Loading Files...' : 'Refresh Files',
1818
2008
  };
1819
2009
  final message = switch ((surface, busy, isLoadingPreview)) {
1820
2010
  (_DeviceSurface.browser, true, _) =>
@@ -1829,6 +2019,8 @@ class _EmptySurfaceState extends StatelessWidget {
1829
2019
  'Waking the phone and downloading the first preview. This can take a little while.',
1830
2020
  (_DeviceSurface.android, false, true) =>
1831
2021
  'Downloading the latest phone preview...',
2022
+ (_DeviceSurface.files, true, _) => 'Loading workspace files...',
2023
+ (_DeviceSurface.files, false, true) => 'Loading workspace files...',
1832
2024
  _ =>
1833
2025
  surface == _DeviceSurface.browser
1834
2026
  ? connectRequired
@@ -2067,6 +2259,246 @@ class _ExtensionStatusBar extends StatelessWidget {
2067
2259
  }
2068
2260
  }
2069
2261
 
2262
+ class _WorkspaceExplorer extends StatefulWidget {
2263
+ const _WorkspaceExplorer({
2264
+ required this.controller,
2265
+ required this.editorController,
2266
+ });
2267
+
2268
+ final NeoAgentController controller;
2269
+ final TextEditingController editorController;
2270
+
2271
+ @override
2272
+ State<_WorkspaceExplorer> createState() => _WorkspaceExplorerState();
2273
+ }
2274
+
2275
+ class _WorkspaceExplorerState extends State<_WorkspaceExplorer> {
2276
+ String? _syncedPath;
2277
+
2278
+ @override
2279
+ void initState() {
2280
+ super.initState();
2281
+ WidgetsBinding.instance.addPostFrameCallback((_) {
2282
+ if (!mounted || widget.controller.workspaceEntries.isNotEmpty) {
2283
+ return;
2284
+ }
2285
+ unawaited(widget.controller.refreshWorkspaceFiles());
2286
+ });
2287
+ }
2288
+
2289
+ @override
2290
+ void didUpdateWidget(covariant _WorkspaceExplorer oldWidget) {
2291
+ super.didUpdateWidget(oldWidget);
2292
+ final selectedPath = widget.controller.workspaceSelectedFilePath;
2293
+ if (selectedPath != _syncedPath) {
2294
+ _syncedPath = selectedPath;
2295
+ widget.editorController.text = widget.controller.workspaceEditorContent;
2296
+ }
2297
+ }
2298
+
2299
+ Future<void> _openEntry(Map<String, dynamic> entry) async {
2300
+ final path = entry['path']?.toString() ?? '';
2301
+ if (path.isEmpty && entry['name']?.toString() != '') {
2302
+ return;
2303
+ }
2304
+ if (entry['type']?.toString() == 'directory') {
2305
+ await widget.controller.openWorkspaceDirectory(path);
2306
+ return;
2307
+ }
2308
+ await widget.controller.openWorkspaceFile(path);
2309
+ }
2310
+
2311
+ @override
2312
+ Widget build(BuildContext context) {
2313
+ final controller = widget.controller;
2314
+ final entries = controller.workspaceEntries;
2315
+ final currentPath = controller.workspaceCurrentPath;
2316
+ final selectedPath = controller.workspaceSelectedFilePath;
2317
+ final busy =
2318
+ controller.isLoadingWorkspaceFiles || controller.isSavingWorkspaceFile;
2319
+ final canGoUp = currentPath.trim().isNotEmpty;
2320
+ final parentPath = currentPath.contains('/')
2321
+ ? currentPath.substring(0, currentPath.lastIndexOf('/'))
2322
+ : '';
2323
+
2324
+ return Container(
2325
+ width: double.infinity,
2326
+ padding: const EdgeInsets.all(14),
2327
+ decoration: BoxDecoration(
2328
+ color: _bgSecondary,
2329
+ borderRadius: BorderRadius.circular(18),
2330
+ border: Border.all(color: _borderLight),
2331
+ ),
2332
+ child: Column(
2333
+ crossAxisAlignment: CrossAxisAlignment.start,
2334
+ children: <Widget>[
2335
+ Row(
2336
+ children: <Widget>[
2337
+ Expanded(
2338
+ child: Text(
2339
+ currentPath.isEmpty ? '/' : '/$currentPath',
2340
+ maxLines: 1,
2341
+ overflow: TextOverflow.ellipsis,
2342
+ style: TextStyle(
2343
+ color: _textPrimary,
2344
+ fontWeight: FontWeight.w700,
2345
+ ),
2346
+ ),
2347
+ ),
2348
+ IconButton.filledTonal(
2349
+ tooltip: 'Parent folder',
2350
+ onPressed: busy || !canGoUp
2351
+ ? null
2352
+ : () => controller.openWorkspaceDirectory(parentPath),
2353
+ icon: const Icon(Icons.arrow_upward_rounded),
2354
+ ),
2355
+ const SizedBox(width: 8),
2356
+ IconButton.filledTonal(
2357
+ tooltip: 'Refresh',
2358
+ onPressed: busy ? null : controller.refreshWorkspaceFiles,
2359
+ icon: const Icon(Icons.refresh_rounded),
2360
+ ),
2361
+ ],
2362
+ ),
2363
+ const SizedBox(height: 12),
2364
+ Container(
2365
+ constraints: const BoxConstraints(maxHeight: 260),
2366
+ decoration: BoxDecoration(
2367
+ color: _bgCard,
2368
+ borderRadius: BorderRadius.circular(14),
2369
+ border: Border.all(color: _borderLight),
2370
+ ),
2371
+ child: entries.isEmpty
2372
+ ? Center(
2373
+ child: Padding(
2374
+ padding: const EdgeInsets.all(24),
2375
+ child: Text(
2376
+ controller.isLoadingWorkspaceFiles
2377
+ ? 'Loading files...'
2378
+ : 'No files in this folder.',
2379
+ style: TextStyle(color: _textSecondary),
2380
+ ),
2381
+ ),
2382
+ )
2383
+ : ListView.separated(
2384
+ shrinkWrap: true,
2385
+ itemCount: entries.length,
2386
+ separatorBuilder: (_, __) => Divider(
2387
+ height: 1,
2388
+ color: _borderLight,
2389
+ ),
2390
+ itemBuilder: (context, index) {
2391
+ final entry = entries[index];
2392
+ final type = entry['type']?.toString() ?? 'file';
2393
+ final path = entry['path']?.toString() ?? '';
2394
+ final selected = selectedPath == path;
2395
+ return ListTile(
2396
+ dense: true,
2397
+ selected: selected,
2398
+ leading: Icon(
2399
+ type == 'directory'
2400
+ ? Icons.folder_outlined
2401
+ : Icons.insert_drive_file_outlined,
2402
+ color: type == 'directory' ? _accent : _textSecondary,
2403
+ ),
2404
+ title: Text(
2405
+ entry['name']?.toString() ?? path,
2406
+ maxLines: 1,
2407
+ overflow: TextOverflow.ellipsis,
2408
+ ),
2409
+ subtitle: type == 'file'
2410
+ ? Text(_formatWorkspaceBytes(entry['size']))
2411
+ : null,
2412
+ trailing: type == 'file'
2413
+ ? IconButton(
2414
+ tooltip: 'Download',
2415
+ onPressed: busy || path.isEmpty
2416
+ ? null
2417
+ : () => controller.downloadWorkspaceFile(
2418
+ path,
2419
+ ),
2420
+ icon: const Icon(Icons.download_outlined),
2421
+ )
2422
+ : const Icon(Icons.chevron_right_rounded),
2423
+ onTap: busy ? null : () => _openEntry(entry),
2424
+ );
2425
+ },
2426
+ ),
2427
+ ),
2428
+ const SizedBox(height: 14),
2429
+ Row(
2430
+ children: <Widget>[
2431
+ Expanded(
2432
+ child: Text(
2433
+ selectedPath ?? 'No file selected',
2434
+ maxLines: 1,
2435
+ overflow: TextOverflow.ellipsis,
2436
+ style: TextStyle(
2437
+ color: selectedPath == null ? _textSecondary : _textPrimary,
2438
+ fontWeight: FontWeight.w700,
2439
+ ),
2440
+ ),
2441
+ ),
2442
+ OutlinedButton.icon(
2443
+ onPressed: busy || selectedPath == null
2444
+ ? null
2445
+ : () => controller.downloadWorkspaceFile(selectedPath),
2446
+ icon: const Icon(Icons.download_outlined, size: 18),
2447
+ label: const Text('Download'),
2448
+ ),
2449
+ const SizedBox(width: 8),
2450
+ FilledButton.icon(
2451
+ onPressed: busy || selectedPath == null
2452
+ ? null
2453
+ : () => controller.saveWorkspaceFile(
2454
+ widget.editorController.text,
2455
+ ),
2456
+ icon: controller.isSavingWorkspaceFile
2457
+ ? const SizedBox(
2458
+ width: 16,
2459
+ height: 16,
2460
+ child: CircularProgressIndicator(strokeWidth: 2),
2461
+ )
2462
+ : const Icon(Icons.save_outlined, size: 18),
2463
+ label: const Text('Save'),
2464
+ ),
2465
+ ],
2466
+ ),
2467
+ const SizedBox(height: 10),
2468
+ TextField(
2469
+ controller: widget.editorController,
2470
+ enabled: selectedPath != null && !controller.isSavingWorkspaceFile,
2471
+ minLines: 12,
2472
+ maxLines: 18,
2473
+ style: const TextStyle(
2474
+ fontFamily: 'monospace',
2475
+ fontSize: 13,
2476
+ height: 1.35,
2477
+ ),
2478
+ decoration: InputDecoration(
2479
+ hintText: selectedPath == null
2480
+ ? 'Select a file to view or edit it.'
2481
+ : 'File contents',
2482
+ alignLabelWithHint: true,
2483
+ ),
2484
+ ),
2485
+ ],
2486
+ ),
2487
+ );
2488
+ }
2489
+ }
2490
+
2491
+ String _formatWorkspaceBytes(Object? value) {
2492
+ final bytes = value is num ? value.toDouble() : double.tryParse('$value') ?? 0;
2493
+ if (bytes >= 1024 * 1024) {
2494
+ return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
2495
+ }
2496
+ if (bytes >= 1024) {
2497
+ return '${(bytes / 1024).toStringAsFixed(1)} KB';
2498
+ }
2499
+ return '${bytes.round()} B';
2500
+ }
2501
+
2070
2502
  class _ResultBlock extends StatelessWidget {
2071
2503
  const _ResultBlock({required this.label, required this.value});
2072
2504
 
@@ -2091,7 +2523,7 @@ class _ResultBlock extends StatelessWidget {
2091
2523
  SelectableText(
2092
2524
  value,
2093
2525
  style: TextStyle(
2094
- fontFamily: GoogleFonts.jetBrainsMono().fontFamily,
2526
+ fontFamily: GoogleFonts.geistMono().fontFamily,
2095
2527
  fontSize: 12,
2096
2528
  color: _textSecondary,
2097
2529
  height: 1.5,