77 For ,
88 Match ,
99 on ,
10+ onCleanup ,
1011 onMount ,
1112 Show ,
1213 Switch ,
@@ -1682,6 +1683,9 @@ function InlineTool(props: {
16821683 error ( ) ?. includes ( "user dismissed" ) ,
16831684 )
16841685
1686+ const timing = createTiming ( ( ) => props . part )
1687+ const interrupted = createMemo ( ( ) => isToolInterrupted ( props . part ) )
1688+
16851689 return (
16861690 < box
16871691 marginTop = { margin ( ) }
@@ -1717,13 +1721,21 @@ function InlineTool(props: {
17171721 >
17181722 < Switch >
17191723 < Match when = { props . spinner } >
1720- < Spinner color = { fg ( ) } children = { props . children } />
1724+ < Spinner color = { fg ( ) } >
1725+ { props . children }
1726+ < Show when = { timing ( ) } >
1727+ < span style = { { fg : interrupted ( ) ? theme . error : theme . textMuted } } > · { timing ( ) } </ span >
1728+ </ Show >
1729+ </ Spinner >
17211730 </ Match >
17221731 < Match when = { true } >
17231732 < text paddingLeft = { 3 } fg = { fg ( ) } attributes = { denied ( ) ? TextAttributes . STRIKETHROUGH : undefined } >
17241733 < Show fallback = { < > ~ { props . pending } </ > } when = { props . complete } >
17251734 < span style = { { fg : props . iconColor } } > { props . icon } </ span > { props . children }
17261735 </ Show >
1736+ < Show when = { timing ( ) } >
1737+ < span style = { { fg : interrupted ( ) ? theme . error : theme . textMuted } } > · { timing ( ) } </ span >
1738+ </ Show >
17271739 </ text >
17281740 </ Match >
17291741 </ Switch >
@@ -1734,6 +1746,65 @@ function InlineTool(props: {
17341746 )
17351747}
17361748
1749+ function createTiming ( part : ( ) => ToolPart | undefined ) {
1750+ const [ now , setNow ] = createSignal ( Date . now ( ) )
1751+ const [ displayMs , setDisplayMs ] = createSignal ( 0 )
1752+ const running = createMemo ( ( ) => part ( ) ?. state . status === "running" )
1753+
1754+ createEffect (
1755+ on (
1756+ ( ) => part ( ) ?. id ,
1757+ ( ) => {
1758+ setDisplayMs ( 0 )
1759+ } ,
1760+ { defer : true } ,
1761+ ) ,
1762+ )
1763+
1764+ createEffect ( ( ) => {
1765+ if ( ! running ( ) ) return
1766+ setNow ( Date . now ( ) )
1767+ const interval = setInterval ( ( ) => setNow ( Date . now ( ) ) , 1000 )
1768+ onCleanup ( ( ) => clearInterval ( interval ) )
1769+ } )
1770+
1771+ createEffect ( ( ) => {
1772+ const p = part ( )
1773+ if ( ! p ) return
1774+ const start = toolStartTime ( p )
1775+ if ( typeof start !== "number" ) return
1776+ const end = toolEndTime ( p )
1777+ const finish = typeof end === "number" ? end : running ( ) ? now ( ) : undefined
1778+ if ( typeof finish !== "number" ) return
1779+ setDisplayMs ( ( prev ) => Math . max ( prev , finish - start ) )
1780+ } )
1781+
1782+ return createMemo ( ( ) => {
1783+ const p = part ( )
1784+ if ( ! p ) return ""
1785+ const start = toolStartTime ( p )
1786+ if ( typeof start !== "number" ) return ""
1787+
1788+ if ( isToolInterrupted ( p ) ) {
1789+ return formatToolClock ( start ) + " · Interrupted"
1790+ }
1791+
1792+ const parts = [ formatToolClock ( start ) ]
1793+ if ( displayMs ( ) > 0 ) {
1794+ parts . push ( formatToolRuntime ( displayMs ( ) ) )
1795+ return parts . join ( " · " )
1796+ }
1797+
1798+ const end = toolEndTime ( p )
1799+ if ( typeof end === "number" ) {
1800+ parts . push ( formatToolRuntime ( Math . max ( 1 , end - start ) ) )
1801+ return parts . join ( " · " )
1802+ }
1803+
1804+ return parts . join ( " · " )
1805+ } )
1806+ }
1807+
17371808function BlockTool ( props : {
17381809 title : string
17391810 children : JSX . Element
@@ -1745,6 +1816,9 @@ function BlockTool(props: {
17451816 const renderer = useRenderer ( )
17461817 const [ hover , setHover ] = createSignal ( false )
17471818 const error = createMemo ( ( ) => ( props . part ?. state . status === "error" ? props . part . state . error : undefined ) )
1819+ const timing = createTiming ( ( ) => props . part )
1820+ const interrupted = createMemo ( ( ) => ( props . part ? isToolInterrupted ( props . part ) : false ) )
1821+
17481822 return (
17491823 < box
17501824 border = { [ "left" ] }
@@ -1768,10 +1842,18 @@ function BlockTool(props: {
17681842 fallback = {
17691843 < text paddingLeft = { 3 } fg = { theme . textMuted } >
17701844 { props . title }
1845+ < Show when = { timing ( ) } >
1846+ < span style = { { fg : interrupted ( ) ? theme . error : theme . textMuted } } > · { timing ( ) } </ span >
1847+ </ Show >
17711848 </ text >
17721849 }
17731850 >
1774- < Spinner color = { theme . textMuted } > { props . title . replace ( / ^ # / , "" ) } </ Spinner >
1851+ < Spinner color = { theme . textMuted } >
1852+ { props . title . replace ( / ^ # / , "" ) }
1853+ < Show when = { timing ( ) } >
1854+ < span style = { { fg : interrupted ( ) ? theme . error : theme . textMuted } } > · { timing ( ) } </ span >
1855+ </ Show >
1856+ </ Spinner >
17751857 </ Show >
17761858 { props . children }
17771859 < Show when = { error ( ) } >
@@ -1781,6 +1863,49 @@ function BlockTool(props: {
17811863 )
17821864}
17831865
1866+ function toolStartTime ( part : ToolPart ) : number | undefined {
1867+ switch ( part . state . status ) {
1868+ case "running" :
1869+ case "completed" :
1870+ case "error" :
1871+ return part . state . time . start
1872+ default :
1873+ return undefined
1874+ }
1875+ }
1876+
1877+ function toolEndTime ( part : ToolPart ) : number | undefined {
1878+ switch ( part . state . status ) {
1879+ case "completed" :
1880+ case "error" :
1881+ return part . state . time . end
1882+ default :
1883+ return undefined
1884+ }
1885+ }
1886+
1887+ function isToolInterrupted ( part : ToolPart ) : boolean {
1888+ if ( part . state . status !== "error" ) return false
1889+ return part . state . metadata ?. interrupted === true
1890+ }
1891+
1892+ function formatToolClock ( input : number ) {
1893+ return new Intl . DateTimeFormat ( undefined , {
1894+ hour : "2-digit" ,
1895+ minute : "2-digit" ,
1896+ } ) . format ( input )
1897+ }
1898+
1899+ function formatToolRuntime ( input : number ) {
1900+ const total = input <= 0 ? 0 : Math . max ( 1 , Math . round ( input / 1000 ) )
1901+ if ( total < 60 ) return `${ total } s`
1902+ const hours = Math . floor ( total / 3600 )
1903+ const minutes = Math . floor ( ( total % 3600 ) / 60 )
1904+ const seconds = total % 60
1905+ if ( hours > 0 ) return `${ hours } h ${ minutes } m ${ seconds } s`
1906+ return `${ minutes } m ${ seconds } s`
1907+ }
1908+
17841909function Bash ( props : ToolProps < typeof BashTool > ) {
17851910 const { theme } = useTheme ( )
17861911 const sync = useSync ( )
0 commit comments