diff --git a/.github/VOUCHED.td b/.github/VOUCHED.td index 28535b5779..ddfa7fd161 100644 --- a/.github/VOUCHED.td +++ b/.github/VOUCHED.td @@ -21,3 +21,4 @@ r44vc0rp rekram1-node -spider-yamet clawdbot/llm psychosis, spam pinging the team thdxr +-OpenCode2026 diff --git a/.opencode/.gitignore b/.opencode/.gitignore index 03445edaf2..d3bf7f8d3b 100644 --- a/.opencode/.gitignore +++ b/.opencode/.gitignore @@ -1,4 +1,6 @@ -plans/ -bun.lock +node_modules +plans package.json -package-lock.json +bun.lock +.gitignore +package-lock.json \ No newline at end of file diff --git a/.opencode/agent/translator.md b/.opencode/agent/translator.md index 263afbe9b5..a987d01927 100644 --- a/.opencode/agent/translator.md +++ b/.opencode/agent/translator.md @@ -1,7 +1,7 @@ --- description: Translate content for a specified locale while preserving technical terms mode: subagent -model: opencode/gemini-3-pro +model: opencode/gpt-5.4 --- You are a professional translator and localization specialist. diff --git a/.opencode/tool/github-pr-search.ts b/.opencode/tool/github-pr-search.ts index 587fdfaaf2..927e68fd73 100644 --- a/.opencode/tool/github-pr-search.ts +++ b/.opencode/tool/github-pr-search.ts @@ -1,7 +1,5 @@ /// import { tool } from "@opencode-ai/plugin" -import DESCRIPTION from "./github-pr-search.txt" - async function githubFetch(endpoint: string, options: RequestInit = {}) { const response = await fetch(`https://api.github.com${endpoint}`, { ...options, @@ -24,7 +22,16 @@ interface PR { } export default tool({ - description: DESCRIPTION, + description: `Use this tool to search GitHub pull requests by title and description. + +This tool searches PRs in the anomalyco/opencode repository and returns LLM-friendly results including: +- PR number and title +- Author +- State (open/closed/merged) +- Labels +- Description snippet + +Use the query parameter to search for keywords that might appear in PR titles or descriptions.`, args: { query: tool.schema.string().describe("Search query for PR titles and descriptions"), limit: tool.schema.number().describe("Maximum number of results to return").default(10), diff --git a/.opencode/tool/github-pr-search.txt b/.opencode/tool/github-pr-search.txt deleted file mode 100644 index 1b658e71c4..0000000000 --- a/.opencode/tool/github-pr-search.txt +++ /dev/null @@ -1,10 +0,0 @@ -Use this tool to search GitHub pull requests by title and description. - -This tool searches PRs in the anomalyco/opencode repository and returns LLM-friendly results including: -- PR number and title -- Author -- State (open/closed/merged) -- Labels -- Description snippet - -Use the query parameter to search for keywords that might appear in PR titles or descriptions. diff --git a/.opencode/tool/github-triage.ts b/.opencode/tool/github-triage.ts index ed80f49d54..c06d2407fe 100644 --- a/.opencode/tool/github-triage.ts +++ b/.opencode/tool/github-triage.ts @@ -1,7 +1,5 @@ /// import { tool } from "@opencode-ai/plugin" -import DESCRIPTION from "./github-triage.txt" - const TEAM = { desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"], zen: ["fwang", "MrMushrooooom"], @@ -40,7 +38,12 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) { } export default tool({ - description: DESCRIPTION, + description: `Use this tool to assign and/or label a GitHub issue. + +Choose labels and assignee using the current triage policy and ownership rules. +Pick the most fitting labels for the issue and assign one owner. + +If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random.`, args: { assignee: tool.schema .enum(ASSIGNEES as [string, ...string[]]) diff --git a/.opencode/tool/github-triage.txt b/.opencode/tool/github-triage.txt deleted file mode 100644 index 4369ed2351..0000000000 --- a/.opencode/tool/github-triage.txt +++ /dev/null @@ -1,6 +0,0 @@ -Use this tool to assign and/or label a GitHub issue. - -Choose labels and assignee using the current triage policy and ownership rules. -Pick the most fitting labels for the issue and assign one owner. - -If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random. diff --git a/bun.lock b/bun.lock index 35841622b7..6d82378458 100644 --- a/bun.lock +++ b/bun.lock @@ -46,7 +46,7 @@ "@solidjs/router": "catalog:", "@thisbeyond/solid-dnd": "0.7.5", "diff": "catalog:", - "effect": "4.0.0-beta.31", + "effect": "catalog:", "fuzzysort": "catalog:", "ghostty-web": "github:anomalyco/ghostty-web#main", "luxon": "catalog:", @@ -227,7 +227,7 @@ "@solid-primitives/storage": "catalog:", "@solidjs/meta": "catalog:", "@solidjs/router": "0.15.4", - "effect": "4.0.0-beta.31", + "effect": "catalog:", "electron-log": "^5", "electron-store": "^10", "electron-updater": "^6", @@ -324,6 +324,7 @@ "@ai-sdk/xai": "2.0.51", "@aws-sdk/credential-providers": "3.993.0", "@clack/prompts": "1.0.0-alpha.1", + "@effect/platform-node": "catalog:", "@gitlab/gitlab-ai-provider": "3.6.0", "@gitlab/opencode-gitlab-auth": "1.3.3", "@hono/standard-validator": "0.1.5", @@ -351,9 +352,10 @@ "bun-pty": "0.4.8", "chokidar": "4.0.3", "clipboardy": "4.0.0", + "cross-spawn": "^7.0.6", "decimal.js": "10.5.0", "diff": "catalog:", - "drizzle-orm": "1.0.0-beta.16-ea816b6", + "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", "glob": "13.0.5", @@ -400,14 +402,15 @@ "@tsconfig/bun": "catalog:", "@types/babel__core": "7.20.5", "@types/bun": "catalog:", + "@types/cross-spawn": "6.0.6", "@types/mime-types": "3.0.1", "@types/semver": "^7.5.8", "@types/turndown": "5.0.5", "@types/which": "3.0.4", "@types/yargs": "17.0.33", "@typescript/native-preview": "catalog:", - "drizzle-kit": "1.0.0-beta.16-ea816b6", - "drizzle-orm": "1.0.0-beta.16-ea816b6", + "drizzle-kit": "catalog:", + "drizzle-orm": "catalog:", "typescript": "catalog:", "vscode-languageserver-types": "3.17.5", "why-is-node-running": "3.2.2", @@ -583,6 +586,7 @@ ], "patchedDependencies": { "@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch", + "@ai-sdk/xai@2.0.51": "patches/@ai-sdk%2Fxai@2.0.51.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", }, "overrides": { @@ -591,6 +595,7 @@ }, "catalog": { "@cloudflare/workers-types": "4.20251008.0", + "@effect/platform-node": "4.0.0-beta.35", "@hono/zod-validator": "0.4.2", "@kobalte/core": "0.13.11", "@octokit/rest": "22.0.0", @@ -612,9 +617,9 @@ "ai": "5.0.124", "diff": "8.0.2", "dompurify": "3.3.1", - "drizzle-kit": "1.0.0-beta.16-ea816b6", - "drizzle-orm": "1.0.0-beta.16-ea816b6", - "effect": "4.0.0-beta.31", + "drizzle-kit": "1.0.0-beta.19-d95b7a4", + "drizzle-orm": "1.0.0-beta.19-d95b7a4", + "effect": "4.0.0-beta.35", "fuzzysort": "3.1.0", "hono": "4.10.7", "hono-openapi": "1.1.2", @@ -972,6 +977,10 @@ "@effect/language-service": ["@effect/language-service@0.79.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="], + "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.35", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.35", "mime": "^4.1.0", "undici": "^7.24.0" }, "peerDependencies": { "effect": "^4.0.0-beta.35", "ioredis": "^5.7.0" } }, "sha512-HPc2xZASl9F9y/xJ01bQgFD6Jf9XP4Fcv/BlVTvG0Yr/uN63lwKZYr/VXor5K5krHfBDeCBD8y7/SICPYZoq3A=="], + + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.35", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.35" } }, "sha512-9bPqNV988itKJ7MQoJuzmR014DB9EZRDOnhJt/+iJlb8qLoR9HnCzNJb9gfBdYhFmVYc8DMsQxG81rdJzpv9tg=="], + "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], "@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="], @@ -1168,6 +1177,8 @@ "@internationalized/number": ["@internationalized/number@3.6.5", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g=="], + "@ioredis/commands": ["@ioredis/commands@1.5.1", "", {}, "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw=="], + "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="], @@ -2052,6 +2063,8 @@ "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + "@types/cross-spawn": ["@types/cross-spawn@6.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA=="], + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], @@ -2536,6 +2549,8 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], @@ -2722,9 +2737,9 @@ "dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], - "drizzle-kit": ["drizzle-kit@1.0.0-beta.16-ea816b6", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-GiJQqCNPZP8Kk+i7/sFa3rtXbq26tLDNi3LbMx9aoLuwF2ofk8CS7cySUGdI+r4J3q0a568quC8FZeaFTCw4IA=="], + "drizzle-kit": ["drizzle-kit@1.0.0-beta.19-d95b7a4", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-M0sqc+42TYBod6kEZ3AsW6+JWe3+76gR1aDFbHH5DmuLKEwewmbzlhBG6qnvV6YA1cIIbkuam3dC7r6PREOCXw=="], - "drizzle-orm": ["drizzle-orm@1.0.0-beta.16-ea816b6", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-k9gT4f0O9Qvah5YK/zL+FZonQ8TPyVxcG/ojN4dzO0fHP8hs8tBno8lqmJo53g0JLWv3Q2nsTUoyBRKM2TljFw=="], + "drizzle-orm": ["drizzle-orm@1.0.0-beta.19-d95b7a4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-bZZKKeoRKrMVU6zKTscjrSH0+WNb1WEi3N0Jl4wEyQ7aQpTgHzdYY6IJQ1P0M74HuSJVeX4UpkFB/S6dtqLEJg=="], "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], @@ -2738,7 +2753,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "effect": ["effect@4.0.0-beta.31", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-w3QwJnlaLtWWiUSzhCXUTIisnULPsxLzpO6uqaBFjXybKx6FvCqsLJT6v4dV7G9eA9jeTtG6Gv7kF+jGe3HxzA=="], + "effect": ["effect@4.0.0-beta.35", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-64j8dgJmoEMeq6Y3WLYcZIRqPZ5E/lqnULCf6QW5te3hQ/sa13UodWLGwBEviEqBoq72U8lArhVX+T7ntzhJGQ=="], "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], @@ -3006,6 +3021,8 @@ "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], + "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#4af877d", {}, "anomalyco-ghostty-web-4af877d", "sha512-fbEK8mtr7ar4ySsF+JUGjhaZrane7dKphanN+SxHt5XXI6yLMAh/Hpf6sNCOyyVa2UlGCd7YpXG/T2v2RUAX+A=="], "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], @@ -3176,6 +3193,8 @@ "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + "ioredis": ["ioredis@5.10.0", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA=="], + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -3408,10 +3427,14 @@ "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], + "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], + "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], + "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], "lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="], @@ -3598,7 +3621,7 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + "mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="], "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], @@ -4022,6 +4045,10 @@ "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], + + "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], @@ -4084,6 +4111,8 @@ "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], @@ -4280,6 +4309,8 @@ "stage-js": ["stage-js@1.0.1", "", {}, "sha512-cz14aPp/wY0s3bkb/B93BPP5ZAEhgBbRmAT3CCDqert8eCAqIpQ0RB2zpK8Ksxf+Pisl5oTzvPHtL4CVzzeHcw=="], + "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], + "stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="], "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], @@ -4986,12 +5017,18 @@ "@bufbuild/protoplugin/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="], + "@cloudflare/kv-asset-handler/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], "@develar/schema-utils/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], "@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "@effect/platform-node/undici": ["undici@7.24.4", "", {}, "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w=="], + + "@effect/platform-node-shared/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], "@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], @@ -5032,6 +5069,8 @@ "@hono/zod-validator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@jimp/core/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + "@jimp/plugin-blit/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@jimp/plugin-circle/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -5352,6 +5391,8 @@ "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "db0/drizzle-orm": ["drizzle-orm@1.0.0-beta.16-ea816b6", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-k9gT4f0O9Qvah5YK/zL+FZonQ8TPyVxcG/ojN4dzO0fHP8hs8tBno8lqmJo53g0JLWv3Q2nsTUoyBRKM2TljFw=="], + "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], "dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], diff --git a/infra/console.ts b/infra/console.ts index c7889c587f..22652f2daa 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -122,6 +122,7 @@ const ZEN_LITE_PRICE = new sst.Linkable("ZEN_LITE_PRICE", { properties: { product: zenLiteProduct.id, price: zenLitePrice.id, + priceInr: 92900, firstMonth50Coupon: zenLiteCouponFirstMonth50.id, }, }) @@ -201,6 +202,10 @@ const bucketNew = new sst.cloudflare.Bucket("ZenDataNew") const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID") const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY") +const SALESFORCE_CLIENT_ID = new sst.Secret("SALESFORCE_CLIENT_ID") +const SALESFORCE_CLIENT_SECRET = new sst.Secret("SALESFORCE_CLIENT_SECRET") +const SALESFORCE_INSTANCE_URL = new sst.Secret("SALESFORCE_INSTANCE_URL") + const logProcessor = new sst.cloudflare.Worker("LogProcessor", { handler: "packages/console/function/src/log-processor.ts", link: [new sst.Secret("HONEYCOMB_API_KEY")], @@ -219,6 +224,9 @@ new sst.cloudflare.x.SolidStart("Console", { EMAILOCTOPUS_API_KEY, AWS_SES_ACCESS_KEY_ID, AWS_SES_SECRET_ACCESS_KEY, + SALESFORCE_CLIENT_ID, + SALESFORCE_CLIENT_SECRET, + SALESFORCE_INSTANCE_URL, ZEN_BLACK_PRICE, ZEN_LITE_PRICE, new sst.Secret("ZEN_LIMITS"), diff --git a/nix/hashes.json b/nix/hashes.json index 06f54dc950..3db81fb5a7 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-WJgo6UclmtQOEubnKMZybdIEhZ1uRTucF61yojjd+l0=", - "aarch64-linux": "sha256-QfZ/g7EZFpe6ndR3dG8WvVfMj5Kyd/R/4kkTJfGJxL4=", - "aarch64-darwin": "sha256-ezr/R70XJr9eN5l3mgb7HzLF6QsofNEKUOtuxbfli80=", - "x86_64-darwin": "sha256-MbsBGS415uEU/n1RQ/5H5pqh+udLY3+oimJ+eS5uJVI=" + "x86_64-linux": "sha256-xq0W2Ym0AzANLXnLyAL+IUwrFm0MKXwkJVdENowoPyY=", + "aarch64-linux": "sha256-RtpiGZXk+BboD9MjBetq5sInIbH/OPkLVNSFgN/0ehY=", + "aarch64-darwin": "sha256-cX6y262OzqRicH4m0/u1DCsMkpJfzCUOOBFQqtQLvls=", + "x86_64-darwin": "sha256-K4UmRKiEfKkvVeKUB85XjHJ1jf0ZUnjL0dWvx9TD4pI=" } } diff --git a/package.json b/package.json index 97087c0e76..2875b9daa2 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "dev:desktop": "bun --cwd packages/desktop tauri dev", "dev:web": "bun --cwd packages/app dev", + "dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev", "dev:storybook": "bun --cwd packages/storybook storybook", "typecheck": "bun turbo typecheck", "prepare": "husky", @@ -24,6 +25,7 @@ "packages/slack" ], "catalog": { + "@effect/platform-node": "4.0.0-beta.35", "@types/bun": "1.3.9", "@octokit/rest": "22.0.0", "@hono/zod-validator": "0.4.2", @@ -41,9 +43,9 @@ "@tailwindcss/vite": "4.1.11", "diff": "8.0.2", "dompurify": "3.3.1", - "drizzle-kit": "1.0.0-beta.16-ea816b6", - "drizzle-orm": "1.0.0-beta.16-ea816b6", - "effect": "4.0.0-beta.31", + "drizzle-kit": "1.0.0-beta.19-d95b7a4", + "drizzle-orm": "1.0.0-beta.19-d95b7a4", + "effect": "4.0.0-beta.35", "ai": "5.0.124", "hono": "4.10.7", "hono-openapi": "1.1.2", @@ -110,6 +112,7 @@ }, "patchedDependencies": { "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", - "@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch" + "@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch", + "@ai-sdk/xai@2.0.51": "patches/@ai-sdk%2Fxai@2.0.51.patch" } } diff --git a/packages/app/e2e/AGENTS.md b/packages/app/e2e/AGENTS.md index 4b62634f0b..f263e49a02 100644 --- a/packages/app/e2e/AGENTS.md +++ b/packages/app/e2e/AGENTS.md @@ -174,6 +174,8 @@ await page.keyboard.press(`${modKey}+Comma`) // Open settings - In terminal tests, type through the browser. Do not write to the PTY through the SDK. - Use `waitTerminalReady(page, { term? })` and `runTerminal(page, { cmd, token, term?, timeout? })` from `actions.ts`. - These helpers use the fixture-enabled test-only terminal driver and wait for output after the terminal writer settles. +- After opening the terminal, use `waitTerminalFocusIdle(...)` before the next keyboard action when prompt focus or keyboard routing matters. +- This avoids racing terminal mount, focus handoff, and prompt readiness when the next step types or sends shortcuts. - Avoid `waitForTimeout` and custom DOM or `data-*` readiness checks. ### Wait on state @@ -182,6 +184,9 @@ await page.keyboard.press(`${modKey}+Comma`) // Open settings - Avoid race-prone flows that assume work is finished after an action - Wait or poll on observable state with `expect(...)`, `expect.poll(...)`, or existing helpers - Prefer locator assertions like `toBeVisible()`, `toHaveCount(0)`, and `toHaveAttribute(...)` for normal UI state, and reserve `expect.poll(...)` for probe, mock, or backend state +- Prefer semantic app state over transient DOM visibility when behavior depends on active selection, focus ownership, or async retry loops +- Do not treat a visible element as proof that the app will route the next action to it +- When fixing a flake, validate with `--repeat-each` and multiple workers when practical ### Add hooks @@ -189,11 +194,16 @@ await page.keyboard.press(`${modKey}+Comma`) // Open settings - Keep these hooks minimal and purpose-built, following the style of `packages/app/src/testing/terminal.ts` - Test-only hooks must be inert unless explicitly enabled; do not add normal-runtime listeners, reactive subscriptions, or per-update allocations for e2e ceremony - When mocking routes or APIs, expose explicit mock state and wait on that before asserting post-action UI +- Add minimal test-only probes for semantic state like the active list item or selected command when DOM intermediates are unstable +- Prefer probing committed app state over asserting on transient highlight, visibility, or animation states ### Prefer helpers - Prefer fluent helpers and drivers when they make intent obvious and reduce locator-heavy noise - Use direct locators when the interaction is simple and a helper would not add clarity +- Prefer helpers that both perform an action and verify the app consumed it +- Avoid composing helpers redundantly when one already includes the other or already waits for the resulting state +- If a helper already covers the required wait or verification, use it directly instead of layering extra clicks, keypresses, or assertions ## Writing New Tests diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts index 8e21579e21..88d71f94cf 100644 --- a/packages/app/e2e/actions.ts +++ b/packages/app/e2e/actions.ts @@ -1,3 +1,4 @@ +import { base64Decode, base64Encode } from "@opencode-ai/util/encode" import { expect, type Locator, type Page } from "@playwright/test" import fs from "node:fs/promises" import os from "node:os" @@ -16,6 +17,7 @@ import { listItemSelector, listItemKeySelector, listItemKeyStartsWithSelector, + promptSelector, terminalSelector, workspaceItemSelector, workspaceMenuTriggerSelector, @@ -61,6 +63,15 @@ async function terminalReady(page: Page, term?: Locator) { }, id) } +async function terminalFocusIdle(page: Page, term?: Locator) { + const next = term ?? page.locator(terminalSelector).first() + const id = await terminalID(next) + return page.evaluate((id) => { + const state = (window as E2EWindow).__opencode_e2e?.terminal?.terminals?.[id] + return (state?.focusing ?? 0) === 0 + }, id) +} + async function terminalHas(page: Page, input: { term?: Locator; token: string }) { const next = input.term ?? page.locator(terminalSelector).first() const id = await terminalID(next) @@ -73,6 +84,29 @@ async function terminalHas(page: Page, input: { term?: Locator; token: string }) ) } +async function promptSlashActive(page: Page, id: string) { + return page.evaluate((id) => { + const state = (window as E2EWindow).__opencode_e2e?.prompt?.current + if (state?.popover !== "slash") return false + if (!state.slash.ids.includes(id)) return false + return state.slash.active === id + }, id) +} + +async function promptSlashSelects(page: Page) { + return page.evaluate(() => { + return (window as E2EWindow).__opencode_e2e?.prompt?.current?.selects ?? 0 + }) +} + +async function promptSlashSelected(page: Page, input: { id: string; count: number }) { + return page.evaluate((input) => { + const state = (window as E2EWindow).__opencode_e2e?.prompt?.current + if (!state) return false + return state.selected === input.id && state.selects >= input.count + }, input) +} + export async function waitTerminalReady(page: Page, input?: { term?: Locator; timeout?: number }) { const term = input?.term ?? page.locator(terminalSelector).first() const timeout = input?.timeout ?? 10_000 @@ -81,6 +115,43 @@ export async function waitTerminalReady(page: Page, input?: { term?: Locator; ti await expect.poll(() => terminalReady(page, term), { timeout }).toBe(true) } +export async function waitTerminalFocusIdle(page: Page, input?: { term?: Locator; timeout?: number }) { + const term = input?.term ?? page.locator(terminalSelector).first() + const timeout = input?.timeout ?? 10_000 + await waitTerminalReady(page, { term, timeout }) + await expect.poll(() => terminalFocusIdle(page, term), { timeout }).toBe(true) +} + +export async function showPromptSlash( + page: Page, + input: { id: string; text: string; prompt?: Locator; timeout?: number }, +) { + const prompt = input.prompt ?? page.locator(promptSelector) + const timeout = input.timeout ?? 10_000 + await expect + .poll( + async () => { + await prompt.click().catch(() => false) + await prompt.fill(input.text).catch(() => false) + return promptSlashActive(page, input.id).catch(() => false) + }, + { timeout }, + ) + .toBe(true) +} + +export async function runPromptSlash( + page: Page, + input: { id: string; text: string; prompt?: Locator; timeout?: number }, +) { + const prompt = input.prompt ?? page.locator(promptSelector) + const timeout = input.timeout ?? 10_000 + const count = await promptSlashSelects(page) + await showPromptSlash(page, input) + await prompt.press("Enter") + await expect.poll(() => promptSlashSelected(page, { id: input.id, count: count + 1 }), { timeout }).toBe(true) +} + export async function runTerminal(page: Page, input: { cmd: string; token: string; term?: Locator; timeout?: number }) { const term = input.term ?? page.locator(terminalSelector).first() const timeout = input.timeout ?? 10_000 @@ -291,6 +362,30 @@ export async function waitSlug(page: Page, skip: string[] = []) { return next } +export async function resolveSlug(slug: string) { + const directory = base64Decode(slug) + if (!directory) throw new Error(`Failed to decode workspace slug: ${slug}`) + const resolved = await resolveDirectory(directory) + return { directory: resolved, slug: base64Encode(resolved), raw: slug } +} + +export async function waitDir(page: Page, directory: string) { + const target = await resolveDirectory(directory) + await expect + .poll( + async () => { + const slug = slugFromUrl(page.url()) + if (!slug) return "" + return resolveSlug(slug) + .then((item) => item.directory) + .catch(() => "") + }, + { timeout: 45_000 }, + ) + .toBe(target) + return { directory: target, slug: base64Encode(target) } +} + export function sessionIDFromUrl(url: string) { const match = /\/session\/([^/?#]+)/.exec(url) return match?.[1] diff --git a/packages/app/e2e/app/home.spec.ts b/packages/app/e2e/app/home.spec.ts index a3cedf7cb6..5deba4300c 100644 --- a/packages/app/e2e/app/home.spec.ts +++ b/packages/app/e2e/app/home.spec.ts @@ -3,8 +3,11 @@ import { serverNamePattern } from "../utils" test("home renders and shows core entrypoints", async ({ page }) => { await page.goto("/") + const nav = page.locator('[data-component="sidebar-nav-desktop"]') await expect(page.getByRole("button", { name: "Open project" }).first()).toBeVisible() + await expect(nav.getByText("No projects open")).toBeVisible() + await expect(nav.getByText("Open a project to get started")).toBeVisible() await expect(page.getByRole("button", { name: serverNamePattern })).toBeVisible() }) diff --git a/packages/app/e2e/fixtures.ts b/packages/app/e2e/fixtures.ts index efefd479ef..7bc994e507 100644 --- a/packages/app/e2e/fixtures.ts +++ b/packages/app/e2e/fixtures.ts @@ -98,6 +98,9 @@ async function seedStorage(page: Page, input: { directory: string; extra?: strin model: { enabled: true, }, + prompt: { + enabled: true, + }, terminal: { enabled: true, terminals: {}, diff --git a/packages/app/e2e/projects/projects-switch.spec.ts b/packages/app/e2e/projects/projects-switch.spec.ts index 6ad64f5927..e9cbf868df 100644 --- a/packages/app/e2e/projects/projects-switch.spec.ts +++ b/packages/app/e2e/projects/projects-switch.spec.ts @@ -1,7 +1,15 @@ import { base64Decode } from "@opencode-ai/util/encode" import type { Page } from "@playwright/test" import { test, expect } from "../fixtures" -import { defocus, createTestProject, cleanupTestProject, openSidebar, sessionIDFromUrl, waitSlug } from "../actions" +import { + defocus, + createTestProject, + cleanupTestProject, + openSidebar, + sessionIDFromUrl, + waitDir, + waitSlug, +} from "../actions" import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors" import { dirSlug, resolveDirectory } from "../utils" @@ -100,11 +108,8 @@ test("switching back to a project opens the latest workspace session", async ({ await expect(btn).toBeVisible() await btn.click({ force: true }) - // A new workspace can be discovered via a transient slug before the route and sidebar - // settle to the canonical workspace path on Windows, so interact with either and assert - // against the resolved workspace slug. await waitSlug(page) - await expect(page).toHaveURL(new RegExp(`/${next}/session(?:[/?#]|$)`)) + await waitDir(page, space) // Create a session by sending a prompt const prompt = page.locator(promptSelector) @@ -132,6 +137,7 @@ test("switching back to a project opens the latest workspace session", async ({ await expect(rootButton).toBeVisible() await rootButton.click() + await waitDir(page, space) await expect.poll(() => sessionIDFromUrl(page.url()) ?? "").toBe(created) await expect(page).toHaveURL(new RegExp(`/session/${created}(?:[/?#]|$)`)) }, diff --git a/packages/app/e2e/projects/workspace-new-session.spec.ts b/packages/app/e2e/projects/workspace-new-session.spec.ts index 18fa46d329..0858f26273 100644 --- a/packages/app/e2e/projects/workspace-new-session.spec.ts +++ b/packages/app/e2e/projects/workspace-new-session.spec.ts @@ -1,18 +1,25 @@ -import { base64Decode } from "@opencode-ai/util/encode" import type { Page } from "@playwright/test" import { test, expect } from "../fixtures" -import { openSidebar, sessionIDFromUrl, setWorkspacesEnabled, slugFromUrl, waitSlug } from "../actions" +import { openSidebar, resolveSlug, sessionIDFromUrl, setWorkspacesEnabled, waitDir, waitSlug } from "../actions" import { promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors" import { createSdk } from "../utils" -async function waitWorkspaceReady(page: Page, slug: string) { +function item(space: { slug: string; raw: string }) { + return `${workspaceItemSelector(space.slug)}, ${workspaceItemSelector(space.raw)}` +} + +function button(space: { slug: string; raw: string }) { + return `${workspaceNewSessionSelector(space.slug)}, ${workspaceNewSessionSelector(space.raw)}` +} + +async function waitWorkspaceReady(page: Page, space: { slug: string; raw: string }) { await openSidebar(page) await expect .poll( async () => { - const item = page.locator(workspaceItemSelector(slug)).first() + const row = page.locator(item(space)).first() try { - await item.hover({ timeout: 500 }) + await row.hover({ timeout: 500 }) return true } catch { return false @@ -27,29 +34,30 @@ async function createWorkspace(page: Page, root: string, seen: string[]) { await openSidebar(page) await page.getByRole("button", { name: "New workspace" }).first().click() - const slug = await waitSlug(page, [root, ...seen]) - const directory = base64Decode(slug) - if (!directory) throw new Error(`Failed to decode workspace slug: ${slug}`) - return { slug, directory } -} - -async function openWorkspaceNewSession(page: Page, slug: string) { - await waitWorkspaceReady(page, slug) - - const item = page.locator(workspaceItemSelector(slug)).first() - await item.hover() - - const button = page.locator(workspaceNewSessionSelector(slug)).first() - await expect(button).toBeVisible() - await button.click({ force: true }) - - const next = await waitSlug(page) - await expect(page).toHaveURL(new RegExp(`/${next}/session(?:[/?#]|$)`)) + const next = await resolveSlug(await waitSlug(page, [root, ...seen])) + await waitDir(page, next.directory) return next } -async function createSessionFromWorkspace(page: Page, slug: string, text: string) { - const next = await openWorkspaceNewSession(page, slug) +async function openWorkspaceNewSession(page: Page, space: { slug: string; raw: string; directory: string }) { + await waitWorkspaceReady(page, space) + + const row = page.locator(item(space)).first() + await row.hover() + + const next = page.locator(button(space)).first() + await expect(next).toBeVisible() + await next.click({ force: true }) + + return waitDir(page, space.directory) +} + +async function createSessionFromWorkspace( + page: Page, + space: { slug: string; raw: string; directory: string }, + text: string, +) { + const next = await openWorkspaceNewSession(page, space) const prompt = page.locator(promptSelector) await expect(prompt).toBeVisible() @@ -60,13 +68,13 @@ async function createSessionFromWorkspace(page: Page, slug: string, text: string await expect.poll(async () => ((await prompt.textContent()) ?? "").trim()).toContain(text) await prompt.press("Enter") - await expect.poll(() => slugFromUrl(page.url())).toBe(next) + await waitDir(page, next.directory) await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 30_000 }).not.toBe("") const sessionID = sessionIDFromUrl(page.url()) if (!sessionID) throw new Error(`Failed to parse session id from url: ${page.url()}`) - await expect(page).toHaveURL(new RegExp(`/${next}/session/${sessionID}(?:[/?#]|$)`)) - return { sessionID, slug: next } + await expect(page).toHaveURL(new RegExp(`/session/${sessionID}(?:[/?#]|$)`)) + return { sessionID, slug: next.slug } } async function sessionDirectory(directory: string, sessionID: string) { @@ -87,11 +95,11 @@ test("new sessions from sidebar workspace actions stay in selected workspace", a const first = await createWorkspace(page, root, []) trackDirectory(first.directory) - await waitWorkspaceReady(page, first.slug) + await waitWorkspaceReady(page, first) const second = await createWorkspace(page, root, [first.slug]) trackDirectory(second.directory) - await waitWorkspaceReady(page, second.slug) + await waitWorkspaceReady(page, second) const firstSession = await createSessionFromWorkspace(page, first.slug, `workspace one ${Date.now()}`) trackSession(firstSession.sessionID, first.directory) diff --git a/packages/app/e2e/projects/workspaces.spec.ts b/packages/app/e2e/projects/workspaces.spec.ts index aeeccb9bba..297cdb9fc9 100644 --- a/packages/app/e2e/projects/workspaces.spec.ts +++ b/packages/app/e2e/projects/workspaces.spec.ts @@ -1,7 +1,7 @@ -import { base64Decode } from "@opencode-ai/util/encode" import fs from "node:fs/promises" import os from "node:os" import path from "node:path" +import { base64Decode } from "@opencode-ai/util/encode" import type { Page } from "@playwright/test" import { test, expect } from "../fixtures" @@ -13,8 +13,10 @@ import { confirmDialog, openSidebar, openWorkspaceMenu, + resolveSlug, setWorkspacesEnabled, slugFromUrl, + waitDir, waitSlug, } from "../actions" import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors" @@ -27,15 +29,15 @@ async function setupWorkspaceTest(page: Page, project: { slug: string }) { await setWorkspacesEnabled(page, rootSlug, true) await page.getByRole("button", { name: "New workspace" }).first().click() - const slug = await waitSlug(page, [rootSlug]) - const dir = base64Decode(slug) + const next = await resolveSlug(await waitSlug(page, [rootSlug])) + await waitDir(page, next.directory) await openSidebar(page) await expect .poll( async () => { - const item = page.locator(workspaceItemSelector(slug)).first() + const item = page.locator(workspaceItemSelector(next.slug)).first() try { await item.hover({ timeout: 500 }) return true @@ -47,7 +49,7 @@ async function setupWorkspaceTest(page: Page, project: { slug: string }) { ) .toBe(true) - return { rootSlug, slug, directory: dir } + return { rootSlug, slug: next.slug, directory: next.directory } } test("can enable and disable workspaces from project menu", async ({ page, withProject }) => { @@ -79,15 +81,15 @@ test("can create a workspace", async ({ page, withProject }) => { await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible() await page.getByRole("button", { name: "New workspace" }).first().click() - const workspaceSlug = await waitSlug(page, [slug]) - const workspaceDir = base64Decode(workspaceSlug) + const next = await resolveSlug(await waitSlug(page, [slug])) + await waitDir(page, next.directory) await openSidebar(page) await expect .poll( async () => { - const item = page.locator(workspaceItemSelector(workspaceSlug)).first() + const item = page.locator(workspaceItemSelector(next.slug)).first() try { await item.hover({ timeout: 500 }) return true @@ -99,9 +101,9 @@ test("can create a workspace", async ({ page, withProject }) => { ) .toBe(true) - await expect(page.locator(workspaceItemSelector(workspaceSlug)).first()).toBeVisible() + await expect(page.locator(workspaceItemSelector(next.slug)).first()).toBeVisible() - await cleanupTestProject(workspaceDir) + await cleanupTestProject(next.directory) }) }) @@ -119,7 +121,7 @@ test("non-git projects keep workspace mode disabled", async ({ page, withProject await expect.poll(() => slugFromUrl(page.url()), { timeout: 30_000 }).not.toBe("") - const activeDir = base64Decode(slugFromUrl(page.url())) + const activeDir = await resolveSlug(slugFromUrl(page.url())).then((item) => item.directory) expect(path.basename(activeDir)).toContain("opencode-e2e-project-nongit-") await openSidebar(page) @@ -331,9 +333,9 @@ test("can reorder workspaces by drag and drop", async ({ page, withProject }) => for (const _ of [0, 1]) { const prev = slugFromUrl(page.url()) await page.getByRole("button", { name: "New workspace" }).first().click() - const slug = await waitSlug(page, [rootSlug, prev]) - const dir = base64Decode(slug) - workspaces.push({ slug, directory: dir }) + const next = await resolveSlug(await waitSlug(page, [rootSlug, prev])) + await waitDir(page, next.directory) + workspaces.push(next) await openSidebar(page) } diff --git a/packages/app/e2e/prompt/prompt-multiline.spec.ts b/packages/app/e2e/prompt/prompt-multiline.spec.ts index 216aa3fdae..3584773bb9 100644 --- a/packages/app/e2e/prompt/prompt-multiline.spec.ts +++ b/packages/app/e2e/prompt/prompt-multiline.spec.ts @@ -7,12 +7,18 @@ test("shift+enter inserts a newline without submitting", async ({ page, gotoSess await expect(page).toHaveURL(/\/session\/?$/) const prompt = page.locator(promptSelector) - await prompt.click() - await page.keyboard.type("line one") - await page.keyboard.press("Shift+Enter") - await page.keyboard.type("line two") + await prompt.focus() + await expect(prompt).toBeFocused() + + await prompt.pressSequentially("line one") + await expect(prompt).toBeFocused() + + await prompt.press("Shift+Enter") + await expect(page).toHaveURL(/\/session\/?$/) + await expect(prompt).toBeFocused() + + await prompt.pressSequentially("line two") await expect(page).toHaveURL(/\/session\/?$/) - await expect(prompt).toContainText("line one") - await expect(prompt).toContainText("line two") + await expect.poll(() => prompt.evaluate((el) => el.innerText)).toBe("line one\nline two") }) diff --git a/packages/app/e2e/prompt/prompt-slash-terminal.spec.ts b/packages/app/e2e/prompt/prompt-slash-terminal.spec.ts index 100d1878ab..466b3ba1bb 100644 --- a/packages/app/e2e/prompt/prompt-slash-terminal.spec.ts +++ b/packages/app/e2e/prompt/prompt-slash-terminal.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../fixtures" -import { waitTerminalReady } from "../actions" +import { runPromptSlash, waitTerminalFocusIdle } from "../actions" import { promptSelector, terminalSelector } from "../selectors" test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => { @@ -7,29 +7,12 @@ test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => { const prompt = page.locator(promptSelector) const terminal = page.locator(terminalSelector) - const slash = page.locator('[data-slash-id="terminal.toggle"]').first() await expect(terminal).not.toBeVisible() - await prompt.fill("/terminal") - await expect(slash).toBeVisible() - await page.keyboard.press("Enter") - await waitTerminalReady(page, { term: terminal }) + await runPromptSlash(page, { prompt, text: "/terminal", id: "terminal.toggle" }) + await waitTerminalFocusIdle(page, { term: terminal }) - // Terminal panel retries focus (immediate, RAF, 120ms, 240ms) after opening, - // which can steal focus from the prompt and prevent fill() from triggering - // the slash popover. Re-attempt click+fill until all retries are exhausted - // and the popover appears. - await expect - .poll( - async () => { - await prompt.click().catch(() => false) - await prompt.fill("/terminal").catch(() => false) - return slash.isVisible().catch(() => false) - }, - { timeout: 10_000 }, - ) - .toBe(true) - await page.keyboard.press("Enter") + await runPromptSlash(page, { prompt, text: "/terminal", id: "terminal.toggle" }) await expect(terminal).not.toBeVisible() }) diff --git a/packages/app/e2e/session/session-model-persistence.spec.ts b/packages/app/e2e/session/session-model-persistence.spec.ts index 933d5e6f96..2c2e4e886d 100644 --- a/packages/app/e2e/session/session-model-persistence.spec.ts +++ b/packages/app/e2e/session/session-model-persistence.spec.ts @@ -1,7 +1,6 @@ -import { base64Decode } from "@opencode-ai/util/encode" import type { Locator, Page } from "@playwright/test" import { test, expect } from "../fixtures" -import { openSidebar, sessionIDFromUrl, setWorkspacesEnabled, waitSessionIdle, waitSlug } from "../actions" +import { openSidebar, resolveSlug, sessionIDFromUrl, setWorkspacesEnabled, waitSessionIdle, waitSlug } from "../actions" import { promptAgentSelector, promptModelSelector, @@ -224,10 +223,9 @@ async function createWorkspace(page: Page, root: string, seen: string[]) { await openSidebar(page) await page.getByRole("button", { name: "New workspace" }).first().click() - const slug = await waitSlug(page, [root, ...seen]) - const directory = base64Decode(slug) - if (!directory) throw new Error(`Failed to decode workspace slug: ${slug}`) - return { slug, directory } + const next = await resolveSlug(await waitSlug(page, [root, ...seen])) + await expect(page).toHaveURL(new RegExp(`/${next.slug}/session(?:[/?#]|$)`)) + return next } async function waitWorkspace(page: Page, slug: string) { @@ -257,8 +255,8 @@ async function newWorkspaceSession(page: Page, slug: string) { await expect(button).toBeVisible() await button.click({ force: true }) - const next = await waitSlug(page) - await expect(page).toHaveURL(new RegExp(`/${next}/session(?:[/?#]|$)`)) + const next = await resolveSlug(await waitSlug(page)) + await expect(page).toHaveURL(new RegExp(`/${next.slug}/session(?:[/?#]|$)`)) await expect(page.locator(promptSelector)).toBeVisible() return currentDir(page) } diff --git a/packages/app/e2e/session/session-review.spec.ts b/packages/app/e2e/session/session-review.spec.ts index 28c85edb0b..c0421f0283 100644 --- a/packages/app/e2e/session/session-review.spec.ts +++ b/packages/app/e2e/session/session-review.spec.ts @@ -123,6 +123,101 @@ async function spot(page: Parameters[0]["page"], file: string) { }, file) } +async function comment(page: Parameters[0]["page"], file: string, note: string) { + const row = page.locator(`[data-file="${file}"]`).first() + await expect(row).toBeVisible() + + const line = row.locator('diffs-container [data-line="2"]').first() + await expect(line).toBeVisible() + await line.hover() + + const add = row.getByRole("button", { name: /^Comment$/ }).first() + await expect(add).toBeVisible() + await add.click() + + const area = row.locator('[data-slot="line-comment-textarea"]').first() + await expect(area).toBeVisible() + await area.fill(note) + + const submit = row.locator('[data-slot="line-comment-action"][data-variant="primary"]').first() + await expect(submit).toBeEnabled() + await submit.click() + + await expect(row.locator('[data-slot="line-comment-content"]').filter({ hasText: note }).first()).toBeVisible() + await expect(row.locator('[data-slot="line-comment-tools"]').first()).toBeVisible() +} + +async function overflow(page: Parameters[0]["page"], file: string) { + const row = page.locator(`[data-file="${file}"]`).first() + const view = page.locator('[data-slot="session-review-scroll"] .scroll-view__viewport').first() + const pop = row.locator('[data-slot="line-comment-popover"][data-inline-body]').first() + const tools = row.locator('[data-slot="line-comment-tools"]').first() + + const [width, viewBox, popBox, toolsBox] = await Promise.all([ + view.evaluate((el) => el.scrollWidth - el.clientWidth), + view.boundingBox(), + pop.boundingBox(), + tools.boundingBox(), + ]) + + if (!viewBox || !popBox || !toolsBox) return null + + return { + width, + pop: popBox.x + popBox.width - (viewBox.x + viewBox.width), + tools: toolsBox.x + toolsBox.width - (viewBox.x + viewBox.width), + } +} + +test("review applies inline comment clicks without horizontal overflow", async ({ page, withProject }) => { + test.setTimeout(180_000) + + const tag = `review-comment-${Date.now()}` + const file = `review-comment-${tag}.txt` + const note = `comment ${tag}` + + await page.setViewportSize({ width: 1280, height: 900 }) + + await withProject(async (project) => { + const sdk = createSdk(project.directory) + + await withSession(sdk, `e2e review comment ${tag}`, async (session) => { + await patch(sdk, session.id, seed([{ file, mark: tag }])) + + await expect + .poll( + async () => { + const diff = await sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? []) + return diff.length + }, + { timeout: 60_000 }, + ) + .toBe(1) + + await project.gotoSession(session.id) + await show(page) + + const tab = page.getByRole("tab", { name: /Review/i }).first() + await expect(tab).toBeVisible() + await tab.click() + + await expand(page) + await waitMark(page, file, tag) + await comment(page, file, note) + + await expect + .poll(async () => (await overflow(page, file))?.width ?? Number.POSITIVE_INFINITY, { timeout: 10_000 }) + .toBeLessThanOrEqual(1) + await expect + .poll(async () => (await overflow(page, file))?.pop ?? Number.POSITIVE_INFINITY, { timeout: 10_000 }) + .toBeLessThanOrEqual(1) + await expect + .poll(async () => (await overflow(page, file))?.tools ?? Number.POSITIVE_INFINITY, { timeout: 10_000 }) + .toBeLessThanOrEqual(1) + }) + }) +}) + test("review keeps scroll position after a live diff update", async ({ page, withProject }) => { test.skip(Boolean(process.env.CI), "Flaky in CI for now.") test.setTimeout(180_000) diff --git a/packages/app/e2e/settings/settings-keybinds.spec.ts b/packages/app/e2e/settings/settings-keybinds.spec.ts index 9fc2a50ad3..5789dc0eb0 100644 --- a/packages/app/e2e/settings/settings-keybinds.spec.ts +++ b/packages/app/e2e/settings/settings-keybinds.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../fixtures" -import { openSettings, closeDialog, waitTerminalReady, withSession } from "../actions" +import { openSettings, closeDialog, waitTerminalFocusIdle, withSession } from "../actions" import { keybindButtonSelector, terminalSelector } from "../selectors" import { modKey } from "../utils" @@ -302,7 +302,7 @@ test("changing terminal toggle keybind works", async ({ page, gotoSession }) => await expect(terminal).not.toBeVisible() await page.keyboard.press(`${modKey}+Y`) - await waitTerminalReady(page, { term: terminal }) + await waitTerminalFocusIdle(page, { term: terminal }) await page.keyboard.press(`${modKey}+Y`) await expect(terminal).not.toBeVisible() diff --git a/packages/app/e2e/terminal/terminal-init.spec.ts b/packages/app/e2e/terminal/terminal-init.spec.ts index d9bbfa2bed..689d0436a5 100644 --- a/packages/app/e2e/terminal/terminal-init.spec.ts +++ b/packages/app/e2e/terminal/terminal-init.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../fixtures" -import { waitTerminalReady } from "../actions" +import { waitTerminalFocusIdle, waitTerminalReady } from "../actions" import { promptSelector, terminalSelector } from "../selectors" import { terminalToggleKey } from "../utils" @@ -14,7 +14,7 @@ test("smoke terminal mounts and can create a second tab", async ({ page, gotoSes await page.keyboard.press(terminalToggleKey) } - await waitTerminalReady(page, { term: terminals.first() }) + await waitTerminalFocusIdle(page, { term: terminals.first() }) await expect(terminals).toHaveCount(1) // Ghostty captures a lot of keybinds when focused; move focus back diff --git a/packages/app/package.json b/packages/app/package.json index 878cfb8e31..545d313098 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -56,7 +56,7 @@ "@solidjs/router": "catalog:", "@thisbeyond/solid-dnd": "0.7.5", "diff": "catalog:", - "effect": "4.0.0-beta.31", + "effect": "catalog:", "fuzzysort": "catalog:", "ghostty-web": "github:anomalyco/ghostty-web#main", "luxon": "catalog:", diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index e370862212..9a282bbb70 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -46,21 +46,13 @@ import Layout from "@/pages/layout" import { ErrorPage } from "./pages/error" import { useCheckServerHealth } from "./utils/server-health" -const Home = lazy(() => import("@/pages/home")) +const HomeRoute = lazy(() => import("@/pages/home")) const Session = lazy(() => import("@/pages/session")) const Loading = () =>
-const HomeRoute = () => ( - }> - - -) - const SessionRoute = () => ( - }> - - + ) @@ -124,8 +116,10 @@ function SessionProviders(props: ParentProps) { function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) { return ( - {props.appChildren} - {props.children} + }> + {props.appChildren} + {props.children} + ) } @@ -265,6 +259,15 @@ function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key: ) } +function ServerKey(props: ParentProps) { + const server = useServer() + return ( + + {props.children} + + ) +} + export function AppInterface(props: { children?: JSX.Element defaultServer: ServerConnection.Key @@ -275,20 +278,22 @@ export function AppInterface(props: { return ( - - - {routerProps.children}} - > - - - - - - - - + + + + {routerProps.children}} + > + + + + + + + + + ) diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index b042205cf4..e4fe9e7c4e 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -15,7 +15,6 @@ import { Link } from "@/components/link" import { useLanguage } from "@/context/language" import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" -import { usePlatform } from "@/context/platform" import { DialogSelectModel } from "./dialog-select-model" import { DialogSelectProvider } from "./dialog-select-provider" @@ -23,7 +22,6 @@ export function DialogConnectProvider(props: { provider: string }) { const dialog = useDialog() const globalSync = useGlobalSync() const globalSDK = useGlobalSDK() - const platform = usePlatform() const language = useLanguage() const alive = { value: true } @@ -49,13 +47,14 @@ export function DialogConnectProvider(props: { provider: string }) { const [store, setStore] = createStore({ methodIndex: undefined as undefined | number, authorization: undefined as undefined | ProviderAuthAuthorization, - state: "pending" as undefined | "pending" | "complete" | "error", + state: "pending" as undefined | "pending" | "complete" | "error" | "prompt", error: undefined as string | undefined, }) type Action = | { type: "method.select"; index: number } | { type: "method.reset" } + | { type: "auth.prompt" } | { type: "auth.pending" } | { type: "auth.complete"; authorization: ProviderAuthAuthorization } | { type: "auth.error"; error: string } @@ -77,6 +76,11 @@ export function DialogConnectProvider(props: { provider: string }) { draft.error = undefined return } + if (action.type === "auth.prompt") { + draft.state = "prompt" + draft.error = undefined + return + } if (action.type === "auth.pending") { draft.state = "pending" draft.error = undefined @@ -120,7 +124,7 @@ export function DialogConnectProvider(props: { provider: string }) { return fallback } - async function selectMethod(index: number) { + async function selectMethod(index: number, inputs?: Record) { if (timer.current !== undefined) { clearTimeout(timer.current) timer.current = undefined @@ -130,6 +134,10 @@ export function DialogConnectProvider(props: { provider: string }) { dispatch({ type: "method.select", index }) if (method.type === "oauth") { + if (method.prompts?.length && !inputs) { + dispatch({ type: "auth.prompt" }) + return + } dispatch({ type: "auth.pending" }) const start = Date.now() await globalSDK.client.provider.oauth @@ -137,6 +145,7 @@ export function DialogConnectProvider(props: { provider: string }) { { providerID: props.provider, method: index, + inputs, }, { throwOnError: true }, ) @@ -163,6 +172,122 @@ export function DialogConnectProvider(props: { provider: string }) { } } + function OAuthPromptsView() { + const [formStore, setFormStore] = createStore({ + value: {} as Record, + index: 0, + }) + + const prompts = createMemo(() => method()?.prompts ?? []) + const matches = (prompt: NonNullable[number]>, value: Record) => { + if (!prompt.when) return true + const actual = value[prompt.when.key] + if (actual === undefined) return false + return prompt.when.op === "eq" ? actual === prompt.when.value : actual !== prompt.when.value + } + const current = createMemo(() => { + const all = prompts() + const index = all.findIndex((prompt, index) => index >= formStore.index && matches(prompt, formStore.value)) + if (index === -1) return + return { + index, + prompt: all[index], + } + }) + const valid = createMemo(() => { + const item = current() + if (!item || item.prompt.type !== "text") return false + const value = formStore.value[item.prompt.key] ?? "" + return value.trim().length > 0 + }) + + async function next(index: number, value: Record) { + if (store.methodIndex === undefined) return + const next = prompts().findIndex((prompt, i) => i > index && matches(prompt, value)) + if (next !== -1) { + setFormStore("index", next) + return + } + await selectMethod(store.methodIndex, value) + } + + async function handleSubmit(e: SubmitEvent) { + e.preventDefault() + const item = current() + if (!item || item.prompt.type !== "text") return + if (!valid()) return + await next(item.index, formStore.value) + } + + const item = () => current() + const text = createMemo(() => { + const prompt = item()?.prompt + if (!prompt || prompt.type !== "text") return + return prompt + }) + const select = createMemo(() => { + const prompt = item()?.prompt + if (!prompt || prompt.type !== "select") return + return prompt + }) + + return ( +
+ + + { + const prompt = text() + if (!prompt) return + setFormStore("value", prompt.key, value) + }} + /> + + + +
+
{select()?.message}
+
+ x.value} + current={select()?.options.find((x) => x.value === formStore.value[select()!.key])} + onSelect={(value) => { + if (!value) return + const prompt = select() + if (!prompt) return + const nextValue = { + ...formStore.value, + [prompt.key]: value.value, + } + setFormStore("value", prompt.key, value.value) + void next(item()!.index, nextValue) + }} + > + {(option) => ( +
+
+ + {option.label} + {option.hint} +
+ )} + +
+
+ + + + ) + } + let listRef: ListRef | undefined function handleKey(e: KeyboardEvent) { if (e.key === "Enter" && e.target instanceof HTMLInputElement) { @@ -301,7 +426,7 @@ export function DialogConnectProvider(props: { provider: string }) { error={formStore.error} />
@@ -314,12 +439,6 @@ export function DialogConnectProvider(props: { provider: string }) { error: undefined as string | undefined, }) - onMount(() => { - if (store.authorization?.method === "code" && store.authorization?.url) { - platform.openLink(store.authorization.url) - } - }) - async function handleSubmit(e: SubmitEvent) { e.preventDefault() @@ -368,7 +487,7 @@ export function DialogConnectProvider(props: { provider: string }) { error={formStore.error} />
@@ -386,10 +505,6 @@ export function DialogConnectProvider(props: { provider: string }) { onMount(() => { void (async () => { - if (store.authorization?.url) { - platform.openLink(store.authorization.url) - } - const result = await globalSDK.client.provider.oauth .callback({ providerID: props.provider, @@ -470,6 +585,9 @@ export function DialogConnectProvider(props: { provider: string }) { + + +
diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index eb039c14d6..f8d14cbb94 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -291,8 +291,8 @@ export function DialogSelectServer() { navigate("/") return } - server.setActive(ServerConnection.key(conn)) navigate("/") + queueMicrotask(() => server.setActive(ServerConnection.key(conn))) } const handleAddChange = (value: string) => { diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index b2553e4c02..55cfaa490f 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1,6 +1,6 @@ import { useFilteredList } from "@opencode-ai/ui/hooks" import { useSpring } from "@opencode-ai/ui/motion-spring" -import { createEffect, on, Component, Show, onCleanup, Switch, Match, createMemo, createSignal } from "solid-js" +import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal } from "solid-js" import { createStore } from "solid-js/store" import { useLocal } from "@/context/local" import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file" @@ -36,6 +36,7 @@ import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { useSessionLayout } from "@/pages/session/session-layout" import { createSessionTabs } from "@/pages/session/helpers" +import { promptEnabled, promptProbe } from "@/testing/prompt" import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom" import { createPromptAttachments } from "./prompt-input/attachments" import { ACCEPTED_FILE_TYPES } from "./prompt-input/files" @@ -243,6 +244,23 @@ export const PromptInput: Component = (props) => { }, ) const working = createMemo(() => status()?.type !== "idle") + const tip = () => { + if (working()) { + return ( +
+ {language.t("prompt.action.stop")} + {language.t("common.key.esc")} +
+ ) + } + + return ( +
+ {language.t("prompt.action.send")} + +
+ ) + } const imageAttachments = createMemo(() => prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"), ) @@ -604,6 +622,7 @@ export const PromptInput: Component = (props) => { const handleSlashSelect = (cmd: SlashCommand | undefined) => { if (!cmd) return + promptProbe.select(cmd.id) closePopover() if (cmd.type === "custom") { @@ -692,6 +711,20 @@ export const PromptInput: Component = (props) => { }) }) + if (promptEnabled()) { + createEffect(() => { + promptProbe.set({ + popover: store.popover, + slash: { + active: slashActive() ?? null, + ids: slashFlat().map((cmd) => cmd.id), + }, + }) + }) + + onCleanup(() => promptProbe.clear()) + } + const selectPopoverActive = () => { if (store.popover === "at") { const items = atFlat() @@ -1208,6 +1241,20 @@ export const PromptInput: Component = (props) => { // Note: Shift+Enter is handled earlier, before IME check if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault() + if (event.repeat) return + if ( + working() && + prompt + .current() + .map((part) => ("content" in part ? part.content : "")) + .join("") + .trim().length === 0 && + imageAttachments().length === 0 && + commentCount() === 0 + ) { + return + } handleSubmit(event) } } @@ -1346,26 +1393,7 @@ export const PromptInput: Component = (props) => { />
- - -
- {language.t("prompt.action.stop")} - {language.t("common.key.esc")} -
-
- -
- {language.t("prompt.action.send")} - -
-
- - } - > +
diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx index 2c6799d12b..65805f40c8 100644 --- a/packages/app/src/context/command.tsx +++ b/packages/app/src/context/command.tsx @@ -1,10 +1,10 @@ -import { createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js" -import { createStore } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" import { useDialog } from "@opencode-ai/ui/context/dialog" -import { dict as en } from "@/i18n/en" +import { type Accessor, createEffect, createMemo, onCleanup, onMount } from "solid-js" +import { createStore } from "solid-js/store" import { useLanguage } from "@/context/language" import { useSettings } from "@/context/settings" +import { dict as en } from "@/i18n/en" import { Persist, persisted } from "@/utils/persist" const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) @@ -238,9 +238,10 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex }) const warnedDuplicates = new Set() + type CommandCatalog = Record const [catalog, setCatalog, _, catalogReady] = persisted( Persist.global("command.catalog.v1"), - createStore>({}), + createStore({}), ) const bind = (id: string, def: KeybindConfig | undefined) => { @@ -259,7 +260,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex if (seen.has(opt.id)) { if (import.meta.env.DEV && !warnedDuplicates.has(opt.id)) { warnedDuplicates.add(opt.id) - console.warn(`[command] duplicate command id \"${opt.id}\" registered; keeping first entry`) + console.warn(`[command] duplicate command id "${opt.id}" registered; keeping first entry`) } continue } @@ -274,16 +275,19 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex createEffect(() => { if (!catalogReady()) return - for (const opt of registered()) { - const id = actionId(opt.id) - setCatalog(id, { - title: opt.title, - description: opt.description, - category: opt.category, - keybind: opt.keybind, - slash: opt.slash, - }) - } + setCatalog( + registered().reduce((acc, opt) => { + const id = actionId(opt.id) + acc[id] = { + title: opt.title, + description: opt.description, + category: opt.category, + keybind: opt.keybind, + slash: opt.slash, + } + return acc + }, {} as CommandCatalog), + ) }) const catalogOptions = createMemo(() => Object.entries(catalog).map(([id, meta]) => ({ id, ...meta }))) diff --git a/packages/app/src/context/prompt.tsx b/packages/app/src/context/prompt.tsx index 4aeb05e618..831fdbca83 100644 --- a/packages/app/src/context/prompt.tsx +++ b/packages/app/src/context/prompt.tsx @@ -1,10 +1,10 @@ -import { createStore, type SetStoreFunction } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" -import { batch, createMemo, createRoot, onCleanup } from "solid-js" +import { checksum } from "@opencode-ai/util/encode" import { useParams } from "@solidjs/router" +import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js" +import { createStore, type SetStoreFunction } from "solid-js/store" import type { FileSelection } from "@/context/file" import { Persist, persisted } from "@/utils/persist" -import { checksum } from "@opencode-ai/util/encode" interface PartBase { content: string @@ -250,6 +250,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext( } } + const owner = getOwner() const load = (dir: string, id: string | undefined) => { const key = `${dir}:${id ?? WORKSPACE_KEY}` const existing = cache.get(key) @@ -259,10 +260,13 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext( return existing.value } - const entry = createRoot((dispose) => ({ - value: createPromptSession(dir, id), - dispose, - })) + const entry = createRoot( + (dispose) => ({ + value: createPromptSession(dir, id), + dispose, + }), + owner, + ) cache.set(key, entry) prune() diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index e65c167884..17355aab9a 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -185,6 +185,60 @@ function createWorkspaceTerminalSession(sdk: ReturnType, dir: str }) onCleanup(unsub) + const update = (client: ReturnType["client"], pty: Partial & { id: string }) => { + const index = store.all.findIndex((x) => x.id === pty.id) + const previous = index >= 0 ? store.all[index] : undefined + if (index >= 0) { + setStore("all", index, (item) => ({ ...item, ...pty })) + } + client.pty + .update({ + ptyID: pty.id, + title: pty.title, + size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined, + }) + .catch((error: unknown) => { + if (previous) { + const currentIndex = store.all.findIndex((item) => item.id === pty.id) + if (currentIndex >= 0) setStore("all", currentIndex, previous) + } + console.error("Failed to update terminal", error) + }) + } + + const clone = async (client: ReturnType["client"], id: string) => { + const index = store.all.findIndex((x) => x.id === id) + const pty = store.all[index] + if (!pty) return + const next = await client.pty + .create({ + title: pty.title, + }) + .catch((error: unknown) => { + console.error("Failed to clone terminal", error) + return undefined + }) + if (!next?.data) return + + const active = store.active === pty.id + + batch(() => { + setStore("all", index, { + id: next.data.id, + title: next.data.title ?? pty.title, + titleNumber: pty.titleNumber, + buffer: undefined, + cursor: undefined, + scrollY: undefined, + rows: undefined, + cols: undefined, + }) + if (active) { + setStore("active", next.data.id) + } + }) + } + return { ready, all: createMemo(() => store.all), @@ -216,24 +270,7 @@ function createWorkspaceTerminalSession(sdk: ReturnType, dir: str }) }, update(pty: Partial & { id: string }) { - const index = store.all.findIndex((x) => x.id === pty.id) - const previous = index >= 0 ? store.all[index] : undefined - if (index >= 0) { - setStore("all", index, (item) => ({ ...item, ...pty })) - } - sdk.client.pty - .update({ - ptyID: pty.id, - title: pty.title, - size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined, - }) - .catch((error: unknown) => { - if (previous) { - const currentIndex = store.all.findIndex((item) => item.id === pty.id) - if (currentIndex >= 0) setStore("all", currentIndex, previous) - } - console.error("Failed to update terminal", error) - }) + update(sdk.client, pty) }, trim(id: string) { const index = store.all.findIndex((x) => x.id === id) @@ -248,37 +285,23 @@ function createWorkspaceTerminalSession(sdk: ReturnType, dir: str }) }, async clone(id: string) { - const index = store.all.findIndex((x) => x.id === id) - const pty = store.all[index] - if (!pty) return - const clone = await sdk.client.pty - .create({ - title: pty.title, - }) - .catch((error: unknown) => { - console.error("Failed to clone terminal", error) - return undefined - }) - if (!clone?.data) return - - const active = store.active === pty.id - - batch(() => { - setStore("all", index, { - id: clone.data.id, - title: clone.data.title ?? pty.title, - titleNumber: pty.titleNumber, - // New PTY process, so start clean. - buffer: undefined, - cursor: undefined, - scrollY: undefined, - rows: undefined, - cols: undefined, - }) - if (active) { - setStore("active", clone.data.id) - } - }) + await clone(sdk.client, id) + }, + bind() { + const client = sdk.client + return { + trim(id: string) { + const index = store.all.findIndex((x) => x.id === id) + if (index === -1) return + setStore("all", index, (pty) => trimTerminal(pty)) + }, + update(pty: Partial & { id: string }) { + update(client, pty) + }, + async clone(id: string) { + await clone(client, id) + }, + } }, open(id: string) { setStore("active", id) @@ -403,6 +426,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont trim: (id: string) => workspace().trim(id), trimAll: () => workspace().trimAll(), clone: (id: string) => workspace().clone(id), + bind: () => workspace(), open: (id: string) => workspace().open(id), close: (id: string) => workspace().close(id), move: (id: string, to: number) => workspace().move(id, to), diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 720045a4d1..c8f58c796e 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -204,6 +204,7 @@ export const dict = { "common.cancel": "إلغاء", "common.connect": "اتصال", "common.disconnect": "قطع الاتصال", + "common.continue": "إرسال", "common.submit": "إرسال", "common.save": "حفظ", "common.saving": "جارٍ الحفظ...", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index a7d7433b02..3112e91bbe 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -204,6 +204,7 @@ export const dict = { "common.cancel": "Cancelar", "common.connect": "Conectar", "common.disconnect": "Desconectar", + "common.continue": "Enviar", "common.submit": "Enviar", "common.save": "Salvar", "common.saving": "Salvando...", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index ccdf2b6044..f2dbd8493c 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -221,6 +221,7 @@ export const dict = { "common.cancel": "Otkaži", "common.connect": "Poveži", "common.disconnect": "Prekini vezu", + "common.continue": "Pošalji", "common.submit": "Pošalji", "common.save": "Sačuvaj", "common.saving": "Čuvanje...", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index f1701094b5..e90e1071ad 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -219,6 +219,7 @@ export const dict = { "common.cancel": "Annuller", "common.connect": "Forbind", "common.disconnect": "Frakobl", + "common.continue": "Indsend", "common.submit": "Indsend", "common.save": "Gem", "common.saving": "Gemmer...", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 2dfeed7203..69658b29e9 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -209,6 +209,7 @@ export const dict = { "common.cancel": "Abbrechen", "common.connect": "Verbinden", "common.disconnect": "Trennen", + "common.continue": "Absenden", "common.submit": "Absenden", "common.save": "Speichern", "common.saving": "Speichert...", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index ad12e1e0de..72caed40ad 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -221,6 +221,7 @@ export const dict = { "common.open": "Open", "common.connect": "Connect", "common.disconnect": "Disconnect", + "common.continue": "Continue", "common.submit": "Submit", "common.save": "Save", "common.saving": "Saving...", @@ -674,6 +675,8 @@ export const dict = { "sidebar.project.recentSessions": "Recent sessions", "sidebar.project.viewAllSessions": "View all sessions", "sidebar.project.clearNotifications": "Clear notifications", + "sidebar.empty.title": "No projects open", + "sidebar.empty.description": "Open a project to get started", "debugBar.ariaLabel": "Development performance diagnostics", "debugBar.na": "n/a", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 1cd47dfc79..9e36e4de6d 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -220,6 +220,7 @@ export const dict = { "common.cancel": "Cancelar", "common.connect": "Conectar", "common.disconnect": "Desconectar", + "common.continue": "Enviar", "common.submit": "Enviar", "common.save": "Guardar", "common.saving": "Guardando...", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index c7d89c3251..f53b3882c6 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -204,6 +204,7 @@ export const dict = { "common.cancel": "Annuler", "common.connect": "Connecter", "common.disconnect": "Déconnecter", + "common.continue": "Soumettre", "common.submit": "Soumettre", "common.save": "Enregistrer", "common.saving": "Enregistrement...", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 267411083f..d66a7341d5 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -203,6 +203,7 @@ export const dict = { "common.cancel": "キャンセル", "common.connect": "接続", "common.disconnect": "切断", + "common.continue": "送信", "common.submit": "送信", "common.save": "保存", "common.saving": "保存中...", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index bb57f99396..d534c27e8f 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -207,6 +207,7 @@ export const dict = { "common.cancel": "취소", "common.connect": "연결", "common.disconnect": "연결 해제", + "common.continue": "제출", "common.submit": "제출", "common.save": "저장", "common.saving": "저장 중...", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 83d6a9903b..c23d0a2792 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -223,6 +223,7 @@ export const dict = { "common.cancel": "Avbryt", "common.connect": "Koble til", "common.disconnect": "Koble fra", + "common.continue": "Send inn", "common.submit": "Send inn", "common.save": "Lagre", "common.saving": "Lagrer...", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index db9ef18003..dac847b217 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -205,6 +205,7 @@ export const dict = { "common.cancel": "Anuluj", "common.connect": "Połącz", "common.disconnect": "Rozłącz", + "common.continue": "Prześlij", "common.submit": "Prześlij", "common.save": "Zapisz", "common.saving": "Zapisywanie...", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index e1abb6e6cf..684d5deecd 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -220,6 +220,7 @@ export const dict = { "common.cancel": "Отмена", "common.connect": "Подключить", "common.disconnect": "Отключить", + "common.continue": "Отправить", "common.submit": "Отправить", "common.save": "Сохранить", "common.saving": "Сохранение...", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index b522e4631b..80f0da94ec 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -220,6 +220,7 @@ export const dict = { "common.cancel": "ยกเลิก", "common.connect": "เชื่อมต่อ", "common.disconnect": "ยกเลิกการเชื่อมต่อ", + "common.continue": "ส่ง", "common.submit": "ส่ง", "common.save": "บันทึก", "common.saving": "กำลังบันทึก...", diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts index 8542dff799..9041e0dd07 100644 --- a/packages/app/src/i18n/tr.ts +++ b/packages/app/src/i18n/tr.ts @@ -225,6 +225,7 @@ export const dict = { "common.cancel": "İptal", "common.connect": "Bağlan", "common.disconnect": "Bağlantı Kes", + "common.continue": "Gönder", "common.submit": "Gönder", "common.save": "Kaydet", "common.saving": "Kaydediliyor...", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index e762ba78d9..cf64ca9b2c 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -242,6 +242,7 @@ export const dict = { "common.cancel": "取消", "common.connect": "连接", "common.disconnect": "断开连接", + "common.continue": "提交", "common.submit": "提交", "common.save": "保存", "common.saving": "保存中...", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 184c789ce3..02c00d17a2 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -220,6 +220,7 @@ export const dict = { "common.cancel": "取消", "common.connect": "連線", "common.disconnect": "中斷連線", + "common.continue": "提交", "common.submit": "提交", "common.save": "儲存", "common.saving": "儲存中...", diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx index f993ffcd89..cd5e079a69 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -1,16 +1,15 @@ -import { batch, createEffect, createMemo, Show, type ParentProps } from "solid-js" -import { createStore } from "solid-js/store" +import { DataProvider } from "@opencode-ai/ui/context" +import { showToast } from "@opencode-ai/ui/toast" +import { base64Encode } from "@opencode-ai/util/encode" import { useLocation, useNavigate, useParams } from "@solidjs/router" +import { createMemo, createResource, type ParentProps, Show } from "solid-js" +import { useGlobalSDK } from "@/context/global-sdk" +import { useLanguage } from "@/context/language" +import { LocalProvider } from "@/context/local" import { SDKProvider } from "@/context/sdk" import { SyncProvider, useSync } from "@/context/sync" -import { LocalProvider } from "@/context/local" -import { useGlobalSDK } from "@/context/global-sdk" - -import { DataProvider } from "@opencode-ai/ui/context" -import { base64Encode } from "@opencode-ai/util/encode" import { decode64 } from "@/utils/base64" -import { showToast } from "@opencode-ai/ui/toast" -import { useLanguage } from "@/context/language" + function DirectoryDataProvider(props: ParentProps<{ directory: string }>) { const navigate = useNavigate() const sync = useSync() @@ -30,57 +29,53 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) { export default function Layout(props: ParentProps) { const params = useParams() - const navigate = useNavigate() const location = useLocation() const language = useLanguage() const globalSDK = useGlobalSDK() - const directory = createMemo(() => decode64(params.dir) ?? "") - const [state, setState] = createStore({ invalid: "", resolved: "" }) + const navigate = useNavigate() + let invalid = "" - createEffect(() => { - if (!params.dir) return - const raw = directory() - if (!raw) { - if (state.invalid === params.dir) return - setState("invalid", params.dir) - showToast({ - variant: "error", - title: language.t("common.requestFailed"), - description: language.t("directory.error.invalidUrl"), - }) - navigate("/", { replace: true }) - return - } + const [resolved] = createResource( + () => { + if (params.dir) return [location.pathname, params.dir] as const + }, + async ([pathname, b64Dir]) => { + const directory = decode64(b64Dir) - const current = params.dir - globalSDK - .createClient({ - directory: raw, - throwOnError: true, - }) - .path.get() - .then((x) => { - if (params.dir !== current) return - const next = x.data?.directory ?? raw - batch(() => { - setState("invalid", "") - setState("resolved", next) + if (!directory) { + if (invalid === params.dir) return + invalid = b64Dir + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: language.t("directory.error.invalidUrl"), }) - if (next === raw) return - const path = location.pathname.slice(current.length + 1) - navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true }) - }) - .catch(() => { - if (params.dir !== current) return - batch(() => { - setState("invalid", "") - setState("resolved", raw) + navigate("/", { replace: true }) + return + } + + return await globalSDK + .createClient({ + directory, + throwOnError: true, }) - }) - }) + .path.get() + .then((x) => { + const next = x.data?.directory ?? directory + invalid = "" + if (next === directory) return next + const path = pathname.slice(b64Dir.length + 1) + navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true }) + }) + .catch(() => { + invalid = "" + return directory + }) + }, + ) return ( - + {(resolved) => ( resolved}> diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index ab2687dcab..52ac7c5f37 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -2,6 +2,7 @@ import { batch, createEffect, createMemo, + createResource, For, on, onCleanup, @@ -277,16 +278,6 @@ export default function Layout(props: ParentProps) { setHoverProject(undefined) }) - const autoselecting = createMemo(() => { - if (params.dir) return false - if (!state.autoselect) return false - if (!pageReady()) return true - if (!layoutReady()) return true - const list = layout.projects.list() - if (list.length > 0) return true - return !!server.projects.last() - }) - createEffect(() => { if (!state.autoselect) return const dir = params.dir @@ -552,13 +543,14 @@ export default function Layout(props: ParentProps) { const currentProject = createMemo(() => { const directory = currentDir() if (!directory) return + const key = workspaceKey(directory) const projects = layout.projects.list() - const sandbox = projects.find((p) => p.sandboxes?.includes(directory)) + const sandbox = projects.find((p) => p.sandboxes?.some((item) => workspaceKey(item) === key)) if (sandbox) return sandbox - const direct = projects.find((p) => p.worktree === directory) + const direct = projects.find((p) => workspaceKey(p.worktree) === key) if (direct) return direct const [child] = globalSync.child(directory, { bootstrap: false }) @@ -572,33 +564,23 @@ export default function Layout(props: ParentProps) { return projects.find((p) => p.worktree === root) }) - createEffect( - on( - () => ({ ready: pageReady(), layoutReady: layoutReady(), dir: params.dir, list: layout.projects.list() }), - (value) => { - if (!value.ready) return - if (!value.layoutReady) return - if (!state.autoselect) return - if (value.dir) return + const [autoselecting] = createResource(async () => { + await ready.promise + await layout.ready.promise + if (!untrack(() => state.autoselect)) return - const last = server.projects.last() + const list = layout.projects.list() + const last = server.projects.last() - if (value.list.length === 0) { - if (!last) return - setState("autoselect", false) - openProject(last, false) - navigateToProject(last) - return - } - - const next = value.list.find((project) => project.worktree === last) ?? value.list[0] - if (!next) return - setState("autoselect", false) - openProject(next.worktree, false) - navigateToProject(next.worktree) - }, - ), - ) + if (list.length === 0) { + if (!last) return + await openProject(last, true) + } else { + const next = list.find((project) => project.worktree === last) ?? list[0] + if (!next) return + await openProject(next.worktree, true) + } + }) const workspaceName = (directory: string, projectId?: string, branch?: string) => { const key = workspaceKey(directory) @@ -649,7 +631,11 @@ export default function Layout(props: ParentProps) { const projects = layout.projects.list() for (const [directory, expanded] of Object.entries(store.workspaceExpanded)) { if (!expanded) continue - const project = projects.find((item) => item.worktree === directory || item.sandboxes?.includes(directory)) + const key = workspaceKey(directory) + const project = projects.find( + (item) => + workspaceKey(item.worktree) === key || item.sandboxes?.some((sandbox) => workspaceKey(sandbox) === key), + ) if (!project) continue if (project.vcs === "git" && layout.sidebar.workspaces(project.worktree)()) continue setStore("workspaceExpanded", directory, false) @@ -1174,13 +1160,17 @@ export default function Layout(props: ParentProps) { } function projectRoot(directory: string) { + const key = workspaceKey(directory) const project = layout.projects .list() - .find((item) => item.worktree === directory || item.sandboxes?.includes(directory)) + .find( + (item) => + workspaceKey(item.worktree) === key || item.sandboxes?.some((sandbox) => workspaceKey(sandbox) === key), + ) if (project) return project.worktree const known = Object.entries(store.workspaceOrder).find( - ([root, dirs]) => root === directory || dirs.includes(directory), + ([root, dirs]) => workspaceKey(root) === key || dirs.some((item) => workspaceKey(item) === key), ) if (known) return known[0] @@ -1196,13 +1186,6 @@ export default function Layout(props: ParentProps) { return currentProject()?.worktree ?? projectRoot(directory) } - function touchProjectRoute() { - const root = currentProject()?.worktree - if (!root) return - if (server.projects.last() !== root) server.projects.touch(root) - return root - } - function rememberSessionRoute(directory: string, id: string, root = activeProjectRoot(directory)) { setStore("lastProjectSession", root, { directory, id, at: Date.now() }) return root @@ -1311,7 +1294,7 @@ export default function Layout(props: ParentProps) { function openProject(directory: string, navigate = true) { layout.projects.open(directory) - if (navigate) navigateToProject(directory) + if (navigate) return navigateToProject(directory) } const handleDeepLinks = (urls: string[]) => { @@ -1366,8 +1349,9 @@ export default function Layout(props: ParentProps) { function closeProject(directory: string) { const list = layout.projects.list() - const index = list.findIndex((x) => x.worktree === directory) - const active = currentProject()?.worktree === directory + const key = workspaceKey(directory) + const index = list.findIndex((x) => workspaceKey(x.worktree) === key) + const active = workspaceKey(currentProject()?.worktree ?? "") === key if (index === -1) return const next = list[index + 1] @@ -1702,38 +1686,55 @@ export default function Layout(props: ParentProps) { const activeRoute = { session: "", sessionProject: "", + directory: "", } createEffect( on( - () => [pageReady(), params.dir, params.id, currentProject()?.worktree] as const, - ([ready, dir, id]) => { - if (!ready || !dir) { + () => { + const dir = params.dir + const directory = dir ? decode64(dir) : undefined + const resolved = directory ? globalSync.child(directory, { bootstrap: false })[0].path.directory : "" + return [pageReady(), dir, params.id, currentProject()?.worktree, directory, resolved] as const + }, + ([ready, dir, id, root, directory, resolved]) => { + if (!ready || !dir || !directory) { activeRoute.session = "" activeRoute.sessionProject = "" + activeRoute.directory = "" return } - const directory = decode64(dir) - if (!directory) return - - const root = touchProjectRoute() ?? activeProjectRoot(directory) - if (!id) { activeRoute.session = "" activeRoute.sessionProject = "" + activeRoute.directory = "" return } + const next = resolved || directory const session = `${dir}/${id}` - if (session !== activeRoute.session) { + + if (!root) { activeRoute.session = session - activeRoute.sessionProject = syncSessionRoute(directory, id, root) + activeRoute.directory = next + activeRoute.sessionProject = "" + return + } + + if (server.projects.last() !== root) server.projects.touch(root) + + const changed = session !== activeRoute.session || next !== activeRoute.directory + if (changed) { + activeRoute.session = session + activeRoute.directory = next + activeRoute.sessionProject = syncSessionRoute(next, id, root) return } if (root === activeRoute.sessionProject) return - activeRoute.sessionProject = rememberSessionRoute(directory, id, root) + activeRoute.directory = next + activeRoute.sessionProject = rememberSessionRoute(next, id, root) }, ), ) @@ -1797,8 +1798,13 @@ export default function Layout(props: ParentProps) { const local = project.worktree const dirs = [local, ...(project.sandboxes ?? [])] const active = currentProject() - const directory = active?.worktree === project.worktree ? currentDir() : undefined - const extra = directory && directory !== local && !dirs.includes(directory) ? directory : undefined + const directory = workspaceKey(active?.worktree ?? "") === workspaceKey(project.worktree) ? currentDir() : undefined + const extra = + directory && + workspaceKey(directory) !== workspaceKey(local) && + !dirs.some((item) => workspaceKey(item) === workspaceKey(directory)) + ? directory + : undefined const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false const ordered = effectiveWorkspaceOrder(local, dirs, store.workspaceOrder[project.worktree]) @@ -1959,6 +1965,7 @@ export default function Layout(props: ParentProps) { const merged = createMemo(() => panelProps.mobile || (panelProps.merged ?? layout.sidebar.opened())) const hover = createMemo(() => !panelProps.mobile && panelProps.merged === false && !layout.sidebar.opened()) const popover = createMemo(() => !!panelProps.mobile || panelProps.merged === false || layout.sidebar.opened()) + const empty = createMemo(() => !params.dir && layout.projects.list().length === 0) const projectName = createMemo(() => { const item = project() if (!item) return "" @@ -2011,7 +2018,26 @@ export default function Layout(props: ParentProps) { width: panelProps.mobile ? undefined : `${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px`, }} > - + +
+
+
+
{language.t("sidebar.empty.title")}
+
+ {language.t("sidebar.empty.description")} +
+
+ +
+
+
+ } + > <>
@@ -2260,13 +2286,7 @@ export default function Layout(props: ParentProps) { helpLabel={() => language.t("sidebar.help")} onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")} renderPanel={() => - mobile ? ( - - ) : ( - - - - ) + mobile ? : } /> ) @@ -2367,7 +2387,7 @@ export default function Layout(props: ParentProps) { "size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true, }} > - }> + }> {props.children} diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts index 9dbc6c72d2..1fe52d47a0 100644 --- a/packages/app/src/pages/layout/helpers.test.ts +++ b/packages/app/src/pages/layout/helpers.test.ts @@ -104,14 +104,14 @@ describe("layout deep links", () => { describe("layout workspace helpers", () => { test("normalizes trailing slash in workspace key", () => { expect(workspaceKey("/tmp/demo///")).toBe("/tmp/demo") - expect(workspaceKey("C:\\tmp\\demo\\\\")).toBe("C:\\tmp\\demo") + expect(workspaceKey("C:\\tmp\\demo\\\\")).toBe("C:/tmp/demo") }) test("preserves posix and drive roots in workspace key", () => { expect(workspaceKey("/")).toBe("/") expect(workspaceKey("///")).toBe("/") - expect(workspaceKey("C:\\")).toBe("C:\\") - expect(workspaceKey("C:\\\\\\")).toBe("C:\\") + expect(workspaceKey("C:\\")).toBe("C:/") + expect(workspaceKey("C://")).toBe("C:/") expect(workspaceKey("C:///")).toBe("C:/") }) diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts index be4ce9f574..209cff8a7c 100644 --- a/packages/app/src/pages/layout/helpers.ts +++ b/packages/app/src/pages/layout/helpers.ts @@ -1,11 +1,17 @@ import { getFilename } from "@opencode-ai/util/path" import { type Session } from "@opencode-ai/sdk/v2/client" +type SessionStore = { + session?: Session[] + path: { directory: string } +} + export const workspaceKey = (directory: string) => { - const drive = directory.match(/^([A-Za-z]:)[\\/]+$/) - if (drive) return `${drive[1]}${directory.includes("\\") ? "\\" : "/"}` - if (/^[\\/]+$/.test(directory)) return directory.includes("\\") ? "\\" : "/" - return directory.replace(/[\\/]+$/, "") + const value = directory.replaceAll("\\", "/") + const drive = value.match(/^([A-Za-z]:)\/+$/) + if (drive) return `${drive[1]}/` + if (/^\/+$/i.test(value)) return "/" + return value.replace(/\/+$/, "") } function sortSessions(now: number) { @@ -25,13 +31,13 @@ function sortSessions(now: number) { const isRootVisibleSession = (session: Session, directory: string) => workspaceKey(session.directory) === workspaceKey(directory) && !session.parentID && !session.time?.archived -export const sortedRootSessions = (store: { session: Session[]; path: { directory: string } }, now: number) => - store.session.filter((session) => isRootVisibleSession(session, store.path.directory)).sort(sortSessions(now)) +const roots = (store: SessionStore) => + (store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory)) -export const latestRootSession = (stores: { session: Session[]; path: { directory: string } }[], now: number) => - stores - .flatMap((store) => store.session.filter((session) => isRootVisibleSession(session, store.path.directory))) - .sort(sortSessions(now))[0] +export const sortedRootSessions = (store: SessionStore, now: number) => roots(store).sort(sortSessions(now)) + +export const latestRootSession = (stores: SessionStore[], now: number) => + stores.flatMap(roots).sort(sortSessions(now))[0] export function hasProjectPermissions( request: Record, @@ -40,9 +46,9 @@ export function hasProjectPermissions( return Object.values(request).some((list) => list?.some(include)) } -export const childMapByParent = (sessions: Session[]) => { +export const childMapByParent = (sessions: Session[] | undefined) => { const map = new Map() - for (const session of sessions) { + for (const session of sessions ?? []) { if (!session.parentID) continue const existing = map.get(session.parentID) if (existing) { diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx index 86ede774e6..127626febe 100644 --- a/packages/app/src/pages/layout/sidebar-workspace.tsx +++ b/packages/app/src/pages/layout/sidebar-workspace.tsx @@ -332,12 +332,13 @@ export const SortableWorkspace = (props: { const open = createMemo(() => props.ctx.workspaceExpanded(props.directory, local())) const boot = createMemo(() => open() || active()) const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false) - const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length) + const count = createMemo(() => sessions()?.length ?? 0) + const hasMore = createMemo(() => workspaceStore.sessionTotal > count()) const busy = createMemo(() => props.ctx.isBusy(props.directory)) const wasBusy = createMemo((prev) => prev || busy(), false) - const loading = createMemo(() => open() && !booted() && sessions().length === 0 && !wasBusy()) + const loading = createMemo(() => open() && !booted() && count() === 0 && !wasBusy()) const touch = createMediaQuery("(hover: none)") - const showNew = createMemo(() => !loading() && (touch() || sessions().length === 0 || (active() && !params.id))) + const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id))) const loadMore = async () => { setWorkspaceStore("limit", (limit) => (limit ?? 0) + 5) await globalSync.project.loadSessions(props.directory) @@ -472,8 +473,9 @@ export const LocalWorkspace = (props: { const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow())) const children = createMemo(() => childMapByParent(workspace().store.session)) const booted = createMemo((prev) => prev || workspace().store.status === "complete", false) - const loading = createMemo(() => !booted() && sessions().length === 0) - const hasMore = createMemo(() => workspace().store.sessionTotal > sessions().length) + const count = createMemo(() => sessions()?.length ?? 0) + const loading = createMemo(() => !booted() && count() === 0) + const hasMore = createMemo(() => workspace().store.sessionTotal > count()) const loadMore = async () => { workspace().setStore("limit", (limit) => (limit ?? 0) + 5) await globalSync.project.loadSessions(props.project.worktree) diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx index e78ebecfc4..c663d7d671 100644 --- a/packages/app/src/pages/session/terminal-panel.tsx +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -18,8 +18,10 @@ import { terminalTabLabel } from "@/pages/session/terminal-label" import { createSizing, focusTerminalById } from "@/pages/session/helpers" import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff" import { useSessionLayout } from "@/pages/session/session-layout" +import { terminalProbe } from "@/testing/terminal" export function TerminalPanel() { + const delays = [120, 240] const layout = useLayout() const terminal = useTerminal() const language = useLanguage() @@ -79,16 +81,20 @@ export function TerminalPanel() { ) const focus = (id: string) => { + const probe = terminalProbe(id) + probe.focus(delays.length + 1) focusTerminalById(id) const frame = requestAnimationFrame(() => { + probe.step() if (!opened()) return if (terminal.active() !== id) return focusTerminalById(id) }) - const timers = [120, 240].map((ms) => + const timers = delays.map((ms) => window.setTimeout(() => { + probe.step() if (!opened()) return if (terminal.active() !== id) return focusTerminalById(id) @@ -96,6 +102,7 @@ export function TerminalPanel() { ) return () => { + probe.focus(0) cancelAnimationFrame(frame) for (const timer of timers) clearTimeout(timer) } @@ -273,21 +280,24 @@ export function TerminalPanel() {
- {(id) => ( - pty.id === id)}> - {(pty) => ( -
- terminal.trim(id)} - onCleanup={terminal.update} - onConnectError={() => terminal.clone(id)} - /> -
- )} -
- )} + {(id) => { + const ops = terminal.bind() + return ( + pty.id === id)}> + {(pty) => ( +
+ ops.trim(id)} + onCleanup={ops.update} + onConnectError={() => ops.clone(id)} + /> +
+ )} +
+ ) + }}
diff --git a/packages/app/src/testing/prompt.ts b/packages/app/src/testing/prompt.ts new file mode 100644 index 0000000000..e11462f301 --- /dev/null +++ b/packages/app/src/testing/prompt.ts @@ -0,0 +1,56 @@ +import type { E2EWindow } from "./terminal" + +export type PromptProbeState = { + popover: "at" | "slash" | null + slash: { + active: string | null + ids: string[] + } + selected: string | null + selects: number +} + +export const promptEnabled = () => { + if (typeof window === "undefined") return false + return (window as E2EWindow).__opencode_e2e?.prompt?.enabled === true +} + +const root = () => { + if (!promptEnabled()) return + return (window as E2EWindow).__opencode_e2e?.prompt +} + +export const promptProbe = { + set(input: Omit) { + const state = root() + if (!state) return + state.current = { + popover: input.popover, + slash: { + active: input.slash.active, + ids: [...input.slash.ids], + }, + selected: state.current?.selected ?? null, + selects: state.current?.selects ?? 0, + } + }, + select(id: string) { + const state = root() + if (!state) return + const prev = state.current + state.current = { + popover: prev?.popover ?? null, + slash: { + active: prev?.slash.active ?? null, + ids: [...(prev?.slash.ids ?? [])], + }, + selected: id, + selects: (prev?.selects ?? 0) + 1, + } + }, + clear() { + const state = root() + if (!state) return + state.current = undefined + }, +} diff --git a/packages/app/src/testing/terminal.ts b/packages/app/src/testing/terminal.ts index af1c333092..2bca39b31c 100644 --- a/packages/app/src/testing/terminal.ts +++ b/packages/app/src/testing/terminal.ts @@ -7,6 +7,7 @@ export type TerminalProbeState = { connects: number rendered: string settled: number + focusing: number } type TerminalProbeControl = { @@ -19,6 +20,10 @@ export type E2EWindow = Window & { enabled?: boolean current?: ModelProbeState } + prompt?: { + enabled?: boolean + current?: import("./prompt").PromptProbeState + } terminal?: { enabled?: boolean terminals?: Record @@ -32,6 +37,7 @@ const seed = (): TerminalProbeState => ({ connects: 0, rendered: "", settled: 0, + focusing: 0, }) const root = () => { @@ -88,6 +94,15 @@ export const terminalProbe = (id: string) => { const prev = state[id] ?? seed() state[id] = { ...prev, settled: prev.settled + 1 } }, + focus(count: number) { + set({ focusing: Math.max(0, count) }) + }, + step() { + const state = terms() + if (!state) return + const prev = state[id] ?? seed() + state[id] = { ...prev, focusing: Math.max(0, prev.focusing - 1) } + }, control(next: Partial) { const state = controls() if (!state) return diff --git a/packages/app/src/utils/persist.ts b/packages/app/src/utils/persist.ts index bee2f3e7d1..3dcbeb7d36 100644 --- a/packages/app/src/utils/persist.ts +++ b/packages/app/src/utils/persist.ts @@ -5,7 +5,12 @@ import { createResource, type Accessor } from "solid-js" import type { SetStoreFunction, Store } from "solid-js/store" type InitType = Promise | string | null -type PersistedWithReady = [Store, SetStoreFunction, InitType, Accessor] +type PersistedWithReady = [ + Store, + SetStoreFunction, + InitType, + Accessor & { promise: undefined | Promise }, +] type PersistTarget = { storage?: string @@ -460,5 +465,12 @@ export function persisted( { initialValue: !isAsync }, ) - return [state, setState, init, () => ready() === true] + return [ + state, + setState, + init, + Object.assign(() => ready() === true, { + promise: init instanceof Promise ? init : undefined, + }), + ] } diff --git a/packages/console/app/src/component/icon.tsx b/packages/console/app/src/component/icon.tsx index fc50b489b9..0aaa302b3e 100644 --- a/packages/console/app/src/component/icon.tsx +++ b/packages/console/app/src/component/icon.tsx @@ -76,6 +76,19 @@ export function IconAlipay(props: JSX.SvgSVGAttributes) { ) } +export function IconUpi(props: JSX.SvgSVGAttributes) { + return ( + + + + + + ) +} + export function IconWechat(props: JSX.SvgSVGAttributes) { return ( @@ -191,6 +204,33 @@ export function IconGemini(props: JSX.SvgSVGAttributes) { ) } +export function IconXiaomi(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconNvidia(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconArcee(props: JSX.SvgSVGAttributes) { + return ( + + + + + + + ) +} + export function IconStealth(props: JSX.SvgSVGAttributes) { return ( diff --git a/packages/console/app/src/component/modal.css b/packages/console/app/src/component/modal.css index 1f47f395de..e71fd1a192 100644 --- a/packages/console/app/src/component/modal.css +++ b/packages/console/app/src/component/modal.css @@ -62,5 +62,6 @@ font-size: var(--font-size-lg); font-weight: 600; color: var(--color-text); + text-align: center; } } diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index b59315aef1..59658f2247 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -249,7 +249,7 @@ export const dict = { "go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع", "go.meta.description": - "يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5 و Kimi K2.5 و MiniMax M2.5.", + "يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5 و Kimi K2.5 و MiniMax M2.5 وMiniMax M2.7.", "go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع", "go.hero.body": "يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.", @@ -297,7 +297,7 @@ export const dict = { "go.problem.item1": "أسعار اشتراك منخفضة التكلفة", "go.problem.item2": "حدود سخية ووصول موثوق", "go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين", - "go.problem.item4": "يتضمن GLM-5 وKimi K2.5 وMiniMax M2.5", + "go.problem.item4": "يتضمن GLM-5 وKimi K2.5 وMiniMax M2.5 وMiniMax M2.7", "go.how.title": "كيف يعمل Go", "go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.", "go.how.step1.title": "أنشئ حسابًا", @@ -318,10 +318,10 @@ export const dict = { "go.faq.q1": "ما هو OpenCode Go؟", "go.faq.a1": "Go هو اشتراك منخفض التكلفة يمنحك وصولًا موثوقًا إلى نماذج مفتوحة المصدر قادرة على البرمجة الوكيلة.", "go.faq.q2": "ما النماذج التي يتضمنها Go؟", - "go.faq.a2": "يتضمن Go نماذج GLM-5 وKimi K2.5 وMiniMax M2.5، مع حدود سخية ووصول موثوق.", + "go.faq.a2": "يتضمن Go نماذج GLM-5 وKimi K2.5 وMiniMax M2.5 وMiniMax M2.7، مع حدود سخية ووصول موثوق.", "go.faq.q3": "هل Go هو نفسه Zen؟", "go.faq.a3": - "لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5 و Kimi K2.5 و MiniMax M2.5.", + "لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5 و Kimi K2.5 و MiniMax M2.5 وMiniMax M2.7.", "go.faq.q4": "كم تكلفة Go؟", "go.faq.a4.p1.beforePricing": "تكلفة Go", "go.faq.a4.p1.pricingLink": "$5 للشهر الأول", @@ -345,7 +345,7 @@ export const dict = { "go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟", "go.faq.a9": - "تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5 وKimi K2.5 وMiniMax M2.5 مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).", + "تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5 وKimi K2.5 وMiniMax M2.5 وMiniMax M2.7 مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).", "zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.", "zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم", @@ -644,6 +644,8 @@ export const dict = { "تم تصميم الخطة بشكل أساسي للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة للحصول على وصول عالمي مستقر. قد تتغير الأسعار وحدود الاستخدام بناءً على تعلمنا من الاستخدام المبكر والملاحظات.", "workspace.lite.promo.subscribe": "الاشتراك في Go", "workspace.lite.promo.subscribing": "جارٍ إعادة التوجيه...", + "workspace.lite.promo.otherMethods": "طرق دفع أخرى", + "workspace.lite.promo.selectMethod": "اختر طريقة الدفع", "download.title": "OpenCode | تنزيل", "download.meta.description": "نزّل OpenCode لـ macOS، Windows، وLinux", @@ -688,8 +690,12 @@ export const dict = { "enterprise.form.name.placeholder": "جيف بيزوس", "enterprise.form.role.label": "المنصب", "enterprise.form.role.placeholder": "رئيس مجلس الإدارة التنفيذي", + "enterprise.form.company.label": "الشركة", + "enterprise.form.company.placeholder": "Acme Inc", "enterprise.form.email.label": "البريد الإلكتروني للشركة", "enterprise.form.email.placeholder": "jeff@amazon.com", + "enterprise.form.phone.label": "رقم الهاتف", + "enterprise.form.phone.placeholder": "+1 234 567 8900", "enterprise.form.message.label": "ما المشكلة التي تحاول حلها؟", "enterprise.form.message.placeholder": "نحتاج مساعدة في...", "enterprise.form.send": "إرسال", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index a18f3e4011..36edd3192b 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -253,7 +253,7 @@ export const dict = { "go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos", "go.meta.description": - "O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5, Kimi K2.5 e MiniMax M2.5.", + "O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5, Kimi K2.5, MiniMax M2.5 e MiniMax M2.7.", "go.hero.title": "Modelos de codificação de baixo custo para todos", "go.hero.body": "O Go traz a codificação com agentes para programadores em todo o mundo. Oferecendo limites generosos e acesso confiável aos modelos de código aberto mais capazes, para que você possa construir com agentes poderosos sem se preocupar com custos ou disponibilidade.", @@ -302,7 +302,7 @@ export const dict = { "go.problem.item1": "Preço de assinatura de baixo custo", "go.problem.item2": "Limites generosos e acesso confiável", "go.problem.item3": "Feito para o maior número possível de programadores", - "go.problem.item4": "Inclui GLM-5, Kimi K2.5 e MiniMax M2.5", + "go.problem.item4": "Inclui GLM-5, Kimi K2.5, MiniMax M2.5 e MiniMax M2.7", "go.how.title": "Como o Go funciona", "go.how.body": "O Go começa em $5 no primeiro mês, depois $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.", @@ -325,10 +325,10 @@ export const dict = { "go.faq.a1": "Go é uma assinatura de baixo custo que oferece acesso confiável a modelos de código aberto capazes para codificação com agentes.", "go.faq.q2": "Quais modelos o Go inclui?", - "go.faq.a2": "Go inclui GLM-5, Kimi K2.5 e MiniMax M2.5, com limites generosos e acesso confiável.", + "go.faq.a2": "Go inclui GLM-5, Kimi K2.5, MiniMax M2.5 e MiniMax M2.7, com limites generosos e acesso confiável.", "go.faq.q3": "O Go é o mesmo que o Zen?", "go.faq.a3": - "Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source GLM-5, Kimi K2.5 e MiniMax M2.5.", + "Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source GLM-5, Kimi K2.5, MiniMax M2.5 e MiniMax M2.7.", "go.faq.q4": "Quanto custa o Go?", "go.faq.a4.p1.beforePricing": "O Go custa", "go.faq.a4.p1.pricingLink": "$5 no primeiro mês", @@ -353,7 +353,7 @@ export const dict = { "go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?", "go.faq.a9": - "Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5, Kimi K2.5 e MiniMax M2.5 com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).", + "Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5, Kimi K2.5, MiniMax M2.5 e MiniMax M2.7 com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).", "zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.", "zen.api.error.modelNotSupported": "Modelo {{model}} não suportado", @@ -654,6 +654,8 @@ export const dict = { "O plano é projetado principalmente para usuários internacionais, com modelos hospedados nos EUA, UE e Singapura para acesso global estável. Preços e limites de uso podem mudar conforme aprendemos com o uso inicial e feedback.", "workspace.lite.promo.subscribe": "Assinar Go", "workspace.lite.promo.subscribing": "Redirecionando...", + "workspace.lite.promo.otherMethods": "Outros métodos de pagamento", + "workspace.lite.promo.selectMethod": "Selecionar método de pagamento", "download.title": "OpenCode | Baixar", "download.meta.description": "Baixe o OpenCode para macOS, Windows e Linux", @@ -700,8 +702,12 @@ export const dict = { "enterprise.form.name.placeholder": "Jeff Bezos", "enterprise.form.role.label": "Cargo", "enterprise.form.role.placeholder": "Presidente Executivo", + "enterprise.form.company.label": "Empresa", + "enterprise.form.company.placeholder": "Acme Inc", "enterprise.form.email.label": "E-mail corporativo", "enterprise.form.email.placeholder": "jeff@amazon.com", + "enterprise.form.phone.label": "Telefone", + "enterprise.form.phone.placeholder": "+1 234 567 8900", "enterprise.form.message.label": "Qual problema você está tentando resolver?", "enterprise.form.message.placeholder": "Precisamos de ajuda com...", "enterprise.form.send": "Enviar", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index ca3231648c..1246f12135 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -251,7 +251,7 @@ export const dict = { "go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle", "go.meta.description": - "Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5, Kimi K2.5 og MiniMax M2.5.", + "Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5, Kimi K2.5, MiniMax M2.5 og MiniMax M2.7.", "go.hero.title": "Kodningsmodeller til lav pris for alle", "go.hero.body": "Go bringer agentisk kodning til programmører over hele verden. Med generøse grænser og pålidelig adgang til de mest kapable open source-modeller, så du kan bygge med kraftfulde agenter uden at bekymre dig om omkostninger eller tilgængelighed.", @@ -299,7 +299,7 @@ export const dict = { "go.problem.item1": "Lavpris abonnementspriser", "go.problem.item2": "Generøse grænser og pålidelig adgang", "go.problem.item3": "Bygget til så mange programmører som muligt", - "go.problem.item4": "Inkluderer GLM-5, Kimi K2.5 og MiniMax M2.5", + "go.problem.item4": "Inkluderer GLM-5, Kimi K2.5, MiniMax M2.5 og MiniMax M2.7", "go.how.title": "Hvordan Go virker", "go.how.body": "Go starter ved $5 for den første måned, derefter $10/måned. Du kan bruge det med OpenCode eller enhver agent.", @@ -322,10 +322,11 @@ export const dict = { "go.faq.a1": "Go er et lavprisabonnement, der giver dig pålidelig adgang til kapable open source-modeller til agentisk kodning.", "go.faq.q2": "Hvilke modeller inkluderer Go?", - "go.faq.a2": "Go inkluderer GLM-5, Kimi K2.5 og MiniMax M2.5, med generøse grænser og pålidelig adgang.", + "go.faq.a2": + "Go inkluderer GLM-5, Kimi K2.5, MiniMax M2.5 og MiniMax M2.7, med generøse grænser og pålidelig adgang.", "go.faq.q3": "Er Go det samme som Zen?", "go.faq.a3": - "Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne GLM-5, Kimi K2.5 og MiniMax M2.5.", + "Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne GLM-5, Kimi K2.5, MiniMax M2.5 og MiniMax M2.7.", "go.faq.q4": "Hvad koster Go?", "go.faq.a4.p1.beforePricing": "Go koster", "go.faq.a4.p1.pricingLink": "$5 første måned", @@ -349,7 +350,7 @@ export const dict = { "go.faq.q9": "Hvad er forskellen på gratis modeller og Go?", "go.faq.a9": - "Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5, Kimi K2.5 og MiniMax M2.5 med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).", + "Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5, Kimi K2.5, MiniMax M2.5 og MiniMax M2.7 med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).", "zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.", "zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke", @@ -650,6 +651,8 @@ export const dict = { "Planen er primært designet til internationale brugere, med modeller hostet i USA, EU og Singapore for stabil global adgang. Priser og forbrugsgrænser kan ændre sig, efterhånden som vi lærer af tidlig brug og feedback.", "workspace.lite.promo.subscribe": "Abonner på Go", "workspace.lite.promo.subscribing": "Omdirigerer...", + "workspace.lite.promo.otherMethods": "Andre betalingsmetoder", + "workspace.lite.promo.selectMethod": "Vælg betalingsmetode", "download.title": "OpenCode | Download", "download.meta.description": "Download OpenCode til macOS, Windows og Linux", @@ -694,8 +697,12 @@ export const dict = { "enterprise.form.name.placeholder": "Jeff Bezos", "enterprise.form.role.label": "Rolle", "enterprise.form.role.placeholder": "Bestyrelsesformand", + "enterprise.form.company.label": "Virksomhed", + "enterprise.form.company.placeholder": "Acme Inc", "enterprise.form.email.label": "Firma-e-mail", "enterprise.form.email.placeholder": "jeff@amazon.com", + "enterprise.form.phone.label": "Telefonnummer", + "enterprise.form.phone.placeholder": "+1 234 567 8900", "enterprise.form.message.label": "Hvilket problem prøver du at løse?", "enterprise.form.message.placeholder": "Vi har brug for hjælp med...", "enterprise.form.send": "Send", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index d7ed88e361..bf26856e35 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -253,7 +253,7 @@ export const dict = { "go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle", "go.meta.description": - "Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5, Kimi K2.5 und MiniMax M2.5.", + "Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5, Kimi K2.5, MiniMax M2.5 und MiniMax M2.7.", "go.hero.title": "Kostengünstige Coding-Modelle für alle", "go.hero.body": "Go bringt Agentic Coding zu Programmierern auf der ganzen Welt. Mit großzügigen Limits und zuverlässigem Zugang zu den leistungsfähigsten Open-Source-Modellen, damit du mit leistungsstarken Agenten entwickeln kannst, ohne dir Gedanken über Kosten oder Verfügbarkeit zu machen.", @@ -301,7 +301,7 @@ export const dict = { "go.problem.item1": "Kostengünstiges Abonnement", "go.problem.item2": "Großzügige Limits und zuverlässiger Zugang", "go.problem.item3": "Für so viele Programmierer wie möglich gebaut", - "go.problem.item4": "Beinhaltet GLM-5, Kimi K2.5 und MiniMax M2.5", + "go.problem.item4": "Beinhaltet GLM-5, Kimi K2.5, MiniMax M2.5 und MiniMax M2.7", "go.how.title": "Wie Go funktioniert", "go.how.body": "Go beginnt bei $5 für den ersten Monat, danach $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.", @@ -324,10 +324,11 @@ export const dict = { "go.faq.a1": "Go ist ein kostengünstiges Abonnement, das dir zuverlässigen Zugang zu leistungsfähigen Open-Source-Modellen für Agentic Coding bietet.", "go.faq.q2": "Welche Modelle beinhaltet Go?", - "go.faq.a2": "Go beinhaltet GLM-5, Kimi K2.5 und MiniMax M2.5, mit großzügigen Limits und zuverlässigem Zugang.", + "go.faq.a2": + "Go beinhaltet GLM-5, Kimi K2.5, MiniMax M2.5 und MiniMax M2.7, mit großzügigen Limits und zuverlässigem Zugang.", "go.faq.q3": "Ist Go dasselbe wie Zen?", "go.faq.a3": - "Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen GLM-5, Kimi K2.5 und MiniMax M2.5.", + "Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen GLM-5, Kimi K2.5, MiniMax M2.5 und MiniMax M2.7.", "go.faq.q4": "Wie viel kostet Go?", "go.faq.a4.p1.beforePricing": "Go kostet", "go.faq.a4.p1.pricingLink": "$5 im ersten Monat", @@ -352,7 +353,7 @@ export const dict = { "go.faq.q9": "Was ist der Unterschied zwischen kostenlosen Modellen und Go?", "go.faq.a9": - "Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet GLM-5, Kimi K2.5 und MiniMax M2.5 mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).", + "Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet GLM-5, Kimi K2.5, MiniMax M2.5 und MiniMax M2.7 mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).", "zen.api.error.rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.", "zen.api.error.modelNotSupported": "Modell {{model}} wird nicht unterstützt", @@ -653,6 +654,8 @@ export const dict = { "Der Plan wurde hauptsächlich für internationale Nutzer entwickelt, wobei die Modelle in den USA, der EU und Singapur gehostet werden, um einen stabilen weltweiten Zugriff zu gewährleisten. Preise und Nutzungslimits können sich ändern, während wir aus der frühen Nutzung und dem Feedback lernen.", "workspace.lite.promo.subscribe": "Go abonnieren", "workspace.lite.promo.subscribing": "Leite weiter...", + "workspace.lite.promo.otherMethods": "Andere Zahlungsmethoden", + "workspace.lite.promo.selectMethod": "Zahlungsmethode auswählen", "download.title": "OpenCode | Download", "download.meta.description": "Lade OpenCode für macOS, Windows und Linux herunter", @@ -699,8 +702,12 @@ export const dict = { "enterprise.form.name.placeholder": "Jeff Bezos", "enterprise.form.role.label": "Rolle", "enterprise.form.role.placeholder": "Executive Chairman", + "enterprise.form.company.label": "Unternehmen", + "enterprise.form.company.placeholder": "Acme Inc", "enterprise.form.email.label": "Firmen-E-Mail", "enterprise.form.email.placeholder": "jeff@amazon.com", + "enterprise.form.phone.label": "Telefonnummer", + "enterprise.form.phone.placeholder": "+1 234 567 8900", "enterprise.form.message.label": "Welches Problem versuchen Sie zu lösen?", "enterprise.form.message.placeholder": "Wir brauchen Hilfe bei...", "enterprise.form.send": "Senden", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index 8b410bb610..212f5167a4 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -248,7 +248,7 @@ export const dict = { "go.title": "OpenCode Go | Low cost coding models for everyone", "go.meta.description": - "Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5, Kimi K2.5, and MiniMax M2.5.", + "Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5, Kimi K2.5, MiniMax M2.5, and MiniMax M2.7.", "go.hero.title": "Low cost coding models for everyone", "go.hero.body": "Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.", @@ -295,7 +295,7 @@ export const dict = { "go.problem.item1": "Low cost subscription pricing", "go.problem.item2": "Generous limits and reliable access", "go.problem.item3": "Built for as many programmers as possible", - "go.problem.item4": "Includes GLM-5, Kimi K2.5, and MiniMax M2.5", + "go.problem.item4": "Includes GLM-5, Kimi K2.5, MiniMax M2.5, and MiniMax M2.7", "go.how.title": "How Go works", "go.how.body": "Go starts at $5 for your first month, then $10/month. You can use it with OpenCode or any agent.", "go.how.step1.title": "Create an account", @@ -317,10 +317,11 @@ export const dict = { "go.faq.a1": "Go is a low-cost subscription that gives you reliable access to capable open-source models for agentic coding.", "go.faq.q2": "What models does Go include?", - "go.faq.a2": "Go includes GLM-5, Kimi K2.5, and MiniMax M2.5, with generous limits and reliable access.", + "go.faq.a2": + "Go includes GLM-5, Kimi K2.5, MiniMax M2.5, and MiniMax M2.7, with generous limits and reliable access.", "go.faq.q3": "Is Go the same as Zen?", "go.faq.a3": - "No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to open-source models GLM-5, Kimi K2.5, and MiniMax M2.5.", + "No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to open-source models GLM-5, Kimi K2.5, MiniMax M2.5, and MiniMax M2.7.", "go.faq.q4": "How much does Go cost?", "go.faq.a4.p1.beforePricing": "Go costs", "go.faq.a4.p1.pricingLink": "$5 first month", @@ -344,7 +345,7 @@ export const dict = { "go.faq.q9": "What is the difference between free models and Go?", "go.faq.a9": - "Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes GLM-5, Kimi K2.5, and MiniMax M2.5 with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).", + "Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes GLM-5, Kimi K2.5, MiniMax M2.5, and MiniMax M2.7 with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).", "zen.api.error.rateLimitExceeded": "Rate limit exceeded. Please try again later.", "zen.api.error.modelNotSupported": "Model {{model}} not supported", @@ -645,6 +646,8 @@ export const dict = { "The plan is designed primarily for international users, with models hosted in the US, EU, and Singapore for stable global access. Pricing and usage limits may change as we learn from early usage and feedback.", "workspace.lite.promo.subscribe": "Subscribe to Go", "workspace.lite.promo.subscribing": "Redirecting...", + "workspace.lite.promo.otherMethods": "Other payment methods", + "workspace.lite.promo.selectMethod": "Select payment method", "download.title": "OpenCode | Download", "download.meta.description": "Download OpenCode for macOS, Windows, and Linux", @@ -689,8 +692,12 @@ export const dict = { "enterprise.form.name.placeholder": "Jeff Bezos", "enterprise.form.role.label": "Role", "enterprise.form.role.placeholder": "Executive Chairman", + "enterprise.form.company.label": "Company", + "enterprise.form.company.placeholder": "Acme Inc", "enterprise.form.email.label": "Company email", "enterprise.form.email.placeholder": "jeff@amazon.com", + "enterprise.form.phone.label": "Phone number", + "enterprise.form.phone.placeholder": "+1 234 567 8900", "enterprise.form.message.label": "What problem are you trying to solve?", "enterprise.form.message.placeholder": "We need help with...", "enterprise.form.send": "Send", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index bb466568e1..9299efb3d2 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -254,7 +254,7 @@ export const dict = { "go.title": "OpenCode Go | Modelos de programación de bajo coste para todos", "go.meta.description": - "Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5, Kimi K2.5 y MiniMax M2.5.", + "Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5, Kimi K2.5, MiniMax M2.5 y MiniMax M2.7.", "go.hero.title": "Modelos de programación de bajo coste para todos", "go.hero.body": "Go lleva la programación agéntica a programadores de todo el mundo. Ofrece límites generosos y acceso fiable a los modelos de código abierto más capaces, para que puedas crear con agentes potentes sin preocuparte por el coste o la disponibilidad.", @@ -303,7 +303,7 @@ export const dict = { "go.problem.item1": "Precios de suscripción de bajo coste", "go.problem.item2": "Límites generosos y acceso fiable", "go.problem.item3": "Creado para tantos programadores como sea posible", - "go.problem.item4": "Incluye GLM-5, Kimi K2.5 y MiniMax M2.5", + "go.problem.item4": "Incluye GLM-5, Kimi K2.5, MiniMax M2.5 y MiniMax M2.7", "go.how.title": "Cómo funciona Go", "go.how.body": "Go comienza en $5 el primer mes, luego 10 $/mes. Puedes usarlo con OpenCode o cualquier agente.", "go.how.step1.title": "Crear una cuenta", @@ -325,10 +325,10 @@ export const dict = { "go.faq.a1": "Go es una suscripción de bajo coste que te da acceso fiable a modelos de código abierto capaces para programación agéntica.", "go.faq.q2": "¿Qué modelos incluye Go?", - "go.faq.a2": "Go incluye GLM-5, Kimi K2.5 y MiniMax M2.5, con límites generosos y acceso fiable.", + "go.faq.a2": "Go incluye GLM-5, Kimi K2.5, MiniMax M2.5 y MiniMax M2.7, con límites generosos y acceso fiable.", "go.faq.q3": "¿Es Go lo mismo que Zen?", "go.faq.a3": - "No. Zen es pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a los modelos de código abierto GLM-5, Kimi K2.5 y MiniMax M2.5.", + "No. Zen es pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a los modelos de código abierto GLM-5, Kimi K2.5, MiniMax M2.5 y MiniMax M2.7.", "go.faq.q4": "¿Cuánto cuesta Go?", "go.faq.a4.p1.beforePricing": "Go cuesta", "go.faq.a4.p1.pricingLink": "$5 el primer mes", @@ -353,7 +353,7 @@ export const dict = { "go.faq.q9": "¿Cuál es la diferencia entre los modelos gratuitos y Go?", "go.faq.a9": - "Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye GLM-5, Kimi K2.5 y MiniMax M2.5 con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).", + "Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye GLM-5, Kimi K2.5, MiniMax M2.5 y MiniMax M2.7 con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).", "zen.api.error.rateLimitExceeded": "Límite de tasa excedido. Por favor, inténtalo de nuevo más tarde.", "zen.api.error.modelNotSupported": "Modelo {{model}} no soportado", @@ -654,6 +654,8 @@ export const dict = { "El plan está diseñado principalmente para usuarios internacionales, con modelos alojados en EE. UU., la UE y Singapur para un acceso global estable. Los precios y los límites de uso pueden cambiar a medida que aprendemos del uso inicial y los comentarios.", "workspace.lite.promo.subscribe": "Suscribirse a Go", "workspace.lite.promo.subscribing": "Redirigiendo...", + "workspace.lite.promo.otherMethods": "Otros métodos de pago", + "workspace.lite.promo.selectMethod": "Seleccionar método de pago", "download.title": "OpenCode | Descargar", "download.meta.description": "Descarga OpenCode para macOS, Windows y Linux", @@ -699,8 +701,12 @@ export const dict = { "enterprise.form.name.placeholder": "Jeff Bezos", "enterprise.form.role.label": "Rol", "enterprise.form.role.placeholder": "Presidente Ejecutivo", + "enterprise.form.company.label": "Empresa", + "enterprise.form.company.placeholder": "Acme Inc", "enterprise.form.email.label": "Correo de empresa", "enterprise.form.email.placeholder": "jeff@amazon.com", + "enterprise.form.phone.label": "Teléfono", + "enterprise.form.phone.placeholder": "+1 234 567 8900", "enterprise.form.message.label": "¿Qué problema estás intentando resolver?", "enterprise.form.message.placeholder": "Necesitamos ayuda con...", "enterprise.form.send": "Enviar", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index 8ac20c47c2..d31226afe8 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -255,7 +255,7 @@ export const dict = { "go.title": "OpenCode Go | Modèles de code à faible coût pour tous", "go.meta.description": - "Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5, Kimi K2.5 et MiniMax M2.5.", + "Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5, Kimi K2.5, MiniMax M2.5 et MiniMax M2.7.", "go.hero.title": "Modèles de code à faible coût pour tous", "go.hero.body": "Go apporte le codage agentique aux programmeurs du monde entier. Offrant des limites généreuses et un accès fiable aux modèles open source les plus capables, pour que vous puissiez construire avec des agents puissants sans vous soucier du coût ou de la disponibilité.", @@ -303,7 +303,7 @@ export const dict = { "go.problem.item1": "Prix d'abonnement bas", "go.problem.item2": "Limites généreuses et accès fiable", "go.problem.item3": "Conçu pour autant de programmeurs que possible", - "go.problem.item4": "Inclut GLM-5, Kimi K2.5 et MiniMax M2.5", + "go.problem.item4": "Inclut GLM-5, Kimi K2.5, MiniMax M2.5 et MiniMax M2.7", "go.how.title": "Comment fonctionne Go", "go.how.body": "Go commence à $5 pour le premier mois, puis 10 $/mois. Vous pouvez l'utiliser avec OpenCode ou n'importe quel agent.", @@ -326,10 +326,11 @@ export const dict = { "go.faq.a1": "Go est un abonnement à faible coût qui vous donne un accès fiable à des modèles open source performants pour le codage agentique.", "go.faq.q2": "Quels modèles Go inclut-il ?", - "go.faq.a2": "Go inclut GLM-5, Kimi K2.5 et MiniMax M2.5, avec des limites généreuses et un accès fiable.", + "go.faq.a2": + "Go inclut GLM-5, Kimi K2.5, MiniMax M2.5 et MiniMax M2.7, avec des limites généreuses et un accès fiable.", "go.faq.q3": "Est-ce que Go est la même chose que Zen ?", "go.faq.a3": - "Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable aux modèles open source GLM-5, Kimi K2.5 et MiniMax M2.5.", + "Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable aux modèles open source GLM-5, Kimi K2.5, MiniMax M2.5 et MiniMax M2.7.", "go.faq.q4": "Combien coûte Go ?", "go.faq.a4.p1.beforePricing": "Go coûte", "go.faq.a4.p1.pricingLink": "$5 le premier mois", @@ -353,7 +354,7 @@ export const dict = { "Oui, vous pouvez utiliser Go avec n'importe quel agent. Suivez les instructions de configuration dans votre agent de code préféré.", "go.faq.q9": "Quelle est la différence entre les modèles gratuits et Go ?", "go.faq.a9": - "Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut GLM-5, Kimi K2.5 et MiniMax M2.5 avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).", + "Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut GLM-5, Kimi K2.5, MiniMax M2.5 et MiniMax M2.7 avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).", "zen.api.error.rateLimitExceeded": "Limite de débit dépassée. Veuillez réessayer plus tard.", "zen.api.error.modelNotSupported": "Modèle {{model}} non pris en charge", @@ -660,6 +661,8 @@ export const dict = { "Le plan est conçu principalement pour les utilisateurs internationaux, avec des modèles hébergés aux États-Unis, dans l'UE et à Singapour pour un accès mondial stable. Les tarifs et les limites d'utilisation peuvent changer à mesure que nous apprenons des premières utilisations et des commentaires.", "workspace.lite.promo.subscribe": "S'abonner à Go", "workspace.lite.promo.subscribing": "Redirection...", + "workspace.lite.promo.otherMethods": "Autres méthodes de paiement", + "workspace.lite.promo.selectMethod": "Sélectionner la méthode de paiement", "download.title": "OpenCode | Téléchargement", "download.meta.description": "Téléchargez OpenCode pour macOS, Windows et Linux", @@ -706,8 +709,12 @@ export const dict = { "enterprise.form.name.placeholder": "Jeff Bezos", "enterprise.form.role.label": "Poste", "enterprise.form.role.placeholder": "Président exécutif", + "enterprise.form.company.label": "Entreprise", + "enterprise.form.company.placeholder": "Acme Inc", "enterprise.form.email.label": "E-mail professionnel", "enterprise.form.email.placeholder": "jeff@amazon.com", + "enterprise.form.phone.label": "Téléphone", + "enterprise.form.phone.placeholder": "+1 234 567 8900", "enterprise.form.message.label": "Quel problème essayez-vous de résoudre ?", "enterprise.form.message.placeholder": "Nous avons besoin d'aide pour...", "enterprise.form.send": "Envoyer", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index bd8e17a5f3..3a99abe850 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -251,7 +251,7 @@ export const dict = { "go.title": "OpenCode Go | Modelli di coding a basso costo per tutti", "go.meta.description": - "Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5, Kimi K2.5 e MiniMax M2.5.", + "Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5, Kimi K2.5, MiniMax M2.5 e MiniMax M2.7.", "go.hero.title": "Modelli di coding a basso costo per tutti", "go.hero.body": "Go porta il coding agentico ai programmatori di tutto il mondo. Offrendo limiti generosi e un accesso affidabile ai modelli open source più capaci, in modo da poter costruire con agenti potenti senza preoccuparsi dei costi o della disponibilità.", @@ -299,7 +299,7 @@ export const dict = { "go.problem.item1": "Prezzo di abbonamento a basso costo", "go.problem.item2": "Limiti generosi e accesso affidabile", "go.problem.item3": "Costruito per il maggior numero possibile di programmatori", - "go.problem.item4": "Include GLM-5, Kimi K2.5 e MiniMax M2.5", + "go.problem.item4": "Include GLM-5, Kimi K2.5, MiniMax M2.5 e MiniMax M2.7", "go.how.title": "Come funziona Go", "go.how.body": "Go inizia a $5 per il primo mese, poi $10/mese. Puoi usarlo con OpenCode o qualsiasi agente.", "go.how.step1.title": "Crea un account", @@ -321,10 +321,10 @@ export const dict = { "go.faq.a1": "Go è un abbonamento a basso costo che ti dà un accesso affidabile a modelli open source capaci per il coding agentico.", "go.faq.q2": "Quali modelli include Go?", - "go.faq.a2": "Go include GLM-5, Kimi K2.5 e MiniMax M2.5, con limiti generosi e accesso affidabile.", + "go.faq.a2": "Go include GLM-5, Kimi K2.5, MiniMax M2.5 e MiniMax M2.7, con limiti generosi e accesso affidabile.", "go.faq.q3": "Go è lo stesso di Zen?", "go.faq.a3": - "No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e accesso affidabile ai modelli open source GLM-5, Kimi K2.5 e MiniMax M2.5.", + "No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e accesso affidabile ai modelli open source GLM-5, Kimi K2.5, MiniMax M2.5 e MiniMax M2.7.", "go.faq.q4": "Quanto costa Go?", "go.faq.a4.p1.beforePricing": "Go costa", "go.faq.a4.p1.pricingLink": "$5 il primo mese", @@ -349,7 +349,7 @@ export const dict = { "go.faq.q9": "Qual è la differenza tra i modelli gratuiti e Go?", "go.faq.a9": - "I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include GLM-5, Kimi K2.5 e MiniMax M2.5 con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).", + "I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include GLM-5, Kimi K2.5, MiniMax M2.5 e MiniMax M2.7 con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).", "zen.api.error.rateLimitExceeded": "Limite di richieste superato. Riprova più tardi.", "zen.api.error.modelNotSupported": "Modello {{model}} non supportato", @@ -652,6 +652,8 @@ export const dict = { "Il piano è progettato principalmente per gli utenti internazionali, con modelli ospitati in US, EU e Singapore per un accesso globale stabile. I prezzi e i limiti di utilizzo potrebbero cambiare man mano che impariamo dall'utilizzo iniziale e dal feedback.", "workspace.lite.promo.subscribe": "Abbonati a Go", "workspace.lite.promo.subscribing": "Reindirizzamento...", + "workspace.lite.promo.otherMethods": "Altri metodi di pagamento", + "workspace.lite.promo.selectMethod": "Seleziona metodo di pagamento", "download.title": "OpenCode | Download", "download.meta.description": "Scarica OpenCode per macOS, Windows e Linux", @@ -696,8 +698,12 @@ export const dict = { "enterprise.form.name.placeholder": "Jeff Bezos", "enterprise.form.role.label": "Ruolo", "enterprise.form.role.placeholder": "Presidente Esecutivo", + "enterprise.form.company.label": "Azienda", + "enterprise.form.company.placeholder": "Acme Inc", "enterprise.form.email.label": "Email aziendale", "enterprise.form.email.placeholder": "jeff@amazon.com", + "enterprise.form.phone.label": "Numero di telefono", + "enterprise.form.phone.placeholder": "+1 234 567 8900", "enterprise.form.message.label": "Quale problema stai cercando di risolvere?", "enterprise.form.message.placeholder": "Abbiamo bisogno di aiuto con...", "enterprise.form.send": "Invia", diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index e1979041cd..9bdc0a5272 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -250,7 +250,7 @@ export const dict = { "go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル", "go.meta.description": - "Goは最初の月$5、その後$10/月で、GLM-5、Kimi K2.5、MiniMax M2.5に対して5時間のゆとりあるリクエスト上限があります。", + "Goは最初の月$5、その後$10/月で、GLM-5、Kimi K2.5、MiniMax M2.5、MiniMax M2.7に対して5時間のゆとりあるリクエスト上限があります。", "go.hero.title": "すべての人のための低価格なコーディングモデル", "go.hero.body": "Goは、世界中のプログラマーにエージェント型コーディングをもたらします。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供し、コストや可用性を気にすることなく強力なエージェントで構築できます。", @@ -299,7 +299,7 @@ export const dict = { "go.problem.item1": "低価格なサブスクリプション料金", "go.problem.item2": "十分な制限と安定したアクセス", "go.problem.item3": "できるだけ多くのプログラマーのために構築", - "go.problem.item4": "GLM-5、Kimi K2.5、MiniMax M2.5を含む", + "go.problem.item4": "GLM-5、Kimi K2.5、MiniMax M2.5、MiniMax M2.7を含む", "go.how.title": "Goの仕組み", "go.how.body": "Goは最初の月$5、その後$10/月で始まります。OpenCodeまたは任意のエージェントで使えます。", "go.how.step1.title": "アカウントを作成", @@ -321,10 +321,11 @@ export const dict = { "go.faq.a1": "Goは、エージェント型コーディングのための有能なオープンソースモデルへの安定したアクセスを提供する低価格なサブスクリプションです。", "go.faq.q2": "Goにはどのモデルが含まれますか?", - "go.faq.a2": "Goには、GLM-5、Kimi K2.5、MiniMax M2.5が含まれており、十分な制限と安定したアクセスが提供されます。", + "go.faq.a2": + "Goには、GLM-5、Kimi K2.5、MiniMax M2.5、MiniMax M2.7が含まれており、十分な制限と安定したアクセスが提供されます。", "go.faq.q3": "GoはZenと同じですか?", "go.faq.a3": - "いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、GLM-5、Kimi K2.5、MiniMax M2.5のオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。", + "いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、GLM-5、Kimi K2.5、MiniMax M2.5、MiniMax M2.7のオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。", "go.faq.q4": "Goの料金は?", "go.faq.a4.p1.beforePricing": "Goは", "go.faq.a4.p1.pricingLink": "最初の月$5", @@ -349,7 +350,7 @@ export const dict = { "go.faq.q9": "無料モデルとGoの違いは何ですか?", "go.faq.a9": - "無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGLM-5、Kimi K2.5、MiniMax M2.5が含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。", + "無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGLM-5、Kimi K2.5、MiniMax M2.5、MiniMax M2.7が含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。", "zen.api.error.rateLimitExceeded": "レート制限を超えました。後でもう一度お試しください。", "zen.api.error.modelNotSupported": "モデル {{model}} はサポートされていません", @@ -652,6 +653,8 @@ export const dict = { "このプランは主にグローバルユーザー向けに設計されており、米国、EU、シンガポールでホストされたモデルにより安定したグローバルアクセスを提供します。料金と利用制限は、初期の利用状況やフィードバックに基づいて変更される可能性があります。", "workspace.lite.promo.subscribe": "Goを購読する", "workspace.lite.promo.subscribing": "リダイレクト中...", + "workspace.lite.promo.otherMethods": "その他の支払い方法", + "workspace.lite.promo.selectMethod": "支払い方法を選択", "download.title": "OpenCode | ダウンロード", "download.meta.description": "OpenCode を macOS、Windows、Linux 向けにダウンロード", @@ -697,8 +700,12 @@ export const dict = { "enterprise.form.name.placeholder": "ジェフ・ベゾス", "enterprise.form.role.label": "役職", "enterprise.form.role.placeholder": "会長", + "enterprise.form.company.label": "会社名", + "enterprise.form.company.placeholder": "Acme Inc", "enterprise.form.email.label": "会社メールアドレス", "enterprise.form.email.placeholder": "jeff@amazon.com", + "enterprise.form.phone.label": "電話番号", + "enterprise.form.phone.placeholder": "+1 234 567 8900", "enterprise.form.message.label": "どのような課題を解決したいですか?", "enterprise.form.message.placeholder": "これについて支援が必要です...", "enterprise.form.send": "送信", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index bf90e9c4e8..7dd4bfbf23 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -247,7 +247,7 @@ export const dict = { "go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델", "go.meta.description": - "Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5, Kimi K2.5, MiniMax M2.5에 대해 넉넉한 5시간 요청 한도를 제공합니다.", + "Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5, Kimi K2.5, MiniMax M2.5, MiniMax M2.7에 대해 넉넉한 5시간 요청 한도를 제공합니다.", "go.hero.title": "모두를 위한 저비용 코딩 모델", "go.hero.body": "Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공합니다. 가장 유능한 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공하므로, 비용이나 가용성 걱정 없이 강력한 에이전트로 빌드할 수 있습니다.", @@ -296,7 +296,7 @@ export const dict = { "go.problem.item1": "저렴한 구독 가격", "go.problem.item2": "넉넉한 한도와 안정적인 액세스", "go.problem.item3": "가능한 한 많은 프로그래머를 위해 제작됨", - "go.problem.item4": "GLM-5, Kimi K2.5, MiniMax M2.5 포함", + "go.problem.item4": "GLM-5, Kimi K2.5, MiniMax M2.5, MiniMax M2.7 포함", "go.how.title": "Go 작동 방식", "go.how.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있습니다.", "go.how.step1.title": "계정 생성", @@ -317,10 +317,11 @@ export const dict = { "go.faq.q1": "OpenCode Go란 무엇인가요?", "go.faq.a1": "Go는 에이전트 코딩을 위한 유능한 오픈 소스 모델에 대해 안정적인 액세스를 제공하는 저비용 구독입니다.", "go.faq.q2": "Go에는 어떤 모델이 포함되나요?", - "go.faq.a2": "Go에는 넉넉한 한도와 안정적인 액세스를 제공하는 GLM-5, Kimi K2.5, MiniMax M2.5가 포함됩니다.", + "go.faq.a2": + "Go에는 넉넉한 한도와 안정적인 액세스를 제공하는 GLM-5, Kimi K2.5, MiniMax M2.5, MiniMax M2.7가 포함됩니다.", "go.faq.q3": "Go는 Zen과 같은가요?", "go.faq.a3": - "아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5, Kimi K2.5, MiniMax M2.5 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.", + "아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5, Kimi K2.5, MiniMax M2.5, MiniMax M2.7 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.", "go.faq.q4": "Go 비용은 얼마인가요?", "go.faq.a4.p1.beforePricing": "Go 비용은", "go.faq.a4.p1.pricingLink": "첫 달 $5", @@ -344,7 +345,7 @@ export const dict = { "go.faq.q9": "무료 모델과 Go의 차이점은 무엇인가요?", "go.faq.a9": - "무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 GLM-5, Kimi K2.5, MiniMax M2.5를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).", + "무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 GLM-5, Kimi K2.5, MiniMax M2.5, MiniMax M2.7를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).", "zen.api.error.rateLimitExceeded": "속도 제한을 초과했습니다. 나중에 다시 시도해 주세요.", "zen.api.error.modelNotSupported": "{{model}} 모델은 지원되지 않습니다", @@ -644,6 +645,8 @@ export const dict = { "이 플랜은 주로 글로벌 사용자를 위해 설계되었으며, 안정적인 글로벌 액세스를 위해 미국, EU 및 싱가포르에 모델이 호스팅되어 있습니다. 가격 및 사용 한도는 초기 사용을 통해 학습하고 피드백을 수집함에 따라 변경될 수 있습니다.", "workspace.lite.promo.subscribe": "Go 구독하기", "workspace.lite.promo.subscribing": "리디렉션 중...", + "workspace.lite.promo.otherMethods": "기타 결제 수단", + "workspace.lite.promo.selectMethod": "결제 수단 선택", "download.title": "OpenCode | 다운로드", "download.meta.description": "macOS, Windows, Linux용 OpenCode 다운로드", @@ -688,8 +691,12 @@ export const dict = { "enterprise.form.name.placeholder": "홍길동", "enterprise.form.role.label": "직책", "enterprise.form.role.placeholder": "CTO / 개발 팀장", + "enterprise.form.company.label": "회사", + "enterprise.form.company.placeholder": "Acme Inc", "enterprise.form.email.label": "회사 이메일", "enterprise.form.email.placeholder": "name@company.com", + "enterprise.form.phone.label": "전화번호", + "enterprise.form.phone.placeholder": "+1 234 567 8900", "enterprise.form.message.label": "어떤 문제를 해결하고 싶으신가요?", "enterprise.form.message.placeholder": "도움이 필요한 부분은...", "enterprise.form.send": "전송", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index 0aef49f0d8..334f3dfb05 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -251,7 +251,7 @@ export const dict = { "go.title": "OpenCode Go | Rimelige kodemodeller for alle", "go.meta.description": - "Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5, Kimi K2.5 og MiniMax M2.5.", + "Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5, Kimi K2.5, MiniMax M2.5 og MiniMax M2.7.", "go.hero.title": "Rimelige kodemodeller for alle", "go.hero.body": "Go bringer agent-koding til programmerere over hele verden. Med rause grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene, kan du bygge med kraftige agenter uten å bekymre deg for kostnader eller tilgjengelighet.", @@ -299,7 +299,7 @@ export const dict = { "go.problem.item1": "Rimelig abonnementspris", "go.problem.item2": "Rause grenser og pålitelig tilgang", "go.problem.item3": "Bygget for så mange programmerere som mulig", - "go.problem.item4": "Inkluderer GLM-5, Kimi K2.5 og MiniMax M2.5", + "go.problem.item4": "Inkluderer GLM-5, Kimi K2.5, MiniMax M2.5 og MiniMax M2.7", "go.how.title": "Hvordan Go fungerer", "go.how.body": "Go starter på $5 for den første måneden, deretter $10/måned. Du kan bruke det med OpenCode eller hvilken som helst agent.", @@ -322,10 +322,10 @@ export const dict = { "go.faq.a1": "Go er et rimelig abonnement som gir deg pålitelig tilgang til kapable åpen kildekode-modeller for agent-koding.", "go.faq.q2": "Hvilke modeller inkluderer Go?", - "go.faq.a2": "Go inkluderer GLM-5, Kimi K2.5 og MiniMax M2.5, med rause grenser og pålitelig tilgang.", + "go.faq.a2": "Go inkluderer GLM-5, Kimi K2.5, MiniMax M2.5 og MiniMax M2.7, med rause grenser og pålitelig tilgang.", "go.faq.q3": "Er Go det samme som Zen?", "go.faq.a3": - "Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til åpen kildekode-modellene GLM-5, Kimi K2.5 og MiniMax M2.5.", + "Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til åpen kildekode-modellene GLM-5, Kimi K2.5, MiniMax M2.5 og MiniMax M2.7.", "go.faq.q4": "Hva koster Go?", "go.faq.a4.p1.beforePricing": "Go koster", "go.faq.a4.p1.pricingLink": "$5 første måned", @@ -350,7 +350,7 @@ export const dict = { "go.faq.q9": "Hva er forskjellen mellom gratis modeller og Go?", "go.faq.a9": - "Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer GLM-5, Kimi K2.5 og MiniMax M2.5 med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).", + "Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer GLM-5, Kimi K2.5, MiniMax M2.5 og MiniMax M2.7 med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).", "zen.api.error.rateLimitExceeded": "Rate limit overskredet. Vennligst prøv igjen senere.", "zen.api.error.modelNotSupported": "Modell {{model}} støttes ikke", @@ -651,6 +651,8 @@ export const dict = { "Planen er primært designet for internasjonale brukere, med modeller driftet i USA, EU og Singapore for stabil global tilgang. Priser og bruksgrenser kan endres etter hvert som vi lærer fra tidlig bruk og tilbakemeldinger.", "workspace.lite.promo.subscribe": "Abonner på Go", "workspace.lite.promo.subscribing": "Omdirigerer...", + "workspace.lite.promo.otherMethods": "Andre betalingsmetoder", + "workspace.lite.promo.selectMethod": "Velg betalingsmetode", "download.title": "OpenCode | Last ned", "download.meta.description": "Last ned OpenCode for macOS, Windows og Linux", @@ -695,8 +697,12 @@ export const dict = { "enterprise.form.name.placeholder": "Jeff Bezos", "enterprise.form.role.label": "Rolle", "enterprise.form.role.placeholder": "Styreleder", + "enterprise.form.company.label": "Selskap", + "enterprise.form.company.placeholder": "Acme Inc", "enterprise.form.email.label": "Bedrifts-e-post", "enterprise.form.email.placeholder": "jeff@amazon.com", + "enterprise.form.phone.label": "Telefonnummer", + "enterprise.form.phone.placeholder": "+1 234 567 8900", "enterprise.form.message.label": "Hvilket problem prøver dere å løse?", "enterprise.form.message.placeholder": "Vi trenger hjelp med...", "enterprise.form.send": "Send", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index 19aa503df5..9daa538cb9 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -252,7 +252,7 @@ export const dict = { "go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego", "go.meta.description": - "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5, Kimi K2.5 i MiniMax M2.5.", + "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5, Kimi K2.5, MiniMax M2.5 i MiniMax M2.7.", "go.hero.title": "Niskokosztowe modele do kodowania dla każdego", "go.hero.body": "Go udostępnia programowanie z agentami programistom na całym świecie. Oferuje hojne limity i niezawodny dostęp do najzdolniejszych modeli open source, dzięki czemu możesz budować za pomocą potężnych agentów, nie martwiąc się o koszty czy dostępność.", @@ -300,7 +300,7 @@ export const dict = { "go.problem.item1": "Niskokosztowa cena subskrypcji", "go.problem.item2": "Hojne limity i niezawodny dostęp", "go.problem.item3": "Stworzony dla jak największej liczby programistów", - "go.problem.item4": "Zawiera GLM-5, Kimi K2.5 i MiniMax M2.5", + "go.problem.item4": "Zawiera GLM-5, Kimi K2.5, MiniMax M2.5 i MiniMax M2.7", "go.how.title": "Jak działa Go", "go.how.body": "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc. Możesz go używać z OpenCode lub dowolnym agentem.", @@ -323,10 +323,10 @@ export const dict = { "go.faq.a1": "Go to niskokosztowa subskrypcja, która daje niezawodny dostęp do zdolnych modeli open source dla agentów kodujących.", "go.faq.q2": "Jakie modele zawiera Go?", - "go.faq.a2": "Go zawiera GLM-5, Kimi K2.5 i MiniMax M2.5, z hojnymi limitami i niezawodnym dostępem.", + "go.faq.a2": "Go zawiera GLM-5, Kimi K2.5, MiniMax M2.5 i MiniMax M2.7, z hojnymi limitami i niezawodnym dostępem.", "go.faq.q3": "Czy Go to to samo co Zen?", "go.faq.a3": - "Nie. Zen to model płatności za użycie, podczas gdy Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi limitami i niezawodnym dostępem do modeli open source GLM-5, Kimi K2.5 i MiniMax M2.5.", + "Nie. Zen to model płatności za użycie, podczas gdy Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi limitami i niezawodnym dostępem do modeli open source GLM-5, Kimi K2.5, MiniMax M2.5 i MiniMax M2.7.", "go.faq.q4": "Ile kosztuje Go?", "go.faq.a4.p1.beforePricing": "Go kosztuje", "go.faq.a4.p1.pricingLink": "$5 za pierwszy miesiąc", @@ -351,7 +351,7 @@ export const dict = { "go.faq.q9": "Jaka jest różnica między darmowymi modelami a Go?", "go.faq.a9": - "Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera GLM-5, Kimi K2.5 i MiniMax M2.5 z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).", + "Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera GLM-5, Kimi K2.5, MiniMax M2.5 i MiniMax M2.7 z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).", "zen.api.error.rateLimitExceeded": "Przekroczono limit zapytań. Spróbuj ponownie później.", "zen.api.error.modelNotSupported": "Model {{model}} nie jest obsługiwany", @@ -652,6 +652,8 @@ export const dict = { "Plan został zaprojektowany głównie dla użytkowników międzynarodowych, z modelami hostowanymi w USA, UE i Singapurze, aby zapewnić stabilny globalny dostęp. Ceny i limity użycia mogą ulec zmianie w miarę analizy wczesnego użycia i zbierania opinii.", "workspace.lite.promo.subscribe": "Subskrybuj Go", "workspace.lite.promo.subscribing": "Przekierowywanie...", + "workspace.lite.promo.otherMethods": "Inne metody płatności", + "workspace.lite.promo.selectMethod": "Wybierz metodę płatności", "download.title": "OpenCode | Pobierz", "download.meta.description": "Pobierz OpenCode na macOS, Windows i Linux", @@ -698,8 +700,12 @@ export const dict = { "enterprise.form.name.placeholder": "Jeff Bezos", "enterprise.form.role.label": "Rola", "enterprise.form.role.placeholder": "Prezes Zarządu", + "enterprise.form.company.label": "Firma", + "enterprise.form.company.placeholder": "Acme Inc", "enterprise.form.email.label": "E-mail firmowy", "enterprise.form.email.placeholder": "jeff@amazon.com", + "enterprise.form.phone.label": "Numer telefonu", + "enterprise.form.phone.placeholder": "+1 234 567 8900", "enterprise.form.message.label": "Jaki problem próbujesz rozwiązać?", "enterprise.form.message.placeholder": "Potrzebujemy pomocy z...", "enterprise.form.send": "Wyślij", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index e5dee8303a..58a4a0d91d 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -255,7 +255,7 @@ export const dict = { "go.title": "OpenCode Go | Недорогие модели для кодинга для всех", "go.meta.description": - "Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5, Kimi K2.5 и MiniMax M2.5.", + "Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5, Kimi K2.5, MiniMax M2.5 и MiniMax M2.7.", "go.hero.title": "Недорогие модели для кодинга для всех", "go.hero.body": "Go открывает доступ к агентам-программистам разработчикам по всему миру. Предлагая щедрые лимиты и надежный доступ к наиболее способным моделям с открытым исходным кодом, вы можете создавать проекты с мощными агентами, не беспокоясь о затратах или доступности.", @@ -304,7 +304,7 @@ export const dict = { "go.problem.item1": "Недорогая подписка", "go.problem.item2": "Щедрые лимиты и надежный доступ", "go.problem.item3": "Создан для максимального числа программистов", - "go.problem.item4": "Включает GLM-5, Kimi K2.5 и MiniMax M2.5", + "go.problem.item4": "Включает GLM-5, Kimi K2.5, MiniMax M2.5 и MiniMax M2.7", "go.how.title": "Как работает Go", "go.how.body": "Go начинается с $5 за первый месяц, затем $10/месяц. Вы можете использовать его с OpenCode или любым агентом.", @@ -327,10 +327,10 @@ export const dict = { "go.faq.a1": "Go — это недорогая подписка, дающая надежный доступ к мощным моделям с открытым исходным кодом для агентов-программистов.", "go.faq.q2": "Какие модели включает Go?", - "go.faq.a2": "Go включает GLM-5, Kimi K2.5 и MiniMax M2.5, с щедрыми лимитами и надежным доступом.", + "go.faq.a2": "Go включает GLM-5, Kimi K2.5, MiniMax M2.5 и MiniMax M2.7, с щедрыми лимитами и надежным доступом.", "go.faq.q3": "Go — это то же самое, что и Zen?", "go.faq.a3": - "Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом GLM-5, Kimi K2.5 и MiniMax M2.5.", + "Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом GLM-5, Kimi K2.5, MiniMax M2.5 и MiniMax M2.7.", "go.faq.q4": "Сколько стоит Go?", "go.faq.a4.p1.beforePricing": "Go стоит", "go.faq.a4.p1.pricingLink": "$5 за первый месяц", @@ -355,7 +355,7 @@ export const dict = { "go.faq.q9": "В чем разница между бесплатными моделями и Go?", "go.faq.a9": - "Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает GLM-5, Kimi K2.5 и MiniMax M2.5 с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).", + "Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает GLM-5, Kimi K2.5, MiniMax M2.5 и MiniMax M2.7 с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).", "zen.api.error.rateLimitExceeded": "Превышен лимит запросов. Пожалуйста, попробуйте позже.", "zen.api.error.modelNotSupported": "Модель {{model}} не поддерживается", @@ -658,6 +658,8 @@ export const dict = { "План предназначен в первую очередь для международных пользователей. Модели размещены в США, ЕС и Сингапуре для стабильного глобального доступа. Цены и лимиты использования могут меняться по мере того, как мы изучаем раннее использование и собираем отзывы.", "workspace.lite.promo.subscribe": "Подписаться на Go", "workspace.lite.promo.subscribing": "Перенаправление...", + "workspace.lite.promo.otherMethods": "Другие способы оплаты", + "workspace.lite.promo.selectMethod": "Выберите способ оплаты", "download.title": "OpenCode | Скачать", "download.meta.description": "Скачать OpenCode для macOS, Windows и Linux", @@ -703,8 +705,12 @@ export const dict = { "enterprise.form.name.placeholder": "Джефф Безос", "enterprise.form.role.label": "Роль", "enterprise.form.role.placeholder": "Исполнительный председатель", + "enterprise.form.company.label": "Компания", + "enterprise.form.company.placeholder": "Acme Inc", "enterprise.form.email.label": "Корпоративная почта", "enterprise.form.email.placeholder": "jeff@amazon.com", + "enterprise.form.phone.label": "Номер телефона", + "enterprise.form.phone.placeholder": "+1 234 567 8900", "enterprise.form.message.label": "Какую проблему вы пытаетесь решить?", "enterprise.form.message.placeholder": "Нам нужна помощь с...", "enterprise.form.send": "Отправить", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index c765a18133..75c8b53b30 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -250,7 +250,7 @@ export const dict = { "go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", "go.meta.description": - "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5, Kimi K2.5 และ MiniMax M2.5", + "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5, Kimi K2.5, MiniMax M2.5 และ MiniMax M2.7", "go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", "go.hero.body": "Go นำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก เสนอขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ เพื่อให้คุณสามารถสร้างสรรค์ด้วยเอเจนต์ที่ทรงพลังโดยไม่ต้องกังวลเรื่องค่าใช้จ่ายหรือความพร้อมใช้งาน", @@ -297,7 +297,7 @@ export const dict = { "go.problem.item1": "ราคาการสมัครสมาชิกที่ต่ำ", "go.problem.item2": "ขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้", "go.problem.item3": "สร้างขึ้นเพื่อโปรแกรมเมอร์จำนวนมากที่สุดเท่าที่จะเป็นไปได้", - "go.problem.item4": "รวมถึง GLM-5, Kimi K2.5 และ MiniMax M2.5", + "go.problem.item4": "รวมถึง GLM-5, Kimi K2.5, MiniMax M2.5 และ MiniMax M2.7", "go.how.title": "Go ทำงานอย่างไร", "go.how.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้", "go.how.step1.title": "สร้างบัญชี", @@ -319,10 +319,11 @@ export const dict = { "go.faq.a1": "Go คือการสมัครสมาชิกราคาประหยัดที่ให้คุณเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสำหรับการเขียนโค้ดแบบเอเจนต์ได้อย่างน่าเชื่อถือ", "go.faq.q2": "Go รวมโมเดลอะไรบ้าง?", - "go.faq.a2": "Go รวมถึง GLM-5, Kimi K2.5 และ MiniMax M2.5 พร้อมขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้", + "go.faq.a2": + "Go รวมถึง GLM-5, Kimi K2.5, MiniMax M2.5 และ MiniMax M2.7 พร้อมขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้", "go.faq.q3": "Go เหมือนกับ Zen หรือไม่?", "go.faq.a3": - "ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5, Kimi K2.5 และ MiniMax M2.5 อย่างเชื่อถือได้", + "ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5, Kimi K2.5, MiniMax M2.5 และ MiniMax M2.7 อย่างเชื่อถือได้", "go.faq.q4": "Go ราคาเท่าไหร่?", "go.faq.a4.p1.beforePricing": "Go ราคา", "go.faq.a4.p1.pricingLink": "$5 เดือนแรก", @@ -346,7 +347,7 @@ export const dict = { "go.faq.q9": "ความแตกต่างระหว่างโมเดลฟรีและ Go คืออะไร?", "go.faq.a9": - "โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง GLM-5, Kimi K2.5 และ MiniMax M2.5 ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)", + "โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง GLM-5, Kimi K2.5, MiniMax M2.5 และ MiniMax M2.7 ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)", "zen.api.error.rateLimitExceeded": "เกินขีดจำกัดอัตราการใช้งาน กรุณาลองใหม่ในภายหลัง", "zen.api.error.modelNotSupported": "ไม่รองรับโมเดล {{model}}", @@ -647,6 +648,8 @@ export const dict = { "แผนนี้ออกแบบมาสำหรับผู้ใช้งานต่างประเทศเป็นหลัก โดยมีโมเดลโฮสต์อยู่ในสหรัฐอเมริกา สหภาพยุโรป และสิงคโปร์ เพื่อการเข้าถึงที่เสถียรทั่วโลก ราคาและขีดจำกัดการใช้งานอาจมีการเปลี่ยนแปลงตามที่เราได้เรียนรู้จากการใช้งานในช่วงแรกและข้อเสนอแนะ", "workspace.lite.promo.subscribe": "สมัครสมาชิก Go", "workspace.lite.promo.subscribing": "กำลังเปลี่ยนเส้นทาง...", + "workspace.lite.promo.otherMethods": "วิธีการชำระเงินอื่นๆ", + "workspace.lite.promo.selectMethod": "เลือกวิธีการชำระเงิน", "download.title": "OpenCode | ดาวน์โหลด", "download.meta.description": "ดาวน์โหลด OpenCode สำหรับ macOS, Windows และ Linux", @@ -691,8 +694,12 @@ export const dict = { "enterprise.form.name.placeholder": "Jeff Bezos", "enterprise.form.role.label": "ตำแหน่ง", "enterprise.form.role.placeholder": "ประธานกรรมการบริหาร", + "enterprise.form.company.label": "บริษัท", + "enterprise.form.company.placeholder": "Acme Inc", "enterprise.form.email.label": "อีเมลบริษัท", "enterprise.form.email.placeholder": "jeff@amazon.com", + "enterprise.form.phone.label": "หมายเลขโทรศัพท์", + "enterprise.form.phone.placeholder": "+1 234 567 8900", "enterprise.form.message.label": "คุณกำลังพยายามแก้ปัญหาอะไร?", "enterprise.form.message.placeholder": "เราต้องการความช่วยเหลือเรื่อง...", "enterprise.form.send": "ส่ง", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index 561153755b..2d7ea4ea87 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -253,7 +253,7 @@ export const dict = { "go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri", "go.meta.description": - "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5, Kimi K2.5 ve MiniMax M2.5 için cömert 5 saatlik istek limitleri sunar.", + "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5, Kimi K2.5, MiniMax M2.5 ve MiniMax M2.7 için cömert 5 saatlik istek limitleri sunar.", "go.hero.title": "Herkes için düşük maliyetli kodlama modelleri", "go.hero.body": "Go, dünya çapındaki programcılara ajan tabanlı kodlama getiriyor. En yetenekli açık kaynaklı modellere cömert limitler ve güvenilir erişim sunarak, maliyet veya erişilebilirlik konusunda endişelenmeden güçlü ajanlarla geliştirme yapmanızı sağlar.", @@ -302,7 +302,7 @@ export const dict = { "go.problem.item1": "Düşük maliyetli abonelik fiyatlandırması", "go.problem.item2": "Cömert limitler ve güvenilir erişim", "go.problem.item3": "Mümkün olduğunca çok programcı için geliştirildi", - "go.problem.item4": "GLM-5, Kimi K2.5 ve MiniMax M2.5 içerir", + "go.problem.item4": "GLM-5, Kimi K2.5, MiniMax M2.5 ve MiniMax M2.7 içerir", "go.how.title": "Go nasıl çalışır?", "go.how.body": "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar. OpenCode veya herhangi bir ajanla kullanabilirsiniz.", @@ -325,10 +325,11 @@ export const dict = { "go.faq.a1": "Go, ajan tabanlı kodlama için yetenekli açık kaynaklı modellere güvenilir erişim sağlayan düşük maliyetli bir aboneliktir.", "go.faq.q2": "Go hangi modelleri içerir?", - "go.faq.a2": "Go, cömert limitler ve güvenilir erişim ile GLM-5, Kimi K2.5 ve MiniMax M2.5 modellerini içerir.", + "go.faq.a2": + "Go, cömert limitler ve güvenilir erişim ile GLM-5, Kimi K2.5, MiniMax M2.5 ve MiniMax M2.7 modellerini içerir.", "go.faq.q3": "Go, Zen ile aynı mı?", "go.faq.a3": - "Hayır. Zen kullandıkça öde modelidir, Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5, Kimi K2.5 ve MiniMax M2.5 açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.", + "Hayır. Zen kullandıkça öde modelidir, Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5, Kimi K2.5, MiniMax M2.5 ve MiniMax M2.7 açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.", "go.faq.q4": "Go ne kadar?", "go.faq.a4.p1.beforePricing": "Go'nun maliyeti", "go.faq.a4.p1.pricingLink": "İlk ay $5", @@ -353,7 +354,7 @@ export const dict = { "go.faq.q9": "Ücretsiz modeller ve Go arasındaki fark nedir?", "go.faq.a9": - "Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise GLM-5, Kimi K2.5 ve MiniMax M2.5 modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).", + "Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise GLM-5, Kimi K2.5, MiniMax M2.5 ve MiniMax M2.7 modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).", "zen.api.error.rateLimitExceeded": "İstek limiti aşıldı. Lütfen daha sonra tekrar deneyin.", "zen.api.error.modelNotSupported": "{{model}} modeli desteklenmiyor", @@ -654,6 +655,8 @@ export const dict = { "Plan öncelikle uluslararası kullanıcılar için tasarlanmıştır; modeller istikrarlı küresel erişim için ABD, AB ve Singapur'da barındırılmaktadır. Erken kullanımdan öğrendikçe ve geri bildirim topladıkça fiyatlandırma ve kullanım limitleri değişebilir.", "workspace.lite.promo.subscribe": "Go'ya Abone Ol", "workspace.lite.promo.subscribing": "Yönlendiriliyor...", + "workspace.lite.promo.otherMethods": "Diğer ödeme yöntemleri", + "workspace.lite.promo.selectMethod": "Ödeme yöntemini seçin", "download.title": "OpenCode | İndir", "download.meta.description": "OpenCode'u macOS, Windows ve Linux için indirin", @@ -700,8 +703,12 @@ export const dict = { "enterprise.form.name.placeholder": "Jeff Bezos", "enterprise.form.role.label": "Rol", "enterprise.form.role.placeholder": "Yönetim Kurulu Başkanı", + "enterprise.form.company.label": "Şirket", + "enterprise.form.company.placeholder": "Acme Inc", "enterprise.form.email.label": "Şirket e-postası", "enterprise.form.email.placeholder": "jeff@amazon.com", + "enterprise.form.phone.label": "Telefon numarası", + "enterprise.form.phone.placeholder": "+1 234 567 8900", "enterprise.form.message.label": "Hangi problemi çözmeye çalışıyorsunuz?", "enterprise.form.message.placeholder": "Şu konuda yardıma ihtiyacımız var...", "enterprise.form.send": "Gönder", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index 1a5fb0ff20..2cf88a0f98 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -240,7 +240,8 @@ export const dict = { "zen.privacy.exceptionsLink": "以下例外情况除外", "go.title": "OpenCode Go | 人人可用的低成本编程模型", - "go.meta.description": "Go 首月 $5,之后 $10/月,提供对 GLM-5、Kimi K2.5 和 MiniMax M2.5 的 5 小时充裕请求额度。", + "go.meta.description": + "Go 首月 $5,之后 $10/月,提供对 GLM-5、Kimi K2.5、MiniMax M2.5 和 MiniMax M2.7 的 5 小时充裕请求额度。", "go.hero.title": "人人可用的低成本编程模型", "go.hero.body": "Go 将代理编程带给全世界的程序员。提供充裕的限额和对最强大的开源模型的可靠访问,让您可以利用强大的代理进行构建,而无需担心成本或可用性。", @@ -287,7 +288,7 @@ export const dict = { "go.problem.item1": "低成本订阅定价", "go.problem.item2": "充裕的限额和可靠的访问", "go.problem.item3": "为尽可能多的程序员打造", - "go.problem.item4": "包含 GLM-5, Kimi K2.5, 和 MiniMax M2.5", + "go.problem.item4": "包含 GLM-5, Kimi K2.5, MiniMax M2.5 和 MiniMax M2.7", "go.how.title": "Go 如何工作", "go.how.body": "Go 起价为首月 $5,之后 $10/月。您可以将其与 OpenCode 或任何代理搭配使用。", "go.how.step1.title": "创建账户", @@ -306,10 +307,10 @@ export const dict = { "go.faq.q1": "什么是 OpenCode Go?", "go.faq.a1": "Go 是一项低成本订阅服务,为您提供对强大的开源模型的可靠访问,用于代理编程。", "go.faq.q2": "Go 包含哪些模型?", - "go.faq.a2": "Go 包含 GLM-5, Kimi K2.5, 和 MiniMax M2.5,并提供充裕的限额和可靠的访问。", + "go.faq.a2": "Go 包含 GLM-5, Kimi K2.5, MiniMax M2.5 和 MiniMax M2.7,并提供充裕的限额和可靠的访问。", "go.faq.q3": "Go 和 Zen 一样吗?", "go.faq.a3": - "不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 GLM-5、Kimi K2.5 和 MiniMax M2.5 等开源模型。", + "不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 GLM-5、Kimi K2.5、MiniMax M2.5 和 MiniMax M2.7 等开源模型。", "go.faq.q4": "Go 多少钱?", "go.faq.a4.p1.beforePricing": "Go 费用为", "go.faq.a4.p1.pricingLink": "首月 $5", @@ -331,7 +332,7 @@ export const dict = { "go.faq.q9": "免费模型和 Go 之间的区别是什么?", "go.faq.a9": - "免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 GLM-5, Kimi K2.5, 和 MiniMax M2.5,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。", + "免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 GLM-5, Kimi K2.5, MiniMax M2.5 和 MiniMax M2.7,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。", "zen.api.error.rateLimitExceeded": "超出速率限制。请稍后重试。", "zen.api.error.modelNotSupported": "不支持模型 {{model}}", @@ -625,6 +626,8 @@ export const dict = { "该计划主要面向国际用户设计,模型部署在美国、欧盟和新加坡,以确保全球范围内的稳定访问体验。定价和使用额度可能会根据早期用户的使用情况和反馈持续调整与优化。", "workspace.lite.promo.subscribe": "订阅 Go", "workspace.lite.promo.subscribing": "正在重定向...", + "workspace.lite.promo.otherMethods": "其他付款方式", + "workspace.lite.promo.selectMethod": "选择付款方式", "download.title": "OpenCode | 下载", "download.meta.description": "下载适用于 macOS, Windows, 和 Linux 的 OpenCode", @@ -669,8 +672,12 @@ export const dict = { "enterprise.form.name.placeholder": "Jeff Bezos", "enterprise.form.role.label": "角色", "enterprise.form.role.placeholder": "执行主席", + "enterprise.form.company.label": "公司", + "enterprise.form.company.placeholder": "Acme Inc", "enterprise.form.email.label": "公司邮箱", "enterprise.form.email.placeholder": "jeff@amazon.com", + "enterprise.form.phone.label": "电话号码", + "enterprise.form.phone.placeholder": "+1 234 567 8900", "enterprise.form.message.label": "您想解决什么问题?", "enterprise.form.message.placeholder": "我们需要帮助...", "enterprise.form.send": "发送", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index 44f3ebee00..a6155b7d4a 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -240,7 +240,8 @@ export const dict = { "zen.privacy.exceptionsLink": "以下例外情況", "go.title": "OpenCode Go | 低成本全民編碼模型", - "go.meta.description": "Go 首月 $5,之後 $10/月,提供對 GLM-5、Kimi K2.5 和 MiniMax M2.5 的 5 小時充裕請求額度。", + "go.meta.description": + "Go 首月 $5,之後 $10/月,提供對 GLM-5、Kimi K2.5、MiniMax M2.5 和 MiniMax M2.7 的 5 小時充裕請求額度。", "go.hero.title": "低成本全民編碼模型", "go.hero.body": "Go 將代理編碼帶給全世界的程式設計師。提供寬裕的限額以及對最強大開源模型的穩定存取,讓你可以使用強大的代理進行構建,而無需擔心成本或可用性。", @@ -287,7 +288,7 @@ export const dict = { "go.problem.item1": "低成本訂閱定價", "go.problem.item2": "寬裕的限額與穩定存取", "go.problem.item3": "專為盡可能多的程式設計師打造", - "go.problem.item4": "包含 GLM-5、Kimi K2.5 與 MiniMax M2.5", + "go.problem.item4": "包含 GLM-5、Kimi K2.5、MiniMax M2.5 與 MiniMax M2.7", "go.how.title": "Go 如何運作", "go.how.body": "Go 起價為首月 $5,之後 $10/月。您可以將其與 OpenCode 或任何代理搭配使用。", "go.how.step1.title": "建立帳號", @@ -306,10 +307,10 @@ export const dict = { "go.faq.q1": "什麼是 OpenCode Go?", "go.faq.a1": "Go 是一個低成本訂閱方案,讓你穩定存取強大的開源模型以進行代理編碼。", "go.faq.q2": "Go 包含哪些模型?", - "go.faq.a2": "Go 包含 GLM-5、Kimi K2.5 與 MiniMax M2.5,並提供寬裕的限額與穩定存取。", + "go.faq.a2": "Go 包含 GLM-5、Kimi K2.5、MiniMax M2.5 與 MiniMax M2.7,並提供寬裕的限額與穩定存取。", "go.faq.q3": "Go 與 Zen 一樣嗎?", "go.faq.a3": - "不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 GLM-5、Kimi K2.5 和 MiniMax M2.5 等開源模型。", + "不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 GLM-5、Kimi K2.5、MiniMax M2.5 和 MiniMax M2.7 等開源模型。", "go.faq.q4": "Go 費用是多少?", "go.faq.a4.p1.beforePricing": "Go 費用為", "go.faq.a4.p1.pricingLink": "首月 $5", @@ -331,7 +332,7 @@ export const dict = { "go.faq.q9": "免費模型與 Go 有什麼區別?", "go.faq.a9": - "免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 GLM-5、Kimi K2.5 與 MiniMax M2.5,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。", + "免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 GLM-5、Kimi K2.5、MiniMax M2.5 與 MiniMax M2.7,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。", "zen.api.error.rateLimitExceeded": "超出頻率限制。請稍後再試。", "zen.api.error.modelNotSupported": "不支援模型 {{model}}", @@ -625,6 +626,8 @@ export const dict = { "該計畫主要面向國際用戶設計,模型部署在美國、歐盟和新加坡,以確保全球範圍內的穩定存取體驗。定價和使用額度可能會根據早期用戶的使用情況和回饋持續調整與優化。", "workspace.lite.promo.subscribe": "訂閱 Go", "workspace.lite.promo.subscribing": "重新導向中...", + "workspace.lite.promo.otherMethods": "其他付款方式", + "workspace.lite.promo.selectMethod": "選擇付款方式", "download.title": "OpenCode | 下載", "download.meta.description": "下載適用於 macOS、Windows 與 Linux 的 OpenCode", @@ -668,8 +671,12 @@ export const dict = { "enterprise.form.name.placeholder": "傑夫·貝佐斯", "enterprise.form.role.label": "職稱", "enterprise.form.role.placeholder": "執行董事長", + "enterprise.form.company.label": "公司", + "enterprise.form.company.placeholder": "Acme Inc", "enterprise.form.email.label": "公司 Email", "enterprise.form.email.placeholder": "jeff@amazon.com", + "enterprise.form.phone.label": "電話號碼", + "enterprise.form.phone.placeholder": "+1 234 567 8900", "enterprise.form.message.label": "你想解決什麼問題?", "enterprise.form.message.placeholder": "我們需要幫助來...", "enterprise.form.send": "傳送", diff --git a/packages/console/app/src/lib/salesforce.ts b/packages/console/app/src/lib/salesforce.ts new file mode 100644 index 0000000000..48e0caee7d --- /dev/null +++ b/packages/console/app/src/lib/salesforce.ts @@ -0,0 +1,81 @@ +import { Resource } from "@opencode-ai/console-resource" + +async function login() { + const url = Resource.SALESFORCE_INSTANCE_URL.value.replace(/\/$/, "") + const clientId = Resource.SALESFORCE_CLIENT_ID.value + const clientSecret = Resource.SALESFORCE_CLIENT_SECRET.value + + const params = new URLSearchParams({ + grant_type: "client_credentials", + client_id: clientId, + client_secret: clientSecret, + }) + + const res = await fetch(`${url}/services/oauth2/token`, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: params.toString(), + }).catch((err) => { + console.error("Failed to fetch Salesforce access token:", err) + }) + + if (!res) return + + if (!res.ok) { + console.error("Failed to fetch Salesforce access token:", res.status, await res.text()) + return + } + + const data = (await res.json()) as { access_token?: string; instance_url?: string } + if (!data.access_token) { + console.error("Salesforce auth response did not include an access token") + return + } + + return { + token: data.access_token, + url: data.instance_url ?? url, + } +} + +export interface SalesforceLeadInput { + name: string + role: string + company?: string + email: string + phone?: string + message: string +} + +export async function createLead(input: SalesforceLeadInput): Promise { + const auth = await login() + if (!auth) return false + + const res = await fetch(`${auth.url}/services/data/v59.0/sobjects/Lead`, { + method: "POST", + headers: { + Authorization: `Bearer ${auth.token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + LastName: input.name, + Company: input.company?.trim() || "Website", + Email: input.email, + Phone: input.phone ?? null, + Title: input.role, + Description: input.message, + LeadSource: "Website", + }), + }).catch((err) => { + console.error("Failed to create Salesforce lead:", err) + }) + + if (!res) return false + + if (!res.ok) { + console.error("Failed to create Salesforce lead:", res.status, await res.text()) + return false + } + + return true +} diff --git a/packages/console/app/src/routes/api/enterprise.ts b/packages/console/app/src/routes/api/enterprise.ts index 27e2dc4938..1bc4d0eb29 100644 --- a/packages/console/app/src/routes/api/enterprise.ts +++ b/packages/console/app/src/routes/api/enterprise.ts @@ -2,11 +2,15 @@ import type { APIEvent } from "@solidjs/start/server" import { AWS } from "@opencode-ai/console-core/aws.js" import { i18n } from "~/i18n" import { localeFromRequest } from "~/lib/language" +import { createLead } from "~/lib/salesforce" interface EnterpriseFormData { name: string role: string + company?: string email: string + phone?: string + alias?: string message: string } @@ -14,33 +18,56 @@ export async function POST(event: APIEvent) { const dict = i18n(localeFromRequest(event.request)) try { const body = (await event.request.json()) as EnterpriseFormData + const trap = typeof body.alias === "string" ? body.alias.trim() : "" + + if (trap) { + return Response.json({ success: true, message: dict["enterprise.form.success.submitted"] }, { status: 200 }) + } - // Validate required fields if (!body.name || !body.role || !body.email || !body.message) { return Response.json({ error: dict["enterprise.form.error.allFieldsRequired"] }, { status: 400 }) } - // Validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ if (!emailRegex.test(body.email)) { return Response.json({ error: dict["enterprise.form.error.invalidEmailFormat"] }, { status: 400 }) } - // Create email content const emailContent = ` ${body.message}

--
${body.name}
${body.role}
-${body.email}`.trim() +${body.company ? `${body.company}
` : ""}${body.email}
+${body.phone ? `${body.phone}
` : ""}`.trim() - // Send email using AWS SES - await AWS.sendEmail({ - to: "contact@anoma.ly", - subject: `Enterprise Inquiry from ${body.name}`, - body: emailContent, - replyTo: body.email, - }) + const [lead, mail] = await Promise.all([ + createLead({ + name: body.name, + role: body.role, + company: body.company, + email: body.email, + phone: body.phone, + message: body.message, + }), + AWS.sendEmail({ + to: "contact@anoma.ly", + subject: `Enterprise Inquiry from ${body.name}`, + body: emailContent, + replyTo: body.email, + }).then( + () => true, + (err) => { + console.error("Failed to send enterprise email:", err) + return false + }, + ), + ]) + + if (!lead && !mail) { + console.error("Enterprise inquiry delivery failed", { email: body.email }) + return Response.json({ error: dict["enterprise.form.error.internalServer"] }, { status: 500 }) + } return Response.json({ success: true, message: dict["enterprise.form.success.submitted"] }, { status: 200 }) } catch (error) { diff --git a/packages/console/app/src/routes/enterprise/index.css b/packages/console/app/src/routes/enterprise/index.css index 584c94fa54..5c594bb51b 100644 --- a/packages/console/app/src/routes/enterprise/index.css +++ b/packages/console/app/src/routes/enterprise/index.css @@ -23,6 +23,7 @@ --color-text-strong: hsl(0, 5%, 12%); --color-text-inverted: hsl(0, 20%, 99%); --color-text-success: hsl(119, 100%, 35%); + --color-text-error: hsl(4, 72%, 45%); --color-border: hsl(30, 2%, 81%); --color-border-weak: hsl(0, 1%, 85%); @@ -50,6 +51,7 @@ --color-text-strong: hsl(0, 15%, 94%); --color-text-inverted: hsl(0, 9%, 7%); --color-text-success: hsl(119, 60%, 72%); + --color-text-error: hsl(4, 76%, 72%); --color-border: hsl(0, 3%, 28%); --color-border-weak: hsl(0, 4%, 23%); @@ -454,6 +456,13 @@ color: var(--color-text-success); text-align: left; } + + [data-component="error-message"] { + margin-top: 1rem; + padding: 1rem 0; + color: var(--color-text-error); + text-align: left; + } } } diff --git a/packages/console/app/src/routes/enterprise/index.tsx b/packages/console/app/src/routes/enterprise/index.tsx index ee323ff826..9e3d034738 100644 --- a/packages/console/app/src/routes/enterprise/index.tsx +++ b/packages/console/app/src/routes/enterprise/index.tsx @@ -13,11 +13,15 @@ export default function Enterprise() { const [formData, setFormData] = createSignal({ name: "", role: "", + company: "", email: "", + phone: "", + alias: "", message: "", }) const [isSubmitting, setIsSubmitting] = createSignal(false) const [showSuccess, setShowSuccess] = createSignal(false) + const [error, setError] = createSignal("") const handleInputChange = (field: string) => (e: Event) => { const target = e.target as HTMLInputElement | HTMLTextAreaElement @@ -26,6 +30,8 @@ export default function Enterprise() { const handleSubmit = async (e: Event) => { e.preventDefault() + setError("") + setShowSuccess(false) setIsSubmitting(true) try { @@ -42,13 +48,21 @@ export default function Enterprise() { setFormData({ name: "", role: "", + company: "", email: "", + phone: "", + alias: "", message: "", }) setTimeout(() => setShowSuccess(false), 5000) + return } + + const data = (await response.json().catch(() => null)) as { error?: string } | null + setError(data?.error ?? i18n.t("enterprise.form.error.internalServer")) } catch (error) { console.error("Failed to submit form:", error) + setError(i18n.t("enterprise.form.error.internalServer")) } finally { setIsSubmitting(false) } @@ -147,6 +161,19 @@ export default function Enterprise() {
+ +
+
+ + +
+
+
+ + +
+