neoagent 3.2.1-beta.0 → 3.2.1-beta.10

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 (175) hide show
  1. package/docs/agent-run-lifecycle.md +10 -0
  2. package/extensions/chrome-browser/background.mjs +318 -88
  3. package/extensions/chrome-browser/http.mjs +136 -0
  4. package/extensions/chrome-browser/protocol.mjs +654 -90
  5. package/flutter_app/lib/main_chat.dart +118 -739
  6. package/flutter_app/lib/main_controller.dart +111 -20
  7. package/flutter_app/lib/main_integrations.dart +607 -8
  8. package/flutter_app/lib/main_models.dart +3 -0
  9. package/flutter_app/lib/main_operations.dart +334 -321
  10. package/flutter_app/lib/main_security.dart +266 -112
  11. package/flutter_app/lib/main_settings.dart +4 -3
  12. package/flutter_app/lib/main_shared.dart +14 -11
  13. package/flutter_app/lib/src/backend_client.dart +78 -0
  14. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  15. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  16. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  17. package/landing/index.html +3 -1
  18. package/lib/manager.js +106 -89
  19. package/lib/schema_migrations.js +145 -13
  20. package/package.json +30 -15
  21. package/runtime/paths.js +49 -5
  22. package/server/db/database.js +4 -4
  23. package/server/guest-agent.cli.package.json +13 -0
  24. package/server/guest_agent.js +85 -40
  25. package/server/http/middleware.js +24 -0
  26. package/server/http/routes.js +11 -6
  27. package/server/public/.last_build_id +1 -1
  28. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  29. package/server/public/flutter_bootstrap.js +2 -2
  30. package/server/public/main.dart.js +73083 -72209
  31. package/server/routes/admin.js +1 -1
  32. package/server/routes/agents.js +35 -2
  33. package/server/routes/android.js +30 -34
  34. package/server/routes/browser.js +23 -15
  35. package/server/routes/desktop.js +18 -1
  36. package/server/routes/integrations.js +107 -1
  37. package/server/routes/memory.js +1 -0
  38. package/server/routes/settings.js +16 -5
  39. package/server/routes/social_reach.js +12 -3
  40. package/server/routes/social_video.js +4 -0
  41. package/server/services/agents/manager.js +1 -1
  42. package/server/services/ai/capabilityHealth.js +62 -96
  43. package/server/services/ai/compaction.js +7 -2
  44. package/server/services/ai/history.js +45 -6
  45. package/server/services/ai/integrated_tools/http_request.js +8 -0
  46. package/server/services/ai/loop/agent_engine_core.js +496 -166
  47. package/server/services/ai/loop/blank_recovery.js +5 -4
  48. package/server/services/ai/loop/callbacks.js +1 -0
  49. package/server/services/ai/loop/completion_judge.js +121 -5
  50. package/server/services/ai/loop/conversation_loop.js +620 -340
  51. package/server/services/ai/loop/lifecycle.js +108 -0
  52. package/server/services/ai/loop/messaging_delivery.js +129 -57
  53. package/server/services/ai/loop/model_call_guard.js +91 -0
  54. package/server/services/ai/loop/model_io.js +48 -56
  55. package/server/services/ai/loop/progress_classification.js +2 -0
  56. package/server/services/ai/loop/tool_dispatch.js +28 -8
  57. package/server/services/ai/loopPolicy.js +48 -21
  58. package/server/services/ai/messagingFallback.js +17 -17
  59. package/server/services/ai/model_discovery.js +227 -0
  60. package/server/services/ai/model_failure_cache.js +108 -0
  61. package/server/services/ai/model_identity.js +71 -0
  62. package/server/services/ai/models.js +68 -163
  63. package/server/services/ai/providerRetry.js +17 -59
  64. package/server/services/ai/provider_selector.js +166 -0
  65. package/server/services/ai/providers/anthropic.js +4 -4
  66. package/server/services/ai/providers/claudeCode.js +21 -33
  67. package/server/services/ai/providers/githubCopilot.js +41 -20
  68. package/server/services/ai/providers/google.js +135 -97
  69. package/server/services/ai/providers/grok.js +6 -5
  70. package/server/services/ai/providers/grokOauth.js +19 -27
  71. package/server/services/ai/providers/nvidia.js +12 -7
  72. package/server/services/ai/providers/ollama.js +114 -86
  73. package/server/services/ai/providers/ollama_stream.js +142 -0
  74. package/server/services/ai/providers/openai.js +41 -7
  75. package/server/services/ai/providers/openaiCodex.js +13 -4
  76. package/server/services/ai/providers/openrouter.js +31 -9
  77. package/server/services/ai/providers/provider_error.js +36 -0
  78. package/server/services/ai/settings.js +26 -2
  79. package/server/services/ai/systemPrompt.js +19 -12
  80. package/server/services/ai/taskAnalysis.js +58 -10
  81. package/server/services/ai/terminal_reply.js +18 -0
  82. package/server/services/ai/toolEvidence.js +350 -29
  83. package/server/services/ai/tools.js +190 -111
  84. package/server/services/android/controller.js +770 -237
  85. package/server/services/android/process.js +140 -0
  86. package/server/services/android/sdk_download.js +143 -0
  87. package/server/services/android/uia.js +6 -5
  88. package/server/services/artifacts/store.js +24 -0
  89. package/server/services/browser/controller.js +843 -385
  90. package/server/services/browser/extension/gateway.js +40 -16
  91. package/server/services/browser/extension/protocol.js +15 -1
  92. package/server/services/browser/extension/provider.js +71 -47
  93. package/server/services/browser/extension/registry.js +155 -34
  94. package/server/services/cli/executor.js +62 -9
  95. package/server/services/credentials/bitwarden_cli.js +322 -0
  96. package/server/services/credentials/broker.js +594 -0
  97. package/server/services/desktop/gateway.js +41 -4
  98. package/server/services/desktop/protocol.js +3 -0
  99. package/server/services/desktop/provider.js +39 -42
  100. package/server/services/desktop/registry.js +137 -52
  101. package/server/services/integrations/bitwarden/constants.js +14 -0
  102. package/server/services/integrations/bitwarden/provider.js +197 -0
  103. package/server/services/integrations/bitwarden/snapshot.js +65 -0
  104. package/server/services/integrations/figma/provider.js +78 -12
  105. package/server/services/integrations/github/common.js +11 -6
  106. package/server/services/integrations/github/provider.js +52 -53
  107. package/server/services/integrations/google/provider.js +55 -19
  108. package/server/services/integrations/home_assistant/network.js +17 -20
  109. package/server/services/integrations/home_assistant/provider.js +7 -5
  110. package/server/services/integrations/home_assistant/tools.js +17 -5
  111. package/server/services/integrations/http.js +51 -0
  112. package/server/services/integrations/manager.js +159 -53
  113. package/server/services/integrations/microsoft/provider.js +80 -13
  114. package/server/services/integrations/neoarchive/provider.js +55 -29
  115. package/server/services/integrations/neorecall/client.js +17 -10
  116. package/server/services/integrations/neorecall/provider.js +20 -11
  117. package/server/services/integrations/notion/provider.js +16 -13
  118. package/server/services/integrations/oauth_provider.js +115 -51
  119. package/server/services/integrations/registry.js +2 -0
  120. package/server/services/integrations/slack/provider.js +98 -9
  121. package/server/services/integrations/spotify/provider.js +67 -71
  122. package/server/services/integrations/trello/provider.js +21 -7
  123. package/server/services/integrations/weather/provider.js +18 -12
  124. package/server/services/integrations/whatsapp/provider.js +76 -16
  125. package/server/services/manager.js +110 -1
  126. package/server/services/memory/embedding_index.js +20 -8
  127. package/server/services/memory/embeddings.js +151 -90
  128. package/server/services/memory/ingestion.js +50 -9
  129. package/server/services/memory/ingestion_documents.js +13 -3
  130. package/server/services/memory/manager.js +52 -19
  131. package/server/services/messaging/automation.js +85 -10
  132. package/server/services/messaging/formatting_guides.js +7 -4
  133. package/server/services/messaging/http_platforms.js +33 -13
  134. package/server/services/messaging/inbound_queue.js +78 -24
  135. package/server/services/messaging/inbound_store.js +224 -0
  136. package/server/services/messaging/manager.js +326 -51
  137. package/server/services/messaging/typing_keepalive.js +5 -2
  138. package/server/services/messaging/whatsapp.js +22 -14
  139. package/server/services/network/http.js +210 -0
  140. package/server/services/network/safe_request.js +307 -0
  141. package/server/services/runtime/backends/local-vm.js +227 -67
  142. package/server/services/runtime/docker-vm-manager.js +9 -0
  143. package/server/services/runtime/guest_bootstrap.js +30 -4
  144. package/server/services/runtime/guest_image.js +43 -12
  145. package/server/services/runtime/manager.js +77 -23
  146. package/server/services/runtime/validation.js +7 -6
  147. package/server/services/security/tool_categories.js +6 -0
  148. package/server/services/social_reach/channels/github.js +10 -4
  149. package/server/services/social_reach/channels/reddit.js +4 -4
  150. package/server/services/social_reach/channels/rss.js +2 -2
  151. package/server/services/social_reach/channels/social_video.js +12 -7
  152. package/server/services/social_reach/channels/v2ex.js +21 -8
  153. package/server/services/social_reach/channels/x.js +2 -2
  154. package/server/services/social_reach/channels/xueqiu.js +5 -5
  155. package/server/services/social_reach/service.js +9 -6
  156. package/server/services/social_reach/utils.js +65 -14
  157. package/server/services/social_video/service.js +160 -50
  158. package/server/services/tasks/integration_runtime.js +18 -8
  159. package/server/services/tasks/runtime.js +39 -4
  160. package/server/services/voice/agentBridge.js +17 -4
  161. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  162. package/server/services/voice/liveSession.js +31 -0
  163. package/server/services/voice/message.js +1 -1
  164. package/server/services/voice/openaiSpeech.js +33 -8
  165. package/server/services/voice/providers.js +233 -151
  166. package/server/services/voice/runtime.js +2 -2
  167. package/server/services/voice/runtimeManager.js +118 -20
  168. package/server/services/voice/turnRunner.js +6 -0
  169. package/server/services/wearable/firmware_manifest.js +51 -13
  170. package/server/services/wearable/service.js +1 -0
  171. package/server/utils/abort.js +96 -0
  172. package/server/utils/cloud-security.js +110 -3
  173. package/server/utils/files.js +31 -0
  174. package/server/utils/image_payload.js +95 -0
  175. package/server/utils/retry.js +107 -0
@@ -23,7 +23,7 @@ class ToolApprovalRequest {
23
23
  category: json['category']?.toString() ?? 'unknown',
24
24
  expiresAt: json['expiresAt'] != null
25
25
  ? DateTime.tryParse(json['expiresAt'].toString()) ??
26
- DateTime.now().add(const Duration(seconds: 30))
26
+ DateTime.now().add(const Duration(seconds: 30))
27
27
  : DateTime.now().add(const Duration(seconds: 30)),
28
28
  );
29
29
  }
@@ -89,6 +89,14 @@ const _kCategoryInfo = <String, _CategoryInfo>{
89
89
  color: Color(0xFF8E24AA),
90
90
  riskLevel: 'high',
91
91
  ),
92
+ 'credential_use': _CategoryInfo(
93
+ label: 'Credential Use',
94
+ subtitle:
95
+ 'Fill approved logins or authenticate requests without showing secrets to the AI.',
96
+ icon: Icons.password_rounded,
97
+ color: Color(0xFF5E35B1),
98
+ riskLevel: 'high',
99
+ ),
92
100
  'network_write': _CategoryInfo(
93
101
  label: 'Network Write Requests',
94
102
  subtitle: 'Send POST / PUT / DELETE requests to external APIs.',
@@ -105,7 +113,8 @@ const _kCategoryInfo = <String, _CategoryInfo>{
105
113
  ),
106
114
  'external': _CategoryInfo(
107
115
  label: 'External & MCP Tools',
108
- subtitle: 'Tools not built into NeoAgent, including connected MCP servers and custom tool providers.',
116
+ subtitle:
117
+ 'Tools not built into NeoAgent, including connected MCP servers and custom tool providers.',
109
118
  icon: Icons.hub_rounded,
110
119
  color: Color(0xFF6D4C41),
111
120
  riskLevel: 'high',
@@ -146,7 +155,9 @@ class _SecurityNotificationService {
146
155
  if (_plugin != null) return _plugin;
147
156
  try {
148
157
  final plugin = FlutterLocalNotificationsPlugin();
149
- final androidSettings = const AndroidInitializationSettings('@mipmap/ic_launcher');
158
+ final androidSettings = const AndroidInitializationSettings(
159
+ '@mipmap/ic_launcher',
160
+ );
150
161
  final darwinSettings = DarwinInitializationSettings(
151
162
  requestAlertPermission: false,
152
163
  requestBadgePermission: false,
@@ -156,10 +167,13 @@ class _SecurityNotificationService {
156
167
  'tool_approval',
157
168
  actions: <DarwinNotificationAction>[
158
169
  DarwinNotificationAction.plain(_approveActionId, 'Allow'),
159
- DarwinNotificationAction.plain(_denyActionId, 'Deny',
160
- options: <DarwinNotificationActionOption>{
161
- DarwinNotificationActionOption.destructive,
162
- }),
170
+ DarwinNotificationAction.plain(
171
+ _denyActionId,
172
+ 'Deny',
173
+ options: <DarwinNotificationActionOption>{
174
+ DarwinNotificationActionOption.destructive,
175
+ },
176
+ ),
163
177
  ],
164
178
  ),
165
179
  ],
@@ -171,7 +185,8 @@ class _SecurityNotificationService {
171
185
  macOS: darwinSettings,
172
186
  ),
173
187
  onDidReceiveNotificationResponse: _onNotificationResponse,
174
- onDidReceiveBackgroundNotificationResponse: _onBackgroundNotificationResponse,
188
+ onDidReceiveBackgroundNotificationResponse:
189
+ _onBackgroundNotificationResponse,
175
190
  );
176
191
  _plugin = plugin;
177
192
  return plugin;
@@ -189,7 +204,11 @@ class _SecurityNotificationService {
189
204
  _handleNotificationAction(response.id, response.actionId, response.payload);
190
205
  }
191
206
 
192
- static void _handleNotificationAction(int? id, String? actionId, String? payload) {
207
+ static void _handleNotificationAction(
208
+ int? id,
209
+ String? actionId,
210
+ String? payload,
211
+ ) {
193
212
  // No-op in background; the app will handle it on resume via foreground listener.
194
213
  // Foreground case is handled directly by the approval gate service.
195
214
  }
@@ -199,10 +218,14 @@ class _SecurityNotificationService {
199
218
  if (plugin == null) return;
200
219
  if (!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) {
201
220
  await plugin
202
- .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
221
+ .resolvePlatformSpecificImplementation<
222
+ AndroidFlutterLocalNotificationsPlugin
223
+ >()
203
224
  ?.requestNotificationsPermission();
204
225
  await plugin
205
- .resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
226
+ .resolvePlatformSpecificImplementation<
227
+ IOSFlutterLocalNotificationsPlugin
228
+ >()
206
229
  ?.requestPermissions(alert: true, badge: false, sound: false);
207
230
  }
208
231
  }
@@ -237,7 +260,11 @@ class _SecurityNotificationService {
237
260
  req.approvalId.hashCode.abs() % 100000,
238
261
  '${info.label} approval needed',
239
262
  body,
240
- NotificationDetails(android: androidDetails, iOS: darwinDetails, macOS: darwinDetails),
263
+ NotificationDetails(
264
+ android: androidDetails,
265
+ iOS: darwinDetails,
266
+ macOS: darwinDetails,
267
+ ),
241
268
  payload: req.approvalId,
242
269
  );
243
270
  }
@@ -272,14 +299,14 @@ class _MainSecurityState extends State<MainSecurity> {
272
299
 
273
300
  Future<void> _load() async {
274
301
  try {
275
- final result = await widget.controller.backendClient.fetchSecurityPolicies(
276
- widget.controller.backendUrl,
277
- );
302
+ final result = await widget.controller.backendClient
303
+ .fetchSecurityPolicies(widget.controller.backendUrl);
278
304
  final raw = result['policies'];
279
305
  Map<String, String> loaded = const <String, String>{};
280
306
  if (raw is Map) {
281
307
  loaded = Map<String, String>.from(
282
- raw.map((k, v) => MapEntry(k.toString(), v.toString())));
308
+ raw.map((k, v) => MapEntry(k.toString(), v.toString())),
309
+ );
283
310
  }
284
311
  setState(() {
285
312
  _policies = loaded;
@@ -287,7 +314,10 @@ class _MainSecurityState extends State<MainSecurity> {
287
314
  _loading = false;
288
315
  });
289
316
  } catch (e) {
290
- setState(() { _error = e.toString(); _loading = false; });
317
+ setState(() {
318
+ _error = e.toString();
319
+ _loading = false;
320
+ });
291
321
  }
292
322
  }
293
323
 
@@ -296,13 +326,17 @@ class _MainSecurityState extends State<MainSecurity> {
296
326
  setState(() => _mode = mode);
297
327
  try {
298
328
  await widget.controller.backendClient.saveSecurityMode(
299
- widget.controller.backendUrl, mode,
329
+ widget.controller.backendUrl,
330
+ mode,
300
331
  );
301
332
  } catch (e) {
302
333
  setState(() => _mode = prev);
303
334
  if (mounted) {
304
335
  ScaffoldMessenger.of(context).showSnackBar(
305
- SnackBar(content: Text('Failed to save: $e'), backgroundColor: _danger),
336
+ SnackBar(
337
+ content: Text('Failed to save: $e'),
338
+ backgroundColor: _danger,
339
+ ),
306
340
  );
307
341
  }
308
342
  }
@@ -313,13 +347,20 @@ class _MainSecurityState extends State<MainSecurity> {
313
347
  setState(() => _policies = {..._policies, category: policy});
314
348
  try {
315
349
  await widget.controller.backendClient.saveSecurityPolicy(
316
- widget.controller.backendUrl, category: category, policy: policy,
350
+ widget.controller.backendUrl,
351
+ category: category,
352
+ policy: policy,
317
353
  );
318
354
  } catch (e) {
319
- setState(() => _policies = {..._policies, category: prev ?? 'require_approval'});
355
+ setState(
356
+ () => _policies = {..._policies, category: prev ?? 'require_approval'},
357
+ );
320
358
  if (mounted) {
321
359
  ScaffoldMessenger.of(context).showSnackBar(
322
- SnackBar(content: Text('Failed to save: $e'), backgroundColor: _danger),
360
+ SnackBar(
361
+ content: Text('Failed to save: $e'),
362
+ backgroundColor: _danger,
363
+ ),
323
364
  );
324
365
  }
325
366
  }
@@ -342,46 +383,50 @@ class _MainSecurityState extends State<MainSecurity> {
342
383
  body: _loading
343
384
  ? const Center(child: CircularProgressIndicator.adaptive())
344
385
  : _error != null
345
- ? _ErrorView(error: _error!, onRetry: _load)
346
- : ListView(
347
- padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
348
- children: <Widget>[
349
- _GlobalModeCard(mode: _mode, onChanged: _setMode),
350
- const SizedBox(height: 16),
351
- if (_mode == 'allow_all')
352
- _InfoBanner(
353
- icon: Icons.warning_amber_rounded,
354
- color: colorScheme.errorContainer,
355
- textColor: colorScheme.onErrorContainer,
356
- message: 'All tools are allowed — the agent can use any capability without asking. '
357
- 'Switch to "Default" or "Always ask" to re-enable approval checks.',
358
- )
359
- else ...<Widget>[
360
- if (_mode == 'always_ask')
361
- _InfoBanner(
362
- icon: Icons.info_outline_rounded,
363
- color: colorScheme.secondaryContainer,
364
- textColor: colorScheme.onSecondaryContainer,
365
- message: 'The agent will ask before every sensitive tool, '
366
- 'regardless of per-category settings below.',
367
- ),
368
- const SizedBox(height: 4),
369
- const Padding(
370
- padding: EdgeInsets.only(top: 4, bottom: 6),
371
- child: _SectionTitle('Per-category permissions'),
386
+ ? _ErrorView(error: _error!, onRetry: _load)
387
+ : ListView(
388
+ padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
389
+ children: <Widget>[
390
+ _GlobalModeCard(mode: _mode, onChanged: _setMode),
391
+ const SizedBox(height: 16),
392
+ if (_mode == 'allow_all')
393
+ _InfoBanner(
394
+ icon: Icons.warning_amber_rounded,
395
+ color: colorScheme.errorContainer,
396
+ textColor: colorScheme.onErrorContainer,
397
+ message:
398
+ 'All tools are allowed the agent can use any capability without asking. '
399
+ 'Switch to "Default" or "Always ask" to re-enable approval checks.',
400
+ )
401
+ else ...<Widget>[
402
+ if (_mode == 'always_ask')
403
+ _InfoBanner(
404
+ icon: Icons.info_outline_rounded,
405
+ color: colorScheme.secondaryContainer,
406
+ textColor: colorScheme.onSecondaryContainer,
407
+ message:
408
+ 'The agent will ask before every sensitive tool, '
409
+ 'regardless of per-category settings below.',
410
+ ),
411
+ const SizedBox(height: 4),
412
+ const Padding(
413
+ padding: EdgeInsets.only(top: 4, bottom: 6),
414
+ child: _SectionTitle('Per-category permissions'),
415
+ ),
416
+ ..._policies.entries.map(
417
+ (e) => Padding(
418
+ padding: const EdgeInsets.only(bottom: 8),
419
+ child: _PolicyCard(
420
+ category: e.key,
421
+ policy: e.value,
422
+ dimmed: _mode == 'always_ask',
423
+ onChanged: (p) => _setPolicy(e.key, p),
372
424
  ),
373
- ..._policies.entries.map((e) => Padding(
374
- padding: const EdgeInsets.only(bottom: 8),
375
- child: _PolicyCard(
376
- category: e.key,
377
- policy: e.value,
378
- dimmed: _mode == 'always_ask',
379
- onChanged: (p) => _setPolicy(e.key, p),
380
- ),
381
- )),
382
- ],
383
- ],
384
- ),
425
+ ),
426
+ ),
427
+ ],
428
+ ],
429
+ ),
385
430
  );
386
431
  }
387
432
  }
@@ -399,11 +444,22 @@ class _ErrorView extends StatelessWidget {
399
444
  children: <Widget>[
400
445
  Icon(Icons.error_outline, size: 48, color: _danger),
401
446
  const SizedBox(height: 12),
402
- Text('Failed to load policies', style: Theme.of(context).textTheme.titleMedium),
447
+ Text(
448
+ 'Failed to load policies',
449
+ style: Theme.of(context).textTheme.titleMedium,
450
+ ),
403
451
  const SizedBox(height: 6),
404
- Text(error, style: TextStyle(fontSize: 12, color: _textSecondary), textAlign: TextAlign.center),
452
+ Text(
453
+ error,
454
+ style: TextStyle(fontSize: 12, color: _textSecondary),
455
+ textAlign: TextAlign.center,
456
+ ),
405
457
  const SizedBox(height: 16),
406
- OutlinedButton.icon(onPressed: onRetry, icon: const Icon(Icons.refresh), label: const Text('Retry')),
458
+ OutlinedButton.icon(
459
+ onPressed: onRetry,
460
+ icon: const Icon(Icons.refresh),
461
+ label: const Text('Retry'),
462
+ ),
407
463
  ],
408
464
  ),
409
465
  );
@@ -427,13 +483,21 @@ class _InfoBanner extends StatelessWidget {
427
483
  return Container(
428
484
  margin: const EdgeInsets.only(bottom: 8),
429
485
  padding: const EdgeInsets.all(12),
430
- decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(12)),
486
+ decoration: BoxDecoration(
487
+ color: color,
488
+ borderRadius: BorderRadius.circular(12),
489
+ ),
431
490
  child: Row(
432
491
  crossAxisAlignment: CrossAxisAlignment.start,
433
492
  children: <Widget>[
434
493
  Icon(icon, color: textColor, size: 18),
435
494
  const SizedBox(width: 10),
436
- Expanded(child: Text(message, style: TextStyle(color: textColor, fontSize: 13))),
495
+ Expanded(
496
+ child: Text(
497
+ message,
498
+ style: TextStyle(color: textColor, fontSize: 13),
499
+ ),
500
+ ),
437
501
  ],
438
502
  ),
439
503
  );
@@ -463,7 +527,10 @@ class _GlobalModeCard extends StatelessWidget {
463
527
  children: <Widget>[
464
528
  Icon(Icons.tune_rounded, size: 18, color: _accent),
465
529
  const SizedBox(width: 8),
466
- const Text('Global security mode', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 14)),
530
+ const Text(
531
+ 'Global security mode',
532
+ style: TextStyle(fontWeight: FontWeight.w700, fontSize: 14),
533
+ ),
467
534
  ],
468
535
  ),
469
536
  const SizedBox(height: 12),
@@ -471,7 +538,8 @@ class _GlobalModeCard extends StatelessWidget {
471
538
  value: 'allow_all',
472
539
  current: mode,
473
540
  label: 'Allow all',
474
- subtitle: 'No approval prompts — agent runs without interruption.',
541
+ subtitle:
542
+ 'No approval prompts — agent runs without interruption.',
475
543
  icon: Icons.lock_open_rounded,
476
544
  color: _warning,
477
545
  onTap: () => onChanged('allow_all'),
@@ -545,13 +613,18 @@ class _ModeOption extends StatelessWidget {
545
613
  child: Column(
546
614
  crossAxisAlignment: CrossAxisAlignment.start,
547
615
  children: <Widget>[
548
- Text(label,
549
- style: TextStyle(
550
- fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
551
- fontSize: 13,
552
- color: selected ? color : null,
553
- )),
554
- Text(subtitle, style: TextStyle(fontSize: 11, color: _textSecondary)),
616
+ Text(
617
+ label,
618
+ style: TextStyle(
619
+ fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
620
+ fontSize: 13,
621
+ color: selected ? color : null,
622
+ ),
623
+ ),
624
+ Text(
625
+ subtitle,
626
+ style: TextStyle(fontSize: 11, color: _textSecondary),
627
+ ),
555
628
  ],
556
629
  ),
557
630
  ),
@@ -609,20 +682,37 @@ class _PolicyCard extends StatelessWidget {
609
682
  child: Column(
610
683
  crossAxisAlignment: CrossAxisAlignment.start,
611
684
  children: <Widget>[
612
- Text(info.label, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13)),
613
- Text(info.subtitle, style: TextStyle(fontSize: 11, color: _textSecondary)),
685
+ Text(
686
+ info.label,
687
+ style: const TextStyle(
688
+ fontWeight: FontWeight.w600,
689
+ fontSize: 13,
690
+ ),
691
+ ),
692
+ Text(
693
+ info.subtitle,
694
+ style: TextStyle(fontSize: 11, color: _textSecondary),
695
+ ),
614
696
  ],
615
697
  ),
616
698
  ),
617
699
  Container(
618
- padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
700
+ padding: const EdgeInsets.symmetric(
701
+ horizontal: 7,
702
+ vertical: 3,
703
+ ),
619
704
  decoration: BoxDecoration(
620
705
  color: riskColor.withAlpha(20),
621
706
  borderRadius: BorderRadius.circular(20),
622
707
  ),
623
708
  child: Text(
624
709
  info.riskLevel.toUpperCase(),
625
- style: TextStyle(fontSize: 9, fontWeight: FontWeight.w700, color: riskColor, letterSpacing: 0.5),
710
+ style: TextStyle(
711
+ fontSize: 9,
712
+ fontWeight: FontWeight.w700,
713
+ color: riskColor,
714
+ letterSpacing: 0.5,
715
+ ),
626
716
  ),
627
717
  ),
628
718
  ],
@@ -630,16 +720,34 @@ class _PolicyCard extends StatelessWidget {
630
720
  const SizedBox(height: 12),
631
721
  SegmentedButton<String>(
632
722
  segments: const <ButtonSegment<String>>[
633
- ButtonSegment<String>(value: 'deny', label: Text('Block'), icon: Icon(Icons.block_rounded, size: 13)),
634
- ButtonSegment<String>(value: 'require_approval', label: Text('Ask me'), icon: Icon(Icons.pan_tool_outlined, size: 13)),
635
- ButtonSegment<String>(value: 'allow', label: Text('Allow'), icon: Icon(Icons.check_rounded, size: 13)),
636
- ButtonSegment<String>(value: 'allow_always', label: Text('Always'), icon: Icon(Icons.verified_rounded, size: 13)),
723
+ ButtonSegment<String>(
724
+ value: 'deny',
725
+ label: Text('Block'),
726
+ icon: Icon(Icons.block_rounded, size: 13),
727
+ ),
728
+ ButtonSegment<String>(
729
+ value: 'require_approval',
730
+ label: Text('Ask me'),
731
+ icon: Icon(Icons.pan_tool_outlined, size: 13),
732
+ ),
733
+ ButtonSegment<String>(
734
+ value: 'allow',
735
+ label: Text('Allow'),
736
+ icon: Icon(Icons.check_rounded, size: 13),
737
+ ),
738
+ ButtonSegment<String>(
739
+ value: 'allow_always',
740
+ label: Text('Always'),
741
+ icon: Icon(Icons.verified_rounded, size: 13),
742
+ ),
637
743
  ],
638
744
  selected: <String>{policy},
639
745
  onSelectionChanged: dimmed ? null : (s) => onChanged(s.first),
640
746
  style: ButtonStyle(
641
747
  visualDensity: VisualDensity.compact,
642
- textStyle: WidgetStateProperty.all(const TextStyle(fontSize: 11)),
748
+ textStyle: WidgetStateProperty.all(
749
+ const TextStyle(fontSize: 11),
750
+ ),
643
751
  ),
644
752
  ),
645
753
  const SizedBox(height: 6),
@@ -659,9 +767,18 @@ class _PolicyHint extends StatelessWidget {
659
767
  @override
660
768
  Widget build(BuildContext context) {
661
769
  final (text, color) = switch (policy) {
662
- 'deny' => ('Completely blocked — the agent cannot use this category.', _danger),
663
- 'require_approval' => ('Agent pauses and asks you before running.', _warning),
664
- 'allow' => ('Allowed for this run — will ask again next session.', _accentAlt),
770
+ 'deny' => (
771
+ 'Completely blocked the agent cannot use this category.',
772
+ _danger,
773
+ ),
774
+ 'require_approval' => (
775
+ 'Agent pauses and asks you before running.',
776
+ _warning,
777
+ ),
778
+ 'allow' => (
779
+ 'Allowed for this run — will ask again next session.',
780
+ _accentAlt,
781
+ ),
665
782
  'allow_always' => ('Permanently allowed — never asks again.', _info),
666
783
  _ => ('', _textSecondary),
667
784
  };
@@ -713,7 +830,9 @@ class _ToolApprovalSheetState extends State<ToolApprovalSheet>
713
830
  });
714
831
 
715
832
  // Cancel the notification now that the sheet is showing
716
- _SecurityNotificationService.cancelApprovalNotification(widget.request.approvalId);
833
+ _SecurityNotificationService.cancelApprovalNotification(
834
+ widget.request.approvalId,
835
+ );
717
836
  }
718
837
 
719
838
  @override
@@ -744,10 +863,7 @@ class _ToolApprovalSheetState extends State<ToolApprovalSheet>
744
863
  widget.controller.clearPendingApproval();
745
864
  if (mounted) {
746
865
  ScaffoldMessenger.of(context).showSnackBar(
747
- SnackBar(
748
- content: Text(error.message),
749
- backgroundColor: _warning,
750
- ),
866
+ SnackBar(content: Text(error.message), backgroundColor: _warning),
751
867
  );
752
868
  }
753
869
  }
@@ -769,7 +885,15 @@ class _ToolApprovalSheetState extends State<ToolApprovalSheet>
769
885
  }
770
886
 
771
887
  String _redact(String key, dynamic value) {
772
- const sensitive = <String>['token', 'secret', 'password', 'key', 'api_key', 'auth', 'credential'];
888
+ const sensitive = <String>[
889
+ 'token',
890
+ 'secret',
891
+ 'password',
892
+ 'key',
893
+ 'api_key',
894
+ 'auth',
895
+ 'credential',
896
+ ];
773
897
  if (sensitive.any((s) => key.toLowerCase().contains(s))) return '••••••';
774
898
  return value?.toString() ?? 'null';
775
899
  }
@@ -793,7 +917,8 @@ class _ToolApprovalSheetState extends State<ToolApprovalSheet>
793
917
  Center(
794
918
  child: Container(
795
919
  margin: const EdgeInsets.only(top: 10, bottom: 6),
796
- width: 38, height: 4,
920
+ width: 38,
921
+ height: 4,
797
922
  decoration: BoxDecoration(
798
923
  color: colorScheme.outlineVariant,
799
924
  borderRadius: BorderRadius.circular(2),
@@ -807,7 +932,8 @@ class _ToolApprovalSheetState extends State<ToolApprovalSheet>
807
932
  children: <Widget>[
808
933
  // Countdown ring
809
934
  SizedBox(
810
- width: 52, height: 52,
935
+ width: 52,
936
+ height: 52,
811
937
  child: Stack(
812
938
  alignment: Alignment.center,
813
939
  children: <Widget>[
@@ -816,7 +942,8 @@ class _ToolApprovalSheetState extends State<ToolApprovalSheet>
816
942
  builder: (_, __) => CircularProgressIndicator(
817
943
  value: 1 - _ringController.value,
818
944
  strokeWidth: 3.5,
819
- backgroundColor: colorScheme.surfaceContainerHighest,
945
+ backgroundColor:
946
+ colorScheme.surfaceContainerHighest,
820
947
  color: ringColor,
821
948
  ),
822
949
  ),
@@ -837,8 +964,13 @@ class _ToolApprovalSheetState extends State<ToolApprovalSheet>
837
964
  child: Column(
838
965
  crossAxisAlignment: CrossAxisAlignment.start,
839
966
  children: <Widget>[
840
- const Text('Approval required',
841
- style: TextStyle(fontWeight: FontWeight.w700, fontSize: 16)),
967
+ const Text(
968
+ 'Approval required',
969
+ style: TextStyle(
970
+ fontWeight: FontWeight.w700,
971
+ fontSize: 16,
972
+ ),
973
+ ),
842
974
  const SizedBox(height: 4),
843
975
  Row(
844
976
  children: <Widget>[
@@ -848,15 +980,21 @@ class _ToolApprovalSheetState extends State<ToolApprovalSheet>
848
980
  color: info.color.withAlpha(22),
849
981
  borderRadius: BorderRadius.circular(6),
850
982
  ),
851
- child: Icon(info.icon, size: 13, color: info.color),
983
+ child: Icon(
984
+ info.icon,
985
+ size: 13,
986
+ color: info.color,
987
+ ),
852
988
  ),
853
989
  const SizedBox(width: 6),
854
- Text(info.label,
855
- style: TextStyle(
856
- fontSize: 12,
857
- color: info.color,
858
- fontWeight: FontWeight.w600,
859
- )),
990
+ Text(
991
+ info.label,
992
+ style: TextStyle(
993
+ fontSize: 12,
994
+ color: info.color,
995
+ fontWeight: FontWeight.w600,
996
+ ),
997
+ ),
860
998
  ],
861
999
  ),
862
1000
  ],
@@ -882,8 +1020,12 @@ class _ToolApprovalSheetState extends State<ToolApprovalSheet>
882
1020
  Row(
883
1021
  children: <Widget>[
884
1022
  Container(
885
- width: 7, height: 7,
886
- decoration: BoxDecoration(color: info.color, shape: BoxShape.circle),
1023
+ width: 7,
1024
+ height: 7,
1025
+ decoration: BoxDecoration(
1026
+ color: info.color,
1027
+ shape: BoxShape.circle,
1028
+ ),
887
1029
  ),
888
1030
  const SizedBox(width: 6),
889
1031
  Text(
@@ -932,7 +1074,9 @@ class _ToolApprovalSheetState extends State<ToolApprovalSheet>
932
1074
  label: const Text('Deny'),
933
1075
  style: OutlinedButton.styleFrom(
934
1076
  foregroundColor: colorScheme.error,
935
- side: BorderSide(color: colorScheme.error.withAlpha(100)),
1077
+ side: BorderSide(
1078
+ color: colorScheme.error.withAlpha(100),
1079
+ ),
936
1080
  ),
937
1081
  onPressed: () => _decide('denied', 'once'),
938
1082
  ),
@@ -940,7 +1084,10 @@ class _ToolApprovalSheetState extends State<ToolApprovalSheet>
940
1084
  const SizedBox(width: 8),
941
1085
  Expanded(
942
1086
  child: OutlinedButton.icon(
943
- icon: const Icon(Icons.check_circle_outline, size: 15),
1087
+ icon: const Icon(
1088
+ Icons.check_circle_outline,
1089
+ size: 15,
1090
+ ),
944
1091
  label: const Text('Allow once'),
945
1092
  onPressed: () => _decide('approved', 'once'),
946
1093
  ),
@@ -954,7 +1101,9 @@ class _ToolApprovalSheetState extends State<ToolApprovalSheet>
954
1101
  child: OutlinedButton.icon(
955
1102
  icon: const Icon(Icons.history_rounded, size: 15),
956
1103
  label: const Text('Allow session'),
957
- style: OutlinedButton.styleFrom(foregroundColor: _info),
1104
+ style: OutlinedButton.styleFrom(
1105
+ foregroundColor: _info,
1106
+ ),
958
1107
  onPressed: () => _decide('approved', 'session'),
959
1108
  ),
960
1109
  ),
@@ -963,7 +1112,9 @@ class _ToolApprovalSheetState extends State<ToolApprovalSheet>
963
1112
  child: FilledButton.icon(
964
1113
  icon: const Icon(Icons.verified_rounded, size: 15),
965
1114
  label: const Text('Always allow'),
966
- style: FilledButton.styleFrom(backgroundColor: info.color),
1115
+ style: FilledButton.styleFrom(
1116
+ backgroundColor: info.color,
1117
+ ),
967
1118
  onPressed: () => _decide('approved', 'always'),
968
1119
  ),
969
1120
  ),
@@ -973,7 +1124,10 @@ class _ToolApprovalSheetState extends State<ToolApprovalSheet>
973
1124
  Text(
974
1125
  '"Always allow" saves the policy permanently — you can change it in Settings.',
975
1126
  textAlign: TextAlign.center,
976
- style: TextStyle(fontSize: 10, color: colorScheme.onSurfaceVariant),
1127
+ style: TextStyle(
1128
+ fontSize: 10,
1129
+ color: colorScheme.onSurfaceVariant,
1130
+ ),
977
1131
  ),
978
1132
  ],
979
1133
  ),
@@ -343,10 +343,11 @@ class _SettingsPanelState extends State<SettingsPanel> {
343
343
  onPopInvokedWithResult: (didPop, result) async {
344
344
  if (didPop) return;
345
345
  final action = await _showLeaveDialog(context);
346
- if (action == _LeaveAction.save && mounted) {
346
+ if (!context.mounted) return;
347
+ if (action == _LeaveAction.save) {
347
348
  await _doSave();
348
- if (mounted) Navigator.of(context).pop();
349
- } else if (action == _LeaveAction.discard && mounted) {
349
+ if (context.mounted) Navigator.of(context).pop();
350
+ } else if (action == _LeaveAction.discard) {
350
351
  _hydrate();
351
352
  setState(() => _hasUnsavedChanges = false);
352
353
  Navigator.of(context).pop();