Skip to content

fix(app): keep task indent and order edits until saved - #25274

Open
stmr wants to merge 3 commits into
BasedHardware:mainfrom
stmr:contrib/5078-indent-persistence
Open

stmr wants to merge 3 commits into
BasedHardware:mainfrom
stmr:contrib/5078-indent-persistence

Conversation

@stmr

@stmr stmr commented Nov 14, 2026 •

Copy link
Copy Markdown

Summary

Refs #5078. Fixes the "changes disappear" part only; parent links and moves are out of scope.

  • _flushIndentUpdates and _flushSortUpdates cleared pending edits before the PATCH and ignored its result, so a failed save was lost.
  • Any list read that landed before the save replaced rows with server values and reverted the indent on screen.
  • Pending indent/sort edits now stay pending until the server acknowledges that exact value. List, load-more, Home and saved-task reads overlay them, and a failed write is retried on the next edit or successful list read.
  • One layout write is in flight at a time, so an older value cannot land after a newer one, and a stale acknowledgement cannot clear a newer edit.
  • The layout write state now lives in app/lib/providers/task_layout_writes.dart, so action_items_provider.dart shrinks rather than growing past its line-count ratchet.

Implementation and verification were AI-assisted.

Verification

  • flutter test --no-pub test/providers/action_items_layout_persistence_test.dart: 3 new tests pass. All 3 fail against origin/main with only the request seam added.
  • flutter test --no-pub test/providers/action_item*_test.dart test/providers/deletion_consistency_test.dart test/providers/siri_authoritative_fetch_test.dart test/perf/scroll_task_cache_test.dart: 93 passed.
  • flutter analyze on the changed provider and test: only the existing material.dart deprecation info.
  • No device run.

UI-Evidence: none -- provider persistence fix; no layout change.

Product invariants affected

none

Failure-Class: none

View guided diff

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-15T20:20:12.554362Z 4eaf458 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d72788fda8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


Future<void> flush() {
_debounce?.cancel();
return _flush ??= _sendPending().whenComplete(() => _flush = null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Queue edits made during an active flush

When a layout save remains in flight for longer than the 500 ms debounce and the user makes a newer edit, that edit's timer reaches flush() while _flush is non-null, so this expression only returns the old future. If the old request then fails, _sendPending exits and whenComplete merely nulls _flush; the newer values remain pending with no timer and are not sent unless another edit or full-list read occurs, so a final reorder or indent can be lost when the process exits. Record a follow-up flush for work queued during the active request.

Useful? React with 👍 / 👎.

Comment on lines +91 to +94
if (_sortOrders[id] == value) _sortOrders.remove(id);
});
indentLevels.forEach((id, value) {
if (_indentLevels[id] == value) _indentLevels.remove(id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fence reads that predate the layout acknowledgement

If a list request takes its server snapshot before the PATCH but completes after the PATCH succeeds, these removals clear the only overlay before that response is applied. Layout edits never advance _taskWriteRevision, so fetchActionItems still accepts the older response and _applyFetchedActionItems replaces the local row with stale order/indent values, visibly reverting an acknowledged edit until another fetch. Retain the acknowledged overlay until pre-write reads drain, or invalidate those reads when the layout write is acknowledged.

Useful? React with 👍 / 👎.

_showCompletedView = true;
_completedViewWasAutoRevealed = true;
}
_layoutWrites.retryPending();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retry pending writes after every successful task read

When a failed layout write is pending and the next successful task request is pagination via loadMoreActionItems or a saved-task point read via receiveSavedTask, neither path reaches this sole retryPending() call even though both now apply overlay(). The edit therefore remains only local unless the user later makes another edit or performs a first-page fetch; invoke the retry hook from each successful read path so restored connectivity actually persists the pending layout.

Useful? React with 👍 / 👎.

@Git-on-my-level

Copy link
Copy Markdown
Member

Verdict: the production change is correct and nicely scoped — it fixes the real data-loss path (pending indent/sort edits were cleared before the PATCH and the result ignored), and the replacement write path is sound. No blocking issues; three small race windows worth a follow-up.

What I verified:

  • app/lib/providers/task_layout_writes.dart — the core design holds up: an edit stays pending until the server acknowledges that exact value (if (_sortOrders[id] == value) _sortOrders.remove(id) stops a stale ack from clearing a newer edit), one write in flight at a time prevents an older layout landing after a newer one, and failed writes remain pending for retry. overlay()'s copyWith use is safe: the schema extension resolves sortOrder ?? this.sortOrder, so a field that is not pending (null) preserves the server value.
  • app/lib/providers/action_items_provider.dart — the overlay is applied on every read path that replaces rows (first page, home-day items, load-more, _receiveCanonicalTask), retryPending() fires after a successful first-page read, and the old pending-map clears on reset are replaced by _layoutWrites.clear() with a generation bump that also fences an in-flight write. Net −25 lines while extracting the write path — good direction for the provider's size ratchet.
  • app/test/providers/action_items_layout_persistence_test.dart — the three tests cover exactly the three guarantees claimed (read during a pending edit does not revert it; a failed write is retried after the next read; an older ack cannot clear or race a newer edit), via a completer-receipt seam with no network.
  • app/changelog/unreleased/task-indent-persistence.json — fragment present and accurate.
  • I also checked the unchanged seam: api.batchUpdateActionItems already returns Future<bool>, so the new BatchUpdateActionItemsRequest typedef matches the real API exactly.

Non-blocking follow-ups (also flagged by the bot review; I verified each against the code):

  1. task_layout_writes.dart flush() — if a flush is in flight and that in-flight send then FAILS, a newer edit whose debounce timer was consumed by that flush call has no timer left and waits for the next edit or first-page read. Latency gap, not loss — the overlay keeps the value on screen — but rescheduling on failure would close it.
  2. A list read whose server snapshot predates a successful PATCH but lands after the ack will briefly revert the acknowledged value (layout edits don't bump _taskWriteRevision). Self-heals on the next read.
  3. retryPending() is only called from the first-page read path; load-more and the saved-task read apply the overlay but never retry. Same class: delayed persistence, not loss.

Performance: no new requests — same 500 ms debounce batching into one PATCH. Security: no new surfaces; data stays on the existing action-items API. Blast radius: confined to task list layout persistence in the app.

USER IMPACT: when you indent a sub-task or reorder tasks, the change now stays put when the list refreshes and is retried instead of silently lost if the save fails.

Nothing blocking from my side; the three races above are worth a small follow-up but don't hold this fix up. Leaving for human maintainer review before merge.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@Git-on-my-level Git-on-my-level added positive-signal Automation verified a genuine fix/quality contribution flutter flutter work labels Nov 14, 2026
@stmr

stmr commented Nov 15, 2026

Copy link
Copy Markdown
Author

Thanks for the review. I addressed all three follow-ups in ff4aa818:

  1. Edit made during a failing in-flight save (Codex P1): flush() now records a request that arrives while a send is in flight. If that send fails and edits are still pending, it schedules a new attempt, so the newer edit no longer waits for another edit or a first-page read. A send that keeps failing is not retried in a loop.
  2. Read whose snapshot predates the ack: each read captures a readMark before it starts. An acknowledged value stays overlaid on rows from reads that started before that acknowledgement. Reads started after it use the server value.
  3. Retry after every successful read: retryPending() now also runs after load-more, the Home read and the saved-task point read.

Tests: three new cases in action_items_layout_persistence_test.dart, one per item. All three fail on d72788fd and pass now (6/6). The related provider suites pass (96), flutter analyze shows only the existing material.dart deprecation info, and local pr_preflight.py passes 27/27.

@stmr

stmr commented Nov 15, 2026

Copy link
Copy Markdown
Author

Follow-up in 4eaf458e: a successful read that lands while a layout save is still in flight is now remembered, so if that save then fails the pending edit is retried. Before this, retryPending() ignored the read whenever a send was in flight. New test a successful read during a write that then fails still retries it fails on ff4aa818 and passes now. Layout tests 7/7, related provider suites 97/97, analyze clean, local preflight 27/27.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

flutter flutter work positive-signal Automation verified a genuine fix/quality contribution

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants