From 9a51765bd2f857eecdba2f0a66b6fb678f96290b Mon Sep 17 00:00:00 2001
From: Luke Parker <10430890+Hona@users.noreply.github.com>
Date: Tue, 14 Jul 2026 10:02:30 +1000
Subject: [PATCH 1/3] fix(ui): preserve code spans adjacent to tildes (#35835)
---
bun.lock | 6 ++++--
package.json | 2 +-
.../ui/src/context/marked-code-span.test.ts | 15 +++++++++++++++
packages/ui/src/context/marked-code-span.ts | 17 +++++++++++++++++
packages/ui/src/context/marked.tsx | 2 ++
5 files changed, 39 insertions(+), 3 deletions(-)
create mode 100644 packages/ui/src/context/marked-code-span.test.ts
create mode 100644 packages/ui/src/context/marked-code-span.ts
diff --git a/bun.lock b/bun.lock
index 7f58e4fd53..d008ac02dc 100644
--- a/bun.lock
+++ b/bun.lock
@@ -1133,7 +1133,7 @@
"hono": "4.10.7",
"hono-openapi": "1.1.2",
"luxon": "3.6.1",
- "marked": "17.0.1",
+ "marked": "17.0.6",
"marked-shiki": "1.2.1",
"opentui-spinner": "0.0.7",
"remeda": "2.26.0",
@@ -4313,7 +4313,7 @@
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
- "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
+ "marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="],
"marked-katex-extension": ["marked-katex-extension@5.1.6", "", { "peerDependencies": { "katex": ">=0.16 <0.17", "marked": ">=4 <18" } }, "sha512-vYpLXwmlIDKILIhJtiRTgdyZRn5sEYdFBuTmbpjD7lbCIzg0/DWyK3HXIntN3Tp8zV6hvOUgpZNLWRCgWVc24A=="],
@@ -6051,6 +6051,8 @@
"@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
+ "@opentui/core/marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
+
"@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
diff --git a/package.json b/package.json
index 4fa3399264..043924143d 100644
--- a/package.json
+++ b/package.json
@@ -70,7 +70,7 @@
"hono-openapi": "1.1.2",
"fuzzysort": "3.1.0",
"luxon": "3.6.1",
- "marked": "17.0.1",
+ "marked": "17.0.6",
"marked-shiki": "1.2.1",
"remend": "1.3.0",
"@playwright/test": "1.59.1",
diff --git a/packages/ui/src/context/marked-code-span.test.ts b/packages/ui/src/context/marked-code-span.test.ts
new file mode 100644
index 0000000000..5721f62b73
--- /dev/null
+++ b/packages/ui/src/context/marked-code-span.test.ts
@@ -0,0 +1,15 @@
+import { expect, test } from "bun:test"
+import { Marked } from "marked"
+import { markedCodeSpanBoundary } from "./marked-code-span"
+
+test("preserves code spans adjacent to tildes", async () => {
+ const marked = new Marked(markedCodeSpanBoundary)
+
+ expect(await marked.parse("~`0.1576` to measurement-window-only `0.00092`")).toBe(
+ "
~0.1576 to measurement-window-only 0.00092
\n",
+ )
+ expect(await marked.parse("`before`~`after`")).toBe(
+ "before~after
\n",
+ )
+ expect(await marked.parse("~~`deleted code`~~")).toBe("deleted code
\n")
+})
diff --git a/packages/ui/src/context/marked-code-span.ts b/packages/ui/src/context/marked-code-span.ts
new file mode 100644
index 0000000000..895935c311
--- /dev/null
+++ b/packages/ui/src/context/marked-code-span.ts
@@ -0,0 +1,17 @@
+import type { MarkedExtension } from "marked"
+
+// Keep adjacent tilde and backtick runs separate until markedjs/marked#4011 is released.
+export const markedCodeSpanBoundary = {
+ tokenizer: {
+ inlineText(src) {
+ const match = /^(`+(?=~)|~+(?=`))/.exec(src)
+ if (!match) return false
+ return {
+ type: "text",
+ raw: match[0],
+ text: match[0],
+ escaped: this.lexer.state.inRawBlock,
+ }
+ },
+ },
+} satisfies MarkedExtension
diff --git a/packages/ui/src/context/marked.tsx b/packages/ui/src/context/marked.tsx
index a4f264ac67..cead61ba60 100644
--- a/packages/ui/src/context/marked.tsx
+++ b/packages/ui/src/context/marked.tsx
@@ -3,6 +3,7 @@ import markedShiki from "marked-shiki"
import katex from "katex"
import { bundledLanguages, type BundledLanguage } from "shiki"
import { createSimpleContext } from "./helper"
+import { markedCodeSpanBoundary } from "./marked-code-span"
import { getSharedHighlighter, registerCustomTheme, ThemeRegistrationResolved } from "@pierre/diffs"
export const OpenCodeTheme = {
@@ -521,6 +522,7 @@ export const { use: useMarked, provider: MarkedProvider } = createSimpleContext(
name: "Marked",
init: (props: { nativeParser?: NativeMarkdownParser }) => {
const jsParser = marked.use(
+ markedCodeSpanBoundary,
{
renderer: {
link({ href, title, text }) {
From b5e09024d564d7a751a6f8489a54297d344a6dc5 Mon Sep 17 00:00:00 2001
From: Luke Parker <10430890+Hona@users.noreply.github.com>
Date: Tue, 14 Jul 2026 10:02:46 +1000
Subject: [PATCH 2/3] fix(app): clarify status indicator severity (#36031)
---
.../status-popover-indicator.test.ts | 36 ++++++++++
.../components/status-popover-indicator.ts | 19 ++++++
.../app/src/components/status-popover.tsx | 68 +++++++------------
3 files changed, 80 insertions(+), 43 deletions(-)
create mode 100644 packages/app/src/components/status-popover-indicator.test.ts
create mode 100644 packages/app/src/components/status-popover-indicator.ts
diff --git a/packages/app/src/components/status-popover-indicator.test.ts b/packages/app/src/components/status-popover-indicator.test.ts
new file mode 100644
index 0000000000..e3c62d2a95
--- /dev/null
+++ b/packages/app/src/components/status-popover-indicator.test.ts
@@ -0,0 +1,36 @@
+import { describe, expect, test } from "bun:test"
+import { hasNonBlockingServiceIssue, serverStatusDotClass } from "./status-popover-indicator"
+
+describe("serverStatusDotClass", () => {
+ test("uses the success token while the server and services are healthy", () => {
+ expect(serverStatusDotClass({ ready: true, serverHealth: true, issue: false })).toBe("bg-icon-success-base")
+ })
+
+ test("uses the warning token for non-blocking issues while the server is online", () => {
+ expect(serverStatusDotClass({ ready: true, serverHealth: true, issue: true })).toBe("bg-icon-warning-base")
+ })
+
+ test("uses the critical token only after the server connection drops", () => {
+ expect(serverStatusDotClass({ ready: true, serverHealth: false, issue: false })).toBe("bg-icon-critical-base")
+ expect(serverStatusDotClass({ ready: true, serverHealth: false, issue: true })).toBe("bg-icon-critical-base")
+ })
+
+ test("stays neutral before status is ready", () => {
+ expect(serverStatusDotClass({ ready: false, serverHealth: true, issue: false })).toBe("bg-border-weak-base")
+ expect(serverStatusDotClass({ ready: false, serverHealth: undefined, issue: false })).toBe("bg-border-weak-base")
+ })
+})
+
+describe("hasNonBlockingServiceIssue", () => {
+ test("detects MCP failures that do not block chatting", () => {
+ expect(hasNonBlockingServiceIssue({ mcp: ["failed"], lsp: [] })).toBe(true)
+ expect(hasNonBlockingServiceIssue({ mcp: ["needs_auth"], lsp: [] })).toBe(true)
+ expect(hasNonBlockingServiceIssue({ mcp: ["needs_client_registration"], lsp: [] })).toBe(true)
+ expect(hasNonBlockingServiceIssue({ mcp: ["connected", "disabled"], lsp: [] })).toBe(false)
+ })
+
+ test("detects LSP failures that do not block chatting", () => {
+ expect(hasNonBlockingServiceIssue({ mcp: [], lsp: ["error"] })).toBe(true)
+ expect(hasNonBlockingServiceIssue({ mcp: [], lsp: ["connected"] })).toBe(false)
+ })
+})
diff --git a/packages/app/src/components/status-popover-indicator.ts b/packages/app/src/components/status-popover-indicator.ts
new file mode 100644
index 0000000000..efb7473753
--- /dev/null
+++ b/packages/app/src/components/status-popover-indicator.ts
@@ -0,0 +1,19 @@
+import type { LspStatus, McpStatus } from "@opencode-ai/sdk/v2/client"
+
+export function hasNonBlockingServiceIssue(input: {
+ mcp: Array
+ lsp: Array
+}) {
+ return (
+ input.mcp.some((status) => status !== "connected" && status !== "disabled") ||
+ input.lsp.some((status) => status === "error")
+ )
+}
+
+export function serverStatusDotClass(input: { ready: boolean; serverHealth: boolean | undefined; issue: boolean }) {
+ if (input.serverHealth === false) return "bg-icon-critical-base"
+ if (!input.ready || input.serverHealth === undefined) return "bg-border-weak-base"
+ if (input.issue) return "bg-icon-warning-base"
+ if (input.serverHealth === true) return "bg-icon-success-base"
+ return "bg-border-weak-base"
+}
diff --git a/packages/app/src/components/status-popover.tsx b/packages/app/src/components/status-popover.tsx
index c75dd5b655..2aed45702a 100644
--- a/packages/app/src/components/status-popover.tsx
+++ b/packages/app/src/components/status-popover.tsx
@@ -9,6 +9,7 @@ import { ServerConnection, useServer } from "@/context/server"
import { useServerSDK } from "@/context/server-sdk"
import { useSync } from "@/context/sync"
import { useGlobal } from "@/context/global"
+import { hasNonBlockingServiceIssue, serverStatusDotClass } from "./status-popover-indicator"
const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody })))
const ServerBody = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverServerBody })))
@@ -19,16 +20,14 @@ export function StatusPopover() {
const global = useGlobal()
const sync = useSync()
const [shown, setShown] = createSignal(false)
- const ready = createMemo(() => global.servers.health[server.key]?.healthy === false || sync().data.mcp_ready)
- const mcpIssue = createMemo(() => {
- const mcp = Object.values(sync().data.mcp ?? {})
- const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
- const warn = mcp.some((item) => item.status === "needs_auth")
- if (failed) return "critical" as const
- if (warn) return "warning" as const
- })
- const serverHealthy = () => global.servers.health[server.key]?.healthy === true
- const healthy = createMemo(() => global.servers.health[server.key]?.healthy === true && !mcpIssue())
+ const serverHealth = () => global.servers.health[server.key]?.healthy
+ const ready = createMemo(() => serverHealth() === false || (sync().data.mcp_ready && sync().data.lsp_ready))
+ const issue = createMemo(() =>
+ hasNonBlockingServiceIssue({
+ mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status),
+ lsp: (sync().data.lsp ?? []).map((item) => item.status),
+ }),
+ )
return (
}
@@ -87,21 +84,18 @@ function DirectoryStatusPopover() {
const sync = useSync()
const [shown, setShown] = createSignal(false)
const serverHealth = () => global.servers.health[ServerConnection.key(server().server)]?.healthy
- const ready = createMemo(() => serverHealth() === false || sync().data.mcp_ready)
- const mcpIssue = createMemo(() => {
- const mcp = Object.values(sync().data.mcp ?? {})
- const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
- const warn = mcp.some((item) => item.status === "needs_auth")
- if (failed) return "critical" as const
- if (warn) return "warning" as const
- })
- const healthy = createMemo(() => serverHealth() === true && !mcpIssue())
+ const ready = createMemo(() => serverHealth() === false || (sync().data.mcp_ready && sync().data.lsp_ready))
+ const issue = createMemo(() =>
+ hasNonBlockingServiceIssue({
+ mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status),
+ lsp: (sync().data.lsp ?? []).map((item) => item.status),
+ }),
+ )
const state = createMemo(() => ({
shown: shown(),
ready: ready(),
- healthy: healthy(),
serverHealth: serverHealth(),
- issue: mcpIssue(),
+ issue: issue(),
label: language.t("status.popover.trigger"),
onOpenChange: setShown,
body: () => (
@@ -123,8 +117,8 @@ function ServerStatusPopover() {
const state = createMemo(() => ({
shown: shown(),
ready: serverHealth() !== undefined,
- healthy: serverHealth() === true,
serverHealth: serverHealth(),
+ issue: false,
label: language.t("status.popover.trigger"),
onOpenChange: setShown,
body: () => (
@@ -140,9 +134,8 @@ function ServerStatusPopover() {
type StatusPopoverState = {
shown: boolean
ready: boolean
- healthy: boolean
serverHealth: boolean | undefined
- issue?: "critical" | "warning"
+ issue: boolean
label: string
onOpenChange: (value: boolean) => void
body: () => JSX.Element
@@ -161,16 +154,6 @@ function StatusPopoverBody(props: { shown: boolean; children: JSX.Element }) {
}
function StatusPopoverView(props: { state: StatusPopoverState }) {
- const statusDotClass = () => ({
- "absolute rounded-full": true,
- "bg-icon-success-base": props.state.ready && props.state.healthy,
- "bg-icon-warning-base": props.state.ready && props.state.serverHealth === true && props.state.issue === "warning",
- "bg-icon-critical-base":
- props.state.serverHealth === false ||
- (props.state.ready && props.state.serverHealth === true && props.state.issue === "critical"),
- "bg-border-weak-base": props.state.serverHealth === undefined || !props.state.ready,
- })
-
const popoverProps = {
class:
"[&_[data-slot=popover-body]]:p-0 w-[360px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl",
@@ -195,8 +178,7 @@ function StatusPopoverView(props: { state: StatusPopoverState }) {
}
From 35c88c3fc86ddc5f1f7c5a607930d871f3645e8a Mon Sep 17 00:00:00 2001
From: Luke Parker <10430890+Hona@users.noreply.github.com>
Date: Tue, 14 Jul 2026 10:03:18 +1000
Subject: [PATCH 3/3] fix(app): preserve timeline bottom anchoring (#36160)
---
bun.lock | 27 +++--
package.json | 7 +-
.../app/test-browser/solid-virtual.test.ts | 61 +++++++---
.../@tanstack%2Fsolid-virtual@3.13.28.patch | 45 --------
patches/@tanstack%2Fvirtual-core@3.17.0.patch | 105 -----------------
patches/@tanstack%2Fvirtual-core@3.17.3.patch | 108 ++++++++++++++++++
6 files changed, 168 insertions(+), 185 deletions(-)
delete mode 100644 patches/@tanstack%2Fsolid-virtual@3.13.28.patch
delete mode 100644 patches/@tanstack%2Fvirtual-core@3.17.0.patch
create mode 100644 patches/@tanstack%2Fvirtual-core@3.17.3.patch
diff --git a/bun.lock b/bun.lock
index d008ac02dc..51aeb1652e 100644
--- a/bun.lock
+++ b/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=="],
diff --git a/package.json b/package.json
index 043924143d..cb8a1a5a27 100644
--- a/package.json
+++ b/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"
}
}
diff --git a/packages/app/test-browser/solid-virtual.test.ts b/packages/app/test-browser/solid-virtual.test.ts
index 716fa7fa9e..9327eacf42 100644
--- a/packages/app/test-browser/solid-virtual.test.ts
+++ b/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({
+ 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({
- count: 2,
- getScrollElement: () => null,
- estimateSize: () => 60,
- initialOffset: 100,
- initialRect: { width: 800, height: 60 },
- })
-
- virtualizer.getTotalSize()
- virtualizer.resizeItem(0, 100)
-
- expect(virtualizer.scrollOffset).toBe(100)
- expect(virtualizer.getLogicalScrollOffset()).toBe(140)
- dispose()
+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(options(64))
+
+ expect(virtualizer.getVirtualItems().map((item) => item.index)).toEqual([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
+
+ 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", () => {
diff --git a/patches/@tanstack%2Fsolid-virtual@3.13.28.patch b/patches/@tanstack%2Fsolid-virtual@3.13.28.patch
deleted file mode 100644
index 3b1cda91e1..0000000000
--- a/patches/@tanstack%2Fsolid-virtual@3.13.28.patch
+++ /dev/null
@@ -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
diff --git a/patches/@tanstack%2Fvirtual-core@3.17.0.patch b/patches/@tanstack%2Fvirtual-core@3.17.0.patch
deleted file mode 100644
index 58616e31e6..0000000000
--- a/patches/@tanstack%2Fvirtual-core@3.17.0.patch
+++ /dev/null
@@ -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 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 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,
- 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
diff --git a/patches/@tanstack%2Fvirtual-core@3.17.3.patch b/patches/@tanstack%2Fvirtual-core@3.17.3.patch
new file mode 100644
index 0000000000..2450d4234b
--- /dev/null
+++ b/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 {
++ (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.