Skip to content

Commit 713acff

Browse files
committed
feat: support agent metadata terminals
1 parent 6214117 commit 713acff

File tree

6 files changed

+256
-9
lines changed

6 files changed

+256
-9
lines changed

examples/templates/jfrog/docker/main.tf

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ resource "coder_agent" "main" {
9898
index-url = https://${local.artifactory_username}:${artifactory_scoped_token.me.access_token}@${var.jfrog_host}/artifactory/api/pypi/${local.artifactory_repository_keys["python"]}/simple
9999
EOF
100100
101+
# project is a small Go project for jf audit to analyze.
102+
cd ~
103+
git clone https://github.com/jfrog/jfrog-cli
101104
EOT
102105
# Set GOPROXY to use the Artifactory "go" repository.
103106
env = {
@@ -109,6 +112,26 @@ resource "coder_agent" "main" {
109112
JFROG_IDE_ACCESS_TOKEN : "${artifactory_scoped_token.me.access_token}"
110113
JFROG_IDE_STORE_CONNECTION : "true"
111114
}
115+
116+
metadata {
117+
key = "cpu"
118+
display_name = "CPU"
119+
script = "coder stat cpu"
120+
timeout = 1
121+
interval = 1
122+
}
123+
124+
metadata {
125+
key = "jfrog"
126+
display_name = "terminal:JFrog Audit"
127+
script = <<-EOT
128+
export CI=true
129+
cd jfrog-cli
130+
script -qec 'jf audit 2>/dev/null' /dev/null
131+
EOT
132+
timeout = 300
133+
interval = 300
134+
}
112135
}
113136

114137
resource "coder_app" "code-server" {

site/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@
103103
"xterm": "5.2.1",
104104
"xterm-addon-canvas": "0.4.0",
105105
"xterm-addon-fit": "0.7.0",
106+
"xterm-addon-unicode11": "0.5.0",
106107
"xterm-addon-web-links": "0.8.0",
108+
"xterm-addon-webgl": "0.15.0",
107109
"yup": "1.2.0"
108110
},
109111
"devDependencies": {

site/pnpm-lock.yaml

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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: "\x1b[32mHello, World!\x1b[0m",
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: 185 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,142 @@
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"
15
import makeStyles from "@mui/styles/makeStyles"
26
import { watchAgentMetadata } from "api/api"
3-
import { WorkspaceAgent, WorkspaceAgentMetadata } from "api/typesGenerated"
7+
import {
8+
WorkspaceAgent,
9+
WorkspaceAgentMetadata,
10+
WorkspaceAgentMetadataResult,
11+
} from "api/typesGenerated"
412
import { Stack } from "components/Stack/Stack"
513
import dayjs from "dayjs"
614
import {
7-
createContext,
815
FC,
16+
createContext,
917
useContext,
1018
useEffect,
1119
useRef,
1220
useState,
1321
} from "react"
14-
import Skeleton from "@mui/material/Skeleton"
22+
import { colors } from "theme/colors"
1523
import { MONOSPACE_FONT_FAMILY } from "theme/constants"
1624
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"
1931

2032
type ItemStatus = "stale" | "valid" | "loading"
2133

2234
export const WatchAgentMetadataContext = createContext(watchAgentMetadata)
2335

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+
const resizeInterval = setInterval(() => {
93+
window.dispatchEvent(new Event("resize"))
94+
}, 100)
95+
96+
return () => {
97+
clearInterval(resizeInterval)
98+
terminal.dispose()
99+
}
100+
}, [xtermRef, open])
101+
102+
useEffect(() => {
103+
writeTerminal()
104+
}, [xtermRef, open, result])
105+
106+
return (
107+
<>
108+
<div
109+
className={styles.viewTerminal}
110+
ref={viewTermRef}
111+
onMouseOver={() => {
112+
setOpen(true)
113+
}}
114+
>
115+
View Terminal
116+
</div>
117+
118+
<Popover
119+
id={id}
120+
open={open}
121+
onClose={() => setOpen(false)}
122+
anchorEl={viewTermRef.current}
123+
anchorOrigin={{
124+
vertical: "bottom",
125+
horizontal: "left",
126+
}}
127+
>
128+
<div
129+
className={styles.terminal}
130+
ref={(el) => {
131+
setXtermRef(el)
132+
}}
133+
data-testid="terminal"
134+
/>
135+
</Popover>
136+
</>
137+
)
138+
}
139+
24140
const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => {
25141
const styles = useStyles()
26142

@@ -31,6 +147,13 @@ const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => {
31147
throw new Error("Metadata item description is undefined")
32148
}
33149

150+
const terminalPrefix = "terminal:"
151+
const isTerminal = item.description.display_name.startsWith(terminalPrefix)
152+
153+
const displayName = isTerminal
154+
? item.description.display_name.slice(terminalPrefix.length)
155+
: item.description.display_name
156+
34157
const staleThreshold = Math.max(
35158
item.description.interval + item.description.timeout * 2,
36159
// In case there is intense backpressure, we give a little bit of slack.
@@ -88,10 +211,15 @@ const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => {
88211

89212
return (
90213
<div className={styles.metadata}>
91-
<div className={styles.metadataLabel}>
92-
{item.description.display_name}
93-
</div>
94-
<Box>{value}</Box>
214+
<div className={styles.metadataLabel}>{displayName}</div>
215+
{isTerminal ? (
216+
<MetadataTerminalPopover
217+
id={`metadata-terminal-${item.description.key}`}
218+
result={item.result}
219+
/>
220+
) : (
221+
<Box>{value}</Box>
222+
)}
95223
</div>
96224
)
97225
}
@@ -105,6 +233,7 @@ export const AgentMetadataView: FC<AgentMetadataViewProps> = ({ metadata }) => {
105233
if (metadata.length === 0) {
106234
return <></>
107235
}
236+
108237
return (
109238
<div className={styles.root}>
110239
<Stack alignItems="baseline" direction="row" spacing={6}>
@@ -228,6 +357,53 @@ const useStyles = makeStyles((theme) => ({
228357
scrollPadding: theme.spacing(0, 4),
229358
},
230359

360+
viewTerminal: {
361+
fontFamily: MONOSPACE_FONT_FAMILY,
362+
display: "inline-block",
363+
textDecoration: "underline",
364+
fontWeight: 600,
365+
margin: 0,
366+
fontSize: 14,
367+
borderRadius: 4,
368+
color: theme.palette.text.primary,
369+
},
370+
371+
terminal: {
372+
width: "80ch",
373+
overflow: "auto",
374+
backgroundColor: theme.palette.background.paper,
375+
// flex: 1,
376+
padding: theme.spacing(1),
377+
// These styles attempt to mimic the VS Code scrollbar.
378+
"& .xterm": {
379+
padding: 4,
380+
width: "100vw",
381+
height: "40vh",
382+
},
383+
"& .xterm-viewport": {
384+
// This is required to force full-width on the terminal.
385+
// Otherwise there's a small white bar to the right of the scrollbar.
386+
width: "auto !important",
387+
},
388+
"& .xterm-viewport::-webkit-scrollbar": {
389+
width: "10px",
390+
},
391+
"& .xterm-viewport::-webkit-scrollbar-track": {
392+
backgroundColor: "inherit",
393+
},
394+
"& .xterm-viewport::-webkit-scrollbar-thumb": {
395+
minHeight: 20,
396+
backgroundColor: "rgba(255, 255, 255, 0.18)",
397+
},
398+
},
399+
400+
popover: {
401+
padding: 0,
402+
width: theme.spacing(38),
403+
color: theme.palette.text.secondary,
404+
marginTop: theme.spacing(0.5),
405+
},
406+
231407
metadata: {
232408
fontSize: 12,
233409
lineHeight: "normal",

site/src/pages/TerminalPage/TerminalPage.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { v4 as uuidv4 } from "uuid"
99
import * as XTerm from "xterm"
1010
import { CanvasAddon } from "xterm-addon-canvas"
1111
import { FitAddon } from "xterm-addon-fit"
12+
import { Unicode11Addon } from "xterm-addon-unicode11"
1213
import { WebLinksAddon } from "xterm-addon-web-links"
1314
import "xterm/css/xterm.css"
1415
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
@@ -176,6 +177,7 @@ const TerminalPage: FC<TerminalPageProps> = ({ renderer }) => {
176177
return
177178
}
178179
const terminal = new XTerm.Terminal({
180+
allowProposedApi: true,
179181
allowTransparency: true,
180182
disableStdin: false,
181183
fontFamily: MONOSPACE_FONT_FAMILY,
@@ -191,6 +193,10 @@ const TerminalPage: FC<TerminalPageProps> = ({ renderer }) => {
191193
const fitAddon = new FitAddon()
192194
setFitAddon(fitAddon)
193195
terminal.loadAddon(fitAddon)
196+
// This addon fixes multi-width codepoint rendering such as
197+
// 🔵.
198+
terminal.loadAddon(new Unicode11Addon())
199+
terminal.unicode.activeVersion = "11"
194200
terminal.loadAddon(
195201
new WebLinksAddon((_, uri) => {
196202
handleWebLink(uri)

0 commit comments

Comments
 (0)