neoagent 3.0.1-beta.9 → 3.1.0
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 +2 -10
- package/README.md +12 -3
- package/docs/automation.md +37 -0
- package/docs/benchmarking.md +102 -0
- package/docs/billing.md +34 -8
- package/docs/configuration.md +11 -2
- package/docs/getting-started.md +10 -8
- package/docs/operations.md +1 -1
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +138 -0
- package/flutter_app/lib/main_app_shell.dart +38 -13
- package/flutter_app/lib/main_billing.dart +1465 -0
- package/flutter_app/lib/main_chat.dart +1612 -214
- package/flutter_app/lib/main_controller.dart +398 -26
- package/flutter_app/lib/main_devices.dart +293 -207
- package/flutter_app/lib/main_models.dart +142 -0
- package/flutter_app/lib/main_navigation.dart +19 -1
- package/flutter_app/lib/main_operations.dart +288 -9
- package/flutter_app/lib/main_settings.dart +510 -300
- package/flutter_app/lib/main_shared.dart +2 -0
- package/flutter_app/lib/main_timeline.dart +1378 -0
- package/flutter_app/lib/src/backend_client.dart +149 -19
- package/flutter_app/lib/src/desktop_companion_actions.dart +132 -21
- package/flutter_app/lib/src/desktop_companion_io.dart +65 -1
- package/flutter_app/lib/src/desktop_companion_stub.dart +12 -0
- package/flutter_app/lib/src/desktop_ocr_bridge.dart +2 -0
- package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +125 -0
- package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +30 -0
- package/flutter_app/lib/src/desktop_passive_history.dart +332 -0
- package/flutter_app/lib/src/recording_bridge_io.dart +80 -72
- package/flutter_app/lib/src/recording_bridge_web.dart +127 -114
- package/flutter_app/lib/src/recording_chunk_queue.dart +149 -0
- package/flutter_app/lib/src/recording_chunk_queue_io.dart +182 -0
- package/flutter_app/lib/src/recording_payloads.dart +9 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +25 -0
- package/flutter_app/windows/runner/flutter_window.cpp +75 -0
- package/landing/index.html +3 -3
- package/lib/manager.js +184 -66
- package/package.json +4 -1
- package/server/admin/access.js +12 -7
- package/server/admin/admin.js +436 -10
- package/server/admin/billing.js +158 -35
- package/server/admin/index.html +72 -2
- package/server/admin/users.js +15 -15
- package/server/db/database.js +125 -20
- package/server/http/routes.js +1 -0
- package/server/http/static.js +4 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/canvaskit/wimp.js.symbols +8475 -8467
- package/server/public/canvaskit/wimp.wasm +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +91077 -87037
- package/server/routes/account.js +53 -0
- package/server/routes/admin.js +345 -64
- package/server/routes/agents.js +203 -21
- package/server/routes/billing.js +5 -0
- package/server/routes/browser.js +8 -1
- package/server/routes/recordings.js +96 -6
- package/server/routes/screenHistory.js +140 -2
- package/server/routes/timeline.js +43 -0
- package/server/services/account/erasure.js +263 -0
- package/server/services/ai/hooks.js +4 -1
- package/server/services/ai/loop/agent_engine_core.js +8 -1
- package/server/services/ai/loop/blank_recovery.js +36 -0
- package/server/services/ai/loop/conversation_loop.js +166 -33
- package/server/services/ai/messagingFallback.js +22 -0
- package/server/services/ai/rate_limits.js +28 -5
- package/server/services/ai/systemPrompt.js +6 -5
- package/server/services/ai/taskAnalysis.js +2 -0
- package/server/services/ai/toolEvidence.js +15 -0
- package/server/services/ai/toolResult.js +40 -0
- package/server/services/ai/tools.js +163 -4
- package/server/services/android/controller.js +6 -2
- package/server/services/billing/plans.js +2 -1
- package/server/services/browser/anti_detection.js +192 -0
- package/server/services/browser/controller.js +180 -54
- package/server/services/desktop/auth.js +3 -0
- package/server/services/desktop/registry.js +50 -2
- package/server/services/integrations/google/calendar.js +22 -14
- package/server/services/manager.js +12 -42
- package/server/services/memory/ingestion_chunking.js +268 -0
- package/server/services/messaging/telnyx.js +9 -8
- package/server/services/recordings/manager.js +60 -27
- package/server/services/runtime/docker-vm-manager.js +157 -266
- package/server/services/runtime/guest_bootstrap.js +17 -5
- package/server/services/runtime/guest_image.js +188 -0
- package/server/services/runtime/manager.js +0 -1
- package/server/services/runtime/validation.js +3 -8
- package/server/services/social_video/service.js +60 -10
- package/server/services/tasks/runtime.js +234 -9
- package/server/services/tasks/task_repository.js +13 -0
- package/server/services/timeline/service.js +558 -0
- package/server/services/wearable/gateway.js +1 -1
- package/server/services/websocket.js +21 -3
- package/server/services/desktop/screenRecorder.js +0 -292
- package/server/services/desktop/screen_recorder_support.js +0 -46
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
part of 'main.dart';
|
|
2
2
|
|
|
3
|
+
enum _LeaveAction { save, discard, cancel }
|
|
4
|
+
|
|
3
5
|
class SettingsPanel extends StatefulWidget {
|
|
4
6
|
const SettingsPanel({
|
|
5
7
|
super.key,
|
|
@@ -190,6 +192,8 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
190
192
|
final Map<String, TextEditingController> _providerBaseUrlControllers =
|
|
191
193
|
<String, TextEditingController>{};
|
|
192
194
|
|
|
195
|
+
bool _hasUnsavedChanges = false;
|
|
196
|
+
|
|
193
197
|
// Inline runtime test state — ephemeral, not stored in controller.
|
|
194
198
|
bool _cliTestRunning = false;
|
|
195
199
|
Map<String, dynamic>? _cliTestResult;
|
|
@@ -314,8 +318,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
314
318
|
final routingModels = availableModels.isEmpty
|
|
315
319
|
? controller.supportedModels
|
|
316
320
|
: availableModels;
|
|
317
|
-
final List<_ModelPickerOption> modelChoices =
|
|
318
|
-
|
|
321
|
+
final List<_ModelPickerOption> modelChoices = _modelPickerOptions(
|
|
322
|
+
routingModels,
|
|
323
|
+
allowAuto: true,
|
|
324
|
+
);
|
|
319
325
|
final enabledSmartModels = _enabledModels
|
|
320
326
|
.where((id) => routingModels.any((model) => model.id == id))
|
|
321
327
|
.length;
|
|
@@ -323,113 +329,131 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
323
329
|
.where((section) => !section.requiresDesktop || _supportsDesktopShell)
|
|
324
330
|
.toSet();
|
|
325
331
|
|
|
326
|
-
return
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
332
|
+
return PopScope(
|
|
333
|
+
canPop: !_hasUnsavedChanges,
|
|
334
|
+
onPopInvokedWithResult: (didPop, result) async {
|
|
335
|
+
if (didPop) return;
|
|
336
|
+
final action = await _showLeaveDialog(context);
|
|
337
|
+
if (action == _LeaveAction.save && mounted) {
|
|
338
|
+
await _doSave();
|
|
339
|
+
if (mounted) Navigator.of(context).pop();
|
|
340
|
+
} else if (action == _LeaveAction.discard && mounted) {
|
|
341
|
+
_hydrate();
|
|
342
|
+
setState(() => _hasUnsavedChanges = false);
|
|
343
|
+
Navigator.of(context).pop();
|
|
344
|
+
}
|
|
345
|
+
// cancel: do nothing
|
|
346
|
+
},
|
|
347
|
+
child: ListView(
|
|
348
|
+
padding: widget.embedded ? EdgeInsets.zero : _pagePadding(context),
|
|
349
|
+
children: <Widget>[
|
|
350
|
+
if (!widget.embedded)
|
|
351
|
+
_PageTitle(
|
|
352
|
+
title: 'Settings',
|
|
353
|
+
subtitle:
|
|
354
|
+
'Workspace, models, recording, and diagnostics controls.',
|
|
355
|
+
trailing: _settingsSaveButton(controller),
|
|
356
|
+
)
|
|
357
|
+
else
|
|
358
|
+
Align(
|
|
359
|
+
alignment: Alignment.centerRight,
|
|
360
|
+
child: Padding(
|
|
361
|
+
padding: const EdgeInsets.only(bottom: 12),
|
|
362
|
+
child: _settingsSaveButton(controller),
|
|
363
|
+
),
|
|
364
|
+
),
|
|
365
|
+
if (controller.errorMessage != null) ...<Widget>[
|
|
366
|
+
_InlineError(message: controller.errorMessage!),
|
|
367
|
+
const SizedBox(height: 16),
|
|
368
|
+
],
|
|
369
|
+
TextField(
|
|
370
|
+
controller: _searchController,
|
|
371
|
+
onChanged: (_) => setState(() {}),
|
|
372
|
+
decoration: InputDecoration(
|
|
373
|
+
labelText: 'Search settings',
|
|
374
|
+
hintText: 'Models, browser, voice, diagnostics...',
|
|
375
|
+
prefixIcon: const Icon(Icons.search),
|
|
376
|
+
suffixIcon: searchQuery.isEmpty
|
|
377
|
+
? null
|
|
378
|
+
: IconButton(
|
|
379
|
+
onPressed: () {
|
|
380
|
+
_searchController.clear();
|
|
381
|
+
setState(() {});
|
|
382
|
+
},
|
|
383
|
+
icon: const Icon(Icons.close),
|
|
384
|
+
),
|
|
342
385
|
),
|
|
343
386
|
),
|
|
344
|
-
if (controller.errorMessage != null) ...<Widget>[
|
|
345
|
-
_InlineError(message: controller.errorMessage!),
|
|
346
|
-
const SizedBox(height: 16),
|
|
347
|
-
],
|
|
348
|
-
TextField(
|
|
349
|
-
controller: _searchController,
|
|
350
|
-
onChanged: (_) => setState(() {}),
|
|
351
|
-
decoration: InputDecoration(
|
|
352
|
-
labelText: 'Search settings',
|
|
353
|
-
hintText: 'Models, browser, voice, diagnostics...',
|
|
354
|
-
prefixIcon: const Icon(Icons.search),
|
|
355
|
-
suffixIcon: searchQuery.isEmpty
|
|
356
|
-
? null
|
|
357
|
-
: IconButton(
|
|
358
|
-
onPressed: () {
|
|
359
|
-
_searchController.clear();
|
|
360
|
-
setState(() {});
|
|
361
|
-
},
|
|
362
|
-
icon: const Icon(Icons.close),
|
|
363
|
-
),
|
|
364
|
-
),
|
|
365
|
-
),
|
|
366
|
-
const SizedBox(height: 16),
|
|
367
|
-
if (_matchesSettingsSection(
|
|
368
|
-
searchQuery,
|
|
369
|
-
_overviewSettingsSection,
|
|
370
|
-
)) ...<Widget>[
|
|
371
|
-
_buildSettingsOverview(controller, availableModels.length),
|
|
372
|
-
const SizedBox(height: 16),
|
|
373
|
-
],
|
|
374
|
-
if (_matchesSettingsSection(
|
|
375
|
-
searchQuery,
|
|
376
|
-
_workspaceSettingsSection,
|
|
377
|
-
)) ...<Widget>[
|
|
378
|
-
_buildWorkspaceSection(controller),
|
|
379
|
-
const SizedBox(height: 16),
|
|
380
|
-
],
|
|
381
|
-
if (_matchesSettingsSection(
|
|
382
|
-
searchQuery,
|
|
383
|
-
_modelsSettingsSection,
|
|
384
|
-
)) ...<Widget>[
|
|
385
|
-
_buildModelsSection(
|
|
386
|
-
context: context,
|
|
387
|
-
controller: controller,
|
|
388
|
-
modelChoices: modelChoices,
|
|
389
|
-
routingModels: routingModels,
|
|
390
|
-
availableModels: availableModels,
|
|
391
|
-
enabledSmartModels: enabledSmartModels,
|
|
392
|
-
),
|
|
393
|
-
const SizedBox(height: 16),
|
|
394
|
-
],
|
|
395
|
-
if (_matchesSettingsSection(
|
|
396
|
-
searchQuery,
|
|
397
|
-
_voiceRecordingSettingsSection,
|
|
398
|
-
)) ...<Widget>[
|
|
399
|
-
_buildVoiceAndRecordingSection(
|
|
400
|
-
controller: controller,
|
|
401
|
-
modelChoices: modelChoices,
|
|
402
|
-
routingModels: routingModels,
|
|
403
|
-
),
|
|
404
|
-
const SizedBox(height: 16),
|
|
405
|
-
],
|
|
406
|
-
if (visibleSearchSections.contains(_desktopSettingsSection) &&
|
|
407
|
-
_matchesSettingsSection(
|
|
408
|
-
searchQuery,
|
|
409
|
-
_desktopSettingsSection,
|
|
410
|
-
)) ...<Widget>[
|
|
411
|
-
_buildDesktopSection(controller),
|
|
412
|
-
const SizedBox(height: 16),
|
|
413
|
-
],
|
|
414
|
-
if (_matchesSettingsSection(
|
|
415
|
-
searchQuery,
|
|
416
|
-
_securitySettingsSection,
|
|
417
|
-
)) ...<Widget>[
|
|
418
|
-
_buildSecuritySection(context, controller),
|
|
419
387
|
const SizedBox(height: 16),
|
|
388
|
+
if (_matchesSettingsSection(
|
|
389
|
+
searchQuery,
|
|
390
|
+
_overviewSettingsSection,
|
|
391
|
+
)) ...<Widget>[
|
|
392
|
+
_buildSettingsOverview(controller, availableModels.length),
|
|
393
|
+
const SizedBox(height: 16),
|
|
394
|
+
],
|
|
395
|
+
if (_matchesSettingsSection(
|
|
396
|
+
searchQuery,
|
|
397
|
+
_workspaceSettingsSection,
|
|
398
|
+
)) ...<Widget>[
|
|
399
|
+
_buildWorkspaceSection(controller),
|
|
400
|
+
const SizedBox(height: 16),
|
|
401
|
+
],
|
|
402
|
+
if (_matchesSettingsSection(
|
|
403
|
+
searchQuery,
|
|
404
|
+
_modelsSettingsSection,
|
|
405
|
+
)) ...<Widget>[
|
|
406
|
+
_buildModelsSection(
|
|
407
|
+
context: context,
|
|
408
|
+
controller: controller,
|
|
409
|
+
modelChoices: modelChoices,
|
|
410
|
+
routingModels: routingModels,
|
|
411
|
+
availableModels: availableModels,
|
|
412
|
+
enabledSmartModels: enabledSmartModels,
|
|
413
|
+
),
|
|
414
|
+
const SizedBox(height: 16),
|
|
415
|
+
],
|
|
416
|
+
if (_matchesSettingsSection(
|
|
417
|
+
searchQuery,
|
|
418
|
+
_voiceRecordingSettingsSection,
|
|
419
|
+
)) ...<Widget>[
|
|
420
|
+
_buildVoiceAndRecordingSection(
|
|
421
|
+
controller: controller,
|
|
422
|
+
modelChoices: modelChoices,
|
|
423
|
+
routingModels: routingModels,
|
|
424
|
+
),
|
|
425
|
+
const SizedBox(height: 16),
|
|
426
|
+
],
|
|
427
|
+
if (visibleSearchSections.contains(_desktopSettingsSection) &&
|
|
428
|
+
_matchesSettingsSection(
|
|
429
|
+
searchQuery,
|
|
430
|
+
_desktopSettingsSection,
|
|
431
|
+
)) ...<Widget>[
|
|
432
|
+
_buildDesktopSection(controller),
|
|
433
|
+
const SizedBox(height: 16),
|
|
434
|
+
],
|
|
435
|
+
if (_matchesSettingsSection(
|
|
436
|
+
searchQuery,
|
|
437
|
+
_securitySettingsSection,
|
|
438
|
+
)) ...<Widget>[
|
|
439
|
+
_buildSecuritySection(context, controller),
|
|
440
|
+
const SizedBox(height: 16),
|
|
441
|
+
],
|
|
442
|
+
if (_matchesSettingsSection(
|
|
443
|
+
searchQuery,
|
|
444
|
+
_diagnosticsSettingsSection,
|
|
445
|
+
)) ...<Widget>[_buildDiagnosticsSection(controller)],
|
|
446
|
+
if (_noSettingsMatches(
|
|
447
|
+
searchQuery,
|
|
448
|
+
visibleSearchSections,
|
|
449
|
+
)) ...<Widget>[
|
|
450
|
+
const _EmptyCard(
|
|
451
|
+
title: 'No matching settings',
|
|
452
|
+
subtitle: 'Try a broader search like models, browser, or voice.',
|
|
453
|
+
),
|
|
454
|
+
],
|
|
420
455
|
],
|
|
421
|
-
|
|
422
|
-
searchQuery,
|
|
423
|
-
_diagnosticsSettingsSection,
|
|
424
|
-
)) ...<Widget>[_buildDiagnosticsSection(controller)],
|
|
425
|
-
if (_noSettingsMatches(searchQuery, visibleSearchSections)) ...<Widget>[
|
|
426
|
-
const _EmptyCard(
|
|
427
|
-
title: 'No matching settings',
|
|
428
|
-
subtitle:
|
|
429
|
-
'Try a broader search like models, browser, or voice.',
|
|
430
|
-
),
|
|
431
|
-
],
|
|
432
|
-
],
|
|
456
|
+
),
|
|
433
457
|
);
|
|
434
458
|
}
|
|
435
459
|
|
|
@@ -456,44 +480,45 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
456
480
|
);
|
|
457
481
|
}
|
|
458
482
|
|
|
483
|
+
Future<void> _doSave() async {
|
|
484
|
+
final controller = widget.controller;
|
|
485
|
+
await controller.saveSettings(
|
|
486
|
+
browserBackend: _browserBackend == 'extension' ? 'extension' : 'vm',
|
|
487
|
+
browserExtensionTokenId: _browserBackend == 'extension'
|
|
488
|
+
? _browserExtensionTokenId
|
|
489
|
+
: null,
|
|
490
|
+
cliBackend: _cliBackend == 'desktop' ? 'desktop' : 'vm',
|
|
491
|
+
cliDesktopDeviceId: _cliDesktopDeviceId,
|
|
492
|
+
smarterSelector: _smarterSelector,
|
|
493
|
+
enabledModels: _enabledModels.toList(),
|
|
494
|
+
defaultChatModel: _defaultChatModel,
|
|
495
|
+
defaultSubagentModel: _defaultSubagentModel,
|
|
496
|
+
defaultRecordingTranscriptionProvider: 'deepgram',
|
|
497
|
+
defaultRecordingTranscriptionModel: _defaultRecordingTranscriptionModel,
|
|
498
|
+
defaultRecordingSummaryProvider: _providerForSelectedModel(
|
|
499
|
+
_defaultRecordingSummaryModel,
|
|
500
|
+
controller.supportedModels,
|
|
501
|
+
),
|
|
502
|
+
defaultRecordingSummaryModel: _defaultRecordingSummaryModel,
|
|
503
|
+
fallbackModel: _fallbackModel,
|
|
504
|
+
defaultSpeechModel: _defaultSpeechModel,
|
|
505
|
+
voiceSttProvider: controller.voiceSttProvider,
|
|
506
|
+
voiceSttModel: controller.voiceSttModel,
|
|
507
|
+
voiceTtsProvider: controller.voiceTtsProvider,
|
|
508
|
+
voiceTtsModel: controller.voiceTtsModel,
|
|
509
|
+
voiceTtsVoice: controller.voiceTtsVoice,
|
|
510
|
+
voiceRuntimeMode: 'live',
|
|
511
|
+
voiceLiveProvider: _voiceLiveProvider,
|
|
512
|
+
voiceLiveModel: _voiceLiveModel,
|
|
513
|
+
voiceLiveVoice: _voiceLiveVoice,
|
|
514
|
+
aiProviderConfigs: _buildProviderPayload(),
|
|
515
|
+
);
|
|
516
|
+
if (mounted) setState(() => _hasUnsavedChanges = false);
|
|
517
|
+
}
|
|
518
|
+
|
|
459
519
|
Widget _settingsSaveButton(NeoAgentController controller) {
|
|
460
|
-
|
|
461
|
-
onPressed: controller.isSavingSettings
|
|
462
|
-
? null
|
|
463
|
-
: () => controller.saveSettings(
|
|
464
|
-
browserBackend: _browserBackend == 'extension'
|
|
465
|
-
? 'extension'
|
|
466
|
-
: 'vm',
|
|
467
|
-
browserExtensionTokenId: _browserBackend == 'extension'
|
|
468
|
-
? _browserExtensionTokenId
|
|
469
|
-
: null,
|
|
470
|
-
cliBackend: _cliBackend == 'desktop' ? 'desktop' : 'vm',
|
|
471
|
-
cliDesktopDeviceId: _cliDesktopDeviceId,
|
|
472
|
-
smarterSelector: _smarterSelector,
|
|
473
|
-
enabledModels: _enabledModels.toList(),
|
|
474
|
-
defaultChatModel: _defaultChatModel,
|
|
475
|
-
defaultSubagentModel: _defaultSubagentModel,
|
|
476
|
-
defaultRecordingTranscriptionProvider: 'deepgram',
|
|
477
|
-
defaultRecordingTranscriptionModel:
|
|
478
|
-
_defaultRecordingTranscriptionModel,
|
|
479
|
-
defaultRecordingSummaryProvider: _providerForSelectedModel(
|
|
480
|
-
_defaultRecordingSummaryModel,
|
|
481
|
-
controller.supportedModels,
|
|
482
|
-
),
|
|
483
|
-
defaultRecordingSummaryModel: _defaultRecordingSummaryModel,
|
|
484
|
-
fallbackModel: _fallbackModel,
|
|
485
|
-
defaultSpeechModel: _defaultSpeechModel,
|
|
486
|
-
voiceSttProvider: controller.voiceSttProvider,
|
|
487
|
-
voiceSttModel: controller.voiceSttModel,
|
|
488
|
-
voiceTtsProvider: controller.voiceTtsProvider,
|
|
489
|
-
voiceTtsModel: controller.voiceTtsModel,
|
|
490
|
-
voiceTtsVoice: controller.voiceTtsVoice,
|
|
491
|
-
voiceRuntimeMode: 'live',
|
|
492
|
-
voiceLiveProvider: _voiceLiveProvider,
|
|
493
|
-
voiceLiveModel: _voiceLiveModel,
|
|
494
|
-
voiceLiveVoice: _voiceLiveVoice,
|
|
495
|
-
aiProviderConfigs: _buildProviderPayload(),
|
|
496
|
-
),
|
|
520
|
+
final button = FilledButton.icon(
|
|
521
|
+
onPressed: controller.isSavingSettings ? null : _doSave,
|
|
497
522
|
style: FilledButton.styleFrom(backgroundColor: _accent),
|
|
498
523
|
icon: controller.isSavingSettings
|
|
499
524
|
? const SizedBox.square(
|
|
@@ -506,6 +531,19 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
506
531
|
: Icon(Icons.save_outlined),
|
|
507
532
|
label: Text('Save'),
|
|
508
533
|
);
|
|
534
|
+
if (!_hasUnsavedChanges) return button;
|
|
535
|
+
return Column(
|
|
536
|
+
crossAxisAlignment: CrossAxisAlignment.end,
|
|
537
|
+
mainAxisSize: MainAxisSize.min,
|
|
538
|
+
children: <Widget>[
|
|
539
|
+
Text(
|
|
540
|
+
'Unsaved changes',
|
|
541
|
+
style: TextStyle(color: Colors.orange, fontSize: 12),
|
|
542
|
+
),
|
|
543
|
+
const SizedBox(height: 4),
|
|
544
|
+
button,
|
|
545
|
+
],
|
|
546
|
+
);
|
|
509
547
|
}
|
|
510
548
|
|
|
511
549
|
Widget _buildSettingsOverview(
|
|
@@ -623,6 +661,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
623
661
|
_browserBackend = value;
|
|
624
662
|
_browserExtensionTokenId ??=
|
|
625
663
|
controller.selectedBrowserExtensionTokenId;
|
|
664
|
+
_hasUnsavedChanges = true;
|
|
626
665
|
});
|
|
627
666
|
}
|
|
628
667
|
},
|
|
@@ -631,21 +670,27 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
631
670
|
if (_browserBackend == 'extension') ...<Widget>[
|
|
632
671
|
if (controller.browserExtensionTokens.isNotEmpty) ...<Widget>[
|
|
633
672
|
DropdownButtonFormField<String>(
|
|
634
|
-
initialValue:
|
|
635
|
-
|
|
636
|
-
|
|
673
|
+
initialValue:
|
|
674
|
+
controller.browserExtensionTokens.any(
|
|
675
|
+
(token) =>
|
|
676
|
+
token['tokenId']?.toString() ==
|
|
677
|
+
_browserExtensionTokenId,
|
|
678
|
+
)
|
|
637
679
|
? _browserExtensionTokenId
|
|
638
680
|
: null,
|
|
639
681
|
decoration: const InputDecoration(
|
|
640
682
|
labelText: 'Default extension',
|
|
641
|
-
helperText:
|
|
683
|
+
helperText:
|
|
684
|
+
'Choose which paired Chrome extension controls browser actions.',
|
|
642
685
|
),
|
|
643
686
|
items: controller.browserExtensionTokens.map((token) {
|
|
644
687
|
final tokenId = token['tokenId']?.toString() ?? '';
|
|
645
|
-
final label =
|
|
688
|
+
final label =
|
|
689
|
+
token['name']?.toString().trim().isNotEmpty == true
|
|
646
690
|
? token['name'].toString()
|
|
647
691
|
: tokenId;
|
|
648
|
-
final online =
|
|
692
|
+
final online =
|
|
693
|
+
token['online'] == true || token['connected'] == true;
|
|
649
694
|
return DropdownMenuItem<String>(
|
|
650
695
|
value: tokenId,
|
|
651
696
|
child: Row(
|
|
@@ -669,7 +714,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
669
714
|
}).toList(),
|
|
670
715
|
onChanged: (value) {
|
|
671
716
|
if (value != null) {
|
|
672
|
-
setState(()
|
|
717
|
+
setState(() {
|
|
718
|
+
_browserExtensionTokenId = value;
|
|
719
|
+
_hasUnsavedChanges = true;
|
|
720
|
+
});
|
|
673
721
|
}
|
|
674
722
|
},
|
|
675
723
|
),
|
|
@@ -683,7 +731,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
683
731
|
? 'Connected — tap Test to verify the live link.'
|
|
684
732
|
: 'Not connected — download the extension, load it in Chrome, then pair after login.',
|
|
685
733
|
onTest: () async {
|
|
686
|
-
setState(() {
|
|
734
|
+
setState(() {
|
|
735
|
+
_extensionTestRunning = true;
|
|
736
|
+
_extensionTestResult = null;
|
|
737
|
+
});
|
|
687
738
|
try {
|
|
688
739
|
final r = await controller.testBrowserExtension();
|
|
689
740
|
if (mounted) {
|
|
@@ -691,7 +742,12 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
691
742
|
}
|
|
692
743
|
} catch (e) {
|
|
693
744
|
if (mounted) {
|
|
694
|
-
setState(
|
|
745
|
+
setState(
|
|
746
|
+
() => _extensionTestResult = <String, dynamic>{
|
|
747
|
+
'passed': false,
|
|
748
|
+
'detail': e.toString(),
|
|
749
|
+
},
|
|
750
|
+
);
|
|
695
751
|
}
|
|
696
752
|
} finally {
|
|
697
753
|
if (mounted) {
|
|
@@ -718,7 +774,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
718
774
|
],
|
|
719
775
|
),
|
|
720
776
|
] else ...<Widget>[
|
|
721
|
-
Text(
|
|
777
|
+
Text(
|
|
778
|
+
'Cloud browser runtime is active.',
|
|
779
|
+
style: TextStyle(color: _textSecondary, height: 1.4),
|
|
780
|
+
),
|
|
722
781
|
],
|
|
723
782
|
const Divider(height: 32),
|
|
724
783
|
Text(
|
|
@@ -746,25 +805,32 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
746
805
|
],
|
|
747
806
|
onChanged: (value) {
|
|
748
807
|
if (value != null) {
|
|
749
|
-
setState(()
|
|
808
|
+
setState(() {
|
|
809
|
+
_cliBackend = value;
|
|
810
|
+
_hasUnsavedChanges = true;
|
|
811
|
+
});
|
|
750
812
|
}
|
|
751
813
|
},
|
|
752
814
|
),
|
|
753
|
-
if (_cliBackend == 'desktop' &&
|
|
815
|
+
if (_cliBackend == 'desktop' &&
|
|
816
|
+
controller.desktopDevices.length > 1) ...<Widget>[
|
|
754
817
|
const SizedBox(height: 12),
|
|
755
818
|
DropdownButtonFormField<String>(
|
|
756
|
-
initialValue:
|
|
757
|
-
|
|
758
|
-
|
|
819
|
+
initialValue:
|
|
820
|
+
controller.desktopDevices.any(
|
|
821
|
+
(d) => d['deviceId']?.toString() == _cliDesktopDeviceId,
|
|
822
|
+
)
|
|
759
823
|
? _cliDesktopDeviceId
|
|
760
824
|
: null,
|
|
761
825
|
decoration: const InputDecoration(
|
|
762
826
|
labelText: 'Desktop device',
|
|
763
|
-
helperText:
|
|
827
|
+
helperText:
|
|
828
|
+
'Choose which desktop companion runs CLI commands.',
|
|
764
829
|
),
|
|
765
830
|
items: controller.desktopDevices.map((device) {
|
|
766
831
|
final deviceId = device['deviceId']?.toString() ?? '';
|
|
767
|
-
final label =
|
|
832
|
+
final label =
|
|
833
|
+
device['hostname']?.toString().isNotEmpty == true
|
|
768
834
|
? device['hostname']!.toString()
|
|
769
835
|
: deviceId;
|
|
770
836
|
final online = device['online'] == true;
|
|
@@ -785,7 +851,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
785
851
|
}).toList(),
|
|
786
852
|
onChanged: (value) {
|
|
787
853
|
if (value != null) {
|
|
788
|
-
setState(()
|
|
854
|
+
setState(() {
|
|
855
|
+
_cliDesktopDeviceId = value;
|
|
856
|
+
_hasUnsavedChanges = true;
|
|
857
|
+
});
|
|
789
858
|
}
|
|
790
859
|
},
|
|
791
860
|
),
|
|
@@ -797,11 +866,14 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
797
866
|
result: _cliTestResult,
|
|
798
867
|
note: _cliBackend == 'desktop'
|
|
799
868
|
? (controller.desktopCompanionConnected
|
|
800
|
-
|
|
801
|
-
|
|
869
|
+
? 'Desktop app connected — commands route locally through the companion.'
|
|
870
|
+
: 'Desktop app selected but not connected. Commands fall back to cloud VM until the companion is online.')
|
|
802
871
|
: 'Cloud VM — commands run in an isolated container.',
|
|
803
872
|
onTest: () async {
|
|
804
|
-
setState(() {
|
|
873
|
+
setState(() {
|
|
874
|
+
_cliTestRunning = true;
|
|
875
|
+
_cliTestResult = null;
|
|
876
|
+
});
|
|
805
877
|
try {
|
|
806
878
|
final r = await controller.testCliRuntime();
|
|
807
879
|
if (mounted) {
|
|
@@ -809,7 +881,12 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
809
881
|
}
|
|
810
882
|
} catch (e) {
|
|
811
883
|
if (mounted) {
|
|
812
|
-
setState(
|
|
884
|
+
setState(
|
|
885
|
+
() => _cliTestResult = <String, dynamic>{
|
|
886
|
+
'passed': false,
|
|
887
|
+
'detail': e.toString(),
|
|
888
|
+
},
|
|
889
|
+
);
|
|
813
890
|
}
|
|
814
891
|
} finally {
|
|
815
892
|
if (mounted) {
|
|
@@ -832,7 +909,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
832
909
|
subtitle:
|
|
833
910
|
'Automatically choose the best enabled model for each task type.',
|
|
834
911
|
value: _smarterSelector,
|
|
835
|
-
onChanged: (value) => setState(()
|
|
912
|
+
onChanged: (value) => setState(() {
|
|
913
|
+
_smarterSelector = value;
|
|
914
|
+
_hasUnsavedChanges = true;
|
|
915
|
+
}),
|
|
836
916
|
),
|
|
837
917
|
],
|
|
838
918
|
),
|
|
@@ -893,7 +973,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
893
973
|
options: modelChoices,
|
|
894
974
|
onChanged: (value) {
|
|
895
975
|
if (value != null) {
|
|
896
|
-
setState(()
|
|
976
|
+
setState(() {
|
|
977
|
+
_defaultChatModel = value;
|
|
978
|
+
_hasUnsavedChanges = true;
|
|
979
|
+
});
|
|
897
980
|
}
|
|
898
981
|
},
|
|
899
982
|
),
|
|
@@ -911,7 +994,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
911
994
|
options: modelChoices,
|
|
912
995
|
onChanged: (value) {
|
|
913
996
|
if (value != null) {
|
|
914
|
-
setState(()
|
|
997
|
+
setState(() {
|
|
998
|
+
_defaultSubagentModel = value;
|
|
999
|
+
_hasUnsavedChanges = true;
|
|
1000
|
+
});
|
|
915
1001
|
}
|
|
916
1002
|
},
|
|
917
1003
|
),
|
|
@@ -929,7 +1015,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
929
1015
|
options: _modelPickerOptions(routingModels),
|
|
930
1016
|
onChanged: (value) {
|
|
931
1017
|
if (value != null) {
|
|
932
|
-
setState(()
|
|
1018
|
+
setState(() {
|
|
1019
|
+
_fallbackModel = value;
|
|
1020
|
+
_hasUnsavedChanges = true;
|
|
1021
|
+
});
|
|
933
1022
|
}
|
|
934
1023
|
},
|
|
935
1024
|
),
|
|
@@ -963,12 +1052,21 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
963
1052
|
barrierColor: Colors.black.withValues(alpha: 0.55),
|
|
964
1053
|
transitionDuration: const Duration(milliseconds: 220),
|
|
965
1054
|
transitionBuilder: (ctx, anim, _, child) => FadeTransition(
|
|
966
|
-
opacity: CurvedAnimation(
|
|
1055
|
+
opacity: CurvedAnimation(
|
|
1056
|
+
parent: anim,
|
|
1057
|
+
curve: Curves.easeOut,
|
|
1058
|
+
),
|
|
967
1059
|
child: SlideTransition(
|
|
968
|
-
position:
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1060
|
+
position:
|
|
1061
|
+
Tween<Offset>(
|
|
1062
|
+
begin: const Offset(0, 0.04),
|
|
1063
|
+
end: Offset.zero,
|
|
1064
|
+
).animate(
|
|
1065
|
+
CurvedAnimation(
|
|
1066
|
+
parent: anim,
|
|
1067
|
+
curve: Curves.easeOutCubic,
|
|
1068
|
+
),
|
|
1069
|
+
),
|
|
972
1070
|
child: child,
|
|
973
1071
|
),
|
|
974
1072
|
),
|
|
@@ -977,7 +1075,11 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
977
1075
|
selectedIds: _enabledModels,
|
|
978
1076
|
),
|
|
979
1077
|
);
|
|
980
|
-
if (result != null)
|
|
1078
|
+
if (result != null)
|
|
1079
|
+
setState(() {
|
|
1080
|
+
_enabledModels = result;
|
|
1081
|
+
_hasUnsavedChanges = true;
|
|
1082
|
+
});
|
|
981
1083
|
},
|
|
982
1084
|
),
|
|
983
1085
|
],
|
|
@@ -1037,9 +1139,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1037
1139
|
options: modelChoices,
|
|
1038
1140
|
onChanged: (value) {
|
|
1039
1141
|
if (value != null) {
|
|
1040
|
-
setState(
|
|
1041
|
-
|
|
1042
|
-
|
|
1142
|
+
setState(() {
|
|
1143
|
+
_defaultRecordingSummaryModel = value;
|
|
1144
|
+
_hasUnsavedChanges = true;
|
|
1145
|
+
});
|
|
1043
1146
|
}
|
|
1044
1147
|
},
|
|
1045
1148
|
),
|
|
@@ -1057,6 +1160,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1057
1160
|
if (value != null) {
|
|
1058
1161
|
setState(() {
|
|
1059
1162
|
_defaultRecordingTranscriptionModel = value;
|
|
1163
|
+
_hasUnsavedChanges = true;
|
|
1060
1164
|
});
|
|
1061
1165
|
}
|
|
1062
1166
|
},
|
|
@@ -1098,7 +1202,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1098
1202
|
options: modelChoices,
|
|
1099
1203
|
onChanged: (value) {
|
|
1100
1204
|
if (value != null) {
|
|
1101
|
-
setState(()
|
|
1205
|
+
setState(() {
|
|
1206
|
+
_defaultSpeechModel = value;
|
|
1207
|
+
_hasUnsavedChanges = true;
|
|
1208
|
+
});
|
|
1102
1209
|
}
|
|
1103
1210
|
},
|
|
1104
1211
|
),
|
|
@@ -1137,9 +1244,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1137
1244
|
label: 'Live Provider',
|
|
1138
1245
|
icon: Icons.call_outlined,
|
|
1139
1246
|
value: _voiceLiveProvider,
|
|
1140
|
-
options: _simplePickerOptions(
|
|
1141
|
-
|
|
1142
|
-
|
|
1247
|
+
options: _simplePickerOptions(const <String>[
|
|
1248
|
+
'openai',
|
|
1249
|
+
'gemini',
|
|
1250
|
+
]),
|
|
1143
1251
|
onChanged: (value) {
|
|
1144
1252
|
if (value == null) return;
|
|
1145
1253
|
setState(() {
|
|
@@ -1158,6 +1266,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1158
1266
|
!voiceOptions.contains(_voiceLiveVoice)) {
|
|
1159
1267
|
_voiceLiveVoice = voiceOptions.first;
|
|
1160
1268
|
}
|
|
1269
|
+
_hasUnsavedChanges = true;
|
|
1161
1270
|
});
|
|
1162
1271
|
},
|
|
1163
1272
|
),
|
|
@@ -1174,7 +1283,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1174
1283
|
),
|
|
1175
1284
|
onChanged: (value) {
|
|
1176
1285
|
if (value != null) {
|
|
1177
|
-
setState(()
|
|
1286
|
+
setState(() {
|
|
1287
|
+
_voiceLiveModel = value;
|
|
1288
|
+
_hasUnsavedChanges = true;
|
|
1289
|
+
});
|
|
1178
1290
|
}
|
|
1179
1291
|
},
|
|
1180
1292
|
),
|
|
@@ -1189,7 +1301,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1189
1301
|
options: _simplePickerOptions(liveVoiceOptions),
|
|
1190
1302
|
onChanged: (value) {
|
|
1191
1303
|
if (value != null) {
|
|
1192
|
-
setState(()
|
|
1304
|
+
setState(() {
|
|
1305
|
+
_voiceLiveVoice = value;
|
|
1306
|
+
_hasUnsavedChanges = true;
|
|
1307
|
+
});
|
|
1193
1308
|
}
|
|
1194
1309
|
},
|
|
1195
1310
|
),
|
|
@@ -1307,6 +1422,18 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1307
1422
|
? controller.setDesktopCompanionPaused
|
|
1308
1423
|
: null,
|
|
1309
1424
|
),
|
|
1425
|
+
SwitchListTile.adaptive(
|
|
1426
|
+
value: controller.desktopPassiveHistoryEnabled,
|
|
1427
|
+
contentPadding: EdgeInsets.zero,
|
|
1428
|
+
title: Text('Passive screen history on this computer'),
|
|
1429
|
+
subtitle: Text(
|
|
1430
|
+
'Capture OCR text plus app/window context locally for the user-wide Timeline. Screenshots stay transient and are not uploaded.',
|
|
1431
|
+
style: TextStyle(color: _textSecondary),
|
|
1432
|
+
),
|
|
1433
|
+
onChanged: controller.desktopCompanionEnabled
|
|
1434
|
+
? controller.setDesktopPassiveHistoryEnabled
|
|
1435
|
+
: null,
|
|
1436
|
+
),
|
|
1310
1437
|
const SizedBox(height: 12),
|
|
1311
1438
|
TextFormField(
|
|
1312
1439
|
initialValue: controller.desktopCompanionLabel,
|
|
@@ -1341,8 +1468,49 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1341
1468
|
? _warning
|
|
1342
1469
|
: _success,
|
|
1343
1470
|
),
|
|
1471
|
+
_DotStatus(
|
|
1472
|
+
label: controller.desktopPassiveHistoryEnabled
|
|
1473
|
+
? 'History enabled'
|
|
1474
|
+
: 'History disabled',
|
|
1475
|
+
color: controller.desktopPassiveHistoryEnabled
|
|
1476
|
+
? _accent
|
|
1477
|
+
: _textMuted,
|
|
1478
|
+
),
|
|
1344
1479
|
],
|
|
1345
1480
|
),
|
|
1481
|
+
if (controller.desktopPassiveHistoryLastUploadedAt != null ||
|
|
1482
|
+
controller.desktopPassiveHistoryLastError != null) ...<Widget>[
|
|
1483
|
+
const SizedBox(height: 12),
|
|
1484
|
+
Container(
|
|
1485
|
+
width: double.infinity,
|
|
1486
|
+
padding: const EdgeInsets.all(12),
|
|
1487
|
+
decoration: BoxDecoration(
|
|
1488
|
+
color: _bgSecondary.withValues(alpha: 0.66),
|
|
1489
|
+
borderRadius: BorderRadius.circular(16),
|
|
1490
|
+
border: Border.all(color: _borderLight),
|
|
1491
|
+
),
|
|
1492
|
+
child: Column(
|
|
1493
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1494
|
+
children: <Widget>[
|
|
1495
|
+
if (controller.desktopPassiveHistoryLastUploadedAt
|
|
1496
|
+
case final uploadedAt?)
|
|
1497
|
+
Text(
|
|
1498
|
+
'Last upload: ${_formatTimestamp(DateTime.parse(uploadedAt))}',
|
|
1499
|
+
style: TextStyle(color: _textSecondary),
|
|
1500
|
+
),
|
|
1501
|
+
if (controller.desktopPassiveHistoryLastError
|
|
1502
|
+
case final passiveError?)
|
|
1503
|
+
Padding(
|
|
1504
|
+
padding: const EdgeInsets.only(top: 6),
|
|
1505
|
+
child: Text(
|
|
1506
|
+
'Last error: $passiveError',
|
|
1507
|
+
style: TextStyle(color: _danger),
|
|
1508
|
+
),
|
|
1509
|
+
),
|
|
1510
|
+
],
|
|
1511
|
+
),
|
|
1512
|
+
),
|
|
1513
|
+
],
|
|
1346
1514
|
if (controller.desktopCompanionErrorMessage
|
|
1347
1515
|
case final message?) ...<Widget>[
|
|
1348
1516
|
const SizedBox(height: 12),
|
|
@@ -1362,7 +1530,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1362
1530
|
? 'Connected — tap Test to fetch live device status from the server.'
|
|
1363
1531
|
: 'Not connected. Make sure the desktop app is running on the target machine.',
|
|
1364
1532
|
onTest: () async {
|
|
1365
|
-
setState(() {
|
|
1533
|
+
setState(() {
|
|
1534
|
+
_desktopTestRunning = true;
|
|
1535
|
+
_desktopTestResult = null;
|
|
1536
|
+
});
|
|
1366
1537
|
try {
|
|
1367
1538
|
final r = await controller.testDesktopCompanion();
|
|
1368
1539
|
final active = r['activeDevice'];
|
|
@@ -1373,20 +1544,29 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1373
1544
|
final plat = active['platform']?.toString() ?? '';
|
|
1374
1545
|
final sc = active['permissions']?['screenCapture'] == true;
|
|
1375
1546
|
final ic = active['permissions']?['inputControl'] == true;
|
|
1376
|
-
detail =
|
|
1547
|
+
detail =
|
|
1548
|
+
'$label${plat.isNotEmpty ? " ($plat)" : ""}'
|
|
1377
1549
|
' — screen: ${sc ? "✓" : "✗"}, input: ${ic ? "✓" : "✗"}';
|
|
1378
1550
|
} else if (multi) {
|
|
1379
|
-
detail =
|
|
1551
|
+
detail =
|
|
1552
|
+
'${r['onlineCount']} devices online — select one in Desktop › Devices';
|
|
1380
1553
|
}
|
|
1381
1554
|
if (mounted) {
|
|
1382
|
-
setState(
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1555
|
+
setState(
|
|
1556
|
+
() => _desktopTestResult = <String, dynamic>{
|
|
1557
|
+
...r,
|
|
1558
|
+
'detail': detail,
|
|
1559
|
+
},
|
|
1560
|
+
);
|
|
1386
1561
|
}
|
|
1387
1562
|
} catch (e) {
|
|
1388
1563
|
if (mounted) {
|
|
1389
|
-
setState(
|
|
1564
|
+
setState(
|
|
1565
|
+
() => _desktopTestResult = <String, dynamic>{
|
|
1566
|
+
'passed': false,
|
|
1567
|
+
'detail': e.toString(),
|
|
1568
|
+
},
|
|
1569
|
+
);
|
|
1390
1570
|
}
|
|
1391
1571
|
} finally {
|
|
1392
1572
|
if (mounted) {
|
|
@@ -1511,7 +1691,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1511
1691
|
);
|
|
1512
1692
|
}
|
|
1513
1693
|
|
|
1514
|
-
Widget _buildSecuritySection(
|
|
1694
|
+
Widget _buildSecuritySection(
|
|
1695
|
+
BuildContext context,
|
|
1696
|
+
NeoAgentController controller,
|
|
1697
|
+
) {
|
|
1515
1698
|
return Card(
|
|
1516
1699
|
child: Padding(
|
|
1517
1700
|
padding: const EdgeInsets.all(20),
|
|
@@ -1661,6 +1844,32 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1661
1844
|
}
|
|
1662
1845
|
}
|
|
1663
1846
|
|
|
1847
|
+
Future<_LeaveAction?> _showLeaveDialog(BuildContext context) {
|
|
1848
|
+
return showDialog<_LeaveAction>(
|
|
1849
|
+
context: context,
|
|
1850
|
+
builder: (ctx) => AlertDialog(
|
|
1851
|
+
title: const Text('Unsaved changes'),
|
|
1852
|
+
content: const Text(
|
|
1853
|
+
'You have unsaved settings. What would you like to do?',
|
|
1854
|
+
),
|
|
1855
|
+
actions: <Widget>[
|
|
1856
|
+
TextButton(
|
|
1857
|
+
onPressed: () => Navigator.pop(ctx, _LeaveAction.cancel),
|
|
1858
|
+
child: const Text('Cancel'),
|
|
1859
|
+
),
|
|
1860
|
+
TextButton(
|
|
1861
|
+
onPressed: () => Navigator.pop(ctx, _LeaveAction.discard),
|
|
1862
|
+
child: const Text('Discard'),
|
|
1863
|
+
),
|
|
1864
|
+
FilledButton(
|
|
1865
|
+
onPressed: () => Navigator.pop(ctx, _LeaveAction.save),
|
|
1866
|
+
child: const Text('Save'),
|
|
1867
|
+
),
|
|
1868
|
+
],
|
|
1869
|
+
),
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1664
1873
|
List<_ModelPickerOption> _recordingTranscriptionOptions(String current) {
|
|
1665
1874
|
const List<String> defaults = <String>['nova-3', 'nova-2-general'];
|
|
1666
1875
|
final String normalizedCurrent = current.trim();
|
|
@@ -1690,14 +1899,20 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1690
1899
|
Row(
|
|
1691
1900
|
children: <Widget>[
|
|
1692
1901
|
Icon(
|
|
1693
|
-
passed
|
|
1902
|
+
passed
|
|
1903
|
+
? Icons.check_circle_rounded
|
|
1904
|
+
: Icons.cancel_rounded,
|
|
1694
1905
|
size: 15,
|
|
1695
|
-
color: passed
|
|
1906
|
+
color: passed
|
|
1907
|
+
? const Color(0xFF22C55E)
|
|
1908
|
+
: const Color(0xFFEF4444),
|
|
1696
1909
|
),
|
|
1697
1910
|
const SizedBox(width: 6),
|
|
1698
1911
|
Expanded(
|
|
1699
1912
|
child: Text(
|
|
1700
|
-
passed
|
|
1913
|
+
passed
|
|
1914
|
+
? (detail.isNotEmpty ? detail : '$label: OK')
|
|
1915
|
+
: detail,
|
|
1701
1916
|
style: TextStyle(
|
|
1702
1917
|
fontSize: 13,
|
|
1703
1918
|
color: passed ? null : const Color(0xFFEF4444),
|
|
@@ -1707,7 +1922,14 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1707
1922
|
],
|
|
1708
1923
|
)
|
|
1709
1924
|
else if (note != null)
|
|
1710
|
-
Text(
|
|
1925
|
+
Text(
|
|
1926
|
+
note,
|
|
1927
|
+
style: TextStyle(
|
|
1928
|
+
fontSize: 13,
|
|
1929
|
+
color: _textSecondary,
|
|
1930
|
+
height: 1.4,
|
|
1931
|
+
),
|
|
1932
|
+
),
|
|
1711
1933
|
],
|
|
1712
1934
|
),
|
|
1713
1935
|
),
|
|
@@ -1844,7 +2066,9 @@ class _SmartPoolSummary extends StatelessWidget {
|
|
|
1844
2066
|
style: TextStyle(fontSize: 12, color: _textMuted),
|
|
1845
2067
|
)
|
|
1846
2068
|
else
|
|
1847
|
-
...providers
|
|
2069
|
+
...providers
|
|
2070
|
+
.take(12)
|
|
2071
|
+
.map(
|
|
1848
2072
|
(p) => Container(
|
|
1849
2073
|
width: 8,
|
|
1850
2074
|
height: 8,
|
|
@@ -1866,8 +2090,7 @@ class _SmartPoolSummary extends StatelessWidget {
|
|
|
1866
2090
|
icon: const Icon(Icons.tune_rounded, size: 14),
|
|
1867
2091
|
label: const Text('Manage'),
|
|
1868
2092
|
style: OutlinedButton.styleFrom(
|
|
1869
|
-
padding:
|
|
1870
|
-
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
2093
|
+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
1871
2094
|
textStyle: const TextStyle(fontSize: 13),
|
|
1872
2095
|
),
|
|
1873
2096
|
),
|
|
@@ -1882,10 +2105,7 @@ class _SmartPoolSummary extends StatelessWidget {
|
|
|
1882
2105
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1883
2106
|
|
|
1884
2107
|
class _SmartPoolDialog extends StatefulWidget {
|
|
1885
|
-
const _SmartPoolDialog({
|
|
1886
|
-
required this.models,
|
|
1887
|
-
required this.selectedIds,
|
|
1888
|
-
});
|
|
2108
|
+
const _SmartPoolDialog({required this.models, required this.selectedIds});
|
|
1889
2109
|
|
|
1890
2110
|
final List<ModelMeta> models;
|
|
1891
2111
|
final Set<String> selectedIds;
|
|
@@ -1919,10 +2139,12 @@ class _SmartPoolDialogState extends State<_SmartPoolDialog> {
|
|
|
1919
2139
|
if (_query.isNotEmpty) {
|
|
1920
2140
|
final q = _query.toLowerCase();
|
|
1921
2141
|
list = list
|
|
1922
|
-
.where(
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2142
|
+
.where(
|
|
2143
|
+
(m) =>
|
|
2144
|
+
m.label.toLowerCase().contains(q) ||
|
|
2145
|
+
m.id.toLowerCase().contains(q) ||
|
|
2146
|
+
m.provider.toLowerCase().contains(q),
|
|
2147
|
+
)
|
|
1926
2148
|
.toList();
|
|
1927
2149
|
}
|
|
1928
2150
|
return list;
|
|
@@ -1940,9 +2162,7 @@ class _SmartPoolDialogState extends State<_SmartPoolDialog> {
|
|
|
1940
2162
|
setState(() {
|
|
1941
2163
|
final toRemove = filtered.map((m) => m.id).toSet();
|
|
1942
2164
|
final remaining = _selected.difference(toRemove);
|
|
1943
|
-
_selected = remaining.isNotEmpty
|
|
1944
|
-
? remaining
|
|
1945
|
-
: <String>{_selected.first};
|
|
2165
|
+
_selected = remaining.isNotEmpty ? remaining : <String>{_selected.first};
|
|
1946
2166
|
});
|
|
1947
2167
|
}
|
|
1948
2168
|
|
|
@@ -1967,79 +2187,82 @@ class _SmartPoolDialogState extends State<_SmartPoolDialog> {
|
|
|
1967
2187
|
final models = grouped[provider]!;
|
|
1968
2188
|
final providerColor = _providerPickerColor(provider);
|
|
1969
2189
|
final available = models.where((m) => m.available).toList();
|
|
1970
|
-
final allGroupSelected =
|
|
2190
|
+
final allGroupSelected =
|
|
2191
|
+
available.isNotEmpty &&
|
|
1971
2192
|
available.every((m) => _selected.contains(m.id));
|
|
1972
2193
|
|
|
1973
|
-
rows.add(
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
2194
|
+
rows.add(
|
|
2195
|
+
Padding(
|
|
2196
|
+
padding: const EdgeInsets.fromLTRB(16, 14, 16, 4),
|
|
2197
|
+
child: Row(
|
|
2198
|
+
children: <Widget>[
|
|
2199
|
+
Container(
|
|
2200
|
+
width: 6,
|
|
2201
|
+
height: 6,
|
|
2202
|
+
decoration: BoxDecoration(
|
|
2203
|
+
color: providerColor,
|
|
2204
|
+
shape: BoxShape.circle,
|
|
2205
|
+
),
|
|
1983
2206
|
),
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
2207
|
+
const SizedBox(width: 8),
|
|
2208
|
+
Expanded(
|
|
2209
|
+
child: Text(
|
|
2210
|
+
_providerPickerLabel(provider).toUpperCase(),
|
|
2211
|
+
style: TextStyle(
|
|
2212
|
+
fontSize: 10.5,
|
|
2213
|
+
fontWeight: FontWeight.w700,
|
|
2214
|
+
color: _textMuted,
|
|
2215
|
+
letterSpacing: 0.8,
|
|
2216
|
+
),
|
|
1994
2217
|
),
|
|
1995
2218
|
),
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
for (final m in available) {
|
|
2013
|
-
_selected.add(m.id);
|
|
2219
|
+
const SizedBox(width: 8),
|
|
2220
|
+
GestureDetector(
|
|
2221
|
+
onTap: available.isEmpty
|
|
2222
|
+
? null
|
|
2223
|
+
: () {
|
|
2224
|
+
setState(() {
|
|
2225
|
+
if (allGroupSelected) {
|
|
2226
|
+
final toRemove = available.map((m) => m.id).toSet();
|
|
2227
|
+
final remaining = _selected.difference(toRemove);
|
|
2228
|
+
_selected = remaining.isNotEmpty
|
|
2229
|
+
? remaining
|
|
2230
|
+
: <String>{_selected.first};
|
|
2231
|
+
} else {
|
|
2232
|
+
for (final m in available) {
|
|
2233
|
+
_selected.add(m.id);
|
|
2234
|
+
}
|
|
2014
2235
|
}
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2236
|
+
});
|
|
2237
|
+
},
|
|
2238
|
+
child: Text(
|
|
2239
|
+
allGroupSelected ? 'None' : 'All',
|
|
2240
|
+
style: TextStyle(
|
|
2241
|
+
fontSize: 11,
|
|
2242
|
+
fontWeight: FontWeight.w600,
|
|
2243
|
+
color: available.isEmpty ? _textMuted : _accent,
|
|
2244
|
+
),
|
|
2024
2245
|
),
|
|
2025
2246
|
),
|
|
2026
|
-
|
|
2027
|
-
|
|
2247
|
+
],
|
|
2248
|
+
),
|
|
2028
2249
|
),
|
|
2029
|
-
)
|
|
2250
|
+
);
|
|
2030
2251
|
|
|
2031
2252
|
for (final model in models) {
|
|
2032
|
-
rows.add(
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
_selected.
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2253
|
+
rows.add(
|
|
2254
|
+
_SmartPoolRow(
|
|
2255
|
+
model: model,
|
|
2256
|
+
selected: _selected.contains(model.id),
|
|
2257
|
+
onToggle: (val) => setState(() {
|
|
2258
|
+
if (val) {
|
|
2259
|
+
_selected.add(model.id);
|
|
2260
|
+
} else if (_selected.length > 1) {
|
|
2261
|
+
_selected.remove(model.id);
|
|
2262
|
+
}
|
|
2263
|
+
}),
|
|
2264
|
+
),
|
|
2265
|
+
);
|
|
2043
2266
|
}
|
|
2044
2267
|
}
|
|
2045
2268
|
|
|
@@ -2093,8 +2316,7 @@ class _SmartPoolDialogState extends State<_SmartPoolDialog> {
|
|
|
2093
2316
|
style: IconButton.styleFrom(
|
|
2094
2317
|
minimumSize: const Size(36, 36),
|
|
2095
2318
|
padding: EdgeInsets.zero,
|
|
2096
|
-
tapTargetSize:
|
|
2097
|
-
MaterialTapTargetSize.shrinkWrap,
|
|
2319
|
+
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
2098
2320
|
),
|
|
2099
2321
|
),
|
|
2100
2322
|
],
|
|
@@ -2144,18 +2366,17 @@ class _SmartPoolDialogState extends State<_SmartPoolDialog> {
|
|
|
2144
2366
|
: null,
|
|
2145
2367
|
isDense: true,
|
|
2146
2368
|
contentPadding: const EdgeInsets.symmetric(
|
|
2147
|
-
|
|
2369
|
+
vertical: 10,
|
|
2370
|
+
),
|
|
2148
2371
|
filled: true,
|
|
2149
2372
|
fillColor: _bgSecondary,
|
|
2150
2373
|
border: OutlineInputBorder(
|
|
2151
2374
|
borderRadius: BorderRadius.circular(12),
|
|
2152
|
-
borderSide:
|
|
2153
|
-
BorderSide(color: _border),
|
|
2375
|
+
borderSide: BorderSide(color: _border),
|
|
2154
2376
|
),
|
|
2155
2377
|
enabledBorder: OutlineInputBorder(
|
|
2156
2378
|
borderRadius: BorderRadius.circular(12),
|
|
2157
|
-
borderSide:
|
|
2158
|
-
BorderSide(color: _border),
|
|
2379
|
+
borderSide: BorderSide(color: _border),
|
|
2159
2380
|
),
|
|
2160
2381
|
focusedBorder: OutlineInputBorder(
|
|
2161
2382
|
borderRadius: BorderRadius.circular(12),
|
|
@@ -2170,7 +2391,8 @@ class _SmartPoolDialogState extends State<_SmartPoolDialog> {
|
|
|
2170
2391
|
const SizedBox(width: 8),
|
|
2171
2392
|
GestureDetector(
|
|
2172
2393
|
onTap: () => setState(
|
|
2173
|
-
|
|
2394
|
+
() => _onlyAvailable = !_onlyAvailable,
|
|
2395
|
+
),
|
|
2174
2396
|
child: AnimatedContainer(
|
|
2175
2397
|
duration: const Duration(milliseconds: 150),
|
|
2176
2398
|
padding: const EdgeInsets.symmetric(
|
|
@@ -2220,10 +2442,7 @@ class _SmartPoolDialogState extends State<_SmartPoolDialog> {
|
|
|
2220
2442
|
const Spacer(),
|
|
2221
2443
|
Text(
|
|
2222
2444
|
'$selectedAvailableCount selected',
|
|
2223
|
-
style: TextStyle(
|
|
2224
|
-
fontSize: 12,
|
|
2225
|
-
color: _textMuted,
|
|
2226
|
-
),
|
|
2445
|
+
style: TextStyle(fontSize: 12, color: _textMuted),
|
|
2227
2446
|
),
|
|
2228
2447
|
],
|
|
2229
2448
|
),
|
|
@@ -2254,8 +2473,7 @@ class _SmartPoolDialogState extends State<_SmartPoolDialog> {
|
|
|
2254
2473
|
),
|
|
2255
2474
|
)
|
|
2256
2475
|
: ListView(
|
|
2257
|
-
padding:
|
|
2258
|
-
const EdgeInsets.only(top: 4, bottom: 8),
|
|
2476
|
+
padding: const EdgeInsets.only(top: 4, bottom: 8),
|
|
2259
2477
|
shrinkWrap: true,
|
|
2260
2478
|
children: rows,
|
|
2261
2479
|
),
|
|
@@ -2298,8 +2516,7 @@ class _SmartPoolRow extends StatelessWidget {
|
|
|
2298
2516
|
child: InkWell(
|
|
2299
2517
|
onTap: model.available ? () => onToggle(!selected) : null,
|
|
2300
2518
|
child: Padding(
|
|
2301
|
-
padding:
|
|
2302
|
-
const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
|
2519
|
+
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
|
2303
2520
|
child: Row(
|
|
2304
2521
|
children: <Widget>[
|
|
2305
2522
|
// Thin provider accent bar on the left
|
|
@@ -2307,8 +2524,7 @@ class _SmartPoolRow extends StatelessWidget {
|
|
|
2307
2524
|
width: 3,
|
|
2308
2525
|
height: 30,
|
|
2309
2526
|
decoration: BoxDecoration(
|
|
2310
|
-
color: color.withValues(
|
|
2311
|
-
alpha: selected ? 0.85 : 0.28),
|
|
2527
|
+
color: color.withValues(alpha: selected ? 0.85 : 0.28),
|
|
2312
2528
|
borderRadius: BorderRadius.circular(2),
|
|
2313
2529
|
),
|
|
2314
2530
|
),
|
|
@@ -2323,8 +2539,7 @@ class _SmartPoolRow extends StatelessWidget {
|
|
|
2323
2539
|
: null,
|
|
2324
2540
|
activeColor: _accent,
|
|
2325
2541
|
side: BorderSide(color: _textMuted, width: 1.5),
|
|
2326
|
-
materialTapTargetSize:
|
|
2327
|
-
MaterialTapTargetSize.shrinkWrap,
|
|
2542
|
+
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
2328
2543
|
visualDensity: VisualDensity.compact,
|
|
2329
2544
|
),
|
|
2330
2545
|
),
|
|
@@ -2338,8 +2553,7 @@ class _SmartPoolRow extends StatelessWidget {
|
|
|
2338
2553
|
style: TextStyle(
|
|
2339
2554
|
fontSize: 13,
|
|
2340
2555
|
fontWeight: FontWeight.w500,
|
|
2341
|
-
color:
|
|
2342
|
-
selected ? _accentHover : _textPrimary,
|
|
2556
|
+
color: selected ? _accentHover : _textPrimary,
|
|
2343
2557
|
),
|
|
2344
2558
|
maxLines: 1,
|
|
2345
2559
|
overflow: TextOverflow.ellipsis,
|
|
@@ -2347,10 +2561,7 @@ class _SmartPoolRow extends StatelessWidget {
|
|
|
2347
2561
|
if (model.purpose.isNotEmpty)
|
|
2348
2562
|
Text(
|
|
2349
2563
|
model.purpose,
|
|
2350
|
-
style: TextStyle(
|
|
2351
|
-
fontSize: 11,
|
|
2352
|
-
color: _textMuted,
|
|
2353
|
-
),
|
|
2564
|
+
style: TextStyle(fontSize: 11, color: _textMuted),
|
|
2354
2565
|
),
|
|
2355
2566
|
],
|
|
2356
2567
|
),
|
|
@@ -2379,8 +2590,7 @@ class _PoolActionChip extends StatelessWidget {
|
|
|
2379
2590
|
return GestureDetector(
|
|
2380
2591
|
onTap: onTap,
|
|
2381
2592
|
child: Container(
|
|
2382
|
-
padding:
|
|
2383
|
-
const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
2593
|
+
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
2384
2594
|
decoration: BoxDecoration(
|
|
2385
2595
|
color: _bgSecondary,
|
|
2386
2596
|
borderRadius: BorderRadius.circular(8),
|