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.
- package/.env.example +4 -0
- package/README.md +16 -7
- package/flutter_app/lib/features/location/location_service.dart +2 -4
- package/flutter_app/lib/main.dart +1 -0
- package/flutter_app/lib/main_app_shell.dart +17 -15
- package/flutter_app/lib/main_chat.dart +46 -42
- package/flutter_app/lib/main_controller.dart +6 -1
- package/flutter_app/lib/main_devices.dart +86 -742
- package/flutter_app/lib/main_integrations.dart +3 -3
- package/flutter_app/lib/main_settings.dart +50 -0
- package/flutter_app/lib/main_spacing.dart +18 -0
- package/flutter_app/lib/main_theme.dart +9 -0
- package/flutter_app/lib/main_unified.dart +3 -3
- package/lib/manager.js +33 -0
- package/package.json +1 -1
- package/server/db/database.js +74 -16
- package/server/guest_agent.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 +50396 -50271
- package/server/services/ai/capabilityHealth.js +2 -3
- package/server/services/android/android_bootstrap_worker.js +18 -3
- package/server/services/android/controller.js +460 -2753
- package/server/services/runtime/backends/local-vm.js +33 -145
- package/server/services/runtime/docker-vm-manager.js +392 -0
- package/server/services/runtime/manager.js +53 -38
- package/server/services/runtime/settings.js +12 -10
- package/server/services/runtime/validation.js +4 -1
- package/server/utils/deployment.js +8 -2
- 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.
|
|
329
|
+
color: _danger.withValues(alpha: 0.1),
|
|
330
330
|
borderRadius: BorderRadius.circular(4),
|
|
331
|
-
border: Border.all(color: _danger.
|
|
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.
|
|
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:
|
|
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:
|
|
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 <
|
|
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
package/server/db/database.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
}
|
package/server/guest_agent.js
CHANGED
|
@@ -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
|
-
|
|
1
|
+
e64803ff760d0ccfd46f2b8d124f526f
|
|
Binary file
|
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"42d3d75a56efe1a2e9902f52dc8006099c45d9
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "1179184661" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|