Skip to content

Fix TODO state detection and prevent duplicate triggers#18

Open
salmonumbrella wants to merge 6 commits intoRoamJS:mainfrom
salmonumbrella:fix/todo-state-detection
Open

Fix TODO state detection and prevent duplicate triggers#18
salmonumbrella wants to merge 6 commits intoRoamJS:mainfrom
salmonumbrella:fix/todo-state-detection

Conversation

@salmonumbrella
Copy link
Copy Markdown

@salmonumbrella salmonumbrella commented Feb 28, 2026

Summary

  • Click handler: Use .closest('.bp3-menu-item') with .querySelector for reliable context menu TODO detection, replacing fragile parent element traversal that missed clicks.
  • Settled state reads: Wrap keydown DONE/TODO detection in setTimeout(50ms) so block text is read after Roam processes state changes, preventing stale reads that caused duplicate triggers.
  • Multi-select fix: Collect block UIDs before the setTimeout to avoid race conditions with DOM changes during state settling.
  • Manual DONE detection: Add focusin/focusout tracking so blocks edited from non-TODO to DONE now fire onDone callbacks correctly.
  • Cleanup: Register focus listeners with proper removal on extension unload.

Stacked PR — depends on #17. Merge #17 first; this PR's diff will auto-update to show only its own changes.

Test plan

  • Right-click a block → select TODO from context menu → onTodo callback fires
  • Press Cmd/Ctrl+Enter to toggle TODO→DONE → no duplicate triggers
  • Select multiple blocks → Cmd/Ctrl+Enter → all blocks toggle correctly
  • Edit a plain block to start with {{[[DONE]]}} → onDone callback fires
  • Unload extension → no stale event listeners remain

Closes #8, closes #10

Summary by CodeRabbit

  • New Features

    • Improved TODO/DONE state detection with better real-time tracking and synchronization across multiple task blocks.
    • Enhanced keyboard shortcut behavior for improved task workflow efficiency.
  • Bug Fixes

    • Fixed state transition handling for archived task items.
    • Improved extension cleanup with proper listener removal on unload.

Open with Devin

When pressing Cmd/Ctrl+Enter on an ARCHIVED block, Roam prepends
TODO to get TODO+ARCHIVED. This collapses that into just TODO.

Also reads block text from Roam's data layer instead of the textarea
DOM value, which can lag behind after API updates.

Closes RoamJS#4
- Use .closest('.bp3-menu-item') for reliable context menu detection
  instead of fragile parent element traversal
- Wrap keydown DONE/TODO detection in setTimeout to read block text
  after Roam processes state changes, preventing stale reads
- Collect multi-select block UIDs before setTimeout to avoid race
  conditions with DOM changes
- Add focusin/focusout tracking to detect manual DONE transitions
  (blocks edited from non-TODO to DONE now fire onDone callbacks)
- Register cleanup for focus listeners on extension unload

Closes RoamJS#8, closes RoamJS#10
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 28, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5b99abf9-3095-425a-a8ba-50c0f433f4c7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This pull request enhances the extension's TODO and DONE state management. A new utility function normalizes TODO prefixes by removing archived markers. The main extension file introduces per-block state tracking via focusin/focusout event listeners to detect state transitions, reworks TODO menu item identification through DOM structure inspection, adds debounced batch processing to defer state detection after UI updates, and extends keyboard shortcut handling to read block text from Roam's data layer and convert ARCHIVED blocks to TODO. Cleanup logic is added to remove listeners and clear state on extension unload.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix TODO state detection and prevent duplicate triggers' directly aligns with the main objectives: fixing TODO detection and preventing duplicate task issues addressed in #8 and #10.
Linked Issues check ✅ Passed The PR implements TODO text appending (issue #8) via the onTodo handler with normalizeTodoArchivedPrefix, and prevents duplicate triggers (issue #10) through debouncing and proper state tracking with focusin/focusout listeners.
Out of Scope Changes check ✅ Passed All code changes directly support the linked issue objectives: context-menu detection improvements, timeout-based state settling, focus tracking, and TODO prefix normalization are all required to fix the stated problems.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

@salmonumbrella
Copy link
Copy Markdown
Author

@mdroidian broke these up for you! please take a look ❤️ (again)

@mdroidian
Copy link
Copy Markdown
Contributor

As per https://github.com/RoamJS/contributing/blob/main/contributing.md, could you include a video walkthrough of this fix then do a CodeRabbitAI review, please? Thank you.

@salmonumbrella
Copy link
Copy Markdown
Author

@coderabbitai full review

@salmonumbrella
Copy link
Copy Markdown
Author

This fix is related to the other two PRs and was split up across PRs 17, 18, and 19 for reviewability, so there is not a separate Loom walkthrough for this one.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 11, 2026

✅ Actions performed

Full review triggered.

@mdroidian
Copy link
Copy Markdown
Contributor

This fix is related to the other two PRs and was split up across PRs 17, 18, and 19 for reviewability, so there is not a separate Loom walkthrough for this one.

Could you please link me to the walkthrough video?

coderabbitai[bot]

This comment was marked as resolved.

@salmonumbrella
Copy link
Copy Markdown
Author

There is no walkthrough video for this split PR set. The work was broken across PRs 17, 18, and 19 for reviewability, so I do not have a single Loom link to share for this branch.

@mdroidian
Copy link
Copy Markdown
Contributor

There is no walkthrough video for this split PR set. The work was broken across PRs 17, 18, and 19 for reviewability, so I do not have a single Loom link to share for this branch.

A video walkthrough is required to help me know that the changes have been tested and are working. Could you please compile the changes locally and use Loom (or other) to show what changes are being proposed in this PR?

@salmonumbrella
Copy link
Copy Markdown
Author

salmonumbrella commented Mar 11, 2026

@mdroidian These three PRs (#17, #18, and #19) were originally one cohesive change that I split into three separate PRs at your request for reviewability. Since they're all part of the same feature set, the video walkthroughs from the other two PRs demonstrate the functionality covered here as well:

PR #17 — ARCHIVED → TODO transition fix:
Video: https://github.com/user-attachments/assets/181cefad-cbd7-4322-a2c2-94087fca65c9

PR #19 — TODONT module with configurable hotkey & bulk archive:
Video: https://github.com/user-attachments/assets/6c0de60f-ec37-48a5-820b-a394b2403363

PR #18's changes (click handler fix, settled state reads, focusin/focusout tracking) are the glue that makes the state detection reliable across both of those workflows. The videos above show the end-to-end behavior with all three PRs compiled together.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

src/index.ts Outdated
Comment on lines +336 to +338
if (initialState === "other" && getTodoState(value) === "done") {
onDone(blockUid, value);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 focusout handler can double-trigger onDone after Ctrl+Enter already handled it

When a user presses Ctrl+Enter to toggle a block's state (e.g., from no-checkbox to TODO, then to DONE), the keydownEventListener at line 419-426 fires onDone via a setTimeout. However, the initialEditStateByBlock map (set once during focusin at line 324) is never updated by the keydown handler. When the user eventually leaves the block (causing focusout), the focusoutListener at line 336 sees initialState === "other" (the state when the block was first focused) and getTodoState(value) === "done" (the current state), so it calls onDone a second time. This causes duplicate side-effects: append-text appended twice, replace-tags applied twice, and the block potentially moved to "send-to-block" destination after already being moved.

Prompt for agents
In src/index.ts, the keydownEventListener (around lines 418-426) needs to update the initialEditStateByBlock map after it processes a Ctrl+Enter toggle, so that the focusoutListener does not re-trigger onDone or onTodo. Inside the setTimeout callback (lines 419-426), after calling onDone or onTodo, update the map:

  initialEditStateByBlock.set(blockUid, getTodoState(value));

This should be added after line 424 (after the if/else if block inside the setTimeout). Similarly, for the ARCHIVED branch (lines 404-417), after the updateBlock call, set:

  initialEditStateByBlock.set(blockUid, "todo");

This ensures the focusout listener sees the state that was already handled, preventing duplicate triggers.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

salmonumbrella and others added 3 commits March 11, 2026 03:22
The domListeners Registry type expects (this: Document, ev: DocumentEventMap[...]) => void,
which is incompatible with (e: MouseEvent) => void due to parameter contravariance.
Cast to MouseEvent inside, matching the keydownEventListener pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The 50ms setTimeout + getTextByBlockUid approach caused a regression
where Roam's data layer hadn't settled, leading to stale pre-toggle
reads — onDone fired repeatedly instead of toggling, duplicating
append text. Reverting to textArea.value (which Roam updates
synchronously) fixes the toggle cycle. The ARCHIVED handler still
uses getTextByBlockUid since textarea lags for that case.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

Results in duplicate tasks On Todo does not add text

2 participants