@@ -55,6 +55,7 @@ import {
5555 KeyRoundIcon ,
5656 LoaderIcon ,
5757 LogInIcon ,
58+ RefreshCwIcon ,
5859 SearchIcon ,
5960 ShieldCheckIcon ,
6061 SplitIcon ,
@@ -142,6 +143,7 @@ interface TimelineRowSharedState {
142143 providerAuthReconnect : ProviderAuthReconnectAction | null ;
143144 resolvedProviderAuthReconnectIds : ReadonlySet < string > ;
144145 mcpAuthReconnectStatusByServerName : ReadonlyMap < string , McpAuthReconnectStatus > ;
146+ failedTurnRetry : FailedTurnRetryAction | null ;
145147 onRevertUserMessage : ( messageId : MessageId ) => void ;
146148 onContinueInNewThread ?: ( messageId : MessageId ) => void ;
147149 onImageExpand : ( preview : ExpandedImagePreview ) => void ;
@@ -166,6 +168,12 @@ export interface TimelineProposedPlanState {
166168 readonly onOpenThread : ( threadId : ThreadId ) => void ;
167169}
168170
171+ interface FailedTurnRetryAction {
172+ readonly messageId : MessageId ;
173+ readonly isRetrying : boolean ;
174+ readonly onRetry : ( ) => void ;
175+ }
176+
169177interface TimelineRowActivityState {
170178 isWorking : boolean ;
171179 isRevertingCheckpoint : boolean ;
@@ -515,6 +523,7 @@ interface MessagesTimelineProps {
515523 workspaceRoot : string | undefined ;
516524 skills ?: ReadonlyArray < Pick < ServerProviderSkill , "name" | "displayName" > > ;
517525 providerAuthReconnect ?: ProviderAuthReconnectAction | null ;
526+ failedTurnRetry ?: FailedTurnRetryAction | null ;
518527 onRunProviderAuthReconnect ?: ( action : ProviderAuthReconnectAction ) => void ;
519528 mcpAuthReconnectStatusByServerName ?: ReadonlyMap < string , McpAuthReconnectStatus > ;
520529 onRunMcpAuthReconnect ?: ( action : McpAuthReconnectAction ) => void ;
@@ -571,6 +580,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
571580 workspaceRoot,
572581 skills = EMPTY_TIMELINE_SKILLS ,
573582 providerAuthReconnect = null ,
583+ failedTurnRetry = null ,
574584 onRunProviderAuthReconnect,
575585 mcpAuthReconnectStatusByServerName = EMPTY_MCP_AUTH_RECONNECT_STATUS ,
576586 onRunMcpAuthReconnect,
@@ -1133,6 +1143,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
11331143 providerAuthReconnect,
11341144 resolvedProviderAuthReconnectIds,
11351145 mcpAuthReconnectStatusByServerName,
1146+ failedTurnRetry,
11361147 onRevertUserMessage,
11371148 ...( onContinueInNewThread ? { onContinueInNewThread } : { } ) ,
11381149 onImageExpand,
@@ -1158,6 +1169,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
11581169 providerAuthReconnect ,
11591170 resolvedProviderAuthReconnectIds ,
11601171 mcpAuthReconnectStatusByServerName ,
1172+ failedTurnRetry ,
11611173 onRevertUserMessage ,
11621174 onContinueInNewThread ,
11631175 onImageExpand ,
@@ -1806,6 +1818,7 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message"
18061818 const terminalContexts = displayedUserMessage . contexts ;
18071819 const transcriptHighlights = displayedUserMessage . transcriptHighlights ;
18081820 const canRevertAgentWork = typeof row . revertTurnCount === "number" ;
1821+ const canRetryFailedTurn = ctx . failedTurnRetry ?. messageId === row . message . id ;
18091822
18101823 return (
18111824 < div className = "flex justify-end" >
@@ -1831,14 +1844,17 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message"
18311844 }
18321845 footer = {
18331846 < >
1834- < div className = "flex items-center gap-1.5 opacity-0 transition-opacity duration-200 focus-within:opacity-100 group-hover:opacity-100" >
1835- { displayedUserMessage . copyText && (
1836- < MessageCopyButton text = { displayedUserMessage . copyText } />
1837- ) }
1838- { displayedUserMessage . copyText && (
1839- < ContinueInNewThreadButton messageId = { row . message . id } action = "edit" />
1840- ) }
1841- { canRevertAgentWork && < RevertUserMessageButton messageId = { row . message . id } /> }
1847+ < div className = "flex items-center gap-1.5" >
1848+ { canRetryFailedTurn && < RetryUserMessageButton /> }
1849+ < div className = "flex items-center gap-1.5 opacity-0 transition-opacity duration-200 focus-within:opacity-100 group-hover:opacity-100" >
1850+ { displayedUserMessage . copyText && (
1851+ < MessageCopyButton text = { displayedUserMessage . copyText } />
1852+ ) }
1853+ { displayedUserMessage . copyText && (
1854+ < ContinueInNewThreadButton messageId = { row . message . id } action = "edit" />
1855+ ) }
1856+ { canRevertAgentWork && < RevertUserMessageButton messageId = { row . message . id } /> }
1857+ </ div >
18421858 </ div >
18431859 < p className = "text-right text-xs tracking-tight tabular-nums text-muted-foreground/50" >
18441860 { formatTimestamp ( row . message . createdAt , ctx . timestampFormat ) }
@@ -1921,6 +1937,31 @@ function RevertUserMessageButton({ messageId }: { messageId: MessageId }) {
19211937 ) ;
19221938}
19231939
1940+ function RetryUserMessageButton ( ) {
1941+ const ctx = use ( TimelineRowCtx ) ;
1942+ const activity = use ( TimelineRowActivityCtx ) ;
1943+ const retry = ctx . failedTurnRetry ;
1944+ if ( ! retry ) {
1945+ return null ;
1946+ }
1947+
1948+ return (
1949+ < Button
1950+ type = "button"
1951+ size = "xs"
1952+ variant = "outline"
1953+ disabled = { retry . isRetrying || activity . isWorking }
1954+ onClick = { retry . onRetry }
1955+ aria-label = "Retry this message"
1956+ tooltip = "Retry this message"
1957+ className = "gap-1 px-2 enabled:cursor-pointer"
1958+ >
1959+ < RefreshCwIcon className = { cn ( "size-3" , retry . isRetrying && "animate-spin" ) } />
1960+ { retry . isRetrying ? "Retrying" : "Retry" }
1961+ </ Button >
1962+ ) ;
1963+ }
1964+
19241965function ContinueInNewThreadButton ( {
19251966 messageId,
19261967 className,
0 commit comments