mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-04 01:06:16 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5eef02d8bf | |||
| b33579c2ec | |||
| c3d10af784 |
@@ -33,6 +33,5 @@ rubdos
|
||||
shantur
|
||||
simonklee
|
||||
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
||||
-terisuke
|
||||
thdxr
|
||||
-toastythebot
|
||||
|
||||
@@ -677,8 +677,8 @@
|
||||
},
|
||||
"catalog": {
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@effect/opentelemetry": "4.0.0-beta.57",
|
||||
"@effect/platform-node": "4.0.0-beta.57",
|
||||
"@effect/opentelemetry": "4.0.0-beta.48",
|
||||
"@effect/platform-node": "4.0.0-beta.48",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@lydell/node-pty": "1.2.0-beta.10",
|
||||
@@ -708,7 +708,7 @@
|
||||
"dompurify": "3.3.1",
|
||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||
"effect": "4.0.0-beta.57",
|
||||
"effect": "4.0.0-beta.48",
|
||||
"fuzzysort": "3.1.0",
|
||||
"hono": "4.10.7",
|
||||
"hono-openapi": "1.1.2",
|
||||
@@ -1071,11 +1071,11 @@
|
||||
|
||||
"@effect/language-service": ["@effect/language-service@0.84.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-l04qNxpiA8rY5yXWckRPJ7Mk5MNerXuNymSFf+IdflfI5i8jgL1bpBNLuP6ijg7wgjdHc/KmTnCj2kT0SCntuA=="],
|
||||
|
||||
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.57", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.57" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-gdjZPEP0QQg4qmI1vd+443kheeQZKytrjJIzCJncy6ZEpyk/SfrqeStLqLXdTRcms3IB0ls0vOV7KNq7YmBRVA=="],
|
||||
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.48", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.48" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-vHk/X1vgDrviGcOTHQqzm2D81TtyPE/C7Qdksg5eAdbGpnqL4Dm4lk6PzTReQ0pO1/avIvWqpxy315IURV0Ldw=="],
|
||||
|
||||
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.57", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.57", "mime": "^4.1.0", "undici": "^8.0.2" }, "peerDependencies": { "effect": "^4.0.0-beta.57", "ioredis": "^5.7.0" } }, "sha512-la0xxPSAYOsY0d+uVxEBxok3jYB31iPQmIaZZRUj2SNWqcGGHJc6KorKtI8guqSLuv9FGZ255kBWXRbG6hMeeg=="],
|
||||
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.48", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.48", "mime": "^4.1.0", "undici": "^8.0.2" }, "peerDependencies": { "effect": "^4.0.0-beta.48", "ioredis": "^5.7.0" } }, "sha512-8J6H0k9rtbp9O1QvKOyOPRcCTJ8WrR7IzZLJtYFTZ4bXVEEMCTo84h0CRpi7ccpA9t7DLqotip0NeFgiBosNKQ=="],
|
||||
|
||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.57", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.57" } }, "sha512-C976X6f+qHUtLSqcqImuCrjhAHnJV17NC2RvvybsAuDfkyIWU4MyiO2XwgiBeijeNupyr1M/KPKnyjtkNxV9Hw=="],
|
||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.48", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.48" } }, "sha512-wlhcdDHyacydCgiWdM8JwtQkViQhZsC8uJZ9wMoZXYxlCTvqfdzLeWw4A1UVMoq7sS6/KR1aZVeFkUjrqonncQ=="],
|
||||
|
||||
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
||||
|
||||
@@ -1601,7 +1601,7 @@
|
||||
|
||||
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="],
|
||||
|
||||
"@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||
"@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="],
|
||||
|
||||
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="],
|
||||
|
||||
@@ -3035,7 +3035,7 @@
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"effect": ["effect@4.0.0-beta.57", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-rg32VgXnLKaPRs9tbRDaZ5jxmzNY7ojXt85gSHGUTwdlbWH5Ik+OCUY2q14TXliygPGoHwCAvNWS4bQJOqf00g=="],
|
||||
"effect": ["effect@4.0.0-beta.48", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw=="],
|
||||
|
||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||
|
||||
@@ -5595,6 +5595,18 @@
|
||||
|
||||
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
||||
|
||||
"@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||
|
||||
"@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||
|
||||
"@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="],
|
||||
|
||||
"@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||
|
||||
"@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||
|
||||
"@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||
|
||||
"@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||
|
||||
"@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="],
|
||||
@@ -5663,10 +5675,6 @@
|
||||
|
||||
"@solidjs/start/vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="],
|
||||
|
||||
"@standard-community/standard-json/effect": ["effect@4.0.0-beta.48", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw=="],
|
||||
|
||||
"@standard-community/standard-openapi/effect": ["effect@4.0.0-beta.48", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw=="],
|
||||
|
||||
"@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
|
||||
@@ -6585,10 +6593,6 @@
|
||||
|
||||
"@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
|
||||
|
||||
"@standard-community/standard-json/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-h2T/LnUnISZZDn9ZQkZ/A59P+6+QdfOlrgl4RXK/vgM=",
|
||||
"aarch64-linux": "sha256-+DRohG1ZEB/2LtZU90GWoqJkeyu/sW8A8oKT3f/TtQ0=",
|
||||
"aarch64-darwin": "sha256-k4nsk/WduuxY8HgjRuqzGT9EjEo7V/2mAzBTYee0fZ0=",
|
||||
"x86_64-darwin": "sha256-3dSvfN2+5lXwOx57x8NSIWbEZ1fp6+1T6bJpAuUNPyk="
|
||||
"x86_64-linux": "sha256-U/LZx/D+5JTT1LHSyZkEuqXP/ky7LkHrEYBW5pcVArk=",
|
||||
"aarch64-linux": "sha256-nGZa04h4y3jbdmf87IRrlQm/E5qYR8lj5OxKgQSR2XU=",
|
||||
"aarch64-darwin": "sha256-GD8pCHWMBppDaIfRKxhY2m4xWo1OrY3wOmGw+EC71mw=",
|
||||
"x86_64-darwin": "sha256-KOH1ZB8pdpF7Xer6QIH7rrr9fwF/BZkCTJndPe0wypg="
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -27,8 +27,8 @@
|
||||
"packages/slack"
|
||||
],
|
||||
"catalog": {
|
||||
"@effect/opentelemetry": "4.0.0-beta.57",
|
||||
"@effect/platform-node": "4.0.0-beta.57",
|
||||
"@effect/opentelemetry": "4.0.0-beta.48",
|
||||
"@effect/platform-node": "4.0.0-beta.48",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@types/bun": "1.3.12",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
@@ -53,7 +53,7 @@
|
||||
"dompurify": "3.3.1",
|
||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||
"effect": "4.0.0-beta.57",
|
||||
"effect": "4.0.0-beta.48",
|
||||
"ai": "6.0.168",
|
||||
"cross-spawn": "7.0.6",
|
||||
"hono": "4.10.7",
|
||||
|
||||
@@ -82,15 +82,7 @@ declare global {
|
||||
}
|
||||
|
||||
function QueryProvider(props: ParentProps) {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
const client = new QueryClient()
|
||||
return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/solid-query"
|
||||
import { Component, createMemo, Show } from "solid-js"
|
||||
import { useMutation } from "@tanstack/solid-query"
|
||||
import { Component, createEffect, createMemo, on, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { loadMcpQuery } from "@/context/global-sync"
|
||||
|
||||
const statusLabels = {
|
||||
connected: "mcp.status.connected",
|
||||
@@ -19,7 +20,48 @@ export const DialogSelectMcp: Component = () => {
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
const language = useLanguage()
|
||||
const queryClient = useQueryClient()
|
||||
const [state, setState] = createStore({
|
||||
done: false,
|
||||
loading: false,
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => sync.data.mcp_ready,
|
||||
(ready, prev) => {
|
||||
if (!ready && prev) setState("done", false)
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
if (state.done || state.loading) return
|
||||
if (sync.data.mcp_ready) {
|
||||
setState("done", true)
|
||||
return
|
||||
}
|
||||
|
||||
setState("loading", true)
|
||||
void sdk.client.mcp
|
||||
.status()
|
||||
.then((result) => {
|
||||
sync.set("mcp", result.data ?? {})
|
||||
sync.set("mcp_ready", true)
|
||||
setState("done", true)
|
||||
})
|
||||
.catch((err) => {
|
||||
setState("done", true)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
setState("loading", false)
|
||||
})
|
||||
})
|
||||
|
||||
const items = createMemo(() =>
|
||||
Object.entries(sync.data.mcp ?? {})
|
||||
@@ -29,10 +71,16 @@ export const DialogSelectMcp: Component = () => {
|
||||
|
||||
const toggle = useMutation(() => ({
|
||||
mutationFn: async (name: string) => {
|
||||
if (sync.data.mcp[name]?.status === "connected") await sdk.client.mcp.disconnect({ name })
|
||||
else await sdk.client.mcp.connect({ name })
|
||||
const status = sync.data.mcp[name]
|
||||
if (status?.status === "connected") {
|
||||
await sdk.client.mcp.disconnect({ name })
|
||||
} else {
|
||||
await sdk.client.mcp.connect({ name })
|
||||
}
|
||||
|
||||
const result = await sdk.client.mcp.status()
|
||||
if (result.data) sync.set("mcp", result.data)
|
||||
},
|
||||
onSuccess: () => queryClient.refetchQueries({ queryKey: loadMcpQuery(sync.directory).queryKey }),
|
||||
}))
|
||||
|
||||
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { useMutation, useQueryClient } from "@tanstack/solid-query"
|
||||
import { useMutation } from "@tanstack/solid-query"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
|
||||
@@ -15,7 +15,6 @@ import { useSDK } from "@/context/sdk"
|
||||
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
|
||||
import { loadMcpQuery } from "@/context/global-sync"
|
||||
|
||||
const pollMs = 10_000
|
||||
|
||||
@@ -138,14 +137,14 @@ const useMcpToggleMutation = () => {
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
const language = useLanguage()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation(() => ({
|
||||
mutationFn: async (name: string) => {
|
||||
const status = sync.data.mcp[name]
|
||||
await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
|
||||
const result = await sdk.client.mcp.status()
|
||||
if (result.data) sync.set("mcp", result.data)
|
||||
},
|
||||
onSuccess: () => queryClient.refetchQueries({ queryKey: loadMcpQuery(sync.directory).queryKey }),
|
||||
onError: (err) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
@@ -163,6 +162,14 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const navigate = useNavigate()
|
||||
const sdk = useSDK()
|
||||
|
||||
const [load, setLoad] = createStore({
|
||||
lspDone: false,
|
||||
lspLoading: false,
|
||||
mcpDone: false,
|
||||
mcpLoading: false,
|
||||
})
|
||||
|
||||
const fail = (err: unknown) => {
|
||||
showToast({
|
||||
@@ -174,6 +181,40 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
||||
|
||||
createEffect(() => {
|
||||
if (!props.shown()) return
|
||||
|
||||
if (!sync.data.mcp_ready && !load.mcpDone && !load.mcpLoading) {
|
||||
setLoad("mcpLoading", true)
|
||||
void sdk.client.mcp
|
||||
.status()
|
||||
.then((result) => {
|
||||
sync.set("mcp", result.data ?? {})
|
||||
sync.set("mcp_ready", true)
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoad("mcpDone", true)
|
||||
fail(err)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoad("mcpLoading", false)
|
||||
})
|
||||
}
|
||||
|
||||
if (!sync.data.lsp_ready && !load.lspDone && !load.lspLoading) {
|
||||
setLoad("lspLoading", true)
|
||||
void sdk.client.lsp
|
||||
.status()
|
||||
.then((result) => {
|
||||
sync.set("lsp", result.data ?? [])
|
||||
sync.set("lsp_ready", true)
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoad("lspDone", true)
|
||||
fail(err)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoad("lspLoading", false)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
let dialogRun = 0
|
||||
|
||||
@@ -14,25 +14,17 @@ import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import type { InitError } from "../pages/error"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import {
|
||||
bootstrapDirectory,
|
||||
bootstrapGlobal,
|
||||
clearProviderRev,
|
||||
loadGlobalConfigQuery,
|
||||
loadPathQuery,
|
||||
loadProjectsQuery,
|
||||
loadProvidersQuery,
|
||||
} from "./global-sync/bootstrap"
|
||||
import { bootstrapDirectory, bootstrapGlobal, clearProviderRev } from "./global-sync/bootstrap"
|
||||
import { createChildStoreManager } from "./global-sync/child-store"
|
||||
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./global-sync/event-reducer"
|
||||
import { createRefreshQueue } from "./global-sync/queue"
|
||||
import { clearSessionPrefetchDirectory } from "./global-sync/session-prefetch"
|
||||
import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
|
||||
import { trimSessions } from "./global-sync/session-trim"
|
||||
import type { ProjectMeta } from "./global-sync/types"
|
||||
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
import { queryOptions, skipToken, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query"
|
||||
import { createRefreshQueue } from "./global-sync/queue"
|
||||
import { queryOptions, skipToken, useQueryClient } from "@tanstack/solid-query"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
@@ -51,18 +43,6 @@ type GlobalStore = {
|
||||
export const loadSessionsQuery = (directory: string) =>
|
||||
queryOptions<null>({ queryKey: [directory, "loadSessions"], queryFn: skipToken })
|
||||
|
||||
export const loadMcpQuery = (directory: string, sdk?: OpencodeClient) =>
|
||||
queryOptions({
|
||||
queryKey: [directory, "mcp"],
|
||||
queryFn: sdk ? () => sdk.mcp.status().then((r) => r.data ?? {}) : skipToken,
|
||||
})
|
||||
|
||||
export const loadLspQuery = (directory: string, sdk?: OpencodeClient) =>
|
||||
queryOptions({
|
||||
queryKey: [directory, "lsp"],
|
||||
queryFn: sdk ? () => sdk.lsp.status().then((r) => r.data ?? []) : skipToken,
|
||||
})
|
||||
|
||||
function createGlobalSync() {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const language = useLanguage()
|
||||
@@ -74,34 +54,15 @@ function createGlobalSync() {
|
||||
const sessionLoads = new Map<string, Promise<void>>()
|
||||
const sessionMeta = new Map<string, { limit: number }>()
|
||||
|
||||
const [configQuery, providerQuery, pathQuery] = useQueries(() => ({
|
||||
queries: [loadGlobalConfigQuery(), loadProvidersQuery(null), loadPathQuery(null), loadProjectsQuery()],
|
||||
}))
|
||||
|
||||
const [globalStore, setGlobalStore] = createStore<GlobalStore>({
|
||||
get ready() {
|
||||
return bootstrap.isPending
|
||||
},
|
||||
ready: false,
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
project: [],
|
||||
session_todo: {},
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
provider_auth: {},
|
||||
get path() {
|
||||
const EMPTY = { state: "", config: "", worktree: "", directory: "", home: "" }
|
||||
if (pathQuery.isLoading) return EMPTY
|
||||
return pathQuery.data ?? EMPTY
|
||||
},
|
||||
get provider() {
|
||||
const EMPTY = { all: [], connected: [], default: {} }
|
||||
if (providerQuery.isLoading) return EMPTY
|
||||
return providerQuery.data ?? EMPTY
|
||||
},
|
||||
get config() {
|
||||
if (configQuery.isLoading) return {}
|
||||
return configQuery.data ?? {}
|
||||
},
|
||||
get reload() {
|
||||
return updateConfigMutation.isPending ? "pending" : undefined
|
||||
},
|
||||
config: {},
|
||||
reload: undefined,
|
||||
})
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
@@ -127,22 +88,6 @@ function createGlobalSync() {
|
||||
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
|
||||
}) as typeof setGlobalStore
|
||||
|
||||
const bootstrap = useQuery(() => ({
|
||||
queryKey: ["bootstrap"],
|
||||
queryFn: async () => {
|
||||
await bootstrapGlobal({
|
||||
globalSDK: globalSDK.client,
|
||||
requestFailedTitle: language.t("common.requestFailed"),
|
||||
translate: language.t,
|
||||
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
||||
setGlobalStore: setBootStore,
|
||||
queryClient,
|
||||
})
|
||||
bootedAt = Date.now()
|
||||
return bootedAt
|
||||
},
|
||||
}))
|
||||
|
||||
const set = ((...input: unknown[]) => {
|
||||
if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) {
|
||||
setProjects(input[1] as Project[] | ((draft: Project[]) => Project[]))
|
||||
@@ -169,21 +114,10 @@ function createGlobalSync() {
|
||||
|
||||
const queue = createRefreshQueue({
|
||||
paused,
|
||||
bootstrap: () => queryClient.fetchQuery({ queryKey: ["bootstrap"] }),
|
||||
bootstrap,
|
||||
bootstrapInstance,
|
||||
})
|
||||
|
||||
const sdkFor = (directory: string) => {
|
||||
const cached = sdkCache.get(directory)
|
||||
if (cached) return cached
|
||||
const sdk = globalSDK.createClient({
|
||||
directory,
|
||||
throwOnError: true,
|
||||
})
|
||||
sdkCache.set(directory, sdk)
|
||||
return sdk
|
||||
}
|
||||
|
||||
const children = createChildStoreManager({
|
||||
owner,
|
||||
isBooting: (directory) => booting.has(directory),
|
||||
@@ -199,9 +133,19 @@ function createGlobalSync() {
|
||||
clearSessionPrefetchDirectory(directory)
|
||||
},
|
||||
translate: language.t,
|
||||
getSdk: sdkFor,
|
||||
})
|
||||
|
||||
const sdkFor = (directory: string) => {
|
||||
const cached = sdkCache.get(directory)
|
||||
if (cached) return cached
|
||||
const sdk = globalSDK.createClient({
|
||||
directory,
|
||||
throwOnError: true,
|
||||
})
|
||||
sdkCache.set(directory, sdk)
|
||||
return sdk
|
||||
}
|
||||
|
||||
async function loadSessions(directory: string) {
|
||||
const pending = sessionLoads.get(directory)
|
||||
if (pending) return pending
|
||||
@@ -320,13 +264,26 @@ function createGlobalSync() {
|
||||
const event = e.details
|
||||
const recent = bootingRoot || Date.now() - bootedAt < 1500
|
||||
|
||||
if (event.type === "session.error") {
|
||||
const error = event.properties.error
|
||||
if (error?.name !== "MessageAbortedError") {
|
||||
console.error("[global-sync] session error", {
|
||||
scope: directory === "global" ? "global" : "workspace",
|
||||
directory: directory === "global" ? undefined : directory,
|
||||
project: directory === "global" ? undefined : getFilename(directory),
|
||||
sessionID: event.properties.sessionID,
|
||||
error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (directory === "global") {
|
||||
applyGlobalEvent({
|
||||
event,
|
||||
project: globalStore.project,
|
||||
refresh: () => {
|
||||
if (recent) return
|
||||
bootstrap.refetch()
|
||||
queue.refresh()
|
||||
},
|
||||
setGlobalProject: setProjects,
|
||||
})
|
||||
@@ -352,7 +309,12 @@ function createGlobalSync() {
|
||||
setSessionTodo,
|
||||
vcsCache: children.vcsCache.get(directory),
|
||||
loadLsp: () => {
|
||||
void queryClient.fetchQuery(loadLspQuery(directory, sdkFor(directory)))
|
||||
void sdkFor(directory)
|
||||
.lsp.status()
|
||||
.then((x) => {
|
||||
setStore("lsp", x.data ?? [])
|
||||
setStore("lsp_ready", true)
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -367,6 +329,23 @@ function createGlobalSync() {
|
||||
}
|
||||
})
|
||||
|
||||
async function bootstrap() {
|
||||
bootingRoot = true
|
||||
try {
|
||||
await bootstrapGlobal({
|
||||
globalSDK: globalSDK.client,
|
||||
requestFailedTitle: language.t("common.requestFailed"),
|
||||
translate: language.t,
|
||||
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
||||
setGlobalStore: setBootStore,
|
||||
queryClient,
|
||||
})
|
||||
bootedAt = Date.now()
|
||||
} finally {
|
||||
bootingRoot = false
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (typeof requestAnimationFrame === "function") {
|
||||
eventFrame = requestAnimationFrame(() => {
|
||||
@@ -382,6 +361,7 @@ function createGlobalSync() {
|
||||
void globalSDK.event.start()
|
||||
}, 0)
|
||||
}
|
||||
void bootstrap()
|
||||
})
|
||||
|
||||
const projectApi = {
|
||||
@@ -394,10 +374,21 @@ function createGlobalSync() {
|
||||
},
|
||||
}
|
||||
|
||||
const updateConfigMutation = useMutation(() => ({
|
||||
mutationFn: (config: Config) => globalSDK.client.global.config.update({ config }),
|
||||
onSuccess: () => bootstrap.refetch(),
|
||||
}))
|
||||
const updateConfig = async (config: Config) => {
|
||||
setGlobalStore("reload", "pending")
|
||||
return globalSDK.client.global.config
|
||||
.update({ config })
|
||||
.then(bootstrap)
|
||||
.then(() => {
|
||||
queue.refresh()
|
||||
setGlobalStore("reload", undefined)
|
||||
queue.refresh()
|
||||
})
|
||||
.catch((error) => {
|
||||
setGlobalStore("reload", undefined)
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
data: globalStore,
|
||||
@@ -410,8 +401,8 @@ function createGlobalSync() {
|
||||
},
|
||||
child: children.child,
|
||||
peek: children.peek,
|
||||
// bootstrap,
|
||||
updateConfig: updateConfigMutation.mutateAsync,
|
||||
bootstrap,
|
||||
updateConfig,
|
||||
project: projectApi,
|
||||
todo: {
|
||||
set: setSessionTodo,
|
||||
|
||||
@@ -19,7 +19,6 @@ import type { State, VcsCache } from "./types"
|
||||
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query"
|
||||
import { loadMcpQuery } from "../global-sync"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
@@ -67,62 +66,6 @@ function runAll(list: Array<() => Promise<unknown>>) {
|
||||
return Promise.allSettled(list.map((item) => item()))
|
||||
}
|
||||
|
||||
function showErrors(input: {
|
||||
errors: unknown[]
|
||||
title: string
|
||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||
formatMoreCount: (count: number) => string
|
||||
}) {
|
||||
if (input.errors.length === 0) return
|
||||
const message = formatServerError(input.errors[0], input.translate)
|
||||
const more = input.errors.length > 1 ? input.formatMoreCount(input.errors.length - 1) : ""
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: input.title,
|
||||
description: message + more,
|
||||
})
|
||||
}
|
||||
|
||||
export const loadGlobalConfigQuery = (
|
||||
sdk?: OpencodeClient,
|
||||
transform?: (x: Awaited<ReturnType<OpencodeClient["global"]["config"]["get"]>>) => void,
|
||||
) =>
|
||||
queryOptions({
|
||||
queryKey: ["config"],
|
||||
queryFn: sdk
|
||||
? () =>
|
||||
retry(() =>
|
||||
sdk.global.config.get().then((x) => {
|
||||
transform?.(x)
|
||||
return x.data!
|
||||
}),
|
||||
)
|
||||
: skipToken,
|
||||
})
|
||||
|
||||
export const loadProjectsQuery = (
|
||||
sdk?: OpencodeClient,
|
||||
transform?: (x: Awaited<ReturnType<OpencodeClient["project"]["list"]>>["data"]) => void,
|
||||
) =>
|
||||
queryOptions({
|
||||
queryKey: ["project"],
|
||||
queryFn: sdk
|
||||
? () =>
|
||||
retry(() =>
|
||||
sdk.project
|
||||
.list()
|
||||
.then((x) => {
|
||||
return (x.data ?? [])
|
||||
.filter((p) => !!p?.id)
|
||||
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
|
||||
.slice()
|
||||
.sort((a, b) => cmp(a.id, b.id))
|
||||
})
|
||||
.then(transform),
|
||||
)
|
||||
: skipToken,
|
||||
})
|
||||
|
||||
export async function bootstrapGlobal(input: {
|
||||
globalSDK: OpencodeClient
|
||||
requestFailedTitle: string
|
||||
@@ -131,15 +74,53 @@ export async function bootstrapGlobal(input: {
|
||||
setGlobalStore: SetStoreFunction<GlobalStore>
|
||||
queryClient: QueryClient
|
||||
}) {
|
||||
const slow = [
|
||||
() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.globalSDK)),
|
||||
() => input.queryClient.fetchQuery(loadProvidersQuery(null, input.globalSDK)),
|
||||
() => input.queryClient.fetchQuery(loadPathQuery(null, input.globalSDK)),
|
||||
const fast = [
|
||||
() =>
|
||||
input.queryClient.fetchQuery(
|
||||
loadProjectsQuery(input.globalSDK, (data) => input.setGlobalStore("project", data ?? [])),
|
||||
retry(() =>
|
||||
input.globalSDK.global.config.get().then((x) => {
|
||||
input.setGlobalStore("config", reconcile(x.data!, { merge: false }))
|
||||
}),
|
||||
),
|
||||
]
|
||||
|
||||
const slow = [
|
||||
() =>
|
||||
input.queryClient.fetchQuery({
|
||||
...loadProvidersQuery(null),
|
||||
queryFn: () =>
|
||||
retry(() =>
|
||||
input.globalSDK.provider.list().then((x) => {
|
||||
input.setGlobalStore("provider", normalizeProviderList(x.data!))
|
||||
return null
|
||||
}),
|
||||
),
|
||||
}),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.globalSDK.path.get().then((x) => {
|
||||
input.setGlobalStore("path", x.data!)
|
||||
}),
|
||||
),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.globalSDK.project.list().then((x) => {
|
||||
const projects = (x.data ?? [])
|
||||
.filter((p) => !!p?.id)
|
||||
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
|
||||
.slice()
|
||||
.sort((a, b) => cmp(a.id, b.id))
|
||||
input.setGlobalStore("project", projects)
|
||||
}),
|
||||
),
|
||||
]
|
||||
await runAll(fast)
|
||||
// showErrors({
|
||||
// errors: errors(await runAll(fast)),
|
||||
// title: input.requestFailedTitle,
|
||||
// translate: input.translate,
|
||||
// formatMoreCount: input.formatMoreCount,
|
||||
// })
|
||||
await waitForPaint()
|
||||
await runAll(slow)
|
||||
// showErrors({
|
||||
// errors: errors(),
|
||||
@@ -147,6 +128,7 @@ export async function bootstrapGlobal(input: {
|
||||
// translate: input.translate,
|
||||
// formatMoreCount: input.formatMoreCount,
|
||||
// })
|
||||
input.setGlobalStore("ready", true)
|
||||
}
|
||||
|
||||
function groupBySession<T extends { id: string; sessionID: string }>(input: T[]) {
|
||||
@@ -197,28 +179,26 @@ function warmSessions(input: {
|
||||
).then(() => undefined)
|
||||
}
|
||||
|
||||
export const loadProvidersQuery = (directory: string | null, sdk?: OpencodeClient) =>
|
||||
queryOptions({
|
||||
queryKey: [directory, "providers"],
|
||||
queryFn: sdk ? () => retry(() => sdk.provider.list().then((x) => normalizeProviderList(x.data!))) : skipToken,
|
||||
})
|
||||
export const loadProvidersQuery = (directory: string | null) =>
|
||||
queryOptions<null>({ queryKey: [directory, "providers"], queryFn: skipToken })
|
||||
|
||||
export const loadAgentsQuery = (
|
||||
directory: string | null,
|
||||
sdk?: OpencodeClient,
|
||||
transform?: (x: Awaited<ReturnType<OpencodeClient["app"]["agents"]>>) => void,
|
||||
) =>
|
||||
queryOptions({
|
||||
queryOptions<null>({
|
||||
queryKey: [directory, "agents"],
|
||||
queryFn: sdk
|
||||
? () =>
|
||||
retry(() =>
|
||||
sdk.app.agents().then((x) => {
|
||||
transform?.(x)
|
||||
return x.data!
|
||||
}),
|
||||
)
|
||||
: skipToken,
|
||||
queryFn:
|
||||
sdk && transform
|
||||
? () =>
|
||||
retry(() =>
|
||||
sdk.app
|
||||
.agents()
|
||||
.then(transform)
|
||||
.then(() => null),
|
||||
)
|
||||
: skipToken,
|
||||
})
|
||||
|
||||
export const loadPathQuery = (
|
||||
@@ -228,15 +208,16 @@ export const loadPathQuery = (
|
||||
) =>
|
||||
queryOptions<Path>({
|
||||
queryKey: [directory, "path"],
|
||||
queryFn: sdk
|
||||
? () =>
|
||||
retry(() =>
|
||||
sdk.path.get().then(async (x) => {
|
||||
transform?.(x)
|
||||
return x.data!
|
||||
}),
|
||||
)
|
||||
: skipToken,
|
||||
queryFn:
|
||||
sdk && transform
|
||||
? () =>
|
||||
retry(() =>
|
||||
sdk.path.get().then(async (x) => {
|
||||
transform(x)
|
||||
return x.data!
|
||||
}),
|
||||
)
|
||||
: skipToken,
|
||||
})
|
||||
|
||||
export async function bootstrapDirectory(input: {
|
||||
@@ -266,6 +247,13 @@ export async function bootstrapDirectory(input: {
|
||||
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
||||
input.setStore("config", reconcile(input.global.config, { merge: false }))
|
||||
}
|
||||
if (loading || input.store.provider.all.length === 0) {
|
||||
input.setStore("provider_ready", false)
|
||||
}
|
||||
input.setStore("mcp_ready", false)
|
||||
input.setStore("mcp", {})
|
||||
input.setStore("lsp_ready", false)
|
||||
input.setStore("lsp", [])
|
||||
if (loading) input.setStore("status", "partial")
|
||||
|
||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||
@@ -352,15 +340,33 @@ export async function bootstrapDirectory(input: {
|
||||
}),
|
||||
),
|
||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||
() => input.queryClient.fetchQuery(loadMcpQuery(input.directory, input.sdk)),
|
||||
() =>
|
||||
input.queryClient.fetchQuery(loadProvidersQuery(input.directory, input.sdk)).catch((err) => {
|
||||
const project = getFilename(input.directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||
description: formatServerError(err, input.translate),
|
||||
})
|
||||
retry(() =>
|
||||
input.sdk.mcp.status().then((x) => {
|
||||
input.setStore("mcp", x.data!)
|
||||
input.setStore("mcp_ready", true)
|
||||
}),
|
||||
),
|
||||
() =>
|
||||
input.queryClient.ensureQueryData({
|
||||
...loadProvidersQuery(input.directory),
|
||||
queryFn: () =>
|
||||
retry(() => input.sdk.provider.list())
|
||||
.then((x) => {
|
||||
if (providerRev.get(input.directory) !== rev) return
|
||||
input.setStore("provider", normalizeProviderList(x.data!))
|
||||
input.setStore("provider_ready", true)
|
||||
})
|
||||
.catch((err) => {
|
||||
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
|
||||
const project = getFilename(input.directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||
description: formatServerError(err, input.translate),
|
||||
})
|
||||
})
|
||||
.then(() => null),
|
||||
}),
|
||||
].filter(Boolean) as (() => Promise<any>)[]
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ describe("createChildStoreManager", () => {
|
||||
onBootstrap() {},
|
||||
onDispose() {},
|
||||
translate: (key) => key,
|
||||
getSdk: () => null!,
|
||||
})
|
||||
|
||||
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
|
||||
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import type { OpencodeClient, VcsInfo } from "@opencode-ai/sdk/v2/client"
|
||||
import type { VcsInfo } from "@opencode-ai/sdk/v2/client"
|
||||
import {
|
||||
DIR_IDLE_TTL_MS,
|
||||
MAX_DIR_STORES,
|
||||
@@ -14,9 +14,8 @@ import {
|
||||
type VcsCache,
|
||||
} from "./types"
|
||||
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
|
||||
import { useQueries } from "@tanstack/solid-query"
|
||||
import { loadPathQuery, loadProvidersQuery } from "./bootstrap"
|
||||
import { loadLspQuery, loadMcpQuery } from "../global-sync"
|
||||
import { useQuery } from "@tanstack/solid-query"
|
||||
import { loadPathQuery } from "./bootstrap"
|
||||
|
||||
export function createChildStoreManager(input: {
|
||||
owner: Owner
|
||||
@@ -25,7 +24,6 @@ export function createChildStoreManager(input: {
|
||||
onBootstrap: (directory: string) => void
|
||||
onDispose: (directory: string) => void
|
||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||
getSdk: (directory: string) => OpencodeClient
|
||||
}) {
|
||||
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
||||
const vcsCache = new Map<string, VcsCache>()
|
||||
@@ -158,27 +156,14 @@ export function createChildStoreManager(input: {
|
||||
|
||||
const init = () =>
|
||||
createRoot((dispose) => {
|
||||
const sdk = input.getSdk(directory)
|
||||
|
||||
const initialMeta = meta[0].value
|
||||
const initialIcon = icon[0].value
|
||||
|
||||
const [pathQuery, mcpQuery, lspQuery, providerQuery] = useQueries(() => ({
|
||||
queries: [
|
||||
loadPathQuery(directory, sdk),
|
||||
loadMcpQuery(directory, sdk),
|
||||
loadLspQuery(directory, sdk),
|
||||
loadProvidersQuery(directory, sdk),
|
||||
],
|
||||
}))
|
||||
|
||||
const pathQuery = useQuery(() => loadPathQuery(directory))
|
||||
const child = createStore<State>({
|
||||
project: "",
|
||||
projectMeta: initialMeta,
|
||||
projectMeta: undefined,
|
||||
icon: initialIcon,
|
||||
get provider_ready() {
|
||||
return providerQuery.isLoading
|
||||
},
|
||||
provider_ready: false,
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
config: {},
|
||||
get path() {
|
||||
@@ -196,18 +181,10 @@ export function createChildStoreManager(input: {
|
||||
todo: {},
|
||||
permission: {},
|
||||
question: {},
|
||||
get mcp_ready() {
|
||||
return mcpQuery.isLoading
|
||||
},
|
||||
get mcp() {
|
||||
return mcpQuery.isLoading ? {} : (mcpQuery.data ?? {})
|
||||
},
|
||||
get lsp_ready() {
|
||||
return lspQuery.isLoading
|
||||
},
|
||||
get lsp() {
|
||||
return lspQuery.isLoading ? [] : (lspQuery.data ?? [])
|
||||
},
|
||||
mcp_ready: false,
|
||||
mcp: {},
|
||||
lsp_ready: false,
|
||||
lsp: [],
|
||||
vcs: vcsStore.value,
|
||||
limit: 5,
|
||||
message: {},
|
||||
@@ -230,11 +207,6 @@ export function createChildStoreManager(input: {
|
||||
child[1]("vcs", (value) => value ?? cached)
|
||||
})
|
||||
|
||||
onPersistedInit(meta[2], () => {
|
||||
if (child[0].projectMeta !== initialMeta) return
|
||||
child[1]("projectMeta", meta[0].value)
|
||||
})
|
||||
|
||||
onPersistedInit(icon[2], () => {
|
||||
if (child[0].icon !== initialIcon) return
|
||||
child[1]("icon", icon[0].value)
|
||||
|
||||
@@ -391,14 +391,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
? globalSync.data.project.find((x) => x.id === projectID)
|
||||
: globalSync.data.project.find((x) => x.worktree === project.worktree)
|
||||
|
||||
// Preserve local icon override from per-workspace localStorage cache (childStore.icon).
|
||||
// Without this, different subdirectories of the same git repo would share the same
|
||||
// icon from the database instead of using their individual overrides.
|
||||
const base = { ...metadata, ...project }
|
||||
if (childStore.icon) {
|
||||
return { ...base, icon: { ...base.icon, override: childStore.icon } }
|
||||
}
|
||||
return base
|
||||
return { ...metadata, ...project }
|
||||
}
|
||||
|
||||
const roots = createMemo(() => {
|
||||
|
||||
@@ -9,8 +9,8 @@ export const config = {
|
||||
github: {
|
||||
repoUrl: "https://github.com/anomalyco/opencode",
|
||||
starsFormatted: {
|
||||
compact: "150K",
|
||||
full: "150,000",
|
||||
compact: "140K",
|
||||
full: "140,000",
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -7,4 +7,3 @@ src/provider/models-snapshot.js
|
||||
src/provider/models-snapshot.d.ts
|
||||
script/build-*.ts
|
||||
temporary-*.md
|
||||
.artifacts
|
||||
|
||||
@@ -1,26 +1,15 @@
|
||||
import { Server } from "../../server/server"
|
||||
import { PublicApi } from "../../server/routes/instance/httpapi/public"
|
||||
import type { CommandModule } from "yargs"
|
||||
import { OpenApi } from "effect/unstable/httpapi"
|
||||
|
||||
type Args = {
|
||||
httpapi: boolean
|
||||
}
|
||||
|
||||
export const GenerateCommand = {
|
||||
command: "generate",
|
||||
builder: (yargs) =>
|
||||
yargs.option("httpapi", {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
description: "Generate OpenAPI from the experimental Effect HttpApi contract",
|
||||
}),
|
||||
handler: async (args) => {
|
||||
const specs = args.httpapi ? OpenApi.fromApi(PublicApi) : await Server.openapi()
|
||||
handler: async () => {
|
||||
const specs = await Server.openapi()
|
||||
for (const item of Object.values(specs.paths)) {
|
||||
for (const method of ["get", "post", "put", "delete", "patch"] as const) {
|
||||
const operation = item[method]
|
||||
if (!operation?.operationId) continue
|
||||
// @ts-expect-error
|
||||
operation["x-codeSamples"] = [
|
||||
{
|
||||
lang: "js",
|
||||
@@ -58,4 +47,4 @@ export const GenerateCommand = {
|
||||
})
|
||||
})
|
||||
},
|
||||
} satisfies CommandModule<object, Args>
|
||||
} satisfies CommandModule
|
||||
|
||||
@@ -116,12 +116,6 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
|
||||
reconnect = setTimeout(connect, delay)
|
||||
}
|
||||
|
||||
const scheduleZedPoll = () => {
|
||||
if (closed) return
|
||||
if (reconnect) clearTimeout(reconnect)
|
||||
reconnect = setTimeout(connect, 1000)
|
||||
}
|
||||
|
||||
const connect = () => {
|
||||
if (closed) return
|
||||
|
||||
@@ -151,7 +145,7 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
|
||||
.finally(() => {
|
||||
zedSelection = undefined
|
||||
})
|
||||
scheduleZedPoll()
|
||||
scheduleReconnect()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ export const layer = Layer.effect(
|
||||
Authorization: `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${Flag.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`,
|
||||
}
|
||||
: undefined,
|
||||
fetch: async (...args) => Server.Default().app.fetch(...args),
|
||||
fetch: async (...args) => (await Server.Default()).app.fetch(...args),
|
||||
})
|
||||
const cfg = yield* config.get()
|
||||
const input: PluginInput = {
|
||||
|
||||
@@ -1089,39 +1089,6 @@ export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema | JS
|
||||
}
|
||||
*/
|
||||
|
||||
// Moonshot models want their tools in MFJS format: https://github.com/MoonshotAI/walle/blob/main/docs/mfjs-spec.md
|
||||
if (model.providerID === "moonshotai" || model.api.id.toLowerCase().includes("kimi")) {
|
||||
const isRecord = (obj: unknown): obj is Record<string, unknown> =>
|
||||
typeof obj === "object" && obj !== null && !Array.isArray(obj)
|
||||
const sanitizeMoonshot = (obj: unknown): void => {
|
||||
if (Array.isArray(obj)) return obj.forEach(sanitizeMoonshot)
|
||||
if (!isRecord(obj)) return
|
||||
// Moonshot expands $ref before validation and rejects sibling keywords like description on the same node.
|
||||
if (typeof obj.$ref === "string") {
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (key !== "$ref") delete obj[key]
|
||||
}
|
||||
return
|
||||
}
|
||||
for (const key of ["title", "$comment", "format"]) {
|
||||
delete obj[key]
|
||||
}
|
||||
for (const key of ["exclusiveMinimum", "exclusiveMaximum", "minContains", "maxContains"]) {
|
||||
delete obj[key]
|
||||
}
|
||||
// MFJS does not support tuple-style arrays (`prefixItems`) or open-ended tuple controls.
|
||||
const prefixItems = Array.isArray(obj.prefixItems) ? obj.prefixItems : undefined
|
||||
delete obj.unevaluatedItems
|
||||
Object.values(obj).forEach(sanitizeMoonshot)
|
||||
// MFJS does not support tuple-style `items` arrays; it requires one schema object for all array items.
|
||||
if (Array.isArray(obj.items)) obj.items = obj.items[0] ?? {}
|
||||
if (prefixItems && !isRecord(obj.items)) obj.items = prefixItems[0] ?? {}
|
||||
delete obj.prefixItems
|
||||
}
|
||||
|
||||
sanitizeMoonshot(schema)
|
||||
}
|
||||
|
||||
// Convert integer enums to string enums for Google/Gemini
|
||||
if (model.providerID === "google" || model.api.id.includes("gemini")) {
|
||||
const isPlainObject = (node: unknown): node is Record<string, any> =>
|
||||
|
||||
@@ -1,44 +1,40 @@
|
||||
import type { Hono } from "hono"
|
||||
import { createBunWebSocket } from "hono/bun"
|
||||
import type { Adapter, FetchApp, Opts } from "./adapter"
|
||||
|
||||
function listen(app: FetchApp, opts: Opts, websocket?: ReturnType<typeof createBunWebSocket>["websocket"]) {
|
||||
const start = (port: number) => {
|
||||
try {
|
||||
if (websocket) {
|
||||
return Bun.serve({ fetch: app.fetch, hostname: opts.hostname, idleTimeout: 0, websocket, port })
|
||||
}
|
||||
return Bun.serve({ fetch: app.fetch, hostname: opts.hostname, idleTimeout: 0, port })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
const server = opts.port === 0 ? (start(4096) ?? start(0)) : start(opts.port)
|
||||
if (!server) {
|
||||
throw new Error(`Failed to start server on port ${opts.port}`)
|
||||
}
|
||||
if (!server.port) {
|
||||
throw new Error(`Failed to resolve server address for port ${opts.port}`)
|
||||
}
|
||||
return {
|
||||
port: server.port,
|
||||
stop(close?: boolean) {
|
||||
return Promise.resolve(server.stop(close))
|
||||
},
|
||||
}
|
||||
}
|
||||
import type { Adapter } from "./adapter"
|
||||
|
||||
export const adapter: Adapter = {
|
||||
create(app: Hono) {
|
||||
const ws = createBunWebSocket()
|
||||
return {
|
||||
upgradeWebSocket: ws.upgradeWebSocket,
|
||||
listen: (opts) => Promise.resolve(listen(app, opts, ws.websocket)),
|
||||
}
|
||||
},
|
||||
createFetch(app) {
|
||||
return {
|
||||
listen: (opts) => Promise.resolve(listen(app, opts)),
|
||||
async listen(opts) {
|
||||
const args = {
|
||||
fetch: app.fetch,
|
||||
hostname: opts.hostname,
|
||||
idleTimeout: 0,
|
||||
websocket: ws.websocket,
|
||||
} as const
|
||||
const start = (port: number) => {
|
||||
try {
|
||||
return Bun.serve({ ...args, port })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
const server = opts.port === 0 ? (start(4096) ?? start(0)) : start(opts.port)
|
||||
if (!server) {
|
||||
throw new Error(`Failed to start server on port ${opts.port}`)
|
||||
}
|
||||
if (!server.port) {
|
||||
throw new Error(`Failed to resolve server address for port ${opts.port}`)
|
||||
}
|
||||
return {
|
||||
port: server.port,
|
||||
stop(close?: boolean) {
|
||||
return Promise.resolve(server.stop(close))
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,73 +1,66 @@
|
||||
import { createAdaptorServer, type ServerType } from "@hono/node-server"
|
||||
import { createNodeWebSocket } from "@hono/node-ws"
|
||||
import type { Hono } from "hono"
|
||||
import type { Adapter, FetchApp, Opts } from "./adapter"
|
||||
|
||||
async function listen(app: FetchApp, opts: Opts, inject?: (server: ServerType) => void) {
|
||||
const start = (port: number) =>
|
||||
new Promise<ServerType>((resolve, reject) => {
|
||||
const server = createAdaptorServer({ fetch: app.fetch })
|
||||
inject?.(server)
|
||||
const fail = (err: Error) => {
|
||||
cleanup()
|
||||
reject(err)
|
||||
}
|
||||
const ready = () => {
|
||||
cleanup()
|
||||
resolve(server)
|
||||
}
|
||||
const cleanup = () => {
|
||||
server.off("error", fail)
|
||||
server.off("listening", ready)
|
||||
}
|
||||
server.once("error", fail)
|
||||
server.once("listening", ready)
|
||||
server.listen(port, opts.hostname)
|
||||
})
|
||||
|
||||
const server = opts.port === 0 ? await start(4096).catch(() => start(0)) : await start(opts.port)
|
||||
const addr = server.address()
|
||||
if (!addr || typeof addr === "string") {
|
||||
throw new Error(`Failed to resolve server address for port ${opts.port}`)
|
||||
}
|
||||
|
||||
let closing: Promise<void> | undefined
|
||||
return {
|
||||
port: addr.port,
|
||||
stop(close?: boolean) {
|
||||
closing ??= new Promise<void>((resolve, reject) => {
|
||||
server.close((err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
if (close) {
|
||||
if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") {
|
||||
server.closeAllConnections()
|
||||
}
|
||||
if ("closeIdleConnections" in server && typeof server.closeIdleConnections === "function") {
|
||||
server.closeIdleConnections()
|
||||
}
|
||||
}
|
||||
})
|
||||
return closing
|
||||
},
|
||||
}
|
||||
}
|
||||
import type { Adapter } from "./adapter"
|
||||
|
||||
export const adapter: Adapter = {
|
||||
create(app: Hono) {
|
||||
const ws = createNodeWebSocket({ app })
|
||||
return {
|
||||
upgradeWebSocket: ws.upgradeWebSocket,
|
||||
listen: (opts) => listen(app, opts, ws.injectWebSocket),
|
||||
}
|
||||
},
|
||||
createFetch(app) {
|
||||
return {
|
||||
listen: (opts) => listen(app, opts),
|
||||
async listen(opts) {
|
||||
const start = (port: number) =>
|
||||
new Promise<ServerType>((resolve, reject) => {
|
||||
const server = createAdaptorServer({ fetch: app.fetch })
|
||||
ws.injectWebSocket(server)
|
||||
const fail = (err: Error) => {
|
||||
cleanup()
|
||||
reject(err)
|
||||
}
|
||||
const ready = () => {
|
||||
cleanup()
|
||||
resolve(server)
|
||||
}
|
||||
const cleanup = () => {
|
||||
server.off("error", fail)
|
||||
server.off("listening", ready)
|
||||
}
|
||||
server.once("error", fail)
|
||||
server.once("listening", ready)
|
||||
server.listen(port, opts.hostname)
|
||||
})
|
||||
|
||||
const server = opts.port === 0 ? await start(4096).catch(() => start(0)) : await start(opts.port)
|
||||
const addr = server.address()
|
||||
if (!addr || typeof addr === "string") {
|
||||
throw new Error(`Failed to resolve server address for port ${opts.port}`)
|
||||
}
|
||||
|
||||
let closing: Promise<void> | undefined
|
||||
return {
|
||||
port: addr.port,
|
||||
stop(close?: boolean) {
|
||||
closing ??= new Promise((resolve, reject) => {
|
||||
server.close((err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
if (close) {
|
||||
if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") {
|
||||
server.closeAllConnections()
|
||||
}
|
||||
if ("closeIdleConnections" in server && typeof server.closeIdleConnections === "function") {
|
||||
server.closeIdleConnections()
|
||||
}
|
||||
}
|
||||
})
|
||||
return closing
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import type { Hono } from "hono"
|
||||
import type { UpgradeWebSocket } from "hono/ws"
|
||||
|
||||
export type FetchApp = {
|
||||
fetch(request: Request): Response | Promise<Response>
|
||||
}
|
||||
|
||||
export type Opts = {
|
||||
port: number
|
||||
hostname: string
|
||||
@@ -22,5 +18,4 @@ export interface Runtime {
|
||||
|
||||
export interface Adapter {
|
||||
create(app: Hono): Runtime
|
||||
createFetch(app: FetchApp): Omit<Runtime, "upgradeWebSocket">
|
||||
}
|
||||
|
||||
@@ -37,16 +37,6 @@ const ConsoleSwitchBody = z.object({
|
||||
orgID: z.string(),
|
||||
})
|
||||
|
||||
const QueryBoolean = z.union([
|
||||
z.preprocess((value) => (value === "true" ? true : value === "false" ? false : value), z.boolean()),
|
||||
z.enum(["true", "false"]),
|
||||
])
|
||||
|
||||
function queryBoolean(value: z.infer<typeof QueryBoolean> | undefined) {
|
||||
if (value === undefined) return
|
||||
return value === true || value === "true"
|
||||
}
|
||||
|
||||
export const ExperimentalRoutes = lazy(() =>
|
||||
new Hono()
|
||||
.get(
|
||||
@@ -356,7 +346,7 @@ export const ExperimentalRoutes = lazy(() =>
|
||||
"query",
|
||||
z.object({
|
||||
directory: z.string().optional().meta({ description: "Filter sessions by project directory" }),
|
||||
roots: QueryBoolean.optional().meta({ description: "Only return root sessions (no parentID)" }),
|
||||
roots: z.coerce.boolean().optional().meta({ description: "Only return root sessions (no parentID)" }),
|
||||
start: z.coerce
|
||||
.number()
|
||||
.optional()
|
||||
@@ -367,7 +357,7 @@ export const ExperimentalRoutes = lazy(() =>
|
||||
.meta({ description: "Return sessions updated before this timestamp (milliseconds since epoch)" }),
|
||||
search: z.string().optional().meta({ description: "Filter sessions by title (case-insensitive)" }),
|
||||
limit: z.coerce.number().optional().meta({ description: "Maximum number of sessions to return" }),
|
||||
archived: QueryBoolean.optional().meta({ description: "Include archived sessions (default false)" }),
|
||||
archived: z.coerce.boolean().optional().meta({ description: "Include archived sessions (default false)" }),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
@@ -376,12 +366,12 @@ export const ExperimentalRoutes = lazy(() =>
|
||||
const sessions: Session.GlobalInfo[] = []
|
||||
for await (const session of Session.listGlobal({
|
||||
directory: query.directory,
|
||||
roots: queryBoolean(query.roots),
|
||||
roots: query.roots,
|
||||
start: query.start,
|
||||
cursor: query.cursor,
|
||||
search: query.search,
|
||||
limit: limit + 1,
|
||||
archived: queryBoolean(query.archived),
|
||||
archived: query.archived,
|
||||
})) {
|
||||
sessions.push(session)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Session } from "@/session/session"
|
||||
import { ToolRegistry } from "@/tool/registry"
|
||||
import * as EffectZod from "@/util/effect-zod"
|
||||
import { Worktree } from "@/worktree"
|
||||
import { Effect, Layer, Option, Schema, SchemaGetter } from "effect"
|
||||
import { Effect, Layer, Option, Schema } from "effect"
|
||||
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"
|
||||
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { Authorization } from "./auth"
|
||||
@@ -51,21 +51,15 @@ const ToolListQuery = Schema.Struct({
|
||||
model: ModelID,
|
||||
})
|
||||
|
||||
const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
|
||||
Schema.decodeTo(Schema.Boolean, {
|
||||
decode: SchemaGetter.transform((value) => value === "true"),
|
||||
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
|
||||
}),
|
||||
)
|
||||
const WorktreeList = Schema.Array(Schema.String).annotate({ identifier: "WorktreeList" })
|
||||
const SessionListQuery = Schema.Struct({
|
||||
directory: Schema.optional(Schema.String),
|
||||
roots: Schema.optional(QueryBoolean),
|
||||
roots: Schema.optional(Schema.Literals(["true", "false"])),
|
||||
start: Schema.optional(Schema.NumberFromString),
|
||||
cursor: Schema.optional(Schema.NumberFromString),
|
||||
search: Schema.optional(Schema.String),
|
||||
limit: Schema.optional(Schema.NumberFromString),
|
||||
archived: Schema.optional(QueryBoolean),
|
||||
archived: Schema.optional(Schema.Literals(["true", "false"])),
|
||||
})
|
||||
|
||||
export const ExperimentalPaths = {
|
||||
@@ -105,7 +99,6 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
||||
HttpApiEndpoint.post("consoleSwitch", ExperimentalPaths.consoleSwitch, {
|
||||
payload: ConsoleSwitchPayload,
|
||||
success: Schema.Boolean,
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.console.switchOrg",
|
||||
@@ -314,12 +307,12 @@ export const experimentalHandlers = Layer.unwrap(
|
||||
const sessions = Array.from(
|
||||
Session.listGlobal({
|
||||
directory: ctx.query.directory,
|
||||
roots: ctx.query.roots,
|
||||
roots: ctx.query.roots === "true" ? true : undefined,
|
||||
start: ctx.query.start,
|
||||
cursor: ctx.query.cursor,
|
||||
search: ctx.query.search,
|
||||
limit: limit + 1,
|
||||
archived: ctx.query.archived,
|
||||
archived: ctx.query.archived === "true" ? true : undefined,
|
||||
}),
|
||||
)
|
||||
const list = sessions.length > limit ? sessions.slice(0, limit) : sessions
|
||||
|
||||
@@ -20,10 +20,6 @@ const AuthCallbackPayload = Schema.Struct({
|
||||
const AuthRemoveResponse = Schema.Struct({
|
||||
success: Schema.Literal(true),
|
||||
}).annotate({ identifier: "McpAuthRemoveResponse" })
|
||||
class UnsupportedOAuthError extends Schema.ErrorClass<UnsupportedOAuthError>("McpUnsupportedOAuthError")(
|
||||
{ error: Schema.String },
|
||||
{ httpApiStatus: 400 },
|
||||
) {}
|
||||
|
||||
export const McpPaths = {
|
||||
status: "/mcp",
|
||||
@@ -50,7 +46,6 @@ export const McpApi = HttpApi.make("mcp")
|
||||
HttpApiEndpoint.post("add", McpPaths.status, {
|
||||
payload: AddPayload,
|
||||
success: StatusMap,
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "mcp.add",
|
||||
@@ -61,7 +56,6 @@ export const McpApi = HttpApi.make("mcp")
|
||||
HttpApiEndpoint.post("authStart", McpPaths.auth, {
|
||||
params: { name: Schema.String },
|
||||
success: AuthStartResponse,
|
||||
error: UnsupportedOAuthError,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "mcp.auth.start",
|
||||
@@ -84,7 +78,6 @@ export const McpApi = HttpApi.make("mcp")
|
||||
HttpApiEndpoint.post("authAuthenticate", McpPaths.authAuthenticate, {
|
||||
params: { name: Schema.String },
|
||||
success: MCP.Status,
|
||||
error: UnsupportedOAuthError,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "mcp.auth.authenticate",
|
||||
@@ -153,9 +146,7 @@ export const mcpHandlers = Layer.unwrap(
|
||||
})
|
||||
|
||||
const authStart = Effect.fn("McpHttpApi.authStart")(function* (ctx: { params: { name: string } }) {
|
||||
if (!(yield* mcp.supportsOAuth(ctx.params.name))) {
|
||||
return yield* new UnsupportedOAuthError({ error: `MCP server ${ctx.params.name} does not support OAuth` })
|
||||
}
|
||||
if (!(yield* mcp.supportsOAuth(ctx.params.name))) return yield* new HttpApiError.BadRequest({})
|
||||
return yield* mcp.startAuth(ctx.params.name)
|
||||
})
|
||||
|
||||
@@ -167,9 +158,7 @@ export const mcpHandlers = Layer.unwrap(
|
||||
})
|
||||
|
||||
const authAuthenticate = Effect.fn("McpHttpApi.authAuthenticate")(function* (ctx: { params: { name: string } }) {
|
||||
if (!(yield* mcp.supportsOAuth(ctx.params.name))) {
|
||||
return yield* new UnsupportedOAuthError({ error: `MCP server ${ctx.params.name} does not support OAuth` })
|
||||
}
|
||||
if (!(yield* mcp.supportsOAuth(ctx.params.name))) return yield* new HttpApiError.BadRequest({})
|
||||
return yield* mcp.authenticate(ctx.params.name)
|
||||
})
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Provider } from "@/provider/provider"
|
||||
import { ProviderID } from "@/provider/schema"
|
||||
import { mapValues } from "remeda"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
||||
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { Authorization } from "./auth"
|
||||
|
||||
@@ -36,8 +35,7 @@ export const ProviderApi = HttpApi.make("provider")
|
||||
HttpApiEndpoint.post("authorize", `${root}/:providerID/oauth/authorize`, {
|
||||
params: { providerID: ProviderID },
|
||||
payload: ProviderAuth.AuthorizeInput,
|
||||
success: Schema.UndefinedOr(ProviderAuth.Authorization),
|
||||
error: HttpApiError.BadRequest,
|
||||
success: ProviderAuth.Authorization,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "provider.oauth.authorize",
|
||||
@@ -49,7 +47,6 @@ export const ProviderApi = HttpApi.make("provider")
|
||||
params: { providerID: ProviderID },
|
||||
payload: ProviderAuth.CallbackInput,
|
||||
success: Schema.Boolean,
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "provider.oauth.callback",
|
||||
@@ -118,22 +115,10 @@ export const providerHandlers = Layer.unwrap(
|
||||
inputs: ctx.payload.inputs,
|
||||
})
|
||||
.pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({}))))
|
||||
if (!result) return yield* new HttpApiError.BadRequest({})
|
||||
return result
|
||||
})
|
||||
|
||||
const authorizeRaw = Effect.fn("ProviderHttpApi.authorizeRaw")(function* (ctx: {
|
||||
params: { providerID: ProviderID }
|
||||
request: HttpServerRequest.HttpServerRequest
|
||||
}) {
|
||||
const body = yield* Effect.orDie(ctx.request.text)
|
||||
const payload = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(ProviderAuth.AuthorizeInput))(body).pipe(
|
||||
Effect.mapError(() => new HttpApiError.BadRequest({})),
|
||||
)
|
||||
const result = yield* authorize({ params: ctx.params, payload })
|
||||
if (result === undefined) return HttpServerResponse.empty({ status: 200 })
|
||||
return HttpServerResponse.jsonUnsafe(result)
|
||||
})
|
||||
|
||||
const callback = Effect.fn("ProviderHttpApi.callback")(function* (ctx: {
|
||||
params: { providerID: ProviderID }
|
||||
payload: ProviderAuth.CallbackInput
|
||||
@@ -149,11 +134,7 @@ export const providerHandlers = Layer.unwrap(
|
||||
})
|
||||
|
||||
return HttpApiBuilder.group(ProviderApi, "provider", (handlers) =>
|
||||
handlers
|
||||
.handle("list", list)
|
||||
.handle("auth", auth)
|
||||
.handleRaw("authorize", authorizeRaw)
|
||||
.handle("callback", callback),
|
||||
handlers.handle("list", list).handle("auth", auth).handle("authorize", authorize).handle("callback", callback),
|
||||
)
|
||||
}),
|
||||
).pipe(
|
||||
|
||||
@@ -118,6 +118,7 @@ export const PtyConnectApi = HttpApi.make("pty-connect").add(
|
||||
.add(
|
||||
HttpApiEndpoint.get("connect", PtyPaths.connect, {
|
||||
params: Params,
|
||||
query: CursorQuery,
|
||||
success: Schema.Boolean,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
||||
@@ -17,56 +17,6 @@ import { SyncApi } from "./sync"
|
||||
import { TuiApi } from "./tui"
|
||||
import { WorkspaceApi } from "./workspace"
|
||||
|
||||
type OpenApiParameter = {
|
||||
name: string
|
||||
in: string
|
||||
required?: boolean
|
||||
schema?: unknown
|
||||
}
|
||||
|
||||
type OpenApiOperation = {
|
||||
parameters?: OpenApiParameter[]
|
||||
}
|
||||
|
||||
type OpenApiPathItem = Partial<Record<"get" | "post" | "put" | "delete" | "patch", OpenApiOperation>>
|
||||
|
||||
type OpenApiSpec = {
|
||||
paths?: Record<string, OpenApiPathItem>
|
||||
}
|
||||
|
||||
const InstanceQueryParameters = [
|
||||
{
|
||||
name: "directory",
|
||||
in: "query",
|
||||
required: false,
|
||||
schema: { type: "string" },
|
||||
},
|
||||
{
|
||||
name: "workspace",
|
||||
in: "query",
|
||||
required: false,
|
||||
schema: { type: "string" },
|
||||
},
|
||||
] satisfies OpenApiParameter[]
|
||||
|
||||
function documentInstanceQueryParameters(input: Record<string, unknown>) {
|
||||
const spec = input as OpenApiSpec
|
||||
for (const [path, item] of Object.entries(spec.paths ?? {})) {
|
||||
if (path.startsWith("/global/") || path.startsWith("/auth/")) continue
|
||||
for (const method of ["get", "post", "put", "delete", "patch"] as const) {
|
||||
const operation = item[method]
|
||||
if (!operation) continue
|
||||
operation.parameters = [
|
||||
...InstanceQueryParameters,
|
||||
...(operation.parameters ?? []).filter(
|
||||
(param) => param.in !== "query" || (param.name !== "directory" && param.name !== "workspace"),
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
export const PublicApi = HttpApi.make("opencode")
|
||||
.addHttpApi(ControlApi)
|
||||
.addHttpApi(GlobalApi)
|
||||
@@ -91,6 +41,5 @@ export const PublicApi = HttpApi.make("opencode")
|
||||
title: "opencode",
|
||||
version: "1.0.0",
|
||||
description: "opencode api",
|
||||
transform: documentInstanceQueryParameters,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { HttpRouter, HttpServer, HttpServerRequest } from "effect/unstable/http"
|
||||
import { Bus } from "@/bus"
|
||||
@@ -41,8 +41,6 @@ const Headers = Schema.Struct({
|
||||
"x-opencode-directory": Schema.optional(Schema.String),
|
||||
})
|
||||
|
||||
export const context = Context.empty() as Context.Context<unknown>
|
||||
|
||||
function decode(input: string) {
|
||||
try {
|
||||
return decodeURIComponent(input)
|
||||
|
||||
@@ -21,7 +21,7 @@ import { MessageID, PartID, SessionID } from "@/session/schema"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { Effect, Layer, Schema, SchemaGetter, Struct } from "effect"
|
||||
import { Effect, Layer, Schema, Struct } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
||||
import {
|
||||
@@ -37,15 +37,9 @@ import { Authorization } from "./auth"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
const root = "/session"
|
||||
const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
|
||||
Schema.decodeTo(Schema.Boolean, {
|
||||
decode: SchemaGetter.transform((value) => value === "true"),
|
||||
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
|
||||
}),
|
||||
)
|
||||
const ListQuery = Schema.Struct({
|
||||
directory: Schema.optional(Schema.String),
|
||||
roots: Schema.optional(QueryBoolean),
|
||||
roots: Schema.optional(Schema.Literals(["true", "false"])),
|
||||
start: Schema.optional(Schema.NumberFromString),
|
||||
search: Schema.optional(Schema.String),
|
||||
limit: Schema.optional(Schema.NumberFromString),
|
||||
@@ -191,7 +185,6 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID },
|
||||
query: MessagesQuery,
|
||||
success: Schema.Array(MessageV2.WithParts),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.messages",
|
||||
@@ -212,7 +205,6 @@ export const SessionApi = HttpApi.make("session")
|
||||
HttpApiEndpoint.post("create", SessionPaths.create, {
|
||||
payload: [HttpApiSchema.NoContent, Session.CreateInput],
|
||||
success: Session.Info,
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.create",
|
||||
@@ -444,7 +436,7 @@ export const sessionHandlers = Layer.unwrap(
|
||||
Array.from(
|
||||
Session.list({
|
||||
directory: ctx.query.directory,
|
||||
roots: ctx.query.roots,
|
||||
roots: ctx.query.roots === "true" ? true : undefined,
|
||||
start: ctx.query.start,
|
||||
search: ctx.query.search,
|
||||
limit: ctx.query.limit,
|
||||
@@ -480,8 +472,8 @@ export const sessionHandlers = Layer.unwrap(
|
||||
params: { sessionID: SessionID }
|
||||
query: typeof MessagesQuery.Type
|
||||
}) {
|
||||
if (ctx.query.before && ctx.query.limit === undefined) return yield* new HttpApiError.BadRequest({})
|
||||
if (ctx.query.before) {
|
||||
if (ctx.query.before !== undefined && ctx.query.limit === undefined) return yield* new HttpApiError.BadRequest({})
|
||||
if (ctx.query.before !== undefined) {
|
||||
const before = ctx.query.before
|
||||
yield* Effect.try({
|
||||
try: () => MessageV2.cursor.decode(before),
|
||||
|
||||
@@ -9,16 +9,15 @@ import { not } from "drizzle-orm"
|
||||
import { or } from "drizzle-orm"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { EventTable } from "@/sync/event.sql"
|
||||
import { NonNegativeInt } from "@/util/schema"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { Authorization } from "./auth"
|
||||
|
||||
const root = "/sync"
|
||||
const ReplayEvent = Schema.Struct({
|
||||
id: Schema.String,
|
||||
aggregateID: Schema.String,
|
||||
seq: NonNegativeInt,
|
||||
seq: Schema.Number,
|
||||
type: Schema.String,
|
||||
data: Schema.Record(Schema.String, Schema.Unknown),
|
||||
}).annotate({ identifier: "SyncReplayEvent" })
|
||||
@@ -29,7 +28,7 @@ const ReplayPayload = Schema.Struct({
|
||||
const ReplayResponse = Schema.Struct({
|
||||
sessionID: Schema.String,
|
||||
}).annotate({ identifier: "SyncReplayResponse" })
|
||||
const HistoryPayload = Schema.Record(Schema.String, NonNegativeInt)
|
||||
const HistoryPayload = Schema.Record(Schema.String, Schema.Number)
|
||||
const HistoryEvent = Schema.Struct({
|
||||
id: Schema.String,
|
||||
aggregate_id: Schema.String,
|
||||
@@ -60,7 +59,6 @@ export const SyncApi = HttpApi.make("sync")
|
||||
HttpApiEndpoint.post("replay", SyncPaths.replay, {
|
||||
payload: ReplayPayload,
|
||||
success: ReplayResponse,
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "sync.replay",
|
||||
@@ -71,7 +69,6 @@ export const SyncApi = HttpApi.make("sync")
|
||||
HttpApiEndpoint.post("history", SyncPaths.history, {
|
||||
payload: HistoryPayload,
|
||||
success: Schema.Array(HistoryEvent),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "sync.history.list",
|
||||
|
||||
@@ -61,7 +61,6 @@ export const TuiApi = HttpApi.make("tui")
|
||||
HttpApiEndpoint.post("appendPrompt", TuiPaths.appendPrompt, {
|
||||
payload: TuiEvent.PromptAppend.properties,
|
||||
success: Schema.Boolean,
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "tui.appendPrompt",
|
||||
@@ -114,7 +113,6 @@ export const TuiApi = HttpApi.make("tui")
|
||||
HttpApiEndpoint.post("executeCommand", TuiPaths.executeCommand, {
|
||||
payload: CommandPayload,
|
||||
success: Schema.Boolean,
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "tui.executeCommand",
|
||||
@@ -135,7 +133,6 @@ export const TuiApi = HttpApi.make("tui")
|
||||
HttpApiEndpoint.post("publish", TuiPaths.publish, {
|
||||
payload: TuiPublishPayload,
|
||||
success: Schema.Boolean,
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "tui.publish",
|
||||
@@ -146,7 +143,7 @@ export const TuiApi = HttpApi.make("tui")
|
||||
HttpApiEndpoint.post("selectSession", TuiPaths.selectSession, {
|
||||
payload: TuiEvent.SessionSelect.properties,
|
||||
success: Schema.Boolean,
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: HttpApiError.NotFound,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "tui.selectSession",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describeRoute, resolver, validator } from "hono-openapi"
|
||||
import { Hono } from "hono"
|
||||
import type { UpgradeWebSocket } from "hono/ws"
|
||||
import { Effect } from "effect"
|
||||
import { Context, Effect } from "effect"
|
||||
import z from "zod"
|
||||
import { Format } from "@/format"
|
||||
import { TuiRoutes } from "./tui"
|
||||
@@ -14,6 +14,18 @@ import { LSP } from "@/lsp/lsp"
|
||||
import { Command } from "@/command"
|
||||
import { QuestionRoutes } from "./question"
|
||||
import { PermissionRoutes } from "./permission"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { ExperimentalHttpApiServer } from "./httpapi/server"
|
||||
import { PtyPaths } from "./httpapi/pty"
|
||||
import { EventPaths } from "./httpapi/event"
|
||||
import { ExperimentalPaths } from "./httpapi/experimental"
|
||||
import { FilePaths } from "./httpapi/file"
|
||||
import { InstancePaths } from "./httpapi/instance"
|
||||
import { McpPaths } from "./httpapi/mcp"
|
||||
import { SessionPaths } from "./httpapi/session"
|
||||
import { SyncPaths } from "./httpapi/sync"
|
||||
import { TuiPaths } from "./httpapi/tui"
|
||||
import { WorkspacePaths } from "./httpapi/workspace"
|
||||
import { ProjectRoutes } from "./project"
|
||||
import { SessionRoutes } from "./session"
|
||||
import { PtyRoutes } from "./pty"
|
||||
@@ -30,6 +42,118 @@ import { jsonRequest } from "./trace"
|
||||
export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
|
||||
const app = new Hono()
|
||||
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) {
|
||||
const handler = ExperimentalHttpApiServer.webHandler().handler
|
||||
const context = Context.empty() as Context.Context<unknown>
|
||||
app.get(EventPaths.event, (c) => handler(c.req.raw, context))
|
||||
app.get("/question", (c) => handler(c.req.raw, context))
|
||||
app.post("/question/:requestID/reply", (c) => handler(c.req.raw, context))
|
||||
app.post("/question/:requestID/reject", (c) => handler(c.req.raw, context))
|
||||
app.get("/permission", (c) => handler(c.req.raw, context))
|
||||
app.post("/permission/:requestID/reply", (c) => handler(c.req.raw, context))
|
||||
app.get("/config", (c) => handler(c.req.raw, context))
|
||||
app.patch("/config", (c) => handler(c.req.raw, context))
|
||||
app.get("/config/providers", (c) => handler(c.req.raw, context))
|
||||
app.get(ExperimentalPaths.console, (c) => handler(c.req.raw, context))
|
||||
app.get(ExperimentalPaths.consoleOrgs, (c) => handler(c.req.raw, context))
|
||||
app.post(ExperimentalPaths.consoleSwitch, (c) => handler(c.req.raw, context))
|
||||
app.get(ExperimentalPaths.tool, (c) => handler(c.req.raw, context))
|
||||
app.get(ExperimentalPaths.toolIDs, (c) => handler(c.req.raw, context))
|
||||
app.get(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context))
|
||||
app.post(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context))
|
||||
app.delete(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context))
|
||||
app.post(ExperimentalPaths.worktreeReset, (c) => handler(c.req.raw, context))
|
||||
app.get(ExperimentalPaths.session, (c) => handler(c.req.raw, context))
|
||||
app.get(ExperimentalPaths.resource, (c) => handler(c.req.raw, context))
|
||||
app.get("/provider", (c) => handler(c.req.raw, context))
|
||||
app.get("/provider/auth", (c) => handler(c.req.raw, context))
|
||||
app.post("/provider/:providerID/oauth/authorize", (c) => handler(c.req.raw, context))
|
||||
app.post("/provider/:providerID/oauth/callback", (c) => handler(c.req.raw, context))
|
||||
app.get("/project", (c) => handler(c.req.raw, context))
|
||||
app.get("/project/current", (c) => handler(c.req.raw, context))
|
||||
app.post("/project/git/init", (c) => handler(c.req.raw, context))
|
||||
app.patch("/project/:projectID", (c) => handler(c.req.raw, context))
|
||||
app.get(FilePaths.findText, (c) => handler(c.req.raw, context))
|
||||
app.get(FilePaths.findFile, (c) => handler(c.req.raw, context))
|
||||
app.get(FilePaths.findSymbol, (c) => handler(c.req.raw, context))
|
||||
app.get(FilePaths.list, (c) => handler(c.req.raw, context))
|
||||
app.get(FilePaths.content, (c) => handler(c.req.raw, context))
|
||||
app.get(FilePaths.status, (c) => handler(c.req.raw, context))
|
||||
app.get(InstancePaths.path, (c) => handler(c.req.raw, context))
|
||||
app.post(InstancePaths.dispose, (c) => handler(c.req.raw, context))
|
||||
app.get(InstancePaths.vcs, (c) => handler(c.req.raw, context))
|
||||
app.get(InstancePaths.vcsDiff, (c) => handler(c.req.raw, context))
|
||||
app.get(InstancePaths.command, (c) => handler(c.req.raw, context))
|
||||
app.get(InstancePaths.agent, (c) => handler(c.req.raw, context))
|
||||
app.get(InstancePaths.skill, (c) => handler(c.req.raw, context))
|
||||
app.get(InstancePaths.lsp, (c) => handler(c.req.raw, context))
|
||||
app.get(InstancePaths.formatter, (c) => handler(c.req.raw, context))
|
||||
app.get(McpPaths.status, (c) => handler(c.req.raw, context))
|
||||
app.post(McpPaths.status, (c) => handler(c.req.raw, context))
|
||||
app.post(McpPaths.auth, (c) => handler(c.req.raw, context))
|
||||
app.post(McpPaths.authCallback, (c) => handler(c.req.raw, context))
|
||||
app.post(McpPaths.authAuthenticate, (c) => handler(c.req.raw, context))
|
||||
app.delete(McpPaths.auth, (c) => handler(c.req.raw, context))
|
||||
app.post(McpPaths.connect, (c) => handler(c.req.raw, context))
|
||||
app.post(McpPaths.disconnect, (c) => handler(c.req.raw, context))
|
||||
app.post(SyncPaths.start, (c) => handler(c.req.raw, context))
|
||||
app.post(SyncPaths.replay, (c) => handler(c.req.raw, context))
|
||||
app.post(SyncPaths.history, (c) => handler(c.req.raw, context))
|
||||
app.get(PtyPaths.shells, (c) => handler(c.req.raw, context))
|
||||
app.get(PtyPaths.list, (c) => handler(c.req.raw, context))
|
||||
app.post(PtyPaths.create, (c) => handler(c.req.raw, context))
|
||||
app.get(PtyPaths.get, (c) => handler(c.req.raw, context))
|
||||
app.put(PtyPaths.update, (c) => handler(c.req.raw, context))
|
||||
app.delete(PtyPaths.remove, (c) => handler(c.req.raw, context))
|
||||
app.get(PtyPaths.connect, (c) => handler(c.req.raw, context))
|
||||
app.get(SessionPaths.list, (c) => handler(c.req.raw, context))
|
||||
app.get(SessionPaths.status, (c) => handler(c.req.raw, context))
|
||||
app.get(SessionPaths.get, (c) => handler(c.req.raw, context))
|
||||
app.get(SessionPaths.children, (c) => handler(c.req.raw, context))
|
||||
app.get(SessionPaths.todo, (c) => handler(c.req.raw, context))
|
||||
app.get(SessionPaths.diff, (c) => handler(c.req.raw, context))
|
||||
app.get(SessionPaths.messages, (c) => handler(c.req.raw, context))
|
||||
app.get(SessionPaths.message, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.create, (c) => handler(c.req.raw, context))
|
||||
app.delete(SessionPaths.remove, (c) => handler(c.req.raw, context))
|
||||
app.patch(SessionPaths.update, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.init, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.fork, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.abort, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.share, (c) => handler(c.req.raw, context))
|
||||
app.delete(SessionPaths.share, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.summarize, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.prompt, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.promptAsync, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.command, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.shell, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.revert, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.unrevert, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.permissions, (c) => handler(c.req.raw, context))
|
||||
app.delete(SessionPaths.deleteMessage, (c) => handler(c.req.raw, context))
|
||||
app.delete(SessionPaths.deletePart, (c) => handler(c.req.raw, context))
|
||||
app.patch(SessionPaths.updatePart, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.appendPrompt, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.openHelp, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.openSessions, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.openThemes, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.openModels, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.submitPrompt, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.clearPrompt, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.executeCommand, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.showToast, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.publish, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.selectSession, (c) => handler(c.req.raw, context))
|
||||
app.get(TuiPaths.controlNext, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.controlResponse, (c) => handler(c.req.raw, context))
|
||||
app.get(WorkspacePaths.adaptors, (c) => handler(c.req.raw, context))
|
||||
app.post(WorkspacePaths.list, (c) => handler(c.req.raw, context))
|
||||
app.get(WorkspacePaths.list, (c) => handler(c.req.raw, context))
|
||||
app.get(WorkspacePaths.status, (c) => handler(c.req.raw, context))
|
||||
app.delete(WorkspacePaths.remove, (c) => handler(c.req.raw, context))
|
||||
app.post(WorkspacePaths.sessionRestore, (c) => handler(c.req.raw, context))
|
||||
}
|
||||
|
||||
return app
|
||||
.route("/project", ProjectRoutes())
|
||||
.route("/pty", PtyRoutes(upgrade))
|
||||
|
||||
@@ -8,21 +8,6 @@ import { lazy } from "@/util/lazy"
|
||||
import { Effect } from "effect"
|
||||
import { jsonRequest, runRequest } from "./trace"
|
||||
|
||||
const UnsupportedOAuthError = z
|
||||
.object({
|
||||
error: z.string(),
|
||||
})
|
||||
.meta({ ref: "McpUnsupportedOAuthError" })
|
||||
|
||||
const unsupportedOAuthErrorResponse = {
|
||||
description: "MCP server does not support OAuth",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(UnsupportedOAuthError),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const McpRoutes = lazy(() =>
|
||||
new Hono()
|
||||
.get(
|
||||
@@ -100,8 +85,7 @@ export const McpRoutes = lazy(() =>
|
||||
},
|
||||
},
|
||||
},
|
||||
400: unsupportedOAuthErrorResponse,
|
||||
...errors(404),
|
||||
...errors(400, 404),
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
@@ -173,8 +157,7 @@ export const McpRoutes = lazy(() =>
|
||||
},
|
||||
},
|
||||
},
|
||||
400: unsupportedOAuthErrorResponse,
|
||||
...errors(404),
|
||||
...errors(400, 404),
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
|
||||
@@ -30,16 +30,6 @@ import { jsonRequest, runRequest } from "./trace"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
const QueryBoolean = z.union([
|
||||
z.preprocess((value) => (value === "true" ? true : value === "false" ? false : value), z.boolean()),
|
||||
z.enum(["true", "false"]),
|
||||
])
|
||||
|
||||
function queryBoolean(value: z.infer<typeof QueryBoolean> | undefined) {
|
||||
if (value === undefined) return
|
||||
return value === true || value === "true"
|
||||
}
|
||||
|
||||
export const SessionRoutes = lazy(() =>
|
||||
new Hono()
|
||||
.get(
|
||||
@@ -63,7 +53,7 @@ export const SessionRoutes = lazy(() =>
|
||||
"query",
|
||||
z.object({
|
||||
directory: z.string().optional().meta({ description: "Filter sessions by project directory" }),
|
||||
roots: QueryBoolean.optional().meta({ description: "Only return root sessions (no parentID)" }),
|
||||
roots: z.coerce.boolean().optional().meta({ description: "Only return root sessions (no parentID)" }),
|
||||
start: z.coerce
|
||||
.number()
|
||||
.optional()
|
||||
@@ -77,7 +67,7 @@ export const SessionRoutes = lazy(() =>
|
||||
const sessions: Session.Info[] = []
|
||||
for await (const session of Session.list({
|
||||
directory: query.directory,
|
||||
roots: queryBoolean(query.roots),
|
||||
roots: query.roots,
|
||||
start: query.start,
|
||||
search: query.search,
|
||||
limit: query.limit,
|
||||
|
||||
@@ -17,6 +17,8 @@ import { WorkspaceRouterMiddleware } from "./workspace"
|
||||
import { InstanceMiddleware } from "./routes/instance/middleware"
|
||||
import { WorkspaceRoutes } from "./routes/control/workspace"
|
||||
import { ExperimentalHttpApiServer } from "./routes/instance/httpapi/server"
|
||||
import { WorkspacePaths } from "./routes/instance/httpapi/workspace"
|
||||
import { Context } from "effect"
|
||||
|
||||
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
|
||||
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||
@@ -32,35 +34,9 @@ export type Listener = {
|
||||
stop: (close?: boolean) => Promise<void>
|
||||
}
|
||||
|
||||
type ServerApp = {
|
||||
fetch(request: Request): Response | Promise<Response>
|
||||
request(input: string | URL | Request, init?: RequestInit): Response | Promise<Response>
|
||||
}
|
||||
|
||||
const DefaultHono = lazy(() => createHono({}))
|
||||
const DefaultHttpApi = lazy(() => createHttpApi())
|
||||
export const Default = () => (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI ? DefaultHttpApi() : DefaultHono())
|
||||
export const Default = lazy(() => create({}))
|
||||
|
||||
function create(opts: { cors?: string[] }) {
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) return createHttpApi()
|
||||
return createHono(opts)
|
||||
}
|
||||
|
||||
function createHttpApi() {
|
||||
const handler = ExperimentalHttpApiServer.webHandler().handler
|
||||
const app: ServerApp = {
|
||||
fetch: (request: Request) => handler(request, ExperimentalHttpApiServer.context),
|
||||
request(input, init) {
|
||||
return app.fetch(input instanceof Request ? input : new Request(new URL(input, "http://localhost"), init))
|
||||
},
|
||||
}
|
||||
return {
|
||||
app,
|
||||
runtime: adapter.createFetch(app),
|
||||
}
|
||||
}
|
||||
|
||||
function createHono(opts: { cors?: string[] }) {
|
||||
const app = new Hono()
|
||||
.onError(ErrorMiddleware)
|
||||
.use(AuthMiddleware)
|
||||
@@ -86,6 +62,16 @@ function createHono(opts: { cors?: string[] }) {
|
||||
.use(InstanceMiddleware())
|
||||
.route("/experimental/workspace", WorkspaceRoutes())
|
||||
.use(WorkspaceRouterMiddleware(runtime.upgradeWebSocket))
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) {
|
||||
const handler = ExperimentalHttpApiServer.webHandler().handler
|
||||
const context = Context.empty() as Context.Context<unknown>
|
||||
workspaceApp.get(WorkspacePaths.adaptors, (c) => handler(c.req.raw, context))
|
||||
workspaceApp.get(WorkspacePaths.list, (c) => handler(c.req.raw, context))
|
||||
workspaceApp.post(WorkspacePaths.list, (c) => handler(c.req.raw, context))
|
||||
workspaceApp.get(WorkspacePaths.status, (c) => handler(c.req.raw, context))
|
||||
workspaceApp.delete(WorkspacePaths.remove, (c) => handler(c.req.raw, context))
|
||||
workspaceApp.post(WorkspacePaths.sessionRestore, (c) => handler(c.req.raw, context))
|
||||
}
|
||||
workspaceApp.route("/", workspaceLegacyApp)
|
||||
|
||||
return {
|
||||
@@ -103,7 +89,7 @@ export async function openapi() {
|
||||
// hono-openapi can see describeRoute metadata (`.route()` wraps
|
||||
// handlers when the sub-app has a custom errorHandler, which
|
||||
// strips the metadata symbol).
|
||||
const { app } = createHono({})
|
||||
const { app } = create({})
|
||||
const result = await generateSpecs(app, {
|
||||
documentation: {
|
||||
info: {
|
||||
|
||||
@@ -37,8 +37,8 @@ const PRUNE_PROTECTED_TOOLS = ["skill"]
|
||||
const DEFAULT_TAIL_TURNS = 2
|
||||
const MIN_PRESERVE_RECENT_TOKENS = 2_000
|
||||
const MAX_PRESERVE_RECENT_TOKENS = 8_000
|
||||
const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside <template> and keep the section order unchanged. Do not include the <template> tags in your response.
|
||||
<template>
|
||||
const SUMMARY_TEMPLATE = `Output exactly this Markdown structure and keep the section order unchanged:
|
||||
---
|
||||
## Goal
|
||||
- [single-sentence task summary]
|
||||
|
||||
@@ -66,7 +66,7 @@ const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside <te
|
||||
|
||||
## Relevant Files
|
||||
- [file or directory path: why it matters, or "(none)"]
|
||||
</template>
|
||||
---
|
||||
|
||||
Rules:
|
||||
- Keep every section, even when empty.
|
||||
|
||||
@@ -12,8 +12,8 @@ export const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0))
|
||||
export const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
|
||||
|
||||
/**
|
||||
* Optional public JSON field that can hold explicit `undefined` on the type
|
||||
* side but encodes it as an omitted key, matching legacy `JSON.stringify`.
|
||||
* Optional public JSON field that accepts explicit `undefined` internally but
|
||||
* encodes it as an omitted key, matching `JSON.stringify` legacy responses.
|
||||
*/
|
||||
export const optionalOmitUndefined = <S extends Schema.Top>(schema: S) =>
|
||||
Schema.optionalKey(schema).pipe(
|
||||
|
||||
@@ -855,224 +855,6 @@ describe("ProviderTransform.schema - gemini non-object properties removal", () =
|
||||
})
|
||||
})
|
||||
|
||||
describe("ProviderTransform.schema - moonshot $ref siblings", () => {
|
||||
const moonshotModel = {
|
||||
providerID: "moonshotai",
|
||||
api: {
|
||||
id: "kimi-k2",
|
||||
},
|
||||
} as any
|
||||
|
||||
test("removes sibling descriptions from referenced tool parameter schemas", () => {
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
deviceType: {
|
||||
description: "Optional. The type of device that captured the screenshot, e.g. mobile or desktop.",
|
||||
enum: ["DEVICE_TYPE_UNSPECIFIED", "MOBILE", "DESKTOP", "TABLET", "AGNOSTIC"],
|
||||
type: "string",
|
||||
},
|
||||
modelId: {
|
||||
description: "Optional. The model to use for generation.",
|
||||
enum: ["MODEL_ID_UNSPECIFIED", "GEMINI_3_PRO", "GEMINI_3_FLASH", "GEMINI_3_1_PRO"],
|
||||
type: "string",
|
||||
},
|
||||
projectId: {
|
||||
description: "Required. The project ID of screens to generate variants for.",
|
||||
type: "string",
|
||||
},
|
||||
prompt: {
|
||||
description: "Required. The input text used to generate the variants.",
|
||||
type: "string",
|
||||
},
|
||||
selectedScreenIds: {
|
||||
description: "Required. The screen ids of screen to generate variants for.",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
type: "array",
|
||||
},
|
||||
variantOptions: {
|
||||
$ref: "#/$defs/VariantOptions",
|
||||
description:
|
||||
"Required. The variant options for generation, including the number of variants, creative range, and aspects to focus on.",
|
||||
},
|
||||
},
|
||||
required: ["projectId", "selectedScreenIds", "prompt", "variantOptions"],
|
||||
$defs: {
|
||||
VariantOptions: {
|
||||
description:
|
||||
"Configuration options for design variant generation. This message captures all parameters used to generate variants, allowing the configuration to be stored, replayed, or analyzed.",
|
||||
properties: {
|
||||
aspects: {
|
||||
description: "Optional. Specific aspects to focus on. If empty, all aspects may be varied.",
|
||||
items: {
|
||||
enum: ["VARIANT_ASPECT_UNSPECIFIED", "LAYOUT", "COLOR_SCHEME", "IMAGES", "TEXT_FONT", "TEXT_CONTENT"],
|
||||
type: "string",
|
||||
},
|
||||
type: "array",
|
||||
},
|
||||
creativeRange: {
|
||||
description: "Optional. Creative range for variations. Default: EXPLORE",
|
||||
enum: ["CREATIVE_RANGE_UNSPECIFIED", "REFINE", "EXPLORE", "REIMAGINE"],
|
||||
type: "string",
|
||||
},
|
||||
variantCount: {
|
||||
description: "Optional. Number of variants to generate (1-5). Default: 3",
|
||||
format: "int32",
|
||||
type: "integer",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
},
|
||||
},
|
||||
description: "Request message for GenerateVariants.",
|
||||
additionalProperties: false,
|
||||
} as any
|
||||
|
||||
const result = ProviderTransform.schema(moonshotModel, schema) as any
|
||||
|
||||
expect(result.properties.variantOptions).toEqual({
|
||||
$ref: "#/$defs/VariantOptions",
|
||||
})
|
||||
expect(result.$defs.VariantOptions.description).toBe(schema.$defs.VariantOptions.description)
|
||||
})
|
||||
|
||||
test("also runs for kimi models outside the moonshot provider", () => {
|
||||
const result = ProviderTransform.schema(
|
||||
{
|
||||
providerID: "openrouter",
|
||||
name: "Kimi K2",
|
||||
api: {
|
||||
id: "moonshotai/kimi-k2",
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
value: {
|
||||
$ref: "#/$defs/Value",
|
||||
description: "Moonshot rejects this sibling after ref expansion.",
|
||||
},
|
||||
},
|
||||
$defs: {
|
||||
Value: {
|
||||
description: "Referenced schema description stays here.",
|
||||
type: "object",
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
) as any
|
||||
|
||||
expect(result.properties.value).toEqual({
|
||||
$ref: "#/$defs/Value",
|
||||
})
|
||||
})
|
||||
|
||||
test("converts tuple-style array items to a single item schema", () => {
|
||||
const result = ProviderTransform.schema(moonshotModel, {
|
||||
type: "object",
|
||||
properties: {
|
||||
codeSpec: {
|
||||
type: "object",
|
||||
properties: {
|
||||
accessibility: {
|
||||
type: "object",
|
||||
properties: {
|
||||
renderedSize: {
|
||||
description: "Rendered size [width, height] in px",
|
||||
type: "array",
|
||||
items: [{ type: "number" }, { type: "number" }],
|
||||
minItems: 2,
|
||||
maxItems: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any) as any
|
||||
|
||||
expect(result.properties.codeSpec.properties.accessibility.properties.renderedSize.items).toEqual({
|
||||
type: "number",
|
||||
})
|
||||
})
|
||||
|
||||
test("converts prefixItems tuples to a single item schema", () => {
|
||||
const result = ProviderTransform.schema(moonshotModel, {
|
||||
type: "object",
|
||||
properties: {
|
||||
renderedSize: {
|
||||
description: "Rendered size [width, height] in px",
|
||||
type: "array",
|
||||
prefixItems: [{ type: "number", title: "Width" }, { type: "number" }],
|
||||
unevaluatedItems: false,
|
||||
},
|
||||
},
|
||||
} as any) as any
|
||||
|
||||
expect(result.properties.renderedSize.prefixItems).toBeUndefined()
|
||||
expect(result.properties.renderedSize.unevaluatedItems).toBeUndefined()
|
||||
expect(result.properties.renderedSize.items).toEqual({
|
||||
type: "number",
|
||||
})
|
||||
})
|
||||
|
||||
test("removes unsupported annotation fields", () => {
|
||||
const result = ProviderTransform.schema(moonshotModel, {
|
||||
title: "Tool input",
|
||||
$comment: "Internal note",
|
||||
type: "object",
|
||||
properties: {
|
||||
count: {
|
||||
title: "Count",
|
||||
$comment: "Generated from int32",
|
||||
description: "How many items to include.",
|
||||
default: 10,
|
||||
format: "int32",
|
||||
type: "integer",
|
||||
},
|
||||
},
|
||||
} as any) as any
|
||||
|
||||
expect(result.title).toBeUndefined()
|
||||
expect(result.$comment).toBeUndefined()
|
||||
expect(result.properties.count.title).toBeUndefined()
|
||||
expect(result.properties.count.$comment).toBeUndefined()
|
||||
expect(result.properties.count.format).toBeUndefined()
|
||||
expect(result.properties.count.description).toBe("How many items to include.")
|
||||
expect(result.properties.count.default).toBe(10)
|
||||
})
|
||||
|
||||
test("removes unsupported complex validation fields", () => {
|
||||
const result = ProviderTransform.schema(moonshotModel, {
|
||||
type: "object",
|
||||
properties: {
|
||||
count: {
|
||||
type: "integer",
|
||||
minimum: 1,
|
||||
exclusiveMinimum: 0,
|
||||
exclusiveMaximum: 10,
|
||||
},
|
||||
values: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
contains: { type: "string" },
|
||||
minContains: 1,
|
||||
maxContains: 3,
|
||||
},
|
||||
},
|
||||
} as any) as any
|
||||
|
||||
expect(result.properties.count.exclusiveMinimum).toBeUndefined()
|
||||
expect(result.properties.count.exclusiveMaximum).toBeUndefined()
|
||||
expect(result.properties.count.minimum).toBe(1)
|
||||
expect(result.properties.values.minContains).toBeUndefined()
|
||||
expect(result.properties.values.maxContains).toBeUndefined()
|
||||
expect(result.properties.values.contains).toEqual({ type: "string" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("ProviderTransform.message - DeepSeek reasoning content", () => {
|
||||
test("DeepSeek with tool calls includes reasoning_content in providerOptions", () => {
|
||||
const msgs = [
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import type { UpgradeWebSocket } from "hono/ws"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { InstanceRoutes } from "../../src/server/routes/instance"
|
||||
import { WorkspaceRoutes } from "../../src/server/routes/control/workspace"
|
||||
import { ConfigApi } from "../../src/server/routes/instance/httpapi/config"
|
||||
import { EventPaths } from "../../src/server/routes/instance/httpapi/event"
|
||||
import { ExperimentalApi } from "../../src/server/routes/instance/httpapi/experimental"
|
||||
import { FileApi, FilePaths } from "../../src/server/routes/instance/httpapi/file"
|
||||
import { InstanceApi } from "../../src/server/routes/instance/httpapi/instance"
|
||||
import { McpApi } from "../../src/server/routes/instance/httpapi/mcp"
|
||||
import { PermissionApi } from "../../src/server/routes/instance/httpapi/permission"
|
||||
import { ProjectApi } from "../../src/server/routes/instance/httpapi/project"
|
||||
import { ProviderApi } from "../../src/server/routes/instance/httpapi/provider"
|
||||
import { PtyApi, PtyPaths } from "../../src/server/routes/instance/httpapi/pty"
|
||||
import { QuestionApi } from "../../src/server/routes/instance/httpapi/question"
|
||||
import { SessionApi } from "../../src/server/routes/instance/httpapi/session"
|
||||
import { SyncApi } from "../../src/server/routes/instance/httpapi/sync"
|
||||
import { TuiApi } from "../../src/server/routes/instance/httpapi/tui"
|
||||
import { WorkspaceApi } from "../../src/server/routes/instance/httpapi/workspace"
|
||||
import { PublicApi } from "../../src/server/routes/instance/httpapi/public"
|
||||
import { Server } from "../../src/server/server"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { OpenApi } from "effect/unstable/httpapi"
|
||||
import { HttpApi, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
@@ -17,13 +34,48 @@ const original = {
|
||||
OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME,
|
||||
}
|
||||
|
||||
const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket
|
||||
const methods = ["get", "post", "put", "delete", "patch"] as const
|
||||
|
||||
function app(input?: { password?: string; username?: string }) {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||
Flag.OPENCODE_SERVER_PASSWORD = input?.password
|
||||
Flag.OPENCODE_SERVER_USERNAME = input?.username
|
||||
return Server.Default().app
|
||||
return InstanceRoutes(websocket)
|
||||
}
|
||||
|
||||
function routeKey(route: ReturnType<typeof InstanceRoutes>["routes"][number]) {
|
||||
return `${route.method} ${route.path}`
|
||||
}
|
||||
|
||||
function reflectedHttpApiRoutes() {
|
||||
const routes = [`GET ${EventPaths.event}`, `GET ${PtyPaths.connect}`]
|
||||
|
||||
function addRoutes<Id extends string, Groups extends HttpApiGroup.Any>(api: HttpApi.HttpApi<Id, Groups>) {
|
||||
HttpApi.reflect(api, {
|
||||
onGroup() {},
|
||||
onEndpoint({ endpoint }) {
|
||||
routes.push(`${endpoint.method} ${endpoint.path}`)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
addRoutes(ConfigApi)
|
||||
addRoutes(ExperimentalApi)
|
||||
addRoutes(FileApi)
|
||||
addRoutes(InstanceApi)
|
||||
addRoutes(McpApi)
|
||||
addRoutes(PermissionApi)
|
||||
addRoutes(ProjectApi)
|
||||
addRoutes(ProviderApi)
|
||||
addRoutes(PtyApi)
|
||||
addRoutes(QuestionApi)
|
||||
addRoutes(SessionApi)
|
||||
addRoutes(SyncApi)
|
||||
addRoutes(TuiApi)
|
||||
addRoutes(WorkspaceApi)
|
||||
|
||||
return [...new Set(routes)]
|
||||
}
|
||||
|
||||
function openApiRouteKeys(spec: { paths: Record<string, Partial<Record<(typeof methods)[number], unknown>>> }) {
|
||||
@@ -34,32 +86,6 @@ function openApiRouteKeys(spec: { paths: Record<string, Partial<Record<(typeof m
|
||||
.sort()
|
||||
}
|
||||
|
||||
function openApiParameters(spec: { paths: Record<string, Partial<Record<(typeof methods)[number], Operation>>> }) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(spec.paths).flatMap(([path, item]) =>
|
||||
methods
|
||||
.filter((method) => item[method])
|
||||
.map((method) => [
|
||||
`${method.toUpperCase()} ${path}`,
|
||||
(item[method]?.parameters ?? [])
|
||||
.map(parameterKey)
|
||||
.filter((param) => param !== undefined)
|
||||
.sort(),
|
||||
]),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
type Operation = {
|
||||
parameters?: unknown[]
|
||||
}
|
||||
|
||||
function parameterKey(param: unknown) {
|
||||
if (!param || typeof param !== "object" || !("in" in param) || !("name" in param)) return
|
||||
if (typeof param.in !== "string" || typeof param.name !== "string") return
|
||||
return `${param.in}:${param.name}:${"required" in param && param.required === true}`
|
||||
}
|
||||
|
||||
function authorization(username: string, password: string) {
|
||||
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
|
||||
}
|
||||
@@ -80,7 +106,40 @@ afterEach(async () => {
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
describe("HttpApi server", () => {
|
||||
describe("HttpApi Hono bridge", () => {
|
||||
test("mounts experimental handlers for every legacy instance route", () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = false
|
||||
const legacy = InstanceRoutes(websocket)
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||
const experimental = InstanceRoutes(websocket)
|
||||
|
||||
const bridge = experimental.routes.slice(0, experimental.routes.length - legacy.routes.length)
|
||||
const workspaceRoutes = WorkspaceRoutes().routes.map((route) => ({
|
||||
...route,
|
||||
path: `/experimental/workspace${route.path === "/" ? "" : route.path}`,
|
||||
}))
|
||||
const legacyRoutes = [...new Set([...legacy.routes, ...workspaceRoutes].map(routeKey))]
|
||||
const bridgeRoutes = new Set(bridge.map(routeKey))
|
||||
|
||||
expect(legacyRoutes.filter((route) => !bridgeRoutes.has(route))).toEqual([])
|
||||
expect([...bridgeRoutes].filter((route) => !legacyRoutes.includes(route)).sort()).toEqual([])
|
||||
})
|
||||
|
||||
test("mounts every Effect HttpApi route through the Hono bridge", () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = false
|
||||
const legacy = InstanceRoutes(websocket)
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||
const experimental = InstanceRoutes(websocket)
|
||||
|
||||
const bridgeRoutes = new Set(
|
||||
experimental.routes.slice(0, experimental.routes.length - legacy.routes.length).map(routeKey),
|
||||
)
|
||||
const httpApiRoutes = reflectedHttpApiRoutes()
|
||||
|
||||
expect(httpApiRoutes.filter((route) => !bridgeRoutes.has(route))).toEqual([])
|
||||
expect([...bridgeRoutes].filter((route) => !httpApiRoutes.includes(route)).sort()).toEqual([])
|
||||
})
|
||||
|
||||
test("covers every generated OpenAPI route with Effect HttpApi contracts", async () => {
|
||||
const honoRoutes = openApiRouteKeys(await Server.openapi())
|
||||
const effectRoutes = openApiRouteKeys(OpenApi.fromApi(PublicApi))
|
||||
@@ -89,17 +148,6 @@ describe("HttpApi server", () => {
|
||||
expect(effectRoutes.filter((route) => !honoRoutes.includes(route))).toEqual([])
|
||||
})
|
||||
|
||||
test("matches generated OpenAPI route parameters", async () => {
|
||||
const hono = openApiParameters(await Server.openapi())
|
||||
const effect = openApiParameters(OpenApi.fromApi(PublicApi))
|
||||
|
||||
expect(
|
||||
Object.keys(hono)
|
||||
.filter((route) => JSON.stringify(hono[route]) !== JSON.stringify(effect[route]))
|
||||
.map((route) => ({ route, hono: hono[route], effect: effect[route] })),
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
test("allows requests when auth is disabled", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Bun.write(`${tmp.path}/hello.txt`, "hello")
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import type { UpgradeWebSocket } from "hono/ws"
|
||||
import path from "path"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { InstanceRoutes } from "../../src/server/routes/instance"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
@@ -11,10 +12,11 @@ import { tmpdir } from "../fixture/fixture"
|
||||
void Log.init({ print: false })
|
||||
|
||||
const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
|
||||
const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket
|
||||
|
||||
function app() {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||
return Server.Default().app
|
||||
return InstanceRoutes(websocket)
|
||||
}
|
||||
|
||||
async function waitDisposed(directory: string) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import type { UpgradeWebSocket } from "hono/ws"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { InstanceRoutes } from "../../src/server/routes/instance"
|
||||
import { EventPaths } from "../../src/server/routes/instance/httpapi/event"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
@@ -10,10 +11,11 @@ import { tmpdir } from "../fixture/fixture"
|
||||
void Log.init({ print: false })
|
||||
|
||||
const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
|
||||
const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket
|
||||
|
||||
function app() {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||
return Server.Default().app
|
||||
return InstanceRoutes(websocket)
|
||||
}
|
||||
|
||||
async function readFirstChunk(response: Response) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import type { UpgradeWebSocket } from "hono/ws"
|
||||
import { Effect } from "effect"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { InstanceRoutes } from "../../src/server/routes/instance"
|
||||
import { ExperimentalPaths } from "../../src/server/routes/instance/httpapi/experimental"
|
||||
import { Session } from "@/session/session"
|
||||
import { Database } from "@/storage/db"
|
||||
@@ -15,11 +16,12 @@ import { tmpdir } from "../fixture/fixture"
|
||||
void Log.init({ print: false })
|
||||
|
||||
const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
|
||||
const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket
|
||||
const testWorktreeMutations = process.platform === "win32" ? test.skip : test
|
||||
|
||||
function app() {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||
return Server.Default().app
|
||||
return InstanceRoutes(websocket)
|
||||
}
|
||||
|
||||
function runSession<A, E>(fx: Effect.Effect<A, E, Session.Service>) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import type { UpgradeWebSocket } from "hono/ws"
|
||||
import path from "path"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { InstanceRoutes } from "../../src/server/routes/instance"
|
||||
import { InstancePaths } from "../../src/server/routes/instance/httpapi/instance"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
@@ -12,10 +13,11 @@ import { tmpdir } from "../fixture/fixture"
|
||||
void Log.init({ print: false })
|
||||
|
||||
const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
|
||||
const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket
|
||||
|
||||
function app() {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||
return Server.Default().app
|
||||
return InstanceRoutes(websocket)
|
||||
}
|
||||
|
||||
async function waitDisposed(directory: string) {
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { ExperimentalPaths } from "../../src/server/routes/instance/httpapi/experimental"
|
||||
import { SessionPaths } from "../../src/server/routes/instance/httpapi/session"
|
||||
import { MessageID, PartID } from "../../src/session/schema"
|
||||
import { Session } from "@/session/session"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
|
||||
|
||||
function app(experimental: boolean) {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental
|
||||
return Server.Default().app
|
||||
}
|
||||
type TestApp = ReturnType<typeof app>
|
||||
|
||||
function pathFor(path: string, params: Record<string, string>) {
|
||||
return Object.entries(params).reduce((result, [key, value]) => result.replace(`:${key}`, value), path)
|
||||
}
|
||||
|
||||
const seedSessions = Effect.gen(function* () {
|
||||
const svc = yield* Session.Service
|
||||
const parent = yield* svc.create({ title: "parent" })
|
||||
yield* svc.create({ title: "child", parentID: parent.id })
|
||||
const message = yield* svc.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID: parent.id,
|
||||
agent: "build",
|
||||
model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") },
|
||||
time: { created: Date.now() },
|
||||
})
|
||||
yield* svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
sessionID: parent.id,
|
||||
messageID: message.id,
|
||||
type: "text",
|
||||
text: "hello",
|
||||
})
|
||||
return { parent, message }
|
||||
})
|
||||
|
||||
function withTmp<A, E, R>(
|
||||
options: Parameters<typeof tmpdir>[0],
|
||||
fn: (tmp: Awaited<ReturnType<typeof tmpdir>>) => Effect.Effect<A, E, R>,
|
||||
) {
|
||||
return Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir(options)),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
).pipe(Effect.flatMap((tmp) => fn(tmp).pipe(provideInstance(tmp.path))))
|
||||
}
|
||||
|
||||
function readJson(label: string, serverApp: TestApp, path: string, headers: HeadersInit) {
|
||||
return Effect.promise(async () => {
|
||||
const response = await serverApp.request(path, { headers })
|
||||
if (response.status !== 200) throw new Error(`${label} returned ${response.status}: ${await response.text()}`)
|
||||
return await response.json()
|
||||
})
|
||||
}
|
||||
|
||||
function expectJsonParity(input: {
|
||||
label: string
|
||||
legacy: TestApp
|
||||
httpapi: TestApp
|
||||
path: string
|
||||
headers: HeadersInit
|
||||
}) {
|
||||
return Effect.gen(function* () {
|
||||
const legacy = yield* readJson(input.label, input.legacy, input.path, input.headers)
|
||||
const httpapi = yield* readJson(input.label, input.httpapi, input.path, input.headers)
|
||||
expect({ label: input.label, body: httpapi }).toEqual({ label: input.label, body: legacy })
|
||||
return httpapi
|
||||
})
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
describe("HttpApi JSON parity", () => {
|
||||
it.live(
|
||||
"matches legacy JSON shape for session read endpoints",
|
||||
withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) =>
|
||||
Effect.gen(function* () {
|
||||
const headers = { "x-opencode-directory": tmp.path }
|
||||
const seeded = yield* seedSessions.pipe(Effect.provide(Session.defaultLayer))
|
||||
const legacy = app(false)
|
||||
const httpapi = app(true)
|
||||
|
||||
const rootsFalse = yield* expectJsonParity({
|
||||
label: "session.list roots false",
|
||||
legacy,
|
||||
httpapi,
|
||||
path: `${SessionPaths.list}?roots=false`,
|
||||
headers,
|
||||
})
|
||||
expect((rootsFalse as Session.Info[]).map((session) => session.id)).toContain(seeded.parent.id)
|
||||
expect((rootsFalse as Session.Info[]).length).toBe(2)
|
||||
|
||||
const experimentalRootsFalse = yield* expectJsonParity({
|
||||
label: "experimental.session roots false",
|
||||
legacy,
|
||||
httpapi,
|
||||
path: `${ExperimentalPaths.session}?${new URLSearchParams({ directory: tmp.path, limit: "10", roots: "false" })}`,
|
||||
headers,
|
||||
})
|
||||
expect((experimentalRootsFalse as Session.GlobalInfo[]).length).toBe(2)
|
||||
|
||||
const experimentalArchivedFalse = yield* expectJsonParity({
|
||||
label: "experimental.session archived false",
|
||||
legacy,
|
||||
httpapi,
|
||||
path: `${ExperimentalPaths.session}?${new URLSearchParams({ directory: tmp.path, limit: "10", archived: "false" })}`,
|
||||
headers,
|
||||
})
|
||||
expect((experimentalArchivedFalse as Session.GlobalInfo[]).length).toBe(2)
|
||||
|
||||
yield* Effect.forEach(
|
||||
[
|
||||
{ label: "session.list roots", path: `${SessionPaths.list}?roots=true`, headers },
|
||||
{ label: "session.list all", path: SessionPaths.list, headers },
|
||||
{ label: "session.get", path: pathFor(SessionPaths.get, { sessionID: seeded.parent.id }), headers },
|
||||
{
|
||||
label: "session.children",
|
||||
path: pathFor(SessionPaths.children, { sessionID: seeded.parent.id }),
|
||||
headers,
|
||||
},
|
||||
{
|
||||
label: "session.messages",
|
||||
path: pathFor(SessionPaths.messages, { sessionID: seeded.parent.id }),
|
||||
headers,
|
||||
},
|
||||
{
|
||||
label: "session.messages empty before",
|
||||
path: `${pathFor(SessionPaths.messages, { sessionID: seeded.parent.id })}?before=`,
|
||||
headers,
|
||||
},
|
||||
{
|
||||
label: "session.message",
|
||||
path: pathFor(SessionPaths.message, { sessionID: seeded.parent.id, messageID: seeded.message.id }),
|
||||
headers,
|
||||
},
|
||||
{
|
||||
label: "experimental.session",
|
||||
path: `${ExperimentalPaths.session}?${new URLSearchParams({ directory: tmp.path, limit: "10" })}`,
|
||||
headers,
|
||||
},
|
||||
],
|
||||
(input) => expectJsonParity({ ...input, legacy, httpapi }),
|
||||
{ concurrency: 1 },
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
@@ -1,27 +1,15 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { Context, Effect, FileSystem, Layer, Path } from "effect"
|
||||
import { NodeFileSystem, NodePath } from "@effect/platform-node"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Context } from "effect"
|
||||
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
|
||||
import { McpPaths } from "../../src/server/routes/instance/httpapi/mcp"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
|
||||
const context = Context.empty() as Context.Context<unknown>
|
||||
const it = testEffect(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer))
|
||||
|
||||
function app(experimental: boolean) {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental
|
||||
return Server.Default().app
|
||||
}
|
||||
type TestApp = ReturnType<typeof app>
|
||||
|
||||
function request(route: string, directory: string, init?: RequestInit) {
|
||||
const headers = new Headers(init?.headers)
|
||||
@@ -35,47 +23,7 @@ function request(route: string, directory: string, init?: RequestInit) {
|
||||
)
|
||||
}
|
||||
|
||||
function withMcpProject<A, E, R>(self: (dir: string) => Effect.Effect<A, E, R>) {
|
||||
return Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const path = yield* Path.Path
|
||||
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "opencode-test-" })
|
||||
|
||||
yield* fs.writeFileString(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
formatter: false,
|
||||
lsp: false,
|
||||
mcp: {
|
||||
demo: {
|
||||
type: "local",
|
||||
command: ["echo", "demo"],
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.promise(() => Instance.provide({ directory: dir, fn: () => Instance.dispose() })).pipe(Effect.ignore),
|
||||
)
|
||||
|
||||
return yield* self(dir).pipe(provideInstance(dir))
|
||||
})
|
||||
}
|
||||
|
||||
const readResponse = Effect.fnUntraced(function* (input: { app: TestApp; path: string; headers: HeadersInit }) {
|
||||
const response = yield* Effect.promise(() =>
|
||||
Promise.resolve(input.app.request(input.path, { method: "POST", headers: input.headers })),
|
||||
)
|
||||
return {
|
||||
status: response.status,
|
||||
body: yield* Effect.promise(() => response.text()),
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
})
|
||||
@@ -159,28 +107,4 @@ describe("mcp HttpApi", () => {
|
||||
expect(removed.status).toBe(200)
|
||||
expect(await removed.json()).toEqual({ success: true })
|
||||
})
|
||||
|
||||
it.live(
|
||||
"matches legacy unsupported OAuth error responses",
|
||||
withMcpProject((dir) =>
|
||||
Effect.gen(function* () {
|
||||
const headers = { "x-opencode-directory": dir }
|
||||
const legacy = app(false)
|
||||
const httpapi = app(true)
|
||||
|
||||
yield* Effect.forEach(["/mcp/demo/auth", "/mcp/demo/auth/authenticate"], (path) =>
|
||||
Effect.gen(function* () {
|
||||
const legacyResponse = yield* readResponse({ app: legacy, path, headers })
|
||||
const httpapiResponse = yield* readResponse({ app: httpapi, path, headers })
|
||||
|
||||
expect(legacyResponse).toEqual({
|
||||
status: 400,
|
||||
body: JSON.stringify({ error: "MCP server demo does not support OAuth" }),
|
||||
})
|
||||
expect(httpapiResponse).toEqual(legacyResponse)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
import { Effect, FileSystem, Layer, Path } from "effect"
|
||||
import { NodeFileSystem, NodePath } from "@effect/platform-node"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { provideInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
|
||||
const it = testEffect(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer))
|
||||
const providerID = "test-oauth-parity"
|
||||
const oauthURL = "https://example.com/oauth"
|
||||
const oauthInstructions = "Finish OAuth"
|
||||
|
||||
function app(experimental: boolean) {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental
|
||||
return Server.Default().app
|
||||
}
|
||||
|
||||
function requestAuthorize(input: {
|
||||
app: ReturnType<typeof app>
|
||||
providerID: string
|
||||
method: number
|
||||
headers: HeadersInit
|
||||
}) {
|
||||
return Effect.promise(async () => {
|
||||
const response = await input.app.request(`/provider/${input.providerID}/oauth/authorize`, {
|
||||
method: "POST",
|
||||
headers: input.headers,
|
||||
body: JSON.stringify({ method: input.method }),
|
||||
})
|
||||
return {
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function writeProviderAuthPlugin(dir: string) {
|
||||
return Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const path = yield* Path.Path
|
||||
|
||||
yield* fs.makeDirectory(path.join(dir, ".opencode", "plugin"), { recursive: true })
|
||||
yield* fs.writeFileString(
|
||||
path.join(dir, ".opencode", "plugin", "provider-oauth-parity.ts"),
|
||||
[
|
||||
"export default {",
|
||||
' id: "test.provider-oauth-parity",',
|
||||
" server: async () => ({",
|
||||
" auth: {",
|
||||
` provider: "${providerID}",`,
|
||||
" methods: [",
|
||||
' { type: "api", label: "API key" },',
|
||||
" {",
|
||||
' type: "oauth",',
|
||||
' label: "OAuth",',
|
||||
" authorize: async () => ({",
|
||||
` url: "${oauthURL}",`,
|
||||
' method: "code",',
|
||||
` instructions: "${oauthInstructions}",`,
|
||||
" callback: async () => ({ type: 'success', key: 'token' }),",
|
||||
" }),",
|
||||
" },",
|
||||
" ],",
|
||||
" },",
|
||||
" }),",
|
||||
"}",
|
||||
"",
|
||||
].join("\n"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function withProviderProject<A, E, R>(self: (dir: string) => Effect.Effect<A, E, R>) {
|
||||
return Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const path = yield* Path.Path
|
||||
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "opencode-test-" })
|
||||
|
||||
yield* fs.writeFileString(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({ $schema: "https://opencode.ai/config.json", formatter: false, lsp: false }),
|
||||
)
|
||||
yield* writeProviderAuthPlugin(dir)
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.promise(() => Instance.provide({ directory: dir, fn: () => Instance.dispose() })).pipe(Effect.ignore),
|
||||
)
|
||||
|
||||
return yield* self(dir).pipe(provideInstance(dir))
|
||||
})
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
describe("provider HttpApi", () => {
|
||||
it.live(
|
||||
"matches legacy OAuth authorize response shapes",
|
||||
withProviderProject((dir) =>
|
||||
Effect.gen(function* () {
|
||||
const headers = { "x-opencode-directory": dir, "content-type": "application/json" }
|
||||
const legacy = app(false)
|
||||
const httpapi = app(true)
|
||||
|
||||
const apiLegacy = yield* requestAuthorize({
|
||||
app: legacy,
|
||||
providerID,
|
||||
method: 0,
|
||||
headers,
|
||||
})
|
||||
const apiHttpApi = yield* requestAuthorize({
|
||||
app: httpapi,
|
||||
providerID,
|
||||
method: 0,
|
||||
headers,
|
||||
})
|
||||
expect(apiLegacy).toEqual({ status: 200, body: "" })
|
||||
expect(apiHttpApi).toEqual(apiLegacy)
|
||||
|
||||
const oauthLegacy = yield* requestAuthorize({
|
||||
app: legacy,
|
||||
providerID,
|
||||
method: 1,
|
||||
headers,
|
||||
})
|
||||
const oauthHttpApi = yield* requestAuthorize({
|
||||
app: httpapi,
|
||||
providerID,
|
||||
method: 1,
|
||||
headers,
|
||||
})
|
||||
expect(oauthHttpApi).toEqual(oauthLegacy)
|
||||
expect(JSON.parse(oauthHttpApi.body)).toEqual({
|
||||
url: oauthURL,
|
||||
method: "code",
|
||||
instructions: oauthInstructions,
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
@@ -1,8 +1,9 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import type { UpgradeWebSocket } from "hono/ws"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { PtyID } from "../../src/pty/schema"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { InstanceRoutes } from "../../src/server/routes/instance"
|
||||
import { PtyPaths } from "../../src/server/routes/instance/httpapi/pty"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
@@ -11,11 +12,12 @@ import { tmpdir } from "../fixture/fixture"
|
||||
void Log.init({ print: false })
|
||||
|
||||
const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
|
||||
const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket
|
||||
const testPty = process.platform === "win32" ? test.skip : test
|
||||
|
||||
function app() {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||
return Server.Default().app
|
||||
return InstanceRoutes(websocket)
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
import type { UpgradeWebSocket } from "hono/ws"
|
||||
import { Effect } from "effect"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { PermissionID } from "../../src/permission/schema"
|
||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { InstanceRoutes } from "../../src/server/routes/instance"
|
||||
import { SessionPaths } from "../../src/server/routes/instance/httpapi/session"
|
||||
import { Session } from "@/session/session"
|
||||
import { MessageID, PartID, type SessionID } from "../../src/session/schema"
|
||||
@@ -17,10 +18,11 @@ import { it } from "../lib/effect"
|
||||
void Log.init({ print: false })
|
||||
|
||||
const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
|
||||
const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket
|
||||
|
||||
function app() {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||
return Server.Default().app
|
||||
return InstanceRoutes(websocket)
|
||||
}
|
||||
|
||||
function runSession<A, E>(fx: Effect.Effect<A, E, Session.Service>) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import type { UpgradeWebSocket } from "hono/ws"
|
||||
import { Effect } from "effect"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { InstanceRoutes } from "../../src/server/routes/instance"
|
||||
import { SyncPaths } from "../../src/server/routes/instance/httpapi/sync"
|
||||
import { Session } from "@/session/session"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
@@ -13,10 +14,11 @@ void Log.init({ print: false })
|
||||
|
||||
const originalHttpApi = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
|
||||
const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
|
||||
const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket
|
||||
|
||||
function app(httpapi = true) {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = httpapi
|
||||
return Server.Default().app
|
||||
function app() {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||
return InstanceRoutes(websocket)
|
||||
}
|
||||
|
||||
function runSession<A, E>(fx: Effect.Effect<A, E, Session.Service>) {
|
||||
@@ -79,48 +81,4 @@ describe("sync HttpApi", () => {
|
||||
expect(replayed.status).toBe(200)
|
||||
expect(await replayed.json()).toEqual({ sessionID: session.id })
|
||||
})
|
||||
|
||||
test("matches legacy seq validation", async () => {
|
||||
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
|
||||
const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" }
|
||||
const cases = [
|
||||
{
|
||||
path: SyncPaths.history,
|
||||
body: { aggregate: -1 },
|
||||
},
|
||||
{
|
||||
path: SyncPaths.history,
|
||||
body: { aggregate: 1.5 },
|
||||
},
|
||||
{
|
||||
path: SyncPaths.replay,
|
||||
body: {
|
||||
directory: tmp.path,
|
||||
events: [{ id: "event", aggregateID: "session", seq: -1, type: "session.created", data: {} }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: SyncPaths.replay,
|
||||
body: {
|
||||
directory: tmp.path,
|
||||
events: [{ id: "event", aggregateID: "session", seq: 1.5, type: "session.created", data: {} }],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
for (const item of cases) {
|
||||
const legacy = await app(false).request(item.path, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(item.body),
|
||||
})
|
||||
const httpapi = await app(true).request(item.path, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(item.body),
|
||||
})
|
||||
expect(httpapi.status).toBe(legacy.status)
|
||||
expect(httpapi.status).toBe(400)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import type { Context } from "hono"
|
||||
import type { UpgradeWebSocket } from "hono/ws"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { SessionID } from "../../src/session/schema"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { TuiApi, TuiPaths } from "../../src/server/routes/instance/httpapi/tui"
|
||||
import { InstanceRoutes } from "../../src/server/routes/instance"
|
||||
import { TuiPaths } from "../../src/server/routes/instance/httpapi/tui"
|
||||
import { callTui } from "../../src/server/routes/instance/tui"
|
||||
import { Server } from "../../src/server/server"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { OpenApi } from "effect/unstable/httpapi"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
|
||||
const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket
|
||||
|
||||
function app() {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||
return Server.Default().app
|
||||
return InstanceRoutes(websocket)
|
||||
}
|
||||
|
||||
async function expectTrue(path: string, headers: Record<string, string>, body?: unknown) {
|
||||
@@ -37,15 +38,6 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe("tui HttpApi bridge", () => {
|
||||
test("documents legacy bad request responses", async () => {
|
||||
const legacy = await Server.openapi()
|
||||
const effect = OpenApi.fromApi(TuiApi)
|
||||
for (const path of [TuiPaths.appendPrompt, TuiPaths.executeCommand, TuiPaths.publish, TuiPaths.selectSession]) {
|
||||
expect(legacy.paths[path].post?.responses?.[400]).toBeDefined()
|
||||
expect(effect.paths[path].post?.responses?.[400]).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
test("serves TUI command and event routes through experimental Effect routes", async () => {
|
||||
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
|
||||
const headers = { "x-opencode-directory": tmp.path }
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { mkdir } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { Effect } from "effect"
|
||||
import type { UpgradeWebSocket } from "hono/ws"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { registerAdaptor } from "../../src/control-plane/adaptors"
|
||||
import type { WorkspaceAdaptor } from "../../src/control-plane/types"
|
||||
@@ -9,7 +10,7 @@ import { Workspace } from "../../src/control-plane/workspace"
|
||||
import { WorkspacePaths } from "../../src/server/routes/instance/httpapi/workspace"
|
||||
import { Session } from "@/session/session"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { InstanceRoutes } from "../../src/server/routes/instance"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
@@ -18,12 +19,13 @@ void Log.init({ print: false })
|
||||
|
||||
const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
|
||||
const originalHttpApi = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
|
||||
const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket
|
||||
|
||||
function request(path: string, directory: string, init: RequestInit = {}) {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||
const headers = new Headers(init.headers)
|
||||
headers.set("x-opencode-directory", directory)
|
||||
return Server.Default().app.request(path, { ...init, headers })
|
||||
return InstanceRoutes(websocket).request(path, { ...init, headers })
|
||||
}
|
||||
|
||||
function runSession<A, E>(fx: Effect.Effect<A, E, Session.Service>) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import { MessageID, SessionID } from "../../src/session/schema"
|
||||
import { SessionID } from "../../src/session/schema"
|
||||
import { Session } from "../../src/session/session"
|
||||
|
||||
const info = {
|
||||
@@ -50,27 +50,4 @@ describe("Session schema", () => {
|
||||
expect(Object.hasOwn(encoded, "parentID")).toBe(false)
|
||||
expect(Object.hasOwn(encoded.project as Record<string, unknown>, "name")).toBe(false)
|
||||
})
|
||||
|
||||
test("encodes nested undefined optional session fields as omitted keys", () => {
|
||||
const encoded = Schema.encodeUnknownSync(Session.Info)({
|
||||
...info,
|
||||
summary: {
|
||||
additions: 1,
|
||||
deletions: 2,
|
||||
files: 3,
|
||||
diffs: undefined,
|
||||
},
|
||||
revert: {
|
||||
messageID: MessageID.ascending(),
|
||||
partID: undefined,
|
||||
snapshot: undefined,
|
||||
diff: undefined,
|
||||
},
|
||||
}) as Record<string, unknown>
|
||||
|
||||
expect(Object.hasOwn(encoded.summary as Record<string, unknown>, "diffs")).toBe(false)
|
||||
for (const key of ["partID", "snapshot", "diff"]) {
|
||||
expect(Object.hasOwn(encoded.revert as Record<string, unknown>, key)).toBe(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,14 +9,7 @@ import path from "path"
|
||||
|
||||
import { createClient } from "@hey-api/openapi-ts"
|
||||
|
||||
const openapiSource = process.env.OPENCODE_SDK_OPENAPI === "httpapi" ? "httpapi" : "hono"
|
||||
const opencode = path.resolve(dir, "../../opencode")
|
||||
|
||||
if (openapiSource === "httpapi") {
|
||||
await $`bun dev generate --httpapi > ${dir}/openapi.json`.cwd(opencode)
|
||||
} else {
|
||||
await $`bun dev generate > ${dir}/openapi.json`.cwd(opencode)
|
||||
}
|
||||
await $`bun dev generate > ${dir}/openapi.json`.cwd(path.resolve(dir, "../../opencode"))
|
||||
|
||||
await createClient({
|
||||
input: "./openapi.json",
|
||||
|
||||
@@ -848,12 +848,12 @@ export class Session extends HeyApiClient {
|
||||
parameters?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
roots?: boolean | "true" | "false"
|
||||
roots?: boolean
|
||||
start?: number
|
||||
cursor?: number
|
||||
search?: string
|
||||
limit?: number
|
||||
archived?: boolean | "true" | "false"
|
||||
archived?: boolean
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
@@ -1647,7 +1647,7 @@ export class Session2 extends HeyApiClient {
|
||||
parameters?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
roots?: boolean | "true" | "false"
|
||||
roots?: boolean
|
||||
start?: number
|
||||
search?: string
|
||||
limit?: number
|
||||
|
||||
@@ -2129,10 +2129,6 @@ export type McpStatus =
|
||||
| McpStatusNeedsAuth
|
||||
| McpStatusNeedsClientRegistration
|
||||
|
||||
export type McpUnsupportedOAuthError = {
|
||||
error: string
|
||||
}
|
||||
|
||||
export type Path = {
|
||||
home: string
|
||||
state: string
|
||||
@@ -3221,7 +3217,7 @@ export type ExperimentalSessionListData = {
|
||||
/**
|
||||
* Only return root sessions (no parentID)
|
||||
*/
|
||||
roots?: boolean | "true" | "false"
|
||||
roots?: boolean
|
||||
/**
|
||||
* Filter sessions updated on or after this timestamp (milliseconds since epoch)
|
||||
*/
|
||||
@@ -3241,7 +3237,7 @@ export type ExperimentalSessionListData = {
|
||||
/**
|
||||
* Include archived sessions (default false)
|
||||
*/
|
||||
archived?: boolean | "true" | "false"
|
||||
archived?: boolean
|
||||
}
|
||||
url: "/experimental/session"
|
||||
}
|
||||
@@ -3289,7 +3285,7 @@ export type SessionListData = {
|
||||
/**
|
||||
* Only return root sessions (no parentID)
|
||||
*/
|
||||
roots?: boolean | "true" | "false"
|
||||
roots?: boolean
|
||||
/**
|
||||
* Filter sessions updated on or after this timestamp (milliseconds since epoch)
|
||||
*/
|
||||
@@ -4911,9 +4907,9 @@ export type McpAuthStartData = {
|
||||
|
||||
export type McpAuthStartErrors = {
|
||||
/**
|
||||
* MCP server does not support OAuth
|
||||
* Bad request
|
||||
*/
|
||||
400: McpUnsupportedOAuthError
|
||||
400: BadRequestError
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
@@ -4989,9 +4985,9 @@ export type McpAuthAuthenticateData = {
|
||||
|
||||
export type McpAuthAuthenticateErrors = {
|
||||
/**
|
||||
* MCP server does not support OAuth
|
||||
* Bad request
|
||||
*/
|
||||
400: McpUnsupportedOAuthError
|
||||
400: BadRequestError
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
|
||||
@@ -2168,15 +2168,7 @@
|
||||
"in": "query",
|
||||
"name": "roots",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["true", "false"]
|
||||
}
|
||||
]
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": "Only return root sessions (no parentID)"
|
||||
},
|
||||
@@ -2216,15 +2208,7 @@
|
||||
"in": "query",
|
||||
"name": "archived",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["true", "false"]
|
||||
}
|
||||
]
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": "Include archived sessions (default false)"
|
||||
}
|
||||
@@ -2324,15 +2308,7 @@
|
||||
"in": "query",
|
||||
"name": "roots",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["true", "false"]
|
||||
}
|
||||
]
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": "Only return root sessions (no parentID)"
|
||||
},
|
||||
@@ -6113,11 +6089,11 @@
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "MCP server does not support OAuth",
|
||||
"description": "Bad request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/McpUnsupportedOAuthError"
|
||||
"$ref": "#/components/schemas/BadRequestError"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6331,11 +6307,11 @@
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "MCP server does not support OAuth",
|
||||
"description": "Bad request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/McpUnsupportedOAuthError"
|
||||
"$ref": "#/components/schemas/BadRequestError"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13276,15 +13252,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"McpUnsupportedOAuthError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["error"]
|
||||
},
|
||||
"Path": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&[data-hide-details="true"] {
|
||||
[data-slot="basic-tool-tool-trigger-content"] {
|
||||
flex: 1 1 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
[data-slot="basic-tool-tool-info"] {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="basic-tool-tool-trigger-content"] {
|
||||
flex: 0 1 auto;
|
||||
width: auto;
|
||||
|
||||
@@ -1207,6 +1207,7 @@
|
||||
|
||||
[data-slot="apply-patch-filename"] {
|
||||
color: var(--text-strong);
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
Reference in New Issue
Block a user