Compare commits

..
Author SHA1 Message Date
Kit Langton 6b04d490a6 fix(cli): remove inherited wizard and log-level flags 2026-09-12 11:52:17 -07:00
14 changed files with 151 additions and 1403 deletions
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-euVUyj0CzjCA1nYbN2vKctEPzLkUlNGTK2dMNbackqM=",
"aarch64-linux": "sha256-qQkjqaxpjAae+rohoWI601QnrgKYghJ+ttqeiQBTwCM=",
"aarch64-darwin": "sha256-HYWs31TJlDZsDBNmbPARo16r7zNKy9x840uHGcUMYsk=",
"x86_64-darwin": "sha256-89FOrX813FENk3u8RAHCfyD7voaZWW++Z4Gpa3SkOJs="
"x86_64-linux": "sha256-QJn59dTbxspHl35vGxs6akynlfaWaPqvbMNsi0EqnbM=",
"aarch64-linux": "sha256-wa54uUIAth/ZOyejZ/TfU0zZdkhNirwq7yK1218jhBQ=",
"aarch64-darwin": "sha256-VvsfVdtxcVijCvlrWpBUkgmkYFoXX7QiXM0Fhz1t2VE=",
"x86_64-darwin": "sha256-IxYFBG5I+znXcTVqrAWKhG2qvVtjM+Amp68xIGKZogE="
}
}
File diff suppressed because it is too large Load Diff
@@ -248,7 +248,7 @@ for (const direction of ["ltr", "rtl"]) {
expect(messageAfter).toEqual(messageBefore)
await page.locator('[data-component="composer-editor"]').pressSequentially("Also: ")
await expect(page.locator('[data-component="composer-editor"]')).toHaveText(`Also: ${followUp}`)
await expect.poll(() => mock.calls).toEqual(["worktree", "session", "prompt"])
expect(mock.calls).toEqual(["worktree", "session", "prompt"])
})
}
@@ -55,8 +55,6 @@ for (const direction of ["ltr", "rtl"] as const) {
"href",
`#opencode-v2-icon-${workspace ? "outline-worktree" : "monitor"}`,
)
// Initial layout scrolls this sticky header's ancestor and dismisses its tooltip.
await expect(page.locator("[data-timeline-virtual-content]")).toHaveCSS("visibility", "visible")
const background = await trigger.evaluate((element) => getComputedStyle(element).backgroundColor)
await trigger.hover()
await expect(trigger).not.toHaveCSS("background-color", background)
+15 -108
View File
@@ -103,7 +103,7 @@ export function createTimelineVirtualizer(input: Input) {
{ defer: true },
),
)
const [rendering, setRendering] = createStore({ initialTail: coldBottomMount, scrollAdjustment: 0 })
const [rendering, setRendering] = createStore({ initialTail: coldBottomMount })
const rows = input.projection.rows
const rowByKey = input.projection.rowByKey
const rowKeys = createMemo(() => rows().map(TimelineRow.key), undefined, {
@@ -154,10 +154,6 @@ export function createTimelineVirtualizer(input: Input) {
})
const measuredElements = new WeakSet<Element>()
let touchStart: number | undefined
let touchTarget: EventTarget | null = null
let touchNested = false
let touchScrolling = false
let touchAdjustment = 0
let pointerHeld = false
let maxScroll = 0
let virtualContent: HTMLDivElement | undefined
@@ -181,23 +177,7 @@ export function createTimelineVirtualizer(input: Input) {
observeElementOffset: (instance, callback) => {
reportOffset = (offset, scrolling) => {
if (!active()) return
// Rows and the sizer use the opposite translation while native touch
// scrolling keeps its own offset. Range selection uses the logical offset.
batch(() => {
const logicalOffset = offset + rendering.scrollAdjustment
callback(rendering.scrollAdjustment ? Math.max(0, logicalOffset) : offset, scrolling)
// Reconcile both start boundaries in one native write. Gradually
// clamping row translations lets the compositor paint between
// corrections and makes the content oscillate at the top.
const root = listRoot()
if (
rendering.scrollAdjustment !== 0 &&
root &&
(logicalOffset <= 0 || offset <= 0 || (touchStart !== undefined && offset <= root.clientHeight))
)
flushTouchAdjustment()
if (!scrolling && touchStart === undefined) finishTouchScroll()
})
callback(offset, scrolling)
settleColdBottom()
}
return observeElementOffsetReconnectAware(instance, reportOffset, () => {
@@ -232,7 +212,6 @@ export function createTimelineVirtualizer(input: Input) {
scrollToFn: (offset, options, instance) => {
if (!active()) return
if (batchingColdSizes && input.pinned()) return
setRendering("scrollAdjustment", 0)
if (virtualContent) virtualContent.style.height = `${instance.getTotalSize()}px`
elementScroll(offset, options, instance)
},
@@ -284,15 +263,7 @@ export function createTimelineVirtualizer(input: Input) {
batch(() => {
sizes.forEach(([index, value]) => {
const row = rows()[index]
if (!row || TimelineRow.key(row) !== value.key) return
resizeItem(index, value.size)
// TanStack recalculates its range after each resize. Advance the
// logical fold before deciding whether the next row needs anchoring.
if (!touchAdjustment) return
setRendering("scrollAdjustment", (value) => value + touchAdjustment)
touchAdjustment = 0
const root = listRoot()
if (root) reportOffset?.(root.scrollTop, virtualizer.isScrolling)
if (row && TimelineRow.key(row) === value.key) resizeItem(index, value.size)
})
})
batchingColdSizes = false
@@ -307,40 +278,13 @@ export function createTimelineVirtualizer(input: Input) {
})
}
onCleanup(() => pendingSizes.clear())
virtualizer.shouldAdjustScrollPositionOnItemSizeChange = (item, delta, instance) => {
virtualizer.shouldAdjustScrollPositionOnItemSizeChange = (item, _delta, instance) => {
// Prepended rows can resize more than once as deferred content mounts. Keep
// compensating while they remain entirely above the visible content fold.
if (addedKeys.has(String(item.key)))
return item.end <= (instance.scrollOffset ?? 0) + instance.scrollAdjustments + instance.options.scrollMargin
const first = instance.range?.startIndex
const adjust = addedKeys.has(String(item.key))
? item.end <= (instance.scrollOffset ?? 0) + instance.scrollAdjustments + instance.options.scrollMargin
: first !== undefined && item.index < first
if (!touchScrolling || input.pinned()) return adjust
// iOS defers native scroll writes until momentum ends. Keep the same visual
// anchor now, rather than moving rows now and snapping the viewport back later.
if (adjust) touchAdjustment += delta
return false
}
function finishTouchScroll() {
touchScrolling = false
flushTouchAdjustment()
}
function prepareNavigation() {
if (touchStart === undefined) touchScrolling = false
flushTouchAdjustment()
}
function flushTouchAdjustment() {
const adjustment = rendering.scrollAdjustment
const root = listRoot()
if (!adjustment || !root) return
// Transfer the translation into the native offset in the same paint.
batch(() => {
setRendering("scrollAdjustment", 0)
if (virtualContent) virtualContent.style.height = `${virtualizer.getTotalSize()}px`
elementScroll(Math.max(0, root.scrollTop + adjustment), {}, virtualizer)
})
return first !== undefined && item.index < first
}
const virtualItemByKey = createMemo(
() => new Map(virtualizer.getVirtualItems().map((item) => [item.key, item] as const)),
@@ -370,12 +314,10 @@ export function createTimelineVirtualizer(input: Input) {
: -1
const index = partIndex >= 0 ? partIndex : input.projection.messageRowIndex().get(id)
if (index === undefined) return
prepareNavigation()
virtualizer.scrollToIndex(index, { align: "center" })
})
input.setScrollToEnd?.(() => {
if (!active() || !listRoot()?.isConnected) return
prepareNavigation()
input.onPin()
virtualizer.scrollToEnd()
})
@@ -480,53 +422,19 @@ export function createTimelineVirtualizer(input: Input) {
}
const handleListTouchStart = (event: TouchEvent) => {
clearTouchTarget()
input.onUserScroll(event.target)
touchScrolling = true
touchStart = event.touches[0]?.clientY
const root = listRoot()
const nested = event.target instanceof Element ? event.target.closest<HTMLElement>("[data-scrollable]") : null
touchNested = !!nested && nested !== root && nested.scrollHeight > nested.clientHeight
// Native touch events keep their original target, even when streaming or
// virtualization detaches it. Listen there instead of relying on bubbling.
touchTarget = event.target
touchTarget?.addEventListener("touchmove", handleListTouchMove, { passive: true })
touchTarget?.addEventListener("touchend", handleListTouchEnd, { passive: true })
touchTarget?.addEventListener("touchcancel", handleListTouchEnd, { passive: true })
if (root) reportOffset?.(root.scrollTop, virtualizer.isScrolling)
}
const handleListTouchMove = (event: Event) => {
if (!(event instanceof TouchEvent)) return
const handleListTouchMove = (event: TouchEvent & { currentTarget: HTMLDivElement }) => {
const current = event.touches[0]?.clientY
if (current === undefined || touchStart === undefined) return
const previous = touchStart
touchStart = current
// A retained target can outlive its whole session view. Only the active
// timeline may change the shared follow state; release still cleans up below.
if (!active()) return
// Dragging the content downward reveals earlier messages.
if (current <= previous) return
// A nested scrollport owns the intent. If it chains into the timeline at a
// boundary, the resulting native timeline scroll below will unpin instead.
if (touchNested) return
if (current <= touchStart) return
touchStart = current
input.onUnpin()
}
const handleListTouchEnd = () => {
clearTouchTarget()
touchStart = undefined
if (!virtualizer.isScrolling) finishTouchScroll()
}
function clearTouchTarget() {
touchTarget?.removeEventListener("touchmove", handleListTouchMove)
touchTarget?.removeEventListener("touchend", handleListTouchEnd)
touchTarget?.removeEventListener("touchcancel", handleListTouchEnd)
touchTarget = null
}
onCleanup(clearTouchTarget)
// Drag-selecting past the edge and dragging the scrollbar both scroll without a wheel or key,
// so a held pointer is what separates those from the virtualizer's own measurement adjustments.
const handleListPointerDown = (event: PointerEvent & { currentTarget: HTMLDivElement }) => {
@@ -567,7 +475,7 @@ export function createTimelineVirtualizer(input: Input) {
const atEnd = maxScroll - scrollTop <= endEpsilon
const arrived = scrollTop > previousTop + endEpsilon || maxScroll < previousMaxScroll
if (maxScroll <= 1 || (atEnd && arrived)) input.onPin()
else if ((pointerHeld || touchScrolling) && scrollTop < previousTop - endEpsilon) input.onUnpin()
else if (pointerHeld && scrollTop < previousTop - endEpsilon) input.onUnpin()
settleColdBottom()
input.onScheduleScrollState(root)
input.onHistoryScroll()
@@ -596,7 +504,7 @@ export function createTimelineVirtualizer(input: Input) {
data-timeline-key={rowProps.rowKey}
style={{
position: "absolute",
top: `${item().start - topOffset() - rendering.scrollAdjustment}px`,
top: `${item().start - topOffset()}px`,
left: "0",
width: "100%",
height: `${item().size}px`,
@@ -674,10 +582,9 @@ export function createTimelineVirtualizer(input: Input) {
<ScrollView
data-slot="session-timeline-scroll"
viewportRef={bindListRoot}
onBeforeScroll={prepareNavigation}
verticalScrollAdjustment={rendering.scrollAdjustment}
onWheel={handleListWheel}
onTouchStart={handleListTouchStart}
onTouchMove={handleListTouchMove}
onPointerDown={handleListPointerDown}
onKeyDown={handleListKeyDown}
onScroll={handleListScroll}
@@ -695,7 +602,7 @@ export function createTimelineVirtualizer(input: Input) {
if (active()) input.setContentRef(element)
}}
style={{
height: `${virtualizer.getTotalSize() - rendering.scrollAdjustment}px`,
height: `${virtualizer.getTotalSize()}px`,
position: "relative",
width: "100%",
visibility: coldBottomMount ? "hidden" : undefined,
@@ -705,7 +612,7 @@ export function createTimelineVirtualizer(input: Input) {
<div
data-timeline-row="bottom-spacer"
class="h-16 absolute top-0 left-0 w-full"
style={{ transform: `translateY(${virtualizer.getTotalSize() - 64 - rendering.scrollAdjustment}px)` }}
style={{ transform: `translateY(${virtualizer.getTotalSize() - 64}px)` }}
>
{props.bottomSpacer}
</div>
+8 -6
View File
@@ -1,5 +1,5 @@
import { Effect, FileSystem, Scope } from "effect"
import { Command } from "effect/unstable/cli"
import { CliConfig, Command, GlobalFlag } from "effect/unstable/cli"
import { PrintLogs } from "../commands/commands"
import { Spec } from "./spec"
import { Global } from "@opencode/util/global"
@@ -82,11 +82,13 @@ export function handlers<const Root extends Spec.Any>(root: Root, handlers: Hand
}
export function run(commands: Spec.Any, handlers: ReadonlyArray<LazyHandler>, options: { readonly version: string }) {
return Command.run(provide(commands, handlers).pipe(Command.withGlobalFlags([PrintLogs])), options) as Effect.Effect<
void,
unknown,
Command.Environment
>
return Command.run(provide(commands, handlers).pipe(Command.withGlobalFlags([PrintLogs])), options).pipe(
Effect.provide(
CliConfig.layer({
builtIns: [GlobalFlag.Help, GlobalFlag.Version, GlobalFlag.Completions],
}),
),
) as Effect.Effect<void, unknown, Command.Environment>
}
function provide(node: Spec.Any, handlers: ReadonlyArray<LazyHandler>): ProvidedCommand {
+12 -75
View File
@@ -34,7 +34,6 @@ import { discoverPluginTargets, localSource } from "./discovery"
import { createPluginSources } from "./source"
import { isMissingPath } from "../util/config-directories"
import { createMarkdownRenderer } from "./markdown"
import { useLog, type LogTags } from "../context/log"
export interface PackageSource {
readonly prepare: (spec: string, install?: boolean) => Promise<Host.Target>
@@ -82,13 +81,11 @@ type Registration = {
// One entry of the desired plugin generation produced by the resolve phase.
type Desired = Pick<Registration, "plugin" | "source" | "target" | "version" | "options"> & { enabled: boolean }
type Trace = <T>(stage: string, tags: LogTags, task: () => Promise<T>) => Promise<T>
const PluginContext = createContext<Value>()
export function PluginProvider(props: ParentProps<{ packages: PackageSource; directories: string[] }>) {
const host = usePluginHost()
const log = useLog({ component: "plugin" })
const config = useConfig()
const lifecycle = useTuiLifecycle()
const client = useClient()
@@ -111,34 +108,6 @@ export function PluginProvider(props: ParentProps<{ packages: PackageSource; dir
// One save can emit several watch events. Remember setup failures so those
// events do not repeatedly tear down and restore the last good generation.
const setupFailures = new Map<string, { version: string; options: Registration["options"]; error: string }>()
let operationID = 0
const trace: Trace = (stage, tags, task) => {
const id = ++operationID
const started = Date.now()
log.debug("plugin operation started", { id, stage, ...tags })
const stalled = setTimeout(
() => log.warn("plugin operation stalled", { id, stage, elapsedMs: Date.now() - started, ...tags }),
5_000,
)
return task()
.then(
(value) => {
log.debug("plugin operation completed", { id, stage, durationMs: Date.now() - started, ...tags })
return value
},
(error) => {
log.warn("plugin operation failed", {
id,
stage,
durationMs: Date.now() - started,
error: errorMessage(error),
...tags,
})
throw error
},
)
.finally(() => clearTimeout(stalled))
}
const markdown = createMarkdownRenderer(() =>
Object.values(store.registrations).flatMap((registration) => (registration.active ? [registration.markdown] : [])),
)
@@ -180,9 +149,7 @@ export function PluginProvider(props: ParentProps<{ packages: PackageSource; dir
active: () => Boolean(store.registrations[id]?.active),
},
})
const cleanup = await trace("setup", { plugin: id, target: item.target }, () =>
setup(item.plugin, context, owned),
).catch((error) => {
const cleanup = await setup(item.plugin, context, owned).catch((error) => {
clearContributions(id)
if (item.target)
setupFailures.set(item.target, {
@@ -214,9 +181,7 @@ export function PluginProvider(props: ParentProps<{ packages: PackageSource; dir
setStore("registrations", id, "active", false)
setStore("registrations", id, "cleanups", [])
})
await trace("cleanup", { plugin: id, target: item.target, cleanups: cleanups.length }, () =>
disposeAll(cleanups),
).finally(() =>
await disposeAll(cleanups).finally(() =>
batch(() => {
if (store.registrations[id]) {
clearContributions(id)
@@ -278,20 +243,10 @@ export function PluginProvider(props: ParentProps<{ packages: PackageSource; dir
// Package resolution failures would otherwise retry a full npm install on
// every watch event; remember them until the configuration changes.
const npmFailures = new Map<string, string>()
let reconciliationID = 0
const reconcile = async () => {
const id = ++reconciliationID
const started = Date.now()
log.info("plugin reconciliation started", { id })
await trace("watch", { reconciliation: id, directories: props.directories }, () =>
Promise.all(props.directories.map(watcher.wait)).then(() => undefined),
)
await Promise.all(props.directories.map(watcher.wait))
const entries = [
...(
await trace("discover", { reconciliation: id, directories: props.directories }, () =>
discoverPluginTargets(props.directories),
)
).map((entry) => ({
...(await discoverPluginTargets(props.directories)).map((entry) => ({
entry,
install: true,
optional: true,
@@ -340,20 +295,12 @@ export function PluginProvider(props: ParentProps<{ packages: PackageSource; dir
const memo = local ? undefined : npmFailures.get(target)
const resolved = memo
? { status: "failed" as const, error: memo }
: await resolvePlugin(
target,
local,
options,
previous,
props.packages,
source.install,
sources.read,
trace,
id,
).catch((error) => ({
status: "failed" as const,
error: errorMessage(error),
}))
: await resolvePlugin(target, local, options, previous, props.packages, source.install, sources.read).catch(
(error) => ({
status: "failed" as const,
error: errorMessage(error),
}),
)
if (resolved.status === "unsupported") {
if (source.optional) continue
failures.push({ target, status: "unsupported" })
@@ -479,7 +426,6 @@ export function PluginProvider(props: ParentProps<{ packages: PackageSource; dir
action: { label: "Open plugins", run: () => host.keymap.dispatch("plugins.list") },
})
setStore("states", reconcileStore(states))
log.info("plugin reconciliation completed", { id, durationMs: Date.now() - started, plugins: desired.size })
}
const slotItems = new WeakMap<SlotRender, Claim<SlotRender>>()
// The mounted slot tree: path -> live <Slot> instance count. Reference
@@ -652,26 +598,17 @@ async function resolvePlugin(
packages: PackageSource,
install: boolean,
readSource: ReturnType<typeof createPluginSources>["read"],
trace: Trace,
reconciliation: number,
) {
// Package entrypoints never change within a session, so a loaded previous
// version needs no re-resolution (which could otherwise hit npm).
if (!local && previous && sameOptions(previous.options, options))
return { status: "unchanged" as const, plugin: previous.plugin, version: previous.version }
const target = local
? { directory: fileURLToPath(local) }
: await trace("prepare", { reconciliation, target: spec, install }, () => packages.prepare(spec, install))
const target = local ? { directory: fileURLToPath(local) } : await packages.prepare(spec, install)
const entrypoint = Host.resolve(target).tui
if (!entrypoint) return { status: "unsupported" as const }
// Content remains stable across the several mtimes one save may expose to
// filesystem watchers, while the generation keeps reverted modules fresh.
let source = local
? await trace("read", { reconciliation, target: spec, entrypoint }, () => readSource(entrypoint))
: {
version: entrypoint,
module: await trace("load", { reconciliation, target: spec, entrypoint }, () => Host.load(entrypoint)),
}
let source = local ? await readSource(entrypoint) : { version: entrypoint, module: await Host.load(entrypoint) }
while (true) {
const version = source.version
if (previous && previous.version === version && sameOptions(previous.options, options))
+6 -36
View File
@@ -20,10 +20,6 @@ export interface ScrollViewProps extends ComponentProps<"div"> {
thumbContainer?: HTMLElement
/** Element whose hover reveals the thumb. Defaults to the ScrollView root when unset. */
thumbHoverTarget?: HTMLElement
/** Reconcile native scroll geometry before keyboard or scrollbar navigation reads it. */
onBeforeScroll?: () => void
/** Offset/extent correction for a virtualized vertical scrollbar. */
verticalScrollAdjustment?: number
}
export const scrollKey = (event: Pick<KeyboardEvent, "key" | "altKey" | "ctrlKey" | "metaKey" | "shiftKey">) => {
@@ -128,8 +124,6 @@ export function ScrollView(props: ScrollViewProps) {
"thumbVisibility",
"thumbContainer",
"thumbHoverTarget",
"onBeforeScroll",
"verticalScrollAdjustment",
"style",
],
[
@@ -195,19 +189,17 @@ export function ScrollView(props: ScrollViewProps) {
const minThumbSize = 32
if (vertical()) {
const adjustment = local.verticalScrollAdjustment ?? 0
const scrollHeight = viewportRef.scrollHeight + adjustment
const trackSize = Math.max(0, (thumbMount()?.clientHeight || viewportRef.clientHeight) - trackPadding * 2)
const size = trackSize
? Math.min(trackSize, Math.max((viewportRef.clientHeight / scrollHeight) * trackSize, minThumbSize))
? Math.min(trackSize, Math.max((viewportRef.clientHeight / viewportRef.scrollHeight) * trackSize, minThumbSize))
: 0
const maxScroll = scrollHeight - viewportRef.clientHeight
const maxScroll = viewportRef.scrollHeight - viewportRef.clientHeight
const maxStart = trackSize - size
setState("showVerticalThumb", maxScroll > 0)
setState("verticalThumbSize", size)
setState(
"verticalThumbStart",
trackPadding + (maxScroll > 0 ? ((viewportRef.scrollTop + adjustment) / maxScroll) * maxStart : 0),
trackPadding + (maxScroll > 0 ? (viewportRef.scrollTop / maxScroll) * maxStart : 0),
)
} else {
setState("showVerticalThumb", false)
@@ -271,17 +263,9 @@ export function ScrollView(props: ScrollViewProps) {
})
})
const prepareScroll = () => {
if (local.onBeforeScroll) {
local.onBeforeScroll()
updateThumb()
}
}
const onThumbPointerDown = (axis: "vertical" | "horizontal", e: PointerEvent) => {
e.preventDefault()
e.stopPropagation()
prepareScroll()
setState("dragging", axis)
const thumb = axis === "vertical" ? verticalThumbRef : horizontalThumbRef
const grabOffset =
@@ -293,7 +277,6 @@ export function ScrollView(props: ScrollViewProps) {
thumb.setPointerCapture(e.pointerId)
const onPointerMove = (e: PointerEvent) => {
prepareScroll()
const vertical = axis === "vertical"
const rtl = !vertical && getComputedStyle(viewportRef).direction === "rtl"
const offset = scrollOffsetFromThumbPointer({
@@ -372,23 +355,10 @@ export function ScrollView(props: ScrollViewProps) {
return
}
const next = scrollKey(e)
// Modified navigation (for example Ctrl+Home) stays native, but must read
// the same reconciled geometry as the keys handled by this component.
const intent =
next ??
scrollKey({
key: e.key,
shiftKey: e.key === " " && e.shiftKey,
altKey: false,
ctrlKey: false,
metaKey: false,
})
if (!intent) return
if (!isScrollKeyTarget(e.target, intent)) return
if (scrollKeyOwner(viewportRef, e.target, intent) !== viewportRef) return
prepareScroll()
if (!next) return
if (!isScrollKeyTarget(e.target, next)) return
if (scrollKeyOwner(viewportRef, e.target, next) !== viewportRef) return
const scrollAmount = viewportRef.clientHeight * 0.8
const lineAmount = 40
+2 -1
View File
@@ -44,7 +44,7 @@ function knownThemes() {
}
const names: Record<string, string> = {
"oc-2": "OpenCode",
"oc-2": "OC-2",
amoled: "AMOLED",
aura: "Aura",
ayu: "Ayu",
@@ -69,6 +69,7 @@ const names: Record<string, string> = {
nord: "Nord",
"one-dark": "One Dark",
onedarkpro: "One Dark Pro",
opencode: "OpenCode",
orng: "Orng",
"osaka-jade": "Osaka Jade",
palenight: "Palenight",
+3
View File
@@ -24,6 +24,7 @@ import nightowlThemeJson from "./themes/nightowl.json"
import nordThemeJson from "./themes/nord.json"
import oneDarkThemeJson from "./themes/one-dark.json"
import oneDarkProThemeJson from "./themes/onedarkpro.json"
import opencodeThemeJson from "./themes/opencode.json"
import orngThemeJson from "./themes/orng.json"
import osakaJadeThemeJson from "./themes/osaka-jade.json"
import palenightThemeJson from "./themes/palenight.json"
@@ -61,6 +62,7 @@ export const nightowlTheme = nightowlThemeJson as DesktopTheme
export const nordTheme = nordThemeJson as DesktopTheme
export const oneDarkTheme = oneDarkThemeJson as DesktopTheme
export const oneDarkProTheme = oneDarkProThemeJson as DesktopTheme
export const opencodeTheme = opencodeThemeJson as DesktopTheme
export const orngTheme = orngThemeJson as DesktopTheme
export const osakaJadeTheme = osakaJadeThemeJson as DesktopTheme
export const palenightTheme = palenightThemeJson as DesktopTheme
@@ -99,6 +101,7 @@ export const DEFAULT_THEMES: Record<string, DesktopTheme> = {
nord: nordTheme,
"one-dark": oneDarkTheme,
onedarkpro: oneDarkProTheme,
opencode: opencodeTheme,
orng: orngTheme,
"osaka-jade": osakaJadeTheme,
palenight: palenightTheme,
+1
View File
@@ -63,6 +63,7 @@ export {
nordTheme,
oneDarkTheme,
oneDarkProTheme,
opencodeTheme,
orngTheme,
osakaJadeTheme,
palenightTheme,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "OpenCode",
"name": "OC-2",
"id": "oc-2",
"light": {
"palette": {
@@ -0,0 +1,89 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "OpenCode",
"id": "opencode",
"light": {
"palette": {
"neutral": "#ffffff",
"ink": "#1a1a1a",
"primary": "#3b7dd8",
"accent": "#d68c27",
"success": "#3d9a57",
"warning": "#d68c27",
"error": "#d1383d",
"info": "#318795",
"diffAdd": "#4db380",
"diffDelete": "#f52a65"
},
"overrides": {
"text-weak": "#8a8a8a",
"syntax-comment": "#8a8a8a",
"syntax-keyword": "#d68c27",
"syntax-string": "#3d9a57",
"syntax-primitive": "#3b7dd8",
"syntax-variable": "#d1383d",
"syntax-property": "#318795",
"syntax-type": "#b0851f",
"syntax-constant": "#d68c27",
"syntax-operator": "#318795",
"syntax-punctuation": "#1a1a1a",
"syntax-object": "#d1383d",
"markdown-heading": "#d68c27",
"markdown-text": "#1a1a1a",
"markdown-link": "#3b7dd8",
"markdown-link-text": "#318795",
"markdown-code": "#3d9a57",
"markdown-block-quote": "#b0851f",
"markdown-emph": "#b0851f",
"markdown-strong": "#d68c27",
"markdown-horizontal-rule": "#8a8a8a",
"markdown-list-item": "#3b7dd8",
"markdown-list-enumeration": "#318795",
"markdown-image": "#3b7dd8",
"markdown-image-text": "#318795",
"markdown-code-block": "#1a1a1a"
}
},
"dark": {
"palette": {
"neutral": "#0a0a0a",
"ink": "#eeeeee",
"primary": "#fab283",
"accent": "#9d7cd8",
"success": "#7fd88f",
"warning": "#f5a742",
"error": "#e06c75",
"info": "#56b6c2",
"diffAdd": "#b8db87",
"diffDelete": "#e26a75"
},
"overrides": {
"text-weak": "#808080",
"syntax-comment": "#808080",
"syntax-keyword": "#9d7cd8",
"syntax-string": "#7fd88f",
"syntax-primitive": "#fab283",
"syntax-variable": "#e06c75",
"syntax-property": "#56b6c2",
"syntax-type": "#e5c07b",
"syntax-constant": "#f5a742",
"syntax-operator": "#56b6c2",
"syntax-punctuation": "#eeeeee",
"syntax-object": "#e06c75",
"markdown-heading": "#9d7cd8",
"markdown-text": "#eeeeee",
"markdown-link": "#fab283",
"markdown-link-text": "#56b6c2",
"markdown-code": "#7fd88f",
"markdown-block-quote": "#e5c07b",
"markdown-emph": "#e5c07b",
"markdown-strong": "#f5a742",
"markdown-horizontal-rule": "#808080",
"markdown-list-item": "#fab283",
"markdown-list-enumeration": "#56b6c2",
"markdown-image": "#fab283",
"markdown-image-text": "#56b6c2",
"markdown-code-block": "#eeeeee"
}
}
}
+9 -139
View File
@@ -1,5 +1,5 @@
diff --git a/dist/cjs/index.cjs b/dist/cjs/index.cjs
index e470032a9572b3ced764ca02238a8c6be435a9d4..4c25e03b5db221a78793083d97970b2d90e28efa 100644
index e470032a9572b3ced764ca02238a8c6be435a9d4..84a7bfca638f70e32a9567bc732a84b83578af4e 100644
--- a/dist/cjs/index.cjs
+++ b/dist/cjs/index.cjs
@@ -289,7 +289,7 @@ class Virtualizer {
@@ -82,50 +82,7 @@ index e470032a9572b3ced764ca02238a8c6be435a9d4..4c25e03b5db221a78793083d97970b2d
followOnAppend,
anchorDelta
];
@@ -440,7 +450,17 @@ class Virtualizer {
);
if ("addEventListener" in this.scrollElement) {
const scrollEl = this.scrollElement;
- const onTouchStart = () => {
+ let touchTarget = null;
+ const clearTouchTarget = () => {
+ touchTarget?.removeEventListener("touchend", onTouchEnd);
+ touchTarget?.removeEventListener("touchcancel", onTouchEnd);
+ touchTarget = null;
+ };
+ const onTouchStart = (event) => {
+ clearTouchTarget();
+ touchTarget = event.target ?? scrollEl;
+ touchTarget.addEventListener("touchend", onTouchEnd, addEventListenerOptions);
+ touchTarget.addEventListener("touchcancel", onTouchEnd, addEventListenerOptions);
this._iosTouching = true;
this._iosJustTouchEnded = false;
if (this._iosTouchEndTimerId !== null && this.targetWindow != null) {
@@ -449,6 +469,7 @@ class Virtualizer {
}
};
const onTouchEnd = () => {
+ clearTouchTarget();
this._iosTouching = false;
if (!isIOSWebKit() || this.targetWindow == null) {
return;
@@ -465,14 +486,9 @@ class Virtualizer {
onTouchStart,
addEventListenerOptions
);
- scrollEl.addEventListener(
- "touchend",
- onTouchEnd,
- addEventListenerOptions
- );
this.unsubs.push(() => {
scrollEl.removeEventListener("touchstart", onTouchStart);
- scrollEl.removeEventListener("touchend", onTouchEnd);
+ clearTouchTarget();
if (this._iosTouchEndTimerId !== null && this.targetWindow != null) {
this.targetWindow.clearTimeout(this._iosTouchEndTimerId);
this._iosTouchEndTimerId = null;
@@ -725,17 +741,20 @@ class Virtualizer {
@@ -725,17 +735,20 @@ class Virtualizer {
this.getMeasurements(),
this.getSize(),
this.getScrollOffset(),
@@ -149,7 +106,7 @@ index e470032a9572b3ced764ca02238a8c6be435a9d4..4c25e03b5db221a78793083d97970b2d
lanes,
// Pass the typed array so binary search + forward-walk can read
// start/end directly from Float64Array, skipping the Proxy traps.
@@ -1095,8 +1114,10 @@ class Virtualizer {
@@ -1095,8 +1108,10 @@ class Virtualizer {
const snapshot = [];
if (this.itemSizeCache.size === 0) return snapshot;
const m = this.getMeasurements();
@@ -189,7 +146,7 @@ index 6b43c0aea7ed9eeef75cbfb1351fcbd243913bdd..7be2680967934ddfbc4583a210a3d11e
getVirtualIndexes: {
(): number[];
diff --git a/dist/esm/index.js b/dist/esm/index.js
index 2495b26cf2c3589213546b3958eaadf2eb6b751d..1398bc7dd33690a8fe3280343b6aabdeaea6435a 100644
index 2495b26cf2c3589213546b3958eaadf2eb6b751d..accd38a89bee766fd568aa8cec9bb90c6789277a 100644
--- a/dist/esm/index.js
+++ b/dist/esm/index.js
@@ -287,7 +287,7 @@ class Virtualizer {
@@ -272,50 +229,7 @@ index 2495b26cf2c3589213546b3958eaadf2eb6b751d..1398bc7dd33690a8fe3280343b6aabde
followOnAppend,
anchorDelta
];
@@ -438,7 +448,17 @@ class Virtualizer {
);
if ("addEventListener" in this.scrollElement) {
const scrollEl = this.scrollElement;
- const onTouchStart = () => {
+ let touchTarget = null;
+ const clearTouchTarget = () => {
+ touchTarget?.removeEventListener("touchend", onTouchEnd);
+ touchTarget?.removeEventListener("touchcancel", onTouchEnd);
+ touchTarget = null;
+ };
+ const onTouchStart = (event) => {
+ clearTouchTarget();
+ touchTarget = event.target ?? scrollEl;
+ touchTarget.addEventListener("touchend", onTouchEnd, addEventListenerOptions);
+ touchTarget.addEventListener("touchcancel", onTouchEnd, addEventListenerOptions);
this._iosTouching = true;
this._iosJustTouchEnded = false;
if (this._iosTouchEndTimerId !== null && this.targetWindow != null) {
@@ -447,6 +467,7 @@ class Virtualizer {
}
};
const onTouchEnd = () => {
+ clearTouchTarget();
this._iosTouching = false;
if (!isIOSWebKit() || this.targetWindow == null) {
return;
@@ -463,14 +484,9 @@ class Virtualizer {
onTouchStart,
addEventListenerOptions
);
- scrollEl.addEventListener(
- "touchend",
- onTouchEnd,
- addEventListenerOptions
- );
this.unsubs.push(() => {
scrollEl.removeEventListener("touchstart", onTouchStart);
- scrollEl.removeEventListener("touchend", onTouchEnd);
+ clearTouchTarget();
if (this._iosTouchEndTimerId !== null && this.targetWindow != null) {
this.targetWindow.clearTimeout(this._iosTouchEndTimerId);
this._iosTouchEndTimerId = null;
@@ -723,17 +739,20 @@ class Virtualizer {
@@ -723,17 +733,20 @@ class Virtualizer {
this.getMeasurements(),
this.getSize(),
this.getScrollOffset(),
@@ -339,7 +253,7 @@ index 2495b26cf2c3589213546b3958eaadf2eb6b751d..1398bc7dd33690a8fe3280343b6aabde
lanes,
// Pass the typed array so binary search + forward-walk can read
// start/end directly from Float64Array, skipping the Proxy traps.
@@ -1093,8 +1112,10 @@ class Virtualizer {
@@ -1093,8 +1106,10 @@ class Virtualizer {
const snapshot = [];
if (this.itemSizeCache.size === 0) return snapshot;
const m = this.getMeasurements();
@@ -353,7 +267,7 @@ index 2495b26cf2c3589213546b3958eaadf2eb6b751d..1398bc7dd33690a8fe3280343b6aabde
index: item.index,
key: item.key,
diff --git a/src/index.ts b/src/index.ts
index dc6f1010c4d4758de9c46fb8d69209e582e47171..9299287d72ce79d5a597cdc3440339ee148e6385 100644
index dc6f1010c4d4758de9c46fb8d69209e582e47171..2578338abb5d9237624dd6d96bb50546c6dc3e68 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -567,7 +567,7 @@ export class Virtualizer<
@@ -466,51 +380,7 @@ index dc6f1010c4d4758de9c46fb8d69209e582e47171..9299287d72ce79d5a597cdc3440339ee
followOnAppend,
anchorDelta,
]
@@ -895,7 +906,18 @@ export class Virtualizer<
// and _flushIosDeferredIfReady so we only burn the path on iOS.
if ('addEventListener' in this.scrollElement) {
const scrollEl = this.scrollElement as unknown as EventTarget
- const onTouchStart = () => {
+ let touchTarget: EventTarget | null = null
+ const clearTouchTarget = () => {
+ touchTarget?.removeEventListener('touchend', onTouchEnd)
+ touchTarget?.removeEventListener('touchcancel', onTouchEnd)
+ touchTarget = null
+ }
+ const onTouchStart = (event: Event) => {
+ clearTouchTarget()
+ // The original target still receives release after its DOM removal.
+ touchTarget = event.target ?? scrollEl
+ touchTarget.addEventListener('touchend', onTouchEnd, addEventListenerOptions)
+ touchTarget.addEventListener('touchcancel', onTouchEnd, addEventListenerOptions)
this._iosTouching = true
this._iosJustTouchEnded = false
if (this._iosTouchEndTimerId !== null && this.targetWindow != null) {
@@ -904,6 +926,7 @@ export class Virtualizer<
}
}
const onTouchEnd = () => {
+ clearTouchTarget()
this._iosTouching = false
if (!isIOSWebKit() || this.targetWindow == null) {
// Non-iOS: nothing more to track. Just clear the touching flag.
@@ -924,14 +947,9 @@ export class Virtualizer<
onTouchStart,
addEventListenerOptions,
)
- scrollEl.addEventListener(
- 'touchend',
- onTouchEnd,
- addEventListenerOptions,
- )
this.unsubs.push(() => {
scrollEl.removeEventListener('touchstart', onTouchStart)
- scrollEl.removeEventListener('touchend', onTouchEnd)
+ clearTouchTarget()
if (this._iosTouchEndTimerId !== null && this.targetWindow != null) {
this.targetWindow.clearTimeout(this._iosTouchEndTimerId)
this._iosTouchEndTimerId = null
@@ -1410,16 +1428,25 @@ export class Virtualizer<
@@ -1410,16 +1421,25 @@ export class Virtualizer<
this.getSize(),
this.getScrollOffset(),
this.options.lanes,
@@ -538,7 +408,7 @@ index dc6f1010c4d4758de9c46fb8d69209e582e47171..9299287d72ce79d5a597cdc3440339ee
lanes,
// Pass the typed array so binary search + forward-walk can read
// start/end directly from Float64Array, skipping the Proxy traps.
@@ -1937,12 +1964,13 @@ export class Virtualizer<
@@ -1937,12 +1957,13 @@ export class Virtualizer<
takeSnapshot = (): Array<VirtualItem> => {
const snapshot: Array<VirtualItem> = []
if (this.itemSizeCache.size === 0) return snapshot