Skip to content

feat(ui): keep the cursor on its task when a background sync lands - #237

Merged
romaintb merged 1 commit into
mainfrom
feat/selection-follows-task
Sep 5, 2026
Merged

romaintb merged 1 commit into
mainfrom
feat/selection-follows-task

Conversation

@romaintb

@romaintb romaintb commented Sep 5, 2026

Copy link
Copy Markdown
Owner

PR #209 item 2, and the last of the family. Builds on #236, which made local UUIDs survive a sync so there is something stable to anchor to.

The bug

A sync completing mid-navigation rebuilds the task list. selected_index is a position among selectable rows, so a task arriving above the cursor slides it onto a different task while the user is looking at it.

The change

Carry why a load was scheduled alongside the load:

  • LoadKind::Background (a completed sync) re-anchors the cursor to the task it was on.
  • LoadKind::User (navigation, a task operation, the debug refresh) leaves the cursor on its row, so marking an overdue task "due today" doesn't drag it into the Today section behind the task.
  • LoadKind::Initial resolves default_project, as before.

The reason has to travel with the load rather than sit in a field on AppComponent: the origins overlap, and a sync landing while the user navigates would otherwise consume the other one's value.

Net effect on the surface is a wash or better. LoadKind replaces spawn_data_load's is_initial_load bool, which was already this idea with one bit, and Action::InitialDataLoaded folds back into DataLoaded as a third reason instead of a near-duplicate variant.

The anchor is read off TaskListComponent before sync_component_data rebuilds it, so no separate copy of the selection needs plumbing. If the sidebar moved while the load was in flight, the task simply isn't in the new list and select_task leaves the cursor alone.

Tests

test_select_task_re_anchors_the_cursor_after_a_reload asserts both halves: the middle assertion pins the pre-fix behaviour (a rebuild alone leaves the cursor on a row number, now pointing at the wrong task), then select_task puts it back on the right one.

cargo fmt, cargo clippy --all-targets, cargo test all clean.

Still open

SidebarSelection::Project(usize) is positional, so a project deleted from another client slides the sidebar selection onto a neighbour. Same disease, one level up, and now unblocked by #236. Not in this PR.

@romaintb romaintb self-assigned this Sep 5, 2026
A sync completing mid-navigation rebuilds the task list, and the cursor is a
position among selectable rows, so a task arriving above it slides the cursor
onto a different task under the user's hands.

Carry why the load was scheduled alongside it. A background sync re-anchors the
cursor to the task it was on; a load the user asked for leaves it on its row, so
marking an overdue task 'due today' does not drag the cursor into the Today
section behind it.

The reason has to travel with the load rather than sit in a field, since the
origins overlap: a sync landing while the user navigates would otherwise consume
the other's value.

LoadKind replaces spawn_data_load's is_initial_load bool, which was already the
same idea with one bit, and the InitialDataLoaded variant folds back into
DataLoaded as a third reason.
@romaintb
romaintb force-pushed the feat/selection-follows-task branch from 295d1c8 to 029ab58 Compare September 5, 2026 15:51
@romaintb
romaintb merged commit 563579a into main Sep 5, 2026
9 checks passed
@romaintb
romaintb deleted the feat/selection-follows-task branch September 5, 2026 16:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant