瀏覽代碼

fix(app): preserve timeline bottom anchoring (#36160)

Luke Parker 1 月之前
父節點
當前提交
35c88c3fc8

+ 13 - 14
bun.lock

@@ -1058,27 +1058,26 @@
     },
   },
   "trustedDependencies": [
-    "esbuild",
     "tree-sitter-powershell",
-    "protobufjs",
-    "electron",
     "web-tree-sitter",
     "tree-sitter-bash",
+    "esbuild",
+    "electron",
+    "protobufjs",
   ],
   "patchedDependencies": {
+    "solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
     "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
-    "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
+    "pacote@21.5.0": "patches/pacote@21.5.0.patch",
+    "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
     "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
     "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
-    "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
+    "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
+    "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
+    "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
     "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
     "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
-    "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
-    "@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch",
-    "solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
-    "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
-    "@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch",
-    "pacote@21.5.0": "patches/pacote@21.5.0.patch",
+    "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
   },
   "overrides": {
     "@opentui/core": "catalog:",
@@ -1113,7 +1112,7 @@
     "@solidjs/router": "0.15.4",
     "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
     "@tailwindcss/vite": "4.1.11",
-    "@tanstack/solid-virtual": "3.13.28",
+    "@tanstack/solid-virtual": "3.13.32",
     "@tsconfig/bun": "1.0.9",
     "@tsconfig/node22": "22.0.2",
     "@types/bun": "1.3.13",
@@ -2781,9 +2780,9 @@
 
     "@tanstack/solid-query": ["@tanstack/solid-query@5.91.4", "", { "dependencies": { "@tanstack/query-core": "5.91.2" }, "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-oCEgn8iT7WnF/7ISd7usBpUK1C9EdvQfg8ZUpKNKZ4edVClICZrCX6f3/Bp8ZlwQnL21KLc2rp+CejEuehlRxg=="],
 
-    "@tanstack/solid-virtual": ["@tanstack/solid-virtual@3.13.28", "", { "dependencies": { "@tanstack/virtual-core": "3.17.0" }, "peerDependencies": { "solid-js": "^1.3.0" } }, "sha512-kRuOEL5orH/rzGgxNgfgOttsgV6cgrUeupVtrHMITb5p0rZ3hnxhbu/lhKcR9+7x+EJdfUtJIb2CVC85mlw15g=="],
+    "@tanstack/solid-virtual": ["@tanstack/solid-virtual@3.13.32", "", { "dependencies": { "@tanstack/virtual-core": "3.17.3" }, "peerDependencies": { "solid-js": "^1.3.0" } }, "sha512-yhX4A4Kgn+wyTg6Mmu8+zwoMTwjz4K1ucvLfRJ8f0rPGDDAIqSaf0v6oU0yT9+SvrjmUaZQ0VX7g4byexbhNng=="],
 
-    "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.0", "", {}, "sha512-gOxY/hFkPh/XQYhnThBHzkbkX3Ed+z/iushyz+R+JAr213aXxUDgQoTgTdrDpBSRsjFM73P/KfUyWmaF9WHMkQ=="],
+    "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.3", "", {}, "sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw=="],
 
     "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
 

+ 3 - 4
package.json

@@ -43,7 +43,7 @@
       "@opentui/core": "0.4.3",
       "@opentui/keymap": "0.4.3",
       "@opentui/solid": "0.4.3",
-      "@tanstack/solid-virtual": "3.13.28",
+      "@tanstack/solid-virtual": "3.13.32",
       "@shikijs/stream": "4.2.0",
       "ulid": "3.0.1",
       "@kobalte/core": "0.13.11",
@@ -152,10 +152,9 @@
     "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
     "pacote@21.5.0": "patches/pacote@21.5.0.patch",
     "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
-    "@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch",
     "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
     "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
-    "@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch",
-    "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch"
+    "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
+    "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch"
   }
 }

+ 43 - 16
packages/app/test-browser/solid-virtual.test.ts

@@ -1,8 +1,32 @@
 import { expect, test } from "bun:test"
-import { createVirtualizer, defaultRangeExtractor } from "@tanstack/solid-virtual"
+import { createVirtualizer, defaultRangeExtractor, Virtualizer } from "@tanstack/solid-virtual"
 import { createRoot, createSignal } from "solid-js"
 import { filterVirtualIndexes } from "@/pages/session/timeline/virtual-items"
 
+test("end anchoring survives consecutive resizes when the first scroll write is clamped", () => {
+  const writes: { offset: number; adjustments?: number }[] = []
+  const virtualizer = new Virtualizer<HTMLDivElement, HTMLDivElement>({
+    count: 5,
+    estimateSize: () => 50,
+    initialOffset: 50,
+    initialRect: { width: 400, height: 200 },
+    anchorTo: "end",
+    scrollEndThreshold: 1,
+    getScrollElement: () => null,
+    scrollToFn: (offset, options) => writes.push({ offset, adjustments: options.adjustments }),
+    observeElementRect: () => {},
+    observeElementOffset: () => {},
+  })
+
+  virtualizer.getTotalSize()
+  virtualizer.resizeItem(4, 120)
+  expect(writes).toEqual([{ offset: 50, adjustments: 70 }])
+  writes.length = 0
+
+  virtualizer.resizeItem(4, 200)
+  expect(writes).toEqual([{ offset: 120, adjustments: 80 }])
+})
+
 test("reactive count updates preserve measured row sizes", () => {
   createRoot((dispose) => {
     const [count, setCount] = createSignal(2)
@@ -42,23 +66,26 @@ test("initial rect projects rows before a scroll element connects", () => {
   })
 })
 
-test("logical scroll offset includes pending measurement adjustments", () => {
-  createRoot((dispose) => {
-    const virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
-      count: 2,
-      getScrollElement: () => null,
-      estimateSize: () => 60,
-      initialOffset: 100,
-      initialRect: { width: 800, height: 60 },
-    })
+test("clamps oversized offsets with scroll margin and padding changes", () => {
+  const options = (paddingEnd: number) => ({
+    count: 20,
+    estimateSize: () => 60,
+    initialOffset: Number.MAX_SAFE_INTEGER,
+    initialRect: { width: 800, height: 600 },
+    scrollMargin: 64,
+    paddingEnd,
+    overscan: 1,
+    getScrollElement: () => null,
+    scrollToFn: () => {},
+    observeElementRect: () => {},
+    observeElementOffset: () => {},
+  })
+  const virtualizer = new Virtualizer<HTMLDivElement, HTMLDivElement>(options(64))
 
-    virtualizer.getTotalSize()
-    virtualizer.resizeItem(0, 100)
+  expect(virtualizer.getVirtualItems().map((item) => item.index)).toEqual([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
 
-    expect(virtualizer.scrollOffset).toBe(100)
-    expect(virtualizer.getLogicalScrollOffset()).toBe(140)
-    dispose()
-  })
+  virtualizer.setOptions(options(600))
+  expect(virtualizer.getVirtualItems().map((item) => item.index)).toEqual([18, 19])
 })
 
 test("stale pinned indexes do not produce missing virtual items after count shrinks", () => {

+ 0 - 45
patches/@tanstack%2Fsolid-virtual@3.13.28.patch

@@ -1,45 +0,0 @@
-diff --git a/dist/cjs/index.cjs b/dist/cjs/index.cjs
-index 7e97823ea769398ccd9cf449b178c77675ed252c..d75183f11421af0e20e4e8a996af99c300ad936d 100644
---- a/dist/cjs/index.cjs
-+++ b/dist/cjs/index.cjs
-@@ -39,7 +39,9 @@ function createVirtualizerBase(options) {
-         (_a = options.onChange) == null ? void 0 : _a.call(options, instance2, sync);
-       }
-     }));
--    virtualizer.measure();
-+    virtualizer._willUpdate();
-+    setVirtualItems(store.reconcile(instance.getVirtualItems(), { key: "index" }));
-+    setTotalSize(instance.getTotalSize());
-   });
-   return virtualizer;
- }
-diff --git a/dist/esm/index.js b/dist/esm/index.js
-index 1d525463775fef3e8ece6ab191061ef9d0a36d73..14c680a2088c49a33959d8118cf32ee599ab83c2 100644
---- a/dist/esm/index.js
-+++ b/dist/esm/index.js
-@@ -38,7 +38,9 @@ function createVirtualizerBase(options) {
-         (_a = options.onChange) == null ? void 0 : _a.call(options, instance2, sync);
-       }
-     }));
--    virtualizer.measure();
-+    virtualizer._willUpdate();
-+    setVirtualItems(reconcile(instance.getVirtualItems(), { key: "index" }));
-+    setTotalSize(instance.getTotalSize());
-   });
-   return virtualizer;
- }
-diff --git a/src/index.tsx b/src/index.tsx
-index 69ac34fd70753b9bd00683c2540be7f62630f8f2..9f16672aa0f4a044aa2b35754d385d7d8031f743 100644
---- a/src/index.tsx
-+++ b/src/index.tsx
-@@ -81,7 +81,9 @@ function createVirtualizerBase<
-         },
-       }),
-     )
--    virtualizer.measure()
-+    virtualizer._willUpdate()
-+    setVirtualItems(reconcile(instance.getVirtualItems(), { key: 'index' }))
-+    setTotalSize(instance.getTotalSize())
-   })
-
-   return virtualizer

+ 0 - 105
patches/@tanstack%2Fvirtual-core@3.17.0.patch

@@ -1,105 +0,0 @@
-diff --git a/dist/cjs/index.cjs b/dist/cjs/index.cjs
-index df75d0cf0347b62906e04e454d4f4ef062ed5c48..e5110715b049c6c8b992bd1375a7432df7c0182e 100644
---- a/dist/cjs/index.cjs
-+++ b/dist/cjs/index.cjs
-@@ -526,6 +526,7 @@ class Virtualizer {
-       this.scrollOffset = this.scrollOffset ?? (typeof this.options.initialOffset === "function" ? this.options.initialOffset() : this.options.initialOffset);
-       return this.scrollOffset;
-     };
-+    this.getLogicalScrollOffset = () => this.getScrollOffset() + this.scrollAdjustments;
-     this.getFurthestMeasurement = (measurements, index) => {
-       const furthestMeasurementsFound = /* @__PURE__ */ new Map();
-       const furthestMeasurements = /* @__PURE__ */ new Map();
-@@ -715,10 +716,12 @@ class Virtualizer {
-         this.options.lanes
-       ],
-       (measurements, outerSize, scrollOffset, lanes) => {
-+        const maxScrollOffset = Math.max(this.getTotalSize() - outerSize, 0);
-+        const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset);
-         return this.range = measurements.length > 0 && outerSize > 0 ? calculateRange({
-           measurements,
-           outerSize,
--          scrollOffset,
-+          scrollOffset: effectiveScrollOffset,
-           lanes,
-           // Pass the typed array so binary search + forward-walk can
-           // read start/end directly from Float64Array, skipping the
-diff --git a/dist/cjs/index.d.cts b/dist/cjs/index.d.cts
-index c61ee17752565253f795c7fc7d57e86237ecbb52..705bb7e3a121b040fb1a3e7890179eaa3e9b219e 100644
---- a/dist/cjs/index.d.cts
-+++ b/dist/cjs/index.d.cts
-@@ -108,6 +108,7 @@ export declare class Virtualizer<TScrollElement extends Element | Window, TItemE
-     scrollRect: Rect | null;
-     scrollOffset: number | null;
-     scrollDirection: ScrollDirection | null;
-+    getLogicalScrollOffset: () => number;
-     private scrollAdjustments;
-     private _iosDeferredAdjustment;
-     private _iosTouching;
-diff --git a/dist/esm/index.d.ts b/dist/esm/index.d.ts
-index b03abab604eb6578f6f56ff92c489259cfaf8f19..0495f372ea000dffc416c4f56809946f7ba73099 100644
---- a/dist/esm/index.d.ts
-+++ b/dist/esm/index.d.ts
-@@ -108,6 +108,7 @@ export declare class Virtualizer<TScrollElement extends Element | Window, TItemE
-     scrollRect: Rect | null;
-     scrollOffset: number | null;
-     scrollDirection: ScrollDirection | null;
-+    getLogicalScrollOffset: () => number;
-     private scrollAdjustments;
-     private _iosDeferredAdjustment;
-     private _iosTouching;
-diff --git a/dist/esm/index.js b/dist/esm/index.js
-index e384cf7541978a2782b9dca68146e869b16ac3f2..77af22006325377bf9ca0052a3554f881f9e75fe 100644
---- a/dist/esm/index.js
-+++ b/dist/esm/index.js
-@@ -524,6 +524,7 @@ class Virtualizer {
-       this.scrollOffset = this.scrollOffset ?? (typeof this.options.initialOffset === "function" ? this.options.initialOffset() : this.options.initialOffset);
-       return this.scrollOffset;
-     };
-+    this.getLogicalScrollOffset = () => this.getScrollOffset() + this.scrollAdjustments;
-     this.getFurthestMeasurement = (measurements, index) => {
-       const furthestMeasurementsFound = /* @__PURE__ */ new Map();
-       const furthestMeasurements = /* @__PURE__ */ new Map();
-@@ -713,10 +714,12 @@ class Virtualizer {
-         this.options.lanes
-       ],
-       (measurements, outerSize, scrollOffset, lanes) => {
-+        const maxScrollOffset = Math.max(this.getTotalSize() - outerSize, 0);
-+        const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset);
-         return this.range = measurements.length > 0 && outerSize > 0 ? calculateRange({
-           measurements,
-           outerSize,
--          scrollOffset,
-+          scrollOffset: effectiveScrollOffset,
-           lanes,
-           // Pass the typed array so binary search + forward-walk can
-           // read start/end directly from Float64Array, skipping the
-diff --git a/src/index.ts b/src/index.ts
-index d35b3e0695a9c85b261bc1a4fbe23c0a60d5b204..c504e630a60a3a7791345622bb02754b1308fd86 100644
---- a/src/index.ts
-+++ b/src/index.ts
-@@ -1047,6 +1047,8 @@ export class Virtualizer<
-     return this.scrollOffset
-   }
-
-+  getLogicalScrollOffset = () => this.getScrollOffset() + this.scrollAdjustments
-+
-   private getFurthestMeasurement = (
-     measurements: Array<VirtualItem>,
-     index: number,
-@@ -1334,12 +1336,14 @@ export class Virtualizer<
-       this.options.lanes,
-     ],
-     (measurements, outerSize, scrollOffset, lanes) => {
-+      const maxScrollOffset = Math.max(this.getTotalSize() - outerSize, 0)
-+      const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset)
-       return (this.range =
-         measurements.length > 0 && outerSize > 0
-           ? calculateRange({
-               measurements,
-               outerSize,
--              scrollOffset,
-+              scrollOffset: effectiveScrollOffset,
-               lanes,
-               // Pass the typed array so binary search + forward-walk can
-               // read start/end directly from Float64Array, skipping the

+ 108 - 0
patches/@tanstack%2Fvirtual-core@3.17.3.patch

@@ -0,0 +1,108 @@
+diff --git a/dist/cjs/index.cjs b/dist/cjs/index.cjs
+index 52ae6ca12f8d1c650ee7f1bd55573ee7d4f8b65f..830f646f5bd80f4e00f442291ad6e4c6c508630d 100644
+--- a/dist/cjs/index.cjs
++++ b/dist/cjs/index.cjs
+@@ -716,17 +716,20 @@ class Virtualizer {
+         this.getMeasurements(),
+         this.getSize(),
+         this.getScrollOffset(),
+-        this.options.lanes
++        this.options.lanes,
++        this.options.paddingEnd
+       ],
+-      (measurements, outerSize, scrollOffset, lanes) => {
++      (measurements, outerSize, scrollOffset, lanes, _paddingEnd) => {
+         if (measurements.length === 0 || outerSize === 0) {
+           this.range = null;
+           return null;
+         }
++        const maxScrollOffset = Math.max(this.options.scrollMargin + this.getTotalSize() - outerSize, 0);
++        const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset);
+         this.range = calculateRangeImpl(
+           measurements,
+           outerSize,
+-          scrollOffset,
++          effectiveScrollOffset,
+           lanes,
+           // Pass the typed array so binary search + forward-walk can read
+           // start/end directly from Float64Array, skipping the Proxy traps.
+diff --git a/dist/cjs/index.d.cts b/dist/cjs/index.d.cts
+index c61ee17752565253f795c7fc7d57e86237ecbb52..430c622a18c69407df9f1ca82ee9440e9be287b7 100644
+--- a/dist/cjs/index.d.cts
++++ b/dist/cjs/index.d.cts
+@@ -144,7 +144,7 @@ export declare class Virtualizer<TScrollElement extends Element | Window, TItemE
+             startIndex: number;
+             endIndex: number;
+         } | null;
+-        updateDeps(newDeps: [VirtualItem[], number, number, number]): void;
++        updateDeps(newDeps: [VirtualItem[], number, number, number, number]): void;
+     };
+     getVirtualIndexes: {
+         (): number[];
+diff --git a/dist/esm/index.d.ts b/dist/esm/index.d.ts
+index b03abab604eb6578f6f56ff92c489259cfaf8f19..940310f9215ba5bf666c5b71e92dddbbbc85635d 100644
+--- a/dist/esm/index.d.ts
++++ b/dist/esm/index.d.ts
+@@ -144,7 +144,7 @@ export declare class Virtualizer<TScrollElement extends Element | Window, TItemE
+             startIndex: number;
+             endIndex: number;
+         } | null;
+-        updateDeps(newDeps: [VirtualItem[], number, number, number]): void;
++        updateDeps(newDeps: [VirtualItem[], number, number, number, number]): void;
+     };
+     getVirtualIndexes: {
+         (): number[];
+diff --git a/dist/esm/index.js b/dist/esm/index.js
+index 3032c0ca457582be3f47923cba1f7d92c848745c..6e52124048fb3bc7b7ee37e763354b33593a6285 100644
+--- a/dist/esm/index.js
++++ b/dist/esm/index.js
+@@ -714,17 +714,20 @@ class Virtualizer {
+         this.getMeasurements(),
+         this.getSize(),
+         this.getScrollOffset(),
+-        this.options.lanes
++        this.options.lanes,
++        this.options.paddingEnd
+       ],
+-      (measurements, outerSize, scrollOffset, lanes) => {
++      (measurements, outerSize, scrollOffset, lanes, _paddingEnd) => {
+         if (measurements.length === 0 || outerSize === 0) {
+           this.range = null;
+           return null;
+         }
++        const maxScrollOffset = Math.max(this.options.scrollMargin + this.getTotalSize() - outerSize, 0);
++        const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset);
+         this.range = calculateRangeImpl(
+           measurements,
+           outerSize,
+-          scrollOffset,
++          effectiveScrollOffset,
+           lanes,
+           // Pass the typed array so binary search + forward-walk can read
+           // start/end directly from Float64Array, skipping the Proxy traps.
+diff --git a/src/index.ts b/src/index.ts
+index 7ad79aacd591c3a4f7855c91b2727a59a9579bca..201406779ee85d2f2e5b01a8ea284aa9a5f694e2 100644
+--- a/src/index.ts
++++ b/src/index.ts
+@@ -1367,16 +1367,19 @@ export class Virtualizer<
+       this.getSize(),
+       this.getScrollOffset(),
+       this.options.lanes,
++      this.options.paddingEnd,
+     ],
+-    (measurements, outerSize, scrollOffset, lanes) => {
++    (measurements, outerSize, scrollOffset, lanes, _paddingEnd) => {
+       if (measurements.length === 0 || outerSize === 0) {
+         this.range = null
+         return null
+       }
++      const maxScrollOffset = Math.max(this.options.scrollMargin + this.getTotalSize() - outerSize, 0)
++      const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset)
+       this.range = calculateRangeImpl(
+         measurements,
+         outerSize,
+-        scrollOffset,
++        effectiveScrollOffset,
+         lanes,
+         // Pass the typed array so binary search + forward-walk can read
+         // start/end directly from Float64Array, skipping the Proxy traps.