neoagent 3.1.0 → 3.1.1-beta.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.
@@ -505,8 +505,10 @@ class _SkillsPanelState extends State<SkillsPanel>
505
505
 
506
506
  // Installed tab search & filter state
507
507
  String _installedQuery = '';
508
- String _installedStatusFilter = 'all'; // 'all' | 'active' | 'draft' | 'disabled'
509
- String _installedSourceFilter = 'all'; // 'all' | 'built-in' | 'learned' | 'user' | 'store'
508
+ String _installedStatusFilter =
509
+ 'all'; // 'all' | 'active' | 'draft' | 'disabled'
510
+ String _installedSourceFilter =
511
+ 'all'; // 'all' | 'built-in' | 'learned' | 'user' | 'store'
510
512
  late final TextEditingController _installedSearchController;
511
513
 
512
514
  @override
@@ -645,18 +647,36 @@ class _SkillsPanelState extends State<SkillsPanel>
645
647
  final q = _installedQuery;
646
648
  if (q.isNotEmpty &&
647
649
  !skill.name.toLowerCase().contains(q) &&
648
- !skill.description.toLowerCase().contains(q)) return false;
650
+ !skill.description.toLowerCase().contains(q)) {
651
+ return false;
652
+ }
649
653
  if (_installedStatusFilter != 'all') {
650
- if (_installedStatusFilter == 'active' && (!skill.enabled || skill.draft)) return false;
651
- if (_installedStatusFilter == 'draft' && !skill.draft) return false;
652
- if (_installedStatusFilter == 'disabled' && skill.enabled) return false;
654
+ if (_installedStatusFilter == 'active' &&
655
+ (!skill.enabled || skill.draft)) {
656
+ return false;
657
+ }
658
+ if (_installedStatusFilter == 'draft' && !skill.draft) {
659
+ return false;
660
+ }
661
+ if (_installedStatusFilter == 'disabled' && skill.enabled) {
662
+ return false;
663
+ }
664
+ }
665
+ if (_installedSourceFilter != 'all' &&
666
+ skill.source != _installedSourceFilter) {
667
+ return false;
653
668
  }
654
- if (_installedSourceFilter != 'all' && skill.source != _installedSourceFilter) return false;
655
669
  return true;
656
670
  }).toList();
657
671
 
658
672
  final statusFilters = <String>['all', 'active', 'draft', 'disabled'];
659
- final sourceFilters = <String>['all', 'built-in', 'learned', 'user', 'store'];
673
+ final sourceFilters = <String>[
674
+ 'all',
675
+ 'built-in',
676
+ 'learned',
677
+ 'user',
678
+ 'store',
679
+ ];
660
680
 
661
681
  return Card(
662
682
  child: Column(
@@ -757,157 +777,174 @@ class _SkillsPanelState extends State<SkillsPanel>
757
777
  separatorBuilder: (_, __) => const SizedBox(height: 10),
758
778
  itemBuilder: (context, index) {
759
779
  final skill = filteredSkills[index];
760
- return LayoutBuilder(
761
- builder: (context, constraints) {
762
- final compact = constraints.maxWidth < 760;
763
- return Container(
764
- padding: const EdgeInsets.all(14),
765
- decoration: BoxDecoration(
766
- color: _bgSecondary,
767
- borderRadius: BorderRadius.circular(14),
768
- border: Border.all(color: _border),
769
- ),
770
- child: compact
771
- ? Column(
772
- crossAxisAlignment: CrossAxisAlignment.start,
773
- children: <Widget>[
774
- Row(
775
- children: <Widget>[
776
- Expanded(
777
- child: Text(
778
- skill.name,
779
- style: TextStyle(fontWeight: FontWeight.w700),
780
- ),
781
- ),
782
- Switch(
783
- value: skill.enabled,
784
- onChanged: (value) => controller
785
- .setSkillEnabled(skill.name, value),
786
- ),
787
- ],
788
- ),
789
- Text(
790
- skill.description.ifEmpty('No description'),
791
- style: TextStyle(color: _textSecondary),
792
- ),
793
- const SizedBox(height: 10),
794
- Wrap(
795
- spacing: 8,
796
- runSpacing: 8,
797
- children: <Widget>[
798
- _MetaPill(
799
- label: skill.category,
800
- icon: Icons.folder_outlined,
801
- ),
802
- _MetaPill(
803
- label: skill.source,
804
- icon: Icons.source_outlined,
805
- ),
806
- if (skill.draft)
807
- const _MetaPill(
808
- label: 'Draft',
809
- icon: Icons.edit_note_outlined,
810
- ),
811
- ],
812
- ),
813
- const SizedBox(height: 10),
814
- Row(
815
- children: <Widget>[
816
- const Spacer(),
817
- OutlinedButton(
818
- onPressed: () =>
819
- _openSkillEditor(context, skill.name),
820
- child: Text('Open'),
821
- ),
822
- const SizedBox(width: 8),
823
- TextButton.icon(
824
- onPressed: () =>
825
- _confirmDeleteSkill(context, skill.name),
826
- icon: Icon(Icons.delete_outline),
827
- style: TextButton.styleFrom(
828
- foregroundColor: _danger,
829
- ),
830
- label: Text('Delete'),
831
- ),
832
- ],
833
- ),
834
- ],
835
- )
836
- : Row(
837
- crossAxisAlignment: CrossAxisAlignment.start,
838
- children: <Widget>[
839
- Expanded(
840
- child: Column(
841
- crossAxisAlignment: CrossAxisAlignment.start,
842
- children: <Widget>[
843
- Text(
844
- skill.name,
845
- style: TextStyle(fontWeight: FontWeight.w700),
846
- ),
847
- const SizedBox(height: 6),
848
- Text(
849
- skill.description.ifEmpty('No description'),
850
- style: TextStyle(color: _textSecondary),
851
- ),
852
- const SizedBox(height: 10),
853
- Wrap(
854
- spacing: 8,
855
- runSpacing: 8,
856
- children: <Widget>[
857
- _MetaPill(
858
- label: skill.category,
859
- icon: Icons.folder_outlined,
860
- ),
861
- _MetaPill(
862
- label: skill.source,
863
- icon: Icons.source_outlined,
780
+ return LayoutBuilder(
781
+ builder: (context, constraints) {
782
+ final compact = constraints.maxWidth < 760;
783
+ return Container(
784
+ padding: const EdgeInsets.all(14),
785
+ decoration: BoxDecoration(
786
+ color: _bgSecondary,
787
+ borderRadius: BorderRadius.circular(14),
788
+ border: Border.all(color: _border),
789
+ ),
790
+ child: compact
791
+ ? Column(
792
+ crossAxisAlignment: CrossAxisAlignment.start,
793
+ children: <Widget>[
794
+ Row(
795
+ children: <Widget>[
796
+ Expanded(
797
+ child: Text(
798
+ skill.name,
799
+ style: TextStyle(
800
+ fontWeight: FontWeight.w700,
801
+ ),
802
+ ),
803
+ ),
804
+ Switch(
805
+ value: skill.enabled,
806
+ onChanged: (value) => controller
807
+ .setSkillEnabled(skill.name, value),
808
+ ),
809
+ ],
810
+ ),
811
+ Text(
812
+ skill.description.ifEmpty('No description'),
813
+ style: TextStyle(color: _textSecondary),
814
+ ),
815
+ const SizedBox(height: 10),
816
+ Wrap(
817
+ spacing: 8,
818
+ runSpacing: 8,
819
+ children: <Widget>[
820
+ _MetaPill(
821
+ label: skill.category,
822
+ icon: Icons.folder_outlined,
823
+ ),
824
+ _MetaPill(
825
+ label: skill.source,
826
+ icon: Icons.source_outlined,
827
+ ),
828
+ if (skill.draft)
829
+ const _MetaPill(
830
+ label: 'Draft',
831
+ icon: Icons.edit_note_outlined,
832
+ ),
833
+ ],
834
+ ),
835
+ const SizedBox(height: 10),
836
+ Row(
837
+ children: <Widget>[
838
+ const Spacer(),
839
+ OutlinedButton(
840
+ onPressed: () => _openSkillEditor(
841
+ context,
842
+ skill.name,
843
+ ),
844
+ child: Text('Open'),
845
+ ),
846
+ const SizedBox(width: 8),
847
+ TextButton.icon(
848
+ onPressed: () => _confirmDeleteSkill(
849
+ context,
850
+ skill.name,
851
+ ),
852
+ icon: Icon(Icons.delete_outline),
853
+ style: TextButton.styleFrom(
854
+ foregroundColor: _danger,
855
+ ),
856
+ label: Text('Delete'),
857
+ ),
858
+ ],
859
+ ),
860
+ ],
861
+ )
862
+ : Row(
863
+ crossAxisAlignment: CrossAxisAlignment.start,
864
+ children: <Widget>[
865
+ Expanded(
866
+ child: Column(
867
+ crossAxisAlignment:
868
+ CrossAxisAlignment.start,
869
+ children: <Widget>[
870
+ Text(
871
+ skill.name,
872
+ style: TextStyle(
873
+ fontWeight: FontWeight.w700,
874
+ ),
875
+ ),
876
+ const SizedBox(height: 6),
877
+ Text(
878
+ skill.description.ifEmpty(
879
+ 'No description',
880
+ ),
881
+ style: TextStyle(
882
+ color: _textSecondary,
883
+ ),
884
+ ),
885
+ const SizedBox(height: 10),
886
+ Wrap(
887
+ spacing: 8,
888
+ runSpacing: 8,
889
+ children: <Widget>[
890
+ _MetaPill(
891
+ label: skill.category,
892
+ icon: Icons.folder_outlined,
893
+ ),
894
+ _MetaPill(
895
+ label: skill.source,
896
+ icon: Icons.source_outlined,
897
+ ),
898
+ if (skill.draft)
899
+ const _MetaPill(
900
+ label: 'Draft',
901
+ icon: Icons.edit_note_outlined,
902
+ ),
903
+ ],
904
+ ),
905
+ ],
864
906
  ),
865
- if (skill.draft)
866
- const _MetaPill(
867
- label: 'Draft',
868
- icon: Icons.edit_note_outlined,
907
+ ),
908
+ const SizedBox(width: 10),
909
+ Column(
910
+ children: <Widget>[
911
+ Switch(
912
+ value: skill.enabled,
913
+ onChanged: (value) => controller
914
+ .setSkillEnabled(skill.name, value),
869
915
  ),
870
- ],
871
- ),
872
- ],
873
- ),
874
- ),
875
- const SizedBox(width: 10),
876
- Column(
877
- children: <Widget>[
878
- Switch(
879
- value: skill.enabled,
880
- onChanged: (value) => controller
881
- .setSkillEnabled(skill.name, value),
882
- ),
883
- OutlinedButton(
884
- onPressed: () =>
885
- _openSkillEditor(context, skill.name),
886
- child: Text('Open'),
887
- ),
888
- const SizedBox(height: 6),
889
- TextButton.icon(
890
- onPressed: () =>
891
- _confirmDeleteSkill(context, skill.name),
892
- icon: Icon(Icons.delete_outline),
893
- style: TextButton.styleFrom(
894
- foregroundColor: _danger,
895
- ),
896
- label: Text('Delete'),
916
+ OutlinedButton(
917
+ onPressed: () => _openSkillEditor(
918
+ context,
919
+ skill.name,
920
+ ),
921
+ child: Text('Open'),
922
+ ),
923
+ const SizedBox(height: 6),
924
+ TextButton.icon(
925
+ onPressed: () => _confirmDeleteSkill(
926
+ context,
927
+ skill.name,
928
+ ),
929
+ icon: Icon(Icons.delete_outline),
930
+ style: TextButton.styleFrom(
931
+ foregroundColor: _danger,
932
+ ),
933
+ label: Text('Delete'),
934
+ ),
935
+ ],
936
+ ),
937
+ ],
897
938
  ),
898
- ],
899
- ),
900
- ],
901
- ),
902
- );
903
- },
904
- );
939
+ );
940
+ },
941
+ );
905
942
  },
906
943
  ),
907
944
  ),
908
- ],
909
- ),
910
- );
945
+ ],
946
+ ),
947
+ );
911
948
  }
912
949
 
913
950
  Widget _buildStoreTab(
@@ -1465,8 +1502,9 @@ class _MemoryPanelState extends State<MemoryPanel>
1465
1502
  );
1466
1503
  if (!mounted) return;
1467
1504
  _llmImportController.clear();
1468
- final warningText =
1469
- result.warnings.isEmpty ? '' : ' ${result.warnings.join(' ')}';
1505
+ final warningText = result.warnings.isEmpty
1506
+ ? ''
1507
+ : ' ${result.warnings.join(' ')}';
1470
1508
  ScaffoldMessenger.of(context).showSnackBar(
1471
1509
  SnackBar(
1472
1510
  content: Text(
@@ -1494,9 +1532,7 @@ class _MemoryPanelState extends State<MemoryPanel>
1494
1532
  : controller.memories;
1495
1533
  if (_entityFilter == null) return base;
1496
1534
  return base
1497
- .where(
1498
- (m) => m.entities.any((e) => e.name == _entityFilter),
1499
- )
1535
+ .where((m) => m.entities.any((e) => e.name == _entityFilter))
1500
1536
  .toList();
1501
1537
  }
1502
1538
 
@@ -1588,7 +1624,10 @@ class _MemoryPanelState extends State<MemoryPanel>
1588
1624
  });
1589
1625
  }
1590
1626
 
1591
- void _openRetrievalInspector(BuildContext context, NeoAgentController controller) {
1627
+ void _openRetrievalInspector(
1628
+ BuildContext context,
1629
+ NeoAgentController controller,
1630
+ ) {
1592
1631
  Navigator.of(context).push(
1593
1632
  MaterialPageRoute(
1594
1633
  builder: (context) => RetrievalInspectorView(controller: controller),
@@ -1603,7 +1642,8 @@ class _MemoryPanelState extends State<MemoryPanel>
1603
1642
  final memoriesToShow = _visibleMemories;
1604
1643
  final selectedIds = _selectedVisibleMemoryIds.toSet();
1605
1644
  final selectedCount = selectedIds.length;
1606
- final allVisibleSelected = memoriesToShow.isNotEmpty &&
1645
+ final allVisibleSelected =
1646
+ memoriesToShow.isNotEmpty &&
1607
1647
  memoriesToShow.every((m) => selectedIds.contains(m.id));
1608
1648
  final showingSearchResults = controller.memoryRecallResults.isNotEmpty;
1609
1649
  final compact = MediaQuery.sizeOf(context).width < 760;
@@ -1644,9 +1684,7 @@ class _MemoryPanelState extends State<MemoryPanel>
1644
1684
  padding: const EdgeInsets.all(18),
1645
1685
  child: Row(
1646
1686
  children: <Widget>[
1647
- _MemoryConfidenceGauge(
1648
- confidence: stats.averageConfidence,
1649
- ),
1687
+ _MemoryConfidenceGauge(confidence: stats.averageConfidence),
1650
1688
  const SizedBox(width: 18),
1651
1689
  Expanded(
1652
1690
  child: Wrap(
@@ -1705,9 +1743,7 @@ class _MemoryPanelState extends State<MemoryPanel>
1705
1743
  children: <Widget>[
1706
1744
  Row(
1707
1745
  children: <Widget>[
1708
- Expanded(
1709
- child: const _SectionTitle('Knowledge Graph'),
1710
- ),
1746
+ Expanded(child: const _SectionTitle('Knowledge Graph')),
1711
1747
  if (_entityFilter != null)
1712
1748
  TextButton.icon(
1713
1749
  onPressed: () =>
@@ -1720,10 +1756,7 @@ class _MemoryPanelState extends State<MemoryPanel>
1720
1756
  const SizedBox(height: 4),
1721
1757
  Text(
1722
1758
  'Tap an entity to filter memories by it.',
1723
- style: TextStyle(
1724
- color: _textSecondary,
1725
- fontSize: 12,
1726
- ),
1759
+ style: TextStyle(color: _textSecondary, fontSize: 12),
1727
1760
  ),
1728
1761
  const SizedBox(height: 14),
1729
1762
  SizedBox(
@@ -1857,15 +1890,16 @@ class _MemoryPanelState extends State<MemoryPanel>
1857
1890
  Wrap(
1858
1891
  spacing: 8,
1859
1892
  runSpacing: 8,
1860
- crossAxisAlignment:
1861
- WrapCrossAlignment.center,
1893
+ crossAxisAlignment: WrapCrossAlignment.center,
1862
1894
  children: <Widget>[
1863
1895
  OutlinedButton.icon(
1864
- onPressed: allVisibleSelected ||
1896
+ onPressed:
1897
+ allVisibleSelected ||
1865
1898
  _bulkActionInFlight
1866
1899
  ? null
1867
1900
  : () => _selectAllVisibleMemories(
1868
- memoriesToShow),
1901
+ memoriesToShow,
1902
+ ),
1869
1903
  icon: Icon(
1870
1904
  Icons.done_all_outlined,
1871
1905
  size: 16,
@@ -1903,9 +1937,7 @@ class _MemoryPanelState extends State<MemoryPanel>
1903
1937
  Icons.archive_outlined,
1904
1938
  size: 16,
1905
1939
  ),
1906
- label: Text(
1907
- 'Archive ($selectedCount)',
1908
- ),
1940
+ label: Text('Archive ($selectedCount)'),
1909
1941
  ),
1910
1942
  OutlinedButton.icon(
1911
1943
  onPressed: _bulkActionInFlight
@@ -1923,9 +1955,7 @@ class _MemoryPanelState extends State<MemoryPanel>
1923
1955
  Icons.delete_sweep_outlined,
1924
1956
  size: 16,
1925
1957
  ),
1926
- label: Text(
1927
- 'Delete ($selectedCount)',
1928
- ),
1958
+ label: Text('Delete ($selectedCount)'),
1929
1959
  ),
1930
1960
  ],
1931
1961
  ],
@@ -1941,8 +1971,9 @@ class _MemoryPanelState extends State<MemoryPanel>
1941
1971
  )
1942
1972
  else
1943
1973
  ...memoriesToShow.map((memory) {
1944
- final isSelected =
1945
- selectedIds.contains(memory.id);
1974
+ final isSelected = selectedIds.contains(
1975
+ memory.id,
1976
+ );
1946
1977
  return _MemoryRow(
1947
1978
  memory: memory,
1948
1979
  isSelected: isSelected,
@@ -1950,8 +1981,7 @@ class _MemoryPanelState extends State<MemoryPanel>
1950
1981
  memory.id,
1951
1982
  !isSelected,
1952
1983
  ),
1953
- onCheck: (value) =>
1954
- _toggleMemorySelection(
1984
+ onCheck: (value) => _toggleMemorySelection(
1955
1985
  memory.id,
1956
1986
  value ?? false,
1957
1987
  ),
@@ -1964,9 +1994,9 @@ class _MemoryPanelState extends State<MemoryPanel>
1964
1994
  'This memory will be removed permanently.',
1965
1995
  onConfirm: () =>
1966
1996
  _deleteSingleMemory(
1967
- controller,
1968
- memory.id,
1969
- ),
1997
+ controller,
1998
+ memory.id,
1999
+ ),
1970
2000
  ),
1971
2001
  );
1972
2002
  }),
@@ -1985,9 +2015,7 @@ class _MemoryPanelState extends State<MemoryPanel>
1985
2015
  Expanded(
1986
2016
  child: Text(
1987
2017
  'Key-value pairs that persist across conversations.',
1988
- style: TextStyle(
1989
- color: _textSecondary,
1990
- ),
2018
+ style: TextStyle(color: _textSecondary),
1991
2019
  ),
1992
2020
  ),
1993
2021
  TextButton.icon(
@@ -2001,75 +2029,75 @@ class _MemoryPanelState extends State<MemoryPanel>
2001
2029
  ],
2002
2030
  ),
2003
2031
  const SizedBox(height: 10),
2004
- if (controller
2005
- .memoryOverview.coreEntries.isEmpty)
2032
+ if (controller.memoryOverview.coreEntries.isEmpty)
2006
2033
  Text(
2007
2034
  'No core memory entries yet.',
2008
2035
  style: TextStyle(color: _textSecondary),
2009
2036
  )
2010
2037
  else
2011
- ...controller.memoryOverview.coreEntries
2012
- .entries
2038
+ ...controller.memoryOverview.coreEntries.entries
2013
2039
  .map((entry) {
2014
- return Container(
2015
- width: double.infinity,
2016
- margin: const EdgeInsets.only(bottom: 10),
2017
- padding: const EdgeInsets.all(12),
2018
- decoration: BoxDecoration(
2019
- color: _bgSecondary,
2020
- borderRadius:
2021
- BorderRadius.circular(12),
2022
- border: Border.all(color: _border),
2023
- ),
2024
- child: Row(
2025
- crossAxisAlignment:
2026
- CrossAxisAlignment.start,
2027
- children: <Widget>[
2028
- Expanded(
2029
- child: Column(
2030
- crossAxisAlignment:
2031
- CrossAxisAlignment.start,
2032
- children: <Widget>[
2033
- Text(
2034
- entry.key,
2035
- style: TextStyle(
2036
- fontWeight: FontWeight.w700,
2037
- ),
2038
- ),
2039
- const SizedBox(height: 6),
2040
- Text(
2041
- entry.value.toString(),
2042
- ),
2043
- ],
2044
- ),
2040
+ return Container(
2041
+ width: double.infinity,
2042
+ margin: const EdgeInsets.only(
2043
+ bottom: 10,
2045
2044
  ),
2046
- IconButton(
2047
- onPressed: () =>
2048
- _openCoreMemoryEditor(
2049
- context,
2050
- controller,
2051
- keyValue: entry,
2045
+ padding: const EdgeInsets.all(12),
2046
+ decoration: BoxDecoration(
2047
+ color: _bgSecondary,
2048
+ borderRadius: BorderRadius.circular(
2049
+ 12,
2052
2050
  ),
2053
- icon: Icon(Icons.edit_outlined),
2051
+ border: Border.all(color: _border),
2054
2052
  ),
2055
- IconButton(
2056
- onPressed: () => _confirmDelete(
2057
- context,
2058
- title:
2059
- 'Delete core memory entry?',
2060
- message:
2061
- 'Remove "${entry.key}" from core memory.',
2062
- onConfirm: () => controller
2063
- .deleteCoreMemory(
2064
- entry.key,
2053
+ child: Row(
2054
+ crossAxisAlignment:
2055
+ CrossAxisAlignment.start,
2056
+ children: <Widget>[
2057
+ Expanded(
2058
+ child: Column(
2059
+ crossAxisAlignment:
2060
+ CrossAxisAlignment.start,
2061
+ children: <Widget>[
2062
+ Text(
2063
+ entry.key,
2064
+ style: TextStyle(
2065
+ fontWeight:
2066
+ FontWeight.w700,
2067
+ ),
2068
+ ),
2069
+ const SizedBox(height: 6),
2070
+ Text(entry.value.toString()),
2071
+ ],
2072
+ ),
2065
2073
  ),
2066
- ),
2067
- icon: Icon(Icons.delete_outline),
2074
+ IconButton(
2075
+ onPressed: () =>
2076
+ _openCoreMemoryEditor(
2077
+ context,
2078
+ controller,
2079
+ keyValue: entry,
2080
+ ),
2081
+ icon: Icon(Icons.edit_outlined),
2082
+ ),
2083
+ IconButton(
2084
+ onPressed: () => _confirmDelete(
2085
+ context,
2086
+ title:
2087
+ 'Delete core memory entry?',
2088
+ message:
2089
+ 'Remove "${entry.key}" from core memory.',
2090
+ onConfirm: () =>
2091
+ controller.deleteCoreMemory(
2092
+ entry.key,
2093
+ ),
2094
+ ),
2095
+ icon: Icon(Icons.delete_outline),
2096
+ ),
2097
+ ],
2068
2098
  ),
2069
- ],
2070
- ),
2071
- );
2072
- }),
2099
+ );
2100
+ }),
2073
2101
  ],
2074
2102
  ),
2075
2103
  ),
@@ -2092,11 +2120,8 @@ class _MemoryPanelState extends State<MemoryPanel>
2092
2120
  FilledButton.icon(
2093
2121
  onPressed: _llmPromptLoading
2094
2122
  ? null
2095
- : () =>
2096
- _loadLlmPrompt(controller),
2097
- icon: Icon(
2098
- Icons.auto_awesome_outlined,
2099
- ),
2123
+ : () => _loadLlmPrompt(controller),
2124
+ icon: Icon(Icons.auto_awesome_outlined),
2100
2125
  label: Text(
2101
2126
  _llmPromptLoading
2102
2127
  ? 'Generating...'
@@ -2105,11 +2130,9 @@ class _MemoryPanelState extends State<MemoryPanel>
2105
2130
  ),
2106
2131
  OutlinedButton.icon(
2107
2132
  onPressed:
2108
- _llmPromptController.text
2109
- .trim()
2110
- .isEmpty
2111
- ? null
2112
- : _copyLlmPrompt,
2133
+ _llmPromptController.text.trim().isEmpty
2134
+ ? null
2135
+ : _copyLlmPrompt,
2113
2136
  icon: Icon(Icons.copy_all_outlined),
2114
2137
  label: Text('Copy Prompt'),
2115
2138
  ),
@@ -2122,8 +2145,7 @@ class _MemoryPanelState extends State<MemoryPanel>
2122
2145
  maxLines: 8,
2123
2146
  readOnly: true,
2124
2147
  decoration: const InputDecoration(
2125
- labelText:
2126
- 'Prompt to paste into another AI',
2148
+ labelText: 'Prompt to paste into another AI',
2127
2149
  ),
2128
2150
  ),
2129
2151
  const SizedBox(height: 16),
@@ -2133,8 +2155,7 @@ class _MemoryPanelState extends State<MemoryPanel>
2133
2155
  onChanged: _llmImporting
2134
2156
  ? null
2135
2157
  : (value) => setState(
2136
- () =>
2137
- _llmApplyBehaviorNotes = value,
2158
+ () => _llmApplyBehaviorNotes = value,
2138
2159
  ),
2139
2160
  title: Text('Apply behavior notes'),
2140
2161
  subtitle: Text(
@@ -2147,8 +2168,7 @@ class _MemoryPanelState extends State<MemoryPanel>
2147
2168
  onChanged: _llmImporting
2148
2169
  ? null
2149
2170
  : (value) => setState(
2150
- () =>
2151
- _llmApplyCoreMemory = value,
2171
+ () => _llmApplyCoreMemory = value,
2152
2172
  ),
2153
2173
  title: Text('Apply core memory'),
2154
2174
  subtitle: Text(
@@ -2168,13 +2188,10 @@ class _MemoryPanelState extends State<MemoryPanel>
2168
2188
  FilledButton.icon(
2169
2189
  onPressed: _llmImporting
2170
2190
  ? null
2171
- : () =>
2172
- _importLlmMemories(controller),
2191
+ : () => _importLlmMemories(controller),
2173
2192
  icon: Icon(Icons.file_download_outlined),
2174
2193
  label: Text(
2175
- _llmImporting
2176
- ? 'Importing...'
2177
- : 'Import',
2194
+ _llmImporting ? 'Importing...' : 'Import',
2178
2195
  ),
2179
2196
  ),
2180
2197
  ],
@@ -2453,9 +2470,10 @@ class _MemoryConfidenceGaugeState extends State<_MemoryConfidenceGauge>
2453
2470
  vsync: this,
2454
2471
  duration: const Duration(milliseconds: 1200),
2455
2472
  );
2456
- _progress = Tween<double>(begin: 0, end: widget.confidence).animate(
2457
- CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic),
2458
- );
2473
+ _progress = Tween<double>(
2474
+ begin: 0,
2475
+ end: widget.confidence,
2476
+ ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic));
2459
2477
  _controller.forward();
2460
2478
  }
2461
2479
 
@@ -2463,12 +2481,10 @@ class _MemoryConfidenceGaugeState extends State<_MemoryConfidenceGauge>
2463
2481
  void didUpdateWidget(_MemoryConfidenceGauge oldWidget) {
2464
2482
  super.didUpdateWidget(oldWidget);
2465
2483
  if (oldWidget.confidence != widget.confidence) {
2466
- _progress = Tween<double>(
2467
- begin: _progress.value,
2468
- end: widget.confidence,
2469
- ).animate(
2470
- CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic),
2471
- );
2484
+ _progress = Tween<double>(begin: _progress.value, end: widget.confidence)
2485
+ .animate(
2486
+ CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic),
2487
+ );
2472
2488
  _controller
2473
2489
  ..reset()
2474
2490
  ..forward();
@@ -2658,10 +2674,7 @@ class _MemoryRow extends StatelessWidget {
2658
2674
  if (onDelete != null)
2659
2675
  IconButton(
2660
2676
  onPressed: onDelete,
2661
- icon: Icon(
2662
- Icons.delete_outline,
2663
- size: 18,
2664
- ),
2677
+ icon: Icon(Icons.delete_outline, size: 18),
2665
2678
  ),
2666
2679
  ],
2667
2680
  ),
@@ -2686,10 +2699,7 @@ class _MemoryRow extends StatelessWidget {
2686
2699
  const SizedBox(height: 6),
2687
2700
  Text(
2688
2701
  memory.createdAtLabel,
2689
- style: TextStyle(
2690
- fontSize: 11,
2691
- color: _textMuted,
2692
- ),
2702
+ style: TextStyle(fontSize: 11, color: _textMuted),
2693
2703
  ),
2694
2704
  ],
2695
2705
  ),
@@ -2778,27 +2788,31 @@ class _EntityGraphViewState extends State<_EntityGraphView>
2778
2788
  for (int i = 0; i < entities.length; i++) {
2779
2789
  final entity = entities[i];
2780
2790
  final sizeFactor = 0.4 + 0.6 * (entity.mentionCount / maxMention);
2781
- _nodes.add(_GraphNode(
2782
- id: entity.name,
2783
- label: entity.name,
2784
- radius: 18 + 20 * sizeFactor,
2785
- color: _kindColors[entity.kind] ?? _kindColors['concept']!,
2786
- kind: entity.kind,
2787
- isReflection: false,
2788
- offsetPhase: i * 0.7,
2789
- ));
2791
+ _nodes.add(
2792
+ _GraphNode(
2793
+ id: entity.name,
2794
+ label: entity.name,
2795
+ radius: 18 + 20 * sizeFactor,
2796
+ color: _kindColors[entity.kind] ?? _kindColors['concept']!,
2797
+ kind: entity.kind,
2798
+ isReflection: false,
2799
+ offsetPhase: i * 0.7,
2800
+ ),
2801
+ );
2790
2802
  }
2791
2803
 
2792
2804
  for (int i = 0; i < views.length && i < 6; i++) {
2793
- _nodes.add(_GraphNode(
2794
- id: 'kv_${views[i].title}',
2795
- label: views[i].title,
2796
- radius: 14,
2797
- color: const Color(0xFF8B7EC8),
2798
- kind: views[i].viewType,
2799
- isReflection: true,
2800
- offsetPhase: (entities.length + i) * 0.9,
2801
- ));
2805
+ _nodes.add(
2806
+ _GraphNode(
2807
+ id: 'kv_${views[i].title}',
2808
+ label: views[i].title,
2809
+ radius: 14,
2810
+ color: const Color(0xFF8B7EC8),
2811
+ kind: views[i].viewType,
2812
+ isReflection: true,
2813
+ offsetPhase: (entities.length + i) * 0.9,
2814
+ ),
2815
+ );
2802
2816
  }
2803
2817
 
2804
2818
  _layoutDone = false;
@@ -2939,10 +2953,12 @@ class _EntityGraphPainter extends CustomPainter {
2939
2953
  void paint(Canvas canvas, Size size) {
2940
2954
  if (nodes.isEmpty) return;
2941
2955
 
2942
- final entityNodes =
2943
- nodes.where((n) => !n.isReflection).toList(growable: false);
2944
- final reflectionNodes =
2945
- nodes.where((n) => n.isReflection).toList(growable: false);
2956
+ final entityNodes = nodes
2957
+ .where((n) => !n.isReflection)
2958
+ .toList(growable: false);
2959
+ final reflectionNodes = nodes
2960
+ .where((n) => n.isReflection)
2961
+ .toList(growable: false);
2946
2962
 
2947
2963
  // Draw connections between entity nodes (subtle web)
2948
2964
  final linePaint = Paint()
@@ -2974,8 +2990,8 @@ class _EntityGraphPainter extends CustomPainter {
2974
2990
  var closest = entityNodes.first;
2975
2991
  var minDist = double.infinity;
2976
2992
  for (final en in entityNodes) {
2977
- final d = (en.x - rn.x) * (en.x - rn.x) +
2978
- (en.y - rn.y) * (en.y - rn.y);
2993
+ final d =
2994
+ (en.x - rn.x) * (en.x - rn.x) + (en.y - rn.y) * (en.y - rn.y);
2979
2995
  if (d < minDist) {
2980
2996
  minDist = d;
2981
2997
  closest = en;
@@ -3002,8 +3018,9 @@ class _EntityGraphPainter extends CustomPainter {
3002
3018
  // Glow
3003
3019
  if (isSelected || isHovered) {
3004
3020
  final glowPaint = Paint()
3005
- ..color = (isSelected ? accentColor : node.color)
3006
- .withValues(alpha: 0.22)
3021
+ ..color = (isSelected ? accentColor : node.color).withValues(
3022
+ alpha: 0.22,
3023
+ )
3007
3024
  ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 12);
3008
3025
  canvas.drawCircle(Offset(cx, cy), r + 6, glowPaint);
3009
3026
  }
@@ -3023,8 +3040,8 @@ class _EntityGraphPainter extends CustomPainter {
3023
3040
  ..color = isSelected
3024
3041
  ? accentColor
3025
3042
  : (isHovered
3026
- ? node.color.withValues(alpha: 0.8)
3027
- : node.color.withValues(alpha: 0.35))
3043
+ ? node.color.withValues(alpha: 0.8)
3044
+ : node.color.withValues(alpha: 0.35))
3028
3045
  ..style = PaintingStyle.stroke
3029
3046
  ..strokeWidth = isSelected ? 2.5 : 1.5;
3030
3047
  canvas.drawCircle(Offset(cx, cy), r, borderPaint);
@@ -3041,10 +3058,7 @@ class _EntityGraphPainter extends CustomPainter {
3041
3058
  maxLines: 1,
3042
3059
  ellipsis: '…',
3043
3060
  )..layout(maxWidth: r * 3);
3044
- tp.paint(
3045
- canvas,
3046
- Offset(cx - tp.width / 2, cy + r + 5),
3047
- );
3061
+ tp.paint(canvas, Offset(cx - tp.width / 2, cy + r + 5));
3048
3062
  }
3049
3063
  }
3050
3064
 
@@ -5025,6 +5039,15 @@ const List<_TaskTriggerOption> _taskTriggerOptions = <_TaskTriggerOption>[
5025
5039
  providerKey: 'google_workspace',
5026
5040
  appKey: 'gmail',
5027
5041
  ),
5042
+ _TaskTriggerOption(
5043
+ type: 'neomail_email_received',
5044
+ section: 'Email',
5045
+ label: 'NeoMail Email Received',
5046
+ description: 'Run when a matching message is synced into NeoMail.',
5047
+ icon: Icons.mark_email_unread_rounded,
5048
+ providerKey: 'neomail',
5049
+ appKey: 'mailbox',
5050
+ ),
5028
5051
  _TaskTriggerOption(
5029
5052
  type: 'outlook_email_received',
5030
5053
  section: 'Email',
@@ -5087,7 +5110,6 @@ _TaskTriggerOption _taskTriggerOptionForType(String type) {
5087
5110
  );
5088
5111
  }
5089
5112
 
5090
-
5091
5113
  Future<String?> _pickTaskTriggerType(
5092
5114
  BuildContext context,
5093
5115
  String selectedType,
@@ -5847,9 +5869,7 @@ class _TasksPanelState extends State<TasksPanel> {
5847
5869
  final selectedConnectionId = ValueNotifier<int?>(
5848
5870
  task?.triggerConfig['connectionId'] is int
5849
5871
  ? task!.triggerConfig['connectionId'] as int
5850
- : int.tryParse(
5851
- task?.triggerConfig['connectionId']?.toString() ?? '',
5852
- ),
5872
+ : int.tryParse(task?.triggerConfig['connectionId']?.toString() ?? ''),
5853
5873
  );
5854
5874
  final queryController = TextEditingController(
5855
5875
  text:
@@ -5869,6 +5889,8 @@ class _TasksPanelState extends State<TasksPanel> {
5869
5889
  );
5870
5890
  final channelController = TextEditingController(
5871
5891
  text:
5892
+ task?.triggerConfig['folder']?.toString() ??
5893
+ task?.triggerConfig['folderId']?.toString() ??
5872
5894
  task?.triggerConfig['channel']?.toString() ??
5873
5895
  task?.triggerConfig['chatId']?.toString() ??
5874
5896
  '',
@@ -6089,6 +6111,8 @@ class _TasksPanelState extends State<TasksPanel> {
6089
6111
  ],
6090
6112
  if (selectedTriggerType ==
6091
6113
  'gmail_message_received' ||
6114
+ selectedTriggerType ==
6115
+ 'neomail_email_received' ||
6092
6116
  selectedTriggerType ==
6093
6117
  'outlook_email_received') ...<Widget>[
6094
6118
  TextField(
@@ -6106,6 +6130,18 @@ class _TasksPanelState extends State<TasksPanel> {
6106
6130
  setLocalState(() => unreadOnly = value),
6107
6131
  ),
6108
6132
  ],
6133
+ if (selectedTriggerType ==
6134
+ 'neomail_email_received') ...<Widget>[
6135
+ TextField(
6136
+ controller: channelController,
6137
+ decoration: const InputDecoration(
6138
+ labelText: 'Folder (optional)',
6139
+ helperText:
6140
+ 'Example: INBOX, archive, or a custom folder path.',
6141
+ ),
6142
+ ),
6143
+ const SizedBox(height: 12),
6144
+ ],
6109
6145
  if (selectedTriggerType ==
6110
6146
  'outlook_email_received') ...<Widget>[
6111
6147
  TextField(
@@ -6362,11 +6398,17 @@ class _TasksPanelState extends State<TasksPanel> {
6362
6398
  triggerConfig['eventTypes'] = eventTypes;
6363
6399
  }
6364
6400
  if (selectedTriggerType == 'gmail_message_received' ||
6401
+ selectedTriggerType == 'neomail_email_received' ||
6365
6402
  selectedTriggerType == 'outlook_email_received') {
6366
6403
  if (queryController.text.trim().isNotEmpty) {
6367
6404
  triggerConfig['query'] = queryController.text.trim();
6368
6405
  }
6369
6406
  triggerConfig['unreadOnly'] = unreadOnly;
6407
+ if (selectedTriggerType == 'neomail_email_received' &&
6408
+ channelController.text.trim().isNotEmpty) {
6409
+ triggerConfig['folder'] = channelController.text
6410
+ .trim();
6411
+ }
6370
6412
  if (selectedTriggerType == 'outlook_email_received' &&
6371
6413
  channelController.text.trim().isNotEmpty) {
6372
6414
  triggerConfig['folderId'] = channelController.text