Compare commits

..

1 Commits

Author SHA1 Message Date
Aiden Cline df4362a719 ignore: example tui plugin 2026-04-15 11:37:36 -05:00
780 changed files with 36877 additions and 43848 deletions
+1
View File
@@ -594,6 +594,7 @@ OPENCODE_DISABLE_CLAUDE_CODE
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT OPENCODE_DISABLE_CLAUDE_CODE_PROMPT
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS OPENCODE_DISABLE_CLAUDE_CODE_SKILLS
OPENCODE_DISABLE_DEFAULT_PLUGINS OPENCODE_DISABLE_DEFAULT_PLUGINS
OPENCODE_DISABLE_FILETIME_CHECK
OPENCODE_DISABLE_LSP_DOWNLOAD OPENCODE_DISABLE_LSP_DOWNLOAD
OPENCODE_DISABLE_MODELS_FETCH OPENCODE_DISABLE_MODELS_FETCH
OPENCODE_DISABLE_PRUNE OPENCODE_DISABLE_PRUNE
+5 -1
View File
@@ -1,6 +1,10 @@
{ {
"$schema": "https://opencode.ai/config.json", "$schema": "https://opencode.ai/config.json",
"provider": {}, "provider": {
"opencode": {
"options": {},
},
},
"permission": { "permission": {
"edit": { "edit": {
"packages/opencode/migration/*": "deny", "packages/opencode/migration/*": "deny",
@@ -0,0 +1,25 @@
import type { TuiPluginModule } from "@opencode-ai/plugin/tui"
let seen = false
const plugin: TuiPluginModule & { id: string } = {
id: "local.config-once-toast",
async tui(api) {
if (seen) return
const cfg = api.state.config
if (cfg.plugin !== undefined && !Array.isArray(cfg.plugin)) {
throw new Error("Invalid config: plugin must be an array")
}
const mdl = typeof cfg.model === "string" && cfg.model.trim() ? cfg.model : "default"
seen = true
api.ui.toast({
title: "Config check",
message: `This is a 1 time toast, validating ur config (model: ${mdl})`,
variant: "info",
})
},
}
export default plugin
+1 -1
View File
@@ -7,7 +7,7 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
Accept: "application/vnd.github+json", Accept: "application/vnd.github+json",
"Content-Type": "application/json", "Content-Type": "application/json",
...(options.headers instanceof Headers ? Object.fromEntries(options.headers.entries()) : options.headers), ...options.headers,
}, },
}) })
if (!response.ok) { if (!response.ok) {
+1 -1
View File
@@ -28,7 +28,7 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
Accept: "application/vnd.github+json", Accept: "application/vnd.github+json",
"Content-Type": "application/json", "Content-Type": "application/json",
...(options.headers instanceof Headers ? Object.fromEntries(options.headers.entries()) : options.headers), ...options.headers,
}, },
}) })
if (!response.ok) { if (!response.ok) {
+1
View File
@@ -1,6 +1,7 @@
{ {
"$schema": "https://opencode.ai/tui.json", "$schema": "https://opencode.ai/tui.json",
"plugin": [ "plugin": [
"./plugins/tui-config-once-toast.tsx",
[ [
"./plugins/tui-smoke.tsx", "./plugins/tui-smoke.tsx",
{ {
-51
View File
@@ -1,51 +0,0 @@
{
"$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
"no-unassigned-vars": "off",
// SolidJS tracks reactive deps by reading properties inside createEffect
"no-unused-expressions": "off",
// 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",
// 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"
},
"options": {
"typeAware": true
},
"options": {
"typeAware": true
},
"ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts", "**/sdk.gen.ts"]
}
+26 -1
View File
@@ -11,10 +11,35 @@
- Keep things in one function unless composable or reusable - Keep things in one function unless composable or reusable
- Avoid `try`/`catch` where possible - Avoid `try`/`catch` where possible
- Avoid using the `any` type - Avoid using the `any` type
- Prefer single word variable names where possible
- Use Bun APIs when possible, like `Bun.file()` - 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 - 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 - 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.
### Naming
Prefer single word names for variables and functions. Only use multiple words if necessary.
### Naming Enforcement (Read This)
THIS RULE IS MANDATORY FOR AGENT WRITTEN CODE.
- Use single word names by default for new locals, params, and helper functions.
- Multi-word names are allowed only when a single word would be unclear or ambiguous.
- Do not introduce new camelCase compounds when a short single-word alternative is clear.
- Before finishing edits, review touched lines and shorten newly introduced identifiers where possible.
- Good short names to prefer: `pid`, `cfg`, `err`, `opts`, `dir`, `root`, `child`, `state`, `timeout`.
- Examples to avoid unless truly required: `inputPID`, `existingClient`, `connectTimeout`, `workerPath`.
```ts
// Good
const foo = 1
function journal(dir: string) {}
// Bad
const fooBar = 1
function prepareJournal(dir: string) {}
```
Reduce total variable count by inlining when a value is only used once. Reduce total variable count by inlining when a value is only used once.
+53 -122
View File
@@ -19,8 +19,6 @@
"@typescript/native-preview": "catalog:", "@typescript/native-preview": "catalog:",
"glob": "13.0.5", "glob": "13.0.5",
"husky": "9.1.7", "husky": "9.1.7",
"oxlint": "1.60.0",
"oxlint-tsgolint": "0.21.0",
"prettier": "3.6.2", "prettier": "3.6.2",
"semver": "^7.6.0", "semver": "^7.6.0",
"sst": "3.18.10", "sst": "3.18.10",
@@ -29,7 +27,7 @@
}, },
"packages/app": { "packages/app": {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.4.11", "version": "1.4.6",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -83,7 +81,7 @@
}, },
"packages/console/app": { "packages/console/app": {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.4.11", "version": "1.4.6",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
@@ -117,7 +115,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.4.11", "version": "1.4.6",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1", "@jsx-email/render": "1.1.1",
@@ -144,7 +142,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.4.11", "version": "1.4.6",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "3.0.64", "@ai-sdk/anthropic": "3.0.64",
"@ai-sdk/openai": "3.0.48", "@ai-sdk/openai": "3.0.48",
@@ -168,7 +166,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.4.11", "version": "1.4.6",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@@ -192,7 +190,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.4.11", "version": "1.4.6",
"dependencies": { "dependencies": {
"@opencode-ai/app": "workspace:*", "@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
@@ -225,7 +223,7 @@
}, },
"packages/desktop-electron": { "packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron", "name": "@opencode-ai/desktop-electron",
"version": "1.4.11", "version": "1.4.6",
"dependencies": { "dependencies": {
"effect": "catalog:", "effect": "catalog:",
"electron-context-menu": "4.1.2", "electron-context-menu": "4.1.2",
@@ -268,7 +266,7 @@
}, },
"packages/enterprise": { "packages/enterprise": {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.4.11", "version": "1.4.6",
"dependencies": { "dependencies": {
"@opencode-ai/shared": "workspace:*", "@opencode-ai/shared": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
@@ -297,7 +295,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.4.11", "version": "1.4.6",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
@@ -313,7 +311,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.4.11", "version": "1.4.6",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -322,15 +320,15 @@
"@actions/github": "6.0.1", "@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.16.1", "@agentclientprotocol/sdk": "0.16.1",
"@ai-sdk/alibaba": "1.0.17", "@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.95", "@ai-sdk/amazon-bedrock": "4.0.93",
"@ai-sdk/anthropic": "3.0.71", "@ai-sdk/anthropic": "3.0.67",
"@ai-sdk/azure": "3.0.49", "@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cerebras": "2.0.41",
"@ai-sdk/cohere": "3.0.27", "@ai-sdk/cohere": "3.0.27",
"@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104", "@ai-sdk/gateway": "3.0.97",
"@ai-sdk/google": "3.0.63", "@ai-sdk/google": "3.0.63",
"@ai-sdk/google-vertex": "4.0.112", "@ai-sdk/google-vertex": "4.0.109",
"@ai-sdk/groq": "3.0.31", "@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.27", "@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.53", "@ai-sdk/openai": "3.0.53",
@@ -359,14 +357,13 @@
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@opencode-ai/server": "workspace:*",
"@openrouter/ai-sdk-provider": "2.5.1", "@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/exporter-trace-otlp-http": "0.214.0",
"@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1",
"@opentelemetry/sdk-trace-node": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1",
"@opentui/core": "catalog:", "@opentui/core": "0.1.99",
"@opentui/solid": "catalog:", "@opentui/solid": "0.1.99",
"@parcel/watcher": "2.5.1", "@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:", "@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2", "@solid-primitives/event-bus": "1.1.2",
@@ -386,7 +383,7 @@
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
"effect": "catalog:", "effect": "catalog:",
"fuzzysort": "3.1.0", "fuzzysort": "3.1.0",
"gitlab-ai-provider": "6.6.0", "gitlab-ai-provider": "6.4.2",
"glob": "13.0.5", "glob": "13.0.5",
"google-auth-library": "10.5.0", "google-auth-library": "10.5.0",
"gray-matter": "4.0.3", "gray-matter": "4.0.3",
@@ -458,23 +455,23 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.4.11", "version": "1.4.6",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"effect": "catalog:", "effect": "catalog:",
"zod": "catalog:", "zod": "catalog:",
}, },
"devDependencies": { "devDependencies": {
"@opentui/core": "catalog:", "@opentui/core": "0.1.99",
"@opentui/solid": "catalog:", "@opentui/solid": "0.1.99",
"@tsconfig/node22": "catalog:", "@tsconfig/node22": "catalog:",
"@types/node": "catalog:", "@types/node": "catalog:",
"@typescript/native-preview": "catalog:", "@typescript/native-preview": "catalog:",
"typescript": "catalog:", "typescript": "catalog:",
}, },
"peerDependencies": { "peerDependencies": {
"@opentui/core": ">=0.1.100", "@opentui/core": ">=0.1.99",
"@opentui/solid": ">=0.1.100", "@opentui/solid": ">=0.1.99",
}, },
"optionalPeers": [ "optionalPeers": [
"@opentui/core", "@opentui/core",
@@ -493,7 +490,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.4.11", "version": "1.4.6",
"dependencies": { "dependencies": {
"cross-spawn": "catalog:", "cross-spawn": "catalog:",
}, },
@@ -506,9 +503,20 @@
"typescript": "catalog:", "typescript": "catalog:",
}, },
}, },
"packages/server": {
"name": "@opencode-ai/server",
"version": "1.4.6",
"dependencies": {
"effect": "catalog:",
},
"devDependencies": {
"@typescript/native-preview": "catalog:",
"typescript": "catalog:",
},
},
"packages/shared": { "packages/shared": {
"name": "@opencode-ai/shared", "name": "@opencode-ai/shared",
"version": "1.4.11", "version": "1.4.6",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -516,23 +524,18 @@
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
"@npmcli/arborist": "catalog:", "@npmcli/arborist": "catalog:",
"effect": "catalog:", "effect": "catalog:",
"glob": "13.0.5",
"mime-types": "3.0.2", "mime-types": "3.0.2",
"minimatch": "10.2.5", "minimatch": "10.2.5",
"semver": "catalog:", "semver": "catalog:",
"xdg-basedir": "5.1.0",
"zod": "catalog:", "zod": "catalog:",
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@types/npmcli__arborist": "6.3.3",
"@types/semver": "catalog:", "@types/semver": "catalog:",
}, },
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.4.11", "version": "1.4.6",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@@ -567,7 +570,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.4.11", "version": "1.4.6",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -616,7 +619,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.4.11", "version": "1.4.6",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
@@ -675,8 +678,6 @@
"@npmcli/arborist": "9.4.0", "@npmcli/arborist": "9.4.0",
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
"@openauthjs/openauth": "0.0.0-20250322224806", "@openauthjs/openauth": "0.0.0-20250322224806",
"@opentui/core": "0.1.99",
"@opentui/solid": "0.1.99",
"@pierre/diffs": "1.1.0-beta.18", "@pierre/diffs": "1.1.0-beta.18",
"@playwright/test": "1.59.1", "@playwright/test": "1.59.1",
"@solid-primitives/storage": "4.3.3", "@solid-primitives/storage": "4.3.3",
@@ -692,7 +693,7 @@
"@types/node": "22.13.9", "@types/node": "22.13.9",
"@types/semver": "7.7.1", "@types/semver": "7.7.1",
"@typescript/native-preview": "7.0.0-dev.20251207.1", "@typescript/native-preview": "7.0.0-dev.20251207.1",
"ai": "6.0.168", "ai": "6.0.158",
"cross-spawn": "7.0.6", "cross-spawn": "7.0.6",
"diff": "8.0.2", "diff": "8.0.2",
"dompurify": "3.3.1", "dompurify": "3.3.1",
@@ -740,7 +741,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/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.95", "", { "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-qJKWEy+cNx3bLSJi/XpIVhv0P8KO0JFB1SvEroNWN8gKm820SIglBmXS10DTeXJdM5PPbQX4i/wJj5BHEk2LRQ=="], "@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/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=="], "@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=="],
@@ -760,11 +761,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/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.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/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/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": ["@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.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/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/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=="], "@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=="],
@@ -1562,6 +1563,8 @@
"@opencode-ai/sdk": ["@opencode-ai/sdk@workspace:packages/sdk/js"], "@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/shared": ["@opencode-ai/shared@workspace:packages/shared"],
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"], "@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
@@ -1588,7 +1591,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/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.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/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-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=="], "@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=="],
@@ -1686,56 +1689,6 @@
"@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.96.0", "", { "os": "win32", "cpu": "x64" }, "sha512-0fI0P0W7bSO/GCP/N5dkmtB9vBqCA4ggo1WmXTnxNJVmFFOtcA1vYm1I9jl8fxo+sucW2WnlpnI4fjKdo3JKxA=="], "@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=="],
"@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.60.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pJsgd9AfplLGBm1fIr25V6V14vMrayhx4uIQvlfH7jWs2SZwSrvi3TfgfJySB8T+hvyEH8K2zXljQiUnkgUnfQ=="],
"@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.60.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ue1aXHX49ivwflKqGJc7zcd/LeLgbhaTcDCQStgx5x06AXgjEAZmvrlMuIkWd4AL4FHQe6QJ9f33z04Cg448VQ=="],
"@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.60.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YCyQzsQtusQw+gNRW9rRTifSO+Dt/+dtCl2NHoDMZqJlRTEZ/Oht9YnuporI9yiTx7+cB+eqzX3MtHHVHGIWhg=="],
"@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-c7dxM2Zksa45Qw16i2iGY3Fti2NirJ38FrsBsKw+qcJ0OtqTsBgKJLF0xV+yLG56UH01Z8WRPgsw31e0MoRoGQ=="],
"@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ZWALoA42UYqBEP1Tbw9OWURgFGS1nWj2AAvLdY6ZcGx/Gj93qVCBKjcvwXMupZibYwFbi9s/rzqkZseb/6gVtQ=="],
"@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-tpy+1w4p9hN5CicMCxqNy6ymfRtV5ayE573vFNjp1k1TN/qhLFgflveZoE/0++RlkHikBz2vY545NWm/hp7big=="],
"@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-eDYDXZGhQAXyn6GwtwiX/qcLS0HlOLPJ/+iiIY8RYr+3P8oKBmgKxADLlniL6FtWfE7pPk7IGN9/xvDEvDvFeg=="],
"@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nxehly5XYBHUWI9VJX1bqCf9j/B43DaK/aS/T1fcxCpX3PA4Rm9BB54nPD1CKayT8xg6REN1ao+01hSRNgy8OA=="],
"@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-j1qf/NaUfOWQutjeoooNG1Q0zsK0XGmSu1uDLq3cctquRF3j7t9Hxqf/76ehCc5GEUAanth2W4Fa+XT1RFg/nw=="],
"@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-YELKPRefQ/q/h3RUmeRfPCUhh2wBvgV1RyZ/F9M9u8cDyXsQW2ojv1DeWQTt466yczDITjZnIOg/s05pk7Ve2A=="],
"@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.60.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-JkO3C6Gki7Y6h/MiIkFKvHFOz98/YWvQ4WYbK9DLXACMP2rjULzkeGyAzorJE5S1dzLQGFgeqvN779kSFwoV1g=="],
"@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XjKHdFVCpZZZSWBCKyyqCq65s2AKXykMXkjLoKYODrD+f5toLhlwsMESscu8FbgnJQ4Y/dpR/zdazsahmgBJIA=="],
"@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-js29ZWIuPhNWzY8NC7KoffEMEeWG105vbmm+8EOJsC+T/jHBiKIJEUF78+F/IrgEWMMP9N0kRND4Pp75+xAhKg=="],
"@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.60.0", "", { "os": "none", "cpu": "arm64" }, "sha512-H+PUITKHk04stFpWj3x3Kg08Afp/bcXSBi0EhasR5a0Vw7StXHTzdl655PUI0fB4qdh2Wsu6Dsi+3ACxPoyQnA=="],
"@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.60.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-WA/yc7f7ZfCefBXVzNHn1Ztulb1EFwNBb4jMZ6pjML0zz6pHujlF3Q3jySluz3XHl/GNeMTntG1seUBWVMlMag=="],
"@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.60.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-33YxL1sqwYNZXtn3MD/4dno6s0xeedXOJlT1WohkVD565WvohClZUr7vwKdAk954n4xiEWJkewiCr+zLeq7AeA=="],
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-JOro4ZcfBLamJCyfURQmOQByoorgOdx3ZjAkSqnb/CyG/i+lN3KoV5LAgk5ZAW6DPq7/Cx7n23f8DuTWXTWgyQ=="],
"@pagefind/darwin-arm64": ["@pagefind/darwin-arm64@1.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ=="], "@pagefind/darwin-arm64": ["@pagefind/darwin-arm64@1.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ=="],
"@pagefind/darwin-x64": ["@pagefind/darwin-x64@1.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw=="], "@pagefind/darwin-x64": ["@pagefind/darwin-x64@1.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw=="],
@@ -2456,7 +2409,7 @@
"@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="], "@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.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
"@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=="], "@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=="],
@@ -2516,7 +2469,7 @@
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"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": ["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-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=="], "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=="],
@@ -3314,7 +3267,7 @@
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
"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=="], "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=="],
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
@@ -4116,10 +4069,6 @@
"oxc-transform": ["oxc-transform@0.96.0", "", { "optionalDependencies": { "@oxc-transform/binding-android-arm64": "0.96.0", "@oxc-transform/binding-darwin-arm64": "0.96.0", "@oxc-transform/binding-darwin-x64": "0.96.0", "@oxc-transform/binding-freebsd-x64": "0.96.0", "@oxc-transform/binding-linux-arm-gnueabihf": "0.96.0", "@oxc-transform/binding-linux-arm-musleabihf": "0.96.0", "@oxc-transform/binding-linux-arm64-gnu": "0.96.0", "@oxc-transform/binding-linux-arm64-musl": "0.96.0", "@oxc-transform/binding-linux-riscv64-gnu": "0.96.0", "@oxc-transform/binding-linux-s390x-gnu": "0.96.0", "@oxc-transform/binding-linux-x64-gnu": "0.96.0", "@oxc-transform/binding-linux-x64-musl": "0.96.0", "@oxc-transform/binding-wasm32-wasi": "0.96.0", "@oxc-transform/binding-win32-arm64-msvc": "0.96.0", "@oxc-transform/binding-win32-x64-msvc": "0.96.0" } }, "sha512-dQPNIF+gHpSkmC0+Vg9IktNyhcn28Y8R3eTLyzn52UNymkasLicl3sFAtz7oEVuFmCpgGjaUTKkwk+jW2cHpDQ=="], "oxc-transform": ["oxc-transform@0.96.0", "", { "optionalDependencies": { "@oxc-transform/binding-android-arm64": "0.96.0", "@oxc-transform/binding-darwin-arm64": "0.96.0", "@oxc-transform/binding-darwin-x64": "0.96.0", "@oxc-transform/binding-freebsd-x64": "0.96.0", "@oxc-transform/binding-linux-arm-gnueabihf": "0.96.0", "@oxc-transform/binding-linux-arm-musleabihf": "0.96.0", "@oxc-transform/binding-linux-arm64-gnu": "0.96.0", "@oxc-transform/binding-linux-arm64-musl": "0.96.0", "@oxc-transform/binding-linux-riscv64-gnu": "0.96.0", "@oxc-transform/binding-linux-s390x-gnu": "0.96.0", "@oxc-transform/binding-linux-x64-gnu": "0.96.0", "@oxc-transform/binding-linux-x64-musl": "0.96.0", "@oxc-transform/binding-wasm32-wasi": "0.96.0", "@oxc-transform/binding-win32-arm64-msvc": "0.96.0", "@oxc-transform/binding-win32-x64-msvc": "0.96.0" } }, "sha512-dQPNIF+gHpSkmC0+Vg9IktNyhcn28Y8R3eTLyzn52UNymkasLicl3sFAtz7oEVuFmCpgGjaUTKkwk+jW2cHpDQ=="],
"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-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
"p-defer": ["p-defer@3.0.0", "", {}, "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw=="], "p-defer": ["p-defer@3.0.0", "", {}, "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw=="],
@@ -5154,11 +5103,7 @@
"@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/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.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="], "@ai-sdk/amazon-bedrock/@ai-sdk/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/@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=="], "@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=="],
@@ -5172,9 +5117,7 @@
"@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/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.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/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/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=="], "@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=="],
@@ -5584,18 +5527,6 @@
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], "@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/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=="], "@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=="],
@@ -5702,7 +5633,7 @@
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"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/@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/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-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=="],
@@ -5920,7 +5851,7 @@
"nypm/tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], "nypm/tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="],
"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/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/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=="], "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=="],
+6 -6
View File
@@ -281,7 +281,7 @@ async function assertOpencodeConnected() {
}) })
connected = true connected = true
break break
} catch {} } catch (e) {}
await sleep(300) await sleep(300)
} while (retry++ < 30) } while (retry++ < 30)
@@ -513,7 +513,7 @@ async function subscribeSessionEvents() {
const decoder = new TextDecoder() const decoder = new TextDecoder()
let text = "" let text = ""
void (async () => { ;(async () => {
while (true) { while (true) {
try { try {
const { done, value } = await reader.read() const { done, value } = await reader.read()
@@ -542,7 +542,7 @@ async function subscribeSessionEvents() {
? JSON.stringify(part.state.input) ? JSON.stringify(part.state.input)
: "Unknown" : "Unknown"
console.log() 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") { if (part.type === "text") {
@@ -561,7 +561,7 @@ async function subscribeSessionEvents() {
if (evt.properties.info.id !== session.id) continue if (evt.properties.info.id !== session.id) continue
session = evt.properties.info session = evt.properties.info
} }
} catch { } catch (e) {
// Ignore parse errors // Ignore parse errors
} }
} }
@@ -576,7 +576,7 @@ async function subscribeSessionEvents() {
async function summarize(response: string) { async function summarize(response: string) {
try { try {
return await chat(`Summarize the following in less than 40 characters:\n\n${response}`) return await chat(`Summarize the following in less than 40 characters:\n\n${response}`)
} catch { } catch (e) {
if (isScheduleEvent()) { if (isScheduleEvent()) {
return "Scheduled task changes" return "Scheduled task changes"
} }
@@ -776,7 +776,7 @@ async function assertPermissions() {
console.log(` permission: ${permission}`) console.log(` permission: ${permission}`)
} catch (error) { } catch (error) {
console.error(`Failed to check permissions: ${error}`) console.error(`Failed to check permissions: ${error}`)
throw new Error(`Failed to check permissions for user ${actor}: ${error}`, { cause: error }) throw new Error(`Failed to check permissions for user ${actor}: ${error}`)
} }
if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`) if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)
+2 -2
View File
@@ -1,9 +1,9 @@
import { SECRET } from "./secret" import { SECRET } from "./secret"
import { shortDomain } from "./stage" import { domain, shortDomain } from "./stage"
const storage = new sst.cloudflare.Bucket("EnterpriseStorage") const storage = new sst.cloudflare.Bucket("EnterpriseStorage")
new sst.cloudflare.x.SolidStart("Teams", { const teams = new sst.cloudflare.x.SolidStart("Teams", {
domain: shortDomain, domain: shortDomain,
path: "packages/enterprise", path: "packages/enterprise",
buildCommand: "bun run build:cloudflare", buildCommand: "bun run build:cloudflare",
+4 -4
View File
@@ -1,8 +1,8 @@
{ {
"nodeModules": { "nodeModules": {
"x86_64-linux": "sha256-GjpBQhvGLTM6NWX29b/mS+KjrQPl0w9VjQHH5jaK9SM=", "x86_64-linux": "sha256-3kpnjBg7AQanyDGTOFdYBFvo9O9Rfnu0Wmi8bY5LpEI=",
"aarch64-linux": "sha256-F5h9p+iZ8CASdUYaYR7O22NwBRa/iT+ZinUxO8lbPTc=", "aarch64-linux": "sha256-8rQ+SNUiSpA2Ea3NrYNGopHQsnY7Y8qBsXCqL6GMt24=",
"aarch64-darwin": "sha256-jWo5yvCtjVKRf9i5XUcTTaLtj2+G6+T1Td2llO/cT5I=", "aarch64-darwin": "sha256-OASMkW5hnXucV6lSmxrQo73lGSEKN4MQPNGNV0i7jdo=",
"x86_64-darwin": "sha256-LzV+5/8P2mkiFHmt+a8zDeJjRbU8z9nssSA4tzv1HxA=" "x86_64-darwin": "sha256-CmHqXlm8wnLcwSSK0ghxAf+DVurEltMaxrUbWh9/ZGE="
} }
} }
-1
View File
@@ -55,7 +55,6 @@ stdenvNoCC.mkDerivation {
--filter './packages/opencode' \ --filter './packages/opencode' \
--filter './packages/desktop' \ --filter './packages/desktop' \
--filter './packages/app' \ --filter './packages/app' \
--filter './packages/shared' \
--frozen-lockfile \ --frozen-lockfile \
--ignore-scripts \ --ignore-scripts \
--no-progress --no-progress
+1 -6
View File
@@ -11,7 +11,6 @@
"dev:web": "bun --cwd packages/app dev", "dev:web": "bun --cwd packages/app dev",
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev", "dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
"dev:storybook": "bun --cwd packages/storybook storybook", "dev:storybook": "bun --cwd packages/storybook storybook",
"lint": "oxlint",
"typecheck": "bun turbo typecheck", "typecheck": "bun turbo typecheck",
"postinstall": "bun run --cwd packages/opencode fix-node-pty", "postinstall": "bun run --cwd packages/opencode fix-node-pty",
"prepare": "husky", "prepare": "husky",
@@ -34,8 +33,6 @@
"@types/cross-spawn": "6.0.6", "@types/cross-spawn": "6.0.6",
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
"@hono/zod-validator": "0.4.2", "@hono/zod-validator": "0.4.2",
"@opentui/core": "0.1.99",
"@opentui/solid": "0.1.99",
"ulid": "3.0.1", "ulid": "3.0.1",
"@kobalte/core": "0.13.11", "@kobalte/core": "0.13.11",
"@types/luxon": "3.7.1", "@types/luxon": "3.7.1",
@@ -53,7 +50,7 @@
"drizzle-kit": "1.0.0-beta.19-d95b7a4", "drizzle-kit": "1.0.0-beta.19-d95b7a4",
"drizzle-orm": "1.0.0-beta.19-d95b7a4", "drizzle-orm": "1.0.0-beta.19-d95b7a4",
"effect": "4.0.0-beta.48", "effect": "4.0.0-beta.48",
"ai": "6.0.168", "ai": "6.0.158",
"cross-spawn": "7.0.6", "cross-spawn": "7.0.6",
"hono": "4.10.7", "hono": "4.10.7",
"hono-openapi": "1.1.2", "hono-openapi": "1.1.2",
@@ -88,8 +85,6 @@
"@typescript/native-preview": "catalog:", "@typescript/native-preview": "catalog:",
"glob": "13.0.5", "glob": "13.0.5",
"husky": "9.1.7", "husky": "9.1.7",
"oxlint": "1.60.0",
"oxlint-tsgolint": "0.21.0",
"prettier": "3.6.2", "prettier": "3.6.2",
"semver": "^7.6.0", "semver": "^7.6.0",
"sst": "3.18.10", "sst": "3.18.10",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.4.11", "version": "1.4.6",
"description": "", "description": "",
"type": "module", "type": "module",
"exports": { "exports": {
+2 -2
View File
@@ -180,8 +180,8 @@ describe("SerializeAddon", () => {
await writeAndWait(term, input) await writeAndWait(term, input)
const origLine = term.buffer.active.getLine(0) const origLine = term.buffer.active.getLine(0)
const _origFg = origLine!.getCell(0)!.getFgColor() const origFg = origLine!.getCell(0)!.getFgColor()
const _origBg = origLine!.getCell(0)!.getBgColor() const origBg = origLine!.getCell(0)!.getBgColor()
expect(origLine!.getCell(0)!.isBold()).toBe(1) expect(origLine!.getCell(0)!.isBold()).toBe(1)
const serialized = addon.serialize({ range: { start: 0, end: 0 } }) const serialized = addon.serialize({ range: { start: 0, end: 0 } })
+2 -2
View File
@@ -258,8 +258,8 @@ class StringSerializeHandler extends BaseSerializeHandler {
} }
protected _beforeSerialize(rows: number, start: number, _end: number): void { protected _beforeSerialize(rows: number, start: number, _end: number): void {
this._allRows = Array.from<string>({ length: rows }) this._allRows = new Array<string>(rows)
this._allRowSeparators = Array.from<string>({ length: rows }) this._allRowSeparators = new Array<string>(rows)
this._rowIndex = 0 this._rowIndex = 0
this._currentRow = "" this._currentRow = ""
+15 -19
View File
@@ -10,7 +10,7 @@ import { ThemeProvider } from "@opencode-ai/ui/theme/context"
import { MetaProvider } from "@solidjs/meta" import { MetaProvider } from "@solidjs/meta"
import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router" import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query" import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
import { Effect } from "effect" import { type Duration, Effect } from "effect"
import { import {
type Component, type Component,
createMemo, createMemo,
@@ -121,10 +121,10 @@ function SessionProviders(props: ParentProps) {
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) { function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
return ( return (
<AppShellProviders> <AppShellProviders>
{/*<Suspense fallback={<Loading />}>*/} <Suspense fallback={<Loading />}>
{props.appChildren} {props.appChildren}
{props.children} {props.children}
{/*</Suspense>*/} </Suspense>
</AppShellProviders> </AppShellProviders>
) )
} }
@@ -156,6 +156,11 @@ 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 }>) { function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
const server = useServer() const server = useServer()
const checkServerHealth = useCheckServerHealth() const checkServerHealth = useCheckServerHealth()
@@ -184,41 +189,32 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
) )
return ( return (
<Suspense <Show
when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
fallback={ fallback={
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base"> <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" /> <Splash class="w-16 h-20 opacity-50 animate-pulse" />
</div> </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 <Show
when={startupHealthCheck()} when={startupHealthCheck()}
fallback={ fallback={
<ConnectionError <ConnectionError
onRetry={() => { onRetry={() => {
if (checkMode() === "background") void healthCheckActions.refetch() if (checkMode() === "background") healthCheckActions.refetch()
}} }}
onServerSelected={(key) => { onServerSelected={(key) => {
setCheckMode("blocking") setCheckMode("blocking")
server.setActive(key) server.setActive(key)
void healthCheckActions.refetch() healthCheckActions.refetch()
}} }}
/> />
} }
> >
{props.children} {props.children}
</Show> </Show>
{/*</Show>*/} </Show>
</Suspense>
) )
} }
@@ -327,7 +327,7 @@ export function DialogConnectProvider(props: { provider: string }) {
if (loading()) return if (loading()) return
if (methods().length === 1) { if (methods().length === 1) {
auto = true auto = true
void selectMethod(0) selectMethod(0)
} }
}) })
@@ -373,7 +373,7 @@ export function DialogConnectProvider(props: { provider: string }) {
key={(m) => m?.label} key={(m) => m?.label}
onSelect={async (selected, index) => { onSelect={async (selected, index) => {
if (!selected) return if (!selected) return
void selectMethod(index) selectMethod(index)
}} }}
> >
{(i) => ( {(i) => (
@@ -348,8 +348,8 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
const open = (path: string) => { const open = (path: string) => {
const value = file.tab(path) const value = file.tab(path)
void tabs().open(value) tabs().open(value)
void file.load(path) file.load(path)
if (!view().reviewPanel.opened()) view().reviewPanel.open() if (!view().reviewPanel.opened()) view().reviewPanel.open()
layout.fileTree.setTab("all") layout.fileTree.setTab("all")
props.onOpenFile?.(path) props.onOpenFile?.(path)
@@ -344,7 +344,7 @@ export function DialogSelectServer() {
createEffect(() => { createEffect(() => {
items() items()
void refreshHealth() refreshHealth()
const interval = setInterval(refreshHealth, 10_000) const interval = setInterval(refreshHealth, 10_000)
onCleanup(() => clearInterval(interval)) onCleanup(() => clearInterval(interval))
}) })
@@ -498,7 +498,7 @@ export function DialogSelectServer() {
async function handleRemove(url: ServerConnection.Key) { async function handleRemove(url: ServerConnection.Key) {
server.remove(url) server.remove(url)
if ((await platform.getDefaultServer?.()) === url) { if ((await platform.getDefaultServer?.()) === url) {
void platform.setDefaultServer?.(null) platform.setDefaultServer?.(null)
} }
} }
@@ -536,7 +536,7 @@ export function DialogSelectServer() {
items={sortedItems} items={sortedItems}
key={(x) => x.http.url} key={(x) => x.http.url}
onSelect={(x) => { onSelect={(x) => {
if (x) void select(x) if (x) select(x)
}} }}
divider={true} 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" 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"
+2 -1
View File
@@ -14,6 +14,7 @@ import {
Switch, Switch,
untrack, untrack,
type ComponentProps, type ComponentProps,
type JSXElement,
type ParentProps, type ParentProps,
} from "solid-js" } from "solid-js"
import { Dynamic } from "solid-js/web" import { Dynamic } from "solid-js/web"
@@ -148,7 +149,7 @@ const FileTreeNode = (
classList={{ classList={{
"w-full min-w-0 h-6 flex items-center justify-start gap-x-1.5 rounded-md px-1.5 py-0 text-left hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true, "w-full min-w-0 h-6 flex items-center justify-start gap-x-1.5 rounded-md px-1.5 py-0 text-left hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true,
"bg-surface-base-active": local.node.path === local.active, "bg-surface-base-active": local.node.path === local.active,
...local.classList, ...(local.classList ?? {}),
[local.class ?? ""]: !!local.class, [local.class ?? ""]: !!local.class,
[local.nodeClass ?? ""]: !!local.nodeClass, [local.nodeClass ?? ""]: !!local.nodeClass,
}} }}
+99 -114
View File
@@ -54,8 +54,6 @@ import { PromptImageAttachments } from "./prompt-input/image-attachments"
import { PromptDragOverlay } from "./prompt-input/drag-overlay" import { PromptDragOverlay } from "./prompt-input/drag-overlay"
import { promptPlaceholder } from "./prompt-input/placeholder" import { promptPlaceholder } from "./prompt-input/placeholder"
import { ImagePreview } from "@opencode-ai/ui/image-preview" import { ImagePreview } from "@opencode-ai/ui/image-preview"
import { useQuery } from "@tanstack/solid-query"
import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"
interface PromptInputProps { interface PromptInputProps {
class?: string class?: string
@@ -102,7 +100,6 @@ const NON_EMPTY_TEXT = /[^\s\u200B]/
export const PromptInput: Component<PromptInputProps> = (props) => { export const PromptInput: Component<PromptInputProps> = (props) => {
const sdk = useSDK() const sdk = useSDK()
const sync = useSync() const sync = useSync()
const local = useLocal() const local = useLocal()
const files = useFile() const files = useFile()
@@ -215,9 +212,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
if (!view().reviewPanel.opened()) view().reviewPanel.open() if (!view().reviewPanel.opened()) view().reviewPanel.open()
layout.fileTree.setTab("all") layout.fileTree.setTab("all")
const tab = files.tab(item.path) const tab = files.tab(item.path)
void tabs().open(tab) tabs().open(tab)
tabs().setActive(tab) tabs().setActive(tab)
void Promise.resolve(files.load(item.path)).finally(() => queueCommentFocus()) Promise.resolve(files.load(item.path)).finally(() => queueCommentFocus())
} }
const recent = createMemo(() => { const recent = createMemo(() => {
@@ -1142,7 +1139,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
} }
if (working()) { if (working()) {
void abort() abort()
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
return return
@@ -1208,7 +1205,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return return
} }
if (working()) { if (working()) {
void abort() abort()
event.preventDefault() event.preventDefault()
} }
return return
@@ -1248,18 +1245,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
) { ) {
return return
} }
void handleSubmit(event) 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 ( return (
<div class="relative size-full _max-h-[320px] flex flex-col gap-0"> <div class="relative size-full _max-h-[320px] flex flex-col gap-0">
<PromptPopover <PromptPopover
@@ -1455,89 +1444,53 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span> <span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
<div class="size-4 shrink-0" /> <div class="size-4 shrink-0" />
</div> </div>
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7"> <div class="flex items-center gap-1.5 min-w-0 flex-1">
<Show when={!agentsLoading()}> <div data-component="prompt-agent-control">
<div data-component="prompt-agent-control"> <TooltipKeybind
<TooltipKeybind placement="top"
placement="top" gutter={4}
gutter={4} title={language.t("command.agent.cycle")}
title={language.t("command.agent.cycle")} keybind={command.keybind("agent.cycle")}
keybind={command.keybind("agent.cycle")} >
> <Select
<Select size="normal"
size="normal" options={agentNames()}
options={agentNames()} current={local.agent.current()?.name ?? ""}
current={local.agent.current()?.name ?? ""} onSelect={(value) => {
onSelect={(value) => { local.agent.set(value)
local.agent.set(value) restoreFocus()
restoreFocus() }}
}} class="capitalize max-w-[160px] text-text-base"
class="capitalize max-w-[160px] text-text-base" valueClass="truncate text-13-regular text-text-base"
valueClass="truncate text-13-regular text-text-base" triggerStyle={control()}
triggerStyle={control()} triggerProps={{ "data-action": "prompt-agent" }}
triggerProps={{ "data-action": "prompt-agent" }} variant="ghost"
variant="ghost" />
/> </TooltipKeybind>
</TooltipKeybind> </div>
</div> <Show when={store.mode !== "shell"}>
</Show> <div data-component="prompt-model-control">
<Show when={!providersLoading()}> <Show
<Show when={store.mode !== "shell"}> when={providers.paid().length > 0}
<div data-component="prompt-model-control"> fallback={
<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 <TooltipKeybind
placement="top" placement="top"
gutter={4} gutter={4}
title={language.t("command.model.choose")} title={language.t("command.model.choose")}
keybind={command.keybind("model.choose")} keybind={command.keybind("model.choose")}
> >
<ModelSelectorPopover <Button
model={local.model} data-action="prompt-model"
triggerAs={Button} as="div"
triggerProps={{ variant="ghost"
variant: "ghost", size="normal"
size: "normal", class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
style: control(), style={control()}
class: "min-w-0 max-w-[320px] text-13-regular text-text-base group", onClick={() => {
"data-action": "prompt-model", void import("@/components/dialog-select-model-unpaid").then((x) => {
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />)
})
}} }}
onClose={restoreFocus}
> >
<Show when={local.model.current()?.provider?.id}> <Show when={local.model.current()?.provider?.id}>
<ProviderIcon <ProviderIcon
@@ -1550,35 +1503,67 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
{local.model.current()?.name ?? language.t("dialog.model.select.title")} {local.model.current()?.name ?? language.t("dialog.model.select.title")}
</span> </span>
<Icon name="chevron-down" size="small" class="shrink-0" /> <Icon name="chevron-down" size="small" class="shrink-0" />
</ModelSelectorPopover> </Button>
</TooltipKeybind> </TooltipKeybind>
</Show> }
</div> >
<div data-component="prompt-variant-control">
<TooltipKeybind <TooltipKeybind
placement="top" placement="top"
gutter={4} gutter={4}
title={language.t("command.model.variant.cycle")} title={language.t("command.model.choose")}
keybind={command.keybind("model.variant.cycle")} keybind={command.keybind("model.choose")}
> >
<Select <ModelSelectorPopover
size="normal" model={local.model}
options={variants()} triggerAs={Button}
current={local.model.variant.current() ?? "default"} triggerProps={{
label={(x) => (x === "default" ? language.t("common.default") : x)} variant: "ghost",
onSelect={(value) => { size: "normal",
local.model.variant.set(value === "default" ? undefined : value) style: control(),
restoreFocus() class: "min-w-0 max-w-[320px] text-13-regular text-text-base group",
"data-action": "prompt-model",
}} }}
class="capitalize max-w-[160px] text-text-base" onClose={restoreFocus}
valueClass="truncate text-13-regular text-text-base" >
triggerStyle={control()} <Show when={local.model.current()?.provider?.id}>
triggerProps={{ "data-action": "prompt-model-variant" }} <ProviderIcon
variant="ghost" 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>
</TooltipKeybind> </TooltipKeybind>
</div> </Show>
</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>
</Show> </Show>
</div> </div>
</div> </div>
@@ -295,7 +295,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const mode = input.mode() const mode = input.mode()
if (text.trim().length === 0 && images.length === 0 && input.commentCount() === 0) { if (text.trim().length === 0 && images.length === 0 && input.commentCount() === 0) {
if (input.working()) void abort() if (input.working()) abort()
return return
} }
@@ -24,7 +24,7 @@ function openSessionContext(args: {
}) { }) {
if (!args.view.reviewPanel.opened()) args.view.reviewPanel.open() if (!args.view.reviewPanel.opened()) args.view.reviewPanel.open()
if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all") if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all")
void args.tabs.open("context") args.tabs.open("context")
args.tabs.setActive("context") args.tabs.setActive("context")
} }
@@ -8,7 +8,7 @@ import { Spinner } from "@opencode-ai/ui/spinner"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { getFilename } from "@opencode-ai/shared/util/path" import { getFilename } from "@opencode-ai/shared/util/path"
import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js" import { createEffect, createMemo, For, onCleanup, Show } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { Portal } from "solid-js/web" import { Portal } from "solid-js/web"
import { useCommand } from "@/context/command" import { useCommand } from "@/context/command"
@@ -16,7 +16,6 @@ import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"
import { useServer } from "@/context/server" import { useServer } from "@/context/server"
import { useSettings } from "@/context/settings"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { useTerminal } from "@/context/terminal" import { useTerminal } from "@/context/terminal"
import { focusTerminalById } from "@/pages/session/helpers" import { focusTerminalById } from "@/pages/session/helpers"
@@ -135,7 +134,6 @@ export function SessionHeader() {
const server = useServer() const server = useServer()
const platform = usePlatform() const platform = usePlatform()
const language = useLanguage() const language = useLanguage()
const settings = useSettings()
const sync = useSync() const sync = useSync()
const terminal = useTerminal() const terminal = useTerminal()
const { params, view } = useSessionLayout() const { params, view } = useSessionLayout()
@@ -153,11 +151,6 @@ export function SessionHeader() {
}) })
const hotkey = createMemo(() => command.keybind("file.open")) const hotkey = createMemo(() => command.keybind("file.open"))
const os = createMemo(() => detectOS(platform)) 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>>>({ const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
finder: true, finder: true,
@@ -269,16 +262,12 @@ export function SessionHeader() {
.catch((err: unknown) => showRequestError(language, err)) .catch((err: unknown) => showRequestError(language, err))
} }
const [centerMount, setCenterMount] = createSignal<HTMLElement | null>(null) const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
const [rightMount, setRightMount] = createSignal<HTMLElement | null>(null) const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
onMount(() => {
setCenterMount(document.getElementById("opencode-titlebar-center"))
setRightMount(document.getElementById("opencode-titlebar-right"))
})
return ( return (
<> <>
<Show when={search() && centerMount()}> <Show when={centerMount()}>
{(mount) => ( {(mount) => (
<Portal mount={mount()}> <Portal mount={mount()}>
<Button <Button
@@ -426,28 +415,24 @@ export function SessionHeader() {
</div> </div>
</Show> </Show>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Show when={status()}> <Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}> <StatusPopover />
<StatusPopover /> </Tooltip>
</Tooltip> <TooltipKeybind
</Show> title={language.t("command.terminal.toggle")}
<Show when={term()}> keybind={command.keybind("terminal.toggle")}
<TooltipKeybind >
title={language.t("command.terminal.toggle")} <Button
keybind={command.keybind("terminal.toggle")} 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"
> >
<Button <Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
variant="ghost" </Button>
class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border shrink-0" </TooltipKeybind>
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"> <div class="hidden md:flex items-center gap-1 shrink-0">
<TooltipKeybind <TooltipKeybind
@@ -466,32 +451,30 @@ export function SessionHeader() {
</Button> </Button>
</TooltipKeybind> </TooltipKeybind>
<Show when={tree()}> <TooltipKeybind
<TooltipKeybind title={language.t("command.fileTree.toggle")}
title={language.t("command.fileTree.toggle")} keybind={command.keybind("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"
> >
<Button <div class="relative flex items-center justify-center size-4">
variant="ghost" <Icon
class="titlebar-icon w-8 h-6 p-0 box-border" size="small"
onClick={() => layout.fileTree.toggle()} name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"}
aria-label={language.t("command.fileTree.toggle")} classList={{
aria-expanded={layout.fileTree.opened()} "text-icon-strong": layout.fileTree.opened(),
aria-controls="file-tree-panel" "text-icon-weak": !layout.fileTree.opened(),
> }}
<div class="relative flex items-center justify-center size-4"> />
<Icon </div>
size="small" </Button>
name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"} </TooltipKeybind>
classList={{
"text-icon-strong": layout.fileTree.opened(),
"text-icon-weak": !layout.fileTree.opened(),
}}
/>
</div>
</Button>
</TooltipKeybind>
</Show>
</div> </div>
</div> </div>
</div> </div>
@@ -44,7 +44,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
const close = () => { const close = () => {
const count = terminal.all().length const count = terminal.all().length
void terminal.close(props.terminal.id) terminal.close(props.terminal.id)
if (count === 1) { if (count === 1) {
props.onClose?.() props.onClose?.()
} }
@@ -106,7 +106,6 @@ export const SettingsGeneral: Component = () => {
permission.disableAutoAccept(params.id, value) permission.disableAutoAccept(params.id, value)
} }
const desktop = createMemo(() => platform.platform === "desktop")
const check = () => { const check = () => {
if (!platform.checkUpdate) return if (!platform.checkUpdate) return
@@ -280,74 +279,6 @@ export const SettingsGeneral: Component = () => {
</div> </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 = () => ( const AppearanceSection = () => (
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.appearance")}</h3> <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.appearance")}</h3>
@@ -596,7 +527,6 @@ export const SettingsGeneral: Component = () => {
</div> </div>
) )
console.log(import.meta.env)
return ( 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="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)]"> <div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
@@ -679,10 +609,6 @@ export const SettingsGeneral: Component = () => {
) )
}} }}
</Show> </Show>
<Show when={desktop() && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"}>
<AdvancedSection />
</Show>
</div> </div>
</div> </div>
) )
+4 -4
View File
@@ -191,7 +191,7 @@ export const Terminal = (props: TerminalProps) => {
const scrollY = typeof local.pty.scrollY === "number" ? local.pty.scrollY : undefined const scrollY = typeof local.pty.scrollY === "number" ? local.pty.scrollY : undefined
let ws: WebSocket | undefined let ws: WebSocket | undefined
let term: Term | undefined let term: Term | undefined
let _ghostty: Ghostty let ghostty: Ghostty
let serializeAddon: SerializeAddon let serializeAddon: SerializeAddon
let fitAddon: FitAddon let fitAddon: FitAddon
let handleResize: () => void let handleResize: () => void
@@ -372,7 +372,7 @@ export const Terminal = (props: TerminalProps) => {
cleanup() cleanup()
return return
} }
_ghostty = g ghostty = g
term = t term = t
output = terminalWriter((data, done) => output = terminalWriter((data, done) =>
t.write(data, () => { t.write(data, () => {
@@ -415,7 +415,7 @@ export const Terminal = (props: TerminalProps) => {
if (local.autoFocus !== false) focusTerminal() if (local.autoFocus !== false) focusTerminal()
if (typeof document !== "undefined" && document.fonts) { if (typeof document !== "undefined" && document.fonts) {
void document.fonts.ready.then(scheduleFit) document.fonts.ready.then(scheduleFit)
} }
const onResize = t.onResize((size) => { const onResize = t.onResize((size) => {
@@ -634,7 +634,7 @@ export const Terminal = (props: TerminalProps) => {
tabIndex={-1} tabIndex={-1}
style={{ "background-color": terminalColors().background }} style={{ "background-color": terminalColors().background }}
classList={{ classList={{
...local.classList, ...(local.classList ?? {}),
"select-text": true, "select-text": true,
"size-full px-6 py-3 font-mono relative overflow-hidden": true, "size-full px-6 py-3 font-mono relative overflow-hidden": true,
[local.class ?? ""]: !!local.class, [local.class ?? ""]: !!local.class,
+34 -43
View File
@@ -1,4 +1,4 @@
import { createEffect, createMemo, Show, untrack } from "solid-js" import { createEffect, createMemo, onCleanup, Show, untrack } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { useLocation, useNavigate, useParams } from "@solidjs/router" import { useLocation, useNavigate, useParams } from "@solidjs/router"
import { IconButton } from "@opencode-ai/ui/icon-button" import { IconButton } from "@opencode-ai/ui/icon-button"
@@ -11,7 +11,6 @@ import { useLayout } from "@/context/layout"
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"
import { useCommand } from "@/context/command" import { useCommand } from "@/context/command"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { applyPath, backPath, forwardPath } from "./titlebar-history" import { applyPath, backPath, forwardPath } from "./titlebar-history"
type TauriDesktopWindow = { type TauriDesktopWindow = {
@@ -41,7 +40,6 @@ export function Titlebar() {
const platform = usePlatform() const platform = usePlatform()
const command = useCommand() const command = useCommand()
const language = useLanguage() const language = useLanguage()
const settings = useSettings()
const theme = useTheme() const theme = useTheme()
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
@@ -80,7 +78,6 @@ export function Titlebar() {
const canBack = createMemo(() => history.index > 0) const canBack = createMemo(() => history.index > 0)
const canForward = createMemo(() => history.index < history.stack.length - 1) const canForward = createMemo(() => history.index < history.stack.length - 1)
const hasProjects = createMemo(() => layout.projects.list().length > 0) const hasProjects = createMemo(() => layout.projects.list().length > 0)
const nav = createMemo(() => import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" || settings.general.showNavigation())
const back = () => { const back = () => {
const next = backPath(history) const next = backPath(history)
@@ -255,47 +252,41 @@ export function Titlebar() {
</div> </div>
</div> </div>
</Show> </Show>
<div <Show when={hasProjects()}>
class="flex items-center shrink-0" <div
classList={{ class="flex items-center gap-0 transition-transform"
"-translate-x-[36px]": layout.sidebar.opened() && !!params.dir, classList={{
"duration-180 ease-out": !layout.sidebar.opened(), "translate-x-0": !layout.sidebar.opened(),
"duration-180 ease-in": layout.sidebar.opened(), "-translate-x-[36px]": layout.sidebar.opened(),
}} "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}> <Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
<Button <Button
variant="ghost" variant="ghost"
icon="chevron-left" icon="chevron-left"
class="titlebar-icon w-6 h-6 p-0 box-border" class="titlebar-icon w-6 h-6 p-0 box-border"
disabled={!canBack()} disabled={!canBack()}
onClick={back} onClick={back}
aria-label={language.t("common.goBack")} aria-label={language.t("common.goBack")}
/> />
</Tooltip> </Tooltip>
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}> <Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
<Button <Button
variant="ghost" variant="ghost"
icon="chevron-right" icon="chevron-right"
class="titlebar-icon w-6 h-6 p-0 box-border" class="titlebar-icon w-6 h-6 p-0 box-border"
disabled={!canForward()} disabled={!canForward()}
onClick={forward} onClick={forward}
aria-label={language.t("common.goForward")} aria-label={language.t("common.goForward")}
/> />
</Tooltip> </Tooltip>
</div> </div>
</Show> </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> </div>
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
</div> </div>
<div class="min-w-0 flex items-center justify-center pointer-events-none"> <div class="min-w-0 flex items-center justify-center pointer-events-none">
-1
View File
@@ -128,7 +128,6 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
if (started) return run if (started) return run
started = true started = true
run = (async () => { 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) { while (!abort.signal.aborted && started) {
attempt = new AbortController() attempt = new AbortController()
lastEventAt = Date.now() lastEventAt = Date.now()
+43 -57
View File
@@ -26,7 +26,6 @@ import type { ProjectMeta } from "./global-sync/types"
import { SESSION_RECENT_LIMIT } from "./global-sync/types" import { SESSION_RECENT_LIMIT } from "./global-sync/types"
import { sanitizeProject } from "./global-sync/utils" import { sanitizeProject } from "./global-sync/utils"
import { formatServerError } from "@/utils/server-errors" import { formatServerError } from "@/utils/server-errors"
import { queryOptions, skipToken, useQueryClient } from "@tanstack/solid-query"
type GlobalStore = { type GlobalStore = {
ready: boolean ready: boolean
@@ -42,9 +41,6 @@ type GlobalStore = {
reload: undefined | "pending" | "complete" reload: undefined | "pending" | "complete"
} }
export const loadSessionsQuery = (directory: string) =>
queryOptions<null>({ queryKey: [directory, "loadSessions"], queryFn: skipToken })
function createGlobalSync() { function createGlobalSync() {
const globalSDK = useGlobalSDK() const globalSDK = useGlobalSDK()
const language = useLanguage() const language = useLanguage()
@@ -71,7 +67,6 @@ function createGlobalSync() {
config: {}, config: {},
reload: undefined, reload: undefined,
}) })
const queryClient = useQueryClient()
let active = true let active = true
let projectWritten = false let projectWritten = false
@@ -203,53 +198,46 @@ function createGlobalSync() {
} }
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT) const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
const promise = queryClient const promise = loadRootSessionsWithFallback({
.fetchQuery({ directory,
...loadSessionsQuery(directory), limit,
queryFn: () => list: (query) => globalSDK.client.session.list(query),
loadRootSessionsWithFallback({ })
directory, .then((x) => {
limit, const nonArchived = (x.data ?? [])
list: (query) => globalSDK.client.session.list(query), .filter((s) => !!s?.id)
}) .filter((s) => !s.time?.archived)
.then((x) => { .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
const nonArchived = (x.data ?? []) const limit = store.limit
.filter((s) => !!s?.id) const childSessions = store.session.filter((s) => !!s.parentID)
.filter((s) => !s.time?.archived) const sessions = trimSessions([...nonArchived, ...childSessions], {
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) limit,
const limit = store.limit permission: store.permission,
const childSessions = store.session.filter((s) => !!s.parentID) })
const sessions = trimSessions([...nonArchived, ...childSessions], { setStore(
limit, "sessionTotal",
permission: store.permission, estimateRootSessionTotal({
}) count: nonArchived.length,
setStore( limit: x.limit,
"sessionTotal", limited: x.limited,
estimateRootSessionTotal({ }),
count: nonArchived.length, )
limit: x.limit, setStore("session", reconcile(sessions, { key: "id" }))
limited: x.limited, cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
}), sessionMeta.set(directory, { limit })
) })
setStore("session", reconcile(sessions, { key: "id" })) .catch((err) => {
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo) console.error("Failed to load sessions", err)
sessionMeta.set(directory, { limit }) const project = getFilename(directory)
}) showToast({
.catch((err) => { variant: "error",
console.error("Failed to load sessions", err) title: language.t("toast.session.listFailed.title", { project }),
const project = getFilename(directory) description: formatServerError(err, language.t),
showToast({ })
variant: "error",
title: language.t("toast.session.listFailed.title", { project }),
description: formatServerError(err, language.t),
})
})
.then(() => null),
}) })
.then(() => {})
sessionLoads.set(directory, promise) sessionLoads.set(directory, promise)
void promise.finally(() => { promise.finally(() => {
sessionLoads.delete(directory) sessionLoads.delete(directory)
children.unpin(directory) children.unpin(directory)
}) })
@@ -262,7 +250,7 @@ function createGlobalSync() {
if (pending) return pending if (pending) return pending
children.pin(directory) children.pin(directory)
const promise = Promise.resolve().then(async () => { const promise = (async () => {
const child = children.ensureChild(directory) const child = children.ensureChild(directory)
const cache = children.vcsCache.get(directory) const cache = children.vcsCache.get(directory)
if (!cache) return if (!cache) return
@@ -281,12 +269,11 @@ function createGlobalSync() {
vcsCache: cache, vcsCache: cache,
loadSessions, loadSessions,
translate: language.t, translate: language.t,
queryClient,
}) })
}) })()
booting.set(directory, promise) booting.set(directory, promise)
void promise.finally(() => { promise.finally(() => {
booting.delete(directory) booting.delete(directory)
children.unpin(directory) children.unpin(directory)
}) })
@@ -330,7 +317,7 @@ function createGlobalSync() {
setSessionTodo, setSessionTodo,
vcsCache: children.vcsCache.get(directory), vcsCache: children.vcsCache.get(directory),
loadLsp: () => { loadLsp: () => {
void sdkFor(directory) sdkFor(directory)
.lsp.status() .lsp.status()
.then((x) => { .then((x) => {
setStore("lsp", x.data ?? []) setStore("lsp", x.data ?? [])
@@ -359,7 +346,6 @@ function createGlobalSync() {
translate: language.t, translate: language.t,
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }), formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
setGlobalStore: setBootStore, setGlobalStore: setBootStore,
queryClient,
}) })
bootedAt = Date.now() bootedAt = Date.now()
} finally { } finally {
@@ -373,13 +359,13 @@ function createGlobalSync() {
eventFrame = undefined eventFrame = undefined
eventTimer = setTimeout(() => { eventTimer = setTimeout(() => {
eventTimer = undefined eventTimer = undefined
void globalSDK.event.start() globalSDK.event.start()
}, 0) }, 0)
}) })
} else { } else {
eventTimer = setTimeout(() => { eventTimer = setTimeout(() => {
eventTimer = undefined eventTimer = undefined
void globalSDK.event.start() globalSDK.event.start()
}, 0) }, 0)
} }
void bootstrap() void bootstrap()
+137 -148
View File
@@ -18,8 +18,6 @@ import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
import type { State, VcsCache } from "./types" import type { State, VcsCache } from "./types"
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils" import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
import { formatServerError } from "@/utils/server-errors" import { formatServerError } from "@/utils/server-errors"
import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query"
import { loadSessionsQuery } from "../global-sync"
type GlobalStore = { type GlobalStore = {
ready: boolean ready: boolean
@@ -67,13 +65,28 @@ function runAll(list: Array<() => Promise<unknown>>) {
return Promise.allSettled(list.map((item) => item())) 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: { export async function bootstrapGlobal(input: {
globalSDK: OpencodeClient globalSDK: OpencodeClient
requestFailedTitle: string requestFailedTitle: string
translate: (key: string, vars?: Record<string, string | number>) => string translate: (key: string, vars?: Record<string, string | number>) => string
formatMoreCount: (count: number) => string formatMoreCount: (count: number) => string
setGlobalStore: SetStoreFunction<GlobalStore> setGlobalStore: SetStoreFunction<GlobalStore>
queryClient: QueryClient
}) { }) {
const fast = [ const fast = [
() => () =>
@@ -83,16 +96,11 @@ export async function bootstrapGlobal(input: {
}), }),
), ),
() => () =>
input.queryClient.fetchQuery({ retry(() =>
...loadProvidersQuery(null), input.globalSDK.provider.list().then((x) => {
queryFn: () => input.setGlobalStore("provider", normalizeProviderList(x.data!))
retry(() => }),
input.globalSDK.provider.list().then((x) => { ),
input.setGlobalStore("provider", normalizeProviderList(x.data!))
return null
}),
),
}),
] ]
const slow = [ const slow = [
@@ -180,12 +188,6 @@ function warmSessions(input: {
).then(() => undefined) ).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: { export async function bootstrapDirectory(input: {
directory: string directory: string
sdk: OpencodeClient sdk: OpencodeClient
@@ -200,7 +202,6 @@ export async function bootstrapDirectory(input: {
project: Project[] project: Project[]
provider: ProviderListResponse provider: ProviderListResponse
} }
queryClient: QueryClient
}) { }) {
const loading = input.store.status !== "complete" const loading = input.store.status !== "complete"
const seededProject = projectID(input.directory, input.global.project) const seededProject = projectID(input.directory, input.global.project)
@@ -222,7 +223,97 @@ export async function bootstrapDirectory(input: {
input.setStore("lsp", []) input.setStore("lsp", [])
if (loading) input.setStore("status", "partial") if (loading) input.setStore("status", "partial")
const fast = [() => Promise.resolve(input.loadSessions(input.directory))] 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 errs = errors(await runAll(fast)) const errs = errors(await runAll(fast))
if (errs.length > 0) { if (errs.length > 0) {
@@ -235,138 +326,36 @@ export async function bootstrapDirectory(input: {
}) })
} }
;(async () => { await waitForPaint()
const slow = [ const slowErrs = errors(await runAll(slow))
() => if (slowErrs.length > 0) {
input.queryClient.ensureQueryData({ console.error("Failed to finish bootstrap instance", slowErrs[0])
...loadAgentsQuery(input.directory), const project = getFilename(input.directory)
queryFn: () => showToast({
retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))).then( variant: "error",
() => null, title: input.translate("toast.project.reloadFailed.title", { project }),
), description: formatServerError(slowErrs[0], input.translate),
}), })
() => 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)
}),
),
]
await waitForPaint() if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
const slowErrs = errors(await runAll(slow))
if (slowErrs.length > 0) { const rev = (providerRev.get(input.directory) ?? 0) + 1
console.error("Failed to finish bootstrap instance", slowErrs[0]) 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)
const project = getFilename(input.directory) const project = getFilename(input.directory)
showToast({ showToast({
variant: "error", variant: "error",
title: input.translate("toast.project.reloadFailed.title", { project }), title: input.translate("toast.project.reloadFailed.title", { project }),
description: formatServerError(slowErrs[0], input.translate), description: formatServerError(err, 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),
}) })
})()
} }
@@ -243,8 +243,8 @@ export function createChildStoreManager(input: {
const cached = metaCache.get(directory) const cached = metaCache.get(directory)
if (!cached) return if (!cached) return
const previous = store.projectMeta ?? {} const previous = store.projectMeta ?? {}
const icon = patch.icon ? { ...previous.icon, ...patch.icon } : previous.icon const icon = patch.icon ? { ...(previous.icon ?? {}), ...patch.icon } : previous.icon
const commands = patch.commands ? { ...previous.commands, ...patch.commands } : previous.commands const commands = patch.commands ? { ...(previous.commands ?? {}), ...patch.commands } : previous.commands
const next = { const next = {
...previous, ...previous,
...patch, ...patch,
@@ -63,7 +63,6 @@ export function createRefreshQueue(input: QueueInput) {
} }
} finally { } finally {
running = false running = false
// oxlint-disable-next-line no-unsafe-finally -- intentional: early return skips schedule() when paused
if (input.paused()) return if (input.paused()) return
if (root || queued.size) schedule() if (root || queued.size) schedule()
} }
@@ -8,6 +8,7 @@ import type {
Part, Part,
Path, Path,
PermissionRequest, PermissionRequest,
Project,
ProviderListResponse, ProviderListResponse,
QuestionRequest, QuestionRequest,
Session, Session,
+3 -3
View File
@@ -344,7 +344,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
return return
} }
setStore("sessionView", sessionKey, "scroll", (prev) => ({ ...prev, ...next })) setStore("sessionView", sessionKey, "scroll", (prev) => ({ ...(prev ?? {}), ...next }))
prune(keep) prune(keep)
}, },
}) })
@@ -399,7 +399,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
local?.icon?.color !== undefined local?.icon?.color !== undefined
const base = { const base = {
...metadata, ...(metadata ?? {}),
...project, ...project,
icon: { icon: {
url: metadata?.icon?.url, url: metadata?.icon?.url,
@@ -582,7 +582,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
open(directory: string) { open(directory: string) {
const root = rootFor(directory) const root = rootFor(directory)
if (server.projects.list().find((x) => x.worktree === root)) return if (server.projects.list().find((x) => x.worktree === root)) return
void globalSync.project.loadSessions(root) globalSync.project.loadSessions(root)
server.projects.open(root) server.projects.open(root)
}, },
close(directory: string) { close(directory: string) {
-30
View File
@@ -23,11 +23,6 @@ export interface Settings {
autoSave: boolean autoSave: boolean
releaseNotes: boolean releaseNotes: boolean
followup: "queue" | "steer" followup: "queue" | "steer"
showFileTree: boolean
showNavigation: boolean
showSearch: boolean
showStatus: boolean
showTerminal: boolean
showReasoningSummaries: boolean showReasoningSummaries: boolean
shellToolPartsExpanded: boolean shellToolPartsExpanded: boolean
editToolPartsExpanded: boolean editToolPartsExpanded: boolean
@@ -94,11 +89,6 @@ const defaultSettings: Settings = {
autoSave: true, autoSave: true,
releaseNotes: true, releaseNotes: true,
followup: "steer", followup: "steer",
showFileTree: false,
showNavigation: false,
showSearch: false,
showStatus: false,
showTerminal: false,
showReasoningSummaries: false, showReasoningSummaries: false,
shellToolPartsExpanded: false, shellToolPartsExpanded: false,
editToolPartsExpanded: false, editToolPartsExpanded: false,
@@ -172,26 +162,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setFollowup(value: "queue" | "steer") { setFollowup(value: "queue" | "steer") {
setStore("general", "followup", value === "queue" ? "steer" : value) 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( showReasoningSummaries: withFallback(
() => store.general?.showReasoningSummaries, () => store.general?.showReasoningSummaries,
defaultSettings.general.showReasoningSummaries, defaultSettings.general.showReasoningSummaries,
+2 -2
View File
@@ -117,7 +117,7 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat
entry?.value.clear() entry?.value.clear()
} }
void removePersisted(Persist.workspace(dir, "terminal"), platform) removePersisted(Persist.workspace(dir, "terminal"), platform)
const legacy = new Set(getLegacyTerminalStorageKeys(dir)) const legacy = new Set(getLegacyTerminalStorageKeys(dir))
for (const id of sessionIDs ?? []) { for (const id of sessionIDs ?? []) {
@@ -126,7 +126,7 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat
} }
} }
for (const key of legacy) { for (const key of legacy) {
void removePersisted({ key }, platform) removePersisted({ key }, platform)
} }
} }
+3 -2
View File
@@ -1,14 +1,15 @@
import "solid-js"
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_OPENCODE_SERVER_HOST: string readonly VITE_OPENCODE_SERVER_HOST: string
readonly VITE_OPENCODE_SERVER_PORT: string readonly VITE_OPENCODE_SERVER_PORT: string
readonly VITE_OPENCODE_CHANNEL?: "dev" | "beta" | "prod"
} }
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv readonly env: ImportMetaEnv
} }
export declare module "solid-js" { declare module "solid-js" {
namespace JSX { namespace JSX {
interface Directives { interface Directives {
sortable: true sortable: true
-11
View File
@@ -719,7 +719,6 @@ export const dict = {
"settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
"settings.general.section.appearance": "Appearance", "settings.general.section.appearance": "Appearance",
"settings.general.section.advanced": "Advanced",
"settings.general.section.notifications": "System notifications", "settings.general.section.notifications": "System notifications",
"settings.general.section.updates": "Updates", "settings.general.section.updates": "Updates",
"settings.general.section.sounds": "Sound effects", "settings.general.section.sounds": "Sound effects",
@@ -742,16 +741,6 @@ export const dict = {
"settings.general.row.followup.description": "Choose whether follow-up prompts steer immediately or wait in a queue", "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.queue": "Queue",
"settings.general.row.followup.option.steer": "Steer", "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.title": "Show reasoning summaries",
"settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline", "settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline",
"settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts", "settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts",
+4
View File
@@ -1,3 +1,7 @@
import { dict as en } from "./en"
type Keys = keyof typeof en
export const dict = { export const dict = {
"command.category.suggested": "추천", "command.category.suggested": "추천",
"command.category.view": "보기", "command.category.view": "보기",
+186 -192
View File
@@ -13,7 +13,7 @@ import {
type Accessor, type Accessor,
} from "solid-js" } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener" import { makeEventListener } from "@solid-primitives/event-listener"
import { useLocation, useNavigate, useParams } from "@solidjs/router" import { useNavigate, useParams } from "@solidjs/router"
import { useLayout, LocalProject } from "@/context/layout" import { useLayout, LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync" import { useGlobalSync } from "@/context/global-sync"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
@@ -127,17 +127,14 @@ export default function Layout(props: ParentProps) {
const theme = useTheme() const theme = useTheme()
const language = useLanguage() const language = useLanguage()
const initialDirectory = decode64(params.dir) const initialDirectory = decode64(params.dir)
const location = useLocation()
const route = createMemo(() => { const route = createMemo(() => {
const slug = params.dir const slug = params.dir
if (!slug) return { slug, dir: "" } if (!slug) return { slug, dir: "" }
const dir = decode64(slug) const dir = decode64(slug)
if (!dir) return { slug, dir: "" } if (!dir) return { slug, dir: "" }
const store = globalSync.peek(dir, { bootstrap: false })
return { return {
slug, slug,
store, dir: globalSync.peek(dir, { bootstrap: false })[0].path.directory || dir,
dir: store[0].path.directory || dir,
} }
}) })
const availableThemeEntries = createMemo(() => theme.ids().map((id) => [id, theme.themes()[id]] as const)) const availableThemeEntries = createMemo(() => theme.ids().map((id) => [id, theme.themes()[id]] as const))
@@ -707,7 +704,7 @@ export default function Layout(props: ParentProps) {
createEffect(() => { createEffect(() => {
const active = new Set(visibleSessionDirs()) const active = new Set(visibleSessionDirs())
for (const directory of prefetchedByDir.keys()) { for (const directory of [...prefetchedByDir.keys()]) {
if (active.has(directory)) continue if (active.has(directory)) continue
prefetchedByDir.delete(directory) prefetchedByDir.delete(directory)
} }
@@ -959,7 +956,7 @@ export default function Layout(props: ParentProps) {
// warm up child store to prevent flicker // warm up child store to prevent flicker
globalSync.child(target.worktree) globalSync.child(target.worktree)
void openProject(target.worktree) openProject(target.worktree)
} }
function navigateSessionByUnseen(offset: number) { function navigateSessionByUnseen(offset: number) {
@@ -1097,7 +1094,7 @@ export default function Layout(props: ParentProps) {
disabled: !params.dir || !params.id, disabled: !params.dir || !params.id,
onSelect: () => { onSelect: () => {
const session = currentSessions().find((s) => s.id === params.id) const session = currentSessions().find((s) => s.id === params.id)
if (session) void archiveSession(session) if (session) archiveSession(session)
}, },
}, },
{ {
@@ -1363,11 +1360,11 @@ export default function Layout(props: ParentProps) {
if (!server.isLocal()) return if (!server.isLocal()) return
for (const directory of collectOpenProjectDeepLinks(urls)) { for (const directory of collectOpenProjectDeepLinks(urls)) {
void openProject(directory) openProject(directory)
} }
for (const link of collectNewSessionDeepLinks(urls)) { for (const link of collectNewSessionDeepLinks(urls)) {
void openProject(link.directory, false) openProject(link.directory, false)
const slug = base64Encode(link.directory) const slug = base64Encode(link.directory)
if (link.prompt) { if (link.prompt) {
setSessionHandoff(slug, { prompt: link.prompt }) setSessionHandoff(slug, { prompt: link.prompt })
@@ -1456,11 +1453,11 @@ export default function Layout(props: ParentProps) {
function resolve(result: string | string[] | null) { function resolve(result: string | string[] | null) {
if (Array.isArray(result)) { if (Array.isArray(result)) {
for (const directory of result) { for (const directory of result) {
void openProject(directory, false) openProject(directory, false)
} }
void navigateToProject(result[0]) navigateToProject(result[0])
} else if (result) { } else if (result) {
void openProject(result) openProject(result)
} }
} }
@@ -1828,7 +1825,7 @@ export default function Layout(props: ParentProps) {
const next = new Set(dirs) const next = new Set(dirs)
for (const directory of next) { for (const directory of next) {
if (loadedSessionDirs.has(directory)) continue if (loadedSessionDirs.has(directory)) continue
void globalSync.project.loadSessions(directory) globalSync.project.loadSessions(directory)
} }
loadedSessionDirs.clear() loadedSessionDirs.clear()
@@ -2103,198 +2100,196 @@ export default function Layout(props: ParentProps) {
</Show> </Show>
} }
> >
{(project) => ( <>
<> <div class="shrink-0 pl-1 py-1">
<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="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0"> <div class="flex flex-col min-w-0">
<div class="flex flex-col min-w-0"> <InlineEditor
<InlineEditor id={`project:${projectId()}`}
id={`project:${projectId()}`} value={projectName}
value={projectName} onSave={(next) => {
onSave={(next) => { const item = project()
const item = project() if (!item) return
if (!item) return renameProject(item, next)
void renameProject(item, next) }}
}} class="text-14-medium text-text-strong truncate"
class="text-14-medium text-text-strong truncate" displayClass="text-14-medium text-text-strong truncate"
displayClass="text-14-medium text-text-strong truncate" stopPropagation
stopPropagation />
/>
<Tooltip <Tooltip
placement="bottom" placement="bottom"
gutter={2} gutter={2}
value={worktree()} value={worktree()}
class="shrink-0" class="shrink-0"
contentStyle={{ contentStyle={{
"max-width": "640px", "max-width": "640px",
transform: "translate3d(52px, 0, 0)", transform: "translate3d(52px, 0, 0)",
}} }}
> >
<span class="text-12-regular text-text-base truncate select-text"> <span class="text-12-regular text-text-base truncate select-text">
{worktree().replace(homedir(), "~")} {worktree().replace(homedir(), "~")}
</span> </span>
</Tooltip> </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> </div>
</div>
<div class="flex-1 min-h-0 flex flex-col"> <DropdownMenu modal={!sidebarHovering()}>
<Show <DropdownMenu.Trigger
when={workspacesEnabled()} as={IconButton}
fallback={ icon="dot-grid"
<> variant="ghost"
<div class="shrink-0 py-4"> data-action="project-menu"
<Button data-project={slug()}
size="large" class="shrink-0 size-6 rounded-md transition-opacity data-[expanded]:bg-surface-base-active"
icon="new-session" classList={{
class="w-full" "opacity-100": panelProps.mobile || merged(),
onClick={() => { "opacity-0 group-hover/project:opacity-100 group-focus-within/project:opacity-100 data-[expanded]:opacity-100":
const dir = worktree() !panelProps.mobile && !merged(),
if (!dir) return }}
navigateWithSidebarReset(`/${base64Encode(dir)}/session`) aria-label={language.t("common.moreOptions")}
}} />
> <DropdownMenu.Portal>
{language.t("command.session.new")} <DropdownMenu.Content class="mt-1">
</Button> <DropdownMenu.Item
</div> onSelect={() => {
<div class="flex-1 min-h-0"> const item = project()
<LocalWorkspace if (!item) return
ctx={workspaceSidebarCtx} showEditProjectDialog(item)
project={project()} }}
sortNow={sortNow} >
mobile={panelProps.mobile} <DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
/> </DropdownMenu.Item>
</div> <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="shrink-0 py-4"> <div class="shrink-0 py-4">
<Button <Button
size="large" size="large"
icon="plus-small" icon="new-session"
class="w-full" class="w-full"
onClick={() => { onClick={() => {
const item = project() const dir = worktree()
if (!item) return if (!dir) return
void createWorkspace(item) navigateWithSidebarReset(`/${base64Encode(dir)}/session`)
}} }}
> >
{language.t("workspace.new")} {language.t("command.session.new")}
</Button> </Button>
</div> </div>
<div class="relative flex-1 min-h-0"> <div class="flex-1 min-h-0">
<DragDropProvider <LocalWorkspace
onDragStart={handleWorkspaceDragStart} ctx={workspaceSidebarCtx}
onDragEnd={handleWorkspaceDragEnd} project={project()!}
onDragOver={handleWorkspaceDragOver} sortNow={sortNow}
collisionDetector={closestCenter} mobile={panelProps.mobile}
> />
<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>
</> </>
</Show> }
</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> </Show>
<div <div
@@ -2360,7 +2355,6 @@ export default function Layout(props: ParentProps) {
return ( 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"> <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 /> <Titlebar />
<div class="flex-1 min-h-0 min-w-0 flex"> <div class="flex-1 min-h-0 min-w-0 flex">
<div class="flex-1 min-h-0 relative"> <div class="flex-1 min-h-0 relative">
@@ -14,11 +14,10 @@ import { Spinner } from "@opencode-ai/ui/spinner"
import { Tooltip } from "@opencode-ai/ui/tooltip" import { Tooltip } from "@opencode-ai/ui/tooltip"
import { type Session } from "@opencode-ai/sdk/v2/client" import { type Session } from "@opencode-ai/sdk/v2/client"
import { type LocalProject } from "@/context/layout" import { type LocalProject } from "@/context/layout"
import { loadSessionsQuery, useGlobalSync } from "@/context/global-sync" import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items" import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
import { sortedRootSessions, workspaceKey } from "./helpers" import { sortedRootSessions, workspaceKey } from "./helpers"
import { useQuery } from "@tanstack/solid-query"
type InlineEditorComponent = (props: { type InlineEditorComponent = (props: {
id: string id: string
@@ -278,7 +277,7 @@ const WorkspaceSessionList = (props: {
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-2 pr-10" class="flex w-full text-left justify-start text-14-regular text-text-weak pl-2 pr-10"
size="large" size="large"
onClick={(e: MouseEvent) => { onClick={(e: MouseEvent) => {
void props.loadMore() props.loadMore()
;(e.currentTarget as HTMLButtonElement).blur() ;(e.currentTarget as HTMLButtonElement).blur()
}} }}
> >
@@ -317,11 +316,12 @@ export const SortableWorkspace = (props: {
}) })
const open = createMemo(() => props.ctx.workspaceExpanded(props.directory, local())) const open = createMemo(() => props.ctx.workspaceExpanded(props.directory, local()))
const boot = createMemo(() => open() || active()) const boot = createMemo(() => open() || active())
const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false)
const count = createMemo(() => sessions()?.length ?? 0) const count = createMemo(() => sessions()?.length ?? 0)
const hasMore = createMemo(() => workspaceStore.sessionTotal > count()) const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) }))
const busy = createMemo(() => props.ctx.isBusy(props.directory)) const busy = createMemo(() => props.ctx.isBusy(props.directory))
const loading = () => query.isLoading const wasBusy = createMemo((prev) => prev || busy(), false)
const loading = createMemo(() => open() && !booted() && count() === 0 && !wasBusy())
const touch = createMediaQuery("(hover: none)") const touch = createMediaQuery("(hover: none)")
const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id))) const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id)))
const loadMore = async () => { const loadMore = async () => {
@@ -426,7 +426,7 @@ export const SortableWorkspace = (props: {
mobile={props.mobile} mobile={props.mobile}
ctx={props.ctx} ctx={props.ctx}
showNew={showNew} showNew={showNew}
loading={() => query.isLoading && count() === 0} loading={loading}
sessions={sessions} sessions={sessions}
hasMore={hasMore} hasMore={hasMore}
loadMore={loadMore} loadMore={loadMore}
@@ -452,10 +452,10 @@ export const LocalWorkspace = (props: {
}) })
const slug = createMemo(() => base64Encode(props.project.worktree)) const slug = createMemo(() => base64Encode(props.project.worktree))
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow())) 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 count = createMemo(() => sessions()?.length ?? 0)
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) })) const loading = createMemo(() => !booted() && count() === 0)
const hasMore = createMemo(() => workspace().store.sessionTotal > count()) const hasMore = createMemo(() => workspace().store.sessionTotal > count())
const loading = () => query.isLoading && count() === 0
const loadMore = async () => { const loadMore = async () => {
workspace().setStore("limit", (limit) => (limit ?? 0) + 5) workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
await globalSync.project.loadSessions(props.project.worktree) await globalSync.project.loadSessions(props.project.worktree)
+145 -55
View File
@@ -1,6 +1,6 @@
import type { Project, UserMessage } from "@opencode-ai/sdk/v2" import type { Project, UserMessage, VcsFileDiff } from "@opencode-ai/sdk/v2"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDialog } from "@opencode-ai/ui/context/dialog"
import { createQuery, skipToken, useMutation, useQueryClient } from "@tanstack/solid-query" import { useMutation } from "@tanstack/solid-query"
import { import {
batch, batch,
onCleanup, onCleanup,
@@ -13,7 +13,6 @@ import {
on, on,
onMount, onMount,
untrack, untrack,
createResource,
} from "solid-js" } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener" import { makeEventListener } from "@solid-primitives/event-listener"
import { createMediaQuery } from "@solid-primitives/media" import { createMediaQuery } from "@solid-primitives/media"
@@ -324,7 +323,6 @@ export default function Page() {
const local = useLocal() const local = useLocal()
const file = useFile() const file = useFile()
const sync = useSync() const sync = useSync()
const queryClient = useQueryClient()
const dialog = useDialog() const dialog = useDialog()
const language = useLanguage() const language = useLanguage()
const sdk = useSDK() const sdk = useSDK()
@@ -434,6 +432,8 @@ export default function Page() {
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const isChildSession = createMemo(() => !!info()?.parentID) const isChildSession = createMemo(() => !!info()?.parentID)
const diffs = createMemo(() => (params.id ? list(sync.data.session_diff[params.id]) : [])) 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 canReview = createMemo(() => !!sync.project)
const reviewTab = createMemo(() => isDesktop()) const reviewTab = createMemo(() => isDesktop())
const tabState = createSessionTabs({ const tabState = createSessionTabs({
@@ -443,6 +443,8 @@ export default function Page() {
review: reviewTab, review: reviewTab,
hasReview: canReview, hasReview: canReview,
}) })
const contextOpen = tabState.contextOpen
const openedTabs = tabState.openedTabs
const activeTab = tabState.activeTab const activeTab = tabState.activeTab
const activeFileTab = tabState.activeFileTab const activeFileTab = tabState.activeFileTab
const revertMessageID = createMemo(() => info()?.revert?.messageID) const revertMessageID = createMemo(() => info()?.revert?.messageID)
@@ -485,7 +487,7 @@ export default function Page() {
if (!tab) return if (!tab) return
const path = file.pathFromTab(tab) const path = file.pathFromTab(tab)
if (path) void file.load(path) if (path) file.load(path)
}) })
createEffect( createEffect(
@@ -519,6 +521,26 @@ export default function Page() {
deferRender: false, 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( const [followup, setFollowup] = persisted(
Persist.workspace(sdk.directory, "followup", ["followup.v1"]), Persist.workspace(sdk.directory, "followup", ["followup.v1"]),
createStore<{ createStore<{
@@ -552,6 +574,68 @@ export default function Page() {
let todoTimer: number | undefined let todoTimer: number | undefined
let diffFrame: number | undefined let diffFrame: number | undefined
let diffTimer: 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) => { createComputed((prev) => {
const open = desktopReviewOpen() const open = desktopReviewOpen()
@@ -582,52 +666,21 @@ export default function Page() {
list.push("turn") list.push("turn")
return list 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>(() => { const vcsMode = createMemo<VcsMode | undefined>(() => {
if (store.changes === "git" || store.changes === "branch") return store.changes if (store.changes === "git" || store.changes === "branch") return store.changes
}) })
const vcsKey = createMemo( const reviewDiffs = createMemo(() => {
() => ["session-vcs", sdk.directory, sync.data.vcs?.branch ?? "", sync.data.vcs?.default_branch ?? ""] as const, if (store.changes === "git") return list(vcs.diff.git)
) if (store.changes === "branch") return list(vcs.diff.branch)
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() return turnDiffs()
} })
const reviewCount = () => reviewDiffs().length const reviewCount = createMemo(() => reviewDiffs().length)
const hasReview = () => reviewCount() > 0 const hasReview = createMemo(() => reviewCount() > 0)
const reviewReady = () => { const reviewReady = createMemo(() => {
if (store.changes === "git" || store.changes === "branch") return !vcsQuery.isPending if (store.changes === "git") return vcs.ready.git
if (store.changes === "branch") return vcs.ready.branch
return true return true
} })
const newSessionWorktree = createMemo(() => { const newSessionWorktree = createMemo(() => {
if (store.newSessionWorktree === "create") return "create" if (store.newSessionWorktree === "create") return "create"
@@ -755,9 +808,8 @@ export default function Page() {
const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs
const [sessionSync] = createResource( createEffect(
() => [sdk.directory, params.id] as const, on([() => sdk.directory, () => params.id] as const, ([, id]) => {
([directory, id]) => {
if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame) if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame)
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer) if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
refreshFrame = undefined refreshFrame = undefined
@@ -768,10 +820,13 @@ export default function Page() {
const stale = !cached const stale = !cached
? false ? false
: (() => { : (() => {
const info = getSessionPrefetch(directory, id) const info = getSessionPrefetch(sdk.directory, id)
if (!info) return true if (!info) return true
return Date.now() - info.at > SESSION_PREFETCH_TTL return Date.now() - info.at > SESSION_PREFETCH_TTL
})() })()
untrack(() => {
void sync.session.sync(id)
})
refreshFrame = requestAnimationFrame(() => { refreshFrame = requestAnimationFrame(() => {
refreshFrame = undefined refreshFrame = undefined
@@ -783,9 +838,7 @@ export default function Page() {
}) })
}, 0) }, 0)
}) })
}),
return sync.session.sync(id)
},
) )
createEffect( createEffect(
@@ -847,6 +900,27 @@ 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) => { const stopVcs = sdk.event.listen((evt) => {
if (evt.details.type !== "file.watcher.updated") return if (evt.details.type !== "file.watcher.updated") return
const props = const props =
@@ -980,6 +1054,13 @@ export default function Page() {
} }
} }
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
const wantsReview = createMemo(() =>
isDesktop()
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
: store.mobileTab === "changes",
)
createEffect(() => { createEffect(() => {
const list = changesOptions() const list = changesOptions()
if (list.includes(store.changes)) return if (list.includes(store.changes)) return
@@ -988,12 +1069,22 @@ export default function Page() {
setStore("changes", next) setStore("changes", next)
}) })
createEffect(() => {
const mode = vcsMode()
if (!mode) return
if (!wantsReview()) return
void loadVcs(mode)
})
createEffect( createEffect(
on( on(
() => sync.data.session_status[params.id ?? ""]?.type, () => sync.data.session_status[params.id ?? ""]?.type,
(next, prev) => { (next, prev) => {
const mode = vcsMode()
if (!mode) return
if (!wantsReview()) return
if (next !== "idle" || prev === undefined || prev === "idle") return if (next !== "idle" || prev === undefined || prev === "idle") return
refreshVcs() void loadVcs(mode, true)
}, },
{ defer: true }, { defer: true },
), ),
@@ -1794,7 +1885,6 @@ export default function Page() {
return ( return (
<div class="relative bg-background-base size-full overflow-hidden flex flex-col"> <div class="relative bg-background-base size-full overflow-hidden flex flex-col">
{sessionSync() ?? ""}
<SessionHeader /> <SessionHeader />
<div class="flex-1 min-h-0 flex flex-col md:flex-row"> <div class="flex-1 min-h-0 flex flex-col md:flex-row">
<Show when={!isDesktop() && !!params.id}> <Show when={!isDesktop() && !!params.id}>
@@ -378,6 +378,12 @@ export function FileTabContent(props: { tab: string }) {
requestAnimationFrame(() => comments.clearFocus()) requestAnimationFrame(() => comments.clearFocus())
}) })
const cancelCommenting = () => {
const p = path()
if (p) file.setSelectedLines(p, null)
setNote("commenting", null)
}
let prev = { let prev = {
loaded: false, loaded: false,
ready: false, ready: false,
+1 -1
View File
@@ -117,7 +117,7 @@ export const createOpenReviewFile = (input: {
input.openTab(tab) input.openTab(tab)
input.setActive(tab) input.setActive(tab)
} }
if (maybePromise instanceof Promise) void maybePromise.then(open) if (maybePromise instanceof Promise) maybePromise.then(open)
else open() else open()
}) })
} }
@@ -1,4 +1,4 @@
import { createEffect, onCleanup, type JSX } from "solid-js" import { createEffect, createSignal, onCleanup, type JSX } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener" import { makeEventListener } from "@solid-primitives/event-listener"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import { SessionReview } from "@opencode-ai/ui/session-review" import { SessionReview } from "@opencode-ai/ui/session-review"
@@ -19,9 +19,6 @@ import { useCommand } from "@/context/command"
import { useFile, type SelectedLineRange } from "@/context/file" import { useFile, type SelectedLineRange } from "@/context/file"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout" 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 { createFileTabListSync } from "@/pages/session/file-tab-scroll"
import { FileTabContent } from "@/pages/session/file-tabs" import { FileTabContent } from "@/pages/session/file-tabs"
import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers" import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
@@ -42,9 +39,6 @@ export function SessionSidePanel(props: {
size: Sizing size: Sizing
}) { }) {
const layout = useLayout() const layout = useLayout()
const platform = usePlatform()
const settings = useSettings()
const sync = useSync()
const file = useFile() const file = useFile()
const language = useLanguage() const language = useLanguage()
const command = useCommand() const command = useCommand()
@@ -52,15 +46,9 @@ export function SessionSidePanel(props: {
const { sessionKey, tabs, view } = useSessionLayout() const { sessionKey, tabs, view } = useSessionLayout()
const isDesktop = createMediaQuery("(min-width: 768px)") 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 reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
const fileOpen = createMemo(() => isDesktop() && shown() && layout.fileTree.opened()) const fileOpen = createMemo(() => isDesktop() && layout.fileTree.opened())
const open = createMemo(() => reviewOpen() || fileOpen()) const open = createMemo(() => reviewOpen() || fileOpen())
const reviewTab = createMemo(() => isDesktop()) const reviewTab = createMemo(() => isDesktop())
const panelWidth = createMemo(() => { const panelWidth = createMemo(() => {
@@ -353,99 +341,98 @@ export function SessionSidePanel(props: {
</div> </div>
</div> </div>
<Show when={shown()}> <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() }}
>
<div <div
id="file-tree-panel" class="h-full flex flex-col overflow-hidden group/filetree"
aria-hidden={!fileOpen()} classList={{ "border-l border-border-weaker-base": reviewOpen() }}
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() }}
> >
<div <Tabs
class="h-full flex flex-col overflow-hidden group/filetree" variant="pill"
classList={{ "border-l border-border-weaker-base": reviewOpen() }} value={fileTreeTab()}
onChange={setFileTreeTabValue}
class="h-full"
data-scope="filetree"
> >
<Tabs <Tabs.List>
variant="pill" <Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
value={fileTreeTab()} {props.reviewCount()}{" "}
onChange={setFileTreeTabValue} {language.t(
class="h-full" props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
data-scope="filetree" )}
> </Tabs.Trigger>
<Tabs.List> <Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}> {language.t("session.files.all")}
{props.reviewCount()}{" "} </Tabs.Trigger>
{language.t( </Tabs.List>
props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other", <Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
)} <Switch>
</Tabs.Trigger> <Match when={props.hasReview() || !props.diffsReady()}>
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}> <Show
{language.t("session.files.all")} when={props.diffsReady()}
</Tabs.Trigger> fallback={
</Tabs.List> <div class="px-2 py-2 text-12-regular text-text-weak">
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0"> {language.t("common.loading")}
<Switch> {language.t("common.loading.ellipsis")}
<Match when={props.hasReview() || !props.diffsReady()}> </div>
<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 <FileTree
path="" path=""
class="pt-3" class="pt-3"
modified={diffFiles()} allowed={diffFiles()}
kinds={kinds()} kinds={kinds()}
onFileClick={(node) => openTab(file.tab(node.path))} draggable={false}
active={props.activeDiff}
onFileClick={(node) => props.focusReviewDiff(node.path)}
/> />
</Match> </Show>
</Switch> </Match>
</Tabs.Content> <Match when={true}>{empty(props.empty())}</Match>
</Tabs> </Switch>
</div> </Tabs.Content>
<Show when={fileOpen()}> <Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
<div onPointerDown={() => props.size.start()}> <Switch>
<ResizeHandle <Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match>
direction="horizontal" <Match when={true}>
edge="start" <FileTree
size={layout.fileTree.width()} path=""
min={200} class="pt-3"
max={480} modified={diffFiles()}
onResize={(width) => { kinds={kinds()}
props.size.touch() onFileClick={(node) => openTab(file.tab(node.path))}
layout.fileTree.resize(width) />
}} </Match>
/> </Switch>
</div> </Tabs.Content>
</Show> </Tabs>
</div> </div>
</Show> <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>
</div> </div>
</aside> </aside>
</Show> </Show>
@@ -7,10 +7,8 @@ import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
import { useLocal } from "@/context/local" import { useLocal } from "@/context/local"
import { usePermission } from "@/context/permission" import { usePermission } from "@/context/permission"
import { usePlatform } from "@/context/platform"
import { usePrompt } from "@/context/prompt" import { usePrompt } from "@/context/prompt"
import { useSDK } from "@/context/sdk" import { useSDK } from "@/context/sdk"
import { useSettings } from "@/context/settings"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { useTerminal } from "@/context/terminal" import { useTerminal } from "@/context/terminal"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
@@ -41,10 +39,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const language = useLanguage() const language = useLanguage()
const local = useLocal() const local = useLocal()
const permission = usePermission() const permission = usePermission()
const platform = usePlatform()
const prompt = usePrompt() const prompt = usePrompt()
const sdk = useSDK() const sdk = useSDK()
const settings = useSettings()
const sync = useSync() const sync = useSync()
const terminal = useTerminal() const terminal = useTerminal()
const layout = useLayout() const layout = useLayout()
@@ -70,10 +66,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
}) })
const activeFileTab = tabState.activeFileTab const activeFileTab = tabState.activeFileTab
const closableTab = tabState.closableTab 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 idle = { type: "idle" as const }
const status = () => sync.data.session_status[params.id ?? ""] ?? idle const status = () => sync.data.session_status[params.id ?? ""] ?? idle
@@ -465,16 +457,12 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
keybind: "mod+shift+r", keybind: "mod+shift+r",
onSelect: () => view().reviewPanel.toggle(), onSelect: () => view().reviewPanel.toggle(),
}), }),
...(shown() viewCommand({
? [ id: "fileTree.toggle",
viewCommand({ title: language.t("command.fileTree.toggle"),
id: "fileTree.toggle", keybind: "mod+\\",
title: language.t("command.fileTree.toggle"), onSelect: () => layout.fileTree.toggle(),
keybind: "mod+\\", }),
onSelect: () => layout.fileTree.toggle(),
}),
]
: []),
viewCommand({ viewCommand({
id: "input.focus", id: "input.focus",
title: language.t("command.input.focus"), title: language.t("command.input.focus"),
+1 -1
View File
@@ -469,7 +469,7 @@ export function persisted<T>(
state, state,
setState, setState,
init, init,
Object.assign(() => (ready.loading ? false : ready.latest === true), { Object.assign(() => ready() === true, {
promise: init instanceof Promise ? init : undefined, promise: init instanceof Promise ? init : undefined,
}), }),
] ]
@@ -46,9 +46,7 @@ describe("runtime adapters", () => {
}) })
test("resolves speech recognition constructor with webkit precedence", () => { test("resolves speech recognition constructor with webkit precedence", () => {
// oxlint-disable-next-line no-extraneous-class
class SpeechCtor {} class SpeechCtor {}
// oxlint-disable-next-line no-extraneous-class
class WebkitCtor {} class WebkitCtor {}
const ctor = getSpeechRecognitionCtor({ const ctor = getSpeechRecognitionCtor({
SpeechRecognition: SpeechCtor, SpeechRecognition: SpeechCtor,
+1 -4
View File
@@ -16,10 +16,7 @@ export function createSdkForServer({
return createOpencodeClient({ return createOpencodeClient({
...config, ...config,
headers: { headers: { ...config.headers, ...auth },
...(config.headers instanceof Headers ? Object.fromEntries(config.headers.entries()) : config.headers),
...auth,
},
baseUrl: server.url, baseUrl: server.url,
}) })
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.4.11", "version": "1.4.6",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
@@ -8,6 +8,7 @@ import { LOCALES, route } from "../src/lib/language.js"
const __dirname = dirname(fileURLToPath(import.meta.url)) const __dirname = dirname(fileURLToPath(import.meta.url))
const BASE_URL = config.baseUrl const BASE_URL = config.baseUrl
const PUBLIC_DIR = join(__dirname, "../public") const PUBLIC_DIR = join(__dirname, "../public")
const ROUTES_DIR = join(__dirname, "../src/routes")
const DOCS_DIR = join(__dirname, "../../../web/src/content/docs") const DOCS_DIR = join(__dirname, "../../../web/src/content/docs")
interface SitemapEntry { interface SitemapEntry {
@@ -105,4 +106,4 @@ async function main() {
console.log(`✓ Sitemap generated at ${outputPath}`) console.log(`✓ Sitemap generated at ${outputPath}`)
} }
void main() main()
@@ -1,4 +1,5 @@
import { action, useSubmission } from "@solidjs/router" import { action, useSubmission } from "@solidjs/router"
import dock from "../asset/lander/dock.png"
import { Resource } from "@opencode-ai/console-resource" import { Resource } from "@opencode-ai/console-resource"
import { Show } from "solid-js" import { Show } from "solid-js"
import { useI18n } from "~/context/i18n" import { useI18n } from "~/context/i18n"
@@ -47,7 +47,7 @@ export function Header(props: { zen?: boolean; go?: boolean; hideGetStarted?: bo
notation: "compact", notation: "compact",
compactDisplay: "short", compactDisplay: "short",
maximumFractionDigits: 0, maximumFractionDigits: 0,
}).format(githubData()?.stars) }).format(githubData()?.stars!)
: config.github.starsFormatted.compact, : config.github.starsFormatted.compact,
) )
+2 -2
View File
@@ -1,6 +1,6 @@
import { JSX } from "solid-js" import { JSX } from "solid-js"
export function IconZen(_props: JSX.SvgSVGAttributes<SVGSVGElement>) { export function IconZen(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return ( return (
<svg width="84" height="30" viewBox="0 0 84 30" fill="none" xmlns="http://www.w3.org/2000/svg"> <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" /> <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 ( return (
<svg width="54" height="30" viewBox="0 0 54 30" fill="none" xmlns="http://www.w3.org/2000/svg"> <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" /> <path d="M24 30H0V0H24V6H6V24H18V18H12V12H24V30Z" fill="currentColor" />
@@ -766,7 +766,7 @@ export default function Spotlight(props: SpotlightProps) {
} }
} }
void initializeWebGPU() initializeWebGPU()
onCleanup(() => { onCleanup(() => {
if (cleanupFunctionRef) { if (cleanupFunctionRef) {
@@ -1 +0,0 @@
export {}
@@ -1,7 +1,7 @@
import { APIEvent } from "@solidjs/start" import { APIEvent } from "@solidjs/start"
import { useAuthSession } from "~/context/auth" import { useAuthSession } from "~/context/auth"
export async function GET(_input: APIEvent) { export async function GET(input: APIEvent) {
const session = await useAuthSession() const session = await useAuthSession()
return Response.json(session.data) return Response.json(session.data)
} }
@@ -1,7 +1,7 @@
import { Title } from "@solidjs/meta" import { Title } from "@solidjs/meta"
import { createAsync, query, useParams } from "@solidjs/router" import { createAsync, query, useParams } from "@solidjs/router"
import { createSignal, For, Show } from "solid-js" import { createSignal, For, Show } from "solid-js"
import { Database, eq } from "@opencode-ai/console-core/drizzle/index.js" import { Database, desc, eq } from "@opencode-ai/console-core/drizzle/index.js"
import { BenchmarkTable } from "@opencode-ai/console-core/schema/benchmark.sql.js" import { BenchmarkTable } from "@opencode-ai/console-core/schema/benchmark.sql.js"
import { useI18n } from "~/context/i18n" import { useI18n } from "~/context/i18n"
@@ -298,7 +298,7 @@ export default function BlackSubscribe() {
// Resolve stripe promise once // Resolve stripe promise once
createEffect(() => { createEffect(() => {
void stripePromise.then((s) => { stripePromise.then((s) => {
if (s) setStripe(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 { Database } from "@opencode-ai/console-core/drizzle/index.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.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({ return json({
data: await Database.use(async (tx) => { data: await Database.use(async (tx) => {
const result = await tx.$count(UserTable) const result = await tx.$count(UserTable)
@@ -1,7 +1,7 @@
import type { APIEvent } from "@solidjs/start" import type { APIEvent } from "@solidjs/start"
import type { DownloadPlatform } from "../types" import type { DownloadPlatform } from "../types"
const prodAssetNames: Record<string, string> = { const assetNames: Record<string, string> = {
"darwin-aarch64-dmg": "opencode-desktop-darwin-aarch64.dmg", "darwin-aarch64-dmg": "opencode-desktop-darwin-aarch64.dmg",
"darwin-x64-dmg": "opencode-desktop-darwin-x64.dmg", "darwin-x64-dmg": "opencode-desktop-darwin-x64.dmg",
"windows-x64-nsis": "opencode-desktop-windows-x64.exe", "windows-x64-nsis": "opencode-desktop-windows-x64.exe",
@@ -10,15 +10,6 @@ const prodAssetNames: Record<string, string> = {
"linux-x64-rpm": "opencode-desktop-linux-x86_64.rpm", "linux-x64-rpm": "opencode-desktop-linux-x86_64.rpm",
} satisfies Record<DownloadPlatform, string> } 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 // 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> = { const downloadNames: Record<string, string> = {
"darwin-aarch64-dmg": "OpenCode Desktop.dmg", "darwin-aarch64-dmg": "OpenCode Desktop.dmg",
@@ -27,7 +18,7 @@ const downloadNames: Record<string, string> = {
} satisfies { [K in DownloadPlatform]?: string } } satisfies { [K in DownloadPlatform]?: string }
export async function GET({ params: { platform, channel } }: APIEvent) { export async function GET({ params: { platform, channel } }: APIEvent) {
const assetName = channel === "stable" ? prodAssetNames[platform] : betaAssetNames[platform] const assetName = assetNames[platform]
if (!assetName) return new Response(null, { status: 404 }) if (!assetName) return new Response(null, { status: 404 })
const resp = await fetch( const resp = await fetch(
@@ -46,5 +37,5 @@ export async function GET({ params: { platform, channel } }: APIEvent) {
const headers = new Headers(resp.headers) const headers = new Headers(resp.headers)
if (downloadName) headers.set("content-disposition", `attachment; filename="${downloadName}"`) if (downloadName) headers.set("content-disposition", `attachment; filename="${downloadName}"`)
return new Response(resp.body, { status: resp.status, statusText: resp.statusText, headers }) return new Response(resp.body, { ...resp, headers })
} }
@@ -77,7 +77,7 @@ export default function Download() {
const handleCopyClick = (command: string) => (event: Event) => { const handleCopyClick = (command: string) => (event: Event) => {
const button = event.currentTarget as HTMLButtonElement const button = event.currentTarget as HTMLButtonElement
void navigator.clipboard.writeText(command) navigator.clipboard.writeText(command)
button.setAttribute("data-copied", "") button.setAttribute("data-copied", "")
setTimeout(() => { setTimeout(() => {
button.removeAttribute("data-copied") button.removeAttribute("data-copied")
+1 -1
View File
@@ -1,5 +1,5 @@
import "./index.css" import "./index.css"
import { createAsync, query } from "@solidjs/router" import { createAsync, query, redirect } from "@solidjs/router"
import { Title, Meta } from "@solidjs/meta" import { Title, Meta } from "@solidjs/meta"
import { For, createMemo, createSignal, onCleanup, onMount } from "solid-js" import { For, createMemo, createSignal, onCleanup, onMount } from "solid-js"
//import { HttpHeader } from "@solidjs/start" //import { HttpHeader } from "@solidjs/start"
+5 -2
View File
@@ -12,6 +12,7 @@ import { Header } from "~/component/header"
import { Footer } from "~/component/footer" import { Footer } from "~/component/footer"
import { Legal } from "~/component/legal" import { Legal } from "~/component/legal"
import { github } from "~/lib/github" import { github } from "~/lib/github"
import { createMemo } from "solid-js"
import { config } from "~/config" import { config } from "~/config"
import { useI18n } from "~/context/i18n" import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language" import { useLanguage } from "~/context/language"
@@ -29,12 +30,14 @@ function CopyStatus() {
export default function Home() { export default function Home() {
const i18n = useI18n() const i18n = useI18n()
const language = useLanguage() const language = useLanguage()
const _githubData = createAsync(() => github()) const githubData = createAsync(() => github())
const release = createMemo(() => githubData()?.release)
const handleCopyClick = (event: Event) => { const handleCopyClick = (event: Event) => {
const button = event.currentTarget as HTMLButtonElement const button = event.currentTarget as HTMLButtonElement
const text = button.textContent const text = button.textContent
if (text) { if (text) {
void navigator.clipboard.writeText(text) navigator.clipboard.writeText(text)
button.setAttribute("data-copied", "") button.setAttribute("data-copied", "")
setTimeout(() => { setTimeout(() => {
button.removeAttribute("data-copied") button.removeAttribute("data-copied")
+1 -1
View File
@@ -27,7 +27,7 @@ export default function Home() {
const callback = () => { const callback = () => {
const text = button.textContent const text = button.textContent
if (text) { if (text) {
void navigator.clipboard.writeText(text) navigator.clipboard.writeText(text)
button.setAttribute("data-copied", "") button.setAttribute("data-copied", "")
setTimeout(() => { setTimeout(() => {
button.removeAttribute("data-copied") button.removeAttribute("data-copied")
@@ -6,7 +6,7 @@ import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language" import { useLanguage } from "~/context/language"
import "./user-menu.css" import "./user-menu.css"
const _logout = action(async () => { const logout = action(async () => {
"use server" "use server"
const auth = await useAuthSession() const auth = await useAuthSession()
const event = getRequestEvent() const event = getRequestEvent()
@@ -1,5 +1,5 @@
import { query, useParams, action, createAsync, redirect, useSubmission } from "@solidjs/router" import { query, useParams, action, createAsync, redirect, useSubmission } from "@solidjs/router"
import { For, createEffect } from "solid-js" import { For, Show, createEffect } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { withActor } from "~/context/auth.withActor" import { withActor } from "~/context/auth.withActor"
import { Actor } from "@opencode-ai/console-core/actor.js" import { Actor } from "@opencode-ai/console-core/actor.js"
@@ -116,9 +116,9 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) =
const setUseBalance = action(async (form: FormData) => { const setUseBalance = action(async (form: FormData) => {
"use server" "use server"
const workspaceID = form.get("workspaceID") as string | null const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: formError.workspaceRequired } if (!workspaceID) return { error: formError.workspaceRequired }
const useBalance = (form.get("useBalance") as string | null) === "true" const useBalance = form.get("useBalance")?.toString() === "true"
return json( return json(
await withActor(async () => { await withActor(async () => {
@@ -10,11 +10,11 @@ import { formError, localizeError } from "~/lib/form-error"
const setMonthlyLimit = action(async (form: FormData) => { const setMonthlyLimit = action(async (form: FormData) => {
"use server" "use server"
const limit = form.get("limit") as string | null const limit = form.get("limit")?.toString()
if (!limit) return { error: formError.limitRequired } if (!limit) return { error: formError.limitRequired }
const numericLimit = parseInt(limit) const numericLimit = parseInt(limit)
if (numericLimit < 0) return { error: formError.monthlyLimitInvalid } if (numericLimit < 0) return { error: formError.monthlyLimitInvalid }
const workspaceID = form.get("workspaceID") as string | null const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: formError.workspaceRequired } if (!workspaceID) return { error: formError.workspaceRequired }
return json( return json(
await withActor( await withActor(
@@ -12,7 +12,7 @@ import { formError, formErrorReloadAmountMin, formErrorReloadTriggerMin, localiz
const reload = action(async (form: FormData) => { const reload = action(async (form: FormData) => {
"use server" "use server"
const workspaceID = form.get("workspaceID") as string | null const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: formError.workspaceRequired } if (!workspaceID) return { error: formError.workspaceRequired }
return json(await withActor(() => Billing.reload(), workspaceID), { return json(await withActor(() => Billing.reload(), workspaceID), {
revalidate: queryBillingInfo.key, revalidate: queryBillingInfo.key,
@@ -21,11 +21,11 @@ const reload = action(async (form: FormData) => {
const setReload = action(async (form: FormData) => { const setReload = action(async (form: FormData) => {
"use server" "use server"
const workspaceID = form.get("workspaceID") as string | null const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: formError.workspaceRequired } if (!workspaceID) return { error: formError.workspaceRequired }
const reloadValue = (form.get("reload") as string | null) === "true" const reloadValue = form.get("reload")?.toString() === "true"
const amountStr = form.get("reloadAmount") as string | null const amountStr = form.get("reloadAmount")?.toString()
const triggerStr = form.get("reloadTrigger") as string | null const triggerStr = form.get("reloadTrigger")?.toString()
const reloadAmount = amountStr && amountStr.trim() !== "" ? parseInt(amountStr) : null const reloadAmount = amountStr && amountStr.trim() !== "" ? parseInt(amountStr) : null
const reloadTrigger = triggerStr && triggerStr.trim() !== "" ? parseInt(triggerStr) : null const reloadTrigger = triggerStr && triggerStr.trim() !== "" ? parseInt(triggerStr) : null
@@ -90,9 +90,9 @@ export function ReloadSection() {
} }
const info = billingInfo()! const info = billingInfo()!
setStore("show", true) setStore("show", true)
setStore("reload", true) setStore("reload", info.reload ? true : true)
setStore("reloadAmount", String(info.reloadAmount)) setStore("reloadAmount", info.reloadAmount.toString())
setStore("reloadTrigger", String(info.reloadTrigger)) setStore("reloadTrigger", info.reloadTrigger.toString())
} }
function hide() { function hide() {
@@ -152,11 +152,11 @@ export function ReloadSection() {
data-component="input" data-component="input"
name="reloadAmount" name="reloadAmount"
type="number" type="number"
min={String(billingInfo()?.reloadAmountMin ?? "")} min={billingInfo()?.reloadAmountMin.toString()}
step="1" step="1"
value={store.reloadAmount} value={store.reloadAmount}
onInput={(e) => setStore("reloadAmount", e.currentTarget.value)} onInput={(e) => setStore("reloadAmount", e.currentTarget.value)}
placeholder={String(billingInfo()?.reloadAmount ?? "")} placeholder={billingInfo()?.reloadAmount.toString()}
disabled={!store.reload} disabled={!store.reload}
/> />
</div> </div>
@@ -166,11 +166,11 @@ export function ReloadSection() {
data-component="input" data-component="input"
name="reloadTrigger" name="reloadTrigger"
type="number" type="number"
min={String(billingInfo()?.reloadTriggerMin ?? "")} min={billingInfo()?.reloadTriggerMin.toString()}
step="1" step="1"
value={store.reloadTrigger} value={store.reloadTrigger}
onInput={(e) => setStore("reloadTrigger", e.currentTarget.value)} onInput={(e) => setStore("reloadTrigger", e.currentTarget.value)}
placeholder={String(billingInfo()?.reloadTrigger ?? "")} placeholder={billingInfo()?.reloadTrigger.toString()}
disabled={!store.reload} disabled={!store.reload}
/> />
</div> </div>
@@ -120,9 +120,9 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) =
const setLiteUseBalance = action(async (form: FormData) => { const setLiteUseBalance = action(async (form: FormData) => {
"use server" "use server"
const workspaceID = form.get("workspaceID") as string | null const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: formError.workspaceRequired } if (!workspaceID) return { error: formError.workspaceRequired }
const useBalance = (form.get("useBalance") as string | null) === "true" const useBalance = form.get("useBalance")?.toString() === "true"
return json( return json(
await withActor(async () => { await withActor(async () => {
@@ -12,18 +12,18 @@ import { formError, localizeError } from "~/lib/form-error"
const removeKey = action(async (form: FormData) => { const removeKey = action(async (form: FormData) => {
"use server" "use server"
const id = form.get("id") as string | null const id = form.get("id")?.toString()
if (!id) return { error: formError.idRequired } if (!id) return { error: formError.idRequired }
const workspaceID = form.get("workspaceID") as string | null const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: formError.workspaceRequired } if (!workspaceID) return { error: formError.workspaceRequired }
return json(await withActor(() => Key.remove({ id }), workspaceID), { revalidate: listKeys.key }) return json(await withActor(() => Key.remove({ id }), workspaceID), { revalidate: listKeys.key })
}, "key.remove") }, "key.remove")
const createKey = action(async (form: FormData) => { const createKey = action(async (form: FormData) => {
"use server" "use server"
const name = (form.get("name") as string | null)?.trim() const name = form.get("name")?.toString().trim()
if (!name) return { error: formError.nameRequired } if (!name) return { error: formError.nameRequired }
const workspaceID = form.get("workspaceID") as string | null const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: formError.workspaceRequired } if (!workspaceID) return { error: formError.workspaceRequired }
return json( return json(
await withActor( await withActor(
@@ -24,13 +24,13 @@ const listMembers = query(async (workspaceID: string) => {
const inviteMember = action(async (form: FormData) => { const inviteMember = action(async (form: FormData) => {
"use server" "use server"
const email = (form.get("email") as string | null)?.trim() const email = form.get("email")?.toString().trim()
if (!email) return { error: formError.emailRequired } if (!email) return { error: formError.emailRequired }
const workspaceID = form.get("workspaceID") as string | null const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: formError.workspaceRequired } if (!workspaceID) return { error: formError.workspaceRequired }
const role = form.get("role") as (typeof UserRole)[number] | null const role = form.get("role")?.toString() as (typeof UserRole)[number]
if (!role) return { error: formError.roleRequired } if (!role) return { error: formError.roleRequired }
const limit = form.get("limit") as string | null const limit = form.get("limit")?.toString()
const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null
if (monthlyLimit !== null && monthlyLimit < 0) return { error: formError.monthlyLimitInvalid } if (monthlyLimit !== null && monthlyLimit < 0) return { error: formError.monthlyLimitInvalid }
return json( return json(
@@ -47,9 +47,9 @@ const inviteMember = action(async (form: FormData) => {
const removeMember = action(async (form: FormData) => { const removeMember = action(async (form: FormData) => {
"use server" "use server"
const id = form.get("id") as string | null const id = form.get("id")?.toString()
if (!id) return { error: formError.idRequired } if (!id) return { error: formError.idRequired }
const workspaceID = form.get("workspaceID") as string | null const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: formError.workspaceRequired } if (!workspaceID) return { error: formError.workspaceRequired }
return json( return json(
await withActor( await withActor(
@@ -66,13 +66,13 @@ const removeMember = action(async (form: FormData) => {
const updateMember = action(async (form: FormData) => { const updateMember = action(async (form: FormData) => {
"use server" "use server"
const id = form.get("id") as string | null const id = form.get("id")?.toString()
if (!id) return { error: formError.idRequired } if (!id) return { error: formError.idRequired }
const workspaceID = form.get("workspaceID") as string | null const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: formError.workspaceRequired } if (!workspaceID) return { error: formError.workspaceRequired }
const role = form.get("role") as (typeof UserRole)[number] | null const role = form.get("role")?.toString() as (typeof UserRole)[number]
if (!role) return { error: formError.roleRequired } if (!role) return { error: formError.roleRequired }
const limit = form.get("limit") as string | null const limit = form.get("limit")?.toString()
const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null
if (monthlyLimit !== null && monthlyLimit < 0) return { error: formError.monthlyLimitInvalid } if (monthlyLimit !== null && monthlyLimit < 0) return { error: formError.monthlyLimitInvalid }
@@ -118,7 +118,7 @@ function MemberRow(props: {
} }
setStore("editing", true) setStore("editing", true)
setStore("selectedRole", props.member.role) setStore("selectedRole", props.member.role)
setStore("limit", props.member.monthlyLimit != null ? String(props.member.monthlyLimit) : "") setStore("limit", props.member.monthlyLimit?.toString() ?? "")
} }
function hide() { function hide() {
@@ -67,11 +67,11 @@ const getModelsInfo = query(async (workspaceID: string) => {
const updateModel = action(async (form: FormData) => { const updateModel = action(async (form: FormData) => {
"use server" "use server"
const model = form.get("model") as string | null const model = form.get("model")?.toString()
if (!model) return { error: formError.modelRequired } if (!model) return { error: formError.modelRequired }
const workspaceID = form.get("workspaceID") as string | null const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: formError.workspaceRequired } if (!workspaceID) return { error: formError.workspaceRequired }
const enabled = (form.get("enabled") as string | null) === "true" const enabled = form.get("enabled")?.toString() === "true"
return json( return json(
withActor(async () => { withActor(async () => {
if (enabled) { if (enabled) {
@@ -163,7 +163,7 @@ export function ModelSection() {
<form action={updateModel} method="post"> <form action={updateModel} method="post">
<input type="hidden" name="model" value={id} /> <input type="hidden" name="model" value={id} />
<input type="hidden" name="workspaceID" value={params.id} /> <input type="hidden" name="workspaceID" value={params.id} />
<input type="hidden" name="enabled" value={String(isEnabled())} /> <input type="hidden" name="enabled" value={isEnabled().toString()} />
<label data-slot="model-toggle-label"> <label data-slot="model-toggle-label">
<input <input
type="checkbox" type="checkbox"
@@ -21,9 +21,9 @@ function maskCredentials(credentials: string) {
const removeProvider = action(async (form: FormData) => { const removeProvider = action(async (form: FormData) => {
"use server" "use server"
const provider = form.get("provider") as string | null const provider = form.get("provider")?.toString()
if (!provider) return { error: formError.providerRequired } if (!provider) return { error: formError.providerRequired }
const workspaceID = form.get("workspaceID") as string | null const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: formError.workspaceRequired } if (!workspaceID) return { error: formError.workspaceRequired }
return json(await withActor(() => Provider.remove({ provider }), workspaceID), { return json(await withActor(() => Provider.remove({ provider }), workspaceID), {
revalidate: listProviders.key, revalidate: listProviders.key,
@@ -32,11 +32,11 @@ const removeProvider = action(async (form: FormData) => {
const saveProvider = action(async (form: FormData) => { const saveProvider = action(async (form: FormData) => {
"use server" "use server"
const provider = form.get("provider") as string | null const provider = form.get("provider")?.toString()
const credentials = form.get("credentials") as string | null const credentials = form.get("credentials")?.toString()
if (!provider) return { error: formError.providerRequired } if (!provider) return { error: formError.providerRequired }
if (!credentials) return { error: formError.apiKeyRequired } if (!credentials) return { error: formError.apiKeyRequired }
const workspaceID = form.get("workspaceID") as string | null const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: formError.workspaceRequired } if (!workspaceID) return { error: formError.workspaceRequired }
return json( return json(
await withActor( await withActor(
@@ -59,13 +59,10 @@ function ProviderRow(props: { provider: Provider }) {
const params = useParams() const params = useParams()
const i18n = useI18n() const i18n = useI18n()
const providers = createAsync(() => listProviders(params.id!)) const providers = createAsync(() => listProviders(params.id!))
const saveSubmission = useSubmission( const saveSubmission = useSubmission(saveProvider, ([fd]) => fd.get("provider")?.toString() === props.provider.key)
saveProvider,
([fd]) => (fd.get("provider") as string | null) === props.provider.key,
)
const removeSubmission = useSubmission( const removeSubmission = useSubmission(
removeProvider, removeProvider,
([fd]) => (fd.get("provider") as string | null) === props.provider.key, ([fd]) => fd.get("provider")?.toString() === props.provider.key,
) )
const [store, setStore] = createStore({ editing: false }) const [store, setStore] = createStore({ editing: false })
@@ -30,10 +30,10 @@ const getWorkspaceInfo = query(async (workspaceID: string) => {
const updateWorkspace = action(async (form: FormData) => { const updateWorkspace = action(async (form: FormData) => {
"use server" "use server"
const name = (form.get("name") as string | null)?.trim() const name = form.get("name")?.toString().trim()
if (!name) return { error: formError.workspaceNameRequired } if (!name) return { error: formError.workspaceNameRequired }
if (name.length > 255) return { error: formError.nameTooLong } if (name.length > 255) return { error: formError.nameTooLong }
const workspaceID = form.get("workspaceID") as string | null const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: formError.workspaceRequired } if (!workspaceID) return { error: formError.workspaceRequired }
return json( return json(
await withActor( await withActor(
@@ -1,5 +1,5 @@
import "./index.css" import "./index.css"
import { createAsync, query } from "@solidjs/router" import { createAsync, query, redirect } from "@solidjs/router"
import { Title, Meta } from "@solidjs/meta" import { Title, Meta } from "@solidjs/meta"
//import { HttpHeader } from "@solidjs/start" //import { HttpHeader } from "@solidjs/start"
import zenLogoLight from "../../asset/zen-ornate-light.svg" import zenLogoLight from "../../asset/zen-ornate-light.svg"
@@ -26,14 +26,14 @@ export function createDataDumper(sessionId: string, requestId: string, projectId
const minute = timestamp.substring(10, 12) const minute = timestamp.substring(10, 12)
const second = timestamp.substring(12, 14) const second = timestamp.substring(12, 14)
void waitUntil( waitUntil(
Resource.ZenDataNew.put( Resource.ZenDataNew.put(
`data/${data.modelName}/${year}/${month}/${day}/${hour}/${minute}/${second}/${requestId}.json`, `data/${data.modelName}/${year}/${month}/${day}/${hour}/${minute}/${second}/${requestId}.json`,
JSON.stringify({ timestamp, ...data }), JSON.stringify({ timestamp, ...data }),
), ),
) )
void waitUntil( waitUntil(
Resource.ZenDataNew.put( Resource.ZenDataNew.put(
`meta/${data.modelName}/${sessionId}/${requestId}.json`, `meta/${data.modelName}/${sessionId}/${requestId}.json`,
JSON.stringify({ timestamp, ...metadata }), JSON.stringify({ timestamp, ...metadata }),
@@ -45,7 +45,6 @@ import { LiteData } from "@opencode-ai/console-core/lite.js"
import { Resource } from "@opencode-ai/console-resource" import { Resource } from "@opencode-ai/console-resource"
import { i18n, type Key } from "~/i18n" import { i18n, type Key } from "~/i18n"
import { localeFromRequest } from "~/lib/language" import { localeFromRequest } from "~/lib/language"
import { createModelTpmLimiter } from "./modelTpmLimiter"
type ZenData = Awaited<ReturnType<typeof ZenData.list>> type ZenData = Awaited<ReturnType<typeof ZenData.list>>
type RetryOptions = { type RetryOptions = {
@@ -122,8 +121,6 @@ export async function handler(
const authInfo = await authenticate(modelInfo, zenApiKey) const authInfo = await authenticate(modelInfo, zenApiKey)
const billingSource = validateBilling(authInfo, modelInfo) const billingSource = validateBilling(authInfo, modelInfo)
logger.metric({ source: billingSource }) logger.metric({ source: billingSource })
const modelTpmLimiter = createModelTpmLimiter(modelInfo.providers)
const modelTpmLimits = await modelTpmLimiter?.check()
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => { const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
const providerInfo = selectProvider( const providerInfo = selectProvider(
@@ -136,7 +133,6 @@ export async function handler(
trialProviders, trialProviders,
retry, retry,
stickyProvider, stickyProvider,
modelTpmLimits,
) )
validateModelSettings(billingSource, authInfo) validateModelSettings(billingSource, authInfo)
updateProviderKey(authInfo, providerInfo) updateProviderKey(authInfo, providerInfo)
@@ -148,7 +144,7 @@ export async function handler(
providerInfo.modifyBody({ providerInfo.modifyBody({
...createBodyConverter(opts.format, providerInfo.format)(body), ...createBodyConverter(opts.format, providerInfo.format)(body),
model: providerInfo.model, model: providerInfo.model,
...providerInfo.payloadModifier, ...(providerInfo.payloadModifier ?? {}),
...Object.fromEntries( ...Object.fromEntries(
Object.entries(providerInfo.payloadMappings ?? {}) Object.entries(providerInfo.payloadMappings ?? {})
.map(([k, v]) => [k, input.request.headers.get(v)]) .map(([k, v]) => [k, input.request.headers.get(v)])
@@ -233,7 +229,6 @@ export async function handler(
const usageInfo = providerInfo.normalizeUsage(json.usage) const usageInfo = providerInfo.normalizeUsage(json.usage)
const costInfo = calculateCost(modelInfo, usageInfo) const costInfo = calculateCost(modelInfo, usageInfo)
await trialLimiter?.track(usageInfo) await trialLimiter?.track(usageInfo)
await modelTpmLimiter?.track(providerInfo.id, providerInfo.model, usageInfo)
await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo) await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo)
await reload(billingSource, authInfo, costInfo) await reload(billingSource, authInfo, costInfo)
json.cost = calculateOccurredCost(billingSource, costInfo) json.cost = calculateOccurredCost(billingSource, costInfo)
@@ -283,7 +278,6 @@ export async function handler(
const usageInfo = providerInfo.normalizeUsage(usage) const usageInfo = providerInfo.normalizeUsage(usage)
const costInfo = calculateCost(modelInfo, usageInfo) const costInfo = calculateCost(modelInfo, usageInfo)
await trialLimiter?.track(usageInfo) await trialLimiter?.track(usageInfo)
await modelTpmLimiter?.track(providerInfo.id, providerInfo.model, usageInfo)
await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo) await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo)
await reload(billingSource, authInfo, costInfo) await reload(billingSource, authInfo, costInfo)
const cost = calculateOccurredCost(billingSource, costInfo) const cost = calculateOccurredCost(billingSource, costInfo)
@@ -351,7 +345,7 @@ export async function handler(
logger.metric({ logger.metric({
"error.cause2": JSON.stringify(error.cause), "error.cause2": JSON.stringify(error.cause),
}) })
} catch {} } catch (e) {}
} }
// Note: both top level "type" and "error.type" fields are used by the @ai-sdk/anthropic client to render the error message. // Note: both top level "type" and "error.type" fields are used by the @ai-sdk/anthropic client to render the error message.
@@ -439,16 +433,12 @@ export async function handler(
trialProviders: string[] | undefined, trialProviders: string[] | undefined,
retry: RetryOptions, retry: RetryOptions,
stickyProvider: string | undefined, stickyProvider: string | undefined,
modelTpmLimits: Record<string, number> | undefined,
) { ) {
const modelProvider = (() => { 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) { if (authInfo?.provider?.credentials) {
return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider) return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)
} }
// Always use the same provider for the same session
if (stickyProvider) { if (stickyProvider) {
const provider = modelInfo.providers.find((provider) => provider.id === stickyProvider) const provider = modelInfo.providers.find((provider) => provider.id === stickyProvider)
if (provider) return provider if (provider) return provider
@@ -461,20 +451,10 @@ export async function handler(
} }
if (retry.retryCount !== MAX_FAILOVER_RETRIES) { if (retry.retryCount !== MAX_FAILOVER_RETRIES) {
const allProviders = modelInfo.providers const providers = modelInfo.providers
.filter((provider) => !provider.disabled) .filter((provider) => !provider.disabled)
.filter((provider) => provider.weight !== 0)
.filter((provider) => !retry.excludeProviders.includes(provider.id)) .filter((provider) => !retry.excludeProviders.includes(provider.id))
.filter((provider) => { .flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(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 // Use the last 4 characters of session ID to select a provider
const identifier = sessionId.length ? sessionId : ip const identifier = sessionId.length ? sessionId : ip
@@ -1,51 +0,0 @@
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}` } }),
)
},
}
}
@@ -153,7 +153,7 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) =>
let json let json
try { try {
json = JSON.parse(data.slice(6)) json = JSON.parse(data.slice(6))
} catch { } catch (e) {
return return
} }
@@ -48,7 +48,7 @@ export const googleHelper: ProviderHelper = ({ providerModel }) => ({
let json let json
try { try {
json = JSON.parse(chunk.slice(6)) as { usageMetadata?: Usage } json = JSON.parse(chunk.slice(6)) as { usageMetadata?: Usage }
} catch { } catch (e) {
return return
} }
@@ -30,7 +30,7 @@ export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage, safetyIdentif
headers.set("authorization", `Bearer ${apiKey}`) headers.set("authorization", `Bearer ${apiKey}`)
headers.set("x-session-affinity", headers.get("x-opencode-session") ?? "") 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 { return {
...body, ...body,
...(body.stream ? { stream_options: { include_usage: true } } : {}), ...(body.stream ? { stream_options: { include_usage: true } } : {}),
@@ -49,7 +49,7 @@ export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage, safetyIdentif
let json let json
try { try {
json = JSON.parse(chunk.slice(6)) as { usage?: Usage } json = JSON.parse(chunk.slice(6)) as { usage?: Usage }
} catch { } catch (e) {
return return
} }
@@ -289,7 +289,7 @@ export function fromOaCompatibleResponse(resp: any): CommonResponse {
index: 0, index: 0,
message: { message: {
role: "assistant" as const, role: "assistant" as const,
...(content.some((c) => c.type === "text") ...(content.length > 0 && content.some((c) => c.type === "text")
? { ? {
content: content content: content
.filter((c) => c.type === "text") .filter((c) => c.type === "text")
@@ -297,7 +297,7 @@ export function fromOaCompatibleResponse(resp: any): CommonResponse {
.join(""), .join(""),
} }
: {}), : {}),
...(content.some((c) => c.type === "tool_use") ...(content.length > 0 && content.some((c) => c.type === "tool_use")
? { ? {
tool_calls: content tool_calls: content
.filter((c) => c.type === "tool_use") .filter((c) => c.type === "tool_use")
@@ -36,7 +36,7 @@ export const openaiHelper: ProviderHelper = ({ workspaceID }) => ({
let json let json
try { try {
json = JSON.parse(data.slice(6)) as { response?: { usage?: Usage } } json = JSON.parse(data.slice(6)) as { response?: { usage?: Usage } }
} catch { } catch (e) {
return return
} }
@@ -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 { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
import { ZenData } from "@opencode-ai/console-core/model.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, { return new Response(null, {
status: 200, status: 200,
headers: { headers: {
@@ -6,8 +6,8 @@ export function POST(input: APIEvent) {
format: "google", format: "google",
modelList: "full", modelList: "full",
parseApiKey: (headers: Headers) => headers.get("x-goog-api-key") ?? undefined, parseApiKey: (headers: Headers) => headers.get("x-goog-api-key") ?? undefined,
parseModel: (url: string, _body: any) => url.split("/").pop()?.split(":")?.[0] ?? "", parseModel: (url: string, body: any) => url.split("/").pop()?.split(":")?.[0] ?? "",
parseIsStream: (url: string, _body: any) => parseIsStream: (url: string, body: any) =>
// ie. url: https://opencode.ai/zen/v1/models/gemini-3-pro:streamGenerateContent?alt=sse' // ie. url: https://opencode.ai/zen/v1/models/gemini-3-pro:streamGenerateContent?alt=sse'
url.split("/").pop()?.split(":")?.[1]?.startsWith("streamGenerateContent") ?? false, url.split("/").pop()?.split(":")?.[1]?.startsWith("streamGenerateContent") ?? false,
}) })
@@ -1,6 +0,0 @@
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
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.4.11", "version": "1.4.6",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
@@ -1,5 +1,7 @@
import { Database, eq } from "../src/drizzle/index.js" import { subscribe } from "diagnostics_channel"
import { BillingTable } from "../src/schema/billing.sql.js" import { Billing } from "../src/billing.js"
import { and, Database, eq } from "../src/drizzle/index.js"
import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
const workspaceID = process.argv[2] const workspaceID = process.argv[2]
+4 -2
View File
@@ -1,10 +1,12 @@
import { Billing } from "../src/billing.js" import { Billing } from "../src/billing.js"
import { and, Database, eq, isNull } from "../src/drizzle/index.js" import { and, Database, eq, isNull, sql } from "../src/drizzle/index.js"
import { UserTable } from "../src/schema/user.sql.js" import { UserTable } from "../src/schema/user.sql.js"
import { BillingTable, SubscriptionTable } from "../src/schema/billing.sql.js" import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
import { Identifier } from "../src/identifier.js" import { Identifier } from "../src/identifier.js"
import { centsToMicroCents } from "../src/util/price.js"
import { AuthTable } from "../src/schema/auth.sql.js" import { AuthTable } from "../src/schema/auth.sql.js"
import { BlackData } from "../src/black.js" import { BlackData } from "../src/black.js"
import { Actor } from "../src/actor.js"
const plan = "200" const plan = "200"
const couponID = "JAIr0Pe1" const couponID = "JAIr0Pe1"
@@ -1,5 +1,7 @@
import { Database, eq } from "../src/drizzle/index.js" import { subscribe } from "diagnostics_channel"
import { BillingTable } from "../src/schema/billing.sql.js" import { Billing } from "../src/billing.js"
import { and, Database, eq } from "../src/drizzle/index.js"
import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
const workspaceID = process.argv[2] const workspaceID = process.argv[2]
@@ -1,4 +1,4 @@
import { Database, eq, and, sql, inArray, isNull } from "../src/drizzle/index.js" import { Database, eq, and, sql, inArray, isNull, count } from "../src/drizzle/index.js"
import { BillingTable, BlackPlans } from "../src/schema/billing.sql.js" import { BillingTable, BlackPlans } from "../src/schema/billing.sql.js"
import { UserTable } from "../src/schema/user.sql.js" import { UserTable } from "../src/schema/user.sql.js"
import { AuthTable } from "../src/schema/auth.sql.js" import { AuthTable } from "../src/schema/auth.sql.js"
+10 -6
View File
@@ -24,9 +24,11 @@ export namespace Key {
.innerJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email"))) .innerJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
.where( .where(
and( and(
eq(KeyTable.workspaceID, Actor.workspace()), ...[
isNull(KeyTable.timeDeleted), eq(KeyTable.workspaceID, Actor.workspace()),
...(Actor.userRole() === "admin" ? [] : [eq(KeyTable.userID, Actor.userID())]), isNull(KeyTable.timeDeleted),
...(Actor.userRole() === "admin" ? [] : [eq(KeyTable.userID, Actor.userID())]),
],
), ),
) )
.orderBy(sql`${KeyTable.name} DESC`), .orderBy(sql`${KeyTable.name} DESC`),
@@ -82,9 +84,11 @@ export namespace Key {
}) })
.where( .where(
and( and(
eq(KeyTable.id, input.id), ...[
eq(KeyTable.workspaceID, Actor.workspace()), eq(KeyTable.id, input.id),
...(Actor.userRole() === "admin" ? [] : [eq(KeyTable.userID, Actor.userID())]), eq(KeyTable.workspaceID, Actor.workspace()),
...(Actor.userRole() === "admin" ? [] : [eq(KeyTable.userID, Actor.userID())]),
],
), ),
), ),
) )
+4 -11
View File
@@ -34,8 +34,6 @@ export namespace ZenData {
z.object({ z.object({
id: z.string(), id: z.string(),
model: z.string(), model: z.string(),
priority: z.number().optional(),
tpmLimit: z.number().optional(),
weight: z.number().optional(), weight: z.number().optional(),
disabled: z.boolean().optional(), disabled: z.boolean().optional(),
storeModel: z.string().optional(), storeModel: z.string().optional(),
@@ -125,16 +123,10 @@ export namespace ZenData {
), ),
models: (() => { models: (() => {
const normalize = (model: z.infer<typeof ModelSchema>) => { const normalize = (model: z.infer<typeof ModelSchema>) => {
const providers = model.providers.map((p) => ({ const composite = model.providers.find((p) => compositeProviders[p.id].length > 1)
...p,
priority: p.priority ?? Infinity,
weight: p.weight ?? 1,
}))
const composite = providers.find((p) => compositeProviders[p.id].length > 1)
if (!composite) if (!composite)
return { return {
trialProvider: model.trialProvider ? [model.trialProvider] : undefined, trialProvider: model.trialProvider ? [model.trialProvider] : undefined,
providers,
} }
const weightMulti = compositeProviders[composite.id].length const weightMulti = compositeProviders[composite.id].length
@@ -145,16 +137,17 @@ export namespace ZenData {
if (model.trialProvider === composite.id) return compositeProviders[composite.id].map((p) => p.id) if (model.trialProvider === composite.id) return compositeProviders[composite.id].map((p) => p.id)
return [model.trialProvider] return [model.trialProvider]
})(), })(),
providers: providers.flatMap((p) => providers: model.providers.flatMap((p) =>
p.id === composite.id p.id === composite.id
? compositeProviders[p.id].map((sub) => ({ ? compositeProviders[p.id].map((sub) => ({
...p, ...p,
id: sub.id, id: sub.id,
weight: p.weight ?? 1,
})) }))
: [ : [
{ {
...p, ...p,
weight: p.weight * weightMulti, weight: (p.weight ?? 1) * weightMulti,
}, },
], ],
), ),
@@ -30,13 +30,3 @@ export const KeyRateLimitTable = mysqlTable(
}, },
(table) => [primaryKey({ columns: [table.key, table.interval] })], (table) => [primaryKey({ columns: [table.key, table.interval] })],
) )
export const ModelRateLimitTable = mysqlTable(
"model_rate_limit",
{
key: varchar("key", { length: 255 }).notNull(),
interval: varchar("interval", { length: 40 }).notNull(),
count: int("count").notNull(),
},
(table) => [primaryKey({ columns: [table.key, table.interval] })],
)

Some files were not shown because too many files have changed in this diff Show More