mirror of
https://github.com/anomalyco/opencode.git
synced 2026-10-07 07:48:16 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e35db1fd0 | ||
|
|
3e4124863c |
No files matched your search
@@ -33,7 +33,7 @@
|
||||
},
|
||||
"packages/ai": {
|
||||
"name": "@opencode/ai",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.1057.0",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -55,7 +55,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode/app",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@corvu/drawer": "catalog:",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
@@ -112,7 +112,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode/cli",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode.cjs",
|
||||
"opencode2": "./bin/opencode2.cjs",
|
||||
@@ -179,7 +179,7 @@
|
||||
},
|
||||
"packages/client": {
|
||||
"name": "@opencode/client",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@opencode/protocol": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -192,6 +192,7 @@
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"solid-js": "catalog:",
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "4.0.0-rc.112",
|
||||
@@ -204,7 +205,7 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode/codemode",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
@@ -217,7 +218,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode/console-app",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -253,7 +254,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode/console-core",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -280,7 +281,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode/console-function",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opencode/console-core": "workspace:*",
|
||||
@@ -297,7 +298,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode/console-mail",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -321,7 +322,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode/console-support",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode/console-core": "workspace:*",
|
||||
@@ -341,7 +342,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode/core",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/perplexity": "3.0.26",
|
||||
@@ -368,11 +369,13 @@
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "3.1.0",
|
||||
"gitlab-ai-provider": "6.19.0",
|
||||
"gitlab-ai-provider": "6.18.0",
|
||||
"google-auth-library": "10.5.0",
|
||||
"gray-matter": "4.0.3",
|
||||
"htmlparser2": "8.0.2",
|
||||
"http-proxy-agent": "7.0.2",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"ignore": "7.0.5",
|
||||
"immer": "11.1.4",
|
||||
"jose": "6.0.11",
|
||||
"jsonc-parser": "3.3.1",
|
||||
@@ -406,7 +409,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode/desktop",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"electron-context-menu": "5.0.0",
|
||||
@@ -451,7 +454,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode/enterprise",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/sdk": "1.18.21",
|
||||
@@ -488,7 +491,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode/function",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -504,12 +507,8 @@
|
||||
},
|
||||
"packages/gui-extensions": {
|
||||
"name": "@opencode/gui-extensions",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@betteroffice/docx": "0.4.3",
|
||||
"@betteroffice/fonts": "0.4.0",
|
||||
"@betteroffice/pptx": "0.2.0",
|
||||
"@betteroffice/xlsx": "0.3.0",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
"@dnd-kit/dom": "0.5.0",
|
||||
"@dnd-kit/solid": "0.5.0",
|
||||
@@ -553,7 +552,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode/http-recorder",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@effect/platform-node-shared": "4.0.0-rc.112",
|
||||
},
|
||||
@@ -572,7 +571,7 @@
|
||||
},
|
||||
"packages/httpapi-codegen": {
|
||||
"name": "@opencode/httpapi-codegen",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
"prettier": "3.6.2",
|
||||
@@ -585,7 +584,7 @@
|
||||
},
|
||||
"packages/latex": {
|
||||
"name": "@opencode/latex",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -599,7 +598,7 @@
|
||||
},
|
||||
"packages/merman": {
|
||||
"name": "@opencode/merman",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -614,7 +613,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode/plugin",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode/ai": "workspace:*",
|
||||
@@ -622,7 +621,9 @@
|
||||
"@opencode/protocol": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
"@opencode/util": "workspace:*",
|
||||
"@standard-schema/spec": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opencode/theme": "workspace:*",
|
||||
@@ -651,7 +652,7 @@
|
||||
},
|
||||
"packages/plugin-browser": {
|
||||
"name": "@opencode/plugin-browser",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -681,7 +682,7 @@
|
||||
},
|
||||
"packages/protocol": {
|
||||
"name": "@opencode/protocol",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@opencode/schema": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -696,7 +697,7 @@
|
||||
},
|
||||
"packages/schema": {
|
||||
"name": "@opencode/schema",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -720,7 +721,7 @@
|
||||
},
|
||||
"packages/sdk": {
|
||||
"name": "@opencode/sdk",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -741,7 +742,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode/server",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@effect/platform-node-shared": "catalog:",
|
||||
@@ -750,6 +751,7 @@
|
||||
"@opencode/schema": "workspace:*",
|
||||
"@opencode/simulation": "workspace:*",
|
||||
"@opencode/util": "workspace:*",
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -762,7 +764,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode/session-ui",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode/client": "workspace:*",
|
||||
@@ -797,7 +799,7 @@
|
||||
},
|
||||
"packages/simulation": {
|
||||
"name": "@opencode/simulation",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@opencode/ai": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -817,7 +819,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode/stats-app",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -851,7 +853,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode/stats-core",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
@@ -870,7 +872,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode/stats-server",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -916,7 +918,7 @@
|
||||
},
|
||||
"packages/theme": {
|
||||
"name": "@opencode/theme",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@opentui/core": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -930,7 +932,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode/tui",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -964,7 +966,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode/ui",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
@@ -973,7 +975,6 @@
|
||||
"fuzzysort": "catalog:",
|
||||
"katex": "0.16.47",
|
||||
"marked": "catalog:",
|
||||
"marked-katex-extension": "5.1.13",
|
||||
"marked-shiki": "catalog:",
|
||||
"motion": "12.34.5",
|
||||
"remeda": "catalog:",
|
||||
@@ -1000,7 +1001,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode/util",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@effect/opentelemetry": "catalog:",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -1038,7 +1039,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode/web",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -1079,7 +1080,7 @@
|
||||
},
|
||||
"services/update": {
|
||||
"name": "@opencode/update",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"dependencies": {
|
||||
"jose": "6.0.11",
|
||||
"semver": "catalog:",
|
||||
@@ -1638,14 +1639,6 @@
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@betteroffice/docx": ["@betteroffice/docx@0.4.3", "", { "peerDependencies": { "@betteroffice/fonts": "^0" }, "optionalPeers": ["@betteroffice/fonts"] }, "sha512-CEErXJH3iCsi2a61gTNkAofLCzSTcktLmp710gGhMwbBfOqwQ2yI7dRFBV3ElGa9JxkS03IfejvLX0sAvlTrUQ=="],
|
||||
|
||||
"@betteroffice/fonts": ["@betteroffice/fonts@0.4.0", "", { "peerDependencies": { "@betteroffice/fonts-cjk": "^0" }, "optionalPeers": ["@betteroffice/fonts-cjk"] }, "sha512-x1BVwYDAgY5R6TWI4t9NLguSiAEbH5GItN2ZlpBIFoRXTIJgjBsXd6sUxPGpexXOyY6KdhRjG8XvHjyAYxEGCw=="],
|
||||
|
||||
"@betteroffice/pptx": ["@betteroffice/pptx@0.2.0", "", {}, "sha512-pNPQ58tlA+pVibERbLdQNskvP7a0GIMf4jhkCU/gPp8043Ichf6gB3PSb4ra9sfqoIaWqOaZKCWyptovA2+Q0A=="],
|
||||
|
||||
"@betteroffice/xlsx": ["@betteroffice/xlsx@0.3.0", "", {}, "sha512-K2bHtdk/epAxsmvxJlIWBRndjP3ltQU0Oh3p4gcZd2n5/YgXglo3SmMYX4nQe8WEm/yvz91p+aIasKyXXQP9Wg=="],
|
||||
|
||||
"@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="],
|
||||
|
||||
"@brendonovich/vite-plugin-opencode": ["@brendonovich/vite-plugin-opencode@0.1.3", "", { "dependencies": { "@babel/core": "^7.29.0", "@opencode-ai/client": "0.0.0-beta-18050" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-iiIwlNoycOMUiaUzL1ZirLapCG3WY8cg9hSj4KgE/JYIWgqauoHikIlfUt7SdMqqGIkb4iZu6I1MxgDDJaRfBA=="],
|
||||
@@ -4160,7 +4153,7 @@
|
||||
|
||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||
|
||||
"gitlab-ai-provider": ["gitlab-ai-provider@6.19.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-ym+p6PxKvCKfBE3VKYTiacJwsmgq/DrOuGctpBnbIBekdnZT31RM6yTTNdzVS83K3+Ym4M+qNY34HEcTVM6EEQ=="],
|
||||
"gitlab-ai-provider": ["gitlab-ai-provider@6.18.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-dXTXkNt1SFCL7jGlqazHL6iUJEug21qq0tx2s+Tui7jWNUqIAHEzY9WY1+PnZlhxvB56jPPBcFAT0attpjnopg=="],
|
||||
|
||||
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
|
||||
|
||||
@@ -4306,7 +4299,7 @@
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"ignore-walk": ["ignore-walk@8.0.0", "", { "dependencies": { "minimatch": "^10.0.3" } }, "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A=="],
|
||||
|
||||
@@ -4620,8 +4613,6 @@
|
||||
|
||||
"marked": ["marked@18.0.11", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-HnslJfsZkRPBDJRHvVtAaWlZHEpSu7u8LgQuJCELjRKuWR+hpq4A7sLq3p8HaI9ypVoXDXxV34CsQJEe1+J5Aw=="],
|
||||
|
||||
"marked-katex-extension": ["marked-katex-extension@5.1.13", "", { "peerDependencies": { "katex": ">=0.16 <0.19", "marked": ">=4 <19" } }, "sha512-+6gVygGg5at9g+YsuIQZk7XcAc6Pg1P0grEJKEWvF7DAwceZ7ZIvU0fhQe6auUqSbnPmSjJxZuICdSCdbGaFRw=="],
|
||||
|
||||
"marked-shiki": ["marked-shiki@1.2.1", "", { "peerDependencies": { "marked": ">=7.0.0", "shiki": ">=1.0.0" } }, "sha512-yHxYQhPY5oYaIRnROn98foKhuClark7M373/VpLxiy5TrDu9Jd/LsMwo8w+U91Up4oDb9IXFrP0N1MFRz8W/DQ=="],
|
||||
|
||||
"marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="],
|
||||
@@ -6526,6 +6517,8 @@
|
||||
|
||||
"gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||
|
||||
"html-minifier-terser/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-vzF5I8/QHutYM6j0R73ghqBCRntL6/fegzivyDTO9Ro=",
|
||||
"aarch64-linux": "sha256-foy1hI+cewpLJvNqTZCCNTqV4tnQSDK55ZRs87MvnI8=",
|
||||
"aarch64-darwin": "sha256-wl6+Omz25VxLRMsh0rvFZCqBErk0o3mh9cjuUYsYAP4="
|
||||
"x86_64-linux": "sha256-4AqU8dPEwo0ukoyEX1SuDVzsXQ7G/fIPnxoL400zH4M=",
|
||||
"aarch64-linux": "sha256-p75sJYtR8a4oVKb1+Dgp8Kl+1onSmUw5NRv95bbV1+E=",
|
||||
"aarch64-darwin": "sha256-Psyvbw/lGulysQlhK/MqcHVIwx2aCDTr+Pfr+gRWsG4="
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "opencode",
|
||||
"description": "AI-powered development tool",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.4.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"name": "@opencode/ai",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -507,7 +507,6 @@ const lowerReasoning = (part: ReasoningPart, providerMetadataKey: string): OpenR
|
||||
typeof metadata.reasoningEncryptedContent === "string" || metadata.reasoningEncryptedContent === null
|
||||
? metadata.reasoningEncryptedContent
|
||||
: undefined
|
||||
if (part.text.length === 0 && !encryptedContent) return undefined
|
||||
return {
|
||||
type: "reasoning",
|
||||
...(id === undefined ? {} : { id }),
|
||||
|
||||
@@ -4011,46 +4011,6 @@ describe("OpenAI Responses route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("drops empty reasoning items without summary or encrypted content", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
id: "req_reasoning_empty_shell",
|
||||
model,
|
||||
messages: [
|
||||
Message.user("What changed?"),
|
||||
Message.assistant([
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "",
|
||||
providerMetadata: {
|
||||
openai: {
|
||||
itemId: "rs_6aa28a10c05cf9f566f44022:rs_01a08aeb51217b92a5b853a0cb5b20ca",
|
||||
reasoningEncryptedContent: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ type: "text", text: "The parser changed." },
|
||||
]),
|
||||
Message.user("Summarize it."),
|
||||
],
|
||||
providerOptions: { store: false },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.input).toEqual([
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "What changed?" }] },
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
content: [{ type: "output_text", text: "The parser changed." }],
|
||||
},
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "Summarize it." }] },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("assembles streamed function call input", () =>
|
||||
Effect.gen(function* () {
|
||||
const body = sseEvents(
|
||||
|
||||
@@ -227,17 +227,17 @@ story("keeps the comment editor and its actions inside the page", async ({ page
|
||||
story("shows the empty state over a blank native page and restores navigation", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Blank page", exact: true }).click()
|
||||
await expect(root.getByText("Open a page", { exact: true })).toBeVisible()
|
||||
await expect(root.getByText("Preview your app, read docs, or browse the web.", { exact: true })).toBeVisible()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeVisible()
|
||||
await expect(root.getByText('Or prompt "Open in the app browser"', { exact: true })).toBeVisible()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
|
||||
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeDisabled()
|
||||
|
||||
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("https://example.com/")
|
||||
await expect(root.getByText("Open a page", { exact: true })).toBeVisible()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeVisible()
|
||||
await address.press("Enter")
|
||||
await expect(address).not.toBeFocused()
|
||||
await expect(root.getByText("Open a page", { exact: true })).toBeHidden()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeHidden()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeEnabled()
|
||||
})
|
||||
@@ -245,9 +245,9 @@ story("shows the empty state over a blank native page and restores navigation",
|
||||
story("keeps Stop available and hides the empty state while a blank page loads", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Loading page", exact: true }).click()
|
||||
await expect(root.getByText("Open a page", { exact: true })).toBeHidden()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeHidden()
|
||||
await root.getByRole("button", { name: "Stop", exact: true }).click()
|
||||
await expect(root.getByText("Open a page", { exact: true })).toBeVisible()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeVisible()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
|
||||
})
|
||||
|
||||
@@ -255,12 +255,12 @@ story("keeps the submitted URL visible until the browser reports navigation", as
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Blank page", exact: true }).click()
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("https://example.com/")
|
||||
await address.press("Enter")
|
||||
await expect(address).not.toBeFocused()
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
await expect(root.getByText("Open a page", { exact: true })).toBeHidden()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeHidden()
|
||||
|
||||
await root.getByRole("button", { name: "Complete navigation", exact: true }).click()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
@@ -274,7 +274,7 @@ story("keeps the submitted URL visible until the browser reports navigation", as
|
||||
story("restores the current URL each time the same submitted navigation is blocked", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
|
||||
for (const submission of [1, 2]) {
|
||||
await story.step(`blocked submission ${submission}`, async () => {
|
||||
@@ -282,28 +282,13 @@ story("restores the current URL each time the same submitted navigation is block
|
||||
await address.press("Enter")
|
||||
await expect(address).toHaveValue("https://blocked.example/")
|
||||
await root.getByRole("button", { name: "Block navigation", exact: true }).click()
|
||||
await expect(root.getByRole("alert")).toHaveText("Only web pages can open here.")
|
||||
await expect(root.getByRole("alert")).toHaveText("Request failed")
|
||||
await expect(address).toHaveValue("https://alpha.example/")
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
story("explains why a typed address cannot open instead of searching for it", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
|
||||
|
||||
for (const [typed, reason] of [
|
||||
["ftp://example.com", "Only web pages can open here."],
|
||||
["https://user:pass@example.com", "Addresses with a user name or password can't open here."],
|
||||
]) {
|
||||
await address.fill(typed)
|
||||
await address.press("Enter")
|
||||
await expect(root.getByRole("alert")).toHaveText(reason)
|
||||
await expect(address).toHaveValue("https://alpha.example/")
|
||||
}
|
||||
})
|
||||
|
||||
story("shows a themed failure state for only the failed tab and allows retry", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Failed page", exact: true }).click()
|
||||
@@ -311,7 +296,7 @@ story("shows a themed failure state for only the failed tab and allows retry", a
|
||||
await expect(root.getByText("Check the URL and your connection, then try again.", { exact: true })).toBeVisible()
|
||||
await expect(root.getByText("Request failed", { exact: true })).toBeHidden()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
|
||||
await expect(root.getByRole("combobox", { name: "Browser address", exact: true })).toHaveValue(
|
||||
await expect(root.getByRole("textbox", { name: "Browser address", exact: true })).toHaveValue(
|
||||
"https://alpha.example/",
|
||||
)
|
||||
|
||||
@@ -332,7 +317,7 @@ story("returns a failed tab to the empty state when an empty URL is submitted",
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeVisible()
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
|
||||
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("")
|
||||
await address.press("Enter")
|
||||
await expect(address).not.toBeFocused()
|
||||
@@ -341,14 +326,14 @@ story("returns a failed tab to the empty state when an empty URL is submitted",
|
||||
await expect(root.getByRole("button", { name: "Stop", exact: true })).toBeEnabled()
|
||||
await root.getByRole("button", { name: "Complete navigation", exact: true }).click()
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeHidden()
|
||||
await expect(root.getByText("Open a page", { exact: true })).toBeVisible()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeVisible()
|
||||
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeDisabled()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
|
||||
})
|
||||
|
||||
story("selects the full URL when the address field gains focus", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.click()
|
||||
await expect(address).toHaveJSProperty("selectionStart", 0)
|
||||
await expect(address).toHaveJSProperty("selectionEnd", "https://alpha.example/".length)
|
||||
@@ -367,55 +352,10 @@ story("selects the full URL when the address field gains focus", async ({ page }
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
story("draws only the address at rest when focus leaves the window with the field selected", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
|
||||
await address.click()
|
||||
// Chromium blurs the field when the window or the native page takes focus, and the field keeps its full selection.
|
||||
await address.evaluate((input: HTMLInputElement) => {
|
||||
input.dispatchEvent(new FocusEvent("blur"))
|
||||
input.select()
|
||||
})
|
||||
await expect(address).toHaveJSProperty("selectionStart", 0)
|
||||
await expect(address).toHaveJSProperty("selectionEnd", "https://alpha.example/".length)
|
||||
await page.mouse.move(0, 0)
|
||||
|
||||
// Computed ::selection styles do not report the default highlight, so compare the input's own paint with the
|
||||
// selection collapsed. The drawn address is hidden: a selection layer can change how its text is antialiased.
|
||||
await page.addStyleTag({ content: '[data-slot="browser-address-display"] { visibility: hidden }' })
|
||||
const field = root.locator('[data-component="browser-address"]')
|
||||
const selected = (await field.screenshot()).toString("base64")
|
||||
await address.evaluate((input: HTMLInputElement) => input.setSelectionRange(0, 0))
|
||||
const collapsed = (await field.screenshot()).toString("base64")
|
||||
|
||||
const changed = await page.evaluate(
|
||||
async (shots) => {
|
||||
const pixels = await Promise.all(
|
||||
shots.map(async (data) => {
|
||||
const image = new Image()
|
||||
image.src = `data:image/png;base64,${data}`
|
||||
await image.decode()
|
||||
const canvas = new OffscreenCanvas(image.width, image.height)
|
||||
canvas.getContext("2d")?.drawImage(image, 0, 0)
|
||||
|
||||
return canvas.getContext("2d")?.getImageData(0, 0, image.width, image.height).data ?? new Uint8ClampedArray()
|
||||
}),
|
||||
)
|
||||
|
||||
return Array.from({ length: (pixels[0]?.length ?? 0) / 4 }, (_, index) => index * 4).filter((offset) =>
|
||||
[0, 1, 2].some((channel) => pixels[0]?.[offset + channel] !== pixels[1]?.[offset + channel]),
|
||||
).length
|
||||
},
|
||||
[selected, collapsed],
|
||||
)
|
||||
|
||||
expect(changed).toBe(0)
|
||||
})
|
||||
|
||||
story("keeps the current page visible while a submitted URL loads", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("https://example.com/")
|
||||
await address.press("Enter")
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
@@ -431,112 +371,12 @@ story("keeps the current page visible while a submitted URL loads", async ({ pag
|
||||
|
||||
story("keeps the current page and restores its URL when an empty address is submitted", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("")
|
||||
await address.press("Enter")
|
||||
await expect(address).not.toBeFocused()
|
||||
await expect(address).toHaveValue("https://alpha.example/")
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await expect(root.getByText("Open a page", { exact: true })).toBeHidden()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeHidden()
|
||||
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeEnabled()
|
||||
})
|
||||
|
||||
story("suggests visited pages and opens the first one that completes the typed address", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Seed history", exact: true }).click()
|
||||
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
|
||||
await address.click()
|
||||
await address.pressSequentially("localhost")
|
||||
const options = page.getByRole("option")
|
||||
await expect(options).toHaveCount(1)
|
||||
await expect(options.first()).toHaveAttribute("aria-selected", "true")
|
||||
// Arrow keys that pick IME candidates leave the suggestions alone.
|
||||
await address.dispatchEvent("keydown", { key: "ArrowDown", isComposing: true })
|
||||
await expect(options.first()).toHaveAttribute("aria-selected", "true")
|
||||
await address.press("Enter")
|
||||
await expect(address).toHaveValue("http://localhost:5173/settings")
|
||||
|
||||
// A search query also opens the first match; stepping past the list leaves the typed text, which searches.
|
||||
await address.click()
|
||||
await address.pressSequentially("color format")
|
||||
await expect(options).toHaveCount(1)
|
||||
await expect(options.first()).toHaveAttribute("aria-selected", "true")
|
||||
await address.press("ArrowDown")
|
||||
await expect(options.first()).toHaveAttribute("aria-selected", "false")
|
||||
await address.press("Enter")
|
||||
await expect(address).toHaveValue("https://www.google.com/search?q=color%20format")
|
||||
})
|
||||
|
||||
story("lists recent pages on a blank tab and opens one", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
|
||||
await root.getByRole("button", { name: "Seed history", exact: true }).click()
|
||||
await root.getByRole("button", { name: "Blank page", exact: true }).click()
|
||||
await expect(root.getByText("Recent", { exact: true })).toBeVisible()
|
||||
// The globe at the field's start belongs to the field: a click there focuses it.
|
||||
await expect(address).not.toBeFocused()
|
||||
await root.locator('[data-component="browser-address"]').click({ position: { x: 16, y: 14 } })
|
||||
await expect(address).toBeFocused()
|
||||
await address.blur()
|
||||
|
||||
await root.getByRole("button", { name: /^Settings - Preview/ }).click()
|
||||
await expect(address).toHaveValue("http://localhost:5173/settings")
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
// The opened page owns the field afterwards: an edit that is never submitted shows its URL again.
|
||||
await address.click()
|
||||
await address.fill("unsubmitted")
|
||||
await address.blur()
|
||||
await expect(address).toHaveValue("http://localhost:5173/settings")
|
||||
})
|
||||
|
||||
story("adds the whole page to the session and keeps the draft when the button is pressed again", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const field = root.locator('[data-component="browser-address"]')
|
||||
const add = root.getByRole("button", { name: "Add page to session", exact: true })
|
||||
await field.hover()
|
||||
await add.click()
|
||||
const editor = root.locator('[data-slot="browser-comment-editor"] textarea')
|
||||
await editor.fill("Check the header layout")
|
||||
await field.hover()
|
||||
await add.click()
|
||||
await expect(editor).toHaveValue("Check the header layout")
|
||||
await editor.press("Enter")
|
||||
await expect(root.getByTestId("fixture-comments").getByRole("listitem")).toHaveText([/: Check the header layout$/])
|
||||
})
|
||||
|
||||
story("focuses the address field when the page uses the address shortcut", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Address shortcut", exact: true }).click()
|
||||
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
|
||||
await expect(address).toBeFocused()
|
||||
await expect(address).toHaveJSProperty("selectionEnd", "https://alpha.example/".length)
|
||||
})
|
||||
|
||||
story("shows the page's cookies in the site information and clears them", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Site information", exact: true }).click()
|
||||
const site = page.getByRole("dialog")
|
||||
await expect(site).toContainText("3 cookies in use")
|
||||
await site.getByRole("button", { name: "Clear", exact: true }).click()
|
||||
await expect(site).toContainText("0 cookies in use")
|
||||
})
|
||||
|
||||
story("zooms the page from the browser options in view and keeps its zoom in the address field", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const options = root.getByRole("button", { name: "Browser options", exact: true })
|
||||
await options.click()
|
||||
await page.getByRole("menuitem", { name: /^Zoom in/ }).click()
|
||||
await expect(root.getByText("Zoom: 110", { exact: true })).toBeVisible()
|
||||
// The menu covered the page with a still; it closes so the zoomed page shows at once.
|
||||
await expect(page.getByRole("menu")).toBeHidden()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
|
||||
const zoom = root.getByRole("button", { name: "110%", exact: true })
|
||||
await expect(zoom).toBeVisible()
|
||||
await options.click()
|
||||
await expect(page.getByRole("menuitem", { name: /^Reset zoom/ })).toContainText("110%")
|
||||
await page.keyboard.press("Escape")
|
||||
await zoom.click()
|
||||
await expect(root.getByText("Zoom: 100", { exact: true })).toBeVisible()
|
||||
await expect(zoom).toBeHidden()
|
||||
})
|
||||
@@ -95,14 +95,7 @@ for (const direction of ["ltr", "rtl"]) {
|
||||
Math.abs((direction === "ltr" ? action.x - last.x - last.width : last.x - action.x - action.width) - 12),
|
||||
).toBeLessThan(1)
|
||||
await page.keyboard.press("Enter")
|
||||
// Below the mobile breakpoint the variant picker opens in a bottom drawer instead of a menu.
|
||||
await expect(
|
||||
width < 768
|
||||
? page
|
||||
.getByRole("dialog", { name: "Choose model variant", exact: true })
|
||||
.getByRole("button", { name: "high", exact: true })
|
||||
: page.getByRole("menuitemradio", { name: "high", exact: true }),
|
||||
).toBeVisible()
|
||||
await expect(page.getByRole("menuitemradio", { name: "high", exact: true })).toBeVisible()
|
||||
await page.keyboard.press("Escape")
|
||||
await expect(variant).toBeFocused()
|
||||
await expect(submit).toBeInViewport()
|
||||
|
||||
@@ -85,7 +85,7 @@ const router: Router = { routing: () => false, path: () => "/" }
|
||||
|
||||
const keybinds: Keybinds = { keybind: () => [], keys: (bind) => bind.split("+"), matches: () => false }
|
||||
|
||||
const servers: Servers = { list: () => [], get: () => undefined }
|
||||
const servers: Servers = { list: () => [] }
|
||||
|
||||
const workspaces: Workspaces = { on: () => () => undefined }
|
||||
|
||||
@@ -348,7 +348,6 @@ function standIn(overrides: Partial<Interface>): Interface {
|
||||
keybind: () => [],
|
||||
matches: () => false,
|
||||
servers: () => ["local"],
|
||||
server: () => undefined,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { holdRoute, NO_PROVIDER, project, REMOTE_SERVER, seed, sessionHref } from "../utils/app"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { holdRoute, seed, sessionHref } from "../utils/app"
|
||||
import { fixture, mockStressTimeline } from "../utils/session-fixture"
|
||||
import { mockRemoteServer } from "../utils/workspace"
|
||||
import { expectAppVisible } from "../utils/waits"
|
||||
|
||||
test.use({ serviceWorkers: "block" })
|
||||
@@ -87,11 +85,8 @@ test("Home shows loaded sessions before the location request resolves", async ({
|
||||
test("Home and the directory picker load without newer browser APIs", async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
// Safari 16.6 has none of these APIs. Remove them before the web entry runs.
|
||||
// SAFETY: `Partial` only makes the static method optional so `delete` type-checks; the target is the real global.
|
||||
delete (Map as Partial<typeof Map>).groupBy
|
||||
// SAFETY: as above, for the real global `Promise`.
|
||||
delete (Promise as Partial<typeof Promise>).withResolvers
|
||||
// SAFETY: as above, for the real global `Promise`.
|
||||
delete (Promise as Partial<typeof Promise>).try
|
||||
})
|
||||
await openHome(page, { fileList: () => [] })
|
||||
@@ -106,81 +101,6 @@ test("Home and the directory picker load without newer browser APIs", async ({ p
|
||||
await expect(target).toBeVisible()
|
||||
})
|
||||
|
||||
test("adding a project to a signed-out server re-pairs it, then continues at the paired address", async ({ page }) => {
|
||||
// The saved address rejects the new token, so saving moves the server to the link's address.
|
||||
const paired = "http://127.0.0.1:4098"
|
||||
await mockRemoteServer(page, { name: "Remote", password: "old-password" })
|
||||
await mockOpenCodeServer(page, {
|
||||
server: paired,
|
||||
directory: "/remote/paired",
|
||||
project: project({ id: "proj_paired", directory: "/remote/paired" }),
|
||||
provider: NO_PROVIDER,
|
||||
sessions: [],
|
||||
pageMessages: () => ({ items: [] }),
|
||||
fileList: () => [],
|
||||
password: "session-token",
|
||||
pairing: { code: "one-time-code", token: "session-token" },
|
||||
})
|
||||
await openHome(page, { fileList: () => [] })
|
||||
const remote = page.locator("[data-home-row]").filter({ hasText: "Remote" })
|
||||
await expect(page.getByRole("button", { name: "Authenticate", exact: true })).toBeVisible()
|
||||
|
||||
await remote.getByRole("button", { name: "Add project", exact: true }).click()
|
||||
const editor = page.getByRole("dialog", { name: "Edit server" })
|
||||
await editor.getByLabel("Pairing link", { exact: true }).fill(`${paired}/auth/connect/one-time-code`)
|
||||
await editor.getByRole("button", { name: "Save", exact: true }).click()
|
||||
|
||||
// The folder picker reads the paired address; the removed one would reject the request.
|
||||
const picker = page.getByRole("dialog").filter({ has: page.getByRole("button", { name: "Select folder" }) })
|
||||
await expect(picker.getByRole("button", { name: "Select folder", exact: true })).toBeEnabled()
|
||||
await expect(picker.getByText("Unable to read this folder")).toHaveCount(0)
|
||||
})
|
||||
|
||||
test("a server whose saved password changes works with the new password", async ({ page }) => {
|
||||
const accepted = { password: "old-password" }
|
||||
await mockOpenCodeServer(page, {
|
||||
server: REMOTE_SERVER,
|
||||
directory: "/remote/project",
|
||||
project: project({ id: "proj_remote", directory: "/remote/project" }),
|
||||
provider: NO_PROVIDER,
|
||||
sessions: [],
|
||||
pageMessages: () => ({ items: [] }),
|
||||
fileList: () => [],
|
||||
password: () => accepted.password,
|
||||
})
|
||||
// Saved with the password the server accepts until the test changes it.
|
||||
await seed(page, {
|
||||
storage: {
|
||||
"opencode.global.dat:server": {
|
||||
list: [{ type: "http", displayName: "Remote", http: { url: REMOTE_SERVER, password: "old-password" } }],
|
||||
},
|
||||
},
|
||||
})
|
||||
await openHome(page, { fileList: () => [] })
|
||||
const remote = page.locator("[data-home-row]").filter({ hasText: "Remote" })
|
||||
const picker = page.getByRole("dialog").filter({ has: page.getByRole("button", { name: "Select folder" }) })
|
||||
|
||||
// Use the server once, so its controller exists with the old password.
|
||||
await remote.getByRole("button", { name: "Add project", exact: true }).click()
|
||||
await expect(picker.getByRole("button", { name: "Select folder", exact: true })).toBeEnabled()
|
||||
await picker.getByRole("button", { name: "Cancel", exact: true }).click()
|
||||
await expect(picker).toHaveCount(0)
|
||||
|
||||
await remote.getByRole("button", { name: "More options", exact: true }).click()
|
||||
await page.getByRole("menuitem", { name: "Edit", exact: true }).click()
|
||||
const editor = page.getByRole("dialog", { name: "Edit server" })
|
||||
await editor.getByPlaceholder("password").fill("new-password")
|
||||
// The server now rejects the old password, as after `opencode service set password`.
|
||||
accepted.password = "new-password"
|
||||
await editor.getByRole("button", { name: "Save", exact: true }).click()
|
||||
await expect(editor).toHaveCount(0)
|
||||
|
||||
// The picker reads the folder through the server's controller, which must use the saved password now.
|
||||
await remote.getByRole("button", { name: "Add project", exact: true }).click()
|
||||
await expect(picker.getByRole("button", { name: "Select folder", exact: true })).toBeEnabled()
|
||||
await expect(picker.getByText("Unable to read this folder")).toHaveCount(0)
|
||||
})
|
||||
|
||||
const recovery = "C:/OpenCode/Worktrees/project-menu-recovery"
|
||||
|
||||
const worktree =
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { fixture, installStressSessionTabs } from "../utils/session-fixture"
|
||||
import { sessionHref } from "../utils/app"
|
||||
import { openWithDirection } from "../utils/direction"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
|
||||
for (const { width, direction } of [
|
||||
{ width: 390, direction: "ltr" },
|
||||
{ width: 390, direction: "rtl" },
|
||||
{ width: 1280, direction: "ltr" },
|
||||
] as const) {
|
||||
test(`submitting a prompt ${width < 768 ? "unfocuses" : "keeps focus on"} the composer at ${width}px in ${direction}`, async ({
|
||||
page,
|
||||
}) => {
|
||||
await openSubmitSession(page, width, direction)
|
||||
|
||||
const editor = page.getByRole("textbox", { name: "Prompt", exact: true })
|
||||
await expect(editor).toBeEditable()
|
||||
await editor.fill("Please check focus after sending.")
|
||||
await expect(editor).toBeFocused()
|
||||
|
||||
const admitted = page.waitForResponse(
|
||||
(response) =>
|
||||
response.request().method() === "POST" &&
|
||||
new URL(response.url()).pathname === `/api/session/${fixture.targetID}/prompt`,
|
||||
)
|
||||
|
||||
await editor.press("Enter")
|
||||
expect((await admitted).ok()).toBe(true)
|
||||
await expect(editor).toBeEmpty()
|
||||
|
||||
if (width < 768) {
|
||||
await expect(editor).not.toBeFocused()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await expect(editor).toBeFocused()
|
||||
})
|
||||
}
|
||||
|
||||
test.describe("touch submission", () => {
|
||||
test.use({ viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true })
|
||||
|
||||
test("tapping Send unfocuses the composer", async ({ page }) => {
|
||||
await openSubmitSession(page, 390, "ltr")
|
||||
const editor = page.getByRole("textbox", { name: "Prompt", exact: true })
|
||||
await expect(editor).toBeEditable()
|
||||
await editor.fill("Please dismiss the keyboard after sending.")
|
||||
await expect(editor).toBeFocused()
|
||||
|
||||
const admitted = page.waitForResponse(
|
||||
(response) =>
|
||||
response.request().method() === "POST" &&
|
||||
new URL(response.url()).pathname === `/api/session/${fixture.targetID}/prompt`,
|
||||
)
|
||||
|
||||
await page.getByRole("button", { name: "Send", exact: true }).tap()
|
||||
expect((await admitted).ok()).toBe(true)
|
||||
await expect(editor).toBeEmpty()
|
||||
await expect(editor).not.toBeFocused()
|
||||
})
|
||||
})
|
||||
|
||||
async function openSubmitSession(page: Page, width: number, direction: "ltr" | "rtl") {
|
||||
await page.setViewportSize({ width, height: 844 })
|
||||
await mockOpenCodeServer(page, {
|
||||
directory: fixture.directory,
|
||||
project: fixture.project,
|
||||
sessions: fixture.sessions,
|
||||
provider: fixture.provider,
|
||||
pageMessages: () => ({ items: [] }),
|
||||
})
|
||||
await installStressSessionTabs(page)
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem("settings.v3", JSON.stringify({ general: { mobileTitlebarPosition: "bottom" } }))
|
||||
})
|
||||
await openWithDirection(page, sessionHref(fixture.targetID), direction)
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
import { expect, test } from "@playwright/test"
|
||||
import { fixture, mockStressTimeline } from "../utils/session-fixture"
|
||||
import { sessionHref } from "../utils/app"
|
||||
import { openWithDirection } from "../utils/direction"
|
||||
|
||||
for (const scheme of ["light", "dark"] as const) {
|
||||
for (const direction of ["ltr", "rtl"] as const) {
|
||||
test(`mobile extensions reuse the session drawer in ${scheme} ${direction}`, async ({ page }, testInfo) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await mockStressTimeline(page)
|
||||
await page.addInitScript((scheme) => {
|
||||
localStorage.setItem("opencode-color-scheme", scheme)
|
||||
localStorage.setItem("settings.v3", JSON.stringify({ general: { mobileTitlebarPosition: "bottom" } }))
|
||||
}, scheme)
|
||||
const warnings: string[] = []
|
||||
page.on("console", (event) => {
|
||||
if (event.text().includes("computations created outside")) warnings.push(event.text())
|
||||
})
|
||||
await openWithDirection(page, sessionHref(fixture.targetID), direction)
|
||||
const tabs = page.getByRole("tablist", { name: "Session view", exact: true })
|
||||
const more = tabs.getByRole("tab", { name: "More...", exact: true })
|
||||
await expect(tabs.getByRole("tab", { selected: true })).toHaveText("Session")
|
||||
await more.click()
|
||||
await page
|
||||
.getByRole("dialog", { name: "More options", exact: true })
|
||||
.getByRole("button", { name: "Session details", exact: true })
|
||||
.click()
|
||||
const details = page.getByRole("dialog", { name: "Session details", exact: true })
|
||||
await expect(details.getByRole("button", { name: "MCP", exact: true })).toBeVisible()
|
||||
await details.evaluate((element) => element.setAttribute("data-drawer-probe", "single"))
|
||||
|
||||
for (const [name, empty] of [
|
||||
["MCP", "No MCP servers configured"],
|
||||
["Plugins", "No plugins configured"],
|
||||
["Skills", "No skills configured"],
|
||||
["LSP", "No LSP servers configured"],
|
||||
]) {
|
||||
const trigger = details.getByRole("button", { name, exact: true })
|
||||
await trigger.click()
|
||||
const extension = page.getByRole("dialog", { name, exact: true })
|
||||
await expect(extension.getByText(empty, { exact: true })).toBeVisible()
|
||||
await expect(extension).toHaveAttribute("data-drawer-probe", "single")
|
||||
await expect(page.getByRole("dialog")).toHaveCount(1)
|
||||
await expect(page.locator('[data-slot="mobile-drawer-overlay"]')).toHaveCount(1)
|
||||
await expect(extension.getByRole("button", { name: "Close", exact: true })).toHaveCount(0)
|
||||
await expect(extension.locator(".session-service-menu")).toHaveCount(0)
|
||||
await expect(extension).toBeInViewport({ ratio: 1 })
|
||||
await extension.getByRole("button", { name: "Navigate back", exact: true }).click()
|
||||
await expect(details.getByRole("button", { name: "MCP", exact: true })).toBeVisible()
|
||||
await expect(trigger).toBeFocused()
|
||||
}
|
||||
|
||||
await details.getByRole("button", { name: "Skills", exact: true }).click()
|
||||
await expect(
|
||||
page.getByRole("dialog", { name: "Skills", exact: true }).getByText("No skills configured", { exact: true }),
|
||||
).toBeVisible()
|
||||
await page.screenshot({ path: testInfo.outputPath("extensions.png") })
|
||||
await testInfo.attach("extensions", { path: testInfo.outputPath("extensions.png"), contentType: "image/png" })
|
||||
await page.keyboard.press("Escape")
|
||||
await expect(page.getByRole("dialog")).toHaveCount(0)
|
||||
await expect(more).toBeFocused()
|
||||
await more.click()
|
||||
await expect(
|
||||
page
|
||||
.getByRole("dialog", { name: "More options", exact: true })
|
||||
.getByRole("button", { name: "Files", exact: true }),
|
||||
).toBeVisible()
|
||||
expect(warnings).toEqual([])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
test("mobile extension drawers keep configured lists, retry, refresh, and MCP toggles working", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await mockStressTimeline(page)
|
||||
const state = { fail: true, extra: false, enabled: true }
|
||||
await page.route(
|
||||
(url) => url.pathname === "/api/plugin",
|
||||
(route) => {
|
||||
if (route.request().method() === "OPTIONS") return route.fallback()
|
||||
|
||||
if (state.fail) return route.fulfill({ status: 500, json: { message: "Unavailable" } })
|
||||
|
||||
return route.fulfill({
|
||||
json: {
|
||||
location: { directory: fixture.directory },
|
||||
data: [
|
||||
{
|
||||
id: "drawer-plugin",
|
||||
source: { type: "package", target: "drawer-plugin" },
|
||||
features: {},
|
||||
state: { status: "active" },
|
||||
},
|
||||
...(state.extra
|
||||
? [
|
||||
{
|
||||
id: "extra-plugin",
|
||||
source: { type: "package", target: "extra-plugin" },
|
||||
features: {},
|
||||
state: { status: "active" },
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
})
|
||||
},
|
||||
)
|
||||
await page.route(
|
||||
(url) => url.pathname === "/api/skill",
|
||||
(route) =>
|
||||
route.fulfill({
|
||||
json: {
|
||||
location: { directory: fixture.directory },
|
||||
data: [
|
||||
{ id: "drawer-skill", name: "drawer-skill", path: "/skills/drawer/SKILL.md", content: "Drawer skill" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
await page.route(
|
||||
(url) => url.pathname === "/api/config",
|
||||
(route) =>
|
||||
route.fulfill({ json: [{ type: "document", info: { lsp: { "drawer-lsp": { command: ["drawer-lsp"] } } } }] }),
|
||||
)
|
||||
await page.route(
|
||||
(url) => url.pathname.startsWith("/api/mcp") || url.pathname.startsWith("/api/experimental/mcp"),
|
||||
(route) => {
|
||||
if (route.request().method() === "OPTIONS") return route.fallback()
|
||||
const path = new URL(route.request().url()).pathname
|
||||
|
||||
if (path.endsWith("/disconnect")) {
|
||||
state.enabled = false
|
||||
|
||||
return route.fulfill({ status: 204 })
|
||||
}
|
||||
|
||||
return route.fulfill({
|
||||
json: {
|
||||
location: { directory: fixture.directory },
|
||||
data:
|
||||
path === "/api/mcp/resource"
|
||||
? { resources: [], templates: [] }
|
||||
: Array.from({ length: 30 }, (_, index) => ({
|
||||
name: `mcp-${String(index).padStart(2, "0")}`,
|
||||
status: { status: index === 0 && !state.enabled ? "disabled" : "connected" },
|
||||
})),
|
||||
},
|
||||
})
|
||||
},
|
||||
)
|
||||
await openWithDirection(page, sessionHref(fixture.targetID), "ltr")
|
||||
const tabs = page.getByRole("tablist", { name: "Session view", exact: true })
|
||||
await expect(tabs.getByRole("tab", { selected: true })).toHaveText("Session")
|
||||
await tabs.getByRole("tab", { name: "More...", exact: true }).click()
|
||||
await page
|
||||
.getByRole("dialog", { name: "More options", exact: true })
|
||||
.getByRole("button", { name: "Session details", exact: true })
|
||||
.click()
|
||||
const details = page.getByRole("dialog", { name: "Session details", exact: true })
|
||||
await details.getByRole("button", { name: "Plugins", exact: true }).click()
|
||||
const plugins = page.getByRole("dialog", { name: "Plugins", exact: true })
|
||||
await expect(plugins.getByRole("alert")).toContainText("Request failed")
|
||||
state.fail = false
|
||||
await plugins.getByRole("button", { name: "Retry", exact: true }).click()
|
||||
await expect(plugins.getByText("drawer-plugin", { exact: true })).toBeVisible()
|
||||
await plugins.getByRole("button", { name: "Navigate back", exact: true }).click()
|
||||
state.extra = true
|
||||
await details.getByRole("button", { name: "Plugins", exact: true }).click()
|
||||
await expect(plugins.getByText("extra-plugin", { exact: true })).toBeVisible()
|
||||
await plugins.getByRole("button", { name: "Navigate back", exact: true }).click()
|
||||
|
||||
for (const [name, item] of [
|
||||
["Skills", "drawer-skill"],
|
||||
["LSP", "drawer-lsp"],
|
||||
]) {
|
||||
await details.getByRole("button", { name, exact: true }).click()
|
||||
const extension = page.getByRole("dialog", { name, exact: true })
|
||||
await expect(extension.getByText(item, { exact: true })).toBeVisible()
|
||||
await extension.getByRole("button", { name: "Navigate back", exact: true }).click()
|
||||
}
|
||||
|
||||
await details.getByRole("button", { name: "MCP", exact: true }).click()
|
||||
const mcp = page.getByRole("dialog", { name: "MCP", exact: true })
|
||||
await expect(mcp.getByRole("switch")).toHaveCount(30)
|
||||
await expect
|
||||
.poll(() =>
|
||||
mcp
|
||||
.locator('[data-slot="mobile-panel-content"]')
|
||||
.evaluate((element) => element.scrollHeight > element.clientHeight),
|
||||
)
|
||||
.toBe(true)
|
||||
await expect(mcp).toBeInViewport({ ratio: 1 })
|
||||
const disconnect = page.waitForRequest((request) => new URL(request.url()).pathname.endsWith("/disconnect"))
|
||||
await mcp.getByRole("switch", { name: "mcp-00", exact: true }).focus()
|
||||
await mcp.getByRole("switch", { name: "mcp-00", exact: true }).press("Space")
|
||||
await disconnect
|
||||
await expect(mcp.getByRole("switch", { name: "mcp-00", exact: true })).not.toBeChecked()
|
||||
|
||||
const last = mcp
|
||||
.locator(".session-mcp-row")
|
||||
.filter({ has: page.getByRole("switch", { name: "mcp-29", exact: true }) })
|
||||
|
||||
await last.scrollIntoViewIfNeeded()
|
||||
await expect(last).toBeInViewport()
|
||||
await mcp.getByRole("switch", { name: "mcp-29", exact: true }).focus()
|
||||
await expect(mcp.getByRole("switch", { name: "mcp-29", exact: true })).toBeFocused()
|
||||
})
|
||||
@@ -1,21 +1,10 @@
|
||||
import { expect, test, type Page, type Request } from "@playwright/test"
|
||||
import { expect, test, type Request } from "@playwright/test"
|
||||
import { sessionHref } from "../utils/app"
|
||||
import { fixture, mockStressTimeline } from "../utils/session-fixture"
|
||||
import { fileNode } from "../utils/workspace"
|
||||
|
||||
test.use({ viewport: { width: 390, height: 844 } })
|
||||
|
||||
async function openFiles(page: Page) {
|
||||
await page
|
||||
.getByRole("tablist", { name: "Session view", exact: true })
|
||||
.getByRole("tab", { name: "More...", exact: true })
|
||||
.click()
|
||||
await page
|
||||
.getByRole("dialog", { name: "More options", exact: true })
|
||||
.getByRole("button", { name: "Files", exact: true })
|
||||
.click()
|
||||
}
|
||||
|
||||
test("mobile files browse, comment, search, and close tabs", async ({ page }) => {
|
||||
await mockStressTimeline(page, {
|
||||
fileList: (path) => (path ? [] : ["first.ts", "second.ts"].map((name) => fileNode(fixture.directory, name))),
|
||||
@@ -25,7 +14,7 @@ test("mobile files browse, comment, search, and close tabs", async ({ page }) =>
|
||||
})
|
||||
await page.goto(sessionHref(fixture.targetID))
|
||||
const navigation = page.getByRole("tablist", { name: "Session view", exact: true })
|
||||
await openFiles(page)
|
||||
await navigation.getByRole("tab", { name: "Files", exact: true }).click()
|
||||
const files = page.locator('[data-slot="session-mobile-files"]')
|
||||
await files.getByRole("button", { name: "first.ts", exact: true }).click()
|
||||
await expect(files.getByText("contents:first.ts", { exact: true })).toBeVisible()
|
||||
@@ -61,7 +50,7 @@ test("mobile files browse, comment, search, and close tabs", async ({ page }) =>
|
||||
.click()
|
||||
await expect(session).toHaveAttribute("aria-selected", "true")
|
||||
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toBeVisible()
|
||||
await openFiles(page)
|
||||
await navigation.getByRole("tab", { name: "Files", exact: true }).click()
|
||||
await expect(files.getByText("contents:first.ts", { exact: true })).toBeVisible()
|
||||
const filter = files.getByRole("combobox", { name: "Filter files", exact: true })
|
||||
await expect(filter).toBeHidden()
|
||||
@@ -74,7 +63,7 @@ test("mobile files browse, comment, search, and close tabs", async ({ page }) =>
|
||||
await openTabs.getByRole("tab", { name: "first.ts", exact: true }).click()
|
||||
await expect(files.getByText("contents:first.ts", { exact: true })).toBeVisible()
|
||||
await navigation.getByRole("tab", { name: "Session", exact: true }).click()
|
||||
await openFiles(page)
|
||||
await navigation.getByRole("tab", { name: "Files", exact: true }).click()
|
||||
await expect(files.getByText("contents:first.ts", { exact: true })).toBeVisible()
|
||||
await files
|
||||
.locator('[data-slot="tabs-v2-trigger-wrapper"]')
|
||||
@@ -250,7 +239,7 @@ test("mobile changes summarize expanded diffs, stage comments, wrap by setting,
|
||||
for (const file of ["added.ts", "modified.ts", "added.ts", "modified.ts"]) {
|
||||
await navigation.getByRole("tab", { name: "Changes", exact: true }).click()
|
||||
await review.locator(`[data-file="${file}"]`).getByRole("button", { name: "Open file", exact: true }).click()
|
||||
await expect(navigation.getByRole("tab", { name: "More...", exact: true })).toHaveAttribute("aria-selected", "true")
|
||||
await expect(navigation.getByRole("tab", { name: "Files", exact: true })).toHaveAttribute("aria-selected", "true")
|
||||
await expect(selected).toHaveText([file])
|
||||
await expect(files).toHaveAttribute("data-browsing", "false")
|
||||
|
||||
@@ -259,23 +248,20 @@ test("mobile changes summarize expanded diffs, stage comments, wrap by setting,
|
||||
}
|
||||
})
|
||||
|
||||
test("summary drawer dismisses by backdrop, Escape, and drag", async ({ page }) => {
|
||||
test("summary drawer dismisses by button, backdrop, Escape, and drag", async ({ page }) => {
|
||||
await mockStressTimeline(page)
|
||||
await page.goto(sessionHref(fixture.targetID))
|
||||
|
||||
const more = page
|
||||
.getByRole("tablist", { name: "Session view", exact: true })
|
||||
.getByRole("tab", { name: "More...", exact: true })
|
||||
.locator('[data-slot="session-mobile-view-navigation"]')
|
||||
.getByRole("button", { name: "More options", exact: true })
|
||||
|
||||
const drawer = page.getByRole("dialog", { name: "Session details", exact: true })
|
||||
const overlay = page.locator('[data-slot="mobile-drawer-overlay"]')
|
||||
|
||||
for (const dismissal of ["backdrop", "escape", "drag"] as const) {
|
||||
for (const dismissal of ["button", "backdrop", "escape", "drag"] as const) {
|
||||
await more.click()
|
||||
await page
|
||||
.getByRole("dialog", { name: "More options", exact: true })
|
||||
.getByRole("button", { name: "Session details", exact: true })
|
||||
.click()
|
||||
await page.getByRole("menuitem", { name: "Session details", exact: true }).click()
|
||||
await expect(drawer.getByRole("button", { name: "MCP", exact: true })).toBeVisible()
|
||||
// Corvu starts opening after paint; the transition flag is also absent
|
||||
// before that callback. Wait for the open position before dismissing.
|
||||
@@ -284,6 +270,8 @@ test("summary drawer dismisses by backdrop, Escape, and drag", async ({ page })
|
||||
.toBe(0)
|
||||
await expect(drawer).not.toHaveAttribute("data-transitioning")
|
||||
|
||||
if (dismissal === "button") await drawer.getByRole("button", { name: "Close", exact: true }).click()
|
||||
|
||||
if (dismissal === "backdrop") await overlay.click({ position: { x: 10, y: 10 } })
|
||||
|
||||
if (dismissal === "escape") await page.keyboard.press("Escape")
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { fixture, installStressSessionTabs } from "../utils/session-fixture"
|
||||
import { sessionHref } from "../utils/app"
|
||||
import { openWithDirection } from "../utils/direction"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectAppVisible } from "../utils/waits"
|
||||
|
||||
for (const direction of ["ltr", "rtl"] as const) {
|
||||
for (const paid of [true, false]) {
|
||||
test(`mobile model and variant pickers use drawers with ${paid ? "paid" : "free"} models in ${direction}`, async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await openPickerSession(page, direction, paid)
|
||||
const composer = page.locator('[data-component="composer"]')
|
||||
const variant = composer.getByRole("button", { name: "Choose model variant", exact: true })
|
||||
await expect(variant).toHaveText("balanced")
|
||||
await variant.click()
|
||||
const variants = page.getByRole("dialog", { name: "Choose model variant", exact: true })
|
||||
await expect(variants.getByRole("button", { name: "balanced", exact: true })).toHaveAttribute(
|
||||
"aria-pressed",
|
||||
"true",
|
||||
)
|
||||
await expect(variants.getByRole("button", { name: "Close", exact: true })).toHaveCount(0)
|
||||
await expect(variants).toBeInViewport({ ratio: 1 })
|
||||
await expect(variants).not.toHaveAttribute("data-transitioning")
|
||||
await variants.screenshot({ path: testInfo.outputPath("variants.png") })
|
||||
await page.keyboard.press("Escape")
|
||||
await expect(variants).toBeHidden()
|
||||
await expect(variant).toBeFocused()
|
||||
await variant.click()
|
||||
await variants.getByRole("button", { name: "high", exact: true }).click()
|
||||
await expect(variants).toBeHidden()
|
||||
await expect(variant).toHaveText("high")
|
||||
await expect(variant).toBeFocused()
|
||||
|
||||
const model = composer.locator('[data-action="composer-model"]')
|
||||
await expect(model).toHaveText("Mobile Model A")
|
||||
await expect(model).toHaveAttribute("data-control-type", "drawer")
|
||||
await model.click()
|
||||
const models = page.getByRole("dialog", { name: "Select model", exact: true })
|
||||
const search = models.getByRole("searchbox", { name: "Search models", exact: true })
|
||||
await expect(models.locator('[data-slot="model-selector-drawer"]')).toBeFocused()
|
||||
await expect(search).not.toBeFocused()
|
||||
await expect(models.getByRole("button", { name: /Mobile Model A/ })).toHaveAttribute("aria-pressed", "true")
|
||||
await expect(models.getByRole("button", { name: "Close", exact: true })).toHaveCount(0)
|
||||
await expect(models).toBeInViewport({ ratio: 1 })
|
||||
await expect(models).not.toHaveAttribute("data-transitioning")
|
||||
await expect
|
||||
.poll(() =>
|
||||
models.evaluate((element) => {
|
||||
const drawer = element.getBoundingClientRect()
|
||||
const search = element.querySelector('[data-component="text-input-v2"]')!.getBoundingClientRect()
|
||||
const row = element.querySelector('[data-component="settings-row"]')!.getBoundingClientRect()
|
||||
const searchClip = element.querySelector('[data-slot="mobile-panel-content"]')!.getBoundingClientRect()
|
||||
const rowClip = element.querySelector(".settings-models")!.getBoundingClientRect()
|
||||
|
||||
return {
|
||||
searchStart: Math.round(search.left - drawer.left),
|
||||
searchEnd: Math.round(drawer.right - search.right),
|
||||
rowStart: Math.round(row.left - drawer.left),
|
||||
rowEnd: Math.round(drawer.right - row.right),
|
||||
searchClipStart: Math.round(search.left - searchClip.left),
|
||||
searchClipEnd: Math.round(searchClip.right - search.right),
|
||||
rowClipStart: Math.round(row.left - rowClip.left),
|
||||
rowClipEnd: Math.round(rowClip.right - row.right),
|
||||
}
|
||||
}),
|
||||
)
|
||||
.toEqual({
|
||||
searchStart: 12,
|
||||
searchEnd: 12,
|
||||
rowStart: 12,
|
||||
rowEnd: 12,
|
||||
searchClipStart: 12,
|
||||
searchClipEnd: 12,
|
||||
rowClipStart: 12,
|
||||
rowClipEnd: 12,
|
||||
})
|
||||
await expect(models.getByRole("button", { name: /Mobile Model A/ })).toHaveCSS("padding-inline-start", "12px")
|
||||
await expect(models.getByRole("button", { name: /Mobile Model A/ })).toHaveCSS("padding-block-start", "16px")
|
||||
await models.screenshot({ path: testInfo.outputPath("models.png") })
|
||||
await search.click()
|
||||
await expect(search).toBeFocused()
|
||||
await search.fill("no-matching-model")
|
||||
await expect(models.getByText("No model results", { exact: true })).toBeVisible()
|
||||
await search.fill("Mobile Model B")
|
||||
await expect(models.getByRole("button", { name: /Mobile Model A/ })).toHaveCount(0)
|
||||
await search.press("Enter")
|
||||
await expect(models).toBeHidden()
|
||||
await expect(model).toHaveText("Mobile Model B")
|
||||
await expect(composer.getByRole("textbox", { name: "Prompt", exact: true })).toBeFocused()
|
||||
|
||||
await model.click()
|
||||
await expect(search).toHaveValue("")
|
||||
await expect(models.locator('[data-slot="model-selector-drawer"]')).toBeFocused()
|
||||
await expect(models.getByRole("button", { name: /Mobile Model B/ })).toHaveAttribute("aria-pressed", "true")
|
||||
await expect
|
||||
.poll(() => models.evaluate((element) => new DOMMatrixReadOnly(getComputedStyle(element).transform).m42))
|
||||
.toBe(0)
|
||||
await expect(models).not.toHaveAttribute("data-transitioning")
|
||||
await page.locator('[data-slot="mobile-drawer-overlay"]').click({ position: { x: 10, y: 10 } })
|
||||
await expect(models).toBeHidden()
|
||||
await expect(model).toBeFocused()
|
||||
|
||||
if (paid) {
|
||||
await model.click()
|
||||
await models.getByRole("button", { name: "Manage models", exact: true }).click()
|
||||
await expect(models).toBeHidden()
|
||||
await expect(page.getByRole("dialog", { name: "Manage models", exact: true })).toBeVisible()
|
||||
await expect(page.getByRole("dialog")).toHaveCount(1)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await model.click()
|
||||
await models.getByRole("button", { name: "Connect provider", exact: true }).click()
|
||||
await expect(models).toBeHidden()
|
||||
const providers = page.getByRole("dialog", { name: "Connect provider", exact: true })
|
||||
await expect(providers.getByRole("searchbox", { name: "Search providers", exact: true })).toBeVisible()
|
||||
await expect(page.getByRole("dialog")).toHaveCount(1)
|
||||
})
|
||||
}
|
||||
|
||||
test(`desktop model and variant pickers retain menus in ${direction}`, async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 900 })
|
||||
await openPickerSession(page, direction, true)
|
||||
const composer = page.locator('[data-component="composer"]')
|
||||
const variant = composer.getByRole("button", { name: "Choose model variant", exact: true })
|
||||
await variant.click()
|
||||
await page.getByRole("menuitemradio", { name: "high", exact: true }).click()
|
||||
await expect(variant).toHaveText("high")
|
||||
await expect(page.locator('[data-slot="mobile-drawer-content"]')).toHaveCount(0)
|
||||
const model = composer.locator('[data-action="composer-model"]')
|
||||
await expect(model).toHaveAttribute("data-control-type", "popover")
|
||||
await model.click()
|
||||
await expect(page.getByPlaceholder("Search models", { exact: true })).toBeFocused()
|
||||
await page.getByRole("menuitemradio", { name: /Mobile Model B/ }).click()
|
||||
await expect(model).toHaveText("Mobile Model B")
|
||||
await expect(page.locator('[data-slot="mobile-drawer-content"]')).toHaveCount(0)
|
||||
})
|
||||
}
|
||||
|
||||
test.describe("touch composer pickers", () => {
|
||||
test.use({ viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true })
|
||||
|
||||
test("model and default variant open with one tap", async ({ page }) => {
|
||||
await openPickerSession(page, "ltr", true)
|
||||
expect(await page.evaluate(() => matchMedia("(hover: none)").matches)).toBe(true)
|
||||
const composer = page.locator('[data-component="composer"]')
|
||||
const variant = composer.getByRole("button", { name: "Choose model variant", exact: true })
|
||||
await variant.tap()
|
||||
const variants = page.getByRole("dialog", { name: "Choose model variant", exact: true })
|
||||
await variants.getByRole("button", { name: "default", exact: true }).tap()
|
||||
await expect(variants).toBeHidden()
|
||||
await expect(variant).toHaveText("default")
|
||||
await page.touchscreen.tap(5, 300)
|
||||
await expect(variant).toHaveCSS("pointer-events", "auto")
|
||||
await expect(variant).toHaveCSS("opacity", "1")
|
||||
await variant.tap()
|
||||
await expect(variants).toBeVisible()
|
||||
await expect
|
||||
.poll(() => variants.evaluate((element) => new DOMMatrixReadOnly(getComputedStyle(element).transform).m42))
|
||||
.toBe(0)
|
||||
await page.locator('[data-slot="mobile-drawer-overlay"]').tap({ position: { x: 10, y: 10 } })
|
||||
await expect(variants).toBeHidden()
|
||||
|
||||
await composer.locator('[data-action="composer-model"]').tap()
|
||||
const models = page.getByRole("dialog", { name: "Select model", exact: true })
|
||||
await expect(models.locator('[data-slot="model-selector-drawer"]')).toBeFocused()
|
||||
const search = models.getByRole("searchbox", { name: "Search models", exact: true })
|
||||
await expect(search).not.toBeFocused()
|
||||
await search.tap()
|
||||
await expect(search).toBeFocused()
|
||||
})
|
||||
})
|
||||
|
||||
async function openPickerSession(page: Page, direction: "ltr" | "rtl", paid: boolean) {
|
||||
await mockOpenCodeServer(page, {
|
||||
...fixture,
|
||||
sessions: fixture.sessions.map((session) => ({
|
||||
...session,
|
||||
model: { id: "mobile-a", providerID: "opencode", variant: "balanced" },
|
||||
})),
|
||||
provider: {
|
||||
all: [
|
||||
{
|
||||
id: "opencode",
|
||||
name: "OpenCode",
|
||||
models: Object.fromEntries(
|
||||
["a", "b"].map((id) => [
|
||||
`mobile-${id}`,
|
||||
{
|
||||
id: `mobile-${id}`,
|
||||
name: `Mobile Model ${id.toUpperCase()}`,
|
||||
cost: { input: paid ? 1 : 0, output: paid ? 1 : 0 },
|
||||
limit: { context: 200_000 },
|
||||
variants: { balanced: {}, high: {} },
|
||||
},
|
||||
]),
|
||||
),
|
||||
},
|
||||
],
|
||||
connected: ["opencode"],
|
||||
default: { providerID: "opencode", modelID: "mobile-a" },
|
||||
},
|
||||
pageMessages: () => ({ items: [] }),
|
||||
})
|
||||
await installStressSessionTabs(page)
|
||||
await page.addInitScript((direction) => {
|
||||
localStorage.setItem("opencode-color-scheme", direction === "rtl" ? "dark" : "light")
|
||||
localStorage.setItem(
|
||||
"opencode.global.dat:model",
|
||||
JSON.stringify({
|
||||
user: ["a", "b"].map((id) => ({ providerID: "opencode", modelID: `mobile-${id}`, visibility: "show" })),
|
||||
recent: [],
|
||||
variant: {},
|
||||
}),
|
||||
)
|
||||
}, direction)
|
||||
await openWithDirection(page, sessionHref(fixture.targetID), direction)
|
||||
await expectAppVisible(page.getByRole("textbox", { name: "Prompt", exact: true }))
|
||||
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toBeEditable()
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import { expect, test } from "@playwright/test"
|
||||
import { fixture, mockStressTimeline } from "../utils/session-fixture"
|
||||
import { sessionHref } from "../utils/app"
|
||||
import { openWithDirection } from "../utils/direction"
|
||||
|
||||
for (const scheme of ["light", "dark"] as const) {
|
||||
for (const direction of ["ltr", "rtl"] as const) {
|
||||
test(`mobile session options are compact and readable in ${scheme} ${direction}`, async ({ page }, testInfo) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await mockStressTimeline(page)
|
||||
await page.addInitScript((scheme) => {
|
||||
localStorage.setItem("opencode-color-scheme", scheme)
|
||||
localStorage.setItem("settings.v3", JSON.stringify({ general: { mobileTitlebarPosition: "bottom" } }))
|
||||
}, scheme)
|
||||
await openWithDirection(page, sessionHref(fixture.targetID), direction)
|
||||
const tabs = page.getByRole("tablist", { name: "Session view", exact: true })
|
||||
const more = tabs.getByRole("tab", { name: "More...", exact: true })
|
||||
await expect(tabs.getByRole("tab", { selected: true })).toHaveText("Session")
|
||||
await more.click()
|
||||
const drawer = page.getByRole("dialog", { name: "More options", exact: true })
|
||||
await expect(drawer).toBeVisible()
|
||||
await expect(drawer).not.toHaveAttribute("data-transitioning")
|
||||
await expect(drawer.locator('[data-slot="mobile-panel-header"]')).toHaveCount(0)
|
||||
await expect(drawer.getByRole("button", { name: "Close", exact: true })).toHaveCount(0)
|
||||
await expect(drawer.getByRole("heading", { name: "More options", exact: true })).toHaveCSS("width", "1px")
|
||||
await expect(page.locator("html")).toHaveAttribute("data-color-scheme", scheme)
|
||||
await expect
|
||||
.poll(() =>
|
||||
drawer.evaluate((element) => {
|
||||
const options = element.querySelector('[data-slot="session-mobile-view-options"]')!
|
||||
|
||||
return getComputedStyle(options).backgroundColor !== getComputedStyle(element).backgroundColor
|
||||
}),
|
||||
)
|
||||
.toBe(true)
|
||||
await drawer.screenshot({ path: testInfo.outputPath("options.png") })
|
||||
await testInfo.attach("options", { path: testInfo.outputPath("options.png"), contentType: "image/png" })
|
||||
|
||||
for (const name of ["Files", "Terminal", "Usage", "Session details"]) {
|
||||
const option = drawer.getByRole("button", { name, exact: true })
|
||||
await expect(option).toHaveCSS("height", "44px")
|
||||
await expect
|
||||
.poll(() =>
|
||||
option.evaluate((element) => {
|
||||
const context = document.createElement("canvas").getContext("2d")!
|
||||
|
||||
function luminance(color: string) {
|
||||
context.clearRect(0, 0, 1, 1)
|
||||
context.fillStyle = color
|
||||
context.fillRect(0, 0, 1, 1)
|
||||
|
||||
const channels = Array.from(context.getImageData(0, 0, 1, 1).data)
|
||||
.slice(0, 3)
|
||||
.map((channel) => {
|
||||
const value = channel / 255
|
||||
|
||||
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4
|
||||
})
|
||||
|
||||
return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722
|
||||
}
|
||||
|
||||
const text = luminance(getComputedStyle(element).color)
|
||||
const surface = luminance(getComputedStyle(element.parentElement!).backgroundColor)
|
||||
|
||||
return (Math.max(text, surface) + 0.05) / (Math.min(text, surface) + 0.05)
|
||||
}),
|
||||
)
|
||||
.toBeGreaterThanOrEqual(4.5)
|
||||
}
|
||||
|
||||
await page.keyboard.press("Escape")
|
||||
await expect(drawer).toBeHidden()
|
||||
await expect(more).toBeFocused()
|
||||
|
||||
for (const dismissal of ["backdrop", "drag"] as const) {
|
||||
await more.click()
|
||||
await expect
|
||||
.poll(() => drawer.evaluate((element) => new DOMMatrixReadOnly(getComputedStyle(element).transform).m42))
|
||||
.toBe(0)
|
||||
await expect(drawer).not.toHaveAttribute("data-transitioning")
|
||||
|
||||
if (dismissal === "backdrop")
|
||||
await page.locator('[data-slot="mobile-drawer-overlay"]').click({ position: { x: 10, y: 10 } })
|
||||
|
||||
if (dismissal === "drag") {
|
||||
const handle = await drawer.locator('[data-slot="mobile-drawer-handle"]').boundingBox()
|
||||
expect(handle).not.toBeNull()
|
||||
await page.mouse.move(handle!.x + handle!.width / 2, handle!.y + handle!.height / 2)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(handle!.x + handle!.width / 2, handle!.y + handle!.height / 2 + 1)
|
||||
await page.mouse.move(handle!.x + handle!.width / 2, 843)
|
||||
await page.mouse.up()
|
||||
}
|
||||
|
||||
await expect(drawer).toBeHidden()
|
||||
await expect(more).toBeFocused()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,7 @@ for (const position of ["top", "bottom"] as const) {
|
||||
|
||||
const tabs = page.getByRole("tablist", { name: "Session view", exact: true })
|
||||
const navigation = page.locator('[data-slot="session-mobile-view-navigation"]')
|
||||
const more = tabs.getByRole("tab", { name: "More...", exact: true })
|
||||
const views = page.getByRole("dialog", { name: "More options", exact: true })
|
||||
const more = navigation.getByRole("button", { name: "More options", exact: true })
|
||||
const picker = tabs.getByRole("tab", { selected: true })
|
||||
|
||||
const message = page.locator(
|
||||
@@ -38,39 +37,32 @@ for (const position of ["top", "bottom"] as const) {
|
||||
|
||||
const composer = page.getByRole("textbox", { name: "Prompt", exact: true })
|
||||
await expect(picker).toHaveText("Session")
|
||||
await expect(navigation).toHaveCount(1)
|
||||
await expect(message).toBeVisible()
|
||||
await expect(composer).toBeVisible()
|
||||
await expect(tabs.getByRole("tab")).toHaveText(["Session", "Changes", "More..."])
|
||||
await expect(tabs.getByRole("tab")).toHaveText(["Session", "Changes", "Files", "Terminal"])
|
||||
await expect(tabs).toHaveCSS("padding-left", "0px")
|
||||
await expect(tabs).toHaveCSS("padding-right", "0px")
|
||||
|
||||
if (position === "top") {
|
||||
const titlebar = page.locator('[data-slot="titlebar-v2"]')
|
||||
await expect(titlebar).toHaveCSS("padding-top", "8px")
|
||||
await expect(titlebar).toHaveCSS("height", "36px")
|
||||
await expect(titlebar).toHaveCSS("padding-top", "16px")
|
||||
await expect(titlebar).toHaveCSS("height", "44px")
|
||||
}
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const bounds = await navigation.boundingBox()
|
||||
|
||||
return !!bounds && bounds.x >= 1 && bounds.x <= 2 && bounds.width >= 386 && bounds.width <= 388
|
||||
return !!bounds && bounds.x >= 8 && bounds.x <= 9 && bounds.width >= 372 && bounds.width <= 374
|
||||
})
|
||||
.toBe(true)
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const bar = await tabs.boundingBox()
|
||||
const input = await composer.boundingBox()
|
||||
const dock = await page.locator('[data-component="session-composer-dock"]').boundingBox()
|
||||
const panel = await page.locator('[data-slot="session-chat-panel"]').boundingBox()
|
||||
|
||||
if (!bar || !input || !dock || !panel) return false
|
||||
|
||||
if (position === "bottom")
|
||||
return bar.y >= dock.y + dock.height && Math.abs(bar.y + bar.height - panel.y - panel.height) <= 1
|
||||
|
||||
return Math.abs(bar.y - panel.y) <= 1 && bar.y + bar.height <= input.y
|
||||
return !!bar && !!input && !!panel && Math.abs(bar.y - panel.y) <= 1 && bar.y + bar.height <= input.y
|
||||
})
|
||||
.toBe(true)
|
||||
await expect(page.locator("[data-session-title]")).toHaveCount(0)
|
||||
@@ -90,21 +82,8 @@ for (const position of ["top", "bottom"] as const) {
|
||||
await expect(drawer).toBeHidden()
|
||||
|
||||
await more.click()
|
||||
await expect(views).toBeVisible()
|
||||
await expect(more).toHaveAttribute("aria-expanded", "true")
|
||||
|
||||
for (const name of ["Files", "Terminal", "Usage", "Session details"])
|
||||
await expect(views.getByRole("button", { name, exact: true })).toBeVisible()
|
||||
await expect(views).not.toHaveAttribute("data-transitioning")
|
||||
await page.keyboard.press("Escape")
|
||||
await expect(views).toBeHidden()
|
||||
await expect(more).toHaveAttribute("aria-expanded", "false")
|
||||
await expect(more).toBeFocused()
|
||||
await expect(picker).toHaveText("Session")
|
||||
await more.press("Enter")
|
||||
await views.getByRole("button", { name: "Usage", exact: true }).click()
|
||||
await expect(views).toBeHidden()
|
||||
await expect(picker).toHaveText("More...")
|
||||
await page.getByRole("menuitem", { name: "Usage", exact: true }).click()
|
||||
await expect(picker).toHaveCount(0)
|
||||
await expect(page.getByText("Total Cost", { exact: true })).toBeVisible()
|
||||
const usage = page.locator('[data-slot="session-usage-content"]')
|
||||
await expect(usage).toHaveCSS("padding-top", "16px")
|
||||
@@ -113,32 +92,24 @@ for (const position of ["top", "bottom"] as const) {
|
||||
await expect(composer).toBeHidden()
|
||||
|
||||
await more.click()
|
||||
await expect(views.getByRole("button", { name: "Usage", exact: true })).toHaveAttribute("aria-pressed", "true")
|
||||
await expect(views.getByRole("button", { name: "Status", exact: true })).toHaveCount(0)
|
||||
await views.evaluate((element) => element.setAttribute("data-drawer-probe", "single"))
|
||||
await views.getByRole("button", { name: "Session details", exact: true }).click()
|
||||
await expect(page.getByRole("menuitem", { name: "Status", exact: true })).toHaveCount(0)
|
||||
await page.getByRole("menuitem", { name: "Session details", exact: true }).click()
|
||||
const details = page.getByRole("dialog", { name: "Session details", exact: true })
|
||||
await expect(details).toHaveAttribute("data-drawer-probe", "single")
|
||||
await expect(page.getByRole("dialog")).toHaveCount(1)
|
||||
await expect(page.locator('[data-slot="mobile-drawer-overlay"]')).toHaveCount(1)
|
||||
await expect(details.locator('[data-slot="mobile-panel-header"]')).toHaveCount(0)
|
||||
await expect(details.getByRole("button", { name: "Close", exact: true })).toHaveCount(0)
|
||||
await expect(details.getByRole("heading", { name: "Session details", exact: true })).toHaveCSS("width", "1px")
|
||||
await expect(details.getByText(fixture.project.name, { exact: true })).toBeVisible()
|
||||
await expect(details.getByRole("button", { name: "No changes", exact: true })).toBeVisible()
|
||||
await expect(details.getByRole("button", { name: "MCP", exact: true })).toBeVisible()
|
||||
await page.locator('[data-slot="mobile-drawer-overlay"]').click({ position: { x: 10, y: 10 } })
|
||||
await details.getByRole("button", { name: "Close", exact: true }).click()
|
||||
await expect(details).toBeHidden()
|
||||
await expect(more).toBeFocused()
|
||||
await more.click()
|
||||
await views.getByRole("button", { name: "Session details", exact: true }).click()
|
||||
await page.getByRole("menuitem", { name: "Session details", exact: true }).click()
|
||||
await expect(details.getByRole("button", { name: "No changes", exact: true })).toBeVisible()
|
||||
await expect(details).not.toHaveAttribute("data-transitioning")
|
||||
await page.keyboard.press("Escape")
|
||||
await expect(details).toBeHidden()
|
||||
await expect(more).toBeFocused()
|
||||
await more.click()
|
||||
await views.getByRole("button", { name: "Session details", exact: true }).click()
|
||||
await page.getByRole("menuitem", { name: "Session details", exact: true }).click()
|
||||
await details.getByRole("button", { name: "No changes", exact: true }).click()
|
||||
await expect(details).toBeHidden()
|
||||
await expect(picker).toHaveText("Changes")
|
||||
@@ -147,23 +118,18 @@ for (const position of ["top", "bottom"] as const) {
|
||||
await expect(page.locator('[data-slot="session-review-header"]')).toHaveCSS("padding-left", "8px")
|
||||
await expect(composer).toBeHidden()
|
||||
await more.click()
|
||||
await views.getByRole("button", { name: "Session details", exact: true }).click()
|
||||
await page.getByRole("menuitem", { name: "Session details", exact: true }).click()
|
||||
await details.getByRole("button", { name: "No changes", exact: true }).click()
|
||||
await expect(details).toBeHidden()
|
||||
await expect(picker).toHaveText("Changes")
|
||||
await expect(page.getByText("No uncommitted changes yet", { exact: true })).toBeVisible()
|
||||
|
||||
await more.click()
|
||||
await views.getByRole("button", { name: "Files", exact: true }).click()
|
||||
await expect(views).toBeHidden()
|
||||
await expect(picker).toHaveText("More...")
|
||||
await tabs.getByRole("tab", { name: "Files", exact: true }).click()
|
||||
await expect(picker).toHaveText("Files")
|
||||
await expect(page.getByRole("combobox", { name: "Filter files", exact: true })).toBeVisible()
|
||||
await expect(composer).toBeHidden()
|
||||
|
||||
await more.click()
|
||||
await expect(views.getByRole("button", { name: "Files", exact: true })).toHaveAttribute("aria-pressed", "true")
|
||||
await views.getByRole("button", { name: "Terminal", exact: true }).click()
|
||||
await expect(views).toBeHidden()
|
||||
await tabs.getByRole("tab", { name: "Terminal", exact: true }).click()
|
||||
const panel = page.locator("#terminal-panel")
|
||||
await expect(panel).toHaveAttribute("data-opened", "true")
|
||||
await expect(panel.getByRole("tab", { name: /Terminal 1/ })).toBeVisible()
|
||||
@@ -180,14 +146,14 @@ for (const position of ["top", "bottom"] as const) {
|
||||
await expect(panel).toHaveAttribute("data-cache-probe", "original")
|
||||
|
||||
await page.keyboard.press("Control+Backquote")
|
||||
await expect(picker).toHaveText("More...")
|
||||
await expect(picker).toHaveText("Terminal")
|
||||
await expect(panel).toBeVisible()
|
||||
await expect(panel).toHaveAttribute("data-cache-probe", "original")
|
||||
await page.keyboard.press("Control+Backquote")
|
||||
await expect(picker).toHaveText("Session")
|
||||
|
||||
await page.keyboard.press("Control+Backquote")
|
||||
await expect(picker).toHaveText("More...")
|
||||
await expect(picker).toHaveText("Terminal")
|
||||
await panel.getByRole("button", { name: "Close terminal", exact: true }).click()
|
||||
await expect(picker).toHaveText("Session")
|
||||
await expect(panel).toBeHidden()
|
||||
@@ -208,20 +174,18 @@ for (const position of ["top", "bottom"] as const) {
|
||||
}
|
||||
|
||||
await more.click()
|
||||
await views.getByRole("button", { name: "Usage", exact: true }).click()
|
||||
await page.getByRole("menuitem", { name: "Usage", exact: true }).click()
|
||||
await expect(page.getByText("Total Cost", { exact: true })).toBeVisible()
|
||||
await openTab(fixture.expected.sourceTitle)
|
||||
await expect(picker).toHaveText("Session")
|
||||
await more.click()
|
||||
await views.getByRole("button", { name: "Files", exact: true }).click()
|
||||
await expect(picker).toHaveText("More...")
|
||||
await tabs.getByRole("tab", { name: "Files", exact: true }).click()
|
||||
await expect(picker).toHaveText("Files")
|
||||
await openTab(fixture.expected.targetTitle)
|
||||
await expect(picker).toHaveText("Session")
|
||||
await openTab(fixture.expected.sourceTitle)
|
||||
await expect(picker).toHaveText("Session")
|
||||
await more.click()
|
||||
await views.getByRole("button", { name: "Files", exact: true }).click()
|
||||
await expect(picker).toHaveText("More...")
|
||||
await tabs.getByRole("tab", { name: "Files", exact: true }).click()
|
||||
await expect(picker).toHaveText("Files")
|
||||
await openTabs()
|
||||
await drawer.getByRole("button", { name: "Home", exact: true }).click()
|
||||
await expect(page).toHaveURL("/")
|
||||
@@ -256,15 +220,15 @@ test("the summary's changes row switches the view and keeps the side tabs", asyn
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
const tabs = page.getByRole("tablist", { name: "Session view", exact: true })
|
||||
const details = page.getByRole("dialog", { name: "Session details", exact: true })
|
||||
const more = tabs.getByRole("tab", { name: "More...", exact: true })
|
||||
const views = page.getByRole("dialog", { name: "More options", exact: true })
|
||||
await more.click()
|
||||
await views.getByRole("button", { name: "Session details", exact: true }).click()
|
||||
await page
|
||||
.locator('[data-slot="session-mobile-view-navigation"]')
|
||||
.getByRole("button", { name: "More options", exact: true })
|
||||
.click()
|
||||
await page.getByRole("menuitem", { name: "Session details", exact: true }).click()
|
||||
await details.getByRole("button", { name: "No changes", exact: true }).click()
|
||||
await expect(details).toBeHidden()
|
||||
await expect(tabs.getByRole("tab", { selected: true })).toHaveText("Changes")
|
||||
await more.click()
|
||||
await views.getByRole("button", { name: "Files", exact: true }).click()
|
||||
await tabs.getByRole("tab", { name: "Files", exact: true }).click()
|
||||
await expect(
|
||||
page.getByRole("tablist", { name: "Open files", exact: true }).getByRole("tab", { name: "README.md", exact: true }),
|
||||
).toHaveAttribute("aria-selected", "true")
|
||||
@@ -280,8 +244,7 @@ test("the summary's changes row switches the view and keeps the side tabs", asyn
|
||||
await page.reload()
|
||||
await expect(launcher).toHaveAttribute("aria-selected", "true")
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await more.click()
|
||||
await views.getByRole("button", { name: "Files", exact: true }).click()
|
||||
await tabs.getByRole("tab", { name: "Files", exact: true }).click()
|
||||
const files = page.locator('[data-slot="session-mobile-files"]')
|
||||
await files.getByRole("button", { name: "README.md", exact: true }).click()
|
||||
await expect(files.getByText("contents:README.md", { exact: true })).toBeVisible()
|
||||
@@ -305,17 +268,14 @@ test("a narrow-screen palette pick keeps the view and dock, and the side region
|
||||
|
||||
const tabs = page.getByRole("tablist", { name: "Session view", exact: true })
|
||||
const terminal = page.locator("#terminal-panel")
|
||||
const views = page.getByRole("dialog", { name: "More options", exact: true })
|
||||
await tabs.getByRole("tab", { name: "More...", exact: true }).click()
|
||||
await views.getByRole("button", { name: "Terminal", exact: true }).click()
|
||||
await expect(views).toBeHidden()
|
||||
await tabs.getByRole("tab", { name: "Terminal", exact: true }).click()
|
||||
await expect(terminal.locator("textarea")).toBeEditable()
|
||||
await page.keyboard.press("ControlOrMeta+p")
|
||||
const dialog = page.getByRole("dialog")
|
||||
await dialog.getByRole("textbox").fill("README")
|
||||
await dialog.getByRole("option", { name: "/ README.md", exact: true }).click()
|
||||
await expect(dialog).toHaveCount(0)
|
||||
await expect(tabs.getByRole("tab", { selected: true })).toHaveText("More...")
|
||||
await expect(tabs.getByRole("tab", { selected: true })).toHaveText("Terminal")
|
||||
await expect(terminal).toHaveAttribute("data-opened", "true")
|
||||
|
||||
await page.setViewportSize({ width: 1280, height: 900 })
|
||||
|
||||
@@ -859,45 +859,6 @@ for (const delivery of ["steer", "queue"] as const) {
|
||||
await expect(pending).toContainText(followUp)
|
||||
await expect(thinking).toHaveCount(0)
|
||||
|
||||
const bubble = pending.locator('[data-slot="user-message-text"]')
|
||||
|
||||
const colors =
|
||||
delivery === "steer"
|
||||
? {
|
||||
delivered: await userRow(page, userID)
|
||||
.locator('[data-slot="user-message-text"]')
|
||||
.evaluate((element) => ({
|
||||
background: getComputedStyle(element).backgroundColor,
|
||||
text: getComputedStyle(element).color,
|
||||
})),
|
||||
pending: await bubble.evaluate((element) => {
|
||||
const probe = document.createElement("span")
|
||||
probe.style.backgroundColor = "var(--v2-background-bg-layer-02)"
|
||||
probe.style.color = "var(--v2-text-text-base)"
|
||||
element.appendChild(probe)
|
||||
|
||||
const colors = {
|
||||
background: getComputedStyle(probe).backgroundColor,
|
||||
text: getComputedStyle(probe).color,
|
||||
}
|
||||
|
||||
probe.remove()
|
||||
|
||||
return colors
|
||||
}),
|
||||
}
|
||||
: undefined
|
||||
|
||||
if (colors) {
|
||||
await expect(bubble).toHaveCSS("background-color", colors.pending.background)
|
||||
await expect(bubble).toHaveCSS("color", colors.pending.text)
|
||||
await pending.hover()
|
||||
await expect(pending.locator('[data-slot="user-message-meta"]')).toHaveText(
|
||||
/^Pending\s*·\s*Build\s*·\s*Queue Model$/,
|
||||
)
|
||||
await bubble.evaluate((element) => (element.dataset.deliveryMarker = "pending"))
|
||||
}
|
||||
|
||||
// The next assistant step still belongs to U1: U2 has been admitted, not delivered.
|
||||
mock.emit("session.step.started", {
|
||||
sessionID,
|
||||
@@ -960,18 +921,6 @@ for (const delivery of ["steer", "queue"] as const) {
|
||||
mock.emit("session.inbox.delivered", { sessionID, inboxID })
|
||||
await expect(thinking).toHaveCount(0)
|
||||
await expect(pending).toHaveCount(1)
|
||||
|
||||
if (colors) {
|
||||
await expect(bubble).toHaveAttribute("data-delivery-marker", "pending")
|
||||
await expect(pending.locator('[data-slot="user-message-meta"]')).toHaveText(/^Build\s*·\s*Queue Model$/)
|
||||
await expect(bubble).toHaveCSS("background-color", colors.delivered.background)
|
||||
await expect(bubble).toHaveCSS("color", colors.delivered.text)
|
||||
await expect(bubble).toHaveCSS("transition-property", "background-color, color")
|
||||
await transcript.screenshot({ path: testInfo.outputPath("delivered-steer.png") })
|
||||
await page.emulateMedia({ reducedMotion: "reduce" })
|
||||
await expect(bubble).toHaveCSS("transition-duration", "0s")
|
||||
}
|
||||
|
||||
await expect(transcript.locator('[data-timeline-row="UserMessage"]')).toHaveCount(2)
|
||||
await expect(transcript.locator('[data-timeline-row="AssistantPart"]').filter({ has: tools })).toHaveAttribute(
|
||||
"data-message-id",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import type { ConfigEntry, OpenCodeEvent, WorktreeDirectory } from "@opencode/client/promise"
|
||||
import { NO_PROVIDER, REMOTE_SERVER, SERVER, holdRoute, project, session } from "../utils/app"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { mockRemoteServer, mockWorkspace, openSettings, type WorkspaceInput } from "../utils/workspace"
|
||||
|
||||
const directory = "C:/Projects/settings-demo"
|
||||
@@ -464,7 +463,7 @@ test("worktrees follow inventory events, wait for session counts, and delete by
|
||||
worktrees: () => inventory,
|
||||
onWorktreeRemove: (body) => {
|
||||
inventory.splice(
|
||||
inventory.findIndex((item) => item.directory === body.directory),
|
||||
inventory.findIndex((item) => item.directory === (body as { directory: string }).directory),
|
||||
1,
|
||||
)
|
||||
},
|
||||
@@ -500,7 +499,6 @@ test("worktrees follow inventory events, wait for session counts, and delete by
|
||||
// Home never listed this session, so only the new worktree's directory read can show it.
|
||||
view.sessions.push(session({ id: "ses_discovered", directory: discovered, title: "Discovered session", projectID }))
|
||||
inventory.push({ directory: discovered, strategy: "git" })
|
||||
// SAFETY: a worktree.updated event carries only the project ID, which is all the app reads from it.
|
||||
await view.push([
|
||||
{ id: "evt_settings_worktree_updated", created: Date.now(), type: "worktree.updated", data: { projectID } },
|
||||
] as OpenCodeEvent[])
|
||||
@@ -584,9 +582,7 @@ test.describe("worktrees prefetch", () => {
|
||||
|
||||
test("server Worktrees hover only prefetches metadata", async ({ page }) => {
|
||||
const { settings } = await open(page)
|
||||
|
||||
const calls = { projects: 0, worktrees: new Array<string>(), refreshes: new Array<string>() }
|
||||
|
||||
const calls = { projects: 0, worktrees: [] as string[], refreshes: [] as string[] }
|
||||
page.on("request", (request) => {
|
||||
if (new URL(request.url()).pathname === "/api/worktree/refresh")
|
||||
calls.refreshes.push(request.postDataJSON().projectID)
|
||||
@@ -758,9 +754,7 @@ test("the add server dialog keeps focus above fullscreen settings", async ({ pag
|
||||
await settings.getByRole("button", { name: "Add server" }).click()
|
||||
|
||||
const editor = page.getByRole("dialog", { name: "Add server" })
|
||||
await expect(editor.getByLabel("Pairing link", { exact: true })).toBeFocused()
|
||||
await expect(editor.getByPlaceholder("password")).toHaveCount(0)
|
||||
await editor.getByRole("button", { name: "Use password", exact: true }).click()
|
||||
await expect(editor.getByPlaceholder("http://localhost:4096")).toBeFocused()
|
||||
await expect(editor.getByPlaceholder("username")).toHaveCount(0)
|
||||
const name = editor.getByPlaceholder("Localhost", { exact: true })
|
||||
await name.click()
|
||||
@@ -772,80 +766,6 @@ test("the add server dialog keeps focus above fullscreen settings", async ({ pag
|
||||
await expect(settings).toBeVisible()
|
||||
})
|
||||
|
||||
test("the add server dialog pairs from a one-time link and explains a spent one", async ({ page }) => {
|
||||
const paired = "http://127.0.0.1:4098"
|
||||
await mockRemoteServer(page, { directory: "/remote/settings-demo" })
|
||||
await mockOpenCodeServer(page, {
|
||||
server: paired,
|
||||
directory: "/remote/paired",
|
||||
project: project({ id: "proj_paired", directory: "/remote/paired" }),
|
||||
provider: NO_PROVIDER,
|
||||
sessions: [],
|
||||
pageMessages: () => ({ items: [] }),
|
||||
password: "session-token",
|
||||
pairing: { code: "one-time-code", token: "session-token" },
|
||||
})
|
||||
const { settings } = await open(page)
|
||||
const link = `${paired}/auth/connect/one-time-code`
|
||||
|
||||
const addServer = async () => {
|
||||
await settings.locator('[data-component="settings-nav-group-header"]').filter({ hasText: "Servers" }).hover()
|
||||
await settings.getByRole("button", { name: "Add server" }).click()
|
||||
const editor = page.getByRole("dialog", { name: "Add server" })
|
||||
await editor.getByLabel("Pairing link", { exact: true }).fill(link)
|
||||
await editor.getByRole("button", { name: "Add server", exact: true }).click()
|
||||
|
||||
return editor
|
||||
}
|
||||
|
||||
const added = await addServer()
|
||||
await expect(added).toBeHidden()
|
||||
await expect(settings.getByText(paired, { exact: true })).toBeVisible()
|
||||
await settings.getByRole("button", { name: "Back to settings" }).click()
|
||||
|
||||
const spent = await addServer()
|
||||
await expect(spent.getByRole("alert")).toHaveText(
|
||||
"This pairing link expired or was already used. Run opencode pair to get a new one.",
|
||||
)
|
||||
})
|
||||
|
||||
test("a pairing code with several addresses keeps the first one that works with its token", async ({ page }) => {
|
||||
// Both addresses reach one server. The second spends the code first, so the first answers 401, and the token works on
|
||||
// it as on every address of that server.
|
||||
const first = "http://127.0.0.1:4098"
|
||||
const second = "http://127.0.0.1:4099"
|
||||
await mockRemoteServer(page, { directory: "/remote/settings-demo" })
|
||||
|
||||
for (const [server, pairing] of [
|
||||
[first, { code: "spent-by-the-other-address", token: "session-token" }],
|
||||
[second, { code: "one-time-code", token: "session-token" }],
|
||||
] as const) {
|
||||
await mockOpenCodeServer(page, {
|
||||
server,
|
||||
directory: "/remote/paired",
|
||||
project: project({ id: "proj_paired", directory: "/remote/paired" }),
|
||||
provider: NO_PROVIDER,
|
||||
sessions: [],
|
||||
pageMessages: () => ({ items: [] }),
|
||||
password: "session-token",
|
||||
pairing,
|
||||
})
|
||||
}
|
||||
|
||||
const { settings } = await open(page)
|
||||
await settings.locator('[data-component="settings-nav-group-header"]').filter({ hasText: "Servers" }).hover()
|
||||
await settings.getByRole("button", { name: "Add server" }).click()
|
||||
const editor = page.getByRole("dialog", { name: "Add server" })
|
||||
await editor
|
||||
.getByLabel("Pairing link", { exact: true })
|
||||
.fill(JSON.stringify({ code: "one-time-code", urls: [first, second] }))
|
||||
await editor.getByRole("button", { name: "Add server", exact: true }).click()
|
||||
|
||||
await expect(editor).toBeHidden()
|
||||
await expect(settings.getByText(first, { exact: true })).toBeVisible()
|
||||
await expect(settings.getByText(second, { exact: true })).toHaveCount(0)
|
||||
})
|
||||
|
||||
test("the tab layout preference switches to vertical tabs and survives reload", async ({ page }) => {
|
||||
const { settings } = await open(page)
|
||||
const layout = settings.locator('[data-action="settings-tab-layout"]')
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { fixture, installStressSessionTabs, mockStressTimeline } from "../utils/session-fixture"
|
||||
import { sessionHref, draftHref } from "../utils/app"
|
||||
import { openWithDirection } from "../utils/direction"
|
||||
|
||||
for (const direction of ["ltr", "rtl"] as const) {
|
||||
test(`bottom mobile navigation adds no extra top panel gutter in ${direction}`, async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await mockStressTimeline(page)
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem("settings.v3", JSON.stringify({ general: { mobileTitlebarPosition: "bottom" } }))
|
||||
})
|
||||
await openWithDirection(page, "/", direction)
|
||||
const panel = page.locator('[data-slot="home-panel"]')
|
||||
await expect(panel).toHaveCSS("--shell-top-inset", "0px")
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const titlebar = await page.locator('[data-slot="titlebar-v2"]').boundingBox()
|
||||
const main = await page.getByRole("main").boundingBox()
|
||||
|
||||
return titlebar && main && titlebar.y >= main.y + main.height
|
||||
})
|
||||
.toBe(true)
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const bounds = await panel.boundingBox()
|
||||
const main = await page.getByRole("main").boundingBox()
|
||||
|
||||
return bounds && main && bounds.y - main.y
|
||||
})
|
||||
.toBe(0)
|
||||
})
|
||||
|
||||
for (const screen of [
|
||||
{ name: "home", href: () => "/", panel: (page: Page) => page.locator('[data-slot="home-panel"]') },
|
||||
{
|
||||
name: "new-session",
|
||||
href: () => draftHref("shell-spacing-draft"),
|
||||
panel: (page: Page) => page.locator('[data-component="new-session"]'),
|
||||
},
|
||||
{
|
||||
name: "session",
|
||||
href: () => sessionHref(fixture.targetID),
|
||||
panel: (page: Page) => page.locator('[data-slot="session-chat-panel"]'),
|
||||
},
|
||||
{ name: "settings", href: () => "/settings", panel: (page: Page) => page.getByTestId("settings-screen") },
|
||||
]) {
|
||||
test(`${screen.name} keeps an 8px top gutter and only inline border clearance on mobile in ${direction}`, async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await mockStressTimeline(page)
|
||||
|
||||
if (screen.name === "new-session") await installStressSessionTabs(page, { draftID: "shell-spacing-draft" })
|
||||
await openWithDirection(page, screen.href(), direction)
|
||||
await expect(page.locator("html")).toHaveAttribute("dir", direction)
|
||||
|
||||
const panel = screen.panel(page)
|
||||
|
||||
await expect(panel).toBeVisible()
|
||||
await expect(panel).toHaveCSS("--shell-top-inset", "8px")
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const bounds = await panel.boundingBox()
|
||||
const main = await page.getByRole("main").boundingBox()
|
||||
|
||||
return bounds && main && bounds.y - main.y
|
||||
})
|
||||
.toBe(8)
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const bounds = await panel.boundingBox()
|
||||
const main = await page.getByRole("main").boundingBox()
|
||||
|
||||
return bounds && main && { x: bounds.x - main.x, width: main.width - bounds.width }
|
||||
})
|
||||
.toEqual({ x: 1, width: 2 })
|
||||
|
||||
// The existing desktop gutter must return at the responsive breakpoint.
|
||||
await page.setViewportSize({ width: 768, height: 900 })
|
||||
await expect(panel).toHaveCSS("--shell-inline-inset", "8px")
|
||||
await expect(panel).toHaveCSS("--shell-top-inset", "8px")
|
||||
|
||||
if (screen.name === "session") return
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const bounds = await panel.boundingBox()
|
||||
const main = await page.getByRole("main").boundingBox()
|
||||
|
||||
return bounds && main && { x: bounds.x - main.x, width: main.width - bounds.width }
|
||||
})
|
||||
.toEqual({ x: 8, width: 16 })
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"
|
||||
import { createServer, type ServerResponse } from "node:http"
|
||||
import type { AddressInfo } from "node:net"
|
||||
import { once } from "node:events"
|
||||
import { createHash } from "node:crypto"
|
||||
import { join, extname, relative, sep } from "node:path"
|
||||
import { tmpdir } from "node:os"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { build } from "vite"
|
||||
import { onDemandAsset, serviceWorker } from "../../vite.pwa"
|
||||
import { serviceWorker } from "../../vite.pwa"
|
||||
|
||||
type Site = {
|
||||
url: string
|
||||
@@ -128,14 +127,14 @@ const fixture = test.extend<{ site: Site }, { builds: Record<string, Record<stri
|
||||
|
||||
const file = builds[state.version][path]
|
||||
|
||||
const types = new Map([
|
||||
[".js", "text/javascript"],
|
||||
[".html", "text/html"],
|
||||
[".json", "application/json"],
|
||||
[".wasm", "application/wasm"],
|
||||
])
|
||||
const types: Record<string, string> = {
|
||||
".js": "text/javascript",
|
||||
".html": "text/html",
|
||||
".json": "application/json",
|
||||
".wasm": "application/wasm",
|
||||
}
|
||||
|
||||
response.setHeader("content-type", types.get(extname(path)) ?? "application/octet-stream")
|
||||
response.setHeader("content-type", types[extname(path)] ?? "application/octet-stream")
|
||||
|
||||
if (file) return void response.end(file)
|
||||
|
||||
@@ -146,8 +145,9 @@ const fixture = test.extend<{ site: Site }, { builds: Record<string, Record<stri
|
||||
|
||||
server.listen(0, "127.0.0.1")
|
||||
await once(server, "listening")
|
||||
// SAFETY: a server listening on a host and port reports an AddressInfo; only pipe servers report a string.
|
||||
const address = server.address() as AddressInfo
|
||||
const address = server.address()
|
||||
|
||||
if (!address || typeof address === "string") throw new Error("Expected a TCP address")
|
||||
|
||||
try {
|
||||
await use({
|
||||
@@ -284,11 +284,9 @@ fixture(
|
||||
await expect
|
||||
.poll(() =>
|
||||
replacement.evaluate(() => {
|
||||
// This runs in the service worker, whose global scope holds its registration.
|
||||
const registration =
|
||||
"registration" in self && self.registration instanceof ServiceWorkerRegistration ? self.registration : undefined
|
||||
const registration = (self as unknown as { registration: ServiceWorkerRegistration }).registration
|
||||
|
||||
return { waiting: !!registration?.waiting, active: registration?.active?.state }
|
||||
return { waiting: !!registration.waiting, active: registration.active?.state }
|
||||
}),
|
||||
)
|
||||
.toEqual({ waiting: false, active: "activated" })
|
||||
@@ -380,22 +378,15 @@ fixture("does not substitute cached HTML for API or missing asset navigations",
|
||||
expect(await asset?.text()).toBe("Not found")
|
||||
})
|
||||
|
||||
test("the production build precaches every deployable file except on-demand Office assets", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
test("the production build precaches every deployable file", async ({ page, context }) => {
|
||||
const directory = new URL("../../dist/", import.meta.url)
|
||||
|
||||
const deployable = (await readdir(directory, { recursive: true, withFileTypes: true }))
|
||||
const files = (await readdir(directory, { recursive: true, withFileTypes: true }))
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => "/" + relative(fileURLToPath(directory), join(entry.parentPath, entry.name)).split(sep).join("/"))
|
||||
.filter((path) => !path.endsWith(".map") && !["/_headers", "/_redirects", "/sw.js"].includes(path))
|
||||
|
||||
const files = deployable.filter((path) => !onDemandAsset(path))
|
||||
|
||||
expect(files.length).toBeGreaterThan(1)
|
||||
// The Office engines ship but load on demand, so a build without them would make this exclusion vacuous.
|
||||
expect(deployable.some((path) => path.endsWith(".wasm") && onDemandAsset(path))).toBe(true)
|
||||
|
||||
const server = createServer(async (request, response) => {
|
||||
const path = new URL(request.url ?? "/", "http://localhost").pathname
|
||||
@@ -420,8 +411,9 @@ test("the production build precaches every deployable file except on-demand Offi
|
||||
|
||||
server.listen(0, "127.0.0.1")
|
||||
await once(server, "listening")
|
||||
// SAFETY: a server listening on a host and port reports an AddressInfo; only pipe servers report a string.
|
||||
const address = server.address() as AddressInfo
|
||||
const address = server.address()
|
||||
|
||||
if (!address || typeof address === "string") throw new Error("Expected a TCP address")
|
||||
const url = `http://127.0.0.1:${address.port}`
|
||||
|
||||
try {
|
||||
|
||||
@@ -135,8 +135,6 @@ export interface MockServerConfig {
|
||||
strictDirectory?: boolean
|
||||
// Answers 401 UnauthorizedError unless a request carries this password; a function may change it mid-test.
|
||||
password?: Resolvable<string>
|
||||
// Serves GET /auth/connect/:code: `code` redeems once for `token` (send it as the password); others answer 401.
|
||||
pairing?: { code: string; token: string }
|
||||
}
|
||||
|
||||
export type MockPtyInfo = {
|
||||
@@ -403,29 +401,6 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
||||
})
|
||||
})
|
||||
|
||||
if (config.pairing) {
|
||||
const pairing = { ...config.pairing, redeemed: false }
|
||||
|
||||
await page.route(`${server}/auth/connect/*`, (route) => {
|
||||
if (route.request().method() === "OPTIONS") return route.fulfill({ status: 204, headers: corsHeaders })
|
||||
const code = decodeURIComponent(new URL(route.request().url()).pathname.split("/").at(-1) ?? "")
|
||||
|
||||
if (code !== pairing.code || pairing.redeemed) {
|
||||
return route.fulfill({
|
||||
status: 401,
|
||||
headers: corsHeaders,
|
||||
json: Schema.encodeSync(MockUnauthorized)(
|
||||
new MockUnauthorized({ message: "Pairing link expired or already used" }),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
pairing.redeemed = true
|
||||
|
||||
return route.fulfill({ headers: corsHeaders, json: { token: pairing.token } })
|
||||
})
|
||||
}
|
||||
|
||||
if (config.pty) {
|
||||
const host = new URL(server).host
|
||||
await page.routeWebSocket(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/app",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.23",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -56,39 +56,19 @@ export type ComposerSession = {
|
||||
clear: (messageID: string) => void
|
||||
}
|
||||
api: {
|
||||
command: (
|
||||
input: Parameters<ServerSDK["api"]["session"]["command"]>[0],
|
||||
) => ReturnType<ServerSDK["api"]["session"]["command"]>
|
||||
shell: (
|
||||
input: Parameters<ServerSDK["api"]["session"]["shell"]>[0],
|
||||
) => ReturnType<ServerSDK["api"]["session"]["shell"]>
|
||||
switchAgent: (
|
||||
input: Parameters<ServerSDK["api"]["session"]["switchAgent"]>[0],
|
||||
) => ReturnType<ServerSDK["api"]["session"]["switchAgent"]>
|
||||
switchModel: (
|
||||
input: Parameters<ServerSDK["api"]["session"]["switchModel"]>[0],
|
||||
) => ReturnType<ServerSDK["api"]["session"]["switchModel"]>
|
||||
revert: {
|
||||
commit: (
|
||||
input: Parameters<ServerSDK["api"]["session"]["revert"]["commit"]>[0],
|
||||
) => ReturnType<ServerSDK["api"]["session"]["revert"]["commit"]>
|
||||
}
|
||||
command: (input: Parameters<ServerSDK["api"]["session"]["command"]>[0]) => Promise<unknown>
|
||||
shell: (input: Parameters<ServerSDK["api"]["session"]["shell"]>[0]) => Promise<unknown>
|
||||
switchAgent: (input: Parameters<ServerSDK["api"]["session"]["switchAgent"]>[0]) => Promise<unknown>
|
||||
switchModel: (input: Parameters<ServerSDK["api"]["session"]["switchModel"]>[0]) => Promise<unknown>
|
||||
}
|
||||
data: {
|
||||
location: { command: Pick<Data["location"]["command"], "list"> }
|
||||
session: {
|
||||
prompt: (input: Parameters<Data["session"]["prompt"]>[0]) => ReturnType<Data["session"]["prompt"]>
|
||||
prompt: (input: Parameters<Data["session"]["prompt"]>[0]) => Promise<unknown>
|
||||
setStatus: Data["session"]["setStatus"]
|
||||
}
|
||||
}
|
||||
current: Accessor<
|
||||
| {
|
||||
agent?: string
|
||||
model?: { id: string; providerID: string; variant?: string }
|
||||
revert?: { messageID: string }
|
||||
}
|
||||
| undefined
|
||||
>
|
||||
current: Accessor<{ agent?: string; model?: { id: string; providerID: string; variant?: string } } | undefined>
|
||||
admitted: (messageID: string) => boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { Accessor } from "solid-js"
|
||||
import { useServer } from "@/runtime/server/current"
|
||||
import { useServerSDK } from "@/runtime/server/client"
|
||||
import { authTokenFromCredentials } from "@/runtime/server/api"
|
||||
import { uuid } from "@/runtime/persistence/uuid"
|
||||
import { useWorkspaceLocation } from "@/workspaces/location"
|
||||
import type { ComposerControls } from "../adapter"
|
||||
|
||||
@@ -30,7 +29,7 @@ export function useAttachmentDestination(controls: Accessor<ComposerControls>) {
|
||||
// normalizes the separators and returns the resolved path.
|
||||
const url = new URL("/api/experimental/fs/write", server.conn.http.url)
|
||||
url.searchParams.set("location[directory]", location().directory)
|
||||
url.searchParams.set("path", `${info.paths.tmp}/uploads/${uuid()}/${file.name}`)
|
||||
url.searchParams.set("path", `${info.paths.tmp}/uploads/${crypto.randomUUID()}/${file.name}`)
|
||||
|
||||
return write(url, file, server.conn.http.password, report, signal)
|
||||
},
|
||||
@@ -51,7 +50,6 @@ function write(url: URL, file: File, password: string | undefined, report: (load
|
||||
xhr.upload.addEventListener("progress", (event) => report(event.loaded))
|
||||
xhr.addEventListener("load", () => {
|
||||
if (xhr.status !== 200) return reject(new Error(`Upload failed with status ${xhr.status}`))
|
||||
// SAFETY: a 200 from `fs.write` is its declared success body, `Location.response(FileSystem.Write)`.
|
||||
resolve((xhr.response as { data: { path: string } }).data.path)
|
||||
})
|
||||
xhr.addEventListener("error", () => reject(new Error("Upload failed")))
|
||||
|
||||
@@ -1,52 +1,123 @@
|
||||
import type { SessionMessageUser } from "@opencode/client/promise"
|
||||
import { Option, Schema } from "effect"
|
||||
import { Persistence } from "@/runtime/persistence/schema"
|
||||
import { uuid } from "@/runtime/persistence/uuid"
|
||||
import { FileSelection } from "@/workspaces/files/types"
|
||||
import type { FileSelection } from "@/workspaces/files/model"
|
||||
import { durableNote, LegacyBrowserNote, NoteComment, type ContextItem } from "./schema"
|
||||
|
||||
export const PromptFileComment = Persistence.struct({
|
||||
type: Schema.optional(Schema.Literal("file")),
|
||||
path: Schema.String,
|
||||
selection: Persistence.optional(FileSelection),
|
||||
comment: Schema.String,
|
||||
preview: Persistence.optional(Schema.String),
|
||||
origin: Persistence.optional(Schema.Literals(["review", "file"])),
|
||||
})
|
||||
|
||||
export type PromptFileComment = typeof PromptFileComment.Type
|
||||
export type PromptFileComment = {
|
||||
type?: "file"
|
||||
path: string
|
||||
selection?: FileSelection
|
||||
comment: string
|
||||
preview?: string
|
||||
origin?: "review" | "file"
|
||||
}
|
||||
|
||||
export type PromptComment = PromptFileComment | NoteComment
|
||||
|
||||
const decodeNoteComment = Schema.decodeUnknownOption(NoteComment)
|
||||
|
||||
const decodeLegacyBrowserNote = Schema.decodeUnknownOption(LegacyBrowserNote)
|
||||
|
||||
/** An attachment the model receives as a path on the server rather than inline bytes. */
|
||||
export const PromptAttachmentReference = Persistence.struct({
|
||||
name: Schema.String,
|
||||
mime: Schema.String,
|
||||
path: Schema.String,
|
||||
})
|
||||
export type PromptAttachmentReference = {
|
||||
name: string
|
||||
mime: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export type PromptAttachmentReference = typeof PromptAttachmentReference.Type
|
||||
function selection(selection: unknown) {
|
||||
if (!selection || typeof selection !== "object") return undefined
|
||||
const startLine = Number((selection as FileSelection).startLine)
|
||||
const startChar = Number((selection as FileSelection).startChar)
|
||||
const endLine = Number((selection as FileSelection).endLine)
|
||||
const endChar = Number((selection as FileSelection).endChar)
|
||||
|
||||
// Presentation metadata needs a comments list, which prompts from other clients lack; a malformed comment or
|
||||
// attachment is skipped rather than discarding the rest.
|
||||
const decodePromptPresentation = Schema.decodeUnknownOption(
|
||||
Schema.Struct({
|
||||
displayText: Schema.String,
|
||||
comments: Schema.Array(Schema.Unknown),
|
||||
attachments: Persistence.array(PromptAttachmentReference),
|
||||
}),
|
||||
)
|
||||
if (![startLine, startChar, endLine, endChar].every(Number.isFinite)) return undefined
|
||||
|
||||
const decodePromptComment = Schema.decodeUnknownOption(Schema.Union([NoteComment, LegacyBrowserNote, PromptFileComment]))
|
||||
return {
|
||||
startLine,
|
||||
startChar,
|
||||
endLine,
|
||||
endChar,
|
||||
} satisfies FileSelection
|
||||
}
|
||||
|
||||
export function readPromptPresentation(metadata: SessionMessageUser["metadata"]) {
|
||||
return Option.getOrUndefined(
|
||||
Option.map(decodePromptPresentation(metadata), (presentation) => ({
|
||||
displayText: presentation.displayText,
|
||||
attachments: presentation.attachments,
|
||||
comments: presentation.comments.flatMap((item): PromptComment[] => Option.toArray(decodePromptComment(item))),
|
||||
})),
|
||||
)
|
||||
export function createCommentMetadata(input: PromptFileComment) {
|
||||
return {
|
||||
opencodeComment: {
|
||||
path: input.path,
|
||||
selection: input.selection,
|
||||
comment: input.comment,
|
||||
preview: input.preview,
|
||||
origin: input.origin,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function readCommentMetadata(value: unknown) {
|
||||
if (!value || typeof value !== "object") return
|
||||
const meta = (value as { opencodeComment?: unknown }).opencodeComment
|
||||
|
||||
if (!meta || typeof meta !== "object") return
|
||||
const path = (meta as { path?: unknown }).path
|
||||
const comment = (meta as { comment?: unknown }).comment
|
||||
|
||||
if (typeof path !== "string" || typeof comment !== "string") return
|
||||
const preview = (meta as { preview?: unknown }).preview
|
||||
const origin = (meta as { origin?: unknown }).origin
|
||||
|
||||
return {
|
||||
path,
|
||||
selection: selection((meta as { selection?: unknown }).selection),
|
||||
comment,
|
||||
preview: typeof preview === "string" ? preview : undefined,
|
||||
origin: origin === "review" || origin === "file" ? origin : undefined,
|
||||
} satisfies PromptComment
|
||||
}
|
||||
|
||||
export function readPromptPresentation(value: unknown) {
|
||||
if (!value || typeof value !== "object") return
|
||||
const displayText = (value as { displayText?: unknown }).displayText
|
||||
const comments = (value as { comments?: unknown }).comments
|
||||
|
||||
if (typeof displayText !== "string" || !Array.isArray(comments)) return
|
||||
const attachments = (value as { attachments?: unknown }).attachments
|
||||
|
||||
return {
|
||||
displayText,
|
||||
attachments: (Array.isArray(attachments) ? attachments : []).flatMap((item): PromptAttachmentReference[] => {
|
||||
if (!item || typeof item !== "object") return []
|
||||
const name = (item as { name?: unknown }).name
|
||||
const mime = (item as { mime?: unknown }).mime
|
||||
const path = (item as { path?: unknown }).path
|
||||
|
||||
if (typeof name !== "string" || typeof mime !== "string" || typeof path !== "string") return []
|
||||
|
||||
return [{ name, mime, path }]
|
||||
}),
|
||||
comments: comments.flatMap((item): PromptComment[] => {
|
||||
if (!item || typeof item !== "object") return []
|
||||
|
||||
if ((item as { type?: unknown }).type === "note") return Option.toArray(decodeNoteComment(item))
|
||||
|
||||
if ((item as { type?: unknown }).type === "browser") return Option.toArray(decodeLegacyBrowserNote(item))
|
||||
const path = (item as { path?: unknown }).path
|
||||
const comment = (item as { comment?: unknown }).comment
|
||||
|
||||
if (typeof path !== "string" || typeof comment !== "string") return []
|
||||
const preview = (item as { preview?: unknown }).preview
|
||||
const origin = (item as { origin?: unknown }).origin
|
||||
|
||||
return [
|
||||
{
|
||||
path,
|
||||
comment,
|
||||
selection: selection((item as { selection?: unknown }).selection),
|
||||
preview: typeof preview === "string" ? preview : undefined,
|
||||
origin: origin === "review" || origin === "file" ? origin : undefined,
|
||||
},
|
||||
]
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export function formatAttachmentReference(input: PromptAttachmentReference) {
|
||||
@@ -61,7 +132,7 @@ export function formatNoteComment(input: NoteComment) {
|
||||
/** Restores a sent comment to the composer, for example after a revert or fork. */
|
||||
export function commentContextItem(comment: PromptComment): ContextItem {
|
||||
// The message may predate this app process, so a note's live references can no longer be trusted.
|
||||
if (comment.type === "note") return { ...durableNote(comment), commentID: uuid() }
|
||||
if (comment.type === "note") return { ...durableNote(comment), commentID: crypto.randomUUID() }
|
||||
|
||||
return {
|
||||
type: "file",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Show, createMemo } from "solid-js"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { Button } from "@opencode/ui/button"
|
||||
import { useDialog } from "@opencode/ui/context/dialog"
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
@@ -67,7 +66,6 @@ function ComposerModelControl(props: {
|
||||
onUnpaidClick: () => void
|
||||
}) {
|
||||
const shouldAnimate = createMemo<boolean>((previous) => previous ?? props.loading)
|
||||
const mobile = createMediaQuery("(max-width: 767px)")
|
||||
|
||||
const content = () => (
|
||||
<>
|
||||
@@ -89,7 +87,6 @@ function ComposerModelControl(props: {
|
||||
return (
|
||||
<Show when={!props.loading}>
|
||||
<Tooltip
|
||||
inactive={mobile()}
|
||||
placement="top"
|
||||
gutter={4}
|
||||
value={
|
||||
@@ -100,7 +97,7 @@ function ComposerModelControl(props: {
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={props.paid || mobile()}
|
||||
when={props.paid}
|
||||
fallback={
|
||||
<Button
|
||||
data-action="composer-model"
|
||||
@@ -118,7 +115,6 @@ function ComposerModelControl(props: {
|
||||
>
|
||||
<ModelSelectorPopover
|
||||
model={props.model}
|
||||
unpaid={!props.paid}
|
||||
trigger={(triggerProps) => (
|
||||
<Button
|
||||
{...triggerProps}
|
||||
@@ -128,7 +124,7 @@ function ComposerModelControl(props: {
|
||||
class="min-w-0 max-w-[220px] justify-start ![font-weight:440] group"
|
||||
classList={{ "animate-in fade-in": shouldAnimate() }}
|
||||
data-action="composer-model"
|
||||
data-control-type={mobile() ? "drawer" : "popover"}
|
||||
data-control-type="popover"
|
||||
>
|
||||
{content()}
|
||||
</Button>
|
||||
|
||||
@@ -25,11 +25,9 @@
|
||||
);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-component="composer"]:not(:hover) .composer-variant-default:not(:focus-visible, [data-expanded]) {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
[data-component="composer"]:not(:hover) .composer-variant-default:not(:focus-visible, [data-expanded]) {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-color-scheme="dark"] [data-component="composer"][data-dock-border-underlay="true"],
|
||||
|
||||
@@ -8,12 +8,10 @@ import {
|
||||
onMount,
|
||||
Show,
|
||||
Suspense,
|
||||
lazy,
|
||||
type JSX,
|
||||
} from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { FileIcon } from "@opencode/ui/file-icon"
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
import { IconButton } from "@opencode/ui/icon-button"
|
||||
@@ -48,12 +46,6 @@ import { isAttachment } from "../prompt-parts"
|
||||
import "../attachments/attachments.css"
|
||||
import "./editor.css"
|
||||
|
||||
const MobilePanelDrawer = lazy(async () => {
|
||||
const { MobilePanelDrawer } = await import("@/shell/mobile-panel-drawer")
|
||||
|
||||
return { default: MobilePanelDrawer }
|
||||
})
|
||||
|
||||
export type {
|
||||
ComposerAttachment,
|
||||
ComposerComment,
|
||||
@@ -358,7 +350,6 @@ export function ComposerEditor(props: ComposerEditorProps) {
|
||||
{(control) => (
|
||||
<Show when={control.options().length > 1}>
|
||||
<ComposerEditorConfiguredSelect
|
||||
mobileDrawer
|
||||
title={i18n.t("ui.promptInput.chooseVariant")}
|
||||
keybind={["Shift", "Mod", "D"]}
|
||||
control={control}
|
||||
@@ -813,7 +804,6 @@ export function ComposerEditorAddMenu(props: {
|
||||
}
|
||||
|
||||
function ComposerEditorConfiguredSelect(props: {
|
||||
mobileDrawer?: boolean
|
||||
title: string
|
||||
keybind?: string[]
|
||||
control: ComposerSelectControl
|
||||
@@ -825,7 +815,6 @@ function ComposerEditorConfiguredSelect(props: {
|
||||
|
||||
return (
|
||||
<ComposerEditorSelect
|
||||
mobileDrawer={props.mobileDrawer}
|
||||
title={props.title}
|
||||
class={props.class}
|
||||
keybind={props.control.keybind?.() ?? props.keybind}
|
||||
@@ -842,7 +831,6 @@ function ComposerEditorConfiguredSelect(props: {
|
||||
}
|
||||
|
||||
export function ComposerEditorSelect(props: {
|
||||
mobileDrawer?: boolean
|
||||
title: string
|
||||
keybind?: string[]
|
||||
options: ComposerOption[]
|
||||
@@ -852,36 +840,8 @@ export function ComposerEditorSelect(props: {
|
||||
onOpenChange?: (open: boolean) => void
|
||||
onSelect: (id: string) => void
|
||||
}) {
|
||||
const mobile = createMediaQuery("(max-width: 767px)")
|
||||
const [store, setStore] = createStore({ open: false, loaded: false })
|
||||
let trigger: HTMLButtonElement | undefined
|
||||
|
||||
const setOpen = (open: boolean) => {
|
||||
setStore("open", open)
|
||||
|
||||
if (open) setStore("loaded", true)
|
||||
props.onOpenChange?.(open)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!mobile() && store.open) setOpen(false)
|
||||
})
|
||||
|
||||
const content = () => (
|
||||
<>
|
||||
{props.currentIcon}
|
||||
<span class="truncate capitalize leading-5">
|
||||
{props.options.find((option) => option.id === props.current)?.label ?? props.current}
|
||||
</span>
|
||||
<span class="-ms-0.5 -me-1 flex shrink-0">
|
||||
<Icon name="chevron-down" />
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
inactive={props.mobileDrawer && mobile()}
|
||||
placement="top"
|
||||
value={
|
||||
<>
|
||||
@@ -890,83 +850,36 @@ export function ComposerEditorSelect(props: {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={props.mobileDrawer && mobile()}
|
||||
fallback={
|
||||
<Menu gutter={6} modal={false} placement="top-start" onOpenChange={props.onOpenChange}>
|
||||
<Menu.Trigger
|
||||
as={Button}
|
||||
variant="ghost-muted"
|
||||
size="normal"
|
||||
class={`max-w-[220px] justify-start ![font-weight:440] ${props.class ?? ""}`}
|
||||
aria-label={props.title}
|
||||
>
|
||||
{content()}
|
||||
</Menu.Trigger>
|
||||
<Menu.Portal>
|
||||
<Menu.Content>
|
||||
<Menu.RadioGroup value={props.current} onChange={props.onSelect}>
|
||||
<For each={props.options}>
|
||||
{(option) => (
|
||||
<Menu.RadioItem value={option.id} class="capitalize" closeOnSelect>
|
||||
{option.label}
|
||||
</Menu.RadioItem>
|
||||
)}
|
||||
</For>
|
||||
</Menu.RadioGroup>
|
||||
</Menu.Content>
|
||||
</Menu.Portal>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
ref={(element: HTMLButtonElement) => {
|
||||
trigger = element
|
||||
}}
|
||||
<Menu gutter={6} modal={false} placement="top-start" onOpenChange={props.onOpenChange}>
|
||||
<Menu.Trigger
|
||||
as={Button}
|
||||
variant="ghost-muted"
|
||||
size="normal"
|
||||
class={`max-w-[220px] justify-start ![font-weight:440] ${props.class ?? ""}`}
|
||||
aria-label={props.title}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={store.open}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{content()}
|
||||
</Button>
|
||||
<Show when={store.loaded}>
|
||||
<Suspense>
|
||||
<MobilePanelDrawer
|
||||
hideHeader
|
||||
title={props.title}
|
||||
open={store.open}
|
||||
onOpenChange={setOpen}
|
||||
returnFocus={() => trigger}
|
||||
>
|
||||
<div class="flex flex-col overflow-hidden rounded-lg border border-v2-border-border-base bg-v2-background-bg-layer-02 divide-y divide-v2-border-border-base">
|
||||
<For each={props.options}>
|
||||
{(option) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full !h-10 !justify-start !rounded-none !px-3 !font-[440] capitalize focus-visible:!outline-offset-[-2px]"
|
||||
aria-pressed={props.current === option.id}
|
||||
data-state={props.current === option.id ? "pressed" : undefined}
|
||||
onClick={() => {
|
||||
props.onSelect(option.id)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
<Show when={props.current === option.id}>
|
||||
<Icon name="check" class="ms-auto" />
|
||||
</Show>
|
||||
</Button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</MobilePanelDrawer>
|
||||
</Suspense>
|
||||
</Show>
|
||||
</Show>
|
||||
{props.currentIcon}
|
||||
<span class="truncate capitalize leading-5">
|
||||
{props.options.find((option) => option.id === props.current)?.label ?? props.current}
|
||||
</span>
|
||||
<span class="-ms-0.5 -me-1 flex shrink-0">
|
||||
<Icon name="chevron-down" />
|
||||
</span>
|
||||
</Menu.Trigger>
|
||||
<Menu.Portal>
|
||||
<Menu.Content>
|
||||
<Menu.RadioGroup value={props.current} onChange={props.onSelect}>
|
||||
<For each={props.options}>
|
||||
{(option) => (
|
||||
<Menu.RadioItem value={option.id} class="capitalize" closeOnSelect>
|
||||
{option.label}
|
||||
</Menu.RadioItem>
|
||||
)}
|
||||
</For>
|
||||
</Menu.RadioGroup>
|
||||
</Menu.Content>
|
||||
</Menu.Portal>
|
||||
</Menu>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -130,11 +130,6 @@ function session(input: {
|
||||
input.calls.push("shell")
|
||||
}),
|
||||
command: input.command ?? (async () => undefined),
|
||||
revert: {
|
||||
commit: async () => {
|
||||
input.calls.push("revert-commit")
|
||||
},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
location: { command: { list: () => [] } },
|
||||
@@ -143,16 +138,6 @@ function session(input: {
|
||||
prompt: async (value) => {
|
||||
input.calls.push("prompt")
|
||||
await input.prompt?.(value)
|
||||
|
||||
// The admitted inbox item, as the server returns it.
|
||||
return {
|
||||
id: value.id ?? "msg_admitted",
|
||||
sessionID: value.sessionID,
|
||||
time: { created: 0 },
|
||||
type: "user" as const,
|
||||
payload: { text: value.text },
|
||||
delivery: value.delivery ?? "steer",
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -198,11 +183,6 @@ describe("Composer submission", () => {
|
||||
current: { agent: "build", model: { providerID: "provider-1", id: "model-1", variant: "balanced" } },
|
||||
calls: ["switch-model"],
|
||||
},
|
||||
// A prompt commits a staged revert, which would delete the model switch made after its boundary.
|
||||
{
|
||||
current: { agent: "plan", model: { id: "old", providerID: "old" }, revert: { messageID: "msg_reverted" } },
|
||||
calls: ["switch-agent", "revert-commit", "switch-model"],
|
||||
},
|
||||
])("applies the selection before sending one captured value: $calls", async (row) => {
|
||||
const state = createMemoryComposerState({ prompt: "ship it" }).capture()
|
||||
state.context.add({ type: "file", path: "notes.md", description: "the failing version" })
|
||||
|
||||
@@ -332,8 +332,6 @@ function clearSubmission(input: ComposerSubmitInput, submission: ReturnType<type
|
||||
submission.target().mode.set("normal")
|
||||
input.setMode("normal")
|
||||
input.closePopover()
|
||||
|
||||
if (window.matchMedia("(max-width: 767px)").matches) input.editor()?.blur()
|
||||
}
|
||||
|
||||
function restoreSubmission(
|
||||
@@ -457,7 +455,6 @@ async function applySelection(
|
||||
session: ComposerSession,
|
||||
selection: ComposerSelection,
|
||||
track?: ModelSelection["trackSessionCommit"],
|
||||
beforeModel?: () => Promise<void>,
|
||||
) {
|
||||
const cancel = track?.(session.id, selection)
|
||||
|
||||
@@ -468,8 +465,6 @@ async function applySelection(
|
||||
await session.api.switchAgent({ sessionID: session.id, agent: selection.agent })
|
||||
}
|
||||
|
||||
await beforeModel?.()
|
||||
|
||||
// The server deduplicates unchanged selections; cached SSE state may still be behind an earlier switch.
|
||||
await session.api.switchModel({
|
||||
sessionID: session.id,
|
||||
@@ -494,15 +489,10 @@ async function sendPrompt(
|
||||
// selection applies now; a queued follow-up must not reconfigure the turn it
|
||||
// waits behind, so it runs with the session selection at delivery time (the
|
||||
// intended selection stays recorded in its metadata).
|
||||
// Like the TUI, a staged revert settles after the agent switch and before the model switch and admission. The
|
||||
// server would otherwise commit it on admission and delete every row from its boundary on, the model switch too.
|
||||
const settle = async () => {
|
||||
if (session.current()?.revert) await session.api.revert.commit({ sessionID: session.id })
|
||||
if (value.delivery === "steer") {
|
||||
await applySelection(session, value.selection, track)
|
||||
}
|
||||
|
||||
if (value.delivery === "steer") await applySelection(session, value.selection, track, settle)
|
||||
else await settle()
|
||||
|
||||
const admission = {
|
||||
id: value.id,
|
||||
sessionID: session.id,
|
||||
|
||||
@@ -31,6 +31,7 @@ export function createHomeProjectsController(home: HomeController) {
|
||||
const settings = useSettingsSurface()
|
||||
const serverManagement = useServerActionsController()
|
||||
const global = useGlobal()
|
||||
const authenticate = ServerConnection.authenticate
|
||||
const revealProject = useRevealProject()
|
||||
const [_state, setState, _, ready] = persisted(Persist.global("home.servers"), HomeServersSchema, { collapsed: {} })
|
||||
|
||||
@@ -40,23 +41,6 @@ export function createHomeProjectsController(home: HomeController) {
|
||||
{ initialValue: _state },
|
||||
)
|
||||
|
||||
function edit(conn: ServerConnection.Http, onSave?: (saved: ServerConnection.Http) => void) {
|
||||
void import("@/servers/connect/dialog").then(({ DialogServer }) => {
|
||||
void dialog.show(() => <DialogServer mode="edit" server={conn} onSave={(saved) => onSave?.(saved)} />)
|
||||
})
|
||||
}
|
||||
|
||||
// An expired pairing session or a changed password signs the app out of an HTTP server; signing in again edits it,
|
||||
// and the action that asked for sign-in continues once the dialog saves, as it does for extension servers. It continues
|
||||
// with the saved connection: the one it started with still carries the rejected credentials.
|
||||
function authenticate(conn: ServerConnection.Any, onConnected?: (conn: ServerConnection.Any) => void) {
|
||||
if (conn.type !== "http" || !home.server.health(conn)?.unauthorized)
|
||||
return ServerConnection.authenticate(conn, () => onConnected?.(conn))
|
||||
edit(conn, onConnected)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function directories(project: LocalProject) {
|
||||
return [project.worktree, ...(project.sandboxes ?? [])]
|
||||
}
|
||||
@@ -94,10 +78,14 @@ export function createHomeProjectsController(home: HomeController) {
|
||||
remove: (conn: ServerConnection.Any) => serverManagement.connection.remove(ServerConnection.key(conn)),
|
||||
canHide: (conn: ServerConnection.Any) => serverManagement.connection.canHide(ServerConnection.key(conn)),
|
||||
hide: (conn: ServerConnection.Any) => serverManagement.connection.setHidden(ServerConnection.key(conn), true),
|
||||
edit: (conn: ServerConnection.Http) => edit(conn),
|
||||
edit: (conn: ServerConnection.Http) => {
|
||||
void import("@/servers/connect/dialog").then(({ DialogServer }) => {
|
||||
void dialog.show(() => <DialogServer mode="edit" server={conn} />)
|
||||
})
|
||||
},
|
||||
authenticate: (conn: ServerConnection.Any) => authenticate(conn),
|
||||
focus: (conn: ServerConnection.Any) => {
|
||||
if (authenticate(conn, (next) => home.selection.focusServer(next))) return
|
||||
if (authenticate(conn, () => home.selection.focusServer(conn))) return
|
||||
home.selection.focusServer(conn)
|
||||
},
|
||||
},
|
||||
@@ -106,12 +94,12 @@ export function createHomeProjectsController(home: HomeController) {
|
||||
recentlyClosed: home.project.recentlyClosed,
|
||||
homedir: home.project.homedir,
|
||||
select: (conn: ServerConnection.Any, directory: string) => {
|
||||
if (authenticate(conn, (next) => home.project.select(next, directory))) return
|
||||
if (authenticate(conn, () => home.project.select(conn, directory))) return
|
||||
home.project.select(conn, directory)
|
||||
},
|
||||
add: home.project.add,
|
||||
openNewSession: (conn: ServerConnection.Any, directory: string) => {
|
||||
if (authenticate(conn, (next) => home.project.openProjectNewSession(next, directory))) return
|
||||
if (authenticate(conn, () => home.project.openProjectNewSession(conn, directory))) return
|
||||
home.project.openProjectNewSession(conn, directory)
|
||||
},
|
||||
canImportSession: !!platform.openAttachmentPickerDialog,
|
||||
@@ -131,7 +119,6 @@ export function createHomeProjectsController(home: HomeController) {
|
||||
|
||||
const api = home.server.context(conn).sdk.api.session
|
||||
|
||||
// SAFETY: the payload is SessionTransfer.Data's own encoding, which the import endpoint decodes and validates.
|
||||
const imported = await api.import({
|
||||
...Schema.encodeSync(SessionTransfer.Data)(data),
|
||||
location: { directory: project.worktree },
|
||||
@@ -165,7 +152,7 @@ export function createHomeProjectsController(home: HomeController) {
|
||||
.forEach((directory) => notification.project.markViewed(directory))
|
||||
},
|
||||
choose: (conn: ServerConnection.Any) => {
|
||||
if (authenticate(conn, (next) => choose(next))) return
|
||||
if (authenticate(conn, () => choose(conn))) return
|
||||
|
||||
if (home.server.health(conn)?.healthy === false) return
|
||||
choose(conn)
|
||||
|
||||
@@ -33,11 +33,6 @@ const HOME_PROJECT_NAV_LABEL = "min-w-0 flex-1 overflow-hidden text-ellipsis whi
|
||||
|
||||
const serverContextMenuID = (server: ServerConnection.Any) => `server:${ServerConnection.key(server)}`
|
||||
|
||||
// Extension servers ask for sign-in themselves; an HTTP server that rejects its saved credentials needs a new
|
||||
// pairing link or password.
|
||||
const signInRequired = (server: ServerConnection.Any, health: ServerHealth | undefined) =>
|
||||
(server.type === "extension" && server.authenticationRequired) || (server.type === "http" && !!health?.unauthorized)
|
||||
|
||||
const projectContextMenuID = (server: ServerConnection.Any, directory: string) =>
|
||||
`project:${ServerConnection.key(server)}:${directory}`
|
||||
|
||||
@@ -163,7 +158,7 @@ export function HomeProjectsView(props: HomeProjectsViewProps) {
|
||||
}
|
||||
|
||||
function HomeProjectsPanel(props: HomeProjectsViewProps) {
|
||||
const [contextMenu, setContextMenu] = createStore<{ open: string | undefined }>({ open: undefined })
|
||||
const [contextMenu, setContextMenu] = createStore({ open: undefined as string | undefined })
|
||||
|
||||
const contextMenuProps = {
|
||||
contextMenuOpen: (id: string) => contextMenu.open === id,
|
||||
@@ -221,9 +216,7 @@ function HomeProjectsPanel(props: HomeProjectsViewProps) {
|
||||
when={
|
||||
props.servers.length > 1 ||
|
||||
props.servers.some(
|
||||
(server) =>
|
||||
signInRequired(server, props.serverHealth(server)) ||
|
||||
(server.type === "extension" && server.connecting),
|
||||
(server) => server.type === "extension" && (server.authenticationRequired || server.connecting),
|
||||
)
|
||||
}
|
||||
fallback={
|
||||
@@ -260,7 +253,7 @@ function HomeProjectsPanel(props: HomeProjectsViewProps) {
|
||||
const healthy = () => !!props.serverHealth(item)?.healthy
|
||||
const hasProjects = () => projects().length > 0
|
||||
const collapsed = () => props.collapsed(item)
|
||||
const authentication = () => signInRequired(item, props.serverHealth(item))
|
||||
const authentication = () => item.type === "extension" && item.authenticationRequired
|
||||
const connecting = () => item.type === "extension" && item.connecting
|
||||
|
||||
return (
|
||||
@@ -365,7 +358,7 @@ function HomeServerRow(props: {
|
||||
health: ServerHealth | undefined
|
||||
}) {
|
||||
const healthy = () => !!props.health?.healthy
|
||||
const authentication = () => signInRequired(props.server, props.health)
|
||||
const authentication = () => props.server.type === "extension" && props.server.authenticationRequired
|
||||
const incompatible = () => !!props.health?.incompatible
|
||||
const canToggle = () => healthy() && props.projectsForServer(props.server).length > 0
|
||||
const contextMenuID = () => serverContextMenuID(props.server)
|
||||
|
||||
@@ -20,9 +20,8 @@ export function Home() {
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="home-panel"
|
||||
class={`
|
||||
mx-[var(--shell-inline-inset,8px)] mb-[var(--shell-bottom-inset,8px)] mt-[var(--shell-top-inset,8px)] flex min-h-0 flex-1 flex-col self-stretch overflow-hidden rounded-[10px]
|
||||
mx-2 mb-[var(--shell-bottom-inset,8px)] mt-[var(--shell-top-inset,8px)] flex min-h-0 flex-1 flex-col self-stretch overflow-hidden rounded-[10px]
|
||||
bg-v2-background-bg-base shadow-[var(--v2-elevation-raised)]
|
||||
`}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Predicate } from "effect"
|
||||
import { base64Encode } from "@opencode/util/encode"
|
||||
import type { SessionMessageUser } from "@opencode/client/promise"
|
||||
import { Session } from "@opencode/schema/session"
|
||||
@@ -48,10 +47,7 @@ export function createNewSessionComposerAdapter(props: {
|
||||
async start(selection, submission, message) {
|
||||
const draftID = props.draftID
|
||||
const currentDirectory = location().directory
|
||||
|
||||
const projectDirectory =
|
||||
data.location.info({ directory: currentDirectory })?.project.canonical ?? currentDirectory
|
||||
|
||||
const projectDirectory = data.location.info({ directory: currentDirectory })?.project.canonical ?? currentDirectory
|
||||
const worktree = props.worktree()
|
||||
const branch = props.branch()
|
||||
const mcp = props.mcp.capture()
|
||||
@@ -167,7 +163,6 @@ export function createNewSessionComposerAdapter(props: {
|
||||
shell: (input) => afterCreation(() => serverSDK.api.session.shell(input)),
|
||||
switchAgent: (input) => afterCreation(() => serverSDK.api.session.switchAgent(input)),
|
||||
switchModel: (input) => afterCreation(() => serverSDK.api.session.switchModel(input)),
|
||||
revert: { commit: (input) => afterCreation(() => serverSDK.api.session.revert.commit(input)) },
|
||||
},
|
||||
data: {
|
||||
location: data.location,
|
||||
@@ -244,16 +239,16 @@ async function resolveSessionDirectory(input: {
|
||||
})
|
||||
}
|
||||
|
||||
function errorMessage(language: ReturnType<typeof useLanguage>, cause: unknown) {
|
||||
if (Predicate.hasProperty(cause, "message") && Predicate.isString(cause.message)) return cause.message
|
||||
function errorMessage(language: ReturnType<typeof useLanguage>, error: unknown) {
|
||||
if (error && typeof error === "object" && "message" in error && typeof error.message === "string") {
|
||||
return error.message
|
||||
}
|
||||
|
||||
if (
|
||||
Predicate.hasProperty(cause, "data") &&
|
||||
Predicate.hasProperty(cause.data, "message") &&
|
||||
Predicate.isString(cause.data.message) &&
|
||||
cause.data.message
|
||||
)
|
||||
return cause.data.message
|
||||
if (error && typeof error === "object" && "data" in error) {
|
||||
const data = (error as { data?: { message?: string } }).data
|
||||
|
||||
if (data?.message) return data.message
|
||||
}
|
||||
|
||||
return language.t("common.requestFailed")
|
||||
}
|
||||
@@ -341,7 +341,7 @@ export function PromptProjectSelector(props: {
|
||||
onCloseAutoFocus={dismiss.onCloseAutoFocus}
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex h-7 items-center gap-2 rounded-sm pl-3 pr-1 text-v2-icon-icon-muted">
|
||||
<div class="flex h-7 items-center gap-2 rounded-sm pl-3 pr-2.5 text-v2-icon-icon-muted">
|
||||
<Icon name="magnifying-glass" size="small" class="shrink-0" />
|
||||
<input
|
||||
ref={(el) => props.controller.setSearchRef(el)}
|
||||
@@ -402,16 +402,16 @@ export function PromptProjectSelector(props: {
|
||||
<Show when={props.controller.search().trim()}>
|
||||
<button
|
||||
type="button"
|
||||
class="flex size-5 items-center justify-center rounded-sm bg-transparent text-v2-icon-icon-faint transition-colors hover:text-v2-icon-icon-base focus-visible:text-v2-icon-icon-base active:text-v2-icon-icon-base"
|
||||
class="flex size-5 items-center justify-center rounded-sm text-v2-icon-icon-muted hover:bg-v2-overlay-simple-overlay-hover"
|
||||
onPointerDown={(event) => event.preventDefault()}
|
||||
onClick={() => props.controller.clearSearch()}
|
||||
aria-label={props.controller.labels.clear()}
|
||||
>
|
||||
<Icon name="circle-xmark" />
|
||||
<Icon name="close-small" size="small" />
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="my-0.5 max-h-[224px] overflow-y-auto">
|
||||
<div class="max-h-[224px] overflow-y-auto">
|
||||
<Show
|
||||
when={props.controller.servers().length > 1}
|
||||
fallback={
|
||||
@@ -451,8 +451,8 @@ export function PromptProjectSelector(props: {
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class="-mx-0.5 h-px bg-v2-border-border-muted" />
|
||||
<div class="flex flex-col pt-0.5">
|
||||
<div class="h-px bg-v2-border-border-muted" />
|
||||
<div class="flex flex-col">
|
||||
<Show
|
||||
when={props.controller.servers().length > 1}
|
||||
fallback={
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function NewSessionPage(props: { draftId: string }) {
|
||||
return (
|
||||
<div class="relative size-full overflow-hidden flex flex-col">
|
||||
{suspendUntilPromptReady()}
|
||||
<div class="flex-1 min-h-0 flex flex-col gap-2 px-[var(--shell-inline-inset,8px)] pb-[var(--shell-bottom-inset,8px)] pt-[var(--shell-top-inset,8px)]">
|
||||
<div class="flex-1 min-h-0 flex flex-col gap-2 px-2 pb-[var(--shell-bottom-inset,8px)] pt-[var(--shell-top-inset,8px)]">
|
||||
<NewSessionView composer={model} project={project} workspace={workspace} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,23 +7,14 @@ import type {
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import { useServerSDK } from "@/runtime/server/client"
|
||||
import { formatServerError } from "@/runtime/server/errors"
|
||||
import { useData } from "@/runtime/server/current"
|
||||
import { createEffect, createMemo, on, onCleanup } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
|
||||
export type ProviderConnectMethod = Extract<IntegrationMethod, { type: "key" | "oauth" | "external" }>
|
||||
export type ProviderConnectMethod = Extract<IntegrationMethod, { type: "key" | "oauth" }>
|
||||
|
||||
type Authorization = IntegrationOauthConnectOutput["data"]
|
||||
|
||||
type Polling = {
|
||||
generation: number
|
||||
timer?: ReturnType<typeof setTimeout>
|
||||
disposed: boolean
|
||||
// An attempt the server still considers open; cancelled when the dialog goes away.
|
||||
attempt?: Authorization
|
||||
}
|
||||
|
||||
// OpenCode Go and OpenCode Zen both bill through the OpenCode Console, so the
|
||||
// Console sign-in is the connection method for both providers.
|
||||
export const CONSOLE_INTEGRATION = "opencode"
|
||||
@@ -44,7 +35,7 @@ export function providerFormDefaults(fields: ProviderConnectMethod["form"]) {
|
||||
if (actual === undefined) return false
|
||||
|
||||
const equal = Array.isArray(actual)
|
||||
? actual.some((item) => item === condition.value)
|
||||
? typeof condition.value === "string" && actual.includes(condition.value)
|
||||
: actual === condition.value
|
||||
|
||||
return condition.op === "eq" ? equal : !equal
|
||||
@@ -52,7 +43,7 @@ export function providerFormDefaults(fields: ProviderConnectMethod["form"]) {
|
||||
|
||||
if (!active) return answer
|
||||
|
||||
return Object.assign(answer, { [field.key]: field.default })
|
||||
return { ...answer, [field.key]: field.default }
|
||||
}, {})
|
||||
}
|
||||
|
||||
@@ -83,8 +74,9 @@ export function createProviderConnectionController(options: {
|
||||
|
||||
// Not createResource: the dialog is owned by whichever page opened it, so reading a pending
|
||||
// resource here would suspend that page's <Suspense> and blank the screen behind the dialog.
|
||||
const [integration, setIntegration] = createStore<{ loading: boolean; latest?: IntegrationInfo }>({
|
||||
const [integration, setIntegration] = createStore({
|
||||
loading: true,
|
||||
latest: undefined as IntegrationInfo | undefined,
|
||||
})
|
||||
|
||||
createEffect(
|
||||
@@ -108,8 +100,7 @@ export function createProviderConnectionController(options: {
|
||||
|
||||
const methods = createMemo<ProviderConnectMethod[]>(() => {
|
||||
const values = integration.latest?.methods.filter(
|
||||
(method): method is ProviderConnectMethod =>
|
||||
method.type === "key" || method.type === "oauth" || method.type === "external",
|
||||
(method): method is ProviderConnectMethod => method.type === "key" || method.type === "oauth",
|
||||
)
|
||||
|
||||
if (values?.length) return [...values]
|
||||
@@ -117,19 +108,14 @@ export function createProviderConnectionController(options: {
|
||||
return [{ type: "key", label: language.t("provider.connect.method.apiKey") }]
|
||||
})
|
||||
|
||||
const [store, setStore] = createStore<{
|
||||
methodIndex?: number
|
||||
authorization?: Authorization
|
||||
formAnswer?: FormAnswer
|
||||
const [store, setStore] = createStore({
|
||||
methodIndex: undefined as number | undefined,
|
||||
authorization: undefined as Authorization | undefined,
|
||||
formAnswer: undefined as FormAnswer | undefined,
|
||||
// Nothing is in flight until a method is selected; `busy()` reads this, so a truthy initial
|
||||
// value would keep multi-method providers on the spinner instead of the method list.
|
||||
state?: "pending" | "waiting" | "refreshing" | "ready" | "error" | "form"
|
||||
error?: string
|
||||
auto: boolean
|
||||
connected: boolean
|
||||
browserFailed: boolean
|
||||
statusFailed: boolean
|
||||
}>({
|
||||
state: undefined as "pending" | "waiting" | "refreshing" | "ready" | "error" | "form" | undefined,
|
||||
error: undefined as string | undefined,
|
||||
auto: false,
|
||||
// The credential is stored; a retry only needs to reload the catalogs.
|
||||
connected: false,
|
||||
@@ -138,9 +124,12 @@ export function createProviderConnectionController(options: {
|
||||
statusFailed: false,
|
||||
})
|
||||
|
||||
const polling: Polling = {
|
||||
const polling = {
|
||||
generation: 0,
|
||||
timer: undefined as ReturnType<typeof setTimeout> | undefined,
|
||||
disposed: false,
|
||||
// An attempt the server still considers open; cancelled when the dialog goes away.
|
||||
attempt: undefined as Authorization | undefined,
|
||||
}
|
||||
|
||||
const currentMethod = createMemo(() =>
|
||||
@@ -217,6 +206,8 @@ export function createProviderConnectionController(options: {
|
||||
)
|
||||
}
|
||||
|
||||
const errorMessage = (error: unknown) => (error instanceof Error ? error.message : String(error))
|
||||
|
||||
const cancelAttempt = () => {
|
||||
const attempt = polling.attempt
|
||||
polling.attempt = undefined
|
||||
@@ -285,7 +276,7 @@ export function createProviderConnectionController(options: {
|
||||
setStore("statusFailed", true)
|
||||
dispatch({
|
||||
type: "auth.error",
|
||||
error: isConsole() ? language.t("provider.connect.console.statusFailed") : formatServerError(result.error, language.t),
|
||||
error: isConsole() ? language.t("provider.connect.console.statusFailed") : errorMessage(result.error),
|
||||
})
|
||||
|
||||
return
|
||||
@@ -367,6 +358,8 @@ export function createProviderConnectionController(options: {
|
||||
return
|
||||
}
|
||||
|
||||
if (selected.type !== "oauth") return
|
||||
|
||||
if (selected.form?.some((field) => field.type !== "string")) {
|
||||
dispatch({ type: "auth.error", error: language.t("provider.connect.error.unsupportedFields") })
|
||||
|
||||
@@ -375,35 +368,11 @@ export function createProviderConnectionController(options: {
|
||||
|
||||
dispatch({ type: "auth.pending" })
|
||||
|
||||
if (selected.type === "external") {
|
||||
const saved = await serverSDK.api.integration.connect
|
||||
.external({
|
||||
integrationID: options.provider(),
|
||||
methodID: selected.id,
|
||||
answer: Object.keys(merged).length ? merged : undefined,
|
||||
location: location(),
|
||||
})
|
||||
.then(() => ({ ok: true as const }))
|
||||
.catch((error) => ({ ok: false as const, error }))
|
||||
|
||||
if (polling.disposed || generation !== polling.generation) return
|
||||
|
||||
if (!saved.ok) {
|
||||
dispatch({ type: "auth.error", error: formatServerError(saved.error, language.t) })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await finish()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const result = await serverSDK.api.integration.oauth
|
||||
.connect({
|
||||
integrationID: options.provider(),
|
||||
methodID: selected.id,
|
||||
answer: Object.keys(merged).length ? merged : undefined,
|
||||
...(Object.keys(merged).length ? { answer: merged } : {}),
|
||||
location: location(),
|
||||
})
|
||||
.then((response) => {
|
||||
@@ -435,7 +404,7 @@ export function createProviderConnectionController(options: {
|
||||
if (!result.ok) {
|
||||
dispatch({
|
||||
type: "auth.error",
|
||||
error: isConsole() ? language.t("provider.connect.console.startFailed") : formatServerError(result.error, language.t),
|
||||
error: isConsole() ? language.t("provider.connect.console.startFailed") : errorMessage(result.error),
|
||||
})
|
||||
|
||||
return
|
||||
@@ -479,7 +448,7 @@ export function createProviderConnectionController(options: {
|
||||
integrationID: options.keyProvider?.() ?? options.provider(),
|
||||
location: location(),
|
||||
key,
|
||||
answer: store.formAnswer,
|
||||
...(store.formAnswer ? { answer: store.formAnswer } : {}),
|
||||
})
|
||||
await finish()
|
||||
}
|
||||
@@ -499,8 +468,7 @@ export function createProviderConnectionController(options: {
|
||||
.then(() => ({ ok: true as const }))
|
||||
.catch((error) => ({ ok: false as const, error }))
|
||||
|
||||
if (!result.ok)
|
||||
return formatServerError(result.error, language.t, language.t("provider.connect.oauth.code.invalid"))
|
||||
if (!result.ok) return errorMessage(result.error) || language.t("provider.connect.oauth.code.invalid")
|
||||
await finish()
|
||||
|
||||
return undefined
|
||||
|
||||
@@ -66,9 +66,8 @@ export const OpenCodeZen = {
|
||||
|
||||
export const LoginMethods = {
|
||||
render: renderConnection("openai", [
|
||||
{ type: "oauth", label: "Sign in with ChatGPT" },
|
||||
{ type: "oauth", label: "Codex browser (legacy)" },
|
||||
{ type: "oauth", label: "Codex device code (legacy)" },
|
||||
{ type: "oauth", label: "ChatGPT Pro/Plus (browser)" },
|
||||
{ type: "oauth", label: "ChatGPT Pro/Plus (headless)" },
|
||||
{ type: "key", label: "API key" },
|
||||
]),
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { Badge } from "@opencode/ui/badge"
|
||||
import { useDialog } from "@opencode/ui/context/dialog"
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
import { List } from "@opencode/ui/list"
|
||||
import { RadioGroup, RadioItem } from "@opencode/ui/radio"
|
||||
import { Spinner } from "@opencode/ui/spinner"
|
||||
import { TextField } from "@opencode/ui/text-field"
|
||||
import { DialogBody, DialogHeader, DialogTitle, Dialog } from "@opencode/ui/dialog"
|
||||
@@ -62,7 +61,7 @@ type IntegrationForm = NonNullable<ProviderConnectMethod["form"]>[number]
|
||||
type StringForm = Extract<IntegrationForm, { type: "string" }>
|
||||
|
||||
export function useProviderConnectController() {
|
||||
const [store, setStore] = createStore<{ selected?: string }>({})
|
||||
const [store, setStore] = createStore({ selected: undefined as string | undefined })
|
||||
const reset = () => setStore("selected", undefined)
|
||||
|
||||
return {
|
||||
@@ -84,13 +83,9 @@ export const DialogConnectProvider: Component<{
|
||||
const fallback = useProviderConnectController()
|
||||
const controller = props.controller ?? fallback
|
||||
|
||||
const [state, setState] = createStore<{
|
||||
completed: boolean
|
||||
modelProvider?: { id: string; name: string }
|
||||
authorization: boolean
|
||||
chatgptWelcome: boolean
|
||||
}>({
|
||||
const [state, setState] = createStore({
|
||||
completed: false,
|
||||
modelProvider: undefined as { id: string; name: string } | undefined,
|
||||
authorization: false,
|
||||
chatgptWelcome: false,
|
||||
})
|
||||
@@ -221,12 +216,10 @@ function ProviderPicker(props: { directory?: string; onSelect: (provider: string
|
||||
const integrations = useIntegrations(() => props.directory)
|
||||
const language = useLanguage()
|
||||
|
||||
const [store, setStore] = createStore<{
|
||||
filter: string
|
||||
active?: string
|
||||
connecting?: string
|
||||
}>({
|
||||
const [store, setStore] = createStore({
|
||||
filter: "",
|
||||
active: undefined as string | undefined,
|
||||
connecting: undefined as string | undefined,
|
||||
})
|
||||
|
||||
const featured = ["opencode-go", "opencode", "anthropic", "openai", "google", "openrouter", "vercel"]
|
||||
@@ -428,24 +421,16 @@ function ProviderConnection(props: {
|
||||
const isConsole = CONSOLE_PROVIDERS.has(props.provider)
|
||||
const remote = isConsole && authServerName(sdk.server) !== undefined
|
||||
|
||||
const [state, setState] = createStore<{
|
||||
copied: boolean
|
||||
copyFailed: boolean
|
||||
firstConnection?: boolean
|
||||
models: boolean
|
||||
noModels: boolean
|
||||
catalogPending: boolean
|
||||
selectedModel: string
|
||||
collapsed: Record<string, boolean>
|
||||
}>({
|
||||
const [state, setState] = createStore({
|
||||
copied: false,
|
||||
copyFailed: false,
|
||||
firstConnection: undefined as boolean | undefined,
|
||||
models: false,
|
||||
noModels: false,
|
||||
// The workspace providers had not loaded when the wait ran out.
|
||||
catalogPending: false,
|
||||
selectedModel: "",
|
||||
collapsed: {},
|
||||
collapsed: {} as Record<string, boolean>,
|
||||
})
|
||||
|
||||
const controller = createProviderConnectionController({
|
||||
@@ -657,22 +642,11 @@ function ProviderConnection(props: {
|
||||
function AuthFormView() {
|
||||
const defaults = providerFormDefaults(controller.currentMethod()?.form)
|
||||
|
||||
const [formStore, setFormStore] = createStore<{
|
||||
value: Record<string, string>
|
||||
index: number
|
||||
customOn: boolean
|
||||
customText: string
|
||||
}>({
|
||||
const [formStore, setFormStore] = createStore({
|
||||
value: Object.fromEntries(
|
||||
(controller.currentMethod()?.form ?? []).flatMap((field) =>
|
||||
field.type === "string" && field.key in defaults && field.default !== undefined
|
||||
? [[field.key, field.default]]
|
||||
: [],
|
||||
),
|
||||
),
|
||||
Object.entries(defaults).flatMap(([key, value]) => (typeof value === "string" ? [[key, value]] : [])),
|
||||
) as Record<string, string>,
|
||||
index: 0,
|
||||
customOn: false,
|
||||
customText: "",
|
||||
})
|
||||
|
||||
const fields = createMemo<StringForm[]>(() => {
|
||||
@@ -703,18 +677,14 @@ function ProviderConnection(props: {
|
||||
}
|
||||
})
|
||||
|
||||
// Visible defaults prefill the field but enter the answer only once that field is answered,
|
||||
// because the server rejects values for fields whose conditions are not met.
|
||||
const answer = (field: StringForm) => formStore.value[field.key] ?? field.default
|
||||
|
||||
const valid = createMemo(() => {
|
||||
const item = current()
|
||||
|
||||
if (!item || (item.field.options && !item.field.custom)) return false
|
||||
if (!item || item.field.options) return false
|
||||
|
||||
if (!item.field.required) return true
|
||||
|
||||
return (answer(item.field) ?? "").trim().length > 0
|
||||
return (formStore.value[item.field.key] ?? "").trim().length > 0
|
||||
})
|
||||
|
||||
async function next(index: number, value: Record<string, string>) {
|
||||
@@ -724,7 +694,7 @@ function ProviderConnection(props: {
|
||||
const next = fields().findIndex((field, i) => i > index && matches(field, value))
|
||||
|
||||
if (next !== -1) {
|
||||
setFormStore({ index: next, customOn: false, customText: "" })
|
||||
setFormStore("index", next)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -736,10 +706,9 @@ function ProviderConnection(props: {
|
||||
e.preventDefault()
|
||||
const item = current()
|
||||
|
||||
if (!item || !valid()) return
|
||||
const value = answer(item.field)
|
||||
if (!item || item.field.options) return
|
||||
|
||||
if (value !== undefined) setFormStore("value", item.field.key, value)
|
||||
if (!valid()) return
|
||||
await next(item.index, formStore.value)
|
||||
}
|
||||
|
||||
@@ -761,15 +730,6 @@ function ProviderConnection(props: {
|
||||
return field
|
||||
})
|
||||
|
||||
const custom = () => {
|
||||
const field = select()
|
||||
|
||||
if (!field?.custom) return false
|
||||
const value = answer(field)
|
||||
|
||||
return formStore.customOn || (value !== undefined && !field.options?.some((option) => option.value === value))
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4 px-3">
|
||||
<Switch>
|
||||
@@ -778,7 +738,7 @@ function ProviderConnection(props: {
|
||||
type="text"
|
||||
label={text()?.title ?? ""}
|
||||
placeholder={text()?.placeholder}
|
||||
value={text() ? (answer(text()!) ?? "") : ""}
|
||||
value={text() ? (formStore.value[text()!.key] ?? "") : ""}
|
||||
onChange={(value) => {
|
||||
const field = text()
|
||||
|
||||
@@ -790,62 +750,6 @@ function ProviderConnection(props: {
|
||||
{language.t("common.continue")}
|
||||
</Button>
|
||||
</Match>
|
||||
<Match when={select()?.custom && select()}>
|
||||
{(field) => (
|
||||
<>
|
||||
<div class="w-full flex flex-col gap-3">
|
||||
<div class="text-14-regular text-text-base">{field().title}</div>
|
||||
<RadioGroup
|
||||
label={field().title}
|
||||
hideLabel
|
||||
description={field().description}
|
||||
value={
|
||||
custom()
|
||||
? "custom"
|
||||
: String(field().options?.findIndex((option) => option.value === answer(field())))
|
||||
}
|
||||
onChange={(value) => {
|
||||
const option = field().options?.[Number(value)]
|
||||
|
||||
if (option) {
|
||||
if (custom()) setFormStore("customText", answer(field()) ?? "")
|
||||
setFormStore("customOn", false)
|
||||
setFormStore("value", field().key, option.value)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setFormStore("customOn", true)
|
||||
setFormStore("value", field().key, formStore.customText)
|
||||
}}
|
||||
>
|
||||
<For each={field().options}>
|
||||
{(option, index) => (
|
||||
<RadioItem value={String(index())} label={option.label} description={option.description} />
|
||||
)}
|
||||
</For>
|
||||
<RadioItem value="custom" label={language.t("ui.messagePart.option.typeOwnAnswer")} />
|
||||
<Show when={custom()}>
|
||||
<div class="pl-6">
|
||||
<TextField
|
||||
ref={(input: HTMLInputElement) => queueMicrotask(() => input.focus())}
|
||||
type="text"
|
||||
label={language.t("ui.messagePart.option.typeOwnAnswer")}
|
||||
hideLabel
|
||||
placeholder={field().placeholder ?? language.t("ui.question.custom.placeholder")}
|
||||
value={answer(field()) ?? ""}
|
||||
onChange={(value) => setFormStore("value", field().key, value)}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<Button class="w-auto" type="submit" size="large" variant="contrast" disabled={!valid()}>
|
||||
{language.t("common.continue")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={item()?.field.options !== undefined}>
|
||||
<div class="w-full flex flex-col gap-1.5">
|
||||
<div class="text-14-regular text-text-base">{select()?.title}</div>
|
||||
@@ -854,7 +758,7 @@ function ProviderConnection(props: {
|
||||
class="px-3"
|
||||
items={select()?.options ?? []}
|
||||
key={(x) => x.value}
|
||||
current={select()?.options?.find((x) => x.value === answer(select()!))}
|
||||
current={select()?.options?.find((x) => x.value === formStore.value[select()!.key])}
|
||||
onSelect={(value) => {
|
||||
if (!value) return
|
||||
const field = select()
|
||||
@@ -972,11 +876,9 @@ function ProviderConnection(props: {
|
||||
let apiKey: HTMLInputElement | undefined
|
||||
const errorID = createUniqueId()
|
||||
|
||||
const [formStore, setFormStore] = createStore<{
|
||||
value: string
|
||||
error?: string
|
||||
}>({
|
||||
const [formStore, setFormStore] = createStore({
|
||||
value: "",
|
||||
error: undefined as string | undefined,
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
@@ -988,16 +890,16 @@ function ProviderConnection(props: {
|
||||
|
||||
if (!(e.currentTarget instanceof HTMLFormElement)) return
|
||||
const value = new FormData(e.currentTarget).get("apiKey")
|
||||
const key = value instanceof File || value === null ? "" : value
|
||||
const apiKey = typeof value === "string" ? value : ""
|
||||
|
||||
if (!key.trim()) {
|
||||
if (!apiKey?.trim()) {
|
||||
setFormStore("error", language.t("provider.connect.apiKey.required"))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setFormStore("error", undefined)
|
||||
await controller.auth.connectKey(key)
|
||||
await controller.auth.connectKey(apiKey)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -1052,11 +954,9 @@ function ProviderConnection(props: {
|
||||
let codeInput: HTMLInputElement | undefined
|
||||
const errorID = createUniqueId()
|
||||
|
||||
const [formStore, setFormStore] = createStore<{
|
||||
value: string
|
||||
error?: string
|
||||
}>({
|
||||
const [formStore, setFormStore] = createStore({
|
||||
value: "",
|
||||
error: undefined as string | undefined,
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
@@ -1068,9 +968,9 @@ function ProviderConnection(props: {
|
||||
|
||||
if (!(e.currentTarget instanceof HTMLFormElement)) return
|
||||
const value = new FormData(e.currentTarget).get("code")
|
||||
const code = value instanceof File || value === null ? "" : value
|
||||
const code = typeof value === "string" ? value : ""
|
||||
|
||||
if (!code.trim()) {
|
||||
if (!code?.trim()) {
|
||||
setFormStore("error", language.t("provider.connect.oauth.code.required"))
|
||||
|
||||
return
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
[data-slot="mobile-drawer-content"]:has([data-slot="model-selector-drawer"]) {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
[data-slot="model-selector-drawer"] {
|
||||
/* Keep the gutter inside each scroll boundary so outlines are not clipped. */
|
||||
[data-slot="model-selector-search"],
|
||||
.settings-models,
|
||||
[data-slot="model-selector-actions"] {
|
||||
padding-left: max(12px, env(safe-area-inset-left, 0px));
|
||||
padding-right: max(12px, env(safe-area-inset-right, 0px));
|
||||
}
|
||||
|
||||
[data-component="settings-list"][data-variant="catalog"] {
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
[data-component="settings-row"] {
|
||||
width: calc(100% + 24px);
|
||||
margin-inline: -12px;
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
.settings-models-console-groups {
|
||||
margin-inline-start: 4px;
|
||||
padding-inline-start: 8px;
|
||||
}
|
||||
|
||||
.provider-model-group-trigger,
|
||||
.provider-model-group-models {
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
.provider-model-group-models > [data-component="settings-list"] {
|
||||
padding-inline: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Popover } from "@kobalte/core/popover"
|
||||
import { Component, ComponentProps, createEffect, createMemo, For, JSX, Show, Suspense, lazy, on } from "solid-js"
|
||||
import { Component, ComponentProps, createEffect, createMemo, For, JSX, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { useLocal, type ModelSelection } from "@/providers/models/selection"
|
||||
import { useDialog } from "@opencode/ui/context/dialog"
|
||||
import { popularProviders } from "@/providers/catalog/providers"
|
||||
@@ -25,20 +24,8 @@ import { createMenuDismissController } from "@/shell/commands/menu-dismiss"
|
||||
import { createEventListener } from "@solid-primitives/event-listener"
|
||||
import { matchesModelSearch } from "./search"
|
||||
import { SettingsList } from "@/settings/list"
|
||||
import {
|
||||
CONSOLE_GROUP_KEY,
|
||||
consoleModelGroup,
|
||||
ProviderModelIcon,
|
||||
ProviderModelSections,
|
||||
} from "@/providers/models/provider-group"
|
||||
import { CONSOLE_GROUP_KEY, consoleModelGroup, ProviderModelIcon, ProviderModelSections } from "@/providers/models/provider-group"
|
||||
import "@/settings/settings.css"
|
||||
import "./select-dialog.css"
|
||||
|
||||
const MobilePanelDrawer = lazy(async () => {
|
||||
const { MobilePanelDrawer } = await import("@/shell/mobile-panel-drawer")
|
||||
|
||||
return { default: MobilePanelDrawer }
|
||||
})
|
||||
|
||||
const isFree = (provider: string, cost: { input: number } | undefined) =>
|
||||
provider === "opencode" && (!cost || cost.input === 0)
|
||||
@@ -67,8 +54,6 @@ const sortModelGroups = (a: { category: string; items: ModelItem[] }, b: { categ
|
||||
}
|
||||
|
||||
const ModelList: Component<{
|
||||
mobile?: boolean
|
||||
open?: boolean
|
||||
provider?: string
|
||||
onSelect: () => void
|
||||
model?: ModelState
|
||||
@@ -81,21 +66,12 @@ const ModelList: Component<{
|
||||
onSelect: props.onSelect,
|
||||
})
|
||||
|
||||
const [store, setStore] = createStore<{ search: string; active: string; collapsed: Record<string, boolean> }>({
|
||||
const [store, setStore] = createStore({
|
||||
search: "",
|
||||
active: props.mobile ? (controller.current() ?? "") : "",
|
||||
collapsed: {},
|
||||
active: "",
|
||||
collapsed: {} as Record<string, boolean>,
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.open,
|
||||
(open) => {
|
||||
if (!props.mobile || !open) return
|
||||
setStore({ search: "", active: controller.current() ?? "" })
|
||||
},
|
||||
),
|
||||
)
|
||||
const models = createMemo(() => controller.models(store.search))
|
||||
const modelGroups = createMemo(() => controller.groups(models()))
|
||||
const managed = createMemo(() => consoleModelGroup(controller.all()))
|
||||
@@ -134,7 +110,7 @@ const ModelList: Component<{
|
||||
if (item) controller.select(item)
|
||||
}
|
||||
|
||||
function ModelRows(props: { items: ModelItem[]; mobile?: boolean }) {
|
||||
function ModelRows(props: { items: ModelItem[] }) {
|
||||
return (
|
||||
<SettingsList variant="catalog">
|
||||
<For each={props.items}>
|
||||
@@ -143,7 +119,6 @@ const ModelList: Component<{
|
||||
type="button"
|
||||
data-component="settings-row"
|
||||
data-option-key={modelKey(item)}
|
||||
aria-pressed={controller.current() === modelKey(item)}
|
||||
class="-mx-4 w-[calc(100%+32px)] px-4 text-start first:rounded-t-lg last:rounded-b-lg hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
|
||||
classList={{ "bg-v2-overlay-simple-overlay-hover": store.active === modelKey(item) }}
|
||||
onMouseEnter={() => setStore("active", modelKey(item))}
|
||||
@@ -153,7 +128,6 @@ const ModelList: Component<{
|
||||
<div data-slot="settings-row-copy">
|
||||
<div data-slot="settings-row-title" class="flex items-center gap-2">
|
||||
<Tooltip
|
||||
inactive={props.mobile}
|
||||
placement="right-start"
|
||||
gutter={12}
|
||||
openDelay={0}
|
||||
@@ -185,7 +159,7 @@ const ModelList: Component<{
|
||||
|
||||
return (
|
||||
<div class="flex min-h-0 flex-1 flex-col">
|
||||
<div data-slot="model-selector-search" class="shrink-0 pt-px pb-3" classList={{ "px-4": !props.mobile }}>
|
||||
<div class="shrink-0 px-4 pt-px pb-3">
|
||||
<div class="relative">
|
||||
<TextInput
|
||||
type="search"
|
||||
@@ -193,7 +167,7 @@ const ModelList: Component<{
|
||||
class="!w-full self-stretch"
|
||||
placeholder={language.t("dialog.model.search.placeholder")}
|
||||
value={store.search}
|
||||
autofocus={!props.mobile}
|
||||
autofocus
|
||||
spellcheck={false}
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
@@ -236,12 +210,8 @@ const ModelList: Component<{
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative min-h-0" classList={{ "flex-1": !props.mobile }}>
|
||||
<div
|
||||
ref={(element) => (scrollRef = element)}
|
||||
class="settings-panel settings-models pt-1 pb-4"
|
||||
classList={{ "h-full px-4": !props.mobile, "max-h-[min(360px,40dvh)]": props.mobile }}
|
||||
>
|
||||
<div class="relative min-h-0 flex-1">
|
||||
<div ref={(element) => (scrollRef = element)} class="settings-panel settings-models h-full px-4 pt-1 pb-4">
|
||||
<Show
|
||||
when={models().length > 0}
|
||||
fallback={<div class="settings-models-status">{language.t("dialog.model.empty")}</div>}
|
||||
@@ -252,7 +222,7 @@ const ModelList: Component<{
|
||||
expanded={expanded}
|
||||
disabled={store.search.length > 0}
|
||||
onExpandedChange={(key, value) => setStore("collapsed", key, !value)}
|
||||
rows={(items) => <ModelRows items={items} mobile={props.mobile} />}
|
||||
rows={(items) => <ModelRows items={items} />}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
@@ -268,12 +238,10 @@ type ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => JSX.Element
|
||||
export function ModelSelectorPopover(props: {
|
||||
provider?: string
|
||||
model?: ModelState
|
||||
unpaid?: boolean
|
||||
trigger: ModelSelectorTrigger
|
||||
onClose?: () => void
|
||||
}) {
|
||||
const dialog = useDialog()
|
||||
const mobile = createMediaQuery("(max-width: 767px)")
|
||||
const data = useData()
|
||||
const location = useWorkspaceLocation()
|
||||
|
||||
@@ -288,171 +256,27 @@ export function ModelSelectorPopover(props: {
|
||||
|
||||
const connection = data.location.integration
|
||||
.list(location().ref)
|
||||
?.find((integration) => integration.id === "openai")?.connections[0]
|
||||
?.find((integration) => integration.id === "openai")
|
||||
?.connections[0]
|
||||
|
||||
return connection?.type === "credential" && connection.method === "oauth"
|
||||
}
|
||||
|
||||
const manage = async () => {
|
||||
const { DialogManageModels } = await import("./manage")
|
||||
void dialog.show(() => <DialogManageModels />)
|
||||
}
|
||||
|
||||
const connect = async () => {
|
||||
const { DialogConnectProvider } = await import("@/providers/connect/dialog")
|
||||
void dialog.show(() => <DialogConnectProvider directory={location().directory} />)
|
||||
}
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={mobile()}
|
||||
fallback={
|
||||
<ModelSelectorPopoverView
|
||||
trigger={props.trigger}
|
||||
models={controller.models}
|
||||
groups={controller.groups}
|
||||
current={controller.current()}
|
||||
chatgptPlan={chatgptPlan()}
|
||||
select={controller.select}
|
||||
onManage={manage}
|
||||
onClose={() => props.onClose?.()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ModelSelectorDrawer
|
||||
trigger={props.trigger}
|
||||
model={props.model}
|
||||
provider={props.provider}
|
||||
onConnect={props.unpaid ? connect : undefined}
|
||||
chatgptPlan={chatgptPlan()}
|
||||
onClose={() => props.onClose?.()}
|
||||
onManage={manage}
|
||||
/>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function ModelSelectorDrawer(props: {
|
||||
trigger: ModelSelectorTrigger
|
||||
model?: ModelState
|
||||
provider?: string
|
||||
chatgptPlan?: boolean
|
||||
onClose: () => void
|
||||
onManage: () => void
|
||||
onConnect?: () => void
|
||||
}) {
|
||||
const language = useLanguage()
|
||||
|
||||
const [store, setStore] = createStore<{
|
||||
open: boolean
|
||||
loaded: boolean
|
||||
restoreTrigger: boolean
|
||||
action?: "select" | "manage" | "connect"
|
||||
}>({
|
||||
open: false,
|
||||
loaded: false,
|
||||
restoreTrigger: true,
|
||||
})
|
||||
|
||||
let trigger: HTMLDivElement | undefined
|
||||
let content: HTMLDivElement | undefined
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={trigger} class="min-w-0">
|
||||
{props.trigger({
|
||||
"aria-haspopup": "dialog",
|
||||
get "aria-expanded"() {
|
||||
return store.open
|
||||
},
|
||||
onClick: () => setStore({ open: true, loaded: true, restoreTrigger: true }),
|
||||
})}
|
||||
</div>
|
||||
<Show when={store.loaded}>
|
||||
<Suspense>
|
||||
<MobilePanelDrawer
|
||||
hideHeader
|
||||
title={language.t("dialog.model.select.title")}
|
||||
open={store.open}
|
||||
onOpenChange={(open) => setStore("open", open)}
|
||||
initialFocus={() => content}
|
||||
returnFocus={() => trigger?.querySelector("button") ?? undefined}
|
||||
onFinalFocus={(event) => {
|
||||
if (!store.restoreTrigger) event.preventDefault()
|
||||
}}
|
||||
onContentPresentChange={(present) => {
|
||||
if (present) return
|
||||
const action = store.action
|
||||
setStore("action", undefined)
|
||||
|
||||
if (action === "manage") {
|
||||
props.onManage()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (action === "connect") {
|
||||
props.onConnect?.()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (action === "select") queueMicrotask(props.onClose)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={content}
|
||||
tabIndex={-1}
|
||||
data-slot="model-selector-drawer"
|
||||
class="flex min-h-0 flex-col gap-2 outline-none"
|
||||
>
|
||||
<div class="flex min-h-0 flex-col">
|
||||
<ModelList
|
||||
mobile
|
||||
open={store.open}
|
||||
model={props.model}
|
||||
provider={props.provider}
|
||||
onSelect={() => setStore({ open: false, action: "select", restoreTrigger: false })}
|
||||
/>
|
||||
</div>
|
||||
<div data-slot="model-selector-actions" class="flex flex-col gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full !h-10 !justify-start"
|
||||
icon="outline-sliders"
|
||||
onClick={() => setStore({ open: false, action: "manage", restoreTrigger: false })}
|
||||
>
|
||||
{language.t("dialog.model.manage")}
|
||||
</Button>
|
||||
<Show when={props.onConnect}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full !h-10 !justify-start"
|
||||
icon="plus"
|
||||
onClick={() => setStore({ open: false, action: "connect", restoreTrigger: false })}
|
||||
>
|
||||
{language.t("command.provider.connect")}
|
||||
</Button>
|
||||
</Show>
|
||||
<Show when={props.chatgptPlan}>
|
||||
<div class="flex min-h-10 items-center gap-2 border-t border-v2-border-border-muted px-3 py-2 text-[13px] leading-5 text-v2-text-text-base">
|
||||
<ProviderModelIcon provider={{ id: "openai", name: "OpenAI" }} class="shrink-0" />
|
||||
<span class="min-w-0 flex-1 truncate">{language.t("dialog.model.chatgptPlan")}</span>
|
||||
<ExternalLink
|
||||
href="https://chatgpt.com/settings/usage"
|
||||
class="flex shrink-0 items-center gap-1 rounded-sm text-v2-text-text-muted no-underline hover:text-v2-text-text-base focus-visible:outline focus-visible:outline-2"
|
||||
>
|
||||
{language.t("dialog.model.chatgptManageUsage")}
|
||||
<Icon name="arrow-up-right" size="small" />
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</MobilePanelDrawer>
|
||||
</Suspense>
|
||||
</Show>
|
||||
</>
|
||||
<ModelSelectorPopoverView
|
||||
trigger={props.trigger}
|
||||
models={controller.models}
|
||||
groups={controller.groups}
|
||||
current={controller.current()}
|
||||
chatgptPlan={chatgptPlan()}
|
||||
select={controller.select}
|
||||
onManage={() => {
|
||||
void import("./manage").then((module) => {
|
||||
void dialog.show(() => <module.DialogManageModels />)
|
||||
})
|
||||
}}
|
||||
onClose={() => props.onClose?.()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ type Attached = {
|
||||
keybind: (command: string) => readonly string[]
|
||||
matches: (command: string, event: KeyboardEvent) => boolean
|
||||
servers: Accessor<readonly string[]>
|
||||
server: (id: string) => ServerRef | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -270,10 +269,7 @@ export function createHostApis() {
|
||||
keys: (bind) => formatKeybindParts(bind, language.t),
|
||||
matches: (command, event) => current()?.matches(command, event) ?? false,
|
||||
}),
|
||||
servers: () => ({
|
||||
list: () => current()?.servers() ?? [],
|
||||
get: (id) => current()?.server(id),
|
||||
}),
|
||||
servers: () => ({ list: () => current()?.servers() ?? [] }),
|
||||
workspaces: (_extension, _owner, _context, register) => ({
|
||||
on(_event, handler) {
|
||||
removed.add(handler)
|
||||
@@ -393,28 +389,15 @@ export function createExtensionAttachment(apis: HostApis) {
|
||||
|
||||
if (existing) return existing
|
||||
const key = ServerConnection.Key.make(id)
|
||||
// The controller starts on first use, not when the ref is made: reading `builtin` to find a server must not
|
||||
// connect to every listed server, such as one the runtime holds back because it rejects its credentials.
|
||||
let started: Accessor<ServerCtx> | undefined
|
||||
|
||||
const live = () => {
|
||||
started ??= runWithOwner(owner, () =>
|
||||
createMemo<ServerCtx>(
|
||||
(previous) => global.serverCtx(key) ?? previous,
|
||||
global.ensureServerCtx(connection(id) ?? conn),
|
||||
),
|
||||
)!
|
||||
|
||||
return started()
|
||||
}
|
||||
|
||||
// What the app's server list already says, without a controller.
|
||||
const listed = () => connection(id) ?? live().sdk.server
|
||||
const live = runWithOwner(owner, () =>
|
||||
createMemo<ServerCtx>((previous) => global.serverCtx(key) ?? previous, global.ensureServerCtx(conn)),
|
||||
)!
|
||||
|
||||
const ref: ServerRef = {
|
||||
id,
|
||||
get name() {
|
||||
return serverName(listed()) || id
|
||||
return serverName(live().sdk.server) || id
|
||||
},
|
||||
get url() {
|
||||
return live().sdk.url
|
||||
@@ -429,10 +412,10 @@ export function createExtensionAttachment(apis: HostApis) {
|
||||
return live().data
|
||||
},
|
||||
get local() {
|
||||
return ServerConnection.local(listed())
|
||||
return ServerConnection.local(live().sdk.server)
|
||||
},
|
||||
get builtin() {
|
||||
return ServerConnection.builtin(listed())
|
||||
return ServerConnection.builtin(live().sdk.server)
|
||||
},
|
||||
get compatible() {
|
||||
return !global.servers.health[key]?.incompatible
|
||||
@@ -800,7 +783,6 @@ export function createExtensionAttachment(apis: HostApis) {
|
||||
keybind: command.keybindParts,
|
||||
matches: command.matches,
|
||||
servers: () => global.servers.list().map(ServerConnection.key),
|
||||
server,
|
||||
// SAFETY: an extension names a page it contributed through `SettingsPage`, which settings lists as an extension tab.
|
||||
settings: (page) => surface.open(page as Parameters<typeof surface.open>[0]),
|
||||
layout: {
|
||||
|
||||
@@ -2,7 +2,10 @@ import { createMemo, For, lazy, Show, Suspense, type JSX } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Tabs } from "@opencode/ui/tabs"
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
import { IconButton } from "@opencode/ui/icon-button"
|
||||
import { Menu } from "@opencode/ui/menu"
|
||||
import {
|
||||
DrawerContext,
|
||||
Panel,
|
||||
type PanelSidebar,
|
||||
type PanelTab,
|
||||
@@ -54,7 +57,7 @@ export function createMobileViews() {
|
||||
|
||||
export type MobileViews = ReturnType<typeof createMobileViews>
|
||||
|
||||
/** The narrow-screen view switcher: the conversation, each tab view, and a More drawer with the other views. */
|
||||
/** The narrow-screen view switcher: the conversation, each panel's view, and an overflow menu. */
|
||||
export function MobileViewTabs(props: {
|
||||
views: MobileViews
|
||||
region: Region
|
||||
@@ -62,38 +65,34 @@ export function MobileViewTabs(props: {
|
||||
session: MountedSession
|
||||
screen: SessionScreen
|
||||
sidebar: PanelSidebar
|
||||
bottom?: boolean
|
||||
onSelect: (key: string) => void
|
||||
}): JSX.Element {
|
||||
const language = useLanguage()
|
||||
|
||||
const [store, setStore] = createStore<{
|
||||
open: boolean
|
||||
loaded: boolean
|
||||
menu: boolean
|
||||
drawer: string | undefined
|
||||
last: string | undefined
|
||||
loaded: boolean
|
||||
pending: string | undefined
|
||||
}>({
|
||||
open: false,
|
||||
loaded: false,
|
||||
menu: false,
|
||||
drawer: undefined,
|
||||
// Keeps the last drawer's content mounted while it animates closed.
|
||||
last: undefined,
|
||||
loaded: false,
|
||||
pending: undefined,
|
||||
})
|
||||
|
||||
const drawer = createMemo(() => (store.drawer ? props.views.find(store.drawer) : undefined))
|
||||
|
||||
const tab = createMemo(() =>
|
||||
props.current === "session" || props.views.tabs().some((entry) => entry.key === props.current)
|
||||
? props.current
|
||||
: "more",
|
||||
)
|
||||
|
||||
const drawer = createMemo(() => (store.last ? props.views.find(store.last) : undefined))
|
||||
let trigger: HTMLButtonElement | undefined
|
||||
|
||||
return (
|
||||
<div
|
||||
class="relative flex shrink-0 items-center before:pointer-events-none before:absolute before:inset-x-0 before:h-px before:bg-v2-border-border-base before:content-['']"
|
||||
classList={{ "before:top-0": props.bottom, "before:bottom-0": !props.bottom }}
|
||||
class="relative flex shrink-0 items-center before:pointer-events-none before:absolute before:inset-x-0 before:bottom-0 before:h-px before:bg-v2-border-border-base before:content-['']"
|
||||
data-slot="session-mobile-view-navigation"
|
||||
>
|
||||
<Tabs value={tab()} variant="line" class="!h-auto min-w-0 flex-1" data-slot="session-mobile-view-tabs">
|
||||
<Tabs value={props.current} variant="line" class="!h-auto min-w-0 flex-1" data-slot="session-mobile-view-tabs">
|
||||
<Tabs.List aria-label={language.t("session.view.select")} class="!h-9 !gap-0 !px-0 before:!hidden">
|
||||
<Tabs.Trigger
|
||||
value="session"
|
||||
@@ -115,88 +114,73 @@ export function MobileViewTabs(props: {
|
||||
</Tabs.Trigger>
|
||||
)}
|
||||
</For>
|
||||
<Show when={props.views.menu().length}>
|
||||
<Tabs.Trigger
|
||||
value="more"
|
||||
ref={(element: HTMLButtonElement) => {
|
||||
trigger = element
|
||||
}}
|
||||
class="min-w-0 flex-1"
|
||||
classes={{ button: "w-full justify-center" }}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={store.open}
|
||||
onClick={() => setStore({ open: true, loaded: true, drawer: undefined })}
|
||||
>
|
||||
{language.t("session.tab.more")}
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
<Menu
|
||||
appearance="standard"
|
||||
modal={false}
|
||||
placement="bottom-end"
|
||||
gutter={4}
|
||||
open={store.menu}
|
||||
onOpenChange={(open) => setStore("menu", open)}
|
||||
>
|
||||
<Menu.Trigger
|
||||
as={IconButton}
|
||||
ref={(element: HTMLButtonElement) => {
|
||||
trigger = element
|
||||
}}
|
||||
icon={<Icon name="menu" />}
|
||||
variant="ghost-muted"
|
||||
size="normal"
|
||||
class="mx-1.5 shrink-0"
|
||||
state={props.views.menu().some((entry) => entry.key === props.current) || store.menu ? "pressed" : undefined}
|
||||
aria-label={language.t("common.moreOptions")}
|
||||
/>
|
||||
<Menu.Portal>
|
||||
<Menu.Content
|
||||
onCloseAutoFocus={(event) => {
|
||||
if (!store.pending) return
|
||||
event.preventDefault()
|
||||
setStore({ drawer: store.pending, last: store.pending, loaded: true, pending: undefined })
|
||||
}}
|
||||
>
|
||||
<For each={props.views.menu()}>
|
||||
{(entry) => (
|
||||
<Menu.Item
|
||||
onSelect={() => {
|
||||
if (entry.mobile.kind === "menu") return props.onSelect(entry.key)
|
||||
setStore({ pending: entry.key, menu: false })
|
||||
}}
|
||||
>
|
||||
{entry.mobile.title}
|
||||
</Menu.Item>
|
||||
)}
|
||||
</For>
|
||||
</Menu.Content>
|
||||
</Menu.Portal>
|
||||
</Menu>
|
||||
<Show when={store.loaded}>
|
||||
<Suspense>
|
||||
<MobilePanelDrawer
|
||||
title={drawer()?.mobile.title ?? language.t("common.moreOptions")}
|
||||
hideHeader
|
||||
open={store.open}
|
||||
onOpenChange={(open) => setStore("open", open)}
|
||||
title={drawer()?.mobile.title ?? ""}
|
||||
open={!!store.drawer}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setStore("drawer", undefined)
|
||||
}}
|
||||
returnFocus={() => trigger}
|
||||
>
|
||||
<Show
|
||||
when={drawer()}
|
||||
keyed
|
||||
fallback={
|
||||
<div
|
||||
data-slot="session-mobile-view-options"
|
||||
class="flex flex-col gap-0.5 rounded-[6px] bg-v2-background-bg-base p-0.5 shadow-[var(--v2-elevation-raised)] [[data-color-scheme=dark]_&]:bg-v2-background-bg-layer-01"
|
||||
>
|
||||
<For each={props.views.menu().filter((entry) => entry.mobile.kind === "menu")}>
|
||||
{(entry) => (
|
||||
<button
|
||||
type="button"
|
||||
class="flex min-h-11 w-full items-center gap-2 rounded-[4px] py-1 ps-3 pe-2 text-start text-[13px] font-[440] leading-[var(--line-height-base)] text-v2-text-text-base hover:bg-[var(--v2-overlay-simple-overlay-hover)] focus-visible:outline-none focus-visible:bg-[var(--v2-overlay-simple-overlay-hover)] aria-pressed:bg-[var(--v2-overlay-simple-overlay-pressed)]"
|
||||
aria-pressed={props.current === entry.key}
|
||||
onClick={() => {
|
||||
props.onSelect(entry.key)
|
||||
setStore("open", false)
|
||||
}}
|
||||
>
|
||||
<Show when={entry.mobile.icon}>
|
||||
{(icon) => <Icon name={icon()} class="shrink-0 text-v2-icon-icon-muted" />}
|
||||
</Show>
|
||||
<span class="min-w-0 flex-1 truncate">{entry.mobile.title}</span>
|
||||
<Show when={props.current === entry.key}>
|
||||
<Icon name="check" class="ms-1 shrink-0 text-v2-icon-icon-muted" />
|
||||
</Show>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
<For each={props.views.menu().filter((entry) => entry.mobile.kind === "drawer")}>
|
||||
{(entry) => (
|
||||
<button
|
||||
type="button"
|
||||
class="flex min-h-11 w-full items-center gap-2 rounded-[4px] py-1 ps-3 pe-2 text-start text-[13px] font-[440] leading-[var(--line-height-base)] text-v2-text-text-base hover:bg-[var(--v2-overlay-simple-overlay-hover)] focus-visible:outline-none focus-visible:bg-[var(--v2-overlay-simple-overlay-hover)]"
|
||||
onClick={() => setStore("drawer", entry.key)}
|
||||
>
|
||||
<Show when={entry.mobile.icon}>
|
||||
{(icon) => <Icon name={icon()} class="shrink-0 text-v2-icon-icon-muted" />}
|
||||
</Show>
|
||||
<span class="min-w-0 flex-1 truncate">{entry.mobile.title}</span>
|
||||
<Icon name="chevron-right" class="ms-1 shrink-0 text-v2-icon-icon-muted" />
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Show when={drawer()} keyed>
|
||||
{(entry) => (
|
||||
<MobilePanel
|
||||
entry={entry}
|
||||
view={props.session}
|
||||
screen={props.screen}
|
||||
sidebar={props.sidebar}
|
||||
visible={store.open}
|
||||
open={() => props.region.openFor(entry.extension)}
|
||||
/>
|
||||
<DrawerContext.Provider value={{ close: () => setStore("drawer", undefined) }}>
|
||||
<MobilePanel
|
||||
entry={entry}
|
||||
view={props.session}
|
||||
screen={props.screen}
|
||||
sidebar={props.sidebar}
|
||||
visible={store.drawer === entry.key}
|
||||
open={() => props.region.openFor(entry.extension)}
|
||||
/>
|
||||
</DrawerContext.Provider>
|
||||
)}
|
||||
</Show>
|
||||
</MobilePanelDrawer>
|
||||
|
||||
@@ -775,12 +775,14 @@ export const dict = {
|
||||
"server.connect.button": "መገናኘት",
|
||||
"server.connect.address.invalid": "የሚሰራ የኤችቲቲፒ ወይም የኤችቲቲፒኤስ አገልጋይ አድራሻ ያስገቡ።",
|
||||
"server.connect.failed": "መገናኘት አልተቻለም። የአገልጋዩን አድራሻ እና የይለፍ ቃል ያረጋግጡ እና ከዚያ እንደገና ይሞክሩ።",
|
||||
"server.connect.pair.description": "የግንኙነት ዝርዝሮችዎን ለማግኘት ይህንን ትዕዛዝ በኮምፒተርዎ ላይ ያሂዱ።",
|
||||
"server.connect.scan": "QR ኮድ ይቃኙ",
|
||||
"server.connect.scan.description": "ካሜራዎን opencode pair ወደሚያሳየው QR ኮድ ያነጣጥሩ።",
|
||||
"server.connect.scan.invalid": "ይህ የOpenCode ማጣመሪያ ኮድ አይደለም። opencode pair የሚያሳየውን ኮድ ይቃኙ።",
|
||||
"server.connect.camera": "ማጣመር ካሜራ",
|
||||
"server.connect.camera.starting": "ካሜራ በመክፈት ላይ...",
|
||||
"server.connect.mixedContent": "ከዚህ ኤችቲቲፒኤስ አገልጋይ ጋር ከኤችቲቲፒኤስ ገጽ ጋር መገናኘት አልተቻለም። በምትኩ የኤችቲቲፒኤስ አገልጋይ አድራሻ ይጠቀሙ።",
|
||||
"server.connect.camera.insecure": "QR ቅኝት ይህንን ገጽ በኤችቲቲፒኤስ ወይም በአካባቢያዊ አስተናጋጅ መክፈት ይጠይቃል።",
|
||||
"server.connect.camera.unavailable": "ለዚህ አሳሽ ምንም ካሜራ አይገኝም። የግንኙነት ዝርዝሮችዎን እራስዎ ያስገቡ።",
|
||||
"server.connect.camera.error": "ካሜራውን መክፈት አልተቻለም። የካሜራ መዳረሻ ይፍቀዱ ወይም የግንኙነት ዝርዝሮችዎን እራስዎ ያስገቡ።",
|
||||
"dialog.server.menu.remove": "አስወግድ",
|
||||
|
||||
@@ -833,12 +833,14 @@ export const dict = {
|
||||
"server.connect.button": "اتصال",
|
||||
"server.connect.address.invalid": "أدخل عنوان خادم HTTP أو HTTPS صالحًا.",
|
||||
"server.connect.failed": "تعذر الاتصال. تحقق من عنوان الخادم وكلمة المرور، ثم أعد المحاولة.",
|
||||
"server.connect.pair.description": "نفّذ هذا الأمر على حاسوبك للحصول على تفاصيل الاتصال.",
|
||||
"server.connect.scan": "مسح رمز QR",
|
||||
"server.connect.scan.description": "وجّه الكاميرا نحو رمز QR الذي يعرضه opencode pair.",
|
||||
"server.connect.scan.invalid": "هذا ليس رمز إقران OpenCode. امسح الرمز الذي يعرضه opencode pair.",
|
||||
"server.connect.camera": "كاميرا الإقران",
|
||||
"server.connect.camera.starting": "جارٍ فتح الكاميرا…",
|
||||
"server.connect.mixedContent": "تعذر الاتصال بخادم HTTP هذا من صفحة HTTPS. استخدم عنوان خادم HTTPS بدلًا منه.",
|
||||
"server.connect.camera.insecure": "يتطلب مسح رمز QR فتح هذه الصفحة عبر HTTPS أو على localhost.",
|
||||
"server.connect.camera.unavailable": "لا تتوفر كاميرا لهذا المتصفح. أدخل تفاصيل الاتصال يدويًا.",
|
||||
"server.connect.camera.error": "تعذر فتح الكاميرا. اسمح بالوصول إليها أو أدخل تفاصيل الاتصال يدويًا.",
|
||||
"dialog.server.menu.remove": "إزالة",
|
||||
|
||||
@@ -799,6 +799,7 @@ export const dict = {
|
||||
"server.connect.button": "Qoşulun",
|
||||
"server.connect.address.invalid": "Etibarlı HTTP və ya HTTPS server ünvanını daxil edin.",
|
||||
"server.connect.failed": "Bağlana bilmədik. Server ünvanını və şifrəni yoxlayın, sonra yenidən cəhd edin.",
|
||||
"server.connect.pair.description": "Bağlantı məlumatlarınızı almaq üçün bu əmri kompüterinizdə işlədin.",
|
||||
"server.connect.scan": "QR kodu skan edin",
|
||||
"server.connect.scan.description": "Kameranızı opencode pair tərəfindən göstərilən QR koda yönəldin.",
|
||||
"server.connect.scan.invalid": "Bu, OpenCode qoşulma kodu deyil. opencode pair tərəfindən göstərilən kodu skan edin.",
|
||||
@@ -806,6 +807,8 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Kamera açılır…",
|
||||
"server.connect.mixedContent":
|
||||
"Bu HTTPS səhifəsindən bu HTTP serverinə qoşulmaq mümkün olmadı. Bunun əvəzinə HTTPS server ünvanından istifadə edin.",
|
||||
"server.connect.camera.insecure":
|
||||
"QR skan edilməsi üçün bu səhifəni HTTPS üzərindən və ya localhost-da açmaq tələb olunur.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Bu brauzer üçün heç bir kamera mövcud deyil. Bağlantı məlumatlarınızı əl ilə daxil edin.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -797,6 +797,7 @@ export const dict = {
|
||||
"server.connect.button": "Свързване",
|
||||
"server.connect.address.invalid": "Въведете валиден HTTP или HTTPS адрес на сървър.",
|
||||
"server.connect.failed": "Свързването е неуспешно. Проверете адреса и паролата на сървъра и опитайте отново.",
|
||||
"server.connect.pair.description": "Изпълнете тази команда на компютъра си, за да получите данните за връзка.",
|
||||
"server.connect.scan": "Сканиране на QR код",
|
||||
"server.connect.scan.description": "Насочете камерата към QR кода, показан от opencode pair.",
|
||||
"server.connect.scan.invalid": "Това не е код за сдвояване с OpenCode. Сканирайте кода, показан от opencode pair.",
|
||||
@@ -804,6 +805,8 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Отваряне на камерата…",
|
||||
"server.connect.mixedContent":
|
||||
"Не може да се осъществи връзка с този HTTP сървър от HTTPS страница. Използвайте HTTPS адрес на сървъра.",
|
||||
"server.connect.camera.insecure":
|
||||
"Сканирането на QR код изисква тази страница да е отворена чрез HTTPS или на localhost.",
|
||||
"server.connect.camera.unavailable": "Няма достъпна камера за този браузър. Въведете ръчно данните за връзка.",
|
||||
"server.connect.camera.error":
|
||||
"Камерата не можа да бъде отворена. Разрешете достъпа до нея или въведете ръчно данните за връзка.",
|
||||
|
||||
@@ -797,6 +797,7 @@ export const dict = {
|
||||
"server.connect.button": "সংযোগ করুন",
|
||||
"server.connect.address.invalid": "একটি বৈধ HTTP বা HTTPS সার্ভার ঠিকানা লিখুন৷",
|
||||
"server.connect.failed": "সংযোগ করা যায়নি। সার্ভার ঠিকানা এবং পাসওয়ার্ড পরীক্ষা করুন, তারপর আবার চেষ্টা করুন.",
|
||||
"server.connect.pair.description": "আপনার সংযোগের বিবরণ পেতে আপনার কম্পিউটারে এই কমান্ডটি চালান।",
|
||||
"server.connect.scan": "QR কোড স্ক্যান করুন",
|
||||
"server.connect.scan.description": "আপনার ক্যামেরাকে ওপেনকোড পেয়ার দ্বারা দেখানো QR কোডে নির্দেশ করুন৷",
|
||||
"server.connect.scan.invalid": "এটি একটি OpenCode পেয়ারিং কোড নয়৷ ওপেনকোড পেয়ার দ্বারা দেখানো কোডটি স্ক্যান করুন।",
|
||||
@@ -804,6 +805,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "ক্যামেরা খোলা হচ্ছে...",
|
||||
"server.connect.mixedContent":
|
||||
"একটি HTTPS পৃষ্ঠা থেকে এই HTTP সার্ভারের সাথে সংযোগ করা যায়নি৷ পরিবর্তে একটি HTTPS সার্ভার ঠিকানা ব্যবহার করুন৷",
|
||||
"server.connect.camera.insecure": "QR স্ক্যান করার জন্য এই পৃষ্ঠাটি HTTPS বা লোকালহোস্টে খুলতে হবে।",
|
||||
"server.connect.camera.unavailable": "এই ব্রাউজারে কোন ক্যামেরা উপলব্ধ নেই৷ ম্যানুয়ালি আপনার সংযোগ বিশদ লিখুন.",
|
||||
"server.connect.camera.error":
|
||||
"ক্যামেরা খুলতে পারিনি। ক্যামেরা অ্যাক্সেসের অনুমতি দিন বা ম্যানুয়ালি আপনার সংযোগের বিবরণ লিখুন।",
|
||||
|
||||
@@ -22,6 +22,7 @@ export const dict = {
|
||||
"server.connect.button": "Conectar",
|
||||
"server.connect.address.invalid": "Insira um endereço de servidor HTTP ou HTTPS válido.",
|
||||
"server.connect.failed": "Não foi possível conectar. Verifique o endereço e a senha do servidor e tente novamente.",
|
||||
"server.connect.pair.description": "Execute este comando em seu computador para obter os detalhes de sua conexão.",
|
||||
"server.connect.scan": "Digitalize o código QR",
|
||||
"server.connect.scan.description": "Aponte sua câmera para o código QR mostrado por opencode pair.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -30,6 +31,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Abrindo a câmera…",
|
||||
"server.connect.mixedContent":
|
||||
"Não foi possível conectar-se a este servidor HTTP a partir de uma página HTTPS. Use um endereço de servidor HTTPS.",
|
||||
"server.connect.camera.insecure": "A varredura QR requer a abertura desta página em HTTPS ou no host local.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Nenhuma câmera está disponível para este navegador. Insira os detalhes da sua conexão manualmente.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -862,6 +862,7 @@ export const dict = {
|
||||
"server.connect.button": "Poveži se",
|
||||
"server.connect.address.invalid": "Unesite ispravnu HTTP ili HTTPS adresu servera.",
|
||||
"server.connect.failed": "Povezivanje nije uspjelo. Provjerite adresu servera i lozinku, pa pokušajte ponovo.",
|
||||
"server.connect.pair.description": "Pokrenite ovu naredbu na računaru da biste dobili podatke za povezivanje.",
|
||||
"server.connect.scan": "Skeniraj QR kod",
|
||||
"server.connect.scan.description": "Usmjerite kameru prema QR kodu koji prikazuje opencode pair.",
|
||||
"server.connect.scan.invalid": "Ovo nije OpenCode kod za uparivanje. Skenirajte kod koji prikazuje opencode pair.",
|
||||
@@ -869,6 +870,8 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Otvaranje kamere…",
|
||||
"server.connect.mixedContent":
|
||||
"Nije se moguće povezati s ovim HTTP serverom s HTTPS stranice. Umjesto toga koristite HTTPS adresu servera.",
|
||||
"server.connect.camera.insecure":
|
||||
"Za skeniranje QR koda ovu stranicu morate otvoriti putem HTTPS-a ili na localhostu.",
|
||||
"server.connect.camera.unavailable": "Ovom pregledniku nije dostupna kamera. Ručno unesite podatke za povezivanje.",
|
||||
"server.connect.camera.error":
|
||||
"Kameru nije bilo moguće otvoriti. Dozvolite pristup kameri ili ručno unesite podatke za povezivanje.",
|
||||
|
||||
@@ -796,6 +796,8 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Introduïu una adreça de servidor HTTP o HTTPS vàlida.",
|
||||
"server.connect.failed":
|
||||
"No s'ha pogut connectar. Comproveu l'adreça i la contrasenya del servidor i torneu-ho a provar.",
|
||||
"server.connect.pair.description":
|
||||
"Executeu aquesta ordre al vostre ordinador per obtenir els detalls de la vostra connexió.",
|
||||
"server.connect.scan": "Escaneja el codi QR",
|
||||
"server.connect.scan.description": "Apunteu la càmera al codi QR que mostra opencode pair.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -804,6 +806,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Obertura de la càmera...",
|
||||
"server.connect.mixedContent":
|
||||
"No s'ha pogut connectar a aquest servidor HTTP des d'una pàgina HTTPS. Utilitzeu una adreça de servidor HTTPS.",
|
||||
"server.connect.camera.insecure": "L'exploració de QR requereix obrir aquesta pàgina a HTTPS o a localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"No hi ha cap càmera disponible per a aquest navegador. Introduïu els detalls de connexió manualment.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -806,6 +806,7 @@ export const dict = {
|
||||
"server.connect.button": "Připojit",
|
||||
"server.connect.address.invalid": "Zadejte platnou adresu serveru HTTP nebo HTTPS.",
|
||||
"server.connect.failed": "Připojení se nezdařilo. Zkontrolujte adresu serveru a heslo a zkuste to znovu.",
|
||||
"server.connect.pair.description": "Spuštěním tohoto příkazu v počítači získáte údaje pro připojení.",
|
||||
"server.connect.scan": "Naskenovat QR kód",
|
||||
"server.connect.scan.description": "Namiřte kameru na QR kód zobrazený příkazem opencode pair.",
|
||||
"server.connect.scan.invalid": "Toto není párovací kód OpenCode. Naskenujte kód zobrazený příkazem opencode pair.",
|
||||
@@ -813,6 +814,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Otevírání kamery…",
|
||||
"server.connect.mixedContent":
|
||||
"K tomuto serveru HTTP se nelze připojit ze stránky HTTPS. Použijte adresu serveru HTTPS.",
|
||||
"server.connect.camera.insecure": "Skenování QR kódu vyžaduje otevření této stránky přes HTTPS nebo na localhostu.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Pro tento prohlížeč není dostupná žádná kamera. Zadejte údaje pro připojení ručně.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -23,6 +23,7 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Indtast en gyldig HTTP- eller HTTPS-serveradresse.",
|
||||
"server.connect.failed":
|
||||
"Kunne ikke oprette forbindelse. Tjek serveradressen og adgangskoden, og prøv derefter igen.",
|
||||
"server.connect.pair.description": "Kør denne kommando på din computer for at få dine forbindelsesdetaljer.",
|
||||
"server.connect.scan": "Scan QR kode",
|
||||
"server.connect.scan.description": "Ret dit kamera mod QR-koden vist af opencode pair.",
|
||||
"server.connect.scan.invalid": "Dette er ikke en OpenCode-parringskode. Scan koden vist af opencode pair.",
|
||||
@@ -30,6 +31,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Åbner kamera...",
|
||||
"server.connect.mixedContent":
|
||||
"Kunne ikke oprette forbindelse til denne HTTP-server fra en HTTPS-side. Brug i stedet en HTTPS-serveradresse.",
|
||||
"server.connect.camera.insecure": "QR-scanning kræver, at denne side åbnes over HTTPS eller på localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Intet kamera er tilgængeligt for denne browser. Indtast dine forbindelsesoplysninger manuelt.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -27,6 +27,8 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Geben Sie eine gültige HTTP- oder HTTPS-Serveradresse ein.",
|
||||
"server.connect.failed":
|
||||
"Es konnte keine Verbindung hergestellt werden. Überprüfen Sie die Serveradresse und das Passwort und versuchen Sie es dann erneut.",
|
||||
"server.connect.pair.description":
|
||||
"Führen Sie diesen Befehl auf Ihrem Computer aus, um Ihre Verbindungsdetails abzurufen.",
|
||||
"server.connect.scan": "Scannen Sie den QR-Code",
|
||||
"server.connect.scan.description": "Richten Sie Ihre Kamera auf den von opencode pair angezeigten Code QR.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -35,6 +37,8 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Kamera wird geöffnet…",
|
||||
"server.connect.mixedContent":
|
||||
"Es konnte keine Verbindung zu diesem HTTP-Server von einer HTTPS-Seite hergestellt werden. Verwenden Sie stattdessen eine HTTPS-Serveradresse.",
|
||||
"server.connect.camera.insecure":
|
||||
"Zum Scannen mit QR muss diese Seite über HTTPS oder auf localhost geöffnet werden.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Für diesen Browser ist keine Kamera verfügbar. Geben Sie Ihre Verbindungsdaten manuell ein.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -816,6 +816,7 @@ export const dict = {
|
||||
"server.connect.address.invalid": "ސައްހަ HTTP ނުވަތަ HTTPS ސަރވަރ އެޑްރެހެއް ލިޔެލާށެވެ.",
|
||||
"server.connect.failed":
|
||||
"ގުޅެވޭ ގޮތެއް ނުވިއެވެ. ސަރވަރ އެޑްރެހާއި ޕާސްވޯޑް ޗެކްކޮށްލުމަށްފަހު އަލުން މަސައްކަތް ކުރާށެވެ.",
|
||||
"server.connect.pair.description": "ކަނެކްޝަންގެ ތަފްސީލު ހޯދުމަށްޓަކައި ކޮމްޕިއުޓަރުގައި މި ކޮމާންޑް ހިންގާށެވެ.",
|
||||
"server.connect.scan": "QR ކޯޑް ސްކޭން ކުރާށެވެ",
|
||||
"server.connect.scan.description": "އޮޕަންކޯޑް ޖޯޑުން ދައްކާ QR ކޯޑަށް ކެމެރާ އިޝާރާތް ކުރާށެވެ.",
|
||||
"server.connect.scan.invalid": "މިއީ OpenCode ޖޯޑު ކޯޑެއް ނޫނެވެ. އޮޕަންކޯޑް ޕެއާ އިން ދައްކާ ކޯޑް ސްކޭން ކުރާށެވެ.",
|
||||
@@ -823,6 +824,8 @@ export const dict = {
|
||||
"server.connect.camera.starting": "ކެމެރާ ހުޅުވަނީ...",
|
||||
"server.connect.mixedContent":
|
||||
"HTTPS ޕޭޖަކުން މި HTTP ސަރވަރ އާއި ގުޅޭ ގޮތެއް ނުވިއެވެ. އޭގެ ބަދަލުގައި HTTPS ސަރވަރ އެޑްރެހެއް ބޭނުން ކުރާށެވެ.",
|
||||
"server.connect.camera.insecure":
|
||||
"QR ސްކޭން ކުރުމަށް މި ޕޭޖް ހުޅުވަން ޖެހެނީ HTTPS ގެ މައްޗަށް ނުވަތަ ލޯކަލްހޯސްޓް އިންނެވެ.",
|
||||
"server.connect.camera.unavailable":
|
||||
"މި ބްރައުޒާއަށް އެއްވެސް ކެމެރާއެއް ނުލިބެއެވެ. ކަނެކްޝަންގެ ތަފްސީލުތައް އަމިއްލައަށް ލިޔެލާށެވެ.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -797,6 +797,8 @@ export const dict = {
|
||||
"server.connect.button": "མཐུད།",
|
||||
"server.connect.address.invalid": "ནུས་ཅན་ HTTP ཡང་ན་ HTTPS སར་བར་ཁ་བྱང་ཅིག་བཙུགས།",
|
||||
"server.connect.failed": "མཐུད་མ་ཚུགས། སར་བར་ཁ་བྱང་དང་ཆོག་ཡིག་ཞིབ་དཔྱད་འབད་ཞིནམ་ལས་ ལོག་སྟེ་འབད་རྩོལ་བསྐྱེད།",
|
||||
"server.connect.pair.description":
|
||||
"ཁྱོད་རའི་མཐུད་ལམ་རྒྱས་བཤད་ཚུ་ཐོབ་ནིའི་དོན་ལུ་ ཁྱོད་རའི་གློག་རིག་གུ་བརྡ་བཀོད་འདི་གཡོག་བཀོལ།",
|
||||
"server.connect.scan": "QR གསང་གྲངས་པར་བཤུས་",
|
||||
"server.connect.scan.description": "ཁྱོད་རའི་པར་ཆས་འདི་ opencode pair གིས་སྟོན་མི་ QR གསང་ཡིག་ལུ་སྟོན་དགོ།",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -805,6 +807,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "པར་ཆས་ཁ་ཕྱེ་དོ...",
|
||||
"server.connect.mixedContent":
|
||||
"HTTPS ཤོག་ལེབ་ཅིག་ལས་ HTTP སར་བར་འདི་ལུ་མཐུད་མ་ཚུགས། དེ་གི་ཚབ་ལུ་ HTTPS སར་བར་ཁ་བྱང་ཅིག་ལག་ལེན་འཐབ།",
|
||||
"server.connect.camera.insecure": "QR པར་ལེན་འབད་ནི་ལུ་ ཤོག་ལེབ་འདི་ HTTPS ཡང་ན་ localhost གུ་ཁ་ཕྱེ་དགོཔ་ཨིན།",
|
||||
"server.connect.camera.unavailable": "བརྡ་འཚོལ་འདི་ལུ་པར་ཆས་མེད། ཁྱོད་རའི་མཐུད་ལམ་རྒྱས་བཤད་ཚུ་ལག་ཐོག་ལས་བཙུགས།",
|
||||
"server.connect.camera.error":
|
||||
"པར་ཆས་ཁ་ཕྱེ་མ་ཚུགས། པར་ཆས་འཛུལ་སྤྱོད་འབད་བཅུགཔ་ ཡང་ན་ ཁྱོད་རའི་མཐུད་ལམ་རྒྱས་བཤད་ཚུ་ལག་ཐོག་ལས་བཙུགས།",
|
||||
|
||||
@@ -796,6 +796,8 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Εισαγάγετε μια έγκυρη διεύθυνση διακομιστή HTTP ή HTTPS.",
|
||||
"server.connect.failed":
|
||||
"Δεν ήταν δυνατή η σύνδεση. Ελέγξτε τη διεύθυνση του διακομιστή και τον κωδικό πρόσβασης και, στη συνέχεια, δοκιμάστε ξανά.",
|
||||
"server.connect.pair.description":
|
||||
"Εκτελέστε αυτήν την εντολή στον υπολογιστή σας για να λάβετε τα στοιχεία της σύνδεσής σας.",
|
||||
"server.connect.scan": "Σάρωση κωδικού QR",
|
||||
"server.connect.scan.description": "Στρέψτε την κάμερά σας στον κωδικό QR που εμφανίζεται από το opencode pair.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -804,6 +806,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Άνοιγμα κάμερας…",
|
||||
"server.connect.mixedContent":
|
||||
"Δεν ήταν δυνατή η σύνδεση σε αυτόν τον διακομιστή HTTP από μια σελίδα HTTPS. Χρησιμοποιήστε μια διεύθυνση διακομιστή HTTPS.",
|
||||
"server.connect.camera.insecure": "Η σάρωση QR απαιτεί άνοιγμα αυτής της σελίδας μέσω HTTPS ή localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Δεν υπάρχει κάμερα διαθέσιμη σε αυτό το πρόγραμμα περιήγησης. Εισαγάγετε τα στοιχεία της σύνδεσής σας με μη αυτόματο τρόπο.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -340,41 +340,24 @@ export const dict = {
|
||||
|
||||
"server.connect.title": "Connect to a server",
|
||||
"server.connect.description": "Enter your server address and password to get started.",
|
||||
"server.connect.description.pairing": "Paste the pairing link from the server, or scan its QR code.",
|
||||
"server.connect.link": "Pairing link",
|
||||
"server.connect.link.placeholder": "http://192.168.1.2:4096/auth/connect/…",
|
||||
"server.connect.link.invalid":
|
||||
"Paste the full pairing link from opencode pair, or from Settings → Pairing on the server's computer.",
|
||||
"server.connect.password.prompt": "No pairing link?",
|
||||
"server.connect.password.use": "Use password",
|
||||
"server.connect.password.hint": "To see it, run opencode service get password on the server's computer.",
|
||||
"server.connect.link.prompt": "Have a pairing link?",
|
||||
"server.connect.link.use": "Use pairing link",
|
||||
"server.connect.button": "Connect",
|
||||
"server.connect.address.invalid": "Enter a valid HTTP or HTTPS server address.",
|
||||
"server.connect.failed": "Could not connect. Check the server address and password, then try again.",
|
||||
"server.connect.pair.link": "Run this command on the server's computer to get a pairing link and QR code.",
|
||||
"server.connect.pair.description": "Run this command on your computer to get your connection details.",
|
||||
"server.connect.scan": "Scan QR code",
|
||||
"server.connect.scan.description": "Point your camera at the QR code shown by opencode pair.",
|
||||
"server.connect.scan.invalid": "This is not an OpenCode pairing code. Scan the code shown by opencode pair.",
|
||||
"server.connect.scan.failed": "This browser could not read the QR code. Enter your connection details manually.",
|
||||
"server.connect.link.expired": "This pairing link expired or was already used. Run opencode pair to get a new one.",
|
||||
"server.connect.link.unreachable":
|
||||
"Could not reach {{url}}. Check that this device can reach the server, then try again.",
|
||||
"server.connect.link.legacy":
|
||||
"This pairing code is from an older version of OpenCode. Update OpenCode on the server, then run opencode pair again.",
|
||||
"server.connect.camera": "Pairing camera",
|
||||
"server.connect.camera.starting": "Opening camera…",
|
||||
"server.connect.mixedContent":
|
||||
"Could not connect to this HTTP server from an HTTPS page. Use an HTTPS server address instead.",
|
||||
"server.connect.camera.native": "This page cannot use the camera over HTTP. Paste the pairing link above.",
|
||||
"server.connect.camera.insecure": "QR scanning requires opening this page over HTTPS or on localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"No camera is available to this browser. Enter your connection details manually.",
|
||||
"server.connect.camera.error":
|
||||
"Could not open the camera. Allow camera access or enter your connection details manually.",
|
||||
"dialog.server.edit.title": "Edit server",
|
||||
"dialog.server.signedOut":
|
||||
"Your sign-in for this server is no longer valid. Paste a new pairing link, or enter the server password.",
|
||||
|
||||
"dialog.server.menu.edit": "Edit",
|
||||
"dialog.server.menu.default": "Set as default",
|
||||
@@ -507,7 +490,6 @@ export const dict = {
|
||||
"home.workspaceTip": "Start next session in a new workspace to keep changes isolated",
|
||||
|
||||
"session.tab.session": "Session",
|
||||
"session.tab.more": "More...",
|
||||
"session.view.select": "Session view",
|
||||
"session.tab.add": "Add tab",
|
||||
"session.tab.unknown": "Unknown Session",
|
||||
@@ -679,21 +661,6 @@ export const dict = {
|
||||
"settings.about.description": "OpenCode, the open source coding agent",
|
||||
"settings.about.trademark": "OpenCode is a registered trademark of Anomaly Innovations, Inc.",
|
||||
"settings.about.typeset": "Typeset in Inter and IBM Plex Mono",
|
||||
"settings.about.notices.title": "Third-party notices",
|
||||
"settings.about.notices.description": "OpenCode includes the following open source software.",
|
||||
"settings.about.notices.license": "License text",
|
||||
"settings.about.notices.fonts.title": "Fonts in the Office previews",
|
||||
"settings.about.notices.fonts.description":
|
||||
"Each family is distributed under the SIL Open Font License 1.1, with the copyright lines below.",
|
||||
"settings.about.notices.eigenpal": "Portions of BetterOffice derive from it.",
|
||||
"settings.about.notices.loadFailed": "Could not load the third-party notices",
|
||||
"settings.about.notices.crates.title": "Rust crates in the Office engines",
|
||||
"settings.about.notices.crates.description":
|
||||
"BetterOffice compiles these crates into its Word, Excel and PowerPoint engines. Where a crate offers a choice of licenses, the highlighted license applies.",
|
||||
"settings.about.notices.crates.licenses": "License texts",
|
||||
"settings.about.notices.crates.count.one": "{{count}} crate",
|
||||
"settings.about.notices.crates.count.other": "{{count}} crates",
|
||||
"settings.about.notices.crates.appliesTo": "Applies to {{names}}",
|
||||
"settings.about.tagline": "AI can’t build great software, without you",
|
||||
"settings.about.copyright": "© Anomaly Innovations, Inc.",
|
||||
"settings.preferences.description": "Customize preferences and theme and default behavior",
|
||||
|
||||
@@ -23,6 +23,7 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Ingrese una dirección de servidor HTTP o HTTPS válida.",
|
||||
"server.connect.failed":
|
||||
"No se pudo conectar. Verifique la dirección y la contraseña del servidor y vuelva a intentarlo.",
|
||||
"server.connect.pair.description": "Ejecute este comando en su computadora para obtener los detalles de su conexión.",
|
||||
"server.connect.scan": "Escanea el código QR",
|
||||
"server.connect.scan.description": "Apunte su cámara al código QR mostrado por opencode pair.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -31,6 +32,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Abriendo cámara...",
|
||||
"server.connect.mixedContent":
|
||||
"No se pudo conectar a este servidor HTTP desde una página HTTPS. Utilice una dirección de servidor HTTPS en su lugar.",
|
||||
"server.connect.camera.insecure": "El escaneo de QR requiere abrir esta página en HTTPS o en localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"No hay ninguna cámara disponible para este navegador. Ingrese los detalles de su conexión manualmente.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -775,6 +775,7 @@ export const dict = {
|
||||
"server.connect.button": "Ühendage",
|
||||
"server.connect.address.invalid": "Sisestage kehtiv serveri aadress HTTP või HTTPS.",
|
||||
"server.connect.failed": "Ühendust ei õnnestunud luua. Kontrollige serveri aadressi ja parooli ning proovige uuesti.",
|
||||
"server.connect.pair.description": "Ühenduse üksikasjade hankimiseks käivitage see käsk oma arvutis.",
|
||||
"server.connect.scan": "Skaneeri QR-kood",
|
||||
"server.connect.scan.description": "Suunake oma kaamera koodile QR, mida näitab opencode pair.",
|
||||
"server.connect.scan.invalid": "See ei ole OpenCode sidumiskood. Skannige koodi, mida näitab opencode pair.",
|
||||
@@ -782,6 +783,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Kaamera avamine…",
|
||||
"server.connect.mixedContent":
|
||||
"Selle HTTP serveriga ei saanud HTTPS lehelt ühendust luua. Kasutage selle asemel HTTPS serveri aadressi.",
|
||||
"server.connect.camera.insecure": "QR skannimine nõuab selle lehe avamist HTTPS või localhost kaudu.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Selle brauseri jaoks pole kaamerat saadaval. Sisestage ühenduse üksikasjad käsitsi.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -795,6 +795,7 @@ export const dict = {
|
||||
"server.connect.button": "اتصال",
|
||||
"server.connect.address.invalid": "یک نشانی معتبر سرور HTTP یا HTTPS وارد کنید.",
|
||||
"server.connect.failed": "اتصال ممکن نشد. نشانی سرور و گذرواژه را بررسی کنید و دوباره تلاش کنید.",
|
||||
"server.connect.pair.description": "برای دریافت جزئیات اتصال، این فرمان را روی رایانهٔ خود اجرا کنید.",
|
||||
"server.connect.scan": "پویش کد QR",
|
||||
"server.connect.scan.description": "دوربین را بهسمت کد QR نمایشدادهشده توسط opencode pair بگیرید.",
|
||||
"server.connect.scan.invalid": "این کد جفتسازی OpenCode نیست. کدی را پویش کنید که opencode pair نشان میدهد.",
|
||||
@@ -802,6 +803,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "در حال باز کردن دوربین…",
|
||||
"server.connect.mixedContent":
|
||||
"اتصال به این سرور HTTP از صفحهٔ HTTPS ممکن نشد. بهجای آن از نشانی سرور HTTPS استفاده کنید.",
|
||||
"server.connect.camera.insecure": "پویش QR نیازمند باز کردن این صفحه از طریق HTTPS یا روی localhost است.",
|
||||
"server.connect.camera.unavailable": "هیچ دوربینی برای این مرورگر در دسترس نیست. جزئیات اتصال را دستی وارد کنید.",
|
||||
"server.connect.camera.error":
|
||||
"باز کردن دوربین ممکن نشد. دسترسی دوربین را مجاز کنید یا جزئیات اتصال را دستی وارد کنید.",
|
||||
|
||||
@@ -23,6 +23,7 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Anna kelvollinen HTTP- tai HTTPS-palvelinosoite.",
|
||||
"server.connect.failed":
|
||||
"Yhteyttä ei voitu muodostaa. Tarkista palvelimen osoite ja salasana ja yritä sitten uudelleen.",
|
||||
"server.connect.pair.description": "Suorita tämä komento tietokoneellasi saadaksesi yhteystiedot.",
|
||||
"server.connect.scan": "Skannaa QR-koodi",
|
||||
"server.connect.scan.description": "Suuntaa kamerasi QR-koodiin, joka näkyy opencode pair:ssä.",
|
||||
"server.connect.scan.invalid": "Tämä ei ole OpenCode-pariliitoskoodi. Skannaa opencode pair:n näyttämä koodi.",
|
||||
@@ -30,6 +31,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Avataan kameraa…",
|
||||
"server.connect.mixedContent":
|
||||
"Yhteyttä tähän HTTP-palvelimeen ei voitu muodostaa HTTPS-sivulta. Käytä sen sijaan HTTPS-palvelinosoitetta.",
|
||||
"server.connect.camera.insecure": "QR-skannaus vaatii tämän sivun avaamisen HTTPS:n kautta tai localhostissa.",
|
||||
"server.connect.camera.unavailable": "Kamera ei ole saatavilla tälle selaimelle. Syötä yhteystietosi manuaalisesti.",
|
||||
"server.connect.camera.error": "Kameraa ei voitu avata. Salli kameran käyttö tai anna yhteystietosi manuaalisesti.",
|
||||
"dialog.server.menu.remove": "Poista",
|
||||
|
||||
@@ -779,6 +779,7 @@ export const dict = {
|
||||
"server.connect.button": "Samband",
|
||||
"server.connect.address.invalid": "Skriva eina gylduga HTTP ella HTTPS ambætaraadressu.",
|
||||
"server.connect.failed": "Fekk ikki samband. Kanna ambætaraadressuna og loyniorðið, og royn síðani aftur.",
|
||||
"server.connect.pair.description": "Koyr hesa skipanina á tínari teldu fyri at fáa tínar sambandsupplýsingar.",
|
||||
"server.connect.scan": "Skanna QR-kotu",
|
||||
"server.connect.scan.description": "Peika myndatólið á QR-kotuna, sum opencode pair vísir.",
|
||||
"server.connect.scan.invalid": "Hetta er ikki ein OpenCode paringarkoda. Skanna kotuna, sum opencode pair vísir.",
|
||||
@@ -786,6 +787,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Opna myndatól...",
|
||||
"server.connect.mixedContent":
|
||||
"Kundi ikki seta samband við hendan HTTP ambætaran frá eini HTTPS síðu. Brúka eina HTTPS ambætaraadressu ístaðin.",
|
||||
"server.connect.camera.insecure": "QR-skanning krevur, at tú letur hesa síðuna upp yvir HTTPS ella á localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Einki myndatól er tøkt til henda kaga. Skriva tínar sambandsupplýsingar manuelt.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -23,6 +23,8 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Entrez une adresse de serveur HTTP ou HTTPS valide.",
|
||||
"server.connect.failed":
|
||||
"Impossible de se connecter. Vérifiez l'adresse et le mot de passe du serveur, puis réessayez.",
|
||||
"server.connect.pair.description":
|
||||
"Exécutez cette commande sur votre ordinateur pour obtenir les détails de votre connexion.",
|
||||
"server.connect.scan": "Scanner le code QR",
|
||||
"server.connect.scan.description": "Pointez votre appareil photo vers le code QR affiché par opencode pair.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -31,6 +33,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Ouverture de la caméra…",
|
||||
"server.connect.mixedContent":
|
||||
"Impossible de se connecter à ce serveur HTTP à partir d'une page HTTPS. Utilisez plutôt une adresse de serveur HTTPS.",
|
||||
"server.connect.camera.insecure": "L'analyse QR nécessite l'ouverture de cette page sur HTTPS ou sur localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Aucune caméra n'est disponible pour ce navigateur. Saisissez manuellement vos informations de connexion.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -793,12 +793,14 @@ export const dict = {
|
||||
"server.connect.button": "התחברות",
|
||||
"server.connect.address.invalid": "יש להזין כתובת שרת HTTP או HTTPS תקינה.",
|
||||
"server.connect.failed": "לא ניתן להתחבר. יש לבדוק את כתובת השרת והסיסמה ולנסות שוב.",
|
||||
"server.connect.pair.description": "יש להריץ פקודה זו במחשב כדי לקבל את פרטי החיבור.",
|
||||
"server.connect.scan": "סריקת קוד QR",
|
||||
"server.connect.scan.description": "יש לכוון את המצלמה לקוד QR שמוצג על ידי opencode pair.",
|
||||
"server.connect.scan.invalid": "זה אינו קוד צימוד של OpenCode. יש לסרוק את הקוד שמוצג על ידי opencode pair.",
|
||||
"server.connect.camera": "מצלמת צימוד",
|
||||
"server.connect.camera.starting": "פותח את המצלמה…",
|
||||
"server.connect.mixedContent": "לא ניתן להתחבר לשרת HTTP זה מדף HTTPS. יש להשתמש בכתובת שרת HTTPS במקום זאת.",
|
||||
"server.connect.camera.insecure": "סריקת QR דורשת פתיחת דף זה דרך HTTPS או ב-localhost.",
|
||||
"server.connect.camera.unavailable": "אין מצלמה זמינה לדפדפן זה. יש להזין את פרטי החיבור ידנית.",
|
||||
"server.connect.camera.error": "לא ניתן לפתוח את המצלמה. יש לאפשר גישה למצלמה או להזין את פרטי החיבור ידנית.",
|
||||
"dialog.server.menu.remove": "הסרה",
|
||||
|
||||
@@ -797,6 +797,7 @@ export const dict = {
|
||||
"server.connect.button": "जोड़ना",
|
||||
"server.connect.address.invalid": "एक वैध HTTP या HTTPS सर्वर पता दर्ज करें।",
|
||||
"server.connect.failed": "कनेक्ट नहीं हो सका। सर्वर पता और पासवर्ड जांचें, फिर पुनः प्रयास करें।",
|
||||
"server.connect.pair.description": "अपना कनेक्शन विवरण प्राप्त करने के लिए इस कमांड को अपने कंप्यूटर पर चलाएँ।",
|
||||
"server.connect.scan": "QR कोड स्कैन करें",
|
||||
"server.connect.scan.description": "अपने कैमरे को ओपनकोड जोड़ी द्वारा दिखाए गए QR कोड पर इंगित करें।",
|
||||
"server.connect.scan.invalid": "यह कोई OpenCode युग्मन कोड नहीं है. ओपनकोड जोड़ी द्वारा दिखाए गए कोड को स्कैन करें।",
|
||||
@@ -804,6 +805,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "कैमरा खुल रहा है...",
|
||||
"server.connect.mixedContent":
|
||||
"किसी HTTPS पेज से इस HTTP सर्वर से कनेक्ट नहीं किया जा सका। इसके बजाय HTTPS सर्वर पते का उपयोग करें।",
|
||||
"server.connect.camera.insecure": "QR स्कैनिंग के लिए इस पृष्ठ को HTTPS या लोकलहोस्ट पर खोलने की आवश्यकता है।",
|
||||
"server.connect.camera.unavailable":
|
||||
"इस ब्राउज़र पर कोई कैमरा उपलब्ध नहीं है. अपना कनेक्शन विवरण मैन्युअल रूप से दर्ज करें।",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -805,6 +805,7 @@ export const dict = {
|
||||
"server.connect.button": "Poveži se",
|
||||
"server.connect.address.invalid": "Unesite ispravnu HTTP ili HTTPS adresu poslužitelja.",
|
||||
"server.connect.failed": "Povezivanje nije uspjelo. Provjerite adresu poslužitelja i lozinku, pa pokušajte ponovno.",
|
||||
"server.connect.pair.description": "Pokrenite ovu naredbu na računalu da biste dobili podatke za povezivanje.",
|
||||
"server.connect.scan": "Skeniraj QR kod",
|
||||
"server.connect.scan.description": "Usmjerite kameru prema QR kodu koji prikazuje opencode pair.",
|
||||
"server.connect.scan.invalid": "Ovo nije OpenCode kod za uparivanje. Skenirajte kod koji prikazuje opencode pair.",
|
||||
@@ -812,6 +813,8 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Otvaranje kamere…",
|
||||
"server.connect.mixedContent":
|
||||
"Nije se moguće povezati s ovim HTTP poslužiteljem s HTTPS stranice. Umjesto toga koristite HTTPS adresu poslužitelja.",
|
||||
"server.connect.camera.insecure":
|
||||
"Za skeniranje QR koda ovu stranicu morate otvoriti putem HTTPS-a ili na localračunalu domaćinu.",
|
||||
"server.connect.camera.unavailable": "Ovom pregledniku nije dostupna kamera. Ručno unesite podatke za povezivanje.",
|
||||
"server.connect.camera.error":
|
||||
"Kameru nije bilo moguće otvoriti. Dozvolite pristup kameri ili ručno unesite podatke za povezivanje.",
|
||||
|
||||
@@ -794,6 +794,8 @@ export const dict = {
|
||||
"server.connect.button": "Csatlakozás",
|
||||
"server.connect.address.invalid": "Adjon meg egy érvényes HTTP vagy HTTPS szervercímet.",
|
||||
"server.connect.failed": "Nem sikerült csatlakozni. Ellenőrizze a szerver címét és jelszavát, majd próbálja újra.",
|
||||
"server.connect.pair.description":
|
||||
"Futtassa ezt a parancsot a számítógépén a kapcsolat részleteinek megtekintéséhez.",
|
||||
"server.connect.scan": "QR-kód beolvasása",
|
||||
"server.connect.scan.description": "Irányítsa kameráját a opencode pair által mutatott QR kódra.",
|
||||
"server.connect.scan.invalid": "Ez nem egy OpenCode párosítási kód. Olvassa be a opencode pair által mutatott kódot.",
|
||||
@@ -801,6 +803,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Kamera nyitása…",
|
||||
"server.connect.mixedContent":
|
||||
"Nem sikerült csatlakozni ehhez a HTTP szerverhez egy HTTPS oldalról. Használjon helyette HTTPS szervercímet.",
|
||||
"server.connect.camera.insecure": "A QR beolvasásához ezt az oldalt a HTTPS vagy a localhost oldalon kell megnyitni.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Ehhez a böngészőhöz nem érhető el kamera. Adja meg manuálisan a kapcsolat adatait.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -795,12 +795,14 @@ export const dict = {
|
||||
"server.connect.button": "Կապվել",
|
||||
"server.connect.address.invalid": "Մուտքագրեք վավեր HTTP կամ HTTPS սերվերի հասցե։",
|
||||
"server.connect.failed": "Չկապվեց. Ստուգեք սերվերի հասցեն և գաղտնաբառը, ապա փորձեք նորից։",
|
||||
"server.connect.pair.description": "Գործարկեք այս հրամանը ձեր համակարգչում՝ ստանալու ձեր կապի մանրամասները։",
|
||||
"server.connect.scan": "Սկանավորեք QR կոդը",
|
||||
"server.connect.scan.description": "Տեսախցիկն ուղղեք opencode pair-ի ցուցադրած QR կոդին։",
|
||||
"server.connect.scan.invalid": "Սա OpenCode-ի զուգակցման կոդ չէ։ Սկանավորեք opencode pair-ի ցուցադրած կոդը։",
|
||||
"server.connect.camera": "Զույգագրման վահանակը",
|
||||
"server.connect.camera.starting": "Բացվում է վահանակը…",
|
||||
"server.connect.mixedContent": "Այս HTTP սերվերին հնարավոր չէ միանալ HTTPS էջից։ Օգտագործեք HTTPS սերվերի հասցե։",
|
||||
"server.connect.camera.insecure": "QR սկանումը պահանջում է այս էջը բացել HTTPS-ով կամ տեղական հոստում։",
|
||||
"server.connect.camera.unavailable": "Այս զննարկչին վահանակ հասանելի չէ։ Մուտքագրեք ձեր կապի մանրամասները ձեռքով։",
|
||||
"server.connect.camera.error":
|
||||
"Չկարողացա բացել տեսախցիկը։ Թույլատրել տեսախցիկի մուտքը կամ ձեռքով ներմուծել ձեր կապի տվյալները։",
|
||||
|
||||
@@ -862,6 +862,7 @@ export const dict = {
|
||||
"server.connect.button": "Sambungkan",
|
||||
"server.connect.address.invalid": "Masukkan alamat server HTTP atau HTTPS yang valid.",
|
||||
"server.connect.failed": "Tidak dapat terhubung. Periksa alamat server dan kata sandi, lalu coba lagi.",
|
||||
"server.connect.pair.description": "Jalankan perintah ini di komputer Anda untuk mendapatkan detail koneksi Anda.",
|
||||
"server.connect.scan": "Pindai kode QR",
|
||||
"server.connect.scan.description": "Arahkan kamera Anda ke kode QR yang ditampilkan oleh opencode pair.",
|
||||
"server.connect.scan.invalid": "Ini bukan kode pemasangan OpenCode. Pindai kode yang ditampilkan oleh opencode pair.",
|
||||
@@ -869,6 +870,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Membuka kamera…",
|
||||
"server.connect.mixedContent":
|
||||
"Tidak dapat terhubung ke server HTTP ini dari halaman HTTPS. Gunakan alamat server HTTPS sebagai gantinya.",
|
||||
"server.connect.camera.insecure": "Pemindaian QR memerlukan halaman ini dibuka melalui HTTPS atau di localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Tidak ada kamera yang tersedia untuk peramban ini. Masukkan detail koneksi Anda secara manual.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -782,6 +782,7 @@ export const dict = {
|
||||
"server.connect.button": "Tengdu",
|
||||
"server.connect.address.invalid": "Sláðu inn gilt HTTP eða HTTPS netfang netþjóns.",
|
||||
"server.connect.failed": "Ekki tókst að tengjast. Athugaðu netfang netþjóns og lykilorð, reyndu svo aftur.",
|
||||
"server.connect.pair.description": "Keyrðu þessa skipun á tölvunni þinni til að fá upplýsingar um tenginguna þína.",
|
||||
"server.connect.scan": "Skanna QR kóða",
|
||||
"server.connect.scan.description": "Beindu myndavélinni þinni að QR kóðanum sem opencode pair sýnir.",
|
||||
"server.connect.scan.invalid": "Þetta er ekki OpenCode pörunarkóði. Skannaðu kóðann sem opencode pair sýnir.",
|
||||
@@ -789,6 +790,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Opnar myndavél...",
|
||||
"server.connect.mixedContent":
|
||||
"Gat ekki tengst þessum HTTP netþjóni frá HTTPS síðu. Notaðu HTTPS netþjónsfang í staðinn.",
|
||||
"server.connect.camera.insecure": "QR skönnun krefst þess að opna þessa síðu yfir HTTPS eða á localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Engin myndavél er í boði fyrir þennan vafra. Sláðu inn tengiupplýsingar þínar handvirkt.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -22,6 +22,8 @@ export const dict = {
|
||||
"server.connect.button": "Connettiti",
|
||||
"server.connect.address.invalid": "Inserisci un indirizzo server HTTP o HTTPS valido.",
|
||||
"server.connect.failed": "Impossibile connettersi. Controlla l'indirizzo del server e la password, quindi riprova.",
|
||||
"server.connect.pair.description":
|
||||
"Esegui questo comando sul tuo computer per ottenere i dettagli della connessione.",
|
||||
"server.connect.scan": "Scansiona il codice QR",
|
||||
"server.connect.scan.description": "Punta la fotocamera sul codice QR mostrato da opencode pair.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -30,6 +32,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Apertura della fotocamera…",
|
||||
"server.connect.mixedContent":
|
||||
"Impossibile connettersi a questo server HTTP da una pagina HTTPS. Utilizza invece un indirizzo server HTTPS.",
|
||||
"server.connect.camera.insecure": "La scansione QR richiede l'apertura di questa pagina su HTTPS o su localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Nessuna fotocamera è disponibile per questo browser. Inserisci manualmente i dettagli della tua connessione.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -808,6 +808,7 @@ export const dict = {
|
||||
"server.connect.button": "接続",
|
||||
"server.connect.address.invalid": "有効なHTTPまたはHTTPSサーバーアドレスを入力してください。",
|
||||
"server.connect.failed": "接続できませんでした。サーバーアドレスとパスワードを確認して、もう一度お試しください。",
|
||||
"server.connect.pair.description": "このコマンドをコンピューターで実行して接続情報を取得してください。",
|
||||
"server.connect.scan": "QRコードをスキャン",
|
||||
"server.connect.scan.description": "opencode pair に表示されたQRコードにカメラを向けてください。",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -816,6 +817,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "カメラを開いています…",
|
||||
"server.connect.mixedContent":
|
||||
"HTTPSページからこのHTTPサーバーに接続できません。代わりにHTTPSサーバーのアドレスを使用してください。",
|
||||
"server.connect.camera.insecure": "QRスキャンには、このページをHTTPSで開くか、ローカルホストで開く必要があります。",
|
||||
"server.connect.camera.unavailable": "このブラウザで使用可能なカメラがありません。接続情報を手動で入力してください。",
|
||||
"server.connect.camera.error":
|
||||
"カメラを開くことができません。カメラへのアクセスを許可するか、接続情報を手動で入力してください。",
|
||||
|
||||
@@ -791,6 +791,7 @@ export const dict = {
|
||||
"server.connect.button": "დაკავშირება",
|
||||
"server.connect.address.invalid": "შეიყვანეთ ვალიდური HTTP ან HTTPS სერვერის მისამართი.",
|
||||
"server.connect.failed": "ვერ მოხერხდა დაკავშირება. შეამოწმეთ სერვერის მისამართი და პაროლი, შემდეგ კვლავ სცადეთ.",
|
||||
"server.connect.pair.description": "ჩაატარეთ ეს ბრძანება თქვენს კომპიუტერზე, რათა მიიღოთ თქვენი კავშირის დეტალები.",
|
||||
"server.connect.scan": "სკანირება QR კოდით",
|
||||
"server.connect.scan.description": "მიმართეთ კამერა opencode pair-ის მიერ ნაჩვენებ QR კოდზე.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -799,6 +800,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "კამერის გახსნა…",
|
||||
"server.connect.mixedContent":
|
||||
"ვერ მოხერხდა დაკავშირება ამ HTTP სერვერთან HTTPS გვერდიდან. გამოიყენეთ HTTPS სერვერის მისამართი.",
|
||||
"server.connect.camera.insecure": "QR სკანირება საჭიროებს ამ გვერდის გახსნა HTTPS-ზე ან localhost-ზე.",
|
||||
"server.connect.camera.unavailable":
|
||||
"ამ ბრაუზერისთვის კამერა არ არის ხელმისაწვდომი. შეიყვანეთ თქვენი კავშირის დეტალები ხელით.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -791,6 +791,7 @@ export const dict = {
|
||||
"server.connect.button": "ភ្ជាប់",
|
||||
"server.connect.address.invalid": "បញ្ចូលអាសយដ្ឋានម៉ាស៊ីនមេ HTTP ឬ HTTPS ត្រឹមត្រូវ។",
|
||||
"server.connect.failed": "មិនអាចភ្ជាប់បានទេ។ សូមពិនិត្យអាសយដ្ឋានម៉ាស៊ីនមេ និងពាក្យសម្ងាត់ រួចព្យាយាមម្តងទៀត។",
|
||||
"server.connect.pair.description": "បើកកម្មង់នេះនៅលើកុំព្យូទ័ររបស់អ្នកដើម្បីទទួលព័ត៌មានការភ្ជាប់របស់អ្នក។",
|
||||
"server.connect.scan": "ស្កេនកូដ QR",
|
||||
"server.connect.scan.description": "បង្ខំកាមេរ៉ារបស់អ្នកទៅកូដ QR ដែលបង្ហាញដោយ opencode pair។",
|
||||
"server.connect.scan.invalid": "នេះមិនមែនជាកូដផ្គុំ OpenCode ទេ។ ស្កេនកូដដែលបង្ហាញដោយ opencode pair។",
|
||||
@@ -798,6 +799,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "កំពុងបើកកាមេរ៉ា…",
|
||||
"server.connect.mixedContent":
|
||||
"មិនអាចភ្ជាប់ទៅម៉ាស៊ីនបម្រើ HTTP នេះពីទំព័រ HTTPS។ សូមប្រើអាសយដ្ឋានម៉ាស៊ីនបម្រើ HTTPS ជំនួស។",
|
||||
"server.connect.camera.insecure": "ការស្កេន QR ត្រូវការបើកទំព័រនេះតាម HTTPS ឬនៅលើ localhost។",
|
||||
"server.connect.camera.unavailable":
|
||||
"មិនមានកាមេរ៉ាដើម្បីប្រើប្រាស់ជាមួយកម្មវិធីនេះទេ។ សូមបញ្ចូលព័ត៌មានការភ្ជាប់ដោយដៃ។",
|
||||
"server.connect.camera.error": "មិនអាចបើកកាមេរ៉ាបាន។ អនុញ្ញាតឱ្យចូលដំណើរការកាមេរ៉ា ឬបញ្ចូលព័ត៌មានការភ្ជាប់ដោយដៃ។",
|
||||
|
||||
@@ -803,6 +803,7 @@ export const dict = {
|
||||
"server.connect.button": "연결",
|
||||
"server.connect.address.invalid": "유효한 HTTP 또는 HTTPS 서버 주소를 입력하세요.",
|
||||
"server.connect.failed": "연결할 수 없습니다. 서버 주소와 비밀번호를 확인한 후 다시 시도하세요.",
|
||||
"server.connect.pair.description": "컴퓨터에서 이 명령어를 실행하여 연결 세부 정보를 확인하세요.",
|
||||
"server.connect.scan": "QR 코드 스캔",
|
||||
"server.connect.scan.description": "opencode pair에서 표시된 QR 코드를 카메라로 가리키세요.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -811,6 +812,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "카메라 열기…",
|
||||
"server.connect.mixedContent":
|
||||
"HTTPS 페이지에서 이 HTTP 서버에 연결할 수 없습니다. 대신 HTTPS 서버 주소를 사용하세요.",
|
||||
"server.connect.camera.insecure": "QR 스캔을 위해서는 HTTPS를 통해 이 페이지를 열거나 로컬호스트에서 열어야 합니다.",
|
||||
"server.connect.camera.unavailable":
|
||||
"이 브라우저에서 사용할 수 있는 카메라가 없습니다. 연결 세부 정보를 수동으로 입력하세요.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -790,12 +790,14 @@ export const dict = {
|
||||
"server.connect.button": "ເຊື່ອມຕໍ່",
|
||||
"server.connect.address.invalid": "ໃສ່ທີ່ຢູ່ເຊີບເວີ HTTP ຫຼື HTTPS ທີ່ຖືກຕ້ອງ.",
|
||||
"server.connect.failed": "ບໍ່ສາມາດເຊື່ອມຕໍ່ໄດ້.ກວດເບິ່ງທີ່ຢູ່ເຊີບເວີແລະລະຫັດຜ່ານ, ແລ້ວລອງອີກຄັ້ງ.",
|
||||
"server.connect.pair.description": "ເຮັດຄໍາສັ່ງນີ້ໃນຄອມພິວເຕີຂອງທ່ານເພື່ອຮັບລາຍລະອຽດການເຊື່ອມຕໍ່.",
|
||||
"server.connect.scan": "ສະແກນ QR code",
|
||||
"server.connect.scan.description": "ຊີ້ກ້ອງຂອງທ່ານໄປທີ່ QR code ທີ່ສະແດງໂດຍຄູ່ opencode.",
|
||||
"server.connect.scan.invalid": "ນີ້ບໍ່ແມ່ນລະຫັດການຈັດຄູ່ OpenCode.ສະແກນລະຫັດທີ່ສະແດງໂດຍຄູ່ OpenCode.",
|
||||
"server.connect.camera": "ການຈັບຄູ່ກ້ອງ",
|
||||
"server.connect.camera.starting": "ກໍາລັງເປີດກ້ອງ...",
|
||||
"server.connect.mixedContent": "ບໍ່ສາມາດເຊື່ອມຕໍ່ກັບເຊີບເວີ HTTP ນີ້ຈາກຫນ້າ HTTPS ໄດ້.ໃຊ້ທີ່ຢູ່ເຊີບເວີ HTTPS ແທນ.",
|
||||
"server.connect.camera.insecure": "ການສະແກນ QR ຈໍາເປັນຕ້ອງເປີດຫນ້ານີ້ຜ່ານ HTTPS ຫຼືໃນ localhost.",
|
||||
"server.connect.camera.unavailable": "ບໍ່ມີກ້ອງຖ່າຍຮູບໃຫ້ໃຊ້ໃນເບຣາວເຊີນີ້.ໃສ່ລາຍລະອຽດການເຊື່ອມຕໍ່ຂອງທ່ານເອງ.",
|
||||
"server.connect.camera.error": "ບໍ່ສາມາດເປີດກ້ອງໄດ້.ອະນຸຍາດໃຫ້ເຂົ້າເຖິງກ້ອງ ຫຼື ປ້ອນລາຍລະອຽດການເຊື່ອມຕໍ່ຂອງທ່ານເອງ.",
|
||||
"dialog.server.menu.remove": "ລຶບ",
|
||||
|
||||
@@ -805,6 +805,8 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Įveskite galiojantį HTTP arba HTTPS serverio adresą.",
|
||||
"server.connect.failed":
|
||||
"Nepavyko prisijungti. Patikrinkite serverio adresą ir slaptažodį, tada bandykite dar kartą.",
|
||||
"server.connect.pair.description":
|
||||
"Paleiskite šią komandą savo kompiuteryje, kad gautumėte išsamią ryšio informaciją.",
|
||||
"server.connect.scan": "Nuskaitykite QR kodą",
|
||||
"server.connect.scan.description": "Nukreipkite fotoaparatą į QR kodą, rodomą opencode pair.",
|
||||
"server.connect.scan.invalid": "Tai nėra OpenCode susiejimo kodas. Nuskaitykite kodą, kurį rodo opencode pair.",
|
||||
@@ -812,6 +814,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Atidaroma kamera…",
|
||||
"server.connect.mixedContent":
|
||||
"Nepavyko prisijungti prie šio HTTP serverio iš HTTPS puslapio. Vietoj to naudokite HTTPS serverio adresą.",
|
||||
"server.connect.camera.insecure": "Norint nuskaityti QR, reikia atidaryti šį puslapį per HTTPS arba localhost.",
|
||||
"server.connect.camera.unavailable": "Šioje naršyklėje nėra fotoaparato. Įveskite savo ryšio duomenis rankiniu būdu.",
|
||||
"server.connect.camera.error":
|
||||
"Nepavyko atidaryti fotoaparato. Suteikite prieigą prie fotoaparato arba įveskite savo ryšio duomenis rankiniu būdu.",
|
||||
|
||||
@@ -792,6 +792,7 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Ievadiet derīgu HTTP vai HTTPS servera adresi.",
|
||||
"server.connect.failed":
|
||||
"Nevarēja izveidot savienojumu. Pārbaudiet servera adresi un paroli, pēc tam mēģiniet vēlreiz.",
|
||||
"server.connect.pair.description": "Palaidiet šo komandu datorā, lai iegūtu informāciju par savienojumu.",
|
||||
"server.connect.scan": "Skenējiet QR kodu",
|
||||
"server.connect.scan.description": "Pavērsiet kameru pret QR kodu, ko parāda opencode pair.",
|
||||
"server.connect.scan.invalid": "Šis nav kods OpenCode savienošanai pārī. Skenējiet kodu, ko parāda opencode pair.",
|
||||
@@ -799,6 +800,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Tiek atvērta kamera…",
|
||||
"server.connect.mixedContent":
|
||||
"Nevarēja izveidot savienojumu ar šo HTTP serveri no HTTPS lapas. Tā vietā izmantojiet HTTPS servera adresi.",
|
||||
"server.connect.camera.insecure": "Lai skenētu QR, šī lapa ir jāatver, izmantojot HTTPS vai localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Šai pārlūkprogrammai nav pieejama neviena kamera. Manuāli ievadiet savienojuma informāciju.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -795,6 +795,8 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Внесете валидна HTTP или HTTPS адреса на серверот.",
|
||||
"server.connect.failed":
|
||||
"Не можеше да се поврзе. Проверете ја адресата на серверот и лозинката, потоа обидете се повторно.",
|
||||
"server.connect.pair.description":
|
||||
"Извршете ја оваа команда на вашиот компјутер за да ги добиете деталите за вашата конекција.",
|
||||
"server.connect.scan": "Скенирај QR код",
|
||||
"server.connect.scan.description": "Насочете ја вашата камера кон QR кодот прикажан од opencode pair.",
|
||||
"server.connect.scan.invalid": "Ова не е OpenCode паринг код. Скенирајте го кодот прикажан од opencode pair.",
|
||||
@@ -802,6 +804,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Отворање на камерата…",
|
||||
"server.connect.mixedContent":
|
||||
"Не можевте да се поврзете на овој HTTP сервер од HTTPS страница. Користете HTTPS сервер адреса наместо тоа.",
|
||||
"server.connect.camera.insecure": "Скенирањето на QR бара отворање на оваа страница преку HTTPS или на localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Нема камера достапна за овој прелистувач. Внесете ги деталите за вашата конекција рачно.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -797,6 +797,8 @@ export const dict = {
|
||||
"server.connect.button": "Холбох",
|
||||
"server.connect.address.invalid": "Хүчинтэй HTTP эсвэл HTTPS серверийн хаягийг оруулна уу.",
|
||||
"server.connect.failed": "Холбогдож чадсангүй. Серверийн хаяг ба нууц үгийг шалгаад дахин оролдоно уу.",
|
||||
"server.connect.pair.description":
|
||||
"Энэ командыг өөрийн компьютер дээр ажиллуулж, холболтын дэлгэрэнгүй мэдээллийг аваарай.",
|
||||
"server.connect.scan": "QR кодыг сканнердах",
|
||||
"server.connect.scan.description": "Камераа opencode pair-ийн харуулсан QR код руу чиглүүлнэ үү.",
|
||||
"server.connect.scan.invalid": "Энэ OpenCode холболтын код биш. opencode pair-ийн харуулсан кодыг уншуулна уу.",
|
||||
@@ -804,6 +806,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Камер нээж байна…",
|
||||
"server.connect.mixedContent":
|
||||
"HTTPS хуудсаас энэ HTTP серверт холбогдож чадсангүй. Үүний оронд HTTPS серверийн хаягийг ашигла.",
|
||||
"server.connect.camera.insecure": "QR сканнердах нь энэ хуудасыг HTTPS дээр эсвэл localhost дээр нээх шаардлагатай.",
|
||||
"server.connect.camera.unavailable": "Энэ хөтчид камер байхгүй байна. Холболтын мэдээллээ гар аргаар оруулна уу.",
|
||||
"server.connect.camera.error":
|
||||
"Камерыг нээж чадсангүй. Камер ашиглахыг зөвшөөрөх эсвэл холболтын дэлгэрэнгүй мэдээллээ гараар оруулна уу.",
|
||||
|
||||
@@ -795,6 +795,7 @@ export const dict = {
|
||||
"server.connect.button": "Sambung",
|
||||
"server.connect.address.invalid": "Masukkan alamat pelayan HTTP atau HTTPS yang sah.",
|
||||
"server.connect.failed": "Tidak dapat menyambung. Semak alamat pelayan dan kata laluan, kemudian cuba lagi.",
|
||||
"server.connect.pair.description": "Jalankan arahan ini pada komputer anda untuk mendapatkan butiran sambungan anda.",
|
||||
"server.connect.scan": "Imbas kod QR",
|
||||
"server.connect.scan.description": "Tujukan kamera anda ke kod QR yang dipaparkan oleh pasangan opencode.",
|
||||
"server.connect.scan.invalid": "Ini bukan kod pasangan OpenCode. Imbas kod yang ditunjukkan oleh pasangan opencode.",
|
||||
@@ -802,6 +803,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Membuka kamera…",
|
||||
"server.connect.mixedContent":
|
||||
"Tidak dapat sambung ke pelayan HTTP ini dari halaman HTTPS. Gunakan alamat pelayan HTTPS sebaliknya.",
|
||||
"server.connect.camera.insecure": "Imbasan QR memerlukan pembukaan halaman ini melalui HTTPS atau di localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Tiada kamera tersedia untuk pelayar ini. Masukkan butiran sambungan anda secara manual.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -808,6 +808,7 @@ export const dict = {
|
||||
"server.connect.button": "ချိတ်ဆက်မည်",
|
||||
"server.connect.address.invalid": "မှန်ကန်သော HTTP သို့မဟုတ် HTTPS server လိပ်စာကို ထည့်ပါ။",
|
||||
"server.connect.failed": "ချိတ်ဆက်၍ မရပါ။ Server လိပ်စာနှင့် စကားဝှက်ကို စစ်ဆေးပြီး ပြန်ကြိုးစားပါ။",
|
||||
"server.connect.pair.description": "ချိတ်ဆက်ရန်အသေးစိတ်ကို ရယူရန် သင့်ကွန်ပျူတာတွင် ဤ command ကို chạy ဆောင်ရန်။",
|
||||
"server.connect.scan": "QR ကုဒ် စကင်",
|
||||
"server.connect.scan.description": "opencode pair မှ ပြသထားသော QR ကုဒ်ကို သင်၏ ကင်မရာဖြင့် ညွှန်ပါ။",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -815,6 +816,8 @@ export const dict = {
|
||||
"server.connect.camera": "ကင်မရာကို အစုံပေါင်းခြင်း",
|
||||
"server.connect.camera.starting": "ကင်မရာဖွင့်နေသည်…",
|
||||
"server.connect.mixedContent": "HTTPS စာမျက်နှာမှ ဤ HTTP ဆာဗာထံ ချိတ်ဆက်၍ မရနိုင်ပါ။ HTTPS ဆာဗာလိပ်စာကို အသုံးပြုပါ။",
|
||||
"server.connect.camera.insecure":
|
||||
"QR စကင်လုပ်ရန် HTTPS ဖြင့် သို့မဟုတ် localhost တွင် ဤစာမျက်နှာကို ဖွင့်ရန် လိုအပ်သည်။",
|
||||
"server.connect.camera.unavailable": "ဤဘရောက်ဇာတွင် ကင်မရာမရှိပါ။ ချိတ်ဆက်မှုအချက်အလက်များကို လက်ဖြင့်ထည့်ပါ။",
|
||||
"server.connect.camera.error":
|
||||
"ကင်မရာဖွင့်၍ မရနိုင်ပါ။ ကင်မရာခွင့်ပြုချက်ပေးပါ သို့မဟုတ် ချိတ်ဆက်မှုအချက်အလက်များကို လက်ဖြင့်ထည့်ပါ။",
|
||||
|
||||
@@ -797,6 +797,7 @@ export const dict = {
|
||||
"server.connect.button": "जडान गर्नुहोस्",
|
||||
"server.connect.address.invalid": "मान्य HTTP वा HTTPS सर्भर ठेगाना प्रविष्ट गर्नुहोस्।",
|
||||
"server.connect.failed": "जडान हुन सकेन। सर्भर ठेगाना र पासवर्ड जाँच गर्नुहोस्, त्यसपछि फेरि प्रयास गर्नुहोस्।",
|
||||
"server.connect.pair.description": "तपाइँको जडान विवरण प्राप्त गर्न तपाइँको कम्प्युटरमा यो आदेश चलाउनुहोस्।",
|
||||
"server.connect.scan": "QR कोड स्क्यान गर्नुहोस्",
|
||||
"server.connect.scan.description": "ओपनकोड जोडीद्वारा देखाइएको QR कोडमा आफ्नो क्यामेरा देखाउनुहोस्।",
|
||||
"server.connect.scan.invalid": "यो एक OpenCode जोडा कोड होइन। ओपनकोड जोडी द्वारा देखाइएको कोड स्क्यान गर्नुहोस्।",
|
||||
@@ -804,6 +805,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "क्यामेरा खोल्दै…",
|
||||
"server.connect.mixedContent":
|
||||
"HTTPS पृष्ठबाट यो HTTP सर्भरमा जडान गर्न सकिएन। यसको सट्टा HTTPS सर्भर ठेगाना प्रयोग गर्नुहोस्।",
|
||||
"server.connect.camera.insecure": "QR स्क्यानिङका लागि यो पृष्ठ HTTPS वा लोकलहोस्टमा खोल्न आवश्यक छ।",
|
||||
"server.connect.camera.unavailable":
|
||||
"यो ब्राउजरमा कुनै क्यामेरा उपलब्ध छैन। आफ्नो जडान विवरणहरू म्यानुअल रूपमा प्रविष्ट गर्नुहोस्।",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -23,6 +23,7 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Voer een geldig HTTP- of HTTPS-serveradres in.",
|
||||
"server.connect.failed":
|
||||
"Kan geen verbinding maken. Controleer het serveradres en het wachtwoord en probeer het opnieuw.",
|
||||
"server.connect.pair.description": "Voer deze opdracht uit op uw computer om uw verbindingsgegevens op te halen.",
|
||||
"server.connect.scan": "Scan de QR-code",
|
||||
"server.connect.scan.description": "Richt uw camera op de QR-code weergegeven door opencode pair.",
|
||||
"server.connect.scan.invalid": "Dit is geen OpenCode-koppelingscode. Scan de code weergegeven door opencode pair.",
|
||||
@@ -30,6 +31,8 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Camera openen…",
|
||||
"server.connect.mixedContent":
|
||||
"Kan geen verbinding maken met deze HTTP-server vanaf een HTTPS-pagina. Gebruik in plaats daarvan een HTTPS-serveradres.",
|
||||
"server.connect.camera.insecure":
|
||||
"Voor het scannen van QR moet deze pagina worden geopend via HTTPS of op localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Er is geen camera beschikbaar voor deze browser. Voer uw verbindingsgegevens handmatig in.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -26,6 +26,7 @@ export const dict = {
|
||||
"server.connect.button": "Koble til",
|
||||
"server.connect.address.invalid": "Skriv inn en gyldig HTTP- eller HTTPS-serveradresse.",
|
||||
"server.connect.failed": "Kunne ikke koble til. Sjekk serveradressen og passordet, og prøv på nytt.",
|
||||
"server.connect.pair.description": "Kjør denne kommandoen på datamaskinen for å få tilkoblingsdetaljer.",
|
||||
"server.connect.scan": "Skann QR-koden",
|
||||
"server.connect.scan.description": "Pek kameraet mot QR-koden vist av opencode pair.",
|
||||
"server.connect.scan.invalid": "Dette er ikke en OpenCode-paringskode. Skann koden vist av opencode pair.",
|
||||
@@ -33,6 +34,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Åpner kamera …",
|
||||
"server.connect.mixedContent":
|
||||
"Kunne ikke koble til denne HTTP-serveren fra en HTTPS-side. Bruk en HTTPS-serveradresse i stedet.",
|
||||
"server.connect.camera.insecure": "QR-skanning krever at denne siden åpnes over HTTPS eller på localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Ingen kamera er tilgjengelig for denne nettleseren. Skriv inn tilkoblingsdetaljene manuelt.",
|
||||
"server.connect.camera.error": "Kunne ikke åpne kameraet. Gi kameratilgang eller angi tilkoblingsdetaljene manuelt.",
|
||||
|
||||
@@ -803,6 +803,7 @@ export const dict = {
|
||||
"server.connect.button": "جڑیں۔",
|
||||
"server.connect.address.invalid": "ایک درست HTTP یا HTTPS سرور دا پتہ درج کرو۔",
|
||||
"server.connect.failed": "رابطہ نئیں ہو سکا۔ سرور دا پتہ اور پاس ورڈ چیک کرو، پھر دوبارہ کوشش کرو۔",
|
||||
"server.connect.pair.description": "اپنے کنکشن دی تفصیلات حاصل کرنے لئی اس کمانڈ کو اپنے کمپیوٹر پر چلائیں۔",
|
||||
"server.connect.scan": "QR کوڈ اسکین کرو۔",
|
||||
"server.connect.scan.description": "اپنے کیمرے کو اوپن کوڈ پیئر دے ذریعے دکھائے گئے QR کوڈ دی طرف پوائنٹ کرو۔",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -811,6 +812,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "کیمرہ کھل رہا ہے…",
|
||||
"server.connect.mixedContent":
|
||||
"ایک HTTPS صفحہ سے اس HTTP سرور سے مربوط نئیں ہو سکا۔ اس دے بجائے ایک HTTPS سرور دا پتہ استعمال کرو۔",
|
||||
"server.connect.camera.insecure": "QR اسکیننگ لئی اس صفحہ کو HTTPS پر یا لوکل ہوسٹ پر کھولنے دی ضرورت اے۔",
|
||||
"server.connect.camera.unavailable":
|
||||
"اس براؤزر پر کوئی کیمرہ دستیاب نئیں اے۔ اپنے کنکشن دی تفصیلات دستی طور پر درج کرو۔",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -820,6 +820,7 @@ export const dict = {
|
||||
"server.connect.button": "Połącz",
|
||||
"server.connect.address.invalid": "Wprowadź prawidłowy adres serwera HTTP lub HTTPS.",
|
||||
"server.connect.failed": "Nie można się połączyć. Sprawdź adres serwera i hasło, a następnie spróbuj ponownie.",
|
||||
"server.connect.pair.description": "Uruchom to polecenie na swoim komputerze, aby uzyskać szczegóły połączenia.",
|
||||
"server.connect.scan": "Zeskanuj kod QR",
|
||||
"server.connect.scan.description": "Skieruj kamerę na kod QR pokazany przez sparowanie opencode.",
|
||||
"server.connect.scan.invalid": "To nie jest kod parowania OpenCode. Zeskanuj kod pokazany przez sparowanie opencode.",
|
||||
@@ -827,6 +828,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Otwieranie kamery…",
|
||||
"server.connect.mixedContent":
|
||||
"Nie można połączyć się z tym serwerem HTTP z strony HTTPS. Zamiast tego użyj adresu serwera HTTPS.",
|
||||
"server.connect.camera.insecure": "Skanowanie QR wymaga otwarcia tej strony przez HTTPS lub na localhost.",
|
||||
"server.connect.camera.unavailable": "Ta przeglądarka nie ma dostępu do kamery. Wprowadź dane połączenia ręcznie.",
|
||||
"server.connect.camera.error":
|
||||
"Nie udało się otworzyć kamery. Zezwól na dostęp do kamery lub wprowadź dane połączenia ręcznie.",
|
||||
|
||||
@@ -790,6 +790,7 @@ export const dict = {
|
||||
"server.connect.button": "Conectați",
|
||||
"server.connect.address.invalid": "Introduceți o adresă validă de server HTTP sau HTTPS.",
|
||||
"server.connect.failed": "Nu s-a putut conecta. Verificați adresa serverului și parola, apoi încercați din nou.",
|
||||
"server.connect.pair.description": "Rulați această comandă pe computer pentru a obține detaliile conexiunii.",
|
||||
"server.connect.scan": "Scanează codul QR",
|
||||
"server.connect.scan.description": "Îndreptați camera către codul QR afișat de opencode pair.",
|
||||
"server.connect.scan.invalid": "Acesta nu este un cod de asociere OpenCode. Scanați codul afișat de opencode pair.",
|
||||
@@ -797,6 +798,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Deschiderea camerei…",
|
||||
"server.connect.mixedContent":
|
||||
"Nu s-a putut conecta la acest server HTTP de pe o pagină HTTPS. Utilizați în schimb o adresă de server HTTPS.",
|
||||
"server.connect.camera.insecure": "Scanarea QR necesită deschiderea acestei pagini prin HTTPS sau pe localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Nicio cameră nu este disponibilă pentru acest browser. Introduceți manual detaliile conexiunii.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -876,6 +876,7 @@ export const dict = {
|
||||
"server.connect.button": "Подключиться",
|
||||
"server.connect.address.invalid": "Введите действительный адрес сервера HTTP или HTTPS.",
|
||||
"server.connect.failed": "Не удалось подключиться. Проверьте адрес сервера и пароль, затем попробуйте снова.",
|
||||
"server.connect.pair.description": "Выполните эту команду на вашем компьютере, чтобы получить данные подключения.",
|
||||
"server.connect.scan": "Сканировать QR-код",
|
||||
"server.connect.scan.description": "Наведите камеру на QR-код, показанный парой opencode.",
|
||||
"server.connect.scan.invalid": "Это не код пары OpenCode. Сканируйте код, показанный парой opencode.",
|
||||
@@ -883,6 +884,8 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Открытие камеры…",
|
||||
"server.connect.mixedContent":
|
||||
"Не удалось подключиться к этому HTTP-серверу с HTTPS-страницы. Используйте адрес HTTPS-сервера вместо этого.",
|
||||
"server.connect.camera.insecure":
|
||||
"Сканирование QR требует открытия этой страницы через HTTPS или на локальном хосте.",
|
||||
"server.connect.camera.unavailable": "Камера недоступна для этого браузера. Введите данные подключения вручную.",
|
||||
"server.connect.camera.error":
|
||||
"Не удалось открыть камеру. Разрешите доступ к камере или введите данные подключения вручную.",
|
||||
|
||||
@@ -792,6 +792,7 @@ export const dict = {
|
||||
"server.connect.button": "සම්බන්ධ කරන්න",
|
||||
"server.connect.address.invalid": "වලංගු HTTP හෝ HTTPS සේවාදායක ලිපිනයක් ඇතුළත් කරන්න.",
|
||||
"server.connect.failed": "සම්බන්ධ වීමට නොහැකි විය. සේවාදායක ලිපිනය සහ මුරපදය පරීක්ෂා කර නැවත උත්සාහ කරන්න.",
|
||||
"server.connect.pair.description": "ඔබගේ සම්බන්ධතා විස්තර ලබා ගැනීමට මෙම විධානය ඔබගේ පරිගණකයේ ක්රියාත්මක කරන්න.",
|
||||
"server.connect.scan": "QR කේතය පරිලෝකනය කරන්න",
|
||||
"server.connect.scan.description": "opencode pair මගින් පෙන්වන QR කේතය වෙත ඔබේ කැමරාව යොමු කරන්න.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -800,6 +801,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "කැමරාව විවෘත කරමින්...",
|
||||
"server.connect.mixedContent":
|
||||
"මෙම HTTP සේවාදායකයට HTTPS පිටුවකින් සම්බන්ධ වීමට නොහැකි විය. ඒ වෙනුවට HTTPS සේවාදායක ලිපිනයක් භාවිතා කරන්න.",
|
||||
"server.connect.camera.insecure": "QR ස්කෑන් කිරීම සඳහා මෙම පිටුව HTTPS හරහා හෝ localhost මත විවෘත කිරීම අවශ්ය වේ.",
|
||||
"server.connect.camera.unavailable": "මෙම බ්රවුසරයට කැමරාවක් නොමැත. ඔබගේ සම්බන්ධතා විස්තර අතින් ඇතුලත් කරන්න.",
|
||||
"server.connect.camera.error":
|
||||
"කැමරාව විවෘත කිරීමට නොහැකි විය. කැමරා ප්රවේශයට ඉඩ දෙන්න හෝ ඔබගේ සම්බන්ධතා විස්තර හස්තීයව ඇතුලත් කරන්න.",
|
||||
|
||||
@@ -815,6 +815,7 @@ export const dict = {
|
||||
"server.connect.button": "Pripojiť sa",
|
||||
"server.connect.address.invalid": "Zadajte platnú adresu servera s protokolom HTTP alebo HTTPS.",
|
||||
"server.connect.failed": "Nepodarilo sa pripojiť. Skontrolujte adresu servera a heslo a skúste znova.",
|
||||
"server.connect.pair.description": "Spustite tento príkaz na svojom počítači, aby ste získali údaje na pripojenie.",
|
||||
"server.connect.scan": "Naskenujte QR kód",
|
||||
"server.connect.scan.description": "Namierte kameru na QR kód zobrazený pomocou opencode pair.",
|
||||
"server.connect.scan.invalid": "Toto nie je párovací kód OpenCode. Naskenujte kód zobrazený pomocou opencode pair.",
|
||||
@@ -822,6 +823,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Otvorenie kamery…",
|
||||
"server.connect.mixedContent":
|
||||
"Nie je možné pripojiť sa k tomuto HTTP serveru z HTTPS stránky. Namiesto toho použite adresu HTTPS.",
|
||||
"server.connect.camera.insecure": "Na skenovanie QR je potrebné otvoriť túto stránku cez HTTPS alebo na localhoste.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Tento prehliadač nemá k dispozícii žiadnu kameru. Zadajte údaje na pripojenie ručne.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -815,6 +815,7 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Vnesite veljaven HTTP ali HTTPS naslov strežnika.",
|
||||
"server.connect.failed":
|
||||
"Povezave ni bilo mogoče vzpostaviti. Preverite naslov strežnika in geslo ter poskusite znova.",
|
||||
"server.connect.pair.description": "Za pridobitev podatkov o povezavi zaženite ta ukaz na svojem računalniku.",
|
||||
"server.connect.scan": "Skeniraj QR kodo",
|
||||
"server.connect.scan.description": "Usmerite kamero na QR kodo, ki jo prikaže opencode pair.",
|
||||
"server.connect.scan.invalid": "To ni OpenCode paritna koda. Skenirajte kodo, ki jo prikaže opencode pair.",
|
||||
@@ -822,6 +823,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Odpiranje kamere…",
|
||||
"server.connect.mixedContent":
|
||||
"Z HTTP strežnikom se ni mogoče povezati iz HTTPS strani. Uporabite HTTPS naslov strežnika.",
|
||||
"server.connect.camera.insecure": "Skeniranje QR zahteva odpiranje te strani preko HTTPS ali na localhost.",
|
||||
"server.connect.camera.unavailable": "Brskalnik nima na voljo kamere. Vnesite podatke o povezavi ročno.",
|
||||
"server.connect.camera.error":
|
||||
"Kamer ne morem odpreti. Dovolite dostop do kamere ali vnesite podatke o povezavi ročno.",
|
||||
|
||||
@@ -782,6 +782,7 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Fut një adresë të vlefshme serveri HTTP ose HTTPS.",
|
||||
"server.connect.failed":
|
||||
"Nuk mund të lidhej. Kontrolloni adresën dhe fjalëkalimin e serverit, më pas provoni përsëri.",
|
||||
"server.connect.pair.description": "Ekzekutoni këtë komandë në kompjuterin tuaj për të marrë detajet e lidhjes suaj.",
|
||||
"server.connect.scan": "Skanoni kodin QR",
|
||||
"server.connect.scan.description": "Drejtoje kamerën drejt kodit QR të treguar nga opencode pair.",
|
||||
"server.connect.scan.invalid": "Ky nuk është një kod çiftimi OpenCode. Skanoni kodin e treguar nga opencode pair.",
|
||||
@@ -789,6 +790,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Hapja e kamerës…",
|
||||
"server.connect.mixedContent":
|
||||
"Nuk mund të lidhej me këtë server HTTP nga një faqe HTTPS. Në vend të kësaj, përdorni një adresë serveri HTTPS.",
|
||||
"server.connect.camera.insecure": "Skanimi QR kërkon hapjen e kësaj faqeje mbi HTTPS ose në localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Asnjë kamerë nuk disponohet për këtë shfletues. Futni manualisht detajet e lidhjes suaj.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -803,6 +803,7 @@ export const dict = {
|
||||
"server.connect.button": "Повежи се",
|
||||
"server.connect.address.invalid": "Унесите важећу HTTP или HTTPS адресу сервера.",
|
||||
"server.connect.failed": "Није могуће повезати се. Проверите адресу сервера и лозинку, па покушајте поново.",
|
||||
"server.connect.pair.description": "Покрените ову команду на рачунару да бисте добили податке за повезивање.",
|
||||
"server.connect.scan": "Скенирај QR код",
|
||||
"server.connect.scan.description": "Усмерите камеру на QR код који приказује opencode pair.",
|
||||
"server.connect.scan.invalid": "Ово није OpenCode код за повезивање. Скенирајте код који приказује opencode pair.",
|
||||
@@ -810,6 +811,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Отварање камере…",
|
||||
"server.connect.mixedContent":
|
||||
"Није могуће повезати се на овај HTTP сервер са HTTPS странице. Уместо тога користите HTTPS адресу сервера.",
|
||||
"server.connect.camera.insecure": "Скенирање QR кода захтева отварање ове странице преко HTTPS-а или на лоцалхосту.",
|
||||
"server.connect.camera.unavailable": "Овај прегледник нема доступну камеру. Унесите податке за повезивање ручно.",
|
||||
"server.connect.camera.error":
|
||||
"Није могуће отворити камеру. Дозволите приступ камери или унесите податке за повезивање ручно.",
|
||||
|
||||
@@ -22,6 +22,7 @@ export const dict = {
|
||||
"server.connect.button": "Anslut",
|
||||
"server.connect.address.invalid": "Ange en giltig HTTP- eller HTTPS-serveradress.",
|
||||
"server.connect.failed": "Kunde inte ansluta. Kontrollera serveradressen och lösenordet och försök sedan igen.",
|
||||
"server.connect.pair.description": "Kör det här kommandot på din dator för att få din anslutningsinformation.",
|
||||
"server.connect.scan": "Skanna QR-koden",
|
||||
"server.connect.scan.description": "Rikta kameran mot QR-koden som visas av opencode pair.",
|
||||
"server.connect.scan.invalid": "Detta är inte en OpenCode parningskod. Skanna koden som visas av opencode pair.",
|
||||
@@ -29,6 +30,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Öppnar kameran...",
|
||||
"server.connect.mixedContent":
|
||||
"Det gick inte att ansluta till denna HTTP-server från en HTTPS-sida. Använd en HTTPS-serveradress istället.",
|
||||
"server.connect.camera.insecure": "QR-skanning kräver att den här sidan öppnas över HTTPS eller på localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Ingen kamera är tillgänglig för den här webbläsaren. Ange dina anslutningsuppgifter manuellt.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -796,6 +796,7 @@ export const dict = {
|
||||
"server.connect.button": "Пайваст кунед",
|
||||
"server.connect.address.invalid": "Суроғаи сервери дурусти HTTP ё HTTPS-ро ворид кунед.",
|
||||
"server.connect.failed": "Пайваст шуда натавонист. Суроғаи сервер ва паролро тафтиш кунед ва аз нав кӯшиш кунед.",
|
||||
"server.connect.pair.description": "Барои гирифтани тафсилоти пайвасти худ ин фармонро дар компютери худ иҷро кунед.",
|
||||
"server.connect.scan": "Рамзи QR-ро скан кунед",
|
||||
"server.connect.scan.description": "Камераро ба рамзи QR, ки opencode pair нишон медиҳад, равона кунед.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -804,6 +805,8 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Кушодани камера…",
|
||||
"server.connect.mixedContent":
|
||||
"Аз саҳифаи HTTPS ба ин сервери HTTP пайваст шуда натавонист. Ба ҷои он суроғаи сервери HTTPS-ро истифода баред.",
|
||||
"server.connect.camera.insecure":
|
||||
"Сканкунии QR талаб мекунад, ки ин саҳифа тавассути HTTPS ё дар localhost кушода шавад.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Барои ин браузер ягон камера дастрас нест. Тафсилоти пайвасти худро дастӣ ворид кунед.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -852,12 +852,14 @@ export const dict = {
|
||||
"server.connect.button": "เชื่อมต่อ",
|
||||
"server.connect.address.invalid": "ใส่ที่อยู่เซิร์ฟเวอร์ HTTP หรือ HTTPS ที่ถูกต้อง",
|
||||
"server.connect.failed": "ไม่สามารถเชื่อมต่อได้ ตรวจสอบที่อยู่เซิร์ฟเวอร์และรหัสผ่าน จากนั้นลองใหม่อีกครั้ง",
|
||||
"server.connect.pair.description": "รันคำสั่งนี้บนคอมพิวเตอร์ของคุณเพื่อรับรายละเอียดการเชื่อมต่อของคุณ",
|
||||
"server.connect.scan": "สแกนรหัส QR",
|
||||
"server.connect.scan.description": "ชี้กล้องของคุณไปที่รหัส QR ที่แสดงโดยการจับคู่ opencode",
|
||||
"server.connect.scan.invalid": "นี่ไม่ใช่รหัสจับคู่ OpenCode สแกนรหัสที่แสดงโดย opencode pair",
|
||||
"server.connect.camera": "กำลังจับคู่กล้อง",
|
||||
"server.connect.camera.starting": "กำลังเปิดกล้อง…",
|
||||
"server.connect.mixedContent": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ HTTP จากหน้า HTTPS ใช้ที่อยู่เซิร์ฟเวอร์ HTTPS แทน",
|
||||
"server.connect.camera.insecure": "การสแกน QR ต้องเปิดหน้านี้ผ่าน HTTPS หรือบน localhost",
|
||||
"server.connect.camera.unavailable": "เบราว์เซอร์นี้ไม่สามารถใช้งานกล้องได้ กรอกรายละเอียดการเชื่อมต่อด้วยตนเอง",
|
||||
"server.connect.camera.error": "ไม่สามารถเปิดกล้องได้ อนุญาตการเข้าถึงกล้องหรือกรอกรายละเอียดการเชื่อมต่อด้วยตนเอง",
|
||||
"dialog.server.menu.remove": "ลบ",
|
||||
|
||||
@@ -791,6 +791,7 @@ export const dict = {
|
||||
"server.connect.button": "Birikdiriň",
|
||||
"server.connect.address.invalid": "Dogry HTTP ýa-da HTTPS serwer salgysyny giriziň.",
|
||||
"server.connect.failed": "Birikip bilmedim Serweriň salgysyny we parolyny barlaň, soňra gaýtadan synanyşyň.",
|
||||
"server.connect.pair.description": "Baglanyş maglumatlaryňyzy almak üçin bu buýrugy kompýuteriňizde işlediň.",
|
||||
"server.connect.scan": "QR koduny skanirläň",
|
||||
"server.connect.scan.description": "Kamerany opencode pair görkezýän QR koda gönükdiriň.",
|
||||
"server.connect.scan.invalid": "Bu OpenCode jübütleme kody däl. opencode pair görkezýän kody skanirläň.",
|
||||
@@ -798,6 +799,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Kamerany açmak…",
|
||||
"server.connect.mixedContent":
|
||||
"Bu HTTP serwerine HTTPS sahypasyndan birigip bilmedim. Munuň ýerine HTTPS serwer salgysyny ulanyň.",
|
||||
"server.connect.camera.insecure": "QR gözlemek bu sahypany HTTPS ýa-da ýerlihostda açmagy talap edýär.",
|
||||
"server.connect.camera.unavailable": "Bu brauzerde kamera ýok. Birikdiriş maglumatlaryňyzy el bilen giriziň.",
|
||||
"server.connect.camera.error":
|
||||
"Kamerany açyp bolmady Kamera girmegine rugsat beriň ýa-da baglanyşyk maglumatlaryňyzy el bilen giriziň.",
|
||||
|
||||
@@ -860,6 +860,7 @@ export const dict = {
|
||||
"server.connect.button": "Bağlan",
|
||||
"server.connect.address.invalid": "Geçerli bir HTTP veya HTTPS sunucu adresi girin.",
|
||||
"server.connect.failed": "Bağlantı kurulamadı. Sunucu adresini ve şifreyi kontrol edip tekrar deneyin.",
|
||||
"server.connect.pair.description": "Bağlantı ayrıntılarınızı almak için bilgisayarınızda bu komutu çalıştırın.",
|
||||
"server.connect.scan": "QR kodunu tarayın",
|
||||
"server.connect.scan.description": "Kameranızı opencode pair tarafından gösterilen QR koduna doğrultun.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -868,6 +869,7 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Kamera açılıyor…",
|
||||
"server.connect.mixedContent":
|
||||
"Bu HTTP sunucusuna bir HTTPS sayfasından bağlanılamadı. Bunun yerine HTTPS sunucu adresini kullanın.",
|
||||
"server.connect.camera.insecure": "QR taraması, bu sayfanın HTTPS veya localhost üzerinden açılmasını gerektirir.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Bu tarayıcıda kamera mevcut değil. Bağlantı ayrıntılarınızı manuel olarak girin.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
Loaded 100 of 471 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user