neoagent 3.0.1-beta.8 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -10
- package/README.md +12 -3
- package/docs/automation.md +37 -0
- package/docs/benchmarking.md +102 -0
- package/docs/billing.md +34 -8
- package/docs/configuration.md +11 -2
- package/docs/getting-started.md +10 -8
- package/docs/operations.md +1 -1
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +138 -0
- package/flutter_app/lib/main_app_shell.dart +38 -13
- package/flutter_app/lib/main_billing.dart +1465 -0
- package/flutter_app/lib/main_chat.dart +1612 -214
- package/flutter_app/lib/main_controller.dart +398 -26
- package/flutter_app/lib/main_devices.dart +293 -207
- package/flutter_app/lib/main_models.dart +142 -0
- package/flutter_app/lib/main_navigation.dart +19 -1
- package/flutter_app/lib/main_operations.dart +288 -9
- package/flutter_app/lib/main_settings.dart +510 -300
- package/flutter_app/lib/main_shared.dart +2 -0
- package/flutter_app/lib/main_timeline.dart +1378 -0
- package/flutter_app/lib/src/backend_client.dart +149 -19
- package/flutter_app/lib/src/desktop_companion_actions.dart +132 -21
- package/flutter_app/lib/src/desktop_companion_io.dart +65 -1
- package/flutter_app/lib/src/desktop_companion_stub.dart +12 -0
- package/flutter_app/lib/src/desktop_ocr_bridge.dart +2 -0
- package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +125 -0
- package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +30 -0
- package/flutter_app/lib/src/desktop_passive_history.dart +332 -0
- package/flutter_app/lib/src/recording_bridge_io.dart +80 -72
- package/flutter_app/lib/src/recording_bridge_web.dart +127 -114
- package/flutter_app/lib/src/recording_chunk_queue.dart +149 -0
- package/flutter_app/lib/src/recording_chunk_queue_io.dart +182 -0
- package/flutter_app/lib/src/recording_payloads.dart +9 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +25 -0
- package/flutter_app/windows/runner/flutter_window.cpp +75 -0
- package/landing/index.html +3 -3
- package/lib/manager.js +184 -66
- package/package.json +4 -1
- package/server/admin/access.js +12 -7
- package/server/admin/admin.css +78 -0
- package/server/admin/admin.js +436 -10
- package/server/admin/billing.js +165 -42
- package/server/admin/index.html +80 -5
- package/server/admin/users.js +15 -15
- package/server/db/database.js +125 -20
- package/server/http/routes.js +1 -0
- package/server/http/static.js +4 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/canvaskit/wimp.js.symbols +8475 -8467
- package/server/public/canvaskit/wimp.wasm +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +91077 -87037
- package/server/routes/account.js +53 -0
- package/server/routes/admin.js +345 -64
- package/server/routes/agents.js +203 -21
- package/server/routes/billing.js +5 -0
- package/server/routes/browser.js +8 -1
- package/server/routes/recordings.js +96 -6
- package/server/routes/screenHistory.js +140 -2
- package/server/routes/timeline.js +43 -0
- package/server/services/account/erasure.js +263 -0
- package/server/services/ai/hooks.js +4 -1
- package/server/services/ai/loop/agent_engine_core.js +8 -1
- package/server/services/ai/loop/blank_recovery.js +36 -0
- package/server/services/ai/loop/conversation_loop.js +166 -33
- package/server/services/ai/messagingFallback.js +22 -0
- package/server/services/ai/rate_limits.js +28 -5
- package/server/services/ai/systemPrompt.js +6 -5
- package/server/services/ai/taskAnalysis.js +2 -0
- package/server/services/ai/toolEvidence.js +15 -0
- package/server/services/ai/toolResult.js +40 -0
- package/server/services/ai/tools.js +163 -4
- package/server/services/android/controller.js +6 -2
- package/server/services/billing/plans.js +2 -1
- package/server/services/browser/anti_detection.js +192 -0
- package/server/services/browser/controller.js +180 -54
- package/server/services/desktop/auth.js +3 -0
- package/server/services/desktop/registry.js +50 -2
- package/server/services/integrations/google/calendar.js +22 -14
- package/server/services/manager.js +12 -42
- package/server/services/memory/ingestion_chunking.js +268 -0
- package/server/services/messaging/telnyx.js +9 -8
- package/server/services/recordings/manager.js +60 -27
- package/server/services/runtime/backends/local-vm.js +40 -22
- package/server/services/runtime/docker-vm-manager.js +157 -266
- package/server/services/runtime/guest_bootstrap.js +17 -5
- package/server/services/runtime/guest_image.js +188 -0
- package/server/services/runtime/manager.js +0 -1
- package/server/services/runtime/validation.js +3 -8
- package/server/services/social_video/service.js +60 -10
- package/server/services/tasks/runtime.js +234 -9
- package/server/services/tasks/schedule_utils.js +5 -5
- package/server/services/tasks/task_repository.js +13 -0
- package/server/services/timeline/service.js +558 -0
- package/server/services/wearable/gateway.js +1 -1
- package/server/services/websocket.js +21 -3
- package/server/services/desktop/screenRecorder.js +0 -292
- package/server/services/desktop/screen_recorder_support.js +0 -46
|
@@ -2302,6 +2302,96 @@ class RunSummary {
|
|
|
2302
2302
|
}
|
|
2303
2303
|
}
|
|
2304
2304
|
|
|
2305
|
+
class TimelineEventItem {
|
|
2306
|
+
const TimelineEventItem({
|
|
2307
|
+
required this.id,
|
|
2308
|
+
required this.sourceKind,
|
|
2309
|
+
required this.eventKind,
|
|
2310
|
+
required this.occurredAt,
|
|
2311
|
+
required this.title,
|
|
2312
|
+
required this.summary,
|
|
2313
|
+
required this.metadata,
|
|
2314
|
+
this.agentId,
|
|
2315
|
+
this.sourceId,
|
|
2316
|
+
});
|
|
2317
|
+
|
|
2318
|
+
factory TimelineEventItem.fromJson(Map<dynamic, dynamic> json) {
|
|
2319
|
+
return TimelineEventItem(
|
|
2320
|
+
id: _asInt(json['id']),
|
|
2321
|
+
sourceKind: json['sourceKind']?.toString() ?? '',
|
|
2322
|
+
eventKind: json['eventKind']?.toString() ?? '',
|
|
2323
|
+
occurredAt: _parseTimestamp(json['occurredAt']?.toString()),
|
|
2324
|
+
title: json['title']?.toString() ?? '',
|
|
2325
|
+
summary: json['summary']?.toString() ?? '',
|
|
2326
|
+
agentId: json['agentId']?.toString(),
|
|
2327
|
+
sourceId: json['sourceId']?.toString(),
|
|
2328
|
+
metadata: json['metadata'] is Map
|
|
2329
|
+
? Map<String, dynamic>.from(json['metadata'] as Map)
|
|
2330
|
+
: const <String, dynamic>{},
|
|
2331
|
+
);
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
final int id;
|
|
2335
|
+
final String sourceKind;
|
|
2336
|
+
final String eventKind;
|
|
2337
|
+
final DateTime occurredAt;
|
|
2338
|
+
final String title;
|
|
2339
|
+
final String summary;
|
|
2340
|
+
final String? agentId;
|
|
2341
|
+
final String? sourceId;
|
|
2342
|
+
final Map<String, dynamic> metadata;
|
|
2343
|
+
|
|
2344
|
+
String get occurredAtLabel => _formatTimestamp(occurredAt);
|
|
2345
|
+
|
|
2346
|
+
String get sourceLabel => switch (sourceKind) {
|
|
2347
|
+
'screen' => 'Screen',
|
|
2348
|
+
'tasks' => 'Task',
|
|
2349
|
+
'runs' => 'Run',
|
|
2350
|
+
_ => _titleCase(sourceKind.replaceAll('_', ' ')),
|
|
2351
|
+
};
|
|
2352
|
+
|
|
2353
|
+
Color get sourceColor => switch (sourceKind) {
|
|
2354
|
+
'screen' => _accent,
|
|
2355
|
+
'tasks' => _warning,
|
|
2356
|
+
'runs' => _success,
|
|
2357
|
+
_ => _textSecondary,
|
|
2358
|
+
};
|
|
2359
|
+
|
|
2360
|
+
String get appName => metadata['appName']?.toString() ?? '';
|
|
2361
|
+
String get windowTitle => metadata['windowTitle']?.toString() ?? '';
|
|
2362
|
+
String get deviceLabel =>
|
|
2363
|
+
metadata['deviceLabel']?.toString() ??
|
|
2364
|
+
metadata['deviceId']?.toString() ??
|
|
2365
|
+
'';
|
|
2366
|
+
String get previewText => metadata['previewText']?.toString() ?? summary;
|
|
2367
|
+
String get runId => metadata['runId']?.toString() ?? sourceId ?? '';
|
|
2368
|
+
String get taskName =>
|
|
2369
|
+
metadata['taskName']?.toString().trim().isNotEmpty == true
|
|
2370
|
+
? metadata['taskName'].toString()
|
|
2371
|
+
: title;
|
|
2372
|
+
DateTime? get startedAt =>
|
|
2373
|
+
_parseOptionalTimestamp(metadata['startedAt']?.toString());
|
|
2374
|
+
DateTime? get endedAt =>
|
|
2375
|
+
_parseOptionalTimestamp(metadata['endedAt']?.toString());
|
|
2376
|
+
|
|
2377
|
+
String get screenSpanLabel {
|
|
2378
|
+
final start = startedAt;
|
|
2379
|
+
final end = endedAt;
|
|
2380
|
+
if (start == null) {
|
|
2381
|
+
return occurredAtLabel;
|
|
2382
|
+
}
|
|
2383
|
+
final localStart = start.toLocal();
|
|
2384
|
+
final startMinute = localStart.minute.toString().padLeft(2, '0');
|
|
2385
|
+
if (end == null) {
|
|
2386
|
+
return '${localStart.hour.toString().padLeft(2, '0')}:$startMinute';
|
|
2387
|
+
}
|
|
2388
|
+
final localEnd = end.toLocal();
|
|
2389
|
+
final endMinute = localEnd.minute.toString().padLeft(2, '0');
|
|
2390
|
+
return '${localStart.hour.toString().padLeft(2, '0')}:$startMinute'
|
|
2391
|
+
' - ${localEnd.hour.toString().padLeft(2, '0')}:$endMinute';
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2305
2395
|
class TokenUsageSnapshot {
|
|
2306
2396
|
const TokenUsageSnapshot({
|
|
2307
2397
|
required this.totalTokens,
|
|
@@ -3147,6 +3237,8 @@ class TaskItem {
|
|
|
3147
3237
|
required this.triggerType,
|
|
3148
3238
|
required this.triggerSummary,
|
|
3149
3239
|
required this.triggerConfig,
|
|
3240
|
+
required this.taskConfig,
|
|
3241
|
+
required this.loopBudget,
|
|
3150
3242
|
required this.nextRun,
|
|
3151
3243
|
required this.prompt,
|
|
3152
3244
|
required this.model,
|
|
@@ -3178,6 +3270,11 @@ class TaskItem {
|
|
|
3178
3270
|
: const <String, dynamic>{}),
|
|
3179
3271
|
};
|
|
3180
3272
|
final triggerSummary = json['triggerSummary']?.toString() ?? '';
|
|
3273
|
+
final loopBudgetJson = json['loopBudget'] is Map
|
|
3274
|
+
? Map<String, dynamic>.from(json['loopBudget'] as Map)
|
|
3275
|
+
: (taskConfig['loopBudget'] is Map
|
|
3276
|
+
? Map<String, dynamic>.from(taskConfig['loopBudget'] as Map)
|
|
3277
|
+
: const <String, dynamic>{});
|
|
3181
3278
|
return TaskItem(
|
|
3182
3279
|
id: _asInt(json['id']),
|
|
3183
3280
|
agentId: json['agentId']?.toString() ?? json['agent_id']?.toString(),
|
|
@@ -3187,6 +3284,8 @@ class TaskItem {
|
|
|
3187
3284
|
? 'Task trigger'
|
|
3188
3285
|
: triggerSummary,
|
|
3189
3286
|
triggerConfig: triggerConfig,
|
|
3287
|
+
taskConfig: taskConfig,
|
|
3288
|
+
loopBudget: TaskLoopBudget.fromJson(loopBudgetJson),
|
|
3190
3289
|
nextRun: _parseOptionalTimestamp(json['nextRun']?.toString()),
|
|
3191
3290
|
prompt:
|
|
3192
3291
|
json['prompt']?.toString().ifEmpty(
|
|
@@ -3222,6 +3321,8 @@ class TaskItem {
|
|
|
3222
3321
|
final String triggerType;
|
|
3223
3322
|
final String triggerSummary;
|
|
3224
3323
|
final Map<String, dynamic> triggerConfig;
|
|
3324
|
+
final Map<String, dynamic> taskConfig;
|
|
3325
|
+
final TaskLoopBudget loopBudget;
|
|
3225
3326
|
final DateTime? nextRun;
|
|
3226
3327
|
final String prompt;
|
|
3227
3328
|
final String model;
|
|
@@ -3245,6 +3346,47 @@ class TaskItem {
|
|
|
3245
3346
|
bool get isWidgetRefresh => taskType == 'widget_refresh';
|
|
3246
3347
|
}
|
|
3247
3348
|
|
|
3349
|
+
class TaskLoopBudget {
|
|
3350
|
+
const TaskLoopBudget({
|
|
3351
|
+
required this.enabled,
|
|
3352
|
+
required this.paused,
|
|
3353
|
+
required this.maxRunsPerDay,
|
|
3354
|
+
required this.maxTokensPerDay,
|
|
3355
|
+
});
|
|
3356
|
+
|
|
3357
|
+
factory TaskLoopBudget.fromJson(Map<String, dynamic> json) {
|
|
3358
|
+
final maxRuns = _asInt(json['maxRunsPerDay'] ?? json['max_runs_per_day']);
|
|
3359
|
+
final maxTokens = _asInt(
|
|
3360
|
+
json['maxTokensPerDay'] ?? json['max_tokens_per_day'],
|
|
3361
|
+
);
|
|
3362
|
+
final enabled = json['enabled'] == false || json['enabled'] == 'false'
|
|
3363
|
+
? false
|
|
3364
|
+
: true;
|
|
3365
|
+
return TaskLoopBudget(
|
|
3366
|
+
enabled: enabled,
|
|
3367
|
+
paused:
|
|
3368
|
+
json['paused'] == true ||
|
|
3369
|
+
json['pause'] == true ||
|
|
3370
|
+
json['paused'] == 'true' ||
|
|
3371
|
+
json['pause'] == 'true',
|
|
3372
|
+
maxRunsPerDay: maxRuns > 0 ? maxRuns : 24,
|
|
3373
|
+
maxTokensPerDay: maxTokens > 0 ? maxTokens : 250000,
|
|
3374
|
+
);
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
final bool enabled;
|
|
3378
|
+
final bool paused;
|
|
3379
|
+
final int maxRunsPerDay;
|
|
3380
|
+
final int maxTokensPerDay;
|
|
3381
|
+
|
|
3382
|
+
Map<String, dynamic> toJson() => <String, dynamic>{
|
|
3383
|
+
'enabled': enabled,
|
|
3384
|
+
'paused': paused,
|
|
3385
|
+
'maxRunsPerDay': maxRunsPerDay,
|
|
3386
|
+
'maxTokensPerDay': maxTokensPerDay,
|
|
3387
|
+
};
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3248
3390
|
class WidgetSnapshotItem {
|
|
3249
3391
|
const WidgetSnapshotItem({
|
|
3250
3392
|
required this.id,
|
|
@@ -14,6 +14,7 @@ NeoAgentAppMode _appModeFromEnvironment() {
|
|
|
14
14
|
|
|
15
15
|
enum AppSection {
|
|
16
16
|
chat,
|
|
17
|
+
timeline,
|
|
17
18
|
voiceAssistant,
|
|
18
19
|
devices,
|
|
19
20
|
recordings,
|
|
@@ -30,15 +31,18 @@ enum AppSection {
|
|
|
30
31
|
mcp,
|
|
31
32
|
health,
|
|
32
33
|
server,
|
|
34
|
+
billing,
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
enum SidebarGroup { chat, recordings, automation, settings }
|
|
37
|
+
enum SidebarGroup { chat, timeline, recordings, automation, settings }
|
|
36
38
|
|
|
37
39
|
extension SidebarGroupX on SidebarGroup {
|
|
38
40
|
String get label {
|
|
39
41
|
switch (this) {
|
|
40
42
|
case SidebarGroup.chat:
|
|
41
43
|
return 'Chat';
|
|
44
|
+
case SidebarGroup.timeline:
|
|
45
|
+
return 'Timeline';
|
|
42
46
|
case SidebarGroup.recordings:
|
|
43
47
|
return 'Recordings';
|
|
44
48
|
case SidebarGroup.automation:
|
|
@@ -52,6 +56,8 @@ extension SidebarGroupX on SidebarGroup {
|
|
|
52
56
|
switch (this) {
|
|
53
57
|
case SidebarGroup.chat:
|
|
54
58
|
return Icons.chat_bubble_outline;
|
|
59
|
+
case SidebarGroup.timeline:
|
|
60
|
+
return Icons.timeline_rounded;
|
|
55
61
|
case SidebarGroup.recordings:
|
|
56
62
|
return Icons.fiber_smart_record_outlined;
|
|
57
63
|
case SidebarGroup.automation:
|
|
@@ -67,6 +73,8 @@ extension AppSectionX on AppSection {
|
|
|
67
73
|
switch (this) {
|
|
68
74
|
case AppSection.chat:
|
|
69
75
|
return 'Chat';
|
|
76
|
+
case AppSection.timeline:
|
|
77
|
+
return 'Timeline';
|
|
70
78
|
case AppSection.voiceAssistant:
|
|
71
79
|
return 'Voice assistant';
|
|
72
80
|
case AppSection.devices:
|
|
@@ -99,6 +107,8 @@ extension AppSectionX on AppSection {
|
|
|
99
107
|
return 'Health';
|
|
100
108
|
case AppSection.server:
|
|
101
109
|
return 'Server';
|
|
110
|
+
case AppSection.billing:
|
|
111
|
+
return 'Billing';
|
|
102
112
|
}
|
|
103
113
|
}
|
|
104
114
|
|
|
@@ -106,6 +116,8 @@ extension AppSectionX on AppSection {
|
|
|
106
116
|
switch (this) {
|
|
107
117
|
case AppSection.chat:
|
|
108
118
|
return Icons.chat_bubble_outline;
|
|
119
|
+
case AppSection.timeline:
|
|
120
|
+
return Icons.timeline_rounded;
|
|
109
121
|
case AppSection.voiceAssistant:
|
|
110
122
|
return Icons.keyboard_voice_outlined;
|
|
111
123
|
case AppSection.devices:
|
|
@@ -138,6 +150,8 @@ extension AppSectionX on AppSection {
|
|
|
138
150
|
return Icons.favorite_border;
|
|
139
151
|
case AppSection.server:
|
|
140
152
|
return Icons.dns_outlined;
|
|
153
|
+
case AppSection.billing:
|
|
154
|
+
return Icons.credit_card;
|
|
141
155
|
}
|
|
142
156
|
}
|
|
143
157
|
|
|
@@ -146,6 +160,8 @@ extension AppSectionX on AppSection {
|
|
|
146
160
|
case AppSection.chat:
|
|
147
161
|
case AppSection.voiceAssistant:
|
|
148
162
|
return SidebarGroup.chat;
|
|
163
|
+
case AppSection.timeline:
|
|
164
|
+
return SidebarGroup.timeline;
|
|
149
165
|
case AppSection.recordings:
|
|
150
166
|
return SidebarGroup.recordings;
|
|
151
167
|
case AppSection.devices:
|
|
@@ -163,6 +179,7 @@ extension AppSectionX on AppSection {
|
|
|
163
179
|
case AppSection.messaging:
|
|
164
180
|
case AppSection.agents:
|
|
165
181
|
case AppSection.server:
|
|
182
|
+
case AppSection.billing:
|
|
166
183
|
return SidebarGroup.settings;
|
|
167
184
|
}
|
|
168
185
|
}
|
|
@@ -193,6 +210,7 @@ extension AppSectionX on AppSection {
|
|
|
193
210
|
return effectiveSection.label;
|
|
194
211
|
}
|
|
195
212
|
if (effectiveSection.group == SidebarGroup.chat ||
|
|
213
|
+
effectiveSection.group == SidebarGroup.timeline ||
|
|
196
214
|
effectiveSection.group == SidebarGroup.recordings) {
|
|
197
215
|
return groupLabel;
|
|
198
216
|
}
|
|
@@ -503,17 +503,30 @@ class _SkillsPanelState extends State<SkillsPanel>
|
|
|
503
503
|
late final TabController _tabController;
|
|
504
504
|
String _selectedCategory = 'all';
|
|
505
505
|
|
|
506
|
+
// Installed tab search & filter state
|
|
507
|
+
String _installedQuery = '';
|
|
508
|
+
String _installedStatusFilter = 'all'; // 'all' | 'active' | 'draft' | 'disabled'
|
|
509
|
+
String _installedSourceFilter = 'all'; // 'all' | 'built-in' | 'learned' | 'user' | 'store'
|
|
510
|
+
late final TextEditingController _installedSearchController;
|
|
511
|
+
|
|
506
512
|
@override
|
|
507
513
|
void initState() {
|
|
508
514
|
super.initState();
|
|
509
515
|
_searchController = TextEditingController();
|
|
510
516
|
_tabController = TabController(length: 2, vsync: this);
|
|
517
|
+
_installedSearchController = TextEditingController();
|
|
518
|
+
_installedSearchController.addListener(() {
|
|
519
|
+
setState(() {
|
|
520
|
+
_installedQuery = _installedSearchController.text.trim().toLowerCase();
|
|
521
|
+
});
|
|
522
|
+
});
|
|
511
523
|
}
|
|
512
524
|
|
|
513
525
|
@override
|
|
514
526
|
void dispose() {
|
|
515
527
|
_tabController.dispose();
|
|
516
528
|
_searchController.dispose();
|
|
529
|
+
_installedSearchController.dispose();
|
|
517
530
|
super.dispose();
|
|
518
531
|
}
|
|
519
532
|
|
|
@@ -628,13 +641,122 @@ class _SkillsPanelState extends State<SkillsPanel>
|
|
|
628
641
|
);
|
|
629
642
|
}
|
|
630
643
|
|
|
644
|
+
final filteredSkills = controller.skills.where((skill) {
|
|
645
|
+
final q = _installedQuery;
|
|
646
|
+
if (q.isNotEmpty &&
|
|
647
|
+
!skill.name.toLowerCase().contains(q) &&
|
|
648
|
+
!skill.description.toLowerCase().contains(q)) return false;
|
|
649
|
+
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;
|
|
653
|
+
}
|
|
654
|
+
if (_installedSourceFilter != 'all' && skill.source != _installedSourceFilter) return false;
|
|
655
|
+
return true;
|
|
656
|
+
}).toList();
|
|
657
|
+
|
|
658
|
+
final statusFilters = <String>['all', 'active', 'draft', 'disabled'];
|
|
659
|
+
final sourceFilters = <String>['all', 'built-in', 'learned', 'user', 'store'];
|
|
660
|
+
|
|
631
661
|
return Card(
|
|
632
|
-
child:
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
662
|
+
child: Column(
|
|
663
|
+
children: <Widget>[
|
|
664
|
+
Padding(
|
|
665
|
+
padding: const EdgeInsets.fromLTRB(14, 14, 14, 0),
|
|
666
|
+
child: Column(
|
|
667
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
668
|
+
children: <Widget>[
|
|
669
|
+
TextField(
|
|
670
|
+
controller: _installedSearchController,
|
|
671
|
+
decoration: InputDecoration(
|
|
672
|
+
labelText: 'Search by name or description',
|
|
673
|
+
prefixIcon: Icon(Icons.search),
|
|
674
|
+
suffixIcon: _installedSearchController.text.isEmpty
|
|
675
|
+
? null
|
|
676
|
+
: IconButton(
|
|
677
|
+
onPressed: () {
|
|
678
|
+
_installedSearchController.clear();
|
|
679
|
+
},
|
|
680
|
+
icon: Icon(Icons.close),
|
|
681
|
+
),
|
|
682
|
+
),
|
|
683
|
+
),
|
|
684
|
+
const SizedBox(height: 10),
|
|
685
|
+
SizedBox(
|
|
686
|
+
height: 38,
|
|
687
|
+
child: ListView.separated(
|
|
688
|
+
scrollDirection: Axis.horizontal,
|
|
689
|
+
itemCount: statusFilters.length,
|
|
690
|
+
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
|
691
|
+
itemBuilder: (context, index) {
|
|
692
|
+
final filter = statusFilters[index];
|
|
693
|
+
final selected = filter == _installedStatusFilter;
|
|
694
|
+
return FilterChip(
|
|
695
|
+
selected: selected,
|
|
696
|
+
label: Text(
|
|
697
|
+
filter == 'all'
|
|
698
|
+
? 'All'
|
|
699
|
+
: filter[0].toUpperCase() + filter.substring(1),
|
|
700
|
+
),
|
|
701
|
+
selectedColor: _accentMuted,
|
|
702
|
+
checkmarkColor: _accent,
|
|
703
|
+
backgroundColor: _bgSecondary,
|
|
704
|
+
side: BorderSide(color: _border),
|
|
705
|
+
onSelected: (_) =>
|
|
706
|
+
setState(() => _installedStatusFilter = filter),
|
|
707
|
+
);
|
|
708
|
+
},
|
|
709
|
+
),
|
|
710
|
+
),
|
|
711
|
+
const SizedBox(height: 8),
|
|
712
|
+
SizedBox(
|
|
713
|
+
height: 38,
|
|
714
|
+
child: ListView.separated(
|
|
715
|
+
scrollDirection: Axis.horizontal,
|
|
716
|
+
itemCount: sourceFilters.length,
|
|
717
|
+
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
|
718
|
+
itemBuilder: (context, index) {
|
|
719
|
+
final filter = sourceFilters[index];
|
|
720
|
+
final selected = filter == _installedSourceFilter;
|
|
721
|
+
return FilterChip(
|
|
722
|
+
selected: selected,
|
|
723
|
+
label: Text(filter == 'all' ? 'All' : filter),
|
|
724
|
+
selectedColor: _accentMuted,
|
|
725
|
+
checkmarkColor: _accent,
|
|
726
|
+
backgroundColor: _bgSecondary,
|
|
727
|
+
side: BorderSide(color: _border),
|
|
728
|
+
onSelected: (_) =>
|
|
729
|
+
setState(() => _installedSourceFilter = filter),
|
|
730
|
+
);
|
|
731
|
+
},
|
|
732
|
+
),
|
|
733
|
+
),
|
|
734
|
+
const SizedBox(height: 8),
|
|
735
|
+
Text(
|
|
736
|
+
'${filteredSkills.length} skill${filteredSkills.length == 1 ? '' : 's'}',
|
|
737
|
+
style: TextStyle(color: _textSecondary),
|
|
738
|
+
),
|
|
739
|
+
const SizedBox(height: 8),
|
|
740
|
+
],
|
|
741
|
+
),
|
|
742
|
+
),
|
|
743
|
+
if (filteredSkills.isEmpty)
|
|
744
|
+
Expanded(
|
|
745
|
+
child: Center(
|
|
746
|
+
child: Text(
|
|
747
|
+
'No skills match your filters',
|
|
748
|
+
style: TextStyle(color: _textSecondary),
|
|
749
|
+
),
|
|
750
|
+
),
|
|
751
|
+
)
|
|
752
|
+
else
|
|
753
|
+
Expanded(
|
|
754
|
+
child: ListView.separated(
|
|
755
|
+
padding: const EdgeInsets.fromLTRB(14, 0, 14, 14),
|
|
756
|
+
itemCount: filteredSkills.length,
|
|
757
|
+
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
|
758
|
+
itemBuilder: (context, index) {
|
|
759
|
+
final skill = filteredSkills[index];
|
|
638
760
|
return LayoutBuilder(
|
|
639
761
|
builder: (context, constraints) {
|
|
640
762
|
final compact = constraints.maxWidth < 760;
|
|
@@ -780,9 +902,12 @@ class _SkillsPanelState extends State<SkillsPanel>
|
|
|
780
902
|
);
|
|
781
903
|
},
|
|
782
904
|
);
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
905
|
+
},
|
|
906
|
+
),
|
|
907
|
+
),
|
|
908
|
+
],
|
|
909
|
+
),
|
|
910
|
+
);
|
|
786
911
|
}
|
|
787
912
|
|
|
788
913
|
Widget _buildStoreTab(
|
|
@@ -5171,6 +5296,24 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
5171
5296
|
}
|
|
5172
5297
|
}
|
|
5173
5298
|
|
|
5299
|
+
String _compactBudgetNumber(int value) {
|
|
5300
|
+
if (value >= 1000000) {
|
|
5301
|
+
final millions = value / 1000000;
|
|
5302
|
+
return '${millions.toStringAsFixed(millions >= 10 ? 0 : 1)}M';
|
|
5303
|
+
}
|
|
5304
|
+
if (value >= 1000) {
|
|
5305
|
+
final thousands = value / 1000;
|
|
5306
|
+
return '${thousands.toStringAsFixed(thousands >= 10 ? 0 : 1)}k';
|
|
5307
|
+
}
|
|
5308
|
+
return '$value';
|
|
5309
|
+
}
|
|
5310
|
+
|
|
5311
|
+
String _taskBudgetLabel(TaskItem task) {
|
|
5312
|
+
final budget = task.loopBudget;
|
|
5313
|
+
return 'Budget: ${budget.maxRunsPerDay} runs/day · '
|
|
5314
|
+
'${_compactBudgetNumber(budget.maxTokensPerDay)} tokens/day';
|
|
5315
|
+
}
|
|
5316
|
+
|
|
5174
5317
|
Future<void> _showLastRun(TaskItem task) async {
|
|
5175
5318
|
final runId = task.lastRunId.trim();
|
|
5176
5319
|
if (runId.isEmpty) return;
|
|
@@ -5387,6 +5530,10 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
5387
5530
|
label: task.enabled ? 'Active' : 'Paused',
|
|
5388
5531
|
color: task.enabled ? _success : _textSecondary,
|
|
5389
5532
|
),
|
|
5533
|
+
if (task.loopBudget.paused) ...<Widget>[
|
|
5534
|
+
const SizedBox(width: 8),
|
|
5535
|
+
_StatusPill(label: 'Loop paused', color: _warning),
|
|
5536
|
+
],
|
|
5390
5537
|
if (task.hasLastRunStatus) ...<Widget>[
|
|
5391
5538
|
const SizedBox(width: 8),
|
|
5392
5539
|
_StatusPill(
|
|
@@ -5417,6 +5564,11 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
5417
5564
|
style: TextStyle(color: _textSecondary),
|
|
5418
5565
|
),
|
|
5419
5566
|
const SizedBox(height: 8),
|
|
5567
|
+
Text(
|
|
5568
|
+
_taskBudgetLabel(task),
|
|
5569
|
+
style: TextStyle(color: _textSecondary),
|
|
5570
|
+
),
|
|
5571
|
+
const SizedBox(height: 8),
|
|
5420
5572
|
Text(task.prompt, style: TextStyle(color: _textPrimary)),
|
|
5421
5573
|
if (task.lastRunLabel.isNotEmpty) ...<Widget>[
|
|
5422
5574
|
const SizedBox(height: 8),
|
|
@@ -5506,6 +5658,10 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
5506
5658
|
label: task.enabled ? 'Active' : 'Paused',
|
|
5507
5659
|
color: task.enabled ? _success : _textSecondary,
|
|
5508
5660
|
),
|
|
5661
|
+
if (task.loopBudget.paused) ...<Widget>[
|
|
5662
|
+
const SizedBox(width: 8),
|
|
5663
|
+
_StatusPill(label: 'Loop paused', color: _warning),
|
|
5664
|
+
],
|
|
5509
5665
|
if (task.hasLastRunStatus) ...<Widget>[
|
|
5510
5666
|
const SizedBox(width: 8),
|
|
5511
5667
|
_StatusPill(
|
|
@@ -5535,6 +5691,11 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
5535
5691
|
style: TextStyle(color: _textSecondary),
|
|
5536
5692
|
),
|
|
5537
5693
|
const SizedBox(height: 8),
|
|
5694
|
+
Text(
|
|
5695
|
+
_taskBudgetLabel(task),
|
|
5696
|
+
style: TextStyle(color: _textSecondary),
|
|
5697
|
+
),
|
|
5698
|
+
const SizedBox(height: 8),
|
|
5538
5699
|
Text(
|
|
5539
5700
|
linkedWidget.prompt,
|
|
5540
5701
|
style: TextStyle(color: _textPrimary),
|
|
@@ -5716,7 +5877,15 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
5716
5877
|
text: task?.triggerConfig['sender']?.toString() ?? '',
|
|
5717
5878
|
);
|
|
5718
5879
|
final promptController = TextEditingController(text: task?.prompt ?? '');
|
|
5880
|
+
final loopBudget = task?.loopBudget ?? TaskLoopBudget.fromJson(const {});
|
|
5881
|
+
final maxRunsController = TextEditingController(
|
|
5882
|
+
text: loopBudget.maxRunsPerDay.toString(),
|
|
5883
|
+
);
|
|
5884
|
+
final maxTokensController = TextEditingController(
|
|
5885
|
+
text: loopBudget.maxTokensPerDay.toString(),
|
|
5886
|
+
);
|
|
5719
5887
|
var enabled = task?.enabled ?? true;
|
|
5888
|
+
var loopPaused = loopBudget.paused;
|
|
5720
5889
|
var unreadOnly = task?.triggerConfig['unreadOnly'] == true;
|
|
5721
5890
|
var ignoreGroups = task?.triggerConfig['ignoreGroups'] == true;
|
|
5722
5891
|
var selectedModel = _ensureModelValue(
|
|
@@ -6019,6 +6188,88 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
6019
6188
|
() => selectedModel = value ?? 'auto',
|
|
6020
6189
|
),
|
|
6021
6190
|
),
|
|
6191
|
+
const SizedBox(height: 12),
|
|
6192
|
+
Container(
|
|
6193
|
+
width: double.infinity,
|
|
6194
|
+
padding: const EdgeInsets.all(14),
|
|
6195
|
+
decoration: BoxDecoration(
|
|
6196
|
+
border: Border.all(
|
|
6197
|
+
color: _border.withValues(alpha: 0.8),
|
|
6198
|
+
),
|
|
6199
|
+
borderRadius: BorderRadius.circular(12),
|
|
6200
|
+
),
|
|
6201
|
+
child: Column(
|
|
6202
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
6203
|
+
children: <Widget>[
|
|
6204
|
+
Row(
|
|
6205
|
+
children: <Widget>[
|
|
6206
|
+
Icon(
|
|
6207
|
+
Icons.speed_outlined,
|
|
6208
|
+
size: 18,
|
|
6209
|
+
color: _textSecondary,
|
|
6210
|
+
),
|
|
6211
|
+
const SizedBox(width: 8),
|
|
6212
|
+
Text(
|
|
6213
|
+
'Loop budget',
|
|
6214
|
+
style: TextStyle(
|
|
6215
|
+
fontWeight: FontWeight.w700,
|
|
6216
|
+
color: _textPrimary,
|
|
6217
|
+
),
|
|
6218
|
+
),
|
|
6219
|
+
],
|
|
6220
|
+
),
|
|
6221
|
+
const SizedBox(height: 12),
|
|
6222
|
+
LayoutBuilder(
|
|
6223
|
+
builder: (context, constraints) {
|
|
6224
|
+
final stacked = constraints.maxWidth < 520;
|
|
6225
|
+
final fields = <Widget>[
|
|
6226
|
+
TextField(
|
|
6227
|
+
controller: maxRunsController,
|
|
6228
|
+
keyboardType: TextInputType.number,
|
|
6229
|
+
decoration: const InputDecoration(
|
|
6230
|
+
labelText: 'Max runs per day',
|
|
6231
|
+
),
|
|
6232
|
+
),
|
|
6233
|
+
TextField(
|
|
6234
|
+
controller: maxTokensController,
|
|
6235
|
+
keyboardType: TextInputType.number,
|
|
6236
|
+
decoration: const InputDecoration(
|
|
6237
|
+
labelText: 'Max tokens per day',
|
|
6238
|
+
),
|
|
6239
|
+
),
|
|
6240
|
+
];
|
|
6241
|
+
if (stacked) {
|
|
6242
|
+
return Column(
|
|
6243
|
+
children: <Widget>[
|
|
6244
|
+
fields[0],
|
|
6245
|
+
const SizedBox(height: 12),
|
|
6246
|
+
fields[1],
|
|
6247
|
+
],
|
|
6248
|
+
);
|
|
6249
|
+
}
|
|
6250
|
+
return Row(
|
|
6251
|
+
children: <Widget>[
|
|
6252
|
+
Expanded(child: fields[0]),
|
|
6253
|
+
const SizedBox(width: 12),
|
|
6254
|
+
Expanded(child: fields[1]),
|
|
6255
|
+
],
|
|
6256
|
+
);
|
|
6257
|
+
},
|
|
6258
|
+
),
|
|
6259
|
+
const SizedBox(height: 8),
|
|
6260
|
+
SwitchListTile(
|
|
6261
|
+
value: loopPaused,
|
|
6262
|
+
contentPadding: EdgeInsets.zero,
|
|
6263
|
+
title: const Text('Pause loop budget'),
|
|
6264
|
+
subtitle: const Text(
|
|
6265
|
+
'Skip this task before it calls the model.',
|
|
6266
|
+
),
|
|
6267
|
+
onChanged: (value) =>
|
|
6268
|
+
setLocalState(() => loopPaused = value),
|
|
6269
|
+
),
|
|
6270
|
+
],
|
|
6271
|
+
),
|
|
6272
|
+
),
|
|
6022
6273
|
if (controller.agentProfiles.isNotEmpty) ...<Widget>[
|
|
6023
6274
|
const SizedBox(height: 12),
|
|
6024
6275
|
DropdownButtonFormField<String>(
|
|
@@ -6139,12 +6390,40 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
6139
6390
|
triggerConfig['ignoreGroups'] = ignoreGroups;
|
|
6140
6391
|
}
|
|
6141
6392
|
}
|
|
6393
|
+
final maxRuns = int.tryParse(maxRunsController.text.trim());
|
|
6394
|
+
final maxTokens = int.tryParse(
|
|
6395
|
+
maxTokensController.text.trim(),
|
|
6396
|
+
);
|
|
6397
|
+
if (maxRuns == null ||
|
|
6398
|
+
maxRuns <= 0 ||
|
|
6399
|
+
maxTokens == null ||
|
|
6400
|
+
maxTokens <= 0) {
|
|
6401
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
6402
|
+
const SnackBar(
|
|
6403
|
+
content: Text(
|
|
6404
|
+
'Loop budget values must be positive numbers.',
|
|
6405
|
+
),
|
|
6406
|
+
backgroundColor: Colors.red,
|
|
6407
|
+
),
|
|
6408
|
+
);
|
|
6409
|
+
return;
|
|
6410
|
+
}
|
|
6411
|
+
final taskConfig = <String, dynamic>{
|
|
6412
|
+
...?task?.taskConfig,
|
|
6413
|
+
'loopBudget': <String, dynamic>{
|
|
6414
|
+
'enabled': true,
|
|
6415
|
+
'paused': loopPaused,
|
|
6416
|
+
'maxRunsPerDay': maxRuns,
|
|
6417
|
+
'maxTokensPerDay': maxTokens,
|
|
6418
|
+
},
|
|
6419
|
+
};
|
|
6142
6420
|
await controller.saveTask(
|
|
6143
6421
|
id: task?.id,
|
|
6144
6422
|
name: nameController.text.trim(),
|
|
6145
6423
|
triggerType: selectedTriggerType,
|
|
6146
6424
|
triggerConfig: triggerConfig,
|
|
6147
6425
|
prompt: promptController.text.trim(),
|
|
6426
|
+
taskConfig: taskConfig,
|
|
6148
6427
|
model: selectedModel == 'auto' ? null : selectedModel,
|
|
6149
6428
|
enabled: enabled,
|
|
6150
6429
|
agentId: selectedAgentId,
|