1
- using System . Diagnostics ;
1
+ using System . Collections . ObjectModel ;
2
+ using System . Diagnostics ;
2
3
using System . Reflection ;
3
4
using System . Text ;
4
5
using System . Management . Automation ;
6
+ using System . Management . Automation . Host ;
5
7
using System . Management . Automation . Runspaces ;
6
8
using AIShell . Abstraction ;
9
+ using Microsoft . PowerShell . Commands ;
10
+ using System . Text . Json ;
7
11
8
12
namespace AIShell . Integration ;
9
13
@@ -15,28 +19,35 @@ public class Channel : IDisposable
15
19
private readonly string _shellPipeName ;
16
20
private readonly Type _psrlType ;
17
21
private readonly Runspace _runspace ;
22
+ private readonly EngineIntrinsics _intrinsics ;
18
23
private readonly MethodInfo _psrlInsert , _psrlRevertLine , _psrlAcceptLine ;
19
24
private readonly FieldInfo _psrlHandleResizing , _psrlReadLineReady ;
20
25
private readonly object _psrlSingleton ;
21
26
private readonly ManualResetEvent _connSetupWaitHandler ;
22
27
private readonly Predictor _predictor ;
23
28
private readonly ScriptBlock _onIdleAction ;
29
+ private readonly List < HistoryInfo > _commandHistory ;
24
30
31
+ private PathInfo _currentLocation ;
25
32
private ShellClientPipe _clientPipe ;
26
33
private ShellServerPipe _serverPipe ;
27
34
private bool ? _setupSuccess ;
28
35
private Exception _exception ;
29
36
private Thread _serverThread ;
30
37
private CodePostData _pendingPostCodeData ;
31
38
32
- private Channel ( Runspace runspace , Type psConsoleReadLineType )
39
+ private Channel ( Runspace runspace , EngineIntrinsics intrinsics , Type psConsoleReadLineType )
33
40
{
34
41
ArgumentNullException . ThrowIfNull ( runspace ) ;
35
42
ArgumentNullException . ThrowIfNull ( psConsoleReadLineType ) ;
36
43
37
44
_runspace = runspace ;
45
+ _intrinsics = intrinsics ;
38
46
_psrlType = psConsoleReadLineType ;
39
47
_connSetupWaitHandler = new ManualResetEvent ( false ) ;
48
+ _currentLocation = _intrinsics . SessionState . Path . CurrentLocation ;
49
+ _runspace . AvailabilityChanged += RunspaceAvailableAction ;
50
+ _intrinsics . InvokeCommand . LocationChangedAction += LocationChangedAction ;
40
51
41
52
_shellPipeName = new StringBuilder ( MaxNamedPipeNameSize )
42
53
. Append ( "pwsh_aish." )
@@ -57,13 +68,14 @@ private Channel(Runspace runspace, Type psConsoleReadLineType)
57
68
_psrlReadLineReady = _psrlType . GetField ( "_readLineReady" , fieldFlags ) ;
58
69
_psrlHandleResizing = _psrlType . GetField ( "_handlePotentialResizing" , fieldFlags ) ;
59
70
71
+ _commandHistory = [ ] ;
60
72
_predictor = new Predictor ( ) ;
61
73
_onIdleAction = ScriptBlock . Create ( "[AIShell.Integration.Channel]::Singleton.OnIdleHandler()" ) ;
62
74
}
63
75
64
- public static Channel CreateSingleton ( Runspace runspace , Type psConsoleReadLineType )
76
+ public static Channel CreateSingleton ( Runspace runspace , EngineIntrinsics intrinsics , Type psConsoleReadLineType )
65
77
{
66
- return Singleton ??= new Channel ( runspace , psConsoleReadLineType ) ;
78
+ return Singleton ??= new Channel ( runspace , intrinsics , psConsoleReadLineType ) ;
67
79
}
68
80
69
81
public static Channel Singleton { get ; private set ; }
@@ -127,6 +139,95 @@ private async void ThreadProc()
127
139
await _serverPipe . StartProcessingAsync ( ConnectionTimeout , CancellationToken . None ) ;
128
140
}
129
141
142
+ private void LocationChangedAction ( object sender , LocationChangedEventArgs e )
143
+ {
144
+ _currentLocation = e . NewPath ;
145
+ }
146
+
147
+ private void RunspaceAvailableAction ( object sender , RunspaceAvailabilityEventArgs e )
148
+ {
149
+ if ( sender is null || e . RunspaceAvailability is not RunspaceAvailability . Available )
150
+ {
151
+ return ;
152
+ }
153
+
154
+ // It's safe to get states of the PowerShell Runspace now because it's available and this event
155
+ // is handled synchronously.
156
+ // We may want to invoke command or script here, and we have to unregister ourself before doing
157
+ // that, because the invocation would change the availability of the Runspace, which will cause
158
+ // the 'AvailabilityChanged' to be fired again and re-enter our handler.
159
+ // We register ourself back after we are done with the processing.
160
+ var pwshRunspace = ( Runspace ) sender ;
161
+ pwshRunspace . AvailabilityChanged -= RunspaceAvailableAction ;
162
+
163
+ try
164
+ {
165
+ using var ps = PowerShell . Create ( ) ;
166
+ ps . Runspace = pwshRunspace ;
167
+
168
+ var results = ps
169
+ . AddCommand ( "Get-History" )
170
+ . AddParameter ( "Count" , 5 )
171
+ . InvokeAndCleanup < HistoryInfo > ( ) ;
172
+
173
+ if ( results . Count is 0 ||
174
+ ( _commandHistory . Count > 0 && _commandHistory [ ^ 1 ] . Id == results [ ^ 1 ] . Id ) )
175
+ {
176
+ // No command history yet, or no change since the last update.
177
+ return ;
178
+ }
179
+
180
+ lock ( _commandHistory )
181
+ {
182
+ _commandHistory . Clear ( ) ;
183
+ _commandHistory . AddRange ( results ) ;
184
+ }
185
+ }
186
+ finally
187
+ {
188
+ pwshRunspace . AvailabilityChanged += RunspaceAvailableAction ;
189
+ }
190
+ }
191
+
192
+ private string CaptureScreen ( )
193
+ {
194
+ if ( ! OperatingSystem . IsWindows ( ) )
195
+ {
196
+ return null ;
197
+ }
198
+
199
+ try
200
+ {
201
+ PSHostRawUserInterface rawUI = _intrinsics . Host . UI . RawUI ;
202
+ Coordinates start = new ( 0 , 0 ) , end = rawUI . CursorPosition ;
203
+ end . X = rawUI . BufferSize . Width - 1 ;
204
+
205
+ BufferCell [ , ] content = rawUI . GetBufferContents ( new Rectangle ( start , end ) ) ;
206
+ StringBuilder line = new ( ) , buffer = new ( ) ;
207
+
208
+ int rows = content . GetLength ( 0 ) ;
209
+ int columns = content . GetLength ( 1 ) ;
210
+
211
+ for ( int row = 0 ; row < rows ; row ++ )
212
+ {
213
+ line . Clear ( ) ;
214
+ for ( int column = 0 ; column < columns ; column ++ )
215
+ {
216
+ line . Append ( content [ row , column ] . Character ) ;
217
+ }
218
+
219
+ line . TrimEnd ( ) ;
220
+ buffer . Append ( line ) . Append ( '\n ' ) ;
221
+ }
222
+
223
+ return buffer . Length is 0 ? string . Empty : buffer . ToString ( ) ;
224
+ }
225
+ catch
226
+ {
227
+ return null ;
228
+ }
229
+ }
230
+
130
231
internal void PostQuery ( PostQueryMessage message )
131
232
{
132
233
ThrowIfNotConnected ( ) ;
@@ -138,6 +239,8 @@ public void Dispose()
138
239
Reset ( ) ;
139
240
_connSetupWaitHandler . Dispose ( ) ;
140
241
_predictor . Unregister ( ) ;
242
+ _runspace . AvailabilityChanged -= RunspaceAvailableAction ;
243
+ _intrinsics . InvokeCommand . LocationChangedAction -= LocationChangedAction ;
141
244
GC . SuppressFinalize ( this ) ;
142
245
}
143
246
@@ -257,8 +360,76 @@ private void OnPostCode(PostCodeMessage postCodeMessage)
257
360
258
361
private PostContextMessage OnAskContext ( AskContextMessage askContextMessage )
259
362
{
260
- // Not implemented yet.
261
- return null ;
363
+ const string RedactedValue = "***<sensitive data redacted>***" ;
364
+
365
+ ContextType type = askContextMessage . ContextType ;
366
+ string [ ] arguments = askContextMessage . Arguments ;
367
+
368
+ string contextInfo ;
369
+ switch ( type )
370
+ {
371
+ case ContextType . CurrentLocation :
372
+ contextInfo = JsonSerializer . Serialize (
373
+ new { Provider = _currentLocation . Provider . Name , _currentLocation . Path } ) ;
374
+ break ;
375
+
376
+ case ContextType . CommandHistory :
377
+ lock ( _commandHistory )
378
+ {
379
+ contextInfo = JsonSerializer . Serialize (
380
+ _commandHistory . Select ( o => new { o . Id , o . CommandLine } ) ) ;
381
+ }
382
+ break ;
383
+
384
+ case ContextType . TerminalContent :
385
+ contextInfo = CaptureScreen ( ) ;
386
+ break ;
387
+
388
+ case ContextType . EnvironmentVariables :
389
+ if ( arguments is { Length : > 0 } )
390
+ {
391
+ var varsCopy = new Dictionary < string , string > ( ) ;
392
+ foreach ( string name in arguments )
393
+ {
394
+ if ( ! string . IsNullOrEmpty ( name ) )
395
+ {
396
+ varsCopy . Add ( name , Environment . GetEnvironmentVariable ( name ) is string value
397
+ ? EnvVarMayBeSensitive ( name ) ? RedactedValue : value
398
+ : $ "[env variable '{ arguments } ' is undefined]") ;
399
+ }
400
+ }
401
+
402
+ contextInfo = varsCopy . Count > 0
403
+ ? JsonSerializer . Serialize ( varsCopy )
404
+ : "The specified environment variable names are invalid" ;
405
+ }
406
+ else
407
+ {
408
+ var vars = Environment . GetEnvironmentVariables ( ) ;
409
+ var varsCopy = new Dictionary < string , string > ( ) ;
410
+
411
+ foreach ( string key in vars . Keys )
412
+ {
413
+ varsCopy . Add ( key , EnvVarMayBeSensitive ( key ) ? RedactedValue : ( string ) vars [ key ] ) ;
414
+ }
415
+
416
+ contextInfo = JsonSerializer . Serialize ( varsCopy ) ;
417
+ }
418
+ break ;
419
+
420
+ default :
421
+ throw new InvalidDataException ( $ "Unknown context type '{ type } '") ;
422
+ }
423
+
424
+ return new PostContextMessage ( contextInfo ) ;
425
+
426
+ static bool EnvVarMayBeSensitive ( string key )
427
+ {
428
+ return key . Contains ( "key" , StringComparison . OrdinalIgnoreCase ) ||
429
+ key . Contains ( "token" , StringComparison . OrdinalIgnoreCase ) ||
430
+ key . Contains ( "pass" , StringComparison . OrdinalIgnoreCase ) ||
431
+ key . Contains ( "secret" , StringComparison . OrdinalIgnoreCase ) ;
432
+ }
262
433
}
263
434
264
435
private void OnAskConnection ( ShellClientPipe clientPipe , Exception exception )
@@ -334,3 +505,39 @@ public void Dispose()
334
505
}
335
506
336
507
internal record CodePostData ( string CodeToInsert , List < PredictionCandidate > PredictionCandidates ) ;
508
+
509
+ internal static class ExtensionMethods
510
+ {
511
+ internal static Collection < T > InvokeAndCleanup < T > ( this PowerShell ps )
512
+ {
513
+ var results = ps . Invoke < T > ( ) ;
514
+ ps . Commands . Clear ( ) ;
515
+
516
+ return results ;
517
+ }
518
+
519
+ internal static void InvokeAndCleanup ( this PowerShell ps )
520
+ {
521
+ ps . Invoke ( ) ;
522
+ ps . Commands . Clear ( ) ;
523
+ }
524
+
525
+ internal static void TrimEnd ( this StringBuilder sb )
526
+ {
527
+ // end will point to the first non-trimmed character on the right.
528
+ int end = sb . Length - 1 ;
529
+ for ( ; end >= 0 ; end -- )
530
+ {
531
+ if ( ! char . IsWhiteSpace ( sb [ end ] ) )
532
+ {
533
+ break ;
534
+ }
535
+ }
536
+
537
+ int index = end + 1 ;
538
+ if ( index < sb . Length )
539
+ {
540
+ sb . Remove ( index , sb . Length - index ) ;
541
+ }
542
+ }
543
+ }
0 commit comments