Compare commits
5 Commits
kyle/go126
...
update-gho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2932c99ea | ||
|
|
0485cafc52 | ||
|
|
100dae81c3 | ||
|
|
80b9d0e5b7 | ||
|
|
f5b4777453 |
@@ -72,7 +72,8 @@ func CSPHeaders(telemetry bool, proxyHosts func() []*proxyhealth.ProxyHost, stat
|
||||
CSPDirectiveConnectSrc: {"'self'"},
|
||||
CSPDirectiveChildSrc: {"'self'"},
|
||||
// https://github.com/suren-atoyan/monaco-react/issues/168
|
||||
CSPDirectiveScriptSrc: {"'self'"},
|
||||
// 'wasm-unsafe-eval' allows WebAssembly instantiation (required for ghostty-web terminal)
|
||||
CSPDirectiveScriptSrc: {"'self' 'wasm-unsafe-eval'"},
|
||||
CSPDirectiveStyleSrc: {"'self' 'unsafe-inline'"},
|
||||
// data: is used by monaco editor on FE for Syntax Highlight
|
||||
CSPDirectiveFontSrc: {"'self' data:"},
|
||||
|
||||
@@ -3582,6 +3582,7 @@ const (
|
||||
ExperimentMCPServerHTTP Experiment = "mcp-server-http" // Enables the MCP HTTP server functionality.
|
||||
ExperimentWorkspaceSharing Experiment = "workspace-sharing" // Enables updating workspace ACLs for sharing with users and groups.
|
||||
ExperimentAIBridge Experiment = "aibridge" // Enables AI Bridge functionality.
|
||||
ExperimentGhosttyWeb Experiment = "ghostty-web" // Enables Ghostty web terminal emulator.
|
||||
)
|
||||
|
||||
func (e Experiment) DisplayName() string {
|
||||
@@ -3604,6 +3605,8 @@ func (e Experiment) DisplayName() string {
|
||||
return "Workspace Sharing"
|
||||
case ExperimentAIBridge:
|
||||
return "AI Bridge"
|
||||
case ExperimentGhosttyWeb:
|
||||
return "Ghostty Web Terminal"
|
||||
default:
|
||||
// Split on hyphen and convert to title case
|
||||
// e.g. "web-push" -> "Web Push", "mcp-server-http" -> "Mcp Server Http"
|
||||
@@ -3623,13 +3626,16 @@ var ExperimentsKnown = Experiments{
|
||||
ExperimentMCPServerHTTP,
|
||||
ExperimentWorkspaceSharing,
|
||||
ExperimentAIBridge,
|
||||
ExperimentGhosttyWeb,
|
||||
}
|
||||
|
||||
// ExperimentsSafe should include all experiments that are safe for
|
||||
// users to opt-in to via --experimental='*'.
|
||||
// Experiments that are not ready for consumption by all users should
|
||||
// not be included here and will be essentially hidden.
|
||||
var ExperimentsSafe = Experiments{}
|
||||
var ExperimentsSafe = Experiments{
|
||||
ExperimentGhosttyWeb,
|
||||
}
|
||||
|
||||
// Experiments is a list of experiments.
|
||||
// Multiple experiments may be enabled at the same time.
|
||||
|
||||
@@ -40,7 +40,8 @@ test("web terminal", async ({ context, page }) => {
|
||||
const agent = await startAgent(page, token);
|
||||
const terminal = await openTerminalWindow(page, context, workspaceName);
|
||||
|
||||
await terminal.waitForSelector("div.xterm-rows", {
|
||||
// Wait for either xterm.js or ghostty-web terminal to be ready
|
||||
await terminal.waitForSelector("div.xterm-rows, canvas", {
|
||||
state: "visible",
|
||||
});
|
||||
|
||||
@@ -55,8 +56,10 @@ test("web terminal", async ({ context, page }) => {
|
||||
// Check if "echo" command was executed
|
||||
// try-catch is used temporarily to find the root cause: https://github.com/coder/coder/actions/runs/6176958762/job/16767089943
|
||||
try {
|
||||
// ghostty-web renders to canvas, so text might not be in DOM
|
||||
// Check the terminal container's textContent or xterm-rows for xterm.js
|
||||
await terminal.waitForSelector(
|
||||
'div.xterm-rows span:text-matches("hello123456")',
|
||||
'div.xterm-rows span:text-matches("hello123456"), [data-testid="terminal"]:has-text("hello123456")',
|
||||
{
|
||||
state: "visible",
|
||||
timeout: 10 * 1000,
|
||||
|
||||
@@ -209,8 +209,9 @@ test.skip("create docker workspace", async ({ context, page }) => {
|
||||
workspaceName,
|
||||
"main",
|
||||
);
|
||||
// Both xterm.js and ghostty-web create a textarea for clipboard
|
||||
await terminal.waitForSelector(
|
||||
`//textarea[contains(@class,"xterm-helper-textarea")]`,
|
||||
`//textarea[contains(@class,"xterm-helper-textarea") or @aria-label="Terminal input"]`,
|
||||
{ state: "visible" },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,220 +1,222 @@
|
||||
{
|
||||
"name": "coder-v2",
|
||||
"description": "Coder V2 (Workspaces V2)",
|
||||
"repository": "https://github.com/coder/coder",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748",
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production pnpm vite build",
|
||||
"check": "biome check --error-on-warnings .",
|
||||
"check:fix": "biome check --error-on-warnings --fix .",
|
||||
"check:all": "pnpm check && pnpm test",
|
||||
"chromatic": "chromatic",
|
||||
"dev": "vite",
|
||||
"format": "biome format --write .",
|
||||
"format:check": "biome format .",
|
||||
"lint": "pnpm run lint:check && pnpm run lint:types && pnpm run lint:circular-deps && knip",
|
||||
"lint:check": "biome lint --error-on-warnings .",
|
||||
"lint:circular-deps": "dpdm --no-tree --no-warning -T ./src/App.tsx",
|
||||
"lint:knip": "knip",
|
||||
"lint:fix": "biome lint --error-on-warnings --write . && knip --fix",
|
||||
"lint:types": "tsc -p .",
|
||||
"playwright:install": "playwright install --with-deps chromium",
|
||||
"playwright:test": "playwright test --config=e2e/playwright.config.ts",
|
||||
"playwright:test-ui": "playwright test --config=e2e/playwright.config.ts --ui $([[ \"$CODER\" == \"true\" ]] && echo --ui-port=7500 --ui-host=0.0.0.0)",
|
||||
"gen:provisioner": "protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./e2e/ --ts_proto_opt=outputJsonMethods=false,outputEncodeMethods=encode-no-creation,outputClientImpl=false,nestJs=false,outputPartialMethods=false,fileSuffix=Generated,suffix=hey -I ../provisionersdk/proto ../provisionersdk/proto/provisioner.proto",
|
||||
"storybook": "STORYBOOK=true storybook dev -p 6006",
|
||||
"storybook:build": "storybook build",
|
||||
"storybook:ci": "storybook build --test",
|
||||
"test": "jest",
|
||||
"test:ci": "jest --selectProjects test --silent",
|
||||
"test:coverage": "jest --selectProjects test --collectCoverage",
|
||||
"test:watch": "jest --selectProjects test --watch",
|
||||
"stats": "STATS=true pnpm build && npx http-server ./stats -p 8081 -c-1",
|
||||
"update-emojis": "cp -rf ./node_modules/emoji-datasource-apple/img/apple/64/* ./static/emojis"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emoji-mart/data": "1.2.1",
|
||||
"@emoji-mart/react": "1.1.1",
|
||||
"@emotion/cache": "11.14.0",
|
||||
"@emotion/css": "11.13.5",
|
||||
"@emotion/react": "11.14.0",
|
||||
"@emotion/styled": "11.14.1",
|
||||
"@fontsource-variable/inter": "5.1.1",
|
||||
"@fontsource/fira-code": "5.2.7",
|
||||
"@fontsource/ibm-plex-mono": "5.2.7",
|
||||
"@fontsource/jetbrains-mono": "5.2.5",
|
||||
"@fontsource/source-code-pro": "5.2.5",
|
||||
"@monaco-editor/react": "4.7.0",
|
||||
"@mui/material": "5.18.0",
|
||||
"@mui/system": "5.18.0",
|
||||
"@mui/utils": "5.17.1",
|
||||
"@mui/x-tree-view": "7.29.10",
|
||||
"@radix-ui/react-avatar": "1.1.2",
|
||||
"@radix-ui/react-checkbox": "1.1.4",
|
||||
"@radix-ui/react-collapsible": "1.1.2",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.4",
|
||||
"@radix-ui/react-label": "2.1.0",
|
||||
"@radix-ui/react-popover": "1.1.5",
|
||||
"@radix-ui/react-radio-group": "1.2.3",
|
||||
"@radix-ui/react-scroll-area": "1.2.3",
|
||||
"@radix-ui/react-select": "2.2.6",
|
||||
"@radix-ui/react-separator": "1.1.7",
|
||||
"@radix-ui/react-slider": "1.2.2",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-switch": "1.1.1",
|
||||
"@radix-ui/react-tooltip": "1.1.7",
|
||||
"@tanstack/react-query-devtools": "5.77.0",
|
||||
"@xterm/addon-canvas": "0.7.0",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"@xterm/addon-unicode11": "0.8.0",
|
||||
"@xterm/addon-web-links": "0.11.0",
|
||||
"@xterm/addon-webgl": "0.18.0",
|
||||
"@xterm/xterm": "5.5.0",
|
||||
"ansi-to-html": "0.7.2",
|
||||
"axios": "1.12.0",
|
||||
"chroma-js": "2.6.0",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
"color-convert": "2.0.1",
|
||||
"cron-parser": "4.9.0",
|
||||
"cronstrue": "2.50.0",
|
||||
"dayjs": "1.11.18",
|
||||
"emoji-mart": "5.6.0",
|
||||
"file-saver": "2.0.5",
|
||||
"formik": "2.4.6",
|
||||
"front-matter": "4.0.2",
|
||||
"humanize-duration": "3.32.2",
|
||||
"jszip": "3.10.1",
|
||||
"lodash": "4.17.21",
|
||||
"lucide-react": "0.545.0",
|
||||
"monaco-editor": "0.53.0",
|
||||
"pretty-bytes": "6.1.1",
|
||||
"react": "19.1.1",
|
||||
"react-color": "2.19.3",
|
||||
"react-confetti": "6.4.0",
|
||||
"react-date-range": "1.4.0",
|
||||
"react-dom": "19.1.1",
|
||||
"react-markdown": "9.1.0",
|
||||
"react-query": "npm:@tanstack/react-query@5.77.0",
|
||||
"react-resizable-panels": "3.0.6",
|
||||
"react-router": "7.8.0",
|
||||
"react-syntax-highlighter": "15.6.1",
|
||||
"react-textarea-autosize": "8.5.9",
|
||||
"react-virtualized-auto-sizer": "1.0.26",
|
||||
"react-window": "1.8.11",
|
||||
"recharts": "2.15.0",
|
||||
"remark-gfm": "4.0.1",
|
||||
"resize-observer-polyfill": "1.5.1",
|
||||
"semver": "7.7.2",
|
||||
"tailwind-merge": "2.6.0",
|
||||
"tailwindcss-animate": "1.0.7",
|
||||
"tzdata": "1.0.46",
|
||||
"ua-parser-js": "1.0.41",
|
||||
"ufuzzy": "npm:@leeoniya/ufuzzy@1.0.10",
|
||||
"undici": "6.21.3",
|
||||
"unique-names-generator": "4.7.1",
|
||||
"uuid": "9.0.1",
|
||||
"websocket-ts": "2.2.1",
|
||||
"yup": "1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.4",
|
||||
"@chromatic-com/storybook": "4.1.0",
|
||||
"@octokit/types": "12.3.0",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@storybook/addon-docs": "9.1.2",
|
||||
"@storybook/addon-links": "9.1.2",
|
||||
"@storybook/addon-themes": "9.1.2",
|
||||
"@storybook/react-vite": "9.1.2",
|
||||
"@swc/core": "1.3.38",
|
||||
"@swc/jest": "0.2.37",
|
||||
"@tailwindcss/typography": "0.5.16",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "14.3.1",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/chroma-js": "2.4.0",
|
||||
"@types/color-convert": "2.0.4",
|
||||
"@types/express": "4.17.17",
|
||||
"@types/file-saver": "2.0.7",
|
||||
"@types/humanize-duration": "3.27.4",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "20.17.16",
|
||||
"@types/react": "19.1.17",
|
||||
"@types/react-color": "3.0.13",
|
||||
"@types/react-date-range": "1.4.4",
|
||||
"@types/react-dom": "19.1.11",
|
||||
"@types/react-syntax-highlighter": "15.5.13",
|
||||
"@types/react-virtualized-auto-sizer": "1.0.8",
|
||||
"@types/react-window": "1.8.8",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/ssh2": "1.15.5",
|
||||
"@types/ua-parser-js": "0.7.36",
|
||||
"@types/uuid": "9.0.2",
|
||||
"@vitejs/plugin-react": "5.0.4",
|
||||
"autoprefixer": "10.4.21",
|
||||
"chromatic": "11.29.0",
|
||||
"dpdm": "3.14.0",
|
||||
"express": "4.21.2",
|
||||
"jest": "29.7.0",
|
||||
"jest-canvas-mock": "2.5.2",
|
||||
"jest-environment-jsdom": "29.5.0",
|
||||
"jest-fixed-jsdom": "0.0.10",
|
||||
"jest-location-mock": "2.0.0",
|
||||
"jest-websocket-mock": "2.5.0",
|
||||
"jest_workaround": "0.1.14",
|
||||
"knip": "5.64.1",
|
||||
"msw": "2.4.8",
|
||||
"postcss": "8.5.6",
|
||||
"protobufjs": "7.4.0",
|
||||
"rollup-plugin-visualizer": "5.14.0",
|
||||
"rxjs": "7.8.1",
|
||||
"ssh2": "1.17.0",
|
||||
"storybook": "9.1.2",
|
||||
"storybook-addon-remix-react-router": "5.0.0",
|
||||
"tailwindcss": "3.4.18",
|
||||
"ts-proto": "1.181.2",
|
||||
"typescript": "5.6.3",
|
||||
"vite": "7.1.11",
|
||||
"vite-plugin-checker": "0.11.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"chrome 110",
|
||||
"firefox 111",
|
||||
"safari 16.0"
|
||||
],
|
||||
"resolutions": {
|
||||
"optionator": "0.9.3",
|
||||
"semver": "7.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": ">=10.0.0 <11.0.0",
|
||||
"node": ">=18.0.0 <23.0.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@babel/runtime": "7.26.10",
|
||||
"@babel/helpers": "7.26.10",
|
||||
"esbuild": "^0.25.0",
|
||||
"form-data": "4.0.4",
|
||||
"prismjs": "1.30.0",
|
||||
"dompurify": "3.2.6",
|
||||
"brace-expansion": "1.1.12"
|
||||
},
|
||||
"ignoredBuiltDependencies": [
|
||||
"cpu-features",
|
||||
"msw",
|
||||
"protobufjs",
|
||||
"storybook-addon-remix-react-router"
|
||||
],
|
||||
"onlyBuiltDependencies": [
|
||||
"@swc/core",
|
||||
"esbuild",
|
||||
"ssh2"
|
||||
]
|
||||
}
|
||||
"name": "coder-v2",
|
||||
"description": "Coder V2 (Workspaces V2)",
|
||||
"repository": "https://github.com/coder/coder",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748",
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production pnpm vite build",
|
||||
"check": "biome check --error-on-warnings .",
|
||||
"check:fix": "biome check --error-on-warnings --fix .",
|
||||
"check:all": "pnpm check && pnpm test",
|
||||
"chromatic": "chromatic",
|
||||
"dev": "vite",
|
||||
"format": "biome format --write .",
|
||||
"format:check": "biome format .",
|
||||
"lint": "pnpm run lint:check && pnpm run lint:types && pnpm run lint:circular-deps && knip",
|
||||
"lint:check": "biome lint --error-on-warnings .",
|
||||
"lint:circular-deps": "dpdm --no-tree --no-warning -T ./src/App.tsx",
|
||||
"lint:knip": "knip",
|
||||
"lint:fix": "biome lint --error-on-warnings --write . && knip --fix",
|
||||
"lint:types": "tsc -p .",
|
||||
"playwright:install": "playwright install --with-deps chromium",
|
||||
"playwright:test": "playwright test --config=e2e/playwright.config.ts",
|
||||
"playwright:test-ui": "playwright test --config=e2e/playwright.config.ts --ui $([[ \"$CODER\" == \"true\" ]] && echo --ui-port=7500 --ui-host=0.0.0.0)",
|
||||
"gen:provisioner": "protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./e2e/ --ts_proto_opt=outputJsonMethods=false,outputEncodeMethods=encode-no-creation,outputClientImpl=false,nestJs=false,outputPartialMethods=false,fileSuffix=Generated,suffix=hey -I ../provisionersdk/proto ../provisionersdk/proto/provisioner.proto",
|
||||
"storybook": "STORYBOOK=true storybook dev -p 6006",
|
||||
"storybook:build": "storybook build",
|
||||
"storybook:ci": "storybook build --test",
|
||||
"test": "jest",
|
||||
"test:ci": "jest --selectProjects test --silent",
|
||||
"test:coverage": "jest --selectProjects test --collectCoverage",
|
||||
"test:watch": "jest --selectProjects test --watch",
|
||||
"stats": "STATS=true pnpm build && npx http-server ./stats -p 8081 -c-1",
|
||||
"update-emojis": "cp -rf ./node_modules/emoji-datasource-apple/img/apple/64/* ./static/emojis",
|
||||
"postinstall": "cp node_modules/ghostty-web/ghostty-vt.wasm static/ 2>/dev/null || true"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emoji-mart/data": "1.2.1",
|
||||
"@emoji-mart/react": "1.1.1",
|
||||
"@emotion/cache": "11.14.0",
|
||||
"@emotion/css": "11.13.5",
|
||||
"@emotion/react": "11.14.0",
|
||||
"@emotion/styled": "11.14.1",
|
||||
"@fontsource-variable/inter": "5.1.1",
|
||||
"@fontsource/fira-code": "5.2.7",
|
||||
"@fontsource/ibm-plex-mono": "5.2.7",
|
||||
"@fontsource/jetbrains-mono": "5.2.5",
|
||||
"@fontsource/source-code-pro": "5.2.5",
|
||||
"@monaco-editor/react": "4.7.0",
|
||||
"@mui/material": "5.18.0",
|
||||
"@mui/system": "5.18.0",
|
||||
"@mui/utils": "5.17.1",
|
||||
"@mui/x-tree-view": "7.29.10",
|
||||
"@radix-ui/react-avatar": "1.1.2",
|
||||
"@radix-ui/react-checkbox": "1.1.4",
|
||||
"@radix-ui/react-collapsible": "1.1.2",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.4",
|
||||
"@radix-ui/react-label": "2.1.0",
|
||||
"@radix-ui/react-popover": "1.1.5",
|
||||
"@radix-ui/react-radio-group": "1.2.3",
|
||||
"@radix-ui/react-scroll-area": "1.2.3",
|
||||
"@radix-ui/react-select": "2.2.6",
|
||||
"@radix-ui/react-separator": "1.1.7",
|
||||
"@radix-ui/react-slider": "1.2.2",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-switch": "1.1.1",
|
||||
"@radix-ui/react-tooltip": "1.1.7",
|
||||
"@tanstack/react-query-devtools": "5.77.0",
|
||||
"@xterm/addon-canvas": "0.7.0",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"@xterm/addon-unicode11": "0.8.0",
|
||||
"@xterm/addon-web-links": "0.11.0",
|
||||
"@xterm/addon-webgl": "0.18.0",
|
||||
"@xterm/xterm": "5.5.0",
|
||||
"ansi-to-html": "0.7.2",
|
||||
"ghostty-web": "0.2.1-next.4.g1680deb",
|
||||
"axios": "1.12.0",
|
||||
"chroma-js": "2.6.0",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
"color-convert": "2.0.1",
|
||||
"cron-parser": "4.9.0",
|
||||
"cronstrue": "2.50.0",
|
||||
"dayjs": "1.11.18",
|
||||
"emoji-mart": "5.6.0",
|
||||
"file-saver": "2.0.5",
|
||||
"formik": "2.4.6",
|
||||
"front-matter": "4.0.2",
|
||||
"humanize-duration": "3.32.2",
|
||||
"jszip": "3.10.1",
|
||||
"lodash": "4.17.21",
|
||||
"lucide-react": "0.545.0",
|
||||
"monaco-editor": "0.53.0",
|
||||
"pretty-bytes": "6.1.1",
|
||||
"react": "19.1.1",
|
||||
"react-color": "2.19.3",
|
||||
"react-confetti": "6.4.0",
|
||||
"react-date-range": "1.4.0",
|
||||
"react-dom": "19.1.1",
|
||||
"react-markdown": "9.1.0",
|
||||
"react-query": "npm:@tanstack/react-query@5.77.0",
|
||||
"react-resizable-panels": "3.0.6",
|
||||
"react-router": "7.8.0",
|
||||
"react-syntax-highlighter": "15.6.1",
|
||||
"react-textarea-autosize": "8.5.9",
|
||||
"react-virtualized-auto-sizer": "1.0.26",
|
||||
"react-window": "1.8.11",
|
||||
"recharts": "2.15.0",
|
||||
"remark-gfm": "4.0.1",
|
||||
"resize-observer-polyfill": "1.5.1",
|
||||
"semver": "7.7.2",
|
||||
"tailwind-merge": "2.6.0",
|
||||
"tailwindcss-animate": "1.0.7",
|
||||
"tzdata": "1.0.46",
|
||||
"ua-parser-js": "1.0.41",
|
||||
"ufuzzy": "npm:@leeoniya/ufuzzy@1.0.10",
|
||||
"undici": "6.21.3",
|
||||
"unique-names-generator": "4.7.1",
|
||||
"uuid": "9.0.1",
|
||||
"websocket-ts": "2.2.1",
|
||||
"yup": "1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.4",
|
||||
"@chromatic-com/storybook": "4.1.0",
|
||||
"@octokit/types": "12.3.0",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@storybook/addon-docs": "9.1.2",
|
||||
"@storybook/addon-links": "9.1.2",
|
||||
"@storybook/addon-themes": "9.1.2",
|
||||
"@storybook/react-vite": "9.1.2",
|
||||
"@swc/core": "1.3.38",
|
||||
"@swc/jest": "0.2.37",
|
||||
"@tailwindcss/typography": "0.5.16",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "14.3.1",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/chroma-js": "2.4.0",
|
||||
"@types/color-convert": "2.0.4",
|
||||
"@types/express": "4.17.17",
|
||||
"@types/file-saver": "2.0.7",
|
||||
"@types/humanize-duration": "3.27.4",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "20.17.16",
|
||||
"@types/react": "19.1.17",
|
||||
"@types/react-color": "3.0.13",
|
||||
"@types/react-date-range": "1.4.4",
|
||||
"@types/react-dom": "19.1.11",
|
||||
"@types/react-syntax-highlighter": "15.5.13",
|
||||
"@types/react-virtualized-auto-sizer": "1.0.8",
|
||||
"@types/react-window": "1.8.8",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/ssh2": "1.15.5",
|
||||
"@types/ua-parser-js": "0.7.36",
|
||||
"@types/uuid": "9.0.2",
|
||||
"@vitejs/plugin-react": "5.0.4",
|
||||
"autoprefixer": "10.4.21",
|
||||
"chromatic": "11.29.0",
|
||||
"dpdm": "3.14.0",
|
||||
"express": "4.21.2",
|
||||
"jest": "29.7.0",
|
||||
"jest-canvas-mock": "2.5.2",
|
||||
"jest-environment-jsdom": "29.5.0",
|
||||
"jest-fixed-jsdom": "0.0.10",
|
||||
"jest-location-mock": "2.0.0",
|
||||
"jest-websocket-mock": "2.5.0",
|
||||
"jest_workaround": "0.1.14",
|
||||
"knip": "5.64.1",
|
||||
"msw": "2.4.8",
|
||||
"postcss": "8.5.6",
|
||||
"protobufjs": "7.4.0",
|
||||
"rollup-plugin-visualizer": "5.14.0",
|
||||
"rxjs": "7.8.1",
|
||||
"ssh2": "1.17.0",
|
||||
"storybook": "9.1.2",
|
||||
"storybook-addon-remix-react-router": "5.0.0",
|
||||
"tailwindcss": "3.4.18",
|
||||
"ts-proto": "1.181.2",
|
||||
"typescript": "5.6.3",
|
||||
"vite": "7.1.11",
|
||||
"vite-plugin-checker": "0.11.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"chrome 110",
|
||||
"firefox 111",
|
||||
"safari 16.0"
|
||||
],
|
||||
"resolutions": {
|
||||
"optionator": "0.9.3",
|
||||
"semver": "7.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": ">=10.0.0 <11.0.0",
|
||||
"node": ">=18.0.0 <23.0.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@babel/runtime": "7.26.10",
|
||||
"@babel/helpers": "7.26.10",
|
||||
"esbuild": "^0.25.0",
|
||||
"form-data": "4.0.4",
|
||||
"prismjs": "1.30.0",
|
||||
"dompurify": "3.2.6",
|
||||
"brace-expansion": "1.1.12"
|
||||
},
|
||||
"ignoredBuiltDependencies": [
|
||||
"cpu-features",
|
||||
"msw",
|
||||
"protobufjs",
|
||||
"storybook-addon-remix-react-router"
|
||||
],
|
||||
"onlyBuiltDependencies": [
|
||||
"@swc/core",
|
||||
"esbuild",
|
||||
"ssh2"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
9
site/pnpm-lock.yaml
generated
9
site/pnpm-lock.yaml
generated
@@ -175,6 +175,9 @@ importers:
|
||||
front-matter:
|
||||
specifier: 4.0.2
|
||||
version: 4.0.2
|
||||
ghostty-web:
|
||||
specifier: 0.2.1-next.4.g1680deb
|
||||
version: 0.2.1-next.4.g1680deb
|
||||
humanize-duration:
|
||||
specifier: 3.32.2
|
||||
version: 3.32.2
|
||||
@@ -4187,6 +4190,10 @@ packages:
|
||||
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==, tarball: https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
ghostty-web@0.2.1-next.4.g1680deb:
|
||||
resolution: {integrity: sha512-GPpYcmoaXLpTiKM0AMBHqXafGOcst4Y47LeKyB6EfU7LH+fWAqed+NSoaY7RWhMPsX86VRjnHn1WCIQQ92TUMw==, tarball: https://registry.npmjs.org/ghostty-web/-/ghostty-web-0.2.1-next.4.g1680deb.tgz}
|
||||
hasBin: true
|
||||
|
||||
glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, tarball: https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -10407,6 +10414,8 @@ snapshots:
|
||||
|
||||
get-stream@6.0.1: {}
|
||||
|
||||
ghostty-web@0.2.1-next.4.g1680deb: {}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
|
||||
2
site/src/api/typesGenerated.ts
generated
2
site/src/api/typesGenerated.ts
generated
@@ -1887,6 +1887,7 @@ export type Experiment =
|
||||
| "aibridge"
|
||||
| "auto-fill-parameters"
|
||||
| "example"
|
||||
| "ghostty-web"
|
||||
| "mcp-server-http"
|
||||
| "notifications"
|
||||
| "oauth2"
|
||||
@@ -1898,6 +1899,7 @@ export const Experiments: Experiment[] = [
|
||||
"aibridge",
|
||||
"auto-fill-parameters",
|
||||
"example",
|
||||
"ghostty-web",
|
||||
"mcp-server-http",
|
||||
"notifications",
|
||||
"oauth2",
|
||||
|
||||
@@ -36,15 +36,27 @@ const renderTerminal = async (
|
||||
const expectTerminalText = (container: HTMLElement, text: string) => {
|
||||
return waitFor(
|
||||
() => {
|
||||
const elements = container.getElementsByClassName("xterm-rows");
|
||||
if (elements.length === 0) {
|
||||
throw new Error("no xterm-rows");
|
||||
// Try xterm.js structure first
|
||||
const xtermRows = container.getElementsByClassName("xterm-rows");
|
||||
if (xtermRows.length > 0) {
|
||||
const row = xtermRows[0] as HTMLDivElement;
|
||||
if (!row.textContent) {
|
||||
throw new Error("no text content in xterm-rows");
|
||||
}
|
||||
expect(row.textContent).toContain(text);
|
||||
return;
|
||||
}
|
||||
const row = elements[0] as HTMLDivElement;
|
||||
if (!row.textContent) {
|
||||
throw new Error("no text content");
|
||||
|
||||
// Try ghostty-web structure (canvas + parent textContent)
|
||||
// Note: ghostty-web renders to canvas, but terminal messages
|
||||
// are still written as text to the parent for accessibility
|
||||
const terminalDiv = container.querySelector('[data-testid="terminal"]');
|
||||
if (terminalDiv?.textContent) {
|
||||
expect(terminalDiv.textContent).toContain(text);
|
||||
return;
|
||||
}
|
||||
expect(row.textContent).toContain(text);
|
||||
|
||||
throw new Error("no terminal element found");
|
||||
},
|
||||
{ timeout: 5_000 },
|
||||
);
|
||||
|
||||
@@ -8,6 +8,11 @@ import { WebglAddon } from "@xterm/addon-webgl";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import { deploymentConfig } from "api/queries/deployment";
|
||||
import { appearanceSettings } from "api/queries/users";
|
||||
import {
|
||||
Terminal as GhosttyTerminal,
|
||||
FitAddon as GhosttyFitAddon,
|
||||
} from "ghostty-web";
|
||||
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||
import {
|
||||
workspaceByOwnerAndName,
|
||||
workspaceUsage,
|
||||
@@ -42,6 +47,16 @@ export const Language = {
|
||||
websocketErrorMessagePrefix: "WebSocket failed: ",
|
||||
};
|
||||
|
||||
/**
|
||||
* TerminalPage provides a web-based terminal interface with automatic reconnection.
|
||||
*
|
||||
* The terminal implementation can be switched between xterm.js and ghostty-web via
|
||||
* the "ghostty-web" experiment. ghostty-web provides better performance and standards
|
||||
* compliance using Ghostty's VT100 parser via WebAssembly.
|
||||
*
|
||||
* When the experiment is disabled (default), xterm.js is used.
|
||||
* When enabled, ghostty-web is used (renderer config is ignored).
|
||||
*/
|
||||
const TerminalPage: FC = () => {
|
||||
// Maybe one day we'll support a light themed terminal, but terminal coloring
|
||||
// is notably a pain because of assumptions certain programs might make about your
|
||||
@@ -54,7 +69,7 @@ const TerminalPage: FC = () => {
|
||||
const terminalWrapperRef = useRef<HTMLDivElement>(null);
|
||||
// The terminal is maintained as a state to trigger certain effects when it
|
||||
// updates.
|
||||
const [terminal, setTerminal] = useState<Terminal>();
|
||||
const [terminal, setTerminal] = useState<Terminal | GhosttyTerminal>();
|
||||
const [connectionStatus, setConnectionStatus] =
|
||||
useState<ConnectionStatus>("initializing");
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -81,6 +96,9 @@ const TerminalPage: FC = () => {
|
||||
const config = useQuery(deploymentConfig());
|
||||
const renderer = config.data?.config.web_terminal_renderer;
|
||||
|
||||
const { experiments } = useDashboard();
|
||||
const useGhosttyWeb = experiments.includes("ghostty-web");
|
||||
|
||||
const { copyToClipboard } = useClipboard();
|
||||
|
||||
// Periodically report workspace usage.
|
||||
@@ -119,36 +137,60 @@ const TerminalPage: FC = () => {
|
||||
appearanceSettingsQuery.data?.terminal_font || DEFAULT_TERMINAL_FONT;
|
||||
|
||||
// Create the terminal!
|
||||
const fitAddonRef = useRef<FitAddon>(undefined);
|
||||
const fitAddonRef = useRef<FitAddon | GhosttyFitAddon>(undefined);
|
||||
useEffect(() => {
|
||||
if (!terminalWrapperRef.current || config.isLoading) {
|
||||
return;
|
||||
}
|
||||
const terminal = new Terminal({
|
||||
allowProposedApi: true,
|
||||
allowTransparency: true,
|
||||
disableStdin: false,
|
||||
|
||||
// Common terminal options shared by both xterm.js and ghostty-web
|
||||
const terminalOptions = {
|
||||
fontFamily: terminalFonts[currentTerminalFont],
|
||||
fontSize: 16,
|
||||
disableStdin: false,
|
||||
theme: {
|
||||
background: theme.palette.background.default,
|
||||
},
|
||||
});
|
||||
if (renderer === "webgl") {
|
||||
terminal.loadAddon(new WebglAddon());
|
||||
} else if (renderer === "canvas") {
|
||||
terminal.loadAddon(new CanvasAddon());
|
||||
}
|
||||
const fitAddon = new FitAddon();
|
||||
};
|
||||
|
||||
// Create terminal - ghostty-web and xterm.js have compatible APIs
|
||||
const terminal = useGhosttyWeb
|
||||
? new GhosttyTerminal({
|
||||
wasmPath: "/ghostty-vt.wasm",
|
||||
...terminalOptions,
|
||||
cursorBlink: true,
|
||||
})
|
||||
: new Terminal({
|
||||
...terminalOptions,
|
||||
allowProposedApi: true,
|
||||
allowTransparency: true,
|
||||
});
|
||||
|
||||
// Load FitAddon - both implementations are API-compatible
|
||||
const fitAddon = useGhosttyWeb ? new GhosttyFitAddon() : new FitAddon();
|
||||
fitAddonRef.current = fitAddon;
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.loadAddon(new Unicode11Addon());
|
||||
terminal.unicode.activeVersion = "11";
|
||||
terminal.loadAddon(
|
||||
new WebLinksAddon((_, uri) => {
|
||||
handleWebLinkRef.current(uri);
|
||||
}),
|
||||
);
|
||||
|
||||
// xterm.js-specific addons (ghostty-web has built-in equivalents)
|
||||
if (!useGhosttyWeb) {
|
||||
// Renderer addon
|
||||
if (renderer === "webgl") {
|
||||
terminal.loadAddon(new WebglAddon());
|
||||
} else if (renderer === "canvas") {
|
||||
terminal.loadAddon(new CanvasAddon());
|
||||
}
|
||||
|
||||
// Unicode support
|
||||
terminal.loadAddon(new Unicode11Addon());
|
||||
(terminal as Terminal).unicode.activeVersion = "11";
|
||||
|
||||
// Web links (ghostty-web has built-in OSC 8 link detection)
|
||||
terminal.loadAddon(
|
||||
new WebLinksAddon((_, uri) => {
|
||||
handleWebLinkRef.current(uri);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const isMac = navigator.platform.match("Mac");
|
||||
|
||||
@@ -159,6 +201,7 @@ const TerminalPage: FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Custom key event handler - works for both xterm.js and ghostty-web
|
||||
// There is no way to remove this handler, so we must attach it once and
|
||||
// rely on a ref to send it to the current socket.
|
||||
const escapedCarriageReturn = "\x1b\r";
|
||||
@@ -205,27 +248,46 @@ const TerminalPage: FC = () => {
|
||||
copySelection();
|
||||
});
|
||||
|
||||
// Open terminal - both xterm.js and ghostty-web have synchronous open()
|
||||
// For ghostty-web, WASM loading happens in constructor, and open() returns void.
|
||||
// The terminal becomes fully ready after onReady fires.
|
||||
terminal.open(terminalWrapperRef.current);
|
||||
|
||||
// We have to fit twice here. It's unknown why, but the first fit will
|
||||
// overflow slightly in some scenarios. Applying a second fit resolves this.
|
||||
fitAddon.fit();
|
||||
fitAddon.fit();
|
||||
// For ghostty-web, FitAddon subscribes to onReady internally and will fit when ready.
|
||||
fitAddonRef.current?.fit();
|
||||
fitAddonRef.current?.fit();
|
||||
|
||||
// This will trigger a resize event on the terminal.
|
||||
const listener = () => fitAddon.fit();
|
||||
window.addEventListener("resize", listener);
|
||||
const resizeListener = () => fitAddonRef.current?.fit();
|
||||
window.addEventListener("resize", resizeListener);
|
||||
|
||||
// Terminal is correctly sized and is ready to be used.
|
||||
// For ghostty-web, wait for onReady before exposing terminal to other effects.
|
||||
// The WASM loads asynchronously in the constructor, so methods like clear()
|
||||
// and write() won't work until onReady fires.
|
||||
if (useGhosttyWeb) {
|
||||
const readyDisposable = (terminal as GhosttyTerminal).onReady(() => {
|
||||
setTerminal(terminal);
|
||||
});
|
||||
return () => {
|
||||
readyDisposable.dispose();
|
||||
window.removeEventListener("resize", resizeListener);
|
||||
terminal.dispose();
|
||||
};
|
||||
}
|
||||
|
||||
// For xterm.js, terminal is ready immediately after open()
|
||||
setTerminal(terminal);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", listener);
|
||||
window.removeEventListener("resize", resizeListener);
|
||||
terminal.dispose();
|
||||
};
|
||||
}, [
|
||||
config.isLoading,
|
||||
renderer,
|
||||
useGhosttyWeb,
|
||||
theme.palette.background.default,
|
||||
currentTerminalFont,
|
||||
copyToClipboard,
|
||||
@@ -263,6 +325,7 @@ const TerminalPage: FC = () => {
|
||||
terminal.focus();
|
||||
|
||||
// Disable input while we connect.
|
||||
// Both xterm.js and ghostty-web support disableStdin option.
|
||||
terminal.options.disableStdin = true;
|
||||
|
||||
// Show a message if we failed to find the workspace or agent.
|
||||
@@ -332,10 +395,13 @@ const TerminalPage: FC = () => {
|
||||
websocketRef.current = websocket;
|
||||
websocket.addEventListener(WebsocketEvent.open, () => {
|
||||
// Now that we are connected, allow user input.
|
||||
terminal.options = {
|
||||
disableStdin: false,
|
||||
windowsMode: workspaceAgent?.operating_system === "windows",
|
||||
};
|
||||
// Both xterm.js and ghostty-web support disableStdin option.
|
||||
terminal.options.disableStdin = false;
|
||||
// windowsMode is xterm.js-specific, only set it for xterm.js
|
||||
if (terminal instanceof Terminal) {
|
||||
terminal.options.windowsMode =
|
||||
workspaceAgent?.operating_system === "windows";
|
||||
}
|
||||
// Send the initial size.
|
||||
websocket?.send(
|
||||
new TextEncoder().encode(
|
||||
@@ -349,10 +415,12 @@ const TerminalPage: FC = () => {
|
||||
});
|
||||
websocket.addEventListener(WebsocketEvent.error, (_, event) => {
|
||||
console.error("WebSocket error:", event);
|
||||
// Disable input on error - both terminals support this
|
||||
terminal.options.disableStdin = true;
|
||||
setConnectionStatus("disconnected");
|
||||
});
|
||||
websocket.addEventListener(WebsocketEvent.close, () => {
|
||||
// Disable input on close - both terminals support this
|
||||
terminal.options.disableStdin = true;
|
||||
setConnectionStatus("disconnected");
|
||||
});
|
||||
@@ -459,6 +527,8 @@ const styles = {
|
||||
overflow: "hidden",
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
flex: 1,
|
||||
|
||||
// xterm.js styles (when experiment disabled)
|
||||
// These styles attempt to mimic the VS Code scrollbar.
|
||||
"& .xterm": {
|
||||
padding: 4,
|
||||
@@ -480,6 +550,13 @@ const styles = {
|
||||
minHeight: 20,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.18)",
|
||||
},
|
||||
|
||||
// ghostty-web styles (when experiment enabled)
|
||||
// ghostty-web renders directly to canvas, no special classes needed
|
||||
"& canvas": {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
|
||||
BIN
site/static/ghostty-vt.wasm
Executable file
BIN
site/static/ghostty-vt.wasm
Executable file
Binary file not shown.
@@ -23,6 +23,7 @@ if (process.env.STATS !== undefined) {
|
||||
export default defineConfig({
|
||||
plugins,
|
||||
publicDir: path.resolve(__dirname, "./static"),
|
||||
assetsInclude: ["**/*.wasm"],
|
||||
build: {
|
||||
outDir: path.resolve(__dirname, "./out"),
|
||||
emptyOutDir: false, // We need to keep the /bin folder and GITKEEP files
|
||||
@@ -47,12 +48,16 @@ export default defineConfig({
|
||||
if (id.includes("@emotion")) return "emotion";
|
||||
if (id.includes("monaco-editor")) return "monaco";
|
||||
if (id.includes("@xterm")) return "xterm";
|
||||
if (id.includes("ghostty-web")) return "ghostty";
|
||||
if (id.includes("emoji-mart")) return "emoji-mart";
|
||||
if (id.includes("radix-ui")) return "radix-ui";
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ["ghostty-web"],
|
||||
},
|
||||
define: {
|
||||
"process.env": {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
|
||||
Reference in New Issue
Block a user