Skip to content

Add the Meeting Tasks prototype: carried-over next actions for sync pages - #25

Draft
mattakamatsu wants to merge 3 commits into
mainfrom
roam-meeting-tasks
Draft

mattakamatsu wants to merge 3 commits into
mainfrom
roam-meeting-tasks

Conversation

@mattakamatsu

@mattakamatsu mattakamatsu commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

What this adds

prototypes/roam-meeting-tasks: the meeting-tasks widget on dg-team's sync pages, moved out of a roam/render code block and into an installable extension. A widget shows the next actions carried over from past meetings, and the other open tasks on the page. Inside a meeting block it shows the previous meeting's next actions instead.

Widgets are still placed with {{roam/render: ((uid))}}. Roam has no API for registering an inline component, so the code block those invocations point at will hold a short shim (src/shim.ts) that hands the extension an empty element to render into. Existing invocations and templates keep working; only the code block's content changes.

Why

A teammate's review found the widget was freezing meeting pages. The old component ran nine synchronous Datalog queries while Roam rendered the page. Measured on Sync / All Hands (26,728 blocks), each widget blocked for 1.6 to 2.0 s, and a page mounts several widgets. The review asked for:

review item what this PR does
Replace general-purpose queries with direct reads Four queries that bind a variable from a list (:in $ [?mu ...]) are now pull_many calls. DataScript answers those queries by scanning the whole attribute, so each took 230 to 360 ms regardless of result size; together they now take about 70 ms. The meeting and TODO/DONE scans start from the .sticky, TODO, and DONE pages' reverse references.
Load tasks asynchronously Reads use data.async.* and run after the widget renders. The page paints first and the widget shows "Loading tasks…" until its reads finish.
Fetch shared data once Widgets on the same page share one set of reads. In a live check, the first widget on All Hands loaded in about 390 ms and a third widget in 24 ms.
Render the collapsed list only when opened The collapsed section, and the block references inside it, are read and rendered on first open.
Modernize the code and separate loading from display TypeScript, const/let, and one module per concern: roam.ts (every graph read and write), model.ts and text.ts (pure rules), view.tsx (React). Each widget has its own React root, so checkbox state is ordinary component state instead of DOM patching.

Behavior changes

Behavior is kept on purpose. Output was compared with the old component on all 20 widgets on the three dg-team sync pages: the same items in both sections, the same counts, and the same footers. The differences are fixes:

  • The "From last meeting" label no longer shows date link brackets.
  • Block references nested inside other references resolve fully (the old component showed the inner ((uid))).
  • A wrapper that points to another wrapper is followed to the task.
  • Forcing meeting mode outside a meeting shows page mode instead of throwing.
  • A checkbox toggle updates every widget showing that task, and reverts if the write fails.

Not done here (needs a decision)

  • Rollout on dg-team. Nothing in the graph has changed. Switching over means adding a roam/js loader block and replacing the content of code block z4qOLRka7 with the shim. Anyone who does not load the extension then sees a one-line note instead of each widget.
  • Invocation syntax. Keeping {{roam/render}} avoids editing 26 blocks and 5 templates. An extension-owned {{meeting-tasks}} component would drop the shim but needs that migration.
  • Live refresh. The extension could now refresh a widget when its tasks change, which the roam/render version could not. That adds a pull watch per page, so it works against this PR's goal and is left out.
  • The matsulab variants (three separate roam/render components with a different header anchor and person scoping) are not ported.

0.2.0: fixes from the matsulab widgets, and a simplify pass

The three akamatsulab roam/render widgets were compared with this extension. Items that are safe for dg-team were folded in:

  • Meeting mode reads only meetings dated before the host meeting. Before, a task carried from last meeting into this meeting's next actions dropped out of "From last meeting", and tasks from the host meeting or newer ones were listed as "older open items".
  • "Last meeting" is the most recent earlier meeting with at least one task under its next actions. dg-team templates add a header to every meeting, so an empty one used to hide the section.
  • Items within a meeting follow outline order, not edit time.
  • The "From last meeting" date links to the meeting.
  • A wrapper such as ((uid)) --> by Friday keeps its note on screen and still writes to the wrapped task.
  • "Proposed next step" wording, the ℹ️ tooltip variant, a list of anchors, and cleaner item text.

Left for a product decision: person scoping across pages, checkboxes on unmarked bullets, and matsulab's three-level group headers.

A four-angle review (reuse, simplification, efficiency, altitude) then led to these changes:

  • Reverse-reference reads compare entity ids and pull only the blocks on the page. Measured on the sandbox copy of dg-team: .sticky 133-201 ms down to 23-37 ms, DONE 42-69 ms down to 29-53 ms.
  • Meetings are read two levels deep; only the header subtrees are read in full.
  • Headers are found once, one ref-chain reader serves wrappers and display text, one rule picks the latest meeting, and one definition covers the checkbox marker.
  • A Roam re-render with unchanged arguments no longer redraws the widget. Expired shared reads are dropped. The sweep timer runs only while widgets are mounted.
  • Rows memoize their display text and skip rendering when unchanged.

Skipped: moving the capability check, failure report, style injection and vitest alias into the extension-base template (outside this prototype; fourth copy across prototypes), moving the legacy-alias source check into the repo validator, and roamjs-components' roamAlphaAPI types (they type pull_many ids as uid pairs, and the entity-id reads need plain ids).

Checked on the published 0.2.0 preview:

  • Compared with the live component on all 20 dg-team sync-page widgets. 4 are identical. 15 differ only by no longer listing the host meeting's or newer meetings' tasks as "older open items". One now shows "From last meeting" where the old one showed nothing, because the meeting before it had an empty header. Cold loads took 90-323 ms; widgets reusing a read took 2-3 ms.
  • In sandbox-discourse-graphs, with the shim in a real roam/render block: the extension loaded after the widgets rendered and picked them up; checking a box wrote to the wrapped source block and updated both widgets without entering edit mode; the collapsed section loaded on first open; shift-click opened the sidebar; navigating away and back remounted cleanly.

Verification

  • pnpm test at the root passes. The prototype has 67 vitest cases: the old component's 34-check offline harness ported check for check, string rules, the shim evaluated the way Roam evaluates a roam/render block (including loading the extension after the widget rendered), a source guard against default imports from roamjs-components and the legacy roamAlphaAPI aliases, and a built-bundle check that runs onload with extensionAPI undefined. Each of three key cases was confirmed to fail when its behavior is broken on purpose.
  • pnpm typecheck (opt-in, strict), pnpm build, pnpm prepare:artifacts, and a frozen-lockfile install succeed.
  • Live on dg-team (read-only, bundle injected into one browser tab): the 20-widget comparison above, load timings, and read sharing. Roam renders the roam/render function as a component with props { args }, and window.React is 18.2, so each widget gets a createRoot root.

Notes for the reviewer

  • Tests use React 18.2 to match Roam. Adding it as a dev dependency re-resolves roamjs-components' React peer for the other prototypes in pnpm-lock.yaml from 17.0.2 to 18.2.0. Every prototype's tests pass.
  • missingCapability and reportLoadFailure in src/index.ts follow roam-inbox's copies; they are a candidate for the extension-base template.
  • Handbook DevOps items an agent cannot satisfy: no Loom. Reviewer assignment is left to whoever picks this up.

🤖 Generated with Claude Code

…ages

Ports dg-team's roam/render meeting-tasks component into an installable
extension. The roam/render code block becomes a short shim that hands the
extension an element to render into, so existing invocations and templates
keep working.

Addresses the performance review of the roam/render version:
- Reads use data.async.* and run after the widget renders, not during
  Roam's render.
- Widgets on the same page share one set of reads.
- Four list-bound Datalog queries (full attribute scans in DataScript)
  become pull_many calls; the meeting and task scans start from reverse
  references. On Sync / All Hands a widget went from 1.6-2.0 s of blocking
  work to about 390 ms after render.
- The collapsed section is read and rendered only when opened, and block
  references in item text are read in one batch.
- TypeScript modules separate reads (roam.ts), rules (model.ts, text.ts)
  and display (view.tsx).

Output matches the old component on all 20 widgets on the three dg-team
sync pages, apart from the fixes listed in the CHANGELOG.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Roam prototype previews

The preview deployment is ready. Paste a URL below into Load Developer Extensions from URL in Roam:

View publishing details

mattakamatsu and others added 2 commits September 29, 2026 00:36
Behavior, from comparing with the three akamatsulab widgets:
- Meeting mode reads only meetings dated before the host meeting, so a
  task carried from last meeting into this one stays under "From last
  meeting", and "older open items" no longer lists this meeting's tasks.
- "Last meeting" skips meetings whose next-actions header has no tasks.
- The "From last meeting" date links to the meeting.
- Items within a meeting follow outline order, not edit time.
- A wrapper such as `((uid)) --> by Friday` keeps its note on screen.
- "Proposed next step" wording, the ℹ️ tooltip variant, a list of anchors,
  and cleaner item text (components, labelled refs, mailto links).

Simplify pass (reuse, simplification, efficiency, altitude reviews):
- Reverse-reference reads compare entity ids and pull only the blocks on
  the page (3-6x cheaper for .sticky, ~30% for DONE, measured).
- Meetings are read two levels deep; only header subtrees in full.
- Headers are found once; one ref-chain reader serves wrappers and
  display text; one latest-meeting rule; one marker definition.
- A Roam re-render with unchanged args no longer redraws the widget;
  expired shared reads are dropped; the sweep runs only while mounted.
- Rows memoize display text and skip rendering when unchanged.
- `meeting` never forced anything, so the flag is now just `page`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <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.

1 participant