diff --git a/package-lock.json b/package-lock.json index 8c90ef10f..dac19fcf8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36381,14 +36381,14 @@ }, "packages/app/node_modules/expo-clipboard": { "version": "8.0.7", + "resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-8.0.7.tgz", + "integrity": "sha512-zvlfFV+wB2QQrQnHWlo0EKHAkdi2tycLtE+EXFUWTPZYkgu1XcH+aiKfd4ul7Z0SDF+1IuwoiW9AA9eO35aj3Q==", "license": "MIT", "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" - }, - "resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-8.0.7.tgz", - "integrity": "sha512-zvlfFV+wB2QQrQnHWlo0EKHAkdi2tycLtE+EXFUWTPZYkgu1XcH+aiKfd4ul7Z0SDF+1IuwoiW9AA9eO35aj3Q==" + } }, "packages/app/node_modules/react-native-nitro-modules": { "version": "0.33.8", @@ -36402,12 +36402,12 @@ }, "packages/app/node_modules/zod": { "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" - }, - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" + } }, "packages/cli": { "name": "@getpaseo/cli", @@ -36435,24 +36435,24 @@ }, "packages/cli/node_modules/chalk": { "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" - }, - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==" + } }, "packages/cli/node_modules/commander": { "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "license": "MIT", "engines": { "node": ">=18" - }, - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==" + } }, "packages/desktop": { "name": "@getpaseo/desktop", @@ -36802,6 +36802,8 @@ }, "packages/server/node_modules/@modelcontextprotocol/sdk": { "version": "1.20.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.1.tgz", + "integrity": "sha512-j/P+yuxXfgxb+mW7OEoRCM3G47zCTDqUPivJo/VzpjbG8I9csTXtOprCf5FfOfHK4whOJny0aHuBEON+kS7CCA==", "license": "MIT", "dependencies": { "ajv": "^6.12.6", @@ -36819,12 +36821,12 @@ }, "engines": { "node": ">=18" - }, - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.1.tgz", - "integrity": "sha512-j/P+yuxXfgxb+mW7OEoRCM3G47zCTDqUPivJo/VzpjbG8I9csTXtOprCf5FfOfHK4whOJny0aHuBEON+kS7CCA==" + } }, "packages/server/node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -36835,12 +36837,12 @@ "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" - }, - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==" + } }, "packages/server/node_modules/@modelcontextprotocol/sdk/node_modules/express": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", @@ -36877,9 +36879,7 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/express" - }, - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==" + } }, "packages/server/node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { "version": "0.4.1", @@ -36889,6 +36889,8 @@ }, "packages/server/node_modules/accepts": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "dependencies": { "mime-types": "^3.0.0", @@ -36896,12 +36898,12 @@ }, "engines": { "node": ">= 0.6" - }, - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==" + } }, "packages/server/node_modules/ajv": { "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -36912,24 +36914,24 @@ "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" - }, - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==" + } }, "packages/server/node_modules/ansi-regex": { "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-regex?sponsor=1" - }, - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==" + } }, "packages/server/node_modules/body-parser": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", @@ -36944,33 +36946,33 @@ }, "engines": { "node": ">=18" - }, - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==" + } }, "packages/server/node_modules/content-disposition": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" }, "engines": { "node": ">= 0.6" - }, - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==" + } }, "packages/server/node_modules/cookie-signature": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", "engines": { "node": ">=6.6.0" - }, - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==" + } }, "packages/server/node_modules/finalhandler": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -36982,60 +36984,58 @@ }, "engines": { "node": ">= 0.8" - }, - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==" + } }, "packages/server/node_modules/fresh": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", "engines": { "node": ">= 0.8" - }, - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==" + } }, "packages/server/node_modules/media-typer": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", "engines": { "node": ">= 0.8" - }, - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" + } }, "packages/server/node_modules/merge-descriptors": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", "engines": { "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" - }, - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==" + } }, "packages/server/node_modules/mime-types": { "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.54.0" }, "engines": { "node": ">= 0.6" - }, - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==" + } }, "packages/server/node_modules/negotiator": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { "node": ">= 0.6" - }, - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==" + } }, "packages/server/node_modules/raw-body": { "version": "3.0.2", @@ -37070,6 +37070,8 @@ }, "packages/server/node_modules/send": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "license": "MIT", "dependencies": { "debug": "^4.3.5", @@ -37086,12 +37088,12 @@ }, "engines": { "node": ">= 18" - }, - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==" + } }, "packages/server/node_modules/serve-static": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", @@ -37101,12 +37103,12 @@ }, "engines": { "node": ">= 18" - }, - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==" + } }, "packages/server/node_modules/strip-ansi": { "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -37116,12 +37118,12 @@ }, "funding": { "url": "https://github.com/chalk/strip-ansi?sponsor=1" - }, - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==" + } }, "packages/server/node_modules/type-is": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { "content-type": "^1.0.5", @@ -37130,18 +37132,16 @@ }, "engines": { "node": ">= 0.6" - }, - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==" + } }, "packages/server/node_modules/zod": { "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" - }, - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" + } }, "packages/website": { "name": "@getpaseo/website", @@ -37172,13 +37172,13 @@ }, "packages/website/node_modules/@types/node": { "version": "22.19.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.6.tgz", + "integrity": "sha512-qm+G8HuG6hOHQigsi7VGuLjUVu6TtBo/F05zvX04Mw2uCg9Dv0Qxy3Qw7j41SidlTcl5D/5yg0SEZqOB+EqZnQ==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" - }, - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.6.tgz", - "integrity": "sha512-qm+G8HuG6hOHQigsi7VGuLjUVu6TtBo/F05zvX04Mw2uCg9Dv0Qxy3Qw7j41SidlTcl5D/5yg0SEZqOB+EqZnQ==" + } }, "packages/website/node_modules/react": { "version": "19.2.4", diff --git a/packages/app/src/components/workspace-hover-card.tsx b/packages/app/src/components/workspace-hover-card.tsx index f66bc75c0..af1019074 100644 --- a/packages/app/src/components/workspace-hover-card.tsx +++ b/packages/app/src/components/workspace-hover-card.tsx @@ -205,7 +205,7 @@ function HoverCardStatusIndicator({ } const KindIcon = - workspace.workspaceKind === "local_checkout" + workspace.workspaceKind === "checkout" ? Monitor : workspace.workspaceKind === "worktree" ? FolderGit2 @@ -251,7 +251,7 @@ function WorkspaceHoverCardContent({ const prHint = useWorkspacePrHint({ serverId: workspace.serverId, cwd: workspace.workspaceId, - enabled: workspace.workspaceKind !== "directory", + enabled: workspace.projectKind !== "directory", }); // Measure trigger — same pattern as tooltip.tsx diff --git a/packages/app/src/contexts/session-context.service-status.test.ts b/packages/app/src/contexts/session-context.service-status.test.ts index 617a0f024..e0851dec9 100644 --- a/packages/app/src/contexts/session-context.service-status.test.ts +++ b/packages/app/src/contexts/session-context.service-status.test.ts @@ -12,8 +12,9 @@ function workspace(input: { projectId: "project-1", projectDisplayName: "Project 1", projectRootPath: "/repo", + workspaceDirectory: input.id, projectKind: "git", - workspaceKind: "local_checkout", + workspaceKind: "checkout", name: "main", status: "running", activityAt: null, diff --git a/packages/app/src/desktop/permissions/desktop-permissions.test.ts b/packages/app/src/desktop/permissions/desktop-permissions.test.ts index a62162835..5db79afd5 100644 --- a/packages/app/src/desktop/permissions/desktop-permissions.test.ts +++ b/packages/app/src/desktop/permissions/desktop-permissions.test.ts @@ -5,16 +5,33 @@ type MockPlatform = "web" | "ios" | "android"; type GlobalSnapshot = { Notification: unknown; navigatorDescriptor?: PropertyDescriptor; + windowDescriptor?: PropertyDescriptor; paseoDesktop: unknown; }; const originalGlobals: GlobalSnapshot = { Notification: (globalThis as { Notification?: unknown }).Notification, navigatorDescriptor: Object.getOwnPropertyDescriptor(globalThis, "navigator"), + windowDescriptor: Object.getOwnPropertyDescriptor(globalThis, "window"), paseoDesktop: typeof window === "undefined" ? undefined : (window as { paseoDesktop?: unknown }).paseoDesktop, }; +function ensureWindow(): { paseoDesktop?: unknown } { + const existingWindow = (globalThis as { window?: { paseoDesktop?: unknown } }).window; + if (existingWindow) { + return existingWindow; + } + + const nextWindow: { paseoDesktop?: unknown } = {}; + Object.defineProperty(globalThis, "window", { + configurable: true, + writable: true, + value: nextWindow, + }); + return nextWindow; +} + function setNavigator(value: unknown): void { Object.defineProperty(globalThis, "navigator", { configurable: true, @@ -32,6 +49,12 @@ function restoreGlobals(): void { delete (globalThis as { navigator?: unknown }).navigator; } + if (originalGlobals.windowDescriptor) { + Object.defineProperty(globalThis, "window", originalGlobals.windowDescriptor); + } else { + delete (globalThis as { window?: unknown }).window; + } + if (typeof window !== "undefined") { (window as { paseoDesktop?: unknown }).paseoDesktop = originalGlobals.paseoDesktop; } @@ -56,7 +79,7 @@ describe("desktop-permissions", () => { expect(shouldShowDesktopPermissionSection()).toBe(false); - (window as { paseoDesktop?: unknown }).paseoDesktop = {}; + ensureWindow().paseoDesktop = {}; expect(shouldShowDesktopPermissionSection()).toBe(true); }); diff --git a/packages/app/src/hooks/use-agent-form-state.test.ts b/packages/app/src/hooks/use-agent-form-state.test.ts index 6cb7f5128..d909d817c 100644 --- a/packages/app/src/hooks/use-agent-form-state.test.ts +++ b/packages/app/src/hooks/use-agent-form-state.test.ts @@ -221,7 +221,7 @@ describe("useAgentFormState", () => { expect(resolved.thinkingOptionId).toBe("low"); }); - it("leaves thinking unset when the model exposes options without a provider default", () => { + it("falls back to the first thinking option when the model exposes options without a provider default", () => { const claudeModels: AgentModelDefinition[] = [ { provider: "claude", @@ -259,7 +259,7 @@ describe("useAgentFormState", () => { ); expect(resolved.model).toBe("default"); - expect(resolved.thinkingOptionId).toBe(""); + expect(resolved.thinkingOptionId).toBe("low"); }); it("resolves provider only from allowed provider map", () => { diff --git a/packages/app/src/hooks/use-agent-input-draft.test.ts b/packages/app/src/hooks/use-agent-input-draft.test.ts index a7590bf2f..75cd673d8 100644 --- a/packages/app/src/hooks/use-agent-input-draft.test.ts +++ b/packages/app/src/hooks/use-agent-input-draft.test.ts @@ -1,4 +1,49 @@ -import { beforeAll, describe, expect, it } from "vitest"; +import { beforeAll, describe, expect, it, vi } from "vitest"; + +vi.mock("@react-native-async-storage/async-storage", () => ({ + default: { + getItem: async () => null, + setItem: async () => undefined, + removeItem: async () => undefined, + }, +})); + +vi.mock("@/attachments/service", () => ({ + garbageCollectAttachments: async () => undefined, +})); + +vi.mock("./use-agent-form-state", () => ({ + useAgentFormState: () => ({ + selectedServerId: "host-1", + setSelectedServerId: () => undefined, + setSelectedServerIdFromUser: () => undefined, + selectedProvider: "codex", + setProviderFromUser: () => undefined, + selectedMode: "auto", + setModeFromUser: () => undefined, + selectedModel: "", + setModelFromUser: () => undefined, + selectedThinkingOptionId: "", + setThinkingOptionFromUser: () => undefined, + workingDir: "/repo", + setWorkingDir: () => undefined, + setWorkingDirFromUser: () => undefined, + providerDefinitions: [{ id: "codex", label: "Codex", modes: [{ id: "auto", label: "Auto" }] }], + providerDefinitionMap: new Map(), + agentDefinition: undefined, + modeOptions: [{ id: "auto", label: "Auto" }], + availableModels: [], + allProviderModels: new Map(), + isAllModelsLoading: false, + availableThinkingOptions: [], + isModelLoading: false, + modelError: null, + refreshProviderModels: () => undefined, + setProviderAndModelFromUser: () => undefined, + workingDirIsEmpty: false, + persistFormPreferences: async () => undefined, + }), +})); let __private__: typeof import("./use-agent-input-draft").__private__; diff --git a/packages/app/src/hooks/use-archive-agent.test.ts b/packages/app/src/hooks/use-archive-agent.test.ts index 173aa9ba1..74f848e96 100644 --- a/packages/app/src/hooks/use-archive-agent.test.ts +++ b/packages/app/src/hooks/use-archive-agent.test.ts @@ -10,6 +10,7 @@ function makeAgent(overrides: Partial = {}): Agent { serverId: "server-a", id: "agent-1", provider: "codex", + terminal: false, status: "running", createdAt: new Date("2026-04-01T03:00:00.000Z"), updatedAt: new Date("2026-04-01T03:00:00.000Z"), @@ -22,6 +23,7 @@ function makeAgent(overrides: Partial = {}): Agent { supportsMcpServers: true, supportsReasoningStream: true, supportsToolInvocations: true, + supportsTerminalMode: false, }, currentModeId: null, availableModes: [], diff --git a/packages/app/src/hooks/use-open-project.test.ts b/packages/app/src/hooks/use-open-project.test.ts index a39789998..e28d6579e 100644 --- a/packages/app/src/hooks/use-open-project.test.ts +++ b/packages/app/src/hooks/use-open-project.test.ts @@ -76,6 +76,7 @@ describe("openProjectDirectly", () => { status: "done" as const, activityAt: null, diffStat: null, + services: [], }, })), }, diff --git a/packages/app/src/keyboard/keyboard-shortcuts.test.ts b/packages/app/src/keyboard/keyboard-shortcuts.test.ts index 21bc67c72..605803d9f 100644 --- a/packages/app/src/keyboard/keyboard-shortcuts.test.ts +++ b/packages/app/src/keyboard/keyboard-shortcuts.test.ts @@ -261,10 +261,10 @@ describe("keyboard-shortcuts", () => { action: "sidebar.toggle.left", }, { - name: "keeps Mod+. as sidebar toggle fallback", + name: "binds Mod+. to toggle both sidebars on non-mac", event: { key: ".", code: "Period", ctrlKey: true }, context: { isMac: false }, - action: "sidebar.toggle.left", + action: "sidebar.toggle.both", }, { name: "routes Mod+D to message-input action outside terminal", @@ -344,11 +344,6 @@ describe("keyboard-shortcuts", () => { event: { key: "k", code: "KeyK", ctrlKey: true }, context: { isMac: false, focusScope: "terminal" }, }, - { - name: "does not bind Ctrl+B on non-mac", - event: { key: "b", code: "KeyB", ctrlKey: true }, - context: { isMac: false }, - }, { name: "does not route message-input actions when terminal is focused", event: { key: "d", code: "KeyD", metaKey: true }, @@ -477,10 +472,11 @@ describe("keyboard-shortcut help sections", () => { }, }, { - name: "uses mod+period as non-mac left sidebar shortcut", + name: "uses mod+b for the left sidebar and mod+period for both sidebars on non-mac", context: { isMac: false, isDesktop: false }, expectedKeys: { - "toggle-left-sidebar": ["mod", "."], + "toggle-left-sidebar": ["mod", "B"], + "toggle-both-sidebars": ["mod", "."], }, }, ]; diff --git a/packages/app/src/runtime/host-runtime.test.ts b/packages/app/src/runtime/host-runtime.test.ts index a1f88849e..5d4ff7fe1 100644 --- a/packages/app/src/runtime/host-runtime.test.ts +++ b/packages/app/src/runtime/host-runtime.test.ts @@ -540,44 +540,37 @@ describe("HostRuntimeController", () => { unsubscribe(); }); - it("logs typed reason codes for connection transitions", async () => { - const infoSpy = vi.spyOn(console, "info").mockImplementation(() => undefined); - try { - const host = makeHost({ - connections: [ - { - id: "direct:lan:6767", - type: "directTcp", - endpoint: "lan:6767", - }, - ], - }); - const clients: FakeDaemonClient[] = []; - const controller = new HostRuntimeController({ - host, - deps: makeDeps( - { - "direct:lan:6767": 12, - }, - clients, - ), - }); + it("preserves transport disconnect reasons on the runtime snapshot", async () => { + const host = makeHost({ + connections: [ + { + id: "direct:lan:6767", + type: "directTcp", + endpoint: "lan:6767", + }, + ], + }); + const clients: FakeDaemonClient[] = []; + const controller = new HostRuntimeController({ + host, + deps: makeDeps( + { + "direct:lan:6767": 12, + }, + clients, + ), + }); - await controller.start({ autoProbe: false }); - clients[0]?.setConnectionState({ - status: "disconnected", - reason: "transport closed", - }); + await controller.start({ autoProbe: false }); + clients[0]?.setConnectionState({ + status: "disconnected", + reason: "transport closed", + }); - const transitionPayloads = infoSpy.mock.calls - .filter((call) => call[0] === "[HostRuntimeTransition]") - .map((call) => call[1] as { reasonCode?: string | null }); - const lastTransition = transitionPayloads[transitionPayloads.length - 1] ?? null; - - expect(lastTransition?.reasonCode).toBe("transport_error"); - } finally { - infoSpy.mockRestore(); - } + expect(controller.getSnapshot()).toMatchObject({ + connectionStatus: "error", + lastError: "transport closed", + }); }); it("marks directory loading on first connection before any directory sync succeeds", async () => { @@ -1189,6 +1182,7 @@ describe("HostRuntimeStore", () => { const staleAgent: Agent = { ...stale, serverId: host.serverId, + terminal: false, createdAt: new Date(stale.createdAt), updatedAt: new Date(stale.updatedAt), lastUserMessageAt: null, diff --git a/packages/app/src/stores/provider-recency-store.test.ts b/packages/app/src/stores/provider-recency-store.test.ts index 8ecbebb44..f9476f56b 100644 --- a/packages/app/src/stores/provider-recency-store.test.ts +++ b/packages/app/src/stores/provider-recency-store.test.ts @@ -33,7 +33,12 @@ describe("provider-recency-store", () => { it("sorts used providers first and keeps unused providers in default order", () => { const sorted = sortProvidersByRecency(AGENT_PROVIDER_DEFINITIONS, ["codex"]); - expect(sorted.map((provider) => provider.id)).toEqual(["codex", "claude", "opencode"]); + expect(sorted.map((provider) => provider.id)).toEqual([ + "codex", + ...AGENT_PROVIDER_DEFINITIONS.filter((provider) => provider.id !== "codex").map( + (provider) => provider.id, + ), + ]); }); it("moves the latest provider to the front without duplicating prior entries", () => { diff --git a/packages/app/src/stores/session-store.test.ts b/packages/app/src/stores/session-store.test.ts index 3a6ae4955..ce0bf67c5 100644 --- a/packages/app/src/stores/session-store.test.ts +++ b/packages/app/src/stores/session-store.test.ts @@ -15,8 +15,9 @@ function workspace( projectId: input.projectId ?? "project-1", projectDisplayName: input.projectDisplayName ?? "Project 1", projectRootPath: input.projectRootPath ?? "/repo", + workspaceDirectory: input.workspaceDirectory ?? "/repo", projectKind: input.projectKind ?? "git", - workspaceKind: input.workspaceKind ?? "local_checkout", + workspaceKind: input.workspaceKind ?? "checkout", name: input.name ?? "main", status: input.status ?? "done", activityAt: input.activityAt ?? null, @@ -41,12 +42,13 @@ describe("normalizeWorkspaceDescriptor", () => { }, ]; const workspace = normalizeWorkspaceDescriptor({ - id: "/repo/main", - projectId: "project-1", + id: 1, + projectId: 1, projectDisplayName: "Project 1", projectRootPath: "/repo", + workspaceDirectory: "/repo", projectKind: "git", - workspaceKind: "local_checkout", + workspaceKind: "checkout", name: "main", status: "running", activityAt: "not-a-date", @@ -69,12 +71,13 @@ describe("normalizeWorkspaceDescriptor", () => { it("defaults missing services to an empty array", () => { const payload = { - id: "/repo/main", - projectId: "project-1", + id: 1, + projectId: 1, projectDisplayName: "Project 1", projectRootPath: "/repo", + workspaceDirectory: "/repo", projectKind: "git", - workspaceKind: "local_checkout", + workspaceKind: "checkout", name: "main", status: "done", activityAt: null, diff --git a/packages/app/src/terminal/runtime/terminal-emulator-runtime.test.ts b/packages/app/src/terminal/runtime/terminal-emulator-runtime.test.ts index ecd7d7be0..504a5a52d 100644 --- a/packages/app/src/terminal/runtime/terminal-emulator-runtime.test.ts +++ b/packages/app/src/terminal/runtime/terminal-emulator-runtime.test.ts @@ -1,5 +1,48 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +vi.mock("@xterm/addon-clipboard", () => ({ + ClipboardAddon: class ClipboardAddon {}, +})); + +vi.mock("@xterm/addon-fit", () => ({ + FitAddon: class FitAddon {}, +})); + +vi.mock("@xterm/addon-image", () => ({ + ImageAddon: class ImageAddon {}, +})); + +vi.mock("@xterm/addon-ligatures/lib/addon-ligatures.mjs", () => ({ + LigaturesAddon: class LigaturesAddon {}, +})); + +vi.mock("@xterm/addon-search", () => ({ + SearchAddon: class SearchAddon {}, +})); + +vi.mock("@xterm/addon-unicode11", () => ({ + Unicode11Addon: class Unicode11Addon {}, +})); + +vi.mock("@xterm/addon-web-links", () => ({ + WebLinksAddon: class WebLinksAddon {}, +})); + +vi.mock("@xterm/addon-webgl", () => ({ + WebglAddon: class WebglAddon { + onContextLoss(): void {} + dispose(): void {} + }, +})); + +vi.mock("@xterm/xterm", () => ({ + Terminal: class Terminal {}, +})); + +vi.mock("@/utils/open-external-url", () => ({ + openExternalUrl: vi.fn(), +})); + import { TerminalEmulatorRuntime } from "./terminal-emulator-runtime"; type StubTerminal = { diff --git a/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts b/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts index e2a4b06f0..b3a055dfc 100644 --- a/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts +++ b/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts @@ -1,11 +1,11 @@ import { ClipboardAddon } from "@xterm/addon-clipboard"; import { FitAddon } from "@xterm/addon-fit"; import { ImageAddon } from "@xterm/addon-image"; -import { LigaturesAddon } from "@xterm/addon-ligatures"; import { SearchAddon } from "@xterm/addon-search"; import { Unicode11Addon } from "@xterm/addon-unicode11"; import { WebLinksAddon } from "@xterm/addon-web-links"; import { WebglAddon } from "@xterm/addon-webgl"; +import { LigaturesAddon } from "@xterm/addon-ligatures/lib/addon-ligatures.mjs"; import { Terminal, type ITheme } from "@xterm/xterm"; import type { TerminalState } from "@server/shared/messages"; import { openExternalUrl } from "@/utils/open-external-url"; diff --git a/packages/app/src/types/xterm-addon-ligatures.d.ts b/packages/app/src/types/xterm-addon-ligatures.d.ts new file mode 100644 index 000000000..f234c995b --- /dev/null +++ b/packages/app/src/types/xterm-addon-ligatures.d.ts @@ -0,0 +1,7 @@ +declare module "@xterm/addon-ligatures/lib/addon-ligatures.mjs" { + export class LigaturesAddon { + constructor(); + activate(terminal: unknown): void; + dispose(): void; + } +} diff --git a/packages/app/src/utils/test-daemon-connection.test.ts b/packages/app/src/utils/test-daemon-connection.test.ts index d0b148575..b33819c81 100644 --- a/packages/app/src/utils/test-daemon-connection.test.ts +++ b/packages/app/src/utils/test-daemon-connection.test.ts @@ -59,6 +59,19 @@ vi.mock("./client-id", () => ({ getOrCreateClientId: clientIdMock.getOrCreateClientId, })); +vi.mock("@/desktop/daemon/desktop-daemon-transport", () => ({ + createDesktopLocalDaemonTransportFactory: vi.fn(() => null), + buildLocalDaemonTransportUrl: vi.fn( + ({ + transportType, + transportPath, + }: { + transportType: "socket" | "pipe"; + transportPath: string; + }) => `paseo+local://${transportType}?path=${encodeURIComponent(transportPath)}`, + ), +})); + describe("test-daemon-connection connectToDaemon", () => { beforeEach(() => { daemonClientMock.createdConfigs.length = 0; diff --git a/packages/app/src/utils/tool-call-display.test.ts b/packages/app/src/utils/tool-call-display.test.ts index 572cc75f9..5c14ca365 100644 --- a/packages/app/src/utils/tool-call-display.test.ts +++ b/packages/app/src/utils/tool-call-display.test.ts @@ -154,7 +154,7 @@ describe("tool-call-display", () => { }); expect(display).toEqual({ - displayName: "Interacted with terminal", + displayName: "Terminal", }); }); @@ -171,7 +171,7 @@ describe("tool-call-display", () => { }); expect(display).toEqual({ - displayName: "Interacted with terminal", + displayName: "Terminal", summary: "npm run test", }); }); diff --git a/packages/app/src/utils/workspace-execution.test.ts b/packages/app/src/utils/workspace-execution.test.ts index 17b9a78cc..77121af04 100644 --- a/packages/app/src/utils/workspace-execution.test.ts +++ b/packages/app/src/utils/workspace-execution.test.ts @@ -22,6 +22,7 @@ function createWorkspace( status: input.status ?? "running", activityAt: input.activityAt ?? null, diffStat: input.diffStat ?? null, + services: input.services ?? [], }; } diff --git a/packages/app/src/utils/workspace-navigation.test.ts b/packages/app/src/utils/workspace-navigation.test.ts index a1ea1722d..afba7fedf 100644 --- a/packages/app/src/utils/workspace-navigation.test.ts +++ b/packages/app/src/utils/workspace-navigation.test.ts @@ -1,5 +1,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +vi.mock("expo-router", () => ({ + router: { + navigate: vi.fn(), + replace: vi.fn(), + }, +})); + vi.mock("@react-native-async-storage/async-storage", () => { const storage = new Map(); return { diff --git a/packages/app/src/voice/voice-runtime.test.ts b/packages/app/src/voice/voice-runtime.test.ts index 76b77d3d2..5afae5101 100644 --- a/packages/app/src/voice/voice-runtime.test.ts +++ b/packages/app/src/voice/voice-runtime.test.ts @@ -153,6 +153,7 @@ describe("voice runtime", () => { await runtime.startVoice("server-1", "agent-1"); runtime.onTurnEvent("server-1", "agent-1", "turn_started"); + vi.mocked(engine.play).mockClear(); runtime.handleAudioOutput( "server-1", @@ -204,6 +205,8 @@ describe("voice runtime", () => { await runtime.startVoice("server-1", "agent-1"); runtime.onTurnEvent("server-1", "agent-1", "turn_started"); + vi.mocked(engine.play).mockClear(); + playResolvers.length = 0; runtime.handleAudioOutput( "server-1", @@ -286,6 +289,8 @@ describe("voice runtime", () => { expect(runtime.getSnapshot().phase).toBe("waiting"); expect(engine.play).toHaveBeenCalledTimes(1); + vi.mocked(engine.stop).mockClear(); + vi.mocked(engine.clearQueue).mockClear(); runtime.handleCaptureVolume(REALTIME_VOICE_VAD_CONFIG.volumeThreshold + 0.05); runtime.handleCaptureVolume(0); @@ -325,6 +330,8 @@ describe("voice runtime", () => { await runtime.startVoice("server-1", "agent-1"); runtime.onTurnEvent("server-1", "agent-1", "turn_started"); runtime.onAssistantAudioStarted("server-1"); + vi.mocked(engine.stop).mockClear(); + vi.mocked(engine.clearQueue).mockClear(); runtime.handleCaptureVolume(0.5); expect(runtime.getTelemetrySnapshot().isSpeaking).toBe(false); diff --git a/packages/app/src/voice/voice-runtime.ts b/packages/app/src/voice/voice-runtime.ts index 5b26e9a99..8fbe3d60f 100644 --- a/packages/app/src/voice/voice-runtime.ts +++ b/packages/app/src/voice/voice-runtime.ts @@ -540,6 +540,7 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime { clearTimeout(cue.timeout); cue.timeout = null; } + cue.playing = false; if (hadActive) { deps.engine.stop(); deps.engine.clearQueue(); @@ -1025,6 +1026,8 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime { } if (state.turnInProgress) { + patchSnapshot((prev) => ({ ...prev, phase: "waiting" })); + reconcileCue(); return; } @@ -1060,9 +1063,15 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime { state.serverSpeechDetected = isSpeaking; state.serverSpeechStartedAt = isSpeaking ? (state.serverSpeechStartedAt ?? Date.now()) : null; if (isSpeaking) { + const shouldInterruptPlayback = + state.snapshot.phase === "playing" || playback.groups.size > 0; + const hadCue = cue.active || cue.timeout !== null || cue.playing; resetPlaybackState(); - deps.engine.stop(); - deps.engine.clearQueue(); + stopCue(); + if (shouldInterruptPlayback && !hadCue) { + deps.engine.stop(); + deps.engine.clearQueue(); + } getActiveSession()?.adapter.setAssistantAudioPlaying(false); } patchTelemetry((prev) => ({ diff --git a/packages/server/src/server/dictation/dictation-stream-manager.test.ts b/packages/server/src/server/dictation/dictation-stream-manager.test.ts index ab584b046..0695c21f8 100644 --- a/packages/server/src/server/dictation/dictation-stream-manager.test.ts +++ b/packages/server/src/server/dictation/dictation-stream-manager.test.ts @@ -266,6 +266,8 @@ describe("DictationStreamManager (provider-agnostic provider)", () => { it("drops dangling uncommitted non-final transcripts when finishing after silence tail clear", async () => { vi.useFakeTimers(); + const previousDebug = process.env.PASEO_DICTATION_DEBUG; + process.env.PASEO_DICTATION_DEBUG = "false"; try { const session = new FakeRealtimeSession(); const emitted: Array<{ type: string; payload: any }> = []; @@ -299,6 +301,7 @@ describe("DictationStreamManager (provider-agnostic provider)", () => { await manager.handleFinish("d-clear-tail", 1); await tick(); await vi.advanceTimersByTimeAsync(5_100); + await tick(); const final = emitted.find((msg) => msg.type === "dictation_stream_final"); const error = emitted.find((msg) => msg.type === "dictation_stream_error"); @@ -306,6 +309,7 @@ describe("DictationStreamManager (provider-agnostic provider)", () => { expect(error).toBeUndefined(); expect(final?.payload.text).toBe("hello"); } finally { + process.env.PASEO_DICTATION_DEBUG = previousDebug; vi.useRealTimers(); } }); diff --git a/packages/server/src/server/service-health-monitor.test.ts b/packages/server/src/server/service-health-monitor.test.ts index 5c2579b43..47f6b3b07 100644 --- a/packages/server/src/server/service-health-monitor.test.ts +++ b/packages/server/src/server/service-health-monitor.test.ts @@ -51,7 +51,7 @@ async function closeServer(server: net.Server): Promise { async function advancePoll(ms: number): Promise { await vi.advanceTimersByTimeAsync(ms); - for (let i = 0; i < 5; i += 1) { + for (let i = 0; i < 20; i += 1) { await scheduler.yield(); } } diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 3d628732e..96b1ccc88 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -4857,7 +4857,7 @@ export class Session { services: this.serviceRouteStore ? buildWorkspaceServicePayloads( this.serviceRouteStore, - workspace.workspaceId, + workspace.directory, this.getDaemonTcpPort?.() ?? null, this.resolveServiceStatus ?? undefined, ) @@ -5574,6 +5574,7 @@ export class Session { private async createPaseoWorktreeInBackground(options: { requestCwd: string; repoRoot: string; + workspaceId: number; worktree: { branchName: string; worktreePath: string }; shouldBootstrap: boolean; }): Promise { diff --git a/packages/server/src/server/session.workspaces.test.ts b/packages/server/src/server/session.workspaces.test.ts index df91016e8..e4a34f164 100644 --- a/packages/server/src/server/session.workspaces.test.ts +++ b/packages/server/src/server/session.workspaces.test.ts @@ -291,30 +291,31 @@ function createTempGitRepo(options?: { describe("workspace aggregation", () => { test("archive request emits agent_archived using the snapshot archive flow", async () => { const emitted: Array<{ type: string; payload: any }> = []; - const archivedAt = "2026-04-01T00:00:00.000Z"; - const archivedRecord = { - id: "agent-1", - provider: "codex", - cwd: "/tmp/repo", - createdAt: "2026-03-30T15:00:00.000Z", - updatedAt: archivedAt, - lastActivityAt: "2026-03-30T15:00:00.000Z", - lastUserMessageAt: null, - lastStatus: "idle" as const, - lastModeId: null, - runtimeInfo: null, - config: { + const archiveSnapshot = vi.fn(async (_agentId: string, archivedAt: string) => { + return { + id: "agent-1", provider: "codex", cwd: "/tmp/repo", - }, - persistence: null, - title: "Archive me", - labels: {}, - requiresAttention: false, - attentionReason: null, - attentionTimestamp: null, - archivedAt, - }; + createdAt: "2026-03-30T15:00:00.000Z", + updatedAt: archivedAt, + lastActivityAt: "2026-03-30T15:00:00.000Z", + lastUserMessageAt: null, + lastStatus: "idle" as const, + lastModeId: null, + runtimeInfo: null, + config: { + provider: "codex", + cwd: "/tmp/repo", + }, + persistence: null, + title: "Archive me", + labels: {}, + requiresAttention: false, + attentionReason: null, + attentionTimestamp: null, + archivedAt, + }; + }); const logger = { child: () => logger, @@ -337,7 +338,7 @@ describe("workspace aggregation", () => { subscribe: () => () => {}, listAgents: () => [], getAgent: (agentId: string) => (agentId === "agent-1" ? { id: agentId } : null), - archiveSnapshot: vi.fn(async () => archivedRecord), + archiveSnapshot, closeAgent, clearAgentAttention: async () => {}, } as any, @@ -391,18 +392,37 @@ describe("workspace aggregation", () => { expect(session.interruptAgentIfRunning).toHaveBeenCalledWith("agent-1"); expect(closeAgent).toHaveBeenCalledWith("agent-1"); - expect( - emitted.find((message) => message.type === "agent_archived")?.payload, - ).toMatchObject({ + const archivedPayload = emitted.find((message) => message.type === "agent_archived")?.payload; + expect(archivedPayload).toMatchObject({ agentId: "agent-1", - archivedAt, + archivedAt: expect.any(String), requestId: "req-archive", }); + expect(archiveSnapshot).toHaveBeenCalledWith("agent-1", archivedPayload.archivedAt); }); test("close_items_request archives agents and kills terminals in one batch", async () => { const emitted: Array<{ type: string; payload: any }> = []; - const archivedAt = "2026-04-01T00:00:00.000Z"; + const archiveSnapshot = vi.fn(async (_agentId: string, archivedAt: string) => ({ + id: "agent-1", + provider: "codex", + cwd: "/tmp/repo", + createdAt: "2026-03-01T12:00:00.000Z", + updatedAt: archivedAt, + lastActivityAt: "2026-03-01T12:00:00.000Z", + lastUserMessageAt: null, + lastStatus: "idle" as const, + lastModeId: null, + runtimeInfo: null, + config: null, + persistence: null, + title: null, + labels: {}, + requiresAttention: false, + attentionReason: null, + attentionTimestamp: null, + archivedAt, + })); const sessionLogger = { child: () => sessionLogger, trace: vi.fn(), @@ -423,26 +443,7 @@ describe("workspace aggregation", () => { subscribe: () => () => {}, listAgents: () => [], getAgent: (agentId: string) => (agentId === "agent-1" ? { id: agentId } : null), - archiveSnapshot: async () => ({ - id: "agent-1", - provider: "codex", - cwd: "/tmp/repo", - createdAt: "2026-03-01T12:00:00.000Z", - updatedAt: archivedAt, - lastActivityAt: "2026-03-01T12:00:00.000Z", - lastUserMessageAt: null, - lastStatus: "idle" as const, - lastModeId: null, - runtimeInfo: null, - config: null, - persistence: null, - title: null, - labels: {}, - requiresAttention: false, - attentionReason: null, - attentionTimestamp: null, - archivedAt, - }), + archiveSnapshot, closeAgent: async () => undefined, clearAgentAttention: async () => {}, } as any, @@ -504,13 +505,13 @@ describe("workspace aggregation", () => { expect(session.interruptAgentIfRunning).toHaveBeenCalledWith("agent-1"); expect(session.terminalManager.killTerminal).toHaveBeenCalledWith("term-1"); - expect( - emitted.find((message) => message.type === "close_items_response")?.payload, - ).toEqual({ - agents: [{ agentId: "agent-1", archivedAt }], + const closePayload = emitted.find((message) => message.type === "close_items_response")?.payload; + expect(closePayload).toEqual({ + agents: [{ agentId: "agent-1", archivedAt: expect.any(String) }], terminals: [{ terminalId: "term-1", success: true }], requestId: "req-close-items", }); + expect(archiveSnapshot).toHaveBeenCalledWith("agent-1", closePayload.agents[0].archivedAt); }); test("close_items_request continues after an archive failure", async () => { @@ -523,7 +524,31 @@ describe("workspace aggregation", () => { warn: vi.fn(), error: vi.fn(), }; - const archivedAt = "2026-04-01T00:00:00.000Z"; + const archiveSnapshot = vi.fn(async (agentId: string, archivedAt: string) => { + if (agentId === "agent-bad") { + throw new Error("archive failed"); + } + return { + id: "agent-good", + provider: "codex", + cwd: "/tmp/repo", + createdAt: "2026-03-01T12:00:00.000Z", + updatedAt: archivedAt, + lastActivityAt: "2026-03-01T12:00:00.000Z", + lastUserMessageAt: null, + lastStatus: "idle" as const, + lastModeId: null, + runtimeInfo: null, + config: null, + persistence: null, + title: null, + labels: {}, + requiresAttention: false, + attentionReason: null, + attentionTimestamp: null, + archivedAt, + }; + }); const session = new Session({ clientId: "test-client", @@ -537,31 +562,7 @@ describe("workspace aggregation", () => { listAgents: () => [], getAgent: (agentId: string) => agentId === "agent-bad" || agentId === "agent-good" ? { id: agentId } : null, - archiveSnapshot: async (agentId: string) => { - if (agentId === "agent-bad") { - throw new Error("archive failed"); - } - return { - id: "agent-good", - provider: "codex", - cwd: "/tmp/repo", - createdAt: "2026-03-01T12:00:00.000Z", - updatedAt: archivedAt, - lastActivityAt: "2026-03-01T12:00:00.000Z", - lastUserMessageAt: null, - lastStatus: "idle" as const, - lastModeId: null, - runtimeInfo: null, - config: null, - persistence: null, - title: null, - labels: {}, - requiresAttention: false, - attentionReason: null, - attentionTimestamp: null, - archivedAt, - }; - }, + archiveSnapshot, closeAgent: async () => undefined, clearAgentAttention: async () => {}, } as any, @@ -624,13 +625,13 @@ describe("workspace aggregation", () => { expect(session.interruptAgentIfRunning).toHaveBeenCalledWith("agent-bad"); expect(session.interruptAgentIfRunning).toHaveBeenCalledWith("agent-good"); expect(session.terminalManager.killTerminal).toHaveBeenCalledWith("term-1"); - expect( - emitted.find((message) => message.type === "close_items_response")?.payload, - ).toEqual({ - agents: [{ agentId: "agent-good", archivedAt }], + const closePayload = emitted.find((message) => message.type === "close_items_response")?.payload; + expect(closePayload).toEqual({ + agents: [{ agentId: "agent-good", archivedAt: expect.any(String) }], terminals: [{ terminalId: "term-1", success: true }], requestId: "req-close-best-effort", }); + expect(archiveSnapshot).toHaveBeenCalledWith("agent-good", closePayload.agents[0].archivedAt); expect(sessionLogger.warn).toHaveBeenCalled(); }); diff --git a/packages/server/src/server/websocket-server.relay-reconnect.test.ts b/packages/server/src/server/websocket-server.relay-reconnect.test.ts index e19dd7d90..fe1b7c53b 100644 --- a/packages/server/src/server/websocket-server.relay-reconnect.test.ts +++ b/packages/server/src/server/websocket-server.relay-reconnect.test.ts @@ -167,15 +167,16 @@ function createServer(options?: { speechReadiness?: SpeechReadinessSnapshot | nu "/tmp/paseo-test", async () => ({}) as any, { allowedOrigins: new Set() }, - undefined, - undefined, - undefined, speechReadiness ? { - getSpeechReadiness: () => speechReadiness, + getReadiness: () => speechReadiness, + onReadinessChange: vi.fn(() => () => {}), } : undefined, undefined, + undefined, + undefined, + undefined, TEST_DAEMON_VERSION, undefined, undefined, diff --git a/packages/server/src/server/worktree-session.test.ts b/packages/server/src/server/worktree-session.test.ts index 54a7d695c..5a1f8eb23 100644 --- a/packages/server/src/server/worktree-session.test.ts +++ b/packages/server/src/server/worktree-session.test.ts @@ -237,6 +237,7 @@ describe("createPaseoWorktreeInBackground", () => { const logger = createLogger(); const emitWorkspaceUpdateForCwd = vi.fn(async () => {}); const archiveWorkspaceRecord = vi.fn(async () => {}); + const workspaceId = 101; await createPaseoWorktreeInBackground( { @@ -253,6 +254,7 @@ describe("createPaseoWorktreeInBackground", () => { { requestCwd: repoDir, repoRoot: repoDir, + workspaceId, worktree: { branchName: "broken-feature", worktreePath, @@ -275,7 +277,7 @@ describe("createPaseoWorktreeInBackground", () => { status: "failed", error: expect.stringContaining("Failed to parse paseo.json"), }); - expect(archiveWorkspaceRecord).toHaveBeenCalledWith(worktreePath); + expect(archiveWorkspaceRecord).toHaveBeenCalledWith(workspaceId); expect(emitWorkspaceUpdateForCwd).toHaveBeenCalledWith(worktreePath); }); diff --git a/packages/server/src/server/worktree-session.ts b/packages/server/src/server/worktree-session.ts index 4a09adbf7..e17ab144a 100644 --- a/packages/server/src/server/worktree-session.ts +++ b/packages/server/src/server/worktree-session.ts @@ -109,7 +109,7 @@ type CreatePaseoWorktreeInBackgroundDependencies = { emit: EmitSessionMessage; sessionLogger: Logger; terminalManager: TerminalManager | null; - archiveWorkspaceRecord: (workspaceId: string) => Promise; + archiveWorkspaceRecord: (workspaceId: number) => Promise; serviceRouteStore: ServiceRouteStore | null; daemonPort?: number | null; }; @@ -134,6 +134,7 @@ type HandleCreatePaseoWorktreeRequestDependencies = { createPaseoWorktreeInBackground: (options: { requestCwd: string; repoRoot: string; + workspaceId: number; worktree: WorktreeConfig; shouldBootstrap: boolean; }) => Promise; @@ -625,6 +626,7 @@ export async function handleCreatePaseoWorktreeRequest( void dependencies.createPaseoWorktreeInBackground({ requestCwd: request.cwd, repoRoot, + workspaceId: workspace.id, worktree: createdWorktree.worktree, shouldBootstrap: createdWorktree.shouldBootstrap, }); @@ -666,6 +668,7 @@ export async function createPaseoWorktreeInBackground( options: { requestCwd: string; repoRoot: string; + workspaceId: number; worktree: WorktreeConfig; shouldBootstrap: boolean; }, @@ -741,7 +744,7 @@ export async function createPaseoWorktreeInBackground( emitSetupProgress("failed", message); if (!setupStarted) { - await dependencies.archiveWorkspaceRecord(normalizePersistedWorkspaceId(worktree.worktreePath)); + await dependencies.archiveWorkspaceRecord(options.workspaceId); } dependencies.sessionLogger.error( diff --git a/packages/server/src/shared/messages.workspaces.test.ts b/packages/server/src/shared/messages.workspaces.test.ts index 01c9ead2c..0e6f69095 100644 --- a/packages/server/src/shared/messages.workspaces.test.ts +++ b/packages/server/src/shared/messages.workspaces.test.ts @@ -58,12 +58,13 @@ describe("workspace message schemas", () => { payload: { kind: "upsert", workspace: { - id: "/repo", - projectId: "/repo", + id: 1, + projectId: 1, projectDisplayName: "repo", projectRootPath: "/repo", - projectKind: "non_git", - workspaceKind: "directory", + workspaceDirectory: "/repo", + projectKind: "directory", + workspaceKind: "checkout", name: "repo", status: "done", activityAt: null,