import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'runtime_models.dart'; class DerivedTasksController extends ChangeNotifier { List queueInternal = const []; List runningInternal = const []; List historyInternal = const []; List failedInternal = const []; List scheduledInternal = const []; List get queue => queueInternal; List get running => runningInternal; List get history => historyInternal; List get failed => failedInternal; List get scheduled => scheduledInternal; int get totalCount => queueInternal.length + runningInternal.length + historyInternal.length + failedInternal.length; void recompute({ required List sessions, required List cronJobs, required String currentSessionKey, required bool hasPendingRun, required String activeAgentName, }) { final sorted = sessions.toList(growable: false) ..sort( (left, right) => (right.updatedAtMs ?? 0).compareTo(left.updatedAtMs ?? 0), ); final queue = []; final running = []; final history = []; final failed = []; for (final session in sorted) { final item = DerivedTaskItem( id: session.key, title: session.label, owner: activeAgentName, status: statusForSessionInternal( session: session, currentSessionKey: currentSessionKey, hasPendingRun: hasPendingRun, ), surface: session.surface ?? session.kind ?? 'Assistant', startedAtLabel: timeLabelInternal(session.updatedAtMs), durationLabel: durationLabelInternal(session.updatedAtMs), summary: session.lastMessagePreview ?? session.subject ?? 'Session activity', sessionKey: session.key, ); switch (item.status) { case 'Running': running.add(item); case 'Failed': failed.add(item); case 'Queued': queue.add(item); default: history.add(item); } } queueInternal = queue; runningInternal = running; historyInternal = history; failedInternal = failed; scheduledInternal = cronJobs .map( (job) => DerivedTaskItem( id: job.id, title: job.name, owner: job.agentId?.trim().isNotEmpty == true ? job.agentId! : activeAgentName, status: job.enabled ? 'Scheduled' : 'Disabled', surface: 'Cron', startedAtLabel: timeLabelInternal(job.nextRunAtMs?.toDouble()), durationLabel: job.scheduleLabel, summary: job.description ?? job.lastError ?? job.lastStatus ?? 'Scheduled automation', sessionKey: 'cron:${job.id}', ), ) .toList(growable: false); notifyListeners(); } String statusForSessionInternal({ required GatewaySessionSummary session, required String currentSessionKey, required bool hasPendingRun, }) { if (session.abortedLastRun == true) { return 'Failed'; } if (hasPendingRun && matchesSessionKey(session.key, currentSessionKey)) { return 'Running'; } if ((session.lastMessagePreview ?? '').isEmpty) { return 'Queued'; } return 'Open'; } String timeLabelInternal(double? timestampMs) { if (timestampMs == null) { return 'Unknown'; } final date = DateTime.fromMillisecondsSinceEpoch(timestampMs.toInt()); return '${date.month}/${date.day} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}'; } String durationLabelInternal(double? timestampMs) { if (timestampMs == null) { return 'n/a'; } final delta = DateTime.now().difference( DateTime.fromMillisecondsSinceEpoch(timestampMs.toInt()), ); if (delta.inMinutes < 1) { return 'just now'; } if (delta.inHours < 1) { return '${delta.inMinutes}m ago'; } if (delta.inDays < 1) { return '${delta.inHours}h ago'; } return '${delta.inDays}d ago'; } } bool matchesSessionKey(String incoming, String current) { final left = incoming.trim().toLowerCase(); final right = current.trim().toLowerCase(); return left == right; } String encodePrettyJson(Object value) { const encoder = JsonEncoder.withIndent(' '); return encoder.convert(value); } String ephemeralIdInternal() => DateTime.now().microsecondsSinceEpoch.toString();