feat(tasks): add flexible quick actions

This commit is contained in:
Ashley Venn 2026-06-19 16:32:26 -07:00
parent 4a307cc522
commit f0f8553042
4 changed files with 194 additions and 0 deletions

View file

@ -8,6 +8,8 @@ Purpose: Define and implement the safe, low-friction actions available from task
Recommended Codex level: medium
Status: Completed
Tasks:
Support these quick actions for flexible task cards:
@ -29,6 +31,17 @@ Acceptance criteria:
- Tests cover done and backlog state transitions.
- Push destination selection is represented in domain layer.
Execution notes:
- Added `FlexibleTaskActionService` for flexible card quick actions.
- Added `FlexibleTaskQuickAction` and `PushDestination` domain enums.
- Done marks a flexible task completed without changing its schedule placement.
- Backlog uses the scheduling engine backlog transition and clears schedule placement.
- Push returns explicit domain destinations: next available slot, tomorrow/top of queue, and backlog.
- Break up returns an explicit child-task-flow intent without creating children yet.
- Added tests for done, backlog, push destination representation, and break-up flow intent.
- `dart format lib test`, `dart analyze`, `dart test`, and `git diff --check` passed.
## Chunk 6.2 — Push destination behavior
Recommended Codex level: high

View file

@ -7,4 +7,5 @@ export 'src/backlog.dart';
export 'src/locked_time.dart';
export 'src/quick_capture.dart';
export 'src/scheduling_engine.dart';
export 'src/task_actions.dart';
export 'src/task_statistics.dart';

86
lib/src/task_actions.dart Normal file
View file

@ -0,0 +1,86 @@
import 'models.dart';
import 'scheduling_engine.dart';
/// Quick actions available from a flexible task card.
enum FlexibleTaskQuickAction {
done,
push,
backlog,
breakUp,
}
/// Explicit push destinations shown after choosing the push quick action.
enum PushDestination {
nextAvailableSlot,
tomorrowTopOfQueue,
backlog,
}
/// Domain result for a flexible task quick action.
class FlexibleTaskActionResult {
const FlexibleTaskActionResult({
required this.action,
required this.task,
this.pushDestinations = const <PushDestination>[],
this.startsChildTaskFlow = false,
});
final FlexibleTaskQuickAction action;
final Task task;
final List<PushDestination> pushDestinations;
final bool startsChildTaskFlow;
bool get changedTask => !startsChildTaskFlow && pushDestinations.isEmpty;
}
/// Applies low-friction quick actions for flexible task cards.
class FlexibleTaskActionService {
const FlexibleTaskActionService({
this.schedulingEngine = const SchedulingEngine(),
});
final SchedulingEngine schedulingEngine;
FlexibleTaskActionResult apply({
required Task task,
required FlexibleTaskQuickAction action,
DateTime? updatedAt,
}) {
if (!task.isFlexible) {
throw ArgumentError.value(
task.type, 'task.type', 'Task must be flexible.');
}
switch (action) {
case FlexibleTaskQuickAction.done:
return FlexibleTaskActionResult(
action: action,
task: task.copyWith(
status: TaskStatus.completed,
updatedAt: updatedAt ?? DateTime.now(),
),
);
case FlexibleTaskQuickAction.push:
return FlexibleTaskActionResult(
action: action,
task: task,
pushDestinations: const [
PushDestination.nextAvailableSlot,
PushDestination.tomorrowTopOfQueue,
PushDestination.backlog,
],
);
case FlexibleTaskQuickAction.backlog:
return FlexibleTaskActionResult(
action: action,
task: schedulingEngine.moveToBacklog(task, updatedAt: updatedAt),
);
case FlexibleTaskQuickAction.breakUp:
return FlexibleTaskActionResult(
action: action,
task: task,
startsChildTaskFlow: true,
);
}
}
}

View file

@ -792,6 +792,100 @@ void main() {
expect(minutes, 60);
});
test('flexible quick action done marks task completed', () {
const service = FlexibleTaskActionService();
final task = Task.quickCapture(
id: 'task-1',
title: 'Handle paperwork',
createdAt: now,
).copyWith(
status: TaskStatus.planned,
scheduledStart: DateTime(2026, 6, 19, 13),
scheduledEnd: DateTime(2026, 6, 19, 13, 30),
);
final result = service.apply(
task: task,
action: FlexibleTaskQuickAction.done,
updatedAt: now,
);
expect(result.action, FlexibleTaskQuickAction.done);
expect(result.task.status, TaskStatus.completed);
expect(result.task.scheduledStart, DateTime(2026, 6, 19, 13));
expect(result.task.updatedAt, now);
expect(result.changedTask, isTrue);
});
test('flexible quick action backlog clears schedule placement', () {
const service = FlexibleTaskActionService();
final task = Task.quickCapture(
id: 'task-1',
title: 'Handle paperwork',
createdAt: now,
).copyWith(
status: TaskStatus.planned,
scheduledStart: DateTime(2026, 6, 19, 13),
scheduledEnd: DateTime(2026, 6, 19, 13, 30),
);
final result = service.apply(
task: task,
action: FlexibleTaskQuickAction.backlog,
updatedAt: now,
);
expect(result.action, FlexibleTaskQuickAction.backlog);
expect(result.task.status, TaskStatus.backlog);
expect(result.task.scheduledStart, isNull);
expect(result.task.scheduledEnd, isNull);
expect(result.task.stats.movedToBacklogCount, 1);
expect(result.changedTask, isTrue);
});
test('flexible quick action push exposes domain destinations', () {
const service = FlexibleTaskActionService();
final task = Task.quickCapture(
id: 'task-1',
title: 'Handle paperwork',
createdAt: now,
).copyWith(status: TaskStatus.planned);
final result = service.apply(
task: task,
action: FlexibleTaskQuickAction.push,
updatedAt: now,
);
expect(result.task, same(task));
expect(result.pushDestinations, const [
PushDestination.nextAvailableSlot,
PushDestination.tomorrowTopOfQueue,
PushDestination.backlog,
]);
expect(result.pushDestinations, isNot(contains('laterToday')));
expect(result.changedTask, isFalse);
});
test('flexible quick action break up starts child task flow', () {
const service = FlexibleTaskActionService();
final task = Task.quickCapture(
id: 'task-1',
title: 'Handle paperwork',
createdAt: now,
).copyWith(status: TaskStatus.planned);
final result = service.apply(
task: task,
action: FlexibleTaskQuickAction.breakUp,
updatedAt: now,
);
expect(result.task, same(task));
expect(result.startsChildTaskFlow, isTrue);
expect(result.changedTask, isFalse);
});
});
group('SchedulingEngine starter behavior', () {