mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-03 08:46:15 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 873cb69319 |
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -44,7 +44,6 @@
|
||||
"@solid-primitives/i18n": "2.2.1",
|
||||
"@solid-primitives/media": "2.3.3",
|
||||
"@solid-primitives/resize-observer": "2.1.5",
|
||||
"@solid-primitives/scheduled": "1.5.3",
|
||||
"@solid-primitives/scroll": "2.1.3",
|
||||
"@solid-primitives/storage": "catalog:",
|
||||
"@solid-primitives/timer": "1.4.4",
|
||||
@@ -85,7 +84,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -120,7 +119,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -147,7 +146,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.64",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -169,7 +168,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -193,21 +192,21 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.107",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.96",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
"@ai-sdk/azure": "3.0.49",
|
||||
"@ai-sdk/cerebras": "2.0.41",
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/deepinfra": "2.0.41",
|
||||
"@ai-sdk/gateway": "3.0.104",
|
||||
"@ai-sdk/google": "3.0.75",
|
||||
"@ai-sdk/google-vertex": "4.0.131",
|
||||
"@ai-sdk/google": "3.0.63",
|
||||
"@ai-sdk/google-vertex": "4.0.112",
|
||||
"@ai-sdk/groq": "3.0.31",
|
||||
"@ai-sdk/mistral": "3.0.27",
|
||||
"@ai-sdk/openai": "3.0.53",
|
||||
@@ -239,7 +238,7 @@
|
||||
"minimatch": "10.2.5",
|
||||
"npm-package-arg": "13.0.2",
|
||||
"semver": "^7.6.3",
|
||||
"venice-ai-sdk-provider": "2.0.2",
|
||||
"venice-ai-sdk-provider": "2.0.1",
|
||||
"xdg-basedir": "5.1.0",
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -254,7 +253,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"drizzle-orm": "catalog:",
|
||||
@@ -309,7 +308,7 @@
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -323,7 +322,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -353,7 +352,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -369,7 +368,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -382,7 +381,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"dependencies": {
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
"@smithy/util-utf8": "4.2.2",
|
||||
@@ -400,7 +399,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -409,15 +408,15 @@
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.21.0",
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.107",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.96",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
"@ai-sdk/azure": "3.0.49",
|
||||
"@ai-sdk/cerebras": "2.0.41",
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/deepinfra": "2.0.41",
|
||||
"@ai-sdk/gateway": "3.0.104",
|
||||
"@ai-sdk/google": "3.0.75",
|
||||
"@ai-sdk/google-vertex": "4.0.131",
|
||||
"@ai-sdk/google": "3.0.63",
|
||||
"@ai-sdk/google-vertex": "4.0.112",
|
||||
"@ai-sdk/groq": "3.0.31",
|
||||
"@ai-sdk/mistral": "3.0.27",
|
||||
"@ai-sdk/openai": "3.0.53",
|
||||
@@ -494,7 +493,7 @@
|
||||
"tree-sitter-powershell": "0.25.10",
|
||||
"turndown": "7.2.0",
|
||||
"ulid": "catalog:",
|
||||
"venice-ai-sdk-provider": "2.0.2",
|
||||
"venice-ai-sdk-provider": "2.0.1",
|
||||
"vscode-jsonrpc": "8.2.1",
|
||||
"web-tree-sitter": "0.25.10",
|
||||
"which": "6.0.1",
|
||||
@@ -538,7 +537,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -576,7 +575,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -591,7 +590,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -626,7 +625,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -675,7 +674,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -810,7 +809,7 @@
|
||||
|
||||
"@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.107", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.78", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8nT08pGPy25rleJNk56ep00UHK6kCtCmu+ZNqVVSSPDieADlIZqcaN1iRXAFBoCH0Fb9F6C2EjFDaySdsargfQ=="],
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.96", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Mc4Ias2jRMD1jOB6xWtKNPdhECeuCZyIlbr9EAGfBnyBt++sS13ziZh9qv9TdyMCAZJ7xoQcpbchoRJcKwPdpA=="],
|
||||
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="],
|
||||
|
||||
@@ -832,9 +831,9 @@
|
||||
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="],
|
||||
|
||||
"@ai-sdk/google": ["@ai-sdk/google@3.0.75", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XAm31ftiOrzlb8NjDzT7kw0xw+4lmgFdGFn1QKM73nXFFKyN1kWLESBV75UGNfjXP8X1YJ0YydnMVqO0jaPghw=="],
|
||||
"@ai-sdk/google": ["@ai-sdk/google@3.0.63", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RfOZWVMYSPu2sPRfGajrauWAZ9BSaRopSn+AszkKWQ1MFj8nhaXvCqRHB5pBQUaHTfZKagvOmMpNfa/s3gPLgQ=="],
|
||||
|
||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.131", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.78", "@ai-sdk/google": "3.0.75", "@ai-sdk/openai-compatible": "2.0.47", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Oj1X8p0rVgvEoR5OOSxWi6XgzJ3QDlE/n30MZVtpKkCiToYYDyvlvVDGXz3IqhMyUev2JhlcuUk1brScKT01kA=="],
|
||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.112", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/google": "3.0.64", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cSfHCkM+9ZrFtQWIN1WlV93JPD+isGSdFxKj7u1L9m2aLVZajlXdcE41GL9hMt7ld7bZYE4NnZ+4VLxBAHE+Eg=="],
|
||||
|
||||
"@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="],
|
||||
|
||||
@@ -2178,7 +2177,7 @@
|
||||
|
||||
"@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="],
|
||||
|
||||
"@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.3", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-oNwLE6E6lxJAWrc8QXuwM0k2oU1BnANnkChwMw82aK1j3+mWGJkG1IFe5gCwbV+afYmjI76t9JJV3md/8tLw+g=="],
|
||||
"@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA=="],
|
||||
|
||||
"@solid-primitives/scroll": ["@solid-primitives/scroll@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Ejq/Z7zKo/6eIEFr1bFLzXFxiGBCMLuqCM8QB8urr3YdPzjSETFLzYRWUyRiDWaBQN0F7k0SY6S7ig5nWOP7vg=="],
|
||||
|
||||
@@ -4912,7 +4911,7 @@
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.0.2", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.47", "@ai-sdk/provider": "^3.0.10", "@ai-sdk/provider-utils": "^4.0.27" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-aoa05nI3BTK5aGbjBflq+Gfln2AHAkwNbWuGGvCzUIsOfp5Y3iPD4O4PUGDAEiWVJWbjpPn0KfDa0H/HebwsaA=="],
|
||||
"venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.0.1", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.37", "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.21" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-6SxA8a4MoA6Q/c+D3q7My0Hfog76enN3n0MXhwosM+tso66rXBEGeBRD/0lravRDVzL2Q1w5QJPc86rAVJtfXg=="],
|
||||
|
||||
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
|
||||
|
||||
@@ -5090,13 +5089,9 @@
|
||||
|
||||
"@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="],
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="],
|
||||
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.13", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
@@ -5112,17 +5107,11 @@
|
||||
|
||||
"@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||
|
||||
"@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="],
|
||||
|
||||
"@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||
"@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CbR82EgGPNrj/6q0HtclwuCqe0/pDShyv3nWDP/A9DroujzWXnLMlUJVrgPOsg4b40zQCwwVs2XSKCxvt/4QaA=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Enm5UlL0zUCrW3792opk5h7hRWxZOZzDe6eQYVFqX9LUOGGCe1h8MZWAGim765nwzgnjlpeYOsuzZmLtRsTPlg=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||
"@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||
|
||||
"@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
@@ -5848,8 +5837,6 @@
|
||||
|
||||
"opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||
|
||||
"opencode/@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA=="],
|
||||
|
||||
"opencode/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
|
||||
|
||||
"opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||
@@ -5966,11 +5953,7 @@
|
||||
|
||||
"unused-filename/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="],
|
||||
|
||||
"venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Enm5UlL0zUCrW3792opk5h7hRWxZOZzDe6eQYVFqX9LUOGGCe1h8MZWAGim765nwzgnjlpeYOsuzZmLtRsTPlg=="],
|
||||
|
||||
"venice-ai-sdk-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||
|
||||
"venice-ai-sdk-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||
"venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||
|
||||
"vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
||||
|
||||
@@ -6016,12 +5999,6 @@
|
||||
|
||||
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec/@smithy/types": ["@smithy/types@4.14.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg=="],
|
||||
|
||||
"@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
@@ -6032,14 +6009,6 @@
|
||||
|
||||
"@ai-sdk/deepinfra/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
|
||||
|
||||
"@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/google/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
|
||||
|
||||
"@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
@@ -6686,10 +6655,6 @@
|
||||
|
||||
"unplugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"venice-ai-sdk-provider/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"venice-ai-sdk-provider/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
|
||||
|
||||
"vitest/@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||
|
||||
@@ -278,6 +278,7 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
//VITE_API_URL: gateway.url.apply((url) => url!),
|
||||
VITE_AUTH_URL: auth.url.apply((url) => url!),
|
||||
VITE_STRIPE_PUBLISHABLE_KEY: STRIPE_PUBLISHABLE_KEY.value,
|
||||
PLACEHOLDER: "keepalive",
|
||||
},
|
||||
transform: {
|
||||
server: {
|
||||
|
||||
+1
-10
@@ -3,7 +3,6 @@
|
||||
stdenv,
|
||||
bun,
|
||||
nodejs,
|
||||
darwin,
|
||||
electron_41,
|
||||
makeWrapper,
|
||||
writableTmpDirAsHomeHook,
|
||||
@@ -15,12 +14,7 @@ let
|
||||
in
|
||||
stdenv.mkDerivation (finalAttrs: {
|
||||
pname = "opencode-desktop";
|
||||
inherit (opencode)
|
||||
version
|
||||
src
|
||||
node_modules
|
||||
patches
|
||||
;
|
||||
inherit (opencode) version src node_modules;
|
||||
|
||||
nativeBuildInputs = [
|
||||
bun
|
||||
@@ -29,9 +23,6 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
writableTmpDirAsHomeHook
|
||||
] ++ lib.optionals stdenv.hostPlatform.isLinux [
|
||||
autoPatchelfHook
|
||||
] ++ lib.optionals stdenv.hostPlatform.isDarwin [
|
||||
# Ad-hoc sign the .app: --config.mac.identity=null below skips signing.
|
||||
darwin.autoSignDarwinBinariesHook
|
||||
];
|
||||
|
||||
buildInputs = lib.optionals stdenv.hostPlatform.isLinux [
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-szOnLhAI4d3O4NaQvCRBufklmtRU8AR8p5LyFxjlTXo=",
|
||||
"aarch64-linux": "sha256-mfyL4Pswl6HeObPg+TMK1r7RjvLCMgDWYrFnfHkXGj4=",
|
||||
"aarch64-darwin": "sha256-XMK5IFCIxlSwH21Nqya8ZkAQ4Skp6GghukIl7NMp+so=",
|
||||
"x86_64-darwin": "sha256-W7nLMO6qhR32442yMebEJJgaZRNpqTbZ8BJhdqMK3S4="
|
||||
"x86_64-linux": "sha256-1RiaZQHzIhdtcOJUMsLagpP+nBBL/Qu6zQgrAXMHDCI=",
|
||||
"aarch64-linux": "sha256-5QZhtkWuNpY/qUxlKRHcGbILOAVnuyzu+h8VDIuMQcU=",
|
||||
"aarch64-darwin": "sha256-9w3QA22XNc1itGLyhikYU90xGH/iLUUM+SSGXla7lhw=",
|
||||
"x86_64-darwin": "sha256-cSYiyhhSqIYiTeK1uWHDkHbYstQYD5jEB7JaYjWjgi4="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -42,10 +42,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@sentry/solid": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@sentry/solid": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@shikijs/transformers": "3.9.2",
|
||||
"@solid-primitives/active-element": "2.1.3",
|
||||
"@solid-primitives/audio": "1.4.2",
|
||||
@@ -54,7 +54,6 @@
|
||||
"@solid-primitives/i18n": "2.2.1",
|
||||
"@solid-primitives/media": "2.3.3",
|
||||
"@solid-primitives/resize-observer": "2.1.5",
|
||||
"@solid-primitives/scheduled": "1.5.3",
|
||||
"@solid-primitives/scroll": "2.1.3",
|
||||
"@solid-primitives/storage": "catalog:",
|
||||
"@solid-primitives/timer": "1.4.4",
|
||||
|
||||
@@ -48,17 +48,22 @@ import { ErrorPage } from "./pages/error"
|
||||
import { useCheckServerHealth } from "./utils/server-health"
|
||||
|
||||
const HomeRoute = lazy(() => import("@/pages/home"))
|
||||
const Session = lazy(() => import("@/pages/session"))
|
||||
const loadSession = () => import("@/pages/session")
|
||||
const Session = lazy(loadSession)
|
||||
const Loading = () => <div class="size-full" />
|
||||
|
||||
const SessionRoute = Object.assign(
|
||||
() => (
|
||||
<SessionProviders>
|
||||
<Session />
|
||||
</SessionProviders>
|
||||
),
|
||||
{ preload: Session.preload },
|
||||
if (typeof location === "object" && /\/session(?:\/|$)/.test(location.pathname)) {
|
||||
void loadSession()
|
||||
}
|
||||
|
||||
const SessionRoute = () => (
|
||||
<SessionProviders>
|
||||
<Session />
|
||||
</SessionProviders>
|
||||
)
|
||||
|
||||
const SessionIndexRoute = () => <Navigate href="session" />
|
||||
|
||||
function UiI18nBridge(props: ParentProps) {
|
||||
const language = useLanguage()
|
||||
return <I18nProvider value={{ locale: language.intl, t: language.t }}>{props.children}</I18nProvider>
|
||||
@@ -312,7 +317,7 @@ export function AppInterface(props: {
|
||||
>
|
||||
<Route path="/" component={HomeRoute} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={() => <Navigate href="session" />} />
|
||||
<Route path="/" component={SessionIndexRoute} />
|
||||
<Route path="/session/:id?" component={SessionRoute} />
|
||||
</Route>
|
||||
</Dynamic>
|
||||
|
||||
@@ -41,7 +41,9 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
})
|
||||
|
||||
const provider = createMemo(
|
||||
() => providers.all().get(props.provider) ?? globalSync.data.provider.all.get(props.provider)!,
|
||||
() =>
|
||||
providers.all().find((x) => x.id === props.provider) ??
|
||||
globalSync.data.provider.all.find((x) => x.id === props.provider)!,
|
||||
)
|
||||
const fallback = createMemo<ProviderAuthMethod[]>(() => [
|
||||
{
|
||||
@@ -524,7 +526,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
const code = createMemo(() => {
|
||||
const instructions = store.authorization?.instructions
|
||||
if (instructions?.includes(":")) {
|
||||
return instructions.split(":").pop()?.trim()
|
||||
return instructions.split(":")[1]?.trim()
|
||||
}
|
||||
return instructions
|
||||
})
|
||||
|
||||
@@ -106,7 +106,7 @@ export function DialogCustomProvider(props: Props) {
|
||||
form,
|
||||
t: language.t,
|
||||
disabledProviders: globalSync.data.config.disabled_providers ?? [],
|
||||
existingProviderIDs: new Set(globalSync.data.provider.all.keys()),
|
||||
existingProviderIDs: new Set(globalSync.data.provider.all.map((p) => p.id)),
|
||||
})
|
||||
batch(() => {
|
||||
setForm("err", output.err)
|
||||
|
||||
@@ -91,7 +91,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
|
||||
<div class="w-full">
|
||||
<List
|
||||
class="w-full px-0"
|
||||
key={(p) => p.id}
|
||||
key={(x) => x?.id}
|
||||
items={providers.popular}
|
||||
activeIcon="plus-small"
|
||||
sortBy={(a, b) => {
|
||||
|
||||
@@ -35,7 +35,7 @@ export const DialogSelectProvider: Component = () => {
|
||||
key={(x) => x?.id}
|
||||
items={() => {
|
||||
language.locale()
|
||||
return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all().values()]
|
||||
return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all()]
|
||||
}}
|
||||
filterKeys={["id", "name"]}
|
||||
groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
|
||||
|
||||
@@ -52,7 +52,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||
}),
|
||||
)
|
||||
|
||||
const metrics = createMemo(() => getSessionContextMetrics(messages(), [...providers.all().values()]))
|
||||
const metrics = createMemo(() => getSessionContextMetrics(messages(), providers.all()))
|
||||
const context = createMemo(() => metrics().context)
|
||||
const cost = createMemo(() => {
|
||||
return usd().format(metrics().totalCost)
|
||||
|
||||
@@ -132,7 +132,7 @@ export function SessionContextTab() {
|
||||
}),
|
||||
)
|
||||
|
||||
const metrics = createMemo(() => getSessionContextMetrics(messages(), [...providers.all().values()]))
|
||||
const metrics = createMemo(() => getSessionContextMetrics(messages(), providers.all()))
|
||||
const ctx = createMemo(() => metrics().context)
|
||||
const formatter = createMemo(() => createSessionContextFormatter(language.intl()))
|
||||
|
||||
|
||||
@@ -23,8 +23,6 @@ import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { Avatar as AvatarV2 } from "@opencode-ai/ui/v2/components/avatar-v2.jsx"
|
||||
import { displayName, getProjectAvatarSource, projectForSession } from "@/pages/layout/helpers"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { StatusPopover } from "./status-popover"
|
||||
import { SDKProvider } from "@/context/sdk"
|
||||
|
||||
type TauriDesktopWindow = {
|
||||
startDragging?: () => Promise<void>
|
||||
@@ -225,13 +223,19 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
const navigate = useNavigate()
|
||||
const homeMatch = useMatch(() => "/")
|
||||
|
||||
const newSessionHref = () => {
|
||||
if (params.dir) return `/${params.dir}/session`
|
||||
const openNewSession = () => {
|
||||
if (params.dir) {
|
||||
navigate(`/${params.dir}/session`)
|
||||
return
|
||||
}
|
||||
|
||||
const project = layout.projects.list()[0]
|
||||
if (!project) return "/"
|
||||
if (!project) {
|
||||
navigate("/")
|
||||
return
|
||||
}
|
||||
|
||||
return `/${base64Encode(project.worktree)}/session`
|
||||
navigate(`/${base64Encode(project.worktree)}/session`)
|
||||
}
|
||||
|
||||
type Tab = { dir: string; sessionId: string; href: string }
|
||||
@@ -298,21 +302,14 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
const currentSessionTab = () => {
|
||||
if (!params.dir || !params.id) return
|
||||
const href = makeSessionHref(params.dir, params.id)
|
||||
return tabsStore.find((tab) => tab.href === href)
|
||||
if (!tabsStore.some((tab) => tab.href === href)) return
|
||||
return href
|
||||
}
|
||||
|
||||
const closeCurrentSessionTab = () => {
|
||||
const tab = currentSessionTab()
|
||||
if (!tab) return false
|
||||
tabsStoreActions.removeTab(tab.href)
|
||||
return true
|
||||
}
|
||||
|
||||
const closeNewSessionTab = () => {
|
||||
if (!(params.dir && !params.id)) return false
|
||||
const last = tabsStore[tabsStore.length - 1]
|
||||
if (last) navigate(last.href)
|
||||
else navigate("/")
|
||||
const href = currentSessionTab()
|
||||
if (!href) return false
|
||||
tabsStoreActions.removeTab(href)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -322,7 +319,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
(event) => {
|
||||
if (!event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return
|
||||
if (event.key.toLowerCase() !== "w") return
|
||||
if (!(closeCurrentSessionTab() || closeNewSessionTab())) return
|
||||
if (!closeCurrentSessionTab()) return
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
@@ -330,63 +327,6 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
{ capture: true },
|
||||
)
|
||||
|
||||
command.register(() => {
|
||||
const commands = [
|
||||
{
|
||||
id: `tab.prev`,
|
||||
category: "tab",
|
||||
title: "",
|
||||
keybind: `mod+option+ArrowLeft`,
|
||||
hidden: true,
|
||||
onSelect: () => {
|
||||
let index = tabsStore.findIndex((tab) => tab.href === currentSessionTab()?.href)
|
||||
if (index === -1) return
|
||||
|
||||
index -= 1
|
||||
if (index === -1) index = tabsStore.length - 1
|
||||
|
||||
const next = tabsStore[index]
|
||||
if (next) navigate(next.href)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: `tab.next`,
|
||||
category: "tab",
|
||||
title: "",
|
||||
keybind: `mod+option+ArrowRight`,
|
||||
hidden: true,
|
||||
onSelect: () => {
|
||||
let index = tabsStore.findIndex((tab) => tab.href === currentSessionTab()?.href)
|
||||
if (index === -1) return
|
||||
|
||||
index += 1
|
||||
if (index === tabsStore.length) index = 0
|
||||
|
||||
const next = tabsStore[index]
|
||||
if (next) navigate(next.href)
|
||||
},
|
||||
},
|
||||
...Array.from({ length: 9 }, (_, i) => {
|
||||
const index = i
|
||||
const number = index + 1
|
||||
return {
|
||||
id: `tab.${number}`,
|
||||
category: "tab",
|
||||
title: "",
|
||||
keybind: `mod+${number}`,
|
||||
disabled: layout.projects.list().length <= index,
|
||||
hidden: true,
|
||||
onSelect: () => {
|
||||
const tab = tabsStore[index]
|
||||
if (tab) navigate(tab.href)
|
||||
},
|
||||
}
|
||||
}),
|
||||
]
|
||||
|
||||
return commands
|
||||
})
|
||||
|
||||
const tabsEnriched = iife(() => {
|
||||
const base = mapArray(
|
||||
() => tabsStore,
|
||||
@@ -451,8 +391,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
size="large"
|
||||
class="shrink-0"
|
||||
icon={<IconV2 name="plus" />}
|
||||
as="a"
|
||||
href={newSessionHref()}
|
||||
onClick={openNewSession}
|
||||
aria-label={language.t("command.session.new")}
|
||||
/>
|
||||
}
|
||||
@@ -465,15 +404,6 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
</Show>
|
||||
<div class="min-w-0 flex-1" />
|
||||
</div>
|
||||
<Show when={currentSessionTab()?.dir} keyed>
|
||||
{(dir) => (
|
||||
<SDKProvider directory={dir}>
|
||||
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
|
||||
<StatusPopover />
|
||||
</Tooltip>
|
||||
</SDKProvider>
|
||||
)}
|
||||
</Show>
|
||||
<TitlebarUpdatePill update={props.update} />
|
||||
<Show when={windows() && !electronWindows()}>
|
||||
<div data-tauri-decorum-tb class="flex flex-row" />
|
||||
|
||||
@@ -232,9 +232,6 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
||||
throwOnError: true,
|
||||
})
|
||||
|
||||
const dirSyncContexts = new Map<string, ReturnType<typeof createDirSdkContext>>()
|
||||
const dirSdkContextRefCounts = new Map<string, number>()
|
||||
|
||||
return {
|
||||
url: currentServer.http.url,
|
||||
client: sdk,
|
||||
@@ -252,58 +249,6 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
||||
...opts,
|
||||
})
|
||||
},
|
||||
createDirSyncContext: (directory: string) => {
|
||||
onCleanup(() => {
|
||||
dirSdkContextRefCounts.set(directory, (dirSdkContextRefCounts.get(directory) ?? 0) - 1)
|
||||
if (dirSdkContextRefCounts.get(directory) === 0) {
|
||||
dirSyncContexts.delete(directory)
|
||||
dirSdkContextRefCounts.delete(directory)
|
||||
}
|
||||
})
|
||||
|
||||
const cached = dirSyncContexts.get(directory)
|
||||
if (cached) {
|
||||
dirSdkContextRefCounts.set(directory, (dirSdkContextRefCounts.get(directory) ?? 0) + 1)
|
||||
return cached
|
||||
}
|
||||
const ctx = createDirSdkContext(directory)
|
||||
dirSyncContexts.set(directory, ctx)
|
||||
dirSdkContextRefCounts.set(directory, 1)
|
||||
|
||||
return ctx
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
type SDKEventMap = {
|
||||
[key in Event["type"]]: Extract<Event, { type: key }>
|
||||
}
|
||||
|
||||
function createDirSdkContext(directory: string) {
|
||||
const globalSDK = useGlobalSDK()
|
||||
|
||||
const client = globalSDK.createClient({
|
||||
directory,
|
||||
throwOnError: true,
|
||||
})
|
||||
|
||||
const emitter = createGlobalEmitter<SDKEventMap>()
|
||||
|
||||
const unsub = globalSDK.event.on(directory, (event) => {
|
||||
emitter.emit(event.type, event)
|
||||
})
|
||||
onCleanup(unsub)
|
||||
|
||||
return {
|
||||
directory,
|
||||
client,
|
||||
event: emitter,
|
||||
get url() {
|
||||
return globalSDK.url
|
||||
},
|
||||
createClient(opts: Parameters<typeof globalSDK.createClient>[0]) {
|
||||
return globalSDK.createClient(opts)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import type { Config, OpencodeClient, Path, Project, ProviderAuthResponse, Todo } from "@opencode-ai/sdk/v2/client"
|
||||
import type {
|
||||
Config,
|
||||
OpencodeClient,
|
||||
Path,
|
||||
Project,
|
||||
ProviderAuthResponse,
|
||||
ProviderListResponse,
|
||||
Todo,
|
||||
} from "@opencode-ai/sdk/v2/client"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { getFilename } from "@opencode-ai/core/util/path"
|
||||
import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
|
||||
@@ -29,7 +37,6 @@ import { createRefreshQueue } from "./global-sync/queue"
|
||||
import { directoryKey } from "./global-sync/utils"
|
||||
import { PathKey } from "@/utils/path-key"
|
||||
import { createDirSyncContext } from "./directory-sync"
|
||||
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
@@ -39,7 +46,7 @@ type GlobalStore = {
|
||||
session_todo: {
|
||||
[sessionID: string]: Todo[]
|
||||
}
|
||||
provider: NormalizedProviderListResponse
|
||||
provider: ProviderListResponse
|
||||
provider_auth: ProviderAuthResponse
|
||||
config: Config
|
||||
reload: undefined | "pending" | "complete"
|
||||
@@ -114,7 +121,7 @@ function createGlobalSync() {
|
||||
return pathQuery.data ?? EMPTY
|
||||
},
|
||||
get provider() {
|
||||
const EMPTY = { all: new Map(), connected: [], default: {} }
|
||||
const EMPTY = { all: [], connected: [], default: {} }
|
||||
if (providerQuery.isLoading) return EMPTY
|
||||
return providerQuery.data ?? EMPTY
|
||||
},
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { QueryClient } from "@tanstack/solid-query"
|
||||
import type { Config, OpencodeClient, Project } from "@opencode-ai/sdk/v2/client"
|
||||
import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||
import { bootstrapDirectory } from "./bootstrap"
|
||||
import type { State, VcsCache } from "./types"
|
||||
|
||||
const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse
|
||||
|
||||
describe("bootstrapDirectory", () => {
|
||||
test("marks a loading directory partial during bootstrap and complete after success", async () => {
|
||||
const [store, setStore] = createStore<State>({
|
||||
status: "loading",
|
||||
agent: [],
|
||||
command: [],
|
||||
project: "",
|
||||
projectMeta: undefined,
|
||||
icon: undefined,
|
||||
provider_ready: true,
|
||||
provider,
|
||||
config: {},
|
||||
path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" },
|
||||
session: [],
|
||||
sessionTotal: 0,
|
||||
session_status: {},
|
||||
session_working(id: string) {
|
||||
return this.session_status[id]?.type !== "idle"
|
||||
},
|
||||
session_diff: {},
|
||||
todo: {},
|
||||
permission: {},
|
||||
question: {},
|
||||
mcp_ready: true,
|
||||
mcp: {},
|
||||
lsp_ready: true,
|
||||
lsp: [],
|
||||
vcs: undefined,
|
||||
limit: 5,
|
||||
message: {},
|
||||
part: {},
|
||||
part_text_accum_delta: {},
|
||||
})
|
||||
|
||||
await bootstrapDirectory({
|
||||
directory: "/project",
|
||||
global: {
|
||||
config: {} satisfies Config,
|
||||
path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" },
|
||||
project: [{ id: "project", worktree: "/project" } as Project],
|
||||
provider,
|
||||
},
|
||||
sdk: {
|
||||
app: { agents: async () => ({ data: [{ name: "build", mode: "primary" }] }) },
|
||||
config: { get: async () => ({ data: {} }) },
|
||||
session: { status: async () => ({ data: {} }) },
|
||||
vcs: { get: async () => ({ data: undefined }) },
|
||||
command: { list: async () => ({ data: [] }) },
|
||||
permission: { list: async () => ({ data: [] }) },
|
||||
question: { list: async () => ({ data: [] }) },
|
||||
mcp: { status: async () => ({ data: {} }) },
|
||||
provider: { list: async () => ({ data: { all: [], connected: [], default: {} } }) },
|
||||
} as unknown as OpencodeClient,
|
||||
store,
|
||||
setStore,
|
||||
vcsCache: { setStore() {} } as unknown as VcsCache,
|
||||
loadSessions() {},
|
||||
translate: (key) => key,
|
||||
queryClient: new QueryClient(),
|
||||
})
|
||||
|
||||
expect(store.status).toBe("partial")
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 80))
|
||||
|
||||
expect(store.status).toBe("complete")
|
||||
})
|
||||
})
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
PermissionRequest,
|
||||
Project,
|
||||
ProviderAuthResponse,
|
||||
ProviderListResponse,
|
||||
QuestionRequest,
|
||||
Session,
|
||||
Todo,
|
||||
@@ -19,7 +20,6 @@ import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
import { QueryClient, queryOptions } from "@tanstack/solid-query"
|
||||
import { loadMcpQuery } from "../global-sync"
|
||||
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
@@ -28,7 +28,7 @@ type GlobalStore = {
|
||||
session_todo: {
|
||||
[sessionID: string]: Todo[]
|
||||
}
|
||||
provider: NormalizedProviderListResponse
|
||||
provider: ProviderListResponse
|
||||
provider_auth: ProviderAuthResponse
|
||||
config: Config
|
||||
reload: undefined | "pending" | "complete"
|
||||
@@ -208,7 +208,7 @@ export async function bootstrapDirectory(input: {
|
||||
config: Config
|
||||
path: Path
|
||||
project: Project[]
|
||||
provider: NormalizedProviderListResponse
|
||||
provider: ProviderListResponse
|
||||
}
|
||||
queryClient: QueryClient
|
||||
}) {
|
||||
|
||||
@@ -1,63 +1,10 @@
|
||||
import { beforeAll, describe, expect, mock, test } from "bun:test"
|
||||
import { createRoot, getOwner, type Owner } from "solid-js"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createRoot, getOwner } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||
import type { State } from "./types"
|
||||
import type { QueryOptionsApi } from "../global-sync"
|
||||
|
||||
let createChildStoreManager: typeof import("./child-store").createChildStoreManager
|
||||
import { createChildStoreManager } from "./child-store"
|
||||
|
||||
const child = () => createStore({} as State)
|
||||
const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse
|
||||
|
||||
const queryOptionsApi = {
|
||||
globalConfig: () => ({ queryKey: ["globalConfig"], queryFn: async () => ({}) }),
|
||||
projects: () => ({ queryKey: ["projects"], queryFn: async () => [] }),
|
||||
providers: (directory: string | null) => ({ queryKey: [directory, "providers"], queryFn: async () => provider }),
|
||||
path: (directory: string | null) => ({
|
||||
queryKey: [directory, "path"],
|
||||
queryFn: async () => ({
|
||||
state: "",
|
||||
config: "",
|
||||
worktree: "",
|
||||
directory: directory ?? "",
|
||||
home: "",
|
||||
}),
|
||||
}),
|
||||
agents: (directory: string) => ({ queryKey: [directory, "agents"], queryFn: async () => [] }),
|
||||
mcp: (directory: string) => ({ queryKey: [directory, "mcp"], queryFn: async () => ({}) }),
|
||||
lsp: (directory: string) => ({ queryKey: [directory, "lsp"], queryFn: async () => [] }),
|
||||
sessions: (directory: string) => ({ queryKey: [directory, "loadSessions"] as const }),
|
||||
} as unknown as QueryOptionsApi
|
||||
|
||||
function createOwner(callback: (owner: Owner) => void) {
|
||||
return createRoot((dispose) => {
|
||||
const owner = getOwner()
|
||||
if (!owner) throw new Error("owner required")
|
||||
callback(owner)
|
||||
|
||||
return dispose
|
||||
})
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
mock.module("@/utils/persist", () => ({
|
||||
Persist: {
|
||||
workspace: (...parts: string[]) => parts.join(":"),
|
||||
},
|
||||
persisted: (_target: string, store: unknown[]) => [store[0], store[1], null, () => true],
|
||||
}))
|
||||
mock.module("@tanstack/solid-query", () => ({
|
||||
useQueries: () => [
|
||||
{ isLoading: false, data: { state: "", config: "", worktree: "", directory: "", home: "" } },
|
||||
{ isLoading: false, data: {} },
|
||||
{ isLoading: false, data: [] },
|
||||
{ isLoading: false, data: provider },
|
||||
],
|
||||
}))
|
||||
|
||||
createChildStoreManager = (await import("./child-store")).createChildStoreManager
|
||||
})
|
||||
|
||||
describe("createChildStoreManager", () => {
|
||||
test("does not evict the active directory during mark", () => {
|
||||
@@ -75,8 +22,8 @@ describe("createChildStoreManager", () => {
|
||||
onBootstrap() {},
|
||||
onDispose() {},
|
||||
translate: (key) => key,
|
||||
queryOptions: queryOptionsApi,
|
||||
global: { provider },
|
||||
queryOptions: {} as any,
|
||||
global: { provider: null! },
|
||||
})
|
||||
|
||||
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
||||
@@ -90,35 +37,4 @@ describe("createChildStoreManager", () => {
|
||||
|
||||
expect(manager.children[directory]).toBeDefined()
|
||||
})
|
||||
|
||||
test("starts new child stores as loading and bootstraps them on first access", () => {
|
||||
const bootstraps: string[] = []
|
||||
let manager: ReturnType<typeof createChildStoreManager> | undefined
|
||||
|
||||
const dispose = createOwner((owner) => {
|
||||
manager = createChildStoreManager({
|
||||
owner,
|
||||
isBooting: () => false,
|
||||
isLoadingSessions: () => false,
|
||||
onBootstrap(directory) {
|
||||
bootstraps.push(directory)
|
||||
},
|
||||
onDispose() {},
|
||||
translate: (key) => key,
|
||||
queryOptions: queryOptionsApi,
|
||||
global: { provider },
|
||||
})
|
||||
})
|
||||
|
||||
try {
|
||||
if (!manager) throw new Error("manager required")
|
||||
|
||||
const [store] = manager.child("/project")
|
||||
|
||||
expect(store.status).toBe("loading")
|
||||
expect(bootstraps).toEqual(["/project"])
|
||||
} finally {
|
||||
dispose()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 { VcsInfo } from "@opencode-ai/sdk/v2/client"
|
||||
import type { ProviderListResponse, VcsInfo } from "@opencode-ai/sdk/v2/client"
|
||||
import {
|
||||
DIR_IDLE_TTL_MS,
|
||||
MAX_DIR_STORES,
|
||||
@@ -17,7 +17,6 @@ import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
|
||||
import { useQueries } from "@tanstack/solid-query"
|
||||
import { QueryOptionsApi } from "../global-sync"
|
||||
import { directoryKey, type DirectoryKey } from "./utils"
|
||||
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||
|
||||
export function createChildStoreManager(input: {
|
||||
owner: Owner
|
||||
@@ -28,7 +27,7 @@ export function createChildStoreManager(input: {
|
||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||
queryOptions: QueryOptionsApi
|
||||
global: {
|
||||
provider: NormalizedProviderListResponse
|
||||
provider: ProviderListResponse
|
||||
}
|
||||
}) {
|
||||
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
||||
@@ -191,9 +190,10 @@ export function createChildStoreManager(input: {
|
||||
return !providerQuery.isLoading
|
||||
},
|
||||
get provider() {
|
||||
const EMPTY = { all: new Map(), connected: [], default: {} }
|
||||
const EMPTY = { all: [], connected: [], default: {} }
|
||||
if (providerQuery.isLoading) return EMPTY
|
||||
if (providerQuery.data?.all.size === 0 && input.global.provider.all.size > 0) return input.global.provider
|
||||
if (providerQuery.data?.all.length === 0 && input.global.provider.all.length > 0)
|
||||
return input.global.provider
|
||||
return providerQuery.data ?? EMPTY
|
||||
},
|
||||
config: {},
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
Part,
|
||||
Path,
|
||||
PermissionRequest,
|
||||
ProviderListResponse,
|
||||
QuestionRequest,
|
||||
Session,
|
||||
SessionStatus,
|
||||
@@ -15,7 +16,6 @@ import type {
|
||||
Todo,
|
||||
VcsInfo,
|
||||
} from "@opencode-ai/sdk/v2/client"
|
||||
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||
import type { Accessor } from "solid-js"
|
||||
import type { SetStoreFunction, Store } from "solid-js/store"
|
||||
|
||||
@@ -38,7 +38,7 @@ export type State = {
|
||||
projectMeta: ProjectMeta | undefined
|
||||
icon: string | undefined
|
||||
provider_ready: boolean
|
||||
provider: NormalizedProviderListResponse
|
||||
provider: ProviderListResponse
|
||||
config: Config
|
||||
path: Path
|
||||
session: Session[]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client"
|
||||
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||
export { pathKey as directoryKey, type PathKey as DirectoryKey } from "@/utils/path-key"
|
||||
|
||||
export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||
@@ -18,23 +17,13 @@ export function normalizeAgentList(input: unknown): Agent[] {
|
||||
return Object.values(input).filter(isAgent)
|
||||
}
|
||||
|
||||
export function normalizeProviderList(input: ProviderListResponse): NormalizedProviderListResponse {
|
||||
export function normalizeProviderList(input: ProviderListResponse): ProviderListResponse {
|
||||
return {
|
||||
...input,
|
||||
all: new Map(
|
||||
input.all.map(
|
||||
(provider) =>
|
||||
[
|
||||
provider.id,
|
||||
{
|
||||
...provider,
|
||||
models: Object.fromEntries(
|
||||
Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated"),
|
||||
),
|
||||
},
|
||||
] as const,
|
||||
),
|
||||
),
|
||||
all: input.all.map((provider) => ({
|
||||
...provider,
|
||||
models: Object.fromEntries(Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated")),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
})
|
||||
|
||||
const validModel = (model: ModelKey) => {
|
||||
const provider = providers.all().get(model.providerID)
|
||||
const provider = providers.all().find((item) => item.id === model.providerID)
|
||||
return !!provider?.models[model.modelID] && connected().has(model.providerID)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,49 @@
|
||||
import type { Event } from "@opencode-ai/sdk/v2/client"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||
import { type Accessor, createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
|
||||
type SDKEventMap = {
|
||||
[key in Event["type"]]: Extract<Event, { type: key }>
|
||||
}
|
||||
|
||||
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
name: "SDK",
|
||||
init: (props: { directory: string }) => {
|
||||
init: (props: { directory: Accessor<string> }) => {
|
||||
const globalSDK = useGlobalSDK()
|
||||
|
||||
return globalSDK.createDirSyncContext(props.directory)
|
||||
const directory = createMemo(props.directory)
|
||||
const client = createMemo(() =>
|
||||
globalSDK.createClient({
|
||||
directory: directory(),
|
||||
throwOnError: true,
|
||||
}),
|
||||
)
|
||||
|
||||
const emitter = createGlobalEmitter<SDKEventMap>()
|
||||
|
||||
createEffect(() => {
|
||||
const unsub = globalSDK.event.on(directory(), (event) => {
|
||||
emitter.emit(event.type, event)
|
||||
})
|
||||
onCleanup(unsub)
|
||||
})
|
||||
|
||||
return {
|
||||
get directory() {
|
||||
return directory()
|
||||
},
|
||||
get client() {
|
||||
return client()
|
||||
},
|
||||
event: emitter,
|
||||
get url() {
|
||||
return globalSDK.url
|
||||
},
|
||||
createClient(opts: Parameters<typeof globalSDK.createClient>[0]) {
|
||||
return globalSDK.createClient(opts)
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Binary } from "@opencode-ai/core/util/binary"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useGlobalSync } from "./global-sync"
|
||||
import { useSDK } from "./sdk"
|
||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
||||
@@ -9,8 +10,26 @@ function sortParts(parts: Part[]) {
|
||||
return parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id))
|
||||
}
|
||||
|
||||
function runInflight(map: Map<string, Promise<void>>, key: string, task: () => Promise<void>) {
|
||||
const pending = map.get(key)
|
||||
if (pending) return pending
|
||||
const promise = task().finally(() => {
|
||||
map.delete(key)
|
||||
})
|
||||
map.set(key, promise)
|
||||
return promise
|
||||
}
|
||||
|
||||
const keyFor = (directory: string, id: string) => `${directory}\n${id}`
|
||||
|
||||
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||
|
||||
function merge<T extends { id: string }>(a: readonly T[], b: readonly T[]) {
|
||||
const map = new Map(a.map((item) => [item.id, item] as const))
|
||||
for (const item of b) map.set(item.id, item)
|
||||
return [...map.values()].sort((x, y) => cmp(x.id, y.id))
|
||||
}
|
||||
|
||||
type OptimisticStore = {
|
||||
message: Record<string, Message[] | undefined>
|
||||
part: Record<string, Part[] | undefined>
|
||||
@@ -108,9 +127,40 @@ export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticR
|
||||
delete draft.part[input.messageID]
|
||||
}
|
||||
|
||||
export const useSync = () => {
|
||||
const globalSync = useGlobalSync()
|
||||
const sdk = useSDK()
|
||||
|
||||
return globalSync.createDirSyncContext(sdk.directory)
|
||||
function setOptimisticAdd(setStore: (...args: unknown[]) => void, input: OptimisticAddInput) {
|
||||
setStore("message", input.sessionID, (messages: Message[] | undefined) => {
|
||||
if (!messages) return [input.message]
|
||||
const result = Binary.search(messages, input.message.id, (m) => m.id)
|
||||
const next = [...messages]
|
||||
next.splice(result.index, 0, input.message)
|
||||
return next
|
||||
})
|
||||
setStore("part", input.message.id, sortParts(input.parts))
|
||||
}
|
||||
|
||||
function setOptimisticRemove(setStore: (...args: unknown[]) => void, input: OptimisticRemoveInput) {
|
||||
setStore("message", input.sessionID, (messages: Message[] | undefined) => {
|
||||
if (!messages) return messages
|
||||
const result = Binary.search(messages, input.messageID, (m) => m.id)
|
||||
if (!result.found) return messages
|
||||
const next = [...messages]
|
||||
next.splice(result.index, 1)
|
||||
return next
|
||||
})
|
||||
setStore("part", (part: Record<string, Part[] | undefined>) => {
|
||||
if (!(input.messageID in part)) return part
|
||||
const next = { ...part }
|
||||
delete next[input.messageID]
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
name: "Sync",
|
||||
init: () => {
|
||||
const globalSync = useGlobalSync()
|
||||
const sdk = useSDK()
|
||||
|
||||
return globalSync.createDirSyncContext(sdk.directory)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { Iterable, pipe } from "effect"
|
||||
import { createMemo } from "solid-js"
|
||||
|
||||
export const popularProviders = [
|
||||
@@ -30,32 +29,16 @@ export function useProviders() {
|
||||
return {
|
||||
all: () => providers().all,
|
||||
default: () => providers().default,
|
||||
popular: () =>
|
||||
pipe(
|
||||
providers().all,
|
||||
Iterable.map(([, p]) => p),
|
||||
Iterable.filter((p) => popularProviderSet.has(p.id)),
|
||||
(v) => Array.from(v),
|
||||
),
|
||||
popular: () => providers().all.filter((p) => popularProviderSet.has(p.id)),
|
||||
connected: () => {
|
||||
const connected = new Set(providers().connected)
|
||||
return pipe(
|
||||
providers().all,
|
||||
Iterable.map(([, p]) => p),
|
||||
Iterable.filter((p) => connected.has(p.id)),
|
||||
(v) => Array.from(v),
|
||||
)
|
||||
return providers().all.filter((p) => connected.has(p.id))
|
||||
},
|
||||
paid: () => {
|
||||
const connected = new Set(providers().connected)
|
||||
return [
|
||||
...Iterable.filter(
|
||||
providers().all,
|
||||
([id]) =>
|
||||
connected.has(id) &&
|
||||
(id !== "opencode" || Object.values(providers().all.get(id)?.models ?? {}).some((m) => m.cost?.input)),
|
||||
),
|
||||
]
|
||||
return providers().all.filter(
|
||||
(p) => connected.has(p.id) && (p.id !== "opencode" || Object.values(p.models).some((m) => m.cost?.input)),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createEffect, createMemo, createResource, type ParentProps, Show } from
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { LocalProvider } from "@/context/local"
|
||||
import { SDKProvider } from "@/context/sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { SyncProvider, useSync } from "@/context/sync"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { Schema } from "effect"
|
||||
|
||||
@@ -81,8 +81,10 @@ export default function Layout(props: ParentProps) {
|
||||
return (
|
||||
<Show when={resolved()} keyed>
|
||||
{(resolved) => (
|
||||
<SDKProvider directory={resolved}>
|
||||
<DirectoryDataProvider directory={resolved}>{props.children}</DirectoryDataProvider>
|
||||
<SDKProvider directory={() => resolved}>
|
||||
<SyncProvider>
|
||||
<DirectoryDataProvider directory={resolved}>{props.children}</DirectoryDataProvider>
|
||||
</SyncProvider>
|
||||
</SDKProvider>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
+181
-57
@@ -1,5 +1,5 @@
|
||||
import type { Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { createMemo, For, Match, Show, Switch } from "solid-js"
|
||||
import { createMemo, createSignal, For, Match, Show, Switch } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useQuery } from "@tanstack/solid-query"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
@@ -18,6 +18,7 @@ import { DateTime } from "luxon"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||
import { DialogSelectModel } from "@/components/dialog-select-model"
|
||||
import { useServer } from "@/context/server"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -466,6 +467,11 @@ function LegacyHome() {
|
||||
const navigate = useNavigate()
|
||||
const server = useServer()
|
||||
const language = useLanguage()
|
||||
|
||||
const [promptText, setPromptText] = createSignal("")
|
||||
const [selectedAgent, setSelectedAgent] = createSignal("frontend-specialist")
|
||||
const [showProjectsDropdown, setShowProjectsDropdown] = createSignal(false)
|
||||
|
||||
const homedir = createMemo(() => sync.data.path.home)
|
||||
const recent = createMemo(() => {
|
||||
return sync.data.project
|
||||
@@ -474,6 +480,8 @@ function LegacyHome() {
|
||||
.slice(0, 5)
|
||||
})
|
||||
|
||||
const currentProject = createMemo(() => recent()[0]?.worktree)
|
||||
|
||||
const serverDotClass = createMemo(() => {
|
||||
const healthy = server.healthy()
|
||||
if (healthy === true) return "bg-icon-success-base"
|
||||
@@ -512,69 +520,185 @@ function LegacyHome() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleModelSelect() {
|
||||
dialog.show(() => <DialogSelectModel />)
|
||||
}
|
||||
|
||||
function toggleAgent() {
|
||||
const agents = ["frontend-specialist", "build", "general"]
|
||||
const nextIndex = (agents.indexOf(selectedAgent()) + 1) % agents.length
|
||||
setSelectedAgent(agents[nextIndex])
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
const projectToOpen = currentProject()
|
||||
if (projectToOpen) {
|
||||
openProject(projectToOpen)
|
||||
} else {
|
||||
chooseProject()
|
||||
}
|
||||
}
|
||||
|
||||
const activeModelName = createMemo(() => {
|
||||
const model = sync.data.config.model
|
||||
if (!model) return "GPT-5.7 Pro"
|
||||
const parts = model.split("/")
|
||||
return parts[parts.length - 1]
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="mx-auto mt-55 w-full md:w-auto px-4">
|
||||
<Logo class="md:w-xl opacity-12" />
|
||||
<Button
|
||||
size="large"
|
||||
variant="ghost"
|
||||
class="mt-4 mx-auto text-14-regular text-text-weak"
|
||||
onClick={() => dialog.show(() => <DialogSelectServer />)}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"size-2 rounded-full": true,
|
||||
[serverDotClass()]: true,
|
||||
}}
|
||||
/>
|
||||
{server.name}
|
||||
</Button>
|
||||
<div class="mx-auto mt-24 w-full max-w-2xl px-6 flex flex-col items-center">
|
||||
<div class="flex flex-col items-center gap-3 mb-10">
|
||||
<div onClick={chooseProject} class="cursor-pointer hover:opacity-25 transition-opacity duration-200">
|
||||
<Logo class="w-48 opacity-15" />
|
||||
</div>
|
||||
<Button
|
||||
size="normal"
|
||||
variant="ghost"
|
||||
class="text-12-regular text-text-weak px-3"
|
||||
onClick={() => dialog.show(() => <DialogSelectServer />)}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"size-1.5 rounded-full mr-2": true,
|
||||
[serverDotClass()]: true,
|
||||
}}
|
||||
/>
|
||||
{server.name}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Switch>
|
||||
<Match when={sync.data.project.length > 0}>
|
||||
<div class="mt-20 w-full flex flex-col gap-4">
|
||||
<div class="flex gap-2 items-center justify-between pl-3">
|
||||
<div class="text-14-medium text-text-strong">{language.t("home.recentProjects")}</div>
|
||||
<Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}>
|
||||
{language.t("command.project.open")}
|
||||
</Button>
|
||||
</div>
|
||||
<ul class="flex flex-col gap-2">
|
||||
<For each={recent()}>
|
||||
{(project) => (
|
||||
<Match when={recent().length > 0}>
|
||||
<div class="w-full flex flex-col items-center gap-6">
|
||||
<div class="text-20-medium text-text-strong text-center">{language.t("session.new.title")}</div>
|
||||
|
||||
<div class="w-full bg-surface-base border border-border-base rounded-xl p-4 flex flex-col gap-3 shadow-md relative">
|
||||
<textarea
|
||||
class="bg-transparent border-none outline-none text-14-regular text-text-base placeholder-text-weak w-full resize-none h-20 focus:outline-none"
|
||||
placeholder="Ask anything, / for commands, @ for context..."
|
||||
value={promptText()}
|
||||
onInput={(e) => setPromptText(e.currentTarget.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSubmit()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2 pt-3 border-t border-border-weak-base">
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
||||
onClick={toggleAgent}
|
||||
>
|
||||
<Icon name="sliders" size="small" class="shrink-0" />
|
||||
<span>Agent: {selectedAgent()}</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
||||
onClick={handleModelSelect}
|
||||
>
|
||||
<Icon name="brain" size="small" class="shrink-0" />
|
||||
<span>Model: {activeModelName()}</span>
|
||||
</Button>
|
||||
|
||||
<div class="relative">
|
||||
<Button
|
||||
size="large"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
class="text-14-mono text-left justify-between px-3"
|
||||
onClick={() => openProject(project.worktree)}
|
||||
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
||||
onClick={() => setShowProjectsDropdown(!showProjectsDropdown())}
|
||||
>
|
||||
{project.worktree.replace(homedir(), "~")}
|
||||
<div class="text-14-regular text-text-weak">
|
||||
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
|
||||
</div>
|
||||
<Icon name="folder" size="small" class="shrink-0" />
|
||||
<span>Project: {currentProject() ? getFilename(currentProject()) : "Select Project"}</span>
|
||||
</Button>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={!sync.ready}>
|
||||
<div class="mt-30 mx-auto flex flex-col items-center gap-3">
|
||||
<div class="text-12-regular text-text-weak">{language.t("common.loading")}</div>
|
||||
<Button class="px-3" onClick={chooseProject}>
|
||||
{language.t("command.project.open")}
|
||||
</Button>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div class="mt-30 mx-auto flex flex-col items-center gap-3">
|
||||
<Icon name="folder-add-left" size="large" />
|
||||
<div class="flex flex-col gap-1 items-center justify-center">
|
||||
<div class="text-14-medium text-text-strong">{language.t("home.empty.title")}</div>
|
||||
<div class="text-12-regular text-text-weak">{language.t("home.empty.description")}</div>
|
||||
|
||||
<Show when={showProjectsDropdown()}>
|
||||
<div class="absolute left-0 mt-1 w-64 bg-surface-raised-base border border-border-base rounded-lg p-2 shadow-lg z-50 flex flex-col gap-1">
|
||||
<div class="text-10-semibold text-text-weak px-2 py-1 uppercase tracking-wider">
|
||||
{language.t("home.recentProjects")}
|
||||
</div>
|
||||
<For each={recent()}>
|
||||
{(project) => (
|
||||
<button
|
||||
class="text-12-mono text-left px-2 py-1.5 hover:bg-surface-raised-base-hover rounded flex items-center justify-between w-full"
|
||||
onClick={() => {
|
||||
openProject(project.worktree)
|
||||
setShowProjectsDropdown(false)
|
||||
}}
|
||||
>
|
||||
<span class="truncate">{getFilename(project.worktree)}</span>
|
||||
<span class="text-10-regular text-text-weak shrink-0 pl-2">
|
||||
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
<div class="border-t border-border-weak-base my-1" />
|
||||
<button
|
||||
class="text-12-medium text-text-strong text-left px-2 py-1.5 hover:bg-surface-raised-base-hover rounded flex items-center gap-2 w-full"
|
||||
onClick={() => {
|
||||
setShowProjectsDropdown(false)
|
||||
chooseProject()
|
||||
}}
|
||||
>
|
||||
<Icon name="folder-add-left" size="small" />
|
||||
{language.t("command.project.open")}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
class="text-12-medium text-text-weak flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base border border-border-weak-base rounded-md cursor-default pointer-events-none"
|
||||
>
|
||||
<Icon name="branch" size="small" class="shrink-0" />
|
||||
<span>Branch: dev</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
<Match when={true}>
|
||||
<div class="w-full flex flex-col items-center gap-6">
|
||||
<div class="text-20-medium text-text-strong text-center">{language.t("home.empty.title")}</div>
|
||||
|
||||
<div class="w-full bg-surface-base border border-border-base rounded-xl p-4 flex flex-col gap-3 shadow-md">
|
||||
<div class="text-14-regular text-text-weak w-full min-h-[4rem] cursor-pointer" onClick={chooseProject}>
|
||||
Ask anything, / for commands, @ for context...
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2 pt-3 border-t border-border-weak-base">
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
||||
onClick={chooseProject}
|
||||
>
|
||||
<Icon name="folder" size="small" class="shrink-0" />
|
||||
<span>Open project</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
||||
onClick={handleModelSelect}
|
||||
>
|
||||
<Icon name="brain" size="small" class="shrink-0" />
|
||||
<span>Model: {activeModelName()}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button class="px-3 mt-1" onClick={chooseProject}>
|
||||
{language.t("command.project.open")}
|
||||
</Button>
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
@@ -89,7 +89,7 @@ import {
|
||||
import { ProjectDragOverlay, SortableProject, type ProjectSidebarContext } from "./layout/sidebar-project"
|
||||
import { SidebarContent } from "./layout/sidebar-shell"
|
||||
|
||||
const USE_NEW_DESIGN = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
|
||||
const USE_HOME_DESIGN = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
|
||||
|
||||
export default function Layout(props: ParentProps) {
|
||||
const [store, setStore, , ready] = persisted(
|
||||
@@ -154,7 +154,7 @@ export default function Layout(props: ParentProps) {
|
||||
const currentDir = createMemo(() => route().dir)
|
||||
|
||||
const [state, setState] = createStore({
|
||||
autoselect: !initialDirectory && !USE_NEW_DESIGN,
|
||||
autoselect: !initialDirectory && !USE_HOME_DESIGN,
|
||||
busyWorkspaces: {} as Record<string, boolean>,
|
||||
hoverProject: undefined as string | undefined,
|
||||
scrollSessionKey: undefined as string | undefined,
|
||||
@@ -1028,6 +1028,19 @@ export default function Layout(props: ParentProps) {
|
||||
keybind: "mod+alt+arrowdown",
|
||||
onSelect: () => navigateProjectByOffset(1),
|
||||
},
|
||||
...Array.from({ length: 9 }, (_, i) => {
|
||||
const index = i
|
||||
const number = index + 1
|
||||
return {
|
||||
id: `project.${number}`,
|
||||
category: language.t("command.category.project"),
|
||||
title: `Open Project {number}`,
|
||||
keybind: `mod+${number}`,
|
||||
disabled: layout.projects.list().length <= index,
|
||||
hidden: true,
|
||||
onSelect: () => navigateToProjectIndex(index),
|
||||
}
|
||||
}),
|
||||
{
|
||||
id: "provider.connect",
|
||||
title: language.t("command.provider.connect"),
|
||||
@@ -1142,21 +1155,6 @@ export default function Layout(props: ParentProps) {
|
||||
},
|
||||
]
|
||||
|
||||
if (!USE_NEW_DESIGN)
|
||||
Array.from({ length: 9 }, (_, i) => {
|
||||
const index = i
|
||||
const number = index + 1
|
||||
commands.push({
|
||||
id: `project.${number}`,
|
||||
category: language.t("command.category.project"),
|
||||
title: `Open Project {number}`,
|
||||
keybind: `mod+${number}`,
|
||||
disabled: layout.projects.list().length <= index,
|
||||
hidden: true,
|
||||
onSelect: () => navigateToProjectIndex(index),
|
||||
})
|
||||
})
|
||||
|
||||
for (const [id] of availableThemeEntries()) {
|
||||
commands.push({
|
||||
id: `theme.set.${id}`,
|
||||
@@ -1821,7 +1819,7 @@ export default function Layout(props: ParentProps) {
|
||||
createEffect(() => {
|
||||
document.documentElement.style.setProperty(
|
||||
"--dialog-left-margin",
|
||||
USE_NEW_DESIGN ? "0px" : `${layout.sidebar.opened() ? layout.sidebar.width() : 48}px`,
|
||||
USE_HOME_DESIGN ? "0px" : `${layout.sidebar.opened() ? layout.sidebar.width() : 48}px`,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2305,7 +2303,7 @@ export default function Layout(props: ParentProps) {
|
||||
<div
|
||||
class="shrink-0 px-3 py-3"
|
||||
classList={{
|
||||
hidden: store.gettingStartedDismissed || !(providers.all().size > 0 && providers.paid().length === 0),
|
||||
hidden: store.gettingStartedDismissed || !(providers.all().length > 0 && providers.paid().length === 0),
|
||||
}}
|
||||
>
|
||||
<div class="rounded-xl bg-background-base shadow-xs-border-base" data-component="getting-started">
|
||||
@@ -2363,7 +2361,7 @@ export default function Layout(props: ParentProps) {
|
||||
/>
|
||||
)
|
||||
|
||||
if (USE_NEW_DESIGN) {
|
||||
if (USE_HOME_DESIGN) {
|
||||
return (
|
||||
<div class="relative bg-v2-background-bg-deep flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
||||
{autoselecting() ?? ""}
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { debounce } from "@solid-primitives/scheduled"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
|
||||
import { createStore } from "solid-js/store"
|
||||
@@ -59,7 +58,6 @@ import { SessionSidePanel } from "@/pages/session/session-side-panel"
|
||||
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
||||
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
||||
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
||||
import { shouldUseV2NewSessionPage } from "@/pages/session/new-session-layout"
|
||||
import { Identifier } from "@/utils/id"
|
||||
import { diffs as list } from "@/utils/diffs"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
@@ -264,10 +262,8 @@ export default function Page() {
|
||||
|
||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||
const size = createSizing()
|
||||
const isV2NewSessionPage = () =>
|
||||
shouldUseV2NewSessionPage({ channel: import.meta.env.VITE_OPENCODE_CHANNEL, sessionID: params.id })
|
||||
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened() && !isV2NewSessionPage())
|
||||
const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened() && !isV2NewSessionPage())
|
||||
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
||||
const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened())
|
||||
const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen())
|
||||
const sessionPanelWidth = createMemo(() => {
|
||||
if (!desktopSidePanelOpen()) return "100%"
|
||||
@@ -481,7 +477,7 @@ export default function Page() {
|
||||
: skipToken,
|
||||
}
|
||||
})
|
||||
const refreshVcs = debounce(() => void queryClient.invalidateQueries({ queryKey: vcsKey() }), 100)
|
||||
const refreshVcs = () => void queryClient.invalidateQueries({ queryKey: vcsKey() })
|
||||
const reviewDiffs = () => {
|
||||
if (store.changes === "git" || store.changes === "branch")
|
||||
// avoids suspense
|
||||
@@ -1737,12 +1733,12 @@ export default function Page() {
|
||||
</Tabs>
|
||||
</Show>
|
||||
|
||||
{/* Session panel */}
|
||||
<div
|
||||
classList={{
|
||||
"@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger flex-1 md:flex-none": true,
|
||||
"duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
|
||||
"transition-[width] duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
|
||||
!size.active() && !ui.reviewSnap,
|
||||
"transition-[width]": !isV2NewSessionPage(),
|
||||
}}
|
||||
style={{
|
||||
width: sessionPanelWidth(),
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { shouldUseV2NewSessionPage } from "./new-session-layout"
|
||||
|
||||
describe("shouldUseV2NewSessionPage", () => {
|
||||
test("keeps prod session pages on the legacy layout", () => {
|
||||
expect(shouldUseV2NewSessionPage({ channel: "prod", sessionID: "ses_123" })).toBe(false)
|
||||
expect(shouldUseV2NewSessionPage({ channel: "prod" })).toBe(false)
|
||||
})
|
||||
|
||||
test("uses the v2 layout only for non-prod new-session pages", () => {
|
||||
expect(shouldUseV2NewSessionPage({ channel: "dev" })).toBe(true)
|
||||
expect(shouldUseV2NewSessionPage({ channel: "dev", sessionID: "ses_123" })).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,3 +0,0 @@
|
||||
export function shouldUseV2NewSessionPage(input: { channel?: "dev" | "beta" | "prod"; sessionID?: string }) {
|
||||
return input.channel !== "prod" && !input.sessionID
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export function SessionSidePanel(props: {
|
||||
const language = useLanguage()
|
||||
const command = useCommand()
|
||||
const dialog = useDialog()
|
||||
const { sessionKey, tabs, view, params } = useSessionLayout()
|
||||
const { sessionKey, tabs, view } = useSessionLayout()
|
||||
|
||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||
const shown = createMemo(
|
||||
@@ -207,7 +207,7 @@ export function SessionSidePanel(props: {
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={isDesktop() && !(import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" && !params.id)}>
|
||||
<Show when={isDesktop()}>
|
||||
<aside
|
||||
id="review-panel"
|
||||
aria-label={language.t("session.panel.reviewAndFiles")}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -10,12 +10,9 @@ export function createRateLimiter(modelId: string, rateLimit: number | undefined
|
||||
const dict = i18n(localeFromRequest(request))
|
||||
|
||||
const limits = Subscription.getFreeLimits()
|
||||
// temporarily disable check headers
|
||||
//const headersExist = Object.entries(limits.checkHeaders).every(
|
||||
// ([name, value]) => request.headers.get(name)?.toLowerCase().includes(value) ?? false,
|
||||
//)
|
||||
//const dailyLimit = !headersExist ? limits.dailyRequestsFallback : (rateLimit ?? limits.dailyRequests)
|
||||
const headersExist = true
|
||||
const headersExist = Object.entries(limits.checkHeaders).every(
|
||||
([name, value]) => request.headers.get(name)?.toLowerCase().includes(value) ?? false,
|
||||
)
|
||||
const dailyLimit = !headersExist ? limits.dailyRequestsFallback : (rateLimit ?? limits.dailyRequests)
|
||||
const isDefaultModel = headersExist && !rateLimit
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createHash } from "crypto"
|
||||
import { ZenData } from "@opencode-ai/console-core/model.js"
|
||||
|
||||
export async function GET() {
|
||||
const zenData = ZenData.list("full")
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
hash: createHash("sha1").update(JSON.stringify(zenData)).digest("hex"),
|
||||
timestamp: Date.now(),
|
||||
check1: "alpha-di-k2.6" in zenData.models,
|
||||
check2: "qwen3.6-plus-free" in zenData.models,
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -5,14 +5,12 @@ export const Resource = new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_target, prop: string) {
|
||||
console.log(`111 ${prop}`)
|
||||
if (`SST_RESOURCE_${prop}` in env) {
|
||||
console.log(`222 ${prop}`)
|
||||
// @ts-expect-error
|
||||
const value = env[`SST_RESOURCE_${prop}`]
|
||||
return typeof value === "string" ? JSON.parse(value) : value
|
||||
}
|
||||
if (prop in env) {
|
||||
// @ts-expect-error
|
||||
const value = env[prop]
|
||||
console.log(`333 ${value}`)
|
||||
return typeof value === "string" ? JSON.parse(value) : value
|
||||
} else if (prop === "App") {
|
||||
// @ts-expect-error
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -27,15 +27,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.107",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.96",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
"@ai-sdk/azure": "3.0.49",
|
||||
"@ai-sdk/cerebras": "2.0.41",
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/deepinfra": "2.0.41",
|
||||
"@ai-sdk/gateway": "3.0.104",
|
||||
"@ai-sdk/google": "3.0.75",
|
||||
"@ai-sdk/google-vertex": "4.0.131",
|
||||
"@ai-sdk/google": "3.0.63",
|
||||
"@ai-sdk/google-vertex": "4.0.112",
|
||||
"@ai-sdk/groq": "3.0.31",
|
||||
"@ai-sdk/mistral": "3.0.27",
|
||||
"@ai-sdk/openai": "3.0.53",
|
||||
@@ -67,7 +67,7 @@
|
||||
"minimatch": "10.2.5",
|
||||
"npm-package-arg": "13.0.2",
|
||||
"semver": "^7.6.3",
|
||||
"venice-ai-sdk-provider": "2.0.2",
|
||||
"venice-ai-sdk-provider": "2.0.1",
|
||||
"xdg-basedir": "5.1.0",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
|
||||
@@ -3,10 +3,9 @@ import { dirname, join, relative, resolve as pathResolve } from "path"
|
||||
import { realpathSync } from "fs"
|
||||
import * as NFS from "fs/promises"
|
||||
import { lookup } from "mime-types"
|
||||
import { Context, Effect, FileSystem, Layer, Schema } from "effect"
|
||||
import { Effect, FileSystem, Layer, Schema, Context } from "effect"
|
||||
import type { PlatformError } from "effect/PlatformError"
|
||||
import { Glob } from "./util/glob"
|
||||
import { serviceUse } from "./effect/service-use"
|
||||
|
||||
export namespace AppFileSystem {
|
||||
export class FileSystemError extends Schema.TaggedErrorClass<FileSystemError>()("FileSystemError", {
|
||||
@@ -40,8 +39,6 @@ export namespace AppFileSystem {
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {}
|
||||
|
||||
export const use = serviceUse(Service)
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
export * as Git from "./git"
|
||||
|
||||
import path from "path"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { AbsolutePath } from "./schema"
|
||||
import { AppFileSystem } from "./filesystem"
|
||||
import { AppProcess } from "./process"
|
||||
|
||||
export interface Repo {
|
||||
/**
|
||||
* The root directory of the working tree that contains the input path.
|
||||
*
|
||||
* For `/home/me/app/src/file.ts` in a normal clone, this is `/home/me/app`.
|
||||
* For `/home/me/app-feature/src/file.ts` in a linked worktree, this is
|
||||
* `/home/me/app-feature`.
|
||||
*/
|
||||
readonly directory: AbsolutePath
|
||||
/**
|
||||
* The shared Git storage directory used by this repo and any linked worktrees.
|
||||
*
|
||||
* For a normal clone at `/home/me/app`, this is usually `/home/me/app/.git`.
|
||||
* For a linked worktree at `/home/me/app-feature` whose main checkout is
|
||||
* `/home/me/app`, this is usually `/home/me/app/.git`.
|
||||
*/
|
||||
readonly store: AbsolutePath
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly find: (input: AbsolutePath) => Effect.Effect<Repo | undefined>
|
||||
readonly remote: (repo: Repo, name?: string) => Effect.Effect<string | undefined>
|
||||
readonly roots: (repo: Repo) => Effect.Effect<string[]>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/GitV2") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const proc = yield* AppProcess.Service
|
||||
|
||||
const find = Effect.fn("Git.find")(function* (input: AbsolutePath) {
|
||||
const dotgit = yield* fs.up({ targets: [".git"], start: input }).pipe(
|
||||
Effect.map((matches) => matches[0]),
|
||||
Effect.catch(() => Effect.succeed(undefined)),
|
||||
)
|
||||
if (!dotgit) return undefined
|
||||
|
||||
const cwd = path.dirname(dotgit)
|
||||
const git = run(cwd, proc)
|
||||
const topLevel = yield* git(["rev-parse", "--show-toplevel"])
|
||||
const commonDir = yield* git(["rev-parse", "--git-common-dir"])
|
||||
if (commonDir.exitCode !== 0) return undefined
|
||||
|
||||
return {
|
||||
directory: AbsolutePath.make(topLevel.exitCode === 0 ? resolvePath(cwd, topLevel.text) : cwd),
|
||||
store: AbsolutePath.make(resolvePath(cwd, commonDir.text)),
|
||||
} satisfies Repo
|
||||
})
|
||||
|
||||
const remote = Effect.fn("Git.remote")(function* (repo: Repo, name = "origin") {
|
||||
const result = yield* run(repo.directory, proc)(["remote", "get-url", name])
|
||||
if (result.exitCode !== 0) return undefined
|
||||
return result.text.trim() || undefined
|
||||
})
|
||||
|
||||
const roots = Effect.fn("Git.roots")(function* (repo: Repo) {
|
||||
const result = yield* run(repo.directory, proc)(["rev-list", "--max-parents=0", "HEAD"])
|
||||
if (result.exitCode !== 0) return []
|
||||
return result.text
|
||||
.split("\n")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
.toSorted()
|
||||
})
|
||||
|
||||
return Service.of({ find, remote, roots })
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(AppProcess.defaultLayer),
|
||||
)
|
||||
|
||||
interface Result {
|
||||
readonly exitCode: number
|
||||
readonly text: string
|
||||
}
|
||||
|
||||
function run(cwd: string, proc: AppProcess.Interface) {
|
||||
return (args: string[]) =>
|
||||
proc
|
||||
.run(
|
||||
ChildProcess.make("git", args, {
|
||||
cwd,
|
||||
extendEnv: true,
|
||||
stdin: "ignore",
|
||||
}),
|
||||
)
|
||||
.pipe(
|
||||
Effect.map((result) => ({ exitCode: result.exitCode, text: result.stdout.toString("utf8") }) satisfies Result),
|
||||
Effect.catch(() => Effect.succeed({ exitCode: 1, text: "" } satisfies Result)),
|
||||
)
|
||||
}
|
||||
|
||||
function resolvePath(cwd: string, value: string) {
|
||||
const trimmed = value.replace(/[\r\n]+$/, "")
|
||||
if (!trimmed) return cwd
|
||||
const normalized = AppFileSystem.windowsPath(trimmed)
|
||||
if (path.isAbsolute(normalized)) return path.normalize(normalized)
|
||||
return path.resolve(cwd, normalized)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as PluginBoot from "./boot"
|
||||
|
||||
import { Context, Deferred, Effect, Layer } from "effect"
|
||||
import { Context, Deferred, Effect, Layer, Scope } from "effect"
|
||||
import { AccountV2 } from "../account"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { Catalog } from "../catalog"
|
||||
|
||||
@@ -25,8 +25,7 @@ function resolveLocation(options: Record<string, any>) {
|
||||
}
|
||||
|
||||
function vertexEndpoint(location: string) {
|
||||
if (location === "global") return "aiplatform.googleapis.com"
|
||||
return `${location}-aiplatform.googleapis.com`
|
||||
return location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`
|
||||
}
|
||||
|
||||
function replaceVertexVars(value: string, project: string | undefined, location: string) {
|
||||
@@ -132,25 +131,16 @@ export const GoogleVertexAnthropicPlugin = PluginV2.define({
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/google-vertex/anthropic") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic"))
|
||||
const project =
|
||||
typeof evt.options.project === "string"
|
||||
? evt.options.project
|
||||
: (process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GCP_PROJECT ?? process.env.GCLOUD_PROJECT)
|
||||
const location =
|
||||
typeof evt.options.location === "string"
|
||||
? evt.options.location
|
||||
: (process.env.GOOGLE_CLOUD_LOCATION ?? process.env.VERTEX_LOCATION ?? "global")
|
||||
evt.sdk = mod.createVertexAnthropic({
|
||||
...evt.options,
|
||||
project,
|
||||
location,
|
||||
// Continental multi-regions (eu, us) require Regional Endpoint Platform
|
||||
// domains; the default {region}-aiplatform.googleapis.com does not resolve.
|
||||
...((location === "eu" || location === "us") && project && !evt.options.baseURL
|
||||
? {
|
||||
baseURL: `https://aiplatform.${location}.rep.googleapis.com/v1/projects/${project}/locations/${location}/publishers/anthropic/models`,
|
||||
}
|
||||
: {}),
|
||||
project:
|
||||
typeof evt.options.project === "string"
|
||||
? evt.options.project
|
||||
: (process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GCP_PROJECT ?? process.env.GCLOUD_PROJECT),
|
||||
location:
|
||||
typeof evt.options.location === "string"
|
||||
? evt.options.location
|
||||
: (process.env.GOOGLE_CLOUD_LOCATION ?? process.env.VERTEX_LOCATION ?? "global"),
|
||||
})
|
||||
}),
|
||||
"aisdk.language": Effect.fn(function* (evt) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const VercelPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("vercel"),
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
export * as Project from "./project"
|
||||
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import path from "path"
|
||||
import { AbsolutePath, withStatics } from "./schema"
|
||||
import { AppFileSystem } from "./filesystem"
|
||||
import { Git } from "./git"
|
||||
import { Hash } from "./util/hash"
|
||||
|
||||
export const ID = Schema.String.pipe(
|
||||
Schema.brand("Project.ID"),
|
||||
withStatics((schema) => ({
|
||||
global: schema.make("global"),
|
||||
})),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const Vcs = Schema.Union([
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("git"),
|
||||
store: AbsolutePath,
|
||||
}),
|
||||
])
|
||||
export type Vcs = typeof Vcs.Type
|
||||
|
||||
export class Info extends Schema.Class<Info>("Project.Info")({
|
||||
id: ID,
|
||||
vcs: Schema.optional(Vcs),
|
||||
}) {}
|
||||
|
||||
export interface Interface {
|
||||
readonly resolve: (input: AbsolutePath) => Effect.Effect<
|
||||
{
|
||||
previous?: ID
|
||||
id: ID
|
||||
directory: AbsolutePath
|
||||
vcs?: Vcs
|
||||
},
|
||||
never
|
||||
>
|
||||
/**
|
||||
* Temporary bridge method for writing the resolved project ID to the repo-local cache.
|
||||
*
|
||||
* This exists while the old opencode project service and this core project
|
||||
* service work together: core resolves the ID, while the old service still owns
|
||||
* database migration and persistence. The old service should call this after it
|
||||
* finishes migrating from `resolve().previous` to `resolve().id`; once project
|
||||
* persistence moves into core, this separate bridge method can go away.
|
||||
*/
|
||||
readonly commit: (input: { store: AbsolutePath; id: ID }) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ProjectV2") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const git = yield* Git.Service
|
||||
|
||||
const cached = Effect.fnUntraced(function* (dir: string) {
|
||||
return yield* fs.readFileString(path.join(dir, "opencode")).pipe(
|
||||
Effect.map((value) => value.trim()),
|
||||
Effect.map((value) => (value ? ID.make(value) : undefined)),
|
||||
Effect.catch(() => Effect.succeed(undefined)),
|
||||
)
|
||||
})
|
||||
|
||||
const remote = Effect.fnUntraced(function* (repo: Git.Repo) {
|
||||
const origin = yield* git.remote(repo)
|
||||
if (!origin) return undefined
|
||||
const normalized = url(origin)
|
||||
if (!normalized) return undefined
|
||||
return ID.make(Hash.fast(`git-remote:${normalized}`))
|
||||
})
|
||||
|
||||
function url(input: string) {
|
||||
const value = input.trim()
|
||||
if (!value) return undefined
|
||||
|
||||
try {
|
||||
const parsed = new URL(value)
|
||||
if (parsed.protocol === "file:") return undefined
|
||||
return parts(parsed.hostname, parsed.pathname)
|
||||
} catch {
|
||||
const scp = value.match(/^([^@/:]+@)?([^/:]+):(.+)$/)
|
||||
if (scp) return parts(scp[2], scp[3])
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function parts(host: string, name: string) {
|
||||
const pathname = name
|
||||
.replace(/^\/+/, "")
|
||||
.replace(/\.git\/?$/, "")
|
||||
.replace(/\/+$/, "")
|
||||
if (!host || !pathname) return undefined
|
||||
return `${host.toLowerCase()}/${pathname}`
|
||||
}
|
||||
|
||||
const root = Effect.fnUntraced(function* (repo: Git.Repo) {
|
||||
const root = (yield* git.roots(repo))[0]
|
||||
return root ? ID.make(root) : undefined
|
||||
})
|
||||
|
||||
const resolve = Effect.fn("Project.resolve")(function* (input: AbsolutePath) {
|
||||
const repo = yield* git.find(input)
|
||||
if (!repo) return { id: ID.global, directory: input, vcs: undefined }
|
||||
|
||||
const previous = yield* cached(repo.store)
|
||||
const id = previous ?? (yield* root(repo))
|
||||
|
||||
return {
|
||||
previous,
|
||||
id: id ?? ID.global,
|
||||
directory: repo.directory,
|
||||
vcs: { type: "git" as const, store: repo.store },
|
||||
}
|
||||
})
|
||||
|
||||
const commit = Effect.fn("Project.commit")(function* (input: { store: AbsolutePath; id: ID }) {
|
||||
yield* fs.writeFileString(path.join(input.store, "opencode"), input.id).pipe(Effect.ignore)
|
||||
})
|
||||
|
||||
return Service.of({ resolve, commit })
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer))
|
||||
@@ -1,11 +1,5 @@
|
||||
import { Option, Schema, SchemaGetter } from "effect"
|
||||
|
||||
export const AbsolutePath = Schema.String.pipe(Schema.brand("AbsolutePath"))
|
||||
export type AbsolutePath = typeof AbsolutePath.Type
|
||||
|
||||
export const RelativePath = Schema.String.pipe(Schema.brand("RelativePath"))
|
||||
export type RelativePath = typeof RelativePath.Type
|
||||
|
||||
/**
|
||||
* Integer greater than zero.
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { CerebrasPlugin } from "@opencode-ai/core/plugin/provider/cerebras"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { it, model } from "./provider-helper"
|
||||
import { it, model, provider } from "./provider-helper"
|
||||
|
||||
const cerebrasOptions: Record<string, unknown>[] = []
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex"
|
||||
import { GoogleVertexAnthropicPlugin } from "@opencode-ai/core/plugin/provider/google-vertex"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { fakeSelectorSdk, it, model, withEnv } from "./provider-helper"
|
||||
|
||||
@@ -109,73 +109,6 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("creates SDKs for google-vertex Anthropic models with multi-region endpoints", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* plugin.add(GoogleVertexAnthropicPlugin)
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("google-vertex", "claude-sonnet-4-5"),
|
||||
package: "@ai-sdk/google-vertex/anthropic",
|
||||
options: { name: "google-vertex", project: "project", location: "eu" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe(
|
||||
"https://aiplatform.eu.rep.googleapis.com/v1/projects/project/locations/eu/publishers/anthropic/models",
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("keeps configured baseURL for google-vertex Anthropic models", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* plugin.add(GoogleVertexAnthropicPlugin)
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("google-vertex", "claude-sonnet-4-5"),
|
||||
package: "@ai-sdk/google-vertex/anthropic",
|
||||
options: { name: "google-vertex", project: "project", location: "eu", baseURL: "https://proxy.example/v1" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe("https://proxy.example/v1")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("selects google-vertex Anthropic language models through V2 plugins", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* plugin.add(GoogleVertexPlugin)
|
||||
yield* plugin.add(GoogleVertexAnthropicPlugin)
|
||||
const sdkResult = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("google-vertex", " claude-sonnet-4-5 "),
|
||||
package: "@ai-sdk/google-vertex/anthropic",
|
||||
options: { name: "google-vertex", project: "project", location: "us" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const languageResult = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("google-vertex", " claude-sonnet-4-5 "),
|
||||
sdk: sdkResult.sdk,
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
const language = languageResult.language as unknown as { config: { baseURL: string }; modelId: string }
|
||||
expect(language.config.baseURL).toBe(
|
||||
"https://aiplatform.us.rep.googleapis.com/v1/projects/project/locations/us/publishers/anthropic/models",
|
||||
)
|
||||
expect(language.modelId).toBe("claude-sonnet-4-5")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("trims model IDs before selecting language models", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
|
||||
@@ -160,32 +160,6 @@ describe("GoogleVertexPlugin", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("keeps OpenAI-compatible Vertex endpoint templates regional for eu", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(GoogleVertexPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
||||
provider.endpoint = {
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
|
||||
}
|
||||
provider.options.aisdk.provider.project = "config-project"
|
||||
provider.options.aisdk.provider.location = "eu"
|
||||
}),
|
||||
)
|
||||
const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
|
||||
expect(provider.endpoint).toEqual({
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
url: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("defaults location to us-central1 when only project is configured", () =>
|
||||
withEnv(
|
||||
{
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { $ } from "bun"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { tmpdir } from "./fixture/tmpdir"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const it = testEffect(Project.defaultLayer)
|
||||
|
||||
function abs(value: string) {
|
||||
return AbsolutePath.make(value)
|
||||
}
|
||||
|
||||
function real(value: string) {
|
||||
return Effect.promise(() => fs.realpath(value)).pipe(Effect.map((value) => AbsolutePath.make(value)))
|
||||
}
|
||||
|
||||
async function initRepo(dir: string, opts?: { commit?: boolean; remote?: string }) {
|
||||
await $`git init`.cwd(dir).quiet()
|
||||
await $`git config core.fsmonitor false`.cwd(dir).quiet()
|
||||
await $`git config commit.gpgsign false`.cwd(dir).quiet()
|
||||
await $`git config user.email test@opencode.test`.cwd(dir).quiet()
|
||||
await $`git config user.name Test`.cwd(dir).quiet()
|
||||
if (opts?.commit) await $`git commit --allow-empty -m root`.cwd(dir).quiet()
|
||||
if (opts?.remote) await $`git remote add origin ${opts.remote}`.cwd(dir).quiet()
|
||||
}
|
||||
|
||||
async function rootCommit(dir: string) {
|
||||
return (await $`git rev-list --max-parents=0 HEAD`.cwd(dir).text()).trim()
|
||||
}
|
||||
|
||||
describe("ProjectV2.resolve", () => {
|
||||
it.live("returns global for non-git directory", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
const project = yield* Project.Service
|
||||
|
||||
const result = yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(result.id).toBe(Project.ID.make("global"))
|
||||
expect(path.resolve(result.directory)).toBe(path.resolve(tmp.path))
|
||||
expect(result.previous).toBeUndefined()
|
||||
expect(result.vcs).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("returns git global for repo with no commits and no remote", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path))
|
||||
const project = yield* Project.Service
|
||||
|
||||
const result = yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(result.id).toBe(Project.ID.make("global"))
|
||||
expect(result.directory).toBe(yield* real(tmp.path))
|
||||
expect(result.previous).toBeUndefined()
|
||||
expect(result.vcs?.type).toBe("git")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("falls back to root commit when origin is missing", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true }))
|
||||
const project = yield* Project.Service
|
||||
|
||||
const result = yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(result.id).toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
|
||||
expect(result.directory).toBe(yield* real(tmp.path))
|
||||
expect(result.previous).toBeUndefined()
|
||||
expect(result.vcs?.type).toBe("git")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("uses root commit when origin exists", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:Acme/App.git" }))
|
||||
const project = yield* Project.Service
|
||||
|
||||
const result = yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(result.id).toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
|
||||
expect(result.directory).toBe(yield* real(tmp.path))
|
||||
expect(result.vcs?.type).toBe("git")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("uses root commit when local remote exists", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: `file://${tmp.path}` }))
|
||||
const project = yield* Project.Service
|
||||
|
||||
const result = yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(result.id).toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("prefers previous cached id over origin", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" }))
|
||||
yield* Effect.promise(() => Bun.write(path.join(tmp.path, ".git", "opencode"), "old-id"))
|
||||
const project = yield* Project.Service
|
||||
|
||||
const result = yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(result.previous).toBe(Project.ID.make("old-id"))
|
||||
expect(result.id).toBe(Project.ID.make("old-id"))
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("does not write the cache while resolving", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" }))
|
||||
const project = yield* Project.Service
|
||||
|
||||
yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, ".git", "opencode")).exists())).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("resolves from nested directories to repo root", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true }))
|
||||
yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "a", "b"), { recursive: true }))
|
||||
const project = yield* Project.Service
|
||||
|
||||
const result = yield* project.resolve(abs(path.join(tmp.path, "a", "b")))
|
||||
|
||||
expect(result.directory).toBe(yield* real(tmp.path))
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("linked worktree returns opened worktree directory and previous from common dir", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
const worktree = `${tmp.path}-worktree`
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.promise(() => $`rm -rf ${worktree}`.quiet().nothrow()).pipe(Effect.ignore),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" }))
|
||||
yield* Effect.promise(() => Bun.write(path.join(tmp.path, ".git", "opencode"), "old-id"))
|
||||
yield* Effect.promise(() => $`git worktree add ${worktree} -b test-${Date.now()}`.cwd(tmp.path).quiet())
|
||||
const project = yield* Project.Service
|
||||
|
||||
const result = yield* project.resolve(abs(worktree))
|
||||
|
||||
expect(result.directory).toBe(yield* real(worktree))
|
||||
expect(result.previous).toBe(Project.ID.make("old-id"))
|
||||
expect(result.id).toBe(Project.ID.make("old-id"))
|
||||
expect(result.vcs?.type).toBe("git")
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import windowState from "electron-window-state"
|
||||
import { resolveThemeVariant } from "@opencode-ai/ui/theme/resolve"
|
||||
import type { DesktopTheme } from "@opencode-ai/ui/theme/types"
|
||||
import oc2ThemeJson from "../../../ui/src/theme/themes/oc-2.json"
|
||||
import { app, BrowserWindow, dialog, net, nativeImage, nativeTheme, protocol } from "electron"
|
||||
import { dirname, isAbsolute, join, relative, resolve } from "node:path"
|
||||
import { fileURLToPath, pathToFileURL } from "node:url"
|
||||
@@ -18,11 +15,6 @@ const rendererHost = "renderer"
|
||||
const clipboardWritePermission = "clipboard-sanitized-write"
|
||||
const notificationPermission = "notifications"
|
||||
const rendererPermissions = new Set([clipboardWritePermission, notificationPermission])
|
||||
const oc2Theme = oc2ThemeJson as DesktopTheme
|
||||
const oc2Background = {
|
||||
light: resolveThemeVariant(oc2Theme.light, false)["background-base"],
|
||||
dark: resolveThemeVariant(oc2Theme.dark, true)["background-base"],
|
||||
}
|
||||
const documentPolicyHeader = "Document-Policy"
|
||||
const jsCallStacksDocumentPolicy = "include-js-call-stacks-in-crash-reports"
|
||||
|
||||
@@ -54,7 +46,6 @@ export function setRelaunchHandler(handler: () => void) {
|
||||
|
||||
export function setBackgroundColor(color: string) {
|
||||
backgroundColor = color
|
||||
BrowserWindow.getAllWindows().forEach((win) => win.setBackgroundColor(color))
|
||||
}
|
||||
|
||||
export function getBackgroundColor(): string | undefined {
|
||||
@@ -74,10 +65,6 @@ function tone() {
|
||||
return nativeTheme.shouldUseDarkColors ? "dark" : "light"
|
||||
}
|
||||
|
||||
function defaultBackgroundColor() {
|
||||
return oc2Background[tone()]
|
||||
}
|
||||
|
||||
function overlay(theme: Partial<TitlebarTheme> = {}, zoom = 1) {
|
||||
const mode = theme.mode ?? tone()
|
||||
return {
|
||||
@@ -133,7 +120,7 @@ export function createMainWindow() {
|
||||
autoHideMenuBar: true,
|
||||
title: "OpenCode",
|
||||
icon: iconPath(),
|
||||
backgroundColor: backgroundColor ?? defaultBackgroundColor(),
|
||||
backgroundColor,
|
||||
...(process.platform === "darwin"
|
||||
? {
|
||||
titleBarStyle: "hidden" as const,
|
||||
@@ -191,7 +178,7 @@ export function createLoadingWindow() {
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
icon: iconPath(),
|
||||
backgroundColor: backgroundColor ?? defaultBackgroundColor(),
|
||||
backgroundColor,
|
||||
...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const } : {}),
|
||||
...(process.platform === "win32"
|
||||
? {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.15.10"
|
||||
version = "1.15.7"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/anomalyco/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.10/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.7/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.10/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.7/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.10/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.7/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.10/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.7/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.10/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.7/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"name": "@opencode-ai/llm",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
ToolChoice,
|
||||
ToolDefinition,
|
||||
type ContentPart,
|
||||
ToolCallPart,
|
||||
ToolResultPart,
|
||||
} from "./schema"
|
||||
import { make as makeTool, type ToolSchema } from "./tool"
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
type ProviderMetadata,
|
||||
type ToolCallPart,
|
||||
type ToolDefinition,
|
||||
type ToolResultContentPart,
|
||||
type ToolResultPart,
|
||||
} from "../schema"
|
||||
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
|
||||
@@ -97,18 +96,10 @@ const AnthropicServerToolResultBlock = Schema.Struct({
|
||||
})
|
||||
type AnthropicServerToolResultBlock = Schema.Schema.Type<typeof AnthropicServerToolResultBlock>
|
||||
|
||||
// Anthropic accepts either a plain string or an ordered array of text/image
|
||||
// blocks inside `tool_result.content`. The array form is required when a tool
|
||||
// returns image bytes (screenshot, image search, etc.) so they can be passed
|
||||
// to the model as proper image inputs instead of being JSON-stringified into
|
||||
// the prompt — which silently inflates context by megabytes and can push the
|
||||
// conversation over the model's token limit.
|
||||
const AnthropicToolResultContent = Schema.Union([AnthropicTextBlock, AnthropicImageBlock])
|
||||
|
||||
const AnthropicToolResultBlock = Schema.Struct({
|
||||
type: Schema.tag("tool_result"),
|
||||
tool_use_id: Schema.String,
|
||||
content: Schema.Union([Schema.String, Schema.Array(AnthropicToolResultContent)]),
|
||||
content: Schema.String,
|
||||
is_error: Schema.optional(Schema.Boolean),
|
||||
cache_control: Schema.optional(AnthropicCacheControl),
|
||||
})
|
||||
@@ -206,13 +197,7 @@ const AnthropicEvent = Schema.Struct({
|
||||
content_block: Schema.optional(AnthropicStreamBlock),
|
||||
delta: Schema.optional(AnthropicStreamDelta),
|
||||
usage: Schema.optional(AnthropicUsage),
|
||||
// `type` and `message` are both required per Anthropic's spec, but
|
||||
// OpenAI-compatible proxies and gateway translations occasionally drop one
|
||||
// or the other; mark them optional so a partial payload still parses and
|
||||
// the parser can fall back to whichever field is populated.
|
||||
error: Schema.optional(
|
||||
Schema.Struct({ type: Schema.optional(Schema.String), message: Schema.optional(Schema.String) }),
|
||||
),
|
||||
error: Schema.optional(Schema.Struct({ type: Schema.String, message: Schema.String })),
|
||||
})
|
||||
type AnthropicEvent = Schema.Schema.Type<typeof AnthropicEvent>
|
||||
|
||||
@@ -313,33 +298,6 @@ const lowerImage = Effect.fn("AnthropicMessages.lowerImage")(function* (part: Me
|
||||
} satisfies AnthropicImageBlock
|
||||
})
|
||||
|
||||
// Tool results may carry structured text/images. Keep media as provider-native
|
||||
// content instead of JSON-stringifying base64 into a prompt string.
|
||||
const lowerToolResultContentItem = Effect.fn("AnthropicMessages.lowerToolResultContentItem")(function* (
|
||||
item: ToolResultContentPart,
|
||||
) {
|
||||
if (item.type === "text") return { type: "text" as const, text: item.text } satisfies AnthropicTextBlock
|
||||
if (item.mediaType.startsWith("image/"))
|
||||
return {
|
||||
type: "image" as const,
|
||||
source: {
|
||||
type: "base64" as const,
|
||||
media_type: item.mediaType,
|
||||
data: ProviderShared.mediaBase64(item),
|
||||
},
|
||||
} satisfies AnthropicImageBlock
|
||||
return yield* invalid(`Anthropic Messages tool-result media content only supports images, got ${item.mediaType}`)
|
||||
})
|
||||
|
||||
const lowerToolResultContent = Effect.fn("AnthropicMessages.lowerToolResultContent")(function* (part: ToolResultPart) {
|
||||
// Text / json / error results stay as a string for backward compatibility
|
||||
// with existing cassettes and provider expectations.
|
||||
if (part.result.type !== "content") return ProviderShared.toolResultText(part)
|
||||
// Preserve the narrowed array element type when compiled through a consumer package.
|
||||
const content: ReadonlyArray<ToolResultContentPart> = part.result.value
|
||||
return yield* Effect.forEach(content, lowerToolResultContentItem)
|
||||
})
|
||||
|
||||
const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
|
||||
request: LLMRequest,
|
||||
breakpoints: Cache.Breakpoints,
|
||||
@@ -402,7 +360,7 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
|
||||
content.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: part.id,
|
||||
content: yield* lowerToolResultContent(part),
|
||||
content: ProviderShared.toolResultText(part),
|
||||
is_error: part.result.type === "error" ? true : undefined,
|
||||
cache_control: cacheControl(breakpoints, part.cache),
|
||||
})
|
||||
@@ -709,18 +667,9 @@ const onMessageDelta = (state: ParserState, event: AnthropicEvent): StepResult =
|
||||
return [{ ...state, lifecycle, usage }, events]
|
||||
}
|
||||
|
||||
// Prefix `error.type` so overloads, rate limits, and quota errors are visible
|
||||
// even when the provider message is generic or empty.
|
||||
const providerErrorMessage = (event: AnthropicEvent): string => {
|
||||
const type = event.error?.type
|
||||
const message = event.error?.message
|
||||
if (type && message) return `${type}: ${message}`
|
||||
return message || type || "Anthropic Messages stream error"
|
||||
}
|
||||
|
||||
const onError = (state: ParserState, event: AnthropicEvent): StepResult => [
|
||||
state,
|
||||
[LLMEvent.providerError({ message: providerErrorMessage(event) })],
|
||||
[LLMEvent.providerError({ message: event.error?.message ?? "Anthropic Messages stream error" })],
|
||||
]
|
||||
|
||||
const step = (state: ParserState, event: AnthropicEvent) => {
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
type TextPart,
|
||||
type ToolCallPart,
|
||||
type ToolDefinition,
|
||||
type ToolResultContentPart,
|
||||
type ToolResultPart,
|
||||
} from "../schema"
|
||||
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
|
||||
import { OpenAIOptions } from "./utils/openai-options"
|
||||
@@ -57,27 +55,11 @@ const OpenAIResponsesReasoningItem = Schema.Struct({
|
||||
encrypted_content: optionalNull(Schema.String),
|
||||
})
|
||||
|
||||
const OpenAIResponsesItemReference = Schema.Struct({
|
||||
type: Schema.tag("item_reference"),
|
||||
id: Schema.String,
|
||||
})
|
||||
|
||||
// `function_call_output.output` accepts either a plain string or an ordered
|
||||
// array of content items so tools can return images in addition to text.
|
||||
// https://platform.openai.com/docs/api-reference/responses/object
|
||||
const OpenAIResponsesFunctionCallOutputContent = Schema.Union([OpenAIResponsesInputText, OpenAIResponsesInputImage])
|
||||
|
||||
const OpenAIResponsesFunctionCallOutput = Schema.Union([
|
||||
Schema.String,
|
||||
Schema.Array(OpenAIResponsesFunctionCallOutputContent),
|
||||
])
|
||||
|
||||
const OpenAIResponsesInputItem = Schema.Union([
|
||||
Schema.Struct({ role: Schema.tag("system"), content: Schema.String }),
|
||||
Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputContent) }),
|
||||
Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenAIResponsesOutputText) }),
|
||||
OpenAIResponsesReasoningItem,
|
||||
OpenAIResponsesItemReference,
|
||||
Schema.Struct({
|
||||
type: Schema.tag("function_call"),
|
||||
call_id: Schema.String,
|
||||
@@ -87,20 +69,11 @@ const OpenAIResponsesInputItem = Schema.Union([
|
||||
Schema.Struct({
|
||||
type: Schema.tag("function_call_output"),
|
||||
call_id: Schema.String,
|
||||
output: OpenAIResponsesFunctionCallOutput,
|
||||
output: Schema.String,
|
||||
}),
|
||||
])
|
||||
type OpenAIResponsesInputItem = Schema.Schema.Type<typeof OpenAIResponsesInputItem>
|
||||
|
||||
// Mutable counterpart of the schema reasoning item so `lowerMessages` can fold
|
||||
// multiple streamed summary parts into the same item before flushing.
|
||||
type OpenAIResponsesReasoningInput = {
|
||||
type: "reasoning"
|
||||
id: string
|
||||
summary: Array<{ type: "summary_text"; text: string }>
|
||||
encrypted_content?: string | null
|
||||
}
|
||||
|
||||
const OpenAIResponsesTool = Schema.Struct({
|
||||
type: Schema.tag("function"),
|
||||
name: Schema.String,
|
||||
@@ -127,7 +100,7 @@ const OpenAIResponsesCoreFields = {
|
||||
tool_choice: Schema.optional(OpenAIResponsesToolChoice),
|
||||
store: Schema.optional(Schema.Boolean),
|
||||
prompt_cache_key: Schema.optional(Schema.String),
|
||||
include: optionalArray(OpenAIOptions.OpenAIResponseIncludable),
|
||||
include: optionalArray(Schema.Literal("reasoning.encrypted_content")),
|
||||
reasoning: Schema.optional(
|
||||
Schema.Struct({
|
||||
effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
|
||||
@@ -193,22 +166,10 @@ const OpenAIResponsesStreamItem = Schema.Struct({
|
||||
})
|
||||
type OpenAIResponsesStreamItem = Schema.Schema.Type<typeof OpenAIResponsesStreamItem>
|
||||
|
||||
// OpenAI Responses surfaces provider failures in two related shapes. The
|
||||
// streaming `error` event carries the details at the top level
|
||||
// (`{ type: "error", code, message, param, sequence_number }`), while
|
||||
// `response.failed` carries them under `response.error`. We capture both so
|
||||
// the parser can surface a useful provider-error message in either path.
|
||||
const OpenAIResponsesErrorPayload = Schema.Struct({
|
||||
code: optionalNull(Schema.String),
|
||||
message: optionalNull(Schema.String),
|
||||
param: optionalNull(Schema.String),
|
||||
})
|
||||
|
||||
const OpenAIResponsesEvent = Schema.Struct({
|
||||
type: Schema.String,
|
||||
delta: Schema.optional(Schema.String),
|
||||
item_id: Schema.optional(Schema.String),
|
||||
summary_index: Schema.optional(Schema.Number),
|
||||
item: Schema.optional(OpenAIResponsesStreamItem),
|
||||
response: Schema.optional(
|
||||
Schema.StructWithRest(
|
||||
@@ -217,14 +178,12 @@ const OpenAIResponsesEvent = Schema.Struct({
|
||||
service_tier: optionalNull(Schema.String),
|
||||
incomplete_details: optionalNull(Schema.Struct({ reason: Schema.String })),
|
||||
usage: optionalNull(OpenAIResponsesUsage),
|
||||
error: optionalNull(OpenAIResponsesErrorPayload),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
),
|
||||
),
|
||||
code: Schema.optional(Schema.String),
|
||||
message: Schema.optional(Schema.String),
|
||||
param: Schema.optional(Schema.String),
|
||||
})
|
||||
type OpenAIResponsesEvent = Schema.Schema.Type<typeof OpenAIResponsesEvent>
|
||||
|
||||
@@ -232,18 +191,6 @@ interface ParserState {
|
||||
readonly tools: ToolStream.State<string>
|
||||
readonly hasFunctionCall: boolean
|
||||
readonly lifecycle: Lifecycle.State
|
||||
readonly reasoningItems: Readonly<Record<string, ReasoningStreamItem>>
|
||||
readonly store: boolean | undefined
|
||||
}
|
||||
|
||||
type ReasoningSummaryStatus = "active" | "can-conclude" | "concluded"
|
||||
|
||||
interface ReasoningStreamItem {
|
||||
readonly encryptedContent: string | null | undefined
|
||||
// Keyed by OpenAI's numeric `summary_index`. JS object keys coerce to
|
||||
// strings, but typing the map as `Record<number, ...>` documents intent
|
||||
// and matches the wire field.
|
||||
readonly summaryParts: Readonly<Record<number, ReasoningSummaryStatus>>
|
||||
}
|
||||
|
||||
const invalid = ProviderShared.invalidRequest
|
||||
@@ -273,21 +220,22 @@ const lowerToolCall = (part: ToolCallPart): OpenAIResponsesInputItem => ({
|
||||
arguments: ProviderShared.encodeJson(part.input),
|
||||
})
|
||||
|
||||
const lowerReasoning = (part: ReasoningPart): OpenAIResponsesReasoningInput | undefined => {
|
||||
const lowerReasoning = (part: ReasoningPart, store: boolean | undefined): OpenAIResponsesInputItem | undefined => {
|
||||
const openai = part.providerMetadata?.openai
|
||||
if (!ProviderShared.isRecord(openai) || typeof openai.itemId !== "string" || openai.itemId.length === 0)
|
||||
return undefined
|
||||
const encryptedContent =
|
||||
typeof openai.reasoningEncryptedContent === "string"
|
||||
? openai.reasoningEncryptedContent
|
||||
: openai.reasoningEncryptedContent === null
|
||||
? null
|
||||
: undefined
|
||||
if (!ProviderShared.isRecord(openai) || typeof openai.itemId !== "string") return undefined
|
||||
// With store:false, OpenAI only accepts previous reasoning items when the
|
||||
// encrypted state is present. Bare rs_* ids point to non-persisted items.
|
||||
if (store === false && typeof openai.reasoningEncryptedContent !== "string") return undefined
|
||||
return {
|
||||
type: "reasoning",
|
||||
id: openai.itemId,
|
||||
summary: part.text.length > 0 ? [{ type: "summary_text", text: part.text }] : [],
|
||||
encrypted_content: encryptedContent,
|
||||
encrypted_content:
|
||||
typeof openai.reasoningEncryptedContent === "string"
|
||||
? openai.reasoningEncryptedContent
|
||||
: openai.reasoningEncryptedContent === null
|
||||
? null
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,27 +250,6 @@ const lowerUserContent = Effect.fn("OpenAIResponses.lowerUserContent")(function*
|
||||
return yield* ProviderShared.unsupportedContent("OpenAI Responses", "user", ["text", "media"])
|
||||
})
|
||||
|
||||
// Tool results may carry structured text/images. Keep media as provider-native
|
||||
// content instead of JSON-stringifying base64 into a prompt string.
|
||||
const lowerToolResultContentItem = Effect.fn("OpenAIResponses.lowerToolResultContentItem")(function* (
|
||||
item: ToolResultContentPart,
|
||||
) {
|
||||
if (item.type === "text") return { type: "input_text" as const, text: item.text }
|
||||
if (item.mediaType.startsWith("image/"))
|
||||
return {
|
||||
type: "input_image" as const,
|
||||
image_url: ProviderShared.mediaDataUrl(item),
|
||||
}
|
||||
return yield* invalid(`OpenAI Responses tool-result media content only supports images, got ${item.mediaType}`)
|
||||
})
|
||||
|
||||
const lowerToolResultOutput = Effect.fn("OpenAIResponses.lowerToolResultOutput")(function* (part: ToolResultPart) {
|
||||
// Text/json/error results are encoded as a plain string for backward
|
||||
// compatibility with existing cassettes and provider expectations.
|
||||
if (part.result.type !== "content") return ProviderShared.toolResultText(part)
|
||||
return yield* Effect.forEach(part.result.value, lowerToolResultContentItem)
|
||||
})
|
||||
|
||||
const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (request: LLMRequest) {
|
||||
const system: OpenAIResponsesInputItem[] =
|
||||
request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }]
|
||||
@@ -337,8 +264,6 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
|
||||
|
||||
if (message.role === "assistant") {
|
||||
const content: TextPart[] = []
|
||||
const reasoningItems: Record<string, OpenAIResponsesReasoningInput> = {}
|
||||
const reasoningReferences = new Set<string>()
|
||||
const flushText = () => {
|
||||
if (content.length === 0) return
|
||||
input.push({ role: "assistant", content: content.map((part) => ({ type: "output_text", text: part.text })) })
|
||||
@@ -351,22 +276,8 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
|
||||
}
|
||||
if (part.type === "reasoning") {
|
||||
flushText()
|
||||
const reasoning = lowerReasoning(part)
|
||||
if (!reasoning) continue
|
||||
if (store !== false && reasoning.id) {
|
||||
if (!reasoningReferences.has(reasoning.id)) input.push({ type: "item_reference", id: reasoning.id })
|
||||
reasoningReferences.add(reasoning.id)
|
||||
continue
|
||||
}
|
||||
const existing = reasoningItems[reasoning.id]
|
||||
if (existing) {
|
||||
existing.summary.push(...reasoning.summary)
|
||||
if (typeof reasoning.encrypted_content === "string")
|
||||
existing.encrypted_content = reasoning.encrypted_content
|
||||
continue
|
||||
}
|
||||
reasoningItems[reasoning.id] = reasoning
|
||||
input.push(reasoning)
|
||||
const reasoning = lowerReasoning(part, store)
|
||||
if (reasoning) input.push(reasoning)
|
||||
continue
|
||||
}
|
||||
if (part.type === "tool-call") {
|
||||
@@ -387,22 +298,11 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
|
||||
for (const part of message.content) {
|
||||
if (!ProviderShared.supportsContent(part, ["tool-result"]))
|
||||
return yield* ProviderShared.unsupportedContent("OpenAI Responses", "tool", ["tool-result"])
|
||||
input.push({
|
||||
type: "function_call_output",
|
||||
call_id: part.id,
|
||||
output: yield* lowerToolResultOutput(part),
|
||||
})
|
||||
input.push({ type: "function_call_output", call_id: part.id, output: ProviderShared.toolResultText(part) })
|
||||
}
|
||||
}
|
||||
|
||||
// With store:false, OpenAI only accepts previous reasoning items when the
|
||||
// complete item has encrypted state. Summary blocks for one item may carry
|
||||
// that state only on the last block, so filter after they have been joined.
|
||||
return store === false
|
||||
? input.filter(
|
||||
(item) => !("type" in item) || item.type !== "reasoning" || typeof item.encrypted_content === "string",
|
||||
)
|
||||
: input
|
||||
return input
|
||||
})
|
||||
|
||||
const lowerOptions = Effect.fn("OpenAIResponses.lowerOptions")(function* (request: LLMRequest) {
|
||||
@@ -412,14 +312,14 @@ const lowerOptions = Effect.fn("OpenAIResponses.lowerOptions")(function* (reques
|
||||
if (effort && !OpenAIOptions.isReasoningEffort(effort))
|
||||
return yield* invalid(`OpenAI Responses does not support reasoning effort ${effort}`)
|
||||
const summary = OpenAIOptions.reasoningSummary(request)
|
||||
const include = OpenAIOptions.include(request)
|
||||
const encryptedState = OpenAIOptions.encryptedReasoning(request)
|
||||
const verbosity = OpenAIOptions.textVerbosity(request)
|
||||
const instructions = OpenAIOptions.instructions(request)
|
||||
return {
|
||||
...(instructions ? { instructions } : {}),
|
||||
...(store !== undefined ? { store } : {}),
|
||||
...(promptCacheKey ? { prompt_cache_key: promptCacheKey } : {}),
|
||||
...(include ? { include } : {}),
|
||||
...(encryptedState ? { include: ["reasoning.encrypted_content"] as const } : {}),
|
||||
...(effort || summary ? { reasoning: { effort, summary } } : {}),
|
||||
...(verbosity ? { text: { verbosity } } : {}),
|
||||
}
|
||||
@@ -567,51 +467,24 @@ const onOutputTextDelta = (state: ParserState, event: OpenAIResponsesEvent): Ste
|
||||
const onReasoningDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
|
||||
if (!event.delta) return [state, NO_EVENTS]
|
||||
const events: LLMEvent[] = []
|
||||
const itemID = event.item_id ?? "reasoning-0"
|
||||
const id =
|
||||
event.summary_index !== undefined || state.reasoningItems[itemID] ? `${itemID}:${event.summary_index ?? 0}` : itemID
|
||||
return [
|
||||
{
|
||||
...state,
|
||||
lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, id, event.delta),
|
||||
lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, event.item_id ?? "reasoning-0", event.delta),
|
||||
},
|
||||
events,
|
||||
]
|
||||
}
|
||||
|
||||
// The summary done event does not carry encrypted continuation state. Finish the
|
||||
// common reasoning block when the full reasoning item arrives in output_item.done.
|
||||
const onReasoningDone = (state: ParserState, _event: OpenAIResponsesEvent): StepResult => [state, NO_EVENTS]
|
||||
|
||||
const reasoningMetadata = (item: OpenAIResponsesStreamItem & { id: string }) =>
|
||||
openaiMetadata({ itemId: item.id, reasoningEncryptedContent: item.encrypted_content ?? null })
|
||||
|
||||
// OpenAI Responses streams reasoning items in a stable order:
|
||||
// `output_item.added` (reasoning) →
|
||||
// `reasoning_summary_part.added` (index=0) →
|
||||
// `reasoning_summary_text.delta` →
|
||||
// `reasoning_summary_part.done` (index=0) →
|
||||
// (repeat for index>0) →
|
||||
// `output_item.done` (reasoning).
|
||||
// The handlers below rely on this ordering: `onOutputItemAdded` seeds the
|
||||
// per-item entry, `onReasoningSummaryPartAdded` for `summary_index === 0`
|
||||
// short-circuits when the entry already exists, and higher-index handlers
|
||||
// fold against the same entry. Behaviour for out-of-order events is
|
||||
// best-effort, not guaranteed.
|
||||
const onOutputItemAdded = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
|
||||
const item = event.item
|
||||
if (item && isReasoningItem(item)) {
|
||||
const events: LLMEvent[] = []
|
||||
return [
|
||||
{
|
||||
...state,
|
||||
lifecycle: Lifecycle.reasoningStart(state.lifecycle, events, `${item.id}:0`, reasoningMetadata(item)),
|
||||
reasoningItems: {
|
||||
...state.reasoningItems,
|
||||
[item.id]: { encryptedContent: item.encrypted_content, summaryParts: { 0: "active" } },
|
||||
},
|
||||
},
|
||||
events,
|
||||
]
|
||||
}
|
||||
if (item?.type !== "function_call" || !item.id) return [state, NO_EVENTS]
|
||||
const providerMetadata = openaiMetadata({ itemId: item.id })
|
||||
const events: LLMEvent[] = []
|
||||
@@ -632,103 +505,6 @@ const onOutputItemAdded = (state: ParserState, event: OpenAIResponsesEvent): Ste
|
||||
]
|
||||
}
|
||||
|
||||
const onReasoningSummaryPartAdded = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
|
||||
if (!event.item_id || event.summary_index === undefined) return [state, NO_EVENTS]
|
||||
const item = state.reasoningItems[event.item_id] ?? { encryptedContent: undefined, summaryParts: {} }
|
||||
if (event.summary_index === 0) {
|
||||
if (state.reasoningItems[event.item_id]) return [state, NO_EVENTS]
|
||||
const events: LLMEvent[] = []
|
||||
return [
|
||||
{
|
||||
...state,
|
||||
lifecycle: Lifecycle.reasoningStart(
|
||||
state.lifecycle,
|
||||
events,
|
||||
`${event.item_id}:0`,
|
||||
openaiMetadata({ itemId: event.item_id, reasoningEncryptedContent: null }),
|
||||
),
|
||||
reasoningItems: {
|
||||
...state.reasoningItems,
|
||||
[event.item_id]: { ...item, summaryParts: { 0: "active" } },
|
||||
},
|
||||
},
|
||||
events,
|
||||
]
|
||||
}
|
||||
|
||||
const events: LLMEvent[] = []
|
||||
const closed = Object.entries(item.summaryParts)
|
||||
.filter((entry) => entry[1] === "can-conclude")
|
||||
.reduce(
|
||||
(lifecycle, entry) =>
|
||||
Lifecycle.reasoningEnd(
|
||||
lifecycle,
|
||||
events,
|
||||
`${event.item_id}:${entry[0]}`,
|
||||
openaiMetadata({ itemId: event.item_id }),
|
||||
),
|
||||
state.lifecycle,
|
||||
)
|
||||
return [
|
||||
{
|
||||
...state,
|
||||
lifecycle: Lifecycle.reasoningStart(
|
||||
closed,
|
||||
events,
|
||||
`${event.item_id}:${event.summary_index}`,
|
||||
openaiMetadata({ itemId: event.item_id, reasoningEncryptedContent: item.encryptedContent ?? null }),
|
||||
),
|
||||
reasoningItems: {
|
||||
...state.reasoningItems,
|
||||
[event.item_id]: {
|
||||
...item,
|
||||
summaryParts: {
|
||||
...Object.fromEntries(
|
||||
Object.entries(item.summaryParts).map((entry) =>
|
||||
entry[1] === "can-conclude" ? [entry[0], "concluded" as const] : entry,
|
||||
),
|
||||
),
|
||||
[event.summary_index]: "active",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
events,
|
||||
]
|
||||
}
|
||||
|
||||
const onReasoningSummaryPartDone = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
|
||||
if (!event.item_id || event.summary_index === undefined) return [state, NO_EVENTS]
|
||||
const item = state.reasoningItems[event.item_id]
|
||||
if (!item) return [state, NO_EVENTS]
|
||||
const events: LLMEvent[] = []
|
||||
return [
|
||||
{
|
||||
...state,
|
||||
lifecycle:
|
||||
state.store !== false
|
||||
? Lifecycle.reasoningEnd(
|
||||
state.lifecycle,
|
||||
events,
|
||||
`${event.item_id}:${event.summary_index}`,
|
||||
openaiMetadata({ itemId: event.item_id }),
|
||||
)
|
||||
: state.lifecycle,
|
||||
reasoningItems: {
|
||||
...state.reasoningItems,
|
||||
[event.item_id]: {
|
||||
...item,
|
||||
summaryParts: {
|
||||
...item.summaryParts,
|
||||
[event.summary_index]: state.store !== false ? "concluded" : "can-conclude",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
events,
|
||||
]
|
||||
}
|
||||
|
||||
const onFunctionCallArgumentsDelta = Effect.fn("OpenAIResponses.onFunctionCallArgumentsDelta")(function* (
|
||||
state: ParserState,
|
||||
event: OpenAIResponsesEvent,
|
||||
@@ -789,17 +565,6 @@ const onOutputItemDone = Effect.fn("OpenAIResponses.onOutputItemDone")(function*
|
||||
if (isReasoningItem(item)) {
|
||||
const events: LLMEvent[] = []
|
||||
const providerMetadata = reasoningMetadata(item)
|
||||
const reasoningItem = state.reasoningItems[item.id]
|
||||
if (reasoningItem) {
|
||||
const lifecycle = Object.entries(reasoningItem.summaryParts)
|
||||
.filter((entry) => entry[1] === "active" || entry[1] === "can-conclude")
|
||||
.reduce(
|
||||
(lifecycle, entry) => Lifecycle.reasoningEnd(lifecycle, events, `${item.id}:${entry[0]}`, providerMetadata),
|
||||
state.lifecycle,
|
||||
)
|
||||
const { [item.id]: _removed, ...reasoningItems } = state.reasoningItems
|
||||
return [{ ...state, lifecycle, reasoningItems }, events] satisfies StepResult
|
||||
}
|
||||
if (!state.lifecycle.reasoning.has(item.id)) {
|
||||
const lifecycle = Lifecycle.stepStart(state.lifecycle, events)
|
||||
events.push(LLMEvent.reasoningStart({ id: item.id, providerMetadata }))
|
||||
@@ -831,27 +596,14 @@ const onResponseFinish = (state: ParserState, event: OpenAIResponsesEvent): Step
|
||||
return [{ ...state, lifecycle }, events]
|
||||
}
|
||||
|
||||
// Build a single human-readable message from whatever the provider supplied.
|
||||
// When both code and message are present, prefix the code so consumers see
|
||||
// the failure mode (e.g. `rate_limit_exceeded: Slow down`) instead of just
|
||||
// the bare message — production rate limits and context-length failures used
|
||||
// to be indistinguishable from generic stream drops.
|
||||
const providerErrorMessage = (event: OpenAIResponsesEvent, fallback: string): string => {
|
||||
const nested = event.response?.error ?? undefined
|
||||
const message = event.message || nested?.message || undefined
|
||||
const code = event.code || nested?.code || undefined
|
||||
if (message && code) return `${code}: ${message}`
|
||||
return message || code || fallback
|
||||
}
|
||||
|
||||
const onResponseFailed = (state: ParserState, event: OpenAIResponsesEvent): StepResult => [
|
||||
state,
|
||||
[LLMEvent.providerError({ message: providerErrorMessage(event, "OpenAI Responses response failed") })],
|
||||
[LLMEvent.providerError({ message: event.message ?? event.code ?? "OpenAI Responses response failed" })],
|
||||
]
|
||||
|
||||
const onError = (state: ParserState, event: OpenAIResponsesEvent): StepResult => [
|
||||
state,
|
||||
[LLMEvent.providerError({ message: providerErrorMessage(event, "OpenAI Responses stream error") })],
|
||||
[LLMEvent.providerError({ message: event.message ?? event.code ?? "OpenAI Responses stream error" })],
|
||||
]
|
||||
|
||||
const step = (state: ParserState, event: OpenAIResponsesEvent) => {
|
||||
@@ -868,10 +620,6 @@ const step = (state: ParserState, event: OpenAIResponsesEvent) => {
|
||||
event.type === "response.reasoning_summary_text.done"
|
||||
)
|
||||
return Effect.succeed(onReasoningDone(state, event))
|
||||
if (event.type === "response.reasoning_summary_part.added")
|
||||
return Effect.succeed(onReasoningSummaryPartAdded(state, event))
|
||||
if (event.type === "response.reasoning_summary_part.done")
|
||||
return Effect.succeed(onReasoningSummaryPartDone(state, event))
|
||||
if (event.type === "response.output_item.added") return Effect.succeed(onOutputItemAdded(state, event))
|
||||
if (event.type === "response.function_call_arguments.delta") return onFunctionCallArgumentsDelta(state, event)
|
||||
if (event.type === "response.output_item.done") return onOutputItemDone(state, event)
|
||||
@@ -898,13 +646,7 @@ export const protocol = Protocol.make({
|
||||
},
|
||||
stream: {
|
||||
event: Protocol.jsonEvent(OpenAIResponsesEvent),
|
||||
initial: (request) => ({
|
||||
hasFunctionCall: false,
|
||||
tools: ToolStream.empty<string>(),
|
||||
lifecycle: Lifecycle.initial(),
|
||||
reasoningItems: {},
|
||||
store: OpenAIOptions.store(request),
|
||||
}),
|
||||
initial: () => ({ hasFunctionCall: false, tools: ToolStream.empty<string>(), lifecycle: Lifecycle.initial() }),
|
||||
step,
|
||||
terminal: (event) => TERMINAL_TYPES.has(event.type),
|
||||
},
|
||||
|
||||
@@ -24,22 +24,14 @@ export const textDelta = (state: State, events: LLMEvent[], id: string, text: st
|
||||
return { ...stepped, text: new Set([...stepped.text, id]) }
|
||||
}
|
||||
|
||||
export const reasoningStart = (
|
||||
state: State,
|
||||
events: LLMEvent[],
|
||||
id: string,
|
||||
providerMetadata?: ProviderMetadata,
|
||||
): State => {
|
||||
if (state.reasoning.has(id)) return state
|
||||
const stepped = stepStart(state, events)
|
||||
events.push(LLMEvent.reasoningStart({ id, providerMetadata }))
|
||||
return { ...stepped, reasoning: new Set([...stepped.reasoning, id]) }
|
||||
}
|
||||
|
||||
export const reasoningDelta = (state: State, events: LLMEvent[], id: string, text: string): State => {
|
||||
const started = reasoningStart(state, events, id)
|
||||
events.push(LLMEvent.reasoningDelta({ id, text }))
|
||||
return started
|
||||
const stepped = stepStart(state, events)
|
||||
if (stepped.reasoning.has(id)) {
|
||||
events.push(LLMEvent.reasoningDelta({ id, text }))
|
||||
return stepped
|
||||
}
|
||||
events.push(LLMEvent.reasoningStart({ id }), LLMEvent.reasoningDelta({ id, text }))
|
||||
return { ...stepped, reasoning: new Set([...stepped.reasoning, id]) }
|
||||
}
|
||||
|
||||
export const reasoningEnd = (
|
||||
|
||||
@@ -7,28 +7,12 @@ export const OpenAIReasoningEfforts = ReasoningEfforts.filter(
|
||||
)
|
||||
export type OpenAIReasoningEffort = (typeof OpenAIReasoningEfforts)[number]
|
||||
|
||||
// Mirrors OpenAI's `ResponseIncludable` union from the official SDK. Keep this
|
||||
// in lockstep with `openai-node/src/resources/responses/responses.ts`.
|
||||
export const OpenAIResponseIncludables = [
|
||||
"file_search_call.results",
|
||||
"web_search_call.results",
|
||||
"web_search_call.action.sources",
|
||||
"message.input_image.image_url",
|
||||
"computer_call_output.output.image_url",
|
||||
"code_interpreter_call.outputs",
|
||||
"reasoning.encrypted_content",
|
||||
"message.output_text.logprobs",
|
||||
] as const
|
||||
export type OpenAIResponseIncludable = (typeof OpenAIResponseIncludables)[number]
|
||||
|
||||
const REASONING_EFFORTS = new Set<string>(ReasoningEfforts)
|
||||
const OPENAI_REASONING_EFFORTS = new Set<string>(OpenAIReasoningEfforts)
|
||||
const TEXT_VERBOSITY = new Set<string>(["low", "medium", "high"])
|
||||
const INCLUDABLES = new Set<string>(OpenAIResponseIncludables)
|
||||
|
||||
export const OpenAIReasoningEffort = Schema.Literals(OpenAIReasoningEfforts)
|
||||
export const OpenAITextVerbosity = TextVerbosity
|
||||
export const OpenAIResponseIncludable = Schema.Literals(OpenAIResponseIncludables)
|
||||
|
||||
const isAnyReasoningEffort = (effort: unknown): effort is ReasoningEffort =>
|
||||
typeof effort === "string" && REASONING_EFFORTS.has(effort)
|
||||
@@ -51,21 +35,13 @@ export const reasoningEffort = (request: LLMRequest): ReasoningEffort | undefine
|
||||
return isAnyReasoningEffort(value) ? value : undefined
|
||||
}
|
||||
|
||||
export const reasoningSummary = (request: LLMRequest): "auto" | undefined =>
|
||||
options(request)?.reasoningSummary === "auto" ? "auto" : undefined
|
||||
|
||||
// Resolve the OpenAI Responses `include` field. Filters out unknown
|
||||
// includable values defensively so a typo in upstream config drops the
|
||||
// invalid entry instead of poisoning the wire body. An empty array (either
|
||||
// passed directly or produced by filtering) is treated as "no include" and
|
||||
// returns undefined so the request body omits the field entirely.
|
||||
export const include = (request: LLMRequest): ReadonlyArray<OpenAIResponseIncludable> | undefined => {
|
||||
const value = options(request)?.include
|
||||
if (!Array.isArray(value)) return undefined
|
||||
const filtered = value.filter((entry): entry is OpenAIResponseIncludable => INCLUDABLES.has(entry))
|
||||
return filtered.length > 0 ? filtered : undefined
|
||||
export const reasoningSummary = (request: LLMRequest): "auto" | undefined => {
|
||||
return options(request)?.reasoningSummary === "auto" ? "auto" : undefined
|
||||
}
|
||||
|
||||
export const encryptedReasoning = (request: LLMRequest) =>
|
||||
options(request)?.includeEncryptedReasoning === true ? true : undefined
|
||||
|
||||
export const promptCacheKey = (request: LLMRequest) => {
|
||||
const value = options(request)?.promptCacheKey
|
||||
return typeof value === "string" ? value : undefined
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { ProviderOptions, ReasoningEffort, TextVerbosity } from "../schema"
|
||||
import { mergeProviderOptions } from "../schema"
|
||||
import type { OpenAIResponseIncludable } from "../protocols/utils/openai-options"
|
||||
|
||||
export type { OpenAIResponseIncludable } from "../protocols/utils/openai-options"
|
||||
|
||||
export interface OpenAIOptionsInput {
|
||||
readonly [key: string]: unknown
|
||||
@@ -10,10 +7,7 @@ export interface OpenAIOptionsInput {
|
||||
readonly promptCacheKey?: string
|
||||
readonly reasoningEffort?: ReasoningEffort
|
||||
readonly reasoningSummary?: "auto"
|
||||
// OpenAI Responses `include` wire field. Mirrors the official SDK's
|
||||
// `ResponseIncludable[]` union exactly so AI SDK callers and direct
|
||||
// native-SDK callers share one shape and no translation is required.
|
||||
readonly include?: ReadonlyArray<OpenAIResponseIncludable>
|
||||
readonly includeEncryptedReasoning?: boolean
|
||||
readonly textVerbosity?: TextVerbosity
|
||||
}
|
||||
|
||||
@@ -31,7 +25,7 @@ const openAIProviderOptions = (options: OpenAIOptionsInput | undefined): Provide
|
||||
promptCacheKey: options?.promptCacheKey,
|
||||
reasoningEffort: options?.reasoningEffort,
|
||||
reasoningSummary: options?.reasoningSummary,
|
||||
include: options?.include,
|
||||
includeEncryptedReasoning: options?.includeEncryptedReasoning,
|
||||
textVerbosity: options?.textVerbosity,
|
||||
}),
|
||||
)
|
||||
@@ -48,12 +42,6 @@ export const gpt5DefaultOptions = (
|
||||
return openAIProviderOptions({
|
||||
reasoningEffort: "medium",
|
||||
reasoningSummary: "auto",
|
||||
// GPT-5 reasoning models are configured stateless (`store: false`) by
|
||||
// `openAIDefaultOptions` below, so the only way a follow-up turn can
|
||||
// carry reasoning state is via the encrypted reasoning include. Without
|
||||
// this, callers using the default model facade get reasoning summaries
|
||||
// they cannot replay statelessly.
|
||||
include: ["reasoning.encrypted_content"],
|
||||
textVerbosity:
|
||||
options.textVerbosity === true && id.includes("gpt-5.") && !id.includes("codex") && !id.includes("-chat")
|
||||
? "low"
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as OpenAIChat from "../protocols/openai-chat"
|
||||
import * as OpenAIResponses from "../protocols/openai-responses"
|
||||
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
|
||||
|
||||
export type { OpenAIOptionsInput, OpenAIResponseIncludable } from "./openai-options"
|
||||
export type { OpenAIOptionsInput } from "./openai-options"
|
||||
|
||||
export const id = ProviderID.make("openai")
|
||||
|
||||
|
||||
@@ -283,7 +283,7 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
|
||||
)
|
||||
return events.pipe(
|
||||
Stream.mapAccumEffect(
|
||||
() => protocol.stream.initial(request),
|
||||
protocol.stream.initial,
|
||||
protocol.stream.step,
|
||||
protocol.stream.onHalt ? { onHalt: protocol.stream.onHalt } : undefined,
|
||||
),
|
||||
|
||||
@@ -52,8 +52,8 @@ export interface ProtocolBody<Body> {
|
||||
export interface ProtocolStream<Frame, Event, State> {
|
||||
/** Schema for one decoded streaming event, decoded from a transport frame. */
|
||||
readonly event: Schema.Codec<Event, Frame>
|
||||
/** Initial parser state. Called once per response with the resolved request. */
|
||||
readonly initial: (request: LLMRequest) => State
|
||||
/** Initial parser state. Called once per response. */
|
||||
readonly initial: () => State
|
||||
/** Translate one event into emitted `LLMEvent`s plus the next state. */
|
||||
readonly step: (state: State, event: Event) => Effect.Effect<readonly [State, ReadonlyArray<LLMEvent>], LLMError>
|
||||
/** Optional request-completion signal for transports that do not end naturally. */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Cause, Context, Effect, Layer, Queue, Stream } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { LLMError, TransportReason } from "../../schema"
|
||||
import { Auth } from "../auth"
|
||||
import { LLMError, TransportReason, type LLMRequest } from "../../schema"
|
||||
import * as HttpTransport from "./http"
|
||||
import type { Transport } from "./index"
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ export function continuationRequest(input: {
|
||||
tools: features.has("tool-call") ? [continuationTool] : [],
|
||||
cache: "none",
|
||||
providerOptions: features.has("encrypted-reasoning")
|
||||
? { openai: { store: false, include: ["reasoning.encrypted_content"], reasoningSummary: "auto" } }
|
||||
? { openai: { store: false, includeEncryptedReasoning: true, reasoningSummary: "auto" } }
|
||||
: undefined,
|
||||
generation: { maxTokens: 80, temperature: 0 },
|
||||
})
|
||||
|
||||
Vendored
-43
File diff suppressed because one or more lines are too long
-42
File diff suppressed because one or more lines are too long
+4
-4
File diff suppressed because one or more lines are too long
Vendored
+3
-3
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
|
||||
import { Stream } from "effect"
|
||||
import { LLMClient } from "../../src/route"
|
||||
import type { Tools } from "../../src/tool"
|
||||
import type { RunOptions } from "../../src/tool-runtime"
|
||||
|
||||
@@ -24,19 +24,6 @@ const request = LLM.request({
|
||||
generation: { maxTokens: 20, temperature: 0 },
|
||||
})
|
||||
|
||||
type AnthropicToolResult = Extract<
|
||||
AnthropicMessages.AnthropicMessagesBody["messages"][number]["content"][number],
|
||||
{ readonly type: "tool_result" }
|
||||
>
|
||||
|
||||
const expectToolResult = (body: AnthropicMessages.AnthropicMessagesBody): AnthropicToolResult => {
|
||||
const result = body.messages
|
||||
.flatMap((message) => (message.role === "user" ? message.content : []))
|
||||
.find((block): block is AnthropicToolResult => block.type === "tool_result")
|
||||
expect(result).toBeDefined()
|
||||
return result!
|
||||
}
|
||||
|
||||
describe("Anthropic Messages route", () => {
|
||||
it.effect("prepares Anthropic Messages target", () =>
|
||||
Effect.gen(function* () {
|
||||
@@ -84,87 +71,6 @@ describe("Anthropic Messages route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
// Regression: screenshot/read tool results must stay structured so base64
|
||||
// image data is not JSON-stringified into `tool_result.content`.
|
||||
it.effect("lowers image tool-result content as structured image blocks", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.request({
|
||||
id: "req_tool_result_image",
|
||||
model,
|
||||
messages: [
|
||||
Message.user("Show me the screenshot."),
|
||||
Message.assistant([ToolCallPart.make({ id: "call_1", name: "read", input: { filePath: "shot.png" } })]),
|
||||
Message.tool({
|
||||
id: "call_1",
|
||||
name: "read",
|
||||
resultType: "content",
|
||||
result: [
|
||||
{ type: "text", text: "Image read successfully" },
|
||||
{ type: "media", mediaType: "image/png", data: "AAECAw==" },
|
||||
],
|
||||
}),
|
||||
],
|
||||
cache: "none",
|
||||
}),
|
||||
)
|
||||
|
||||
expect(expectToolResult(prepared.body).content).toEqual([
|
||||
{ type: "text", text: "Image read successfully" },
|
||||
{ type: "image", source: { type: "base64", media_type: "image/png", data: "AAECAw==" } },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("lowers single-image tool-result content as a structured image block", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.request({
|
||||
id: "req_tool_result_image_only",
|
||||
model,
|
||||
messages: [
|
||||
Message.assistant([ToolCallPart.make({ id: "call_1", name: "screenshot", input: {} })]),
|
||||
Message.tool({
|
||||
id: "call_1",
|
||||
name: "screenshot",
|
||||
resultType: "content",
|
||||
result: [{ type: "media", mediaType: "image/jpeg", data: "/9j/AA==" }],
|
||||
}),
|
||||
],
|
||||
cache: "none",
|
||||
}),
|
||||
)
|
||||
|
||||
expect(expectToolResult(prepared.body).content).toEqual([
|
||||
{ type: "image", source: { type: "base64", media_type: "image/jpeg", data: "/9j/AA==" } },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects non-image media in tool-result content with a clear error", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_tool_result_unsupported_media",
|
||||
model,
|
||||
messages: [
|
||||
Message.assistant([ToolCallPart.make({ id: "call_1", name: "fetch", input: {} })]),
|
||||
Message.tool({
|
||||
id: "call_1",
|
||||
name: "fetch",
|
||||
resultType: "content",
|
||||
result: [{ type: "media", mediaType: "audio/mpeg", data: "AAECAw==" }],
|
||||
}),
|
||||
],
|
||||
cache: "none",
|
||||
}),
|
||||
).pipe(Effect.flip)
|
||||
|
||||
expect(error.message).toContain("Anthropic Messages")
|
||||
expect(error.message).toContain("audio/mpeg")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("prepares the composed native continuation request", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
@@ -337,29 +243,7 @@ describe("Anthropic Messages route", () => {
|
||||
),
|
||||
)
|
||||
|
||||
// Prefix the error type so consumers can distinguish overloads, rate
|
||||
// limits, and quota errors without parsing the message string.
|
||||
expect(response.events).toEqual([{ type: "provider-error", message: "overloaded_error: Overloaded" }])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("falls back to error type when no message is present", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(request).pipe(
|
||||
Effect.provide(fixedResponse(sseEvents({ type: "error", error: { type: "overloaded_error", message: "" } }))),
|
||||
)
|
||||
|
||||
expect(response.events).toEqual([{ type: "provider-error", message: "overloaded_error" }])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("falls back to a stable default when error payload is absent", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(request).pipe(
|
||||
Effect.provide(fixedResponse(sseEvents({ type: "error" }))),
|
||||
)
|
||||
|
||||
expect(response.events).toEqual([{ type: "provider-error", message: "Anthropic Messages stream error" }])
|
||||
expect(response.events).toEqual([{ type: "provider-error", message: "Overloaded" }])
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Effect } from "effect"
|
||||
import { CacheHint, LLM, Message, ToolCallPart, ToolChoice } from "../../src"
|
||||
import { LLMClient } from "../../src/route"
|
||||
import { AmazonBedrock } from "../../src/providers"
|
||||
import * as BedrockConverse from "../../src/protocols/bedrock-converse"
|
||||
import { it } from "../lib/effect"
|
||||
import { fixedResponse } from "../lib/http"
|
||||
import {
|
||||
|
||||
@@ -87,7 +87,6 @@ describeRecordedGoldenScenarios([
|
||||
{ id: "reasoning-continuation", temperature: false },
|
||||
{ id: "tool-call", temperature: false },
|
||||
{ id: "tool-loop", temperature: false },
|
||||
{ id: "image-tool-result", temperature: false, maxTokens: 40 },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -113,10 +112,7 @@ describeRecordedGoldenScenarios([
|
||||
requires: ["ANTHROPIC_API_KEY"],
|
||||
tags: ["flagship"],
|
||||
options: { redactor: Redactor.defaults({ requestHeaders: { allow: ["content-type", "anthropic-version"] } }) },
|
||||
scenarios: [
|
||||
{ id: "tool-loop", temperature: false },
|
||||
{ id: "image-tool-result", temperature: false, maxTokens: 40 },
|
||||
],
|
||||
scenarios: [{ id: "tool-loop", temperature: false }],
|
||||
},
|
||||
{
|
||||
name: "Gemini 2.5 Flash",
|
||||
|
||||
@@ -26,19 +26,6 @@ const request = LLM.request({
|
||||
|
||||
const configEnv = (env: Record<string, string>) => Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env })))
|
||||
|
||||
type OpenAIToolOutput = Extract<
|
||||
OpenAIResponses.OpenAIResponsesBody["input"][number],
|
||||
{ readonly type: "function_call_output" }
|
||||
>
|
||||
|
||||
const expectToolOutput = (body: OpenAIResponses.OpenAIResponsesBody): OpenAIToolOutput => {
|
||||
const output = body.input.find(
|
||||
(item): item is OpenAIToolOutput => "type" in item && item.type === "function_call_output",
|
||||
)
|
||||
expect(output).toBeDefined()
|
||||
return output!
|
||||
}
|
||||
|
||||
describe("OpenAI Responses route", () => {
|
||||
it.effect("prepares OpenAI Responses target", () =>
|
||||
Effect.gen(function* () {
|
||||
@@ -261,84 +248,6 @@ describe("OpenAI Responses route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
// Regression: screenshot/read tool results must stay structured so base64
|
||||
// image data is not JSON-stringified into `function_call_output.output`.
|
||||
it.effect("lowers image tool-result content as structured input_image items", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
id: "req_tool_result_image",
|
||||
model,
|
||||
messages: [
|
||||
Message.user("Show me the screenshot."),
|
||||
Message.assistant([ToolCallPart.make({ id: "call_1", name: "read", input: { filePath: "shot.png" } })]),
|
||||
Message.tool({
|
||||
id: "call_1",
|
||||
name: "read",
|
||||
resultType: "content",
|
||||
result: [
|
||||
{ type: "text", text: "Image read successfully" },
|
||||
{ type: "media", mediaType: "image/png", data: "AAECAw==" },
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(expectToolOutput(prepared.body).output).toEqual([
|
||||
{ type: "input_text", text: "Image read successfully" },
|
||||
{ type: "input_image", image_url: "data:image/png;base64,AAECAw==" },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("lowers single-image tool-result content as structured input_image array", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
id: "req_tool_result_image_only",
|
||||
model,
|
||||
messages: [
|
||||
Message.assistant([ToolCallPart.make({ id: "call_1", name: "screenshot", input: {} })]),
|
||||
Message.tool({
|
||||
id: "call_1",
|
||||
name: "screenshot",
|
||||
resultType: "content",
|
||||
result: [{ type: "media", mediaType: "image/png", data: "AAECAw==" }],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(expectToolOutput(prepared.body).output).toEqual([
|
||||
{ type: "input_image", image_url: "data:image/png;base64,AAECAw==" },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects non-image media in tool-result content with a clear error", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_tool_result_unsupported_media",
|
||||
model,
|
||||
messages: [
|
||||
Message.assistant([ToolCallPart.make({ id: "call_1", name: "fetch", input: {} })]),
|
||||
Message.tool({
|
||||
id: "call_1",
|
||||
name: "fetch",
|
||||
resultType: "content",
|
||||
result: [{ type: "media", mediaType: "audio/mpeg", data: "AAECAw==" }],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
).pipe(Effect.flip)
|
||||
|
||||
expect(error.message).toContain("OpenAI Responses")
|
||||
expect(error.message).toContain("audio/mpeg")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("prepares the composed native continuation request", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
@@ -393,7 +302,7 @@ describe("OpenAI Responses route", () => {
|
||||
promptCacheKey: "session_123",
|
||||
reasoningEffort: "high",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
includeEncryptedReasoning: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -407,108 +316,6 @@ describe("OpenAI Responses route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("accepts the full ResponseIncludable union", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "hi",
|
||||
providerOptions: {
|
||||
openai: {
|
||||
include: ["reasoning.encrypted_content", "code_interpreter_call.outputs", "web_search_call.results"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.include).toEqual([
|
||||
"reasoning.encrypted_content",
|
||||
"code_interpreter_call.outputs",
|
||||
"web_search_call.results",
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("filters unknown includable values out of the include array", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "hi",
|
||||
// The user passed one invalid entry alongside a valid one. Keep the
|
||||
// valid one so the request still succeeds rather than failing on a
|
||||
// typo from upstream config.
|
||||
providerOptions: { openai: { include: ["reasoning.encrypted_content", "bogus.thing"] } },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.include).toEqual(["reasoning.encrypted_content"])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("treats an explicit empty include as no include at all", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({ model, prompt: "hi", providerOptions: { openai: { include: [] } } }),
|
||||
)
|
||||
|
||||
expect(prepared.body.include).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("treats an all-invalid include as no include at all", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({ model, prompt: "hi", providerOptions: { openai: { include: ["bogus.thing"] } } }),
|
||||
)
|
||||
|
||||
expect(prepared.body.include).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("omits include when no include is set", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({ model, prompt: "hi", providerOptions: { openai: { store: false } } }),
|
||||
)
|
||||
|
||||
expect(prepared.body.include).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("requests encrypted reasoning by default for GPT-5 reasoning models", () =>
|
||||
Effect.gen(function* () {
|
||||
// The native OpenAI facade configures GPT-5 stateless (store: false) with
|
||||
// reasoningSummary: "auto" by default. Without `include`, a follow-up
|
||||
// turn cannot replay reasoning state, so the facade also opts into
|
||||
// `reasoning.encrypted_content` automatically.
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responses("gpt-5.2"),
|
||||
prompt: "hi",
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.store).toBe(false)
|
||||
expect(prepared.body.include).toEqual(["reasoning.encrypted_content"])
|
||||
expect(prepared.body.reasoning).toEqual({ effort: "medium", summary: "auto" })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("lets callers opt out of the GPT-5 default include", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responses("gpt-5.2"),
|
||||
prompt: "hi",
|
||||
providerOptions: { openai: { include: [] } },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.include).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("request OpenAI provider options override route defaults", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
@@ -649,94 +456,6 @@ describe("OpenAI Responses route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("streams each reasoning summary part as a separate block", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(
|
||||
LLM.updateRequest(request, { providerOptions: { openai: { store: false } } }),
|
||||
).pipe(
|
||||
Effect.provide(
|
||||
fixedResponse(
|
||||
sseEvents(
|
||||
{
|
||||
type: "response.output_item.added",
|
||||
item: { type: "reasoning", id: "rs_1", encrypted_content: null },
|
||||
},
|
||||
{ type: "response.reasoning_summary_part.added", item_id: "rs_1", summary_index: 0 },
|
||||
{ type: "response.reasoning_summary_text.delta", item_id: "rs_1", summary_index: 0, delta: "First" },
|
||||
{ type: "response.reasoning_summary_part.done", item_id: "rs_1", summary_index: 0 },
|
||||
{ type: "response.reasoning_summary_part.added", item_id: "rs_1", summary_index: 1 },
|
||||
{ type: "response.reasoning_summary_text.delta", item_id: "rs_1", summary_index: 1, delta: "Second" },
|
||||
{ type: "response.reasoning_summary_part.done", item_id: "rs_1", summary_index: 1 },
|
||||
{
|
||||
type: "response.output_item.done",
|
||||
item: { type: "reasoning", id: "rs_1", encrypted_content: "encrypted-state" },
|
||||
},
|
||||
{ type: "response.completed", response: { id: "resp_1" } },
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
expect(response.reasoning).toBe("FirstSecond")
|
||||
expect(response.events).toMatchObject([
|
||||
{ type: "step-start", index: 0 },
|
||||
{
|
||||
type: "reasoning-start",
|
||||
id: "rs_1:0",
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
},
|
||||
{ type: "reasoning-delta", id: "rs_1:0", text: "First" },
|
||||
{ type: "reasoning-end", id: "rs_1:0", providerMetadata: { openai: { itemId: "rs_1" } } },
|
||||
{
|
||||
type: "reasoning-start",
|
||||
id: "rs_1:1",
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
},
|
||||
{ type: "reasoning-delta", id: "rs_1:1", text: "Second" },
|
||||
{
|
||||
type: "reasoning-end",
|
||||
id: "rs_1:1",
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
},
|
||||
{ type: "step-finish", index: 0, reason: "stop" },
|
||||
{ type: "finish", reason: "stop" },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("closes reasoning summary parts when storage is not disabled", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(request).pipe(
|
||||
Effect.provide(
|
||||
fixedResponse(
|
||||
sseEvents(
|
||||
{
|
||||
type: "response.output_item.added",
|
||||
item: { type: "reasoning", id: "rs_1", encrypted_content: null },
|
||||
},
|
||||
{ type: "response.reasoning_summary_part.added", item_id: "rs_1", summary_index: 0 },
|
||||
{ type: "response.reasoning_summary_text.delta", item_id: "rs_1", summary_index: 0, delta: "First" },
|
||||
{ type: "response.reasoning_summary_part.done", item_id: "rs_1", summary_index: 0 },
|
||||
{ type: "response.reasoning_summary_part.added", item_id: "rs_1", summary_index: 1 },
|
||||
{ type: "response.reasoning_summary_text.delta", item_id: "rs_1", summary_index: 1, delta: "Second" },
|
||||
{ type: "response.reasoning_summary_part.done", item_id: "rs_1", summary_index: 1 },
|
||||
{
|
||||
type: "response.output_item.done",
|
||||
item: { type: "reasoning", id: "rs_1", encrypted_content: null },
|
||||
},
|
||||
{ type: "response.completed", response: { id: "resp_1" } },
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
expect(response.events.filter((event) => event.type === "reasoning-end")).toEqual([
|
||||
{ type: "reasoning-end", id: "rs_1:0", providerMetadata: { openai: { itemId: "rs_1" } } },
|
||||
{ type: "reasoning-end", id: "rs_1:1", providerMetadata: { openai: { itemId: "rs_1" } } },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("continues a stateless reasoning conversation", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(
|
||||
@@ -760,7 +479,6 @@ describe("OpenAI Responses route", () => {
|
||||
]),
|
||||
Message.user("Summarize it."),
|
||||
],
|
||||
providerOptions: { openai: { store: false } },
|
||||
}),
|
||||
).pipe(
|
||||
Effect.provide(
|
||||
@@ -818,7 +536,6 @@ describe("OpenAI Responses route", () => {
|
||||
{ type: "text", text: "After." },
|
||||
]),
|
||||
],
|
||||
providerOptions: { openai: { store: false } },
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -835,66 +552,6 @@ describe("OpenAI Responses route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("references stored reasoning items by id", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
Message.assistant([
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Checked the previous diff.",
|
||||
providerMetadata: { openai: { itemId: "rs_1" } },
|
||||
},
|
||||
]),
|
||||
],
|
||||
providerOptions: { openai: { store: true } },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.input).toEqual([{ type: "item_reference", id: "rs_1" }])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("joins streamed summary blocks into one continuation reasoning item", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
id: "req_multi_summary_continuation",
|
||||
model,
|
||||
messages: [
|
||||
Message.assistant([
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "First",
|
||||
providerMetadata: { openai: { itemId: "rs_1" } },
|
||||
},
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Second",
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
},
|
||||
]),
|
||||
],
|
||||
providerOptions: { openai: { store: false } },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.input).toEqual([
|
||||
{
|
||||
type: "reasoning",
|
||||
id: "rs_1",
|
||||
encrypted_content: "encrypted-state",
|
||||
summary: [
|
||||
{ type: "summary_text", text: "First" },
|
||||
{ type: "summary_text", text: "Second" },
|
||||
],
|
||||
},
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("skips non-persisted reasoning ids without encrypted state", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
@@ -1129,11 +786,7 @@ describe("OpenAI Responses route", () => {
|
||||
Effect.provide(fixedResponse(sseEvents({ type: "error", code: "rate_limit_exceeded", message: "Slow down" }))),
|
||||
)
|
||||
|
||||
// Prefix the code so consumers see the failure mode, not just the
|
||||
// sometimes-generic provider message. The bare message alone meant
|
||||
// production errors like rate limits were indistinguishable from
|
||||
// unrelated stream failures.
|
||||
expect(response.events).toEqual([{ type: "provider-error", message: "rate_limit_exceeded: Slow down" }])
|
||||
expect(response.events).toEqual([{ type: "provider-error", message: "Slow down" }])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1147,99 +800,6 @@ describe("OpenAI Responses route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("falls back to error code when message is empty", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(request).pipe(
|
||||
Effect.provide(fixedResponse(sseEvents({ type: "error", code: "internal_error", message: "" }))),
|
||||
)
|
||||
|
||||
expect(response.events).toEqual([{ type: "provider-error", message: "internal_error" }])
|
||||
}),
|
||||
)
|
||||
|
||||
// Regression: `response.failed` carries the failure details under
|
||||
// `response.error`, not at the top level. The previous handler only
|
||||
// checked top-level `message`/`code` and so always emitted the bare
|
||||
// "OpenAI Responses response failed" string, hiding the real cause.
|
||||
it.effect("surfaces response.failed details from response.error", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(request).pipe(
|
||||
Effect.provide(
|
||||
fixedResponse(
|
||||
sseEvents({
|
||||
type: "response.failed",
|
||||
response: {
|
||||
id: "resp_failed_1",
|
||||
error: { code: "server_error", message: "Upstream model unavailable" },
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
expect(response.events).toEqual([{ type: "provider-error", message: "server_error: Upstream model unavailable" }])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("surfaces response.failed code when no nested message is present", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(request).pipe(
|
||||
Effect.provide(
|
||||
fixedResponse(
|
||||
sseEvents({
|
||||
type: "response.failed",
|
||||
response: { id: "resp_failed_2", error: { code: "invalid_prompt" } },
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
expect(response.events).toEqual([{ type: "provider-error", message: "invalid_prompt" }])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("surfaces error event details even when they arrive nested under response.error", () =>
|
||||
Effect.gen(function* () {
|
||||
// Some OpenAI-compatible proxies and older SDK versions wrap the
|
||||
// top-level error fields into a nested `response.error` payload
|
||||
// when they bubble up an HTTP error as an SSE `error` event. Honour
|
||||
// both shapes so the user still sees the underlying cause instead
|
||||
// of the catch-all string.
|
||||
const response = yield* LLMClient.generate(request).pipe(
|
||||
Effect.provide(
|
||||
fixedResponse(
|
||||
sseEvents({
|
||||
type: "error",
|
||||
response: { error: { code: "context_length_exceeded", message: "prompt too long" } },
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
expect(response.events).toEqual([{ type: "provider-error", message: "context_length_exceeded: prompt too long" }])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("falls back to a stable default when both error and response are absent", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(request).pipe(
|
||||
Effect.provide(fixedResponse(sseEvents({ type: "error" }))),
|
||||
)
|
||||
|
||||
expect(response.events).toEqual([{ type: "provider-error", message: "OpenAI Responses stream error" }])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("falls back to a stable default when response.failed has no error payload", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(request).pipe(
|
||||
Effect.provide(fixedResponse(sseEvents({ type: "response.failed", response: { id: "resp_failed_3" } }))),
|
||||
)
|
||||
|
||||
expect(response.events).toEqual([{ type: "provider-error", message: "OpenAI Responses response failed" }])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("fails HTTP provider errors before stream parsing", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLMClient.generate(request).pipe(
|
||||
|
||||
@@ -158,7 +158,7 @@ const normalizeImageText = (value: string) =>
|
||||
const encryptedReasoningOptions = {
|
||||
openai: {
|
||||
store: false,
|
||||
include: ["reasoning.encrypted_content"],
|
||||
includeEncryptedReasoning: true,
|
||||
reasoningEffort: "low",
|
||||
reasoningSummary: "auto",
|
||||
},
|
||||
@@ -317,47 +317,6 @@ const runImageScenario = (context: GoldenScenarioContext) =>
|
||||
])
|
||||
})
|
||||
|
||||
// Reproduces a tool-result image round trip: a tool returns image bytes, and
|
||||
// the next model turn must receive provider-native image content instead of a
|
||||
// JSON-stringified base64 blob.
|
||||
const screenshotToolName = "read_screenshot"
|
||||
const runImageToolResultScenario = (context: GoldenScenarioContext) =>
|
||||
Effect.gen(function* () {
|
||||
const image = yield* restroomImage()
|
||||
const response = yield* generate(
|
||||
LLM.request({
|
||||
id: `${context.id}_image_tool_result`,
|
||||
model: context.model,
|
||||
system: "Read images carefully. Reply only with the visible text, lowercase, no punctuation.",
|
||||
cache: "none",
|
||||
generation: generation(context, context.maxTokens ?? 40),
|
||||
messages: [
|
||||
Message.user("Use the read_screenshot tool, then reply with the words shown."),
|
||||
Message.assistant([{ type: "tool-call", id: "call_screenshot_1", name: screenshotToolName, input: {} }]),
|
||||
Message.tool({
|
||||
id: "call_screenshot_1",
|
||||
name: screenshotToolName,
|
||||
resultType: "content",
|
||||
result: [
|
||||
{ type: "text", text: "Image read successfully" },
|
||||
{ type: "media", mediaType: "image/png", data: image },
|
||||
],
|
||||
}),
|
||||
],
|
||||
tools: [
|
||||
ToolDefinition.make({
|
||||
name: screenshotToolName,
|
||||
description: "Capture a screenshot of the current screen.",
|
||||
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
expectFinish(response.events, "stop")
|
||||
expect(normalizeImageText(response.text)).toBe(RESTROOM_IMAGE_TEXT)
|
||||
})
|
||||
|
||||
const runReasoningScenario = (context: GoldenScenarioContext) =>
|
||||
runGeneratedConversation(context, [
|
||||
user("Think briefly, then reply exactly with: Hello!"),
|
||||
@@ -400,11 +359,6 @@ const goldenScenarios = {
|
||||
"tool-call": { title: "streams tool call", tags: ["tool", "tool-call", "golden"], run: runToolCallScenario },
|
||||
"tool-loop": { title: "drives a tool loop", tags: ["tool", "tool-loop", "golden"], run: runToolLoopScenario },
|
||||
image: { title: "reads image text", tags: ["media", "image", "vision", "golden"], run: runImageScenario },
|
||||
"image-tool-result": {
|
||||
title: "reads image returned from tool result",
|
||||
tags: ["media", "image", "vision", "tool", "tool-result", "golden"],
|
||||
run: runImageToolResultScenario,
|
||||
},
|
||||
reasoning: { title: "uses reasoning", tags: ["reasoning", "golden"], run: runReasoningScenario },
|
||||
"reasoning-continuation": {
|
||||
title: "continues encrypted reasoning",
|
||||
|
||||
@@ -4,7 +4,6 @@ import { GenerationOptions, LLM, LLMEvent, LLMRequest, LLMResponse, ToolChoice }
|
||||
import { Auth, LLMClient } from "../src/route"
|
||||
import * as AnthropicMessages from "../src/protocols/anthropic-messages"
|
||||
import * as OpenAIChat from "../src/protocols/openai-chat"
|
||||
import * as OpenAIResponses from "../src/protocols/openai-responses"
|
||||
import { tool, ToolFailure, type ToolExecuteContext } from "../src/tool"
|
||||
import { ToolRuntime } from "../src/tool-runtime"
|
||||
import { it } from "./lib/effect"
|
||||
@@ -310,80 +309,6 @@ describe("LLMClient tools", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("replays encrypted OpenAI reasoning items with tool outputs", () =>
|
||||
Effect.gen(function* () {
|
||||
const bodies: unknown[] = []
|
||||
const layer = dynamicResponse((input) =>
|
||||
Effect.sync(() => {
|
||||
bodies.push(decodeJson(input.text))
|
||||
return input.respond(
|
||||
bodies.length === 1
|
||||
? sseEvents(
|
||||
{
|
||||
type: "response.output_item.added",
|
||||
item: { type: "reasoning", id: "rs_1", encrypted_content: null },
|
||||
},
|
||||
{ type: "response.reasoning_summary_part.added", item_id: "rs_1", summary_index: 0 },
|
||||
{ type: "response.reasoning_summary_part.done", item_id: "rs_1", summary_index: 0 },
|
||||
{
|
||||
type: "response.output_item.done",
|
||||
item: { type: "reasoning", id: "rs_1", encrypted_content: "encrypted-state" },
|
||||
},
|
||||
{
|
||||
type: "response.output_item.added",
|
||||
item: {
|
||||
type: "function_call",
|
||||
id: "item_1",
|
||||
call_id: "call_1",
|
||||
name: "get_weather",
|
||||
arguments: "",
|
||||
},
|
||||
},
|
||||
{ type: "response.function_call_arguments.delta", item_id: "item_1", delta: '{"city":"Paris"}' },
|
||||
{
|
||||
type: "response.output_item.done",
|
||||
item: {
|
||||
type: "function_call",
|
||||
id: "item_1",
|
||||
call_id: "call_1",
|
||||
name: "get_weather",
|
||||
arguments: '{"city":"Paris"}',
|
||||
},
|
||||
},
|
||||
{ type: "response.completed", response: {} },
|
||||
)
|
||||
: sseEvents(
|
||||
{ type: "response.output_text.delta", item_id: "msg_1", delta: "Done." },
|
||||
{ type: "response.completed", response: {} },
|
||||
),
|
||||
{ headers: { "content-type": "text/event-stream" } },
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
yield* TestToolRuntime.runTools({
|
||||
request: LLM.request({
|
||||
model: OpenAIResponses.route
|
||||
.with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") })
|
||||
.model({ id: "gpt-5.5" }),
|
||||
prompt: "Use the tool.",
|
||||
providerOptions: { openai: { store: false, include: ["reasoning.encrypted_content"] } },
|
||||
}),
|
||||
tools: { get_weather },
|
||||
}).pipe(Stream.runCollect, Effect.provide(layer))
|
||||
|
||||
expect(bodies[1]).toMatchObject({
|
||||
include: ["reasoning.encrypted_content"],
|
||||
input: [
|
||||
{ role: "user" },
|
||||
{ type: "reasoning", id: "rs_1", summary: [], encrypted_content: "encrypted-state" },
|
||||
{ type: "function_call", call_id: "call_1", name: "get_weather" },
|
||||
{ type: "function_call_output", call_id: "call_1" },
|
||||
],
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("emits tool-error for unknown tools so the model can self-correct", () =>
|
||||
Effect.gen(function* () {
|
||||
const layer = scriptedResponses([
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.15.10",
|
||||
"version": "1.15.7",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -74,15 +74,15 @@
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.21.0",
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.107",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.96",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
"@ai-sdk/azure": "3.0.49",
|
||||
"@ai-sdk/cerebras": "2.0.41",
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/deepinfra": "2.0.41",
|
||||
"@ai-sdk/gateway": "3.0.104",
|
||||
"@ai-sdk/google": "3.0.75",
|
||||
"@ai-sdk/google-vertex": "4.0.131",
|
||||
"@ai-sdk/google": "3.0.63",
|
||||
"@ai-sdk/google-vertex": "4.0.112",
|
||||
"@ai-sdk/groq": "3.0.31",
|
||||
"@ai-sdk/mistral": "3.0.27",
|
||||
"@ai-sdk/openai": "3.0.53",
|
||||
@@ -159,7 +159,7 @@
|
||||
"tree-sitter-powershell": "0.25.10",
|
||||
"turndown": "7.2.0",
|
||||
"ulid": "catalog:",
|
||||
"venice-ai-sdk-provider": "2.0.2",
|
||||
"venice-ai-sdk-provider": "2.0.1",
|
||||
"vscode-jsonrpc": "8.2.1",
|
||||
"web-tree-sitter": "0.25.10",
|
||||
"which": "6.0.1",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, Context } from "effect"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { serviceUse } from "@/effect/service-use"
|
||||
import {
|
||||
FetchHttpClient,
|
||||
HttpClient,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { eq } from "drizzle-orm"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { serviceUse } from "@/effect/service-use"
|
||||
import { Effect, Layer, Option, Schema, Context } from "effect"
|
||||
|
||||
import { Database } from "@/storage/db"
|
||||
|
||||
@@ -42,7 +42,9 @@ import type { ACPConfig } from "./types"
|
||||
import { ACPRuntime } from "./runtime"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ModelID, ProviderID } from "../provider/schema"
|
||||
import { Installation } from "@/installation"
|
||||
import { MessageV2 } from "@/session/message-v2"
|
||||
import { Config } from "@/config/config"
|
||||
import { ConfigMCP } from "@/config/mcp"
|
||||
import { Todo } from "@/session/todo"
|
||||
import { Result, Schema } from "effect"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Config } from "@/config/config"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { serviceUse } from "@/effect/service-use"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ModelID, ProviderID } from "../provider/schema"
|
||||
import { generateObject, streamObject, type ModelMessage } from "ai"
|
||||
@@ -69,7 +69,7 @@ export interface Interface {
|
||||
whenToUse: string
|
||||
systemPrompt: string
|
||||
},
|
||||
Provider.DefaultModelError
|
||||
Provider.ModelNotFoundError
|
||||
>
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BusEvent } from "./bus-event"
|
||||
import { GlobalBus } from "./global"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { serviceUse } from "@/effect/service-use"
|
||||
import { Identifier } from "@/id/id"
|
||||
import type { InstanceContext } from "@/project/instance-context"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EOL } from "os"
|
||||
import { basename } from "path"
|
||||
import { Cause, Effect } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Agent } from "../../../agent/agent"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { Session } from "@/session/session"
|
||||
@@ -80,21 +80,7 @@ const run = Effect.fn("Cli.debug.agent.body")(function* (
|
||||
const getAvailableTools = Effect.fn("Cli.debug.agent.getAvailableTools")(function* (agent: Agent.Info) {
|
||||
const provider = yield* Provider.Service
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const model =
|
||||
agent.model ??
|
||||
(yield* provider.defaultModel().pipe(
|
||||
Effect.matchCauseEffect({
|
||||
onSuccess: Effect.succeed,
|
||||
onFailure: (cause) => {
|
||||
const error = Cause.squash(cause) as Provider.DefaultModelError
|
||||
if (error instanceof Provider.ModelNotFoundError) {
|
||||
return fail(`Model not found: ${error.providerID}/${error.modelID}`)
|
||||
}
|
||||
if (error instanceof Provider.NoModelsError) return fail(`No models found for provider ${error.providerID}`)
|
||||
return fail("No providers found")
|
||||
},
|
||||
}),
|
||||
))
|
||||
const model = agent.model ?? (yield* provider.defaultModel())
|
||||
return yield* registry.tools({ ...model, agent })
|
||||
})
|
||||
|
||||
@@ -147,20 +133,7 @@ const createToolContext = Effect.fn("Cli.debug.agent.createToolContext")(functio
|
||||
? agent.model
|
||||
: yield* Effect.gen(function* () {
|
||||
const provider = yield* Provider.Service
|
||||
return yield* provider.defaultModel().pipe(
|
||||
Effect.matchCauseEffect({
|
||||
onSuccess: Effect.succeed,
|
||||
onFailure: (cause) => {
|
||||
const error = Cause.squash(cause) as Provider.DefaultModelError
|
||||
if (error instanceof Provider.ModelNotFoundError) {
|
||||
return fail(`Model not found: ${error.providerID}/${error.modelID}`)
|
||||
}
|
||||
if (error instanceof Provider.NoModelsError)
|
||||
return fail(`No models found for provider ${error.providerID}`)
|
||||
return fail("No providers found")
|
||||
},
|
||||
}),
|
||||
)
|
||||
return yield* provider.defaultModel()
|
||||
})
|
||||
const now = Date.now()
|
||||
const message: MessageV2.Assistant = {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { McpOAuthProvider } from "../../mcp/oauth-provider"
|
||||
import { Config } from "@/config/config"
|
||||
import { ConfigMCP } from "../../config/mcp"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { Installation } from "../../installation"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import path from "path"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
|
||||
@@ -34,10 +34,10 @@ const AUTOCOMPLETE_ROWS = FOOTER_MENU_ROWS
|
||||
const AUTOCOMPLETE_BOTTOM_ROWS = 1
|
||||
|
||||
export const TEXTAREA_MIN_ROWS = 1
|
||||
export const TEXTAREA_MAX_ROWS = 6
|
||||
const TEXTAREA_MAX_ROWS = 6
|
||||
export const PROMPT_MAX_ROWS = TEXTAREA_MAX_ROWS + AUTOCOMPLETE_ROWS - 1 + AUTOCOMPLETE_BOTTOM_ROWS
|
||||
|
||||
export const HINT_BREAKPOINTS = {
|
||||
const HINT_BREAKPOINTS = {
|
||||
send: 50,
|
||||
newline: 66,
|
||||
history: 80,
|
||||
|
||||
@@ -96,8 +96,6 @@ type RunFooterViewProps = {
|
||||
onSubagentSelect?: (sessionID: string | undefined) => void
|
||||
}
|
||||
|
||||
export { TEXTAREA_MIN_ROWS, TEXTAREA_MAX_ROWS } from "./footer.prompt"
|
||||
|
||||
export function RunFooterView(props: RunFooterViewProps) {
|
||||
const term = useTerminalDimensions()
|
||||
const active = createMemo<FooterView>(() => props.view?.() ?? { type: "prompt" })
|
||||
|
||||
@@ -27,7 +27,7 @@ function todoColor(theme: RunTheme, status: string) {
|
||||
return status === "in_progress" ? theme.footer.warning : theme.block.muted
|
||||
}
|
||||
|
||||
export function entryGroupKey(commit: StreamCommit): string | undefined {
|
||||
function entryGroupKey(commit: StreamCommit): string | undefined {
|
||||
if (!commit.partID) {
|
||||
return undefined
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export function sameEntryGroup(left: StreamCommit | undefined, right: StreamComm
|
||||
return Boolean(current && next && current === next)
|
||||
}
|
||||
|
||||
export function entryLayout(commit: StreamCommit, body: RunEntryBody = entryBody(commit)): EntryLayout {
|
||||
function entryLayout(commit: StreamCommit, body: RunEntryBody = entryBody(commit)): EntryLayout {
|
||||
if (commit.kind === "tool") {
|
||||
if (body.type === "structured" || body.type === "markdown") {
|
||||
return "block"
|
||||
|
||||
@@ -25,7 +25,7 @@ import { DialogProvider, useDialog } from "@tui/ui/dialog"
|
||||
import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider"
|
||||
import { ErrorComponent } from "@tui/component/error-component"
|
||||
import { PluginRouteMissing } from "@tui/component/plugin-route-missing"
|
||||
import { ProjectProvider, useProject } from "@tui/context/project"
|
||||
import { ProjectProvider } from "@tui/context/project"
|
||||
import { EditorContextProvider } from "@tui/context/editor"
|
||||
import { useEvent } from "@tui/context/event"
|
||||
import { SDKProvider, useSDK } from "@tui/context/sdk"
|
||||
@@ -279,7 +279,6 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
const themeState = useTheme()
|
||||
const { theme, mode, setMode, locked, lock, unlock } = themeState
|
||||
const sync = useSync()
|
||||
const project = useProject()
|
||||
const exit = useExit()
|
||||
const promptRef = usePromptRef()
|
||||
const routes: RouteMap = new Map()
|
||||
@@ -448,13 +447,6 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
)
|
||||
|
||||
const connected = useConnected()
|
||||
const currentWorktreeWorkspace = createMemo(() => {
|
||||
const workspaceID = project.workspace.current()
|
||||
if (!workspaceID) return
|
||||
const workspace = project.workspace.get(workspaceID)
|
||||
if (workspace?.type !== "worktree" || !workspace.directory) return
|
||||
return workspace
|
||||
})
|
||||
const appCommands = createMemo(() =>
|
||||
[
|
||||
{
|
||||
@@ -491,20 +483,6 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "workspace.copy_path",
|
||||
title: "Copy worktree path",
|
||||
category: "Workspace",
|
||||
enabled: () => currentWorktreeWorkspace() !== undefined,
|
||||
run: async () => {
|
||||
const workspace = currentWorktreeWorkspace()
|
||||
if (!workspace?.directory) return
|
||||
await Clipboard.copy(workspace.directory)
|
||||
.then(() => toast.show({ message: "Copied worktree path", variant: "info" }))
|
||||
.catch(toast.error)
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
...Array.from({ length: 9 }, (_, i) => ({
|
||||
name: `session.quick_switch.${i + 1}`,
|
||||
title: `Switch to session in quick slot ${i + 1}`,
|
||||
|
||||
@@ -5,8 +5,6 @@ import { useDialog } from "@tui/ui/dialog"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { For, Match, Switch, Show, createMemo } from "solid-js"
|
||||
|
||||
export type DialogStatusProps = {}
|
||||
|
||||
export function DialogStatus() {
|
||||
const sync = useSync()
|
||||
const { theme } = useTheme()
|
||||
|
||||
@@ -49,7 +49,7 @@ export function recentConnectedWorkspaces<WorkspaceInfo extends { id: string; ti
|
||||
return { recent, hasMore: recent.length < workspaces.length }
|
||||
}
|
||||
|
||||
export function warpReminderText(dir: string) {
|
||||
function warpReminderText(dir: string) {
|
||||
return `<system-reminder>The user has changed the current working directory to "${dir}". This is still the same project but at a possibly new location; take this into account when working with any files from now on.</system-reminder>`
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ export async function confirmWorkspaceFileChanges(input: {
|
||||
return fileChangeChoice === "yes"
|
||||
}
|
||||
|
||||
export function DialogWorkspaceSelect(props: {
|
||||
function DialogWorkspaceSelect(props: {
|
||||
adapters?: Adapter[]
|
||||
onSelect: (selection: WorkspaceSelection) => Promise<void> | void
|
||||
}) {
|
||||
|
||||
@@ -878,8 +878,3 @@ export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } =
|
||||
)
|
||||
}
|
||||
|
||||
export function GoLogo() {
|
||||
const { theme } = useTheme()
|
||||
const base = tint(theme.background, theme.text, 0.62)
|
||||
return <Logo shape={go} ink={base} idle />
|
||||
}
|
||||
|
||||
@@ -1438,10 +1438,7 @@ export function Prompt(props: PromptProps) {
|
||||
})
|
||||
|
||||
const spinnerDef = createMemo(() => {
|
||||
const agent =
|
||||
status().type !== "idle"
|
||||
? (local.agent.list().find((a) => a.name === lastUserMessage()?.agent) ?? local.agent.current())
|
||||
: local.agent.current()
|
||||
const agent = local.agent.current()
|
||||
const color = agent ? local.agent.color(agent.name) : theme.border
|
||||
return {
|
||||
frames: createFrames({
|
||||
|
||||
@@ -62,16 +62,14 @@ export const Definitions = {
|
||||
diff_close: keybind("escape,q", "Close diff viewer"),
|
||||
diff_toggle: keybind("enter,space", "Toggle diff viewer item"),
|
||||
diff_expand: keybind("right", "Expand diff viewer item"),
|
||||
diff_expand_all: keybind("E", "Expand all diff viewer folders"),
|
||||
diff_collapse: keybind("left", "Collapse diff viewer item"),
|
||||
diff_switch_focus: keybind("tab", "Switch diff viewer focus"),
|
||||
diff_next_file: keybind("n", "Jump to next diff file"),
|
||||
diff_previous_file: keybind("p", "Jump to previous diff file"),
|
||||
diff_toggle_file_tree: keybind("b", "Toggle diff viewer file tree"),
|
||||
diff_single_patch: keybind("s", "Toggle single patch view"),
|
||||
diff_switch_source: keybind("d", "Switch diff viewer source"),
|
||||
diff_switch_diff: keybind("d", "Switch diff viewer source"),
|
||||
diff_toggle_view: keybind("v", "Toggle diff viewer split or unified view"),
|
||||
diff_help: keybind("?", "Show more diff viewer shortcuts"),
|
||||
|
||||
editor_open: keybind("<leader>e", "Open external editor"),
|
||||
theme_list: keybind("<leader>t", "List available themes"),
|
||||
@@ -261,16 +259,14 @@ export const CommandMap = {
|
||||
diff_close: "diff.close",
|
||||
diff_toggle: "diff.toggle",
|
||||
diff_expand: "diff.expand",
|
||||
diff_expand_all: "diff.expand_all",
|
||||
diff_collapse: "diff.collapse",
|
||||
diff_switch_focus: "diff.switch_focus",
|
||||
diff_next_file: "diff.next_file",
|
||||
diff_previous_file: "diff.previous_file",
|
||||
diff_toggle_file_tree: "diff.toggle_file_tree",
|
||||
diff_single_patch: "diff.single_patch",
|
||||
diff_switch_source: "diff.switch_source",
|
||||
diff_switch_diff: "diff.switch_diff",
|
||||
diff_toggle_view: "diff.toggle_view",
|
||||
diff_help: "diff.help",
|
||||
editor_open: "prompt.editor",
|
||||
theme_list: "theme.switch",
|
||||
theme_switch_mode: "theme.switch_mode",
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useSDK } from "./sdk"
|
||||
import { RGBA } from "@opentui/core"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
|
||||
export function parseModel(model: string) {
|
||||
function parseModel(model: string) {
|
||||
const [providerID, ...rest] = model.split("/")
|
||||
return {
|
||||
providerID: providerID,
|
||||
|
||||
@@ -2,18 +2,18 @@ import { createStore, reconcile } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import type { PromptInfo } from "../component/prompt/history"
|
||||
|
||||
export type HomeRoute = {
|
||||
type HomeRoute = {
|
||||
type: "home"
|
||||
prompt?: PromptInfo
|
||||
}
|
||||
|
||||
export type SessionRoute = {
|
||||
type SessionRoute = {
|
||||
type: "session"
|
||||
sessionID: string
|
||||
prompt?: PromptInfo
|
||||
}
|
||||
|
||||
export type PluginRoute = {
|
||||
type PluginRoute = {
|
||||
type: "plugin"
|
||||
id: string
|
||||
data?: Record<string, unknown>
|
||||
@@ -44,8 +44,6 @@ export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
|
||||
},
|
||||
})
|
||||
|
||||
export type RouteContext = ReturnType<typeof useRoute>
|
||||
|
||||
export function useRouteData<T extends Route["type"]>(type: T) {
|
||||
const route = useRoute()
|
||||
return route.data as Extract<Route, { type: typeof type }>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user