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 { WebglAddon } from "xterm-addon-webgl"
28
+ import { Unicode11Addon } from "xterm-addon-unicode11"
29
+
30
+ import "xterm/css/xterm.css"
19
31
20
32
type ItemStatus = "stale" | "valid" | "loading"
21
33
22
34
export const WatchAgentMetadataContext = createContext ( watchAgentMetadata )
23
35
36
+ const MetadataTerminalPopover : FC < {
37
+ id : string
38
+ result : WorkspaceAgentMetadataResult
39
+ } > = ( { id, result } ) => {
40
+ const styles = useStyles ( )
41
+
42
+ const viewTermRef = useRef < HTMLDivElement > ( null )
43
+ const [ open , setOpen ] = useState ( false )
44
+
45
+ const [ xtermRef , setXtermRef ] = useState < HTMLDivElement | null > ( null )
46
+ const [ terminal , setTerminal ] = useState < XTerm . Terminal | null > ( null )
47
+ const [ fitAddon , setFitAddon ] = useState < FitAddon | null > ( null )
48
+
49
+ const writeTerminal = ( ) => {
50
+ if ( ! terminal || ! fitAddon ) {
51
+ return
52
+ }
53
+
54
+ // We write the clearCode with the new value to avoid a flash of blankness
55
+ // when the result value updates.
56
+ const clearCode = "\x1B[2J\x1B[H"
57
+ terminal . write ( clearCode + result . value , ( ) => {
58
+ fitAddon . fit ( )
59
+ } )
60
+ }
61
+
62
+ // Create the terminal.
63
+ // Largely taken from TerminalPage.
64
+ useEffect ( ( ) => {
65
+ if ( ! xtermRef ) {
66
+ return
67
+ }
68
+ const terminal = new XTerm . Terminal ( {
69
+ allowTransparency : true ,
70
+ allowProposedApi : true ,
71
+ disableStdin : true ,
72
+ fontFamily : MONOSPACE_FONT_FAMILY ,
73
+ fontSize : 16 ,
74
+ theme : {
75
+ background : colors . gray [ 16 ] ,
76
+ } ,
77
+ } )
78
+ terminal . loadAddon ( new WebglAddon ( ) )
79
+ terminal . loadAddon ( new FitAddon ( ) )
80
+
81
+ // This addon fixes multi-width codepoint rendering such as
82
+ // 🟢.
83
+ terminal . loadAddon ( new Unicode11Addon ( ) )
84
+ terminal . unicode . activeVersion = "11"
85
+
86
+ const fitAddon = new FitAddon ( )
87
+ setTerminal ( terminal )
88
+ setFitAddon ( fitAddon )
89
+ terminal . open ( xtermRef )
90
+ writeTerminal ( )
91
+
92
+ // This listener doesn't appear to have an effect, but it's used
93
+ // in TerminalPage.tsx so I've copied it here.
94
+ const listener = ( ) => {
95
+ fitAddon . fit ( )
96
+ }
97
+ window . addEventListener ( "resize" , listener )
98
+
99
+ // TODO: this is obviously immensely janky. But, it's the only
100
+ // way I'm able to get the text to render correctly.
101
+ const resizeInterval = setInterval ( ( ) => {
102
+ window . dispatchEvent ( new Event ( "resize" ) )
103
+ } , 100 )
104
+
105
+ return ( ) => {
106
+ window . removeEventListener ( "resize" , listener )
107
+ clearInterval ( resizeInterval )
108
+ terminal . dispose ( )
109
+ }
110
+ } , [ xtermRef , open ] )
111
+
112
+ useEffect ( ( ) => {
113
+ writeTerminal ( )
114
+ } , [ xtermRef , open , result ] )
115
+
116
+ return (
117
+ < >
118
+ < div
119
+ className = { styles . viewTerminal }
120
+ ref = { viewTermRef }
121
+ onMouseOver = { ( ) => {
122
+ setOpen ( true )
123
+ } }
124
+ >
125
+ View Terminal
126
+ </ div >
127
+
128
+ < Popover
129
+ id = { id }
130
+ open = { open }
131
+ onClose = { ( ) => setOpen ( false ) }
132
+ anchorEl = { viewTermRef . current }
133
+ anchorOrigin = { {
134
+ vertical : "bottom" ,
135
+ horizontal : "left" ,
136
+ } }
137
+ >
138
+ < div
139
+ className = { styles . terminal }
140
+ ref = { ( el ) => {
141
+ setXtermRef ( el )
142
+ } }
143
+ data-testid = "terminal"
144
+ />
145
+ </ Popover >
146
+ </ >
147
+ )
148
+ }
149
+
24
150
const MetadataItem : FC < { item : WorkspaceAgentMetadata } > = ( { item } ) => {
25
151
const styles = useStyles ( )
26
152
@@ -31,6 +157,13 @@ const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => {
31
157
throw new Error ( "Metadata item description is undefined" )
32
158
}
33
159
160
+ const terminalPrefix = "terminal:"
161
+ const isTerminal = item . description . display_name . startsWith ( terminalPrefix )
162
+
163
+ const displayName = isTerminal
164
+ ? item . description . display_name . slice ( terminalPrefix . length )
165
+ : item . description . display_name
166
+
34
167
const staleThreshold = Math . max (
35
168
item . description . interval + item . description . timeout * 2 ,
36
169
// In case there is intense backpressure, we give a little bit of slack.
@@ -88,10 +221,15 @@ const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => {
88
221
89
222
return (
90
223
< div className = { styles . metadata } >
91
- < div className = { styles . metadataLabel } >
92
- { item . description . display_name }
93
- </ div >
94
- < Box > { value } </ Box >
224
+ < div className = { styles . metadataLabel } > { displayName } </ div >
225
+ { isTerminal ? (
226
+ < MetadataTerminalPopover
227
+ id = { `metadata-terminal-${ item . description . key } ` }
228
+ result = { item . result }
229
+ />
230
+ ) : (
231
+ < Box > { value } </ Box >
232
+ ) }
95
233
</ div >
96
234
)
97
235
}
@@ -105,6 +243,7 @@ export const AgentMetadataView: FC<AgentMetadataViewProps> = ({ metadata }) => {
105
243
if ( metadata . length === 0 ) {
106
244
return < > </ >
107
245
}
246
+
108
247
return (
109
248
< div className = { styles . root } >
110
249
< Stack alignItems = "baseline" direction = "row" spacing = { 6 } >
@@ -228,6 +367,54 @@ const useStyles = makeStyles((theme) => ({
228
367
scrollPadding : theme . spacing ( 0 , 4 ) ,
229
368
} ,
230
369
370
+ viewTerminal : {
371
+ fontFamily : MONOSPACE_FONT_FAMILY ,
372
+ display : "inline-block" ,
373
+ textDecoration : "underline" ,
374
+ fontWeight : 600 ,
375
+ margin : 0 ,
376
+ fontSize : 14 ,
377
+ borderRadius : 4 ,
378
+ color : theme . palette . text . primary ,
379
+ } ,
380
+
381
+ terminal : {
382
+ width : "80ch" ,
383
+ height : "30vh" ,
384
+ padding : theme . spacing ( 1 ) ,
385
+ // overflow: "hidden",
386
+ backgroundColor : theme . palette . background . paper ,
387
+ flex : 1 ,
388
+ // These styles attempt to mimic the VS Code scrollbar.
389
+ "& .xterm" : {
390
+ padding : 4 ,
391
+ width : "100vw" ,
392
+ height : "100vh" ,
393
+ } ,
394
+ "& .xterm-viewport" : {
395
+ // This is required to force full-width on the terminal.
396
+ // Otherwise there's a small white bar to the right of the scrollbar.
397
+ width : "auto !important" ,
398
+ } ,
399
+ "& .xterm-viewport::-webkit-scrollbar" : {
400
+ width : "10px" ,
401
+ } ,
402
+ "& .xterm-viewport::-webkit-scrollbar-track" : {
403
+ backgroundColor : "inherit" ,
404
+ } ,
405
+ "& .xterm-viewport::-webkit-scrollbar-thumb" : {
406
+ minHeight : 20 ,
407
+ backgroundColor : "rgba(255, 255, 255, 0.18)" ,
408
+ } ,
409
+ } ,
410
+
411
+ popover : {
412
+ padding : 0 ,
413
+ width : theme . spacing ( 38 ) ,
414
+ color : theme . palette . text . secondary ,
415
+ marginTop : theme . spacing ( 0.5 ) ,
416
+ } ,
417
+
231
418
metadata : {
232
419
fontSize : 12 ,
233
420
lineHeight : "normal" ,
0 commit comments