fix(oxlint/cli): load root config by searching up parent directories#22272
Merged
graphite-app[bot] merged 1 commit intoMay 11, 2026
Conversation
Member
Author
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
Sysix
force-pushed
the
05-09-fix_oxlint_cli_load_root_config_by_searching_up_parent_directories
branch
from
May 9, 2026 12:43
ffb00da to
ca12e68
Compare
Sysix
marked this pull request as ready for review
May 9, 2026 12:46
camc314
approved these changes
May 11, 2026
Contributor
Merge activity
|
…22272) Allow users to run `oxlint` in a sub package, when the monorepo has a base config. Example: `cd project/packages/pack-a && oxlint` Now it detects the config from `project/.oxlintrc.json`, aligning how the LSP side works. Aligns too how oxfmt searches config path. closes #21533 found in voidzero-dev/vite-plus#1378 (comment)
graphite-app
Bot
force-pushed
the
05-09-fix_oxlint_cli_load_root_config_by_searching_up_parent_directories
branch
from
May 11, 2026 08:09
ca12e68 to
6b50f23
Compare
graphite-app
Bot
deleted the
05-09-fix_oxlint_cli_load_root_config_by_searching_up_parent_directories
branch
May 11, 2026 08:13
camc314
added a commit
that referenced
this pull request
May 11, 2026
# Oxlint ### 💥 BREAKING CHANGES - 00ce512 oxlint/lsp: [**BREAKING**] Don't fix suggestions on fixAll code actions & command (#22195) (Sysix) ### 🚀 Features - 0eeceaf linter/no-unused-vars: Rename parameter with initializer (#22308) (camc314) - fa0232b linter/no-unused-vars: Add param rename suggestion (#22285) (Ryota Misumi) - ae59305 linter/promise/no-promise-in-callback: Add `exemptDeclarations` option (#22275) (Mikhail Baev) - 60bed4a linter: Extends `no-redundant-roles` and `prefer-tag-over-role` support roles (#22069) (mehm8128) - 545c80f linter/eslint: Implement `prefer-regex-literals` rule (#22192) (Mikhail Baev) - cf86d7a linter: Bulk suppression (#19328) (Said Atrahouch) - 23abd22 linter/jsx-a11y: Implement no-noninteractive-element-to-interactive-role (#21264) (Pedro Tainha) - fbb8f22 linter: Support `ignores` in overrides (#22148) (camc314) - 5a4414d oxlint/lsp: Support `rulesCustomization` lsp option (#21858) (Sysix) ### 🐛 Bug Fixes - 610f4c7 linter/no-unused-vars: Avoid renaming captured vars (#22310) (camc314) - 6b50f23 oxlint/cli: Load root config by searching up parent directories (#22272) (Sysix) - 31a5de7 linter: Rename override `ignores` to `excludeFiles` (#22283) (camc314) - 26d5d7b linter: Add missing vitest/valid-describe-callback functionality (#22279) (camchenry) - 784530f linter: `valid-title`: detect `String.raw` strings (#22271) (Sysix) - 080d90e linter: Move `no-debugger` fix to suggestion (#22256) (Sysix) - 25b7017 linter: Undocument override `ignores` option (#22213) (camc314) - 7bb00dd linter: Fix role-has-required-aria-props (#22097) (mehm8128) - d25279e linter/disable-directives: Improve parsing of names, descriptions (#22184) (camc314) - a59e447 linter/disable-directives: Ignore invalid enable suffixes (#22179) (camc314) - aafef0f ci: Disable bulk supression test on big endian (#22175) (camc314) - 281daec linter/vue/define-props-destructuring: Add `only-when-assigned` config opt (#22142) (camc314) - 46ab679 linter/plugins: Trim leading newline for partial sources (#20928) (bab) - 29ff6d9 linter: Update docs for no_alias_methods rule to be Vitest-specific and add toThrowError alias (#22129) (camchenry) ### ⚡ Performance - 9414bee linter/role-has-required-aria-props: Avoid intermediate vec (#22212) (camc314) - 3883ea3 linter/no-useless-escape: Drop unnecessary Vec collect (#22171) (connorshea) - 42c3029 linter/check-property-names: Replace split-collect-pop-join with rfind (#22172) (connorshea) - 9551d53 linter: Remove unnecessary Vec collect in CFG edge traversal (#22167) (connorshea) - 26fa2fc linter/aria-role: Remove unnecessary string allocations in run method (#22168) (connorshea) - c9ce045 linter/getter-return: Remove unnecessary Vec collect in CFG edge traversal (#22166) (connorshea) - 72bd846 linter/no-this-in-sfc: Reorder cheap name check, avoid String allocation (#22164) (connorshea) ### 📚 Documentation - 4da212a linter/no-unused-vars: Add docs to `rename_unused_function_parameter` (#22311) (camc314) - 27c4628 linter/forbid-dom-props: Escape jsx examples in lint rule docs (#22254) (4MBL) - 3f81147 linter: Improve the `react/jsx-key` rule docs. (#22162) (connorshea) - 07f03cc linter/consistent-return: Add note about `noImplicitReturns` coverage (#22156) (camc314) - 7c1e049 oxlint/lsp: Improve autogenerated lsp docs (#22154) (Sysix) - 87b3e38 linter: Update docs to be vitest-specific for consistent-test-it (#22128) (camchenry) # Oxfmt ### 💥 BREAKING CHANGES - 5c6c390 oxfmt: [**BREAKING**] Respect more git ignore options, align with Oxlint (#22210) (leaysgur) ### 🚀 Features - 6e8e818 oxfmt: Experimental .svelte support (#21700) (leaysgur) ### 🐛 Bug Fixes - e2a20b6 formatter: Add space after commas in import attributes (#22274) (Leonabcd123) ### ⚡ Performance - b756682 oxfmt: Optimize nested config prescan (#22232) (Jovi De Croock) - f14e81e formatter/sort_imports: Skip sort for single import runs (#22204) (leaysgur) - 32255b1 formatter: Process `ImportDeclaration`s in a run (#22079) (overlookmotel) ### 📚 Documentation - 4da6f4c formatter: Correct comment (#22217) (overlookmotel) - ef3507d formatter/sort_imports: Refresh docs (#22203) (leaysgur) Co-authored-by: Cameron <[email protected]>
frolic
added a commit
to tldraw/tldraw
that referenced
this pull request
May 27, 2026
The worktree config bug was fixed upstream in oxlint 1.64 (oxc-project/oxc#22272 "load root config by searching up parent directories"). Bumping from 1.58 to 1.66 resolves it at the source, so the `--disable-nested-config` workaround added in the previous commit is no longer needed and is reverted here. Verified by re-running `oxlint <file>` from inside `.claude/worktrees/<name>/` with the plain `oxlint` command (no flag): exits 0, no parse error. Full repo `oxlint .` also passes on 1.66.
1 task
pull Bot
pushed a commit
to limerencel/tldraw
that referenced
this pull request
May 29, 2026
…raw#8945) In order to let pre-commit hooks run cleanly from inside git worktrees, this PR bumps `oxlint` from 1.58.0 to 1.66.0. ### The bug When a worktree lives under the main repo (for example `.claude/worktrees/<name>/`), oxlint 1.58 walks up from the file being linted, finds the worktree's `.oxlintrc.json`, and then keeps walking and also finds the main repo's `.oxlintrc.json`. It treats one of the two identical configs as a "nested" config and rejects `options.typeAware`: ``` The `options.typeAware` option is only supported in the root config, but it was found in /Users/.../.oxlintrc.json ``` The net effect: the lint-staged step in `.husky/pre-commit` blows up for every commit made from inside a worktree, regardless of what the diff is, which forces contributors (and Claude Code agents) to use `git commit --no-verify`. ### The fix Upstream fixed this in oxlint 1.64 — [oxc-project/oxc#22272](oxc-project/oxc#22272) ("load root config by searching up parent directories"). Bumping to 1.66 (latest available in our registry mirror) picks up the fix at the source. No config or script changes needed. The earlier commit on this branch added a `--disable-nested-config` workaround flag to `lint-staged` and `internal/scripts/lint.ts`; the follow-up commit reverts it now that the bump makes it unnecessary. ### Change type - [x] `other` ### Test plan 1. `git worktree add .claude/worktrees/test-worktree` 2. `cd .claude/worktrees/test-worktree && yarn install` 3. `yarn oxlint packages/tldraw/src/lib/ui/hooks/useClipboardEvents.ts` — exits 0, no `options.typeAware` parse error. 4. Make any trivial change inside the worktree, `git add` it, and `git commit` (without `--no-verify`) — pre-commit hook completes cleanly. ### Code changes | Section | LOC change | | -------------- | ------------ | | Config/tooling | +85 / -85 | (`package.json` +1 / -1, `yarn.lock` +84 / -84.)
kaznaan
added a commit
to lumalabs/tldraw
that referenced
this pull request
Jul 6, 2026
* fix(docs): canonical URLs for SEO — metadata, sitemap, and footer links (#8952) Emit rel=canonical on every article via alias→canonical mapping (quick-start, installation, releases, examples, starter-kits). Filter sitemap to canonical paths only. Point footer, category menu, and 404 nav at canonical hrefs instead of URLs that 301. ### Change type - [ ] `bugfix` - [x] `improvement` - [ ] `feature` - [ ] `api` - [ ] `other` * fix(docs): fixed the quick-start and some more redirections (#8959) Seems I went a bit too quickly with the previous work on docs and canonical urls and actually removed the thing that makes the quick-start page actually work ### Change type - [x] `bugfix` - [ ] `improvement` - [ ] `feature` - [ ] `api` - [ ] `other` * fix(infra): restore workspace:* rewrite in published tarballs (#8954) ## Summary The latest few `tldraw@next` versions on npm ship with `"workspace:*"` literals in their `package.json`, which breaks installs for any consumer outside this monorepo: ``` $ yarn up tldraw@next Error: @tldraw/driver@workspace:*: Workspace not found (@tldraw/driver@workspace:*) ``` Inspecting the broken tarball confirms the source `package.json` (tab-indented, `workspace:*` deps) is shipped untouched: ``` $ npm pack [email protected] $ tar -xzOf tldraw-5.1.0-next.d83788c14b6f.tgz package/package.json | grep '@tldraw/' "@tldraw/driver": "workspace:*", "@tldraw/editor": "workspace:*", "@tldraw/store": "workspace:*", ``` Comparing to a prior good publish (`5.1.0-next.d7c83ba698ae`) shows the expected output — 2-space-indented `package.json` with rewritten npm versions. The registry metadata also tells the story: | | Good (`d7c83ba698ae`) | Bad (`d83788c14b6f`) | |---|---|---| | `_npmUser` | `tldraw-personal <[email protected]>` | `GitHub Actions <[email protected]>` | | `_npmVersion` | (yarn) | `11.8.0` (npm CLI) | | provenance | none | OIDC attestation | ## Root cause PR #8913 swapped the publish call in `internal/scripts/lib/publishing.ts` from `yarn npm publish` to `npm publish`. The PR motivation — switching to npm's OIDC trusted-publisher flow — was correct, but the swap had a silent side effect: `yarn npm publish` rewrites `workspace:*` dependency specifiers into concrete sibling versions during pack, and `npm publish` doesn't (the workspace protocol is a yarn-specific extension that npm has no concept of). The swap turns out to have been unnecessary. **Yarn 4.10** ([yarnpkg/berry#6898](https://github.com/yarnpkg/berry/pull/6898), Oct 2025) added native OIDC trusted-publishing support. When `yarn npm publish` runs in GitHub Actions with `permissions: id-token: write` granted, yarn requests the GitHub-issued OIDC token, exchanges it with the npm registry, and publishes — exactly like the npm CLI does. Provenance attestations are produced automatically. This repo is already on yarn 4.12 via the `packageManager` field, so OIDC was supported the whole time #8913 was being written. ## Change This PR reverts only the publish-call change from #8913 and updates the surrounding comment to reflect the yarn-driven OIDC path. The workflow-side changes from #8913 (`permissions: id-token: write` on each publish job, removal of `NPM_TOKEN` env, trusted-publisher configuration on npmjs.com) are unchanged and remain required. `--tolerate-republish` is restored alongside the yarn invocation since it's a yarn-specific flag; the substring-match catch block (added in #8913 to compensate for npm CLI not having `--tolerate-republish`) is kept as a belt-and-braces defense, because the original code comment notes `--tolerate-republish` is unreliable for canary versions. ## Test plan - [ ] Merge to `main` and watch `publish.yml` run `canary` mode end-to-end on the next push to `main`. - [ ] After publish, `npm pack <new-canary-version>` and confirm dependency specifiers in the tarball's `package.json` show concrete versions (e.g. `"@tldraw/editor": "5.1.0-canary.<sha>"`), not `"workspace:*"`. - [ ] Confirm the new canary version still shows a provenance attestation badge on npmjs.com (yarn 4.10+ produces these automatically under OIDC). - [ ] In a consumer repo, run `yarn up tldraw@canary` and verify it resolves without the "Workspace not found" error. After this lands, the `5.1.0-next.*` versions published since #8913 merged are still broken on npm; the registry doesn't allow unpublishing after 72 hours so they'll need to be left as-is, with the next good `next` publish (post-merge) being the recommended upgrade target. * fix(ci): tolerate Supabase CLI branch-provisioning output in deploy preview (#8964) In order to stop the "Deploy .com to preview" workflow from failing on the "Create Supabase preview branch" step, this PR makes the branch-provisioning wait loop tolerant of the Supabase CLI's output while a branch is still being created. The Supabase CLI [v2.102.0](https://github.com/supabase/cli/releases/tag/v2.102.0) [ported the `branches` commands from Go to native TypeScript](https://github.com/supabase/cli/pull/5374). Because our workflow installs the CLI with `version: latest`, CI picked this up automatically. The rewrite changed what `supabase branches get -o json` writes to **stdout** while a branch is still provisioning: the old (Go) CLI wrote empty stdout there, but the new (TS) CLI renders a non-JSON error/status line. The workflow piped that straight into an unguarded `jq`, which exited with code 5 on the parse error, and since the step runs under `set -e` the whole step aborted on the first loop iteration — before the retry loop could wait for the branch to become ready. This was a CLI behavior change, not a regression in our code: the script had been unchanged since the [Supabase preview-DB migration](https://github.com/tldraw/tldraw/pull/8066), and the same workflow passed on older CLI versions. The fix adds `2>/dev/null || true` to `supabase branches get` and to both `jq` calls so that, while the branch is provisioning, non-JSON / non-zero output is treated as "not ready yet" and the loop keeps retrying as it was always meant to. Once the branch is ready the CLI returns valid JSON, `jq` extracts `POSTGRES_URL`, and the loop breaks normally. Field names (`POSTGRES_URL`, `POSTGRES_URL_NON_POOLING`) are unchanged in the new CLI, so no other adjustments are needed, and the CLI stays on `latest`. ### Change type - [x] `other` (CI) ### Test plan 1. Open a PR that triggers the "Deploy .com to preview" workflow with a brand-new preview branch (one that has to provision from scratch). 2. Confirm the "Create Supabase preview branch" step logs "Waiting for branch to be ready (i/24)..." while provisioning and then succeeds, instead of failing with `jq: parse error` / exit code 5. ### Code changes | Section | LOC change | | -------------- | ---------- | | Config/tooling | +8 / -3 | * chore(deps): bump oxlint to 1.66 to fix worktree pre-commit hook (#8945) In order to let pre-commit hooks run cleanly from inside git worktrees, this PR bumps `oxlint` from 1.58.0 to 1.66.0. ### The bug When a worktree lives under the main repo (for example `.claude/worktrees/<name>/`), oxlint 1.58 walks up from the file being linted, finds the worktree's `.oxlintrc.json`, and then keeps walking and also finds the main repo's `.oxlintrc.json`. It treats one of the two identical configs as a "nested" config and rejects `options.typeAware`: ``` The `options.typeAware` option is only supported in the root config, but it was found in /Users/.../.oxlintrc.json ``` The net effect: the lint-staged step in `.husky/pre-commit` blows up for every commit made from inside a worktree, regardless of what the diff is, which forces contributors (and Claude Code agents) to use `git commit --no-verify`. ### The fix Upstream fixed this in oxlint 1.64 — [oxc-project/oxc#22272](https://github.com/oxc-project/oxc/pull/22272) ("load root config by searching up parent directories"). Bumping to 1.66 (latest available in our registry mirror) picks up the fix at the source. No config or script changes needed. The earlier commit on this branch added a `--disable-nested-config` workaround flag to `lint-staged` and `internal/scripts/lint.ts`; the follow-up commit reverts it now that the bump makes it unnecessary. ### Change type - [x] `other` ### Test plan 1. `git worktree add .claude/worktrees/test-worktree` 2. `cd .claude/worktrees/test-worktree && yarn install` 3. `yarn oxlint packages/tldraw/src/lib/ui/hooks/useClipboardEvents.ts` — exits 0, no `options.typeAware` parse error. 4. Make any trivial change inside the worktree, `git add` it, and `git commit` (without `--no-verify`) — pre-commit hook completes cleanly. ### Code changes | Section | LOC change | | -------------- | ------------ | | Config/tooling | +85 / -85 | (`package.json` +1 / -1, `yarn.lock` +84 / -84.) * fix(dotcom): always deploy Zero for dotcom previews (#8967) Closes #8965 ## What Dotcom previews now always deploy a single-node Zero backend, so the `@tldraw.com` "proper Zero" opt-in is valid on every preview. Previously a plain `dotcom-preview-please` deploy set `DEPLOY_ZERO=false` unless the `preview-flyio-zero-deploy-please` label was present, but the client still opted any `@tldraw.com` user into proper Zero — pointing staff at the placeholder backend `zero-backend-not-deployed.tldraw.com`, which doesn't exist for the preview. This takes option 2 from the issue (deploy Zero for previews) rather than gating the opt-in client-side. ## Changes - **`.github/workflows/deploy-dotcom.yml`** — "Determine zero deploy target": any PR preview now sets `DEPLOY_ZERO=flyio` (single-node). Removed the `preview-flyio-zero-deploy-please` label check, the commented-out auto-label block, and the diff-based `apps/dotcom/` fallback that resolved to `false`. `production`/`main` stay on `flyio-multinode`. - **`internal/scripts/deploy-dotcom.ts`** — `getZeroUrl()` preview case now always returns the deployed fly URL; removed the dead `zero-backend-not-deployed` preview branch. ## Pruning / cleanup No prune changes needed: each preview creates `pr-<n>-zero-cache`, which already matches `prune-preview-deploys.ts`'s `ZERO_CACHE_APP_REGEX` and is destroyed daily once the PR has been closed for >2 days. ## Follow-ups - Delete the now-unused `preview-flyio-zero-deploy-please` GitHub label (not referenced anywhere in code). - Every dotcom preview now provisions a fly `zero-cache` app + runs Zero migrations against its Supabase branch DB — small added per-preview cost/latency. ## Test plan 1. Open a PR and apply `dotcom-preview-please`. 2. Sign into the preview with a `@tldraw.com` account. 3. Confirm the console logs `[Zero] Using proper Zero (@tldraw.com email)` against the preview's `pr-<n>-zero-cache.fly.dev` backend and that Zero-backed features work. * feat(dotcom): link signed-in logo to tldraw.dev and track logo clicks (#8979) In order to extend the tldraw.com → tldraw.dev logo link to signed-in users, this PR makes the sidebar workspace logo an external link to tldraw.dev, and routes both logo links through the shared `ExternalLink` wrapper so they emit click analytics consistently. It follows up #8938, which repointed the logo only on the anonymous editor panel (where the logo sits in the top-left). **Signed-in logo (new link).** Signed-in users never see the anonymous panel's logo — for them the logo lives in the sidebar via `TlaSidebarWorkspaceLink`, which until now was a non-clickable `<div>`. It now renders `ExternalLink` to `https://tldraw.dev`, opening in a new tab with `target="_blank"`, `rel="noopener noreferrer"`, and an `aria-label` (the logo is image-only). It reuses the same `utm_campaign=top-left-logo` value as #8938 so both logos roll up together on tldraw.dev's side. **Anonymous logo (tracking + consistency).** #8938 used a raw `<a>` for the anon top-left logo, which had no click tracking. It now uses `ExternalLink` too, so both surfaces emit PostHog events — `top-left-logo-clicked` (anon) and `sidebar-logo-clicked` (signed-in) — distinct names so the two can be segmented, while the shared UTM campaign still attributes them together. This also moves `target`/`rel` into the wrapper rather than hand-maintained attributes. Relates to #8871. Follows up #8938. ### Change type - [x] `improvement` ### Test plan 1. **Signed in** (`yarn dev-app`, then sign in): click the tldraw logo at the top of the left sidebar. 2. **Anonymous** (logged out, shared file, or published file): click the tldraw logo in the top-left of the editor. 3. In both cases confirm a new tab opens at `https://tldraw.dev?utm_source=dotcom&utm_medium=organic&utm_campaign=top-left-logo`, and the original tab stays on tldraw.com. 4. Inspect each logo's `<a>` element: `target="_blank"`, `rel="noopener noreferrer"`, `aria-label="tldraw.dev"`. - [ ] Unit tests - [ ] End to end tests ### Release notes - Link the tldraw logo in the signed-in sidebar to tldraw.dev. ### Code changes | Section | LOC change | | --- | --- | | Apps | +15 / -8 | Co-authored-by: Jessica Claire Edwards <[email protected]> * trigger working next npm package (#8975) ### Change type - [ ] `bugfix` - [ ] `improvement` - [ ] `feature` - [ ] `api` - [x] `other` * test(dotcom): fix flaky e2e sign-out (#8966) In order to stop the dotcom `not-logged-in.spec.ts › can sign out` e2e test from intermittently failing, this PR de-flakes the sidebar sign-out fixture. The fixture located the button with a loose `getByText('Sign out')` and clicked it without waiting for the Radix user-settings dropdown to finish opening. Playwright could therefore click mid-animation ("element is not stable") or match a node that re-portals and detaches ("element is detached from the DOM"). The fix targets the stable testid the menu item already renders (`dialog.sign-out`, from the `TldrawUiMenuItem` `${sourceId}.${id}` convention) and waits for the dropdown menu content to become visible in `openUserSettingsMenu()` before any child-item click, so child clicks no longer race the open animation. The change is confined to the e2e fixture; no project-wide `reducedMotion` override is used, so `cookie-consent.spec.ts` (which relies on animations running) is unaffected. ### Change type - [x] `other` ### Test plan 1. From `apps/dotcom/client`, run the sign-out test repeatedly to confirm the flake is gone: `yarn e2e --grep "can sign out" --repeat-each=5` 2. Or from the repo root: `yarn e2e-dotcom`. - [x] End to end tests ### Code changes | Section | LOC change | | ------- | ---------- | | Tests | +3 / -1 | * fix(utils): reject pending promise on debounce.cancel() (#8683) In order to stop callers from hanging forever when a debounced function is cancelled mid-flight, this PR makes `debounce(...).cancel()` reject the pending promise with `Error('Debounced function was cancelled')`. Follow-up to a review note on #8604, where `cancel()` was updated to release captured arguments but the pending promise was still left unsettled. Previously, `cancel()` cleared the pending timeout but never resolved or rejected the in-flight promise — so any code that did `await debouncedFn(...)` and then called `.cancel()` (or had it called externally, e.g. via a hook cleanup) would hang forever. To avoid breaking the common fire-and-forget pattern (`useZoomCss`, `Editor.deepLink`, search debouncers, bookmark debouncer, VS Code change responder, etc.), an internal no-op `.catch` is attached so callers that discard the promise don't trigger unhandled-rejection warnings. Consumers that `await` or chain `.then` / `.catch` on the returned promise still observe the rejection through their own chain. ### Change type - [x] `bugfix` ### Test plan - [x] Unit tests New tests cover: - `cancel()` rejects all pending callers from the same debounce window with the expected error. - A fresh call after `cancel()` gets a brand-new promise (and verifies state is reset cleanly). - `cancel()` is a no-op when nothing is pending; double-cancel is safe. - No `unhandledRejection` event fires when callers discard the promise and call `cancel()` (verifies the fire-and-forget pattern is unaffected). ### Release notes - Fixed `debounce(...).cancel()` leaving any in-flight promise unsettled. Calling `cancel()` while a debounced call is pending now rejects the returned promise with `Error('Debounced function was cancelled')`. Code that `await`s a debounced call across a cancel should wrap it in `try/catch`. ### Code changes | Section | LOC change | | --------- | ---------- | | Core code | +21 / -3 | | Tests | +62 / -0 | Made with [Cursor](https://cursor.com) --------- Co-authored-by: Copilot <[email protected]> * fix(tldraw): indent list on Tab in note shape instead of creating new note (#8958) In order to make list editing in note shapes behave like text and geo shape labels, this PR stops `Tab` from creating a new adjacent note when the caret is inside a bullet or ordered list. Closes #8713. Previously the note's keydown handler always created a new note on `Tab`, while the rich text editor's own keymap *also* indented the list item on the original note. This produced two bugs: an unwanted new note was created and focused, and the original note's list item was silently indented. Now, when a bullet or ordered list is active, the handler bails out early and lets the rich text editor indent the list item. ### Change type - [x] `bugfix` ### Test plan 1. Create a note shape and start editing it. 2. Open the rich text toolbar and turn on a bullet list (or ordered list). 3. Type some text, press `Enter` to start a second item, then press `Tab`. 4. Confirm the second item is indented under the first and no new note is created. 5. With no list active, confirm `Tab` still creates an adjacent note as before. ### Release notes - Fixed a bug where pressing `Tab` while editing a list inside a note shape created a new note instead of indenting the list item. * fix(tldraw): fix color picker crash by persisting the new styles (#8980) Fixing the app example page for custom colors: the example did not take reloading into account and when a shape is created with a custom style unfortunately it crashes the app because we don't persist the new additions to palette and fonts ### Change type - [x] `bugfix` - [ ] `improvement` - [ ] `feature` - [ ] `api` - [ ] `other` ### Test plan 1. go to example `/color-picker` 2. add a colour in the palette and some fonts 3. create a shape with new colour and font 4. reload - [ ] Unit tests - [ ] End to end tests ### Release notes - Fixed a bug in color-picker example that occurs when reloading the canvas without persisting the added styles Co-authored-by: Guillaume <[email protected]> * refactor: enforce tldraw/no-exported-arrow-const repo-wide (#8947) In order to actually enforce `tldraw/no-exported-arrow-const` everywhere it was originally intended, this PR removes the three `.oxlintrc.json` overrides that turned it off for `packages/editor`, `packages/tldraw`, `packages/utils`, `apps/examples`, and `templates`, and converts the 50 violations those overrides were hiding. The pattern is mechanical: `export const fn = (args) => {...}` becomes `export function fn(args) {...}`. Async functions get `export async function`. For React component aliases like `const X: TLErrorFallbackComponent = (...) =>` the form becomes `export const X: TLErrorFallbackComponent = function X(...) {...}` — a named function expression keeps the type alias visible to api-extractor (the rule only fires on arrows, not function expressions). No behavior changes. ### Affected directories | Directory | Files | Violations | | --- | ---: | ---: | | `packages/tldraw` | 14 | 19 | | `apps/examples` | 8 | 16 | | `packages/editor` | 10 | 11 | | `packages/utils` | 4 | 4 | | **Total** | **36** | **50** | ### Background Relates to #7572. The rule was added in #8258 along with the JS plugin, but with these overrides in place it has never fired in the packages it most needed to apply to. The plugin itself works correctly — see commit history for the exploration that confirmed that. ### API changes The shift to function declarations changes how api-extractor renders the affected exports — `const X: (args) => Y` becomes `function X(args): Y`. No call-site changes required; the public signatures are equivalent. Specifically affected `@public` exports: - `@tldraw/editor`: `DefaultSvgDefs`, `isSafeFloat`, `stopEventPropagation` - `@tldraw/tldraw`: `handleNativeOrMenuCopy`, `TldrawUiContextualToolbar`, `TldrawUiToolbarToggleGroup`, `TldrawUiToolbarToggleItem`, `truncateStringWithEllipsis`, `useSelectedShapesAnnouncer`, `interpolateSegments` - `@tldraw/utils`: `Image`, `noop`, `safeParseUrl`, `isAvifAnimated` `DefaultErrorFallback` keeps its `TLErrorFallbackComponent` alias in the api-report — typed as `export const DefaultErrorFallback: TLErrorFallbackComponent = function DefaultErrorFallback(...) {...}` so the alias survives. ### Change type - [x] `improvement` ### Test plan - `yarn oxlint .` — 0 errors. - `yarn typecheck` — passes. - `yarn test run` in `packages/editor`, `packages/tldraw`, `packages/utils` — all green. - `yarn api-check` — passes (api reports are regenerated). - [ ] Unit tests - [ ] End to end tests ### Code changes | Section | LOC change | | --------------- | ---------- | | Core code | +97 / -88 | | Automated files | +12 / -12 | | Documentation | +24 / -18 | | Config/tooling | +0 / -3 | * chore: ban bare lodash imports (#8984) In order to keep `import { x } from 'lodash'` from silently pulling in the full ~72 KB lodash library, this PR adds an oxlint `no-restricted-imports` rule for the bare `lodash` package and migrates the existing usages to `lodash/X` deep imports. Lodash's main package is CommonJS — its `lodash.js` is a UMD wrapper that `module.exports = _`, with no `module` or `exports` field. Neither Rollup (Vite) nor esbuild can tree-shake that. Bundling a one-line entry confirmed this on the version actually installed (`[email protected]`): | Import shape | Vite (Rollup) | esbuild | | ---------------------------------------- | ------------: | ------: | | `import { throttle } from 'lodash'` | 72.1 KB | 73.8 KB | | `import _ from 'lodash'` | 72.6 KB | 73.8 KB | | `import throttle from 'lodash/throttle'` | 4.2 KB | 3.7 KB | | `import isEqual from 'lodash/isEqual'` | 15.5 KB | 16.0 KB | Deep imports (`lodash/X`) work because each method lives in its own file in the main package and `require`s only the helpers it actually needs. They tree-shake naturally under both bundlers, and we only need one `lodash` dep + one `@types/lodash` per workspace instead of one per method. Affected: - `apps/vscode/extension/src/WebViewMessageHandler.ts` was importing `{ isEqual }` from `lodash`, shipping the full ~72 KB library in the extension bundle. Now uses `lodash/isEqual` (~16 KB). - `apps/examples/*` had four files using bare `lodash`. Migrated `throttle`, `isEqual`, `reduce`, and `capitalize` to `lodash/X` deep imports. `lodash.round(x)` → `Math.round(x)` in `GlobShapeUtil.tsx` because every call site is single-argument and behaves identically without the precision parameter. - `apps/dotcom/client/scripts/build.ts` was using `_.chunk` — replaced with a 3-line inline `for` loop. The build script is Node-only (no bundle impact) and chunking has no edge cases here, so it didn't seem worth adding `lodash` + `@types/lodash` to `apps/dotcom/client` for one call site. - `packages/state`'s fuzz test was using `lodash.times` — now `lodash/times`. - Dropped truly-unused `lodash` / `lodash.throttle` deps from `packages/state-react` and `templates/sync-cloudflare`. The new lint rule restricts only the bare `'lodash'` specifier, so `lodash/X` deep imports, `lodash-es`, and the existing `lodash.<method>` per-method packages (`packages/utils`, `apps/dotcom/sync-worker`, `apps/dotcom/client`'s `lodash.pick`) all still pass. ### Change type - [x] `improvement` ### Test plan 1. `npx oxlint` from the repo root → 0 errors across the codebase. 2. With a planted `import { capitalize } from 'lodash'`, the rule fires with a message pointing to `lodash/X` deep imports, `lodash-es`, and per-method packages. 3. `yarn typecheck` → clean. 4. `packages/state` fuzz tests → 21/21 pass. - [ ] Unit tests - [ ] End to end tests ### Release notes - Shrink the VS Code extension bundle by ~56 KB by switching `isEqual` off of the full lodash package. ### Code changes | Section | LOC change | | -------------- | ---------- | | Core code | +0 / -2 | | Tests | +1 / -1 | | Documentation | +14 / -14 | | Apps | +6 / -3 | | Templates | +0 / -2 | | Config/tooling | +17 / -4 | * fix(tldraw): don't steal selection when pasting mid-interaction (#8976) ## Overview In order to keep an in-progress interaction intact when content is pasted mid-gesture, this PR stops `defaultHandleExternalTldrawContent` from selecting the pasted shapes while the user is dragging a handle, translating, resizing, or rotating. This is an **alternative** to #8968 for the same underlying problem (#8305). Where #8968 keeps the binding-hint overlay alive by gating the overlay's `isActive`/`getOverlays` on the interaction state, this PR instead avoids the root cause for the paste case: the pasted content no longer steals selection from the shape being manipulated. Opened as a draft so the two approaches can be compared side by side. When the user pastes tldraw content while mid-interaction — for example dragging an arrow's endpoint handle — the default handler used to `select` the newly pasted shapes. That stole selection from the shape under manipulation and interrupted the gesture (the arrow's in-progress binding hint, for instance, disappeared until the drag ended). With this change the selection is left untouched while mid-interaction, so the gesture continues uninterrupted. When not mid-interaction, paste still selects the pasted content as before. Because we no longer reselect mid-interaction, the selection bounds before and after paste are identical, which would make the paste "puff" overlap check always pass. The puff is meant to signal that the newly-selected pasted content landed on the old selection, so it's now skipped while mid-interaction. ## Scope note The known undo-grouping quirk tracked in #8969 (paste landing in the same undo entry as the drag) is **intentionally left out of scope** here, to keep this change focused and comparable to #8968. Thanks to @SomeHats for suggesting this approach in https://github.com/tldraw/tldraw/pull/8968#issuecomment-4577902029. ## Change type - [x] `bugfix` ## Test plan 1. Create a shape and an arrow bound to it. 2. Start dragging the arrow's free endpoint handle. 3. While still dragging, paste some tldraw content. 4. The arrow stays selected and its binding hint remains visible; the pasted content is added without stealing selection. 5. Paste while idle — the pasted content is selected as normal. - [x] Unit tests added in `SelectTool.test.ts` covering dragging a handle, translating, resizing, the no-puff case, and the idle (still-selects) case. ## Release notes - Pasting content while dragging, translating, resizing, or rotating no longer steals selection from the shape being manipulated. ## Code changes | Area | Files | +/- | | --- | --- | --- | | Core | `defaultExternalContentHandlers.ts` | +19/-1 | | Tests | `SelectTool.test.ts` | +134/-0 | Relates to #8305. Alternative to #8968. * feat(examples): add native flex layout shape example (#8715) Partially inspired by customer request, partially to showcase new framelike behavior, this example adds a drag-and-droppable container that uses native css flexbox as the layout engine. https://github.com/user-attachments/assets/508d8f8e-bb33-45f1-b9e9-87c1d18d5b63 ### Change type - [ ] `bugfix` - [ ] `improvement` - [x] `feature` - [ ] `api` - [ ] `other` ### Test plan 1. Open the flex layout example. 2. Select the frame background and use the contextual toolbar to toggle horizontal/vertical layout. 3. Drag the outside green rectangle into the layout and verify the hint/drop line, pointer-up reparenting, and growth. 4. Drag a child out and verify it deparents and the layout shrinks during mouse move before pointer-up. - [ ] Unit tests - [ ] End to end tests ### Release notes - Added a flex layout custom shape example. <sub>To show artifacts inline, <a href="https://cursor.com/dashboard/cloud-agents#team-pull-requests">enable</a> in settings.</sub> <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-d6af83b5-a09f-4f7a-abc7-0a597d9d6e13"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-d6af83b5-a09f-4f7a-abc7-0a597d9d6e13"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div> --------- Co-authored-by: Cursor <[email protected]> * docs(releases): update release notes (#9007) In order to keep the next release notes aligned with what will ship in v5.1.0, this updates `apps/docs/content/releases/next.mdx` from the `production` branch (freeze week). All 14 existing entries are present on production, so no stale entries needed pruning and no archival was required (`last_version` stays at v5.0.2). Two new editor performance entries were added to the Improvements section — skipping spatial index updates on prop-only shape diffs (#8799) and tiering `notVisibleShapes` per-shape subscriptions (#8804). These are complementary changes that improve drawing performance on pages with many shapes, so they're grouped into a single entry. The intro paragraph now mentions canvas performance improvements. Other production PRs since the last update were skipped per the style guide: same-cycle page-menu polish (#8927, #8924, which fix the in-release page-menu redesign), internal refactors and manager hardening (#8833, #8895), examples-only additions (#8914, #8916, #8837), and the usual infra/chore/dotcom/docs/CI changes. ### Change type - [x] `other` ### Test plan - [ ] Unit tests - [ ] End to end tests ### Code changes | Section | LOC change | | ------------- | ---------- | | Documentation | +2 / -1 | * Revert "feat(tldraw): add allowVideoAutoplay option to TldrawOptions" (#9010) Reverts tldraw/tldraw#8943 (don't wanna ship this yet while pending discussion #8915) * other(tldraw): revert press q to copy hovered styles (#8917) (#9011) In order to ship the upcoming release without the in-progress "press q to copy hovered styles" feature, this PR reverts #8917. The original feature added a `copy-hovered-styles` action that let users press `q` while hovering a shape to copy its styles onto the next-created shape. We want to release without it, so this revert removes the action, its keyboard shortcut entry, and the associated translation key. The only non-trivial part of the revert was a conflict in the generated `api-report.api.md`: a later PR added the `page-menu.max-pages-reached` translation key in the same union type. That key is preserved; only `action.copy-hovered-styles` was removed. ### Change type - [x] `other` (revert) ### Test plan 1. Hover a shape, press `q`, then draw a new shape. 2. Confirm the hovered shape's styles are no longer copied (the keyboard shortcut is gone). ### Release notes - Remove the "press q to copy hovered styles" action. ### API changes - Removed `'action.copy-hovered-styles'` from the `TLUiTranslationKey` union. ### Code changes | Section | LOC change | | --------------- | ---------- | | Core code | +0 / -33 | | Automated files | +1 / -1 | | Config/tooling | +0 / -1 | * Bump versions to 5.1.0 [skip ci] * fix(ci): check out Discord notify action when a step fails before checkout (#9013) In order to make our failure alerts reliable, this PR ensures the Discord notify step can always find its local action. The notify step uses a local action (`./.github/actions/discord-fail-notify`), but several publish workflows run steps before `Check out code` (e.g. `Determine publish mode`, `Generate GH token`). When one of those fails, checkout is skipped, so the local action can't be found and the failure notification itself fails — as seen in [this run](https://github.com/tldraw/tldraw/actions/runs/26878352925/job/79271600922): ``` Can't find 'action.yml', 'action.yaml' or 'Dockerfile' under '.github/actions/discord-fail-notify'. Did you forget to run actions/checkout before running your local action? ``` The fix adds a `failure()`-gated sparse checkout of just the action immediately before each notify step, so the action is always present regardless of where the failure occurred. It's a no-op on success. A scan of all workflows found four with this pattern (a `failure()` local-action notify after a step that runs before checkout): `publish.yml`, `bump-versions.yml`, `publish-editor-extensions.yml`, `publish-vscode-extension.yml`. Other workflows using the action run checkout as their first step, so they're unaffected. ### Change type - [x] `other` ### Test plan - Cannot be meaningfully tested without deliberately failing a publish workflow; verified by inspecting step ordering across all workflows. ### Code changes | Section | LOC change | | -------------- | ---------- | | Config/tooling | +28 / -0 | * feat(dotcom): global Clerk CAPTCHA mount point (#8960) In order to stop every new auth surface from having to render its own CAPTCHA target, this PR hoists Clerk's `#clerk-captcha` element to the always-mounted app root (`TlaRootProviders`) so any Clerk client call anywhere in the app can attach its challenge. Closes #8951. Previously the element only existed inside `TlaSignInDialog` (added in #7105), so any Clerk call outside that dialog — a resend after the dialog closes, or future flows like adding an email, MFA, passkeys (#8774), or password reset — could fail with a CAPTCHA error in production. The element now lives inside the themed `.tla` container for the lifetime of the page, the dialog-scoped duplicate is removed (Clerk only uses the first matching element), and global styling keeps an invisible challenge hidden while centering a visible one over the app. ### Change type - [x] `improvement` ### Test plan 1. Sign in / sign up with email and confirm the verification flow still works, including the invisible CAPTCHA path. 2. Trigger a resend after closing the sign-in dialog and confirm the Clerk call no longer errors for a missing CAPTCHA target. 3. Inspect the DOM and confirm a single `#clerk-captcha` is present at the app root and that the empty element is invisible / takes no space. ### Release notes - Mount the Clerk CAPTCHA target globally so authentication flows outside the sign-in dialog no longer fail with a CAPTCHA error. ### Code changes | Section | LOC change | | ------- | ---------- | | Apps | +21 / -8 | * fix(editor): restore pre-pinch selection on touch devices (#9006) In order to stop a two-finger pinch from leaving the selection changed on touch devices, this PR captures the pre-gesture selection on the first `pointer_down` and rolls back any incidental change when the pinch starts. Fixes #8397. On touch, the first finger of a pinch fires a `pointer_down` that can change the selection (for example by landing on a different shape) before the second finger starts the gesture. The snapshot used to roll this back was taken at `pinch_start`, by which point the change had already happened, so the incidental selection persisted after the pinch. Now the snapshot is taken on the first `pointer_down` only, and `pinch_start` keeps that snapshot rather than overwriting it. When no `pointer_down` precedes the pinch — Safari delivers trackpad pinches as gesture events — `pinch_start` still captures the live selection, so the Safari fix from #6907 / #7777 is preserved. The rollback happens at `pinch_start` (as soon as we know it's a pinch), not at `pinch_end`, so the pre-gesture selection is what's shown *during* the pinch rather than the wrong shape appearing selected for the whole gesture and snapping back on release. The regression dates to #7777 (which made the `pinch_start` snapshot unconditional to fix a Safari case) and was surfaced by the gesture-handling rewrite in #8392. A deeper follow-up — unifying pinch detection and selection onto a single pointer event stream — is tracked separately. ### Change type - [x] `bugfix` ### Test plan Three layers of coverage, each of which fails on the pre-fix behavior and passes with the fix: - `packages/tldraw/src/test/pinch.test.ts` — unit tests via `TestEditor`: rollback at pinch start (not just end), empty selection, consecutive pinches, the Safari/no-pointer-down path, and the editing guard. - `packages/tldraw/src/test/pinch-dom.test.tsx` — integration tests that drive the real canvas handlers (`useCanvasEvents`, `useGestureEvents`) with native `pointerdown` and `touchstart`/`touchmove`/`touchend` events, including the case where the second finger's `pointer_down` is processed before `pinch_start`. - `apps/examples/e2e/tests/test-camera.spec.ts` — a Mobile Chrome e2e that dispatches a real two-finger touch pinch via CDP `Input.dispatchTouchEvent`, asserting the selection is rolled back both during and after the pinch. Manual check on a touch device: select a shape, start a two-finger pinch with the first finger landing on a different shape, zoom, lift both fingers — the originally selected shape stays selected throughout. - [x] Unit tests - [x] End to end tests ### Release notes - Fix a two-finger pinch unintentionally changing the selection on touch devices. ### Code changes | Section | LOC change | | --------- | ---------- | | Core code | +39 / -6 | | Tests | +376 / -0 | * fix(tldraw): correct fill dropdown trigger tooltip (#9023) In order to stop the fill dropdown trigger from showing a misleading tooltip, this PR makes the trigger describe what the dropdown opens instead of the currently selected value. Closes #8955. The fill style panel shows the main fills (`none`, `semi`, `solid`) as buttons and the extra fills (`pattern`, `lined fill`, `fill`) behind a dropdown trigger. The trigger's tooltip was always built from the current selection, so hovering it with a main fill selected showed something like "Fill — Solid" — a value that isn't even in that dropdown. Now the tooltip only names the selected value when that value is actually one of the dropdown's items; otherwise it just shows the style name ("Fill"). This mirrors the existing icon logic, which already falls back to the first item when the current value isn't in the dropdown. Standalone dropdown pickers (geo, spline, arrowhead kind) always hold the current value, so their tooltips are unchanged. ### Change type - [x] `bugfix` ### Test plan 1. Select a shape with a fill (or set the fill to `none`, `semi`, or `solid`). 2. Hover the fill dropdown trigger (the last item in the fill row). 3. The tooltip reads "Fill", not the name of the currently selected fill. 4. Open the dropdown and select an extra fill (e.g. pattern); the trigger tooltip then reads "Fill — Pattern". - [x] End to end tests ### Release notes - Fix the fill style dropdown trigger showing a misleading tooltip for the currently selected fill. * improvement(dotcom): let owners demote themselves to admin (#9035) Lets group owners demote themselves to admin from the group settings dialog, but only when the group has another owner. The role dropdown now appears on your own member row when `ownersCount > 1`; with a single owner you still see read-only role text. The backend mutator `setGroupMemberRole` already permits self-targeting and blocks demoting the last owner, so this is a client-only change to surface the dropdown in the valid case. ### Change type - [ ] `bugfix` - [x] `improvement` - [ ] `feature` - [ ] `api` - [ ] `other` ### Test plan 1. Create a group with two owners. 2. Open group settings as one of the owners. 3. On your own row, change your role from Owner to Admin — succeeds. 4. With only one owner, confirm your own row shows read-only role text (no dropdown). - [ ] Unit tests - [ ] End to end tests ### Release notes - Group owners can now demote themselves to admin, as long as another owner remains. * other(dotcom): disable zero-cache startup message in dev (#9036) In order to stop the Zero Cloud promo banner from printing on every local zero-cache startup, this PR sets `ZERO_ENABLE_STARTUP_MESSAGE=0` in the dev `.env`, alongside the existing `DO_NOT_TRACK` opt-out. ### Change type - [x] `other` ### Test plan 1. Run the zero-cache dev server (`yarn dev` in `apps/dotcom/zero-cache`). 2. Confirm the "Cloud Zero is now available" startup message no longer prints. * feat(editor): simplify multi-click handling to double-click (#8897) This simplifies tldraw's multi-click handling. Previously `ClickManager` tracked double, triple, and quadruple clicks, each emitting its own event with `down`, `up`, and `settle` phases. In practice only double-click and the hand tool's zoom shortcuts used the higher counts, and the `settle` phase couldn't tell a multi-click that finished with the pointer held apart from one that finished with it released. This PR: - Removes `triple_click` and `quadruple_click` entirely. `TLCLickEventName` is now just `'double_click'`, and the `onTripleClick` / `onQuadrupleClick` handlers are gone from `TLEventHandlers` and `StateNode`. - Splits the `settle` phase into `settle-down` and `settle-up`. `ClickManager` tracks whether the pointer is pressed when the click timer fires and emits the matching phase. - Fires double-click-to-edit on the `down` phase. Select tool child states (`Idle`, `PointingShape`, `PointingCanvas`, `PointingHandle`, `PointingSelection`, and the crop states) now act on the second pointer-down instead of waiting for pointer-up, so editing and handle behavior trigger immediately. - Updates `HandTool` to keep only double-click zoom-in (on `settle-up`); triple-click zoom-out and quadruple-click reset/zoom-to-fit are removed. ### API changes - Breaking! `TLCLickEventName` no longer includes `'triple_click'` or `'quadruple_click'`; only `'double_click'` remains. - Breaking! `onTripleClick` and `onQuadrupleClick` are removed from `TLEventHandlers` and `StateNode`. - Breaking! `TLClickEventInfo.phase` replaces `'settle'` with `'settle-down'` and `'settle-up'`. Consumers that checked `phase === 'settle'` should switch to `'settle-up'` (or branch on both if they care whether the pointer is still pressed). - `TLClickState` drops `'pendingTriple'` and `'pendingQuadruple'`. ### Change type - [x] `api` ### Test plan 1. With the hand tool, double-click the canvas and release — should zoom in. 2. With the select tool, double-click a shape — should enter editing immediately on the second press. 3. Confirm triple- and quadruple-clicks no longer zoom out or reset zoom with the hand tool. - [x] Unit tests - [x] End to end tests ### Release notes - Simplified multi-click handling: triple- and quadruple-click events are removed, and the `settle` click phase is split into `settle-down` and `settle-up` so tools can tell whether a multi-click finished with the pointer held or released. ### Code changes | Section | LOC change | | ---------------- | ------------ | | Core code | +157 / -112 | | API report | +3 / -15 | | Tests and infra | +107 / -141 | | Documentation | +55 / -63 | --------- Co-authored-by: Max Drake <[email protected]> * chore(examples): disable console.createTask to declutter Performance flame chart (#8312) In order to make the Chrome Performance flame chart more readable when profiling the examples app, this PR disables React's `console.createTask` instrumentation by setting it to `undefined` before the app script loads. React uses `console.createTask` to wrap every component render in a "Run console task" entry. While this provides async stack traces in DevTools, it adds significant visual noise to the Performance panel's flame chart, making it harder to identify actual performance bottlenecks. ### Change type - [x] `improvement` ### Test plan 1. Open the examples app in Chrome 2. Record a Performance trace 3. Verify the flame chart no longer shows "Run console task" wrappers around component renders ### Release notes - N/A (internal development tooling change) ### Code changes | Section | LOC change | | --------------- | ---------- | | Apps | +6 / -0 | * chore(deps): bump jsdom to v29 (#9019) In order to run tests against a modern jsdom — and specifically to get a native `PointerEvent` constructor (jsdom 25 has none, which forces hand-rolled mocks) — this PR bumps `jsdom` from v25.0.1 to v29.1.1, fixes the test fallout the upgrade surfaces, and then removes the test polyfills jsdom now implements natively. It's split into two commits so the upgrade and the polyfill cleanup review independently. Relates to #9006, where a new test had to polyfill `PointerEvent` because of the old jsdom. ### What changed between jsdom v25 and v29 Newly available behaviours (relied on or cleaned up here): - **`PointerEvent` constructor** (v27) — implemented (without jsdom ever firing pointer events itself); preserves `pointerId`, `pointerType`, `clientX/Y`, etc. - **`TouchEvent` constructor** (v27) — implemented; preserves `touches`. - **`movementX`/`movementY` on `MouseEvent`** (v27). - **`TextEncoder`/`TextDecoder`** on jsdom `Window`s (v27.4) — and exposed on the vitest global, so the hand-rolled polyfills are now redundant. - Other event constructors (v27): `BeforeUnloadEvent`, `BlobEvent`, `DeviceMotionEvent`, `DeviceOrientationEvent`, `PromiseRejectionEvent`, `TransitionEvent`. - New CSS selector engine (v27), CSSOM overhaul (v29), `blob.text()/arrayBuffer()/bytes()` (v28.1), `getComputedStyle()` specificity and `!important` fixes (v28.1). Breaking changes handled: - **`element.click()` now fires a `PointerEvent` instead of a `MouseEvent`** (v27). No fallout — our suites drive clicks through the editor, not via `element.click()` with `MouseEvent` assertions. - **jsdom now fires real pointer events**, so React's canvas handlers reach the pointer-capture API, which jsdom still doesn't implement. Added no-op `setPointerCapture`/`releasePointerCapture`/`hasPointerCapture` stubs to the shared vitest setup. - **CSSOM overhaul (v29)** changed SVG-export style serialization. Updated the `getSvgString` snapshot — it now serializes the shapes' real styles instead of `getComputedStyle` polyfill placeholders (a fidelity improvement). - **jsdom 28+ ships a global `WebSocket`** (its bundled undici) whose events fail the Node `EventTarget` realm check in the jsdom test environment. sync-core tests now point the global `WebSocket` at the `ws` package, matching the `WebSocketServer` they connect to. - **Minimum Node** rose to v20.19.0+ / v22.13.0+ / v24.0.0+ (v27.0.1, v29.0.0). CI runs Node 24.13.1, so this is fine. ### Polyfills removed vs kept (commit 2) Each shim was removed and the affected suites (editor 809, tldraw 2468, sync-core 608) re-run to confirm they stay green. Removed (jsdom 29 / the vitest jsdom env provides them natively): - `PointerEvent` and `TouchEvent` constructor mocks (jsdom added these in v27) - the `DOMRect` polyfill (jsdom's native `DOMRect` is complete and spec-accurate) - the `TextEncoder`/`TextDecoder` polyfills — not just redundant but counterproductive: the shared `setup.ts` did `import { TextEncoder } from 'util'`, whose broken ESM interop on the CJS builtin bound to `undefined` and *clobbered* the native global, which the per-package `require()` calls then merely restored. - the `requestAnimationFrame`/`cancelAnimationFrame` polyfill (guarded; jsdom provides both) - the `delete global.crypto; global.crypto = require('crypto')` reassignment in `setup.ts` (jsdom's `window.crypto` already exposes `subtle`; the `node:crypto` module does not). The `@peculiar/webcrypto` fallback for crypto-less environments is kept. - the guarded `CSSStyleDeclaration` `getPropertyValue`/`setProperty`/`removeProperty` fallbacks (jsdom implements all three) Kept (jsdom 29 still doesn't provide these, or the shim does more than the native API): - the `DragEvent` mock (jsdom has no `DragEvent` constructor) - `ResizeObserver`, `FontFace`, `document.fonts`, `window.matchMedia`, `URL.createObjectURL`, `HTMLImageElement.prototype.decode`, `CSS.supports`, `Path2D.prototype.roundRect`, `fake-indexeddb` - the `@peculiar/webcrypto` crypto fallback, the sync-core `WebSocket` override, and the tldraw `getComputedStyle` override ### Change type - [x] `other` ### Test plan - jsdom-based suites pass: `packages/editor` (809), `packages/tldraw` (2468), `packages/sync-core` (608), `packages/mermaid` (53), `apps/docs` (18), `apps/dotcom/client` (133). - Each polyfill removal was independently verified by removing it and re-running the affected suites; ones that broke (e.g. the native `WebSocket`, `DragEvent`) were kept. - `yarn typecheck` passes; no public API changes (api-extractor reports unchanged). - [x] Unit tests - [ ] End to end tests ### Code changes | Section | LOC change | | --------------- | ----------- | | Automated files | +4 / -6 | | Config/tooling | +244 / -257 | * refactor(editor): decouple FocusManager from select tool state (#9003) In order to remove an upward dependency from the editor package on the tldraw package's tool tree, this PR changes `FocusManager` to stop checking the select tool's `'select.editing_shape'` state path. As reported in #8888, the editor package referenced a tldraw-package tool state held together only by a string, which would break silently if the select tool were renamed, restructured, or swapped out. Instead, `FocusManager` now uses the editor's own first-class, tool-agnostic editing state via `editor.getEditingShapeId()`. This is the same state that `select.editing_shape` sets on enter and clears on exit, so the focus-ring behavior is equivalent — but it no longer couples the editor to a specific tool, and it correctly suppresses the focus ring whenever any tool is editing a shape. The blur point from #8888 ("`editor.blur({ blurContainer: false })` bypasses the manager") was already resolved separately by #8895, which centralized the blur contract on `FocusManager`. ### Change type - [x] `improvement` ### Test plan 1. Double-click a shape to start editing its text label. 2. Press Tab or an arrow key — the container focus ring should stay suppressed while editing. 3. Tab into the contextual toolbar — the focus ring should still be allowed there. - [x] Unit tests ### Code changes | Section | LOC change | | --------- | ---------- | | Core code | +1 / -2 | | Tests | +4 / -4 | Closes #8888 * feat(templates): port agent highlights to overlay util (#8764) In order to render the agent template's context and viewport-bounds highlights through tldraw's first-class overlay system instead of bespoke React/SVG components mounted via \`OnTheCanvas\`, this PR replaces the \`AgentViewportBoundsHighlights\`, \`ContextHighlights\`, \`AreaHighlight\`, and \`PointHighlight\` components with a single \`AgentHighlightOverlayUtil\` registered through the new \`overlayUtils\` prop. The overlay util enumerates active agents, derives area/point overlays from each agent's active request and selected context items, and draws them on both the main canvas and the minimap with the existing dashed/animated styling. This branch is a clean replay of [\`max/port-agent-starter-highlight-to-overlays\`](https://github.com/tldraw/tldraw/compare/main...max/port-agent-starter-highlight-to-overlays) split into reviewable commits. The second commit is supporting work that the overlay port surfaced: \`FocusedFontSize\` now derives the focused font size from the text shape util's display values (via \`getDisplayValues\`) instead of a static multiplier table and a hard-coded 16px base, so the conversion stays in sync with whatever the editor actually renders. The forward conversion now also forwards the text shape's \`font\` prop so the nearest size is chosen per-font. ### Change type - [x] \`improvement\` ### Test plan 1. \`yarn dev-template agent\` and open the demo. 2. Send a prompt so the agent runs; confirm the dashed viewport-bounds highlight and the agent-side context area highlights render on the canvas while generating, including the \"Reviewing\" / \"Agent ...'s view\" labels. 3. Pick context (shape, multiple shapes, area, point) before sending; confirm each highlight kind renders correctly with the selection-stroke colour. 4. Open the minimap and confirm filled rounded-rect highlights pulse while generating and stay solid otherwise. 5. Edit a text shape, change its \`size\`/\`scale\`/\`font\` and confirm focused font size round-trips correctly across a request. - [ ] Unit tests - [ ] End to end tests ### Release notes - Agent template now renders highlights through an \`OverlayUtil\`, including minimap support. ### Code changes | Section | LOC change | | --------- | ------------ | | Templates | +434 / -460 | * fix(editor): keep focal point fixed when clamping zoom to min/max (#8957) Thinks this works better than preserving the center, wdyt @steveruizok? In order to stop the viewport from shifting when a zoom gesture overshoots the zoom limits, this PR makes `getConstrainedCamera` preserve the caller's focal point (e.g. the cursor) when it clamps the zoom, instead of snapping back to the viewport center. Closes #8942. When you wheel/pinch zoom out past the minimum (or in past the maximum), the gesture computes a camera that keeps the point under the cursor fixed at the *requested* zoom. The old code then clamped the zoom and re-derived `x`/`y` to keep the viewport *center* fixed, discarding the cursor anchor — so the canvas visibly jumped. Now the clamped camera keeps the same focal point fixed. When that focal point happens to be the viewport center (e.g. the zoom-out button), the math reduces to the previous center-preserving behaviour, so nothing else changes. `zoomIn`/`zoomOut` are unaffected because they snap exactly onto zoom steps and never overshoot. ### Before https://github.com/user-attachments/assets/0cc13c03-2ea5-4642-bcf4-2c5f62e57e4d ### After https://github.com/user-attachments/assets/b800a301-b101-4e98-8303-3e5801b7bbeb ### Change type - [x] `bugfix` ### Test plan 1. Open the editor at any zoom level with the pointer away from the screen center. 2. Wheel- or pinch-zoom out until the zoom clamps at the minimum. 3. The point under the cursor should stay put as the zoom hits the limit; further zoom-out gestures should not move the canvas. 4. Repeat zooming in past the maximum. - [x] Unit tests ### Release notes - Fix the viewport shifting slightly when zooming past the minimum or maximum zoom level. ### Code changes | Section | LOC change | | --------- | ---------- | | Core code | +20 / -14 | | Tests | +97 / -1 | * fix(dotcom): require source read access when creating a file from another file (#9042) * chore(skills): rework /issue into a confidence-scored Q&A flow (#9034) In order to make `/issue` useful for quick, low-context reports without sending downstream agents past missing intent, this PR creates issues immediately and then matures them through a confidence-scored follow-up flow. The old flow front-loaded codebase research before creating the issue. The new flow inverts that: create the issue from the user's initial description, read back the problem in product terms, score how complete the captured intent is, and ask the questions that would fill the gaps — updating the issue in place as answers arrive. ### What changes - **Create first, mature later.** The skill creates the issue right away so the user can track it, rather than researching the codebase to death first. - **Standard body shape.** The body starts with the user's verbatim description, then a short product-facing **readback** of the problem, a numbered list of **Open questions**, and a plain-text **Confidence** status line (e.g. `Confidence: 84%, ready to get started.`). - **Light investigation.** Codebase searching is kept to just enough to ground the confidence score and ask sharp questions. Anything answerable from the code should not become an open question, and implementation breadcrumbs (file paths, function names, fix recipes) stay out of the body unless the user asks for them. - **Q&A loop.** As the user answers, the issue is updated via `gh issue edit`: answers are written beneath each question, the readback is revised, the confidence line is recomputed, and the issue framing (title, type, labels) can be adjusted if an answer changes what the issue is. - **Critical vs. deferred questions.** Questions prefixed `Critical:` block readiness and cannot be skipped. Non-critical questions the user chooses not to answer are marked `_Deferred by user; not blocking implementation._` and do not block. - **Downstream guardrails.** The `take` skill now treats unresolved `Critical:` questions, `_Awaiting answer._` placeholders, and the `More Info Needed` label as blockers before implementation. - **Issue type helper.** `skills/issue/scripts/set-issue-type.sh` sets GitHub issue types via GraphQL because `gh issue create --type` is unreliable across versions. ### Change type - [x] `other` ### Test plan 1. Read the `/issue` workflow and confirm follow-up answers retrigger the skill and update the issue body in place. 2. Read the `write-issue` standards and confirm the readback, open questions, and deferred-question conventions are documented. 3. Read the `take` workflow and confirm unresolved agent-drafted questions block implementation. 4. Run `git diff --check`. ### Code changes | Section | LOC change | | -------------- | ---------- | | Config/tooling | +110 / -23 | * chore(publish): include DOCS.md in published npm packages (#8503) In order to make package documentation and release history available to users browsing npm / unpkg package artifacts, this PR ships authored `DOCS.md` files for packages that have them, generates a `DOCS.md` rollup for the `tldraw` package from the docs-site content during `prepack`, and generates a `RELEASE_NOTES.md` rollup for the `tldraw` package from the release notes for the package version. Closes #8502. ### Change type - [x] `improvement` ### Test plan 1. `yarn oxfmt --check --no-error-on-unmatched-pattern internal/scripts/generate-tldraw-package-docs.ts internal/scripts/prepack.ts internal/scripts/postpack.sh packages/tldraw/README.md` 2. `git diff --check` 3. `yarn build-types` 4. `cd packages/tldraw && yarn pack --out "$tmp/tldraw.tgz"`, then inspect the tarball and verify `package/DOCS.md` and `package/RELEASE_NOTES.md` are present. 5. `cd packages/state && yarn pack --dry-run --json | rg 'DOCS|RELEASE_NOTES|package\.json'`, then verify packages with authored docs include `DOCS.md` without adding `RELEASE_NOTES.md`. - [ ] Unit tests - [ ] End to end tests ### Release notes - Include package `DOCS.md` files in published npm packages that have them. - Include generated `DOCS.md` and `RELEASE_NOTES.md` files in the published `tldraw` package. ### Code changes | Section | LOC change | | -------------- | ---------- | | Config/tooling | +372 / -2 | | Documentation | +36 / -0 | * docs(releases): update 5.1 release (#9052) Moves release notes from next -> 5.1, sets up next with changes since the release. Also updates our team members for our new members! * feat(sync): keep full sync rate with multiple sessions in a room (#8988) Closes #8920. In order to stop multi-tab editing from feeling sluggish, this PR keeps a room at the full network sync rate whenever it has more than one session. Today a tab drops into "solo" mode — which throttles network sync to 1fps — whenever there are no *other users* present. But each tab, window, or device the same user opens is its own session, so two tabs of your own were both treated as single-player and both throttled, leaving edits to propagate between your own tabs with up to ~1s of lag. The fix is to count *all* other sessions in the room, not just other users'. Every session pushes its presence once on connect (ungated by presence mode), and the server never echoes a session its own record, so the store already holds one `instance_presence` record per other session — including your own other tabs and devices. An empty set means we're genuinely the only session and can throttle to solo; anything else (another user, or just another tab of our own) keeps us at full rate. ### Relationship to #8920 / #8949 This resolves #8920 by targeting the symptom users actually feel — multi-tab edit lag — rather than its original framing of sharing a single WebSocket to cut connection count. That heavier approach was explored in #8949 (a leader/presenter/follower layer, ~850 lines of new code) and closed in favor of this ~9-line change. Note this does **not** reduce the WebSocket count: each tab keeps its own socket, as today. If reducing connections per user becomes a priority later, #8949 remains the reference for that work. Because detection is server-mediated (via `instance_presence`) rather than a `BroadcastChannel`, it also works across **separate devices**, not just tabs in the same browser. ### Trade-off worth knowing With multiple of your own sessions in full mode, each pushes live presence. The editor renders one cursor per remote user (`CollaboratorsManager` dedupes by `userId`, newest activity wins), so collaborators see a single cursor that "teleports" to whichever of your windows you last touched — rather than two cursors. This is already today's behavior when you have multiple tabs open alongside other collaborators; this PR just extends full mode to the no-other-users case. Avoiding the teleport entirely is what the presenter role in #8949 buys. One caveat: detection relies on presence being enabled (the default). A consumer that disables presence pushes no `instance_presence` record, so its multi-tab sessions would still throttle to solo. ### Per-session presence is preserved end-to-end Worth flagging for future work: the collapse to one cursor per user happens *only* in the editor's view layer — not in the protocol or the store. The server assigns every session (each tab, window, or device) its own `presenceId` and broadcasts a distinct `instance_presence` record per session, and the client store keeps all of them (one per other session, including the user's own other tabs). It's `CollaboratorsManager.getCollaborators()` that groups those by `userId` and keeps only the most-recently-active record per user — which is why collaborators see one cursor rather than several. So showing a cursor *per session* — multiple live cursors for one user, one per tab/device — is already supported by the backend and the store. It would only take a change to that editor-side derivation (keying by the presence record instead of by `userId`), with no protocol or server change. This PR doesn't do that, but it doesn't foreclose it either. ### Change type - [x] `improvement` ### Test plan 1. Open a room in a single tab with no other users. Confirm it's in solo mode (network sync throttled). 2. Open the same room in a second tab as the same user. Confirm both tabs now sync at full rate — an edit in one appears in the other promptly, not ~1s later. 3. Open the same room on a second device as the same user. Confirm the same full-rate behavior (validates server-mediated, cross-device detection). 4. Close the second tab/device. Confirm the remaining lone session returns to solo throttling. 5. Confirm existing multi-user behavior is unchanged: with another user present, the room is at full rate. - [ ] Unit tests - [ ] End to end tests ### Release notes - Opening the same multiplayer room in multiple tabs, windows, or devices now keeps changes syncing at full speed between them, instead of throttling to once per second. ### Code changes | Section | LOC change | | --------- | ---------- | | Core code | +9 / -5 | Of the 9 added lines, 7 are an explanatory comment; the behavioral change is dropping the `userId` filter on the presence query and simplifying the `presenceMode`…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Allow users to run
oxlintin a sub package, when the monorepo has a base config.Example:
cd project/packages/pack-a && oxlintNow it detects the config from
project/.oxlintrc.json, aligning how the LSP side works.Aligns too how oxfmt searches config path.
closes #21533
found in voidzero-dev/vite-plus#1378 (comment)