@@ -180,7 +180,7 @@ export const MessageBlock: React.FC<MessageBlockProps> = ({
180180 width : '100%' ,
181181 } }
182182 >
183- { /* User message timestamp with copy button and error indicator (non-bash commands) */ }
183+ { /* User message timestamp with error indicator (non-bash commands) */ }
184184 { isUser && ! bashCwd && (
185185 < box style = { { flexDirection : 'row' , alignItems : 'center' , gap : 1 } } >
186186 < text
@@ -193,8 +193,6 @@ export const MessageBlock: React.FC<MessageBlockProps> = ({
193193 { `[${ timestamp } ]` }
194194 </ text >
195195
196- < CopyIconButton textToCopy = { content } />
197-
198196 { validationErrors && validationErrors . length > 0 && (
199197 < Button
200198 onClick = { ( ) => setShowValidationPopover ( ! showValidationPopover ) }
@@ -212,7 +210,7 @@ export const MessageBlock: React.FC<MessageBlockProps> = ({
212210 </ box >
213211 ) }
214212
215- { /* Bash command metadata header (timestamp + copy button + cwd) */ }
213+ { /* Bash command metadata header (timestamp + cwd) - copy button moved inline */ }
216214 { bashCwd && (
217215 < box style = { { flexDirection : 'row' , alignItems : 'center' , gap : 1 } } >
218216 < text
@@ -224,7 +222,6 @@ export const MessageBlock: React.FC<MessageBlockProps> = ({
224222 >
225223 { `[${ timestamp } ]` }
226224 </ text >
227- < CopyIconButton textToCopy = { content } />
228225 < text
229226 attributes = { TextAttributes . DIM }
230227 style = { {
@@ -285,9 +282,15 @@ export const MessageBlock: React.FC<MessageBlockProps> = ({
285282 onBuildMax = { onBuildMax }
286283 isLastMessage = { isLastMessage }
287284 />
285+ { /* Copy button after user message content (for block-based messages) */ }
286+ { isUser && (
287+ < box style = { { flexDirection : 'row' , marginTop : 0 } } >
288+ < CopyIconButton textToCopy = { content } />
289+ </ box >
290+ ) }
288291 </ box >
289292 ) : (
290- < SimpleContent
293+ < UserContentWithCopyButton
291294 content = { content }
292295 messageId = { messageId }
293296 isLoading = { isLoading }
@@ -296,6 +299,7 @@ export const MessageBlock: React.FC<MessageBlockProps> = ({
296299 textColor = { resolvedTextColor }
297300 codeBlockWidth = { markdownOptions . codeBlockWidth }
298301 palette = { markdownOptions . palette }
302+ showCopyButton = { isUser }
299303 />
300304 ) }
301305 { /* Show image attachments for user messages */ }
@@ -815,7 +819,7 @@ const AgentBranchWrapper = memo(
815819 } ,
816820)
817821
818- interface SimpleContentProps {
822+ interface UserContentWithCopyButtonProps {
819823 content : string
820824 messageId : string
821825 isLoading : boolean
@@ -824,9 +828,14 @@ interface SimpleContentProps {
824828 textColor : string
825829 codeBlockWidth : number
826830 palette : MarkdownPalette
831+ showCopyButton : boolean
827832}
828833
829- const SimpleContent = memo (
834+ /**
835+ * Renders user content with an inline copy button that wraps together with the last word.
836+ * Uses a wrapping flexbox so the copy button stays attached to the last word when line-breaking.
837+ */
838+ const UserContentWithCopyButton = memo (
830839 ( {
831840 content,
832841 messageId,
@@ -836,25 +845,99 @@ const SimpleContent = memo(
836845 textColor,
837846 codeBlockWidth,
838847 palette,
839- } : SimpleContentProps ) => {
848+ showCopyButton,
849+ } : UserContentWithCopyButtonProps ) => {
840850 const isStreamingMessage = isLoading || ! isComplete
841851 const normalizedContent = isStreamingMessage
842852 ? trimTrailingNewlines ( content )
843853 : content . trim ( )
844854
855+ if ( ! showCopyButton ) {
856+ return (
857+ < text
858+ key = { `message-content-${ messageId } ` }
859+ style = { { wrapMode : 'word' , fg : textColor } }
860+ attributes = { isUser ? TextAttributes . ITALIC : undefined }
861+ >
862+ < ContentWithMarkdown
863+ content = { normalizedContent }
864+ isStreaming = { isStreamingMessage }
865+ codeBlockWidth = { codeBlockWidth }
866+ palette = { palette }
867+ />
868+ </ text >
869+ )
870+ }
871+
872+ // For user messages with copy button, we need to handle the last word specially
873+ // so the copy button wraps with it as a unit.
874+ // Split by newlines first to handle multi-line content correctly.
875+ const lines = normalizedContent . split ( '\n' )
876+ const lastLine = lines [ lines . length - 1 ] || ''
877+ const lastSpaceIndex = lastLine . lastIndexOf ( ' ' )
878+
879+ let contentBeforeLastWord : string
880+ let lastWord : string
881+
882+ if ( lastSpaceIndex > 0 ) {
883+ // Last line has multiple words - split on the last space
884+ const beforeLastWordOnLastLine = lastLine . slice ( 0 , lastSpaceIndex + 1 )
885+ lastWord = lastLine . slice ( lastSpaceIndex + 1 )
886+ if ( lines . length > 1 ) {
887+ contentBeforeLastWord = lines . slice ( 0 , - 1 ) . join ( '\n' ) + '\n' + beforeLastWordOnLastLine
888+ } else {
889+ contentBeforeLastWord = beforeLastWordOnLastLine
890+ }
891+ } else if ( lines . length > 1 ) {
892+ // Last line is a single word, but there are previous lines
893+ contentBeforeLastWord = lines . slice ( 0 , - 1 ) . join ( '\n' ) + '\n'
894+ lastWord = lastLine
895+ } else {
896+ // Single word, single line
897+ contentBeforeLastWord = ''
898+ lastWord = normalizedContent
899+ }
900+
845901 return (
846- < text
902+ < box
847903 key = { `message-content-${ messageId } ` }
848- style = { { wrapMode : 'word' , fg : textColor } }
849- attributes = { isUser ? TextAttributes . ITALIC : undefined }
904+ style = { {
905+ flexDirection : 'row' ,
906+ flexWrap : 'wrap' ,
907+ alignItems : 'baseline' ,
908+ } }
850909 >
851- < ContentWithMarkdown
852- content = { normalizedContent }
853- isStreaming = { isStreamingMessage }
854- codeBlockWidth = { codeBlockWidth }
855- palette = { palette }
856- />
857- </ text >
910+ { contentBeforeLastWord && (
911+ < text
912+ style = { { wrapMode : 'word' , fg : textColor } }
913+ attributes = { isUser ? TextAttributes . ITALIC : undefined }
914+ >
915+ < ContentWithMarkdown
916+ content = { contentBeforeLastWord }
917+ isStreaming = { isStreamingMessage }
918+ codeBlockWidth = { codeBlockWidth }
919+ palette = { palette }
920+ />
921+ </ text >
922+ ) }
923+ { /* Last word + copy button wrapped together so they line-break as a unit */ }
924+ < box
925+ style = { {
926+ flexDirection : 'row' ,
927+ alignItems : 'center' ,
928+ flexWrap : 'no-wrap' ,
929+ gap : 1 ,
930+ } }
931+ >
932+ < text
933+ style = { { wrapMode : 'none' , fg : textColor } }
934+ attributes = { isUser ? TextAttributes . ITALIC : undefined }
935+ >
936+ { lastWord }
937+ </ text >
938+ < CopyIconButton textToCopy = { content } />
939+ </ box >
940+ </ box >
858941 )
859942 } ,
860943)
0 commit comments