Skip to content

feat: support agent metadata terminals #9159

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions examples/templates/jfrog/docker/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ resource "coder_agent" "main" {
index-url = https://${local.artifactory_username}:${artifactory_scoped_token.me.access_token}@${var.jfrog_host}/artifactory/api/pypi/${local.artifactory_repository_keys["python"]}/simple
EOF

# project is a small Go project for jf audit to analyze.
cd ~
git clone https://github.com/jfrog/jfrog-cli
EOT
# Set GOPROXY to use the Artifactory "go" repository.
env = {
Expand All @@ -109,6 +112,26 @@ resource "coder_agent" "main" {
JFROG_IDE_ACCESS_TOKEN : "${artifactory_scoped_token.me.access_token}"
JFROG_IDE_STORE_CONNECTION : "true"
}

metadata {
key = "cpu"
display_name = "CPU"
script = "coder stat cpu"
timeout = 1
interval = 1
}

metadata {
key = "jfrog"
display_name = "terminal:JFrog Audit"
script = <<-EOT
export CI=true
cd jfrog-cli
script -qec 'jf audit 2>/dev/null' /dev/null
EOT
timeout = 300
interval = 300
}
}

resource "coder_app" "code-server" {
Expand Down
2 changes: 2 additions & 0 deletions site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@
"xterm": "5.2.1",
"xterm-addon-canvas": "0.4.0",
"xterm-addon-fit": "0.7.0",
"xterm-addon-unicode11": "0.5.0",
"xterm-addon-web-links": "0.8.0",
"xterm-addon-webgl": "0.15.0",
"yup": "1.2.0"
},
"devDependencies": {
Expand Down
22 changes: 22 additions & 0 deletions site/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions site/src/components/Resources/AgentMetadata.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,21 @@ Example.args = {
},
],
}

export const Terminal = Template.bind({})
Terminal.args = {
metadata: [
{
result: {
...resultDefaults,
value: "\x1b[32mHello, World!\x1b[0m",
},
description: {
...descriptionDefaults,
display_name: "terminal:hello world",
key: "term",
script: "echo hello world",
},
},
],
}
194 changes: 185 additions & 9 deletions site/src/components/Resources/AgentMetadata.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,142 @@
import Box, { BoxProps } from "@mui/material/Box"
import Popover from "@mui/material/Popover"
import Skeleton from "@mui/material/Skeleton"
import Tooltip from "@mui/material/Tooltip"
import makeStyles from "@mui/styles/makeStyles"
import { watchAgentMetadata } from "api/api"
import { WorkspaceAgent, WorkspaceAgentMetadata } from "api/typesGenerated"
import {
WorkspaceAgent,
WorkspaceAgentMetadata,
WorkspaceAgentMetadataResult,
} from "api/typesGenerated"
import { Stack } from "components/Stack/Stack"
import dayjs from "dayjs"
import {
createContext,
FC,
createContext,
useContext,
useEffect,
useRef,
useState,
} from "react"
import Skeleton from "@mui/material/Skeleton"
import { colors } from "theme/colors"
import { MONOSPACE_FONT_FAMILY } from "theme/constants"
import { combineClasses } from "utils/combineClasses"
import Tooltip from "@mui/material/Tooltip"
import Box, { BoxProps } from "@mui/material/Box"
import * as XTerm from "xterm"
import { FitAddon } from "xterm-addon-fit"
import { WebglAddon } from "xterm-addon-webgl"
import { Unicode11Addon } from "xterm-addon-unicode11"

import "xterm/css/xterm.css"

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

export const WatchAgentMetadataContext = createContext(watchAgentMetadata)

const MetadataTerminalPopover: FC<{
id: string
result: WorkspaceAgentMetadataResult
}> = ({ id, result }) => {
const styles = useStyles()

const viewTermRef = useRef<HTMLDivElement>(null)
const [open, setOpen] = useState(false)

const [xtermRef, setXtermRef] = useState<HTMLDivElement | null>(null)
const [terminal, setTerminal] = useState<XTerm.Terminal | null>(null)
const [fitAddon, setFitAddon] = useState<FitAddon | null>(null)

const writeTerminal = () => {
if (!terminal || !fitAddon) {
return
}

// We write the clearCode with the new value to avoid a flash of blankness
// when the result value updates.
const clearCode = "\x1B[2J\x1B[H"
terminal.write(clearCode + result.value, () => {
fitAddon.fit()
})
}

// Create the terminal.
// Largely taken from TerminalPage.
useEffect(() => {
if (!xtermRef) {
return
}
const terminal = new XTerm.Terminal({
allowTransparency: true,
allowProposedApi: true,
disableStdin: true,
fontFamily: MONOSPACE_FONT_FAMILY,
fontSize: 16,
theme: {
background: colors.gray[16],
},
})
terminal.loadAddon(new WebglAddon())
terminal.loadAddon(new FitAddon())

// This addon fixes multi-width codepoint rendering such as
// 🟢.
terminal.loadAddon(new Unicode11Addon())
terminal.unicode.activeVersion = "11"

const fitAddon = new FitAddon()
setTerminal(terminal)
setFitAddon(fitAddon)
terminal.open(xtermRef)
writeTerminal()

const resizeInterval = setInterval(() => {
window.dispatchEvent(new Event("resize"))
}, 100)

return () => {
clearInterval(resizeInterval)
terminal.dispose()
}
}, [xtermRef, open])

useEffect(() => {
writeTerminal()
}, [xtermRef, open, result])

return (
<>
<div
className={styles.viewTerminal}
ref={viewTermRef}
onMouseOver={() => {
setOpen(true)
}}
>
View Terminal
</div>

<Popover
id={id}
open={open}
onClose={() => setOpen(false)}
anchorEl={viewTermRef.current}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
>
<div
className={styles.terminal}
ref={(el) => {
setXtermRef(el)
}}
data-testid="terminal"
/>
</Popover>
</>
)
}

const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => {
const styles = useStyles()

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

const terminalPrefix = "terminal:"
const isTerminal = item.description.display_name.startsWith(terminalPrefix)

const displayName = isTerminal
? item.description.display_name.slice(terminalPrefix.length)
: item.description.display_name

const staleThreshold = Math.max(
item.description.interval + item.description.timeout * 2,
// In case there is intense backpressure, we give a little bit of slack.
Expand Down Expand Up @@ -88,10 +211,15 @@ const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => {

return (
<div className={styles.metadata}>
<div className={styles.metadataLabel}>
{item.description.display_name}
</div>
<Box>{value}</Box>
<div className={styles.metadataLabel}>{displayName}</div>
{isTerminal ? (
<MetadataTerminalPopover
id={`metadata-terminal-${item.description.key}`}
result={item.result}
/>
) : (
<Box>{value}</Box>
)}
</div>
)
}
Expand All @@ -105,6 +233,7 @@ export const AgentMetadataView: FC<AgentMetadataViewProps> = ({ metadata }) => {
if (metadata.length === 0) {
return <></>
}

return (
<div className={styles.root}>
<Stack alignItems="baseline" direction="row" spacing={6}>
Expand Down Expand Up @@ -228,6 +357,53 @@ const useStyles = makeStyles((theme) => ({
scrollPadding: theme.spacing(0, 4),
},

viewTerminal: {
fontFamily: MONOSPACE_FONT_FAMILY,
display: "inline-block",
textDecoration: "underline",
fontWeight: 600,
margin: 0,
fontSize: 14,
borderRadius: 4,
color: theme.palette.text.primary,
},

terminal: {
width: "80ch",
overflow: "auto",
backgroundColor: theme.palette.background.paper,
// flex: 1,
padding: theme.spacing(1),
// These styles attempt to mimic the VS Code scrollbar.
"& .xterm": {
padding: 4,
width: "100vw",
height: "40vh",
},
"& .xterm-viewport": {
// This is required to force full-width on the terminal.
// Otherwise there's a small white bar to the right of the scrollbar.
width: "auto !important",
},
"& .xterm-viewport::-webkit-scrollbar": {
width: "10px",
},
"& .xterm-viewport::-webkit-scrollbar-track": {
backgroundColor: "inherit",
},
"& .xterm-viewport::-webkit-scrollbar-thumb": {
minHeight: 20,
backgroundColor: "rgba(255, 255, 255, 0.18)",
},
},

popover: {
padding: 0,
width: theme.spacing(38),
color: theme.palette.text.secondary,
marginTop: theme.spacing(0.5),
},

metadata: {
fontSize: 12,
lineHeight: "normal",
Expand Down
6 changes: 6 additions & 0 deletions site/src/pages/TerminalPage/TerminalPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { v4 as uuidv4 } from "uuid"
import * as XTerm from "xterm"
import { CanvasAddon } from "xterm-addon-canvas"
import { FitAddon } from "xterm-addon-fit"
import { Unicode11Addon } from "xterm-addon-unicode11"
import { WebLinksAddon } from "xterm-addon-web-links"
import "xterm/css/xterm.css"
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
Expand Down Expand Up @@ -176,6 +177,7 @@ const TerminalPage: FC<TerminalPageProps> = ({ renderer }) => {
return
}
const terminal = new XTerm.Terminal({
allowProposedApi: true,
allowTransparency: true,
disableStdin: false,
fontFamily: MONOSPACE_FONT_FAMILY,
Expand All @@ -191,6 +193,10 @@ const TerminalPage: FC<TerminalPageProps> = ({ renderer }) => {
const fitAddon = new FitAddon()
setFitAddon(fitAddon)
terminal.loadAddon(fitAddon)
// This addon fixes multi-width codepoint rendering such as
// 🔵.
terminal.loadAddon(new Unicode11Addon())
terminal.unicode.activeVersion = "11"
terminal.loadAddon(
new WebLinksAddon((_, uri) => {
handleWebLink(uri)
Expand Down