neoagent 2.4.1-beta.9 → 2.4.1

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 (234) hide show
  1. package/.env.example +33 -3
  2. package/LICENSE +111 -56
  3. package/README.md +8 -3
  4. package/docs/configuration.md +8 -0
  5. package/docs/getting-started.md +9 -3
  6. package/docs/index.md +4 -0
  7. package/extensions/chrome-browser/background.mjs +36 -4
  8. package/extensions/chrome-browser/icons/icon128.png +0 -0
  9. package/extensions/chrome-browser/icons/icon16.png +0 -0
  10. package/extensions/chrome-browser/icons/icon48.png +0 -0
  11. package/extensions/chrome-browser/icons/logo.svg +39 -8
  12. package/extensions/chrome-browser/manifest.json +3 -3
  13. package/extensions/chrome-browser/popup.html +5 -1
  14. package/extensions/chrome-browser/popup.js +13 -0
  15. package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
  16. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  17. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  18. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  19. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  20. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  21. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  22. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  23. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  24. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  25. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  26. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  27. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  28. package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
  29. package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
  30. package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
  31. package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
  32. package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
  33. package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
  34. package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
  35. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  36. package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
  37. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  38. package/flutter_app/lib/features/location/location_service.dart +3 -0
  39. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
  40. package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
  41. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
  42. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
  43. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
  44. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
  45. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
  46. package/flutter_app/lib/main.dart +2 -0
  47. package/flutter_app/lib/main_account_settings.dart +10 -34
  48. package/flutter_app/lib/main_admin.dart +14 -1
  49. package/flutter_app/lib/main_app_shell.dart +377 -340
  50. package/flutter_app/lib/main_chat.dart +707 -227
  51. package/flutter_app/lib/main_controller.dart +197 -42
  52. package/flutter_app/lib/main_devices.dart +436 -4
  53. package/flutter_app/lib/main_integrations.dart +255 -6
  54. package/flutter_app/lib/main_launcher.dart +1 -1
  55. package/flutter_app/lib/main_model_picker.dart +685 -0
  56. package/flutter_app/lib/main_models.dart +212 -0
  57. package/flutter_app/lib/main_navigation.dart +1 -9
  58. package/flutter_app/lib/main_operations.dart +1417 -614
  59. package/flutter_app/lib/main_settings.dart +673 -871
  60. package/flutter_app/lib/main_shared.dart +971 -443
  61. package/flutter_app/lib/main_spacing.dart +4 -4
  62. package/flutter_app/lib/main_theme.dart +22 -14
  63. package/flutter_app/lib/main_unified.dart +5 -72
  64. package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
  65. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
  66. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
  67. package/flutter_app/lib/src/backend_client.dart +46 -0
  68. package/flutter_app/lib/src/desktop_companion_actions.dart +30 -7
  69. package/flutter_app/lib/src/desktop_companion_io.dart +71 -1
  70. package/flutter_app/lib/src/security/password_strength.dart +84 -0
  71. package/flutter_app/lib/src/stream_renderer.dart +42 -3
  72. package/flutter_app/lib/src/theme/palette.dart +76 -34
  73. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  74. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  75. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  76. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  77. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  78. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  79. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  80. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  81. package/flutter_app/pubspec.lock +2 -2
  82. package/flutter_app/pubspec.yaml +7 -1
  83. package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
  84. package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
  85. package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
  86. package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
  87. package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
  88. package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
  89. package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
  90. package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
  91. package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
  92. package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
  93. package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
  94. package/flutter_app/tool/generate_desktop_branding.py +154 -152
  95. package/flutter_app/web/favicon.png +0 -0
  96. package/flutter_app/web/favicon.svg +40 -9
  97. package/flutter_app/web/favicon_light.svg +43 -0
  98. package/flutter_app/web/icons/Icon-192-light.png +0 -0
  99. package/flutter_app/web/icons/Icon-192.png +0 -0
  100. package/flutter_app/web/icons/Icon-512-light.png +0 -0
  101. package/flutter_app/web/icons/Icon-512.png +0 -0
  102. package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
  103. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  104. package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
  105. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  106. package/flutter_app/windows/runner/main.cpp +7 -1
  107. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  108. package/lib/manager.js +445 -83
  109. package/package.json +17 -3
  110. package/runtime/paths.js +3 -1
  111. package/server/admin/access.js +198 -0
  112. package/server/admin/admin.css +268 -0
  113. package/server/admin/admin.js +348 -0
  114. package/server/admin/analytics.js +128 -0
  115. package/server/admin/index.html +1015 -0
  116. package/server/admin/login.html +290 -0
  117. package/server/admin/logo.svg +43 -0
  118. package/server/admin/sql.js +134 -0
  119. package/server/admin/users.js +147 -0
  120. package/server/config/origins.js +3 -1
  121. package/server/db/database.js +92 -0
  122. package/server/db/ftsQuery.js +27 -0
  123. package/server/http/routes.js +1 -0
  124. package/server/http/static.js +23 -6
  125. package/server/index.js +1 -1
  126. package/server/middleware/adminAuth.js +48 -0
  127. package/server/middleware/auth.js +1 -40
  128. package/server/public/.last_build_id +1 -1
  129. package/server/public/app_icon.png +0 -0
  130. package/server/public/assets/AssetManifest.bin +1 -1
  131. package/server/public/assets/AssetManifest.bin.json +1 -1
  132. package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
  133. package/server/public/assets/assets/branding/app_icon_256.png +0 -0
  134. package/server/public/assets/assets/branding/app_icon_512.png +0 -0
  135. package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
  136. package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
  137. package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
  138. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  139. package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
  140. package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
  141. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  142. package/server/public/assets/web/icons/Icon-192.png +0 -0
  143. package/server/public/favicon.png +0 -0
  144. package/server/public/favicon.svg +40 -9
  145. package/server/public/favicon_light.svg +43 -0
  146. package/server/public/flutter_bootstrap.js +2 -2
  147. package/server/public/icons/Icon-192-light.png +0 -0
  148. package/server/public/icons/Icon-192.png +0 -0
  149. package/server/public/icons/Icon-512-light.png +0 -0
  150. package/server/public/icons/Icon-512.png +0 -0
  151. package/server/public/icons/Icon-maskable-192-light.png +0 -0
  152. package/server/public/icons/Icon-maskable-192.png +0 -0
  153. package/server/public/icons/Icon-maskable-512-light.png +0 -0
  154. package/server/public/icons/Icon-maskable-512.png +0 -0
  155. package/server/public/main.dart.js +88537 -85936
  156. package/server/routes/admin.js +632 -0
  157. package/server/routes/agent_profiles.js +3 -0
  158. package/server/routes/agents.js +1 -1
  159. package/server/routes/auth.js +3 -1
  160. package/server/routes/mcp.js +29 -13
  161. package/server/routes/memory.js +23 -1
  162. package/server/routes/messaging.js +2 -0
  163. package/server/routes/screenHistory.js +14 -8
  164. package/server/routes/settings.js +10 -2
  165. package/server/routes/stream.js +1 -1
  166. package/server/routes/triggers.js +4 -4
  167. package/server/routes/voice_assistant.js +36 -1
  168. package/server/routes/workspace.js +86 -0
  169. package/server/services/account/admin_two_factor.js +132 -0
  170. package/server/services/account/password_policy.js +6 -1
  171. package/server/services/ai/completion.js +44 -0
  172. package/server/services/ai/engine.js +321 -500
  173. package/server/services/ai/imageAnalysis.js +9 -5
  174. package/server/services/ai/logFormat.js +46 -0
  175. package/server/services/ai/loopPolicy.js +11 -0
  176. package/server/services/ai/messagingFallback.js +228 -0
  177. package/server/services/ai/models.js +194 -55
  178. package/server/services/ai/providerRetry.js +169 -0
  179. package/server/services/ai/providers/anthropic.js +15 -4
  180. package/server/services/ai/providers/google.js +38 -11
  181. package/server/services/ai/providers/grok.js +19 -68
  182. package/server/services/ai/providers/grokOauth.js +141 -0
  183. package/server/services/ai/providers/nvidia.js +154 -0
  184. package/server/services/ai/providers/ollama.js +76 -39
  185. package/server/services/ai/providers/openai.js +20 -31
  186. package/server/services/ai/providers/openaiCodex.js +6 -2
  187. package/server/services/ai/providers/openaiCompatible.js +70 -0
  188. package/server/services/ai/providers/openrouter.js +162 -0
  189. package/server/services/ai/settings.js +30 -0
  190. package/server/services/ai/systemPrompt.js +51 -29
  191. package/server/services/ai/taskAnalysis.js +60 -1
  192. package/server/services/ai/toolEvidence.js +207 -0
  193. package/server/services/ai/toolSelector.js +8 -1
  194. package/server/services/ai/tools.js +78 -13
  195. package/server/services/browser/extension/registry.js +94 -9
  196. package/server/services/desktop/registry.js +104 -1
  197. package/server/services/desktop/screenRecorder.js +208 -98
  198. package/server/services/desktop/screen_recorder_support.js +46 -0
  199. package/server/services/integrations/google/provider.js +13 -0
  200. package/server/services/integrations/home_assistant/constants.js +88 -0
  201. package/server/services/integrations/home_assistant/network.js +207 -0
  202. package/server/services/integrations/home_assistant/provider.js +249 -0
  203. package/server/services/integrations/home_assistant/snapshot.js +98 -0
  204. package/server/services/integrations/home_assistant/tools.js +101 -0
  205. package/server/services/integrations/manager.js +5 -0
  206. package/server/services/integrations/registry.js +2 -0
  207. package/server/services/integrations/trello/provider.js +8 -2
  208. package/server/services/manager.js +51 -53
  209. package/server/services/mcp/client.js +208 -268
  210. package/server/services/mcp/client_support.js +172 -0
  211. package/server/services/mcp/recovery.js +116 -0
  212. package/server/services/mcp/tool_operations.js +123 -0
  213. package/server/services/memory/ingestion.js +185 -370
  214. package/server/services/memory/ingestion_coverage.js +129 -0
  215. package/server/services/memory/ingestion_documents.js +123 -0
  216. package/server/services/memory/ingestion_support.js +171 -0
  217. package/server/services/memory/intelligence.js +181 -0
  218. package/server/services/memory/manager.js +476 -32
  219. package/server/services/messaging/automation.js +71 -134
  220. package/server/services/messaging/formatting_guides.js +26 -1
  221. package/server/services/messaging/inbound_queue.js +111 -0
  222. package/server/services/messaging/manager.js +25 -8
  223. package/server/services/messaging/typing_keepalive.js +110 -0
  224. package/server/services/streaming/android-stream.js +293 -40
  225. package/server/services/tasks/integration_runtime.js +4 -1
  226. package/server/services/tasks/runtime.js +415 -56
  227. package/server/services/tasks/task_repository.js +48 -6
  228. package/server/services/websocket.js +8 -2
  229. package/server/services/widgets/service.js +17 -1
  230. package/server/services/workspace/manager.js +116 -0
  231. package/server/utils/logger.js +44 -6
  232. package/server/utils/security.js +3 -0
  233. package/server/utils/version.js +29 -19
  234. package/server/services/memory/openhuman_uplift.test.js +0 -98
@@ -11,6 +11,8 @@ EdgeInsets _pagePadding(BuildContext context) {
11
11
  return const EdgeInsets.fromLTRB(20, 20, 20, 28);
12
12
  }
13
13
 
14
+ final ValueNotifier<bool> _partyModeEnabled = ValueNotifier<bool>(false);
15
+
14
16
  class _AmbientBackdrop extends StatefulWidget {
15
17
  const _AmbientBackdrop({required this.child});
16
18
 
@@ -41,81 +43,266 @@ class _AmbientBackdropState extends State<_AmbientBackdrop>
41
43
 
42
44
  @override
43
45
  Widget build(BuildContext context) {
44
- return DecoratedBox(
45
- decoration: BoxDecoration(gradient: _appBackgroundGradient),
46
- child: AnimatedBuilder(
47
- animation: _controller,
48
- builder: (context, _) {
49
- final t = Curves.easeInOut.transform(_controller.value);
50
- return Stack(
51
- children: <Widget>[
52
- Positioned(
53
- top: -120 + (t * 22),
54
- left: -90 + (t * 18),
55
- child: _BlurOrb(
56
- size: 340,
57
- color: _accent.withValues(alpha: 0.9),
58
- ),
59
- ),
60
- Positioned(
61
- top: 90 - (t * 26),
62
- right: -120 + (t * 22),
63
- child: _BlurOrb(
64
- size: 280,
65
- color: _accentAlt.withValues(alpha: 0.85),
66
- ),
67
- ),
68
- Positioned(
69
- bottom: -140 + (t * 16),
70
- left: 100 - (t * 24),
71
- child: _BlurOrb(
72
- size: 360,
73
- color: _accent.withValues(alpha: 0.45),
74
- ),
75
- ),
76
- Positioned.fill(
77
- child: IgnorePointer(
78
- child: DecoratedBox(
79
- decoration: BoxDecoration(
80
- gradient: LinearGradient(
81
- colors: <Color>[
82
- Colors.white.withValues(alpha: 0.05),
83
- Colors.transparent,
84
- Colors.black.withValues(alpha: 0.12),
85
- ],
86
- stops: const <double>[0, 0.32, 1],
87
- begin: Alignment.topCenter,
88
- end: Alignment.bottomCenter,
46
+ return ValueListenableBuilder<bool>(
47
+ valueListenable: _partyModeEnabled,
48
+ builder: (context, partyMode, _) {
49
+ return DecoratedBox(
50
+ decoration: BoxDecoration(gradient: _appBackgroundGradient),
51
+ child: AnimatedBuilder(
52
+ animation: _controller,
53
+ builder: (context, _) {
54
+ final t = Curves.easeInOut.transform(_controller.value);
55
+ return Stack(
56
+ children: <Widget>[
57
+ Positioned.fill(
58
+ child: IgnorePointer(
59
+ child: CustomPaint(
60
+ painter: _AuroraFieldPainter(
61
+ progress: t,
62
+ partyMode: partyMode,
63
+ primary: _accent,
64
+ secondary: _accentAlt,
65
+ base: _bgPrimary,
66
+ ),
89
67
  ),
90
68
  ),
91
69
  ),
92
- ),
93
- ),
94
- Positioned.fill(
95
- child: IgnorePointer(
96
- child: DecoratedBox(
97
- decoration: BoxDecoration(
98
- gradient: RadialGradient(
99
- center: Alignment(0.75 - (t * 0.15), -0.9 + (t * 0.1)),
100
- radius: 0.95,
101
- colors: <Color>[
102
- _glassHighlight.withValues(alpha: 0.14),
103
- Colors.transparent,
104
- ],
70
+ Positioned.fill(
71
+ child: IgnorePointer(
72
+ child: CustomPaint(
73
+ painter: _ArcadeGridPainter(
74
+ progress: t,
75
+ color: _accentAlt,
76
+ partyMode: partyMode,
77
+ ),
105
78
  ),
106
79
  ),
107
80
  ),
108
- ),
109
- ),
110
- widget.child,
111
- ],
112
- );
113
- },
81
+ Positioned.fill(
82
+ child: IgnorePointer(
83
+ child: DecoratedBox(
84
+ decoration: BoxDecoration(
85
+ gradient: LinearGradient(
86
+ colors: <Color>[
87
+ Colors.white.withValues(alpha: 0.05),
88
+ Colors.transparent,
89
+ Colors.black.withValues(alpha: 0.12),
90
+ ],
91
+ stops: const <double>[0, 0.32, 1],
92
+ begin: Alignment.topCenter,
93
+ end: Alignment.bottomCenter,
94
+ ),
95
+ ),
96
+ ),
97
+ ),
98
+ ),
99
+ Positioned.fill(
100
+ child: IgnorePointer(
101
+ child: CustomPaint(
102
+ painter: _ConfettiBitsPainter(
103
+ progress: t,
104
+ active: partyMode,
105
+ primary: _accent,
106
+ secondary: _accentAlt,
107
+ ),
108
+ ),
109
+ ),
110
+ ),
111
+ widget.child,
112
+ ],
113
+ );
114
+ },
115
+ ),
116
+ );
117
+ },
118
+ );
119
+ }
120
+ }
121
+
122
+ class _ControlSurfaceBackdrop extends StatelessWidget {
123
+ const _ControlSurfaceBackdrop({required this.child});
124
+
125
+ final Widget child;
126
+
127
+ @override
128
+ Widget build(BuildContext context) {
129
+ return DecoratedBox(
130
+ decoration: BoxDecoration(
131
+ gradient: LinearGradient(
132
+ colors: <Color>[
133
+ _bgPrimary,
134
+ Color.lerp(_bgPrimary, _accentAlt, 0.025)!,
135
+ Color.lerp(_bgPrimary, _accent, 0.02)!,
136
+ ],
137
+ stops: const <double>[0, 0.58, 1],
138
+ begin: const Alignment(-0.95, -1),
139
+ end: const Alignment(0.96, 1),
140
+ ),
114
141
  ),
142
+ child: child,
115
143
  );
116
144
  }
117
145
  }
118
146
 
147
+ class _AuroraFieldPainter extends CustomPainter {
148
+ const _AuroraFieldPainter({
149
+ required this.progress,
150
+ required this.partyMode,
151
+ required this.primary,
152
+ required this.secondary,
153
+ required this.base,
154
+ });
155
+
156
+ final double progress;
157
+ final bool partyMode;
158
+ final Color primary;
159
+ final Color secondary;
160
+ final Color base;
161
+
162
+ @override
163
+ void paint(Canvas canvas, Size size) {
164
+ final rect = Offset.zero & size;
165
+ final energy = partyMode ? 1.45 : 1.0;
166
+ final wash = Paint()
167
+ ..shader = LinearGradient(
168
+ colors: <Color>[
169
+ primary.withValues(alpha: 0.18 * energy),
170
+ secondary.withValues(alpha: 0.12 * energy),
171
+ base.withValues(alpha: 0),
172
+ ],
173
+ stops: const <double>[0, 0.42, 1],
174
+ begin: Alignment(-0.95 + progress * 0.35, -1),
175
+ end: Alignment(0.85 - progress * 0.25, 1),
176
+ ).createShader(rect);
177
+ canvas.drawRect(rect, wash);
178
+
179
+ for (var lane = 0; lane < 4; lane++) {
180
+ final yBase = size.height * (0.16 + lane * 0.18);
181
+ final path = Path()..moveTo(-size.width * 0.12, yBase);
182
+ for (var i = 0; i <= 8; i++) {
183
+ final x = size.width * (i / 8);
184
+ final phase = progress * math.pi * 2 + lane * 0.9 + i * 0.28;
185
+ final y = yBase + math.sin(phase) * (28 + lane * 9) * energy;
186
+ path.lineTo(x, y);
187
+ }
188
+ path.lineTo(size.width * 1.12, yBase + size.height * 0.34);
189
+ path.lineTo(-size.width * 0.12, yBase + size.height * 0.28);
190
+ path.close();
191
+
192
+ final lanePaint = Paint()
193
+ ..shader = LinearGradient(
194
+ colors: <Color>[
195
+ (lane.isEven ? primary : secondary).withValues(
196
+ alpha: (0.07 + lane * 0.012) * energy,
197
+ ),
198
+ Colors.transparent,
199
+ ],
200
+ begin: Alignment.topLeft,
201
+ end: Alignment.bottomRight,
202
+ ).createShader(rect)
203
+ ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 42);
204
+ canvas.drawPath(path, lanePaint);
205
+ }
206
+ }
207
+
208
+ @override
209
+ bool shouldRepaint(covariant _AuroraFieldPainter oldDelegate) {
210
+ return oldDelegate.progress != progress ||
211
+ oldDelegate.partyMode != partyMode ||
212
+ oldDelegate.primary != primary ||
213
+ oldDelegate.secondary != secondary ||
214
+ oldDelegate.base != base;
215
+ }
216
+ }
217
+
218
+ class _ArcadeGridPainter extends CustomPainter {
219
+ const _ArcadeGridPainter({
220
+ required this.progress,
221
+ required this.color,
222
+ required this.partyMode,
223
+ });
224
+
225
+ final double progress;
226
+ final Color color;
227
+ final bool partyMode;
228
+
229
+ @override
230
+ void paint(Canvas canvas, Size size) {
231
+ final paint = Paint()
232
+ ..color = color.withValues(alpha: partyMode ? 0.13 : 0.055)
233
+ ..strokeWidth = 1;
234
+ const step = 48.0;
235
+ final drift = progress * step;
236
+ for (double x = -step + drift; x < size.width + step; x += step) {
237
+ canvas.drawLine(
238
+ Offset(x, 0),
239
+ Offset(x - size.width * 0.08, size.height),
240
+ paint,
241
+ );
242
+ }
243
+ for (double y = size.height * 0.58; y < size.height + step; y += step) {
244
+ canvas.drawLine(
245
+ Offset(0, y),
246
+ Offset(size.width, y - progress * 12),
247
+ paint,
248
+ );
249
+ }
250
+ }
251
+
252
+ @override
253
+ bool shouldRepaint(covariant _ArcadeGridPainter oldDelegate) {
254
+ return oldDelegate.progress != progress ||
255
+ oldDelegate.color != color ||
256
+ oldDelegate.partyMode != partyMode;
257
+ }
258
+ }
259
+
260
+ class _ConfettiBitsPainter extends CustomPainter {
261
+ const _ConfettiBitsPainter({
262
+ required this.progress,
263
+ required this.active,
264
+ required this.primary,
265
+ required this.secondary,
266
+ });
267
+
268
+ final double progress;
269
+ final bool active;
270
+ final Color primary;
271
+ final Color secondary;
272
+
273
+ @override
274
+ void paint(Canvas canvas, Size size) {
275
+ if (!active) return;
276
+ final paint = Paint()..style = PaintingStyle.fill;
277
+ for (var i = 0; i < 26; i++) {
278
+ final seed = i * 37.0;
279
+ final x = ((seed * 17 + progress * size.width * 0.35) % size.width);
280
+ final y = ((seed * 29 + progress * size.height * 0.9) % size.height);
281
+ final color = i.isEven ? primary : secondary;
282
+ paint.color = color.withValues(alpha: 0.16 + (i % 4) * 0.035);
283
+ canvas.save();
284
+ canvas.translate(x, y);
285
+ canvas.rotate(progress * math.pi * 2 + i);
286
+ canvas.drawRRect(
287
+ RRect.fromRectAndRadius(
288
+ Rect.fromCenter(center: Offset.zero, width: 5 + (i % 3), height: 2.5),
289
+ const Radius.circular(2),
290
+ ),
291
+ paint,
292
+ );
293
+ canvas.restore();
294
+ }
295
+ }
296
+
297
+ @override
298
+ bool shouldRepaint(covariant _ConfettiBitsPainter oldDelegate) {
299
+ return oldDelegate.progress != progress ||
300
+ oldDelegate.active != active ||
301
+ oldDelegate.primary != primary ||
302
+ oldDelegate.secondary != secondary;
303
+ }
304
+ }
305
+
119
306
  class _EntranceMotion extends StatefulWidget {
120
307
  const _EntranceMotion({required this.child});
121
308
 
@@ -231,7 +418,6 @@ List<AppSection> _mainSections(NeoAgentController controller) {
231
418
  return <AppSection>[
232
419
  AppSection.chat,
233
420
  AppSection.recordings,
234
- AppSection.runs,
235
421
  AppSection.devices,
236
422
  AppSection.tasks,
237
423
  AppSection.widgets,
@@ -239,6 +425,7 @@ List<AppSection> _mainSections(NeoAgentController controller) {
239
425
  AppSection.memory,
240
426
  if (controller.showHealthSection) AppSection.health,
241
427
  AppSection.settings,
428
+ AppSection.runs,
242
429
  AppSection.agents,
243
430
  AppSection.messaging,
244
431
  ];
@@ -263,11 +450,19 @@ List<Widget> _buildSidebarItems(
263
450
  continue;
264
451
  }
265
452
 
453
+ final defaultSection = sections.first;
454
+ final parentSection = group == SidebarGroup.settings
455
+ ? AppSection.settings
456
+ : defaultSection;
457
+ final childSections = group == SidebarGroup.settings
458
+ ? sections.where((section) => section != parentSection).toList()
459
+ : sections;
460
+ final hasChildren =
461
+ childSections.length > 1 ||
462
+ (group == SidebarGroup.settings && childSections.isNotEmpty);
266
463
  final active =
267
464
  selectedSidebarSection &&
268
- controller.selectedSection.sidebarSection.group == group;
269
- final defaultSection = sections.first;
270
- final hasChildren = sections.length > 1;
465
+ controller.selectedSection.sidebarSection == parentSection;
271
466
  final expanded = expandedGroup == group;
272
467
 
273
468
  widgets.add(
@@ -279,12 +474,14 @@ List<Widget> _buildSidebarItems(
279
474
  ? Icon(
280
475
  expanded ? Icons.expand_less : Icons.expand_more,
281
476
  size: 16,
282
- color: active ? _accent : _textMuted,
477
+ color: active ? _textSecondary : _textMuted,
283
478
  )
284
479
  : null,
285
480
  onTap: hasChildren
286
- ? () => onToggleGroup(group)
287
- : () => onSelect(defaultSection),
481
+ ? group == SidebarGroup.settings
482
+ ? () => onSelect(parentSection)
483
+ : () => onToggleGroup(group)
484
+ : () => onSelect(parentSection),
288
485
  ),
289
486
  );
290
487
 
@@ -292,19 +489,23 @@ List<Widget> _buildSidebarItems(
292
489
  continue;
293
490
  }
294
491
 
295
- for (final section in sections) {
296
- widgets.add(
297
- _SidebarButton(
298
- label: section.label,
299
- icon: section.icon,
300
- active: controller.selectedSection.sidebarSection == section,
301
- indent: 18,
302
- iconSize: 16,
303
- fontSize: 12,
304
- onTap: () => onSelect(section),
305
- ),
306
- );
307
- }
492
+ widgets.add(
493
+ _SidebarSubnav(
494
+ children: childSections
495
+ .map(
496
+ (section) => _SidebarButton(
497
+ label: section.label,
498
+ icon: section.icon,
499
+ active: controller.selectedSection.sidebarSection == section,
500
+ iconSize: 16,
501
+ fontSize: 13,
502
+ compact: true,
503
+ onTap: () => onSelect(section),
504
+ ),
505
+ )
506
+ .toList(growable: false),
507
+ ),
508
+ );
308
509
  }
309
510
  return widgets;
310
511
  }
@@ -438,6 +639,53 @@ class _PageTitle extends StatelessWidget {
438
639
  }
439
640
  }
440
641
 
642
+ IconData _runPhaseIcon(String phase) {
643
+ switch (phase.toLowerCase()) {
644
+ case 'thinking':
645
+ return Icons.psychology_outlined;
646
+ case 'analyzing':
647
+ return Icons.analytics_outlined;
648
+ case 'planning':
649
+ return Icons.list_alt_outlined;
650
+ case 'verifying':
651
+ return Icons.fact_check_outlined;
652
+ case 'streaming':
653
+ case 'responding':
654
+ return Icons.chat_outlined;
655
+ case 'awaiting approval':
656
+ return Icons.lock_clock_outlined;
657
+ case 'incorporating steering':
658
+ return Icons.tune_outlined;
659
+ case 'completed':
660
+ return Icons.check_circle_outline_rounded;
661
+ case 'stopped':
662
+ case 'stopping':
663
+ return Icons.stop_circle_outlined;
664
+ default:
665
+ return Icons.sync_outlined;
666
+ }
667
+ }
668
+
669
+ Color _runPhaseColor(String phase) {
670
+ switch (phase.toLowerCase()) {
671
+ case 'thinking':
672
+ case 'analyzing':
673
+ case 'planning':
674
+ return _info;
675
+ case 'verifying':
676
+ return _accentAlt;
677
+ case 'awaiting approval':
678
+ return _warning;
679
+ case 'completed':
680
+ return _success;
681
+ case 'stopped':
682
+ case 'stopping':
683
+ return _textSecondary;
684
+ default:
685
+ return _accent;
686
+ }
687
+ }
688
+
441
689
  class _RunStatusPanel extends StatelessWidget {
442
690
  const _RunStatusPanel({required this.run, required this.tools});
443
691
 
@@ -449,6 +697,9 @@ class _RunStatusPanel extends StatelessWidget {
449
697
  final runningCount = tools.where((tool) => tool.status == 'running').length;
450
698
  final helperCount = tools.where((tool) => tool.isHelperRelated).length;
451
699
  final webCount = tools.where((tool) => tool.isWebRelated).length;
700
+ final phase = run?.phase ?? '';
701
+ final phaseColor = _runPhaseColor(phase);
702
+ final isDeepRun = (run?.iteration ?? 0) >= 8;
452
703
 
453
704
  return Card(
454
705
  child: Padding(
@@ -458,6 +709,30 @@ class _RunStatusPanel extends StatelessWidget {
458
709
  children: <Widget>[
459
710
  Row(
460
711
  children: <Widget>[
712
+ if (phase.isNotEmpty) ...<Widget>[
713
+ _PulseHalo(
714
+ color: phaseColor,
715
+ animate: phase.toLowerCase() != 'completed' &&
716
+ phase.toLowerCase() != 'stopped',
717
+ child: Container(
718
+ width: 30,
719
+ height: 30,
720
+ decoration: BoxDecoration(
721
+ color: phaseColor.withValues(alpha: 0.14),
722
+ shape: BoxShape.circle,
723
+ border: Border.all(
724
+ color: phaseColor.withValues(alpha: 0.28),
725
+ ),
726
+ ),
727
+ child: Icon(
728
+ _runPhaseIcon(phase),
729
+ size: 16,
730
+ color: phaseColor,
731
+ ),
732
+ ),
733
+ ),
734
+ const SizedBox(width: 12),
735
+ ],
461
736
  Expanded(
462
737
  child: Column(
463
738
  crossAxisAlignment: CrossAxisAlignment.start,
@@ -469,7 +744,7 @@ class _RunStatusPanel extends StatelessWidget {
469
744
  fontWeight: FontWeight.w700,
470
745
  ),
471
746
  ),
472
- const SizedBox(height: 6),
747
+ const SizedBox(height: 4),
473
748
  Text(
474
749
  run == null
475
750
  ? 'Waiting for run events...'
@@ -478,7 +753,12 @@ class _RunStatusPanel extends StatelessWidget {
478
753
  if (run!.pendingSteeringCount > 0)
479
754
  '${run!.pendingSteeringCount} steering ${run!.pendingSteeringCount == 1 ? 'update' : 'updates'} queued',
480
755
  ].join(' · '),
481
- style: TextStyle(color: _textSecondary),
756
+ style: TextStyle(
757
+ color: phase.isNotEmpty
758
+ ? phaseColor.withValues(alpha: 0.8)
759
+ : _textSecondary,
760
+ fontSize: 12.5,
761
+ ),
482
762
  ),
483
763
  ],
484
764
  ),
@@ -513,6 +793,12 @@ class _RunStatusPanel extends StatelessWidget {
513
793
  label: '$helperCount helpers',
514
794
  icon: Icons.account_tree_outlined,
515
795
  ),
796
+ if (isDeepRun)
797
+ _MetaPill(
798
+ label: 'deep run · step ${run!.iteration}',
799
+ icon: Icons.warning_amber_outlined,
800
+ color: _warning,
801
+ ),
516
802
  ],
517
803
  ),
518
804
  const SizedBox(height: 14),
@@ -548,6 +834,7 @@ class _ToolEventTimelineRow extends StatelessWidget {
548
834
  @override
549
835
  Widget build(BuildContext context) {
550
836
  Color color;
837
+ final running = tool.status == 'running';
551
838
  switch (tool.status) {
552
839
  case 'running':
553
840
  color = _warning;
@@ -565,14 +852,26 @@ class _ToolEventTimelineRow extends StatelessWidget {
565
852
  width: 28,
566
853
  child: Column(
567
854
  children: <Widget>[
568
- Container(
569
- width: 28,
570
- height: 28,
571
- decoration: BoxDecoration(
572
- color: color.withValues(alpha: 0.14),
573
- shape: BoxShape.circle,
855
+ _PulseHalo(
856
+ color: color,
857
+ animate: running,
858
+ child: Container(
859
+ width: 28,
860
+ height: 28,
861
+ decoration: BoxDecoration(
862
+ gradient: LinearGradient(
863
+ colors: <Color>[
864
+ color.withValues(alpha: running ? 0.26 : 0.16),
865
+ color.withValues(alpha: 0.08),
866
+ ],
867
+ begin: Alignment.topLeft,
868
+ end: Alignment.bottomRight,
869
+ ),
870
+ shape: BoxShape.circle,
871
+ border: Border.all(color: color.withValues(alpha: 0.24)),
872
+ ),
873
+ child: Icon(tool.laneIcon, size: 16, color: color),
574
874
  ),
575
- child: Icon(tool.laneIcon, size: 16, color: color),
576
875
  ),
577
876
  if (!isLast)
578
877
  Container(
@@ -592,9 +891,18 @@ class _ToolEventTimelineRow extends StatelessWidget {
592
891
  child: Container(
593
892
  padding: const EdgeInsets.all(12),
594
893
  decoration: BoxDecoration(
595
- color: _bgSecondary,
894
+ gradient: LinearGradient(
895
+ colors: <Color>[
896
+ _bgSecondary,
897
+ if (running) color.withValues(alpha: 0.055) else _bgCard,
898
+ ],
899
+ begin: Alignment.topLeft,
900
+ end: Alignment.bottomRight,
901
+ ),
596
902
  borderRadius: BorderRadius.circular(14),
597
- border: Border.all(color: _border),
903
+ border: Border.all(
904
+ color: running ? color.withValues(alpha: 0.25) : _border,
905
+ ),
598
906
  ),
599
907
  child: Column(
600
908
  crossAxisAlignment: CrossAxisAlignment.start,
@@ -770,36 +1078,127 @@ class _DotStatus extends StatelessWidget {
770
1078
 
771
1079
  @override
772
1080
  Widget build(BuildContext context) {
773
- return _GlassSurface(
774
- padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
775
- borderRadius: BorderRadius.circular(999),
776
- blurSigma: 16,
777
- fillColor: _bgSecondary.withValues(alpha: 0.28),
778
- borderColor: _glassBorder.withValues(alpha: 0.8),
779
- child: Row(
780
- mainAxisSize: MainAxisSize.min,
781
- children: <Widget>[
782
- Container(
783
- width: 8,
784
- height: 8,
785
- decoration: BoxDecoration(color: color, shape: BoxShape.circle),
786
- ),
787
- const SizedBox(width: 8),
788
- Text(label),
789
- ],
1081
+ return _PulseHalo(
1082
+ color: color,
1083
+ child: _GlassSurface(
1084
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
1085
+ borderRadius: BorderRadius.circular(999),
1086
+ blurSigma: 16,
1087
+ fillColor: _bgSecondary.withValues(alpha: 0.3),
1088
+ borderColor: color.withValues(alpha: 0.24),
1089
+ child: Row(
1090
+ mainAxisSize: MainAxisSize.min,
1091
+ children: <Widget>[
1092
+ Container(
1093
+ width: 8,
1094
+ height: 8,
1095
+ decoration: BoxDecoration(
1096
+ color: color,
1097
+ shape: BoxShape.circle,
1098
+ boxShadow: <BoxShadow>[
1099
+ BoxShadow(
1100
+ color: color.withValues(alpha: 0.42),
1101
+ blurRadius: 9,
1102
+ spreadRadius: 1,
1103
+ ),
1104
+ ],
1105
+ ),
1106
+ ),
1107
+ const SizedBox(width: 8),
1108
+ Text(label),
1109
+ ],
1110
+ ),
790
1111
  ),
791
1112
  );
792
1113
  }
793
1114
  }
794
1115
 
795
- class _SidebarButton extends StatelessWidget {
1116
+ class _PulseHalo extends StatefulWidget {
1117
+ const _PulseHalo({
1118
+ required this.child,
1119
+ required this.color,
1120
+ this.animate = true,
1121
+ });
1122
+
1123
+ final Widget child;
1124
+ final Color color;
1125
+ final bool animate;
1126
+
1127
+ @override
1128
+ State<_PulseHalo> createState() => _PulseHaloState();
1129
+ }
1130
+
1131
+ class _PulseHaloState extends State<_PulseHalo>
1132
+ with SingleTickerProviderStateMixin {
1133
+ late final AnimationController _controller;
1134
+
1135
+ @override
1136
+ void initState() {
1137
+ super.initState();
1138
+ _controller = AnimationController(
1139
+ vsync: this,
1140
+ duration: const Duration(milliseconds: 1600),
1141
+ );
1142
+ if (widget.animate) {
1143
+ _controller.repeat(reverse: true);
1144
+ }
1145
+ }
1146
+
1147
+ @override
1148
+ void didUpdateWidget(_PulseHalo oldWidget) {
1149
+ super.didUpdateWidget(oldWidget);
1150
+ if (widget.animate != oldWidget.animate) {
1151
+ if (widget.animate) {
1152
+ _controller.repeat(reverse: true);
1153
+ } else {
1154
+ _controller.stop();
1155
+ _controller.value = 0.0;
1156
+ }
1157
+ }
1158
+ }
1159
+
1160
+ @override
1161
+ void dispose() {
1162
+ _controller.dispose();
1163
+ super.dispose();
1164
+ }
1165
+
1166
+ @override
1167
+ Widget build(BuildContext context) {
1168
+ if (!widget.animate) {
1169
+ return widget.child;
1170
+ }
1171
+ return AnimatedBuilder(
1172
+ animation: _controller,
1173
+ builder: (context, child) {
1174
+ final t = Curves.easeInOut.transform(_controller.value);
1175
+ return DecoratedBox(
1176
+ decoration: BoxDecoration(
1177
+ borderRadius: BorderRadius.circular(999),
1178
+ boxShadow: <BoxShadow>[
1179
+ BoxShadow(
1180
+ color: widget.color.withValues(alpha: 0.04 + t * 0.08),
1181
+ blurRadius: 12 + t * 10,
1182
+ spreadRadius: t * 1.5,
1183
+ ),
1184
+ ],
1185
+ ),
1186
+ child: child,
1187
+ );
1188
+ },
1189
+ child: widget.child,
1190
+ );
1191
+ }
1192
+ }
1193
+
1194
+ class _SidebarButton extends StatefulWidget {
796
1195
  const _SidebarButton({
797
1196
  required this.label,
798
1197
  required this.icon,
799
1198
  this.active = false,
800
- this.indent = 0,
801
1199
  this.iconSize = 18,
802
1200
  this.fontSize = 13,
1201
+ this.compact = false,
803
1202
  this.trailing,
804
1203
  required this.onTap,
805
1204
  });
@@ -807,89 +1206,108 @@ class _SidebarButton extends StatelessWidget {
807
1206
  final String label;
808
1207
  final IconData icon;
809
1208
  final bool active;
810
- final double indent;
811
1209
  final double iconSize;
812
1210
  final double fontSize;
1211
+ final bool compact;
813
1212
  final Widget? trailing;
814
1213
  final VoidCallback? onTap;
815
1214
 
1215
+ @override
1216
+ State<_SidebarButton> createState() => _SidebarButtonState();
1217
+ }
1218
+
1219
+ class _SidebarButtonState extends State<_SidebarButton> {
1220
+ bool _hovering = false;
1221
+
816
1222
  @override
817
1223
  Widget build(BuildContext context) {
1224
+ final active = widget.active;
1225
+ final BoxDecoration decoration = active
1226
+ ? BoxDecoration(
1227
+ color: _bgCard,
1228
+ borderRadius: BorderRadius.circular(9),
1229
+ border: Border.all(color: _border),
1230
+ boxShadow: <BoxShadow>[
1231
+ BoxShadow(
1232
+ color: Colors.black.withValues(alpha: 0.05),
1233
+ blurRadius: 10,
1234
+ offset: const Offset(0, 2),
1235
+ ),
1236
+ ],
1237
+ )
1238
+ : BoxDecoration(
1239
+ color: _hovering
1240
+ ? _bgTertiary.withValues(alpha: 0.72)
1241
+ : Colors.transparent,
1242
+ borderRadius: BorderRadius.circular(9),
1243
+ );
818
1244
  return Padding(
819
- padding: const EdgeInsets.only(bottom: 6),
820
- child: AnimatedScale(
821
- duration: const Duration(milliseconds: 220),
822
- curve: Curves.easeOutCubic,
823
- scale: active ? 1.01 : 1,
824
- child: _GlassSurface(
825
- borderRadius: BorderRadius.circular(18),
826
- blurSigma: 18,
827
- fillColor: active
828
- ? _accentMuted.withValues(alpha: 0.32)
829
- : _bgCard.withValues(alpha: 0.2),
830
- borderColor: active
831
- ? _accent.withValues(alpha: 0.32)
832
- : Colors.white.withValues(alpha: 0.03),
833
- boxShadow: active
834
- ? <BoxShadow>[
835
- BoxShadow(
836
- color: _accent.withValues(alpha: 0.12),
837
- blurRadius: 22,
838
- offset: const Offset(0, 8),
839
- ),
840
- ]
841
- : null,
842
- child: Material(
843
- color: Colors.transparent,
844
- child: InkWell(
845
- borderRadius: BorderRadius.circular(18),
846
- onTap: onTap,
847
- child: Container(
848
- width: double.infinity,
849
- padding: EdgeInsets.fromLTRB(12 + indent, 12, 12, 12),
850
- child: Row(
851
- children: <Widget>[
852
- if (active)
853
- Container(
854
- width: 6,
855
- height: 26,
856
- margin: const EdgeInsets.only(right: 10),
1245
+ padding: const EdgeInsets.only(bottom: 2),
1246
+ child: MouseRegion(
1247
+ onEnter: (_) => setState(() => _hovering = true),
1248
+ onExit: (_) => setState(() => _hovering = false),
1249
+ child: Material(
1250
+ color: Colors.transparent,
1251
+ child: InkWell(
1252
+ borderRadius: BorderRadius.circular(9),
1253
+ onTap: widget.onTap,
1254
+ child: AnimatedContainer(
1255
+ duration: const Duration(milliseconds: 180),
1256
+ curve: Curves.easeOutCubic,
1257
+ width: double.infinity,
1258
+ padding: EdgeInsets.fromLTRB(
1259
+ 12,
1260
+ widget.compact ? 8 : 10,
1261
+ 12,
1262
+ widget.compact ? 8 : 10,
1263
+ ),
1264
+ decoration: decoration,
1265
+ child: Stack(
1266
+ alignment: Alignment.centerLeft,
1267
+ children: <Widget>[
1268
+ if (active)
1269
+ Positioned(
1270
+ left: -12,
1271
+ child: Container(
1272
+ width: 3,
1273
+ height: 18,
857
1274
  decoration: BoxDecoration(
1275
+ borderRadius: BorderRadius.circular(999),
858
1276
  gradient: LinearGradient(
859
- colors: <Color>[
860
- _accentHover,
861
- _accentAlt.withValues(alpha: 0.9),
862
- ],
1277
+ colors: <Color>[_accentAlt, _accent],
863
1278
  begin: Alignment.topCenter,
864
1279
  end: Alignment.bottomCenter,
865
1280
  ),
866
- borderRadius: BorderRadius.circular(999),
867
1281
  ),
868
1282
  ),
869
- Icon(
870
- icon,
871
- size: iconSize,
872
- color: active ? _accentHover : _textSecondary,
873
1283
  ),
874
- const SizedBox(width: 10),
875
- Expanded(
876
- child: Text(
877
- label,
878
- style: TextStyle(
879
- fontSize: fontSize,
880
- fontWeight: active
881
- ? FontWeight.w700
882
- : FontWeight.w600,
883
- color: active ? _textPrimary : _textSecondary,
1284
+ Row(
1285
+ children: <Widget>[
1286
+ Icon(
1287
+ widget.icon,
1288
+ size: widget.iconSize,
1289
+ color: active ? _accent : _textMuted,
1290
+ ),
1291
+ const SizedBox(width: 11),
1292
+ Expanded(
1293
+ child: Text(
1294
+ widget.label,
1295
+ maxLines: 1,
1296
+ overflow: TextOverflow.ellipsis,
1297
+ style: GoogleFonts.geist(
1298
+ fontSize: widget.fontSize,
1299
+ fontWeight: FontWeight.w600,
1300
+ color: active ? _textPrimary : _textSecondary,
1301
+ ),
884
1302
  ),
885
1303
  ),
886
- ),
887
- if (trailing != null) ...<Widget>[
888
- const SizedBox(width: 8),
889
- trailing!,
1304
+ if (widget.trailing != null) ...<Widget>[
1305
+ const SizedBox(width: 8),
1306
+ widget.trailing!,
1307
+ ],
890
1308
  ],
891
- ],
892
- ),
1309
+ ),
1310
+ ],
893
1311
  ),
894
1312
  ),
895
1313
  ),
@@ -899,6 +1317,24 @@ class _SidebarButton extends StatelessWidget {
899
1317
  }
900
1318
  }
901
1319
 
1320
+ class _SidebarSubnav extends StatelessWidget {
1321
+ const _SidebarSubnav({required this.children});
1322
+
1323
+ final List<Widget> children;
1324
+
1325
+ @override
1326
+ Widget build(BuildContext context) {
1327
+ return Container(
1328
+ margin: const EdgeInsets.fromLTRB(14, 2, 0, 2),
1329
+ padding: const EdgeInsets.only(left: 12),
1330
+ decoration: BoxDecoration(
1331
+ border: Border(left: BorderSide(color: _border)),
1332
+ ),
1333
+ child: Column(children: children),
1334
+ );
1335
+ }
1336
+ }
1337
+
902
1338
  class _SidebarIconButton extends StatelessWidget {
903
1339
  const _SidebarIconButton({
904
1340
  required this.tooltip,
@@ -914,21 +1350,21 @@ class _SidebarIconButton extends StatelessWidget {
914
1350
  Widget build(BuildContext context) {
915
1351
  return Tooltip(
916
1352
  message: tooltip,
917
- child: _GlassSurface(
918
- borderRadius: BorderRadius.circular(999),
919
- blurSigma: 18,
920
- fillColor: _bgCard.withValues(alpha: 0.3),
921
- child: Material(
922
- color: Colors.transparent,
923
- shape: const CircleBorder(),
924
- child: InkWell(
925
- customBorder: const CircleBorder(),
926
- onTap: onTap,
927
- child: SizedBox(
928
- width: 38,
929
- height: 38,
930
- child: Icon(icon, size: 17, color: _textSecondary),
1353
+ child: Material(
1354
+ color: Colors.transparent,
1355
+ borderRadius: BorderRadius.circular(9),
1356
+ child: InkWell(
1357
+ borderRadius: BorderRadius.circular(9),
1358
+ onTap: onTap,
1359
+ child: Container(
1360
+ width: 32,
1361
+ height: 32,
1362
+ decoration: BoxDecoration(
1363
+ borderRadius: BorderRadius.circular(9),
1364
+ color: _bgSecondary.withValues(alpha: 0.01),
1365
+ border: Border.all(color: Colors.transparent),
931
1366
  ),
1367
+ child: Icon(icon, size: 17, color: _textSecondary),
932
1368
  ),
933
1369
  ),
934
1370
  ),
@@ -936,63 +1372,87 @@ class _SidebarIconButton extends StatelessWidget {
936
1372
  }
937
1373
  }
938
1374
 
939
- class _BlurOrb extends StatelessWidget {
940
- const _BlurOrb({required this.size, required this.color});
1375
+ class _LogoBadge extends StatefulWidget {
1376
+ const _LogoBadge({required this.size});
941
1377
 
942
1378
  final double size;
943
- final Color color;
944
1379
 
945
1380
  @override
946
- Widget build(BuildContext context) {
947
- return IgnorePointer(
948
- child: Container(
949
- width: size,
950
- height: size,
951
- decoration: BoxDecoration(
952
- shape: BoxShape.circle,
953
- boxShadow: <BoxShadow>[
954
- BoxShadow(
955
- color: color.withValues(alpha: 0.18),
956
- blurRadius: 120,
957
- spreadRadius: 30,
958
- ),
959
- ],
960
- ),
961
- ),
962
- );
963
- }
1381
+ State<_LogoBadge> createState() => _LogoBadgeState();
964
1382
  }
965
1383
 
966
- class _LogoBadge extends StatelessWidget {
967
- const _LogoBadge({required this.size});
1384
+ class _LogoBadgeState extends State<_LogoBadge> {
1385
+ int _tapCount = 0;
1386
+ Timer? _tapResetTimer;
1387
+ Timer? _partyResetTimer;
968
1388
 
969
- final double size;
1389
+ @override
1390
+ void dispose() {
1391
+ _tapResetTimer?.cancel();
1392
+ if (_partyResetTimer != null && _partyResetTimer!.isActive) {
1393
+ _partyResetTimer?.cancel();
1394
+ _partyModeEnabled.value = false;
1395
+ }
1396
+ super.dispose();
1397
+ }
1398
+
1399
+ void _handleTap() {
1400
+ _tapResetTimer?.cancel();
1401
+ _tapCount += 1;
1402
+ if (_tapCount >= 5) {
1403
+ _tapCount = 0;
1404
+ _partyModeEnabled.value = true;
1405
+ _partyResetTimer?.cancel();
1406
+ _partyResetTimer = Timer(const Duration(seconds: 7), () {
1407
+ _partyModeEnabled.value = false;
1408
+ });
1409
+ } else {
1410
+ _tapResetTimer = Timer(const Duration(milliseconds: 1250), () {
1411
+ _tapCount = 0;
1412
+ });
1413
+ }
1414
+ }
970
1415
 
971
1416
  @override
972
1417
  Widget build(BuildContext context) {
973
- return Container(
974
- width: size,
975
- height: size,
976
- decoration: BoxDecoration(
977
- gradient: LinearGradient(
978
- colors: <Color>[_brandAccent, _brandAccentAlt],
979
- begin: Alignment.topLeft,
980
- end: Alignment.bottomRight,
981
- ),
982
- borderRadius: BorderRadius.circular(size * 0.34),
983
- boxShadow: <BoxShadow>[
984
- BoxShadow(
985
- color: _brandAccent.withValues(alpha: 0.32),
986
- blurRadius: 36,
987
- offset: const Offset(0, 10),
1418
+ final isDark = Theme.of(context).brightness == Brightness.dark;
1419
+ return ValueListenableBuilder<bool>(
1420
+ valueListenable: _partyModeEnabled,
1421
+ builder: (context, partyMode, _) {
1422
+ return GestureDetector(
1423
+ onTap: _handleTap,
1424
+ child: AnimatedScale(
1425
+ duration: const Duration(milliseconds: 240),
1426
+ curve: Curves.easeOutBack,
1427
+ scale: partyMode ? 1.08 : 1,
1428
+ child: AnimatedContainer(
1429
+ duration: const Duration(milliseconds: 260),
1430
+ width: widget.size,
1431
+ height: widget.size,
1432
+ decoration: BoxDecoration(
1433
+ borderRadius: BorderRadius.circular(widget.size * 0.24),
1434
+ boxShadow: <BoxShadow>[
1435
+ BoxShadow(
1436
+ color: (partyMode ? _accentAlt : _accent).withValues(
1437
+ alpha: partyMode ? 0.34 : 0.16,
1438
+ ),
1439
+ blurRadius: partyMode ? 28 : 14,
1440
+ spreadRadius: partyMode ? 1 : 0,
1441
+ ),
1442
+ ],
1443
+ ),
1444
+ child: Image.asset(
1445
+ isDark
1446
+ ? 'assets/branding/app_icon_1024.png'
1447
+ : 'assets/branding/app_icon_light_1024.png',
1448
+ width: widget.size,
1449
+ height: widget.size,
1450
+ filterQuality: FilterQuality.high,
1451
+ ),
1452
+ ),
988
1453
  ),
989
- ],
990
- border: Border.all(color: Colors.white.withValues(alpha: 0.12)),
991
- ),
992
- child: Padding(
993
- padding: EdgeInsets.all(size * 0.18),
994
- child: CustomPaint(painter: _NeoAgentLogoPainter()),
995
- ),
1454
+ );
1455
+ },
996
1456
  );
997
1457
  }
998
1458
  }
@@ -1014,12 +1474,13 @@ class _BrandLockup extends StatelessWidget {
1014
1474
 
1015
1475
  @override
1016
1476
  Widget build(BuildContext context) {
1477
+ final titleColor = Theme.of(context).colorScheme.onSurface;
1017
1478
  final title = Text(
1018
- 'NeoOS',
1019
- style: GoogleFonts.spaceGrotesk(
1479
+ 'NeoAgent',
1480
+ style: GoogleFonts.geist(
1020
1481
  fontSize: titleFontSize,
1021
1482
  fontWeight: FontWeight.w700,
1022
- color: _textPrimary,
1483
+ color: titleColor,
1023
1484
  letterSpacing: -0.4,
1024
1485
  ),
1025
1486
  );
@@ -1047,44 +1508,6 @@ class _BrandLockup extends StatelessWidget {
1047
1508
  }
1048
1509
  }
1049
1510
 
1050
- class _NeoAgentLogoPainter extends CustomPainter {
1051
- @override
1052
- void paint(Canvas canvas, Size size) {
1053
- final fillPaint = Paint()
1054
- ..color = Colors.white
1055
- ..style = PaintingStyle.fill;
1056
- final strokePaint = Paint()
1057
- ..color = Colors.white
1058
- ..style = PaintingStyle.stroke
1059
- ..strokeWidth = size.width * 0.08
1060
- ..strokeCap = StrokeCap.round
1061
- ..strokeJoin = StrokeJoin.round;
1062
-
1063
- final top = Path()
1064
- ..moveTo(size.width * 0.5, size.height * 0.08)
1065
- ..lineTo(size.width * 0.1, size.height * 0.3)
1066
- ..lineTo(size.width * 0.5, size.height * 0.52)
1067
- ..lineTo(size.width * 0.9, size.height * 0.3)
1068
- ..close();
1069
- canvas.drawPath(top, fillPaint);
1070
-
1071
- final middle = Path()
1072
- ..moveTo(size.width * 0.1, size.height * 0.52)
1073
- ..lineTo(size.width * 0.5, size.height * 0.74)
1074
- ..lineTo(size.width * 0.9, size.height * 0.52);
1075
- canvas.drawPath(middle, strokePaint);
1076
-
1077
- final bottom = Path()
1078
- ..moveTo(size.width * 0.1, size.height * 0.72)
1079
- ..lineTo(size.width * 0.5, size.height * 0.94)
1080
- ..lineTo(size.width * 0.9, size.height * 0.72);
1081
- canvas.drawPath(bottom, strokePaint);
1082
- }
1083
-
1084
- @override
1085
- bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
1086
- }
1087
-
1088
1511
  class _EmptyState extends StatelessWidget {
1089
1512
  const _EmptyState({required this.title, required this.subtitle});
1090
1513
 
@@ -1096,7 +1519,27 @@ class _EmptyState extends StatelessWidget {
1096
1519
  return Column(
1097
1520
  mainAxisSize: MainAxisSize.min,
1098
1521
  children: <Widget>[
1099
- const _LogoBadge(size: 52),
1522
+ Stack(
1523
+ alignment: Alignment.center,
1524
+ children: <Widget>[
1525
+ Container(
1526
+ width: 74,
1527
+ height: 74,
1528
+ decoration: BoxDecoration(
1529
+ shape: BoxShape.circle,
1530
+ gradient: SweepGradient(
1531
+ colors: <Color>[
1532
+ _accent.withValues(alpha: 0.18),
1533
+ _accentAlt.withValues(alpha: 0.28),
1534
+ Colors.transparent,
1535
+ _accent.withValues(alpha: 0.18),
1536
+ ],
1537
+ ),
1538
+ ),
1539
+ ),
1540
+ const _LogoBadge(size: 52),
1541
+ ],
1542
+ ),
1100
1543
  const SizedBox(height: 12),
1101
1544
  Text(
1102
1545
  title,
@@ -1121,10 +1564,15 @@ class _EmptyState extends StatelessWidget {
1121
1564
  }
1122
1565
 
1123
1566
  class _ChatBubble extends StatelessWidget {
1124
- const _ChatBubble({required this.entry, this.onLoadRunDetail});
1567
+ const _ChatBubble({
1568
+ required this.entry,
1569
+ this.onLoadRunDetail,
1570
+ this.onSendMessage,
1571
+ });
1125
1572
 
1126
1573
  final ChatEntry entry;
1127
1574
  final Future<RunDetailSnapshot> Function(String runId)? onLoadRunDetail;
1575
+ final void Function(String)? onSendMessage;
1128
1576
 
1129
1577
  @override
1130
1578
  Widget build(BuildContext context) {
@@ -1155,162 +1603,189 @@ class _ChatBubble extends StatelessWidget {
1155
1603
  const SizedBox(width: 12),
1156
1604
  ],
1157
1605
  Flexible(
1158
- child: Container(
1159
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 11),
1160
- decoration: BoxDecoration(
1161
- color: isUser ? _accent : _bgCard,
1162
- borderRadius: BorderRadius.only(
1163
- topLeft: const Radius.circular(14),
1164
- topRight: const Radius.circular(14),
1165
- bottomLeft: Radius.circular(isUser ? 14 : 4),
1166
- bottomRight: Radius.circular(isUser ? 4 : 14),
1606
+ child: ConstrainedBox(
1607
+ constraints: const BoxConstraints(maxWidth: 672),
1608
+ child: AnimatedContainer(
1609
+ duration: const Duration(milliseconds: 220),
1610
+ curve: Curves.easeOutCubic,
1611
+ padding: const EdgeInsets.symmetric(
1612
+ horizontal: 20,
1613
+ vertical: 16,
1167
1614
  ),
1168
- border: isUser ? null : Border.all(color: _border),
1169
- boxShadow: isUser
1170
- ? <BoxShadow>[
1171
- BoxShadow(
1172
- color: _accentAlt.withValues(alpha: 0.30),
1173
- blurRadius: 12,
1174
- offset: const Offset(0, 2),
1175
- ),
1176
- ]
1177
- : null,
1178
- ),
1179
- child: Column(
1180
- crossAxisAlignment: isUser
1181
- ? CrossAxisAlignment.end
1182
- : CrossAxisAlignment.start,
1183
- children: <Widget>[
1184
- if (!isUser && entry.platformTag != null)
1185
- Padding(
1186
- padding: const EdgeInsets.only(bottom: 8),
1187
- child: _StatusPill(
1188
- label: entry.platformTag!,
1189
- color: entry.platform == 'live' ? _info : _warning,
1190
- ),
1615
+ decoration: BoxDecoration(
1616
+ gradient: isUser
1617
+ ? LinearGradient(
1618
+ colors: <Color>[
1619
+ _accentAlt,
1620
+ Color.lerp(_accent, _accentAlt, 0.28)!,
1621
+ ],
1622
+ begin: Alignment.topLeft,
1623
+ end: Alignment.bottomRight,
1624
+ )
1625
+ : null,
1626
+ color: isUser ? null : _bgCard,
1627
+ borderRadius: BorderRadius.only(
1628
+ topLeft: Radius.circular(isUser ? 21 : 8),
1629
+ topRight: Radius.circular(isUser ? 8 : 21),
1630
+ bottomLeft: const Radius.circular(21),
1631
+ bottomRight: const Radius.circular(21),
1632
+ ),
1633
+ border: Border.all(
1634
+ color: isUser ? Colors.transparent : _border,
1635
+ ),
1636
+ boxShadow: <BoxShadow>[
1637
+ BoxShadow(
1638
+ color: Colors.black.withValues(alpha: 0.05),
1639
+ blurRadius: 10,
1640
+ offset: const Offset(0, 2),
1191
1641
  ),
1192
- MarkdownBody(
1193
- data: entry.content,
1194
- selectable: false,
1195
- styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context))
1196
- .copyWith(
1197
- p: Theme.of(context).textTheme.bodyMedium?.copyWith(
1198
- color: isUser ? Colors.white : _textPrimary,
1199
- height: 1.65,
1200
- ),
1201
- code: Theme.of(context).textTheme.bodyMedium
1202
- ?.copyWith(
1203
- fontFamily:
1204
- GoogleFonts.jetBrainsMono().fontFamily,
1205
- backgroundColor: _bgPrimary,
1206
- color: isUser ? Colors.white : _textPrimary,
1207
- ),
1208
- blockquoteDecoration: BoxDecoration(
1209
- borderRadius: BorderRadius.circular(14),
1210
- color: const Color(0x22000000),
1211
- ),
1642
+ ],
1643
+ ),
1644
+ child: Column(
1645
+ crossAxisAlignment: isUser
1646
+ ? CrossAxisAlignment.end
1647
+ : CrossAxisAlignment.start,
1648
+ children: <Widget>[
1649
+ if (!isUser && entry.platformTag != null)
1650
+ Padding(
1651
+ padding: const EdgeInsets.only(bottom: 8),
1652
+ child: _StatusPill(
1653
+ label: entry.platformTag!,
1654
+ color: entry.platform == 'live' ? _info : _warning,
1212
1655
  ),
1213
- ),
1214
- if (sharedAttachments.isNotEmpty) ...<Widget>[
1215
- const SizedBox(height: 10),
1216
- Wrap(
1217
- spacing: 8,
1218
- runSpacing: 8,
1219
- children: sharedAttachments
1220
- .map((attachment) {
1221
- final icon =
1222
- attachment.mimeType.toLowerCase().startsWith(
1223
- 'video/',
1224
- )
1225
- ? Icons.videocam_outlined
1226
- : attachment.mimeType.toLowerCase().startsWith(
1227
- 'image/',
1228
- )
1229
- ? Icons.image_outlined
1230
- : attachment.mimeType.toLowerCase().startsWith(
1231
- 'audio/',
1232
- )
1233
- ? Icons.audiotrack_outlined
1234
- : Icons.attach_file_rounded;
1235
- return Container(
1236
- padding: const EdgeInsets.symmetric(
1237
- horizontal: 10,
1238
- vertical: 7,
1239
- ),
1240
- decoration: BoxDecoration(
1241
- color: isUser
1242
- ? const Color(0x1FFFFFFF)
1243
- : _bgSecondary,
1244
- borderRadius: BorderRadius.circular(999),
1245
- border: Border.all(
1246
- color: isUser
1247
- ? const Color(0x40FFFFFF)
1248
- : _border,
1656
+ ),
1657
+ MarkdownBody(
1658
+ data: entry.content,
1659
+ selectable: false,
1660
+ styleSheet:
1661
+ MarkdownStyleSheet.fromTheme(
1662
+ Theme.of(context),
1663
+ ).copyWith(
1664
+ p: Theme.of(context).textTheme.bodyMedium?.copyWith(
1665
+ color: isUser ? Colors.white : _textPrimary,
1666
+ fontSize: 14.5,
1667
+ height: 1.62,
1668
+ ),
1669
+ code: Theme.of(context).textTheme.bodyMedium
1670
+ ?.copyWith(
1671
+ fontFamily:
1672
+ GoogleFonts.geistMono().fontFamily,
1673
+ backgroundColor: _bgPrimary,
1674
+ color: isUser ? Colors.white : _textPrimary,
1249
1675
  ),
1250
- ),
1251
- child: Row(
1252
- mainAxisSize: MainAxisSize.min,
1253
- children: <Widget>[
1254
- Icon(
1255
- icon,
1256
- size: 14,
1257
- color: isUser
1258
- ? Colors.white
1259
- : _textSecondary,
1260
- ),
1261
- const SizedBox(width: 6),
1262
- ConstrainedBox(
1263
- constraints: const BoxConstraints(
1264
- maxWidth: 180,
1265
- ),
1266
- child: Text(
1267
- attachment.name,
1268
- maxLines: 1,
1269
- overflow: TextOverflow.ellipsis,
1270
- style: TextStyle(
1271
- color: isUser
1272
- ? Colors.white
1273
- : _textPrimary,
1274
- fontSize: 12,
1275
- ),
1276
- ),
1277
- ),
1278
- ],
1279
- ),
1280
- );
1281
- })
1282
- .toList(growable: false),
1676
+ blockquoteDecoration: BoxDecoration(
1677
+ borderRadius: BorderRadius.circular(14),
1678
+ color: const Color(0x22000000),
1679
+ ),
1680
+ ),
1283
1681
  ),
1284
- ],
1285
- if (!isUser &&
1286
- entry.runId?.trim().isNotEmpty == true) ...<Widget>[
1287
- const SizedBox(height: 12),
1288
- _MessageRunPreview(
1289
- runId: entry.runId!.trim(),
1290
- onLoadRunDetail: onLoadRunDetail,
1682
+ if (sharedAttachments.isNotEmpty) ...<Widget>[
1683
+ const SizedBox(height: 10),
1684
+ _SharedAttachmentChipWrap(
1685
+ attachments: sharedAttachments,
1686
+ isUser: isUser,
1687
+ ),
1688
+ ],
1689
+ if (!isUser &&
1690
+ entry.runId?.trim().isNotEmpty == true) ...<Widget>[
1691
+ const SizedBox(height: 12),
1692
+ _MessageRunPreview(
1693
+ runId: entry.runId!.trim(),
1694
+ onLoadRunDetail: onLoadRunDetail,
1695
+ ),
1696
+ ],
1697
+ if (!isUser &&
1698
+ entry.richPayload != null &&
1699
+ onSendMessage != null) ...<Widget>[
1700
+ const SizedBox(height: 12),
1701
+ _QuickReplyBar(
1702
+ payload: entry.richPayload!,
1703
+ onSelected: onSendMessage!,
1704
+ ),
1705
+ ],
1706
+ const SizedBox(height: 10),
1707
+ Text(
1708
+ entry.createdAtLabel,
1709
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
1710
+ color: isUser
1711
+ ? const Color(0xCCFFFFFF)
1712
+ : _textSecondary,
1713
+ ),
1291
1714
  ),
1292
1715
  ],
1293
- const SizedBox(height: 10),
1294
- Text(
1295
- entry.createdAtLabel,
1296
- style: Theme.of(context).textTheme.bodySmall?.copyWith(
1297
- color: isUser ? const Color(0xCCFFFFFF) : _textSecondary,
1298
- ),
1299
- ),
1300
- ],
1716
+ ),
1301
1717
  ),
1302
1718
  ),
1303
1719
  ),
1304
- if (isUser) ...<Widget>[
1305
- const SizedBox(width: 12),
1306
- const _MessageAvatar(assistant: false),
1307
- ],
1308
1720
  ],
1309
1721
  ),
1310
1722
  );
1311
1723
  }
1312
1724
  }
1313
1725
 
1726
+ class _SharedAttachmentChipWrap extends StatelessWidget {
1727
+ const _SharedAttachmentChipWrap({
1728
+ required this.attachments,
1729
+ required this.isUser,
1730
+ });
1731
+
1732
+ final List<SharedChatAttachment> attachments;
1733
+ final bool isUser;
1734
+
1735
+ @override
1736
+ Widget build(BuildContext context) {
1737
+ return Wrap(
1738
+ spacing: 8,
1739
+ runSpacing: 8,
1740
+ children: attachments
1741
+ .map((attachment) {
1742
+ final mimeType = attachment.mimeType.toLowerCase();
1743
+ final icon = mimeType.startsWith('video/')
1744
+ ? Icons.videocam_outlined
1745
+ : mimeType.startsWith('image/')
1746
+ ? Icons.image_outlined
1747
+ : mimeType.startsWith('audio/')
1748
+ ? Icons.audiotrack_outlined
1749
+ : Icons.attach_file_rounded;
1750
+ return Container(
1751
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
1752
+ decoration: BoxDecoration(
1753
+ color: isUser ? const Color(0x1FFFFFFF) : _bgSecondary,
1754
+ borderRadius: BorderRadius.circular(999),
1755
+ border: Border.all(
1756
+ color: isUser ? const Color(0x40FFFFFF) : _border,
1757
+ ),
1758
+ ),
1759
+ child: Row(
1760
+ mainAxisSize: MainAxisSize.min,
1761
+ children: <Widget>[
1762
+ Icon(
1763
+ icon,
1764
+ size: 14,
1765
+ color: isUser ? Colors.white : _textSecondary,
1766
+ ),
1767
+ const SizedBox(width: 6),
1768
+ ConstrainedBox(
1769
+ constraints: const BoxConstraints(maxWidth: 180),
1770
+ child: Text(
1771
+ attachment.name,
1772
+ maxLines: 1,
1773
+ overflow: TextOverflow.ellipsis,
1774
+ style: TextStyle(
1775
+ color: isUser ? Colors.white : _textPrimary,
1776
+ fontSize: 12,
1777
+ ),
1778
+ ),
1779
+ ),
1780
+ ],
1781
+ ),
1782
+ );
1783
+ })
1784
+ .toList(growable: false),
1785
+ );
1786
+ }
1787
+ }
1788
+
1314
1789
  class _MessageRunPreview extends StatefulWidget {
1315
1790
  const _MessageRunPreview({
1316
1791
  required this.runId,
@@ -1557,29 +2032,22 @@ class _MessageAvatar extends StatelessWidget {
1557
2032
  @override
1558
2033
  Widget build(BuildContext context) {
1559
2034
  return Container(
1560
- width: 30,
1561
- height: 30,
2035
+ width: assistant ? 34 : 30,
2036
+ height: assistant ? 34 : 30,
1562
2037
  decoration: BoxDecoration(
1563
- borderRadius: BorderRadius.circular(8),
1564
- gradient: assistant
1565
- ? LinearGradient(colors: <Color>[_accent, _accentAlt])
1566
- : null,
2038
+ borderRadius: BorderRadius.circular(assistant ? 10 : 999),
1567
2039
  color: assistant ? null : _bgTertiary,
1568
- boxShadow: assistant
1569
- ? <BoxShadow>[
1570
- BoxShadow(
1571
- color: _accentAlt.withValues(alpha: 0.35),
1572
- blurRadius: 10,
1573
- offset: const Offset(0, 2),
1574
- ),
1575
- ]
1576
- : null,
1577
- ),
1578
- child: Icon(
1579
- assistant ? Icons.auto_awesome : Icons.person,
1580
- size: 16,
1581
- color: assistant ? Colors.white : _textSecondary,
2040
+ border: assistant ? null : Border.all(color: _borderLight),
1582
2041
  ),
2042
+ clipBehavior: Clip.antiAlias,
2043
+ child: assistant
2044
+ ? Image.asset(
2045
+ MediaQuery.platformBrightnessOf(context) == Brightness.dark
2046
+ ? 'assets/branding/app_icon_1024.png'
2047
+ : 'assets/branding/app_icon_light_1024.png',
2048
+ filterQuality: FilterQuality.high,
2049
+ )
2050
+ : Icon(Icons.person, size: 16, color: _textSecondary),
1583
2051
  );
1584
2052
  }
1585
2053
  }
@@ -1597,8 +2065,22 @@ class _StatusPill extends StatelessWidget {
1597
2065
  padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
1598
2066
  decoration: BoxDecoration(
1599
2067
  borderRadius: BorderRadius.circular(999),
1600
- color: color.withValues(alpha: 0.14),
1601
- border: Border.all(color: color.withValues(alpha: 0.18)),
2068
+ gradient: LinearGradient(
2069
+ colors: <Color>[
2070
+ color.withValues(alpha: 0.2),
2071
+ color.withValues(alpha: 0.08),
2072
+ ],
2073
+ begin: Alignment.topLeft,
2074
+ end: Alignment.bottomRight,
2075
+ ),
2076
+ border: Border.all(color: color.withValues(alpha: 0.28)),
2077
+ boxShadow: <BoxShadow>[
2078
+ BoxShadow(
2079
+ color: color.withValues(alpha: 0.08),
2080
+ blurRadius: 12,
2081
+ offset: const Offset(0, 4),
2082
+ ),
2083
+ ],
1602
2084
  ),
1603
2085
  child: Text(
1604
2086
  label,
@@ -3252,6 +3734,16 @@ int _asInt(dynamic value) {
3252
3734
  return int.tryParse(value?.toString() ?? '') ?? 0;
3253
3735
  }
3254
3736
 
3737
+ double _asDouble(dynamic value, {double fallback = 0}) {
3738
+ if (value is double) {
3739
+ return value;
3740
+ }
3741
+ if (value is int) {
3742
+ return value.toDouble();
3743
+ }
3744
+ return double.tryParse(value?.toString() ?? '') ?? fallback;
3745
+ }
3746
+
3255
3747
  DateTime _parseTimestamp(String? raw) {
3256
3748
  if (raw == null || raw.isEmpty) {
3257
3749
  return DateTime.now();
@@ -3364,6 +3856,42 @@ String _summarizeToolResult(dynamic raw) {
3364
3856
  return text.length > 140 ? '${text.substring(0, 140)}…' : text;
3365
3857
  }
3366
3858
 
3859
+ class _QuickReplyBar extends StatelessWidget {
3860
+ const _QuickReplyBar({required this.payload, required this.onSelected});
3861
+
3862
+ final ChatRichPayload payload;
3863
+ final void Function(String value) onSelected;
3864
+
3865
+ @override
3866
+ Widget build(BuildContext context) {
3867
+ return Wrap(
3868
+ spacing: 8,
3869
+ runSpacing: 8,
3870
+ children: payload.options.map((option) {
3871
+ return GestureDetector(
3872
+ onTap: () => onSelected(option.value),
3873
+ child: Container(
3874
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
3875
+ decoration: BoxDecoration(
3876
+ color: _accentMuted,
3877
+ borderRadius: BorderRadius.circular(999),
3878
+ border: Border.all(color: _accent.withValues(alpha: 0.4)),
3879
+ ),
3880
+ child: Text(
3881
+ option.label,
3882
+ style: TextStyle(
3883
+ fontSize: 13,
3884
+ color: _accent,
3885
+ fontWeight: FontWeight.w600,
3886
+ ),
3887
+ ),
3888
+ ),
3889
+ );
3890
+ }).toList(growable: false),
3891
+ );
3892
+ }
3893
+ }
3894
+
3367
3895
  String _titleCase(String value) {
3368
3896
  final normalized = value.trim();
3369
3897
  if (normalized.isEmpty) {