mirror of
https://github.com/anomalyco/opencode.git
synced 2026-04-09 07:24:51 +00:00
Compare commits
6 Commits
v1.4.0
...
no-diff-vi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb29742b57 | ||
|
|
039c60170d | ||
|
|
cd87d4f9d3 | ||
|
|
988c9894f2 | ||
|
|
ae614d919f | ||
|
|
65cde7f494 |
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-r1+AehuOGIOaaxfXkQGracT/6OdFRn5Ub8s7H+MeKFY=",
|
||||
"aarch64-linux": "sha256-WkMSRF/ZJLyzxNBjpiMR459C9G0NVOEw31tm8roPneA=",
|
||||
"aarch64-darwin": "sha256-Z127cxFpTl8Ml7PB3CG9TcCU08oYCPuk0FECK2MQ2CI=",
|
||||
"x86_64-darwin": "sha256-pkRoFtnVjyl+5fm+rrFyRnEwvptxylnFxPAcEv4ZOCg="
|
||||
"x86_64-linux": "sha256-85wpU1oCWbthPleNIOj5d5AOuuYZ6rM7gMLZR6YJ2WU=",
|
||||
"aarch64-linux": "sha256-C3A56SDQGJquCpIRj2JhIzr4A7N4cc9lxtEjl8bXDeM=",
|
||||
"aarch64-darwin": "sha256-/Ij3qhGRrcLlMfl9uEacDNnGK5URxhctuQFBW4Njrog=",
|
||||
"x86_64-darwin": "sha256-10sOPuN4eZ75orw4FI8ztCq1+AKS2e8aAfg3Z6Yn56w="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
|
||||
import { DialogVariant } from "./dialog-variant"
|
||||
import { useKeybind } from "../context/keybind"
|
||||
import * as fuzzysort from "fuzzysort"
|
||||
import { consoleManagedProviderLabel } from "@tui/util/provider-origin"
|
||||
|
||||
export function useConnected() {
|
||||
const sync = useSync()
|
||||
@@ -47,11 +46,7 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
key: item,
|
||||
value: { providerID: provider.id, modelID: model.id },
|
||||
title: model.name ?? item.modelID,
|
||||
description: consoleManagedProviderLabel(
|
||||
sync.data.console_state.consoleManagedProviders,
|
||||
provider.id,
|
||||
provider.name,
|
||||
),
|
||||
description: provider.name,
|
||||
category,
|
||||
disabled: provider.id === "opencode" && model.id.includes("-nano"),
|
||||
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||
@@ -89,9 +84,7 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
description: favorites.some((item) => item.providerID === provider.id && item.modelID === model)
|
||||
? "(Favorite)"
|
||||
: undefined,
|
||||
category: connected()
|
||||
? consoleManagedProviderLabel(sync.data.console_state.consoleManagedProviders, provider.id, provider.name)
|
||||
: undefined,
|
||||
category: connected() ? provider.name : undefined,
|
||||
disabled: provider.id === "opencode" && model.includes("-nano"),
|
||||
footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||
onSelect() {
|
||||
@@ -142,7 +135,7 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
const title = createMemo(() => {
|
||||
const value = provider()
|
||||
if (!value) return "Select model"
|
||||
return consoleManagedProviderLabel(sync.data.console_state.consoleManagedProviders, value.id, value.name)
|
||||
return value.name
|
||||
})
|
||||
|
||||
function onSelect(providerID: string, modelID: string) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { DialogModel } from "./dialog-model"
|
||||
import { useKeyboard } from "@opentui/solid"
|
||||
import { Clipboard } from "@tui/util/clipboard"
|
||||
import { useToast } from "../ui/toast"
|
||||
import { CONSOLE_MANAGED_ICON, isConsoleManagedProvider } from "@tui/util/provider-origin"
|
||||
import { isConsoleManagedProvider } from "@tui/util/provider-origin"
|
||||
|
||||
const PROVIDER_PRIORITY: Record<string, number> = {
|
||||
opencode: 0,
|
||||
@@ -49,11 +49,7 @@ export function createDialogProviderOptions() {
|
||||
}[provider.id],
|
||||
footer: consoleManaged ? sync.data.console_state.activeOrgName : undefined,
|
||||
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
|
||||
gutter: consoleManaged ? (
|
||||
<text fg={theme.textMuted}>{CONSOLE_MANAGED_ICON}</text>
|
||||
) : connected ? (
|
||||
<text fg={theme.success}>✓</text>
|
||||
) : undefined,
|
||||
gutter: connected ? <text fg={theme.success}>✓</text> : undefined,
|
||||
async onSelect() {
|
||||
if (consoleManaged) return
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ import { useToast } from "../../ui/toast"
|
||||
import { useKV } from "../../context/kv"
|
||||
import { useTextareaKeybindings } from "../textarea-keybindings"
|
||||
import { DialogSkill } from "../dialog-skill"
|
||||
import { CONSOLE_MANAGED_ICON, consoleManagedProviderLabel } from "@tui/util/provider-origin"
|
||||
|
||||
export type PromptProps = {
|
||||
sessionID?: string
|
||||
@@ -96,15 +95,8 @@ export function Prompt(props: PromptProps) {
|
||||
const list = createMemo(() => props.placeholders?.normal ?? [])
|
||||
const shell = createMemo(() => props.placeholders?.shell ?? [])
|
||||
const [auto, setAuto] = createSignal<AutocompleteRef>()
|
||||
const activeOrgName = createMemo(() => sync.data.console_state.activeOrgName)
|
||||
const canSwitchOrgs = createMemo(() => sync.data.console_state.switchableOrgCount > 1)
|
||||
const currentProviderLabel = createMemo(() => {
|
||||
const current = local.model.current()
|
||||
const provider = local.model.parsed().provider
|
||||
if (!current) return provider
|
||||
return consoleManagedProviderLabel(sync.data.console_state.consoleManagedProviders, current.providerID, provider)
|
||||
})
|
||||
const hasRightContent = createMemo(() => Boolean(props.right || activeOrgName()))
|
||||
const currentProviderLabel = createMemo(() => local.model.parsed().provider)
|
||||
const hasRightContent = createMemo(() => Boolean(props.right))
|
||||
|
||||
function promptModelWarning() {
|
||||
toast.show({
|
||||
@@ -1120,17 +1112,6 @@ export function Prompt(props: PromptProps) {
|
||||
<Show when={hasRightContent()}>
|
||||
<box flexDirection="row" gap={1} alignItems="center">
|
||||
{props.right}
|
||||
<Show when={activeOrgName()}>
|
||||
<text
|
||||
fg={theme.textMuted}
|
||||
onMouseUp={() => {
|
||||
if (!canSwitchOrgs()) return
|
||||
command.trigger("console.org.switch")
|
||||
}}
|
||||
>
|
||||
{`${CONSOLE_MANAGED_ICON} ${activeOrgName()}`}
|
||||
</text>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
@@ -1162,7 +1143,7 @@ export function Prompt(props: PromptProps) {
|
||||
}
|
||||
/>
|
||||
</box>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<box width="100%" flexDirection="row" justifyContent="space-between">
|
||||
<Show when={status().type !== "idle"} fallback={props.hint ?? <text />}>
|
||||
<box
|
||||
flexDirection="row"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export const CONSOLE_MANAGED_ICON = "⌂"
|
||||
|
||||
const contains = (consoleManagedProviders: string[] | ReadonlySet<string>, providerID: string) =>
|
||||
Array.isArray(consoleManagedProviders)
|
||||
? consoleManagedProviders.includes(providerID)
|
||||
@@ -7,14 +5,3 @@ const contains = (consoleManagedProviders: string[] | ReadonlySet<string>, provi
|
||||
|
||||
export const isConsoleManagedProvider = (consoleManagedProviders: string[] | ReadonlySet<string>, providerID: string) =>
|
||||
contains(consoleManagedProviders, providerID)
|
||||
|
||||
export const consoleManagedProviderSuffix = (
|
||||
consoleManagedProviders: string[] | ReadonlySet<string>,
|
||||
providerID: string,
|
||||
) => (contains(consoleManagedProviders, providerID) ? ` ${CONSOLE_MANAGED_ICON}` : "")
|
||||
|
||||
export const consoleManagedProviderLabel = (
|
||||
consoleManagedProviders: string[] | ReadonlySet<string>,
|
||||
providerID: string,
|
||||
providerName: string,
|
||||
) => `${providerName}${consoleManagedProviderSuffix(consoleManagedProviders, providerID)}`
|
||||
|
||||
@@ -28,7 +28,7 @@ export const ProviderRoutes = lazy(() =>
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
z.object({
|
||||
all: ModelsDev.Provider.array(),
|
||||
all: Provider.Info.array(),
|
||||
default: z.record(z.string(), z.string()),
|
||||
connected: z.array(z.string()),
|
||||
}),
|
||||
|
||||
@@ -906,7 +906,7 @@ export const SessionRoutes = lazy(() =>
|
||||
description: "Created message",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(MessageV2.Assistant),
|
||||
schema: resolver(MessageV2.WithParts),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -98,6 +98,37 @@ describe("tool.registry", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
await Bun.write(
|
||||
path.join(opencodeDir, "package-lock.json"),
|
||||
JSON.stringify({
|
||||
name: "custom-tools",
|
||||
lockfileVersion: 3,
|
||||
packages: {
|
||||
"": {
|
||||
dependencies: {
|
||||
"@opencode-ai/plugin": "^0.0.0",
|
||||
cowsay: "^1.6.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const cowsayDir = path.join(opencodeDir, "node_modules", "cowsay")
|
||||
await fs.mkdir(cowsayDir, { recursive: true })
|
||||
await Bun.write(
|
||||
path.join(cowsayDir, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "cowsay",
|
||||
type: "module",
|
||||
exports: "./index.js",
|
||||
}),
|
||||
)
|
||||
await Bun.write(
|
||||
path.join(cowsayDir, "index.js"),
|
||||
["export function say({ text }) {", " return `moo ${text}`", "}", ""].join("\n"),
|
||||
)
|
||||
|
||||
await Bun.write(
|
||||
path.join(toolsDir, "cowsay.ts"),
|
||||
[
|
||||
|
||||
@@ -17,58 +17,25 @@ const ctx = {
|
||||
ask: async () => {},
|
||||
}
|
||||
|
||||
type TimerID = ReturnType<typeof setTimeout>
|
||||
|
||||
async function withFetch(
|
||||
mockFetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>,
|
||||
fn: () => Promise<void>,
|
||||
) {
|
||||
const originalFetch = globalThis.fetch
|
||||
globalThis.fetch = mockFetch as unknown as typeof fetch
|
||||
try {
|
||||
await fn()
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch
|
||||
}
|
||||
}
|
||||
|
||||
async function withTimers(fn: (state: { ids: TimerID[]; cleared: TimerID[] }) => Promise<void>) {
|
||||
const set = globalThis.setTimeout
|
||||
const clear = globalThis.clearTimeout
|
||||
const ids: TimerID[] = []
|
||||
const cleared: TimerID[] = []
|
||||
|
||||
globalThis.setTimeout = ((...args: Parameters<typeof setTimeout>) => {
|
||||
const id = set(...args)
|
||||
ids.push(id)
|
||||
return id
|
||||
}) as typeof setTimeout
|
||||
|
||||
globalThis.clearTimeout = ((id?: TimerID) => {
|
||||
if (id !== undefined) cleared.push(id)
|
||||
return clear(id)
|
||||
}) as typeof clearTimeout
|
||||
|
||||
try {
|
||||
await fn({ ids, cleared })
|
||||
} finally {
|
||||
ids.forEach(clear)
|
||||
globalThis.setTimeout = set
|
||||
globalThis.clearTimeout = clear
|
||||
}
|
||||
async function withFetch(fetch: (req: Request) => Response | Promise<Response>, fn: (url: URL) => Promise<void>) {
|
||||
using server = Bun.serve({ port: 0, fetch })
|
||||
await fn(server.url)
|
||||
}
|
||||
|
||||
describe("tool.webfetch", () => {
|
||||
test("returns image responses as file attachments", async () => {
|
||||
const bytes = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])
|
||||
await withFetch(
|
||||
async () => new Response(bytes, { status: 200, headers: { "content-type": "IMAGE/PNG; charset=binary" } }),
|
||||
async () => {
|
||||
() => new Response(bytes, { status: 200, headers: { "content-type": "IMAGE/PNG; charset=binary" } }),
|
||||
async (url) => {
|
||||
await Instance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const webfetch = await WebFetchTool.init()
|
||||
const result = await webfetch.execute({ url: "https://example.com/image.png", format: "markdown" }, ctx)
|
||||
const result = await webfetch.execute(
|
||||
{ url: new URL("/image.png", url).toString(), format: "markdown" },
|
||||
ctx,
|
||||
)
|
||||
expect(result.output).toBe("Image fetched successfully")
|
||||
expect(result.attachments).toBeDefined()
|
||||
expect(result.attachments?.length).toBe(1)
|
||||
@@ -87,17 +54,17 @@ describe("tool.webfetch", () => {
|
||||
test("keeps svg as text output", async () => {
|
||||
const svg = '<svg xmlns="http://www.w3.org/2000/svg"><text>hello</text></svg>'
|
||||
await withFetch(
|
||||
async () =>
|
||||
() =>
|
||||
new Response(svg, {
|
||||
status: 200,
|
||||
headers: { "content-type": "image/svg+xml; charset=UTF-8" },
|
||||
}),
|
||||
async () => {
|
||||
async (url) => {
|
||||
await Instance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const webfetch = await WebFetchTool.init()
|
||||
const result = await webfetch.execute({ url: "https://example.com/image.svg", format: "html" }, ctx)
|
||||
const result = await webfetch.execute({ url: new URL("/image.svg", url).toString(), format: "html" }, ctx)
|
||||
expect(result.output).toContain("<svg")
|
||||
expect(result.attachments).toBeUndefined()
|
||||
},
|
||||
@@ -108,17 +75,17 @@ describe("tool.webfetch", () => {
|
||||
|
||||
test("keeps text responses as text output", async () => {
|
||||
await withFetch(
|
||||
async () =>
|
||||
() =>
|
||||
new Response("hello from webfetch", {
|
||||
status: 200,
|
||||
headers: { "content-type": "text/plain; charset=utf-8" },
|
||||
}),
|
||||
async () => {
|
||||
async (url) => {
|
||||
await Instance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const webfetch = await WebFetchTool.init()
|
||||
const result = await webfetch.execute({ url: "https://example.com/file.txt", format: "text" }, ctx)
|
||||
const result = await webfetch.execute({ url: new URL("/file.txt", url).toString(), format: "text" }, ctx)
|
||||
expect(result.output).toBe("hello from webfetch")
|
||||
expect(result.attachments).toBeUndefined()
|
||||
},
|
||||
@@ -126,29 +93,4 @@ describe("tool.webfetch", () => {
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test("clears timeout when fetch rejects", async () => {
|
||||
await withTimers(async ({ ids, cleared }) => {
|
||||
await withFetch(
|
||||
async () => {
|
||||
throw new Error("boom")
|
||||
},
|
||||
async () => {
|
||||
await Instance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const webfetch = await WebFetchTool.init()
|
||||
await expect(
|
||||
webfetch.execute({ url: "https://example.com/file.txt", format: "text" }, ctx),
|
||||
).rejects.toThrow("boom")
|
||||
},
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
expect(ids).toHaveLength(1)
|
||||
expect(cleared).toHaveLength(1)
|
||||
expect(cleared[0]).toBe(ids[0])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3936,7 +3936,10 @@ export type SessionShellResponses = {
|
||||
/**
|
||||
* Created message
|
||||
*/
|
||||
200: AssistantMessage
|
||||
200: {
|
||||
info: Message
|
||||
parts: Array<Part>
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionShellResponse = SessionShellResponses[keyof SessionShellResponses]
|
||||
@@ -4212,68 +4215,7 @@ export type ProviderListResponses = {
|
||||
* List of providers
|
||||
*/
|
||||
200: {
|
||||
all: Array<{
|
||||
api?: string
|
||||
name: string
|
||||
env: Array<string>
|
||||
id: string
|
||||
npm?: string
|
||||
models: {
|
||||
[key: string]: {
|
||||
id: string
|
||||
name: string
|
||||
family?: string
|
||||
release_date: string
|
||||
attachment: boolean
|
||||
reasoning: boolean
|
||||
temperature: boolean
|
||||
tool_call: boolean
|
||||
interleaved?:
|
||||
| true
|
||||
| {
|
||||
field: "reasoning_content" | "reasoning_details"
|
||||
}
|
||||
cost?: {
|
||||
input: number
|
||||
output: number
|
||||
cache_read?: number
|
||||
cache_write?: number
|
||||
context_over_200k?: {
|
||||
input: number
|
||||
output: number
|
||||
cache_read?: number
|
||||
cache_write?: number
|
||||
}
|
||||
}
|
||||
limit: {
|
||||
context: number
|
||||
input?: number
|
||||
output: number
|
||||
}
|
||||
modalities?: {
|
||||
input: Array<"text" | "audio" | "image" | "video" | "pdf">
|
||||
output: Array<"text" | "audio" | "image" | "video" | "pdf">
|
||||
}
|
||||
experimental?: boolean
|
||||
status?: "alpha" | "beta" | "deprecated"
|
||||
options: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
headers?: {
|
||||
[key: string]: string
|
||||
}
|
||||
provider?: {
|
||||
npm?: string
|
||||
api?: string
|
||||
}
|
||||
variants?: {
|
||||
[key: string]: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}>
|
||||
all: Array<Provider>
|
||||
default: {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
@@ -4098,7 +4098,19 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AssistantMessage"
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"info": {
|
||||
"$ref": "#/components/schemas/Message"
|
||||
},
|
||||
"parts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Part"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["info", "parts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4790,211 +4802,7 @@
|
||||
"all": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"api": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"env": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"npm": {
|
||||
"type": "string"
|
||||
},
|
||||
"models": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"family": {
|
||||
"type": "string"
|
||||
},
|
||||
"release_date": {
|
||||
"type": "string"
|
||||
},
|
||||
"attachment": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"reasoning": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tool_call": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"interleaved": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"const": true
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"enum": ["reasoning_content", "reasoning_details"]
|
||||
}
|
||||
},
|
||||
"required": ["field"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"cost": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input": {
|
||||
"type": "number"
|
||||
},
|
||||
"output": {
|
||||
"type": "number"
|
||||
},
|
||||
"cache_read": {
|
||||
"type": "number"
|
||||
},
|
||||
"cache_write": {
|
||||
"type": "number"
|
||||
},
|
||||
"context_over_200k": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input": {
|
||||
"type": "number"
|
||||
},
|
||||
"output": {
|
||||
"type": "number"
|
||||
},
|
||||
"cache_read": {
|
||||
"type": "number"
|
||||
},
|
||||
"cache_write": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["input", "output"]
|
||||
}
|
||||
},
|
||||
"required": ["input", "output"]
|
||||
},
|
||||
"limit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"context": {
|
||||
"type": "number"
|
||||
},
|
||||
"input": {
|
||||
"type": "number"
|
||||
},
|
||||
"output": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["context", "output"]
|
||||
},
|
||||
"modalities": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["text", "audio", "image", "video", "pdf"]
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["text", "audio", "image", "video", "pdf"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["input", "output"]
|
||||
},
|
||||
"experimental": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["alpha", "beta", "deprecated"]
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"provider": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"npm": {
|
||||
"type": "string"
|
||||
},
|
||||
"api": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"variants": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"release_date",
|
||||
"attachment",
|
||||
"reasoning",
|
||||
"temperature",
|
||||
"tool_call",
|
||||
"limit",
|
||||
"options"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "env", "id", "models"]
|
||||
"$ref": "#/components/schemas/Provider"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DIFFS_TAG_NAME, FileDiff, VirtualizedFileDiff } from "@pierre/diffs"
|
||||
import { DIFFS_TAG_NAME, FileDiff } from "@pierre/diffs"
|
||||
import { type PreloadFileDiffResult, type PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
|
||||
import { createEffect, onCleanup, onMount, Show, splitProps } from "solid-js"
|
||||
import { Dynamic, isServer } from "solid-js/web"
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
notifyShadowReady,
|
||||
observeViewerScheme,
|
||||
} from "../pierre/file-runtime"
|
||||
import { acquireVirtualizer, virtualMetrics } from "../pierre/virtualizer"
|
||||
import { File, type DiffFileProps, type FileProps } from "./file"
|
||||
|
||||
type DiffPreload<T> = PreloadMultiFileDiffResult<T> | PreloadFileDiffResult<T>
|
||||
@@ -26,7 +25,6 @@ function DiffSSRViewer<T>(props: SSRDiffFileProps<T>) {
|
||||
let container!: HTMLDivElement
|
||||
let fileDiffRef!: HTMLElement
|
||||
let fileDiffInstance: FileDiff<T> | undefined
|
||||
let sharedVirtualizer: NonNullable<ReturnType<typeof acquireVirtualizer>> | undefined
|
||||
|
||||
const ready = createReadyWatcher()
|
||||
const workerPool = useWorkerPool(props.diffStyle)
|
||||
@@ -51,14 +49,6 @@ function DiffSSRViewer<T>(props: SSRDiffFileProps<T>) {
|
||||
|
||||
const getRoot = () => fileDiffRef?.shadowRoot ?? undefined
|
||||
|
||||
const getVirtualizer = () => {
|
||||
if (sharedVirtualizer) return sharedVirtualizer.virtualizer
|
||||
const result = acquireVirtualizer(container)
|
||||
if (!result) return
|
||||
sharedVirtualizer = result
|
||||
return result.virtualizer
|
||||
}
|
||||
|
||||
const setSelectedLines = (range: DiffFileProps<T>["selectedLines"], attempt = 0) => {
|
||||
const diff = fileDiffInstance
|
||||
if (!diff) return
|
||||
@@ -92,27 +82,15 @@ function DiffSSRViewer<T>(props: SSRDiffFileProps<T>) {
|
||||
|
||||
onCleanup(observeViewerScheme(() => fileDiffRef))
|
||||
|
||||
const virtualizer = getVirtualizer()
|
||||
const annotations = local.annotations ?? local.preloadedDiff.annotations ?? []
|
||||
fileDiffInstance = virtualizer
|
||||
? new VirtualizedFileDiff<T>(
|
||||
{
|
||||
...createDefaultOptions(props.diffStyle),
|
||||
...others,
|
||||
...(local.preloadedDiff.options ?? {}),
|
||||
},
|
||||
virtualizer,
|
||||
virtualMetrics,
|
||||
workerPool,
|
||||
)
|
||||
: new FileDiff<T>(
|
||||
{
|
||||
...createDefaultOptions(props.diffStyle),
|
||||
...others,
|
||||
...(local.preloadedDiff.options ?? {}),
|
||||
},
|
||||
workerPool,
|
||||
)
|
||||
fileDiffInstance = new FileDiff<T>(
|
||||
{
|
||||
...createDefaultOptions(props.diffStyle),
|
||||
...others,
|
||||
...(local.preloadedDiff.options ?? {}),
|
||||
},
|
||||
workerPool,
|
||||
)
|
||||
|
||||
applyViewerScheme(fileDiffRef)
|
||||
|
||||
@@ -163,8 +141,6 @@ function DiffSSRViewer<T>(props: SSRDiffFileProps<T>) {
|
||||
onCleanup(() => {
|
||||
clearReadyWatcher(ready)
|
||||
fileDiffInstance?.cleanUp()
|
||||
sharedVirtualizer?.release()
|
||||
sharedVirtualizer = undefined
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { sampledChecksum } from "@opencode-ai/util/encode"
|
||||
import {
|
||||
DEFAULT_VIRTUAL_FILE_METRICS,
|
||||
type DiffLineAnnotation,
|
||||
type FileContents,
|
||||
type FileDiffMetadata,
|
||||
@@ -10,10 +9,6 @@ import {
|
||||
type FileOptions,
|
||||
type LineAnnotation,
|
||||
type SelectedLineRange,
|
||||
type VirtualFileMetrics,
|
||||
VirtualizedFile,
|
||||
VirtualizedFileDiff,
|
||||
Virtualizer,
|
||||
} from "@pierre/diffs"
|
||||
import { type PreloadFileDiffResult, type PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
@@ -40,19 +35,10 @@ import {
|
||||
readShadowLineSelection,
|
||||
} from "../pierre/file-selection"
|
||||
import { createLineNumberSelectionBridge, restoreShadowTextSelection } from "../pierre/selection-bridge"
|
||||
import { acquireVirtualizer, virtualMetrics } from "../pierre/virtualizer"
|
||||
import { getWorkerPool } from "../pierre/worker"
|
||||
import { FileMedia, type FileMediaOptions } from "./file-media"
|
||||
import { FileSearchBar } from "./file-search"
|
||||
|
||||
const VIRTUALIZE_BYTES = 500_000
|
||||
|
||||
const codeMetrics = {
|
||||
...DEFAULT_VIRTUAL_FILE_METRICS,
|
||||
lineHeight: 24,
|
||||
fileGap: 0,
|
||||
} satisfies Partial<VirtualFileMetrics>
|
||||
|
||||
type SharedProps<T> = {
|
||||
annotations?: LineAnnotation<T>[] | DiffLineAnnotation<T>[]
|
||||
selectedLines?: SelectedLineRange | null
|
||||
@@ -386,11 +372,6 @@ type AnnotationTarget<A> = {
|
||||
rerender: () => void
|
||||
}
|
||||
|
||||
type VirtualStrategy = {
|
||||
get: () => Virtualizer | undefined
|
||||
cleanup: () => void
|
||||
}
|
||||
|
||||
function useModeViewer(config: ModeConfig, adapter: ModeAdapter) {
|
||||
return useFileViewer({
|
||||
enableLineSelection: config.enableLineSelection,
|
||||
@@ -532,64 +513,6 @@ function scrollParent(el: HTMLElement): HTMLElement | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
function createLocalVirtualStrategy(host: () => HTMLDivElement | undefined, enabled: () => boolean): VirtualStrategy {
|
||||
let virtualizer: Virtualizer | undefined
|
||||
let root: Document | HTMLElement | undefined
|
||||
|
||||
const release = () => {
|
||||
virtualizer?.cleanUp()
|
||||
virtualizer = undefined
|
||||
root = undefined
|
||||
}
|
||||
|
||||
return {
|
||||
get: () => {
|
||||
if (!enabled()) {
|
||||
release()
|
||||
return
|
||||
}
|
||||
if (typeof document === "undefined") return
|
||||
|
||||
const wrapper = host()
|
||||
if (!wrapper) return
|
||||
|
||||
const next = scrollParent(wrapper) ?? document
|
||||
if (virtualizer && root === next) return virtualizer
|
||||
|
||||
release()
|
||||
virtualizer = new Virtualizer()
|
||||
root = next
|
||||
virtualizer.setup(next, next instanceof Document ? undefined : wrapper)
|
||||
return virtualizer
|
||||
},
|
||||
cleanup: release,
|
||||
}
|
||||
}
|
||||
|
||||
function createSharedVirtualStrategy(host: () => HTMLDivElement | undefined): VirtualStrategy {
|
||||
let shared: NonNullable<ReturnType<typeof acquireVirtualizer>> | undefined
|
||||
|
||||
const release = () => {
|
||||
shared?.release()
|
||||
shared = undefined
|
||||
}
|
||||
|
||||
return {
|
||||
get: () => {
|
||||
if (shared) return shared.virtualizer
|
||||
|
||||
const container = host()
|
||||
if (!container) return
|
||||
|
||||
const result = acquireVirtualizer(container)
|
||||
if (!result) return
|
||||
shared = result
|
||||
return result.virtualizer
|
||||
},
|
||||
cleanup: release,
|
||||
}
|
||||
}
|
||||
|
||||
function parseLine(node: HTMLElement) {
|
||||
if (!node.dataset.line) return
|
||||
const value = parseInt(node.dataset.line, 10)
|
||||
@@ -688,7 +611,7 @@ function ViewerShell(props: {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function TextViewer<T>(props: TextFileProps<T>) {
|
||||
let instance: PierreFile<T> | VirtualizedFile<T> | undefined
|
||||
let instance: PierreFile<T> | undefined
|
||||
let viewer!: Viewer
|
||||
|
||||
const [local, others] = splitProps(props, textKeys)
|
||||
@@ -707,34 +630,12 @@ function TextViewer<T>(props: TextFileProps<T>) {
|
||||
return Math.max(1, total)
|
||||
}
|
||||
|
||||
const bytes = createMemo(() => {
|
||||
const value = local.file.contents as unknown
|
||||
if (typeof value === "string") return value.length
|
||||
if (Array.isArray(value)) {
|
||||
return value.reduce(
|
||||
(sum, part) => sum + (typeof part === "string" ? part.length + 1 : String(part).length + 1),
|
||||
0,
|
||||
)
|
||||
}
|
||||
if (value == null) return 0
|
||||
return String(value).length
|
||||
})
|
||||
|
||||
const virtual = createMemo(() => bytes() > VIRTUALIZE_BYTES)
|
||||
|
||||
const virtuals = createLocalVirtualStrategy(() => viewer.wrapper, virtual)
|
||||
|
||||
const lineFromMouseEvent = (event: MouseEvent): MouseHit => mouseHit(event, parseLine)
|
||||
|
||||
const applySelection = (range: SelectedLineRange | null) => {
|
||||
const current = instance
|
||||
if (!current) return false
|
||||
|
||||
if (virtual()) {
|
||||
current.setSelectedLines(range)
|
||||
return true
|
||||
}
|
||||
|
||||
const root = viewer.getRoot()
|
||||
if (!root) return false
|
||||
|
||||
@@ -833,10 +734,7 @@ function TextViewer<T>(props: TextFileProps<T>) {
|
||||
const notify = () => {
|
||||
notifyRendered({
|
||||
viewer,
|
||||
isReady: (root) => {
|
||||
if (virtual()) return root.querySelector("[data-line]") != null
|
||||
return root.querySelectorAll("[data-line]").length >= lineCount()
|
||||
},
|
||||
isReady: (root) => root.querySelectorAll("[data-line]").length >= lineCount(),
|
||||
onReady: () => {
|
||||
applySelection(viewer.lastSelection)
|
||||
viewer.find.refresh({ reset: true })
|
||||
@@ -855,17 +753,11 @@ function TextViewer<T>(props: TextFileProps<T>) {
|
||||
createEffect(() => {
|
||||
const opts = options()
|
||||
const workerPool = getWorkerPool("unified")
|
||||
const isVirtual = virtual()
|
||||
|
||||
const virtualizer = virtuals.get()
|
||||
|
||||
renderViewer({
|
||||
viewer,
|
||||
current: instance,
|
||||
create: () =>
|
||||
isVirtual && virtualizer
|
||||
? new VirtualizedFile<T>(opts, virtualizer, codeMetrics, workerPool)
|
||||
: new PierreFile<T>(opts, workerPool),
|
||||
create: () => new PierreFile<T>(opts, workerPool),
|
||||
assign: (value) => {
|
||||
instance = value
|
||||
},
|
||||
@@ -892,7 +784,6 @@ function TextViewer<T>(props: TextFileProps<T>) {
|
||||
onCleanup(() => {
|
||||
instance?.cleanUp()
|
||||
instance = undefined
|
||||
virtuals.cleanup()
|
||||
})
|
||||
|
||||
return <ViewerShell mode="text" viewer={viewer} class={local.class} classList={local.classList} />
|
||||
@@ -988,8 +879,6 @@ function DiffViewer<T>(props: DiffFileProps<T>) {
|
||||
adapter,
|
||||
)
|
||||
|
||||
const virtuals = createSharedVirtualStrategy(() => viewer.container)
|
||||
|
||||
const large = createMemo(() => {
|
||||
if (local.fileDiff) {
|
||||
const before = local.fileDiff.deletionLines.join("")
|
||||
@@ -1052,7 +941,6 @@ function DiffViewer<T>(props: DiffFileProps<T>) {
|
||||
createEffect(() => {
|
||||
const opts = options()
|
||||
const workerPool = large() ? getWorkerPool("unified") : getWorkerPool(props.diffStyle)
|
||||
const virtualizer = virtuals.get()
|
||||
const beforeContents = typeof local.before?.contents === "string" ? local.before.contents : ""
|
||||
const afterContents = typeof local.after?.contents === "string" ? local.after.contents : ""
|
||||
const done = preserve(viewer)
|
||||
@@ -1067,10 +955,7 @@ function DiffViewer<T>(props: DiffFileProps<T>) {
|
||||
renderViewer({
|
||||
viewer,
|
||||
current: instance,
|
||||
create: () =>
|
||||
virtualizer
|
||||
? new VirtualizedFileDiff<T>(opts, virtualizer, virtualMetrics, workerPool)
|
||||
: new FileDiff<T>(opts, workerPool),
|
||||
create: () => new FileDiff<T>(opts, workerPool),
|
||||
assign: (value) => {
|
||||
instance = value
|
||||
},
|
||||
@@ -1108,7 +993,6 @@ function DiffViewer<T>(props: DiffFileProps<T>) {
|
||||
onCleanup(() => {
|
||||
instance?.cleanUp()
|
||||
instance = undefined
|
||||
virtuals.cleanup()
|
||||
dragSide = undefined
|
||||
dragEndSide = undefined
|
||||
})
|
||||
|
||||
@@ -26,7 +26,6 @@ import type { LineCommentEditorProps } from "./line-comment"
|
||||
import { normalize, text, type ViewDiff } from "./session-diff"
|
||||
|
||||
const MAX_DIFF_CHANGED_LINES = 500
|
||||
const REVIEW_MOUNT_MARGIN = 300
|
||||
|
||||
export type SessionReviewDiffStyle = "unified" | "split"
|
||||
|
||||
@@ -139,14 +138,11 @@ type SessionReviewSelection = {
|
||||
export const SessionReview = (props: SessionReviewProps) => {
|
||||
let scroll: HTMLDivElement | undefined
|
||||
let focusToken = 0
|
||||
let frame: number | undefined
|
||||
const i18n = useI18n()
|
||||
const fileComponent = useFileComponent()
|
||||
const anchors = new Map<string, HTMLElement>()
|
||||
const nodes = new Map<string, HTMLDivElement>()
|
||||
const [store, setStore] = createStore({
|
||||
open: [] as string[],
|
||||
visible: {} as Record<string, boolean>,
|
||||
force: {} as Record<string, boolean>,
|
||||
selection: null as SessionReviewSelection | null,
|
||||
commenting: null as SessionReviewSelection | null,
|
||||
@@ -174,44 +170,7 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
const diffStyle = () => props.diffStyle ?? (props.split ? "split" : "unified")
|
||||
const hasDiffs = () => files().length > 0
|
||||
|
||||
const syncVisible = () => {
|
||||
frame = undefined
|
||||
if (!scroll) return
|
||||
|
||||
const root = scroll.getBoundingClientRect()
|
||||
const top = root.top - REVIEW_MOUNT_MARGIN
|
||||
const bottom = root.bottom + REVIEW_MOUNT_MARGIN
|
||||
const openSet = new Set(open())
|
||||
const next: Record<string, boolean> = {}
|
||||
|
||||
for (const [file, el] of nodes) {
|
||||
if (!openSet.has(file)) continue
|
||||
const rect = el.getBoundingClientRect()
|
||||
if (rect.bottom < top || rect.top > bottom) continue
|
||||
next[file] = true
|
||||
}
|
||||
|
||||
const prev = untrack(() => store.visible)
|
||||
const prevKeys = Object.keys(prev)
|
||||
const nextKeys = Object.keys(next)
|
||||
if (prevKeys.length === nextKeys.length && nextKeys.every((file) => prev[file])) return
|
||||
setStore("visible", next)
|
||||
}
|
||||
|
||||
const queue = () => {
|
||||
if (frame !== undefined) return
|
||||
frame = requestAnimationFrame(syncVisible)
|
||||
}
|
||||
|
||||
const pinned = (file: string) =>
|
||||
props.focusedComment?.file === file ||
|
||||
props.focusedFile === file ||
|
||||
selection()?.file === file ||
|
||||
commenting()?.file === file ||
|
||||
opened()?.file === file
|
||||
|
||||
const handleScroll: JSX.EventHandler<HTMLDivElement, Event> = (event) => {
|
||||
queue()
|
||||
const next = props.onScroll
|
||||
if (!next) return
|
||||
if (Array.isArray(next)) {
|
||||
@@ -222,21 +181,9 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
;(next as JSX.EventHandler<HTMLDivElement, Event>)(event)
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
if (frame === undefined) return
|
||||
cancelAnimationFrame(frame)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
props.open
|
||||
files()
|
||||
queue()
|
||||
})
|
||||
|
||||
const handleChange = (next: string[]) => {
|
||||
props.onOpenChange?.(next)
|
||||
if (props.open === undefined) setStore("open", next)
|
||||
queue()
|
||||
}
|
||||
|
||||
const handleExpandOrCollapseAll = () => {
|
||||
@@ -350,7 +297,6 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
viewportRef={(el) => {
|
||||
scroll = el
|
||||
props.scrollRef?.(el)
|
||||
queue()
|
||||
}}
|
||||
onScroll={handleScroll}
|
||||
classList={{
|
||||
@@ -363,11 +309,9 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
<Accordion multiple value={open()} onChange={handleChange}>
|
||||
<For each={items()}>
|
||||
{(diff) => {
|
||||
let wrapper: HTMLDivElement | undefined
|
||||
const file = diff.file
|
||||
|
||||
const expanded = createMemo(() => open().includes(file))
|
||||
const mounted = createMemo(() => expanded() && (!!store.visible[file] || pinned(file)))
|
||||
const force = () => !!store.force[file]
|
||||
|
||||
const comments = createMemo(() => grouped().get(file) ?? [])
|
||||
@@ -458,8 +402,6 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
|
||||
onCleanup(() => {
|
||||
anchors.delete(file)
|
||||
nodes.delete(file)
|
||||
queue()
|
||||
})
|
||||
|
||||
const handleLineSelected = (range: SelectedLineRange | null) => {
|
||||
@@ -542,21 +484,11 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
<div
|
||||
data-slot="session-review-diff-wrapper"
|
||||
ref={(el) => {
|
||||
wrapper = el
|
||||
anchors.set(file, el)
|
||||
nodes.set(file, el)
|
||||
queue()
|
||||
}}
|
||||
>
|
||||
<Show when={expanded()}>
|
||||
<Switch>
|
||||
<Match when={!mounted() && !tooLarge()}>
|
||||
<div
|
||||
data-slot="session-review-diff-placeholder"
|
||||
class="rounded-lg border border-border-weak-base bg-background-stronger/40"
|
||||
style={{ height: "160px" }}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={tooLarge()}>
|
||||
<div data-slot="session-review-large-diff">
|
||||
<div data-slot="session-review-large-diff-title">
|
||||
|
||||
@@ -94,9 +94,15 @@
|
||||
|
||||
[data-slot="session-turn-diffs-header"] {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 12px;
|
||||
position: sticky;
|
||||
top: var(--sticky-accordion-top, 0px);
|
||||
z-index: 20;
|
||||
background-color: var(--background-stronger);
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-diffs-label"] {
|
||||
|
||||
@@ -447,7 +447,7 @@ export function SessionTurn(
|
||||
<div data-component="session-turn-diffs-content">
|
||||
<Accordion
|
||||
multiple
|
||||
style={{ "--sticky-accordion-offset": "40px" }}
|
||||
style={{ "--sticky-accordion-offset": "44px" }}
|
||||
value={expanded()}
|
||||
onChange={(value) => setState("expanded", Array.isArray(value) ? value : value ? [value] : [])}
|
||||
>
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import { type VirtualFileMetrics, Virtualizer } from "@pierre/diffs"
|
||||
|
||||
type Target = {
|
||||
key: Document | HTMLElement
|
||||
root: Document | HTMLElement
|
||||
content: HTMLElement | undefined
|
||||
}
|
||||
|
||||
type Entry = {
|
||||
virtualizer: Virtualizer
|
||||
refs: number
|
||||
}
|
||||
|
||||
const cache = new WeakMap<Document | HTMLElement, Entry>()
|
||||
|
||||
export const virtualMetrics: Partial<VirtualFileMetrics> = {
|
||||
lineHeight: 24,
|
||||
hunkSeparatorHeight: 24,
|
||||
fileGap: 0,
|
||||
}
|
||||
|
||||
function scrollable(value: string) {
|
||||
return value === "auto" || value === "scroll" || value === "overlay"
|
||||
}
|
||||
|
||||
function scrollRoot(container: HTMLElement) {
|
||||
let node = container.parentElement
|
||||
while (node) {
|
||||
const style = getComputedStyle(node)
|
||||
if (scrollable(style.overflowY)) return node
|
||||
node = node.parentElement
|
||||
}
|
||||
}
|
||||
|
||||
function target(container: HTMLElement): Target | undefined {
|
||||
if (typeof document === "undefined") return
|
||||
|
||||
const review = container.closest("[data-component='session-review']")
|
||||
if (review instanceof HTMLElement) {
|
||||
const root = scrollRoot(container) ?? review
|
||||
const content = review.querySelector("[data-slot='session-review-container']")
|
||||
return {
|
||||
key: review,
|
||||
root,
|
||||
content: content instanceof HTMLElement ? content : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const root = scrollRoot(container)
|
||||
if (root) {
|
||||
const content = root.querySelector("[role='log']")
|
||||
return {
|
||||
key: root,
|
||||
root,
|
||||
content: content instanceof HTMLElement ? content : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
key: document,
|
||||
root: document,
|
||||
content: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function acquireVirtualizer(container: HTMLElement) {
|
||||
const resolved = target(container)
|
||||
if (!resolved) return
|
||||
|
||||
let entry = cache.get(resolved.key)
|
||||
if (!entry) {
|
||||
const virtualizer = new Virtualizer()
|
||||
virtualizer.setup(resolved.root, resolved.content)
|
||||
entry = {
|
||||
virtualizer,
|
||||
refs: 0,
|
||||
}
|
||||
cache.set(resolved.key, entry)
|
||||
}
|
||||
|
||||
entry.refs += 1
|
||||
let done = false
|
||||
|
||||
return {
|
||||
virtualizer: entry.virtualizer,
|
||||
release() {
|
||||
if (done) return
|
||||
done = true
|
||||
|
||||
const current = cache.get(resolved.key)
|
||||
if (!current) return
|
||||
|
||||
current.refs -= 1
|
||||
if (current.refs > 0) return
|
||||
|
||||
current.virtualizer.cleanUp()
|
||||
cache.delete(resolved.key)
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user