Skip to content

Commit 38e6a2e

Browse files
committed
feat: support agent metadata terminals
1 parent be40dc8 commit 38e6a2e

File tree

3 files changed

+175
-4
lines changed

3 files changed

+175
-4
lines changed

examples/templates/jfrog/docker/main.tf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@ resource "coder_agent" "main" {
109109
JFROG_IDE_ACCESS_TOKEN : "${artifactory_scoped_token.me.access_token}"
110110
JFROG_IDE_STORE_CONNECTION : "true"
111111
}
112+
113+
metadata {
114+
key = "cpu"
115+
display_name = "CPU"
116+
script = "coder stat cpu"
117+
timeout = 1
118+
interval = 1
119+
}
112120
}
113121

114122
resource "coder_app" "code-server" {

site/src/components/Resources/AgentMetadata.stories.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,21 @@ Example.args = {
105105
},
106106
],
107107
}
108+
109+
export const Terminal = Template.bind({})
110+
Terminal.args = {
111+
metadata: [
112+
{
113+
result: {
114+
...resultDefaults,
115+
value: "$ blah blah",
116+
},
117+
description: {
118+
...descriptionDefaults,
119+
display_name: "terminal:hello world",
120+
key: "term",
121+
script: "echo hello world",
122+
},
123+
},
124+
],
125+
}

site/src/components/Resources/AgentMetadata.tsx

Lines changed: 149 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import makeStyles from "@mui/styles/makeStyles"
22
import { watchAgentMetadata } from "api/api"
3+
import Popover from "@mui/material/Popover"
34
import { WorkspaceAgent, WorkspaceAgentMetadata } from "api/typesGenerated"
45
import { Stack } from "components/Stack/Stack"
56
import dayjs from "dayjs"
67
import {
78
createContext,
89
FC,
10+
Ref,
911
useContext,
1012
useEffect,
1113
useRef,
@@ -16,11 +18,94 @@ import { MONOSPACE_FONT_FAMILY } from "theme/constants"
1618
import { combineClasses } from "utils/combineClasses"
1719
import Tooltip from "@mui/material/Tooltip"
1820
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"
1925

2026
type ItemStatus = "stale" | "valid" | "loading"
2127

2228
export const WatchAgentMetadataContext = createContext(watchAgentMetadata)
2329

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+
24109
const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => {
25110
const styles = useStyles()
26111

@@ -31,6 +116,13 @@ const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => {
31116
throw new Error("Metadata item description is undefined")
32117
}
33118

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+
34126
const staleThreshold = Math.max(
35127
item.description.interval + item.description.timeout * 2,
36128
5,
@@ -87,10 +179,15 @@ const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => {
87179

88180
return (
89181
<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+
)}
94191
</div>
95192
)
96193
}
@@ -104,6 +201,7 @@ export const AgentMetadataView: FC<AgentMetadataViewProps> = ({ metadata }) => {
104201
if (metadata.length === 0) {
105202
return <></>
106203
}
204+
107205
return (
108206
<div className={styles.root}>
109207
<Stack alignItems="baseline" direction="row" spacing={6}>
@@ -227,6 +325,53 @@ const useStyles = makeStyles((theme) => ({
227325
scrollPadding: theme.spacing(0, 4),
228326
},
229327

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+
230375
metadata: {
231376
fontSize: 12,
232377
lineHeight: "normal",

0 commit comments

Comments
 (0)