Skip to content

perf: increase target rate to 120fps and add dynamic throttling#6868

Merged
MitjaBezensek merged 8 commits intomainfrom
mitja/120fps
Oct 20, 2025
Merged

perf: increase target rate to 120fps and add dynamic throttling#6868
MitjaBezensek merged 8 commits intomainfrom
mitja/120fps

Conversation

@MitjaBezensek
Copy link
Copy Markdown
Contributor

@MitjaBezensek MitjaBezensek commented Oct 2, 2025

Browser will throttle us on screens that can't support 120hz (raf won't run more often than the device supports).

Change type

  • improvement

Test plan

  • Unit tests
  • End to end tests

Release notes

  • Support high refresh devices (up to 120hz)

API changes

  • fpsThrottle no accepts an optional callback which returns the target fps for the throttled function to run at. If no function is provided we will use the default fps of 120.

Note

Increase throttling to 120fps with optional per-caller FPS, use presence-aware network sync rates (1fps solo, 30fps collaborative), and show max FPS in debug panel.

  • Utils:
    • fpsThrottle: Increase default target to 120fps and accept optional getTargetFps callback for per-caller throttling. Implementation tracks per-function timing and variance; docs/comments updated. (packages/utils/src/lib/throttle.ts)
    • API: Update signature in packages/utils/api-report.api.md to include getTargetFps?: () => number.
  • Sync/Core:
    • Dynamic network FPS: Add SOLO_MODE_FPS (1) and COLLABORATIVE_MODE_FPS (30); compute via getSyncFps() and pass to fpsThrottle for flushPendingPushRequests and scheduleRebase. (packages/sync-core/src/lib/TLSyncClient.ts)
  • UI:
    • Debug panel: Display max observed FPS in DefaultDebugPanel's FPS readout. (packages/tldraw/src/lib/ui/components/DefaultDebugPanel.tsx)

Written by Cursor Bugbot for commit d7f6bfc. This will update automatically on new commits. Configure here.

@vercel
Copy link
Copy Markdown

vercel bot commented Oct 2, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
analytics Ready Ready Preview Oct 20, 2025 8:43am
examples Ready Ready Preview Oct 20, 2025 8:43am
4 Skipped Deployments
Project Deployment Preview Updated (UTC)
chat-template Ignored Ignored Oct 20, 2025 8:43am
tldraw-docs Ignored Ignored Oct 20, 2025 8:43am
tldraw-shader Ignored Ignored Preview Oct 20, 2025 8:43am
workflow-template Ignored Ignored Preview Oct 20, 2025 8:43am

💡 Enable Vercel Agent with $100 free credit for automated AI reviews

@huppy-bot
Copy link
Copy Markdown
Contributor

huppy-bot bot commented Oct 2, 2025

API Changes Check Passed

Great! The PR description now includes the required "### API changes" section. This helps reviewers and SDK users understand the impact of your changes.

@claude
Copy link
Copy Markdown

claude bot commented Oct 2, 2025

🤖 Claude Code Review

Summary

This PR increases the target FPS from 60 to 120 and adds dynamic FPS throttling based on presence mode ('solo' vs collaborative). The browser will naturally throttle via requestAnimationFrame based on display capabilities.

Code Quality ✅

Strengths:

  • Clean implementation of optional custom FPS getter parameter
  • Proper use of WeakMap for storing per-function state
  • Good backward compatibility - existing calls work without changes
  • Comments updated to reflect new behavior

Areas for Improvement:

1. Logic Issue: Duplicate Timing Checks ⚠️

In packages/utils/src/lib/throttle.ts:92-112, there's a potential issue with the throttling logic:

const throttledFn = () => {
    // Custom FPS check here (lines 94-104)
    if (getTargetFps) {
        const lastRun = customFpsLastRunTime.get(fn) ?? -Infinity
        const customTimePerFrame = Math.floor(1000 / getTargetFps()) * 0.9
        const elapsed = Date.now() - lastRun
        
        if (elapsed < customTimePerFrame) {
            return  // Early return - skips adding to queue
        }
        
        customFpsLastRunTime.set(fn, Date.now())
    }
    
    // Then adds to global fpsQueue (lines 107-111)
    if (fpsQueue.includes(fn)) {
        return
    }
    fpsQueue.push(fn)
    tick()
}

Problem: Functions with custom FPS throttling are being checked twice:

  1. First by the custom timing logic (Date.now() based)
  2. Then by the global queue/RAF system

This means scheduleRebase and flushPendingPushRequests will:

  • Check if 1000/1 or 1000/30 ms have elapsed (custom timing)
  • Still get added to the shared fpsQueue
  • Still wait for the next RAF callback based on the global 120fps timing

Impact: In 'solo' mode (1 fps target), the custom check expects ~900ms between calls, but the global queue still processes at 120fps cadence (~7ms). The custom timing might not work as intended.

Suggestion: Consider one of these approaches:

  • Option A: Custom FPS functions bypass the global queue entirely and schedule their own RAF
  • Option B: Make the global targetFps configurable per function instead of adding a separate timing layer
  • Option C: Document that custom FPS is a "minimum delay" on top of the global throttling

2. Unused WeakMap 📝

customFpsGetter is stored but never read (line 89). Either use it or remove it:

const customFpsGetter = new WeakMap<Function, () => number>()
// ...
if (getTargetFps) {
    customFpsGetter.set(fn, getTargetFps)  // Stored but never retrieved
}

3. Magic Numbers in TLSyncClient 📝

The FPS values 1 and 30 in packages/sync-core/src/lib/TLSyncClient.ts:585,695 should be constants:

const SOLO_MODE_FPS = 1
const COLLABORATIVE_FPS = 30

() => (this.presenceMode?.get() === 'solo' ? SOLO_MODE_FPS : COLLABORATIVE_FPS)

This improves readability and makes it easier to tune these values.

4. API Design Consideration 💭

The getTargetFps callback is called on every throttled invocation to get the current FPS. For presenceMode?.get(), this creates a reactive dependency each time. Consider if this is intentional or if FPS should be determined once at throttle creation time.

Performance Considerations ⚡

Positive:

  • 120fps target allows smoother animations on high-refresh displays
  • Dynamic throttling in solo mode (1fps) reduces unnecessary work
  • Browser naturally caps at display refresh rate

Concerns:

  • More frequent RAF callbacks (7ms vs 15ms) means more tick() invocations when the queue is active
  • Date.now() calls on every throttled invocation add minor overhead
  • The solo mode 1fps throttling may not work as designed (see logic issue above)

Security Concerns 🔒

No security issues identified. This is internal performance tuning code.

Test Coverage 📋

Missing:

  • No tests for the new getTargetFps parameter
  • No tests verifying custom FPS timing works correctly
  • No tests for solo vs collaborative mode behavior
  • The PR description has incomplete test plan checkboxes

Recommendations:

  • Add unit tests for fpsThrottle with custom FPS getter
  • Test that functions with 1fps target truly throttle to ~1 call/second
  • Add integration test in TLSyncClient testing solo vs collaborative presence modes
  • Consider adding a test with __FORCE_RAF_IN_TESTS__ to verify RAF behavior

API Changes ✅

API report updated correctly. The change is backward compatible (optional parameter).

Recommendations

  1. High Priority: Fix or clarify the dual-throttling behavior (custom timing + global queue)
  2. Medium: Add test coverage for new functionality
  3. Low: Extract magic numbers to constants
  4. Low: Remove unused customFpsGetter WeakMap

Verdict

The intent is good - smoother animations and smarter throttling. However, the implementation needs clarification on how custom FPS interacts with the global queue system. I'd recommend addressing the logic issue before merging.

Browser will throttle us on screens that can suport it (raf won't run
more often than the device supports).
We double checked whether they should be called.
@claude
Copy link
Copy Markdown

claude bot commented Oct 2, 2025

🤖 Claude Code Review

Summary

This PR increases the target frame rate from 60fps to 120fps and adds custom FPS throttling capabilities to fpsThrottle(). The changes allow high-refresh-rate devices to benefit from smoother updates while maintaining backwards compatibility.

Code Quality and Best Practices

Positive Aspects

  • Good API design: The optional getTargetFps callback parameter maintains backward compatibility
  • Clear documentation: JSDoc comments properly updated to reflect the 120fps target
  • Appropriate use of WeakMap for tracking per-function timing (memory-efficient, prevents leaks)
  • Smart optimization in TLSyncClient: Different FPS targets for solo mode (1fps) vs collaborative mode (30fps)

Issues and Concerns

1. Logic Bug: Premature timing update
Location: packages/utils/src/lib/throttle.ts:99

The custom FPS timing is updated BEFORE the function is queued, not when it actually runs. If the function gets queued but doesn't execute immediately, the timing will be inaccurate. The next call could be rejected even though the function hasn't actually run yet.

Recommendation: Update the timestamp AFTER the function executes, within the flush() function.

2. Mixed throttling strategies
The implementation now has TWO separate throttling mechanisms:

  • Global queue with RAF-based throttling (targetTimePerFrame)
  • Per-function timing for custom FPS (customFpsLastRunTime)

Functions with custom FPS are checked twice. Is this intentional? A function with getTargetFps() = 1 could still run up to 120 times per second if called frequently enough.

3. Performance consideration
Every call to a custom-FPS-throttled function now calls getTargetFps(), does Date.now() twice, and performs WeakMap lookups. For high-frequency calls, consider caching the getTargetFps() result.

Test Coverage

No tests found for the fpsThrottle function or these changes.

Recommendations:

  • Add unit tests for fpsThrottle with custom FPS
  • Test interaction between global and custom timing
  • Test with presenceMode switching in TLSyncClient
  • Add integration tests verifying high-refresh-rate drawing is smoother

Conclusion

The change addresses a real UX improvement for high-refresh-rate displays. However, there is a timing logic bug that should be fixed before merging. The implementation also introduces complexity with dual throttling that deserves clearer documentation.

Recommendation: Request changes to fix timing update logic and add test coverage.

@claude
Copy link
Copy Markdown

claude bot commented Oct 2, 2025

🤖 Claude Code Review

Summary

This PR increases the target FPS from 60 to 120 and adds custom per-function FPS throttling capabilities. The changes are well-structured and the implementation appears sound for supporting high refresh rate displays.

Code Quality ✅

Strengths:

  • Clean API design with backward compatibility (optional getTargetFps parameter)
  • Proper use of WeakMap to avoid memory leaks when tracking per-function state
  • Good inline comments explaining the purpose of new code
  • API documentation updated correctly

Minor concerns:

  1. Magic number in TLSyncClient (lines 585, 694): The values 1 and 30 are hardcoded. Consider extracting these as named constants:

    const SOLO_MODE_TARGET_FPS = 1
    const MULTI_MODE_TARGET_FPS = 30
  2. Inconsistent naming: customFpsLastRunTime uses "custom" prefix while customFpsGetters does too, but the variable names could be more descriptive (e.g., functionLastRunTime, functionFpsGetters)

Performance Considerations ⚠️

Potential issue - Race condition with custom FPS:

In throttle.ts:98-109, there's a timing check that happens BEFORE queuing:

if (getTargetFps) {
    const lastRun = customFpsLastRunTime.get(fn) ?? -Infinity
    const customTimePerFrame = Math.floor(1000 / getTargetFps()) * 0.9
    const elapsed = Date.now() - lastRun
    
    if (elapsed < customTimePerFrame) {
        return // Not ready yet, don't queue
    }
}

However, the timestamp is only updated in flush() (line 25). This creates a potential issue:

  • Function is called and queued (passes timing check)
  • Before flush executes, function is called again
  • Second call also passes timing check (timestamp not updated yet)
  • Function gets queued twice due to line 110 check: if (fpsQueue.includes(fn))

Wait, actually line 110 prevents duplicate queuing, so this is handled correctly. ✅

Performance benefit:

  • Early return in throttledFn (line 106-108) avoids unnecessary queue operations when throttling at custom FPS
  • This is good for the sync client use case where solo mode wants very low FPS (1 fps)

Security Concerns ✅

No security issues identified. The use of WeakMap is appropriate and prevents memory leaks.

Test Coverage ⚠️

Concerns:

  1. No direct tests for new fpsThrottle behavior: The custom FPS functionality isn't tested directly. The existing test in presenceMode.test.ts mocks fpsThrottle entirely (line 12), so it doesn't validate the new behavior.

  2. Missing test cases:

    • Custom FPS with different values
    • Behavior when getTargetFps returns varying values over time
    • Edge case: getTargetFps returning 0 or negative values (could cause division issues)

Recommendation: Add tests like:

test('fpsThrottle respects custom FPS rates', () => {
  let callCount = 0
  const fn = vi.fn(() => callCount++)
  const throttled = fpsThrottle(fn, () => 2) // 2 FPS = 500ms per frame
  
  throttled()
  expect(callCount).toBe(1)
  
  // Call immediately - should be throttled
  throttled()
  expect(callCount).toBe(1)
  
  // Wait 500ms and call - should execute
  vi.advanceTimersByTime(500)
  throttled()
  expect(callCount).toBe(2)
})

Potential Bugs 🐛

Issue 1: Division by zero risk
In throttle.ts:102, if getTargetFps() returns 0 or a very small number:

const customTimePerFrame = Math.floor(1000 / getTargetFps()) * 0.9

This could result in Infinity or unexpectedly large values. Consider adding validation.

Issue 2: Global targetTimePerFrame vs custom FPS
The global targetTimePerFrame (line 9) is now ~7ms for 120fps, but the global flush timing in tick() (line 37) still uses this global value. This means:

  • A function with custom FPS=1 will still be affected by the global 120fps flush timing
  • The custom FPS check happens BEFORE queuing, so rapid calls at 1fps will just return early
  • This is probably intended behavior, but worth documenting

API Changes 📝

The API change is documented in the PR description:

  • fpsThrottle now accepts an optional callback which returns the target fps for the throttled function to run at. If no function is provided we will use the default fps of 120.

Suggestion: Update the JSDoc in throttle.ts:68 to include an example:

* @example
* // Custom FPS based on mode
* const throttled = fpsThrottle(myFn, () => isHighPerformanceMode ? 60 : 30)

Recommendations

  1. Add input validation for getTargetFps:

    if (getTargetFps) {
        const targetFps = Math.max(1, getTargetFps()) // Ensure minimum 1 FPS
        const customTimePerFrame = Math.floor(1000 / targetFps) * 0.9
        // ...
    }
  2. Extract magic numbers in TLSyncClient to constants

  3. Add unit tests for custom FPS behavior

  4. Consider renaming WeakMap variables for clarity

Overall Assessment

Verdict: Approve with minor suggestions

The implementation is solid and achieves the goal of supporting 120hz displays. The custom FPS feature is a good addition for the sync client's solo mode optimization. The main concerns are around test coverage and input validation, which are relatively minor.

The browser will naturally throttle requestAnimationFrame on devices that don't support 120hz, so the change is safe and backward compatible.

@vercel vercel bot temporarily deployed to Preview – chat-template October 2, 2025 09:33 Inactive
@vercel vercel bot temporarily deployed to Preview – workflow-template October 2, 2025 09:33 Inactive
@claude
Copy link
Copy Markdown

claude bot commented Oct 2, 2025

🤖 Claude Code Review

Summary

This PR bumps the target frame rate from 60fps to 120fps and adds adaptive throttling for network sync operations. The changes are well-structured, but there are a few concerns to address.

Code Quality & Architecture

✅ Strengths

  • Clear separation of concerns: The custom FPS tracking is cleanly separated using WeakMaps
  • Backward compatible: The optional getTargetFps parameter maintains existing API
  • Well-documented: Constants and functions have clear documentation
  • Smart optimization: Using WeakMaps prevents memory leaks by allowing garbage collection

⚠️ Issues & Concerns

1. Potential Race Condition in Custom FPS Tracking

Location: packages/utils/src/lib/throttle.ts:98-108

The custom FPS check happens BEFORE queuing, but the timestamp update happens during flush. This creates a potential issue:

const throttledFn = () => {
    if (getTargetFps) {
        const lastRun = customFpsLastRunTime.get(fn) ?? -Infinity
        const customTimePerFrame = Math.floor(1000 / getTargetFps()) * 0.9
        const elapsed = Date.now() - lastRun
        
        if (elapsed < customTimePerFrame) {
            return  // Don't queue
        }
    }
    // ... queue the function
}

Problem: If throttledFn() is called multiple times between frames, it could queue the same function multiple times before the first execution updates the timestamp. The fpsQueue.includes(fn) check at line 110 mitigates this, but the timing check at line 105 becomes ineffective.

Recommendation: Update the timestamp when queuing, not when executing:

if (getTargetFps) {
    const lastRun = customFpsLastRunTime.get(fn) ?? -Infinity
    const customTimePerFrame = Math.floor(1000 / getTargetFps()) * 0.9
    const elapsed = Date.now() - lastRun
    
    if (elapsed < customTimePerFrame) {
        return
    }
    customFpsLastRunTime.set(fn, Date.now())  // Update here
}

And remove the timestamp update from the flush function.

2. API Inconsistency

Location: packages/utils/src/lib/throttle.ts:72-74

The API accepts () => number but the description says it "returns the current target FPS rate". Consider renaming to make it clearer:

  • getTargetFpsgetTargetFpsRate or targetFpsGetter

Or update the documentation to be more explicit about what the callback should return.

3. Memory Leak Risk with Bound Methods

Location: packages/sync-core/src/lib/TLSyncClient.ts:594, 702

private flushPendingPushRequests = fpsThrottle(() => {
    // ...
}, this.getSyncFps.bind(this))

Problem: this.getSyncFps.bind(this) creates a new function reference each time. However, since this is a class field initialization (only happens once), this is actually fine. But it's worth documenting.

Note: This is NOT a leak in the current implementation, but future maintainers should be aware not to recreate the throttled functions.

4. Missing Cleanup in Test Mode

Location: packages/utils/src/lib/throttle.ts:93-96

When getTargetFps is provided in test mode, the WeakMap entry is still created even though the throttling is bypassed:

if (isTest()) {
    // ... return unthrottled fn
    return fn
}

// This happens even in test mode
if (getTargetFps) {
    customFpsGetters.set(fn, getTargetFps)
}

Recommendation: Move the WeakMap setup before the test mode check, or skip it in test mode.

Performance Considerations

✅ Good Decisions

  • 120fps cap is reasonable: Browsers throttle RAF to display refresh rate anyway
  • Adaptive network sync: 1fps solo mode vs 30fps collaborative is a smart optimization
  • WeakMap usage: No manual cleanup needed, prevents memory leaks

⚠️ Potential Issues

  1. Double Date.now() calls: In the custom FPS path, Date.now() is called twice - once in the check and once in flush. Consider storing the timestamp once.

  2. Global vs Per-Function Throttling: The global targetTimePerFrame (line 9) is still 120fps, but individual functions can have custom rates. This means:

    • A 1fps function will still be flushed up to 120fps if other functions are queued
    • This is probably fine, but worth documenting the behavior

Security Concerns

✅ No security issues identified. The changes don't introduce any attack vectors.

Test Coverage

⚠️ Missing Tests: No test files for throttle functionality found. The PR should include tests for:

  1. Custom FPS throttling behavior
  2. Adaptive FPS in TLSyncClient
  3. Edge cases (rapid calls, FPS changes, etc.)

Recommendation: Add tests in packages/utils/src/lib/throttle.test.ts

API Changes

The API change is documented in the api-report:

export function fpsThrottle(fn: {
    (): void;
    cancel?(): void;
}, getTargetFps?: () => number): {
    (): void;
    cancel?(): void;
};

✅ Backward compatible (optional parameter)
✅ Well-typed

Recommendations

High Priority

  1. Fix the timestamp update location to prevent the race condition
  2. Add tests for the new functionality

Medium Priority

  1. Consider renaming getTargetFps for clarity
  2. Document the global vs per-function throttling interaction
  3. Move WeakMap setup to avoid dead code in test mode

Low Priority

  1. Optimize double Date.now() calls
  2. Add inline comments explaining the solo vs collaborative FPS rationale

Conclusion

The core idea is solid and the implementation is mostly clean. The main concerns are the timestamp update timing and lack of tests. Once those are addressed, this should be a good improvement for high refresh rate displays.

Overall Assessment: ⚠️ Approve with changes requested

Copy link
Copy Markdown
Member

@mimecuvalo mimecuvalo left a comment

Choose a reason for hiding this comment

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

nice! i'm down with this change.

  • nit: can we also pull the change from #6470 that updates DefaultDebugPanel.tsx‎?
  • does this work as expected on iOS?


/** Get the target FPS for network operations based on presence mode */
private getSyncFps(): number {
return this.presenceMode?.get() === 'solo' ? SOLO_MODE_FPS : COLLABORATIVE_MODE_FPS
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nice, much clearer than my version!

const targetFps = 60
const targetTimePerFrame = Math.floor(1000 / targetFps) * 0.9 // ~15ms - we allow for some variance as browsers aren't that precise.
const targetFps = 120
const targetTimePerFrame = Math.floor(1000 / targetFps) * 0.9 // ~7ms - we allow for some variance as browsers aren't that precise.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

actually, is the 0.9 still necessary? i'm forgetting a bit on why this was needed... let's expand the comment if we still need this, we're gonna forget again in the future why we did this 🙃

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a comment here.

@mimecuvalo mimecuvalo mentioned this pull request Oct 3, 2025
3 tasks
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Oct 20, 2025

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
🔵 In progress
View logs
multiplayer-template a0a5622 Oct 20 2025, 08:30 AM

@MitjaBezensek MitjaBezensek added this pull request to the merge queue Oct 20, 2025
@huppy-bot huppy-bot bot added the improvement Product improvement label Oct 20, 2025
Merged via the queue into main with commit 2e5f9f2 Oct 20, 2025
22 of 24 checks passed
@MitjaBezensek MitjaBezensek deleted the mitja/120fps branch October 20, 2025 10:08
@mimecuvalo
Copy link
Copy Markdown
Member

woo woo! 🎉

MitjaBezensek added a commit that referenced this pull request Oct 22, 2025
github-merge-queue bot pushed a commit that referenced this pull request Oct 22, 2025
This reverts commit 2e5f9f2.

### API changes
- Revert the 120 fps changes.

### Change type

- [x] `bugfix`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Standardizes throttling to 60fps (removing custom/120fps logic),
updates TLSyncClient scheduling, simplifies FPS debug display, and
adjusts the utils API.
> 
> - **Utils**:
> - Reverts `fpsThrottle` to fixed 60fps; removes custom FPS support and
related state.
> - Updates docs/comments and `throttleToNextFrame` to reference 60fps.
>   - API change: `fpsThrottle(fn)` no longer accepts `getTargetFps`.
> - **Sync Core** (`packages/sync-core/src/lib/TLSyncClient.ts`):
> - Removes dynamic sync FPS logic (`SOLO_MODE_FPS`,
`COLLABORATIVE_MODE_FPS`, `getSyncFps`).
> - `flushPendingPushRequests` and `scheduleRebase` now use
`fpsThrottle()` without FPS getter.
> - **UI**
(`packages/tldraw/src/lib/ui/components/DefaultDebugPanel.tsx`):
> - Simplifies FPS output to `FPS <current>` (removes max FPS display).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
c9995c0. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
huppy-bot bot pushed a commit that referenced this pull request Oct 22, 2025
This reverts commit 2e5f9f2.

### API changes
- Revert the 120 fps changes.

### Change type

- [x] `bugfix`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Standardizes throttling to 60fps (removing custom/120fps logic),
updates TLSyncClient scheduling, simplifies FPS debug display, and
adjusts the utils API.
> 
> - **Utils**:
> - Reverts `fpsThrottle` to fixed 60fps; removes custom FPS support and
related state.
> - Updates docs/comments and `throttleToNextFrame` to reference 60fps.
>   - API change: `fpsThrottle(fn)` no longer accepts `getTargetFps`.
> - **Sync Core** (`packages/sync-core/src/lib/TLSyncClient.ts`):
> - Removes dynamic sync FPS logic (`SOLO_MODE_FPS`,
`COLLABORATIVE_MODE_FPS`, `getSyncFps`).
> - `flushPendingPushRequests` and `scheduleRebase` now use
`fpsThrottle()` without FPS getter.
> - **UI**
(`packages/tldraw/src/lib/ui/components/DefaultDebugPanel.tsx`):
> - Simplifies FPS output to `FPS <current>` (removes max FPS display).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
c9995c0. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
huppy-bot bot added a commit that referenced this pull request Oct 22, 2025
This is an automated hotfix for dotcom deployment.

Original PR: #6985
Original Author: @MitjaBezensek
MitjaBezensek added a commit that referenced this pull request Oct 24, 2025
mimecuvalo added a commit that referenced this pull request Dec 17, 2025
@steveruizok steveruizok added sdk Affects the tldraw sdk api API change labels Jan 2, 2026
@steveruizok steveruizok changed the title Bump up the target rate to 120fps. perf: increase target rate to 120fps and add dynamic throttling Jan 2, 2026
github-merge-queue bot pushed a commit that referenced this pull request Jan 14, 2026
…7418)

once more with feeling. the previous attempts were trying to put
schedule UI and network events on the same queue, which ... I don't know
what we were thinking :P
Anyway, this creates a proper FpsScheduler class that can take different
target rates which solves the issues we were seeing. But also, it solves
the fact that even without the 120fps change, we shouldn't be combining
these queues.

I recommend reviewing this PR with "hide whitespace" on. you'll note
that it had less changes than you would expect. it's really more about
just creating a JS class to encapsulate the throttle queue.

this lays the groundwork for the child PR here that does the network bit
of this: #7657

previous PRs: #6868 to
#6470

### Change type

- [ ] `bugfix`
- [x] `improvement`
- [ ] `feature`
- [ ] `api`
- [ ] `other`

### Test plan

- [x] Unit tests (if present)
- [ ] End to end tests (if present)

### Release notes

- Improved performance by separating UI and network scheduling queues.

### API changes

- adds `FpsScheduler` to be able to create a FPS-throttled queue of
functions to execute



<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Introduce per-instance FPS scheduling**
> 
> - Add `FpsScheduler` class in `lib/throttle` with its own queue/state
and configurable target FPS; create a default 120fps instance and have
`fpsThrottle`/`throttleToNextFrame` delegate to it
> - Export `FpsScheduler` from `utils` (`src/index.ts`); update API
report accordingly
> - Add comprehensive unit tests (`lib/throttle.test.ts`) covering
throttling, next-frame batching, cancelation, and real-world scenarios
> - UI tweak: `DefaultDebugPanel` FPS readout now includes `maxKnownFps`
(`FPS ${fps} (max: ${maxKnownFps})`)
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
871ad95. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
dodo-Riley added a commit to toonsquare/tldraw that referenced this pull request Jan 15, 2026
* Add VSCode extension v2.129.0 [skip ci]

* embeds: fix replit if it has a hash present (#6892)

if a Replit URL is like:
https://replit.com/@omar/Blob-Generator#index.html it was failing.

before: `https://replit.com/@omar/Blob-Generator#index.html?embed=true`
after: `https://replit.com/@omar/Blob-Generator?embed=true#index.html`

### Change type

- [x] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [ ] `other`

### Test plan

- [x] Unit tests
- [ ] End to end tests

### Release notes

- embeds: fix replit if it has a hash present

* license: record sku in watermark ping (#6902)

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

* a11y: fix up togglegroup warning for shared state (#6904)

fix up warning in console, this happens when selecting multiple shapes,
some that don't have a `size` property set.

<img width="2594" height="998" alt="Screenshot 2025-10-09 at 12 17 55"
src="https://github.com/user-attachments/assets/1e36e023-3eef-4d48-93fc-f01be0536cb0"
/>


### Change type

- [x] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [ ] `other`

### Release notes

- a11y: fix up togglegroup warning for shared state

* Stop agent prompting when agent is stopped (via stop button) (#6901)

Fixed a bug in the AI Agent template where stopping the agent was not
possible if there are open items on the todo list

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

### Test plan

1. Prompt the AI agent
2. make sure there are multiple items in the todo list
3. hit the `stop` button
4. -> agents doesnt stop without the fix in this PR

- [ ] Unit tests
- [ ] End to end tests

### Release notes

- Fixed a bug in the AI Agent template where stopping the agent was not
possible if there are open items on the todo list

Co-authored-by: Lu Wilson <[email protected]>

* claude: fix deleting old comments (#6914)

Describe what your pull request does. If you can, add GIFs or images
showing the before and after of your change.

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

* claude: actually do the removal in a separate step (#6915)

followup to https://github.com/tldraw/tldraw/pull/6914

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

* claude: update PR perms to write (#6916)

once more, w feeling

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

* Fix convert-to-bookmark action (#6894)

Consolidated the logic we use for pasting and the conversion action, and
exposed it as a helper function for our users.

### Change type

- [x] `bugfix`

### API Changes
- Added `createBookmarkFromUrl` helper function for creating bookmark
shapes more easily.

* Improve agent: better bends, better arrows, don't stay inside the viewport (#6898)

This PR improves the agent based on some feedback.

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

### Test plan

1. Create a shape...
2.

- [ ] Unit tests
- [ ] End to end tests

### Release notes

- We made the agent better at bending and attaching arrows.
- We made the agent free to manipulate the canvas outside its viewport.

* Fix broken ink on iOS by disabling coalesced events (#6917)

## Summary

- Fixes ink/drawing issues on iOS by disabling coalesced pointer events
on that platform
- iOS sometimes doesn't have `getCoalescedEvents` available, causing
broken drawing behavior
- Adds platform check to only use coalesced events when not on iOS

## Changes

Modified `packages/editor/src/lib/hooks/useCanvasEvents.ts:166`:
- Added `!tlenv.isIos` check before using coalesced events
- This prevents attempting to use `getCoalescedEvents()` on iOS devices
where it may be unreliable

## Test plan

- [ ] Test drawing/ink tool on iOS devices
- [ ] Verify drawing works smoothly without breaks
- [ ] Ensure drawing still works correctly on other platforms (desktop,
Android)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Mime Čuvalo <[email protected]>

* Remove excalidraw embed definition, it doesn't support collaboration inside iframes (#6897)

Excalidraw stopped supporting collaboration inside iframes (pr for this
is [here](https://github.com/excalidraw/excalidraw/pull/6646)) which
means nothing syncs between the iframe inside the tldraw canvas and your
excalidraw room. This removes the usefulness of this particular embed,
so lets remove it.


### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

### API changes
- Removed the excalidraw embed 

### Release notes

- Removed the excalidraw embed

* new extract draft changelog script (#6771)

Adds a new script for extracting draft changelogs. This is based
entirely on my personal workflow for writing changelogs, which usually
involves checking each PR anyway. So this script includes almost the
entire commit message, just cleaning it up a little by removing sections
and commits we don't need.

### Change type

- [x] `other`

---------

Co-authored-by: David Sheldrick <[email protected]>

* Add marquee animation to cookie accept button (#6922)

## Summary
- Added marquee animation to the "Accept all" button text in the cookie
consent banner
- Added slide-up animation to the cookie banner itself for a smoother
entrance
- Updated button styling for better visual consistency

## Changes
- Wrapped button text in divs with animation classes
- Added CSS keyframe animations for marquee and slide-up effects
- Adjusted cookie banner entrance animation timing

## Test plan
- [x] View the cookie consent banner on tldraw.com
- [x] Verify the "Accept all" button text scrolls continuously
- [x] Verify the banner slides up smoothly on page load
- [x] Test button functionality remains intact

🤖 Generated with [Claude Code](https://claude.com/claude-code)

* analytics: add consent property to identify and ui events (#6924)

- add analytics_consent property to a person
- add UI event when changing the consent state

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

* SDK publish: Use github release draft instead of gist (#6923)

This PR makes it so we can use the github release draft system as a
source of truth for upcoming release notes, instead of pasting the notes
into a gist before hitting the deploy button.

The SDK release will fail if there isn't a draft release notes available
named after the upcoming version e.g. `v4.1.0`

For patch releases nothing changes, it generates notes on its own.

### Change type

- [x] `other`

* Update `create-tldraw` and starter kit descriptions. (#6921)

This PR:

- updates the descriptions of the starter kits
- gives explicit order to the starter kits
- in the CLI
  - moves the description to a bottom line
  - adds some links to tldraw.dev/docs
  - adds a post to an endpoint on dashboard.tldraw.pro

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Steve Ruiz <[email protected]>

* Swap Settings and Opt out buttons in cookie consent (#6925)

## Summary
Swaps the position of the "Settings" and "Opt out" buttons in the cookie
consent dialog.

## Changes
- "Opt out" button now appears first (left position)
- "Settings" button now appears second (middle position)
- "Accept" button remains in the rightmost position

## Related
Follows up on recent cookie consent UI changes (#6924, #6922)

* Add custom error capture example (#6927)

## Summary

Adds a new example demonstrating how to customize the `ErrorFallback`
component to capture and display error information from the editor.

## Changes

- New example at `apps/examples/src/examples/custom-error-capture/`
- Shows how to override the `ErrorFallback` component in the
`components` prop
- Demonstrates using `getErrorAnnotations` to retrieve debugging
information attached to errors
- Displays error annotations (tags and extras) in a scrollable UI
- Useful for integrating with error reporting services like Sentry

## Test plan

- Run the example with `yarn dev` and navigate to
`/custom-error-capture`
- Click the "Throw an error" button to trigger the custom error fallback
- Verify the custom error screen displays with error message and
annotations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

* Add slide-in and marquee animations to analytics cookie banner (#6929)

## Summary
Adds slide-in and marquee text animations to the analytics cookie
banner, matching the animations used in the dotcom client (#6925,
#6927).

## Changes
- **Slide-in animation**: Banner slides up from bottom over 5 seconds
with 2-second delay
- **Marquee animation**: "Accept all" button text scrolls continuously
- **Accessibility**: Respects `prefers-reduced-motion` setting to
disable animations for users with motion sensitivity
- **Preview HTML**: Added `index.html` for local development testing

## Implementation Details
- Slide-in uses `translateY(100%)` → `translateY(0)` with 5s ease-in-out
timing
- Marquee uses `translateX(100%)` → `translateX(-100%)` in a 3s infinite
loop
- Animations match dotcom client cookie consent styling
- `@media (prefers-reduced-motion: reduce)` disables both animations for
accessibility

## Accessibility
When users have "Reduce motion" enabled in their browser or OS settings:
- Cookie banner appears immediately without slide-in animation
- Button text remains stationary without marquee scrolling

## Test plan
1. Run `cd apps/analytics && yarn dev`
2. Visit http://localhost:5173/
3. Verify banner slides in after 2 second delay
4. Verify "Accept all" button text marquee scrolls
5. Enable "Reduce motion" in browser/OS settings and verify animations
are disabled

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

---------

Co-authored-by: Claude <[email protected]>

* dotcom hotfix sdk release infra (#6926)

adds some infra we're gonna need for tomorrow to dotcom. will merge in
the morning

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

---------

Co-authored-by: alex <[email protected]>

* Add VSCode extension v2.130.0 [skip ci]

* Add option to pass callback to onInteractionEnd (#6919)

Simplifies and standardizes the logic for handling onInteractionEnd
across SelectTool child states by using early returns and reducing
nested conditionals. This improves code readability and ensures
consistent behavior when transitioning between tool states (picked up
from @steveruizok commit).

Lets ship this change in a separate PR from the
https://github.com/tldraw/tldraw/pull/6908.

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [x] `api`
- [ ] `other`

### Release notes

- Add an option to add `onInteractionEnd` callback instead of passing a
tool state string

---------

Co-authored-by: Steve Ruiz <[email protected]>

* Add shader starter kit to docs (#6928)

This PR adds the shader starter kit to the docs site.

Still in progress: Deploying the starter kit. Don't hotfix until that's
done. See project:
https://www.notion.so/tldraw/Starter-kit-Shader-2783e4c324c080dbbaf4f8f61183556c

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Lu Wilson <[email protected]>
Co-authored-by: Steve Ruiz <[email protected]>

* [HOTFIX] Add shader starter kit to docs (#6931)

This is an automated hotfix PR for dotcom deployment.

**Original PR:** [#6928](https://github.com/tldraw/tldraw/pull/6928)
**Original Title:** Add shader starter kit to docs
**Original Author:** @TodePond

This PR cherry-picks the changes from the original PR to the hotfixes
branch for immediate dotcom deployment.

/cc @TodePond

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Adds the Shader starter kit documentation and updates the starter kits
overview to include it.
> 
> - **Docs**:
> - **New page**: `apps/docs/content/starter-kits/shader.mdx`
documenting the Shader starter kit, usage, examples, and resources.
> - **Overview update**: `apps/docs/content/starter-kits/overview.mdx`
adds `Shader` entry with description and use cases.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
ef8646b3d80a3d8454918a44e05a07e0c5a7e60b. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Lu Wilson <[email protected]>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Lu Wilson <[email protected]>
Co-authored-by: Steve Ruiz <[email protected]>

* Add VSCode extension v2.131.0 [skip ci]

* Add tldraw SDK promo link to sidebar (#6930)

## Summary
Adds a promotional link to the tldraw SDK in the sidebar.

<img width="572" height="303" alt="image"
src="https://github.com/user-attachments/assets/ea34c9ca-6d27-4418-96df-f79fa649ac8b"
/>

## Test plan
- Verify the link appears in the sidebar
- Verify the link points to the correct destination

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Adds a dismissible "Build with the tldraw SDK" link in the sidebar
with i18n and styles, and adds a safety check for extra applied
migrations in the replicator.
> 
> - **Frontend (Sidebar)**
> - **New promo link**: Adds `TlaSidebarDotDevLink` with external link
to `https://tldraw.dev`, shown in `TlaSidebar` bottom area; dismissible
via `useLocalStorageState('showDotDevLink')`.
> - **i18n**: Adds `"Build with the tldraw SDK"` string in
`public/tla/locales*.json` under key `95d2109dc8`.
> - **Styles**: Introduces `.sidebarDotDevLink` and
`.sidebarDotDevDismissButton` in `sidebar.module.css`.
> - **Backend (Replicator)**
> - **Migration validation**: In `replicatorMigrations.ts`, throws if
there are more applied migrations than defined, with counts and
offending `id`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
f9fb75469b55cf1fef2d01ec026332854355de7e. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude <[email protected]>

* Refactor shader template CSS class names for consistency (#6932)

## Summary

Fixed missing / wrong styles on the config panel. Refactored CSS class
names in the shader template to follow a more consistent BEM-like naming
convention, improving maintainability and clarity.

## Changes

- Updated all shader-related CSS classes to use consistent prefixes:
  - `shader-app__*` for app-level components
  - `shader-config-panel__*` for config panel components
- Adopted BEM-style modifiers with `--` separator (e.g.,
`shader-app__canvas--pixelated`)
- Applied naming convention across all files:
  - App.tsx: `shader-example-toggle` → `shader-app__example-menu`
  - WebGLCanvas.tsx: `shader-canvas` → `shader-app__canvas`
- ConfigPanel.tsx: `shader-config-header/content` →
`shader-config-panel__header/content`
- ConfigPanelBooleanControl.tsx: `shader-boolean-control/input` →
`shader-config-panel__control--boolean` and
`shader-config-panel__boolean-input`
- ConfigPanelSlider.tsx: `shader-slider-container` →
`shader-config-panel__control--slider`
- ConfigPanelLabel.tsx: `shader-panel-label` →
`shader-config-panel__label`
- Updated documentation in config-panel.md to reflect new class names
- Improved CSS organization and structure

## Test plan

- [x] Verified shader template still renders correctly
- [x] Confirmed config panel functionality works as expected
- [x] Checked both expanded and collapsed states
- [x] Tested all control types (sliders, booleans)

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Renames shader CSS classes to a consistent BEM scheme and updates
components, styles, and docs accordingly.
> 
> - **UI Components**:
> - `templates/shader/src/App.tsx`: `shader-example-toggle` →
`shader-app__example-menu`.
> - `templates/shader/src/WebGLCanvas.tsx` and
`templates/shader/src/fluid/FluidRenderer.tsx`: `shader-canvas` →
`shader-app__canvas` with `--pixelated` modifier.
> - Config panel components (`ConfigPanel.tsx`, `ConfigPanelSlider.tsx`,
`ConfigPanelBooleanControl.tsx`, `ConfigPanelLabel.tsx`): migrate to
`shader-config-panel__*` classes (header, content, reset button, toggle,
control variants, label).
> - **Styles (`shader.css`)**:
> - Introduce `.shader-app` container; rename selectors to
`shader-app__*` and `shader-config-panel__*`.
> - Add `shader-app__canvas--pixelated` modifier; update example menu
class.
> - Adjust config panel layout (max-height calc, padding, control
heights) and simplify selector nesting.
> - **Docs**:
> - `config-panel.md`: update class names list and example canvas class
to `shader-app__canvas`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
1e00f8dbccfcb6abb78a9f8079957f4bd592db55. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* [automated] update i18n strings (#6933)

This PR updates the i18n strings.

### Change type
- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Updates and expands i18n strings across numerous locales (new actions,
tools, menus, accessibility, media/image, navigation, toasts, etc.),
plus a minor README list formatting fix.
> 
> - **i18n/localization**:
> - Add and expand translation keys across many locales (actions,
accessibility, menus, input modes, navigation/minimap, tools,
image/media crop/alt text/zoom/replace, rotate/resize handles, text
formatting, keyboard shortcuts, toasts, statuses, arrow kinds, shapes,
style panel, UI states).
> - Include new strings for toggle options
(mouse/trackpad/auto-pan/auto-zoom), enhanced accessibility mode, and
avatar color.
> - **Docs**:
>   - Fix ordered list formatting in `templates/workflow/README.md`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
4d1fc1d5622af448fabcf6a02328db103613b1a9. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: huppy-bot[bot] <128400622+huppy-bot[bot]@users.noreply.github.com>
Co-authored-by: Mime Čuvalo <[email protected]>

* Refactor style panel pickers for inline variants (#6920)

Introduces inline variants for StylePanelButtonPicker,
StylePanelDropdownPicker, and StylePanelDoubleDropdownPicker to improve
composability and UI flexibility. Updates usage in
DefaultStylePanelContent and adjusts exports and internal structure for
better separation between toolbar-wrapped and inline picker components.

The bug was that several components included <TldrawUiToolbar> wrappers,
but were included in other rows that also include <TldrawUiToolbar>
wrappers. Nesting these would cause the bug.

An alternative solution (and a better one, though breaking) would be for
the "inline" variants shared here to be the default, and for the
<TldrawUiToolbars> to be provided where the components are used.

<img width="307" height="731" alt="image"
src="https://github.com/user-attachments/assets/2ce4b6c3-4ffd-43b8-9b08-c92a894f3e9b"
/>

### Change type

- [x] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [x] `api`
- [ ] `other`

### Test plan

1. Create lines and text shapes and geo shapes
2. Zoom out to 90% in the browser
3. Toolbar items should not wrap

### API changes
- Adds `StylePanelButtonPickerInline`,
`StylePanelDoubleDropdownPickerInline`, and
`StylePanelDropdownPickerInline`. Use these components inside of
`TldrawUiToolbar` components where needed.

### Release notes

- Fixed a bug with the style panel

* claude review: dont run in merge-queue or when merged to main (#6934)

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Add a job-level condition to skip the Claude review workflow when base
or head refs contain `gh-readonly-queue`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
2417c7c0458297e1ba505aa29a229d027b7929df. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* Fix starter kit page titles (#6937)

This PR fixes this:
<img width="688" height="217" alt="image"
src="https://github.com/user-attachments/assets/1fad1021-5ea0-4d9f-9aa3-56a5ed7fb27a"
/>


### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Update HTML page titles in `templates/shader` and
`templates/sync-cloudflare` to reflect shader and multiplayer templates.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
87503909569db4ed0d2de59a90d9015ce9928734. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* Remove a gotcha implementation detail from `fromScratch` (#6936)

This is just an internal change to remove a tricky behavior spotted by
Cursor
[here](https://github.com/tldraw/tldraw/pull/6739#discussion_r2431748869).

empty slots are usually skipped by iteration methods so all of this
works fine, see:
```ts
let arr = Array(3)

arr.push('foo')
arr.push('bar')

console.log('length', arr.length)

arr.sort((a, b) => {
    console.log('sort', { a, b })
    return a.length - b.length
})

arr.forEach((v) => {
    console.log('forEach', { v })
})
```

But the array is preallocated for no reason at all given the `.push`
usage. In cases like that a direct index-based assignment should be used
instead but it feels to me this preallocation doesn't matter anyway so I
just decided to replace it with an idiomatic `Array.from` call.

* [HOTFIX] Refactor shader template CSS class names for consistency (#6938)

This is an automated hotfix PR for dotcom deployment.

**Original PR:** [#6932](https://github.com/tldraw/tldraw/pull/6932)
**Original Title:** Refactor shader template CSS class names for
consistency
**Original Author:** @steveruizok

This PR cherry-picks the changes from the original PR to the hotfixes
branch for immediate dotcom deployment.

/cc @steveruizok

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Renames shader template CSS classes to a new BEM-style scheme and
updates components, docs, and styles (canvas, example menu, config panel
controls).
> 
> - **UI/CSS Refactor**
> - Rename shader classes to BEM-style: `shader-app__canvas` (+
`--pixelated`), `shader-app__example-menu`,
`shader-config-panel__header|__content|__reset-button|__toggle|__label|__control`
(+ `--slider`, `--boolean`).
> - Adjust styles: panel expanded `max-height` and padding; boolean
control/label layout; app container fixed positioning; remove old scoped
selectors.
> - **Components**
> - Update class names in `App.tsx`, `WebGLCanvas.tsx`,
`fluid/FluidRenderer.tsx` and config panel components (`ConfigPanel*`)
to match new classes.
> - **Docs**
> - Update `config-panel.md` to reference new class names and canvas
class in examples.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
3c7e9e1f437efcd9130674c6f7526a1cc1e6c8cf. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Steve Ruiz <[email protected]>

* Add VSCode extension v2.132.0 [skip ci]

* Fix dragging behavior when menu is open (#6918)

## Summary

- Fixes dragging behavior when a menu overlay is open
- Ensures pointer move events are properly dispatched after drag starts
- Prevents duplicate pointer down events during drag sequences

## Changes

The `MenuClickCapture` component now:
- Tracks whether a pointer down and drag has occurred while the menu is
open using a ref
- Dispatches `pointer_move` events to the editor after drag starts
- Only calls `onPointerDown` once when the drag threshold is exceeded
- Resets the drag tracking state on pointer up

This fixes an issue where dragging interactions weren't properly handled
when initiated over the menu click capture overlay.

## Test plan

- [ ] Open a menu in the editor
- [ ] Click and drag over the canvas while the menu is open
- [ ] Verify that dragging works correctly and the interaction is smooth
- [ ] Verify that pointer move events are properly tracked during the
drag

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Steve Ruiz <[email protected]>

* More shader starter style fixes (#6940)

This PR fixes various styling issues with the shader starter's config
panel.

- Spacing on boolean options.
- Restore scrolling.
- Fix tooltips.
- Fix cursor on boolean options.

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Show percentage on slider labels and adjust boolean control
spacing/cursor in the shader config panel.
> 
> - **Shader config panel**:
> - **Slider
(`templates/shader/src/config-panel/ConfigPanelSlider.tsx`)**: Display
current value as a percentage in the slider label (`label` now uses
`sliderValue.toFixed(0) + '%'`).
>   - **Boolean controls (`templates/shader/src/shader.css`)**:
> - Adjust spacing: change padding to `padding-right: var(--tl-space-4)`
on `.shader-config-panel__control--boolean`.
> - Improve usability: add `cursor: pointer` to
`.shader-config-panel__boolean-input`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
9fa66b3b3a35de3323037b2cdd1ce3cd4484ba0d. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* Change everything to sentence case (#6869)

This PR changes every title in the repo to sentence case. This will help
agents to stick to sentence case when creating things for us. For
example, agents currently like to use title case for example titles.
This is annoying because you have to go back and manually change them.

This PR also updates our agent style guides to remind them to stick to
sentence case.

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Standardizes documentation headings, UI labels, and translations to
sentence case across the repo, including adding style guidance.
> 
> - **Docs and guidelines**:
> - Convert headings and section titles to sentence case across
`CONTEXT.md` files, templates, and docs.
> - Add explicit sentence‑case guidance in `CLAUDE.md` (writing style
guidelines).
> - **UI and translations**:
> - Standardize UI labels/toast titles and submenu labels to sentence
case in `tldraw` UI components.
> - Update translation keys/values (and default translations) to
sentence case (e.g., bookmark/embed, wrap mode, people menu).
> - **Scope**: Widespread textual changes only; no functional logic
changes.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
e65ce30a16d82664cc19046c16db24d894d81a5d. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Steve Ruiz <[email protected]>

* [hotfix] starter kit titles (#6941)

This PR fixes this:
<img width="688" height="217" alt="image"

src="https://github.com/user-attachments/assets/1fad1021-5ea0-4d9f-9aa3-56a5ed7fb27a"
/>


- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> <sup>[Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) is
generating a summary for commit
d32c355d58297fa5393610a03316b01354d97839. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Lu Wilson <[email protected]>

* Add VSCode extension v2.133.0 [skip ci]

* Add e2e tests for cookie consent banner and sidebar dotdev link (#6939)

## Summary

Adds comprehensive end-to-end tests for the cookie consent banner and
sidebar dotdev link functionality, plus some related improvements.

## Changes

### New E2E Tests

- **Cookie consent banner**
(apps/dotcom/client/e2e/tests/cookie-consent.spec.ts):
  - Tests banner visibility and button presence
  - Verifies animations work correctly without `prefers-reduced-motion`
  - Verifies animations are instant with `prefers-reduced-motion`
- Tests accepting cookies hides banner and persists choice in
localStorage
  - Tests opting out hides banner and persists choice in localStorage
- Tests that banner remains hidden after page reload for both scenarios

- **Sidebar dotdev link**
(apps/dotcom/client/e2e/tests/sidebar-dotdev-link.spec.ts):
  - Tests link visibility and correct href with UTM parameters
  - Verifies clicking opens tldraw.dev in a new tab
  - Tests dismiss button functionality
- Verifies dismissal persists in localStorage and remains hidden after
reload

### Improvements

- **UTM Parameters**: Added tracking parameters to tldraw.dev links in
sidebar and menu
(`utm_source=dotcom&utm_medium=organic&utm_campaign=...`)
- **Accessibility**: Added `aria-label="Dismiss"` to sidebar dotdev
dismiss button
- **Media Query Fix**: Wrapped sidebar hover styles in `@media (hover:
hover)` to prevent issues on touch devices

## Test plan

- Run `yarn e2e-dotcom` to verify all new tests pass
- Manually test cookie consent banner on tldraw.com
- Manually test sidebar dotdev link interactions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Adds e2e tests for cookie consent and sidebar dotdev link, and updates
UI links with UTM params plus accessibility and hover behavior tweaks.
> 
> - **E2E tests**:
> - `apps/dotcom/client/e2e/tests/cookie-consent.spec.ts`: Verify banner
visibility, buttons, animation behavior (incl.
`prefers-reduced-motion`), accept/opt-out persistence in `localStorage`,
and hidden state after reload.
> - `apps/dotcom/client/e2e/tests/sidebar-dotdev-link.spec.ts`: Verify
sidebar link visibility, UTM `href`, opens in new tab, dismiss button
hides and persists via `localStorage`.
> - **UI updates**:
> - `TlaSidebarDotDevLink.tsx`: Update link to
`https://tldraw.dev?utm_source=dotcom&utm_medium=organic&utm_campaign=sidebar-link`;
add `aria-label="Dismiss"` to dismiss button.
> - `menu-items.tsx` (`DotDevMenuItem`): Use UTM URL
`...utm_campaign=sidebar-menu`.
> - `sidebar.module.css`: Scope hover animations and dismiss-button
opacity changes under `@media (hover: hover)`; keep arrow animation
defaults; adjust hover styles accordingly.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
d331ab7a721575ce9a9f99bd72fe562e57823cdb. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* [automated] update i18n strings (#6933) (#6944)

This PR updates the i18n strings.

### Change type
- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Updates and expands i18n strings across numerous locales (new actions,
tools, menus, accessibility, media/image, navigation, toasts, etc.),
plus a minor README list formatting fix.
> 
> - **i18n/localization**:
> - Add and expand translation keys across many locales (actions,
accessibility, menus, input modes, navigation/minimap, tools,
image/media crop/alt text/zoom/replace, rotate/resize handles, text
formatting, keyboard shortcuts, toasts, statuses, arrow kinds, shapes,
style panel, UI states).
> - Include new strings for toggle options
(mouse/trackpad/auto-pan/auto-zoom), enhanced accessibility mode, and
avatar color.
> - **Docs**:
>   - Fix ordered list formatting in `templates/workflow/README.md`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
4d1fc1d5622af448fabcf6a02328db103613b1a9. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup> <!--
/CURSOR_SUMMARY -->

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: huppy-bot[bot] <128400622+huppy-bot[bot]@users.noreply.github.com>

* Add VSCode extension v2.134.0 [skip ci]

* Shader: Final fixes (#6943)

This PR fixes:
- Hidden reset button in light mode.
- Non-existant CSS variable use.
- Hopefully fix production-only issue where resizes don't happen on
first page load.

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Ensure initial canvas resize on initialization and update shader
config panel styles to use tl-prefixed theme variables.
> 
> - **WebGL**:
> - Call `resize()` during `initialize()` in
`templates/shader/src/WebGLManager.ts` to set canvas size before
starting the animation loop.
> - **Styles** (`templates/shader/src/shader.css`):
> - Replace non-existent theme vars with `--tl-*` equivalents (e.g.,
`--tl-color-text`, `--tl-color-muted-2`).
> - Adjust `.shader-config-panel` and `.shader-config-panel__header`
colors/border to tl theme.
> - Simplify `.shader-config-panel__reset-button` (use text color,
remove background/hover styles).
>   - Remove `accent-color` from `.shader-config-panel__boolean-input`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
d7dafc31cc3bf30101965e19065dc57226583770. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* [HOTFIX] Refactor style panel pickers for inline variants (#6920) (#6945)

- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Introduces inline variants of style panel pickers and updates
DefaultStylePanelContent to use them with enhanced a11y, plus exports
and minor type adjustments.
> 
> - **UI • Style Panel**:
> - **Inline variants**: Add `StylePanelButtonPickerInline`,
`StylePanelDropdownPickerInline`, and
`StylePanelDoubleDropdownPickerInline`.
> - **Refactor**: Split pickers into wrapper (with toolbar/subheading)
and core inline implementations; `DefaultStylePanelContent` now uses
inline variants and shows subheadings when `enhancedA11yMode` is
enabled.
> - **Exports/APIs**:
> - Export new inline components from `src/index.ts`; API report updated
accordingly.
> - Adjust `StylePanelButtonPicker` return type to `React.JSX.Element`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
f686e6c2e98b54e56aa46ce4a66f35ffae26db2c. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Steve Ruiz <[email protected]>

* [HOTFIX] More shader starter style fixes (#6942)

This is an automated hotfix PR for dotcom deployment.

**Original PR:** [#6940](https://github.com/tldraw/tldraw/pull/6940)
**Original Title:** More shader starter style fixes
**Original Author:** @TodePond

This PR cherry-picks the changes from the original PR to the hotfixes
branch for immediate dotcom deployment.

/cc @TodePond

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Display slider value as a percentage and refine boolean control styles
in the shader template.
> 
> - **Shader UI**:
> - `ConfigPanelSlider`: slider `label` now displays the current value
as a percentage (e.g., `"75%"`).
> - `shader.css`: adjust boolean control padding to use `padding-right`
only; add `cursor: pointer` to `.shader-config-panel__boolean-input`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
f579ae4c5dc003f57f00938a2bd85be74c80556d. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Lu Wilson <[email protected]>
Co-authored-by: David Sheldrick <[email protected]>

* [HOTFIX] Shader: Final fixes (#6946)

This is an automated hotfix PR for dotcom deployment.

**Original PR:** [#6943](https://github.com/tldraw/tldraw/pull/6943)
**Original Title:** Shader: Final fixes
**Original Author:** @TodePond

This PR cherry-picks the changes from the original PR to the hotfixes
branch for immediate dotcom deployment.

/cc @TodePond

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Call resize during WebGL initialization and update shader config panel
styles to use tl color tokens with simplified button/input styling.
> 
> - **WebGL**:
> - `WebGLManager.initialize()` now calls `resize()` before starting the
animation loop to ensure canvas/viewport match current size.
> - **Styles**:
> - Migrate shader config panel colors from `--color-*` to
`--tl-color-*` (e.g., text and header border).
> - Simplify `shader-config-panel__reset-button` (remove accent
background/hover; use `--tl-color-text`).
>   - Remove `accent-color` from `shader-config-panel__boolean-input`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
c265ad460ef531f28fb9adf80e2b1cda6804621a. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Lu Wilson <[email protected]>

* Add VSCode extension v2.135.0 [skip ci]

* Bump versions to 4.1.0 [skip ci]

* Add VSCode extension v2.136.0 [skip ci]

* Bump versions to 4.1.1 [skip ci]

* Fix fresh shader starter installs (#6947)

This PR fixes the shader starter not working when you do a fresh install
from `npm create tldraw@latest`. The issue was caused by the starter
using decorators but we didn't have the right config for this in the
exported starter itself.

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

### Release notes

- Fixed fresh installs of the shader starter.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Enable TypeScript decorators for the shader template and streamline
Vite config to ensure decorator support on fresh installs.
> 
> - **Shader template**:
> - **TypeScript config**: Add `"experimentalDecorators": true` in
`templates/shader/tsconfig.json` to enable decorators.
> - **Vite config**: Simplify `templates/shader/vite.config.ts` plugin
setup to `react({ tsDecorators: true })`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
394f781e924088af77a1e81fa32fbeed32ce8e23. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* claude: omg the github api doesnt always return all comments :smh: (#6948)

Describe what your pull request does. If you can, add GIFs or images
showing the before and after of your change.

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Enhances the GitHub Actions cleanup to delete Claude review issue
comments, inline review comments, and review summaries using paginated
GH API calls.
> 
> - **CI Workflow (`.github/workflows/claude-code-review.yml`)**:
> - **Cleanup step**: Replaces single delete command with paginated,
scoped deletions using `gh api`.
>     - Deletes `issues` comments containing "🤖 Claude Code Review".
>     - Deletes `pulls` review comments (inline code comments).
>     - Deletes `pulls` review summaries.
>     - Introduces `PR_NUMBER` and `REPO` env vars for API calls.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
7ccfc25ed2b7446cd517158ae5857d3f111a79d3. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* tiptap v3 (#5717)

This updates our TipTap usage to v3 to get the latest and greatest.

- https://tiptap.dev/tiptap-editor-v3
- https://next.tiptap.dev/docs/guides/upgrade-tiptap-v2
- https://github.com/ueberdosis/tiptap/discussions/5793

### Change type

- [ ] `bugfix`
- [x] `improvement`
- [ ] `feature`
- [ ] `api`
- [ ] `other`

### Release notes

- Upgrade TipTap to version 3. If you've done any customization to our
standard TipTap kit, please refer to TipTap's Guide [How to upgrade
Tiptap v2 to v3](https://tiptap.dev/docs/guides/upgrade-tiptap-v2) as to
any breaking changes you might experience from a custom homebrew of the
rich text editor in tldraw.

* Groups data model (#6905)

Describe what your pull request does. If you can, add GIFs or images
showing the before and after of your change.

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Introduce groups (group, group_user, group_file) with owningGroupId;
update mutators, permissions, sync, and UI to support group ownership,
pinning, and dual init modes.
> 
> - **Data Model & Permissions**:
> - Add `group`, `group_user`, `group_file` tables and
`file.owningGroupId`; relax `file.ownerId` PK; new constraints,
triggers, and migrations (incl. user migration function).
> - Extend Zero schema, permissions, and types (incl.
`TlaFlags.groups_backend`).
> - **Mutators & Store**:
> - New mutators: `init`, `createFile`, `pinFile`/`unpinFile`,
`removeFileFromGroup`, `onEnterFile`; legacy paths kept but gated.
> - Optimistic store/update behavior hardened (allow insert overwrite;
no-op update guard).
> - **Sync Worker & Replicator**:
> - Subscribe/propagate via topic graph (user/group/file), include new
tables in WAL; update history migrations and add subscription backfill
migration.
> - Enforce group access for file write/connect; DOs handle group
ownership and session permissions.
> - Full-load SQL now returns group, group_user, group_file; replay and
topic building updated.
> - **Client App (dotcom)**:
> - TldrawApp supports groups: flags, group memberships, recent/pinned
via `group_file`, canUpdateFile checks, new file creation flow,
enter-file hook.
> - UI updates for pin/unpin (group-aware), menus, recent files, and
duplicate/copy flows; various handlers now route by `fileId`.
> - **Admin & E2E**:
> - Admin: migrate user to groups endpoint/UI; hard delete improvements;
testing routes to prepare users.
> - E2E: add dual init-mode runner and verification test;
env/localStorage flag wiring.
> - **Misc/Build**:
> - Adjust worker bundle limit; tsconfig moduleResolution change; utils
add `sortByMaybeIndex`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
d5afd7f75cd4113c42821fff0bfb788c2deeb859. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* fairy (#6909)

you know, for kids: https://www.youtube.com/watch?v=7G5F8ObYgjI


![fairy](https://github.com/user-attachments/assets/3a1d4392-53c6-46a4-beeb-0d0150045211)


### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [x] `feature`
- [ ] `api`
- [ ] `other`

### Release notes

- Adds ✨magic pixie dust ✨  to the canvas (internal only for now)

### API changes

- adds customizability to `DefaultDebugMenuContent` and exports some
members from `debug-flags` to assist with creating custom debug/feature
flags.
- adds `setTool`/`removeTool` to the `Editor`. Useful if you need to
add/remove a tool to the state chart on demand, after the editor has
already been initialized.




<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Adds an internal AI “Fairy” agent end‑to‑end (client UI + shared lib +
Cloudflare worker) with editor/debug hooks and infra/tooling updates.
> 
> - **AI “Fairy” System (internal)**:
> - **Client (dotcom)**: New fairy UI (HUD, sprite, vision), drag/throw
tool, agent wiring, lazy‑loaded under a feature flag; adds
`FAIRY_WORKER` env/config.
> - **Shared Lib**: New `@tldraw/fairy-shared` package (actions, prompt
parts, models, helpers, icons).
> - **Worker**: New Cloudflare `fairy-worker` (Durable Object, streaming
via Anthropic/OpenAI/Google, Clerk auth, CORS), with wrangler config and
deploy integration.
> - **Editor & UI APIs**:
>   - Add `editor.setTool` / `editor.removeTool`.
> - Expose `createDebugValue` and allow custom debug/feature flags in
`DefaultDebugMenuContent`; new Debug/Feature flags props.
> - **Tooling/Infra**:
> - Vite plugin to shim zod locales (client/templates); CSP/connect and
deploy script updated for `FAIRY_WORKER`; new ESLint rule
`no-fairy-imports`.
>   - Templates bump `ai`/`zod` and add the zod‑locale shim.
> - **Misc**: Test/asset additions (fairy sprites/placeholders),
yarn/deps updates.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
a6b5622ac281c66066109b99e7e4ecb0ec956d8c. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Lu[ke] Wilson <[email protected]>
Co-authored-by: Max Drake <[email protected]>

* docs: add podcast redirect (#6953)

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Add a permanent redirect from `/codewithjason` to `/` with UTM
campaign parameters.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
928d799bef0c9bc58aaf9014ecd95baf66176cf7. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* Use locks to prevent legacy mutations committing during/after migration (#6955)

prevent race conditions where legacy mutations may commit after the data
migration has run.

### Change type

- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Add per-user advisory locks (shared in mutations, exclusive in
migration) to prevent race conditions during groups migration.
> 
> - **Backend**:
>   - `apps/dotcom/sync-worker/src/TLUserDurableObject.ts`:
> - Before running mutators, acquire a transaction-scoped shared
advisory lock using `pg_advisory_xact_lock_shared(hashtext(userId))` to
coordinate with migration.
>   - `apps/dotcom/zero-cache/migrations/023_groups.sql`:
> - In `migrate_user_to_groups`, acquire a per-user transaction-scoped
exclusive advisory lock via
`pg_advisory_xact_lock(hashtext(target_user_id))` before migrating; add
minimal notices for lock acquisition.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
48b56b9764f05a85fd896f2300f78652082e3632. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* `useSync` custom socket api (#6859)

Alternative take on #6855.

Instead of exposing `TLSyncClient` and forcing people to re-implement
most of `useSync`, this is a little more targeted. As an alternative to
passing `uri`, you can now pass a `connect` function that returns a
`TLPersistentClientSocket`. With this hook, it's possible to use a
custom transport with tldraw sync without having to re-implement all the
complex logic around the store and presence that lives in `useSync`.

This also adds a template that uses this custom hook with socket.io.
Thanks @Digital39999 - that's mostly copied from your version.

### Change type

- [x] `api`

### API changes

- `useSync` now accepts a `connect` method, which allows creating custom
socket-like transports for tldraw sync.
- The `TLSyncClient` class now public for people who need to stray from
the happy path of `useSync`.


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Adds a connect option to useSync for custom transports, exposes a
generic/public TLPersistentClientSocket/TLSyncClient API, and includes a
Socket.IO example template.
> 
> - **Sync (useSync)**:
> - Introduces `connect` option returning a `TLPersistentClientSocket`,
as an alternative to `uri` (`UseSyncOptions` split into
`UseSyncOptionsWithUri` and `UseSyncOptionsWithConnectFn`;
`UseSyncConnectFn` exported).
> - Implementation chooses between `connect` and `uri`, wires status via
`onStatusChange`, and cleans up listeners on unmount.
>   - Re-exports new option types in `packages/sync/src/index.ts`.
> - **Sync Core (API surface and typing)**:
> - Make `TLPersistentClientSocket` public and generic:
`TLPersistentClientSocket<ClientSentMessage, ServerSentMessage>`; adds
`close()`; `onStatusChange` uses `TLSocketStatusChangeEvent`.
> - Update `ClientWebSocketAdapter` and tests to implement the new
generic interface.
> - Promote `SubscribingFn`, `TLPresenceMode`, and `TLSyncClient` to
public; rename `TlSocketStatusChangeEvent` to
`TLSocketStatusChangeEvent` (and adjust exports/usages).
> - Narrow several `TLSyncClient` members to `@internal` and refine
socket typing to `TLPersistentClientSocket<TLSocketClientSentEvent<R>,
TLSocketServerSentEvent<R>>`.
> - **Templates**:
> - Add `templates/socketio-server-example` showing a custom Socket.IO
transport (client + Node server), asset upload, and bookmark unfurling.
> - **API Reports**:
> - Update `@tldraw/sync` and `@tldraw/sync-core` API reports to reflect
new public types, renamed events, and `useSync` options.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
b848292bb5ce82e30c0aa67f207272cb8d6e23dd. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: David Sheldrick <[email protected]>
Co-authored-by: Digital39999 <[email protected]>

* robots: allow crawling favicon and static assets on dotcom (#6956)

related to Daniel's note that this was busted on google search

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Updates robots.txt to allow crawling favicon, sitemap, and common
static assets while keeping the rest of the site disallowed.
> 
> - **Robots config**: Update `apps/dotcom/client/public/robots.txt`
> - Add `Allow` rules for `/favicon*`, `/sitemap.xml`, `/*.css`,
`/*.js`, `/*.png`, `/assets/*`, and `/manifest.webmanifest`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
444ce0a1b8a1498e320353f4d70013fe7280d54a. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* robots: allow crawling favicon and static assets on dotcom (#6956) (#6958)

related to Daniel's note that this was busted on google search

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Updates robots.txt to permit indexing of favicon, sitemap, static
assets, and manifest while keeping other paths disallowed.
> 
> - **Robots** (`apps/dotcom/client/public/robots.txt`):
> - Add `Allow` rules for `/favicon*`, `/sitemap.xml`, `/*.css`,
`/*.js`, `/*.png`, `/assets/*`, and `/manifest.webmanifest`.
>   - Retain global `Disallow: /` with `Allow: /$`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
1f0771f91c9033c3cec96dcd6a4fd40758d7e0ef. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* Add VSCode extension v2.137.0 [skip ci]

* claude: use_sticky_comment (#6959)

- still too many comments, after reading the docs we should use
`use_sticky_comment` and remove all the custom logic
- also we need to have concurrency block b/c opened+synchornized can
have a race condition

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Updates the Claude review workflow to use a single sticky comment, add
concurrency control, expand PR triggers, and simplify the review prompt.
> 
> - **GitHub Actions** (`.github/workflows/claude-code-review.yml`):
>   - **Review behavior**:
> - Enable `use_sticky_comment: true` to keep a single updated summary
comment.
> - Simplify prompt to focus on correctness, security, tests, and
performance; prefer concise inline comments.
> - Remove manual cleanup step for previous comments and `claude_args`
tool allowances.
>   - **Execution controls**:
> - Add `concurrency` group `claude-review-${{
github.event.pull_request.number }}` with `cancel-in-progress: true`.
> - Expand `pull_request` triggers to include `reopened` and
`ready_for_review`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
cf0e9b28ca2d76031f7cf03ab99c13f1fc392936. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* license: narrow reporting, add markdown, add inline code (#6952)

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Enhances license reporting to scan inline licenses, generate
HTML+Markdown with a new Source column and filtered workspaces; adds CSS
license header and ignores generated markdown report.
> 
> - **License reporting** (`internal/scripts/license-report.ts`):
> - Parse and include inline license comments from source files in
reports.
> - Generate both HTML and Markdown outputs
(`license-report[-dev|-prod].{html,md}`).
> - Add `Source` column to HTML and Markdown tables; adjust HTML rows
accordingly.
> - Filter workspaces to exclude `apps/`, `internal/`, and `templates/`;
simplify parsing of `yarn workspaces list` output.
> - **Docs** (`apps/docs/app/github-dark.css`):
>   - Add MIT license attribution lines.
> - **Repo config** (`.gitignore`):
>   - Ignore generated `license-report.md`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
8900ce85d2944964e5ef5c9bd55da4cd5bf03599. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* claude: try #457 (#6960)

testing 123

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Updates the Claude code review workflow to allow specific gh Bash
tools and expands the review prompt with clearer, concise guidance.
> 
> - **CI/GitHub Actions** (`.github/workflows/claude-code-review.yml`):
> - Configure `claude_args` to allow Bash `gh` tools: `issue view`,
`search`, `issue list`, `pr comment`, `pr diff`, `pr view`, `pr list`.
> - Expand and clarify the review `prompt` (quality, bugs, performance,
security, tests; be terse; reference `CLAUDE.md`; use `gh pr comment`).
>   - Add reference comment to action usage docs.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
d51de07b465bd4eaad9c3a60e15c4dc49c2bd828. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* Remove Claude Code Review workflow (#6965)

## Summary

Removed the automated Claude Code Review GitHub Actions workflow.

## Test plan

- N/A - This is a configuration change removing an automated review
workflow

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Removes the Claude Code Review GitHub Actions workflow from
`.github/workflows`.
> 
> - **CI**:
> - Remove ` .github/workflows/claude-code-review.yml`, eliminating the
PR review job using `anthropics/claude-code-action@v1` (including its
triggers, concurrency settings, permissions, and steps).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
b646db40cc59c92d706b8ede958a23c68f3a4515. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Claude <[email protected]>

* Bump up the target rate to 120fps. (#6868)

Browser will throttle us on screens that can't support 120hz (raf won't
run more often than the device supports).

### Change type

- [x] `improvement`

### Release notes

- Support high refresh devices (up to 120hz)

### API changes

* `fpsThrottle` no accepts an optional callback which returns the target
fps for the throttled function to run at. If no function is provided we
will use the default fps of 120.


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Increase throttling to 120fps with optional per-caller FPS, use
presence-aware network sync rates (1fps solo, 30fps collaborative), and
show max FPS in debug panel.
> 
> - **Utils**:
> - **`fpsThrottle`**: Increase default target to 120fps and accept
optional `getTargetFps` callback for per-caller throttling.
Implementation tracks per-function timing and variance; docs/comments
updated. (`packages/utils/src/lib/throttle.ts`)
> - **API**: Update signature in `packages/utils/api-report.api.md` to
include `getTargetFps?: () => number`.
> - **Sync/Core**:
> - **Dynamic network FPS**: Add `SOLO_MODE_FPS` (1) and
`COLLABORATIVE_MODE_FPS` (30); compute via `getSyncFps()` and pass to
`fpsThrottle` for `flushPendingPushRequests` and `scheduleRebase`.
(`packages/sync-core/src/lib/TLSyncClient.ts`)
> - **UI**:
> - **Debug panel**: Display `max` observed FPS in `DefaultDebugPanel`'s
FPS readout.
(`packages/tldraw/src/lib/ui/components/DefaultDebugPanel.tsx`)
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
d7f6bfca9dd7756d38d5eda9082b48851b30b378. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* [automated] update i18n strings (#6966)

This PR updates the i18n strings.

### Change type
- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Adds new i18n key `95d2109dc8` ("Build with tldraw SDK") across all
locales and compiled locale files.
> 
> - **Internationalization**:
> - Add new translation key `95d2109dc8` for "Build with tldraw SDK" in
`apps/dotcom/client/public/tla/locales/*` and compiled
`locales-compiled/*` across all supported languages.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
bc1373b8d6b25e10b9883a0a042c1a53302c8744. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: huppy-bot[bot] <128400622+huppy-bot[bot]@users.noreply.github.com>
Co-authored-by: Mime Čuvalo <[email protected]>

* analytics: fix regression on properties (#6967)

@steveruizok oh i see - yeah this is a regression from last week's
change https://github.com/tldraw/tldraw/pull/6924

this app will just hotfix by itself when i land this PR

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Spread user properties in PostHog `identify` call instead of nesting
them under `properties`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
76ea674504a88eec3772566503811908a3452190. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* workers: unify origin checks (#6951)

We have disparate origin checks that are varying in domains, and we want
to tighten up asset-upload as well.

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Moves origin checks into shared utilities and applies unified
CORS/origin middleware across workers, updating the request
post-processing hook signature.
> 
> - **Shared (worker-shared)**:
> - **Origins utilities**: Add `isAllowedOrigin` and
`blockUnknownOrigins` in `packages/worker-shared/src/origins.ts` and
export from `index.ts`.
> - **Request handling**: Change `handleApiRequest` `after` hook
signature to `(response, request)` to support CORS helpers that need the
request.
> - **Workers**:
> - **asset-upload-worker**: Switch CORS `origin` to `isAllowedOrigin`;
add `blockUnknownOrigins` middleware; update to `corsify` via new hook
signature.
> - **sync-worker**: Replace local origin logic with shared
`blockUnknownOrigins`/`isAllowedOrigin`; pass `request` to `corsify`;
remove duplicated origin helpers.
> - **image-resize-worker**: Reuse shared `isAllowedOrigin` for
`isValidOrigin`; tidy imports.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
60477d1e277131dcd393836c5106ca586b568e17. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* csp: allow analytics.google.com (#6969)

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [ ] `feature`
- [ ] `api`
- [x] `other`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Adds `https://analytics.google.com` to CSP `connect-src` allowlist.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
8e97fc56a6975196d0ff02a574e133238b4f837c. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

* [HOTFIX] csp: allow analytics.google.com (#6970)

This is an automated hotfix PR for dotcom deployment.

**Original PR:** [#6969](https://github.com/tldraw/tldraw/pull/6969)
**Original Title:** csp: allow analytics.google.com
**Original Author:** @mimecuvalo

This PR cherry-picks the changes from the original PR to the hotfixes
branch for immediate dotcom deployment.

/cc @mimecuvalo

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Add `https://analytics.google.com` to CSP `connect-src` in
`apps/dotcom/client/src/utils/csp.ts`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
28ca54fab746e8d69867e83bea1ee2fe15c8b3a8. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Mime Čuvalo <[email protected]>

* Add VSCode extension v2.138.0 [skip ci]

* worker-shared: move build dependencies to devDependencies (#6968)

## Summary
- Reorganized `packages/worker-shared/package.json` to correctly
classify development and build-time dependencies
- Moved `@cloudflare/workers-types`, `typescript`, and `lazyrepo` to
`devDependencies`

## Rationale
These dependencies are only needed during development and build time,
not at runtime. This change:
- Clarifies the actual runtime dependencies of the package
- Follows best practices for dependency classification
- May reduce bundle size in environments that distinguish between
production and development dependencies

## Changes
**Moved to devDependencies:**
- `@cloudflare/workers-types` - TypeScript type definitions
- `typescript` - Build-time compiler
- `lazyrepo` - Build system tool

**Remaining in dependencies:**
- `@tldraw/utils`
- `@tldraw/validate`
- `cloudflare-workers-unfurl`
- `itty-router`
- `toucan-js`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Reclassifies build-only packages from dependencies to devDependencies
in packages/worker-shared/package.json.
> 
> - **package.json (worker-shared)**:
>   - **Dependency classification**:
> - Moved `@cloudflare/workers-types`, `typescript`, and `lazyrepo` to
`devDependencies`.
> - Kept runtime deps: `@tldraw/utils`, `@tldraw/validate`,
`cloudflare-workers-unfurl`, `itty-router`, `toucan-js`.
>   - **Dev tooling**:
>     - Ensured `vitest` remains in `devDependencies`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
cf8887d8297598c95480553061ae41777d7e0470. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude <[email protected]>

* Improve hotfix pr checking (#6972)

Add 15-minute timeout to dotcom hotfix script to prevent action
timeouts. Now throws an error (sent to Discord) if PR checks don't
complete in time, leaving 5 minutes buffer before the 20-minute GitHub
Action timeout.

### Change type

- [x] `improvement`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Adds a 15-minute timeout to the dotcom hotfix script while waiting for
PR checks, throwing an error if not ready in time.
> 
> - **Scripts**:
>   - `internal/scripts/trigger-dotcom-hotfix.ts`
> - Add 15-minute maximum wait with elapsed-time tracking when polling
PR `mergeable_state`.
>     - On timeout, log and throw an error with PR link.
>     - Retains initial 5-minute delay and 15s polling cadence.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
c3dfe88188a8437d193bfcc09…
NathanFlurry pushed a commit to rivet-dev/tldraw that referenced this pull request Jan 18, 2026
This reverts commit 2e5f9f2.

### API changes
- Revert the 120 fps changes.

### Change type

- [x] `bugfix`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Standardizes throttling to 60fps (removing custom/120fps logic),
updates TLSyncClient scheduling, simplifies FPS debug display, and
adjusts the utils API.
> 
> - **Utils**:
> - Reverts `fpsThrottle` to fixed 60fps; removes custom FPS support and
related state.
> - Updates docs/comments and `throttleToNextFrame` to reference 60fps.
>   - API change: `fpsThrottle(fn)` no longer accepts `getTargetFps`.
> - **Sync Core** (`packages/sync-core/src/lib/TLSyncClient.ts`):
> - Removes dynamic sync FPS logic (`SOLO_MODE_FPS`,
`COLLABORATIVE_MODE_FPS`, `getSyncFps`).
> - `flushPendingPushRequests` and `scheduleRebase` now use
`fpsThrottle()` without FPS getter.
> - **UI**
(`packages/tldraw/src/lib/ui/components/DefaultDebugPanel.tsx`):
> - Simplifies FPS output to `FPS <current>` (removes max FPS display).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
c9995c0. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api API change improvement Product improvement sdk Affects the tldraw sdk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants