@@ -11,7 +11,7 @@ import {
1111 type Accessor ,
1212 type JSX ,
1313} from "solid-js"
14- import { animate , type AnimationPlaybackControls } from "motion "
14+ import { Option , Schema } from "effect "
1515import { useI18n } from "@opencode/ui/context/i18n"
1616import { createStore } from "solid-js/store"
1717import { Collapsible } from "@opencode/ui/collapsible"
@@ -28,13 +28,8 @@ export type TriggerTitle = {
2828 action ?: JSX . Element
2929}
3030
31- const isTriggerTitle = ( val : unknown ) : val is TriggerTitle => {
32- if ( typeof val !== "object" || val === null ) return false
33-
34- if ( typeof Node !== "undefined" && val instanceof Node ) return false
35-
36- return "title" in val && typeof val . title === "string"
37- }
31+ const isTriggerTitle = ( value : TriggerTitle | JSX . Element ) : value is TriggerTitle =>
32+ value instanceof Object && ! ( "nodeType" in value ) && "title" in value
3833
3934export interface BasicToolProps {
4035 icon : IconProps [ "name" ]
@@ -51,7 +46,6 @@ export interface BasicToolProps {
5146 allowOpenWhilePending ?: boolean
5247 defer ?: boolean
5348 locked ?: boolean
54- animated ?: boolean
5549 rail ?: boolean
5650 onSubtitleClick ?: ( ) => void
5751 onTriggerClick ?: JSX . EventHandlerUnion < HTMLElement , MouseEvent >
@@ -62,8 +56,6 @@ export interface BasicToolProps {
6256 compact ?: boolean
6357}
6458
65- const SPRING = { type : "spring" as const , visualDuration : 0.35 , bounce : 0 }
66-
6759const deferredMounts : Array < { active : boolean ; fn : ( ) => void } > = [ ]
6860
6961let deferredFrame : number | undefined
@@ -122,7 +114,7 @@ export function BasicTool(props: BasicToolProps) {
122114 const triggerContent = createMemo ( ( ) => {
123115 const value = props . trigger
124116
125- return typeof value === "function" ? value ( open ) : value
117+ return value instanceof Function ? value ( open ) : value
126118 } )
127119
128120 const triggerTitle = createMemo ( ( ) => {
@@ -131,6 +123,12 @@ export function BasicTool(props: BasicToolProps) {
131123 return isTriggerTitle ( value ) ? value : undefined
132124 } )
133125
126+ const triggerElement = ( ) => {
127+ const value = triggerContent ( )
128+
129+ return isTriggerTitle ( value ) ? undefined : value
130+ }
131+
134132 let cancelReady : ( ( ) => void ) | undefined
135133
136134 const cancel = ( ) => {
@@ -185,39 +183,6 @@ export function BasicTool(props: BasicToolProps) {
185183 ) ,
186184 )
187185
188- // Animated height for collapsible open/close
189- let contentRef : HTMLDivElement | undefined
190- let heightAnim : AnimationPlaybackControls | undefined
191- const initialOpen = open ( )
192-
193- createEffect (
194- on (
195- open ,
196- ( isOpen ) => {
197- if ( ! props . animated || ! contentRef ) return
198- heightAnim ?. stop ( )
199-
200- if ( isOpen ) {
201- contentRef . style . overflow = "hidden"
202- heightAnim = animate ( contentRef , { height : "auto" } , SPRING )
203- void heightAnim . finished . then ( ( ) => {
204- if ( ! contentRef || ! open ( ) ) return
205- contentRef . style . overflow = "visible"
206- contentRef . style . height = "auto"
207- } )
208- } else {
209- contentRef . style . overflow = "hidden"
210- heightAnim = animate ( contentRef , { height : "0px" } , SPRING )
211- }
212- } ,
213- { defer : true } ,
214- ) ,
215- )
216-
217- onCleanup ( ( ) => {
218- heightAnim ?. stop ( )
219- } )
220-
221186 const handleOpenChange = ( value : boolean ) => {
222187 if ( pending ( ) && ! props . allowOpenWhilePending ) return
223188
@@ -286,7 +251,7 @@ export function BasicTool(props: BasicToolProps) {
286251 </ div >
287252 ) }
288253 </ Match >
289- < Match when = { true } > { triggerContent ( ) as JSX . Element } </ Match >
254+ < Match when = { true } > { triggerElement ( ) } </ Match >
290255 </ Switch >
291256 </ div >
292257 </ div >
@@ -340,20 +305,7 @@ export function BasicTool(props: BasicToolProps) {
340305 { trigger ( ) }
341306 </ Collapsible . Trigger >
342307 </ Show >
343- < Show when = { props . animated && hasChildren ( ) && ! props . hideDetails } >
344- < div
345- ref = { contentRef }
346- data-slot = "collapsible-content"
347- data-animated
348- style = { {
349- height : initialOpen ? "auto" : "0px" ,
350- overflow : initialOpen ? "visible" : "hidden" ,
351- } }
352- >
353- < Show when = { ! props . defer || ready ( ) } > { props . children } </ Show >
354- </ div >
355- </ Show >
356- < Show when = { ! props . animated && hasChildren ( ) && ! props . hideDetails } >
308+ < Show when = { hasChildren ( ) && ! props . hideDetails } >
357309 < Show
358310 when = { props . locked && open ( ) && ! props . defer }
359311 fallback = {
@@ -371,36 +323,30 @@ export function BasicTool(props: BasicToolProps) {
371323 )
372324}
373325
374- function label ( input : Record < string , unknown > | undefined ) {
375- const keys = [ "description" , "query" , "url" , "path" , "pattern" , "name" ]
326+ // SAFETY: tool input is an open per-tool record. GenericTool shows arbitrary keys and decodes each value it reads.
327+ // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- see SAFETY above
328+ type GenericToolInput = Record < string , unknown >
376329
377- return keys . map ( ( key ) => input ?. [ key ] ) . find ( ( value ) : value is string => typeof value === "string" && value . length > 0 )
378- }
330+ const labelKeys = [ "description" , "query" , "url" , "path" , "pattern" , "name" ]
379331
380- function args ( input : Record < string , unknown > | undefined ) {
381- if ( ! input ) return [ ]
382- const skip = new Set ( [ "description" , "query" , "url" , "path" , "pattern" , "name" ] )
332+ const labelValue = Schema . decodeUnknownOption ( Schema . NonEmptyString )
383333
384- return Object . entries ( input )
385- . filter ( ( [ key ] ) => ! skip . has ( key ) )
386- . flatMap ( ( [ key , value ] ) => {
387- if ( typeof value === "string" ) return [ `${ key } =${ value } ` ]
334+ const argValue = Schema . decodeUnknownOption ( Schema . Union ( [ Schema . String , Schema . Number , Schema . Boolean ] ) )
388335
389- if ( typeof value === "number" ) return [ `${ key } =${ value } ` ]
336+ function label ( input : GenericToolInput | undefined ) {
337+ return labelKeys . flatMap ( ( key ) => Option . toArray ( labelValue ( input ?. [ key ] ) ) ) [ 0 ]
338+ }
390339
391- if ( typeof value === "boolean" ) return [ `${ key } =${ value } ` ]
340+ function args ( input : GenericToolInput | undefined ) {
341+ if ( ! input ) return [ ]
392342
393- return [ ]
394- } )
343+ return Object . entries ( input )
344+ . filter ( ( [ key ] ) => ! labelKeys . includes ( key ) )
345+ . flatMap ( ( [ key , value ] ) => Option . toArray ( argValue ( value ) ) . map ( ( arg ) => `${ key } =${ arg } ` ) )
395346 . slice ( 0 , 3 )
396347}
397348
398- export function GenericTool ( props : {
399- tool : string
400- status ?: string
401- hideDetails ?: boolean
402- input ?: Record < string , unknown >
403- } ) {
349+ export function GenericTool ( props : { tool : string ; status ?: string ; hideDetails ?: boolean ; input ?: GenericToolInput } ) {
404350 const i18n = useI18n ( )
405351
406352 return (
0 commit comments