Skip to content

Add sticky note command and UI#1

Merged
mdroidian merged 13 commits intomainfrom
codex/create-roam-research-sticky-note-extension
Feb 7, 2026
Merged

Add sticky note command and UI#1
mdroidian merged 13 commits intomainfrom
codex/create-roam-research-sticky-note-extension

Conversation

@mdroidian
Copy link
Copy Markdown
Contributor

@mdroidian mdroidian commented Feb 7, 2026

Motivation

  • Provide a lightweight, page-backed sticky note overlay so users can create quick, draggable/resizable notes that persist as Roam blocks under Roam/js/sticky-note.
  • Allow creation via the command palette with a simple Create Sticky Note command and keep layout state across sessions.

Description

  • Added new layout types and localStorage persistence for note positions and sizes via STORAGE_KEY and StickyNoteLayouts stored/loaded with getLayouts/setLayouts.
  • Ensure the backing page exists using createPage for Roam/js/sticky-note and create new note blocks with createBlock, and enumerate existing notes via window.roamAlphaAPI.q.
  • Render each note as a yellow, styled, draggable and resizable DOM element that mounts the Roam block UI with window.roamAlphaAPI.ui.components.renderBlock, supports minimize/delete actions, and updates persisted layout on drag/resize using pointer events and a ResizeObserver.
  • Add styles with addStyle, create a fixed container for notes, and register the Create Sticky Note command in the command palette; include cleanup via the extension unload return value.

Testing

  • No automated tests were run for this change.

Codex Task


Open with Devin

Summary by CodeRabbit

  • New Features

    • Draggable, resizable sticky-note widgets displayed over the page
    • Widgets render live dynamic content from Roam blocks
    • Per-note state persists across sessions with minimize, restore, and delete controls
    • Create new notes via the command palette; UI elements and commands available
  • Documentation

    • README rebranded to "Sticky Notes" with user-focused usage and feature guidance
  • Chores

    • Project renamed and package metadata/scripts updated for development workflow

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 7, 2026

Walkthrough

Adds a full Roam extension that renders draggable, resizable sticky-note widgets on a full-page overlay. Implements UI and CSS injection, per-note state persisted to localStorage, drag-to-move, minimize/restore, delete controls, and Roam block rendering inside notes. Initializes existing notes, registers styles, wires a command-palette action to create notes, exposes created elements and commands on the extension export, and performs cleanup on unload.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main change: adding sticky note functionality with both command palette integration and UI implementation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/create-roam-research-sticky-note-extension

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.

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 2 potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@src/index.ts`:
- Around line 122-131: The calls to window.roamAlphaAPI.updateBlock and
window.roamAlphaAPI.deleteBlock (used in the functions returning { titleUid,
titleText } and other places) are currently not awaited or error-handled; modify
those call sites to either await the Promise (making the enclosing function
async if it isn't) or append a .catch(...) to handle and log errors so failures
don't become unhandled rejections and the UI stays consistent; ensure you
consistently handle the Promise for updateBlock and deleteBlock across all
occurrences (e.g., in the functions that create/update/delete notes) and
surface/log any errors via the existing logger or console.
- Around line 632-635: The concise arrow functions passed to
resizeObservers.forEach and blockUnmounts.forEach implicitly return a value;
replace them with block-bodied arrow functions to make the intent explicit
(e.g., change the callbacks used in resizeObservers.forEach((observer) =>
observer.disconnect()) and blockUnmounts.forEach((unmount) => unmount()) to
block bodies that call the methods without returning anything, e.g. (observer)
=> { observer.disconnect(); } and (unmount) => { unmount(); }).
- Around line 560-568: The overlay container created as container (id
"roamjs-sticky-note-container") is missing a z-index, which can cause child
notes (even with NOTE_CLASS z-index) to be painted beneath other fixed elements;
set a high z-index on the container (e.g., container.style.zIndex =
"<high-value>") so it creates a stacking context above Roam UI, and then remove
or limit the z-index rule from the .NOTE_CLASS CSS (keep z-index only for
ordering among notes inside the container).
- Around line 324-333: The onPointerMove handler is persisting layouts on every
pointer move via updateLayout -> setLayouts causing sync localStorage writes and
jank; change behavior so onPointerMove only updates DOM position
(note.style.left/top and in-memory layout values) and move the call to persist
(updateLayout/setLayouts) into onPointerUp (or schedule it once via
requestAnimationFrame) so persistence happens once per drag end; additionally,
wrap the ResizeObserver callback persistence in a debounced function (e.g.,
200–300ms) so setLayouts is not called on every resize event but only after
resizing settles.
🧹 Nitpick comments (3)
src/index.ts (3)

73-78: Datalog query uses string interpolation for PAGE_TITLE — safe here, but fragile if the constant changes.

PAGE_TITLE is a hardcoded constant without special characters, so injection isn't a current risk. However, fetchStickyNoteUids (line 92) and getPageUid (line 75) both embed the title directly in the query string. If PAGE_TITLE ever contains quotes or special Datalog characters, these queries will break or misbehave. Consider using :in $ parameterized inputs as done in fetchBlockText (line 103) for consistency.


233-243: Canvas-based text measurement creates a new canvas element on every keystroke.

measureTitleWidth allocates a <canvas> and a 2D context each time it runs. For a title input's input event this is moderately expensive. Consider caching the canvas/context in the closure.

Proposed fix
+ const measureCanvas = document.createElement("canvas");
+ const measureCtx = measureCanvas.getContext("2d");
  const measureTitleWidth = (value: string): number => {
    const text = value.trim() || "Sticky Note";
-   const canvas = document.createElement("canvas");
-   const ctx = canvas.getContext("2d");
-   if (!ctx) {
+   if (!measureCtx) {
      return Math.max(36, text.length * 8);
    }
    const computed = window.getComputedStyle(title);
-   ctx.font = `${computed.fontWeight} ${computed.fontSize} ${computed.fontFamily}`;
-   return Math.max(36, Math.ceil(ctx.measureText(text).width) + 10);
+   measureCtx.font = `${computed.fontWeight} ${computed.fontSize} ${computed.fontFamily}`;
+   return Math.max(36, Math.ceil(measureCtx.measureText(text).width) + 10);
  };

282-297: MutationObserver on the embedded block container fires on every subtree change to hide the root title.

This observer runs hideEmbeddedRootTitle() on every DOM mutation within the block container (which can be very frequent during editing). The CSS rule at lines 522–524 (.roamjs-sticky-note__embedded-root > .roam-block-container > .rm-block-main { display: none; }) already hides the root block. Consider whether the JS-based MutationObserver approach (lines 282–292) is still necessary, or if the CSS rule alone suffices — removing it would eliminate unnecessary DOM thrashing.

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 2 new potential issues.

View 9 additional findings in Devin Review.

Open in Devin Review

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 2 new potential issues.

View 11 additional findings in Devin Review.

Open in Devin Review

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/index.ts`:
- Around line 747-776: The function createStickyNote may persist the parent
block and layout before the child content block is created, leaving dangling
state if the second createBlock throws; wrap the sequence of createBlock calls
and subsequent layout assignment/DOM creation in a try/catch so failures roll
back persisted state: create the parent and child blocks first, then compute and
assign layouts[uid] and call setLayouts and
createStickyNoteElement/container.append; on catch, remove any partial state by
deleting the parent block (call deleteBlock(uid) or your rollback API if
available) and removing layouts[uid] then call setLayouts, and rethrow or
surface the error; ensure focusStickyRenderedUidWithRetries is only called after
successful creation.
🧹 Nitpick comments (2)
src/index.ts (2)

268-293: Pending timers in focusStickyRenderedUidWithRetries are not cancelled on unload.

If the extension unloads while retry timers are still pending, the callbacks will fire on potentially removed DOM elements. Consider returning a cancel function and invoking it during cleanup — or at minimum, guard the callback against a missing container.

Suggested fix: return a cancellation handle
 const focusStickyRenderedUidWithRetries = ({
   uid,
   root,
 }: {
   uid: string;
   root: ParentNode;
-}): void => {
+}): (() => void) => {
   const timerIds: number[] = [];
   let focused = false;
   [60, 140, 280, 520, 900].forEach((delay) => {
     const timerId = window.setTimeout(() => {
       if (focused) {
         return;
       }
       focused = focusStickyRenderedUid({ uid, root });
       if (focused) {
         timerIds.forEach((id) => {
           if (id !== timerId) {
             window.clearTimeout(id);
           }
         });
       }
     }, delay);
     timerIds.push(timerId);
   });
+  return () => {
+    timerIds.forEach((id) => window.clearTimeout(id));
+  };
 };

534-547: Debug utilities attached to window should be gated behind a development check.

Exposing roamjsStickyNoteDebug on the global window in production gives users access to internal focus/ID-resolution helpers. Consider gating this behind a process.env.NODE_ENV !== "production" check, or removing it entirely if it was only needed during development.

@mdroidian mdroidian merged commit 7be3f8b into main Feb 7, 2026
1 of 2 checks passed
@mdroidian mdroidian deleted the codex/create-roam-research-sticky-note-extension branch February 7, 2026 22:01
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/index.ts`:
- Around line 780-796: The catch block in createStickyNote currently rethrows
the error after performing rollback (calling window.roamAlphaAPI.deleteBlock,
logRoamMutationError, delete layouts and setLayouts), which can create an
unhandled promise rejection when createStickyNote is used as a command palette
callback; remove the final "throw error" and instead return (or resolve) after
logging/rollback so the promise completes gracefully; ensure the rollback steps
(deleteBlock + logRoamMutationError and setLayouts) remain unchanged and that
createStickyNote returns normally from the catch path.
🧹 Nitpick comments (2)
src/index.ts (2)

295-531: createStickyNoteElement is a 230+ line function — consider extracting sub-concerns.

The function handles header construction, title measurement, drag logic, resize observation, block mounting, minimize toggling, and delete with rollback. Each of these could be a small helper, improving readability and testability. Not blocking, but worth considering for future maintainability.


564-705: Large CSS string injected inline — consider moving to a .css file.

A ~140-line template literal of CSS is harder to lint, syntax-check, and maintain than a dedicated stylesheet. If the build tooling supports it, importing a .css asset would give you editor highlighting, auto-complete, and linting for free.

Comment on lines +780 to +796
} catch (error) {
if (uid) {
try {
await window.roamAlphaAPI.deleteBlock({ block: { uid } });
} catch (rollbackError) {
logRoamMutationError({
operation: "deleteBlock",
uid,
error: rollbackError,
});
}
delete layouts[uid];
setLayouts(layouts);
}
console.error("[sticky-note] Failed to create sticky note", error);
throw error;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Re-throwing after rollback may produce an unhandled promise rejection.

createStickyNote is registered as the command palette callback (line 801). If the catch block fires, you log the error, roll back, and then throw error — but the command palette is unlikely to attach a .catch() to the returned promise, so this becomes an unhandled rejection at the top level.

Either remove the throw (the error is already logged and rolled back) or wrap the callback invocation with its own .catch().

Suggested fix
       console.error("[sticky-note] Failed to create sticky note", error);
-      throw error;
     }
🤖 Prompt for AI Agents
In `@src/index.ts` around lines 780 - 796, The catch block in createStickyNote
currently rethrows the error after performing rollback (calling
window.roamAlphaAPI.deleteBlock, logRoamMutationError, delete layouts and
setLayouts), which can create an unhandled promise rejection when
createStickyNote is used as a command palette callback; remove the final "throw
error" and instead return (or resolve) after logging/rollback so the promise
completes gracefully; ensure the rollback steps (deleteBlock +
logRoamMutationError and setLayouts) remain unchanged and that createStickyNote
returns normally from the catch path.

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