1
+ import Box , { BoxProps } from "@mui/material/Box"
2
+ import Popover from "@mui/material/Popover"
3
+ import Skeleton from "@mui/material/Skeleton"
4
+ import Tooltip from "@mui/material/Tooltip"
1
5
import makeStyles from "@mui/styles/makeStyles"
2
6
import { watchAgentMetadata } from "api/api"
3
- import { WorkspaceAgent , WorkspaceAgentMetadata } from "api/typesGenerated"
7
+ import {
8
+ WorkspaceAgent ,
9
+ WorkspaceAgentMetadata ,
10
+ WorkspaceAgentMetadataResult ,
11
+ } from "api/typesGenerated"
4
12
import { Stack } from "components/Stack/Stack"
5
13
import dayjs from "dayjs"
6
14
import {
7
- createContext ,
8
15
FC ,
16
+ createContext ,
9
17
useContext ,
10
18
useEffect ,
11
19
useRef ,
12
20
useState ,
13
21
} from "react"
14
- import Skeleton from "@mui/material/Skeleton "
22
+ import { colors } from "theme/colors "
15
23
import { MONOSPACE_FONT_FAMILY } from "theme/constants"
16
24
import { combineClasses } from "utils/combineClasses"
17
- import Tooltip from "@mui/material/Tooltip"
18
- import Box , { BoxProps } from "@mui/material/Box"
25
+ import * as XTerm from "xterm"
26
+ import { FitAddon } from "xterm-addon-fit"
27
+ import "xterm/css/xterm.css"
19
28
20
29
type ItemStatus = "stale" | "valid" | "loading"
21
30
22
31
export const WatchAgentMetadataContext = createContext ( watchAgentMetadata )
23
32
33
+ const MetadataTerminalPopover : FC < {
34
+ id : string
35
+ result : WorkspaceAgentMetadataResult
36
+ } > = ( { id, result } ) => {
37
+ const styles = useStyles ( )
38
+
39
+ const viewTermRef = useRef < HTMLDivElement > ( null )
40
+ const [ open , setOpen ] = useState ( false )
41
+
42
+ const [ xtermRef , setXtermRef ] = useState < HTMLDivElement | null > ( null )
43
+ const [ terminal , setTerminal ] = useState < XTerm . Terminal | null > ( null )
44
+ const [ fitAddon , setFitAddon ] = useState < FitAddon | null > ( null )
45
+
46
+ // Create the terminal.
47
+ // Largely taken from TerminalPage.
48
+ useEffect ( ( ) => {
49
+ if ( ! xtermRef ) {
50
+ return
51
+ }
52
+ const terminal = new XTerm . Terminal ( {
53
+ allowTransparency : true ,
54
+ disableStdin : true ,
55
+ fontFamily : MONOSPACE_FONT_FAMILY ,
56
+ fontSize : 16 ,
57
+ theme : {
58
+ background : colors . gray [ 16 ] ,
59
+ } ,
60
+ } )
61
+ console . log ( "created terminal" , terminal )
62
+
63
+ const fitAddon = new FitAddon ( )
64
+ setTerminal ( terminal )
65
+ setFitAddon ( fitAddon )
66
+ fitAddon . fit ( )
67
+ terminal . open ( xtermRef )
68
+ terminal . write ( result . value )
69
+
70
+ return ( ) => {
71
+ terminal . dispose ( )
72
+ }
73
+ } , [ xtermRef , open ] )
74
+
75
+ useEffect ( ( ) => {
76
+ if ( ! xtermRef || ! open || ! terminal ) {
77
+ return
78
+ }
79
+
80
+ // We write the clearCode with the new value to avoid a flash of blankness
81
+ // when the result value updates.
82
+ const clearCode = "\x1B[2J\x1B[H"
83
+ terminal . write ( clearCode + result . value )
84
+ } , [ xtermRef , open , result ] )
85
+
86
+ return (
87
+ < >
88
+ < div
89
+ className = { styles . viewTerminal }
90
+ ref = { viewTermRef }
91
+ onMouseOver = { ( ) => {
92
+ setOpen ( true )
93
+ } }
94
+ >
95
+ View Terminal
96
+ </ div >
97
+
98
+ < Popover
99
+ id = { id }
100
+ open = { open }
101
+ onClose = { ( ) => setOpen ( false ) }
102
+ anchorEl = { viewTermRef . current }
103
+ anchorOrigin = { {
104
+ vertical : "bottom" ,
105
+ horizontal : "left" ,
106
+ } }
107
+ >
108
+ < Box p = { 1 } >
109
+ < div
110
+ className = { styles . terminal }
111
+ ref = { ( el ) => {
112
+ setXtermRef ( el )
113
+ } }
114
+ data-testid = "terminal"
115
+ />
116
+ </ Box >
117
+ </ Popover >
118
+ </ >
119
+ )
120
+ }
121
+
24
122
const MetadataItem : FC < { item : WorkspaceAgentMetadata } > = ( { item } ) => {
25
123
const styles = useStyles ( )
26
124
@@ -31,6 +129,13 @@ const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => {
31
129
throw new Error ( "Metadata item description is undefined" )
32
130
}
33
131
132
+ const terminalPrefix = "terminal:"
133
+ const isTerminal = item . description . display_name . startsWith ( terminalPrefix )
134
+
135
+ const displayName = isTerminal
136
+ ? item . description . display_name . slice ( terminalPrefix . length )
137
+ : item . description . display_name
138
+
34
139
const staleThreshold = Math . max (
35
140
item . description . interval + item . description . timeout * 2 ,
36
141
// In case there is intense backpressure, we give a little bit of slack.
@@ -88,10 +193,15 @@ const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => {
88
193
89
194
return (
90
195
< div className = { styles . metadata } >
91
- < div className = { styles . metadataLabel } >
92
- { item . description . display_name }
93
- </ div >
94
- < Box > { value } </ Box >
196
+ < div className = { styles . metadataLabel } > { displayName } </ div >
197
+ { isTerminal ? (
198
+ < MetadataTerminalPopover
199
+ id = { `metadata-terminal-${ item . description . key } ` }
200
+ result = { item . result }
201
+ />
202
+ ) : (
203
+ < Box > { value } </ Box >
204
+ ) }
95
205
</ div >
96
206
)
97
207
}
@@ -105,6 +215,7 @@ export const AgentMetadataView: FC<AgentMetadataViewProps> = ({ metadata }) => {
105
215
if ( metadata . length === 0 ) {
106
216
return < > </ >
107
217
}
218
+
108
219
return (
109
220
< div className = { styles . root } >
110
221
< Stack alignItems = "baseline" direction = "row" spacing = { 6 } >
@@ -228,6 +339,53 @@ const useStyles = makeStyles((theme) => ({
228
339
scrollPadding : theme . spacing ( 0 , 4 ) ,
229
340
} ,
230
341
342
+ viewTerminal : {
343
+ fontFamily : MONOSPACE_FONT_FAMILY ,
344
+ display : "inline-block" ,
345
+ textDecoration : "underline" ,
346
+ fontWeight : 600 ,
347
+ margin : 0 ,
348
+ fontSize : 14 ,
349
+ borderRadius : 4 ,
350
+ color : theme . palette . text . primary ,
351
+ } ,
352
+
353
+ terminal : {
354
+ width : "100vw" ,
355
+ height : "30vh" ,
356
+ overflow : "hidden" ,
357
+ backgroundColor : theme . palette . background . paper ,
358
+ flex : 1 ,
359
+ // These styles attempt to mimic the VS Code scrollbar.
360
+ "& .xterm" : {
361
+ padding : 4 ,
362
+ width : "100vw" ,
363
+ height : "100vh" ,
364
+ } ,
365
+ "& .xterm-viewport" : {
366
+ // This is required to force full-width on the terminal.
367
+ // Otherwise there's a small white bar to the right of the scrollbar.
368
+ width : "auto !important" ,
369
+ } ,
370
+ "& .xterm-viewport::-webkit-scrollbar" : {
371
+ width : "10px" ,
372
+ } ,
373
+ "& .xterm-viewport::-webkit-scrollbar-track" : {
374
+ backgroundColor : "inherit" ,
375
+ } ,
376
+ "& .xterm-viewport::-webkit-scrollbar-thumb" : {
377
+ minHeight : 20 ,
378
+ backgroundColor : "rgba(255, 255, 255, 0.18)" ,
379
+ } ,
380
+ } ,
381
+
382
+ popover : {
383
+ padding : 0 ,
384
+ width : theme . spacing ( 38 ) ,
385
+ color : theme . palette . text . secondary ,
386
+ marginTop : theme . spacing ( 0.5 ) ,
387
+ } ,
388
+
231
389
metadata : {
232
390
fontSize : 12 ,
233
391
lineHeight : "normal" ,
0 commit comments