Task execution Elapsed timer

This commit is contained in:
Marc Rejohn Castillano 2026-03-04 18:42:37 +08:00
parent 94088a8796
commit 82fe619f22

View File

@ -124,6 +124,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
bool _actionSaved = false; bool _actionSaved = false;
bool _actionProcessing = false; bool _actionProcessing = false;
bool _pauseActionInFlight = false; bool _pauseActionInFlight = false;
Timer? _elapsedTicker;
DateTime _elapsedNow = AppTime.now();
late final AnimationController _saveAnimController; late final AnimationController _saveAnimController;
late final Animation<double> _savePulse; late final Animation<double> _savePulse;
static const List<String> _statusOptions = [ static const List<String> _statusOptions = [
@ -172,6 +174,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
_notedDebounce?.cancel(); _notedDebounce?.cancel();
_receivedDebounce?.cancel(); _receivedDebounce?.cancel();
_actionDebounce?.cancel(); _actionDebounce?.cancel();
_elapsedTicker?.cancel();
_actionController?.dispose(); _actionController?.dispose();
_actionFocusNode.dispose(); _actionFocusNode.dispose();
_actionScrollController.dispose(); _actionScrollController.dispose();
@ -264,6 +267,11 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
ref.watch(taskActivityLogsProvider(task.id)).valueOrNull ?? ref.watch(taskActivityLogsProvider(task.id)).valueOrNull ??
<TaskActivityLog>[]; <TaskActivityLog>[];
final isTaskPaused = _isTaskCurrentlyPaused(task, taskLogs); final isTaskPaused = _isTaskCurrentlyPaused(task, taskLogs);
final elapsedDuration = _currentElapsedDuration(
task,
taskLogs,
isTaskPaused,
);
final typingState = ref.watch(typingIndicatorProvider(typingChannelId)); final typingState = ref.watch(typingIndicatorProvider(typingChannelId));
final canSendMessages = task.status != 'completed'; final canSendMessages = task.status != 'completed';
@ -272,7 +280,10 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
ticketId == null ? null : ref.watch(ticketMessagesProvider(ticketId)), ticketId == null ? null : ref.watch(ticketMessagesProvider(ticketId)),
); );
WidgetsBinding.instance.addPostFrameCallback((_) => _updateSaveAnim()); WidgetsBinding.instance.addPostFrameCallback((_) {
_updateSaveAnim();
_syncElapsedTicker(task, taskLogs, isTaskPaused);
});
final realtime = ref.watch(realtimeControllerProvider); final realtime = ref.watch(realtimeControllerProvider);
final isRetrieving = final isRetrieving =
@ -596,7 +607,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
top: 8.0, top: 8.0,
bottom: 68, bottom: 92,
), ),
child: Column( child: Column(
crossAxisAlignment: crossAxisAlignment:
@ -618,7 +629,12 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
Positioned( Positioned(
right: 8, right: 8,
bottom: 8, bottom: 8,
child: IconButton.filledTonal( child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.end,
children: [
IconButton.filledTonal(
tooltip: isTaskPaused tooltip: isTaskPaused
? 'Resume task' ? 'Resume task'
: 'Pause task', : 'Pause task',
@ -626,7 +642,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
? null ? null
: () async { : () async {
setState( setState(
() => _pauseActionInFlight = () =>
_pauseActionInFlight =
true, true,
); );
try { try {
@ -701,6 +718,15 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
: Icons.pause, : Icons.pause,
), ),
), ),
const SizedBox(height: 4),
Text(
'Elapsed ${_formatDurationClock(elapsedDuration)}',
style: Theme.of(
context,
).textTheme.labelSmall,
),
],
),
), ),
], ],
), ),
@ -3232,6 +3258,60 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
return started ?? task.startedAt; return started ?? task.startedAt;
} }
DateTime? _latestPauseSince(List<TaskActivityLog> logs, DateTime notBefore) {
for (final entry in logs) {
if (entry.actionType != 'paused') continue;
if (entry.createdAt.isBefore(notBefore)) continue;
return entry.createdAt;
}
return null;
}
Duration? _currentElapsedDuration(
Task task,
List<TaskActivityLog> logs,
bool isPaused,
) {
if (task.status != 'in_progress') {
return null;
}
final start = _resolveExecutionStart(task, logs);
if (start == null) {
return null;
}
final pausedAt = isPaused ? _latestPauseSince(logs, start) : null;
final endAt = pausedAt ?? _elapsedNow;
return _computeEffectiveExecutionDuration(task, logs, endAt);
}
void _syncElapsedTicker(
Task task,
List<TaskActivityLog> logs,
bool isPaused,
) {
final shouldRun =
task.status == 'in_progress' &&
!isPaused &&
_resolveExecutionStart(task, logs) != null;
if (shouldRun) {
if (_elapsedTicker?.isActive != true) {
_elapsedNow = AppTime.now();
_elapsedTicker = Timer.periodic(const Duration(seconds: 1), (_) {
if (!mounted) return;
setState(() {
_elapsedNow = AppTime.now();
});
});
}
} else {
_elapsedTicker?.cancel();
_elapsedTicker = null;
_elapsedNow = AppTime.now();
}
}
Duration _computeEffectiveExecutionDuration( Duration _computeEffectiveExecutionDuration(
Task task, Task task,
List<TaskActivityLog> logs, List<TaskActivityLog> logs,
@ -3319,6 +3399,17 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
return '${minutes}m'; return '${minutes}m';
} }
String _formatDurationClock(Duration? duration) {
if (duration == null || duration.isNegative) {
return '00:00:00';
}
final totalSeconds = duration.inSeconds;
final hours = (totalSeconds ~/ 3600).toString().padLeft(2, '0');
final minutes = ((totalSeconds % 3600) ~/ 60).toString().padLeft(2, '0');
final seconds = (totalSeconds % 60).toString().padLeft(2, '0');
return '$hours:$minutes:$seconds';
}
Widget _buildMentionText( Widget _buildMentionText(
String text, String text,
Color baseColor, Color baseColor,
@ -4087,6 +4178,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
context: context, context: context,
builder: (dialogContext) { builder: (dialogContext) {
var isSaving = false; var isSaving = false;
var reasonProcessing = false;
var reasonDeepSeek = false;
return StatefulBuilder( return StatefulBuilder(
builder: (ctx, setState) { builder: (ctx, setState) {
return AlertDialog( return AlertDialog(
@ -4097,14 +4190,46 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
width: 360, width: 360,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
child: TextField( child: Row(
children: [
Expanded(
child: GeminiAnimatedTextField(
controller: reasonCtrl, controller: reasonCtrl,
maxLines: 3, enabled: !isSaving,
decoration: const InputDecoration(
labelText: 'Reason', labelText: 'Reason',
hintText: 'Provide a justification for cancelling', maxLines: 3,
isProcessing: reasonProcessing,
useDeepSeekColors: reasonDeepSeek,
), ),
), ),
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: GeminiButton(
textController: reasonCtrl,
onTextUpdated: (updatedText) {
setState(() {
reasonCtrl.text = updatedText;
});
},
onProcessingStateChanged: (isProcessing) {
setState(() {
reasonProcessing = isProcessing;
});
},
onProviderChanged: (isDeepSeek) {
setState(() => reasonDeepSeek = isDeepSeek);
},
tooltip:
'Improve cancellation reason with Gemini',
promptBuilder: (_) =>
'Improve this task cancellation reason for '
'clarity, professionalism, and concise '
'English. Keep the original intent. Return '
'ONLY the improved reason, no explanations:',
),
),
],
),
), ),
), ),
actions: [ actions: [