From f0f85530426922a6ce3d5e78940c3dc03213d6f1 Mon Sep 17 00:00:00 2001 From: Ashley Venn Date: Fri, 19 Jun 2026 16:32:26 -0700 Subject: [PATCH] feat(tasks): add flexible quick actions --- ...BLOCK_06_Task_Actions_State_Transitions.md | 13 +++ lib/scheduler_core.dart | 1 + lib/src/task_actions.dart | 86 +++++++++++++++++ test/scheduling_engine_test.dart | 94 +++++++++++++++++++ 4 files changed, 194 insertions(+) create mode 100644 lib/src/task_actions.dart diff --git a/Codex Documentation/Current Software Plan/V1_BLOCK_06_Task_Actions_State_Transitions.md b/Codex Documentation/Current Software Plan/V1_BLOCK_06_Task_Actions_State_Transitions.md index 146caee..c4a0e70 100644 --- a/Codex Documentation/Current Software Plan/V1_BLOCK_06_Task_Actions_State_Transitions.md +++ b/Codex Documentation/Current Software Plan/V1_BLOCK_06_Task_Actions_State_Transitions.md @@ -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 diff --git a/lib/scheduler_core.dart b/lib/scheduler_core.dart index 70fa664..d63fb0a 100644 --- a/lib/scheduler_core.dart +++ b/lib/scheduler_core.dart @@ -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'; diff --git a/lib/src/task_actions.dart b/lib/src/task_actions.dart new file mode 100644 index 0000000..0da49fd --- /dev/null +++ b/lib/src/task_actions.dart @@ -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 [], + this.startsChildTaskFlow = false, + }); + + final FlexibleTaskQuickAction action; + final Task task; + final List 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, + ); + } + } +} diff --git a/test/scheduling_engine_test.dart b/test/scheduling_engine_test.dart index 734fdb3..3469ca6 100644 --- a/test/scheduling_engine_test.dart +++ b/test/scheduling_engine_test.dart @@ -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', () {