chore: cleanup (#17284)

This commit is contained in:
Adam 2026-03-13 06:27:58 -05:00 committed by GitHub
parent 46ba9c8170
commit 270cb0b8b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 516 additions and 357 deletions

View File

@ -86,15 +86,17 @@ const useServerHealth = (servers: Accessor<ServerConnection.Any[]>) => {
const useDefaultServerKey = (
get: (() => string | Promise<string | null | undefined> | null | undefined) | undefined,
) => {
const [url, setUrl] = createSignal<string | undefined>()
const [tick, setTick] = createSignal(0)
const [state, setState] = createStore({
url: undefined as string | undefined,
tick: 0,
})
createEffect(() => {
tick()
state.tick
let dead = false
const result = get?.()
if (!result) {
setUrl(undefined)
setState("url", undefined)
onCleanup(() => {
dead = true
})
@ -104,7 +106,7 @@ const useDefaultServerKey = (
if (result instanceof Promise) {
void result.then((next) => {
if (dead) return
setUrl(next ? normalizeServerUrl(next) : undefined)
setState("url", next ? normalizeServerUrl(next) : undefined)
})
onCleanup(() => {
dead = true
@ -112,7 +114,7 @@ const useDefaultServerKey = (
return
}
setUrl(normalizeServerUrl(result))
setState("url", normalizeServerUrl(result))
onCleanup(() => {
dead = true
})
@ -120,11 +122,11 @@ const useDefaultServerKey = (
return {
key: () => {
const u = url()
const u = state.url
if (!u) return
return ServerConnection.key({ type: "http", http: { url: u } })
},
refresh: () => setTick((value) => value + 1),
refresh: () => setState("tick", (value) => value + 1),
}
}

View File

@ -1,16 +1,4 @@
import {
batch,
createEffect,
createMemo,
createSignal,
For,
on,
onCleanup,
onMount,
ParentProps,
Show,
untrack,
} from "solid-js"
import { batch, createEffect, createMemo, For, on, onCleanup, onMount, ParentProps, Show, untrack } from "solid-js"
import { useNavigate, useParams } from "@solidjs/router"
import { useLayout, LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync"
@ -145,6 +133,10 @@ export default function Layout(props: ParentProps) {
hoverProject: undefined as string | undefined,
scrollSessionKey: undefined as string | undefined,
nav: undefined as HTMLElement | undefined,
sortNow: Date.now(),
sizing: false,
peek: undefined as LocalProject | undefined,
peeked: false,
})
const editor = createInlineEditorController()
@ -163,14 +155,13 @@ export default function Layout(props: ParentProps) {
}
const isBusy = (directory: string) => !!state.busyWorkspaces[workspaceKey(directory)]
const navLeave = { current: undefined as number | undefined }
const [sortNow, setSortNow] = createSignal(Date.now())
const [sizing, setSizing] = createSignal(false)
const sortNow = () => state.sortNow
let sizet: number | undefined
let sortNowInterval: ReturnType<typeof setInterval> | undefined
const sortNowTimeout = setTimeout(
() => {
setSortNow(Date.now())
sortNowInterval = setInterval(() => setSortNow(Date.now()), 60_000)
setState("sortNow", Date.now())
sortNowInterval = setInterval(() => setState("sortNow", Date.now()), 60_000)
},
60_000 - (Date.now() % 60_000),
)
@ -196,7 +187,7 @@ export default function Layout(props: ParentProps) {
})
onMount(() => {
const stop = () => setSizing(false)
const stop = () => setState("sizing", false)
window.addEventListener("pointerup", stop)
window.addEventListener("pointercancel", stop)
window.addEventListener("blur", stop)
@ -234,8 +225,6 @@ export default function Layout(props: ParentProps) {
}, 300)
}
const [peek, setPeek] = createSignal<LocalProject | undefined>(undefined)
const [peeked, setPeeked] = createSignal(false)
let peekt: number | undefined
const hoverProjectData = createMemo(() => {
@ -251,17 +240,17 @@ export default function Layout(props: ParentProps) {
clearTimeout(peekt)
peekt = undefined
}
setPeek(p)
setPeeked(true)
setState("peek", p)
setState("peeked", true)
return
}
setPeeked(false)
if (peek() === undefined) return
setState("peeked", false)
if (state.peek === undefined) return
if (peekt !== undefined) clearTimeout(peekt)
peekt = window.setTimeout(() => {
peekt = undefined
setPeek(undefined)
setState("peek", undefined)
}, 180)
})
@ -2245,7 +2234,7 @@ export default function Layout(props: ParentProps) {
>
<div class="@container w-full h-full contain-strict">{sidebarContent()}</div>
<Show when={layout.sidebar.opened()}>
<div onPointerDown={() => setSizing(true)}>
<div onPointerDown={() => setState("sizing", true)}>
<ResizeHandle
direction="horizontal"
size={layout.sidebar.width()}
@ -2253,9 +2242,9 @@ export default function Layout(props: ParentProps) {
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
collapseThreshold={244}
onResize={(w) => {
setSizing(true)
setState("sizing", true)
if (sizet !== undefined) clearTimeout(sizet)
sizet = window.setTimeout(() => setSizing(false), 120)
sizet = window.setTimeout(() => setState("sizing", false), 120)
layout.sidebar.resize(w)
}}
onCollapse={layout.sidebar.close}
@ -2300,7 +2289,7 @@ export default function Layout(props: ParentProps) {
"xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
"z-20": true,
"transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
!sizing(),
!state.sizing,
}}
style={{
"--main-left": layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "4rem",
@ -2320,11 +2309,11 @@ export default function Layout(props: ParentProps) {
<div
classList={{
"hidden xl:flex absolute inset-y-0 left-16 z-30": true,
"opacity-100 translate-x-0 pointer-events-auto": peeked() && !layout.sidebar.opened(),
"opacity-0 -translate-x-2 pointer-events-none": !peeked() || layout.sidebar.opened(),
"opacity-100 translate-x-0 pointer-events-auto": state.peeked && !layout.sidebar.opened(),
"opacity-0 -translate-x-2 pointer-events-none": !state.peeked || layout.sidebar.opened(),
"transition-[opacity,transform] motion-reduce:transition-none": true,
"duration-180 ease-out": peeked() && !layout.sidebar.opened(),
"duration-120 ease-in": !peeked() || layout.sidebar.opened(),
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
"duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
}}
onMouseMove={disarm}
onMouseEnter={() => {
@ -2336,19 +2325,19 @@ export default function Layout(props: ParentProps) {
arm()
}}
>
<Show when={peek()}>
<SidebarPanel project={peek()} merged={false} />
<Show when={state.peek}>
<SidebarPanel project={state.peek} merged={false} />
</Show>
</div>
<div
classList={{
"hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
"opacity-100 translate-x-0": peeked() && !layout.sidebar.opened(),
"opacity-0 -translate-x-2": !peeked() || layout.sidebar.opened(),
"opacity-100 translate-x-0": state.peeked && !layout.sidebar.opened(),
"opacity-0 -translate-x-2": !state.peeked || layout.sidebar.opened(),
"transition-[opacity,transform] motion-reduce:transition-none": true,
"duration-180 ease-out": peeked() && !layout.sidebar.opened(),
"duration-120 ease-in": !peeked() || layout.sidebar.opened(),
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
"duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
}}
style={{ left: `calc(4rem + ${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px)` }}
>

View File

@ -1,4 +1,5 @@
import { For, Index, createEffect, createMemo, createSignal, on } from "solid-js"
import { For, Index, createEffect, createMemo, on } from "solid-js"
import { createStore } from "solid-js/store"
const TRACK = Array.from({ length: 30 }, (_, index) => index % 10)
const DURATION = 600
@ -14,8 +15,12 @@ function spin(from: number, to: number, direction: 1 | -1) {
}
function Digit(props: { value: number; direction: 1 | -1 }) {
const [step, setStep] = createSignal(props.value + 10)
const [animating, setAnimating] = createSignal(false)
const [state, setState] = createStore({
step: props.value + 10,
animating: false,
})
const step = () => state.step
const animating = () => state.animating
let last = props.value
createEffect(
@ -25,13 +30,13 @@ function Digit(props: { value: number; direction: 1 | -1 }) {
const delta = spin(last, next, props.direction)
last = next
if (!delta) {
setAnimating(false)
setStep(next + 10)
setState("animating", false)
setState("step", next + 10)
return
}
setAnimating(true)
setStep((value) => value + delta)
setState("animating", true)
setState("step", (value) => value + delta)
},
{ defer: true },
),
@ -43,8 +48,8 @@ function Digit(props: { value: number; direction: 1 | -1 }) {
data-slot="animated-number-strip"
data-animating={animating() ? "true" : "false"}
onTransitionEnd={() => {
setAnimating(false)
setStep((value) => normalize(value) + 10)
setState("animating", false)
setState("step", (value) => normalize(value) + 10)
}}
style={{
"--animated-number-offset": `${step()}`,
@ -63,8 +68,12 @@ export function AnimatedNumber(props: { value: number; class?: string }) {
return Math.max(0, Math.round(props.value))
})
const [value, setValue] = createSignal(target())
const [direction, setDirection] = createSignal<1 | -1>(1)
const [state, setState] = createStore({
value: target(),
direction: 1 as 1 | -1,
})
const value = () => state.value
const direction = () => state.direction
createEffect(
on(
@ -73,8 +82,8 @@ export function AnimatedNumber(props: { value: number; class?: string }) {
const current = value()
if (next === current) return
setDirection(next > current ? 1 : -1)
setValue(next)
setState("direction", next > current ? 1 : -1)
setState("value", next)
},
{ defer: true },
),

View File

@ -1,5 +1,6 @@
import { createEffect, createSignal, For, Match, on, onCleanup, Show, Switch, type JSX } from "solid-js"
import { createEffect, For, Match, on, onCleanup, Show, Switch, type JSX } from "solid-js"
import { animate, type AnimationPlaybackControls } from "motion"
import { createStore } from "solid-js/store"
import { Collapsible } from "./collapsible"
import type { IconProps } from "./icon"
import { TextShimmer } from "./text-shimmer"
@ -37,8 +38,12 @@ export interface BasicToolProps {
const SPRING = { type: "spring" as const, visualDuration: 0.35, bounce: 0 }
export function BasicTool(props: BasicToolProps) {
const [open, setOpen] = createSignal(props.defaultOpen ?? false)
const [ready, setReady] = createSignal(open())
const [state, setState] = createStore({
open: props.defaultOpen ?? false,
ready: props.defaultOpen ?? false,
})
const open = () => state.open
const ready = () => state.ready
const pending = () => props.status === "pending" || props.status === "running"
let frame: number | undefined
@ -52,7 +57,7 @@ export function BasicTool(props: BasicToolProps) {
onCleanup(cancel)
createEffect(() => {
if (props.forceOpen) setOpen(true)
if (props.forceOpen) setState("open", true)
})
createEffect(
@ -62,7 +67,7 @@ export function BasicTool(props: BasicToolProps) {
if (!props.defer) return
if (!value) {
cancel()
setReady(false)
setState("ready", false)
return
}
@ -70,7 +75,7 @@ export function BasicTool(props: BasicToolProps) {
frame = requestAnimationFrame(() => {
frame = undefined
if (!open()) return
setReady(true)
setState("ready", true)
})
},
{ defer: true },
@ -112,7 +117,7 @@ export function BasicTool(props: BasicToolProps) {
const handleOpenChange = (value: boolean) => {
if (pending()) return
if (props.locked && !value) return
setOpen(value)
setState("open", value)
}
return (

View File

@ -1,5 +1,6 @@
import { type DiffLineAnnotation, type SelectedLineRange } from "@pierre/diffs"
import { createEffect, createMemo, createSignal, onCleanup, Show, type Accessor, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { render as renderSolid } from "solid-js/web"
import { createHoverCommentUtility } from "../pierre/comment-hover"
import { cloneSelectedLineRange, formatSelectedLineLabel, lineInSelectedRange } from "../pierre/selection-bridge"
@ -200,8 +201,14 @@ export function createLineCommentAnnotationRenderer<T>(props: {
}
export function createLineCommentState<T>(props: LineCommentStateProps<T>) {
const [draft, setDraft] = createSignal("")
const [editing, setEditing] = createSignal<T | null>(null)
const [state, setState] = createStore({
draft: "",
editing: null as T | null,
})
const draft = () => state.draft
const setDraft = (value: string) => setState("draft", value)
const editing = () => state.editing
const setEditing = (value: T | null) => setState("editing", typeof value === "function" ? () => value : value)
const toRange = (range: SelectedLineRange | null) => (range ? cloneSelectedLineRange(range) : null)
const setSelected = (range: SelectedLineRange | null) => {
@ -261,7 +268,7 @@ export function createLineCommentState<T>(props: LineCommentStateProps<T>) {
closeComment()
setSelected(range)
props.setCommenting(null)
setEditing(() => id)
setEditing(id)
setDraft(value)
}

View File

@ -1,5 +1,5 @@
import { type FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks"
import { createEffect, createSignal, For, onCleanup, type JSX, on, Show } from "solid-js"
import { createEffect, For, onCleanup, type JSX, on, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { useI18n } from "../context/i18n"
import { Icon, type IconProps } from "./icon"
@ -56,12 +56,16 @@ export interface ListRef {
export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void }) {
const i18n = useI18n()
const [scrollRef, setScrollRef] = createSignal<HTMLDivElement | undefined>(undefined)
const [internalFilter, setInternalFilter] = createSignal("")
let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined
const [store, setStore] = createStore({
mouseActive: false,
scrollRef: undefined as HTMLDivElement | undefined,
internalFilter: "",
})
const scrollRef = () => store.scrollRef
const setScrollRef = (el: HTMLDivElement | undefined) => setStore("scrollRef", el)
const internalFilter = () => store.internalFilter
const setInternalFilter = (value: string) => setStore("internalFilter", value)
const scrollIntoView = (container: HTMLDivElement, node: HTMLElement, block: "center" | "nearest") => {
const containerRect = container.getBoundingClientRect()
@ -208,18 +212,20 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
}
function GroupHeader(groupProps: { group: { category: string; items: T[] } }): JSX.Element {
const [stuck, setStuck] = createSignal(false)
const [header, setHeader] = createSignal<HTMLDivElement | undefined>(undefined)
const [state, setState] = createStore({
stuck: false,
header: undefined as HTMLDivElement | undefined,
})
createEffect(() => {
const scroll = scrollRef()
const node = header()
const node = state.header
if (!scroll || !node) return
const handler = () => {
const rect = node.getBoundingClientRect()
const scrollRect = scroll.getBoundingClientRect()
setStuck(rect.top <= scrollRect.top + 1 && scroll.scrollTop > 0)
setState("stuck", rect.top <= scrollRect.top + 1 && scroll.scrollTop > 0)
}
scroll.addEventListener("scroll", handler, { passive: true })
@ -228,7 +234,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
})
return (
<div data-slot="list-header" data-stuck={stuck()} ref={setHeader}>
<div data-slot="list-header" data-stuck={state.stuck} ref={(el) => setState("header", el)}>
{props.groupHeader?.(groupProps.group) ?? groupProps.group.category}
</div>
)

View File

@ -12,6 +12,7 @@ import {
Index,
type JSX,
} from "solid-js"
import { createStore } from "solid-js/store"
import stripAnsi from "strip-ansi"
import { Dynamic } from "solid-js/web"
import {
@ -885,8 +886,12 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
const data = useData()
const dialog = useDialog()
const i18n = useI18n()
const [copied, setCopied] = createSignal(false)
const [busy, setBusy] = createSignal<"fork" | "revert" | undefined>()
const [state, setState] = createStore({
copied: false,
busy: undefined as "fork" | "revert" | undefined,
})
const copied = () => state.copied
const busy = () => state.busy
const textPart = createMemo(
() => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined,
@ -946,14 +951,14 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
const content = text()
if (!content) return
await navigator.clipboard.writeText(content)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
setState("copied", true)
setTimeout(() => setState("copied", false), 2000)
}
const run = (kind: "fork" | "revert") => {
const act = kind === "fork" ? props.actions?.fork : props.actions?.revert
if (!act || busy()) return
setBusy(kind)
setState("busy", kind)
void Promise.resolve()
.then(() =>
act({
@ -962,7 +967,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
}),
)
.finally(() => {
if (busy() === kind) setBusy(undefined)
if (busy() === kind) setState("busy", undefined)
})
}

View File

@ -5,11 +5,11 @@ import {
ParentProps,
Show,
createEffect,
createSignal,
onCleanup,
splitProps,
ValidComponent,
} from "solid-js"
import { createStore } from "solid-js/store"
import { useI18n } from "../context/i18n"
import { IconButton } from "./icon-button"
@ -46,23 +46,24 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
"modal",
])
const [contentRef, setContentRef] = createSignal<HTMLElement | undefined>(undefined)
const [triggerRef, setTriggerRef] = createSignal<HTMLElement | undefined>(undefined)
const [dismiss, setDismiss] = createSignal<"escape" | "outside" | null>(null)
const [uncontrolledOpen, setUncontrolledOpen] = createSignal<boolean>(local.defaultOpen ?? false)
const [state, setState] = createStore({
contentRef: undefined as HTMLElement | undefined,
triggerRef: undefined as HTMLElement | undefined,
dismiss: null as "escape" | "outside" | null,
uncontrolledOpen: local.defaultOpen ?? false,
})
const controlled = () => local.open !== undefined
const opened = () => {
if (controlled()) return local.open ?? false
return uncontrolledOpen()
return state.uncontrolledOpen
}
const onOpenChange = (next: boolean) => {
if (next) setDismiss(null)
if (next) setState("dismiss", null)
if (local.onOpenChange) local.onOpenChange(next)
if (controlled()) return
setUncontrolledOpen(next)
setState("uncontrolledOpen", next)
}
createEffect(() => {
@ -70,15 +71,15 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
const inside = (node: Node | null | undefined) => {
if (!node) return false
const content = contentRef()
const content = state.contentRef
if (content && content.contains(node)) return true
const trigger = triggerRef()
const trigger = state.triggerRef
if (trigger && trigger.contains(node)) return true
return false
}
const close = (reason: "escape" | "outside") => {
setDismiss(reason)
setState("dismiss", reason)
onOpenChange(false)
}
@ -116,7 +117,7 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
const content = () => (
<Kobalte.Content
ref={(el: HTMLElement | undefined) => setContentRef(el)}
ref={(el: HTMLElement | undefined) => setState("contentRef", el)}
data-component="popover-content"
classList={{
...(local.classList ?? {}),
@ -124,8 +125,8 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
}}
style={local.style}
onCloseAutoFocus={(event: Event) => {
if (dismiss() === "outside") event.preventDefault()
setDismiss(null)
if (state.dismiss === "outside") event.preventDefault()
setState("dismiss", null)
}}
>
{/* <Kobalte.Arrow data-slot="popover-arrow" /> */}
@ -151,7 +152,7 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
return (
<Kobalte gutter={4} {...rest} open={opened()} onOpenChange={onOpenChange} modal={local.modal ?? false}>
<Kobalte.Trigger
ref={(el: HTMLElement) => setTriggerRef(el)}
ref={(el: HTMLElement) => setState("triggerRef", el)}
as={local.triggerAs ?? "div"}
data-slot="popover-trigger"
{...(local.triggerProps as any)}

View File

@ -1,5 +1,6 @@
// @ts-nocheck
import { createSignal } from "solid-js"
import { createStore } from "solid-js/store"
import * as mod from "./resize-handle"
const docs = `### Overview
@ -94,8 +95,12 @@ export const Vertical = {
export const Collapse = {
render: () => {
const [size, setSize] = createSignal(200)
const [collapsed, setCollapsed] = createSignal(false)
const [state, setState] = createStore({
size: 200,
collapsed: false,
})
const size = () => state.size
const collapsed = () => state.collapsed
return (
<div style={{ display: "grid", gap: "8px" }}>
<div style={{ color: "var(--text-weak)", "font-size": "12px" }}>
@ -116,10 +121,10 @@ export const Collapse = {
max={360}
collapseThreshold={100}
onResize={(next) => {
setCollapsed(false)
setSize(next)
setState("collapsed", false)
setState("size", next)
}}
onCollapse={() => setCollapsed(true)}
onCollapse={() => setState("collapsed", true)}
style="height:24px;border:1px dashed color-mix(in oklab, var(--text-base) 20%, transparent)"
/>
</div>

View File

@ -1,4 +1,5 @@
import { createSignal, onCleanup, onMount, splitProps, type ComponentProps, Show, mergeProps } from "solid-js"
import { onCleanup, onMount, splitProps, type ComponentProps, Show, mergeProps } from "solid-js"
import { createStore } from "solid-js/store"
import { useI18n } from "../context/i18n"
export interface ScrollViewProps extends ComponentProps<"div"> {
@ -48,23 +49,29 @@ export function ScrollView(props: ScrollViewProps) {
let viewportRef!: HTMLDivElement
let thumbRef!: HTMLDivElement
const [isHovered, setIsHovered] = createSignal(false)
const [isDragging, setIsDragging] = createSignal(false)
const [thumbHeight, setThumbHeight] = createSignal(0)
const [thumbTop, setThumbTop] = createSignal(0)
const [showThumb, setShowThumb] = createSignal(false)
const [state, setState] = createStore({
isHovered: false,
isDragging: false,
thumbHeight: 0,
thumbTop: 0,
showThumb: false,
})
const isHovered = () => state.isHovered
const isDragging = () => state.isDragging
const thumbHeight = () => state.thumbHeight
const thumbTop = () => state.thumbTop
const showThumb = () => state.showThumb
const updateThumb = () => {
if (!viewportRef) return
const { scrollTop, scrollHeight, clientHeight } = viewportRef
if (scrollHeight <= clientHeight || scrollHeight === 0) {
setShowThumb(false)
setState("showThumb", false)
return
}
setShowThumb(true)
setState("showThumb", true)
const trackPadding = 8
const trackHeight = clientHeight - trackPadding * 2
@ -81,8 +88,8 @@ export function ScrollView(props: ScrollViewProps) {
// Ensure thumb stays within bounds (shouldn't be necessary due to math above, but good for safety)
const boundedTop = trackPadding + Math.max(0, Math.min(top, maxThumbTop))
setThumbHeight(height)
setThumbTop(boundedTop)
setState("thumbHeight", height)
setState("thumbTop", boundedTop)
}
onMount(() => {
@ -113,7 +120,7 @@ export function ScrollView(props: ScrollViewProps) {
const onThumbPointerDown = (e: PointerEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(true)
setState("isDragging", true)
startY = e.clientY
startScrollTop = viewportRef.scrollTop
@ -132,7 +139,7 @@ export function ScrollView(props: ScrollViewProps) {
}
const onPointerUp = (e: PointerEvent) => {
setIsDragging(false)
setState("isDragging", false)
thumbRef.releasePointerCapture(e.pointerId)
thumbRef.removeEventListener("pointermove", onPointerMove)
thumbRef.removeEventListener("pointerup", onPointerUp)
@ -191,8 +198,8 @@ export function ScrollView(props: ScrollViewProps) {
ref={rootRef}
class={`scroll-view ${local.class || ""}`}
style={local.style}
onPointerEnter={() => setIsHovered(true)}
onPointerLeave={() => setIsHovered(false)}
onPointerEnter={() => setState("isHovered", true)}
onPointerLeave={() => setState("isHovered", false)}
{...rest}
>
{/* Viewport */}

View File

@ -13,7 +13,7 @@ import { useFileComponent } from "../context/file"
import { useI18n } from "../context/i18n"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { checksum } from "@opencode-ai/util/encode"
import { createEffect, createMemo, createSignal, For, Match, Show, Switch, untrack, type JSX } from "solid-js"
import { createEffect, createMemo, For, Match, Show, Switch, untrack, type JSX } from "solid-js"
import { onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { type FileContent, type FileDiff } from "@opencode-ai/sdk/v2"
@ -138,14 +138,16 @@ export const SessionReview = (props: SessionReviewProps) => {
const i18n = useI18n()
const fileComponent = useFileComponent()
const anchors = new Map<string, HTMLElement>()
const [store, setStore] = createStore<{ open: string[]; force: Record<string, boolean> }>({
open: [],
force: {},
const [store, setStore] = createStore({
open: [] as string[],
force: {} as Record<string, boolean>,
selection: null as SessionReviewSelection | null,
commenting: null as SessionReviewSelection | null,
opened: null as SessionReviewFocus | null,
})
const [selection, setSelection] = createSignal<SessionReviewSelection | null>(null)
const [commenting, setCommenting] = createSignal<SessionReviewSelection | null>(null)
const [opened, setOpened] = createSignal<SessionReviewFocus | null>(null)
const selection = () => store.selection
const commenting = () => store.commenting
const opened = () => store.opened
const open = () => props.open ?? store.open
const files = createMemo(() => props.diffs.map((diff) => diff.file))
@ -184,10 +186,10 @@ export const SessionReview = (props: SessionReviewProps) => {
focusToken++
const token = focusToken
setOpened(focus)
setStore("opened", focus)
const comment = (props.comments ?? []).find((c) => c.file === focus.file && c.id === focus.id)
if (comment) setSelection({ file: comment.file, range: cloneSelectedLineRange(comment.selection) })
if (comment) setStore("selection", { file: comment.file, range: cloneSelectedLineRange(comment.selection) })
const current = open()
if (!current.includes(focus.file)) {
@ -331,11 +333,11 @@ export const SessionReview = (props: SessionReviewProps) => {
if (!current || current.file !== file) return null
return current.id
},
setOpened: (id) => setOpened(id ? { file, id } : null),
setOpened: (id) => setStore("opened", id ? { file, id } : null),
selected: selectedLines,
setSelected: (range) => setSelection(range ? { file, range } : null),
setSelected: (range) => setStore("selection", range ? { file, range } : null),
commenting: draftRange,
setCommenting: (range) => setCommenting(range ? { file, range } : null),
setCommenting: (range) => setStore("commenting", range ? { file, range } : null),
},
getSide: selectionSide,
clearSelectionOnSelectionEndNull: false,

View File

@ -6,6 +6,7 @@ import { useFileComponent } from "../context/file"
import { Binary } from "@opencode-ai/util/binary"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { Dynamic } from "solid-js/web"
import { AssistantParts, Message, MessageDivider, PART_MAPPING, type UserActions } from "./message-part"
import { Card } from "./card"
@ -240,14 +241,18 @@ export function SessionTurn(
.reverse()
})
const edited = createMemo(() => diffs().length)
const [open, setOpen] = createSignal(false)
const [expanded, setExpanded] = createSignal<string[]>([])
const [state, setState] = createStore({
open: false,
expanded: [] as string[],
})
const open = () => state.open
const expanded = () => state.expanded
createEffect(
on(
open,
(value, prev) => {
if (!value && prev) setExpanded([])
if (!value && prev) setState("expanded", [])
},
{ defer: true },
),
@ -425,7 +430,7 @@ export function SessionTurn(
<SessionRetry status={status()} show={active()} />
<Show when={edited() > 0 && !working()}>
<div data-slot="session-turn-diffs">
<Collapsible open={open()} onOpenChange={setOpen} variant="ghost">
<Collapsible open={open()} onOpenChange={(value) => setState("open", value)} variant="ghost">
<Collapsible.Trigger>
<div data-component="session-turn-diffs-trigger">
<div data-slot="session-turn-diffs-title">
@ -447,7 +452,9 @@ export function SessionTurn(
multiple
style={{ "--sticky-accordion-offset": "40px" }}
value={expanded()}
onChange={(value) => setExpanded(Array.isArray(value) ? value : value ? [value] : [])}
onChange={(value) =>
setState("expanded", Array.isArray(value) ? value : value ? [value] : [])
}
>
<For each={diffs()}>
{(diff) => {

View File

@ -1,5 +1,6 @@
// @ts-nocheck
import { createEffect, createSignal, onCleanup } from "solid-js"
import { createEffect, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { BasicTool } from "./basic-tool"
import { animate } from "motion"
@ -138,29 +139,39 @@ function SpringSubmessage(props: { text: string; visible: boolean; visualDuratio
export const Playground = {
render: () => {
const [text, setText] = createSignal("Prints five topic blocks between timed commands")
const [show, setShow] = createSignal(true)
const [visualDuration, setVisualDuration] = createSignal(0.35)
const [bounce, setBounce] = createSignal(0)
const [fadeMs, setFadeMs] = createSignal(320)
const [blur, setBlur] = createSignal(2)
const [fadeEase, setFadeEase] = createSignal<keyof typeof ease>("snappy")
const [auto, setAuto] = createSignal(false)
const [state, setState] = createStore({
text: "Prints five topic blocks between timed commands",
show: true,
visualDuration: 0.35,
bounce: 0,
fadeMs: 320,
blur: 2,
fadeEase: "snappy",
auto: false,
})
const text = () => state.text
const show = () => state.show
const visualDuration = () => state.visualDuration
const bounce = () => state.bounce
const fadeMs = () => state.fadeMs
const blur = () => state.blur
const fadeEase = () => state.fadeEase
const auto = () => state.auto
let replayTimer
let autoTimer
const replay = () => {
setShow(false)
setState("show", false)
if (replayTimer) clearTimeout(replayTimer)
replayTimer = setTimeout(() => {
setShow(true)
setState("show", true)
}, 50)
}
const stopAuto = () => {
if (autoTimer) clearInterval(autoTimer)
autoTimer = undefined
setAuto(false)
setState("auto", false)
}
const toggleAuto = () => {
@ -168,7 +179,7 @@ export const Playground = {
stopAuto()
return
}
setAuto(true)
setState("auto", true)
autoTimer = setInterval(replay, 2200)
}
@ -224,7 +235,7 @@ export const Playground = {
<button onClick={replay} style={btn()}>
Replay entry
</button>
<button onClick={() => setShow((v) => !v)} style={btn(show())}>
<button onClick={() => setState("show", (value) => !value)} style={btn(show())}>
{show() ? "Hide subtitle" : "Show subtitle"}
</button>
<button onClick={toggleAuto} style={btn(auto())}>
@ -244,7 +255,7 @@ export const Playground = {
<span style={sliderLabel}>subtitle</span>
<input
value={text()}
onInput={(e) => setText(e.currentTarget.value)}
onInput={(e) => setState("text", e.currentTarget.value)}
style={{
width: "420px",
"max-width": "100%",
@ -265,7 +276,7 @@ export const Playground = {
max={1.5}
step={0.01}
value={visualDuration()}
onInput={(e) => setVisualDuration(Number(e.currentTarget.value))}
onInput={(e) => setState("visualDuration", Number(e.currentTarget.value))}
/>
<span style={sliderValue}>{visualDuration().toFixed(2)}s</span>
</div>
@ -278,7 +289,7 @@ export const Playground = {
max={0.5}
step={0.01}
value={bounce()}
onInput={(e) => setBounce(Number(e.currentTarget.value))}
onInput={(e) => setState("bounce", Number(e.currentTarget.value))}
/>
<span style={sliderValue}>{bounce().toFixed(2)}</span>
</div>
@ -287,8 +298,14 @@ export const Playground = {
<span style={sliderLabel}>fade ease</span>
<button
onClick={() =>
setFadeEase((v) =>
v === "snappy" ? "smooth" : v === "smooth" ? "standard" : v === "standard" ? "linear" : "snappy",
setState("fadeEase", (value) =>
value === "snappy"
? "smooth"
: value === "smooth"
? "standard"
: value === "standard"
? "linear"
: "snappy",
)
}
style={btn()}
@ -305,7 +322,7 @@ export const Playground = {
max={1400}
step={10}
value={fadeMs()}
onInput={(e) => setFadeMs(Number(e.currentTarget.value))}
onInput={(e) => setState("fadeMs", Number(e.currentTarget.value))}
/>
<span style={sliderValue}>{fadeMs()}ms</span>
</div>
@ -318,7 +335,7 @@ export const Playground = {
max={14}
step={0.5}
value={blur()}
onInput={(e) => setBlur(Number(e.currentTarget.value))}
onInput={(e) => setState("blur", Number(e.currentTarget.value))}
/>
<span style={sliderValue}>{blur()}px</span>
</div>

View File

@ -1,5 +1,6 @@
// @ts-nocheck
import { createSignal, onCleanup } from "solid-js"
import { onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { TextReveal } from "./text-reveal"
export default {
@ -87,33 +88,42 @@ const headingSlot = {
export const Playground = {
render: () => {
const [index, setIndex] = createSignal(0)
const [cycling, setCycling] = createSignal(false)
const [growOnly, setGrowOnly] = createSignal(true)
const [duration, setDuration] = createSignal(600)
const [bounce, setBounce] = createSignal(1.0)
const [bounceSoft, setBounceSoft] = createSignal(1.0)
const [hybridTravel, setHybridTravel] = createSignal(25)
const [hybridEdge, setHybridEdge] = createSignal(17)
const [edge, setEdge] = createSignal(17)
const [revealTravel, setRevealTravel] = createSignal(0)
const [state, setState] = createStore({
index: 0,
cycling: false,
growOnly: true,
duration: 600,
bounce: 1.0,
bounceSoft: 1.0,
hybridTravel: 25,
hybridEdge: 17,
edge: 17,
revealTravel: 0,
})
const index = () => state.index
const cycling = () => state.cycling
const growOnly = () => state.growOnly
const duration = () => state.duration
const bounce = () => state.bounce
const bounceSoft = () => state.bounceSoft
const hybridTravel = () => state.hybridTravel
const hybridEdge = () => state.hybridEdge
const edge = () => state.edge
const revealTravel = () => state.revealTravel
let timer: number | undefined
const text = () => TEXTS[index()]
const next = () => setIndex((i) => (i + 1) % TEXTS.length)
const prev = () => setIndex((i) => (i - 1 + TEXTS.length) % TEXTS.length)
const next = () => setState("index", (value) => (value + 1) % TEXTS.length)
const prev = () => setState("index", (value) => (value - 1 + TEXTS.length) % TEXTS.length)
const toggleCycle = () => {
if (cycling()) {
if (timer) clearTimeout(timer)
timer = undefined
setCycling(false)
setState("cycling", false)
return
}
setCycling(true)
setState("cycling", true)
const tick = () => {
next()
timer = window.setTimeout(tick, 700 + Math.floor(Math.random() * 600))
@ -172,7 +182,7 @@ export const Playground = {
<div style={{ display: "flex", gap: "6px", "flex-wrap": "wrap" }}>
{TEXTS.map((t, i) => (
<button onClick={() => setIndex(i)} style={btn(index() === i)}>
<button onClick={() => setState("index", i)} style={btn(index() === i)}>
{t ?? "(none)"}
</button>
))}
@ -188,7 +198,7 @@ export const Playground = {
<button onClick={toggleCycle} style={btn(cycling())}>
{cycling() ? "Stop cycle" : "Auto cycle"}
</button>
<button onClick={() => setGrowOnly((v) => !v)} style={btn(growOnly())}>
<button onClick={() => setState("growOnly", (value) => !value)} style={btn(growOnly())}>
{growOnly() ? "growOnly: on" : "growOnly: off"}
</button>
</div>
@ -204,7 +214,7 @@ export const Playground = {
max="40"
step="1"
value={hybridEdge()}
onInput={(e) => setHybridEdge(e.currentTarget.valueAsNumber)}
onInput={(e) => setState("hybridEdge", e.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{hybridEdge()}%</span>
@ -218,7 +228,7 @@ export const Playground = {
max="40"
step="1"
value={hybridTravel()}
onInput={(e) => setHybridTravel(e.currentTarget.valueAsNumber)}
onInput={(e) => setState("hybridTravel", e.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{hybridTravel()}px</span>
@ -234,7 +244,7 @@ export const Playground = {
max="1400"
step="10"
value={duration()}
onInput={(e) => setDuration(e.currentTarget.valueAsNumber)}
onInput={(e) => setState("duration", e.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{duration()}ms</span>
@ -248,7 +258,7 @@ export const Playground = {
max="2"
step="0.01"
value={bounce()}
onInput={(e) => setBounce(e.currentTarget.valueAsNumber)}
onInput={(e) => setState("bounce", e.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{bounce().toFixed(2)}</span>
@ -262,7 +272,7 @@ export const Playground = {
max="1.5"
step="0.01"
value={bounceSoft()}
onInput={(e) => setBounceSoft(e.currentTarget.valueAsNumber)}
onInput={(e) => setState("bounceSoft", e.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{bounceSoft().toFixed(2)}</span>
@ -280,7 +290,7 @@ export const Playground = {
max="40"
step="1"
value={edge()}
onInput={(e) => setEdge(e.currentTarget.valueAsNumber)}
onInput={(e) => setState("edge", e.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{edge()}%</span>
@ -294,7 +304,7 @@ export const Playground = {
max="16"
step="1"
value={revealTravel()}
onInput={(e) => setRevealTravel(e.currentTarget.valueAsNumber)}
onInput={(e) => setState("revealTravel", e.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{revealTravel()}px</span>

View File

@ -1,4 +1,5 @@
import { createEffect, createSignal, on, onCleanup, onMount } from "solid-js"
import { createEffect, on, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
const px = (value: number | string | undefined, fallback: number) => {
if (typeof value === "number") return `${value}px`
@ -30,11 +31,18 @@ export function TextReveal(props: {
growOnly?: boolean
truncate?: boolean
}) {
const [cur, setCur] = createSignal(props.text)
const [old, setOld] = createSignal<string | undefined>()
const [width, setWidth] = createSignal("auto")
const [ready, setReady] = createSignal(false)
const [swapping, setSwapping] = createSignal(false)
const [state, setState] = createStore({
cur: props.text,
old: undefined as string | undefined,
width: "auto",
ready: false,
swapping: false,
})
const cur = () => state.cur
const old = () => state.old
const width = () => state.width
const ready = () => state.ready
const swapping = () => state.swapping
let inRef: HTMLSpanElement | undefined
let outRef: HTMLSpanElement | undefined
let rootRef: HTMLSpanElement | undefined
@ -49,7 +57,7 @@ export function TextReveal(props: {
const prev = Number.parseFloat(width())
if (Number.isFinite(prev) && next <= prev) return
}
setWidth(`${next}px`)
setState("width", `${next}px`)
}
createEffect(
@ -58,25 +66,25 @@ export function TextReveal(props: {
(next, prev) => {
if (next === prev) return
if (typeof next === "string" && typeof prev === "string" && next.startsWith(prev)) {
setCur(next)
setState("cur", next)
widen(win())
return
}
setSwapping(true)
setOld(prev)
setCur(next)
setState("swapping", true)
setState("old", prev)
setState("cur", next)
if (typeof requestAnimationFrame !== "function") {
widen(Math.max(win(), wout()))
rootRef?.offsetHeight
setSwapping(false)
setState("swapping", false)
return
}
if (frame !== undefined && typeof cancelAnimationFrame === "function") cancelAnimationFrame(frame)
frame = requestAnimationFrame(() => {
widen(Math.max(win(), wout()))
rootRef?.offsetHeight
setSwapping(false)
setState("swapping", false)
frame = undefined
})
},
@ -87,16 +95,16 @@ export function TextReveal(props: {
widen(win())
const fonts = typeof document !== "undefined" ? document.fonts : undefined
if (typeof requestAnimationFrame !== "function") {
setReady(true)
setState("ready", true)
return
}
if (!fonts) {
requestAnimationFrame(() => setReady(true))
requestAnimationFrame(() => setState("ready", true))
return
}
fonts.ready.finally(() => {
widen(win())
requestAnimationFrame(() => setReady(true))
requestAnimationFrame(() => setState("ready", true))
})
})

View File

@ -1,5 +1,6 @@
// @ts-nocheck
import { createEffect, createSignal, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
import { useSpring } from "./motion-spring"
import { TextStrikethrough } from "./text-strikethrough"
@ -130,12 +131,16 @@ function VariantF(props: { active: boolean; text: string }) {
)
let baseRef: HTMLSpanElement | undefined
let containerRef: HTMLSpanElement | undefined
const [textWidth, setTextWidth] = createSignal(0)
const [containerWidth, setContainerWidth] = createSignal(0)
const [state, setState] = createStore({
textWidth: 0,
containerWidth: 0,
})
const textWidth = () => state.textWidth
const containerWidth = () => state.containerWidth
const measure = () => {
if (baseRef) setTextWidth(baseRef.scrollWidth)
if (containerRef) setContainerWidth(containerRef.offsetWidth)
if (baseRef) setState("textWidth", baseRef.scrollWidth)
if (containerRef) setState("containerWidth", containerRef.offsetWidth)
}
onMount(measure)

View File

@ -1,5 +1,6 @@
import type { JSX } from "solid-js"
import { createEffect, createSignal, onCleanup, onMount } from "solid-js"
import { createEffect, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
import { useSpring } from "./motion-spring"
export function TextStrikethrough(props: {
@ -19,12 +20,16 @@ export function TextStrikethrough(props: {
let baseRef: HTMLSpanElement | undefined
let containerRef: HTMLSpanElement | undefined
const [textWidth, setTextWidth] = createSignal(0)
const [containerWidth, setContainerWidth] = createSignal(0)
const [state, setState] = createStore({
textWidth: 0,
containerWidth: 0,
})
const textWidth = () => state.textWidth
const containerWidth = () => state.containerWidth
const measure = () => {
if (baseRef) setTextWidth(baseRef.scrollWidth)
if (containerRef) setContainerWidth(containerRef.offsetWidth)
if (baseRef) setState("textWidth", baseRef.scrollWidth)
if (containerRef) setState("containerWidth", containerRef.offsetWidth)
}
onMount(measure)

View File

@ -1,5 +1,6 @@
// @ts-nocheck
import { createSignal, createEffect, on, onMount, onCleanup } from "solid-js"
import { createEffect, on, onMount, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { TextShimmer } from "./text-shimmer"
import { TextReveal } from "./text-reveal"
@ -375,11 +376,18 @@ input[type="range"].heading-slider::-webkit-slider-thumb {
// ---------------------------------------------------------------------------
function AnimatedHeading(props) {
const [current, setCurrent] = createSignal(props.text)
const [leaving, setLeaving] = createSignal(undefined)
const [width, setWidth] = createSignal("auto")
const [ready, setReady] = createSignal(false)
const [swapping, setSwapping] = createSignal(false)
const [state, setState] = createStore({
current: props.text,
leaving: undefined,
width: "auto",
ready: false,
swapping: false,
})
const current = () => state.current
const leaving = () => state.leaving
const width = () => state.width
const ready = () => state.ready
const swapping = () => state.swapping
let enterRef
let leaveRef
let containerRef
@ -391,16 +399,16 @@ function AnimatedHeading(props) {
if (px <= 0) return
const w = Number.parseFloat(width())
if (Number.isFinite(w) && px <= w) return
setWidth(`${px}px`)
setState("width", `${px}px`)
}
const measure = () => {
if (!current()) {
setWidth("0px")
setState("width", "0px")
return
}
const px = measureEnter()
if (px > 0) setWidth(`${px}px`)
if (px > 0) setState("width", `${px}px`)
}
createEffect(
@ -408,9 +416,9 @@ function AnimatedHeading(props) {
() => props.text,
(next, prev) => {
if (next === prev) return
setSwapping(true)
setLeaving(prev)
setCurrent(next)
setState("swapping", true)
setState("leaving", prev)
setState("current", next)
if (frame) cancelAnimationFrame(frame)
frame = requestAnimationFrame(() => {
@ -420,10 +428,10 @@ function AnimatedHeading(props) {
const leaveW = measureLeave()
widen(Math.max(enterW, leaveW))
containerRef?.offsetHeight // reflow with max width + swap positions
setSwapping(false)
setState("swapping", false)
} else {
containerRef?.offsetHeight
setSwapping(false)
setState("swapping", false)
measure()
}
frame = undefined
@ -436,7 +444,7 @@ function AnimatedHeading(props) {
measure()
document.fonts?.ready.finally(() => {
measure()
requestAnimationFrame(() => setReady(true))
requestAnimationFrame(() => setState("ready", true))
})
})
@ -552,47 +560,56 @@ const VARIANTS: { key: string; label: string }[] = []
export const Playground = {
render: () => {
const [heading, setHeading] = createSignal(HEADINGS[0])
const [headingIndex, setHeadingIndex] = createSignal(0)
const [active, setActive] = createSignal(true)
const [cycling, setCycling] = createSignal(false)
const [state, setState] = createStore({
heading: HEADINGS[0],
headingIndex: 0,
active: true,
cycling: false,
duration: 550,
blur: 2,
travel: 4,
bounce: 1.35,
maskSize: 12,
maskPad: 9,
maskHeight: 0,
debug: false,
odoBlur: false,
})
const heading = () => state.heading
const headingIndex = () => state.headingIndex
const active = () => state.active
const cycling = () => state.cycling
const duration = () => state.duration
const blur = () => state.blur
const travel = () => state.travel
const bounce = () => state.bounce
const maskSize = () => state.maskSize
const maskPad = () => state.maskPad
const maskHeight = () => state.maskHeight
const debug = () => state.debug
const odoBlur = () => state.odoBlur
let cycleTimer
// tunable params
const [duration, setDuration] = createSignal(550)
const [blur, setBlur] = createSignal(2)
const [travel, setTravel] = createSignal(4)
const [bounce, setBounce] = createSignal(1.35)
const [maskSize, setMaskSize] = createSignal(12)
const [maskPad, setMaskPad] = createSignal(9)
const [maskHeight, setMaskHeight] = createSignal(0)
const [debug, setDebug] = createSignal(false)
const [odoBlur, setOdoBlur] = createSignal(false)
const nextHeading = () => {
setHeadingIndex((i) => {
const next = (i + 1) % HEADINGS.length
setHeading(HEADINGS[next])
return next
})
const next = (headingIndex() + 1) % HEADINGS.length
setState("headingIndex", next)
setState("heading", HEADINGS[next])
}
const prevHeading = () => {
setHeadingIndex((i) => {
const prev = (i - 1 + HEADINGS.length) % HEADINGS.length
setHeading(HEADINGS[prev])
return prev
})
const prev = (headingIndex() - 1 + HEADINGS.length) % HEADINGS.length
setState("headingIndex", prev)
setState("heading", HEADINGS[prev])
}
const toggleCycling = () => {
if (cycling()) {
clearTimeout(cycleTimer)
cycleTimer = undefined
setCycling(false)
setState("cycling", false)
return
}
setCycling(true)
setState("cycling", true)
const tick = () => {
if (!cycling()) return
nextHeading()
@ -602,11 +619,11 @@ export const Playground = {
}
const clearHeading = () => {
setHeading(undefined)
setState("heading", undefined)
if (cycling()) {
clearTimeout(cycleTimer)
cycleTimer = undefined
setCycling(false)
setState("cycling", false)
}
}
@ -686,7 +703,7 @@ export const Playground = {
max={1400}
step={50}
value={duration()}
onInput={(e) => setDuration(Number(e.currentTarget.value))}
onInput={(e) => setState("duration", Number(e.currentTarget.value))}
/>
<span style={sliderValue}>{duration()}ms</span>
</div>
@ -700,7 +717,7 @@ export const Playground = {
max={16}
step={0.5}
value={blur()}
onInput={(e) => setBlur(Number(e.currentTarget.value))}
onInput={(e) => setState("blur", Number(e.currentTarget.value))}
/>
<span style={sliderValue}>{blur()}px</span>
</div>
@ -714,7 +731,7 @@ export const Playground = {
max={120}
step={1}
value={travel()}
onInput={(e) => setTravel(Number(e.currentTarget.value))}
onInput={(e) => setState("travel", Number(e.currentTarget.value))}
/>
<span style={sliderValue}>{travel()}px</span>
</div>
@ -728,7 +745,7 @@ export const Playground = {
max={2.2}
step={0.05}
value={bounce()}
onInput={(e) => setBounce(Number(e.currentTarget.value))}
onInput={(e) => setState("bounce", Number(e.currentTarget.value))}
/>
<span style={sliderValue}>
{bounce().toFixed(2)} {bounce() <= 1.05 ? "(none)" : bounce() >= 1.9 ? "(heavy)" : ""}
@ -744,7 +761,7 @@ export const Playground = {
max={50}
step={1}
value={maskSize()}
onInput={(e) => setMaskSize(Number(e.currentTarget.value))}
onInput={(e) => setState("maskSize", Number(e.currentTarget.value))}
/>
<span style={sliderValue}>
{maskSize()}px {maskSize() === 0 ? "(hard)" : ""}
@ -760,7 +777,7 @@ export const Playground = {
max={60}
step={1}
value={maskPad()}
onInput={(e) => setMaskPad(Number(e.currentTarget.value))}
onInput={(e) => setState("maskPad", Number(e.currentTarget.value))}
/>
<span style={sliderValue}>{maskPad()}px</span>
</div>
@ -774,7 +791,7 @@ export const Playground = {
max={80}
step={1}
value={maskHeight()}
onInput={(e) => setMaskHeight(Number(e.currentTarget.value))}
onInput={(e) => setState("maskHeight", Number(e.currentTarget.value))}
/>
<span style={sliderValue}>{maskHeight()}px</span>
</div>
@ -795,13 +812,13 @@ export const Playground = {
<button onClick={clearHeading} style={btn()}>
Clear
</button>
<button onClick={() => setActive((v) => !v)} style={smallBtn(active())}>
<button onClick={() => setState("active", (value) => !value)} style={smallBtn(active())}>
{active() ? "Shimmer: on" : "Shimmer: off"}
</button>
<button onClick={() => setDebug((v) => !v)} style={smallBtn(debug())}>
<button onClick={() => setState("debug", (value) => !value)} style={smallBtn(debug())}>
{debug() ? "Debug mask: on" : "Debug mask"}
</button>
<button onClick={() => setOdoBlur((v) => !v)} style={smallBtn(odoBlur())}>
<button onClick={() => setState("odoBlur", (value) => !value)} style={smallBtn(odoBlur())}>
{odoBlur() ? "Odo blur: on" : "Odo blur"}
</button>
</div>
@ -810,8 +827,8 @@ export const Playground = {
{HEADINGS.map((h, i) => (
<button
onClick={() => {
setHeadingIndex(i)
setHeading(h)
setState("headingIndex", i)
setState("heading", h)
}}
style={smallBtn(headingIndex() === i)}
>

View File

@ -1,5 +1,6 @@
// @ts-nocheck
import { createEffect, createMemo, createSignal, onCleanup } from "solid-js"
import { createEffect, createMemo, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import type { Todo } from "@opencode-ai/sdk/v2"
import { useGlobalSync } from "@/context/global-sync"
import { SessionComposerRegion, createSessionComposerState } from "@/pages/session/composer"
@ -129,24 +130,44 @@ const css = `
export const Playground = {
render: () => {
const global = useGlobalSync()
const [open, setOpen] = createSignal(true)
const [step, setStep] = createSignal(1)
const [dockOpenDuration, setDockOpenDuration] = createSignal(0.3)
const [dockOpenBounce, setDockOpenBounce] = createSignal(0)
const [dockCloseDuration, setDockCloseDuration] = createSignal(0.3)
const [dockCloseBounce, setDockCloseBounce] = createSignal(0)
const [drawerExpandDuration, setDrawerExpandDuration] = createSignal(0.3)
const [drawerExpandBounce, setDrawerExpandBounce] = createSignal(0)
const [drawerCollapseDuration, setDrawerCollapseDuration] = createSignal(0.3)
const [drawerCollapseBounce, setDrawerCollapseBounce] = createSignal(0)
const [subtitleDuration, setSubtitleDuration] = createSignal(600)
const [subtitleAuto, setSubtitleAuto] = createSignal(true)
const [subtitleTravel, setSubtitleTravel] = createSignal(25)
const [subtitleEdge, setSubtitleEdge] = createSignal(17)
const [countDuration, setCountDuration] = createSignal(600)
const [countMask, setCountMask] = createSignal(18)
const [countMaskHeight, setCountMaskHeight] = createSignal(0)
const [countWidthDuration, setCountWidthDuration] = createSignal(560)
const [cfg, setCfg] = createStore({
open: true,
step: 1,
dockOpenDuration: 0.3,
dockOpenBounce: 0,
dockCloseDuration: 0.3,
dockCloseBounce: 0,
drawerExpandDuration: 0.3,
drawerExpandBounce: 0,
drawerCollapseDuration: 0.3,
drawerCollapseBounce: 0,
subtitleDuration: 600,
subtitleAuto: true,
subtitleTravel: 25,
subtitleEdge: 17,
countDuration: 600,
countMask: 18,
countMaskHeight: 0,
countWidthDuration: 560,
})
const open = () => cfg.open
const step = () => cfg.step
const dockOpenDuration = () => cfg.dockOpenDuration
const dockOpenBounce = () => cfg.dockOpenBounce
const dockCloseDuration = () => cfg.dockCloseDuration
const dockCloseBounce = () => cfg.dockCloseBounce
const drawerExpandDuration = () => cfg.drawerExpandDuration
const drawerExpandBounce = () => cfg.drawerExpandBounce
const drawerCollapseDuration = () => cfg.drawerCollapseDuration
const drawerCollapseBounce = () => cfg.drawerCollapseBounce
const subtitleDuration = () => cfg.subtitleDuration
const subtitleAuto = () => cfg.subtitleAuto
const subtitleTravel = () => cfg.subtitleTravel
const subtitleEdge = () => cfg.subtitleEdge
const countDuration = () => cfg.countDuration
const countMask = () => cfg.countMask
const countMaskHeight = () => cfg.countMaskHeight
const countWidthDuration = () => cfg.countWidthDuration
const state = createSessionComposerState({ closeMs: () => Math.round(dockCloseDuration() * 1000) })
let frame
let composerRef
@ -187,7 +208,7 @@ export const Playground = {
const openDock = () => {
clear()
setOpen(true)
setCfg("open", true)
frame = requestAnimationFrame(() => {
pin()
frame = undefined
@ -196,7 +217,7 @@ export const Playground = {
const closeDock = () => {
clear()
setOpen(false)
setCfg("open", false)
}
const dockOpen = () => open()
@ -223,7 +244,7 @@ export const Playground = {
}
const cycle = () => {
setStep((value) => (value + 1) % 4)
setCfg("step", (value) => (value + 1) % 4)
}
onCleanup(clear)
@ -289,7 +310,7 @@ export const Playground = {
Cycle progress ({step()}/3 done)
</button>
{[0, 1, 2, 3].map((value) => (
<button onClick={() => setStep(value)} style={btn(step() === value)}>
<button onClick={() => setCfg("step", value)} style={btn(step() === value)}>
{value} done
</button>
))}
@ -307,7 +328,7 @@ export const Playground = {
max="1"
step="0.01"
value={dockOpenDuration()}
onInput={(event) => setDockOpenDuration(event.currentTarget.valueAsNumber)}
onInput={(event) => setCfg("dockOpenDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@ -324,7 +345,7 @@ export const Playground = {
max="1"
step="0.01"
value={dockOpenBounce()}
onInput={(event) => setDockOpenBounce(event.currentTarget.valueAsNumber)}
onInput={(event) => setCfg("dockOpenBounce", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@ -345,7 +366,7 @@ export const Playground = {
max="1"
step="0.01"
value={dockCloseDuration()}
onInput={(event) => setDockCloseDuration(event.currentTarget.valueAsNumber)}
onInput={(event) => setCfg("dockCloseDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@ -362,7 +383,7 @@ export const Playground = {
max="1"
step="0.01"
value={dockCloseBounce()}
onInput={(event) => setDockCloseBounce(event.currentTarget.valueAsNumber)}
onInput={(event) => setCfg("dockCloseBounce", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@ -383,7 +404,7 @@ export const Playground = {
max="1"
step="0.01"
value={drawerExpandDuration()}
onInput={(event) => setDrawerExpandDuration(event.currentTarget.valueAsNumber)}
onInput={(event) => setCfg("drawerExpandDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@ -400,7 +421,7 @@ export const Playground = {
max="1"
step="0.01"
value={drawerExpandBounce()}
onInput={(event) => setDrawerExpandBounce(event.currentTarget.valueAsNumber)}
onInput={(event) => setCfg("drawerExpandBounce", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@ -421,7 +442,7 @@ export const Playground = {
max="1"
step="0.01"
value={drawerCollapseDuration()}
onInput={(event) => setDrawerCollapseDuration(event.currentTarget.valueAsNumber)}
onInput={(event) => setCfg("drawerCollapseDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@ -438,7 +459,7 @@ export const Playground = {
max="1"
step="0.01"
value={drawerCollapseBounce()}
onInput={(event) => setDrawerCollapseBounce(event.currentTarget.valueAsNumber)}
onInput={(event) => setCfg("drawerCollapseBounce", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@ -459,7 +480,7 @@ export const Playground = {
max="1400"
step="10"
value={subtitleDuration()}
onInput={(event) => setSubtitleDuration(event.currentTarget.valueAsNumber)}
onInput={(event) => setCfg("subtitleDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@ -473,7 +494,7 @@ export const Playground = {
<input
type="checkbox"
checked={subtitleAuto()}
onInput={(event) => setSubtitleAuto(event.currentTarget.checked)}
onInput={(event) => setCfg("subtitleAuto", event.currentTarget.checked)}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
{subtitleAuto() ? "on" : "off"}
@ -489,7 +510,7 @@ export const Playground = {
max="40"
step="1"
value={subtitleTravel()}
onInput={(event) => setSubtitleTravel(event.currentTarget.valueAsNumber)}
onInput={(event) => setCfg("subtitleTravel", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{subtitleTravel()}px</span>
@ -504,7 +525,7 @@ export const Playground = {
max="40"
step="1"
value={subtitleEdge()}
onInput={(event) => setSubtitleEdge(event.currentTarget.valueAsNumber)}
onInput={(event) => setCfg("subtitleEdge", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{subtitleEdge()}%</span>
@ -523,7 +544,7 @@ export const Playground = {
max="1400"
step="10"
value={countDuration()}
onInput={(event) => setCountDuration(event.currentTarget.valueAsNumber)}
onInput={(event) => setCfg("countDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@ -540,7 +561,7 @@ export const Playground = {
max="40"
step="1"
value={countMask()}
onInput={(event) => setCountMask(event.currentTarget.valueAsNumber)}
onInput={(event) => setCfg("countMask", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{countMask()}%</span>
@ -555,7 +576,7 @@ export const Playground = {
max="14"
step="1"
value={countMaskHeight()}
onInput={(event) => setCountMaskHeight(event.currentTarget.valueAsNumber)}
onInput={(event) => setCfg("countMaskHeight", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{countMaskHeight()}px</span>
@ -570,7 +591,7 @@ export const Playground = {
max="1200"
step="10"
value={countWidthDuration()}
onInput={(event) => setCountWidthDuration(event.currentTarget.valueAsNumber)}
onInput={(event) => setCfg("countWidthDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>

View File

@ -1,5 +1,6 @@
// @ts-nocheck
import { createSignal, onCleanup } from "solid-js"
import { onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { AnimatedCountList, type CountItem } from "./tool-count-summary"
import { ToolStatusTitle } from "./tool-status-title"
@ -57,11 +58,18 @@ const smallBtn = (active?: boolean) =>
export const Playground = {
render: () => {
const [reads, setReads] = createSignal(0)
const [searches, setSearches] = createSignal(0)
const [lists, setLists] = createSignal(0)
const [active, setActive] = createSignal(false)
const [reducedMotion, setReducedMotion] = createSignal(false)
const [state, setState] = createStore({
reads: 0,
searches: 0,
lists: 0,
active: false,
reducedMotion: false,
})
const reads = () => state.reads
const searches = () => state.searches
const lists = () => state.lists
const active = () => state.active
const reducedMotion = () => state.reducedMotion
let timeouts: ReturnType<typeof setTimeout>[] = []
@ -74,10 +82,10 @@ export const Playground = {
const startSim = () => {
clearAll()
setReads(0)
setSearches(0)
setLists(0)
setActive(true)
setState("reads", 0)
setState("searches", 0)
setState("lists", 0)
setState("active", true)
const steps = rand(3, 10)
let elapsed = 0
@ -86,27 +94,27 @@ export const Playground = {
elapsed += delay
const t = setTimeout(() => {
const pick = rand(0, 2)
if (pick === 0) setReads((n) => n + 1)
else if (pick === 1) setSearches((n) => n + 1)
else setLists((n) => n + 1)
if (pick === 0) setState("reads", (value) => value + 1)
else if (pick === 1) setState("searches", (value) => value + 1)
else setState("lists", (value) => value + 1)
}, elapsed)
timeouts.push(t)
}
const end = setTimeout(() => setActive(false), elapsed + 100)
const end = setTimeout(() => setState("active", false), elapsed + 100)
timeouts.push(end)
}
const stopSim = () => {
clearAll()
setActive(false)
setState("active", false)
}
const reset = () => {
stopSim()
setReads(0)
setSearches(0)
setLists(0)
setState("reads", 0)
setState("searches", 0)
setState("lists", 0)
}
const items = (): CountItem[] => [
@ -164,19 +172,19 @@ export const Playground = {
<button onClick={reset} style={btn()}>
Reset
</button>
<button onClick={() => setReducedMotion((v) => !v)} style={smallBtn(reducedMotion())}>
<button onClick={() => setState("reducedMotion", (value) => !value)} style={smallBtn(reducedMotion())}>
{reducedMotion() ? "Motion: reduced" : "Motion: normal"}
</button>
</div>
<div style={{ display: "flex", gap: "8px", "flex-wrap": "wrap" }}>
<button onClick={() => setReads((n) => n + 1)} style={smallBtn()}>
<button onClick={() => setState("reads", (value) => value + 1)} style={smallBtn()}>
+ read
</button>
<button onClick={() => setSearches((n) => n + 1)} style={smallBtn()}>
<button onClick={() => setState("searches", (value) => value + 1)} style={smallBtn()}>
+ search
</button>
<button onClick={() => setLists((n) => n + 1)} style={smallBtn()}>
<button onClick={() => setState("lists", (value) => value + 1)} style={smallBtn()}>
+ list
</button>
</div>

View File

@ -1,4 +1,5 @@
import { type ComponentProps, createMemo, createSignal, Show, splitProps } from "solid-js"
import { type ComponentProps, createMemo, Show, splitProps } from "solid-js"
import { createStore } from "solid-js/store"
import { Card, CardDescription } from "./card"
import { Collapsible } from "./collapsible"
import { Icon } from "./icon"
@ -16,8 +17,12 @@ export interface ToolErrorCardProps extends Omit<ComponentProps<typeof Card>, "c
export function ToolErrorCard(props: ToolErrorCardProps) {
const i18n = useI18n()
const [open, setOpen] = createSignal(props.defaultOpen ?? false)
const [copied, setCopied] = createSignal(false)
const [state, setState] = createStore({
open: props.defaultOpen ?? false,
copied: false,
})
const open = () => state.open
const copied = () => state.copied
const [split, rest] = splitProps(props, ["tool", "error", "defaultOpen", "subtitle", "href"])
const name = createMemo(() => {
const map: Record<string, string> = {
@ -65,13 +70,18 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
const text = cleaned()
if (!text) return
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
setState("copied", true)
setTimeout(() => setState("copied", false), 2000)
}
return (
<Card {...rest} data-kind="tool-error-card" data-open={open() ? "true" : "false"} variant="error">
<Collapsible class="tool-collapsible" data-open={open() ? "true" : "false"} open={open()} onOpenChange={setOpen}>
<Collapsible
class="tool-collapsible"
data-open={open() ? "true" : "false"}
open={open()}
onOpenChange={(value) => setState("open", value)}
>
<Collapsible.Trigger>
<div data-component="tool-trigger">
<div data-slot="basic-tool-tool-trigger-content">

View File

@ -1,4 +1,5 @@
import { Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from "solid-js"
import { Show, createEffect, createMemo, on, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
import { TextShimmer } from "./text-shimmer"
function common(active: string, done: string) {
@ -35,8 +36,12 @@ export function ToolStatusTitle(props: {
const activeTail = createMemo(() => (suffix() ? split().active : props.activeText))
const doneTail = createMemo(() => (suffix() ? split().done : props.doneText))
const [width, setWidth] = createSignal("auto")
const [ready, setReady] = createSignal(false)
const [state, setState] = createStore({
width: "auto",
ready: false,
})
const width = () => state.width
const ready = () => state.ready
let activeRef: HTMLSpanElement | undefined
let doneRef: HTMLSpanElement | undefined
let frame: number | undefined
@ -45,7 +50,7 @@ export function ToolStatusTitle(props: {
const measure = () => {
const target = props.active ? activeRef : doneRef
const px = contentWidth(target)
if (px > 0) setWidth(`${px}px`)
if (px > 0) setState("width", `${px}px`)
}
const schedule = () => {
@ -62,13 +67,13 @@ export function ToolStatusTitle(props: {
const finish = () => {
if (typeof requestAnimationFrame !== "function") {
setReady(true)
setState("ready", true)
return
}
if (readyFrame !== undefined) cancelAnimationFrame(readyFrame)
readyFrame = requestAnimationFrame(() => {
readyFrame = undefined
setReady(true)
setState("ready", true)
})
}

View File

@ -1,4 +1,5 @@
import { createEffect, createSignal, onCleanup, onMount } from "solid-js"
import { createEffect, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
export type FindHost = {
element: () => HTMLElement | undefined
@ -107,11 +108,18 @@ export function createFileFind(opts: CreateFileFindOptions) {
let mode: "highlights" | "overlay" = "overlay"
let hits: Range[] = []
const [open, setOpen] = createSignal(false)
const [query, setQuery] = createSignal("")
const [index, setIndex] = createSignal(0)
const [count, setCount] = createSignal(0)
const [pos, setPos] = createSignal({ top: 8, right: 8 })
const [state, setState] = createStore({
open: false,
query: "",
index: 0,
count: 0,
pos: { top: 8, right: 8 },
})
const open = () => state.open
const query = () => state.query
const index = () => state.index
const count = () => state.count
const pos = () => state.pos
const clearOverlayScroll = () => {
for (const el of overlayScroll) el.removeEventListener("scroll", scheduleOverlay)
@ -200,8 +208,8 @@ export function createFileFind(opts: CreateFileFindOptions) {
clearOverlay()
clearOverlayScroll()
hits = []
setCount(0)
setIndex(0)
setState("count", 0)
setState("index", 0)
}
const positionBar = () => {
@ -214,7 +222,7 @@ export function createFileFind(opts: CreateFileFindOptions) {
const title = parseFloat(getComputedStyle(root).getPropertyValue("--session-title-height"))
const header = Number.isNaN(title) ? 0 : title
setPos({
setState("pos", {
top: Math.round(rect.top) + header - 4,
right: Math.round(window.innerWidth - rect.right) + 8,
})
@ -318,8 +326,8 @@ export function createFileFind(opts: CreateFileFindOptions) {
const currentIndex = total ? Math.min(desired, total - 1) : 0
hits = ranges
setCount(total)
setIndex(currentIndex)
setState("count", total)
setState("index", currentIndex)
const active = ranges[currentIndex]
if (mode === "highlights") {
@ -342,8 +350,8 @@ export function createFileFind(opts: CreateFileFindOptions) {
}
const close = () => {
setOpen(false)
setQuery("")
setState("open", false)
setState("query", "")
clearFind()
if (current === host) current = undefined
}
@ -352,7 +360,7 @@ export function createFileFind(opts: CreateFileFindOptions) {
if (current && current !== host) current.close()
current = host
target = host
if (!open()) setOpen(true)
if (!open()) setState("open", true)
requestAnimationFrame(() => {
apply({ scroll: true })
input?.focus()
@ -366,7 +374,7 @@ export function createFileFind(opts: CreateFileFindOptions) {
if (total <= 0) return
const currentIndex = (index() + dir + total) % total
setIndex(currentIndex)
setState("index", currentIndex)
const active = hits[currentIndex]
if (!active) return
@ -449,8 +457,8 @@ export function createFileFind(opts: CreateFileFindOptions) {
input = el
},
setQuery: (value: string) => {
setQuery(value)
setIndex(0)
setState("query", value)
setState("index", 0)
apply({ reset: true, scroll: true })
},
focus,