1
1
import makeStyles from "@mui/styles/makeStyles"
2
2
import { watchAgentMetadata } from "api/api"
3
+ import Popover from "@mui/material/Popover"
3
4
import { WorkspaceAgent , WorkspaceAgentMetadata } from "api/typesGenerated"
4
5
import { Stack } from "components/Stack/Stack"
5
6
import dayjs from "dayjs"
6
7
import {
7
8
createContext ,
8
9
FC ,
10
+ Ref ,
9
11
useContext ,
10
12
useEffect ,
11
13
useRef ,
@@ -16,11 +18,94 @@ import { MONOSPACE_FONT_FAMILY } from "theme/constants"
16
18
import { combineClasses } from "utils/combineClasses"
17
19
import Tooltip from "@mui/material/Tooltip"
18
20
import Box , { BoxProps } from "@mui/material/Box"
21
+ import * as XTerm from "xterm"
22
+ import { FitAddon } from "xterm-addon-fit"
23
+ import { WebLinksAddon } from "xterm-addon-web-links"
24
+ import { colors } from "theme/colors"
19
25
20
26
type ItemStatus = "stale" | "valid" | "loading"
21
27
22
28
export const WatchAgentMetadataContext = createContext ( watchAgentMetadata )
23
29
30
+ const MetadataTerminalPopover : FC < {
31
+ id : string
32
+ value : string
33
+ } > = ( { id, value : value } ) => {
34
+ const styles = useStyles ( )
35
+
36
+ const popRef = useRef < HTMLDivElement > ( null )
37
+ const [ open , setOpen ] = useState ( false )
38
+
39
+ const xtermRef = useRef < HTMLDivElement > ( null )
40
+ const [ terminal , setTerminal ] = useState < XTerm . Terminal | null > ( null )
41
+ const [ fitAddon , setFitAddon ] = useState < FitAddon | null > ( null )
42
+
43
+ // Create the terminal.
44
+ // Largely taken from TerminalPage.
45
+ useEffect ( ( ) => {
46
+ if ( ! xtermRef . current ) {
47
+ return
48
+ }
49
+ const terminal = new XTerm . Terminal ( {
50
+ allowTransparency : true ,
51
+ disableStdin : true ,
52
+ fontFamily : MONOSPACE_FONT_FAMILY ,
53
+ fontSize : 16 ,
54
+ theme : {
55
+ background : colors . gray [ 16 ] ,
56
+ } ,
57
+ } )
58
+
59
+ const fitAddon = new FitAddon ( )
60
+ setFitAddon ( fitAddon )
61
+ setTerminal ( terminal )
62
+ terminal . open ( xtermRef . current )
63
+ terminal . write ( value )
64
+ const listener = ( ) => {
65
+ // This will trigger a resize event on the terminal.
66
+ fitAddon . fit ( )
67
+ }
68
+ window . addEventListener ( "resize" , listener )
69
+ return ( ) => {
70
+ window . removeEventListener ( "resize" , listener )
71
+ terminal . dispose ( )
72
+ }
73
+ } , [ xtermRef ] )
74
+
75
+ return (
76
+ < >
77
+ < div
78
+ className = { styles . viewTerminal }
79
+ ref = { popRef }
80
+ onMouseOver = { ( ) => {
81
+ setOpen ( true )
82
+ } }
83
+ >
84
+ View Terminal
85
+ </ div >
86
+
87
+ < Popover
88
+ id = { id }
89
+ open = { open }
90
+ onClose = { ( ) => setOpen ( false ) }
91
+ anchorEl = { popRef . current }
92
+ anchorOrigin = { {
93
+ vertical : "bottom" ,
94
+ horizontal : "left" ,
95
+ } }
96
+ >
97
+ < Box p = { 1 } >
98
+ < div
99
+ className = { styles . terminal }
100
+ ref = { xtermRef }
101
+ data-testid = "terminal"
102
+ />
103
+ </ Box >
104
+ </ Popover >
105
+ </ >
106
+ )
107
+ }
108
+
24
109
const MetadataItem : FC < { item : WorkspaceAgentMetadata } > = ( { item } ) => {
25
110
const styles = useStyles ( )
26
111
@@ -31,6 +116,13 @@ const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => {
31
116
throw new Error ( "Metadata item description is undefined" )
32
117
}
33
118
119
+ const terminalPrefix = "terminal:"
120
+ const isTerminal = item . description . display_name . startsWith ( terminalPrefix )
121
+
122
+ const displayName = isTerminal
123
+ ? item . description . display_name . slice ( terminalPrefix . length )
124
+ : item . description . display_name
125
+
34
126
const staleThreshold = Math . max (
35
127
item . description . interval + item . description . timeout * 2 ,
36
128
5 ,
@@ -87,10 +179,15 @@ const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => {
87
179
88
180
return (
89
181
< div className = { styles . metadata } >
90
- < div className = { styles . metadataLabel } >
91
- { item . description . display_name }
92
- </ div >
93
- < Box > { value } </ Box >
182
+ < div className = { styles . metadataLabel } > { displayName } </ div >
183
+ { isTerminal ? (
184
+ < MetadataTerminalPopover
185
+ id = { `metadata-terminal-${ item . description . key } ` }
186
+ value = { item . result . value }
187
+ />
188
+ ) : (
189
+ < Box > { value } </ Box >
190
+ ) }
94
191
</ div >
95
192
)
96
193
}
@@ -104,6 +201,7 @@ export const AgentMetadataView: FC<AgentMetadataViewProps> = ({ metadata }) => {
104
201
if ( metadata . length === 0 ) {
105
202
return < > </ >
106
203
}
204
+
107
205
return (
108
206
< div className = { styles . root } >
109
207
< Stack alignItems = "baseline" direction = "row" spacing = { 6 } >
@@ -227,6 +325,53 @@ const useStyles = makeStyles((theme) => ({
227
325
scrollPadding : theme . spacing ( 0 , 4 ) ,
228
326
} ,
229
327
328
+ viewTerminal : {
329
+ fontFamily : MONOSPACE_FONT_FAMILY ,
330
+ display : "inline-block" ,
331
+ textDecoration : "underline" ,
332
+ fontWeight : 600 ,
333
+ margin : 0 ,
334
+ fontSize : 14 ,
335
+ borderRadius : 4 ,
336
+ color : theme . palette . text . primary ,
337
+ } ,
338
+
339
+ terminal : {
340
+ width : "100vw" ,
341
+ height : "30vh" ,
342
+ overflow : "hidden" ,
343
+ backgroundColor : theme . palette . background . paper ,
344
+ flex : 1 ,
345
+ // These styles attempt to mimic the VS Code scrollbar.
346
+ "& .xterm" : {
347
+ padding : 4 ,
348
+ width : "100vw" ,
349
+ height : "100vh" ,
350
+ } ,
351
+ "& .xterm-viewport" : {
352
+ // This is required to force full-width on the terminal.
353
+ // Otherwise there's a small white bar to the right of the scrollbar.
354
+ width : "auto !important" ,
355
+ } ,
356
+ "& .xterm-viewport::-webkit-scrollbar" : {
357
+ width : "10px" ,
358
+ } ,
359
+ "& .xterm-viewport::-webkit-scrollbar-track" : {
360
+ backgroundColor : "inherit" ,
361
+ } ,
362
+ "& .xterm-viewport::-webkit-scrollbar-thumb" : {
363
+ minHeight : 20 ,
364
+ backgroundColor : "rgba(255, 255, 255, 0.18)" ,
365
+ } ,
366
+ } ,
367
+
368
+ popover : {
369
+ padding : 0 ,
370
+ width : theme . spacing ( 38 ) ,
371
+ color : theme . palette . text . secondary ,
372
+ marginTop : theme . spacing ( 0.5 ) ,
373
+ } ,
374
+
230
375
metadata : {
231
376
fontSize : 12 ,
232
377
lineHeight : "normal" ,
0 commit comments