From db8f4ffbebb6d0dead472bad1807b2770aebf937 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 21 Jun 2025 16:10:39 +0200 Subject: [PATCH] Add first iteration of file browser --- web/bun.lock | 64 +- web/package-lock.json | 87 +- web/package.json | 2 + web/src/client/app.ts | 25 + web/src/client/components/app-header.ts | 18 + .../components/file-browser-enhanced.ts | 766 ++++++++++++++++++ web/src/client/components/file-browser-fab.ts | 99 +++ web/src/client/components/session-view.ts | 30 + web/src/client/types/monaco.d.ts | 8 + web/src/index.ts | 6 +- web/src/server/app.ts | 5 + web/src/server/routes/filesystem.ts | 462 +++++++++++ web/src/server/routes/sessions.ts | 120 ++- web/src/server/server.ts | 3 +- .../server/services/control-dir-watcher.ts | 13 + web/src/server/version.ts | 4 +- 16 files changed, 1651 insertions(+), 61 deletions(-) create mode 100644 web/src/client/components/file-browser-enhanced.ts create mode 100644 web/src/client/components/file-browser-fab.ts create mode 100644 web/src/client/types/monaco.d.ts create mode 100644 web/src/server/routes/filesystem.ts diff --git a/web/bun.lock b/web/bun.lock index 66e4843d..444e981d 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -5,10 +5,12 @@ "name": "vibetunnel-web", "dependencies": { "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0", + "@types/mime-types": "^3.0.1", "@xterm/headless": "^5.5.0", "chalk": "^4.1.2", "express": "^4.19.2", "lit": "^3.3.0", + "mime-types": "^3.0.1", "signal-exit": "^4.1.0", "ws": "^8.18.2", }, @@ -382,6 +384,8 @@ "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + "@types/mime-types": ["@types/mime-types@3.0.1", "", {}, "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ=="], + "@types/node": ["@types/node@24.0.3", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg=="], "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], @@ -1100,9 +1104,9 @@ "mime": ["mime@2.6.0", "", { "bin": "cli.js" }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], @@ -1544,12 +1548,8 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "@eslint/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - "@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.15.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw=="], "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], @@ -1576,7 +1576,7 @@ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], @@ -1594,16 +1594,12 @@ "cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "cliui/string-width": ["string-width@3.1.0", "", { "dependencies": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^5.1.0" } }, "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w=="], - "cliui/wrap-ansi": ["wrap-ansi@5.1.0", "", { "dependencies": { "ansi-styles": "^3.2.0", "string-width": "^3.0.0", "strip-ansi": "^5.0.0" } }, "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q=="], "concurrently/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - "espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - "execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], "execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], @@ -1616,6 +1612,8 @@ "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "get-uri/data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -1624,8 +1622,6 @@ "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jake/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "jest-circus/pretty-format": ["pretty-format@30.0.2", "", { "dependencies": { "@jest/schemas": "30.0.1", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg=="], "jest-cli/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -1644,14 +1640,10 @@ "jest-snapshot/pretty-format": ["pretty-format@30.0.2", "", { "dependencies": { "@jest/schemas": "30.0.1", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg=="], - "jest-util/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - "jest-validate/camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], "jest-validate/pretty-format": ["pretty-format@30.0.2", "", { "dependencies": { "@jest/schemas": "30.0.1", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg=="], - "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "lint-staged/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], "log-update/ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="], @@ -1660,8 +1652,6 @@ "log-update/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - "log-update/wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -1692,8 +1682,6 @@ "string-length/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "string-width/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="], - "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -1710,11 +1698,7 @@ "test-exclude/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - - "vite/fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], - - "vite/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + "type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], @@ -1730,10 +1714,6 @@ "yargs/yargs-parser": ["yargs-parser@13.1.2", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg=="], - "@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], @@ -1760,9 +1740,9 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "babel-plugin-istanbul/test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "babel-plugin-istanbul/test-exclude/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "babel-plugin-istanbul/test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], @@ -1774,10 +1754,6 @@ "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - "cliui/string-width/emoji-regex": ["emoji-regex@7.0.3", "", {}, "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="], - - "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@2.0.0", "", {}, "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w=="], - "cliui/wrap-ansi/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], "concurrently/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], @@ -1792,12 +1768,12 @@ "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "jake/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "jest-circus/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "jest-circus/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], @@ -1846,24 +1822,20 @@ "log-update/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - "log-update/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - - "log-update/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], "prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "string-width/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], - "tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "test-exclude/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "wrap-ansi-cjs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -1888,8 +1860,6 @@ "@puppeteer/browsers/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "babel-plugin-istanbul/test-exclude/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "chokidar-cli/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], @@ -1916,8 +1886,6 @@ "jest-cli/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "log-update/wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], - "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], diff --git a/web/package-lock.json b/web/package-lock.json index 00637af6..c4dc377d 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,10 +10,12 @@ "license": "MIT", "dependencies": { "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0", + "@types/mime-types": "^3.0.1", "@xterm/headless": "^5.5.0", "chalk": "^4.1.2", "express": "^4.19.2", "lit": "^3.3.0", + "mime-types": "^3.0.1", "signal-exit": "^4.1.0", "ws": "^8.18.2" }, @@ -2779,6 +2781,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.0.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", @@ -3615,6 +3623,27 @@ "node": ">= 0.6" } }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -6356,6 +6385,29 @@ "node": ">= 6" } }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -8846,21 +8898,21 @@ } }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" @@ -11586,6 +11638,27 @@ "node": ">= 0.6" } }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-query-selector": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", diff --git a/web/package.json b/web/package.json index 713c7679..9069aacc 100644 --- a/web/package.json +++ b/web/package.json @@ -34,10 +34,12 @@ }, "dependencies": { "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0", + "@types/mime-types": "^3.0.1", "@xterm/headless": "^5.5.0", "chalk": "^4.1.2", "express": "^4.19.2", "lit": "^3.3.0", + "mime-types": "^3.0.1", "signal-exit": "^4.1.0", "ws": "^8.18.2" }, diff --git a/web/src/client/app.ts b/web/src/client/app.ts index 38d689cd..377f1077 100644 --- a/web/src/client/app.ts +++ b/web/src/client/app.ts @@ -8,6 +8,7 @@ import './components/session-create-form.js'; import './components/session-list.js'; import './components/session-view.js'; import './components/session-card.js'; +import './components/file-browser-enhanced.js'; import type { Session } from './components/session-list.js'; import type { SessionCard } from './components/session-card.js'; @@ -27,6 +28,7 @@ export class VibeTunnelApp extends LitElement { @state() private selectedSessionId: string | null = null; @state() private hideExited = this.loadHideExitedState(); @state() private showCreateModal = false; + @state() private showFileBrowser = false; private hotReloadWs: WebSocket | null = null; private errorTimeoutId: number | null = null; @@ -38,6 +40,7 @@ export class VibeTunnelApp extends LitElement { this.loadSessions(); this.startAutoRefresh(); this.setupRouting(); + this.setupKeyboardShortcuts(); } disconnectedCallback() { @@ -47,6 +50,20 @@ export class VibeTunnelApp extends LitElement { } // Clean up routing listeners window.removeEventListener('popstate', this.handlePopState); + // Clean up keyboard shortcuts + window.removeEventListener('keydown', this.handleKeyDown); + } + + private handleKeyDown = (e: KeyboardEvent) => { + // Handle Cmd+O / Ctrl+O to open file browser + if ((e.metaKey || e.ctrlKey) && e.key === 'o' && this.currentView === 'list') { + e.preventDefault(); + this.showFileBrowser = true; + } + }; + + private setupKeyboardShortcuts() { + window.addEventListener('keydown', this.handleKeyDown); } private showError(message: string) { @@ -452,6 +469,7 @@ export class VibeTunnelApp extends LitElement { @hide-exited-change=${this.handleHideExitedChange} @kill-all-sessions=${this.handleKillAll} @clean-exited-sessions=${this.handleCleanExited} + @open-file-browser=${() => (this.showFileBrowser = true)} > `} + + + (this.showFileBrowser = false)} + > `; } } diff --git a/web/src/client/components/app-header.ts b/web/src/client/components/app-header.ts index ca61893d..e9ab44b9 100644 --- a/web/src/client/components/app-header.ts +++ b/web/src/client/components/app-header.ts @@ -46,6 +46,10 @@ export class AppHeader extends LitElement { this.dispatchEvent(new CustomEvent('clean-exited-sessions')); } + private handleOpenFileBrowser() { + this.dispatchEvent(new CustomEvent('open-file-browser')); + } + render() { const runningSessions = this.sessions.filter((session) => session.status === 'running'); const exitedSessions = this.sessions.filter((session) => session.status === 'exited'); @@ -116,6 +120,13 @@ export class AppHeader extends LitElement {
+ + + ${this.gitStatus + ? html` +
+ 📍 ${this.gitStatus.branch} +
+ ` + : ''} +
+ +
+ 📂 ${this.currentPath} + +
+ +
+
+ ${this.loading + ? html`
Loading...
` + : html` + ${this.currentPath !== '.' && this.currentPath !== '/' + ? html` +
+ ⬆️ + .. +
+ ` + : ''} + ${this.files.map( + (file) => html` +
this.handleFileClick(file)} + > + ${this.renderFileIcon(file)} + ${file.name} + ${this.renderGitStatus(file.gitStatus)} +
+ ` + )} + `} +
+ +
+ ${this.selectedFile + ? html` +
+
+ ${this.renderFileIcon(this.selectedFile)} + ${this.selectedFile.name} + ${this.renderGitStatus(this.selectedFile.gitStatus)} +
+ ${this.selectedFile.gitStatus && this.selectedFile.gitStatus !== 'unchanged' + ? html` + + ` + : ''} +
+ ` + : ''} +
${this.renderPreview()}
+
+
+ + ${this.mode === 'select' + ? html` +
+ + +
+ ` + : ''} + + + `; + } + + disconnectedCallback() { + super.disconnectedCallback(); + document.removeEventListener('keydown', this.handleKeyDown); + if (this.monacoEditor) { + this.monacoEditor.dispose(); + this.monacoEditor = null; + } + } + + private handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && this.visible) { + e.preventDefault(); + this.handleCancel(); + } + }; +} diff --git a/web/src/client/components/file-browser-fab.ts b/web/src/client/components/file-browser-fab.ts new file mode 100644 index 00000000..b76d98a6 --- /dev/null +++ b/web/src/client/components/file-browser-fab.ts @@ -0,0 +1,99 @@ +import { LitElement, html, css } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; + +@customElement('file-browser-fab') +export class FileBrowserFAB extends LitElement { + static styles = css` + :host { + position: fixed; + bottom: 24px; + right: 24px; + z-index: 100; + } + + .fab { + width: 56px; + height: 56px; + border-radius: 50%; + background: #007acc; + color: white; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); + transition: all 0.3s ease; + } + + .fab:hover { + background: #005a9e; + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4); + transform: translateY(-2px); + } + + .fab:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + } + + .icon { + font-size: 24px; + } + + .tooltip { + position: absolute; + bottom: 100%; + right: 0; + margin-bottom: 8px; + background: #333; + color: white; + padding: 6px 12px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s; + } + + .fab:hover + .tooltip { + opacity: 1; + } + + @media (max-width: 768px) { + :host { + bottom: 16px; + right: 16px; + } + + .fab { + width: 48px; + height: 48px; + } + + .icon { + font-size: 20px; + } + } + `; + + @property({ type: Boolean }) visible = true; + + private handleClick() { + this.dispatchEvent(new CustomEvent('open-file-browser')); + } + + render() { + if (!this.visible) { + return html``; + } + + return html` + +
Browse Files (⌘O)
+ `; + } +} diff --git a/web/src/client/components/session-view.ts b/web/src/client/components/session-view.ts index c4f78ab4..2427e557 100644 --- a/web/src/client/components/session-view.ts +++ b/web/src/client/components/session-view.ts @@ -2,6 +2,8 @@ import { LitElement, PropertyValues, html } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import type { Session } from './session-list.js'; import './terminal.js'; +import './file-browser-fab.js'; +import './file-browser-enhanced.js'; import type { Terminal } from './terminal.js'; import { CastConverter } from '../utils/cast-converter.js'; import { @@ -35,6 +37,7 @@ export class SessionView extends LitElement { @state() private terminalMaxCols = 0; @state() private showWidthSelector = false; @state() private customWidth = ''; + @state() private showFileBrowser = false; private preferencesManager = TerminalPreferencesManager.getInstance(); @state() private reconnectCount = 0; @@ -48,6 +51,12 @@ export class SessionView extends LitElement { private lastResizeHeight = 0; private keyboardHandler = (e: KeyboardEvent) => { + // Handle Cmd+O / Ctrl+O to open file browser + if ((e.metaKey || e.ctrlKey) && e.key === 'o') { + e.preventDefault(); + this.showFileBrowser = true; + return; + } if (!this.session) return; // Allow important browser shortcuts to pass through @@ -847,6 +856,14 @@ export class SessionView extends LitElement { return commonWidth ? commonWidth.label : this.terminalMaxCols.toString(); } + private handleOpenFileBrowser() { + this.showFileBrowser = true; + } + + private handleCloseFileBrowser() { + this.showFileBrowser = false; + } + private async sendInputText(text: string) { if (!this.session) return; @@ -1359,6 +1376,19 @@ export class SessionView extends LitElement { ` : ''} + + + + + + `; } diff --git a/web/src/client/types/monaco.d.ts b/web/src/client/types/monaco.d.ts new file mode 100644 index 00000000..7f7db407 --- /dev/null +++ b/web/src/client/types/monaco.d.ts @@ -0,0 +1,8 @@ +declare global { + interface Window { + monaco: any; + require: any; + } +} + +export {}; diff --git a/web/src/index.ts b/web/src/index.ts index a19a98c5..e4fd6ceb 100644 --- a/web/src/index.ts +++ b/web/src/index.ts @@ -2,6 +2,7 @@ // Entry point for the server - imports the modular server which starts automatically import { startVibeTunnelForward } from './server/fwd.js'; import { startVibeTunnelServer } from './server/server.js'; +import { VERSION } from './server/version.js'; // Handle uncaught exceptions process.on('uncaughtException', (error) => { @@ -14,7 +15,10 @@ process.on('unhandledRejection', (reason, promise) => { process.exit(1); }); -if (process.argv[2] === 'fwd') { +if (process.argv[2] === 'version') { + console.log(`VibeTunnel Linux v${VERSION}`); + process.exit(0); +} else if (process.argv[2] === 'fwd') { startVibeTunnelForward(process.argv.slice(3)).catch((error) => { console.error('Fatal error:', error); process.exit(1); diff --git a/web/src/server/app.ts b/web/src/server/app.ts index 0a46bba5..39c45f38 100644 --- a/web/src/server/app.ts +++ b/web/src/server/app.ts @@ -13,6 +13,7 @@ import { HQClient } from './services/hq-client.js'; import { createAuthMiddleware } from './middleware/auth.js'; import { createSessionRoutes } from './routes/sessions.js'; import { createRemoteRoutes } from './routes/remotes.js'; +import { createFilesystemRoutes } from './routes/filesystem.js'; import { ControlDirWatcher } from './services/control-dir-watcher.js'; import { BufferAggregator } from './services/buffer-aggregator.js'; import { v4 as uuidv4 } from 'uuid'; @@ -354,6 +355,9 @@ export function createApp(): AppInstance { }) ); + // Mount filesystem routes + app.use('/api', createFilesystemRoutes()); + // WebSocket endpoint for buffer updates wss.on('connection', (ws, _req) => { if (bufferAggregator) { @@ -438,6 +442,7 @@ export function createApp(): AppInstance { remoteRegistry, isHQMode: config.isHQMode, hqClient, + ptyManager, }); controlDirWatcher.start(); }); diff --git a/web/src/server/routes/filesystem.ts b/web/src/server/routes/filesystem.ts new file mode 100644 index 00000000..fbcd23e7 --- /dev/null +++ b/web/src/server/routes/filesystem.ts @@ -0,0 +1,462 @@ +import { Router, Request, Response } from 'express'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import mime from 'mime-types'; +import { createReadStream, statSync } from 'fs'; + +const execAsync = promisify(exec); + +interface FileInfo { + name: string; + path: string; + type: 'file' | 'directory'; + size: number; + modified: string; + permissions?: string; + isGitTracked?: boolean; + gitStatus?: 'modified' | 'added' | 'deleted' | 'untracked' | 'unchanged'; +} + +interface GitStatus { + isGitRepo: boolean; + branch?: string; + modified: string[]; + added: string[]; + deleted: string[]; + untracked: string[]; +} + +export function createFilesystemRoutes(): Router { + const router = Router(); + + // Helper to check if path is safe (no directory traversal) + function isPathSafe(requestedPath: string, basePath: string): boolean { + const resolved = path.resolve(basePath, requestedPath); + return resolved.startsWith(path.resolve(basePath)); + } + + // Helper to get Git status for a directory + async function getGitStatus(dirPath: string): Promise { + try { + // Check if directory is a git repository + await execAsync('git rev-parse --git-dir', { cwd: dirPath }); + + // Get current branch + const { stdout: branch } = await execAsync('git branch --show-current', { cwd: dirPath }); + + // Get status + const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd: dirPath }); + + const status: GitStatus = { + isGitRepo: true, + branch: branch.trim(), + modified: [], + added: [], + deleted: [], + untracked: [], + }; + + // Parse git status output + statusOutput.split('\n').forEach((line) => { + if (!line) return; + + const statusCode = line.substring(0, 2); + const filename = line.substring(3); + + if (statusCode === ' M' || statusCode === 'M ' || statusCode === 'MM') { + status.modified.push(filename); + } else if (statusCode === 'A ' || statusCode === 'AM') { + status.added.push(filename); + } else if (statusCode === ' D' || statusCode === 'D ') { + status.deleted.push(filename); + } else if (statusCode === '??') { + status.untracked.push(filename); + } + }); + + return status; + } catch { + return null; + } + } + + // Helper to get file Git status + function getFileGitStatus(filePath: string, gitStatus: GitStatus | null): FileInfo['gitStatus'] { + if (!gitStatus) return undefined; + + const relativePath = path.relative(process.cwd(), filePath); + + if (gitStatus.modified.includes(relativePath)) return 'modified'; + if (gitStatus.added.includes(relativePath)) return 'added'; + if (gitStatus.deleted.includes(relativePath)) return 'deleted'; + if (gitStatus.untracked.includes(relativePath)) return 'untracked'; + + return 'unchanged'; + } + + // Browse directory endpoint + router.get('/fs/browse', async (req: Request, res: Response) => { + try { + const requestedPath = (req.query.path as string) || '.'; + const showHidden = req.query.showHidden === 'true'; + const gitFilter = req.query.gitFilter as string; // 'all' | 'changed' | 'none' + + // Security check + if (!isPathSafe(requestedPath, process.cwd())) { + return res.status(403).json({ error: 'Access denied' }); + } + + const fullPath = path.resolve(process.cwd(), requestedPath); + + // Check if path exists and is a directory + const stats = await fs.stat(fullPath); + if (!stats.isDirectory()) { + return res.status(400).json({ error: 'Path is not a directory' }); + } + + // Get Git status if requested + const gitStatus = gitFilter !== 'none' ? await getGitStatus(fullPath) : null; + + // Read directory contents + const entries = await fs.readdir(fullPath, { withFileTypes: true }); + + // Build file list + const files: FileInfo[] = await Promise.all( + entries + .filter((entry) => showHidden || !entry.name.startsWith('.')) + .map(async (entry) => { + const entryPath = path.join(fullPath, entry.name); + const stats = await fs.stat(entryPath); + const relativePath = path.relative(process.cwd(), entryPath); + + const fileInfo: FileInfo = { + name: entry.name, + path: relativePath, + type: entry.isDirectory() ? 'directory' : 'file', + size: stats.size, + modified: stats.mtime.toISOString(), + permissions: stats.mode.toString(8).slice(-3), + isGitTracked: gitStatus?.isGitRepo || false, + gitStatus: getFileGitStatus(entryPath, gitStatus), + }; + + return fileInfo; + }) + ); + + // Filter by Git status if requested + let filteredFiles = files; + if (gitFilter === 'changed' && gitStatus) { + filteredFiles = files.filter((file) => file.gitStatus && file.gitStatus !== 'unchanged'); + } + + // Sort: directories first, then by name + filteredFiles.sort((a, b) => { + if (a.type !== b.type) { + return a.type === 'directory' ? -1 : 1; + } + return a.name.localeCompare(b.name); + }); + + res.json({ + path: requestedPath, + fullPath, + gitStatus, + files: filteredFiles, + }); + } catch (error: any) { + console.error('Browse error:', error); + res.status(500).json({ error: error.message }); + } + }); + + // Get file preview + router.get('/fs/preview', async (req: Request, res: Response) => { + try { + const requestedPath = req.query.path as string; + if (!requestedPath) { + return res.status(400).json({ error: 'Path is required' }); + } + + // Security check + if (!isPathSafe(requestedPath, process.cwd())) { + return res.status(403).json({ error: 'Access denied' }); + } + + const fullPath = path.resolve(process.cwd(), requestedPath); + const stats = await fs.stat(fullPath); + + if (stats.isDirectory()) { + return res.status(400).json({ error: 'Cannot preview directories' }); + } + + // Determine file type + const mimeType = mime.lookup(fullPath) || 'application/octet-stream'; + const isText = + mimeType.startsWith('text/') || + mimeType === 'application/json' || + mimeType === 'application/javascript' || + mimeType === 'application/typescript' || + mimeType === 'application/xml'; + const isImage = mimeType.startsWith('image/'); + + if (isImage) { + // For images, return URL to fetch the image + res.json({ + type: 'image', + mimeType, + url: `/api/fs/raw?path=${encodeURIComponent(requestedPath)}`, + size: stats.size, + }); + } else if (isText || stats.size < 1024 * 1024) { + // Text or small files (< 1MB) + const content = await fs.readFile(fullPath, 'utf-8'); + const language = getLanguageFromPath(fullPath); + + res.json({ + type: 'text', + content, + language, + mimeType, + size: stats.size, + }); + } else { + // Binary or large files + res.json({ + type: 'binary', + mimeType, + size: stats.size, + humanSize: formatBytes(stats.size), + }); + } + } catch (error: any) { + console.error('Preview error:', error); + res.status(500).json({ error: error.message }); + } + }); + + // Serve raw file content + router.get('/fs/raw', (req: Request, res: Response) => { + try { + const requestedPath = req.query.path as string; + if (!requestedPath) { + return res.status(400).json({ error: 'Path is required' }); + } + + // Security check + if (!isPathSafe(requestedPath, process.cwd())) { + return res.status(403).json({ error: 'Access denied' }); + } + + const fullPath = path.resolve(process.cwd(), requestedPath); + + // Check if file exists + if (!statSync(fullPath).isFile()) { + return res.status(404).json({ error: 'File not found' }); + } + + // Set appropriate content type + const mimeType = mime.lookup(fullPath) || 'application/octet-stream'; + res.setHeader('Content-Type', mimeType); + + // Stream the file + const stream = createReadStream(fullPath); + stream.pipe(res); + } catch (error: any) { + console.error('Raw file error:', error); + res.status(500).json({ error: error.message }); + } + }); + + // Get file content (text files only) + router.get('/fs/content', async (req: Request, res: Response) => { + try { + const requestedPath = req.query.path as string; + if (!requestedPath) { + return res.status(400).json({ error: 'Path is required' }); + } + + // Security check + if (!isPathSafe(requestedPath, process.cwd())) { + return res.status(403).json({ error: 'Access denied' }); + } + + const fullPath = path.resolve(process.cwd(), requestedPath); + const content = await fs.readFile(fullPath, 'utf-8'); + + res.json({ + path: requestedPath, + content, + language: getLanguageFromPath(fullPath), + }); + } catch (error: any) { + console.error('Content error:', error); + res.status(500).json({ error: error.message }); + } + }); + + // Get Git diff for a file + router.get('/fs/diff', async (req: Request, res: Response) => { + try { + const requestedPath = req.query.path as string; + if (!requestedPath) { + return res.status(400).json({ error: 'Path is required' }); + } + + // Security check + if (!isPathSafe(requestedPath, process.cwd())) { + return res.status(403).json({ error: 'Access denied' }); + } + + const fullPath = path.resolve(process.cwd(), requestedPath); + const relativePath = path.relative(process.cwd(), fullPath); + + // Get git diff + const { stdout: diff } = await execAsync(`git diff HEAD -- "${relativePath}"`, { + cwd: process.cwd(), + }); + + res.json({ + path: requestedPath, + diff, + hasDiff: diff.length > 0, + }); + } catch (error: any) { + console.error('Diff error:', error); + res.status(500).json({ error: error.message }); + } + }); + + // Create directory + router.post('/fs/mkdir', async (req: Request, res: Response) => { + try { + const { path: dirPath, name } = req.body; + + if (!dirPath || !name) { + return res.status(400).json({ error: 'Path and name are required' }); + } + + // Validate name (no slashes, no dots at start) + if (name.includes('/') || name.includes('\\') || name.startsWith('.')) { + return res.status(400).json({ error: 'Invalid directory name' }); + } + + // Security check + if (!isPathSafe(dirPath, process.cwd())) { + return res.status(403).json({ error: 'Access denied' }); + } + + const fullPath = path.resolve(process.cwd(), dirPath, name); + + // Create directory + await fs.mkdir(fullPath, { recursive: true }); + + res.json({ + success: true, + path: path.relative(process.cwd(), fullPath), + }); + } catch (error: any) { + console.error('Mkdir error:', error); + res.status(500).json({ error: error.message }); + } + }); + + return router; +} + +// Helper function to determine language from file path +function getLanguageFromPath(filePath: string): string { + const ext = path.extname(filePath).toLowerCase(); + const languageMap: Record = { + '.js': 'javascript', + '.jsx': 'javascript', + '.ts': 'typescript', + '.tsx': 'typescript', + '.py': 'python', + '.java': 'java', + '.c': 'c', + '.cpp': 'cpp', + '.cs': 'csharp', + '.php': 'php', + '.rb': 'ruby', + '.go': 'go', + '.rs': 'rust', + '.swift': 'swift', + '.kt': 'kotlin', + '.scala': 'scala', + '.r': 'r', + '.m': 'objective-c', + '.mm': 'objective-c', + '.h': 'c', + '.hpp': 'cpp', + '.sh': 'shell', + '.bash': 'shell', + '.zsh': 'shell', + '.fish': 'shell', + '.ps1': 'powershell', + '.html': 'html', + '.htm': 'html', + '.xml': 'xml', + '.css': 'css', + '.scss': 'scss', + '.sass': 'sass', + '.less': 'less', + '.json': 'json', + '.yaml': 'yaml', + '.yml': 'yaml', + '.toml': 'toml', + '.ini': 'ini', + '.cfg': 'ini', + '.conf': 'ini', + '.sql': 'sql', + '.md': 'markdown', + '.markdown': 'markdown', + '.tex': 'latex', + '.dockerfile': 'dockerfile', + '.makefile': 'makefile', + '.cmake': 'cmake', + '.gradle': 'gradle', + '.vue': 'vue', + '.svelte': 'svelte', + '.elm': 'elm', + '.clj': 'clojure', + '.cljs': 'clojure', + '.ex': 'elixir', + '.exs': 'elixir', + '.erl': 'erlang', + '.hrl': 'erlang', + '.fs': 'fsharp', + '.fsx': 'fsharp', + '.fsi': 'fsharp', + '.ml': 'ocaml', + '.mli': 'ocaml', + '.pas': 'pascal', + '.pp': 'pascal', + '.pl': 'perl', + '.pm': 'perl', + '.t': 'perl', + '.lua': 'lua', + '.dart': 'dart', + '.nim': 'nim', + '.nims': 'nim', + '.zig': 'zig', + '.jl': 'julia', + }; + + return languageMap[ext] || 'plaintext'; +} + +// Helper function to format bytes +function formatBytes(bytes: number, decimals = 2): string { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} diff --git a/web/src/server/routes/sessions.ts b/web/src/server/routes/sessions.ts index 739a09bf..88eb0fb8 100644 --- a/web/src/server/routes/sessions.ts +++ b/web/src/server/routes/sessions.ts @@ -6,6 +6,7 @@ import { RemoteRegistry } from '../services/remote-registry.js'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; +import * as net from 'net'; interface SessionRoutesConfig { ptyManager: PtyManager; @@ -121,12 +122,47 @@ export function createSessionRoutes(config: SessionRoutesConfig): Router { // Create new session (local or on remote) router.post('/sessions', async (req, res) => { - const { command, workingDir, name, remoteId } = req.body; + const { command, workingDir, name, remoteId, spawn_terminal } = req.body; if (!command || !Array.isArray(command) || command.length === 0) { return res.status(400).json({ error: 'Command array is required' }); } + // If spawn_terminal is true, use the spawn-terminal logic + if (spawn_terminal) { + try { + // Generate session ID + const sessionId = generateSessionId(); + const sessionName = name || `session_${Date.now()}`; + + // Request Mac app to spawn terminal + const spawnResult = await requestTerminalSpawn({ + sessionId, + sessionName, + command, + workingDir: resolvePath(workingDir, process.cwd()), + }); + + if (!spawnResult.success) { + throw new Error(spawnResult.error || 'Failed to spawn terminal'); + } + + // Wait a bit for the session to be created + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Return the session ID - client will poll for the session to appear + res.json({ sessionId, message: 'Terminal spawn requested' }); + return; + } catch (error) { + console.error('Error spawning terminal:', error); + res.status(500).json({ + error: 'Failed to spawn terminal', + details: error instanceof Error ? error.message : 'Unknown error', + }); + return; + } + } + try { // If remoteId is specified and we're in HQ mode, forward to remote if (remoteId && isHQMode && remoteRegistry) { @@ -773,3 +809,85 @@ export function createSessionRoutes(config: SessionRoutesConfig): Router { return router; } + +// Generate a unique session ID +function generateSessionId(): string { + // Generate UUID v4 + const bytes = new Uint8Array(16); + for (let i = 0; i < 16; i++) { + bytes[i] = Math.floor(Math.random() * 256); + } + + // Set version (4) and variant bits + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + + // Convert to hex string with dashes + const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); + return [ + hex.slice(0, 8), + hex.slice(8, 12), + hex.slice(12, 16), + hex.slice(16, 20), + hex.slice(20, 32), + ].join('-'); +} + +// Request terminal spawn from Mac app +async function requestTerminalSpawn(params: { + sessionId: string; + sessionName: string; + command: string[]; + workingDir: string; +}): Promise<{ success: boolean; error?: string }> { + const socketPath = '/tmp/vibetunnel-terminal.sock'; + + // Check if socket exists + if (!fs.existsSync(socketPath)) { + return { + success: false, + error: 'Terminal spawn service not available. Is the Mac app running?', + }; + } + + const spawnRequest = { + workingDir: params.workingDir, + sessionId: params.sessionId, + command: params.command.join(' '), + terminal: null, // Let Mac app use default terminal + }; + + return new Promise((resolve) => { + const client = net.createConnection(socketPath, () => { + console.log(`Connected to terminal spawn service for session ${params.sessionId}`); + client.write(JSON.stringify(spawnRequest)); + }); + + client.on('data', (data) => { + try { + const response = JSON.parse(data.toString()); + console.log(`Terminal spawn response:`, response); + resolve({ success: response.success, error: response.error }); + } catch (error) { + console.error('Failed to parse terminal spawn response:', error); + resolve({ success: false, error: 'Invalid response from terminal spawn service' }); + } + client.end(); + }); + + client.on('error', (error) => { + console.error('Failed to connect to terminal spawn service:', error); + resolve({ + success: false, + error: `Connection failed: ${error.message}`, + }); + }); + + client.on('timeout', () => { + client.destroy(); + resolve({ success: false, error: 'Terminal spawn request timed out' }); + }); + + client.setTimeout(5000); // 5 second timeout + }); +} diff --git a/web/src/server/server.ts b/web/src/server/server.ts index 10754927..e8bf82f1 100644 --- a/web/src/server/server.ts +++ b/web/src/server/server.ts @@ -70,8 +70,7 @@ export function startVibeTunnelServer() { // When running with tsx, the main module check is different // NOTE: When bundled as 'vibetunnel' executable, index.ts handles the startup const isMainModule = - process.argv[1]?.endsWith('server.ts') || - process.argv[1]?.endsWith('server/index.ts'); + process.argv[1]?.endsWith('server.ts') || process.argv[1]?.endsWith('server/index.ts'); if (isMainModule) { startVibeTunnelServer(); } diff --git a/web/src/server/services/control-dir-watcher.ts b/web/src/server/services/control-dir-watcher.ts index 5e26995d..631cc08d 100644 --- a/web/src/server/services/control-dir-watcher.ts +++ b/web/src/server/services/control-dir-watcher.ts @@ -4,12 +4,14 @@ import chalk from 'chalk'; import { RemoteRegistry } from './remote-registry.js'; import { HQClient } from './hq-client.js'; import { isShuttingDown } from './shutdown-state.js'; +import { PtyManager } from '../pty/index.js'; interface ControlDirWatcherConfig { controlDir: string; remoteRegistry: RemoteRegistry | null; isHQMode: boolean; hqClient: HQClient | null; + ptyManager?: PtyManager; } export class ControlDirWatcher { @@ -57,6 +59,17 @@ export class ControlDirWatcher { console.log(chalk.blue(`Detected new external session: ${sessionId}`)); + // Check if PtyManager already knows about this session + if (this.config.ptyManager) { + const existingSession = this.config.ptyManager.getSession(sessionId); + if (!existingSession) { + // This is a new external session, PtyManager needs to track it + console.log(chalk.green(`Attaching to external session: ${sessionId}`)); + // PtyManager will pick it up through its own session listing + // since it reads from the control directory + } + } + // If we're a remote server registered with HQ, immediately notify HQ if (this.config.hqClient && !isShuttingDown()) { try { diff --git a/web/src/server/version.ts b/web/src/server/version.ts index 30246ea4..768d0a32 100644 --- a/web/src/server/version.ts +++ b/web/src/server/version.ts @@ -1,7 +1,7 @@ // Version information for VibeTunnel Server // This file is updated during the build process -export const VERSION = '1.0.0'; +export const VERSION = '1.0.0-beta.3'; // BUILD_DATE will be replaced by build script, fallback to current time in dev export const BUILD_DATE = process.env.BUILD_DATE || new Date().toISOString(); export const BUILD_TIMESTAMP = process.env.BUILD_TIMESTAMP || Date.now(); @@ -31,4 +31,4 @@ export function printVersionBanner() { console.log(`Built: ${BUILD_DATE}`); console.log(`Platform: ${PLATFORM}/${ARCH} Node ${NODE_VERSION}`); console.log(`PID: ${process.pid}`); -} \ No newline at end of file +}