diff --git a/web/package-lock.json b/web/package-lock.json index 9080f356..3e64f879 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,11 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@xterm/addon-fit": "^0.10.0", - "@xterm/addon-web-links": "^0.11.0", "@xterm/headless": "^5.5.0", - "@xterm/xterm": "^5.5.0", - "asciinema-player": "^3.7.0", "express": "^4.18.2", "lit": "^3.1.0", "node-pty": "^1.0.0", @@ -30,6 +26,7 @@ "@typescript-eslint/parser": "^8.34.1", "autoprefixer": "^10.4.21", "chokidar": "^3.5.3", + "chokidar-cli": "^3.0.0", "concurrently": "^8.2.2", "esbuild": "^0.25.5", "eslint": "^9.29.0", @@ -546,6 +543,7 @@ "version": "7.27.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2654,36 +2652,12 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@xterm/addon-fit": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", - "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", - "license": "MIT", - "peerDependencies": { - "@xterm/xterm": "^5.0.0" - } - }, - "node_modules/@xterm/addon-web-links": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.11.0.tgz", - "integrity": "sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q==", - "license": "MIT", - "peerDependencies": { - "@xterm/xterm": "^5.0.0" - } - }, "node_modules/@xterm/headless": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.5.0.tgz", "integrity": "sha512-5xXB7kdQlFBP82ViMJTwwEc3gKCLGKR/eoxQm4zge7GPBl86tCdI0IdPJjoKd8mUSFXz5V7i/25sfsEkP4j46g==", "license": "MIT" }, - "node_modules/@xterm/xterm": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", - "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -2834,16 +2808,6 @@ "dev": true, "license": "MIT" }, - "node_modules/asciinema-player": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.10.0.tgz", - "integrity": "sha512-shoOK6F606nDKZxDVM7JuGSCAyWLePoGRFNlV+FqiP5Sqvyn0BlE7wlbjZyd2X4P1iRhv/HKfVNtnQIxmgphRA==", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.21.0", - "solid-js": "^1.3.0" - } - }, "node_modules/ast-types": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", @@ -3407,6 +3371,240 @@ "fsevents": "~2.3.2" } }, + "node_modules/chokidar-cli": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chokidar-cli/-/chokidar-cli-3.0.0.tgz", + "integrity": "sha512-xVW+Qeh7z15uZRxHOkP93Ux8A0xbPzwK4GaqD8dQOYc34TlkqUhVSS59fK36DOp5WdJlrRzlYSy02Ht99FjZqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "lodash.debounce": "^4.0.8", + "lodash.throttle": "^4.1.1", + "yargs": "^13.3.0" + }, + "bin": { + "chokidar": "index.js" + }, + "engines": { + "node": ">= 8.10.0" + } + }, + "node_modules/chokidar-cli/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar-cli/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar-cli/node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/chokidar-cli/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/chokidar-cli/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar-cli/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar-cli/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar-cli/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar-cli/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar-cli/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chokidar-cli/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar-cli/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar-cli/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar-cli/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar-cli/node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar-cli/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/chokidar-cli/node_modules/yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "node_modules/chokidar-cli/node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, "node_modules/chokidar/node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -3865,12 +4063,6 @@ "node": ">=4" } }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" - }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -3907,6 +4099,16 @@ "ms": "2.0.0" } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dedent": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", @@ -6901,6 +7103,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -6915,6 +7124,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -8351,6 +8567,13 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true, + "license": "ISC" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -8598,27 +8821,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/seroval": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", - "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/seroval-plugins": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.2.tgz", - "integrity": "sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "seroval": "^1.0" - } - }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -8634,6 +8836,13 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -8881,17 +9090,6 @@ "dev": true, "license": "MIT" }, - "node_modules/solid-js": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.7.tgz", - "integrity": "sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw==", - "license": "MIT", - "dependencies": { - "csstype": "^3.1.0", - "seroval": "~1.3.0", - "seroval-plugins": "~1.3.0" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -9863,6 +10061,13 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true, + "license": "ISC" + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/web/package.json b/web/package.json index db93d917..a681e726 100644 --- a/web/package.json +++ b/web/package.json @@ -4,24 +4,19 @@ "description": "Web frontend for terminal multiplexer", "main": "dist/server.js", "scripts": { - "dev": "npm run build:css && concurrently --kill-others-on-fail \"npm run watch:css\" \"npm run bundle:watch\" \"npm run watch:server\"", + "dev": "npm run bundle:assets && npm run bundle:css && concurrently --kill-others-on-fail \"npm run watch:css\" \"npm run watch:assets\" \"npm run bundle:client -- --watch\" \"npm run bundle:test -- --watch\" \"npm run watch:server\"", "watch:server": "tsx watch src/server.ts", - "watch:client": "tsc -p tsconfig.client.json --watch --preserveWatchOutput", "watch:css": "npx tailwindcss -i ./src/input.css -o ./public/bundle/output.css --watch", - "build": "npm run build:css && npm run build:client && npm run build:server", - "build:server": "tsc", - "build:client": "tsc -p tsconfig.client.json", - "build:css": "npx tailwindcss -i ./src/input.css -o ./public/bundle/output.css --minify", - "bundle": "npm run bundle:client && npm run bundle:renderer && npm run bundle:test", - "bundle:client": "esbuild src/client/app-entry.ts --bundle --outfile=public/bundle/client-bundle.js --format=esm --sourcemap --external:@xterm/xterm/css/xterm.css", - "bundle:renderer": "esbuild src/client/renderer-entry.ts --bundle --outfile=public/bundle/renderer.js --format=esm --sourcemap", - "bundle:test": "esbuild src/client/test-terminals-entry.ts --bundle --outfile=public/bundle/terminal.js --format=esm --sourcemap", - "bundle:watch": "concurrently \"npm run bundle:client -- --watch\" \"npm run bundle:renderer -- --watch\" \"npm run bundle:test -- --watch\"", + "watch:assets": "chokidar 'src/client/assets/**/*' -c 'npm run bundle:assets'", + "clean": "rm -rf public/* && rm -rf dist/", + "build": "npm run bundle", + "typecheck": "tsc --noEmit", + "bundle": "npm run clean && npm run bundle:assets && npm run bundle:css && npm run bundle:client && npm run bundle:test", + "bundle:assets": "mkdir -p public && cp -r src/client/assets/* public/", + "bundle:css": "mkdir -p public/bundle && npx tailwindcss -i ./src/input.css -o ./public/bundle/output.css --minify", + "bundle:client": "mkdir -p public/bundle && esbuild src/client/app-entry.ts --bundle --outfile=public/bundle/client-bundle.js --format=esm --sourcemap", + "bundle:test": "mkdir -p public/bundle && esbuild src/client/test-terminals-entry.ts --bundle --outfile=public/bundle/terminal.js --format=esm --sourcemap", "start": "node dist/server.js", - "test": "jest", - "test:watch": "jest --watch", - "test:mobile-terminal": "jest src/client/components/__tests__/mobile-terminal.test.ts", - "test:components": "jest src/client/components", "lint": "eslint 'src/**/*.{ts,tsx}'", "lint:fix": "eslint 'src/**/*.{ts,tsx}' --fix", "format": "prettier --write 'src/**/*.{ts,tsx,js,jsx,json,css,md}'", @@ -29,11 +24,7 @@ "pre-commit": "./scripts/pre-commit-check.sh" }, "dependencies": { - "@xterm/addon-fit": "^0.10.0", - "@xterm/addon-web-links": "^0.11.0", "@xterm/headless": "^5.5.0", - "@xterm/xterm": "^5.5.0", - "asciinema-player": "^3.7.0", "express": "^4.18.2", "lit": "^3.1.0", "node-pty": "^1.0.0", @@ -50,6 +41,7 @@ "@typescript-eslint/parser": "^8.34.1", "autoprefixer": "^10.4.21", "chokidar": "^3.5.3", + "chokidar-cli": "^3.0.0", "concurrently": "^8.2.2", "esbuild": "^0.25.5", "eslint": "^9.29.0", diff --git a/web/src/client/app.ts b/web/src/client/app.ts index 00ed26f2..02cbeb53 100644 --- a/web/src/client/app.ts +++ b/web/src/client/app.ts @@ -10,6 +10,7 @@ import './components/session-view.js'; import './components/session-card.js'; import type { Session } from './components/session-list.js'; +import type { SessionCard } from './components/session-card.js'; @customElement('vibetunnel-app') export class VibeTunnelApp extends LitElement { @@ -180,9 +181,9 @@ export class VibeTunnelApp extends LitElement { private async handleKillAll() { // Find all session cards and trigger their kill buttons - const sessionCards = this.querySelectorAll('session-card'); + const sessionCards = this.querySelectorAll('session-card'); - sessionCards.forEach((card: Element) => { + sessionCards.forEach((card: SessionCard) => { // Check if this session is running if (card.session && card.session.status === 'running') { // Find all buttons within this card and look for the kill button diff --git a/web/src/client/components/mobile-terminal.ts b/web/src/client/components/mobile-terminal.ts deleted file mode 100644 index 6bd1ad73..00000000 --- a/web/src/client/components/mobile-terminal.ts +++ /dev/null @@ -1,815 +0,0 @@ -import { LitElement, html, nothing } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; -import { Terminal } from '@xterm/xterm'; -import { ScaleFitAddon } from '../scale-fit-addon.js'; - -// Simplified - only fit-both mode - -@customElement('responsive-terminal') -export class ResponsiveTerminal extends LitElement { - // Disable shadow DOM for Tailwind compatibility - createRenderRoot() { - return this; - } - - @property({ type: String }) sessionId = ''; - @property({ type: Number }) cols = 80; - @property({ type: Number }) rows = 24; - @property({ type: Number }) fontSize = 14; - // Removed fitMode - always fit-both - @property({ type: Boolean }) showControls = false; - @property({ type: Boolean }) enableInput = false; - @property({ type: String }) containerClass = ''; - - @state() private terminal: Terminal | null = null; - @state() private scaleFitAddon: ScaleFitAddon | null = null; - @state() private isMobile = false; - @state() private touches = new Map(); - @state() private currentTerminalSize = { cols: 80, rows: 24 }; - @state() private actualLineHeight = 16; - @state() private touchCount = 0; - @state() private terminalStatus = 'Initializing...'; - - // Inertial scrolling state - private velocity = 0; - private lastTouchTime = 0; - private momentumAnimationId: number | null = null; - - // Long press detection for text selection - private longPressTimer: number | null = null; - private longPressThreshold = 500; // ms - private isInSelectionMode = false; - - private container: HTMLElement | null = null; - private wrapper: HTMLElement | null = null; - private resizeObserver: ResizeObserver | null = null; - private boundCopyHandler: ((e: ClipboardEvent) => void) | null = null; - private resizeTimeout: number | null = null; - - connectedCallback() { - super.connectedCallback(); - this.detectMobile(); - this.currentTerminalSize = { cols: this.cols, rows: this.rows }; - } - - disconnectedCallback() { - this.cleanup(); - super.disconnectedCallback(); - } - - private detectMobile() { - this.isMobile = window.innerWidth <= 768 || 'ontouchstart' in window; - } - - private cleanup() { - if (this.resizeObserver) { - this.resizeObserver.disconnect(); - this.resizeObserver = null; - } - if (this.terminal) { - this.terminal.dispose(); - this.terminal = null; - } - // Remove copy event listener - if (this.boundCopyHandler) { - document.removeEventListener('copy', this.boundCopyHandler); - this.boundCopyHandler = null; - } - // Stop momentum animation - if (this.momentumAnimationId) { - cancelAnimationFrame(this.momentumAnimationId); - this.momentumAnimationId = null; - } - // Clear long press timer - if (this.longPressTimer) { - clearTimeout(this.longPressTimer); - this.longPressTimer = null; - } - this.scaleFitAddon = null; - } - - updated(changedProperties: Map) { - if (changedProperties.has('cols') || changedProperties.has('rows')) { - this.currentTerminalSize = { cols: this.cols, rows: this.rows }; - if (this.terminal) { - this.reinitializeTerminal(); - } - } - } - - firstUpdated() { - this.initializeTerminal(); - } - - private async initializeTerminal() { - try { - this.terminalStatus = 'Initializing...'; - this.requestUpdate(); - - this.container = this.querySelector('#terminal-container') as HTMLElement; - this.wrapper = this.querySelector('#terminal-wrapper') as HTMLElement; - - if (!this.container || !this.wrapper) { - throw new Error('Terminal container or wrapper not found'); - } - - await this.setupTerminal(); - this.setupTouchHandling(); - this.setupResize(); - this.generateMockData(); - - this.terminalStatus = 'Ready'; - this.requestUpdate(); - } catch (error) { - this.terminalStatus = `Error: ${error instanceof Error ? error.message : String(error)}`; - this.requestUpdate(); - } - } - - private async reinitializeTerminal() { - if (this.terminal) { - this.terminal.clear(); - this.fitTerminal(); - this.generateMockData(); - } - } - - private async setupTerminal() { - // EXACT terminal config from working file - this.terminal = new Terminal({ - cursorBlink: false, - fontSize: 14, - fontFamily: 'Consolas, "Liberation Mono", monospace', - lineHeight: 1.2, - scrollback: 10000, - // Disable built-in link handling completely - linkHandler: null, - windowOptions: {}, - theme: { - background: '#1e1e1e', - foreground: '#d4d4d4', - cursor: '#00ff00', - black: '#000000', - red: '#f14c4c', - green: '#23d18b', - yellow: '#f5f543', - blue: '#3b8eea', - magenta: '#d670d6', - cyan: '#29b8db', - white: '#e5e5e5', - }, - }); - - // Open terminal first - if (this.wrapper) { - this.terminal.open(this.wrapper); - } - - // Always disable default terminal handlers first - this.disableDefaultTerminalHandlers(); - - // Fit terminal to container - this.fitTerminal(); - } - - private disableDefaultTerminalHandlers() { - if (!this.terminal || !this.wrapper) return; - - // Back to aggressive approach - disable all XTerm touch handling so we can control everything - const terminalEl = this.wrapper.querySelector('.xterm'); - const screenEl = this.wrapper.querySelector('.xterm-screen'); - const rowsEl = this.wrapper.querySelector('.xterm-rows'); - const textareaEl = this.wrapper.querySelector('.xterm-helper-textarea'); - - if (terminalEl) { - // Disable all default behaviors on terminal elements - [terminalEl, screenEl, rowsEl].forEach((el) => { - if (el) { - el.addEventListener('touchstart', (e) => e.preventDefault(), { passive: false }); - el.addEventListener('touchmove', (e) => e.preventDefault(), { passive: false }); - el.addEventListener('touchend', (e) => e.preventDefault(), { passive: false }); - el.addEventListener('wheel', this.handleWheel.bind(this), { passive: false }); - el.addEventListener('contextmenu', (e) => e.preventDefault()); - } - }); - - // Disable the hidden textarea that XTerm uses for input - if (textareaEl) { - textareaEl.disabled = true; - textareaEl.readOnly = true; - textareaEl.style.pointerEvents = 'none'; - textareaEl.addEventListener('focus', (e) => { - e.preventDefault(); - textareaEl.blur(); - }); - } - } - } - - private setupTouchHandling() { - if (!this.wrapper) return; - - // ONLY attach touch handlers to the terminal wrapper (the XTerm content area) - // This way buttons and other elements outside aren't affected - this.wrapper.addEventListener('touchstart', this.handleTouchStart.bind(this), { - passive: false, - }); - this.wrapper.addEventListener('touchmove', this.handleTouchMove.bind(this), { passive: false }); - this.wrapper.addEventListener('touchend', this.handleTouchEnd.bind(this), { passive: false }); - this.wrapper.addEventListener('touchcancel', this.handleTouchEnd.bind(this), { - passive: false, - }); - - // Prevent context menu ONLY on terminal wrapper - this.wrapper.addEventListener('contextmenu', (e) => e.preventDefault()); - - // Mouse wheel ONLY on terminal wrapper - this.wrapper.addEventListener('wheel', this.handleWheel.bind(this) as EventListener, { - passive: false, - }); - - // Copy support for desktop - this.boundCopyHandler = this.handleCopy.bind(this); - document.addEventListener('copy', this.boundCopyHandler); - } - - private setupResize() { - if (!this.container) return; - - // Use debounced ResizeObserver to avoid infinite resize loops - this.resizeObserver = new ResizeObserver(() => { - // Debounce to avoid resize loops - clearTimeout(this.resizeTimeout); - this.resizeTimeout = setTimeout(() => { - this.fitTerminal(); - }, 50); - }); - this.resizeObserver.observe(this.container); - - window.addEventListener('resize', () => { - this.detectMobile(); - this.fitTerminal(); - }); - } - - private fitTerminal() { - if (!this.terminal || !this.container) return; - - // Use the actual rendered dimensions of the component itself - const hostElement = this as HTMLElement; - const containerWidth = hostElement.clientWidth; - const containerHeight = hostElement.clientHeight; - - // EXACT copy of working fitTerminal() method from mobile-terminal-test-fixed.html - - // Resize to target dimensions first - this.terminal.resize(this.currentTerminalSize.cols, this.currentTerminalSize.rows); - - // Calculate font size to fit the target columns exactly in container width - const charWidthRatio = 0.6; // More conservative estimate - const calculatedFontSize = containerWidth / (this.currentTerminalSize.cols * charWidthRatio); - const fontSize = Math.max(1, calculatedFontSize); // Allow very small fonts - - // Apply font size - this.terminal.options.fontSize = fontSize; - if (this.terminal.element) { - this.terminal.element.style.fontSize = `${fontSize}px`; - } - - // Calculate line height and rows - const lineHeight = fontSize * 1.2; - this.actualLineHeight = lineHeight; - const rows = Math.max(1, Math.floor(containerHeight / lineHeight)); - - this.terminal.resize(this.currentTerminalSize.cols, rows); - - // Force a refresh to apply the new sizing - requestAnimationFrame(() => { - if (this.terminal) { - this.terminal.refresh(0, this.terminal.rows - 1); - - // After rendering, check if we need to adjust row count - setTimeout(() => { - const xtermRows = this.terminal.element?.querySelector('.xterm-rows'); - const firstRow = xtermRows?.children[0]; - if (firstRow && xtermRows) { - // Use the REAL CSS line-height, not offsetHeight - const realLineHeight = - parseFloat(getComputedStyle(firstRow).lineHeight) || firstRow.offsetHeight; - const rowsThatFit = Math.floor(containerHeight / realLineHeight); - - if (rowsThatFit !== this.terminal.rows) { - this.actualLineHeight = realLineHeight; - this.terminal.resize(this.currentTerminalSize.cols, rowsThatFit); - } - } - }, 100); - } - }); - } - - // Removed - not needed in simplified version - - private handleTouchStart(e: TouchEvent) { - // If in selection mode, let browser handle it - if (this.isInSelectionMode) { - return; - } - - e.preventDefault(); - - // Stop any ongoing momentum scrolling - if (this.momentumAnimationId) { - cancelAnimationFrame(this.momentumAnimationId); - this.momentumAnimationId = null; - } - this.velocity = 0; - this.lastTouchTime = Date.now(); - - // Clear any existing long press timer - if (this.longPressTimer) { - clearTimeout(this.longPressTimer); - this.longPressTimer = null; - } - - for (let i = 0; i < e.changedTouches.length; i++) { - const touch = e.changedTouches[i]; - this.touches.set(touch.identifier, { - x: touch.clientX, - y: touch.clientY, - startX: touch.clientX, - startY: touch.clientY, - startTime: Date.now(), - }); - } - - // Start long press detection for single finger - if (this.touches.size === 1 && this.isMobile) { - this.longPressTimer = setTimeout(() => { - this.startTextSelection(); - }, this.longPressThreshold); - } - - this.touchCount = this.touches.size; - this.requestUpdate(); - } - - private handleTouchMove(e: TouchEvent) { - e.preventDefault(); - - // If we moved, cancel long press detection (unless already in selection mode) - if (this.longPressTimer && !this.isInSelectionMode) { - const touch = Array.from(this.touches.values())[0]; - if (touch) { - const deltaX = Math.abs(e.changedTouches[0].clientX - touch.startX); - const deltaY = Math.abs(e.changedTouches[0].clientY - touch.startY); - - // Cancel long press if significant movement (more than 5px) - if (deltaX > 5 || deltaY > 5) { - clearTimeout(this.longPressTimer); - this.longPressTimer = null; - } - } - } - - for (let i = 0; i < e.changedTouches.length; i++) { - const touch = e.changedTouches[i]; - const stored = this.touches.get(touch.identifier); - if (stored) { - stored.x = touch.clientX; - stored.y = touch.clientY; - } - } - - // Only handle scrolling if we're NOT in selection mode - if (this.touches.size === 1 && !this.isInSelectionMode) { - this.handleSingleTouch(); - } - - this.requestUpdate(); - } - - private handleTouchEnd(e: TouchEvent) { - e.preventDefault(); - - // Clear long press timer if still running - if (this.longPressTimer) { - clearTimeout(this.longPressTimer); - this.longPressTimer = null; - } - - for (let i = 0; i < e.changedTouches.length; i++) { - const touch = e.changedTouches[i]; - this.touches.delete(touch.identifier); - } - - // Start inertial scrolling if we had velocity on mobile and NOT in selection mode - if ( - this.touches.size === 0 && - this.isMobile && - Math.abs(this.velocity) > 0.5 && - !this.isInSelectionMode - ) { - this.startMomentumScroll(); - } - - this.touchCount = this.touches.size; - this.requestUpdate(); - } - - private handleSingleTouch() { - const touch = Array.from(this.touches.values())[0]; - const deltaY = touch.y - touch.startY; - const currentTime = Date.now(); - - // Only handle vertical scroll - if (Math.abs(deltaY) > 2) { - // Calculate velocity for inertial scrolling - const timeDelta = currentTime - this.lastTouchTime; - if (timeDelta > 0) { - this.velocity = deltaY / timeDelta; // pixels per millisecond - } - this.lastTouchTime = currentTime; - - this.handleScroll(deltaY); - touch.startY = touch.y; // Update immediately for smooth tracking - } - } - - private handleScroll(deltaY: number) { - if (!this.terminal) return; - - // Simple, direct scroll calculation like the working version - const scrollLines = Math.round(deltaY / this.actualLineHeight); - - if (scrollLines !== 0) { - this.terminal.scrollLines(-scrollLines); // Negative for natural scroll direction - } - } - - private startMomentumScroll() { - // Stop any existing momentum animation - if (this.momentumAnimationId) { - cancelAnimationFrame(this.momentumAnimationId); - } - - // Start momentum animation - const animate = () => { - if (!this.terminal || Math.abs(this.velocity) < 0.01) { - this.momentumAnimationId = null; - return; - } - - // Apply momentum scroll - const scrollDelta = this.velocity * 16; // 16ms frame time - this.handleScroll(scrollDelta); - - // Apply friction to slow down - this.velocity *= 0.95; // Friction coefficient - - // Continue animation - this.momentumAnimationId = requestAnimationFrame(animate); - }; - - this.momentumAnimationId = requestAnimationFrame(animate); - } - - private startTextSelection() { - if (!this.terminal || !this.wrapper) return; - - // Provide haptic feedback - if (navigator.vibrate) { - navigator.vibrate(50); - } - - const touch = Array.from(this.touches.values())[0]; - if (!touch) return; - - // Enable browser selection for both desktop and mobile - this.isInSelectionMode = true; - this.wrapper.classList.add('selection-mode'); - - const terminalEl = this.wrapper.querySelector('.xterm'); - const screenEl = this.wrapper.querySelector('.xterm-screen'); - const rowsEl = this.wrapper.querySelector('.xterm-rows'); - - [terminalEl, screenEl, rowsEl].forEach((el) => { - if (el) { - const element = el as HTMLElement; - element.style.pointerEvents = 'auto'; - element.style.webkitUserSelect = 'text'; - element.style.userSelect = 'text'; - } - }); - - console.log('Text selection mode enabled'); - } - - private exitTextSelection() { - this.isInSelectionMode = false; - - // Clean up browser selection - if (this.wrapper) { - this.wrapper.classList.remove('selection-mode'); - - const terminalEl = this.wrapper.querySelector('.xterm'); - const screenEl = this.wrapper.querySelector('.xterm-screen'); - const rowsEl = this.wrapper.querySelector('.xterm-rows'); - - [terminalEl, screenEl, rowsEl].forEach((el) => { - if (el) { - const element = el as HTMLElement; - element.style.pointerEvents = ''; - element.style.webkitUserSelect = ''; - element.style.userSelect = ''; - element.style.webkitTouchCallout = ''; - } - }); - - const selection = window.getSelection(); - selection?.removeAllRanges(); - } - - console.log('Text selection mode deactivated'); - } - - private handleWheel(e: Event) { - e.preventDefault(); - e.stopPropagation(); - - const wheelEvent = e as WheelEvent; - - if (this.terminal) { - // EXACT same logic as working version - let scrollLines = 0; - - if (wheelEvent.deltaMode === WheelEvent.DOM_DELTA_LINE) { - // deltaY is already in lines - scrollLines = Math.round(wheelEvent.deltaY); - } else if (wheelEvent.deltaMode === WheelEvent.DOM_DELTA_PIXEL) { - // deltaY is in pixels, convert to lines - scrollLines = Math.round(wheelEvent.deltaY / this.actualLineHeight); - } else if (wheelEvent.deltaMode === WheelEvent.DOM_DELTA_PAGE) { - // deltaY is in pages, convert to lines - scrollLines = Math.round(wheelEvent.deltaY * this.terminal.rows); - } - - if (scrollLines !== 0) { - this.terminal.scrollLines(scrollLines); - } - } - } - - private handleCopy(e: ClipboardEvent) { - if (!this.terminal) return; - - // Get selected text from XTerm regardless of focus - const selection = this.terminal.getSelection(); - - if (selection && selection.trim()) { - e.preventDefault(); - e.clipboardData?.setData('text/plain', selection); - } - } - - private handleSizeChange(cols: number, rows: number) { - // Old method that regenerates content - keeping for backward compatibility - this.cols = cols; - this.rows = rows; - this.currentTerminalSize = { cols, rows }; - - if (this.terminal) { - this.terminal.clear(); - this.fitTerminal(); - this.generateMockData(); - } - } - - // New method for viewport size changes without content regeneration - public setViewportSize(cols: number, rows: number) { - this.cols = cols; - this.rows = rows; - this.currentTerminalSize = { cols, rows }; - - if (this.terminal) { - // Just resize the viewport - XTerm will reflow the existing content - this.fitTerminal(); - } - - this.requestUpdate(); // Update the UI to show new size in status - } - - // Removed fitMode handling - only fit-both mode now - - private generateMockData() { - if (!this.terminal) return; - - // Always generate content for 120x40, regardless of current viewport size - // This way we can see XTerm reflow the same content for different viewport sizes - const contentCols = 120; - const contentRows = 40; - const numPages = 10; - - let lineNumber = 1; - - for (let page = 1; page <= numPages; page++) { - // Page header with special characters and highlighting - const headerLine = '\x1b[43m◄\x1b[0m' + '='.repeat(contentCols - 2) + '\x1b[43m►\x1b[0m'; - this.terminal.writeln(headerLine); - lineNumber++; - - // Fill the page with numbered lines (rows - 2 for header/footer only) - const contentLines = contentRows - 2; - for (let line = 1; line <= contentLines; line++) { - this.terminal.writeln( - `Line ${lineNumber.toString().padStart(4, '0')}: Content originally sized for ${contentCols}x${contentRows} terminal - watch it reflow!` - ); - lineNumber++; - } - - // Page footer with special characters and highlighting - const footerLine = '\x1b[43m◄\x1b[0m' + '='.repeat(contentCols - 2) + '\x1b[43m►\x1b[0m'; - this.terminal.writeln(footerLine); - lineNumber++; - } - - this.terminal.writeln('\x1b[1;31m>>> END OF ALL CONTENT - THIS IS THE BOTTOM <<<\x1b[0m'); - this.terminal.writeln( - '\x1b[1;33mIf you can see this, you reached the end. Scroll up to see all pages.\x1b[0m' - ); - - // Ensure we can scroll to the bottom - this.terminal.scrollToBottom(); - } - - // Public API methods - public write(data: string) { - if (this.terminal) { - this.terminal.write(data); - } - } - - public clear() { - if (this.terminal) { - this.terminal.clear(); - } - } - - public getTerminal(): Terminal | null { - return this.terminal; - } - - render() { - // EXACT aggressive CSS constraints from working file (keep as CSS for XTerm) - const aggressiveXTermStyles = html` - - `; - - return html` - ${aggressiveXTermStyles} -
- ${this.showControls ? this.renderControls() : nothing} ${this.renderStatus()} -
-
- `; - } - - private renderControls() { - const sizeOptions = [ - { cols: 60, rows: 15, label: '60x15' }, - { cols: 80, rows: 20, label: '80x20' }, - { cols: 120, rows: 40, label: '120x40' }, - { cols: 160, rows: 50, label: '160x50' }, - ]; - - // Use position: fixed like working version so controls don't affect layout - return html` -
- ${sizeOptions.map( - (size) => html` - - ` - )} -
- `; - } - - private renderStatus() { - // Position relative to the component, not the viewport - return html` -
-
Size: ${this.currentTerminalSize.cols}x${this.currentTerminalSize.rows}
-
Font: ${this.terminal?.options.fontSize?.toFixed(1) || 14}px
-
Touch: ${this.touchCount}
-
Status: ${this.terminalStatus}
-
Device: ${this.isMobile ? 'Mobile' : 'Desktop'}
-
- `; - } -} - -declare global { - interface HTMLElementTagNameMap { - 'responsive-terminal': ResponsiveTerminal; - } -} diff --git a/web/src/client/components/terminal.ts b/web/src/client/components/terminal.ts index 54558349..ce733bf3 100644 --- a/web/src/client/components/terminal.ts +++ b/web/src/client/components/terminal.ts @@ -1,6 +1,6 @@ import { LitElement, html } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; -import { Terminal as XtermTerminal, IBufferLine, IBufferCell } from '@xterm/xterm'; +import { Terminal as XtermTerminal, IBufferLine, IBufferCell } from '@xterm/headless'; import { UrlHighlighter } from '../utils/url-highlighter.js'; @customElement('vibe-terminal') @@ -177,10 +177,9 @@ export class Terminal extends LitElement { // Create regular terminal but don't call .open() to make it headless this.terminal = new XtermTerminal({ cursorBlink: false, - fontSize: this.fontSize, - fontFamily: 'Fira Code, ui-monospace, SFMono-Regular, monospace', lineHeight: 1.2, scrollback: 10000, + allowProposedApi: true, theme: { background: '#1e1e1e', foreground: '#d4d4d4', diff --git a/web/src/client/custom-weblinks-addon.ts b/web/src/client/custom-weblinks-addon.ts deleted file mode 100644 index 96fc60b5..00000000 --- a/web/src/client/custom-weblinks-addon.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Terminal, ITerminalAddon } from '@xterm/xterm'; - -export class CustomWebLinksAddon implements ITerminalAddon { - private _terminal?: Terminal; - private _linkMatcher?: number; - - constructor(private _handler?: (event: MouseEvent, uri: string) => void) {} - - public activate(terminal: Terminal): void { - this._terminal = terminal; - - // URL regex pattern - matches http/https URLs - const urlRegex = /(https?:\/\/[^\s]+)/gi; - - this._linkMatcher = this._terminal.registerLinkMatcher( - urlRegex, - (event: MouseEvent, uri: string) => { - console.log('Custom WebLinks click:', uri); - if (this._handler) { - this._handler(event, uri); - } else { - window.open(uri, '_blank'); - } - }, - { - // Custom styling options - hover: ( - event: MouseEvent, - uri: string, - _location: { start: { x: number; y: number }; end: { x: number; y: number } } - ) => { - console.log('Custom WebLinks hover:', uri); - // Style the link on hover - const linkElement = event.target as HTMLElement; - if (linkElement) { - linkElement.style.backgroundColor = 'rgba(59, 142, 234, 0.2)'; - linkElement.style.color = '#ffffff'; - linkElement.style.textDecoration = 'underline'; - } - }, - leave: (event: MouseEvent, uri: string) => { - console.log('Custom WebLinks leave:', uri); - // Remove hover styling - const linkElement = event.target as HTMLElement; - if (linkElement) { - linkElement.style.backgroundColor = ''; - linkElement.style.color = '#3b8eea'; - linkElement.style.textDecoration = 'underline'; - } - }, - priority: 1, - willLinkActivate: (event: MouseEvent, uri: string) => { - console.log('Custom WebLinks will activate:', uri); - return true; - }, - } - ); - - console.log('Custom WebLinks addon activated with matcher ID:', this._linkMatcher); - } - - public dispose(): void { - if (this._linkMatcher !== undefined && this._terminal) { - this._terminal.deregisterLinkMatcher(this._linkMatcher); - this._linkMatcher = undefined; - } - this._terminal = undefined; - } -} diff --git a/web/src/client/renderer-entry.ts b/web/src/client/renderer-entry.ts deleted file mode 100644 index a2633a33..00000000 --- a/web/src/client/renderer-entry.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Entry point for renderer bundle - exports XTerm-based renderer -export { Renderer } from './renderer'; diff --git a/web/src/client/renderer.ts b/web/src/client/renderer.ts deleted file mode 100644 index 319f6fba..00000000 --- a/web/src/client/renderer.ts +++ /dev/null @@ -1,414 +0,0 @@ -// Terminal renderer for asciinema cast format using XTerm.js -// Professional-grade terminal emulation with full VT compatibility - -import { Terminal } from '@xterm/xterm'; -import { WebLinksAddon } from '@xterm/addon-web-links'; -import { ScaleFitAddon } from './scale-fit-addon.js'; - -// interface CastHeader { -// version: number; -// width: number; -// height: number; -// timestamp?: number; -// env?: Record; -// } - -interface CastEvent { - timestamp: number; - type: 'o' | 'i' | 'r'; // output, input, or resize - data: string; -} - -export class Renderer { - private static activeCount: number = 0; - - private container: HTMLElement; - private terminal: Terminal; - private scaleFitAddon: ScaleFitAddon; - private webLinksAddon: WebLinksAddon; - - constructor( - container: HTMLElement, - width: number = 80, - height: number = 20, - scrollback: number = 1000000, - fontSize: number = 14 - ) { - Renderer.activeCount++; - console.log(`Renderer constructor called (active: ${Renderer.activeCount})`); - this.container = container; - - // Create terminal with options similar to the custom renderer - this.terminal = new Terminal({ - cols: width, - rows: height, - fontFamily: 'Monaco, "Lucida Console", monospace', - fontSize: fontSize, - lineHeight: 1.2, - theme: { - background: '#1e1e1e', - foreground: '#d4d4d4', - cursor: '#00ff00', - cursorAccent: '#1e1e1e', - selectionBackground: '#264f78', - // VS Code Dark theme colors - black: '#000000', - red: '#f14c4c', - green: '#23d18b', - yellow: '#f5f543', - blue: '#3b8eea', - magenta: '#d670d6', - cyan: '#29b8db', - white: '#e5e5e5', - // Bright colors - brightBlack: '#666666', - brightRed: '#f14c4c', - brightGreen: '#23d18b', - brightYellow: '#f5f543', - brightBlue: '#3b8eea', - brightMagenta: '#d670d6', - brightCyan: '#29b8db', - brightWhite: '#ffffff', - }, - allowProposedApi: true, - scrollback: scrollback, // Configurable scrollback buffer - convertEol: true, - altClickMovesCursor: false, - rightClickSelectsWord: false, - disableStdin: true, // We handle input separately - cursorBlink: true, - cursorStyle: 'block', - cursorWidth: 1, - cursorInactiveStyle: 'block', - }); - - // Add addons - this.scaleFitAddon = new ScaleFitAddon(); - this.webLinksAddon = new WebLinksAddon(); - - this.terminal.loadAddon(this.scaleFitAddon); - this.terminal.loadAddon(this.webLinksAddon); - - this.setupDOM(); - } - - private setupDOM(): void { - // Clear container and add CSS - this.container.innerHTML = ''; - - // Full terminals get padding - this.container.style.padding = '10px'; - this.container.style.backgroundColor = '#1e1e1e'; - this.container.style.overflow = 'hidden'; - this.container.style.maxWidth = '100%'; - this.container.style.boxSizing = 'border-box'; - - // Create terminal wrapper - const terminalWrapper = document.createElement('div'); - terminalWrapper.style.width = '100%'; - terminalWrapper.style.height = '100%'; - terminalWrapper.style.maxWidth = '100%'; - terminalWrapper.style.overflow = 'hidden'; - this.container.appendChild(terminalWrapper); - - // Open terminal in the wrapper - this.terminal.open(terminalWrapper); - - // Disable XTerm's input handling completely while preserving cursor - this.terminal.onData(() => { - // Do nothing - we handle input via our own system - }); - - // Ensure cursor is visible without focus by forcing it to stay active - requestAnimationFrame(() => { - if (this.terminal.element) { - const helperTextarea = this.terminal.element.querySelector( - '.xterm-helper-textarea' - ) as HTMLTextAreaElement; - if (helperTextarea) { - // Prevent the helper textarea from stealing focus but allow cursor rendering - helperTextarea.addEventListener('focus', (e) => { - e.preventDefault(); - helperTextarea.blur(); - }); - } - } - }); - - // Add CSS to override XTerm's fixed width/height on .xterm-screen within this container - // Apply to both previews and full terminals - const containerId = `terminal-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - this.container.id = containerId; - - // Always use ScaleFitAddon for better scaling - this.scaleFitAddon.fit(); - - // Handle container resize - const resizeObserver = new ResizeObserver(() => { - this.scaleFitAddon.fit(); - }); - resizeObserver.observe(this.container); - } - - // Public API methods - maintain compatibility with custom renderer - - async loadCastFile(url: string): Promise { - const response = await fetch(url); - const text = await response.text(); - this.parseCastFile(text); - } - - parseCastFile(content: string): void { - const lines = content.trim().split('\n'); - const outputEvents: string[] = []; - - // Clear terminal - this.terminal.clear(); - - // First pass: collect all output events and process headers/resizes immediately - for (const line of lines) { - if (!line.trim()) continue; - - try { - const parsed = JSON.parse(line); - - if (parsed.version && parsed.width && parsed.height) { - // Header - // header = parsed; - this.resize(parsed.width, parsed.height); - } else if (Array.isArray(parsed) && parsed.length >= 3) { - // Event: [timestamp, type, data] - const event: CastEvent = { - timestamp: parsed[0], - type: parsed[1], - data: parsed[2], - }; - - if (event.type === 'o') { - outputEvents.push(event.data); - } else if (event.type === 'r') { - this.processResize(event.data); - } - } - } catch (_e) { - console.warn('Failed to parse cast line:', line); - } - } - - // Write all output at once, then scroll when rendering is complete - if (outputEvents.length > 0) { - const allOutput = outputEvents.join(''); - this.terminal.write(allOutput, () => { - // This callback fires when XTerm has finished rendering the content - this.scrollToBottom(); - }); - } else { - // No output to render, scroll immediately - this.scrollToBottom(); - } - } - - processOutput(data: string): void { - // XTerm handles all ANSI escape sequences automatically - this.terminal.write(data); - } - - processResize(data: string): void { - // Parse resize data in format "WIDTHxHEIGHT" (e.g., "80x24") - const match = data.match(/^(\d+)x(\d+)$/); - if (match) { - const width = parseInt(match[1], 10); - const height = parseInt(match[2], 10); - this.resize(width, height); - } - } - - processEvent(event: CastEvent): void { - if (event.type === 'o') { - this.processOutput(event.data); - } else if (event.type === 'r') { - this.processResize(event.data); - } - } - - resize(width: number, height: number): void { - // Resize terminal to session dimensions - this.terminal.resize(width, height); - // Always use ScaleFitAddon for consistent scaling behavior - this.scaleFitAddon.fit(); - - // Emit custom event with terminal dimensions - const event = new CustomEvent('terminal-resize', { - detail: { cols: width, rows: height }, - bubbles: true, - }); - this.container.dispatchEvent(event); - } - - clear(): void { - this.terminal.clear(); - } - - // Stream support - connect to SSE endpoint - connectToStream(sessionId: string): EventSource { - console.log('connectToStream called for session:', sessionId); - return this.connectToUrl(`/api/sessions/${sessionId}/stream`); - } - - // Connect to any SSE URL - connectToUrl(url: string): EventSource { - console.log('Creating new EventSource connection to:', url); - const eventSource = new EventSource(url); - - // Don't clear terminal for live streams - just append new content - - eventSource.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - - if (data.version && data.width && data.height) { - // Header - console.log('Received header:', data); - this.resize(data.width, data.height); - } else if (Array.isArray(data) && data.length >= 3) { - // Check if this is an exit event - if (data[0] === 'exit') { - const exitCode = data[1]; - const sessionId = data[2]; - console.log(`Session ${sessionId} exited with code ${exitCode}`); - - // Close the SSE connection immediately - if (this.eventSource) { - console.log('Closing SSE connection due to session exit'); - this.eventSource.close(); - this.eventSource = null; - } - - // Dispatch custom event that session-view can listen to - const exitEvent = new CustomEvent('session-exit', { - detail: { sessionId, exitCode }, - }); - this.container.dispatchEvent(exitEvent); - return; - } - - // Regular cast event - const castEvent: CastEvent = { - timestamp: data[0], - type: data[1], - data: data[2], - }; - // Process event without verbose logging - this.processEvent(castEvent); - } - } catch (_e) { - console.warn('Failed to parse stream event:', event.data); - } - }; - - eventSource.onerror = (error) => { - console.error('Stream error:', error); - // Close the connection to prevent automatic reconnection attempts - if (eventSource.readyState === EventSource.CLOSED) { - console.log('Stream closed, cleaning up...'); - if (this.eventSource === eventSource) { - this.eventSource = null; - } - } - }; - - return eventSource; - } - - private eventSource: EventSource | null = null; - - // Load content from URL - pass isStream to determine how to handle it - async loadFromUrl(url: string, isStream: boolean): Promise { - // Clean up existing connection - if (this.eventSource) { - console.log('Explicitly closing existing EventSource connection'); - this.eventSource.close(); - this.eventSource = null; - } - - if (isStream) { - // It's a stream URL, connect via SSE (don't clear - append to existing content) - this.eventSource = this.connectToUrl(url); - } else { - // It's a snapshot URL, clear first then load as cast file - this.terminal.clear(); - await this.loadCastFile(url); - } - } - - // Additional methods for terminal control - - focus(): void { - this.terminal.focus(); - } - - blur(): void { - this.terminal.blur(); - } - - getTerminal(): Terminal { - return this.terminal; - } - - dispose(): void { - if (this.eventSource) { - console.log('Explicitly closing EventSource connection in dispose()'); - this.eventSource.close(); - this.eventSource = null; - } - this.terminal.dispose(); - Renderer.activeCount--; - console.log(`Renderer disposed (active: ${Renderer.activeCount})`); - } - - // Method to fit terminal to container (useful for responsive layouts) - fit(): void { - this.scaleFitAddon.fit(); - } - - // Scroll terminal to bottom - scrollToBottom(): void { - this.terminal.scrollToBottom(); - } - - // Get terminal dimensions - getDimensions(): { cols: number; rows: number } { - return { - cols: this.terminal.cols, - rows: this.terminal.rows, - }; - } - - // Write raw data to terminal (useful for testing) - write(data: string): void { - this.terminal.write(data); - } - - // Enable/disable input (though we keep it disabled by default) - setInputEnabled(enabled: boolean): void { - // XTerm doesn't have a direct way to disable input, so we override onData - if (enabled) { - // Remove any existing handler first - this.terminal.onData(() => { - // Input is handled by the session component - }); - } else { - this.terminal.onData(() => { - // Do nothing - input disabled - }); - } - } - - // Disable all pointer events for previews so clicks pass through to parent - setPointerEventsEnabled(enabled: boolean): void { - const terminalElement = this.container.querySelector('.xterm') as HTMLElement; - if (terminalElement) { - terminalElement.style.pointerEvents = enabled ? 'auto' : 'none'; - } - } -} diff --git a/web/src/client/scale-fit-addon.ts b/web/src/client/scale-fit-addon.ts deleted file mode 100644 index 1a784ad9..00000000 --- a/web/src/client/scale-fit-addon.ts +++ /dev/null @@ -1,294 +0,0 @@ -/** - * Custom FitAddon that scales font size to fit terminal columns to container width, - * then calculates optimal rows for the container height. - */ - -import type { Terminal, ITerminalAddon } from '@xterm/xterm'; - -interface ITerminalDimensions { - rows: number; - cols: number; -} - -const MINIMUM_ROWS = 1; -const MIN_FONT_SIZE = 4; -const MAX_FONT_SIZE = 16; - -export class ScaleFitAddon implements ITerminalAddon { - private _terminal: Terminal | undefined; - - constructor() {} - - public activate(terminal: Terminal): void { - this._terminal = terminal; - } - - public dispose(): void {} - - public fit(): void { - // For full terminals, resize both font and dimensions - const dims = this.proposeDimensions(); - if (!dims || !this._terminal || isNaN(dims.cols) || isNaN(dims.rows)) { - return; - } - - // Only resize rows, keep cols the same (font scaling handles width) - if (this._terminal.rows !== dims.rows) { - this._terminal.resize(this._terminal.cols, dims.rows); - } - - // Force responsive sizing by overriding XTerm's fixed dimensions - this.forceResponsiveSizing(); - } - - public proposeDimensions(): ITerminalDimensions | undefined { - if (!this._terminal?.element?.parentElement) { - return undefined; - } - - // Get the renderer container (parent of parent - the one with 10px padding) - const terminalWrapper = this._terminal.element.parentElement; - const rendererContainer = terminalWrapper.parentElement; - - if (!rendererContainer) return undefined; - - // Get container dimensions and exact padding - const containerStyle = window.getComputedStyle(rendererContainer); - const containerWidth = parseInt(containerStyle.getPropertyValue('width')); - const containerHeight = parseInt(containerStyle.getPropertyValue('height')); - const containerPadding = { - top: parseInt(containerStyle.getPropertyValue('padding-top')), - bottom: parseInt(containerStyle.getPropertyValue('padding-bottom')), - left: parseInt(containerStyle.getPropertyValue('padding-left')), - right: parseInt(containerStyle.getPropertyValue('padding-right')), - }; - - // Calculate exact available space using known padding - const availableWidth = containerWidth - containerPadding.left - containerPadding.right; - const availableHeight = containerHeight - containerPadding.top - containerPadding.bottom; - - // Current terminal dimensions - const currentCols = this._terminal.cols; - - // Get exact character dimensions from XTerm's measurement system first - const charDimensions = this.getXTermCharacterDimensions(); - - if (charDimensions) { - // Use actual measured dimensions for linear scaling calculation - const { charWidth, lineHeight } = charDimensions; - const currentFontSize = this._terminal.options.fontSize || 14; - - // Calculate current total rendered width for all columns - const currentRenderedWidth = currentCols * charWidth; - - // Calculate scale factor needed to fit exactly in available width - const scaleFactor = availableWidth / currentRenderedWidth; - - // Apply linear scaling to font size - const newFontSize = currentFontSize * scaleFactor; - const clampedFontSize = Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, newFontSize)); - - // Calculate actual font scaling that was applied (accounting for clamping) - const actualFontScaling = clampedFontSize / currentFontSize; - - // Apply the actual font scaling to line height - const newLineHeight = lineHeight * actualFontScaling; - - // Calculate how many rows fit with the scaled line height - const optimalRows = Math.max(MINIMUM_ROWS, Math.floor(availableHeight / newLineHeight)); - - // Apply the new font size - requestAnimationFrame(() => this.applyFontSize(clampedFontSize)); - - // Log all calculations for debugging - console.log( - `ScaleFitAddon: ${availableWidth}×${availableHeight}px available, ${currentCols}×${this._terminal.rows} terminal, charWidth=${charWidth.toFixed(2)}px, lineHeight=${lineHeight.toFixed(2)}px, currentRenderedWidth=${currentRenderedWidth.toFixed(2)}px, scaleFactor=${scaleFactor.toFixed(3)}, actualFontScaling=${actualFontScaling.toFixed(3)}, fontSize ${currentFontSize}px→${clampedFontSize.toFixed(2)}px, lineHeight ${lineHeight.toFixed(2)}px→${newLineHeight.toFixed(2)}px, rows ${this._terminal.rows}→${optimalRows}` - ); - - return { - cols: currentCols, // ALWAYS keep exact column count - rows: optimalRows, // Maximize rows that fit - }; - } else { - // Fallback: estimate font size and dimensions if measurements aren't available - const charWidthRatio = 0.63; - const calculatedFontSize = - Math.floor((availableWidth / (currentCols * charWidthRatio)) * 10) / 10; - const optimalFontSize = Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, calculatedFontSize)); - - // Apply the calculated font size - requestAnimationFrame(() => this.applyFontSize(optimalFontSize)); - - const lineHeight = optimalFontSize * (this._terminal.options.lineHeight || 1.2); - const optimalRows = Math.max(MINIMUM_ROWS, Math.floor(availableHeight / lineHeight)); - - return { - cols: currentCols, - rows: optimalRows, - }; - } - } - - private applyFontSize(fontSize: number): void { - if (!this._terminal?.element) return; - - // Prevent infinite recursion by checking if font size changed significantly - const currentFontSize = this._terminal.options.fontSize || 14; - if (Math.abs(fontSize - currentFontSize) < 0.1) return; - - const terminalElement = this._terminal.element; - - // Update terminal's font size - this._terminal.options.fontSize = fontSize; - - // Apply CSS font size to the element - terminalElement.style.fontSize = `${fontSize}px`; - - // Force a refresh to apply the new font size and ensure responsive sizing - requestAnimationFrame(() => { - if (this._terminal) { - this._terminal.refresh(0, this._terminal.rows - 1); - // Force responsive sizing after refresh since XTerm might reset dimensions - this.forceResponsiveSizing(); - } - }); - } - - /** - * Get the calculated font size that would fit the current columns in the container - */ - private scaleFontOnly(): void { - if (!this._terminal?.element?.parentElement) return; - - // Get container dimensions for font scaling - const terminalWrapper = this._terminal.element.parentElement; - const rendererContainer = terminalWrapper.parentElement; - if (!rendererContainer) return; - - const containerStyle = window.getComputedStyle(rendererContainer); - const containerWidth = parseInt(containerStyle.getPropertyValue('width')); - const containerPadding = { - left: parseInt(containerStyle.getPropertyValue('padding-left')), - right: parseInt(containerStyle.getPropertyValue('padding-right')), - }; - - const availableWidth = containerWidth - containerPadding.left - containerPadding.right; - const currentCols = this._terminal.cols; - - // Calculate font size to fit columns in available width - const charWidthRatio = 0.63; - // Calculate font size and round down for precision - const calculatedFontSize = - Math.floor((availableWidth / (currentCols * charWidthRatio)) * 10) / 10; - const optimalFontSize = Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, calculatedFontSize)); - - // Apply the font size without changing terminal dimensions - this.applyFontSize(optimalFontSize); - - // Also force responsive sizing for previews - this.forceResponsiveSizing(); - } - - public getOptimalFontSize(): number { - if (!this._terminal?.element?.parentElement) { - return this._terminal?.options.fontSize || 14; - } - - const parentElement = this._terminal.element.parentElement; - const parentStyle = window.getComputedStyle(parentElement); - const parentWidth = parseInt(parentStyle.getPropertyValue('width')); - - const elementStyle = window.getComputedStyle(this._terminal.element); - const paddingHor = - parseInt(elementStyle.getPropertyValue('padding-left')) + - parseInt(elementStyle.getPropertyValue('padding-right')); - - const availableWidth = parentWidth - paddingHor; - const charWidthRatio = 0.63; - const calculatedFontSize = availableWidth / (this._terminal.cols * charWidthRatio); - - return Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, calculatedFontSize)); - } - - /** - * Get exact character dimensions from XTerm's built-in measurement system - */ - private getXTermCharacterDimensions(): { charWidth: number; lineHeight: number } | null { - if (!this._terminal?.element) return null; - - // XTerm has a built-in character measurement system with multiple font styles - const measureContainer = this._terminal.element.querySelector( - '.xterm-width-cache-measure-container' - ); - - // Find the first measurement element (normal weight, usually 'm' characters) - // This is what XTerm uses for baseline character width calculations - const firstMeasureElement = measureContainer?.querySelector('.xterm-char-measure-element'); - - if (firstMeasureElement) { - const measureRect = firstMeasureElement.getBoundingClientRect(); - const measureText = firstMeasureElement.textContent || ''; - - if (measureText.length > 0 && measureRect.width > 0) { - // Calculate actual character width from the primary measurement element - const actualCharWidth = measureRect.width / measureText.length; - - // Get line height from the first row in .xterm-rows - const xtermRows = this._terminal.element.querySelector('.xterm-rows'); - const firstRow = xtermRows?.querySelector('div'); - let lineHeight = 21.5; // fallback - - if (firstRow) { - const rowStyle = window.getComputedStyle(firstRow); - const rowLineHeight = parseFloat(rowStyle.lineHeight); - if (!isNaN(rowLineHeight) && rowLineHeight > 0) { - lineHeight = rowLineHeight; - } - } - - return { - charWidth: actualCharWidth, - lineHeight: lineHeight, - }; - } - } - - // Fallback: try to measure from the xterm-screen dimensions and terminal cols/rows - const xtermScreen = this._terminal.element.querySelector('.xterm-screen') as HTMLElement; - if (xtermScreen) { - const screenRect = xtermScreen.getBoundingClientRect(); - const charWidth = screenRect.width / this._terminal.cols; - const lineHeight = screenRect.height / this._terminal.rows; - - if (charWidth > 0 && lineHeight > 0) { - return { charWidth, lineHeight }; - } - } - - return null; - } - - /** - * Force XTerm elements to use responsive sizing instead of fixed dimensions - */ - private forceResponsiveSizing(): void { - if (!this._terminal?.element) return; - - // Find the xterm-screen element within the terminal - const xtermScreen = this._terminal.element.querySelector('.xterm-screen') as HTMLElement; - const xtermViewport = this._terminal.element.querySelector('.xterm-viewport') as HTMLElement; - - if (xtermScreen) { - // Remove any fixed width/height styles and force responsive sizing - xtermScreen.style.width = '100%'; - xtermScreen.style.height = '100%'; - xtermScreen.style.maxWidth = '100%'; - xtermScreen.style.maxHeight = '100%'; - } - - if (xtermViewport) { - xtermViewport.style.width = '100%'; - xtermViewport.style.maxWidth = '100%'; - } - } -} diff --git a/web/src/client/test-terminals-entry.ts b/web/src/client/test-terminals-entry.ts index 64256ff0..af2209f7 100644 --- a/web/src/client/test-terminals-entry.ts +++ b/web/src/client/test-terminals-entry.ts @@ -1,6 +1,2 @@ -// Entry point for test pages - includes both terminal implementations -import './components/mobile-terminal.js'; -import './components/terminal.js'; - -// Re-export CastConverter for use in test pages -export { CastConverter } from './utils/cast-converter.js'; +export * from './components/terminal.js'; +export * from './utils/cast-converter.js'; diff --git a/web/src/server.ts b/web/src/server.ts index f02f4c29..ffb4ddf6 100644 --- a/web/src/server.ts +++ b/web/src/server.ts @@ -1,4 +1,4 @@ -import express from 'express'; +import express, { Response } from 'express'; import { createServer } from 'http'; import { WebSocketServer, WebSocket } from 'ws'; import path from 'path'; @@ -347,7 +347,7 @@ app.post('/api/cleanup-exited', async (req, res) => { const activeStreams = new Map< string, { - clients: Set; + clients: Set; tailProcess: ChildProcess; lastPosition: number; }