diff options
| author | Adam <[email protected]> | 2026-03-09 07:36:39 -0500 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-09 07:36:39 -0500 |
| commit | c71d1bde5e8dcc8be49c15697ad2e5d0f2607e5e (patch) | |
| tree | a30482cedb38dc24cad70e24ad717817065620d6 /packages/ui/src/components/basic-tool.tsx | |
| parent | f27ef595f65aa719be3f8d08665d683e95083ed3 (diff) | |
| download | opencode-c71d1bde5e8dcc8be49c15697ad2e5d0f2607e5e.tar.gz opencode-c71d1bde5e8dcc8be49c15697ad2e5d0f2607e5e.zip | |
revert(app): "STUPID SEXY TIMELINE (#16420)" (#16745)
Diffstat (limited to 'packages/ui/src/components/basic-tool.tsx')
| -rw-r--r-- | packages/ui/src/components/basic-tool.tsx | 356 |
1 files changed, 97 insertions, 259 deletions
diff --git a/packages/ui/src/components/basic-tool.tsx b/packages/ui/src/components/basic-tool.tsx index 3210b4870..4ad91824d 100644 --- a/packages/ui/src/components/basic-tool.tsx +++ b/packages/ui/src/components/basic-tool.tsx @@ -1,20 +1,8 @@ -import { - createEffect, - createSignal, - For, - Match, - on, - onCleanup, - onMount, - Show, - splitProps, - Switch, - type JSX, -} from "solid-js" -import { animate, type AnimationPlaybackControls, tunableSpringValue, COLLAPSIBLE_SPRING } from "./motion" +import { createEffect, createSignal, For, Match, on, onCleanup, Show, Switch, type JSX } from "solid-js" +import { animate, type AnimationPlaybackControls } from "motion" import { Collapsible } from "./collapsible" +import type { IconProps } from "./icon" import { TextShimmer } from "./text-shimmer" -import { hold } from "./tool-utils" export type TriggerTitle = { title: string @@ -32,99 +20,26 @@ const isTriggerTitle = (val: any): val is TriggerTitle => { ) } -interface ToolCallPanelBaseProps { - icon: string +export interface BasicToolProps { + icon: IconProps["name"] trigger: TriggerTitle | JSX.Element children?: JSX.Element status?: string - animate?: boolean hideDetails?: boolean defaultOpen?: boolean forceOpen?: boolean defer?: boolean locked?: boolean - watchDetails?: boolean - springContent?: boolean + animated?: boolean onSubtitleClick?: () => void } -function ToolCallTriggerBody(props: { - trigger: TriggerTitle | JSX.Element - pending: boolean - onSubtitleClick?: () => void - arrow?: boolean -}) { - return ( - <div data-component="tool-trigger" data-arrow={props.arrow ? "" : undefined}> - <div data-slot="basic-tool-tool-trigger-content"> - <div data-slot="basic-tool-tool-info"> - <Switch> - <Match when={isTriggerTitle(props.trigger) && props.trigger}> - {(trigger) => ( - <div data-slot="basic-tool-tool-info-structured"> - <div data-slot="basic-tool-tool-info-main"> - <span - data-slot="basic-tool-tool-title" - classList={{ - [trigger().titleClass ?? ""]: !!trigger().titleClass, - }} - > - <TextShimmer text={trigger().title} active={props.pending} /> - </span> - <Show when={!props.pending}> - <Show when={trigger().subtitle}> - <span - data-slot="basic-tool-tool-subtitle" - classList={{ - [trigger().subtitleClass ?? ""]: !!trigger().subtitleClass, - clickable: !!props.onSubtitleClick, - }} - onClick={(e) => { - if (!props.onSubtitleClick) return - e.stopPropagation() - props.onSubtitleClick() - }} - > - {trigger().subtitle} - </span> - </Show> - <Show when={trigger().args?.length}> - <For each={trigger().args}> - {(arg) => ( - <span - data-slot="basic-tool-tool-arg" - classList={{ - [trigger().argsClass ?? ""]: !!trigger().argsClass, - }} - > - {arg} - </span> - )} - </For> - </Show> - </Show> - </div> - <Show when={!props.pending && trigger().action}>{trigger().action}</Show> - </div> - )} - </Match> - <Match when={true}>{props.trigger as JSX.Element}</Match> - </Switch> - </div> - </div> - <Show when={props.arrow}> - <Collapsible.Arrow /> - </Show> - </div> - ) -} +const SPRING = { type: "spring" as const, visualDuration: 0.35, bounce: 0 } -function ToolCallPanel(props: ToolCallPanelBaseProps) { +export function BasicTool(props: BasicToolProps) { const [open, setOpen] = createSignal(props.defaultOpen ?? false) const [ready, setReady] = createSignal(open()) - const pendingRaw = () => props.status === "pending" || props.status === "running" - const pending = hold(pendingRaw, 1000) - const watchDetails = () => props.watchDetails !== false + const pending = () => props.status === "pending" || props.status === "running" let frame: number | undefined @@ -144,7 +59,7 @@ function ToolCallPanel(props: ToolCallPanelBaseProps) { on( open, (value) => { - if (!props.defer || props.springContent) return + if (!props.defer) return if (!value) { cancel() setReady(false) @@ -162,110 +77,36 @@ function ToolCallPanel(props: ToolCallPanelBaseProps) { ), ) - // Animated content height — single springValue drives all height changes + // Animated height for collapsible open/close let contentRef: HTMLDivElement | undefined - let bodyRef: HTMLDivElement | undefined - let fadeAnim: AnimationPlaybackControls | undefined - let observer: ResizeObserver | undefined - let resizeFrame: number | undefined + let heightAnim: AnimationPlaybackControls | undefined const initialOpen = open() - const heightSpring = tunableSpringValue<number>(0, COLLAPSIBLE_SPRING) - - const read = () => Math.max(0, Math.ceil(bodyRef?.getBoundingClientRect().height ?? 0)) - - const doOpen = () => { - if (!contentRef || !bodyRef) return - contentRef.style.display = "" - // Ensure fade starts from 0 if content was hidden (first open or after close cleared styles) - if (bodyRef.style.opacity === "") { - bodyRef.style.opacity = "0" - bodyRef.style.filter = "blur(2px)" - } - const next = read() - fadeAnim?.stop() - fadeAnim = animate(bodyRef, { opacity: 1, filter: "blur(0px)" }, COLLAPSIBLE_SPRING) - fadeAnim.finished.then(() => { - if (!bodyRef) return - bodyRef.style.opacity = "" - bodyRef.style.filter = "" - }) - heightSpring.set(next) - } - - const doClose = () => { - if (!contentRef || !bodyRef) return - fadeAnim?.stop() - fadeAnim = animate(bodyRef, { opacity: 0, filter: "blur(2px)" }, COLLAPSIBLE_SPRING) - fadeAnim.finished.then(() => { - if (!contentRef || open()) return - contentRef.style.display = "none" - }) - heightSpring.set(0) - } - - const grow = () => { - if (!contentRef || !open()) return - const next = read() - if (Math.abs(next - heightSpring.get()) < 1) return - heightSpring.set(next) - } - - onMount(() => { - if (!props.springContent || props.animate === false || !contentRef || !bodyRef) return - - const offChange = heightSpring.on("change", (v) => { - if (!contentRef) return - contentRef.style.height = `${Math.max(0, Math.ceil(v))}px` - }) - onCleanup(() => { - offChange() - }) - - if (watchDetails()) { - observer = new ResizeObserver(() => { - if (resizeFrame !== undefined) return - resizeFrame = requestAnimationFrame(() => { - resizeFrame = undefined - grow() - }) - }) - observer.observe(bodyRef) - } - - if (!open()) return - if (contentRef.style.display !== "none") { - const next = read() - heightSpring.jump(next) - contentRef.style.height = `${next}px` - return - } - let mountFrame: number | undefined = requestAnimationFrame(() => { - mountFrame = undefined - if (!open()) return - doOpen() - }) - onCleanup(() => { - if (mountFrame !== undefined) cancelAnimationFrame(mountFrame) - }) - }) createEffect( on( open, (isOpen) => { - if (!props.springContent || props.animate === false || !contentRef) return - if (isOpen) doOpen() - else doClose() + if (!props.animated || !contentRef) return + heightAnim?.stop() + if (isOpen) { + contentRef.style.overflow = "hidden" + heightAnim = animate(contentRef, { height: "auto" }, SPRING) + heightAnim.finished.then(() => { + if (!contentRef || !open()) return + contentRef.style.overflow = "visible" + contentRef.style.height = "auto" + }) + } else { + contentRef.style.overflow = "hidden" + heightAnim = animate(contentRef, { height: "0px" }, SPRING) + } }, { defer: true }, ), ) onCleanup(() => { - if (resizeFrame !== undefined) cancelAnimationFrame(resizeFrame) - observer?.disconnect() - fadeAnim?.stop() - heightSpring.destroy() + heightAnim?.stop() }) const handleOpenChange = (value: boolean) => { @@ -277,34 +118,85 @@ function ToolCallPanel(props: ToolCallPanelBaseProps) { return ( <Collapsible open={open()} onOpenChange={handleOpenChange} class="tool-collapsible"> <Collapsible.Trigger> - <ToolCallTriggerBody - trigger={props.trigger} - pending={pending()} - onSubtitleClick={props.onSubtitleClick} - arrow={!!props.children && !props.hideDetails && !props.locked && !pending()} - /> + <div data-component="tool-trigger"> + <div data-slot="basic-tool-tool-trigger-content"> + <div data-slot="basic-tool-tool-info"> + <Switch> + <Match when={isTriggerTitle(props.trigger) && props.trigger}> + {(trigger) => ( + <div data-slot="basic-tool-tool-info-structured"> + <div data-slot="basic-tool-tool-info-main"> + <span + data-slot="basic-tool-tool-title" + classList={{ + [trigger().titleClass ?? ""]: !!trigger().titleClass, + }} + > + <TextShimmer text={trigger().title} active={pending()} /> + </span> + <Show when={!pending()}> + <Show when={trigger().subtitle}> + <span + data-slot="basic-tool-tool-subtitle" + classList={{ + [trigger().subtitleClass ?? ""]: !!trigger().subtitleClass, + clickable: !!props.onSubtitleClick, + }} + onClick={(e) => { + if (props.onSubtitleClick) { + e.stopPropagation() + props.onSubtitleClick() + } + }} + > + {trigger().subtitle} + </span> + </Show> + <Show when={trigger().args?.length}> + <For each={trigger().args}> + {(arg) => ( + <span + data-slot="basic-tool-tool-arg" + classList={{ + [trigger().argsClass ?? ""]: !!trigger().argsClass, + }} + > + {arg} + </span> + )} + </For> + </Show> + </Show> + </div> + <Show when={!pending() && trigger().action}>{trigger().action}</Show> + </div> + )} + </Match> + <Match when={true}>{props.trigger as JSX.Element}</Match> + </Switch> + </div> + </div> + <Show when={props.children && !props.hideDetails && !props.locked && !pending()}> + <Collapsible.Arrow /> + </Show> + </div> </Collapsible.Trigger> - <Show when={props.springContent && props.animate !== false && props.children && !props.hideDetails}> + <Show when={props.animated && props.children && !props.hideDetails}> <div ref={contentRef} data-slot="collapsible-content" - data-spring-content + data-animated style={{ height: initialOpen ? "auto" : "0px", - overflow: "hidden", - display: initialOpen ? undefined : "none", + overflow: initialOpen ? "visible" : "hidden", }} > - <div ref={bodyRef} data-slot="basic-tool-content-inner"> - {props.children} - </div> + {props.children} </div> </Show> - <Show when={(!props.springContent || props.animate === false) && props.children && !props.hideDetails}> + <Show when={!props.animated && props.children && !props.hideDetails}> <Collapsible.Content> - <Show when={!props.defer || ready()}> - <div data-slot="basic-tool-content-inner">{props.children}</div> - </Show> + <Show when={!props.defer || ready()}>{props.children}</Show> </Collapsible.Content> </Show> </Collapsible> @@ -330,60 +222,6 @@ function args(input: Record<string, unknown> | undefined) { .slice(0, 3) } -export interface ToolCallRowProps { - variant: "row" - icon: string - trigger: TriggerTitle | JSX.Element - status?: string - animate?: boolean - onSubtitleClick?: () => void - open?: boolean - showArrow?: boolean - onOpenChange?: (value: boolean) => void -} -export interface ToolCallPanelProps extends Omit<ToolCallPanelBaseProps, "hideDetails"> { - variant: "panel" -} -export type ToolCallProps = ToolCallRowProps | ToolCallPanelProps -function ToolCallRoot(props: ToolCallProps) { - const pending = () => props.status === "pending" || props.status === "running" - if (props.variant === "row") { - return ( - <Show - when={props.onOpenChange} - fallback={ - <div data-component="collapsible" data-variant="normal" class="tool-collapsible"> - <div data-slot="collapsible-trigger"> - <ToolCallTriggerBody - trigger={props.trigger} - pending={pending()} - onSubtitleClick={props.onSubtitleClick} - /> - </div> - </div> - } - > - {(onOpenChange) => ( - <Collapsible open={props.open ?? true} onOpenChange={onOpenChange()} class="tool-collapsible"> - <Collapsible.Trigger> - <ToolCallTriggerBody - trigger={props.trigger} - pending={pending()} - onSubtitleClick={props.onSubtitleClick} - arrow={!!props.showArrow} - /> - </Collapsible.Trigger> - </Collapsible> - )} - </Show> - ) - } - - const [, rest] = splitProps(props, ["variant"]) - return <ToolCallPanel {...rest} /> -} -export const ToolCall = ToolCallRoot - export function GenericTool(props: { tool: string status?: string @@ -391,8 +229,7 @@ export function GenericTool(props: { input?: Record<string, unknown> }) { return ( - <ToolCall - variant={props.hideDetails ? "row" : "panel"} + <BasicTool icon="mcp" status={props.status} trigger={{ @@ -400,6 +237,7 @@ export function GenericTool(props: { subtitle: label(props.input), args: args(props.input), }} + hideDetails={props.hideDetails} /> ) } |
