summaryrefslogtreecommitdiffhomepage
path: root/packages/ui/src/components/basic-tool.tsx
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-03-09 07:36:39 -0500
committerGitHub <[email protected]>2026-03-09 07:36:39 -0500
commitc71d1bde5e8dcc8be49c15697ad2e5d0f2607e5e (patch)
treea30482cedb38dc24cad70e24ad717817065620d6 /packages/ui/src/components/basic-tool.tsx
parentf27ef595f65aa719be3f8d08665d683e95083ed3 (diff)
downloadopencode-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.tsx356
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}
/>
)
}