chore: update merge branch with latest dev
This commit is contained in:
commit
44adde1533
12 changed files with 277 additions and 221 deletions
15
bun.lock
15
bun.lock
|
|
@ -1153,6 +1153,7 @@
|
|||
],
|
||||
"patchedDependencies": {
|
||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
|
||||
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.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",
|
||||
|
|
@ -1160,10 +1161,8 @@
|
|||
"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",
|
||||
},
|
||||
"overrides": {
|
||||
|
|
@ -1199,7 +1198,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",
|
||||
|
|
@ -1219,7 +1218,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",
|
||||
|
|
@ -3123,7 +3122,7 @@
|
|||
|
||||
"@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/start-client-core": ["@tanstack/start-client-core@1.170.13", "", { "dependencies": { "@tanstack/router-core": "1.171.14", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-storage-context": "1.167.16", "seroval": "^1.5.4" } }, "sha512-o37M3msIK5ec87kPrIYJWXb1XPnjIe5/jrkGLXiXpFuVL99z7mhoBCzftKtVPtzqI8EElnRE/VGFYT9BHNnWcw=="],
|
||||
|
||||
|
|
@ -3137,7 +3136,7 @@
|
|||
|
||||
"@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.162.0", "", {}, "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA=="],
|
||||
|
||||
|
|
@ -4855,7 +4854,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=="],
|
||||
|
||||
|
|
@ -6889,6 +6888,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=="],
|
||||
|
|
|
|||
|
|
@ -49,7 +49,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",
|
||||
|
|
@ -76,7 +76,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",
|
||||
|
|
@ -161,10 +161,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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
36
packages/app/src/components/status-popover-indicator.test.ts
Normal file
36
packages/app/src/components/status-popover-indicator.test.ts
Normal file
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
19
packages/app/src/components/status-popover-indicator.ts
Normal file
19
packages/app/src/components/status-popover-indicator.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { LspStatus, McpStatus } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
export function hasNonBlockingServiceIssue(input: {
|
||||
mcp: Array<McpStatus["status"]>
|
||||
lsp: Array<LspStatus["status"]>
|
||||
}) {
|
||||
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"
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<Popover
|
||||
|
|
@ -47,13 +46,11 @@ export function StatusPopover() {
|
|||
<Icon name={shown() ? "status-active" : "status"} size="small" />
|
||||
</div>
|
||||
<div
|
||||
classList={{
|
||||
"absolute -top-px -right-px size-1.5 rounded-full": true,
|
||||
"bg-icon-success-base": ready() && healthy(),
|
||||
"bg-icon-warning-base": ready() && serverHealthy() && mcpIssue() === "warning",
|
||||
"bg-icon-critical-base": serverHealthy() || (ready() && serverHealthy() && mcpIssue() === "critical"),
|
||||
"bg-border-weak-base": serverHealthy() || !ready(),
|
||||
}}
|
||||
class={`absolute -top-px -right-px size-1.5 rounded-full ${serverStatusDotClass({
|
||||
ready: ready(),
|
||||
serverHealth: serverHealth(),
|
||||
issue: issue(),
|
||||
})}`}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -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<StatusPopoverState>(() => ({
|
||||
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<StatusPopoverState>(() => ({
|
||||
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 }) {
|
|||
<div class="relative size-4">
|
||||
<IconV2 name={props.state.shown ? "status-active" : "status"} />
|
||||
<div
|
||||
classList={statusDotClass()}
|
||||
class="-top-1 -right-1 size-2 border border-[var(--v2-background-bg-deep)]"
|
||||
class={`absolute -top-1 -right-1 size-2 rounded-full border border-[var(--v2-background-bg-deep)] ${serverStatusDotClass(props.state)}`}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
})
|
||||
|
||||
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<HTMLDivElement, HTMLDivElement>(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", () => {
|
||||
|
|
|
|||
15
packages/ui/src/context/marked-code-span.test.ts
Normal file
15
packages/ui/src/context/marked-code-span.test.ts
Normal file
|
|
@ -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(
|
||||
"<p>~<code>0.1576</code> to measurement-window-only <code>0.00092</code></p>\n",
|
||||
)
|
||||
expect(await marked.parse("`before`~`after`")).toBe(
|
||||
"<p><code>before</code>~<code>after</code></p>\n",
|
||||
)
|
||||
expect(await marked.parse("~~`deleted code`~~")).toBe("<p><del><code>deleted code</code></del></p>\n")
|
||||
})
|
||||
17
packages/ui/src/context/marked-code-span.ts
Normal file
17
packages/ui/src/context/marked-code-span.ts
Normal file
|
|
@ -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
|
||||
|
|
@ -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 }) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
patches/@tanstack%2Fvirtual-core@3.17.3.patch
Normal file
108
patches/@tanstack%2Fvirtual-core@3.17.3.patch
Normal file
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue