Skip to content

Improve task navigation and bulk workflows - #186

Closed
rsheyd wants to merge 11 commits into
romaintb:mainfrom
rsheyd:codex/ui-navigation-improvements
Closed

Improve task navigation and bulk workflows#186
rsheyd wants to merge 11 commits into
romaintb:mainfrom
rsheyd:codex/ui-navigation-improvements

Conversation

@rsheyd

@rsheyd rsheyd commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Screenshot

Sanitized example using fictional tasks and projects:

Terminalist navigation and task panes with fictional dummy data

What

  • size and resize the navigation pane more naturally
  • add navigation counts, selected-row styling, and pane focus indicators
  • support left/right pane navigation
  • keep matching subtasks visible with parent context
  • add bulk task marking, completion, scheduling, and unscheduling
  • show shared processing feedback and block overlapping foreground mutations
  • normalize Todoist date handling and reliably clear due dates
  • document the behavioral baseline and follow-up architecture plan

Why

The navigation pane used excessive space, count semantics did not consistently match visible task rows, and longer-running commands remained interactive without feedback. Several related behaviors also needed a stable baseline before deeper state-management and transport refactors.

Impact

This changes navigation layout and keyboard behavior, adds bulk workflows, makes filtered subtasks visible, and temporarily blocks input during foreground mutations. Search and background sync remain non-blocking.

Due-date clearing currently uses a dedicated reqwest path because the existing Todoist wrapper cannot express the required clear operation. Consolidating that path is tracked in the architecture refactor plan.

Checks

  • cargo fmt --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test
  • cargo build

All pass. The storage integration test requires normal host filesystem access and passes with that expected access.

Follow-up

docs/ARCHITECTURE_REFACTOR_PLAN.md separates typed operations, versioned snapshots, cache preservation, grouped counts, component-update optimization, and Todoist transport consolidation into later focused branches and pull requests.

Summary by CodeRabbit

  • New Features
    • Mark multiple tasks and toggle completion or due dates in bulk.
    • Navigate and manage search results with keyboard shortcuts.
    • Resize and focus the sidebar and task pane; view updated task counts.
    • Show or hide the configurable shortcut bar.
    • Clear task due dates reliably.
  • Bug Fixes
    • Improved handling of date and datetime values.
    • Preserved due dates during unrelated task updates.
  • Documentation
    • Expanded keyboard shortcut, configuration, baseline, and architecture documentation.
  • Tests
    • Added coverage for new task, search, configuration, and date behaviors.

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@rsheyd, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 16 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4e5e2c26-352a-4ae9-9232-683a24876b61

📥 Commits

Reviewing files that changed from the base of the PR and between 311f7f0 and 692def9.

📒 Files selected for processing (22)
  • docs/ARCHITECTURE_REFACTOR_PLAN.md
  • docs/CURRENT_UI_WORK_BASELINE.md
  • docs/KEYBOARD_SHORTCUTS.md
  • docs/README.md
  • src/main.rs
  • src/storage.rs
  • src/sync/mod.rs
  • src/sync/storage.rs
  • src/ui/app_component.rs
  • src/ui/components/dialog_component.rs
  • src/ui/components/dialogs/mod.rs
  • src/ui/components/sidebar_component.rs
  • src/ui/components/sidebar_item_component.rs
  • src/ui/components/task_list_component.rs
  • src/ui/core/actions.rs
  • src/ui/core/mod.rs
  • src/ui/core/operations.rs
  • src/ui/core/task_manager.rs
  • src/ui/core/view_snapshot.rs
  • tests/storage/db.rs
  • tests/ui/components/dialog_component.rs
  • tests/ui/components/task_list_component.rs

Walkthrough

Changes

UI task workflow and backend synchronization

Layer / File(s) Summary
Task data contracts and loading
src/ui/core/actions.rs, src/ui/core/task_manager.rs, src/utils/datetime.rs, tests/ui/core/task_manager.rs, tests/utils/datetime.rs
Task-loading payloads now include all tasks and navigation counts, with background-work status and datetime parsing support.
Due-date clearing synchronization
Cargo.toml, src/backend/*, src/sync/tasks.rs
Todoist updates distinguish due-date clearing from no update and persist returned due-date fields.
Task selection and search actions
src/ui/components/*, tests/ui/components/*
Task lists support marking, bulk completion, due-date actions, orphaned subtasks, parent context, and selectable search results.
Application navigation and layout
src/ui/app_component.rs, src/ui/components/sidebar_component.rs, src/config.rs, tests/config.rs
Pane focus, sidebar resizing, navigation counts, processing feedback, and configurable shortcut-bar rendering are wired into the application.
User and developer documentation
README.md, docs/*
Keyboard controls, configuration, UI baseline, architecture plans, and documentation indexes are updated.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: romaintb

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main theme of the PR: improved navigation plus bulk task actions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@rsheyd
rsheyd force-pushed the codex/ui-navigation-improvements branch from 7532f08 to c03065c Compare July 18, 2026 16:06
@rsheyd
rsheyd marked this pull request as ready for review July 18, 2026 17:25

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/ui/components/dialog_component.rs (2)

137-142: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reset selection when accepting results for a new query.

Clamping preserves the old numeric index even though the result identities changed. A subsequent t can therefore mutate an unrelated task at that index. Reset to 0, or preserve selection by UUID.

Proposed fix
 if query == self.input_buffer {
     self.search_results = results;
-    self.search_selected_index =
-        self.search_selected_index.min(self.search_results.len().saturating_sub(1));
+    self.search_selected_index = 0;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/components/dialog_component.rs` around lines 137 - 142, Update
update_search_results so that when query matches self.input_buffer and new
results are accepted, reset search_selected_index to 0 instead of clamping the
previous numeric index; keep the existing stale-query guard and result
assignment unchanged.

466-495: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Scroll the result list with its selected item.

The index can advance beyond the visible popup, but this stateless List always renders from offset zero. Use a persistent ListState, select search_selected_index, and call render_stateful_widget.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/components/dialog_component.rs` around lines 466 - 495, Update the
search-results rendering block in the dialog component to use a persistent
ListState, selecting search_selected_index before rendering. Replace the
stateless results_list_widget rendering with render_stateful_widget and pass the
state so the list scrolls to keep the selected result visible.
src/ui/app_component.rs (1)

401-440: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove task-specific global shortcuts to enforce bulk logic.

These t, T, w, and W shortcuts are still defined as global keys that operate only on a single selected task. If triggered while the Navigation pane is focused, they bypass the new bulk operation logic (SetTasksDueDate) and ignore any marked tasks.

By contrast, the new u (unschedule) and x (mark) shortcuts are appropriately omitted from this global handler. Removing these legacy mappings ensures all task-related shortcuts properly delegate to task_list_component and behave consistently.

♻️ Proposed fix

Remove these match arms:

-            KeyCode::Char('t') => {
-                // Set task due date to today
-                if let Some(task) = self.task_list.get_selected_task() {
-                    info!("Global key: 't' - setting task '{}' due today", task.content);
-                    Action::SetTaskDueToday(task.uuid)
-                } else {
-                    info!("Global key: 't' - no task selected");
-                    Action::ShowDialog(DialogType::Info(UI_NO_TASK_SELECTED_DUE_DATE.to_string()))
-                }
-            }
-            KeyCode::Char('T') => {
-                // Set task due date to tomorrow
-                if let Some(task) = self.task_list.get_selected_task() {
-                    info!("Global key: 'T' - setting task '{}' due tomorrow", task.content);
-                    Action::SetTaskDueTomorrow(task.uuid)
-                } else {
-                    info!("Global key: 'T' - no task selected");
-                    Action::ShowDialog(DialogType::Info(UI_NO_TASK_SELECTED_DUE_DATE.to_string()))
-                }
-            }
-            KeyCode::Char('w') => {
-                // Set task due date to next week (Monday)
-                if let Some(task) = self.task_list.get_selected_task() {
-                    info!("Global key: 'w' - setting task '{}' due next week", task.content);
-                    Action::SetTaskDueNextWeek(task.uuid)
-                } else {
-                    info!("Global key: 'w' - no task selected");
-                    Action::ShowDialog(DialogType::Info(UI_NO_TASK_SELECTED_DUE_DATE.to_string()))
-                }
-            }
-            KeyCode::Char('W') => {
-                // Set task due date to weekend (Saturday)
-                if let Some(task) = self.task_list.get_selected_task() {
-                    info!("Global key: 'W' - setting task '{}' due weekend", task.content);
-                    Action::SetTaskDueWeekEnd(task.uuid)
-                } else {
-                    info!("Global key: 'W' - no task selected");
-                    Action::ShowDialog(DialogType::Info(UI_NO_TASK_SELECTED_DUE_DATE.to_string()))
-                }
-            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/app_component.rs` around lines 401 - 440, Remove the
KeyCode::Char('t'), 'T', 'w', and 'W' match arms from the global key handler so
task due-date shortcuts are delegated to task_list_component and bulk
marked-task handling via SetTasksDueDate is preserved.
🧹 Nitpick comments (2)
src/ui/core/task_manager.rs (1)

261-270: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider optimizing label task counts.

This loop executes a sequential database query for each label (N+1 queries). While local SQLite queries are generally fast and this runs in a background task, it could eventually become a performance bottleneck for users with a large number of labels. Consider adding a bulk query to SyncService (e.g., using a GROUP BY clause) to fetch all label counts at once in a future optimization.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/core/task_manager.rs` around lines 261 - 270, Optimize the
label-counting flow around the labels loop by adding a SyncService bulk query
that returns active, non-deleted task counts grouped by label UUID, then
populate navigation_counts.labels from that result instead of calling
get_tasks_with_label separately for each label. Preserve zero counts for labels
absent from the grouped results.
src/ui/app_component.rs (1)

576-593: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider accumulating errors for bulk operations.

Using the ? operator inside the iteration causes the loop to exit on the very first failure (e.g. a momentary network hiccup). This leaves the remaining marked tasks completely unprocessed, which can be confusing for a bulk action.

Consider capturing the errors and continuing the loop so that as many tasks as possible are processed, similar to the pattern you might want for Action::SetTasksDueDate below.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/app_component.rs` around lines 576 - 593, The Action::ToggleTasks
handler currently stops at the first restore_task or complete_task error;
accumulate each operation’s errors while continuing through all tasks, then
return a result that reports the collected failures after processing every task,
following the existing bulk-operation pattern used by Action::SetTasksDueDate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/ui/app_component.rs`:
- Around line 1229-1236: Update the sidebar drag handling in the
resizing_sidebar branch so the upper bound passed to mouse.column.clamp is never
below SIDEBAR_MIN_WIDTH. Ensure the screen_width subtraction result is raised to
at least SIDEBAR_MIN_WIDTH before calling clamp, preserving the existing
minimum-width behavior.

In `@src/ui/components/dialog_component.rs`:
- Around line 607-623: Update the TaskSearch handling in
src/ui/components/dialog_component.rs lines 607-623 to stop consuming printable
j, k, and t characters as result commands; use arrow keys, modifiers, or an
explicit navigation mode, while preserving result navigation and due-today
actions through non-conflicting bindings. In
tests/ui/components/dialog_component.rs lines 39-54, verify arrow-key navigation
without requiring j/k behavior; in lines 56-67, update the due-today shortcut
test to the new binding and verify lowercase t remains searchable.

---

Outside diff comments:
In `@src/ui/app_component.rs`:
- Around line 401-440: Remove the KeyCode::Char('t'), 'T', 'w', and 'W' match
arms from the global key handler so task due-date shortcuts are delegated to
task_list_component and bulk marked-task handling via SetTasksDueDate is
preserved.

In `@src/ui/components/dialog_component.rs`:
- Around line 137-142: Update update_search_results so that when query matches
self.input_buffer and new results are accepted, reset search_selected_index to 0
instead of clamping the previous numeric index; keep the existing stale-query
guard and result assignment unchanged.
- Around line 466-495: Update the search-results rendering block in the dialog
component to use a persistent ListState, selecting search_selected_index before
rendering. Replace the stateless results_list_widget rendering with
render_stateful_widget and pass the state so the list scrolls to keep the
selected result visible.

---

Nitpick comments:
In `@src/ui/app_component.rs`:
- Around line 576-593: The Action::ToggleTasks handler currently stops at the
first restore_task or complete_task error; accumulate each operation’s errors
while continuing through all tasks, then return a result that reports the
collected failures after processing every task, following the existing
bulk-operation pattern used by Action::SetTasksDueDate.

In `@src/ui/core/task_manager.rs`:
- Around line 261-270: Optimize the label-counting flow around the labels loop
by adding a SyncService bulk query that returns active, non-deleted task counts
grouped by label UUID, then populate navigation_counts.labels from that result
instead of calling get_tasks_with_label separately for each label. Preserve zero
counts for labels absent from the grouped results.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 07031024-f083-4822-9b5e-e1f2541e7311

📥 Commits

Reviewing files that changed from the base of the PR and between 81c680e and 311f7f0.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • docs/assets/pr-186/navigation-after-dummy.png is excluded by !**/*.png
📒 Files selected for processing (25)
  • Cargo.toml
  • README.md
  • docs/ARCHITECTURE_REFACTOR_PLAN.md
  • docs/CONFIGURATION.md
  • docs/CURRENT_UI_WORK_BASELINE.md
  • docs/KEYBOARD_SHORTCUTS.md
  • docs/README.md
  • src/backend/mod.rs
  • src/backend/todoist.rs
  • src/config.rs
  • src/sync/tasks.rs
  • src/ui/app_component.rs
  • src/ui/components/dialog_component.rs
  • src/ui/components/dialogs/system_dialogs.rs
  • src/ui/components/sidebar_component.rs
  • src/ui/components/task_list_component.rs
  • src/ui/components/task_list_item_component.rs
  • src/ui/core/actions.rs
  • src/ui/core/task_manager.rs
  • src/utils/datetime.rs
  • tests/config.rs
  • tests/ui/components/dialog_component.rs
  • tests/ui/components/task_list_component.rs
  • tests/ui/core/task_manager.rs
  • tests/utils/datetime.rs

Comment thread src/ui/app_component.rs
Comment on lines +1229 to +1236
if self.resizing_sidebar {
if matches!(mouse.kind, crossterm::event::MouseEventKind::Drag(_)) {
self.sidebar_width_override = Some(
mouse
.column
.clamp(SIDEBAR_MIN_WIDTH, self.screen_width.saturating_sub(MAIN_AREA_MIN_WIDTH)),
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Prevent panic during mouse drag on small terminal sizes.

If the terminal is resized to be very small, self.screen_width.saturating_sub(MAIN_AREA_MIN_WIDTH) can evaluate to a value smaller than SIDEBAR_MIN_WIDTH. When this happens, u16::clamp will panic because its max argument is less than its min argument, instantly crashing the app.

Ensure the calculated maximum is at least SIDEBAR_MIN_WIDTH.

🐛 Proposed fix
                     if self.resizing_sidebar {
                         if matches!(mouse.kind, crossterm::event::MouseEventKind::Drag(_)) {
+                            let max_width = self.screen_width.saturating_sub(MAIN_AREA_MIN_WIDTH).max(SIDEBAR_MIN_WIDTH);
                             self.sidebar_width_override = Some(
                                 mouse
                                     .column
-                                    .clamp(SIDEBAR_MIN_WIDTH, self.screen_width.saturating_sub(MAIN_AREA_MIN_WIDTH)),
+                                    .clamp(SIDEBAR_MIN_WIDTH, max_width),
                             );
                         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if self.resizing_sidebar {
if matches!(mouse.kind, crossterm::event::MouseEventKind::Drag(_)) {
self.sidebar_width_override = Some(
mouse
.column
.clamp(SIDEBAR_MIN_WIDTH, self.screen_width.saturating_sub(MAIN_AREA_MIN_WIDTH)),
);
}
if self.resizing_sidebar {
if matches!(mouse.kind, crossterm::event::MouseEventKind::Drag(_)) {
let max_width = self
.screen_width
.saturating_sub(MAIN_AREA_MIN_WIDTH)
.max(SIDEBAR_MIN_WIDTH);
self.sidebar_width_override = Some(
mouse
.column
.clamp(SIDEBAR_MIN_WIDTH, max_width),
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/app_component.rs` around lines 1229 - 1236, Update the sidebar drag
handling in the resizing_sidebar branch so the upper bound passed to
mouse.column.clamp is never below SIDEBAR_MIN_WIDTH. Ensure the screen_width
subtraction result is raised to at least SIDEBAR_MIN_WIDTH before calling clamp,
preserving the existing minimum-width behavior.

Comment thread src/ui/components/dialog_component.rs
@rsheyd

rsheyd commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by consolidated upstream PR #188, which includes this UI navigation and bulk-workflow baseline together with the cache lifecycle, typed operations, versioned snapshots, and search-focus work. Please review and test #188 instead.

@rsheyd rsheyd closed this Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant