Compare commits
326 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b6e4150625 | |||
| 1229ccca54 | |||
| 1643fd390e | |||
| a6e6b42837 | |||
| 919c2fb83d | |||
| 3afb6765a5 | |||
| 108d3b930a | |||
| de5691503a | |||
| 4b08910bbf | |||
| 3831af62b5 | |||
| 32c3017b00 | |||
| f0051d95cb | |||
| 0a1c0663e9 | |||
| ff80694ddf | |||
| e438ce9c50 | |||
| 08710de30f | |||
| db322bdb85 | |||
| ed50480ba1 | |||
| 5264cffe42 | |||
| 777a534766 | |||
| 1251c61f48 | |||
| 7f4564ee5c | |||
| 0d5ef472ce | |||
| 11488f5e36 | |||
| b9e0d0c1fc | |||
| 995f4cee56 | |||
| 839be49f54 | |||
| 37552df84a | |||
| 1c02eb64b4 | |||
| eed3a0d64f | |||
| c8b854f651 | |||
| 9ff289fe66 | |||
| 57357b6779 | |||
| caf95068a8 | |||
| cdc1e5daf3 | |||
| 7b6d07f74a | |||
| 926c92cc7c | |||
| f8327dea11 | |||
| 081707691a | |||
| 7ea774bedf | |||
| c951c438fc | |||
| 68563a3e5d | |||
| b9de690ad6 | |||
| 665a86a320 | |||
| bb98729bb5 | |||
| 39c717ab2a | |||
| 1762028f59 | |||
| f835376491 | |||
| 14a218cb68 | |||
| 0fd839cd9f | |||
| 82e2236bd2 | |||
| 75ee8c3148 | |||
| 7bedccacce | |||
| abe67f0140 | |||
| 6796e6704c | |||
| 4ca91ab698 | |||
| 3c5bf8b2a3 | |||
| 00f0febe02 | |||
| 742ed593b0 | |||
| ee46adfe42 | |||
| d3ec3f6cd6 | |||
| 7abe09bbd9 | |||
| c2654aee58 | |||
| 643537c01c | |||
| 0cc0ecd5f2 | |||
| dbd1131e3d | |||
| 57908d0e67 | |||
| eb2a78fd0f | |||
| da74142b42 | |||
| 58d24fff76 | |||
| 29c200e790 | |||
| b74337606d | |||
| 691f2200ca | |||
| 3620877510 | |||
| eef12eef4d | |||
| d6a26ff4fe | |||
| f6b8e559be | |||
| 3aa96e9db2 | |||
| 0a8aaafe93 | |||
| 339f4a2c16 | |||
| dedc0b1c16 | |||
| b895e66465 | |||
| 24fc5d4339 | |||
| 951e33c34a | |||
| d1857c2bc1 | |||
| 8dcab8cef7 | |||
| 99871ed954 | |||
| 171cdc1f5a | |||
| 0b8e65bacb | |||
| f8fe5dc89a | |||
| f05db05a03 | |||
| 3f4cdda3c4 | |||
| bc5ef2be93 | |||
| 04e81510ad | |||
| b9dc2be4cc | |||
| ea2fc2262b | |||
| c00c594239 | |||
| 7a5278d5b4 | |||
| e698ce3ac5 | |||
| 3d6f4dbdbe | |||
| fe34b0f40a | |||
| bda6b99142 | |||
| 8a20b79359 | |||
| 9bce498764 | |||
| 97d1aca4f9 | |||
| 10db5e78c0 | |||
| f1d03ff886 | |||
| 1e90d3f579 | |||
| 9853a5993e | |||
| c14a4b6d0b | |||
| 41ed145807 | |||
| b22e6b7884 | |||
| 5e577e913d | |||
| a4abf03cf9 | |||
| 69c6969ac9 | |||
| 152c26a567 | |||
| e616f040a1 | |||
| 7d615e81c8 | |||
| 438eb6edf5 | |||
| 2ed0d6479e | |||
| a4df4d44ee | |||
| 784c8c3b03 | |||
| a13c34d6e0 | |||
| 28ce430a27 | |||
| f50f9ca6e6 | |||
| 2aa8cec363 | |||
| cfc87d84a6 | |||
| e4087e4d9c | |||
| ca967c59dd | |||
| 3ab214209c | |||
| 0fce19f42c | |||
| bb8e86d1e5 | |||
| f00c3204bf | |||
| d6e43861d5 | |||
| 64e2d12d2b | |||
| 3d12e0668b | |||
| 2fbc8c959e | |||
| 53e47bc9ca | |||
| b0e6764895 | |||
| d9eb9d7ca0 | |||
| 3d36cc8c72 | |||
| 938e428e82 | |||
| 8e8f7a1fba | |||
| 65372e02b9 | |||
| 8e9172e230 | |||
| bc034ccd93 | |||
| 6a090caf99 | |||
| 31baaf507b | |||
| 8e7996fe26 | |||
| 103d30f53a | |||
| af93eb5041 | |||
| 898e6243f4 | |||
| 996a92556f | |||
| 419de8eb84 | |||
| 05240cfc2e | |||
| 9120a8902a | |||
| 500ed30754 | |||
| dd12f6da94 | |||
| e2c7f6b543 | |||
| 06da224cde | |||
| 206e073a5c | |||
| 84b3dda387 | |||
| 107a83c3e1 | |||
| 60e95c664c | |||
| d2a41b2224 | |||
| d81f3358ef | |||
| 31670f655c | |||
| 9b072e8aef | |||
| 92013b8ef0 | |||
| be58af7018 | |||
| e467281b76 | |||
| cd6302db74 | |||
| c8655f6903 | |||
| e5793ea5b8 | |||
| 730155023b | |||
| 378d934122 | |||
| 3d789619ba | |||
| 12469e258f | |||
| 77fddd2e47 | |||
| 70aa00c4e9 | |||
| b638e7ef47 | |||
| 9c0678626f | |||
| d476fd22f7 | |||
| 2a33b20cad | |||
| d5d638460c | |||
| 42d63ba4d2 | |||
| 1806d799a1 | |||
| 8ffb5340aa | |||
| 4f0ccadab7 | |||
| 6afc2c0cca | |||
| 861c1dc0a9 | |||
| bbb113d24a | |||
| a281e8656d | |||
| 402b3c0bc9 | |||
| d9f6ff0d44 | |||
| e552698a24 | |||
| 2d3b5e1dc2 | |||
| 556174196a | |||
| 49f8f087bc | |||
| bf429102de | |||
| b3fb3fe933 | |||
| 90dfe63e72 | |||
| 43c27708b2 | |||
| 12b7ba4ac1 | |||
| 46b8e7a034 | |||
| 329dcc3a33 | |||
| 44e80315df | |||
| 5b272e151c | |||
| 4366fd7a14 | |||
| 73b84e1c8f | |||
| 6d4e75b55d | |||
| 1882dd032a | |||
| defb3f64af | |||
| d32c4622fd | |||
| e70d4c3224 | |||
| 936b8c2c0b | |||
| 77a901069b | |||
| eb17982249 | |||
| b57815ba22 | |||
| 2bc1411fe4 | |||
| a18eece477 | |||
| 218689103e | |||
| 7155c95662 | |||
| 465991be46 | |||
| 15aaf884d8 | |||
| d06012083f | |||
| 2902acc08c | |||
| 61f887383c | |||
| 010bba974e | |||
| 99533a9dc4 | |||
| c311b72695 | |||
| a2f8b3b9f0 | |||
| 5c1749478b | |||
| 359d134354 | |||
| 58db5dfccd | |||
| d354d5fb04 | |||
| a0a4ad793f | |||
| 2281845f47 | |||
| fe972955b4 | |||
| e4217c982d | |||
| 3519d2cbb1 | |||
| 85ff86fe9e | |||
| 9dd99d19d5 | |||
| acfb0b8073 | |||
| 4a9532dd55 | |||
| c150b57903 | |||
| a598ec3eea | |||
| 23cd589656 | |||
| 7b779f4862 | |||
| 5b94e49a4b | |||
| 5e0135f0b8 | |||
| 1da77c773e | |||
| eee7abc9b6 | |||
| f1dae64d39 | |||
| 1eb13ba4af | |||
| 2bba026d98 | |||
| d803e5fdee | |||
| 4afe7699d0 | |||
| b7733d83fd | |||
| 541318b34e | |||
| 5df71fad7e | |||
| 02fcb24323 | |||
| eef729924d | |||
| ed51d214d9 | |||
| 5851b77196 | |||
| 75d82f7dc9 | |||
| 8a41e28f5b | |||
| b0955d0079 | |||
| 8a117354fd | |||
| 2188c1a235 | |||
| 181ca1a17f | |||
| cd564678ae | |||
| 946ea995e3 | |||
| 673c2e4e8e | |||
| eea507f2eb | |||
| dfaec0c494 | |||
| e06de8d075 | |||
| 28fca2ce79 | |||
| 073d4c02f6 | |||
| 97d87eac9f | |||
| 0249f58509 | |||
| 138d37a87d | |||
| cde3ff010c | |||
| 2d9b134b1b | |||
| 6071cafce1 | |||
| 949ac8ed15 | |||
| d2ec847f0c | |||
| aae9495938 | |||
| f53f31d33f | |||
| 186907ef94 | |||
| 7825daef72 | |||
| ed448861ed | |||
| 7159d09978 | |||
| 7a85583582 | |||
| ce815ee2aa | |||
| e9dc9d2d01 | |||
| 612ed0fc18 | |||
| 0cbf9e9199 | |||
| 5229f9f700 | |||
| 21110551ee | |||
| d4b2eb10f6 | |||
| cdea1a959e | |||
| 7c3958e658 | |||
| b78e5c1962 | |||
| 6578a2db95 | |||
| b6670fa42d | |||
| 70d454489e | |||
| 2e1e60ca0f | |||
| 5ba13419b3 | |||
| c8d2ca87aa | |||
| 64d4f5efe1 | |||
| ea45b1a447 | |||
| b7d1eab2bb | |||
| 23fcb7fb2e | |||
| a2aa93c6bd | |||
| 1461c05776 | |||
| c905c43333 | |||
| 9c6585cad7 | |||
| 773998133d | |||
| 021fe76e47 | |||
| 4a078e05b1 | |||
| 8a8dfd5e2f | |||
| 1f77795ac6 | |||
| 5f3ad19794 | |||
| a1ae53e35d | |||
| b047f7af36 |
@@ -594,7 +594,6 @@ OPENCODE_DISABLE_CLAUDE_CODE
|
||||
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT
|
||||
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS
|
||||
OPENCODE_DISABLE_DEFAULT_PLUGINS
|
||||
OPENCODE_DISABLE_FILETIME_CHECK
|
||||
OPENCODE_DISABLE_LSP_DOWNLOAD
|
||||
OPENCODE_DISABLE_MODELS_FETCH
|
||||
OPENCODE_DISABLE_PRUNE
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"opencode": {
|
||||
"options": {},
|
||||
},
|
||||
},
|
||||
"provider": {},
|
||||
"permission": {
|
||||
"edit": {
|
||||
"packages/opencode/migration/*": "deny",
|
||||
|
||||
@@ -7,7 +7,7 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
||||
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
||||
Accept: "application/vnd.github+json",
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
...(options.headers instanceof Headers ? Object.fromEntries(options.headers.entries()) : options.headers),
|
||||
},
|
||||
})
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -28,7 +28,7 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
||||
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
||||
Accept: "application/vnd.github+json",
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
...(options.headers instanceof Headers ? Object.fromEntries(options.headers.entries()) : options.headers),
|
||||
},
|
||||
})
|
||||
if (!response.ok) {
|
||||
|
||||
+37
-2
@@ -1,6 +1,13 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxc-project.github.io/refs/heads/json-schema/src/public/.oxlintrc.schema.json",
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
"categories": {
|
||||
"suspicious": "warn"
|
||||
},
|
||||
"rules": {
|
||||
"typescript/no-base-to-string": "warn",
|
||||
// Effect uses `function*` with Effect.gen/Effect.fnUntraced that don't always yield
|
||||
"require-yield": "off",
|
||||
// SolidJS uses `let ref: T | undefined` for JSX ref bindings assigned at runtime
|
||||
@@ -10,7 +17,35 @@
|
||||
// Intentional control char matching (ANSI escapes, null byte sanitization)
|
||||
"no-control-regex": "off",
|
||||
// SST and plugin tools require triple-slash references
|
||||
"triple-slash-reference": "off"
|
||||
"triple-slash-reference": "off",
|
||||
|
||||
// Suspicious category: suppress noisy rules
|
||||
// Effect's nested function* closures inherently shadow outer scope
|
||||
"no-shadow": "off",
|
||||
// Namespace-heavy codebase makes this too noisy
|
||||
"unicorn/consistent-function-scoping": "off",
|
||||
// Opinionated — .sort()/.reverse() mutation is fine in this codebase
|
||||
"unicorn/no-array-sort": "off",
|
||||
"unicorn/no-array-reverse": "off",
|
||||
// Not relevant — this isn't a DOM event handler codebase
|
||||
"unicorn/prefer-add-event-listener": "off",
|
||||
// Bundler handles module resolution
|
||||
"unicorn/require-module-specifiers": "off",
|
||||
// postMessage target origin not relevant for this codebase
|
||||
"unicorn/require-post-message-target-origin": "off",
|
||||
// Side-effectful constructors are intentional in some places
|
||||
"no-new": "off",
|
||||
|
||||
// Type-aware: catch unhandled promises
|
||||
"typescript/no-floating-promises": "warn",
|
||||
// Warn when spreading non-plain objects (Headers, class instances, etc.)
|
||||
"typescript/no-misused-spread": "warn"
|
||||
},
|
||||
"ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts"]
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
"ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts", "**/sdk.gen.ts"]
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
- Use Bun APIs when possible, like `Bun.file()`
|
||||
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
|
||||
- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
|
||||
- In `src/config`, follow the existing self-export pattern at the top of the file (for example `export * as ConfigAgent from "./agent"`) when adding a new config module.
|
||||
|
||||
Reduce total variable count by inlining when a value is only used once.
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"glob": "13.0.5",
|
||||
"husky": "9.1.7",
|
||||
"oxlint": "1.60.0",
|
||||
"oxlint-tsgolint": "0.21.0",
|
||||
"prettier": "3.6.2",
|
||||
"semver": "^7.6.0",
|
||||
"sst": "3.18.10",
|
||||
@@ -28,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.14",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -82,7 +83,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.14",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -116,7 +117,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.14",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -143,7 +144,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.14",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.64",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -167,7 +168,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.14",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -191,7 +192,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.14",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -224,7 +225,7 @@
|
||||
},
|
||||
"packages/desktop-electron": {
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.14",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
"electron-context-menu": "4.1.2",
|
||||
@@ -267,7 +268,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.14",
|
||||
"dependencies": {
|
||||
"@opencode-ai/shared": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -296,7 +297,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.14",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -312,7 +313,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.14",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -321,15 +322,15 @@
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.16.1",
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.93",
|
||||
"@ai-sdk/anthropic": "3.0.67",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.96",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
"@ai-sdk/azure": "3.0.49",
|
||||
"@ai-sdk/cerebras": "2.0.41",
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/deepinfra": "2.0.41",
|
||||
"@ai-sdk/gateway": "3.0.97",
|
||||
"@ai-sdk/gateway": "3.0.104",
|
||||
"@ai-sdk/google": "3.0.63",
|
||||
"@ai-sdk/google-vertex": "4.0.109",
|
||||
"@ai-sdk/google-vertex": "4.0.112",
|
||||
"@ai-sdk/groq": "3.0.31",
|
||||
"@ai-sdk/mistral": "3.0.27",
|
||||
"@ai-sdk/openai": "3.0.53",
|
||||
@@ -358,15 +359,14 @@
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/server": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "2.5.1",
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
"@opentelemetry/context-async-hooks": "2.6.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
|
||||
"@opentelemetry/sdk-trace-base": "2.6.1",
|
||||
"@opentelemetry/sdk-trace-node": "2.6.1",
|
||||
"@opentui/core": "0.1.99",
|
||||
"@opentui/solid": "0.1.99",
|
||||
"@opentui/core": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -386,7 +386,7 @@
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "3.1.0",
|
||||
"gitlab-ai-provider": "6.4.2",
|
||||
"gitlab-ai-provider": "6.6.0",
|
||||
"glob": "13.0.5",
|
||||
"google-auth-library": "10.5.0",
|
||||
"gray-matter": "4.0.3",
|
||||
@@ -458,23 +458,23 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.14",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opentui/core": "0.1.99",
|
||||
"@opentui/solid": "0.1.99",
|
||||
"@opentui/core": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.1.99",
|
||||
"@opentui/solid": ">=0.1.99",
|
||||
"@opentui/core": ">=0.1.100",
|
||||
"@opentui/solid": ">=0.1.100",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"@opentui/core",
|
||||
@@ -493,7 +493,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.14",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -506,20 +506,9 @@
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.4.6",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/shared": {
|
||||
"name": "@opencode-ai/shared",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.14",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -527,6 +516,7 @@
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@npmcli/arborist": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"glob": "13.0.5",
|
||||
"mime-types": "3.0.2",
|
||||
"minimatch": "10.2.5",
|
||||
"semver": "catalog:",
|
||||
@@ -534,13 +524,15 @@
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/npmcli__arborist": "6.3.3",
|
||||
"@types/semver": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.14",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -575,7 +567,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.14",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -624,7 +616,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.14",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -683,6 +675,8 @@
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opentui/core": "0.1.99",
|
||||
"@opentui/solid": "0.1.99",
|
||||
"@pierre/diffs": "1.1.0-beta.18",
|
||||
"@playwright/test": "1.59.1",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
@@ -698,7 +692,7 @@
|
||||
"@types/node": "22.13.9",
|
||||
"@types/semver": "7.7.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||
"ai": "6.0.158",
|
||||
"ai": "6.0.168",
|
||||
"cross-spawn": "7.0.6",
|
||||
"diff": "8.0.2",
|
||||
"dompurify": "3.3.1",
|
||||
@@ -746,7 +740,7 @@
|
||||
|
||||
"@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.93", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hcXDU8QDwpAzLVTuY932TQVlIij9+iaVTxc5mPGY6yb//JMAAC5hMVhg93IrxlrxWLvMgjezNgoZGwquR+SGnw=="],
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.96", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Mc4Ias2jRMD1jOB6xWtKNPdhECeuCZyIlbr9EAGfBnyBt++sS13ziZh9qv9TdyMCAZJ7xoQcpbchoRJcKwPdpA=="],
|
||||
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="],
|
||||
|
||||
@@ -766,11 +760,11 @@
|
||||
|
||||
"@ai-sdk/fireworks": ["@ai-sdk/fireworks@2.0.46", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XRKR0zgRyegdmtK5CDUEjlyRp0Fo+XVCdoG+301U1SGtgRIAYG3ObVtgzVJBVpJdHFSLHuYeLTnNiQoUxD7+FQ=="],
|
||||
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.97", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ERHmVGX30YKTwxObuHQzNqoOf8Nb5WwYMDBn34e3TGGVn0vLEXwMimo7uRVTbhhi4gfu9WtwYTE4x1+csZok1w=="],
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="],
|
||||
|
||||
"@ai-sdk/google": ["@ai-sdk/google@3.0.63", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RfOZWVMYSPu2sPRfGajrauWAZ9BSaRopSn+AszkKWQ1MFj8nhaXvCqRHB5pBQUaHTfZKagvOmMpNfa/s3gPLgQ=="],
|
||||
|
||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.109", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/google": "3.0.63", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-QzQ+DgOoSYlkU4mK0H+iaCaW1bl5zOimH9X2E2oylcVyUtAdCuduQ959Uw1ygW3l09J2K/ceEDtK8OUPHyOA7g=="],
|
||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.112", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/google": "3.0.64", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cSfHCkM+9ZrFtQWIN1WlV93JPD+isGSdFxKj7u1L9m2aLVZajlXdcE41GL9hMt7ld7bZYE4NnZ+4VLxBAHE+Eg=="],
|
||||
|
||||
"@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="],
|
||||
|
||||
@@ -1568,8 +1562,6 @@
|
||||
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@workspace:packages/sdk/js"],
|
||||
|
||||
"@opencode-ai/server": ["@opencode-ai/server@workspace:packages/server"],
|
||||
|
||||
"@opencode-ai/shared": ["@opencode-ai/shared@workspace:packages/shared"],
|
||||
|
||||
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
|
||||
@@ -1596,7 +1588,7 @@
|
||||
|
||||
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="],
|
||||
|
||||
"@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||
"@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="],
|
||||
|
||||
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="],
|
||||
|
||||
@@ -1694,6 +1686,18 @@
|
||||
|
||||
"@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.96.0", "", { "os": "win32", "cpu": "x64" }, "sha512-0fI0P0W7bSO/GCP/N5dkmtB9vBqCA4ggo1WmXTnxNJVmFFOtcA1vYm1I9jl8fxo+sucW2WnlpnI4fjKdo3JKxA=="],
|
||||
|
||||
"@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.21.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-P20j3MLqfwIT+94qGU3htC7dWp4pXGZW1p1p7FRUzu1aopq7c9nPCgf0W/WjktqQ57+iuTq9mbSlwWinl6+H1A=="],
|
||||
|
||||
"@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@0.21.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-81TmmuBcPedEA0MwRmObuQuXnCprS1UiHQWGe7pseqNAJzUWXeAPrayqKTACX92VpruJI+yvY0XJrFp11PpcTA=="],
|
||||
|
||||
"@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@0.21.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-sbjBr6zDduX8rNO0PTjhf7VYLCPWqdijWiMPp8e10qu6Tam1GdaVLaLlX8QrNupTgglO1GvqqgY/jcacWL8a6g=="],
|
||||
|
||||
"@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@0.21.0", "", { "os": "linux", "cpu": "x64" }, "sha512-jNrOcy53R5TJQfrK444Cm60bW9437xDoxPbm3AdvFSo/fhdFMllawc7uZC2Wzr+EAjTkW13K8R4QHzsUdBG9fQ=="],
|
||||
|
||||
"@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@0.21.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-xWeRxJJILDE4b9UqHEWGBxcBc1TUS6zWHhxcyxTZMwf4q3wdKeu0OHYAcwLGJzoSjEIf6FTjyfPiRNil2oqsdg=="],
|
||||
|
||||
"@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@0.21.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Ob9AA9teI8ckPo1whV1smLr5NrqwgBv/8boDbK0YZG+fKgNGRwr1hBj1ORgFWOQaUBv+5njp5A0RAfJJjQ95QQ=="],
|
||||
|
||||
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-YdeJKaZckDQL1qa62a1aKq/goyq48aX3yOxaaWqWb4sau4Ee4IiLbamftNLU3zbePky6QsDj6thnSSzHRBjDfA=="],
|
||||
|
||||
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.60.0", "", { "os": "android", "cpu": "arm64" }, "sha512-7ANS7PpXCfq84xZQ8E5WPs14gwcuPcl+/8TFNXfpSu0CQBXz3cUo2fDpHT8v8HJN+Ut02eacvMAzTnc9s6X4tw=="],
|
||||
@@ -2452,7 +2456,7 @@
|
||||
|
||||
"@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="],
|
||||
|
||||
"@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
|
||||
"@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
||||
|
||||
@@ -2512,7 +2516,7 @@
|
||||
|
||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||
|
||||
"ai": ["ai@6.0.158", "", { "dependencies": { "@ai-sdk/gateway": "3.0.95", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gLTp1UXFtMqKUi3XHs33K7UFglbvojkxF/aq337TxnLGOhHIW9+GyP2jwW4hYX87f1es+wId3VQoPRRu9zEStQ=="],
|
||||
"ai": ["ai@6.0.168", "", { "dependencies": { "@ai-sdk/gateway": "3.0.104", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ=="],
|
||||
|
||||
"ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="],
|
||||
|
||||
@@ -3310,7 +3314,7 @@
|
||||
|
||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||
|
||||
"gitlab-ai-provider": ["gitlab-ai-provider@6.4.2", "", { "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-Wyw6uslCuipBOr/NYwAtpgXEUJj68iJY5aekad2DjePN99JetKVQBqkLgAy9PZp2EA4OuscfRQu9qKIBN/evNw=="],
|
||||
"gitlab-ai-provider": ["gitlab-ai-provider@6.6.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-jUxYnKA4XQaPc3wxACDZ8bPDXO0Mzx7cZaBDxbT2uGgLqtGZmSi+9tVNIg7louSS+s/ioVra3SoUz3iOFVhKPA=="],
|
||||
|
||||
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
|
||||
|
||||
@@ -4114,6 +4118,8 @@
|
||||
|
||||
"oxlint": ["oxlint@1.60.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.60.0", "@oxlint/binding-android-arm64": "1.60.0", "@oxlint/binding-darwin-arm64": "1.60.0", "@oxlint/binding-darwin-x64": "1.60.0", "@oxlint/binding-freebsd-x64": "1.60.0", "@oxlint/binding-linux-arm-gnueabihf": "1.60.0", "@oxlint/binding-linux-arm-musleabihf": "1.60.0", "@oxlint/binding-linux-arm64-gnu": "1.60.0", "@oxlint/binding-linux-arm64-musl": "1.60.0", "@oxlint/binding-linux-ppc64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-musl": "1.60.0", "@oxlint/binding-linux-s390x-gnu": "1.60.0", "@oxlint/binding-linux-x64-gnu": "1.60.0", "@oxlint/binding-linux-x64-musl": "1.60.0", "@oxlint/binding-openharmony-arm64": "1.60.0", "@oxlint/binding-win32-arm64-msvc": "1.60.0", "@oxlint/binding-win32-ia32-msvc": "1.60.0", "@oxlint/binding-win32-x64-msvc": "1.60.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.18.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-tnRzTWiWJ9pg3ftRWnD0+Oqh78L6ZSwcEudvCZaER0PIqiAnNyXj5N1dPwjmNpDalkKS9m/WMLN1CTPUBPmsgw=="],
|
||||
|
||||
"oxlint-tsgolint": ["oxlint-tsgolint@0.21.0", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.21.0", "@oxlint-tsgolint/darwin-x64": "0.21.0", "@oxlint-tsgolint/linux-arm64": "0.21.0", "@oxlint-tsgolint/linux-x64": "0.21.0", "@oxlint-tsgolint/win32-arm64": "0.21.0", "@oxlint-tsgolint/win32-x64": "0.21.0" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-HiWPhANwRnN1pZJQ2SgNB3WRR+1etLJHmRzQ/MJhyINsEIaOUCjxhlXJKbEaVUwdnyXwRWqo/P9Fx21lz0/mSg=="],
|
||||
|
||||
"p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
|
||||
|
||||
"p-defer": ["p-defer@3.0.0", "", {}, "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw=="],
|
||||
@@ -5148,7 +5154,11 @@
|
||||
|
||||
"@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="],
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.13", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
@@ -5162,7 +5172,9 @@
|
||||
|
||||
"@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="],
|
||||
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CbR82EgGPNrj/6q0HtclwuCqe0/pDShyv3nWDP/A9DroujzWXnLMlUJVrgPOsg4b40zQCwwVs2XSKCxvt/4QaA=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||
|
||||
@@ -5572,6 +5584,18 @@
|
||||
|
||||
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
||||
|
||||
"@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||
|
||||
"@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||
|
||||
"@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="],
|
||||
|
||||
"@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||
|
||||
"@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||
|
||||
"@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||
|
||||
"@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||
|
||||
"@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="],
|
||||
@@ -5678,7 +5702,7 @@
|
||||
|
||||
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||
|
||||
"ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="],
|
||||
"ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.93", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hcXDU8QDwpAzLVTuY932TQVlIij9+iaVTxc5mPGY6yb//JMAAC5hMVhg93IrxlrxWLvMgjezNgoZGwquR+SGnw=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="],
|
||||
|
||||
@@ -5896,7 +5920,7 @@
|
||||
|
||||
"nypm/tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="],
|
||||
|
||||
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FFX4P5Fd6lcQJc2OLngZQkbbJHa0IDDZi087Edb8qRZx6h90krtM61ArbMUL8us/7ZUwojCXnyJ/wQ2Eflx2jQ=="],
|
||||
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="],
|
||||
|
||||
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
|
||||
|
||||
|
||||
+3
-3
@@ -513,7 +513,7 @@ async function subscribeSessionEvents() {
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
let text = ""
|
||||
;(async () => {
|
||||
void (async () => {
|
||||
while (true) {
|
||||
try {
|
||||
const { done, value } = await reader.read()
|
||||
@@ -542,7 +542,7 @@ async function subscribeSessionEvents() {
|
||||
? JSON.stringify(part.state.input)
|
||||
: "Unknown"
|
||||
console.log()
|
||||
console.log(color + `|`, "\x1b[0m\x1b[2m" + ` ${tool.padEnd(7, " ")}`, "", "\x1b[0m" + title)
|
||||
console.log(`${color}|`, `\x1b[0m\x1b[2m ${tool.padEnd(7, " ")}`, "", `\x1b[0m${title}`)
|
||||
}
|
||||
|
||||
if (part.type === "text") {
|
||||
@@ -776,7 +776,7 @@ async function assertPermissions() {
|
||||
console.log(` permission: ${permission}`)
|
||||
} catch (error) {
|
||||
console.error(`Failed to check permissions: ${error}`)
|
||||
throw new Error(`Failed to check permissions for user ${actor}: ${error}`)
|
||||
throw new Error(`Failed to check permissions for user ${actor}: ${error}`, { cause: error })
|
||||
}
|
||||
|
||||
if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)
|
||||
|
||||
@@ -236,7 +236,6 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
SALESFORCE_INSTANCE_URL,
|
||||
ZEN_BLACK_PRICE,
|
||||
ZEN_LITE_PRICE,
|
||||
new sst.Secret("ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES"),
|
||||
new sst.Secret("ZEN_LIMITS"),
|
||||
new sst.Secret("ZEN_SESSION_SECRET"),
|
||||
...ZEN_MODELS,
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { shortDomain } from "./stage"
|
||||
|
||||
const storage = new sst.cloudflare.Bucket("EnterpriseStorage")
|
||||
|
||||
const teams = new sst.cloudflare.x.SolidStart("Teams", {
|
||||
new sst.cloudflare.x.SolidStart("Teams", {
|
||||
domain: shortDomain,
|
||||
path: "packages/enterprise",
|
||||
buildCommand: "bun run build:cloudflare",
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-PvIx2g1J5QIUIzkz2ABaAM4K/k/+xlBPDUExoOJNNuo=",
|
||||
"aarch64-linux": "sha256-YTAL+P13L5hgNJdDSiBED/UNa5zdTntnUUYDYL+Jdzo=",
|
||||
"aarch64-darwin": "sha256-y2VCJifYAp+H0lpDcJ0QfKNMG00Q/usFElaUIpdc8Vs=",
|
||||
"x86_64-darwin": "sha256-yz8edIlqLp06Y95ad8YjKz5azP7YATPle4TcDx6lM+U="
|
||||
"x86_64-linux": "sha256-Spc0qzg4he0d0GwHcqj7uBmvK4DIF1tEbsZ6M+pxpWc=",
|
||||
"aarch64-linux": "sha256-gwz/PKBbT+72hr7vUG28cdx4Z7/Sf06PNMr9JBjAYg0=",
|
||||
"aarch64-darwin": "sha256-Lj8p9P/QzLqxiM1OVSwcbtTsms8AcW3A6H0575ERufw=",
|
||||
"x86_64-darwin": "sha256-y0e+TnXj6wKDqSC5hQAWjpKadaFvL6GJ6Mba5anBM+Y="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ stdenvNoCC.mkDerivation {
|
||||
--filter './packages/opencode' \
|
||||
--filter './packages/desktop' \
|
||||
--filter './packages/app' \
|
||||
--filter './packages/shared' \
|
||||
--frozen-lockfile \
|
||||
--ignore-scripts \
|
||||
--no-progress
|
||||
|
||||
+4
-1
@@ -34,6 +34,8 @@
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@opentui/core": "0.1.99",
|
||||
"@opentui/solid": "0.1.99",
|
||||
"ulid": "3.0.1",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@types/luxon": "3.7.1",
|
||||
@@ -51,7 +53,7 @@
|
||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||
"effect": "4.0.0-beta.48",
|
||||
"ai": "6.0.158",
|
||||
"ai": "6.0.168",
|
||||
"cross-spawn": "7.0.6",
|
||||
"hono": "4.10.7",
|
||||
"hono-openapi": "1.1.2",
|
||||
@@ -87,6 +89,7 @@
|
||||
"glob": "13.0.5",
|
||||
"husky": "9.1.7",
|
||||
"oxlint": "1.60.0",
|
||||
"oxlint-tsgolint": "0.21.0",
|
||||
"prettier": "3.6.2",
|
||||
"semver": "^7.6.0",
|
||||
"sst": "3.18.10",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.14",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -180,8 +180,8 @@ describe("SerializeAddon", () => {
|
||||
await writeAndWait(term, input)
|
||||
|
||||
const origLine = term.buffer.active.getLine(0)
|
||||
const origFg = origLine!.getCell(0)!.getFgColor()
|
||||
const origBg = origLine!.getCell(0)!.getBgColor()
|
||||
const _origFg = origLine!.getCell(0)!.getFgColor()
|
||||
const _origBg = origLine!.getCell(0)!.getBgColor()
|
||||
expect(origLine!.getCell(0)!.isBold()).toBe(1)
|
||||
|
||||
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
|
||||
|
||||
+19
-15
@@ -10,7 +10,7 @@ import { ThemeProvider } from "@opencode-ai/ui/theme/context"
|
||||
import { MetaProvider } from "@solidjs/meta"
|
||||
import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
|
||||
import { type Duration, Effect } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import {
|
||||
type Component,
|
||||
createMemo,
|
||||
@@ -121,10 +121,10 @@ function SessionProviders(props: ParentProps) {
|
||||
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
||||
return (
|
||||
<AppShellProviders>
|
||||
<Suspense fallback={<Loading />}>
|
||||
{props.appChildren}
|
||||
{props.children}
|
||||
</Suspense>
|
||||
{/*<Suspense fallback={<Loading />}>*/}
|
||||
{props.appChildren}
|
||||
{props.children}
|
||||
{/*</Suspense>*/}
|
||||
</AppShellProviders>
|
||||
)
|
||||
}
|
||||
@@ -156,11 +156,6 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
||||
)
|
||||
}
|
||||
|
||||
const effectMinDuration =
|
||||
(duration: Duration.Input) =>
|
||||
<A, E, R>(e: Effect.Effect<A, E, R>) =>
|
||||
Effect.all([e, Effect.sleep(duration)], { concurrency: "unbounded" }).pipe(Effect.map((v) => v[0]))
|
||||
|
||||
function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
||||
const server = useServer()
|
||||
const checkServerHealth = useCheckServerHealth()
|
||||
@@ -189,32 +184,41 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
||||
)
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
|
||||
<Suspense
|
||||
fallback={
|
||||
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
||||
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{/*<Show
|
||||
when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
|
||||
fallback={
|
||||
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
||||
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||
</div>
|
||||
}
|
||||
>*/}
|
||||
{checkMode() === "blocking" ? startupHealthCheck() : startupHealthCheck.latest}
|
||||
<Show
|
||||
when={startupHealthCheck()}
|
||||
fallback={
|
||||
<ConnectionError
|
||||
onRetry={() => {
|
||||
if (checkMode() === "background") healthCheckActions.refetch()
|
||||
if (checkMode() === "background") void healthCheckActions.refetch()
|
||||
}}
|
||||
onServerSelected={(key) => {
|
||||
setCheckMode("blocking")
|
||||
server.setActive(key)
|
||||
healthCheckActions.refetch()
|
||||
void healthCheckActions.refetch()
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</Show>
|
||||
</Show>
|
||||
{/*</Show>*/}
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -327,7 +327,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
if (loading()) return
|
||||
if (methods().length === 1) {
|
||||
auto = true
|
||||
selectMethod(0)
|
||||
void selectMethod(0)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -373,7 +373,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
key={(m) => m?.label}
|
||||
onSelect={async (selected, index) => {
|
||||
if (!selected) return
|
||||
selectMethod(index)
|
||||
void selectMethod(index)
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
|
||||
@@ -348,8 +348,8 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
||||
|
||||
const open = (path: string) => {
|
||||
const value = file.tab(path)
|
||||
tabs().open(value)
|
||||
file.load(path)
|
||||
void tabs().open(value)
|
||||
void file.load(path)
|
||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
layout.fileTree.setTab("all")
|
||||
props.onOpenFile?.(path)
|
||||
|
||||
@@ -344,7 +344,7 @@ export function DialogSelectServer() {
|
||||
|
||||
createEffect(() => {
|
||||
items()
|
||||
refreshHealth()
|
||||
void refreshHealth()
|
||||
const interval = setInterval(refreshHealth, 10_000)
|
||||
onCleanup(() => clearInterval(interval))
|
||||
})
|
||||
@@ -498,7 +498,7 @@ export function DialogSelectServer() {
|
||||
async function handleRemove(url: ServerConnection.Key) {
|
||||
server.remove(url)
|
||||
if ((await platform.getDefaultServer?.()) === url) {
|
||||
platform.setDefaultServer?.(null)
|
||||
void platform.setDefaultServer?.(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,7 +536,7 @@ export function DialogSelectServer() {
|
||||
items={sortedItems}
|
||||
key={(x) => x.http.url}
|
||||
onSelect={(x) => {
|
||||
if (x) select(x)
|
||||
if (x) void select(x)
|
||||
}}
|
||||
divider={true}
|
||||
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
Switch,
|
||||
untrack,
|
||||
type ComponentProps,
|
||||
type JSXElement,
|
||||
type ParentProps,
|
||||
} from "solid-js"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
|
||||
@@ -54,6 +54,8 @@ import { PromptImageAttachments } from "./prompt-input/image-attachments"
|
||||
import { PromptDragOverlay } from "./prompt-input/drag-overlay"
|
||||
import { promptPlaceholder } from "./prompt-input/placeholder"
|
||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||
import { useQuery } from "@tanstack/solid-query"
|
||||
import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"
|
||||
|
||||
interface PromptInputProps {
|
||||
class?: string
|
||||
@@ -100,6 +102,7 @@ const NON_EMPTY_TEXT = /[^\s\u200B]/
|
||||
|
||||
export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const sdk = useSDK()
|
||||
|
||||
const sync = useSync()
|
||||
const local = useLocal()
|
||||
const files = useFile()
|
||||
@@ -212,9 +215,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
layout.fileTree.setTab("all")
|
||||
const tab = files.tab(item.path)
|
||||
tabs().open(tab)
|
||||
void tabs().open(tab)
|
||||
tabs().setActive(tab)
|
||||
Promise.resolve(files.load(item.path)).finally(() => queueCommentFocus())
|
||||
void Promise.resolve(files.load(item.path)).finally(() => queueCommentFocus())
|
||||
}
|
||||
|
||||
const recent = createMemo(() => {
|
||||
@@ -1139,7 +1142,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
|
||||
if (working()) {
|
||||
abort()
|
||||
void abort()
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
@@ -1205,7 +1208,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
return
|
||||
}
|
||||
if (working()) {
|
||||
abort()
|
||||
void abort()
|
||||
event.preventDefault()
|
||||
}
|
||||
return
|
||||
@@ -1245,10 +1248,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
) {
|
||||
return
|
||||
}
|
||||
handleSubmit(event)
|
||||
void handleSubmit(event)
|
||||
}
|
||||
}
|
||||
|
||||
const agentsQuery = useQuery(() => loadAgentsQuery(sdk.directory))
|
||||
const agentsLoading = () => agentsQuery.isLoading
|
||||
|
||||
const globalProvidersQuery = useQuery(() => loadProvidersQuery(null))
|
||||
const providersQuery = useQuery(() => loadProvidersQuery(sdk.directory))
|
||||
|
||||
const providersLoading = () => agentsLoading() || providersQuery.isLoading || globalProvidersQuery.isLoading
|
||||
|
||||
return (
|
||||
<div class="relative size-full _max-h-[320px] flex flex-col gap-0">
|
||||
<PromptPopover
|
||||
@@ -1444,53 +1455,89 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
|
||||
<div class="size-4 shrink-0" />
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 min-w-0 flex-1">
|
||||
<div data-component="prompt-agent-control">
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.agent.cycle")}
|
||||
keybind={command.keybind("agent.cycle")}
|
||||
>
|
||||
<Select
|
||||
size="normal"
|
||||
options={agentNames()}
|
||||
current={local.agent.current()?.name ?? ""}
|
||||
onSelect={(value) => {
|
||||
local.agent.set(value)
|
||||
restoreFocus()
|
||||
}}
|
||||
class="capitalize max-w-[160px] text-text-base"
|
||||
valueClass="truncate text-13-regular text-text-base"
|
||||
triggerStyle={control()}
|
||||
triggerProps={{ "data-action": "prompt-agent" }}
|
||||
variant="ghost"
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<Show when={store.mode !== "shell"}>
|
||||
<div data-component="prompt-model-control">
|
||||
<Show
|
||||
when={providers.paid().length > 0}
|
||||
fallback={
|
||||
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7">
|
||||
<Show when={!agentsLoading()}>
|
||||
<div data-component="prompt-agent-control">
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.agent.cycle")}
|
||||
keybind={command.keybind("agent.cycle")}
|
||||
>
|
||||
<Select
|
||||
size="normal"
|
||||
options={agentNames()}
|
||||
current={local.agent.current()?.name ?? ""}
|
||||
onSelect={(value) => {
|
||||
local.agent.set(value)
|
||||
restoreFocus()
|
||||
}}
|
||||
class="capitalize max-w-[160px] text-text-base"
|
||||
valueClass="truncate text-13-regular text-text-base"
|
||||
triggerStyle={control()}
|
||||
triggerProps={{ "data-action": "prompt-agent" }}
|
||||
variant="ghost"
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!providersLoading()}>
|
||||
<Show when={store.mode !== "shell"}>
|
||||
<div data-component="prompt-model-control">
|
||||
<Show
|
||||
when={providers.paid().length > 0}
|
||||
fallback={
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.model.choose")}
|
||||
keybind={command.keybind("model.choose")}
|
||||
>
|
||||
<Button
|
||||
data-action="prompt-model"
|
||||
as="div"
|
||||
variant="ghost"
|
||||
size="normal"
|
||||
class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
|
||||
style={control()}
|
||||
onClick={() => {
|
||||
void import("@/components/dialog-select-model-unpaid").then((x) => {
|
||||
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />)
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon
|
||||
id={local.model.current()?.provider?.id ?? ""}
|
||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||
/>
|
||||
</Show>
|
||||
<span class="truncate">
|
||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||
</span>
|
||||
<Icon name="chevron-down" size="small" class="shrink-0" />
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
}
|
||||
>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.model.choose")}
|
||||
keybind={command.keybind("model.choose")}
|
||||
>
|
||||
<Button
|
||||
data-action="prompt-model"
|
||||
as="div"
|
||||
variant="ghost"
|
||||
size="normal"
|
||||
class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
|
||||
style={control()}
|
||||
onClick={() => {
|
||||
void import("@/components/dialog-select-model-unpaid").then((x) => {
|
||||
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />)
|
||||
})
|
||||
<ModelSelectorPopover
|
||||
model={local.model}
|
||||
triggerAs={Button}
|
||||
triggerProps={{
|
||||
variant: "ghost",
|
||||
size: "normal",
|
||||
style: control(),
|
||||
class: "min-w-0 max-w-[320px] text-13-regular text-text-base group",
|
||||
"data-action": "prompt-model",
|
||||
}}
|
||||
onClose={restoreFocus}
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon
|
||||
@@ -1503,67 +1550,35 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||
</span>
|
||||
<Icon name="chevron-down" size="small" class="shrink-0" />
|
||||
</Button>
|
||||
</ModelSelectorPopover>
|
||||
</TooltipKeybind>
|
||||
}
|
||||
>
|
||||
</Show>
|
||||
</div>
|
||||
<div data-component="prompt-variant-control">
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.model.choose")}
|
||||
keybind={command.keybind("model.choose")}
|
||||
title={language.t("command.model.variant.cycle")}
|
||||
keybind={command.keybind("model.variant.cycle")}
|
||||
>
|
||||
<ModelSelectorPopover
|
||||
model={local.model}
|
||||
triggerAs={Button}
|
||||
triggerProps={{
|
||||
variant: "ghost",
|
||||
size: "normal",
|
||||
style: control(),
|
||||
class: "min-w-0 max-w-[320px] text-13-regular text-text-base group",
|
||||
"data-action": "prompt-model",
|
||||
<Select
|
||||
size="normal"
|
||||
options={variants()}
|
||||
current={local.model.variant.current() ?? "default"}
|
||||
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
||||
onSelect={(value) => {
|
||||
local.model.variant.set(value === "default" ? undefined : value)
|
||||
restoreFocus()
|
||||
}}
|
||||
onClose={restoreFocus}
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon
|
||||
id={local.model.current()?.provider?.id ?? ""}
|
||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||
/>
|
||||
</Show>
|
||||
<span class="truncate">
|
||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||
</span>
|
||||
<Icon name="chevron-down" size="small" class="shrink-0" />
|
||||
</ModelSelectorPopover>
|
||||
class="capitalize max-w-[160px] text-text-base"
|
||||
valueClass="truncate text-13-regular text-text-base"
|
||||
triggerStyle={control()}
|
||||
triggerProps={{ "data-action": "prompt-model-variant" }}
|
||||
variant="ghost"
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
</div>
|
||||
<div data-component="prompt-variant-control">
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.model.variant.cycle")}
|
||||
keybind={command.keybind("model.variant.cycle")}
|
||||
>
|
||||
<Select
|
||||
size="normal"
|
||||
options={variants()}
|
||||
current={local.model.variant.current() ?? "default"}
|
||||
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
||||
onSelect={(value) => {
|
||||
local.model.variant.set(value === "default" ? undefined : value)
|
||||
restoreFocus()
|
||||
}}
|
||||
class="capitalize max-w-[160px] text-text-base"
|
||||
valueClass="truncate text-13-regular text-text-base"
|
||||
triggerStyle={control()}
|
||||
triggerProps={{ "data-action": "prompt-model-variant" }}
|
||||
variant="ghost"
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -295,7 +295,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
const mode = input.mode()
|
||||
|
||||
if (text.trim().length === 0 && images.length === 0 && input.commentCount() === 0) {
|
||||
if (input.working()) abort()
|
||||
if (input.working()) void abort()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ function openSessionContext(args: {
|
||||
}) {
|
||||
if (!args.view.reviewPanel.opened()) args.view.reviewPanel.open()
|
||||
if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all")
|
||||
args.tabs.open("context")
|
||||
void args.tabs.open("context")
|
||||
args.tabs.setActive("context")
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||
import { createEffect, createMemo, For, Show } from "solid-js"
|
||||
import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Portal } from "solid-js/web"
|
||||
import { useCommand } from "@/context/command"
|
||||
@@ -16,6 +16,7 @@ import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useServer } from "@/context/server"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useTerminal } from "@/context/terminal"
|
||||
import { focusTerminalById } from "@/pages/session/helpers"
|
||||
@@ -134,6 +135,7 @@ export function SessionHeader() {
|
||||
const server = useServer()
|
||||
const platform = usePlatform()
|
||||
const language = useLanguage()
|
||||
const settings = useSettings()
|
||||
const sync = useSync()
|
||||
const terminal = useTerminal()
|
||||
const { params, view } = useSessionLayout()
|
||||
@@ -151,6 +153,11 @@ export function SessionHeader() {
|
||||
})
|
||||
const hotkey = createMemo(() => command.keybind("file.open"))
|
||||
const os = createMemo(() => detectOS(platform))
|
||||
const isDesktopBeta = platform.platform === "desktop" && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"
|
||||
const search = createMemo(() => !isDesktopBeta || settings.general.showSearch())
|
||||
const tree = createMemo(() => !isDesktopBeta || settings.general.showFileTree())
|
||||
const term = createMemo(() => !isDesktopBeta || settings.general.showTerminal())
|
||||
const status = createMemo(() => !isDesktopBeta || settings.general.showStatus())
|
||||
|
||||
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
|
||||
finder: true,
|
||||
@@ -262,12 +269,16 @@ export function SessionHeader() {
|
||||
.catch((err: unknown) => showRequestError(language, err))
|
||||
}
|
||||
|
||||
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
|
||||
const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
|
||||
const [centerMount, setCenterMount] = createSignal<HTMLElement | null>(null)
|
||||
const [rightMount, setRightMount] = createSignal<HTMLElement | null>(null)
|
||||
onMount(() => {
|
||||
setCenterMount(document.getElementById("opencode-titlebar-center"))
|
||||
setRightMount(document.getElementById("opencode-titlebar-right"))
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Show when={centerMount()}>
|
||||
<Show when={search() && centerMount()}>
|
||||
{(mount) => (
|
||||
<Portal mount={mount()}>
|
||||
<Button
|
||||
@@ -415,24 +426,28 @@ export function SessionHeader() {
|
||||
</div>
|
||||
</Show>
|
||||
<div class="flex items-center gap-1">
|
||||
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
|
||||
<StatusPopover />
|
||||
</Tooltip>
|
||||
<TooltipKeybind
|
||||
title={language.t("command.terminal.toggle")}
|
||||
keybind={command.keybind("terminal.toggle")}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border shrink-0"
|
||||
onClick={toggleTerminal}
|
||||
aria-label={language.t("command.terminal.toggle")}
|
||||
aria-expanded={view().terminal.opened()}
|
||||
aria-controls="terminal-panel"
|
||||
<Show when={status()}>
|
||||
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
|
||||
<StatusPopover />
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<Show when={term()}>
|
||||
<TooltipKeybind
|
||||
title={language.t("command.terminal.toggle")}
|
||||
keybind={command.keybind("terminal.toggle")}
|
||||
>
|
||||
<Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border shrink-0"
|
||||
onClick={toggleTerminal}
|
||||
aria-label={language.t("command.terminal.toggle")}
|
||||
aria-expanded={view().terminal.opened()}
|
||||
aria-controls="terminal-panel"
|
||||
>
|
||||
<Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
|
||||
<div class="hidden md:flex items-center gap-1 shrink-0">
|
||||
<TooltipKeybind
|
||||
@@ -451,30 +466,32 @@ export function SessionHeader() {
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
|
||||
<TooltipKeybind
|
||||
title={language.t("command.fileTree.toggle")}
|
||||
keybind={command.keybind("fileTree.toggle")}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="titlebar-icon w-8 h-6 p-0 box-border"
|
||||
onClick={() => layout.fileTree.toggle()}
|
||||
aria-label={language.t("command.fileTree.toggle")}
|
||||
aria-expanded={layout.fileTree.opened()}
|
||||
aria-controls="file-tree-panel"
|
||||
<Show when={tree()}>
|
||||
<TooltipKeybind
|
||||
title={language.t("command.fileTree.toggle")}
|
||||
keybind={command.keybind("fileTree.toggle")}
|
||||
>
|
||||
<div class="relative flex items-center justify-center size-4">
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"}
|
||||
classList={{
|
||||
"text-icon-strong": layout.fileTree.opened(),
|
||||
"text-icon-weak": !layout.fileTree.opened(),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="titlebar-icon w-8 h-6 p-0 box-border"
|
||||
onClick={() => layout.fileTree.toggle()}
|
||||
aria-label={language.t("command.fileTree.toggle")}
|
||||
aria-expanded={layout.fileTree.opened()}
|
||||
aria-controls="file-tree-panel"
|
||||
>
|
||||
<div class="relative flex items-center justify-center size-4">
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"}
|
||||
classList={{
|
||||
"text-icon-strong": layout.fileTree.opened(),
|
||||
"text-icon-weak": !layout.fileTree.opened(),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
||||
|
||||
const close = () => {
|
||||
const count = terminal.all().length
|
||||
terminal.close(props.terminal.id)
|
||||
void terminal.close(props.terminal.id)
|
||||
if (count === 1) {
|
||||
props.onClose?.()
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ export const SettingsGeneral: Component = () => {
|
||||
|
||||
permission.disableAutoAccept(params.id, value)
|
||||
}
|
||||
const desktop = createMemo(() => platform.platform === "desktop")
|
||||
|
||||
const check = () => {
|
||||
if (!platform.checkUpdate) return
|
||||
@@ -279,6 +280,74 @@ export const SettingsGeneral: Component = () => {
|
||||
</div>
|
||||
)
|
||||
|
||||
const AdvancedSection = () => (
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.advanced")}</h3>
|
||||
|
||||
<SettingsList>
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.showFileTree.title")}
|
||||
description={language.t("settings.general.row.showFileTree.description")}
|
||||
>
|
||||
<div data-action="settings-show-file-tree">
|
||||
<Switch
|
||||
checked={settings.general.showFileTree()}
|
||||
onChange={(checked) => settings.general.setShowFileTree(checked)}
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.showNavigation.title")}
|
||||
description={language.t("settings.general.row.showNavigation.description")}
|
||||
>
|
||||
<div data-action="settings-show-navigation">
|
||||
<Switch
|
||||
checked={settings.general.showNavigation()}
|
||||
onChange={(checked) => settings.general.setShowNavigation(checked)}
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.showSearch.title")}
|
||||
description={language.t("settings.general.row.showSearch.description")}
|
||||
>
|
||||
<div data-action="settings-show-search">
|
||||
<Switch
|
||||
checked={settings.general.showSearch()}
|
||||
onChange={(checked) => settings.general.setShowSearch(checked)}
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.showTerminal.title")}
|
||||
description={language.t("settings.general.row.showTerminal.description")}
|
||||
>
|
||||
<div data-action="settings-show-terminal">
|
||||
<Switch
|
||||
checked={settings.general.showTerminal()}
|
||||
onChange={(checked) => settings.general.setShowTerminal(checked)}
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.showStatus.title")}
|
||||
description={language.t("settings.general.row.showStatus.description")}
|
||||
>
|
||||
<div data-action="settings-show-status">
|
||||
<Switch
|
||||
checked={settings.general.showStatus()}
|
||||
onChange={(checked) => settings.general.setShowStatus(checked)}
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
</SettingsList>
|
||||
</div>
|
||||
)
|
||||
|
||||
const AppearanceSection = () => (
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.appearance")}</h3>
|
||||
@@ -527,6 +596,7 @@ export const SettingsGeneral: Component = () => {
|
||||
</div>
|
||||
)
|
||||
|
||||
console.log(import.meta.env)
|
||||
return (
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||
@@ -609,6 +679,10 @@ export const SettingsGeneral: Component = () => {
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
|
||||
<Show when={desktop() && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"}>
|
||||
<AdvancedSection />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -191,7 +191,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
const scrollY = typeof local.pty.scrollY === "number" ? local.pty.scrollY : undefined
|
||||
let ws: WebSocket | undefined
|
||||
let term: Term | undefined
|
||||
let ghostty: Ghostty
|
||||
let _ghostty: Ghostty
|
||||
let serializeAddon: SerializeAddon
|
||||
let fitAddon: FitAddon
|
||||
let handleResize: () => void
|
||||
@@ -372,7 +372,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
cleanup()
|
||||
return
|
||||
}
|
||||
ghostty = g
|
||||
_ghostty = g
|
||||
term = t
|
||||
output = terminalWriter((data, done) =>
|
||||
t.write(data, () => {
|
||||
@@ -415,7 +415,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
if (local.autoFocus !== false) focusTerminal()
|
||||
|
||||
if (typeof document !== "undefined" && document.fonts) {
|
||||
document.fonts.ready.then(scheduleFit)
|
||||
void document.fonts.ready.then(scheduleFit)
|
||||
}
|
||||
|
||||
const onResize = t.onResize((size) => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useLayout } from "@/context/layout"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { applyPath, backPath, forwardPath } from "./titlebar-history"
|
||||
|
||||
type TauriDesktopWindow = {
|
||||
@@ -40,6 +41,7 @@ export function Titlebar() {
|
||||
const platform = usePlatform()
|
||||
const command = useCommand()
|
||||
const language = useLanguage()
|
||||
const settings = useSettings()
|
||||
const theme = useTheme()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
@@ -78,6 +80,7 @@ export function Titlebar() {
|
||||
const canBack = createMemo(() => history.index > 0)
|
||||
const canForward = createMemo(() => history.index < history.stack.length - 1)
|
||||
const hasProjects = createMemo(() => layout.projects.list().length > 0)
|
||||
const nav = createMemo(() => import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" || settings.general.showNavigation())
|
||||
|
||||
const back = () => {
|
||||
const next = backPath(history)
|
||||
@@ -252,41 +255,47 @@ export function Titlebar() {
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={hasProjects()}>
|
||||
<div
|
||||
class="flex items-center gap-0 transition-transform"
|
||||
classList={{
|
||||
"translate-x-0": !layout.sidebar.opened(),
|
||||
"-translate-x-[36px]": layout.sidebar.opened(),
|
||||
"duration-180 ease-out": !layout.sidebar.opened(),
|
||||
"duration-180 ease-in": layout.sidebar.opened(),
|
||||
}}
|
||||
>
|
||||
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="chevron-left"
|
||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||
disabled={!canBack()}
|
||||
onClick={back}
|
||||
aria-label={language.t("common.goBack")}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="chevron-right"
|
||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||
disabled={!canForward()}
|
||||
onClick={forward}
|
||||
aria-label={language.t("common.goForward")}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
class="flex items-center shrink-0"
|
||||
classList={{
|
||||
"-translate-x-[36px]": layout.sidebar.opened() && !!params.dir,
|
||||
"duration-180 ease-out": !layout.sidebar.opened(),
|
||||
"duration-180 ease-in": layout.sidebar.opened(),
|
||||
}}
|
||||
>
|
||||
<Show when={hasProjects() && nav()}>
|
||||
<div class="flex items-center gap-0 transition-transform">
|
||||
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="chevron-left"
|
||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||
disabled={!canBack()}
|
||||
onClick={back}
|
||||
aria-label={language.t("common.goBack")}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="chevron-right"
|
||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||
disabled={!canForward()}
|
||||
onClick={forward}
|
||||
aria-label={language.t("common.goForward")}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Show>
|
||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||
{["beta", "dev"].includes(import.meta.env.VITE_OPENCODE_CHANNEL) && (
|
||||
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
|
||||
{import.meta.env.VITE_OPENCODE_CHANNEL.toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex items-center justify-center pointer-events-none">
|
||||
|
||||
@@ -128,6 +128,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
||||
if (started) return run
|
||||
started = true
|
||||
run = (async () => {
|
||||
// oxlint-disable-next-line no-unmodified-loop-condition -- `started` is set to false by stop() which also aborts; both flags are checked to allow graceful exit
|
||||
while (!abort.signal.aborted && started) {
|
||||
attempt = new AbortController()
|
||||
lastEventAt = Date.now()
|
||||
|
||||
@@ -26,6 +26,7 @@ import type { ProjectMeta } from "./global-sync/types"
|
||||
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
||||
import { sanitizeProject } from "./global-sync/utils"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
import { queryOptions, skipToken, useQueryClient } from "@tanstack/solid-query"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
@@ -41,6 +42,9 @@ type GlobalStore = {
|
||||
reload: undefined | "pending" | "complete"
|
||||
}
|
||||
|
||||
export const loadSessionsQuery = (directory: string) =>
|
||||
queryOptions<null>({ queryKey: [directory, "loadSessions"], queryFn: skipToken })
|
||||
|
||||
function createGlobalSync() {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const language = useLanguage()
|
||||
@@ -67,6 +71,7 @@ function createGlobalSync() {
|
||||
config: {},
|
||||
reload: undefined,
|
||||
})
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
let active = true
|
||||
let projectWritten = false
|
||||
@@ -198,46 +203,53 @@ function createGlobalSync() {
|
||||
}
|
||||
|
||||
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
|
||||
const promise = loadRootSessionsWithFallback({
|
||||
directory,
|
||||
limit,
|
||||
list: (query) => globalSDK.client.session.list(query),
|
||||
})
|
||||
.then((x) => {
|
||||
const nonArchived = (x.data ?? [])
|
||||
.filter((s) => !!s?.id)
|
||||
.filter((s) => !s.time?.archived)
|
||||
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
const limit = store.limit
|
||||
const childSessions = store.session.filter((s) => !!s.parentID)
|
||||
const sessions = trimSessions([...nonArchived, ...childSessions], {
|
||||
limit,
|
||||
permission: store.permission,
|
||||
})
|
||||
setStore(
|
||||
"sessionTotal",
|
||||
estimateRootSessionTotal({
|
||||
count: nonArchived.length,
|
||||
limit: x.limit,
|
||||
limited: x.limited,
|
||||
}),
|
||||
)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
||||
sessionMeta.set(directory, { limit })
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to load sessions", err)
|
||||
const project = getFilename(directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.session.listFailed.title", { project }),
|
||||
description: formatServerError(err, language.t),
|
||||
})
|
||||
const promise = queryClient
|
||||
.fetchQuery({
|
||||
...loadSessionsQuery(directory),
|
||||
queryFn: () =>
|
||||
loadRootSessionsWithFallback({
|
||||
directory,
|
||||
limit,
|
||||
list: (query) => globalSDK.client.session.list(query),
|
||||
})
|
||||
.then((x) => {
|
||||
const nonArchived = (x.data ?? [])
|
||||
.filter((s) => !!s?.id)
|
||||
.filter((s) => !s.time?.archived)
|
||||
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
const limit = store.limit
|
||||
const childSessions = store.session.filter((s) => !!s.parentID)
|
||||
const sessions = trimSessions([...nonArchived, ...childSessions], {
|
||||
limit,
|
||||
permission: store.permission,
|
||||
})
|
||||
setStore(
|
||||
"sessionTotal",
|
||||
estimateRootSessionTotal({
|
||||
count: nonArchived.length,
|
||||
limit: x.limit,
|
||||
limited: x.limited,
|
||||
}),
|
||||
)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
||||
sessionMeta.set(directory, { limit })
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to load sessions", err)
|
||||
const project = getFilename(directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.session.listFailed.title", { project }),
|
||||
description: formatServerError(err, language.t),
|
||||
})
|
||||
})
|
||||
.then(() => null),
|
||||
})
|
||||
.then(() => {})
|
||||
|
||||
sessionLoads.set(directory, promise)
|
||||
promise.finally(() => {
|
||||
void promise.finally(() => {
|
||||
sessionLoads.delete(directory)
|
||||
children.unpin(directory)
|
||||
})
|
||||
@@ -250,7 +262,7 @@ function createGlobalSync() {
|
||||
if (pending) return pending
|
||||
|
||||
children.pin(directory)
|
||||
const promise = (async () => {
|
||||
const promise = Promise.resolve().then(async () => {
|
||||
const child = children.ensureChild(directory)
|
||||
const cache = children.vcsCache.get(directory)
|
||||
if (!cache) return
|
||||
@@ -269,11 +281,12 @@ function createGlobalSync() {
|
||||
vcsCache: cache,
|
||||
loadSessions,
|
||||
translate: language.t,
|
||||
queryClient,
|
||||
})
|
||||
})()
|
||||
})
|
||||
|
||||
booting.set(directory, promise)
|
||||
promise.finally(() => {
|
||||
void promise.finally(() => {
|
||||
booting.delete(directory)
|
||||
children.unpin(directory)
|
||||
})
|
||||
@@ -317,7 +330,7 @@ function createGlobalSync() {
|
||||
setSessionTodo,
|
||||
vcsCache: children.vcsCache.get(directory),
|
||||
loadLsp: () => {
|
||||
sdkFor(directory)
|
||||
void sdkFor(directory)
|
||||
.lsp.status()
|
||||
.then((x) => {
|
||||
setStore("lsp", x.data ?? [])
|
||||
@@ -346,6 +359,7 @@ function createGlobalSync() {
|
||||
translate: language.t,
|
||||
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
||||
setGlobalStore: setBootStore,
|
||||
queryClient,
|
||||
})
|
||||
bootedAt = Date.now()
|
||||
} finally {
|
||||
@@ -359,13 +373,13 @@ function createGlobalSync() {
|
||||
eventFrame = undefined
|
||||
eventTimer = setTimeout(() => {
|
||||
eventTimer = undefined
|
||||
globalSDK.event.start()
|
||||
void globalSDK.event.start()
|
||||
}, 0)
|
||||
})
|
||||
} else {
|
||||
eventTimer = setTimeout(() => {
|
||||
eventTimer = undefined
|
||||
globalSDK.event.start()
|
||||
void globalSDK.event.start()
|
||||
}, 0)
|
||||
}
|
||||
void bootstrap()
|
||||
|
||||
@@ -18,6 +18,8 @@ import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||
import type { State, VcsCache } from "./types"
|
||||
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query"
|
||||
import { loadSessionsQuery } from "../global-sync"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
@@ -65,28 +67,13 @@ function runAll(list: Array<() => Promise<unknown>>) {
|
||||
return Promise.allSettled(list.map((item) => item()))
|
||||
}
|
||||
|
||||
function showErrors(input: {
|
||||
errors: unknown[]
|
||||
title: string
|
||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||
formatMoreCount: (count: number) => string
|
||||
}) {
|
||||
if (input.errors.length === 0) return
|
||||
const message = formatServerError(input.errors[0], input.translate)
|
||||
const more = input.errors.length > 1 ? input.formatMoreCount(input.errors.length - 1) : ""
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: input.title,
|
||||
description: message + more,
|
||||
})
|
||||
}
|
||||
|
||||
export async function bootstrapGlobal(input: {
|
||||
globalSDK: OpencodeClient
|
||||
requestFailedTitle: string
|
||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||
formatMoreCount: (count: number) => string
|
||||
setGlobalStore: SetStoreFunction<GlobalStore>
|
||||
queryClient: QueryClient
|
||||
}) {
|
||||
const fast = [
|
||||
() =>
|
||||
@@ -96,11 +83,16 @@ export async function bootstrapGlobal(input: {
|
||||
}),
|
||||
),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.globalSDK.provider.list().then((x) => {
|
||||
input.setGlobalStore("provider", normalizeProviderList(x.data!))
|
||||
}),
|
||||
),
|
||||
input.queryClient.fetchQuery({
|
||||
...loadProvidersQuery(null),
|
||||
queryFn: () =>
|
||||
retry(() =>
|
||||
input.globalSDK.provider.list().then((x) => {
|
||||
input.setGlobalStore("provider", normalizeProviderList(x.data!))
|
||||
return null
|
||||
}),
|
||||
),
|
||||
}),
|
||||
]
|
||||
|
||||
const slow = [
|
||||
@@ -188,6 +180,12 @@ function warmSessions(input: {
|
||||
).then(() => undefined)
|
||||
}
|
||||
|
||||
export const loadProvidersQuery = (directory: string | null) =>
|
||||
queryOptions<null>({ queryKey: [directory, "providers"], queryFn: skipToken })
|
||||
|
||||
export const loadAgentsQuery = (directory: string | null) =>
|
||||
queryOptions<null>({ queryKey: [directory, "agents"], queryFn: skipToken })
|
||||
|
||||
export async function bootstrapDirectory(input: {
|
||||
directory: string
|
||||
sdk: OpencodeClient
|
||||
@@ -202,6 +200,7 @@ export async function bootstrapDirectory(input: {
|
||||
project: Project[]
|
||||
provider: ProviderListResponse
|
||||
}
|
||||
queryClient: QueryClient
|
||||
}) {
|
||||
const loading = input.store.status !== "complete"
|
||||
const seededProject = projectID(input.directory, input.global.project)
|
||||
@@ -223,97 +222,7 @@ export async function bootstrapDirectory(input: {
|
||||
input.setStore("lsp", [])
|
||||
if (loading) input.setStore("status", "partial")
|
||||
|
||||
const fast = [
|
||||
() => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))),
|
||||
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
|
||||
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
||||
]
|
||||
|
||||
const slow = [
|
||||
() =>
|
||||
seededProject
|
||||
? Promise.resolve()
|
||||
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
|
||||
() =>
|
||||
seededPath
|
||||
? Promise.resolve()
|
||||
: retry(() =>
|
||||
input.sdk.path.get().then((x) => {
|
||||
input.setStore("path", x.data!)
|
||||
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
|
||||
if (next) input.setStore("project", next)
|
||||
}),
|
||||
),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.vcs.get().then((x) => {
|
||||
const next = x.data ?? input.store.vcs
|
||||
input.setStore("vcs", next)
|
||||
if (next) input.vcsCache.setStore("value", next)
|
||||
}),
|
||||
),
|
||||
() => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.permission.list().then((x) => {
|
||||
const ids = (x.data ?? []).map((perm) => perm?.sessionID).filter((id): id is string => !!id)
|
||||
const grouped = groupBySession(
|
||||
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
|
||||
)
|
||||
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
||||
batch(() => {
|
||||
for (const sessionID of Object.keys(input.store.permission)) {
|
||||
if (grouped[sessionID]) continue
|
||||
input.setStore("permission", sessionID, [])
|
||||
}
|
||||
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
||||
input.setStore(
|
||||
"permission",
|
||||
sessionID,
|
||||
reconcile(
|
||||
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.question.list().then((x) => {
|
||||
const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id)
|
||||
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
|
||||
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
||||
batch(() => {
|
||||
for (const sessionID of Object.keys(input.store.question)) {
|
||||
if (grouped[sessionID]) continue
|
||||
input.setStore("question", sessionID, [])
|
||||
}
|
||||
for (const [sessionID, questions] of Object.entries(grouped)) {
|
||||
input.setStore(
|
||||
"question",
|
||||
sessionID,
|
||||
reconcile(
|
||||
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.mcp.status().then((x) => {
|
||||
input.setStore("mcp", x.data!)
|
||||
input.setStore("mcp_ready", true)
|
||||
}),
|
||||
),
|
||||
]
|
||||
const fast = [() => Promise.resolve(input.loadSessions(input.directory))]
|
||||
|
||||
const errs = errors(await runAll(fast))
|
||||
if (errs.length > 0) {
|
||||
@@ -326,36 +235,138 @@ export async function bootstrapDirectory(input: {
|
||||
})
|
||||
}
|
||||
|
||||
await waitForPaint()
|
||||
const slowErrs = errors(await runAll(slow))
|
||||
if (slowErrs.length > 0) {
|
||||
console.error("Failed to finish bootstrap instance", slowErrs[0])
|
||||
const project = getFilename(input.directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||
description: formatServerError(slowErrs[0], input.translate),
|
||||
})
|
||||
}
|
||||
;(async () => {
|
||||
const slow = [
|
||||
() =>
|
||||
input.queryClient.ensureQueryData({
|
||||
...loadAgentsQuery(input.directory),
|
||||
queryFn: () =>
|
||||
retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))).then(
|
||||
() => null,
|
||||
),
|
||||
}),
|
||||
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
|
||||
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
||||
() =>
|
||||
seededProject
|
||||
? Promise.resolve()
|
||||
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
|
||||
() =>
|
||||
seededPath
|
||||
? Promise.resolve()
|
||||
: retry(() =>
|
||||
input.sdk.path.get().then((x) => {
|
||||
input.setStore("path", x.data!)
|
||||
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
|
||||
if (next) input.setStore("project", next)
|
||||
}),
|
||||
),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.vcs.get().then((x) => {
|
||||
const next = x.data ?? input.store.vcs
|
||||
input.setStore("vcs", next)
|
||||
if (next) input.vcsCache.setStore("value", next)
|
||||
}),
|
||||
),
|
||||
() => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.permission.list().then((x) => {
|
||||
const ids = (x.data ?? []).map((perm) => perm?.sessionID).filter((id): id is string => !!id)
|
||||
const grouped = groupBySession(
|
||||
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
|
||||
)
|
||||
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
||||
batch(() => {
|
||||
for (const sessionID of Object.keys(input.store.permission)) {
|
||||
if (grouped[sessionID]) continue
|
||||
input.setStore("permission", sessionID, [])
|
||||
}
|
||||
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
||||
input.setStore(
|
||||
"permission",
|
||||
sessionID,
|
||||
reconcile(
|
||||
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.question.list().then((x) => {
|
||||
const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id)
|
||||
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
|
||||
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
||||
batch(() => {
|
||||
for (const sessionID of Object.keys(input.store.question)) {
|
||||
if (grouped[sessionID]) continue
|
||||
input.setStore("question", sessionID, [])
|
||||
}
|
||||
for (const [sessionID, questions] of Object.entries(grouped)) {
|
||||
input.setStore(
|
||||
"question",
|
||||
sessionID,
|
||||
reconcile(
|
||||
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.mcp.status().then((x) => {
|
||||
input.setStore("mcp", x.data!)
|
||||
input.setStore("mcp_ready", true)
|
||||
}),
|
||||
),
|
||||
]
|
||||
|
||||
if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
|
||||
|
||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||
providerRev.set(input.directory, rev)
|
||||
void retry(() => input.sdk.provider.list())
|
||||
.then((x) => {
|
||||
if (providerRev.get(input.directory) !== rev) return
|
||||
input.setStore("provider", normalizeProviderList(x.data!))
|
||||
input.setStore("provider_ready", true)
|
||||
})
|
||||
.catch((err) => {
|
||||
if (providerRev.get(input.directory) !== rev) return
|
||||
console.error("Failed to refresh provider list", err)
|
||||
await waitForPaint()
|
||||
const slowErrs = errors(await runAll(slow))
|
||||
if (slowErrs.length > 0) {
|
||||
console.error("Failed to finish bootstrap instance", slowErrs[0])
|
||||
const project = getFilename(input.directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||
description: formatServerError(err, input.translate),
|
||||
description: formatServerError(slowErrs[0], input.translate),
|
||||
})
|
||||
}
|
||||
|
||||
if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
|
||||
|
||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||
providerRev.set(input.directory, rev)
|
||||
void input.queryClient.ensureQueryData({
|
||||
...loadSessionsQuery(input.directory),
|
||||
queryFn: () =>
|
||||
retry(() => input.sdk.provider.list())
|
||||
.then((x) => {
|
||||
if (providerRev.get(input.directory) !== rev) return
|
||||
input.setStore("provider", normalizeProviderList(x.data!))
|
||||
input.setStore("provider_ready", true)
|
||||
})
|
||||
.catch((err) => {
|
||||
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
|
||||
const project = getFilename(input.directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||
description: formatServerError(err, input.translate),
|
||||
})
|
||||
})
|
||||
.then(() => null),
|
||||
})
|
||||
})()
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
Part,
|
||||
Path,
|
||||
PermissionRequest,
|
||||
Project,
|
||||
ProviderListResponse,
|
||||
QuestionRequest,
|
||||
Session,
|
||||
|
||||
@@ -582,7 +582,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
open(directory: string) {
|
||||
const root = rootFor(directory)
|
||||
if (server.projects.list().find((x) => x.worktree === root)) return
|
||||
globalSync.project.loadSessions(root)
|
||||
void globalSync.project.loadSessions(root)
|
||||
server.projects.open(root)
|
||||
},
|
||||
close(directory: string) {
|
||||
|
||||
@@ -23,6 +23,11 @@ export interface Settings {
|
||||
autoSave: boolean
|
||||
releaseNotes: boolean
|
||||
followup: "queue" | "steer"
|
||||
showFileTree: boolean
|
||||
showNavigation: boolean
|
||||
showSearch: boolean
|
||||
showStatus: boolean
|
||||
showTerminal: boolean
|
||||
showReasoningSummaries: boolean
|
||||
shellToolPartsExpanded: boolean
|
||||
editToolPartsExpanded: boolean
|
||||
@@ -89,6 +94,11 @@ const defaultSettings: Settings = {
|
||||
autoSave: true,
|
||||
releaseNotes: true,
|
||||
followup: "steer",
|
||||
showFileTree: false,
|
||||
showNavigation: false,
|
||||
showSearch: false,
|
||||
showStatus: false,
|
||||
showTerminal: false,
|
||||
showReasoningSummaries: false,
|
||||
shellToolPartsExpanded: false,
|
||||
editToolPartsExpanded: false,
|
||||
@@ -162,6 +172,26 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
setFollowup(value: "queue" | "steer") {
|
||||
setStore("general", "followup", value === "queue" ? "steer" : value)
|
||||
},
|
||||
showFileTree: withFallback(() => store.general?.showFileTree, defaultSettings.general.showFileTree),
|
||||
setShowFileTree(value: boolean) {
|
||||
setStore("general", "showFileTree", value)
|
||||
},
|
||||
showNavigation: withFallback(() => store.general?.showNavigation, defaultSettings.general.showNavigation),
|
||||
setShowNavigation(value: boolean) {
|
||||
setStore("general", "showNavigation", value)
|
||||
},
|
||||
showSearch: withFallback(() => store.general?.showSearch, defaultSettings.general.showSearch),
|
||||
setShowSearch(value: boolean) {
|
||||
setStore("general", "showSearch", value)
|
||||
},
|
||||
showStatus: withFallback(() => store.general?.showStatus, defaultSettings.general.showStatus),
|
||||
setShowStatus(value: boolean) {
|
||||
setStore("general", "showStatus", value)
|
||||
},
|
||||
showTerminal: withFallback(() => store.general?.showTerminal, defaultSettings.general.showTerminal),
|
||||
setShowTerminal(value: boolean) {
|
||||
setStore("general", "showTerminal", value)
|
||||
},
|
||||
showReasoningSummaries: withFallback(
|
||||
() => store.general?.showReasoningSummaries,
|
||||
defaultSettings.general.showReasoningSummaries,
|
||||
|
||||
@@ -117,7 +117,7 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat
|
||||
entry?.value.clear()
|
||||
}
|
||||
|
||||
removePersisted(Persist.workspace(dir, "terminal"), platform)
|
||||
void removePersisted(Persist.workspace(dir, "terminal"), platform)
|
||||
|
||||
const legacy = new Set(getLegacyTerminalStorageKeys(dir))
|
||||
for (const id of sessionIDs ?? []) {
|
||||
@@ -126,7 +126,7 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat
|
||||
}
|
||||
}
|
||||
for (const key of legacy) {
|
||||
removePersisted({ key }, platform)
|
||||
void removePersisted({ key }, platform)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+2
-3
@@ -1,15 +1,14 @@
|
||||
import "solid-js"
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_OPENCODE_SERVER_HOST: string
|
||||
readonly VITE_OPENCODE_SERVER_PORT: string
|
||||
readonly VITE_OPENCODE_CHANNEL?: "dev" | "beta" | "prod"
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
|
||||
declare module "solid-js" {
|
||||
export declare module "solid-js" {
|
||||
namespace JSX {
|
||||
interface Directives {
|
||||
sortable: true
|
||||
|
||||
@@ -719,6 +719,7 @@ export const dict = {
|
||||
"settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
|
||||
|
||||
"settings.general.section.appearance": "Appearance",
|
||||
"settings.general.section.advanced": "Advanced",
|
||||
"settings.general.section.notifications": "System notifications",
|
||||
"settings.general.section.updates": "Updates",
|
||||
"settings.general.section.sounds": "Sound effects",
|
||||
@@ -741,6 +742,16 @@ export const dict = {
|
||||
"settings.general.row.followup.description": "Choose whether follow-up prompts steer immediately or wait in a queue",
|
||||
"settings.general.row.followup.option.queue": "Queue",
|
||||
"settings.general.row.followup.option.steer": "Steer",
|
||||
"settings.general.row.showFileTree.title": "File tree",
|
||||
"settings.general.row.showFileTree.description": "Show the file tree toggle and panel in desktop sessions",
|
||||
"settings.general.row.showNavigation.title": "Navigation controls",
|
||||
"settings.general.row.showNavigation.description": "Show the back and forward buttons in the desktop title bar",
|
||||
"settings.general.row.showSearch.title": "Command palette",
|
||||
"settings.general.row.showSearch.description": "Show the search and command palette button in the desktop title bar",
|
||||
"settings.general.row.showTerminal.title": "Terminal",
|
||||
"settings.general.row.showTerminal.description": "Show the terminal button in the desktop title bar",
|
||||
"settings.general.row.showStatus.title": "Server status",
|
||||
"settings.general.row.showStatus.description": "Show the server status button in the desktop title bar",
|
||||
"settings.general.row.reasoningSummaries.title": "Show reasoning summaries",
|
||||
"settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline",
|
||||
"settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts",
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import { dict as en } from "./en"
|
||||
|
||||
type Keys = keyof typeof en
|
||||
|
||||
export const dict = {
|
||||
"command.category.suggested": "추천",
|
||||
"command.category.view": "보기",
|
||||
|
||||
+190
-184
@@ -13,7 +13,7 @@ import {
|
||||
type Accessor,
|
||||
} from "solid-js"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||
import { useLayout, LocalProject } from "@/context/layout"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
@@ -127,14 +127,17 @@ export default function Layout(props: ParentProps) {
|
||||
const theme = useTheme()
|
||||
const language = useLanguage()
|
||||
const initialDirectory = decode64(params.dir)
|
||||
const location = useLocation()
|
||||
const route = createMemo(() => {
|
||||
const slug = params.dir
|
||||
if (!slug) return { slug, dir: "" }
|
||||
const dir = decode64(slug)
|
||||
if (!dir) return { slug, dir: "" }
|
||||
const store = globalSync.peek(dir, { bootstrap: false })
|
||||
return {
|
||||
slug,
|
||||
dir: globalSync.peek(dir, { bootstrap: false })[0].path.directory || dir,
|
||||
store,
|
||||
dir: store[0].path.directory || dir,
|
||||
}
|
||||
})
|
||||
const availableThemeEntries = createMemo(() => theme.ids().map((id) => [id, theme.themes()[id]] as const))
|
||||
@@ -956,7 +959,7 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
// warm up child store to prevent flicker
|
||||
globalSync.child(target.worktree)
|
||||
openProject(target.worktree)
|
||||
void openProject(target.worktree)
|
||||
}
|
||||
|
||||
function navigateSessionByUnseen(offset: number) {
|
||||
@@ -1094,7 +1097,7 @@ export default function Layout(props: ParentProps) {
|
||||
disabled: !params.dir || !params.id,
|
||||
onSelect: () => {
|
||||
const session = currentSessions().find((s) => s.id === params.id)
|
||||
if (session) archiveSession(session)
|
||||
if (session) void archiveSession(session)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1360,11 +1363,11 @@ export default function Layout(props: ParentProps) {
|
||||
if (!server.isLocal()) return
|
||||
|
||||
for (const directory of collectOpenProjectDeepLinks(urls)) {
|
||||
openProject(directory)
|
||||
void openProject(directory)
|
||||
}
|
||||
|
||||
for (const link of collectNewSessionDeepLinks(urls)) {
|
||||
openProject(link.directory, false)
|
||||
void openProject(link.directory, false)
|
||||
const slug = base64Encode(link.directory)
|
||||
if (link.prompt) {
|
||||
setSessionHandoff(slug, { prompt: link.prompt })
|
||||
@@ -1453,11 +1456,11 @@ export default function Layout(props: ParentProps) {
|
||||
function resolve(result: string | string[] | null) {
|
||||
if (Array.isArray(result)) {
|
||||
for (const directory of result) {
|
||||
openProject(directory, false)
|
||||
void openProject(directory, false)
|
||||
}
|
||||
navigateToProject(result[0])
|
||||
void navigateToProject(result[0])
|
||||
} else if (result) {
|
||||
openProject(result)
|
||||
void openProject(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1825,7 +1828,7 @@ export default function Layout(props: ParentProps) {
|
||||
const next = new Set(dirs)
|
||||
for (const directory of next) {
|
||||
if (loadedSessionDirs.has(directory)) continue
|
||||
globalSync.project.loadSessions(directory)
|
||||
void globalSync.project.loadSessions(directory)
|
||||
}
|
||||
|
||||
loadedSessionDirs.clear()
|
||||
@@ -2100,196 +2103,198 @@ export default function Layout(props: ParentProps) {
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<>
|
||||
<div class="shrink-0 pl-1 py-1">
|
||||
<div class="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0">
|
||||
<div class="flex flex-col min-w-0">
|
||||
<InlineEditor
|
||||
id={`project:${projectId()}`}
|
||||
value={projectName}
|
||||
onSave={(next) => {
|
||||
const item = project()
|
||||
if (!item) return
|
||||
renameProject(item, next)
|
||||
}}
|
||||
class="text-14-medium text-text-strong truncate"
|
||||
displayClass="text-14-medium text-text-strong truncate"
|
||||
stopPropagation
|
||||
/>
|
||||
{(project) => (
|
||||
<>
|
||||
<div class="shrink-0 pl-1 py-1">
|
||||
<div class="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0">
|
||||
<div class="flex flex-col min-w-0">
|
||||
<InlineEditor
|
||||
id={`project:${projectId()}`}
|
||||
value={projectName}
|
||||
onSave={(next) => {
|
||||
const item = project()
|
||||
if (!item) return
|
||||
void renameProject(item, next)
|
||||
}}
|
||||
class="text-14-medium text-text-strong truncate"
|
||||
displayClass="text-14-medium text-text-strong truncate"
|
||||
stopPropagation
|
||||
/>
|
||||
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
gutter={2}
|
||||
value={worktree()}
|
||||
class="shrink-0"
|
||||
contentStyle={{
|
||||
"max-width": "640px",
|
||||
transform: "translate3d(52px, 0, 0)",
|
||||
}}
|
||||
>
|
||||
<span class="text-12-regular text-text-base truncate select-text">
|
||||
{worktree().replace(homedir(), "~")}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
gutter={2}
|
||||
value={worktree()}
|
||||
class="shrink-0"
|
||||
contentStyle={{
|
||||
"max-width": "640px",
|
||||
transform: "translate3d(52px, 0, 0)",
|
||||
}}
|
||||
>
|
||||
<span class="text-12-regular text-text-base truncate select-text">
|
||||
{worktree().replace(homedir(), "~")}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<DropdownMenu modal={!sidebarHovering()}>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
data-action="project-menu"
|
||||
data-project={slug()}
|
||||
class="shrink-0 size-6 rounded-md transition-opacity data-[expanded]:bg-surface-base-active"
|
||||
classList={{
|
||||
"opacity-100": panelProps.mobile || merged(),
|
||||
"opacity-0 group-hover/project:opacity-100 group-focus-within/project:opacity-100 data-[expanded]:opacity-100":
|
||||
!panelProps.mobile && !merged(),
|
||||
}}
|
||||
aria-label={language.t("common.moreOptions")}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="mt-1">
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
const item = project()
|
||||
if (!item) return
|
||||
showEditProjectDialog(item)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
data-action="project-workspaces-toggle"
|
||||
data-project={slug()}
|
||||
disabled={!canToggle()}
|
||||
onSelect={() => {
|
||||
const item = project()
|
||||
if (!item) return
|
||||
toggleProjectWorkspaces(item)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{workspacesEnabled()
|
||||
? language.t("sidebar.workspaces.disable")
|
||||
: language.t("sidebar.workspaces.enable")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
data-action="project-clear-notifications"
|
||||
data-project={slug()}
|
||||
disabled={unseenCount() === 0}
|
||||
onSelect={clearNotifications}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("sidebar.project.clearNotifications")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
data-action="project-close-menu"
|
||||
data-project={slug()}
|
||||
onSelect={() => {
|
||||
const dir = worktree()
|
||||
if (!dir) return
|
||||
closeProject(dir)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<DropdownMenu modal={!sidebarHovering()}>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
data-action="project-menu"
|
||||
data-project={slug()}
|
||||
class="shrink-0 size-6 rounded-md transition-opacity data-[expanded]:bg-surface-base-active"
|
||||
classList={{
|
||||
"opacity-100": panelProps.mobile || merged(),
|
||||
"opacity-0 group-hover/project:opacity-100 group-focus-within/project:opacity-100 data-[expanded]:opacity-100":
|
||||
!panelProps.mobile && !merged(),
|
||||
}}
|
||||
aria-label={language.t("common.moreOptions")}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="mt-1">
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
const item = project()
|
||||
if (!item) return
|
||||
showEditProjectDialog(item)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
data-action="project-workspaces-toggle"
|
||||
data-project={slug()}
|
||||
disabled={!canToggle()}
|
||||
onSelect={() => {
|
||||
const item = project()
|
||||
if (!item) return
|
||||
toggleProjectWorkspaces(item)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{workspacesEnabled()
|
||||
? language.t("sidebar.workspaces.disable")
|
||||
: language.t("sidebar.workspaces.enable")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
data-action="project-clear-notifications"
|
||||
data-project={slug()}
|
||||
disabled={unseenCount() === 0}
|
||||
onSelect={clearNotifications}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("sidebar.project.clearNotifications")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
data-action="project-close-menu"
|
||||
data-project={slug()}
|
||||
onSelect={() => {
|
||||
const dir = worktree()
|
||||
if (!dir) return
|
||||
closeProject(dir)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-h-0 flex flex-col">
|
||||
<Show
|
||||
when={workspacesEnabled()}
|
||||
fallback={
|
||||
<div class="flex-1 min-h-0 flex flex-col">
|
||||
<Show
|
||||
when={workspacesEnabled()}
|
||||
fallback={
|
||||
<>
|
||||
<div class="shrink-0 py-4">
|
||||
<Button
|
||||
size="large"
|
||||
icon="new-session"
|
||||
class="w-full"
|
||||
onClick={() => {
|
||||
const dir = worktree()
|
||||
if (!dir) return
|
||||
navigateWithSidebarReset(`/${base64Encode(dir)}/session`)
|
||||
}}
|
||||
>
|
||||
{language.t("command.session.new")}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex-1 min-h-0">
|
||||
<LocalWorkspace
|
||||
ctx={workspaceSidebarCtx}
|
||||
project={project()}
|
||||
sortNow={sortNow}
|
||||
mobile={panelProps.mobile}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<>
|
||||
<div class="shrink-0 py-4">
|
||||
<Button
|
||||
size="large"
|
||||
icon="new-session"
|
||||
icon="plus-small"
|
||||
class="w-full"
|
||||
onClick={() => {
|
||||
const dir = worktree()
|
||||
if (!dir) return
|
||||
navigateWithSidebarReset(`/${base64Encode(dir)}/session`)
|
||||
const item = project()
|
||||
if (!item) return
|
||||
void createWorkspace(item)
|
||||
}}
|
||||
>
|
||||
{language.t("command.session.new")}
|
||||
{language.t("workspace.new")}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex-1 min-h-0">
|
||||
<LocalWorkspace
|
||||
ctx={workspaceSidebarCtx}
|
||||
project={project()!}
|
||||
sortNow={sortNow}
|
||||
mobile={panelProps.mobile}
|
||||
/>
|
||||
<div class="relative flex-1 min-h-0">
|
||||
<DragDropProvider
|
||||
onDragStart={handleWorkspaceDragStart}
|
||||
onDragEnd={handleWorkspaceDragEnd}
|
||||
onDragOver={handleWorkspaceDragOver}
|
||||
collisionDetector={closestCenter}
|
||||
>
|
||||
<DragDropSensors />
|
||||
<ConstrainDragXAxis />
|
||||
<div
|
||||
ref={(el) => {
|
||||
if (!panelProps.mobile) scrollContainerRef = el
|
||||
}}
|
||||
class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
||||
>
|
||||
<SortableProvider ids={workspaces()}>
|
||||
<For each={workspaces()}>
|
||||
{(directory) => (
|
||||
<SortableWorkspace
|
||||
ctx={workspaceSidebarCtx}
|
||||
directory={directory}
|
||||
project={project()}
|
||||
sortNow={sortNow}
|
||||
mobile={panelProps.mobile}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</SortableProvider>
|
||||
</div>
|
||||
<DragOverlay>
|
||||
<WorkspaceDragOverlay
|
||||
sidebarProject={sidebarProject}
|
||||
activeWorkspace={() => store.activeWorkspace}
|
||||
workspaceLabel={workspaceLabel}
|
||||
/>
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<>
|
||||
<div class="shrink-0 py-4">
|
||||
<Button
|
||||
size="large"
|
||||
icon="plus-small"
|
||||
class="w-full"
|
||||
onClick={() => {
|
||||
const item = project()
|
||||
if (!item) return
|
||||
createWorkspace(item)
|
||||
}}
|
||||
>
|
||||
{language.t("workspace.new")}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="relative flex-1 min-h-0">
|
||||
<DragDropProvider
|
||||
onDragStart={handleWorkspaceDragStart}
|
||||
onDragEnd={handleWorkspaceDragEnd}
|
||||
onDragOver={handleWorkspaceDragOver}
|
||||
collisionDetector={closestCenter}
|
||||
>
|
||||
<DragDropSensors />
|
||||
<ConstrainDragXAxis />
|
||||
<div
|
||||
ref={(el) => {
|
||||
if (!panelProps.mobile) scrollContainerRef = el
|
||||
}}
|
||||
class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
||||
>
|
||||
<SortableProvider ids={workspaces()}>
|
||||
<For each={workspaces()}>
|
||||
{(directory) => (
|
||||
<SortableWorkspace
|
||||
ctx={workspaceSidebarCtx}
|
||||
directory={directory}
|
||||
project={project()!}
|
||||
sortNow={sortNow}
|
||||
mobile={panelProps.mobile}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</SortableProvider>
|
||||
</div>
|
||||
<DragOverlay>
|
||||
<WorkspaceDragOverlay
|
||||
sidebarProject={sidebarProject}
|
||||
activeWorkspace={() => store.activeWorkspace}
|
||||
workspaceLabel={workspaceLabel}
|
||||
/>
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
</div>
|
||||
</>
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<div
|
||||
@@ -2355,6 +2360,7 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
return (
|
||||
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
||||
{autoselecting() ?? ""}
|
||||
<Titlebar />
|
||||
<div class="flex-1 min-h-0 min-w-0 flex">
|
||||
<div class="flex-1 min-h-0 relative">
|
||||
|
||||
@@ -14,10 +14,11 @@ import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { type LocalProject } from "@/context/layout"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { loadSessionsQuery, useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
|
||||
import { sortedRootSessions, workspaceKey } from "./helpers"
|
||||
import { useQuery } from "@tanstack/solid-query"
|
||||
|
||||
type InlineEditorComponent = (props: {
|
||||
id: string
|
||||
@@ -277,7 +278,7 @@ const WorkspaceSessionList = (props: {
|
||||
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-2 pr-10"
|
||||
size="large"
|
||||
onClick={(e: MouseEvent) => {
|
||||
props.loadMore()
|
||||
void props.loadMore()
|
||||
;(e.currentTarget as HTMLButtonElement).blur()
|
||||
}}
|
||||
>
|
||||
@@ -316,12 +317,11 @@ export const SortableWorkspace = (props: {
|
||||
})
|
||||
const open = createMemo(() => props.ctx.workspaceExpanded(props.directory, local()))
|
||||
const boot = createMemo(() => open() || active())
|
||||
const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false)
|
||||
const count = createMemo(() => sessions()?.length ?? 0)
|
||||
const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
|
||||
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) }))
|
||||
const busy = createMemo(() => props.ctx.isBusy(props.directory))
|
||||
const wasBusy = createMemo((prev) => prev || busy(), false)
|
||||
const loading = createMemo(() => open() && !booted() && count() === 0 && !wasBusy())
|
||||
const loading = () => query.isLoading
|
||||
const touch = createMediaQuery("(hover: none)")
|
||||
const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id)))
|
||||
const loadMore = async () => {
|
||||
@@ -426,7 +426,7 @@ export const SortableWorkspace = (props: {
|
||||
mobile={props.mobile}
|
||||
ctx={props.ctx}
|
||||
showNew={showNew}
|
||||
loading={loading}
|
||||
loading={() => query.isLoading && count() === 0}
|
||||
sessions={sessions}
|
||||
hasMore={hasMore}
|
||||
loadMore={loadMore}
|
||||
@@ -452,10 +452,10 @@ export const LocalWorkspace = (props: {
|
||||
})
|
||||
const slug = createMemo(() => base64Encode(props.project.worktree))
|
||||
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
|
||||
const booted = createMemo((prev) => prev || workspace().store.status === "complete", false)
|
||||
const count = createMemo(() => sessions()?.length ?? 0)
|
||||
const loading = createMemo(() => !booted() && count() === 0)
|
||||
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) }))
|
||||
const hasMore = createMemo(() => workspace().store.sessionTotal > count())
|
||||
const loading = () => query.isLoading && count() === 0
|
||||
const loadMore = async () => {
|
||||
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
|
||||
await globalSync.project.loadSessions(props.project.worktree)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Project, UserMessage, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
||||
import type { Project, UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useMutation } from "@tanstack/solid-query"
|
||||
import { createQuery, skipToken, useMutation, useQueryClient } from "@tanstack/solid-query"
|
||||
import {
|
||||
batch,
|
||||
onCleanup,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
on,
|
||||
onMount,
|
||||
untrack,
|
||||
createResource,
|
||||
} from "solid-js"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
@@ -323,6 +324,7 @@ export default function Page() {
|
||||
const local = useLocal()
|
||||
const file = useFile()
|
||||
const sync = useSync()
|
||||
const queryClient = useQueryClient()
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const sdk = useSDK()
|
||||
@@ -432,8 +434,6 @@ export default function Page() {
|
||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||
const isChildSession = createMemo(() => !!info()?.parentID)
|
||||
const diffs = createMemo(() => (params.id ? list(sync.data.session_diff[params.id]) : []))
|
||||
const sessionCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
|
||||
const hasSessionReview = createMemo(() => sessionCount() > 0)
|
||||
const canReview = createMemo(() => !!sync.project)
|
||||
const reviewTab = createMemo(() => isDesktop())
|
||||
const tabState = createSessionTabs({
|
||||
@@ -443,8 +443,6 @@ export default function Page() {
|
||||
review: reviewTab,
|
||||
hasReview: canReview,
|
||||
})
|
||||
const contextOpen = tabState.contextOpen
|
||||
const openedTabs = tabState.openedTabs
|
||||
const activeTab = tabState.activeTab
|
||||
const activeFileTab = tabState.activeFileTab
|
||||
const revertMessageID = createMemo(() => info()?.revert?.messageID)
|
||||
@@ -487,7 +485,7 @@ export default function Page() {
|
||||
if (!tab) return
|
||||
|
||||
const path = file.pathFromTab(tab)
|
||||
if (path) file.load(path)
|
||||
if (path) void file.load(path)
|
||||
})
|
||||
|
||||
createEffect(
|
||||
@@ -521,26 +519,6 @@ export default function Page() {
|
||||
deferRender: false,
|
||||
})
|
||||
|
||||
const [vcs, setVcs] = createStore<{
|
||||
diff: {
|
||||
git: VcsFileDiff[]
|
||||
branch: VcsFileDiff[]
|
||||
}
|
||||
ready: {
|
||||
git: boolean
|
||||
branch: boolean
|
||||
}
|
||||
}>({
|
||||
diff: {
|
||||
git: [] as VcsFileDiff[],
|
||||
branch: [] as VcsFileDiff[],
|
||||
},
|
||||
ready: {
|
||||
git: false,
|
||||
branch: false,
|
||||
},
|
||||
})
|
||||
|
||||
const [followup, setFollowup] = persisted(
|
||||
Persist.workspace(sdk.directory, "followup", ["followup.v1"]),
|
||||
createStore<{
|
||||
@@ -574,68 +552,6 @@ export default function Page() {
|
||||
let todoTimer: number | undefined
|
||||
let diffFrame: number | undefined
|
||||
let diffTimer: number | undefined
|
||||
const vcsTask = new Map<VcsMode, Promise<void>>()
|
||||
const vcsRun = new Map<VcsMode, number>()
|
||||
|
||||
const bumpVcs = (mode: VcsMode) => {
|
||||
const next = (vcsRun.get(mode) ?? 0) + 1
|
||||
vcsRun.set(mode, next)
|
||||
return next
|
||||
}
|
||||
|
||||
const resetVcs = (mode?: VcsMode) => {
|
||||
const list = mode ? [mode] : (["git", "branch"] as const)
|
||||
list.forEach((item) => {
|
||||
bumpVcs(item)
|
||||
vcsTask.delete(item)
|
||||
setVcs("diff", item, [])
|
||||
setVcs("ready", item, false)
|
||||
})
|
||||
}
|
||||
|
||||
const loadVcs = (mode: VcsMode, force = false) => {
|
||||
if (sync.project?.vcs !== "git") return Promise.resolve()
|
||||
if (!force && vcs.ready[mode]) return Promise.resolve()
|
||||
|
||||
if (force) {
|
||||
if (vcsTask.has(mode)) bumpVcs(mode)
|
||||
vcsTask.delete(mode)
|
||||
setVcs("ready", mode, false)
|
||||
}
|
||||
|
||||
const current = vcsTask.get(mode)
|
||||
if (current) return current
|
||||
|
||||
const run = bumpVcs(mode)
|
||||
|
||||
const task = sdk.client.vcs
|
||||
.diff({ mode })
|
||||
.then((result) => {
|
||||
if (vcsRun.get(mode) !== run) return
|
||||
setVcs("diff", mode, list(result.data))
|
||||
setVcs("ready", mode, true)
|
||||
})
|
||||
.catch((error) => {
|
||||
if (vcsRun.get(mode) !== run) return
|
||||
console.debug("[session-review] failed to load vcs diff", { mode, error })
|
||||
setVcs("diff", mode, [])
|
||||
setVcs("ready", mode, true)
|
||||
})
|
||||
.finally(() => {
|
||||
if (vcsTask.get(mode) === task) vcsTask.delete(mode)
|
||||
})
|
||||
|
||||
vcsTask.set(mode, task)
|
||||
return task
|
||||
}
|
||||
|
||||
const refreshVcs = () => {
|
||||
resetVcs()
|
||||
const mode = untrack(vcsMode)
|
||||
if (!mode) return
|
||||
if (!untrack(wantsReview)) return
|
||||
void loadVcs(mode, true)
|
||||
}
|
||||
|
||||
createComputed((prev) => {
|
||||
const open = desktopReviewOpen()
|
||||
@@ -666,21 +582,52 @@ export default function Page() {
|
||||
list.push("turn")
|
||||
return list
|
||||
})
|
||||
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
|
||||
const wantsReview = createMemo(() =>
|
||||
isDesktop()
|
||||
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
|
||||
: store.mobileTab === "changes",
|
||||
)
|
||||
const vcsMode = createMemo<VcsMode | undefined>(() => {
|
||||
if (store.changes === "git" || store.changes === "branch") return store.changes
|
||||
})
|
||||
const reviewDiffs = createMemo(() => {
|
||||
if (store.changes === "git") return list(vcs.diff.git)
|
||||
if (store.changes === "branch") return list(vcs.diff.branch)
|
||||
const vcsKey = createMemo(
|
||||
() => ["session-vcs", sdk.directory, sync.data.vcs?.branch ?? "", sync.data.vcs?.default_branch ?? ""] as const,
|
||||
)
|
||||
const vcsQuery = createQuery(() => {
|
||||
const mode = vcsMode()
|
||||
const enabled = wantsReview() && sync.project?.vcs === "git"
|
||||
|
||||
return {
|
||||
queryKey: [...vcsKey(), mode] as const,
|
||||
enabled,
|
||||
staleTime: Number.POSITIVE_INFINITY,
|
||||
gcTime: 60 * 1000,
|
||||
queryFn: mode
|
||||
? () =>
|
||||
sdk.client.vcs
|
||||
.diff({ mode })
|
||||
.then((result) => list(result.data))
|
||||
.catch((error) => {
|
||||
console.debug("[session-review] failed to load vcs diff", { mode, error })
|
||||
return []
|
||||
})
|
||||
: skipToken,
|
||||
}
|
||||
})
|
||||
const refreshVcs = () => void queryClient.invalidateQueries({ queryKey: vcsKey() })
|
||||
const reviewDiffs = () => {
|
||||
if (store.changes === "git" || store.changes === "branch")
|
||||
// avoids suspense
|
||||
return vcsQuery.isFetched ? (vcsQuery.data ?? []) : []
|
||||
return turnDiffs()
|
||||
})
|
||||
const reviewCount = createMemo(() => reviewDiffs().length)
|
||||
const hasReview = createMemo(() => reviewCount() > 0)
|
||||
const reviewReady = createMemo(() => {
|
||||
if (store.changes === "git") return vcs.ready.git
|
||||
if (store.changes === "branch") return vcs.ready.branch
|
||||
}
|
||||
const reviewCount = () => reviewDiffs().length
|
||||
const hasReview = () => reviewCount() > 0
|
||||
const reviewReady = () => {
|
||||
if (store.changes === "git" || store.changes === "branch") return !vcsQuery.isPending
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
const newSessionWorktree = createMemo(() => {
|
||||
if (store.newSessionWorktree === "create") return "create"
|
||||
@@ -808,8 +755,9 @@ export default function Page() {
|
||||
|
||||
const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs
|
||||
|
||||
createEffect(
|
||||
on([() => sdk.directory, () => params.id] as const, ([, id]) => {
|
||||
const [sessionSync] = createResource(
|
||||
() => [sdk.directory, params.id] as const,
|
||||
([directory, id]) => {
|
||||
if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame)
|
||||
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
|
||||
refreshFrame = undefined
|
||||
@@ -820,13 +768,10 @@ export default function Page() {
|
||||
const stale = !cached
|
||||
? false
|
||||
: (() => {
|
||||
const info = getSessionPrefetch(sdk.directory, id)
|
||||
const info = getSessionPrefetch(directory, id)
|
||||
if (!info) return true
|
||||
return Date.now() - info.at > SESSION_PREFETCH_TTL
|
||||
})()
|
||||
untrack(() => {
|
||||
void sync.session.sync(id)
|
||||
})
|
||||
|
||||
refreshFrame = requestAnimationFrame(() => {
|
||||
refreshFrame = undefined
|
||||
@@ -838,7 +783,9 @@ export default function Page() {
|
||||
})
|
||||
}, 0)
|
||||
})
|
||||
}),
|
||||
|
||||
return sync.session.sync(id)
|
||||
},
|
||||
)
|
||||
|
||||
createEffect(
|
||||
@@ -900,27 +847,6 @@ export default function Page() {
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => sdk.directory,
|
||||
() => {
|
||||
resetVcs()
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => [sync.data.vcs?.branch, sync.data.vcs?.default_branch] as const,
|
||||
(next, prev) => {
|
||||
if (prev === undefined || same(next, prev)) return
|
||||
refreshVcs()
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const stopVcs = sdk.event.listen((evt) => {
|
||||
if (evt.details.type !== "file.watcher.updated") return
|
||||
const props =
|
||||
@@ -1054,13 +980,6 @@ export default function Page() {
|
||||
}
|
||||
}
|
||||
|
||||
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
|
||||
const wantsReview = createMemo(() =>
|
||||
isDesktop()
|
||||
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
|
||||
: store.mobileTab === "changes",
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
const list = changesOptions()
|
||||
if (list.includes(store.changes)) return
|
||||
@@ -1069,22 +988,12 @@ export default function Page() {
|
||||
setStore("changes", next)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const mode = vcsMode()
|
||||
if (!mode) return
|
||||
if (!wantsReview()) return
|
||||
void loadVcs(mode)
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => sync.data.session_status[params.id ?? ""]?.type,
|
||||
(next, prev) => {
|
||||
const mode = vcsMode()
|
||||
if (!mode) return
|
||||
if (!wantsReview()) return
|
||||
if (next !== "idle" || prev === undefined || prev === "idle") return
|
||||
void loadVcs(mode, true)
|
||||
refreshVcs()
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
@@ -1885,6 +1794,7 @@ export default function Page() {
|
||||
|
||||
return (
|
||||
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
|
||||
{sessionSync() ?? ""}
|
||||
<SessionHeader />
|
||||
<div class="flex-1 min-h-0 flex flex-col md:flex-row">
|
||||
<Show when={!isDesktop() && !!params.id}>
|
||||
|
||||
@@ -378,12 +378,6 @@ export function FileTabContent(props: { tab: string }) {
|
||||
requestAnimationFrame(() => comments.clearFocus())
|
||||
})
|
||||
|
||||
const cancelCommenting = () => {
|
||||
const p = path()
|
||||
if (p) file.setSelectedLines(p, null)
|
||||
setNote("commenting", null)
|
||||
}
|
||||
|
||||
let prev = {
|
||||
loaded: false,
|
||||
ready: false,
|
||||
|
||||
@@ -117,7 +117,7 @@ export const createOpenReviewFile = (input: {
|
||||
input.openTab(tab)
|
||||
input.setActive(tab)
|
||||
}
|
||||
if (maybePromise instanceof Promise) maybePromise.then(open)
|
||||
if (maybePromise instanceof Promise) void maybePromise.then(open)
|
||||
else open()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ import { useCommand } from "@/context/command"
|
||||
import { useFile, type SelectedLineRange } from "@/context/file"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
||||
import { FileTabContent } from "@/pages/session/file-tabs"
|
||||
import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
|
||||
@@ -39,6 +42,9 @@ export function SessionSidePanel(props: {
|
||||
size: Sizing
|
||||
}) {
|
||||
const layout = useLayout()
|
||||
const platform = usePlatform()
|
||||
const settings = useSettings()
|
||||
const sync = useSync()
|
||||
const file = useFile()
|
||||
const language = useLanguage()
|
||||
const command = useCommand()
|
||||
@@ -46,9 +52,15 @@ export function SessionSidePanel(props: {
|
||||
const { sessionKey, tabs, view } = useSessionLayout()
|
||||
|
||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||
const shown = createMemo(
|
||||
() =>
|
||||
platform.platform !== "desktop" ||
|
||||
import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" ||
|
||||
settings.general.showFileTree(),
|
||||
)
|
||||
|
||||
const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
||||
const fileOpen = createMemo(() => isDesktop() && layout.fileTree.opened())
|
||||
const fileOpen = createMemo(() => isDesktop() && shown() && layout.fileTree.opened())
|
||||
const open = createMemo(() => reviewOpen() || fileOpen())
|
||||
const reviewTab = createMemo(() => isDesktop())
|
||||
const panelWidth = createMemo(() => {
|
||||
@@ -341,98 +353,99 @@ export function SessionSidePanel(props: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="file-tree-panel"
|
||||
aria-hidden={!fileOpen()}
|
||||
inert={!fileOpen()}
|
||||
class="relative min-w-0 h-full shrink-0 overflow-hidden"
|
||||
classList={{
|
||||
"pointer-events-none": !fileOpen(),
|
||||
"transition-[width] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
|
||||
!props.size.active(),
|
||||
}}
|
||||
style={{ width: treeWidth() }}
|
||||
>
|
||||
<Show when={shown()}>
|
||||
<div
|
||||
class="h-full flex flex-col overflow-hidden group/filetree"
|
||||
classList={{ "border-l border-border-weaker-base": reviewOpen() }}
|
||||
id="file-tree-panel"
|
||||
aria-hidden={!fileOpen()}
|
||||
inert={!fileOpen()}
|
||||
class="relative min-w-0 h-full shrink-0 overflow-hidden"
|
||||
classList={{
|
||||
"pointer-events-none": !fileOpen(),
|
||||
"transition-[width] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
|
||||
!props.size.active(),
|
||||
}}
|
||||
style={{ width: treeWidth() }}
|
||||
>
|
||||
<Tabs
|
||||
variant="pill"
|
||||
value={fileTreeTab()}
|
||||
onChange={setFileTreeTabValue}
|
||||
class="h-full"
|
||||
data-scope="filetree"
|
||||
<div
|
||||
class="h-full flex flex-col overflow-hidden group/filetree"
|
||||
classList={{ "border-l border-border-weaker-base": reviewOpen() }}
|
||||
>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
|
||||
{props.reviewCount()}{" "}
|
||||
{language.t(
|
||||
props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
|
||||
)}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
|
||||
{language.t("session.files.all")}
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
|
||||
<Switch>
|
||||
<Match when={props.hasReview() || !props.diffsReady()}>
|
||||
<Show
|
||||
when={props.diffsReady()}
|
||||
fallback={
|
||||
<div class="px-2 py-2 text-12-regular text-text-weak">
|
||||
{language.t("common.loading")}
|
||||
{language.t("common.loading.ellipsis")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tabs
|
||||
variant="pill"
|
||||
value={fileTreeTab()}
|
||||
onChange={setFileTreeTabValue}
|
||||
class="h-full"
|
||||
data-scope="filetree"
|
||||
>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
|
||||
{props.reviewCount()}{" "}
|
||||
{language.t(
|
||||
props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
|
||||
)}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
|
||||
{language.t("session.files.all")}
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
|
||||
<Switch>
|
||||
<Match when={props.hasReview() || !props.diffsReady()}>
|
||||
<Show
|
||||
when={props.diffsReady()}
|
||||
fallback={
|
||||
<div class="px-2 py-2 text-12-regular text-text-weak">
|
||||
{language.t("common.loading")}
|
||||
{language.t("common.loading.ellipsis")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FileTree
|
||||
path=""
|
||||
class="pt-3"
|
||||
allowed={diffFiles()}
|
||||
kinds={kinds()}
|
||||
draggable={false}
|
||||
active={props.activeDiff}
|
||||
onFileClick={(node) => props.focusReviewDiff(node.path)}
|
||||
/>
|
||||
</Show>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
|
||||
<Switch>
|
||||
<Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match>
|
||||
<Match when={true}>
|
||||
<FileTree
|
||||
path=""
|
||||
class="pt-3"
|
||||
allowed={diffFiles()}
|
||||
modified={diffFiles()}
|
||||
kinds={kinds()}
|
||||
draggable={false}
|
||||
active={props.activeDiff}
|
||||
onFileClick={(node) => props.focusReviewDiff(node.path)}
|
||||
onFileClick={(node) => openTab(file.tab(node.path))}
|
||||
/>
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={true}>{empty(props.empty())}</Match>
|
||||
</Switch>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
|
||||
<Switch>
|
||||
<Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match>
|
||||
<Match when={true}>
|
||||
<FileTree
|
||||
path=""
|
||||
class="pt-3"
|
||||
modified={diffFiles()}
|
||||
kinds={kinds()}
|
||||
onFileClick={(node) => openTab(file.tab(node.path))}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Tabs.Content>
|
||||
</Tabs>
|
||||
</div>
|
||||
<Show when={fileOpen()}>
|
||||
<div onPointerDown={() => props.size.start()}>
|
||||
<ResizeHandle
|
||||
direction="horizontal"
|
||||
edge="start"
|
||||
size={layout.fileTree.width()}
|
||||
min={200}
|
||||
max={480}
|
||||
onResize={(width) => {
|
||||
props.size.touch()
|
||||
layout.fileTree.resize(width)
|
||||
}}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Tabs.Content>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={fileOpen()}>
|
||||
<div onPointerDown={() => props.size.start()}>
|
||||
<ResizeHandle
|
||||
direction="horizontal"
|
||||
edge="start"
|
||||
size={layout.fileTree.width()}
|
||||
min={200}
|
||||
max={480}
|
||||
onResize={(width) => {
|
||||
props.size.touch()
|
||||
layout.fileTree.resize(width)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</aside>
|
||||
</Show>
|
||||
|
||||
@@ -7,8 +7,10 @@ import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { usePrompt } from "@/context/prompt"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useTerminal } from "@/context/terminal"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
@@ -39,8 +41,10 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
const language = useLanguage()
|
||||
const local = useLocal()
|
||||
const permission = usePermission()
|
||||
const platform = usePlatform()
|
||||
const prompt = usePrompt()
|
||||
const sdk = useSDK()
|
||||
const settings = useSettings()
|
||||
const sync = useSync()
|
||||
const terminal = useTerminal()
|
||||
const layout = useLayout()
|
||||
@@ -66,6 +70,10 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
})
|
||||
const activeFileTab = tabState.activeFileTab
|
||||
const closableTab = tabState.closableTab
|
||||
const shown = () =>
|
||||
platform.platform !== "desktop" ||
|
||||
import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" ||
|
||||
settings.general.showFileTree()
|
||||
|
||||
const idle = { type: "idle" as const }
|
||||
const status = () => sync.data.session_status[params.id ?? ""] ?? idle
|
||||
@@ -457,12 +465,16 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
keybind: "mod+shift+r",
|
||||
onSelect: () => view().reviewPanel.toggle(),
|
||||
}),
|
||||
viewCommand({
|
||||
id: "fileTree.toggle",
|
||||
title: language.t("command.fileTree.toggle"),
|
||||
keybind: "mod+\\",
|
||||
onSelect: () => layout.fileTree.toggle(),
|
||||
}),
|
||||
...(shown()
|
||||
? [
|
||||
viewCommand({
|
||||
id: "fileTree.toggle",
|
||||
title: language.t("command.fileTree.toggle"),
|
||||
keybind: "mod+\\",
|
||||
onSelect: () => layout.fileTree.toggle(),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
viewCommand({
|
||||
id: "input.focus",
|
||||
title: language.t("command.input.focus"),
|
||||
|
||||
@@ -469,7 +469,7 @@ export function persisted<T>(
|
||||
state,
|
||||
setState,
|
||||
init,
|
||||
Object.assign(() => ready() === true, {
|
||||
Object.assign(() => (ready.loading ? false : ready.latest === true), {
|
||||
promise: init instanceof Promise ? init : undefined,
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -46,7 +46,9 @@ describe("runtime adapters", () => {
|
||||
})
|
||||
|
||||
test("resolves speech recognition constructor with webkit precedence", () => {
|
||||
// oxlint-disable-next-line no-extraneous-class
|
||||
class SpeechCtor {}
|
||||
// oxlint-disable-next-line no-extraneous-class
|
||||
class WebkitCtor {}
|
||||
const ctor = getSpeechRecognitionCtor({
|
||||
SpeechRecognition: SpeechCtor,
|
||||
|
||||
@@ -16,7 +16,10 @@ export function createSdkForServer({
|
||||
|
||||
return createOpencodeClient({
|
||||
...config,
|
||||
headers: { ...config.headers, ...auth },
|
||||
headers: {
|
||||
...(config.headers instanceof Headers ? Object.fromEntries(config.headers.entries()) : config.headers),
|
||||
...auth,
|
||||
},
|
||||
baseUrl: server.url,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -8,7 +8,6 @@ import { LOCALES, route } from "../src/lib/language.js"
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const BASE_URL = config.baseUrl
|
||||
const PUBLIC_DIR = join(__dirname, "../public")
|
||||
const ROUTES_DIR = join(__dirname, "../src/routes")
|
||||
const DOCS_DIR = join(__dirname, "../../../web/src/content/docs")
|
||||
|
||||
interface SitemapEntry {
|
||||
@@ -106,4 +105,4 @@ async function main() {
|
||||
console.log(`✓ Sitemap generated at ${outputPath}`)
|
||||
}
|
||||
|
||||
main()
|
||||
void main()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { JSX } from "solid-js"
|
||||
|
||||
export function IconZen(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
export function IconZen(_props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg width="84" height="30" viewBox="0 0 84 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 24H6V18H18V12H24V24ZM6 18H0V12H6V18Z" fill="currentColor" fill-opacity="0.2" />
|
||||
@@ -13,7 +13,7 @@ export function IconZen(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconGo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
export function IconGo(_props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg width="54" height="30" viewBox="0 0 54 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 30H0V0H24V6H6V24H18V18H12V12H24V30Z" fill="currentColor" />
|
||||
|
||||
@@ -766,7 +766,7 @@ export default function Spotlight(props: SpotlightProps) {
|
||||
}
|
||||
}
|
||||
|
||||
initializeWebGPU()
|
||||
void initializeWebGPU()
|
||||
|
||||
onCleanup(() => {
|
||||
if (cleanupFunctionRef) {
|
||||
|
||||
@@ -558,6 +558,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "الاستخدام الحالي لـ",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "هو $",
|
||||
|
||||
"workspace.redeem.title": "استرداد قسيمة",
|
||||
"workspace.redeem.subtitle": "استرد رمز القسيمة للحصول على رصيد أو مزايا.",
|
||||
"workspace.redeem.placeholder": "أدخل رمز القسيمة",
|
||||
"workspace.redeem.redeem": "استرداد",
|
||||
"workspace.redeem.redeeming": "جارٍ الاسترداد...",
|
||||
"workspace.redeem.success": "تم استرداد القسيمة بنجاح.",
|
||||
|
||||
"workspace.reload.title": "إعادة الشحن التلقائي",
|
||||
"workspace.reload.disabled.before": "إعادة الشحن التلقائي",
|
||||
"workspace.reload.disabled.state": "معطّل",
|
||||
|
||||
@@ -567,6 +567,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Uso atual para",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "é $",
|
||||
|
||||
"workspace.redeem.title": "Resgatar Cupom",
|
||||
"workspace.redeem.subtitle": "Resgate um código de cupom para receber créditos ou vantagens.",
|
||||
"workspace.redeem.placeholder": "Digite o código do cupom",
|
||||
"workspace.redeem.redeem": "Resgatar",
|
||||
"workspace.redeem.redeeming": "Resgatando...",
|
||||
"workspace.redeem.success": "Cupom resgatado com sucesso.",
|
||||
|
||||
"workspace.reload.title": "Recarga Automática",
|
||||
"workspace.reload.disabled.before": "A recarga automática está",
|
||||
"workspace.reload.disabled.state": "desativada",
|
||||
|
||||
@@ -563,6 +563,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Nuværende brug for",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "er $",
|
||||
|
||||
"workspace.redeem.title": "Indløs kupon",
|
||||
"workspace.redeem.subtitle": "Indløs en kuponkode for at få kreditter eller fordele.",
|
||||
"workspace.redeem.placeholder": "Indtast kuponkode",
|
||||
"workspace.redeem.redeem": "Indløs",
|
||||
"workspace.redeem.redeeming": "Indløser...",
|
||||
"workspace.redeem.success": "Kuponen blev indløst.",
|
||||
|
||||
"workspace.reload.title": "Automatisk genopfyldning",
|
||||
"workspace.reload.disabled.before": "Automatisk genopfyldning er",
|
||||
"workspace.reload.disabled.state": "deaktiveret",
|
||||
|
||||
@@ -566,6 +566,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Aktuelle Nutzung für",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "ist $",
|
||||
|
||||
"workspace.redeem.title": "Gutschein einlösen",
|
||||
"workspace.redeem.subtitle": "Löse einen Gutscheincode ein, um Guthaben oder Vorteile zu erhalten.",
|
||||
"workspace.redeem.placeholder": "Gutscheincode eingeben",
|
||||
"workspace.redeem.redeem": "Einlösen",
|
||||
"workspace.redeem.redeeming": "Wird eingelöst...",
|
||||
"workspace.redeem.success": "Gutschein erfolgreich eingelöst.",
|
||||
|
||||
"workspace.reload.title": "Auto-Reload",
|
||||
"workspace.reload.disabled.before": "Auto-Reload ist",
|
||||
"workspace.reload.disabled.state": "deaktiviert",
|
||||
|
||||
@@ -559,6 +559,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Current usage for",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "is $",
|
||||
|
||||
"workspace.redeem.title": "Redeem Coupon",
|
||||
"workspace.redeem.subtitle": "Redeem a coupon code to claim credits or perks.",
|
||||
"workspace.redeem.placeholder": "Enter coupon code",
|
||||
"workspace.redeem.redeem": "Redeem",
|
||||
"workspace.redeem.redeeming": "Redeeming...",
|
||||
"workspace.redeem.success": "Coupon redeemed successfully.",
|
||||
|
||||
"workspace.reload.title": "Auto Reload",
|
||||
"workspace.reload.disabled.before": "Auto reload is",
|
||||
"workspace.reload.disabled.state": "disabled",
|
||||
|
||||
@@ -567,6 +567,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Uso actual para",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "es $",
|
||||
|
||||
"workspace.redeem.title": "Canjear cupón",
|
||||
"workspace.redeem.subtitle": "Canjea un código de cupón para obtener crédito o beneficios.",
|
||||
"workspace.redeem.placeholder": "Introduce el código del cupón",
|
||||
"workspace.redeem.redeem": "Canjear",
|
||||
"workspace.redeem.redeeming": "Canjeando...",
|
||||
"workspace.redeem.success": "Cupón canjeado correctamente.",
|
||||
|
||||
"workspace.reload.title": "Auto Recarga",
|
||||
"workspace.reload.disabled.before": "La auto recarga está",
|
||||
"workspace.reload.disabled.state": "deshabilitada",
|
||||
|
||||
@@ -569,6 +569,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "L'utilisation actuelle pour",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "est de",
|
||||
|
||||
"workspace.redeem.title": "Utiliser un coupon",
|
||||
"workspace.redeem.subtitle": "Utilisez un code promo pour obtenir du crédit ou des avantages.",
|
||||
"workspace.redeem.placeholder": "Saisissez le code promo",
|
||||
"workspace.redeem.redeem": "Utiliser",
|
||||
"workspace.redeem.redeeming": "Utilisation...",
|
||||
"workspace.redeem.success": "Coupon utilisé avec succès.",
|
||||
|
||||
"workspace.reload.title": "Rechargement automatique",
|
||||
"workspace.reload.disabled.before": "Le rechargement automatique est",
|
||||
"workspace.reload.disabled.state": "désactivé",
|
||||
|
||||
@@ -565,6 +565,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Utilizzo attuale per",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "è $",
|
||||
|
||||
"workspace.redeem.title": "Riscatta Coupon",
|
||||
"workspace.redeem.subtitle": "Riscatta un codice coupon per ottenere credito o vantaggi.",
|
||||
"workspace.redeem.placeholder": "Inserisci il codice coupon",
|
||||
"workspace.redeem.redeem": "Riscatta",
|
||||
"workspace.redeem.redeeming": "Riscatto in corso...",
|
||||
"workspace.redeem.success": "Coupon riscattato con successo.",
|
||||
|
||||
"workspace.reload.title": "Ricarica Auto",
|
||||
"workspace.reload.disabled.before": "La ricarica auto è",
|
||||
"workspace.reload.disabled.state": "disabilitata",
|
||||
|
||||
@@ -564,6 +564,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "現在の使用状況(",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": ")は $",
|
||||
|
||||
"workspace.redeem.title": "クーポンを利用",
|
||||
"workspace.redeem.subtitle": "クーポンコードを利用して、クレジットや特典を受け取ります。",
|
||||
"workspace.redeem.placeholder": "クーポンコードを入力",
|
||||
"workspace.redeem.redeem": "利用する",
|
||||
"workspace.redeem.redeeming": "利用中...",
|
||||
"workspace.redeem.success": "クーポンを利用しました。",
|
||||
|
||||
"workspace.reload.title": "自動チャージ",
|
||||
"workspace.reload.disabled.before": "自動チャージは",
|
||||
"workspace.reload.disabled.state": "無効",
|
||||
|
||||
@@ -558,6 +558,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "현재",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "사용량: $",
|
||||
|
||||
"workspace.redeem.title": "쿠폰 사용",
|
||||
"workspace.redeem.subtitle": "쿠폰 코드를 사용해 크레딧이나 혜택을 받으세요.",
|
||||
"workspace.redeem.placeholder": "쿠폰 코드를 입력하세요",
|
||||
"workspace.redeem.redeem": "사용",
|
||||
"workspace.redeem.redeeming": "사용 중...",
|
||||
"workspace.redeem.success": "쿠폰을 성공적으로 사용했습니다.",
|
||||
|
||||
"workspace.reload.title": "자동 충전",
|
||||
"workspace.reload.disabled.before": "자동 충전이",
|
||||
"workspace.reload.disabled.state": "비활성화",
|
||||
|
||||
@@ -564,6 +564,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Gjeldende forbruk for",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "er $",
|
||||
|
||||
"workspace.redeem.title": "Løs inn kupong",
|
||||
"workspace.redeem.subtitle": "Løs inn en kupongkode for å få kreditt eller fordeler.",
|
||||
"workspace.redeem.placeholder": "Skriv inn kupongkode",
|
||||
"workspace.redeem.redeem": "Løs inn",
|
||||
"workspace.redeem.redeeming": "Løser inn...",
|
||||
"workspace.redeem.success": "Kupongen ble løst inn.",
|
||||
|
||||
"workspace.reload.title": "Auto-påfyll",
|
||||
"workspace.reload.disabled.before": "Auto-påfyll er",
|
||||
"workspace.reload.disabled.state": "deaktivert",
|
||||
|
||||
@@ -565,6 +565,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Aktualne użycie za",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "wynosi $",
|
||||
|
||||
"workspace.redeem.title": "Zrealizuj kupon",
|
||||
"workspace.redeem.subtitle": "Zrealizuj kod kuponu, aby otrzymać środki lub korzyści.",
|
||||
"workspace.redeem.placeholder": "Wpisz kod kuponu",
|
||||
"workspace.redeem.redeem": "Zrealizuj",
|
||||
"workspace.redeem.redeeming": "Realizowanie...",
|
||||
"workspace.redeem.success": "Kupon został zrealizowany.",
|
||||
|
||||
"workspace.reload.title": "Automatyczne doładowanie",
|
||||
"workspace.reload.disabled.before": "Automatyczne doładowanie jest",
|
||||
"workspace.reload.disabled.state": "wyłączone",
|
||||
|
||||
@@ -571,6 +571,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Текущее использование за",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "составляет $",
|
||||
|
||||
"workspace.redeem.title": "Активировать купон",
|
||||
"workspace.redeem.subtitle": "Активируйте код купона, чтобы получить кредит или бонусы.",
|
||||
"workspace.redeem.placeholder": "Введите код купона",
|
||||
"workspace.redeem.redeem": "Активировать",
|
||||
"workspace.redeem.redeeming": "Активация...",
|
||||
"workspace.redeem.success": "Купон успешно активирован.",
|
||||
|
||||
"workspace.reload.title": "Автопополнение",
|
||||
"workspace.reload.disabled.before": "Автопополнение",
|
||||
"workspace.reload.disabled.state": "отключено",
|
||||
|
||||
@@ -560,6 +560,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "การใช้งานปัจจุบันสำหรับ",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "คือ $",
|
||||
|
||||
"workspace.redeem.title": "แลกคูปอง",
|
||||
"workspace.redeem.subtitle": "แลกรหัสคูปองเพื่อรับเครดิตหรือสิทธิพิเศษ",
|
||||
"workspace.redeem.placeholder": "กรอกรหัสคูปอง",
|
||||
"workspace.redeem.redeem": "แลก",
|
||||
"workspace.redeem.redeeming": "กำลังแลก...",
|
||||
"workspace.redeem.success": "แลกคูปองสำเร็จ",
|
||||
|
||||
"workspace.reload.title": "โหลดซ้ำอัตโนมัติ",
|
||||
"workspace.reload.disabled.before": "การโหลดซ้ำอัตโนมัติ",
|
||||
"workspace.reload.disabled.state": "ปิดใช้งานอยู่",
|
||||
|
||||
@@ -567,6 +567,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Şu anki kullanım",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "$",
|
||||
|
||||
"workspace.redeem.title": "Kupon Kullan",
|
||||
"workspace.redeem.subtitle": "Kredi veya avantajlardan yararlanmak için bir kupon kodu kullanın.",
|
||||
"workspace.redeem.placeholder": "Kupon kodunu girin",
|
||||
"workspace.redeem.redeem": "Kullan",
|
||||
"workspace.redeem.redeeming": "Kullanılıyor...",
|
||||
"workspace.redeem.success": "Kupon başarıyla kullanıldı.",
|
||||
|
||||
"workspace.reload.title": "Otomatik Yeniden Yükleme",
|
||||
"workspace.reload.disabled.before": "Otomatik yeniden yükleme:",
|
||||
"workspace.reload.disabled.state": "devre dışı",
|
||||
|
||||
@@ -542,6 +542,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "当前",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量为 $",
|
||||
|
||||
"workspace.redeem.title": "兑换优惠券",
|
||||
"workspace.redeem.subtitle": "兑换优惠码以领取充值额度或权益。",
|
||||
"workspace.redeem.placeholder": "输入优惠码",
|
||||
"workspace.redeem.redeem": "兑换",
|
||||
"workspace.redeem.redeeming": "兑换中...",
|
||||
"workspace.redeem.success": "优惠券兑换成功。",
|
||||
|
||||
"workspace.reload.title": "自动充值",
|
||||
"workspace.reload.disabled.before": "自动充值已",
|
||||
"workspace.reload.disabled.state": "禁用",
|
||||
|
||||
@@ -542,6 +542,13 @@ export const dict = {
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "目前",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量為 $",
|
||||
|
||||
"workspace.redeem.title": "兌換優惠券",
|
||||
"workspace.redeem.subtitle": "兌換優惠碼以領取儲值額度或權益。",
|
||||
"workspace.redeem.placeholder": "輸入優惠碼",
|
||||
"workspace.redeem.redeem": "兌換",
|
||||
"workspace.redeem.redeeming": "兌換中...",
|
||||
"workspace.redeem.success": "優惠券兌換成功。",
|
||||
|
||||
"workspace.reload.title": "自動儲值",
|
||||
"workspace.reload.disabled.before": "自動儲值已",
|
||||
"workspace.reload.disabled.state": "停用",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { APIEvent } from "@solidjs/start"
|
||||
import { useAuthSession } from "~/context/auth"
|
||||
|
||||
export async function GET(input: APIEvent) {
|
||||
export async function GET(_input: APIEvent) {
|
||||
const session = await useAuthSession()
|
||||
return Response.json(session.data)
|
||||
}
|
||||
|
||||
@@ -298,7 +298,7 @@ export default function BlackSubscribe() {
|
||||
|
||||
// Resolve stripe promise once
|
||||
createEffect(() => {
|
||||
stripePromise.then((s) => {
|
||||
void stripePromise.then((s) => {
|
||||
if (s) setStripe(s)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { json } from "@solidjs/router"
|
||||
import { Database } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||
|
||||
export async function GET(evt: APIEvent) {
|
||||
export async function GET(_evt: APIEvent) {
|
||||
return json({
|
||||
data: await Database.use(async (tx) => {
|
||||
const result = await tx.$count(UserTable)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { APIEvent } from "@solidjs/start"
|
||||
import type { DownloadPlatform } from "../types"
|
||||
|
||||
const assetNames: Record<string, string> = {
|
||||
const prodAssetNames: Record<string, string> = {
|
||||
"darwin-aarch64-dmg": "opencode-desktop-darwin-aarch64.dmg",
|
||||
"darwin-x64-dmg": "opencode-desktop-darwin-x64.dmg",
|
||||
"windows-x64-nsis": "opencode-desktop-windows-x64.exe",
|
||||
@@ -10,6 +10,15 @@ const assetNames: Record<string, string> = {
|
||||
"linux-x64-rpm": "opencode-desktop-linux-x86_64.rpm",
|
||||
} satisfies Record<DownloadPlatform, string>
|
||||
|
||||
const betaAssetNames: Record<string, string> = {
|
||||
"darwin-aarch64-dmg": "opencode-electron-mac-arm64.dmg",
|
||||
"darwin-x64-dmg": "opencode-electron-mac-x64.dmg",
|
||||
"windows-x64-nsis": "opencode-electron-win-x64.exe",
|
||||
"linux-x64-deb": "opencode-electron-linux-amd64.deb",
|
||||
"linux-x64-appimage": "opencode-electron-linux-x86_64.AppImage",
|
||||
"linux-x64-rpm": "opencode-electron-linux-x86_64.rpm",
|
||||
} satisfies Record<DownloadPlatform, string>
|
||||
|
||||
// Doing this on the server lets us preserve the original name for platforms we don't care to rename for
|
||||
const downloadNames: Record<string, string> = {
|
||||
"darwin-aarch64-dmg": "OpenCode Desktop.dmg",
|
||||
@@ -18,7 +27,7 @@ const downloadNames: Record<string, string> = {
|
||||
} satisfies { [K in DownloadPlatform]?: string }
|
||||
|
||||
export async function GET({ params: { platform, channel } }: APIEvent) {
|
||||
const assetName = assetNames[platform]
|
||||
const assetName = channel === "stable" ? prodAssetNames[platform] : betaAssetNames[platform]
|
||||
if (!assetName) return new Response(null, { status: 404 })
|
||||
|
||||
const resp = await fetch(
|
||||
@@ -37,5 +46,5 @@ export async function GET({ params: { platform, channel } }: APIEvent) {
|
||||
const headers = new Headers(resp.headers)
|
||||
if (downloadName) headers.set("content-disposition", `attachment; filename="${downloadName}"`)
|
||||
|
||||
return new Response(resp.body, { ...resp, headers })
|
||||
return new Response(resp.body, { status: resp.status, statusText: resp.statusText, headers })
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export default function Download() {
|
||||
|
||||
const handleCopyClick = (command: string) => (event: Event) => {
|
||||
const button = event.currentTarget as HTMLButtonElement
|
||||
navigator.clipboard.writeText(command)
|
||||
void navigator.clipboard.writeText(command)
|
||||
button.setAttribute("data-copied", "")
|
||||
setTimeout(() => {
|
||||
button.removeAttribute("data-copied")
|
||||
|
||||
@@ -12,7 +12,6 @@ import { Header } from "~/component/header"
|
||||
import { Footer } from "~/component/footer"
|
||||
import { Legal } from "~/component/legal"
|
||||
import { github } from "~/lib/github"
|
||||
import { createMemo } from "solid-js"
|
||||
import { config } from "~/config"
|
||||
import { useI18n } from "~/context/i18n"
|
||||
import { useLanguage } from "~/context/language"
|
||||
@@ -30,14 +29,12 @@ function CopyStatus() {
|
||||
export default function Home() {
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
const githubData = createAsync(() => github())
|
||||
const release = createMemo(() => githubData()?.release)
|
||||
|
||||
const _githubData = createAsync(() => github())
|
||||
const handleCopyClick = (event: Event) => {
|
||||
const button = event.currentTarget as HTMLButtonElement
|
||||
const text = button.textContent
|
||||
if (text) {
|
||||
navigator.clipboard.writeText(text)
|
||||
void navigator.clipboard.writeText(text)
|
||||
button.setAttribute("data-copied", "")
|
||||
setTimeout(() => {
|
||||
button.removeAttribute("data-copied")
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { LiteData } from "@opencode-ai/console-core/lite.js"
|
||||
import { BlackData } from "@opencode-ai/console-core/black.js"
|
||||
import { User } from "@opencode-ai/console-core/user.js"
|
||||
|
||||
export async function POST(input: APIEvent) {
|
||||
const body = await Billing.stripe().webhooks.constructEventAsync(
|
||||
@@ -109,6 +110,8 @@ export async function POST(input: APIEvent) {
|
||||
if (type === "lite") {
|
||||
const workspaceID = body.data.object.metadata?.workspaceID
|
||||
const userID = body.data.object.metadata?.userID
|
||||
const userEmail = body.data.object.metadata?.userEmail
|
||||
const coupon = body.data.object.metadata?.coupon
|
||||
const customerID = body.data.object.customer as string
|
||||
const invoiceID = body.data.object.latest_invoice as string
|
||||
const subscriptionID = body.data.object.id as string
|
||||
@@ -156,6 +159,10 @@ export async function POST(input: APIEvent) {
|
||||
id: Identifier.create("lite"),
|
||||
userID: userID,
|
||||
})
|
||||
|
||||
if (userEmail && coupon === LiteData.firstMonth100Coupon) {
|
||||
await Billing.redeemCoupon(userEmail, "GOFREEMONTH")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function Home() {
|
||||
const callback = () => {
|
||||
const text = button.textContent
|
||||
if (text) {
|
||||
navigator.clipboard.writeText(text)
|
||||
void navigator.clipboard.writeText(text)
|
||||
button.setAttribute("data-copied", "")
|
||||
setTimeout(() => {
|
||||
button.removeAttribute("data-copied")
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useI18n } from "~/context/i18n"
|
||||
import { useLanguage } from "~/context/language"
|
||||
import "./user-menu.css"
|
||||
|
||||
const logout = action(async () => {
|
||||
const _logout = action(async () => {
|
||||
"use server"
|
||||
const auth = await useAuthSession()
|
||||
const event = getRequestEvent()
|
||||
|
||||
@@ -116,9 +116,9 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) =
|
||||
|
||||
const setUseBalance = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
const useBalance = form.get("useBalance")?.toString() === "true"
|
||||
const useBalance = (form.get("useBalance") as string | null) === "true"
|
||||
|
||||
return json(
|
||||
await withActor(async () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BillingSection } from "./billing-section"
|
||||
import { ReloadSection } from "./reload-section"
|
||||
import { PaymentSection } from "./payment-section"
|
||||
import { BlackSection } from "./black-section"
|
||||
import { RedeemSection } from "./redeem-section"
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { createAsync, useParams } from "@solidjs/router"
|
||||
import { queryBillingInfo, querySessionInfo } from "../../common"
|
||||
@@ -24,6 +25,7 @@ export default function () {
|
||||
<Show when={billingInfo()?.customerID}>
|
||||
<ReloadSection />
|
||||
<MonthlyLimitSection />
|
||||
<RedeemSection />
|
||||
<PaymentSection />
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
@@ -10,11 +10,11 @@ import { formError, localizeError } from "~/lib/form-error"
|
||||
|
||||
const setMonthlyLimit = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const limit = form.get("limit")?.toString()
|
||||
const limit = form.get("limit") as string | null
|
||||
if (!limit) return { error: formError.limitRequired }
|
||||
const numericLimit = parseInt(limit)
|
||||
if (numericLimit < 0) return { error: formError.monthlyLimitInvalid }
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
return json(
|
||||
await withActor(
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
.root {
|
||||
[data-slot="redeem-container"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
min-width: 20rem;
|
||||
width: fit-content;
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="redeem-form"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
|
||||
[data-slot="input-row"] {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: stretch;
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-mono);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="form-error"] {
|
||||
color: var(--color-danger);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
[data-slot="form-success"] {
|
||||
color: var(--color-success, var(--color-accent));
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { json, action, useParams, useSubmission } from "@solidjs/router"
|
||||
import { Show } from "solid-js"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||
import { User } from "@opencode-ai/console-core/user.js"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { CouponType } from "@opencode-ai/console-core/schema/billing.sql.js"
|
||||
import styles from "./redeem-section.module.css"
|
||||
import { queryBillingInfo } from "../../common"
|
||||
import { useI18n } from "~/context/i18n"
|
||||
import { formError, localizeError } from "~/lib/form-error"
|
||||
|
||||
const redeem = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
const code = (form.get("code") as string | null)?.trim().toUpperCase()
|
||||
if (!code) return { error: "Coupon code is required." }
|
||||
if (!(CouponType as readonly string[]).includes(code)) return { error: "Invalid coupon code." }
|
||||
|
||||
return json(
|
||||
await withActor(async () => {
|
||||
const actor = Actor.assert("user")
|
||||
const email = await User.getAuthEmail(actor.properties.userID)
|
||||
if (!email) return { error: "No email on account." }
|
||||
return Billing.redeemCoupon(email, code as (typeof CouponType)[number])
|
||||
.then(() => ({ error: undefined, data: true }))
|
||||
.catch((e) => ({ error: e.message as string }))
|
||||
}, workspaceID),
|
||||
{ revalidate: queryBillingInfo.key },
|
||||
)
|
||||
}, "billing.redeemCoupon")
|
||||
|
||||
export function RedeemSection() {
|
||||
const params = useParams()
|
||||
const i18n = useI18n()
|
||||
const submission = useSubmission(redeem)
|
||||
|
||||
return (
|
||||
<section class={styles.root}>
|
||||
<div data-slot="section-title">
|
||||
<h2>{i18n.t("workspace.redeem.title")}</h2>
|
||||
<p>{i18n.t("workspace.redeem.subtitle")}</p>
|
||||
</div>
|
||||
<div data-slot="redeem-container">
|
||||
<form action={redeem} method="post" data-slot="redeem-form">
|
||||
<div data-slot="input-row">
|
||||
<input
|
||||
required
|
||||
data-component="input"
|
||||
name="code"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
placeholder={i18n.t("workspace.redeem.placeholder")}
|
||||
/>
|
||||
<button type="submit" data-color="primary" disabled={submission.pending}>
|
||||
{submission.pending ? i18n.t("workspace.redeem.redeeming") : i18n.t("workspace.redeem.redeem")}
|
||||
</button>
|
||||
</div>
|
||||
<Show when={submission.result && (submission.result as any).error}>
|
||||
{(err: any) => <div data-slot="form-error">{localizeError(i18n.t, err())}</div>}
|
||||
</Show>
|
||||
<Show when={submission.result && !(submission.result as any).error && (submission.result as any).data}>
|
||||
<div data-slot="form-success">{i18n.t("workspace.redeem.success")}</div>
|
||||
</Show>
|
||||
<input type="hidden" name="workspaceID" value={params.id} />
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import { formError, formErrorReloadAmountMin, formErrorReloadTriggerMin, localiz
|
||||
|
||||
const reload = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
return json(await withActor(() => Billing.reload(), workspaceID), {
|
||||
revalidate: queryBillingInfo.key,
|
||||
@@ -21,11 +21,11 @@ const reload = action(async (form: FormData) => {
|
||||
|
||||
const setReload = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
const reloadValue = form.get("reload")?.toString() === "true"
|
||||
const amountStr = form.get("reloadAmount")?.toString()
|
||||
const triggerStr = form.get("reloadTrigger")?.toString()
|
||||
const reloadValue = (form.get("reload") as string | null) === "true"
|
||||
const amountStr = form.get("reloadAmount") as string | null
|
||||
const triggerStr = form.get("reloadTrigger") as string | null
|
||||
|
||||
const reloadAmount = amountStr && amountStr.trim() !== "" ? parseInt(amountStr) : null
|
||||
const reloadTrigger = triggerStr && triggerStr.trim() !== "" ? parseInt(triggerStr) : null
|
||||
@@ -90,9 +90,9 @@ export function ReloadSection() {
|
||||
}
|
||||
const info = billingInfo()!
|
||||
setStore("show", true)
|
||||
setStore("reload", info.reload ? true : true)
|
||||
setStore("reloadAmount", info.reloadAmount.toString())
|
||||
setStore("reloadTrigger", info.reloadTrigger.toString())
|
||||
setStore("reload", true)
|
||||
setStore("reloadAmount", String(info.reloadAmount))
|
||||
setStore("reloadTrigger", String(info.reloadTrigger))
|
||||
}
|
||||
|
||||
function hide() {
|
||||
@@ -152,11 +152,11 @@ export function ReloadSection() {
|
||||
data-component="input"
|
||||
name="reloadAmount"
|
||||
type="number"
|
||||
min={billingInfo()?.reloadAmountMin.toString()}
|
||||
min={String(billingInfo()?.reloadAmountMin ?? "")}
|
||||
step="1"
|
||||
value={store.reloadAmount}
|
||||
onInput={(e) => setStore("reloadAmount", e.currentTarget.value)}
|
||||
placeholder={billingInfo()?.reloadAmount.toString()}
|
||||
placeholder={String(billingInfo()?.reloadAmount ?? "")}
|
||||
disabled={!store.reload}
|
||||
/>
|
||||
</div>
|
||||
@@ -166,11 +166,11 @@ export function ReloadSection() {
|
||||
data-component="input"
|
||||
name="reloadTrigger"
|
||||
type="number"
|
||||
min={billingInfo()?.reloadTriggerMin.toString()}
|
||||
min={String(billingInfo()?.reloadTriggerMin ?? "")}
|
||||
step="1"
|
||||
value={store.reloadTrigger}
|
||||
onInput={(e) => setStore("reloadTrigger", e.currentTarget.value)}
|
||||
placeholder={billingInfo()?.reloadTrigger.toString()}
|
||||
placeholder={String(billingInfo()?.reloadTrigger ?? "")}
|
||||
disabled={!store.reload}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -120,9 +120,9 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) =
|
||||
|
||||
const setLiteUseBalance = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
const useBalance = form.get("useBalance")?.toString() === "true"
|
||||
const useBalance = (form.get("useBalance") as string | null) === "true"
|
||||
|
||||
return json(
|
||||
await withActor(async () => {
|
||||
|
||||
@@ -12,18 +12,18 @@ import { formError, localizeError } from "~/lib/form-error"
|
||||
|
||||
const removeKey = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const id = form.get("id")?.toString()
|
||||
const id = form.get("id") as string | null
|
||||
if (!id) return { error: formError.idRequired }
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
return json(await withActor(() => Key.remove({ id }), workspaceID), { revalidate: listKeys.key })
|
||||
}, "key.remove")
|
||||
|
||||
const createKey = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const name = form.get("name")?.toString().trim()
|
||||
const name = (form.get("name") as string | null)?.trim()
|
||||
if (!name) return { error: formError.nameRequired }
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
return json(
|
||||
await withActor(
|
||||
|
||||
@@ -24,13 +24,13 @@ const listMembers = query(async (workspaceID: string) => {
|
||||
|
||||
const inviteMember = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const email = form.get("email")?.toString().trim()
|
||||
const email = (form.get("email") as string | null)?.trim()
|
||||
if (!email) return { error: formError.emailRequired }
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
const role = form.get("role")?.toString() as (typeof UserRole)[number]
|
||||
const role = form.get("role") as (typeof UserRole)[number] | null
|
||||
if (!role) return { error: formError.roleRequired }
|
||||
const limit = form.get("limit")?.toString()
|
||||
const limit = form.get("limit") as string | null
|
||||
const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null
|
||||
if (monthlyLimit !== null && monthlyLimit < 0) return { error: formError.monthlyLimitInvalid }
|
||||
return json(
|
||||
@@ -47,9 +47,9 @@ const inviteMember = action(async (form: FormData) => {
|
||||
|
||||
const removeMember = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const id = form.get("id")?.toString()
|
||||
const id = form.get("id") as string | null
|
||||
if (!id) return { error: formError.idRequired }
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
return json(
|
||||
await withActor(
|
||||
@@ -66,13 +66,13 @@ const removeMember = action(async (form: FormData) => {
|
||||
const updateMember = action(async (form: FormData) => {
|
||||
"use server"
|
||||
|
||||
const id = form.get("id")?.toString()
|
||||
const id = form.get("id") as string | null
|
||||
if (!id) return { error: formError.idRequired }
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
const role = form.get("role")?.toString() as (typeof UserRole)[number]
|
||||
const role = form.get("role") as (typeof UserRole)[number] | null
|
||||
if (!role) return { error: formError.roleRequired }
|
||||
const limit = form.get("limit")?.toString()
|
||||
const limit = form.get("limit") as string | null
|
||||
const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null
|
||||
if (monthlyLimit !== null && monthlyLimit < 0) return { error: formError.monthlyLimitInvalid }
|
||||
|
||||
@@ -118,7 +118,7 @@ function MemberRow(props: {
|
||||
}
|
||||
setStore("editing", true)
|
||||
setStore("selectedRole", props.member.role)
|
||||
setStore("limit", props.member.monthlyLimit?.toString() ?? "")
|
||||
setStore("limit", props.member.monthlyLimit != null ? String(props.member.monthlyLimit) : "")
|
||||
}
|
||||
|
||||
function hide() {
|
||||
|
||||
@@ -67,11 +67,11 @@ const getModelsInfo = query(async (workspaceID: string) => {
|
||||
|
||||
const updateModel = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const model = form.get("model")?.toString()
|
||||
const model = form.get("model") as string | null
|
||||
if (!model) return { error: formError.modelRequired }
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
const enabled = form.get("enabled")?.toString() === "true"
|
||||
const enabled = (form.get("enabled") as string | null) === "true"
|
||||
return json(
|
||||
withActor(async () => {
|
||||
if (enabled) {
|
||||
@@ -163,7 +163,7 @@ export function ModelSection() {
|
||||
<form action={updateModel} method="post">
|
||||
<input type="hidden" name="model" value={id} />
|
||||
<input type="hidden" name="workspaceID" value={params.id} />
|
||||
<input type="hidden" name="enabled" value={isEnabled().toString()} />
|
||||
<input type="hidden" name="enabled" value={String(isEnabled())} />
|
||||
<label data-slot="model-toggle-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
@@ -21,9 +21,9 @@ function maskCredentials(credentials: string) {
|
||||
|
||||
const removeProvider = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const provider = form.get("provider")?.toString()
|
||||
const provider = form.get("provider") as string | null
|
||||
if (!provider) return { error: formError.providerRequired }
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
return json(await withActor(() => Provider.remove({ provider }), workspaceID), {
|
||||
revalidate: listProviders.key,
|
||||
@@ -32,11 +32,11 @@ const removeProvider = action(async (form: FormData) => {
|
||||
|
||||
const saveProvider = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const provider = form.get("provider")?.toString()
|
||||
const credentials = form.get("credentials")?.toString()
|
||||
const provider = form.get("provider") as string | null
|
||||
const credentials = form.get("credentials") as string | null
|
||||
if (!provider) return { error: formError.providerRequired }
|
||||
if (!credentials) return { error: formError.apiKeyRequired }
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
return json(
|
||||
await withActor(
|
||||
@@ -59,10 +59,13 @@ function ProviderRow(props: { provider: Provider }) {
|
||||
const params = useParams()
|
||||
const i18n = useI18n()
|
||||
const providers = createAsync(() => listProviders(params.id!))
|
||||
const saveSubmission = useSubmission(saveProvider, ([fd]) => fd.get("provider")?.toString() === props.provider.key)
|
||||
const saveSubmission = useSubmission(
|
||||
saveProvider,
|
||||
([fd]) => (fd.get("provider") as string | null) === props.provider.key,
|
||||
)
|
||||
const removeSubmission = useSubmission(
|
||||
removeProvider,
|
||||
([fd]) => fd.get("provider")?.toString() === props.provider.key,
|
||||
([fd]) => (fd.get("provider") as string | null) === props.provider.key,
|
||||
)
|
||||
const [store, setStore] = createStore({ editing: false })
|
||||
|
||||
|
||||
@@ -30,10 +30,10 @@ const getWorkspaceInfo = query(async (workspaceID: string) => {
|
||||
|
||||
const updateWorkspace = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const name = form.get("name")?.toString().trim()
|
||||
const name = (form.get("name") as string | null)?.trim()
|
||||
if (!name) return { error: formError.workspaceNameRequired }
|
||||
if (name.length > 255) return { error: formError.nameTooLong }
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
const workspaceID = form.get("workspaceID") as string | null
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
return json(
|
||||
await withActor(
|
||||
|
||||
@@ -26,14 +26,14 @@ export function createDataDumper(sessionId: string, requestId: string, projectId
|
||||
const minute = timestamp.substring(10, 12)
|
||||
const second = timestamp.substring(12, 14)
|
||||
|
||||
waitUntil(
|
||||
void waitUntil(
|
||||
Resource.ZenDataNew.put(
|
||||
`data/${data.modelName}/${year}/${month}/${day}/${hour}/${minute}/${second}/${requestId}.json`,
|
||||
JSON.stringify({ timestamp, ...data }),
|
||||
),
|
||||
)
|
||||
|
||||
waitUntil(
|
||||
void waitUntil(
|
||||
Resource.ZenDataNew.put(
|
||||
`meta/${data.modelName}/${sessionId}/${requestId}.json`,
|
||||
JSON.stringify({ timestamp, ...metadata }),
|
||||
|
||||
@@ -45,6 +45,7 @@ import { LiteData } from "@opencode-ai/console-core/lite.js"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { i18n, type Key } from "~/i18n"
|
||||
import { localeFromRequest } from "~/lib/language"
|
||||
import { createModelTpmLimiter } from "./modelTpmLimiter"
|
||||
|
||||
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
|
||||
type RetryOptions = {
|
||||
@@ -121,6 +122,8 @@ export async function handler(
|
||||
const authInfo = await authenticate(modelInfo, zenApiKey)
|
||||
const billingSource = validateBilling(authInfo, modelInfo)
|
||||
logger.metric({ source: billingSource })
|
||||
const modelTpmLimiter = createModelTpmLimiter(modelInfo.providers)
|
||||
const modelTpmLimits = await modelTpmLimiter?.check()
|
||||
|
||||
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
|
||||
const providerInfo = selectProvider(
|
||||
@@ -133,6 +136,7 @@ export async function handler(
|
||||
trialProviders,
|
||||
retry,
|
||||
stickyProvider,
|
||||
modelTpmLimits,
|
||||
)
|
||||
validateModelSettings(billingSource, authInfo)
|
||||
updateProviderKey(authInfo, providerInfo)
|
||||
@@ -229,6 +233,7 @@ export async function handler(
|
||||
const usageInfo = providerInfo.normalizeUsage(json.usage)
|
||||
const costInfo = calculateCost(modelInfo, usageInfo)
|
||||
await trialLimiter?.track(usageInfo)
|
||||
await modelTpmLimiter?.track(providerInfo.id, providerInfo.model, usageInfo)
|
||||
await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo)
|
||||
await reload(billingSource, authInfo, costInfo)
|
||||
json.cost = calculateOccurredCost(billingSource, costInfo)
|
||||
@@ -278,6 +283,7 @@ export async function handler(
|
||||
const usageInfo = providerInfo.normalizeUsage(usage)
|
||||
const costInfo = calculateCost(modelInfo, usageInfo)
|
||||
await trialLimiter?.track(usageInfo)
|
||||
await modelTpmLimiter?.track(providerInfo.id, providerInfo.model, usageInfo)
|
||||
await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo)
|
||||
await reload(billingSource, authInfo, costInfo)
|
||||
const cost = calculateOccurredCost(billingSource, costInfo)
|
||||
@@ -433,12 +439,16 @@ export async function handler(
|
||||
trialProviders: string[] | undefined,
|
||||
retry: RetryOptions,
|
||||
stickyProvider: string | undefined,
|
||||
modelTpmLimits: Record<string, number> | undefined,
|
||||
) {
|
||||
const modelProvider = (() => {
|
||||
// Byok is top priority b/c if user set their own API key, we should use it
|
||||
// instead of using the sticky provider for the same session
|
||||
if (authInfo?.provider?.credentials) {
|
||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)
|
||||
}
|
||||
|
||||
// Always use the same provider for the same session
|
||||
if (stickyProvider) {
|
||||
const provider = modelInfo.providers.find((provider) => provider.id === stickyProvider)
|
||||
if (provider) return provider
|
||||
@@ -451,10 +461,20 @@ export async function handler(
|
||||
}
|
||||
|
||||
if (retry.retryCount !== MAX_FAILOVER_RETRIES) {
|
||||
const providers = modelInfo.providers
|
||||
const allProviders = modelInfo.providers
|
||||
.filter((provider) => !provider.disabled)
|
||||
.filter((provider) => provider.weight !== 0)
|
||||
.filter((provider) => !retry.excludeProviders.includes(provider.id))
|
||||
.flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider))
|
||||
.filter((provider) => {
|
||||
if (!provider.tpmLimit) return true
|
||||
const usage = modelTpmLimits?.[`${provider.id}/${provider.model}`] ?? 0
|
||||
return usage < provider.tpmLimit * 1_000_000
|
||||
})
|
||||
|
||||
const topPriority = Math.min(...allProviders.map((p) => p.priority))
|
||||
const providers = allProviders
|
||||
.filter((p) => p.priority <= topPriority)
|
||||
.flatMap((provider) => Array<typeof provider>(provider.weight).fill(provider))
|
||||
|
||||
// Use the last 4 characters of session ID to select a provider
|
||||
const identifier = sessionId.length ? sessionId : ip
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { and, Database, eq, inArray, sql } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { ModelRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
|
||||
import { UsageInfo } from "./provider/provider"
|
||||
|
||||
export function createModelTpmLimiter(providers: { id: string; model: string; tpmLimit?: number }[]) {
|
||||
const keys = providers.filter((p) => p.tpmLimit).map((p) => `${p.id}/${p.model}`)
|
||||
if (keys.length === 0) return
|
||||
|
||||
const yyyyMMddHHmm = new Date(Date.now())
|
||||
.toISOString()
|
||||
.replace(/[^0-9]/g, "")
|
||||
.substring(0, 12)
|
||||
|
||||
return {
|
||||
check: async () => {
|
||||
const data = await Database.use((tx) =>
|
||||
tx
|
||||
.select()
|
||||
.from(ModelRateLimitTable)
|
||||
.where(and(inArray(ModelRateLimitTable.key, keys), eq(ModelRateLimitTable.interval, yyyyMMddHHmm))),
|
||||
)
|
||||
|
||||
// convert to map of model to count
|
||||
return data.reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr.key] = curr.count
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
)
|
||||
},
|
||||
track: async (id: string, model: string, usageInfo: UsageInfo) => {
|
||||
const key = `${id}/${model}`
|
||||
if (!keys.includes(key)) return
|
||||
const usage =
|
||||
usageInfo.inputTokens +
|
||||
usageInfo.outputTokens +
|
||||
(usageInfo.reasoningTokens ?? 0) +
|
||||
(usageInfo.cacheReadTokens ?? 0) +
|
||||
(usageInfo.cacheWrite5mTokens ?? 0) +
|
||||
(usageInfo.cacheWrite1hTokens ?? 0)
|
||||
if (usage <= 0) return
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.insert(ModelRateLimitTable)
|
||||
.values({ key, interval: yyyyMMddHHmm, count: usage })
|
||||
.onDuplicateKeyUpdate({ set: { count: sql`${ModelRateLimitTable.count} + ${usage}` } }),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage, safetyIdentif
|
||||
headers.set("authorization", `Bearer ${apiKey}`)
|
||||
headers.set("x-session-affinity", headers.get("x-opencode-session") ?? "")
|
||||
},
|
||||
modifyBody: (body: Record<string, any>, workspaceID?: string) => {
|
||||
modifyBody: (body: Record<string, any>, _workspaceID?: string) => {
|
||||
return {
|
||||
...body,
|
||||
...(body.stream ? { stream_options: { include_usage: true } } : {}),
|
||||
|
||||
@@ -5,7 +5,7 @@ import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.j
|
||||
import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
|
||||
import { ZenData } from "@opencode-ai/console-core/model.js"
|
||||
|
||||
export async function OPTIONS(input: APIEvent) {
|
||||
export async function OPTIONS(_input: APIEvent) {
|
||||
return new Response(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
|
||||
@@ -6,8 +6,8 @@ export function POST(input: APIEvent) {
|
||||
format: "google",
|
||||
modelList: "full",
|
||||
parseApiKey: (headers: Headers) => headers.get("x-goog-api-key") ?? undefined,
|
||||
parseModel: (url: string, body: any) => url.split("/").pop()?.split(":")?.[0] ?? "",
|
||||
parseIsStream: (url: string, body: any) =>
|
||||
parseModel: (url: string, _body: any) => url.split("/").pop()?.split(":")?.[0] ?? "",
|
||||
parseIsStream: (url: string, _body: any) =>
|
||||
// ie. url: https://opencode.ai/zen/v1/models/gemini-3-pro:streamGenerateContent?alt=sse'
|
||||
url.split("/").pop()?.split(":")?.[1]?.startsWith("streamGenerateContent") ?? false,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE `model_rate_limit` (
|
||||
`key` varchar(255) NOT NULL,
|
||||
`interval` varchar(40) NOT NULL,
|
||||
`count` int NOT NULL,
|
||||
CONSTRAINT PRIMARY KEY(`key`,`interval`)
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE `coupon` (
|
||||
`email` varchar(255),
|
||||
`type` enum('BUILDATHON','GOFREEMONTH') NOT NULL,
|
||||
`time_redeemed` timestamp(3),
|
||||
CONSTRAINT PRIMARY KEY(`email`,`type`)
|
||||
);
|
||||
+2619
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user