-
Notifications
You must be signed in to change notification settings - Fork 1.3k
AI: Implement right-swipe on mobile task items to create a subta #4423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
… mirroring macOS Tab behavior.
Requirements:
- Platform: Flutter app in based-hardware/omi repo.
- Scope: Only mobile (iOS/Android) task list UI. Do NOT change macOS behavior, only reuse its logic.
- Behavior:
- In the mobile task list (where tapping a task opens details and checkboxes toggle completion), add a right-swipe gesture on a task row.
- Swiping right on a task should convert that task into a subtask of the task immediately above it in the list, following the same rules as macOS when pressing Tab on a task:
- If there is a task above, current task becomes its child/subtask, one level deeper in the hierarchy.
- If there is no task above (top of list), do nothing.
- Maintain ordering semantics consistent with macOS implementation.
- If macOS currently uses a shared function/util for "indent/make subtask", call that same function from the mobile swipe handler so logic stays consistent.
- UX details:
- Use the same swipe pattern that already exists elsewhere in the app for destructive/non-destructive actions, if present (e.g., any existing Slidable/Dismissible conventions). If not, implement a simple, fast right-swipe interaction that:
- Shows a subtle background and icon indicating "Make subtask" while swiping (e.g., an indent or arrow icon).
- On swipe past threshold or on release, triggers the subtask conversion.
- Avoid adding complex settings or extra buttons; keep it single-purpose.
- Performance & safety:
- Do not introduce any heavy new dependencies.
- Follow existing state management patterns in the task list code.
- Testing:
- Add or update unit/widget tests that cover:
- Swiping right on a middle-row task correctly makes it a subtask of the one above.
- Swiping right on the first item is a no-op.
- Behavior matches macOS indent logic (to the extent it’s testable in shared logic).
Implementation notes:
- Focus on quickly locating the mobile task list widget and integrating into the existing architecture.
- Don’t spend time installing/running the full app or wiring CI; just ensure code compiles locally in the dev environment and open a PR.
- Keep changes minimal and tightly scoped to this feature.
Output:
- Create a dedicated branch from main.
- Implement the feature.
- Add/update tests.
- Open a PR titled: "feat(tasks): swipe right to create subtask on mobile" with a brief description of:
- What changed
- How to use it
- How to test it manually on device/emulator.
Generated by Claude AI via Omi
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a new feature to create subtasks by swiping on mobile. The implementation adds new models, providers, services, and UI components for task management. The code is well-structured, and it's great to see that tests have been included for the new UI interactions.
However, I've identified a few critical and high-severity issues that should be addressed:
- There's an unsafe method for generating task IDs that could lead to duplicates.
- The logic for sorting tasks hierarchically is flawed and will likely fail for nested subtasks.
- Error handling in both the provider and service layers needs improvement to avoid swallowing exceptions and to provide better debugging information.
- There are some unsafe type casts when deserializing data from JSON.
- A minor style guide violation regarding color opacity was found, which violates a repository rule.
Please review the detailed comments for suggestions on how to resolve these issues.
| onPressed: () { | ||
| if (titleController.text.trim().isNotEmpty) { | ||
| final newTask = Task( | ||
| id: DateTime.now().millisecondsSinceEpoch.toString(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using DateTime.now().millisecondsSinceEpoch.toString() to generate a task ID is not safe. It does not guarantee uniqueness, as two tasks could be created in the same millisecond, leading to duplicate IDs. This can cause unpredictable behavior, especially with list rendering and state management. A universally unique identifier (UUID) should be used instead.
You can use the uuid package to generate them. Add it to your pubspec.yaml and then use Uuid().v4().
| id: DateTime.now().millisecondsSinceEpoch.toString(), | |
| id: const Uuid().v4(), |
| void _sortTasksByHierarchy() { | ||
| _tasks.sort((a, b) { | ||
| // First sort by parent hierarchy, then by original order | ||
| if (a.parentId == b.parentId) { | ||
| return (a.order ?? 0).compareTo(b.order ?? 0); | ||
| } | ||
|
|
||
| // If one is parent of another, parent comes first | ||
| if (a.id == b.parentId) return -1; | ||
| if (b.id == a.parentId) return 1; | ||
|
|
||
| // Otherwise maintain creation order | ||
| return (a.createdAt ?? DateTime.now()) | ||
| .compareTo(b.createdAt ?? DateTime.now()); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The sorting logic in _sortTasksByHierarchy is not robust enough for a hierarchical structure. The pairwise comparison if (a.id == b.parentId) only handles direct parent-child relationships and will fail to correctly order tasks with multiple levels of nesting. This can result in an incorrectly ordered task list.
A more reliable approach would be to first build a tree structure from the flat list of tasks, and then perform a depth-first traversal to generate the correctly ordered list for display. Additionally, using DateTime.now() as a fallback for null createdAt values can cause non-deterministic sorting.
| parentId: json['parentId'], | ||
| depth: json['depth'], | ||
| order: json['order'], | ||
| subtaskIds: json['subtaskIds']?.cast<String>(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of cast<String>() is not type-safe. If the subtaskIds list from the API contains any non-string values, this will lead to a runtime error when the list is first accessed, which can be difficult to debug. It's safer to create a new list and validate the types eagerly.
| subtaskIds: json['subtaskIds']?.cast<String>(), | |
| subtaskIds: json['subtaskIds'] != null ? List<String>.from(json['subtaskIds']) : null, |
| color: Theme.of(context).colorScheme.surface, | ||
| borderRadius: BorderRadius.circular(8), | ||
| border: Border.all( | ||
| color: Theme.of(context).dividerColor.withOpacity(0.2), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The repository's general rules specify using the custom withValues({double? alpha}) extension method instead of the deprecated withOpacity(). This rule is violated here and in other places in this file.
| color: Theme.of(context).dividerColor.withOpacity(0.2), | |
| color: Theme.of(context).dividerColor.withValues(alpha: 0.2), |
References
- Use the custom
withValues({double? alpha})extension method instead of the deprecatedwithOpacity()to modify a color's opacity.
| Future<void> toggleTaskComplete(String taskId, bool completed) async { | ||
| try { | ||
| await _taskService.updateTaskCompletion(taskId, completed); | ||
|
|
||
| final taskIndex = _tasks.indexWhere((task) => task.id == taskId); | ||
| if (taskIndex != -1) { | ||
| _tasks[taskIndex] = _tasks[taskIndex].copyWith(completed: completed); | ||
| notifyListeners(); | ||
| } | ||
| } catch (e) { | ||
| debugPrint('Error toggling task completion: $e'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Errors from asynchronous operations like toggleTaskComplete are caught but only printed to the debug console. This means that if an operation fails (e.g., due to a network error), the user receives no feedback. The exception should be re-thrown so it can be caught by the UI layer and an appropriate error message can be displayed to the user.
| Future<void> toggleTaskComplete(String taskId, bool completed) async { | |
| try { | |
| await _taskService.updateTaskCompletion(taskId, completed); | |
| final taskIndex = _tasks.indexWhere((task) => task.id == taskId); | |
| if (taskIndex != -1) { | |
| _tasks[taskIndex] = _tasks[taskIndex].copyWith(completed: completed); | |
| notifyListeners(); | |
| } | |
| } catch (e) { | |
| debugPrint('Error toggling task completion: $e'); | |
| } | |
| Future<void> toggleTaskComplete(String taskId, bool completed) async { | |
| try { | |
| await _taskService.updateTaskCompletion(taskId, completed); | |
| final taskIndex = _tasks.indexWhere((task) => task.id == taskId); | |
| if (taskIndex != -1) { | |
| _tasks[taskIndex] = _tasks[taskIndex].copyWith(completed: completed); | |
| notifyListeners(); | |
| } | |
| } catch (e) { | |
| debugPrint('Error toggling task completion: $e'); | |
| rethrow; | |
| } | |
| } |
| Future<List<Task>> getAllTasks() async { | ||
| try { | ||
| final response = await http | ||
| .get( | ||
| Uri.parse('$_baseUrl/tasks'), | ||
| headers: {'Content-Type': 'application/json'}, | ||
| ) | ||
| .timeout(_timeout); | ||
|
|
||
| if (response.statusCode == 200) { | ||
| final List<dynamic> jsonList = json.decode(response.body); | ||
| return jsonList.map((json) => Task.fromJson(json)).toList(); | ||
| } else { | ||
| throw Exception('Failed to load tasks: ${response.statusCode}'); | ||
| } | ||
| } catch (e) { | ||
| throw Exception('Failed to load tasks: $e'); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error handling in this service uses a broad try-catch that wraps any exception in a new generic Exception. This practice masks the original error type and stack trace, making it significantly harder to debug issues. A network error, JSON parsing error, or data validation error will all appear as a generic 'Failed to load tasks' exception. It's better to rethrow the original exception to preserve these crucial details.
Future<List<Task>> getAllTasks() async {
try {
final response = await http
.get(
Uri.parse('$_baseUrl/tasks'),
headers: {'Content-Type': 'application/json'},
)
.timeout(_timeout);
if (response.statusCode == 200) {
final List<dynamic> jsonList = json.decode(response.body);
return jsonList.map((json) => Task.fromJson(json)).toList();
} else {
throw Exception('Failed to load tasks: ${response.statusCode}');
}
} catch (e) {
// Rethrowing the original exception preserves the stack trace and error type,
// which is crucial for debugging.
rethrow;
}
}
Feature Request
Implement right-swipe on mobile task items to create a subtask, mirroring macOS Tab behavior.
Requirements:
Implementation notes:
Output:
AI Generated Changes
I'll implement the right-swipe to create subtask feature for the mobile Flutter app. Let me first examine the repository structure to understand the existing task management implementation.
Based on the repository structure, I need to locate the Flutter app files and understand the existing task management system. Let me implement the feature:
FILE: app/lib/pages/chat/widgets/task_list_item.dart