11using System . Collections ;
2+ using System . Text ;
23using System . Management . Automation ;
4+ using System . Management . Automation . Host ;
35using Microsoft . PowerShell . Commands ;
46using AIShell . Abstraction ;
57
@@ -59,7 +61,7 @@ protected override void EndProcessing()
5961 targetObject : null ) ;
6062 ThrowTerminatingError ( error ) ;
6163 }
62- else if ( UseClipboardForCommandOutput ( lastExitCode ) )
64+ else
6365 {
6466 // '$? == False' but no 'ErrorRecord' can be found that is associated with the last command line,
6567 // and '$LASTEXITCODE' is non-zero, which indicates the last failed command is a native command.
@@ -68,19 +70,27 @@ Running the command line `{commandLine}` in PowerShell v{channel.PSVersion} fail
6870 Please try to explain the failure and suggest the right fix.
6971 Output of the command line can be found in the context information below.
7072 """ ;
71- IncludeOutputFromClipboard = true ;
72- }
73- else
74- {
75- ThrowTerminatingError ( new (
76- new NotSupportedException ( $ "The output content is needed for suggestions on native executable failures.") ,
77- errorId : "OutputNeededForNativeCommand" ,
78- ErrorCategory . InvalidData ,
79- targetObject : null
80- ) ) ;
73+
74+ context = ScrapeScreenForNativeCommandOutput ( commandLine ) ;
75+ if ( context is null )
76+ {
77+ if ( UseClipboardForCommandOutput ( ) )
78+ {
79+ IncludeOutputFromClipboard = true ;
80+ }
81+ else
82+ {
83+ ThrowTerminatingError ( new (
84+ new NotSupportedException ( $ "The output content is needed for suggestions on native executable failures.") ,
85+ errorId : "OutputNeededForNativeCommand" ,
86+ ErrorCategory . InvalidData ,
87+ targetObject : null
88+ ) ) ;
89+ }
90+ }
8191 }
8292
83- if ( IncludeOutputFromClipboard )
93+ if ( context is null && IncludeOutputFromClipboard )
8494 {
8595 pwsh . Commands . Clear ( ) ;
8696 var r = pwsh
@@ -94,7 +104,7 @@ Output of the command line can be found in the context information below.
94104 channel . PostQuery ( new PostQueryMessage ( query , context , Agent ) ) ;
95105 }
96106
97- private bool UseClipboardForCommandOutput ( int lastExitCode )
107+ private bool UseClipboardForCommandOutput ( )
98108 {
99109 if ( IncludeOutputFromClipboard )
100110 {
@@ -127,4 +137,93 @@ private bool TryGetLastError(HistoryInfo lastHistory, out ErrorRecord lastError)
127137
128138 return true ;
129139 }
140+
141+ private string ScrapeScreenForNativeCommandOutput ( string lastCommandLine )
142+ {
143+ if ( ! OperatingSystem . IsWindows ( ) )
144+ {
145+ return null ;
146+ }
147+
148+ try
149+ {
150+ PSHostRawUserInterface rawUI = Host . UI . RawUI ;
151+ Coordinates start = new ( 0 , 0 ) , end = rawUI . CursorPosition ;
152+
153+ string currentCommandLine = MyInvocation . Line ;
154+ end . X = rawUI . BufferSize . Width - 1 ;
155+
156+ BufferCell [ , ] content = rawUI . GetBufferContents ( new Rectangle ( start , end ) ) ;
157+ StringBuilder line = new ( ) , buffer = new ( ) ;
158+
159+ bool collect = false ;
160+ int rows = content . GetLength ( 0 ) ;
161+ int columns = content . GetLength ( 1 ) ;
162+
163+ for ( int row = 0 ; row < rows ; row ++ )
164+ {
165+ line . Clear ( ) ;
166+ for ( int column = 0 ; column < columns ; column ++ )
167+ {
168+ line . Append ( content [ row , column ] . Character ) ;
169+ }
170+
171+ string lineStr = line . ToString ( ) . TrimEnd ( ) ;
172+ if ( ! collect && IsStartOfCommand ( lineStr , columns , lastCommandLine ) )
173+ {
174+ collect = true ;
175+ buffer . Append ( lineStr ) ;
176+ continue ;
177+ }
178+
179+ if ( collect )
180+ {
181+ // The current command line is just `Resolve-Error` or `fixit`, which should be on the same line
182+ // and thus there is no need to check for the span-to-the-next-line case.
183+ if ( lineStr . EndsWith ( currentCommandLine , StringComparison . Ordinal ) )
184+ {
185+ break ;
186+ }
187+
188+ buffer . Append ( '\n ' ) . Append ( lineStr ) ;
189+ }
190+ }
191+
192+ return buffer . Length is 0 ? null : buffer . ToString ( ) ;
193+ }
194+ catch
195+ {
196+ return null ;
197+ }
198+
199+ static bool IsStartOfCommand ( string lineStr , int columns , string commandLine )
200+ {
201+ if ( lineStr . EndsWith ( commandLine , StringComparison . Ordinal ) )
202+ {
203+ return true ;
204+ }
205+
206+ // Handle the case where the command line is too long and spans to the next line on screen,
207+ // like az, gcloud, and aws CLI commands which are usually long with many parameters.
208+ if ( columns - lineStr . Length > 3 || commandLine . Length < 20 )
209+ {
210+ // The line on screen unlikely spanned to the next line in this case.
211+ return false ;
212+ }
213+
214+ // We check if the prefix of the command line is the suffix of the current line on screen.
215+ ReadOnlySpan < char > lineStrSpan = lineStr . AsSpan ( ) ;
216+ ReadOnlySpan < char > cmdLineSpan = commandLine . AsSpan ( ) ;
217+
218+ // We assume the first 20 chars of the command line should be in the current line on screen.
219+ // This assumption is not perfect but practically good enough.
220+ int index = lineStrSpan . IndexOf ( cmdLineSpan [ ..20 ] , StringComparison . Ordinal ) ;
221+ if ( index >= 0 && cmdLineSpan . StartsWith ( lineStrSpan [ index ..] , StringComparison . Ordinal ) )
222+ {
223+ return true ;
224+ }
225+
226+ return false ;
227+ }
228+ }
130229}
0 commit comments