neoagent 2.3.1-beta.89 → 2.3.1-beta.91

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 (31) hide show
  1. package/.env.example +4 -0
  2. package/README.md +16 -7
  3. package/flutter_app/lib/features/location/location_service.dart +2 -4
  4. package/flutter_app/lib/main.dart +1 -0
  5. package/flutter_app/lib/main_app_shell.dart +17 -15
  6. package/flutter_app/lib/main_chat.dart +46 -42
  7. package/flutter_app/lib/main_controller.dart +6 -1
  8. package/flutter_app/lib/main_devices.dart +86 -742
  9. package/flutter_app/lib/main_integrations.dart +3 -3
  10. package/flutter_app/lib/main_settings.dart +50 -0
  11. package/flutter_app/lib/main_spacing.dart +18 -0
  12. package/flutter_app/lib/main_theme.dart +9 -0
  13. package/flutter_app/lib/main_unified.dart +3 -3
  14. package/lib/manager.js +33 -0
  15. package/package.json +1 -1
  16. package/server/db/database.js +74 -16
  17. package/server/guest_agent.js +1 -0
  18. package/server/public/.last_build_id +1 -1
  19. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  20. package/server/public/flutter_bootstrap.js +1 -1
  21. package/server/public/main.dart.js +50396 -50271
  22. package/server/services/ai/capabilityHealth.js +2 -3
  23. package/server/services/android/android_bootstrap_worker.js +18 -3
  24. package/server/services/android/controller.js +460 -2753
  25. package/server/services/runtime/backends/local-vm.js +33 -145
  26. package/server/services/runtime/docker-vm-manager.js +392 -0
  27. package/server/services/runtime/manager.js +53 -38
  28. package/server/services/runtime/settings.js +12 -10
  29. package/server/services/runtime/validation.js +4 -1
  30. package/server/utils/deployment.js +8 -2
  31. package/server/services/runtime/qemu.js +0 -1118
@@ -326,9 +326,9 @@ Future<void> _showTrelloSetupDialog(
326
326
  Container(
327
327
  padding: const EdgeInsets.all(8),
328
328
  decoration: BoxDecoration(
329
- color: _danger.withOpacity(0.1),
329
+ color: _danger.withValues(alpha: 0.1),
330
330
  borderRadius: BorderRadius.circular(4),
331
- border: Border.all(color: _danger.withOpacity(0.3)),
331
+ border: Border.all(color: _danger.withValues(alpha: 0.3)),
332
332
  ),
333
333
  child: Text(
334
334
  formError,
@@ -543,7 +543,7 @@ class _TrelloStatusItem extends StatelessWidget {
543
543
  color: _bgSecondary,
544
544
  borderRadius: BorderRadius.circular(8),
545
545
  border: Border.all(
546
- color: isConnected ? _success.withOpacity(0.3) : _border,
546
+ color: isConnected ? _success.withValues(alpha: 0.3) : _border,
547
547
  ),
548
548
  ),
549
549
  child: Row(
@@ -98,6 +98,9 @@ const _workspaceSettingsSection = _SettingsSection('workspace', <String>[
98
98
  'workspace',
99
99
  'browser',
100
100
  'extension',
101
+ 'cli',
102
+ 'claude code',
103
+ 'desktop',
101
104
  'routing',
102
105
  ]);
103
106
 
@@ -167,6 +170,7 @@ const List<_SettingsSection> _settingsSearchSections = <_SettingsSection>[
167
170
  class _SettingsPanelState extends State<SettingsPanel> {
168
171
  late final TextEditingController _searchController;
169
172
  late String _browserBackend;
173
+ late String _cliBackend;
170
174
  late bool _smarterSelector;
171
175
  late Set<String> _enabledModels;
172
176
  late String _defaultChatModel;
@@ -220,6 +224,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
220
224
  .map((model) => model.id)
221
225
  .toSet();
222
226
  _browserBackend = _normalizeBrowserBackend(controller.browserBackend);
227
+ _cliBackend = _normalizeCliBackend(controller.cliBackend);
223
228
  _smarterSelector = controller.smarterSelector;
224
229
  _enabledModels = controller.enabledModelIds
225
230
  .where((id) => knownModels.contains(id))
@@ -279,6 +284,11 @@ class _SettingsPanelState extends State<SettingsPanel> {
279
284
  return normalized == 'extension' ? 'extension' : 'vm';
280
285
  }
281
286
 
287
+ String _normalizeCliBackend(String value) {
288
+ final normalized = value.trim().toLowerCase();
289
+ return normalized == 'desktop' ? 'desktop' : 'vm';
290
+ }
291
+
282
292
  @override
283
293
  Widget build(BuildContext context) {
284
294
  final controller = widget.controller;
@@ -456,6 +466,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
456
466
  browserBackend: _browserBackend == 'extension'
457
467
  ? 'extension'
458
468
  : 'vm',
469
+ cliBackend: _cliBackend == 'desktop' ? 'desktop' : 'vm',
459
470
  smarterSelector: _smarterSelector,
460
471
  enabledModels: _enabledModels.toList(),
461
472
  defaultChatModel: _defaultChatModel,
@@ -651,6 +662,45 @@ class _SettingsPanelState extends State<SettingsPanel> {
651
662
  ],
652
663
  ),
653
664
  const Divider(height: 32),
665
+ Text(
666
+ 'CLI Runtime',
667
+ style: TextStyle(
668
+ fontWeight: FontWeight.w700,
669
+ color: _textPrimary,
670
+ ),
671
+ ),
672
+ const SizedBox(height: 12),
673
+
674
+ DropdownButtonFormField<String>(
675
+ initialValue: _cliBackend,
676
+ decoration: const InputDecoration(
677
+ labelText: 'CLI backend',
678
+ helperText:
679
+ 'Cloud runs the CLI in the isolated VM. Desktop app runs it through the connected desktop companion.',
680
+ ),
681
+ items: const <DropdownMenuItem<String>>[
682
+ DropdownMenuItem<String>(value: 'vm', child: Text('Cloud')),
683
+ DropdownMenuItem<String>(
684
+ value: 'desktop',
685
+ child: Text('Desktop app'),
686
+ ),
687
+ ],
688
+ onChanged: (value) {
689
+ if (value != null) {
690
+ setState(() => _cliBackend = value);
691
+ }
692
+ },
693
+ ),
694
+ const SizedBox(height: 10),
695
+ Text(
696
+ _cliBackend == 'desktop'
697
+ ? (controller.desktopCompanionConnected
698
+ ? 'Desktop app connected.'
699
+ : 'Desktop app selected. Make sure the desktop companion is running and connected on your machine.')
700
+ : 'Cloud CLI runtime is active.',
701
+ style: TextStyle(color: _textSecondary, height: 1.4),
702
+ ),
703
+ const Divider(height: 32),
654
704
  Text(
655
705
  'Routing Behavior',
656
706
  style: TextStyle(
@@ -0,0 +1,18 @@
1
+ part of 'main.dart';
2
+
3
+ abstract class AppSpacing {
4
+ static const double xs = 8.0;
5
+ static const double sm = 12.0;
6
+ static const double md = 16.0;
7
+ static const double lg = 24.0;
8
+ static const double xl = 32.0;
9
+ }
10
+
11
+ abstract class AppBreakpoints {
12
+ static const double mobile = 480.0;
13
+ static const double tablet = 960.0;
14
+ }
15
+
16
+ /// Deep dark color used for QR code modules rendered on a white background.
17
+ /// Matches the dark theme's primary background hue for brand consistency.
18
+ const Color _qrDarkColor = Color(0xFF04111D);
@@ -109,6 +109,7 @@ ThemeData _buildNeoAgentTheme(NeoAgentPalette palette, Brightness brightness) {
109
109
  );
110
110
 
111
111
  return base.copyWith(
112
+ focusColor: palette.accent.withValues(alpha: 0.2),
112
113
  scaffoldBackgroundColor: palette.bgPrimary,
113
114
  colorScheme: base.colorScheme.copyWith(
114
115
  primary: palette.accent,
@@ -229,6 +230,14 @@ ThemeData _buildNeoAgentTheme(NeoAgentPalette palette, Brightness brightness) {
229
230
  ),
230
231
  ),
231
232
  ),
233
+ elevatedButtonTheme: ElevatedButtonThemeData(
234
+ style: ElevatedButton.styleFrom(
235
+ padding: const EdgeInsets.symmetric(
236
+ horizontal: AppSpacing.md,
237
+ vertical: AppSpacing.sm,
238
+ ),
239
+ ),
240
+ ),
232
241
  appBarTheme: AppBarTheme(
233
242
  backgroundColor: Colors.transparent,
234
243
  surfaceTintColor: Colors.transparent,
@@ -90,7 +90,7 @@ class _ToolsPanelState extends State<ToolsPanel>
90
90
  ),
91
91
  child: TabBar(
92
92
  controller: _tabController,
93
- dividerColor: Colors.transparent,
93
+ dividerColor: _border,
94
94
  indicatorSize: TabBarIndicatorSize.tab,
95
95
  labelStyle: const TextStyle(fontWeight: FontWeight.w700),
96
96
  tabs: <Widget>[
@@ -190,7 +190,7 @@ class _RunsAndLogsPanelState extends State<RunsAndLogsPanel>
190
190
  ),
191
191
  child: TabBar(
192
192
  controller: _tabController,
193
- dividerColor: Colors.transparent,
193
+ dividerColor: _border,
194
194
  indicatorSize: TabBarIndicatorSize.tab,
195
195
  labelStyle: const TextStyle(fontWeight: FontWeight.w700),
196
196
  tabs: <Widget>[
@@ -251,7 +251,7 @@ class _SettingsWorkspacePanelState extends State<SettingsWorkspacePanel> {
251
251
 
252
252
  @override
253
253
  Widget build(BuildContext context) {
254
- final compact = MediaQuery.sizeOf(context).width < 960;
254
+ final compact = MediaQuery.sizeOf(context).width < AppBreakpoints.tablet;
255
255
  return Padding(
256
256
  padding: _pagePadding(context),
257
257
  child: Column(
package/lib/manager.js CHANGED
@@ -288,6 +288,38 @@ function ensureLogDir() {
288
288
  ensureRuntimeDirs();
289
289
  }
290
290
 
291
+ function pruneOldRuntimeBackups(backupsDir, keepLatest = 3) {
292
+ if (!fs.existsSync(backupsDir) || keepLatest < 0) return;
293
+
294
+ const backupDirs = fs
295
+ .readdirSync(backupsDir, { withFileTypes: true })
296
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith('pre-update-'))
297
+ .map((entry) => {
298
+ const fullPath = path.join(backupsDir, entry.name);
299
+ let mtimeMs = 0;
300
+ try {
301
+ mtimeMs = fs.statSync(fullPath).mtimeMs;
302
+ } catch {
303
+ // Skip entries that disappear or cannot be statted.
304
+ return null;
305
+ }
306
+ return { name: entry.name, fullPath, mtimeMs };
307
+ })
308
+ .filter(Boolean)
309
+ .sort((a, b) => {
310
+ if (b.mtimeMs !== a.mtimeMs) return b.mtimeMs - a.mtimeMs;
311
+ return b.name.localeCompare(a.name);
312
+ });
313
+
314
+ for (const backup of backupDirs.slice(keepLatest)) {
315
+ try {
316
+ fs.rmSync(backup.fullPath, { recursive: true, force: true });
317
+ } catch {
318
+ // Best-effort cleanup only.
319
+ }
320
+ }
321
+ }
322
+
291
323
  function backupRuntimeData() {
292
324
  const backupsDir = path.join(RUNTIME_HOME, 'backups');
293
325
  fs.mkdirSync(backupsDir, { recursive: true });
@@ -297,6 +329,7 @@ function backupRuntimeData() {
297
329
 
298
330
  if (fs.existsSync(ENV_FILE)) fs.copyFileSync(ENV_FILE, path.join(target, '.env'));
299
331
  if (fs.existsSync(DATA_DIR)) fs.cpSync(DATA_DIR, path.join(target, 'data'), { recursive: true, force: false, errorOnExist: false });
332
+ pruneOldRuntimeBackups(backupsDir, 3);
300
333
  }
301
334
 
302
335
  function killByPort(port) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.3.1-beta.89",
3
+ "version": "2.3.1-beta.91",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -50,6 +50,43 @@ function initializeDatabase(db, dbPath) {
50
50
  return db;
51
51
  }
52
52
 
53
+ function sleepSync(ms) {
54
+ if (ms <= 0) return;
55
+ const buffer = new SharedArrayBuffer(4);
56
+ const view = new Int32Array(buffer);
57
+ Atomics.wait(view, 0, 0, ms);
58
+ }
59
+
60
+ function isSqliteBusyError(error) {
61
+ return Boolean(
62
+ error &&
63
+ (
64
+ error.code === 'SQLITE_BUSY' ||
65
+ error.code === 'SQLITE_LOCKED' ||
66
+ /database is locked|database table is locked/i.test(error.message || '')
67
+ ),
68
+ );
69
+ }
70
+
71
+ function runWithBusyRetry(action, { attempts = 5, delayMs = 50, label = 'SQLite operation' } = {}) {
72
+ let lastError = null;
73
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
74
+ try {
75
+ return action();
76
+ } catch (error) {
77
+ lastError = error;
78
+ if (!isSqliteBusyError(error) || attempt === attempts) {
79
+ throw error;
80
+ }
81
+ console.warn(
82
+ `[Database] ${label} hit a busy lock on attempt ${attempt}/${attempts}; retrying in ${delayMs}ms.`,
83
+ );
84
+ sleepSync(delayMs);
85
+ }
86
+ }
87
+ throw lastError;
88
+ }
89
+
53
90
  let db = new Database(DB_PATH);
54
91
  db = initializeDatabase(db, DB_PATH);
55
92
 
@@ -1047,7 +1084,19 @@ function backfillAgentIds() {
1047
1084
  }
1048
1085
  }
1049
1086
  });
1050
- tx();
1087
+
1088
+ try {
1089
+ runWithBusyRetry(
1090
+ () => tx(),
1091
+ { attempts: 8, label: 'backfillAgentIds' },
1092
+ );
1093
+ } catch (error) {
1094
+ if (isSqliteBusyError(error)) {
1095
+ console.warn('[Database] Skipping backfillAgentIds because the database stayed locked.');
1096
+ return;
1097
+ }
1098
+ throw error;
1099
+ }
1051
1100
  }
1052
1101
 
1053
1102
  function backfillAgentPolicies() {
@@ -1059,21 +1108,30 @@ function backfillAgentPolicies() {
1059
1108
  return;
1060
1109
  }
1061
1110
  try {
1062
- db.prepare(
1063
- `UPDATE agents
1064
- SET can_delegate = COALESCE(can_delegate, 1),
1065
- can_be_delegated_to = COALESCE(can_be_delegated_to, 0),
1066
- delegate_targets_json = COALESCE(delegate_targets_json, '[]')
1067
- WHERE slug = 'main'`
1068
- ).run();
1069
- db.prepare(
1070
- `UPDATE agents
1071
- SET can_delegate = COALESCE(can_delegate, 0),
1072
- can_be_delegated_to = COALESCE(can_be_delegated_to, 1),
1073
- delegate_targets_json = COALESCE(delegate_targets_json, '[]')
1074
- WHERE slug != 'main'`
1075
- ).run();
1076
- } catch {
1111
+ runWithBusyRetry(
1112
+ () => {
1113
+ db.prepare(
1114
+ `UPDATE agents
1115
+ SET can_delegate = COALESCE(can_delegate, 1),
1116
+ can_be_delegated_to = COALESCE(can_be_delegated_to, 0),
1117
+ delegate_targets_json = COALESCE(delegate_targets_json, '[]')
1118
+ WHERE slug = 'main'`
1119
+ ).run();
1120
+ db.prepare(
1121
+ `UPDATE agents
1122
+ SET can_delegate = COALESCE(can_delegate, 0),
1123
+ can_be_delegated_to = COALESCE(can_be_delegated_to, 1),
1124
+ delegate_targets_json = COALESCE(delegate_targets_json, '[]')
1125
+ WHERE slug != 'main'`
1126
+ ).run();
1127
+ },
1128
+ { attempts: 8, label: 'backfillAgentPolicies' },
1129
+ );
1130
+ } catch (error) {
1131
+ if (isSqliteBusyError(error)) {
1132
+ console.warn('[Database] Skipping backfillAgentPolicies because the database stayed locked.');
1133
+ return;
1134
+ }
1077
1135
  // Keep startup resilient for partially migrated databases.
1078
1136
  }
1079
1137
  }
@@ -150,6 +150,7 @@ app.post('/files/read', async (req, res) => {
150
150
  }
151
151
  const realTarget = resolveReadablePath(filePath);
152
152
  if (!realTarget) {
153
+ console.warn('[GuestAgent] files/read rejected path', { requestedPath: filePath });
153
154
  return { error: 'path is outside guest-agent readable roots' };
154
155
  }
155
156
  const data = fs.readFileSync(realTarget);
@@ -1 +1 @@
1
- 565ea9475bb1a5c2efca6dc427d87b38
1
+ e64803ff760d0ccfd46f2b8d124f526f
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"42d3d75a56efe1a2e9902f52dc8006099c45d9
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "2533429045" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "1179184661" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });