1- import { HTMLAttributes , useCallback , useEffect , useMemo , useState } from "react"
1+ import { useCallback , useState , memo } from "react"
22import { useEvent } from "react-use"
3- import { Virtuoso } from "react-virtuoso"
43import { ChevronDown , Skull } from "lucide-react"
54
65import { CommandExecutionStatus , commandExecutionStatusSchema } from "@roo/schemas"
@@ -19,6 +18,17 @@ interface CommandExecutionProps {
1918 text ?: string
2019}
2120
21+ const parseCommandAndOutput = ( text : string ) => {
22+ const index = text . indexOf ( COMMAND_OUTPUT_STRING )
23+ if ( index === - 1 ) {
24+ return { command : text , output : "" }
25+ }
26+ return {
27+ command : text . slice ( 0 , index ) ,
28+ output : text . slice ( index + COMMAND_OUTPUT_STRING . length ) ,
29+ }
30+ }
31+
2232export const CommandExecution = ( { executionId, text } : CommandExecutionProps ) => {
2333 const { terminalShellIntegrationDisabled = false } = useExtensionState ( )
2434
@@ -27,13 +37,11 @@ export const CommandExecution = ({ executionId, text }: CommandExecutionProps) =
2737 const [ isExpanded , setIsExpanded ] = useState ( terminalShellIntegrationDisabled )
2838
2939 const [ status , setStatus ] = useState < CommandExecutionStatus | null > ( null )
30- const [ output , setOutput ] = useState ( "" )
31- const [ command , setCommand ] = useState ( text )
32-
33- const lines = useMemo (
34- ( ) => [ `$ ${ command } ` , ...output . split ( "\n" ) . filter ( ( line ) => line . trim ( ) !== "" ) ] ,
35- [ output , command ] ,
36- )
40+ const { command : initialCommand , output : initialOutput } = text
41+ ? parseCommandAndOutput ( text )
42+ : { command : "" , output : "" }
43+ const [ output , setOutput ] = useState ( initialOutput )
44+ const [ command , setCommand ] = useState ( initialCommand )
3745
3846 const onMessage = useCallback (
3947 ( event : MessageEvent ) => {
@@ -55,7 +63,7 @@ export const CommandExecution = ({ executionId, text }: CommandExecutionProps) =
5563 setStatus ( data )
5664 break
5765 case "output" :
58- setOutput ( ( output ) => output + data . output )
66+ setOutput ( data . output )
5967 break
6068 case "fallback" :
6169 setIsExpanded ( true )
@@ -72,22 +80,9 @@ export const CommandExecution = ({ executionId, text }: CommandExecutionProps) =
7280
7381 useEvent ( "message" , onMessage )
7482
75- useEffect ( ( ) => {
76- if ( ! status && text ) {
77- const index = text . indexOf ( COMMAND_OUTPUT_STRING )
78-
79- if ( index === - 1 ) {
80- setCommand ( text )
81- } else {
82- setCommand ( text . slice ( 0 , index ) )
83- setOutput ( text . slice ( index + COMMAND_OUTPUT_STRING . length ) )
84- }
85- }
86- } , [ status , text ] )
87-
8883 return (
8984 < div className = "w-full bg-vscode-editor-background border border-vscode-border rounded-xs p-2" >
90- < CodeBlock source = { command } language = "shell" />
85+ < CodeBlock source = { text ? parseCommandAndOutput ( text ) . command : command } language = "shell" />
9186 < div className = "flex flex-row items-center justify-between gap-2 px-1" >
9287 < div className = "flex flex-row items-center gap-1" >
9388 { status ?. status === "started" && (
@@ -116,7 +111,7 @@ export const CommandExecution = ({ executionId, text }: CommandExecutionProps) =
116111 < div className = "whitespace-nowrap" > Exited ({ status . exitCode } )</ div >
117112 </ div >
118113 ) }
119- { lines . length > 0 && (
114+ { output . length > 0 && (
120115 < Button variant = "ghost" size = "icon" onClick = { ( ) => setIsExpanded ( ! isExpanded ) } >
121116 < ChevronDown
122117 className = { cn ( "size-4 transition-transform duration-300" , {
@@ -127,31 +122,21 @@ export const CommandExecution = ({ executionId, text }: CommandExecutionProps) =
127122 ) }
128123 </ div >
129124 </ div >
130- < div
131- className = { cn ( "mt-1 pt-1 border-t border-border/25" , { hidden : ! isExpanded } ) }
132- style = { { height : Math . min ( ( lines . length + 1 ) * 16 , 200 ) } } >
133- { lines . length > 0 && (
134- < Virtuoso
135- className = "h-full"
136- totalCount = { lines . length }
137- itemContent = { ( i ) => < Line className = "text-sm" > { lines [ i ] } </ Line > }
138- followOutput = "auto"
139- />
140- ) }
141- </ div >
125+ < MemoizedOutputContainer isExpanded = { isExpanded } output = { output } />
142126 </ div >
143127 )
144128}
145129
146- type LineProps = HTMLAttributes < HTMLDivElement >
147-
148- const Line = ( { className, ...props } : LineProps ) => {
149- return (
150- < div
151- className = { cn ( "font-mono text-vscode-editor-foreground whitespace-pre-wrap break-words" , className ) }
152- { ...props }
153- />
154- )
155- }
156-
157130CommandExecution . displayName = "CommandExecution"
131+
132+ const OutputContainer = ( { isExpanded, output } : { isExpanded : boolean ; output : string } ) => (
133+ < div
134+ className = { cn ( "mt-1 pt-1 border-t border-border/25 overflow-hidden transition-[max-height] duration-300" , {
135+ "max-h-0" : ! isExpanded ,
136+ "max-h-[100%]" : isExpanded ,
137+ } ) } >
138+ { output . length > 0 && < CodeBlock source = { output } language = "log" /> }
139+ </ div >
140+ )
141+
142+ const MemoizedOutputContainer = memo ( OutputContainer )
0 commit comments