diff --git a/package-lock.json b/package-lock.json index 10a3375ac..6b255e413 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6402,128 +6402,6 @@ "node": ">=20.0" } }, - "node_modules/@openai/codex": { - "version": "0.114.0", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.114.0.tgz", - "integrity": "sha512-HMo8LRR6CtfKkaa28xvFK6eOarmBFTDfsrS9GJtEoaspGGemFok494CpafDspiTZaHZZGHfSUe5SWTUxYq7OaA==", - "license": "Apache-2.0", - "bin": { - "codex": "bin/codex.js" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "@openai/codex-darwin-arm64": "npm:@openai/codex@0.114.0-darwin-arm64", - "@openai/codex-darwin-x64": "npm:@openai/codex@0.114.0-darwin-x64", - "@openai/codex-linux-arm64": "npm:@openai/codex@0.114.0-linux-arm64", - "@openai/codex-linux-x64": "npm:@openai/codex@0.114.0-linux-x64", - "@openai/codex-win32-arm64": "npm:@openai/codex@0.114.0-win32-arm64", - "@openai/codex-win32-x64": "npm:@openai/codex@0.114.0-win32-x64" - } - }, - "node_modules/@openai/codex-darwin-arm64": { - "name": "@openai/codex", - "version": "0.114.0-darwin-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.114.0-darwin-arm64.tgz", - "integrity": "sha512-c9dlgo9O+66alr3s3G36fKE3KaO2+xrLZ/QcU3oujDruV86pqgVSEK2njHi+WIyyOa6m2AyWxdaHLEbKxPCNRg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-darwin-x64": { - "name": "@openai/codex", - "version": "0.114.0-darwin-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.114.0-darwin-x64.tgz", - "integrity": "sha512-oPAPeZSaJZ1AQneFPSJu44uqbZr63Bxut9FOTWZwuSqYx4YEees7i0HJmJcqQc0XnCbYtHJdqo/i07Uv374NLw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-linux-arm64": { - "name": "@openai/codex", - "version": "0.114.0-linux-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.114.0-linux-arm64.tgz", - "integrity": "sha512-oUFZGZgMiEqmo85C+dfhckpVApH25alLWfwBgfKr2IRgdx/tovy8vN101f6kj01E6nDDe4LfJdNSZGtZht4fRA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-linux-x64": { - "name": "@openai/codex", - "version": "0.114.0-linux-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.114.0-linux-x64.tgz", - "integrity": "sha512-MF6RhxfBoccccd5CYII2C7TL2TvYzombBQhanDZfQAajr6n7IuoazCmoHDlrFHS4AcSw9bYA4MRlrwEjbEjgsw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-win32-arm64": { - "name": "@openai/codex", - "version": "0.114.0-win32-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.114.0-win32-arm64.tgz", - "integrity": "sha512-CnRMHopj3en9aqQ2UaDW7EgpEGkxHdZVLLRq2cOy5D0HyuzF6Qb6595ADoFVJHEmPeN5Iz/KUbiGs5GLDjUwOA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-win32-x64": { - "name": "@openai/codex", - "version": "0.114.0-win32-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.114.0-win32-x64.tgz", - "integrity": "sha512-ztRsH5Z+gPVZ5ZInx6HzXsTFFkuEdjk3Ay0UGVOhRRCWvevXQi/SL4eU8hwysCYrs8K/WRq3mo4c95w9zQ2wLw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, "node_modules/@opencode-ai/sdk": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.2.6.tgz", @@ -12835,6 +12713,18 @@ "node": ">=0.10.0" } }, + "node_modules/default-shell": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/default-shell/-/default-shell-2.2.0.tgz", + "integrity": "sha512-sPpMZcVhRQ0nEMDtuMJ+RtCxt7iHPAMBU+I4tAlo5dU1sjRpNax0crj6nR3qKpvVnckaQ9U38enXcwW9nZJeCw==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -14462,6 +14352,35 @@ "dev": true, "license": "MIT" }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -17102,6 +17021,18 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -17686,6 +17617,15 @@ "node": ">= 6" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -18346,7 +18286,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -20787,7 +20726,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -21368,6 +21306,18 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -21586,7 +21536,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -21682,161 +21631,6 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, - "node_modules/opencode-ai": { - "version": "1.2.24", - "resolved": "https://registry.npmjs.org/opencode-ai/-/opencode-ai-1.2.24.tgz", - "integrity": "sha512-LaSoATkVEF6jyXNnAPrkqYpmHsZfuf2uPLDSEGJkL4hT6HYFq8LkoknHDjCGIiE5Be3MmId4+l17eSIKDfmddw==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "opencode": "bin/opencode" - }, - "optionalDependencies": { - "opencode-darwin-arm64": "1.2.24", - "opencode-darwin-x64": "1.2.24", - "opencode-darwin-x64-baseline": "1.2.24", - "opencode-linux-arm64": "1.2.24", - "opencode-linux-arm64-musl": "1.2.24", - "opencode-linux-x64": "1.2.24", - "opencode-linux-x64-baseline": "1.2.24", - "opencode-linux-x64-baseline-musl": "1.2.24", - "opencode-linux-x64-musl": "1.2.24", - "opencode-windows-x64": "1.2.24", - "opencode-windows-x64-baseline": "1.2.24" - } - }, - "node_modules/opencode-darwin-arm64": { - "version": "1.2.24", - "resolved": "https://registry.npmjs.org/opencode-darwin-arm64/-/opencode-darwin-arm64-1.2.24.tgz", - "integrity": "sha512-mk5AkNbmATLwjnczd09Fu9dLTPcW4GSlRlIGWJBbLuGSPe7DWfbuhA5tj00VuaGeC09wDKVnW06BxWp/gRzgMQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/opencode-darwin-x64": { - "version": "1.2.24", - "resolved": "https://registry.npmjs.org/opencode-darwin-x64/-/opencode-darwin-x64-1.2.24.tgz", - "integrity": "sha512-SuG7ItUejx42z9Bz46t0R3LLFIM34vZUcwJAkqqpS6MRd1KV9G4OQIJl9BR+Cwwvk70iqCnoDxj5NgFZjeeNOw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/opencode-darwin-x64-baseline": { - "version": "1.2.24", - "resolved": "https://registry.npmjs.org/opencode-darwin-x64-baseline/-/opencode-darwin-x64-baseline-1.2.24.tgz", - "integrity": "sha512-+MT/ZkgXxeID/kOcT04SBn8Vg+4tk8CytKldaNDLgUnsi/78vTxDXWs0Cc21Ohc894r45fEtOOb5npSNIMOVfQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/opencode-linux-arm64": { - "version": "1.2.24", - "resolved": "https://registry.npmjs.org/opencode-linux-arm64/-/opencode-linux-arm64-1.2.24.tgz", - "integrity": "sha512-zSpbVCwVfUQ/Bey1MEGkJg3A52EiIPgnpS44HwmSANkiLPVXRBVuwbFYN2qJ7eJVAH939K9+QPmDOW4X8DWKeQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/opencode-linux-arm64-musl": { - "version": "1.2.24", - "resolved": "https://registry.npmjs.org/opencode-linux-arm64-musl/-/opencode-linux-arm64-musl-1.2.24.tgz", - "integrity": "sha512-LY28mspJOJ2s32E0AbrfbdWH5Bw+5YQkuB3ACJiVxW1MafjhRgPOsXjnw38WMiFjZ+NbhSPBOKnL0guKseVd4Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/opencode-linux-x64": { - "version": "1.2.24", - "resolved": "https://registry.npmjs.org/opencode-linux-x64/-/opencode-linux-x64-1.2.24.tgz", - "integrity": "sha512-OM3umRJ2ZpDe4YtDOGp32cQCqvMPuFJBKKngfvWL4zz4EWDzLPttb0sNL7xdC1xR/XidtADRYcvtzc2rr7/4Rw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/opencode-linux-x64-baseline": { - "version": "1.2.24", - "resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline/-/opencode-linux-x64-baseline-1.2.24.tgz", - "integrity": "sha512-yakykM+GxCMZ5WK2G0CQk5cGJkoKt09A/8Y0bhKy7l+Jc/QiTV2ZIlBwT5FO15dnuGlNH6zvukLyOQvOZinM+Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/opencode-linux-x64-baseline-musl": { - "version": "1.2.24", - "resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline-musl/-/opencode-linux-x64-baseline-musl-1.2.24.tgz", - "integrity": "sha512-XHPuf9Rj9nWkVX7JJZ5NtYx9UWT0hPiem/kAhZzYITseRGmQMMfFAhIOBYYvvlzJzgdhcrLi198kJHaKCdXjag==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/opencode-linux-x64-musl": { - "version": "1.2.24", - "resolved": "https://registry.npmjs.org/opencode-linux-x64-musl/-/opencode-linux-x64-musl-1.2.24.tgz", - "integrity": "sha512-nFT4J4m4wKiZh4RsfgQJlW6W2w/VtMNnXnMiQk93FnncSf0nUXEB5Nam3P+vCjdR3i4cs355JiOvbshnMiJGSA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/opencode-windows-x64": { - "version": "1.2.24", - "resolved": "https://registry.npmjs.org/opencode-windows-x64/-/opencode-windows-x64-1.2.24.tgz", - "integrity": "sha512-7nkwFgS8Jn35kgw+6JIWRU6PmbggSuTDiQ+RnFGoySFgYW/Sz/R30Z4KmnbfJLElzEef0kX+nZQasfVl0pSf6A==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/opencode-windows-x64-baseline": { - "version": "1.2.24", - "resolved": "https://registry.npmjs.org/opencode-windows-x64-baseline/-/opencode-windows-x64-baseline-1.2.24.tgz", - "integrity": "sha512-hXp1nPnkrHgM6cid1AogRJvjz931UbZ/G165B6AAR1gOb5EqtIh2gZcT0IRUyjooEpwH7+oGFRhT+3Kql/M0WA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -24688,6 +24482,50 @@ "node": ">=8" } }, + "node_modules/shell-env": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/shell-env/-/shell-env-4.0.3.tgz", + "integrity": "sha512-Ioe5h+hCDZ7pKL5+JGzbtPvZ5ESMHePZ8nLxohlDL+twmlcmutttMhRkrQOed8DeLT8mkYBgbwZfohe8pqaA3g==", + "license": "MIT", + "dependencies": { + "default-shell": "^2.0.0", + "execa": "^5.1.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shell-env/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" + } + }, + "node_modules/shell-env/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/shell-quote": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", @@ -25427,6 +25265,15 @@ "node": ">=4" } }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/strip-indent": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", @@ -28073,7 +27920,6 @@ "@lezer/markdown": "^1.6.2", "@lezer/python": "^1.1.18", "@modelcontextprotocol/sdk": "^1.20.1", - "@openai/codex": "^0.114.0", "@opencode-ai/sdk": "1.2.6", "@sctg/sentencepiece-js": "^1.1.0", "@xterm/headless": "^6.0.0", @@ -28088,11 +27934,11 @@ "node-pty": "1.2.0-beta.11", "onnxruntime-node": "^1.23.0", "openai": "^4.20.0", - "opencode-ai": "^1.2.24", "pino": "^10.2.0", "pino-pretty": "^13.1.3", "qrcode": "^1.5.4", "rotating-file-stream": "^3.2.9", + "shell-env": "^4.0.3", "sherpa-onnx": "1.12.28", "sherpa-onnx-node": "1.12.28", "strip-ansi": "^7.1.2", diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index d082e1f25..aecf39d3e 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -50,6 +50,7 @@ import { getIsTauri } from "@/constants/layout"; import { CommandCenter } from "@/components/command-center"; import { ProjectPickerModal } from "@/components/project-picker-modal"; import { KeyboardShortcutsDialog } from "@/components/keyboard-shortcuts-dialog"; +import { listenToDesktopNotificationClicks } from "@/desktop/notifications/desktop-notifications"; import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts"; import { queryClient } from "@/query/query-client"; import { @@ -89,20 +90,39 @@ function PushNotificationRouter() { useEffect(() => { if (Platform.OS === "web") { if (getTauri()) { - void ensureOsNotificationPermission().then((granted) => { - console.log( - "[OSNotifications][Tauri] Startup permission preflight result:", - granted ? "granted" : "not-granted" - ); - }); + void ensureOsNotificationPermission(); + + let disposed = false; + let unlisten: (() => void) | null = null; + + void listenToDesktopNotificationClicks((payload) => { + router.push(buildNotificationRoute(payload.data) as any); + }) + .then((cleanup) => { + if (disposed) { + cleanup(); + return; + } + unlisten = cleanup; + }) + .catch((error) => { + console.error( + "[OSNotifications][Desktop] Failed to register notification click listener", + error + ); + }); + + return () => { + disposed = true; + unlisten?.(); + }; } const target = globalThis as unknown as EventTarget; const openFromWebClick = (event: Event) => { const customEvent = event as CustomEvent; - const route = buildNotificationRoute(customEvent.detail?.data); event.preventDefault(); - router.push(route as any); + router.push(buildNotificationRoute(customEvent.detail?.data) as any); }; target.addEventListener( diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx index 441448d73..77ff03e23 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -41,6 +41,7 @@ import { import { useDraftStore } from "@/stores/draft-store"; import type { AgentDirectoryEntry } from "@/types/agent-directory"; import { sendOsNotification } from "@/utils/os-notifications"; +import { getIsAppActivelyVisible } from "@/utils/app-visibility"; import { getInitKey, getInitDeferred, @@ -570,18 +571,10 @@ function SessionProviderInternal({ if (params.reason === "error") { return; } - const isActive = appState ? appState === "active" : true; - const isAwayFromAgent = !isActive || focusedAgentId !== params.agentId; + const isActivelyVisible = getIsAppActivelyVisible(appState); + const isAwayFromAgent = + !isActivelyVisible || focusedAgentId !== params.agentId; if (!isAwayFromAgent) { - console.log( - "[OSNotifications] Skipping attention notification: user already focused on agent", - { - agentId: params.agentId, - reason: params.reason, - appState, - focusedAgentId, - } - ); return; } diff --git a/packages/app/src/desktop/notifications/desktop-notifications.ts b/packages/app/src/desktop/notifications/desktop-notifications.ts new file mode 100644 index 000000000..8d15d5304 --- /dev/null +++ b/packages/app/src/desktop/notifications/desktop-notifications.ts @@ -0,0 +1,58 @@ +import { invokeDesktopCommand } from "@/desktop/tauri/invoke-desktop-command"; +import { getTauri } from "@/utils/tauri"; + +export const DESKTOP_NOTIFICATION_CLICK_EVENT = "desktop-notification-click"; + +export interface DesktopNotificationInput { + title: string; + body?: string; + data?: Record; +} + +export interface DesktopNotificationClickPayload { + data?: Record; +} + +export type DesktopNotificationClickHandler = ( + payload: DesktopNotificationClickPayload +) => void; + +export type DesktopNotificationClickUnlisten = () => void; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +export async function sendDesktopNotification( + input: DesktopNotificationInput +): Promise { + try { + await invokeDesktopCommand("send_desktop_notification", { input }); + return true; + } catch (error) { + console.warn("[OSNotifications][Desktop] Failed to send desktop notification", error); + return false; + } +} + +export async function listenToDesktopNotificationClicks( + handler: DesktopNotificationClickHandler +): Promise { + const listen = getTauri()?.event?.listen; + if (typeof listen !== "function") { + throw new Error("Tauri event API is unavailable."); + } + + const unlisten = await listen(DESKTOP_NOTIFICATION_CLICK_EVENT, (event: unknown) => { + const payload = isRecord(event) && isRecord(event.payload) ? event.payload : null; + if (!payload) { + return; + } + + handler({ + data: isRecord(payload.data) ? payload.data : undefined, + }); + }); + + return typeof unlisten === "function" ? unlisten : () => {}; +} diff --git a/packages/app/src/hooks/use-agent-attention-clear.ts b/packages/app/src/hooks/use-agent-attention-clear.ts index ee2c1b803..20a7cfe72 100644 --- a/packages/app/src/hooks/use-agent-attention-clear.ts +++ b/packages/app/src/hooks/use-agent-attention-clear.ts @@ -5,6 +5,7 @@ import { shouldClearAgentAttention, type AgentAttentionClearTrigger, } from "@/utils/agent-attention"; +import { getIsAppActivelyVisible } from "@/utils/app-visibility"; type AttentionReason = "finished" | "error" | "permission" | null | undefined; @@ -23,20 +24,6 @@ interface AgentAttentionClearController { clearOnAgentBlur: () => void; } -function getIsAppVisible(): boolean { - const isAppStateActive = AppState.currentState === "active"; - if (Platform.OS !== "web") { - return isAppStateActive; - } - const documentVisible = - typeof document === "undefined" || document.visibilityState === "visible"; - const windowFocused = - typeof document === "undefined" || - typeof document.hasFocus !== "function" || - document.hasFocus(); - return isAppStateActive && documentVisible && windowFocused; -} - export function useAgentAttentionClear({ agentId, client, @@ -45,12 +32,14 @@ export function useAgentAttentionClear({ attentionReason, isScreenFocused, }: UseAgentAttentionClearParams): AgentAttentionClearController { - const [isAppVisible, setIsAppVisible] = useState(() => getIsAppVisible()); + const [isAppVisible, setIsAppVisible] = useState(() => + getIsAppActivelyVisible() + ); const deferredFocusEntryClearRef = useRef(false); const prevRequiresAttentionRef = useRef(Boolean(requiresAttention)); - const prevActivelyViewedRef = useRef(isScreenFocused && getIsAppVisible()); + const prevActivelyViewedRef = useRef(isScreenFocused && getIsAppActivelyVisible()); const prevScreenFocusedRef = useRef(false); - const prevAppVisibleRef = useRef(getIsAppVisible()); + const prevAppVisibleRef = useRef(getIsAppActivelyVisible()); const clearAttention = useCallback( (trigger: AgentAttentionClearTrigger) => { @@ -78,7 +67,7 @@ export function useAgentAttentionClear({ useEffect(() => { const updateVisibility = () => { - setIsAppVisible(getIsAppVisible()); + setIsAppVisible(getIsAppActivelyVisible()); }; const appStateSubscription = AppState.addEventListener( diff --git a/packages/app/src/utils/app-visibility.ts b/packages/app/src/utils/app-visibility.ts new file mode 100644 index 000000000..92dbce6aa --- /dev/null +++ b/packages/app/src/utils/app-visibility.ts @@ -0,0 +1,22 @@ +import { AppState, Platform } from "react-native"; + +export function getIsAppActivelyVisible( + appState: string = AppState.currentState +): boolean { + if (appState !== "active") { + return false; + } + + if (Platform.OS !== "web") { + return true; + } + + const documentVisible = + typeof document === "undefined" || document.visibilityState === "visible"; + const windowFocused = + typeof document === "undefined" || + typeof document.hasFocus !== "function" || + document.hasFocus(); + + return documentVisible && windowFocused; +} diff --git a/packages/app/src/utils/os-notifications.test.ts b/packages/app/src/utils/os-notifications.test.ts index 2f53dfc6c..43edf530f 100644 --- a/packages/app/src/utils/os-notifications.test.ts +++ b/packages/app/src/utils/os-notifications.test.ts @@ -71,6 +71,7 @@ describe("sendOsNotification", () => { }); afterEach(() => { + vi.doUnmock("@/desktop/notifications/desktop-notifications"); vi.doUnmock("react-native"); vi.restoreAllMocks(); vi.resetModules(); @@ -167,18 +168,13 @@ describe("sendOsNotification", () => { expect(assign).toHaveBeenCalledWith("/h/srv%20with%20space/agent/agent%2F1"); }); - it("uses Tauri notification module when available", async () => { - const isPermissionGranted = vi.fn(async () => false); - const requestPermission = vi.fn(async () => "granted"); - const sendNotification = vi.fn(async () => undefined); + it("uses the desktop notification bridge when Tauri is available", async () => { + const sendDesktopNotification = vi.fn(async () => true); + vi.doMock("@/desktop/notifications/desktop-notifications", () => ({ + sendDesktopNotification, + })); - (globalThis as { __TAURI__?: unknown }).__TAURI__ = { - notification: { - isPermissionGranted, - requestPermission, - sendNotification, - }, - }; + (globalThis as { __TAURI__?: unknown }).__TAURI__ = {}; const { sendOsNotification } = await loadModuleForPlatform("web"); const sent = await sendOsNotification({ @@ -188,11 +184,10 @@ describe("sendOsNotification", () => { }); expect(sent).toBe(true); - expect(isPermissionGranted).toHaveBeenCalledTimes(1); - expect(requestPermission).toHaveBeenCalledTimes(1); - expect(sendNotification).toHaveBeenCalledWith({ + expect(sendDesktopNotification).toHaveBeenCalledWith({ title: "Agent finished", body: "Done", + data: { serverId: "srv-1", agentId: "agent-1" }, }); }); }); diff --git a/packages/app/src/utils/os-notifications.ts b/packages/app/src/utils/os-notifications.ts index e951e688c..e05b6d804 100644 --- a/packages/app/src/utils/os-notifications.ts +++ b/packages/app/src/utils/os-notifications.ts @@ -1,4 +1,5 @@ import { Platform } from "react-native"; +import { sendDesktopNotification } from "@/desktop/notifications/desktop-notifications"; import { buildNotificationRoute } from "./notification-routing"; import { getTauri, type TauriNotificationApi } from "@/utils/tauri"; @@ -14,13 +15,16 @@ export type WebNotificationClickDetail = { type WebNotificationInstance = { onclick?: ((event: Event) => void) | null; + onclose?: ((event: Event) => void) | null; addEventListener?: (type: string, listener: (event: Event) => void) => void; + removeEventListener?: (type: string, listener: (event: Event) => void) => void; close?: () => void; }; export const WEB_NOTIFICATION_CLICK_EVENT = "paseo:web-notification-click"; let permissionRequest: Promise | null = null; +const activeWebNotifications = new Set(); function getTauriNotificationModule(): TauriNotificationApi | null { if (Platform.OS !== "web") { @@ -113,35 +117,6 @@ async function ensureTauriNotificationPermission( } } -async function sendTauriNotification( - payload: OsNotificationPayload, - notificationModule: TauriNotificationApi -): Promise { - if (typeof notificationModule.sendNotification !== "function") { - console.warn( - "[OSNotifications][Tauri] notification.sendNotification is unavailable" - ); - return false; - } - - const granted = await ensureTauriNotificationPermission(notificationModule); - if (!granted) { - console.log("[OSNotifications][Tauri] Permission not granted"); - return false; - } - - try { - await notificationModule.sendNotification({ - title: payload.title, - body: payload.body, - }); - return true; - } catch (error) { - console.warn("[OSNotifications][Tauri] Failed to send notification", error); - return false; - } -} - function dispatchWebNotificationClick(detail: WebNotificationClickDetail): boolean { const dispatch = (globalThis as { dispatchEvent?: (event: Event) => boolean }).dispatchEvent; const CustomEventConstructor = (globalThis as { CustomEvent?: typeof CustomEvent }) @@ -183,6 +158,12 @@ function attachWebClickHandler( notification: WebNotificationInstance, data: Record | undefined ): void { + activeWebNotifications.add(notification); + + const cleanup = () => { + activeWebNotifications.delete(notification); + }; + const onClick = () => { const focus = (globalThis as { focus?: () => void }).focus; if (typeof focus === "function") { @@ -194,17 +175,24 @@ function attachWebClickHandler( fallbackNavigateToNotificationTarget(data); } + cleanup(); if (typeof notification.close === "function") { notification.close(); } }; + const onClose = () => { + cleanup(); + }; + if (typeof notification.addEventListener === "function") { notification.addEventListener("click", onClick); + notification.addEventListener("close", onClose); return; } notification.onclick = onClick; + notification.onclose = onClose; } export async function sendOsNotification( @@ -215,34 +203,27 @@ export async function sendOsNotification( return false; } - const tauriNotification = getTauriNotificationModule(); - if (tauriNotification) { - console.log("[OSNotifications] Using Tauri notification module"); - return await sendTauriNotification(payload, tauriNotification); + if (getTauri()) { + return await sendDesktopNotification(payload); } const NotificationConstructor = getWebNotificationConstructor(); - if (!NotificationConstructor) { - console.log( - "[OSNotifications][Web] Notification constructor unavailable", - typeof window !== "undefined" && window.location - ? window.location.origin - : "unknown-origin" - ); - return false; + if (NotificationConstructor) { + const granted = await ensureNotificationPermission(); + if (!granted) { + console.log( + "[OSNotifications][Web] Permission not granted:", + NotificationConstructor.permission + ); + } else { + const notification = new NotificationConstructor(payload.title, { + body: payload.body, + data: payload.data, + }) as WebNotificationInstance; + attachWebClickHandler(notification, payload.data); + return true; + } } - const granted = await ensureOsNotificationPermission(); - if (!granted) { - console.log( - "[OSNotifications][Web] Permission not granted:", - NotificationConstructor.permission - ); - return false; - } - const notification = new NotificationConstructor(payload.title, { - body: payload.body, - data: payload.data, - }) as WebNotificationInstance; - attachWebClickHandler(notification, payload.data); - return true; + + return false; } diff --git a/packages/app/src/utils/tauri-attach-console.ts b/packages/app/src/utils/tauri-attach-console.ts index 0b86b504e..c6faffee8 100644 --- a/packages/app/src/utils/tauri-attach-console.ts +++ b/packages/app/src/utils/tauri-attach-console.ts @@ -34,7 +34,7 @@ export function attachConsole(): void { console.log = (...args: unknown[]) => { originalLog.apply(console, args); - forwardToRust(0, args); // debug + forwardToRust(1, args); // info }; console.info = (...args: unknown[]) => { originalInfo.apply(console, args); diff --git a/packages/desktop/scripts/build-managed-runtime.mjs b/packages/desktop/scripts/build-managed-runtime.mjs index 0412a5472..01e3fa9af 100644 --- a/packages/desktop/scripts/build-managed-runtime.mjs +++ b/packages/desktop/scripts/build-managed-runtime.mjs @@ -480,71 +480,12 @@ async function pruneClaudeAgentSdk(runtimeRoot) { } } -async function pruneCodexCli(runtimeRoot) { - const codexTargetTriple = { - darwin: { arm64: "aarch64-apple-darwin", x64: "x86_64-apple-darwin" }, - linux: { arm64: "aarch64-unknown-linux-musl", x64: "x86_64-unknown-linux-musl" }, - win32: { arm64: "aarch64-pc-windows-msvc", x64: "x86_64-pc-windows-msvc" }, - }; - const codexPlatformPackages = [ - "@openai/codex-darwin-arm64", - "@openai/codex-darwin-x64", - "@openai/codex-linux-arm64", - "@openai/codex-linux-x64", - "@openai/codex-win32-arm64", - "@openai/codex-win32-x64", - ]; - const currentTriple = codexTargetTriple[process.platform]?.[process.arch]; - const currentPackage = currentTriple - ? `@openai/codex-${process.platform === "win32" ? "win32" : process.platform}-${process.arch}` - : null; - - const openaiScope = path.join(runtimeRoot, "node_modules", "@openai"); - for (const pkg of codexPlatformPackages) { - const pkgName = pkg.replace("@openai/", ""); - if (currentPackage && pkg === currentPackage) { - continue; - } - await removeIfExists(path.join(openaiScope, pkgName)); - } -} - -async function pruneOpenCodeCli(runtimeRoot) { - const opencodePlatformPackages = [ - "opencode-darwin-arm64", - "opencode-darwin-x64", - "opencode-linux-arm64", - "opencode-linux-x64", - "opencode-linux-x64-baseline", - "opencode-linux-arm64-musl", - "opencode-linux-x64-musl", - "opencode-linux-x64-baseline-musl", - "opencode-windows-x64", - "opencode-windows-arm64", - ]; - const platformMap = { darwin: "darwin", linux: "linux", win32: "windows" }; - const currentPlatform = platformMap[process.platform]; - const currentPackage = currentPlatform - ? `opencode-${currentPlatform}-${process.arch}` - : null; - - const nodeModules = path.join(runtimeRoot, "node_modules"); - for (const pkg of opencodePlatformPackages) { - if (currentPackage && pkg === currentPackage) { - continue; - } - await removeIfExists(path.join(nodeModules, pkg)); - } -} - async function pruneManagedRuntime(runtimeRoot) { await Promise.all([ pruneNodeDistribution(runtimeRoot), pruneOnnxRuntime(runtimeRoot), pruneNodePty(runtimeRoot), pruneClaudeAgentSdk(runtimeRoot), - pruneCodexCli(runtimeRoot), - pruneOpenCodeCli(runtimeRoot), ]); } diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index c3b424e71..c5d6c090b 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -2637,6 +2637,7 @@ dependencies = [ "futures-util", "http", "log", + "mac-notification-sys", "serde", "serde_json", "tauri", diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index 00e314cf4..58b2f53e9 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -41,6 +41,8 @@ tauri-plugin-websocket = "2" tokio = { version = "1", features = ["net", "sync"] } tokio-tungstenite = "0.24" +[target.'cfg(target_os = "macos")'.dependencies] +mac-notification-sys = "0.6" diff --git a/packages/desktop/src-tauri/src/desktop_notifications.rs b/packages/desktop/src-tauri/src/desktop_notifications.rs new file mode 100644 index 000000000..b57a4aaf5 --- /dev/null +++ b/packages/desktop/src-tauri/src/desktop_notifications.rs @@ -0,0 +1,110 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tauri::{AppHandle, Runtime}; + +pub const DESKTOP_NOTIFICATION_CLICK_EVENT: &str = "desktop-notification-click"; + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DesktopNotificationInput { + pub title: String, + pub body: Option, + pub data: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct DesktopNotificationClickPayload { + data: Option, +} + +#[cfg(target_os = "macos")] +mod platform { + use super::{ + AppHandle, DesktopNotificationClickPayload, DesktopNotificationInput, + DESKTOP_NOTIFICATION_CLICK_EVENT, + }; + use std::sync::OnceLock; + use tauri::{async_runtime, Emitter, Runtime}; + + const MACOS_NOTIFICATION_SENDER_BUNDLE_ID: &str = "dev.paseo.desktop"; + static MACOS_NOTIFICATION_SENDER_INIT: OnceLock> = OnceLock::new(); + + fn ensure_notification_sender() -> Result<(), String> { + MACOS_NOTIFICATION_SENDER_INIT + .get_or_init(|| { + mac_notification_sys::set_application(MACOS_NOTIFICATION_SENDER_BUNDLE_ID) + .map_err(|error| { + format!( + "Failed to initialize macOS notification sender for {}: {error}", + MACOS_NOTIFICATION_SENDER_BUNDLE_ID + ) + }) + }) + .clone() + } + + pub fn send_notification( + app: AppHandle, + input: DesktopNotificationInput, + ) -> Result<(), String> { + let _task = async_runtime::spawn_blocking(move || { + if let Err(error) = ensure_notification_sender() { + log::warn!("{error}"); + return; + } + + let mut notification = mac_notification_sys::Notification::default(); + notification + .title(&input.title) + .message(input.body.as_deref().unwrap_or("")) + .wait_for_click(true); + + match notification.send() { + Ok(mac_notification_sys::NotificationResponse::Click) => { + let payload = DesktopNotificationClickPayload { data: input.data }; + if let Err(error) = app.emit(DESKTOP_NOTIFICATION_CLICK_EVENT, payload) { + log::warn!("failed to emit desktop notification click event: {error}"); + } + } + Ok(_) => {} + Err(error) => { + log::warn!("failed to send macOS notification: {error}"); + } + } + }); + + Ok(()) + } +} + +#[cfg(not(target_os = "macos"))] +mod platform { + use super::{AppHandle, DesktopNotificationInput}; + use tauri::Runtime; + use tauri_plugin_notification::NotificationExt; + + pub fn send_notification( + app: AppHandle, + input: DesktopNotificationInput, + ) -> Result<(), String> { + let mut notification = app.notification().builder().title(&input.title); + if let Some(body) = &input.body { + notification = notification.body(body); + } + + notification + .show() + .map_err(|error| format!("Failed to send desktop notification: {error}"))?; + + Ok(()) + } +} + +#[tauri::command] +pub async fn send_desktop_notification( + app: AppHandle, + input: DesktopNotificationInput, +) -> Result<(), String> { + platform::send_notification(app, input) +} diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index da58b69f7..6a25fcf80 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -11,7 +11,9 @@ use tauri::menu::{Menu, MenuItemBuilder, MenuItemKind, PredefinedMenuItem, Subme use tauri::{AppHandle, Manager, WebviewWindow}; use tauri_plugin_updater::UpdaterExt; +mod desktop_notifications; mod runtime_manager; +use desktop_notifications::send_desktop_notification; use runtime_manager::{ cli_symlink_instructions, close_local_daemon_transport, managed_daemon_logs, managed_daemon_pairing, managed_daemon_status, managed_runtime_status, @@ -544,6 +546,7 @@ pub fn run() { read_file_base64, delete_attachment_file, garbage_collect_attachment_files, + send_desktop_notification, webview_log ]) .setup(|app| { diff --git a/packages/server/package.json b/packages/server/package.json index 6df580f67..654f7069b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -62,7 +62,6 @@ "dependencies": { "@ai-sdk/openai": "2.0.52", "@anthropic-ai/claude-agent-sdk": "^0.2.11", - "@openai/codex": "^0.114.0", "@deepgram/sdk": "^3.4.0", "@getpaseo/relay": "0.1.26", "@lezer/common": "^1.5.0", @@ -88,11 +87,11 @@ "node-pty": "1.2.0-beta.11", "onnxruntime-node": "^1.23.0", "openai": "^4.20.0", - "opencode-ai": "^1.2.24", "pino": "^10.2.0", "pino-pretty": "^13.1.3", "qrcode": "^1.5.4", "rotating-file-stream": "^3.2.9", + "shell-env": "^4.0.3", "sherpa-onnx": "1.12.28", "sherpa-onnx-node": "1.12.28", "strip-ansi": "^7.1.2", diff --git a/packages/server/src/server/agent/provider-launch-config.ts b/packages/server/src/server/agent/provider-launch-config.ts index 55940749b..b036243af 100644 --- a/packages/server/src/server/agent/provider-launch-config.ts +++ b/packages/server/src/server/agent/provider-launch-config.ts @@ -1,6 +1,7 @@ import { execFileSync, execSync } from "node:child_process"; import { existsSync } from "node:fs"; import { platform } from "node:os"; +import { shellEnvSync } from "shell-env"; import { z } from "zod"; import type { AgentProvider } from "./agent-sdk-types.js"; @@ -104,11 +105,24 @@ export function resolveProviderCommandPrefix( }; } +let cachedShellEnv: Record | null = null; + +export function resolveShellEnv(): Record { + if (cachedShellEnv) return cachedShellEnv; + try { + cachedShellEnv = shellEnvSync(); + } catch { + cachedShellEnv = { ...process.env } as Record; + } + return cachedShellEnv; +} + export function applyProviderEnv( baseEnv: Record, runtimeSettings?: ProviderRuntimeSettings ): Record { return { + ...resolveShellEnv(), ...baseEnv, ...(runtimeSettings?.env ?? {}), }; diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index ea20b1ad8..f38988fae 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -27,10 +27,8 @@ import type { Logger } from "pino"; import { execSync, spawn } from "node:child_process"; import type { ChildProcessWithoutNullStreams } from "node:child_process"; import { randomUUID } from "node:crypto"; -import { existsSync } from "node:fs"; +import { existsSync, Dirent } from "node:fs"; import fs from "node:fs/promises"; -import { Dirent } from "node:fs"; -import { createRequire } from "node:module"; import os from "node:os"; import path from "node:path"; import readline from "node:readline"; @@ -42,6 +40,7 @@ import { } from "./codex/tool-call-mapper.js"; import { applyProviderEnv, + findExecutable, resolveProviderCommandPrefix, type ProviderRuntimeSettings, } from "../provider-launch-config.js"; @@ -150,52 +149,13 @@ function mergeCodexConfiguredDefaults( }; } -const CODEX_TARGET_TRIPLE: Record> = { - linux: { x64: "x86_64-unknown-linux-musl", arm64: "aarch64-unknown-linux-musl" }, - darwin: { x64: "x86_64-apple-darwin", arm64: "aarch64-apple-darwin" }, - win32: { x64: "x86_64-pc-windows-msvc", arm64: "aarch64-pc-windows-msvc" }, -}; - -const CODEX_PLATFORM_PACKAGE: Record = { - "x86_64-unknown-linux-musl": "@openai/codex-linux-x64", - "aarch64-unknown-linux-musl": "@openai/codex-linux-arm64", - "x86_64-apple-darwin": "@openai/codex-darwin-x64", - "aarch64-apple-darwin": "@openai/codex-darwin-arm64", - "x86_64-pc-windows-msvc": "@openai/codex-win32-x64", - "aarch64-pc-windows-msvc": "@openai/codex-win32-arm64", -}; - -function resolveBundledCodexBinary(): string | null { - const targetTriple = CODEX_TARGET_TRIPLE[process.platform]?.[process.arch]; - if (!targetTriple) { - return null; - } - const platformPackage = CODEX_PLATFORM_PACKAGE[targetTriple]; - if (!platformPackage) { - return null; - } - const binaryName = process.platform === "win32" ? "codex.exe" : "codex"; - try { - const require = createRequire(import.meta.url); - const packageJsonPath = require.resolve(`${platformPackage}/package.json`); - const vendorRoot = path.join(path.dirname(packageJsonPath), "vendor"); - const binaryPath = path.join(vendorRoot, targetTriple, "codex", binaryName); - if (existsSync(binaryPath)) { - return binaryPath; - } - } catch { - // Platform package not installed - } - return null; -} - function resolveCodexBinary(): string { - const bundled = resolveBundledCodexBinary(); - if (bundled) { - return bundled; + const found = findExecutable("codex"); + if (found) { + return found; } throw new Error( - `Bundled Codex binary not found for platform ${process.platform}-${process.arch}. Ensure the @openai/codex package and its platform-specific optional dependency are installed.` + "Codex binary not found. Install the Codex CLI (https://github.com/openai/codex) and ensure it is available in your shell PATH." ); } diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index 7481a3b74..94629f63d 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -1,8 +1,5 @@ import { spawn, type ChildProcess } from "node:child_process"; import { existsSync } from "node:fs"; -import { createRequire } from "node:module"; -import path from "node:path"; -import os from "node:os"; import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2/client"; import net from "node:net"; import type { Logger } from "pino"; @@ -33,6 +30,7 @@ import type { } from "../agent-sdk-types.js"; import { applyProviderEnv, + findExecutable, resolveProviderCommandPrefix, type ProviderRuntimeSettings, } from "../provider-launch-config.js"; @@ -152,40 +150,13 @@ const OpencodeToolPartToTimelineItemSchema = OpencodeToolPartTimelineEnvelopeSch }) ); -const OPENCODE_PLATFORM_MAP: Record = { - darwin: "darwin", - linux: "linux", - win32: "windows", -}; - -function resolveBundledOpenCodeBinary(): string | null { - const platform = OPENCODE_PLATFORM_MAP[os.platform()]; - if (!platform) { - return null; - } - const arch = os.arch(); - const packageName = `opencode-${platform}-${arch}`; - const binaryName = platform === "windows" ? "opencode.exe" : "opencode"; - try { - const require = createRequire(import.meta.url); - const packageJsonPath = require.resolve(`${packageName}/package.json`); - const binaryPath = path.join(path.dirname(packageJsonPath), "bin", binaryName); - if (existsSync(binaryPath)) { - return binaryPath; - } - } catch { - // Platform package not installed - } - return null; -} - function resolveOpenCodeBinary(): string { - const bundled = resolveBundledOpenCodeBinary(); - if (bundled) { - return bundled; + const found = findExecutable("opencode"); + if (found) { + return found; } throw new Error( - `Bundled OpenCode binary not found for platform ${os.platform()}-${os.arch()}. Ensure the opencode-ai package and its platform-specific optional dependency are installed.` + "OpenCode binary not found. Install OpenCode (https://github.com/opencode-ai/opencode) and ensure it is available in your shell PATH." ); } diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 67d26e789..9fd79539d 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -7379,7 +7379,7 @@ export class Session { const requestedResumeOffset = typeof msg.resumeOffset === 'number' ? msg.resumeOffset - : session.getOutputOffset() + : 0 const initialOffset = Math.max(0, Math.floor(requestedResumeOffset)) const binding: { terminalId: string