mirror of
https://github.com/anomalyco/opencode.git
synced 2026-10-17 01:40:19 +00:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1f604bbe0 | ||
|
|
60c9be0921 | ||
|
|
d12161eecd | ||
|
|
e03feec313 | ||
|
|
3be039593a | ||
|
|
bc7c890d97 | ||
|
|
d320177aa4 | ||
|
|
a95d9c0fe4 | ||
|
|
abce98e0eb | ||
|
|
dfb56f76c7 | ||
|
|
add9b4ba5a | ||
|
|
9c97b807d0 | ||
|
|
c28b6a0a3b | ||
|
|
7b39a338a0 | ||
|
|
03f499dc6c | ||
|
|
7062121a19 | ||
|
|
9b14c13476 |
No files matched your search
@@ -3,21 +3,19 @@ name: deploy
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- production
|
||||
- v1
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production')
|
||||
runs-on: ${{ github.ref_name == 'dev' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
|
||||
environment: ${{ github.ref_name }}
|
||||
if: github.repository == 'anomalyco/opencode' && github.ref_name == 'v1'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
environment: v1
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
@@ -27,23 +25,14 @@ jobs:
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
|
||||
with:
|
||||
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
|
||||
role-session-name: opencode-${{ github.run_id }}
|
||||
aws-region: us-east-1
|
||||
|
||||
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||
# Only the V1 web app (app.opencode.ai) deploys from v1.
|
||||
- run: bun sst deploy --stage=v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }}
|
||||
PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
|
||||
STRIPE_SECRET_KEY: ${{ github.ref_name == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }}
|
||||
HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }}
|
||||
SENTRY_RELEASE: web@${{ github.sha }}
|
||||
VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }}
|
||||
VITE_SENTRY_ENVIRONMENT: production
|
||||
VITE_SENTRY_RELEASE: web@${{ github.sha }}
|
||||
@@ -3,7 +3,7 @@ name: generate
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- v1
|
||||
|
||||
jobs:
|
||||
generate:
|
||||
|
||||
@@ -2,9 +2,9 @@ name: nix-eval
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
branches: [v1]
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
branches: [v1]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
|
||||
@@ -6,7 +6,7 @@ permissions:
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [dev, beta]
|
||||
branches: [v1, beta]
|
||||
paths:
|
||||
- "bun.lock"
|
||||
- "package.json"
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- ci
|
||||
- dev
|
||||
- v1
|
||||
- beta
|
||||
- fix/npm-native-binary-install
|
||||
- snapshot-*
|
||||
@@ -31,6 +31,10 @@ permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
env:
|
||||
# V1 previews keep the dev channel so existing opencode-ai@dev installs keep upgrading and their data path.
|
||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'v1' && !inputs.bump && !inputs.version && 'dev') || '' }}
|
||||
|
||||
jobs:
|
||||
version:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
|
||||
@@ -3,14 +3,14 @@ name: test
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- v1
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
# Keep every run on dev so cancelled checks do not pollute the default branch
|
||||
# Keep every run on v1 so cancelled checks do not pollute the release branch
|
||||
# commit history. PRs and other branches still share a group and cancel stale runs.
|
||||
group: ${{ case(github.ref == 'refs/heads/dev', format('{0}-{1}', github.workflow, github.run_id), format('{0}-{1}', github.workflow, github.event.pull_request.number || github.ref)) }}
|
||||
group: ${{ case(github.ref == 'refs/heads/v1', format('{0}-{1}', github.workflow, github.run_id), format('{0}-{1}', github.workflow, github.event.pull_request.number || github.ref)) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -2,9 +2,9 @@ name: typecheck
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
branches: [v1]
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
branches: [v1]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@corvu/drawer": "catalog:",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"bin": {
|
||||
"lildax": "./bin/lildax.cjs",
|
||||
},
|
||||
@@ -144,7 +144,7 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
@@ -158,7 +158,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -195,7 +195,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -222,7 +222,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.82",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -245,7 +245,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -269,7 +269,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode-ai/console-core": "workspace:*",
|
||||
@@ -289,7 +289,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -384,7 +384,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"drizzle-orm": "catalog:",
|
||||
@@ -438,7 +438,7 @@
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -452,7 +452,7 @@
|
||||
},
|
||||
"packages/effect-sqlite-node": {
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -464,7 +464,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -496,7 +496,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -512,7 +512,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "4.0.0-beta.83",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||
@@ -543,7 +543,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
@@ -562,7 +562,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -692,7 +692,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -768,7 +768,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -783,7 +783,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
@@ -798,7 +798,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz",
|
||||
@@ -838,7 +838,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -851,7 +851,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -878,7 +878,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@planetscale/database": "1.19.0",
|
||||
"drizzle-orm": "catalog:",
|
||||
@@ -896,7 +896,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/stats-core": "workspace:*",
|
||||
@@ -937,7 +937,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
@@ -963,7 +963,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
@@ -1014,7 +1014,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
||||
+1
-2
@@ -14,7 +14,7 @@ inputs:
|
||||
required: false
|
||||
|
||||
share:
|
||||
description: "Share the opencode session (defaults to true for public repos)"
|
||||
description: "Deprecated and ignored: session sharing was discontinued"
|
||||
required: false
|
||||
|
||||
prompt:
|
||||
@@ -71,7 +71,6 @@ runs:
|
||||
env:
|
||||
MODEL: ${{ inputs.model }}
|
||||
AGENT: ${{ inputs.agent }}
|
||||
SHARE: ${{ inputs.share }}
|
||||
PROMPT: ${{ inputs.prompt }}
|
||||
USE_GITHUB_TOKEN: ${{ inputs.use_github_token }}
|
||||
MENTIONS: ${{ inputs.mentions }}
|
||||
|
||||
@@ -65,6 +65,18 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
requested_version="${requested_version#v}"
|
||||
|
||||
# This installer only installs OpenCode 1. OpenCode 1 clients run it with VERSION set when upgrading,
|
||||
# so refusing 2.x also keeps older clients from replacing themselves with OpenCode 2 by accident.
|
||||
if [[ "$requested_version" =~ ^([0-9]+)\. ]] && [ "${BASH_REMATCH[1]}" -ge 2 ]; then
|
||||
echo -e "${RED}Error: OpenCode ${requested_version} is not an OpenCode 1 release, so this installer can't install it.${NC}"
|
||||
echo -e "${MUTED}To install OpenCode 2, run: ${NC}curl -fsSL https://opencode.ai/v2/install | bash"
|
||||
echo -e "${MUTED}To upgrade an existing OpenCode 1 install, run: ${NC}opencode upgrade --major"
|
||||
echo -e "${MUTED}Docs: https://opencode.ai/v2/docs${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INSTALL_DIR=$HOME/.opencode/bin
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
@@ -181,23 +193,35 @@ else
|
||||
fi
|
||||
|
||||
if [ -z "$requested_version" ]; then
|
||||
url="https://github.com/anomalyco/opencode/releases/latest/download/$filename"
|
||||
specific_version=$(curl -s https://api.github.com/repos/anomalyco/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
|
||||
# The opencode-ai npm package only contains OpenCode 1 (OpenCode 2 ships as @opencode/cli). npm is
|
||||
# published before the GitHub release, so fall back to the newest v1 entry in GitHub's release list
|
||||
# until its assets exist. GitHub's Latest release may be OpenCode 2, so it's never used.
|
||||
specific_version=$(curl -fsSL --max-time 10 "https://registry.npmjs.org/opencode-ai/latest" 2>/dev/null | grep -o '"version": *"[^"]*"' | head -n 1 | sed 's/.*"\([^"]*\)"$/\1/' || true)
|
||||
if [[ "$specific_version" =~ ^1\.[0-9]+\.[0-9]+$ ]]; then
|
||||
asset_status=$(curl -sIL --max-time 10 -o /dev/null -w "%{http_code}" "https://github.com/anomalyco/opencode/releases/download/v${specific_version}/$filename" || true)
|
||||
if [ "$asset_status" != "200" ]; then
|
||||
specific_version=""
|
||||
fi
|
||||
else
|
||||
specific_version=""
|
||||
fi
|
||||
if [ -z "$specific_version" ]; then
|
||||
specific_version=$(curl -fsSL --max-time 30 "https://api.github.com/repos/anomalyco/opencode/releases?per_page=100" 2>/dev/null | grep -o '"tag_name": *"v1\.[0-9]*\.[0-9]*"' | head -n 1 | sed 's/.*"v\([^"]*\)"$/\1/' || true)
|
||||
fi
|
||||
|
||||
if [[ $? -ne 0 || -z "$specific_version" ]]; then
|
||||
if [[ -z "$specific_version" ]]; then
|
||||
echo -e "${RED}Failed to fetch version information${NC}"
|
||||
exit 1
|
||||
fi
|
||||
url="https://github.com/anomalyco/opencode/releases/download/v${specific_version}/$filename"
|
||||
else
|
||||
# Strip leading 'v' if present
|
||||
requested_version="${requested_version#v}"
|
||||
url="https://github.com/anomalyco/opencode/releases/download/v${requested_version}/$filename"
|
||||
specific_version=$requested_version
|
||||
|
||||
# Verify the release exists before downloading
|
||||
http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/anomalyco/opencode/releases/tag/v${requested_version}")
|
||||
# Verify the release asset exists before downloading. A release that is still publishing has none yet.
|
||||
http_status=$(curl -sIL -o /dev/null -w "%{http_code}" "$url" || true)
|
||||
if [ "$http_status" = "404" ]; then
|
||||
echo -e "${RED}Error: Release v${requested_version} not found${NC}"
|
||||
echo -e "${RED}Error: Release v${requested_version} not found. If it was just released, try again in a few minutes.${NC}"
|
||||
echo -e "${MUTED}Available releases: https://github.com/anomalyco/opencode/releases${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -299,7 +299,8 @@ export function MessageTimeline(props: {
|
||||
const titleValue = createMemo(() => info()?.title)
|
||||
const titleLabel = createMemo(() => sessionTitle(titleValue()))
|
||||
const shareUrl = createMemo(() => info()?.share?.url)
|
||||
const shareEnabled = createMemo(() => sync().data.config.share !== "disabled")
|
||||
// Session sharing was discontinued; keep the share menu and popover hidden.
|
||||
const shareEnabled = () => false
|
||||
const parentID = createMemo(() => info()?.parentID)
|
||||
const parent = createMemo(() => {
|
||||
const id = parentID()
|
||||
|
||||
@@ -147,93 +147,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
if (sessionID) return permission.isAutoAccepting(sessionID, sdk().directory)
|
||||
return permission.isAutoAcceptingDirectory(sdk().directory)
|
||||
}
|
||||
const write = async (value: string) => {
|
||||
const body = typeof document === "undefined" ? undefined : document.body
|
||||
if (body) {
|
||||
const textarea = document.createElement("textarea")
|
||||
textarea.value = value
|
||||
textarea.setAttribute("readonly", "")
|
||||
textarea.style.position = "fixed"
|
||||
textarea.style.opacity = "0"
|
||||
textarea.style.pointerEvents = "none"
|
||||
body.appendChild(textarea)
|
||||
textarea.select()
|
||||
const copied = document.execCommand("copy")
|
||||
body.removeChild(textarea)
|
||||
if (copied) return true
|
||||
}
|
||||
|
||||
const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard
|
||||
if (!clipboard?.writeText) return false
|
||||
return clipboard.writeText(value).then(
|
||||
() => true,
|
||||
() => false,
|
||||
)
|
||||
}
|
||||
|
||||
const copyShare = async (url: string, existing: boolean) => {
|
||||
if (!(await write(url))) {
|
||||
showToast({
|
||||
title: language.t("toast.session.share.copyFailed.title"),
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
showToast({
|
||||
title: existing ? language.t("session.share.copy.copied") : language.t("toast.session.share.success.title"),
|
||||
description: language.t("toast.session.share.success.description"),
|
||||
variant: "success",
|
||||
})
|
||||
}
|
||||
|
||||
const share = async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
|
||||
const existing = info()?.share?.url
|
||||
if (existing) {
|
||||
await copyShare(existing, true)
|
||||
return
|
||||
}
|
||||
|
||||
const url = await sdk()
|
||||
.client.session.share({ sessionID })
|
||||
.then((res) => res.data?.share?.url)
|
||||
.catch(() => undefined)
|
||||
if (!url) {
|
||||
showToast({
|
||||
title: language.t("toast.session.share.failed.title"),
|
||||
description: language.t("toast.session.share.failed.description"),
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await copyShare(url, false)
|
||||
}
|
||||
|
||||
const unshare = async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
|
||||
await sdk()
|
||||
.client.session.unshare({ sessionID })
|
||||
.then(() =>
|
||||
showToast({
|
||||
title: language.t("toast.session.unshare.success.title"),
|
||||
description: language.t("toast.session.unshare.success.description"),
|
||||
variant: "success",
|
||||
}),
|
||||
)
|
||||
.catch(() =>
|
||||
showToast({
|
||||
title: language.t("toast.session.unshare.failed.title"),
|
||||
description: language.t("toast.session.unshare.failed.description"),
|
||||
variant: "error",
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const exportSession = async () => {
|
||||
const sessionID = params.id
|
||||
@@ -421,30 +334,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
)
|
||||
}
|
||||
|
||||
const shareCmds = () => {
|
||||
if (sync().data.config.share === "disabled") return []
|
||||
return [
|
||||
sessionCommand({
|
||||
id: "session.share",
|
||||
title: info()?.share?.url ? language.t("session.share.copy.copyLink") : language.t("command.session.share"),
|
||||
description: info()?.share?.url
|
||||
? language.t("toast.session.share.success.description")
|
||||
: language.t("command.session.share.description"),
|
||||
slash: "share",
|
||||
disabled: !params.id,
|
||||
onSelect: share,
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.unshare",
|
||||
title: language.t("command.session.unshare"),
|
||||
description: language.t("command.session.unshare.description"),
|
||||
slash: "unshare",
|
||||
disabled: !params.id || !info()?.share?.url,
|
||||
onSelect: unshare,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
const sessionCmds = () => [
|
||||
sessionCommand({
|
||||
id: "session.new",
|
||||
@@ -645,7 +534,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
|
||||
command.register("session", () => [
|
||||
...sessionCmds(),
|
||||
...shareCmds(),
|
||||
...fileCmds(),
|
||||
...contextCmds(),
|
||||
...viewCmds(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"description": "Effect-native confined code execution over schema-described tools",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -63,7 +63,7 @@ export const Info = Schema.Struct({
|
||||
}),
|
||||
autoupdate: Schema.optional(Schema.Union([Schema.Boolean, Schema.Literal("notify")])).annotate({
|
||||
description:
|
||||
"Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications",
|
||||
"Check for new versions and show an update notice. Updates are never installed automatically; run /update or `opencode upgrade`. Set to false to disable update checks. true and 'notify' behave the same",
|
||||
}),
|
||||
disabled_providers: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
|
||||
description: "Disable providers that are loaded automatically",
|
||||
|
||||
@@ -10,6 +10,17 @@ const InterleavedField = Schema.Union([
|
||||
Schema.String,
|
||||
])
|
||||
|
||||
// models.dev's `reasoning_options`: OpenCode builds the model's reasoning variants from them.
|
||||
const ReasoningOption = Schema.Union([
|
||||
Schema.Struct({ type: Schema.Literal("effort"), values: Schema.Array(Schema.NullOr(Schema.String)) }),
|
||||
Schema.Struct({ type: Schema.Literal("toggle") }),
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("budget_tokens"),
|
||||
min: Schema.optional(Schema.Finite),
|
||||
max: Schema.optional(Schema.Finite),
|
||||
}),
|
||||
])
|
||||
|
||||
export const Model = Schema.Struct({
|
||||
id: Schema.optional(Schema.String),
|
||||
name: Schema.optional(Schema.String),
|
||||
@@ -17,6 +28,11 @@ export const Model = Schema.Struct({
|
||||
release_date: Schema.optional(Schema.String),
|
||||
attachment: Schema.optional(Schema.Boolean),
|
||||
reasoning: Schema.optional(Schema.Boolean),
|
||||
reasoning_options: Schema.optional(
|
||||
Schema.Array(ReasoningOption).annotate({
|
||||
description: "Reasoning controls the model accepts, used to build its variants",
|
||||
}),
|
||||
),
|
||||
temperature: Schema.optional(Schema.Boolean),
|
||||
tool_call: Schema.optional(Schema.Boolean),
|
||||
interleaved: Schema.optional(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"name": "@opencode-ai/llm",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -19,7 +19,6 @@ import type {
|
||||
import { UI } from "../ui"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { SessionShare } from "@/share/session"
|
||||
import { Session } from "@/session/session"
|
||||
import type { SessionID } from "../../session/schema"
|
||||
import { MessageID, PartID } from "../../session/schema"
|
||||
@@ -380,7 +379,6 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
|
||||
if (!ctx) return yield* Effect.die("InstanceRef not provided")
|
||||
const gitSvc = yield* Git.Service
|
||||
const sessionSvc = yield* Session.Service
|
||||
const sessionShare = yield* SessionShare.Service
|
||||
const sessionPrompt = yield* SessionPrompt.Service
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const runLocalEffect = <A, E>(effect: Effect.Effect<A, E>) =>
|
||||
@@ -407,7 +405,6 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
|
||||
const { providerID, modelID } = normalizeModel()
|
||||
const variant = process.env["VARIANT"] || undefined
|
||||
const runId = normalizeRunId()
|
||||
const share = normalizeShare()
|
||||
const oidcBaseUrl = normalizeOidcBaseUrl()
|
||||
const { owner, repo } = context.repo
|
||||
// For repo events (schedule, workflow_dispatch), payload has no issue/comment data
|
||||
@@ -428,14 +425,12 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
|
||||
? (payload as IssueCommentEvent | IssuesEvent).issue.number
|
||||
: (payload as PullRequestEvent | PullRequestReviewCommentEvent).pull_request.number
|
||||
const runUrl = `/${owner}/${repo}/actions/runs/${runId}`
|
||||
const shareBaseUrl = isMock ? "https://dev.opencode.ai" : "https://opencode.ai"
|
||||
|
||||
let appToken: string
|
||||
let octoRest: Octokit
|
||||
let octoGraph: typeof graphql
|
||||
let gitConfig: string
|
||||
let session: { id: SessionID; title: string; version: string }
|
||||
let shareId: string | undefined
|
||||
let exitCode = 0
|
||||
let githubClientReady = false
|
||||
type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
|
||||
@@ -512,12 +507,6 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
|
||||
}),
|
||||
)
|
||||
await subscribeSessionEvents()
|
||||
shareId = await (async () => {
|
||||
if (share === false) return
|
||||
if (!share && repoData.data.private) return
|
||||
await runLocalEffect(sessionShare.share(session.id))
|
||||
return session.id.slice(-8)
|
||||
})()
|
||||
console.log("opencode session", session.id)
|
||||
|
||||
// Handle event types:
|
||||
@@ -547,7 +536,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
|
||||
repoData.data.default_branch,
|
||||
branch,
|
||||
summary,
|
||||
`${response}\n\nTriggered by ${triggerType}${footer({ image: true })}`,
|
||||
`${response}\n\nTriggered by ${triggerType}${footer()}`,
|
||||
)
|
||||
if (pr) {
|
||||
console.log(`Created PR #${pr}`)
|
||||
@@ -576,8 +565,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
|
||||
const summary = await summarize(response)
|
||||
await pushToLocalBranch(summary, uncommittedChanges)
|
||||
}
|
||||
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
|
||||
await createComment(`${response}${footer({ image: !hasShared })}`)
|
||||
await createComment(`${response}${footer()}`)
|
||||
await removeReaction(commentType)
|
||||
}
|
||||
// Fork PR
|
||||
@@ -594,8 +582,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
|
||||
const summary = await summarize(response)
|
||||
await pushToForkBranch(summary, prData, uncommittedChanges)
|
||||
}
|
||||
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
|
||||
await createComment(`${response}${footer({ image: !hasShared })}`)
|
||||
await createComment(`${response}${footer()}`)
|
||||
await removeReaction(commentType)
|
||||
}
|
||||
}
|
||||
@@ -610,7 +597,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
|
||||
if (switched) {
|
||||
// Agent switched branches (likely created its own branch/PR).
|
||||
// Don't push the stale infrastructure branch — just comment.
|
||||
await createComment(`${response}${footer({ image: true })}`)
|
||||
await createComment(`${response}${footer()}`)
|
||||
await removeReaction(commentType)
|
||||
} else if (dirty) {
|
||||
const summary = await summarize(response)
|
||||
@@ -619,16 +606,16 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
|
||||
repoData.data.default_branch,
|
||||
branch,
|
||||
summary,
|
||||
`${response}\n\nCloses #${issueId}${footer({ image: true })}`,
|
||||
`${response}\n\nCloses #${issueId}${footer()}`,
|
||||
)
|
||||
if (pr) {
|
||||
await createComment(`Created PR #${pr}${footer({ image: true })}`)
|
||||
await createComment(`Created PR #${pr}${footer()}`)
|
||||
} else {
|
||||
await createComment(`${response}${footer({ image: true })}`)
|
||||
await createComment(`${response}${footer()}`)
|
||||
}
|
||||
await removeReaction(commentType)
|
||||
} else {
|
||||
await createComment(`${response}${footer({ image: true })}`)
|
||||
await createComment(`${response}${footer()}`)
|
||||
await removeReaction(commentType)
|
||||
}
|
||||
}
|
||||
@@ -677,14 +664,6 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
|
||||
return value
|
||||
}
|
||||
|
||||
function normalizeShare() {
|
||||
const value = process.env["SHARE"]
|
||||
if (!value) return undefined
|
||||
if (value === "true") return true
|
||||
if (value === "false") return false
|
||||
throw new Error(`Invalid share value: ${value}. Share must be a boolean.`)
|
||||
}
|
||||
|
||||
function normalizeUseGithubToken() {
|
||||
const value = process.env["USE_GITHUB_TOKEN"]
|
||||
if (!value) return false
|
||||
@@ -1351,18 +1330,8 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
|
||||
}
|
||||
}
|
||||
|
||||
function footer(opts?: { image?: boolean }) {
|
||||
const image = (() => {
|
||||
if (!shareId) return ""
|
||||
if (!opts?.image) return ""
|
||||
|
||||
const titleAlt = encodeURIComponent(session.title.substring(0, 50))
|
||||
const title64 = Buffer.from(session.title.substring(0, 700), "utf8").toString("base64")
|
||||
|
||||
return `<a href="${shareBaseUrl}/s/${shareId}"><img width="200" alt="${titleAlt}" src="https://social-cards.sst.dev/opencode-share/${title64}.png?model=${providerID}/${modelID}&version=${session.version}&id=${shareId}" /></a>\n`
|
||||
})()
|
||||
const shareUrl = shareId ? `[opencode session](${shareBaseUrl}/s/${shareId}) | ` : ""
|
||||
return `\n\n${image}${shareUrl}[github run](${runUrl})`
|
||||
function footer() {
|
||||
return `\n\n[github run](${runUrl})`
|
||||
}
|
||||
|
||||
async function fetchRepo() {
|
||||
|
||||
@@ -2,11 +2,10 @@ import type { Session as SDKSession, Message, Part } from "@opencode-ai/sdk/v2"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
import { Session } from "@/session/session"
|
||||
import { MessageV2 } from "../../session/message-v2"
|
||||
import { CliError, effectCmd } from "../effect-cmd"
|
||||
import { CliError, effectCmd, fail } from "../effect-cmd"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { SessionTable, MessageTable, PartTable } from "@opencode-ai/core/session/sql"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
import { EOL } from "os"
|
||||
import path from "path"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
@@ -16,28 +15,6 @@ import type { InstanceContext } from "@/project/instance-context"
|
||||
const decodeMessageInfo = Schema.decodeUnknownSync(SessionV1.Info)
|
||||
const decodePart = Schema.decodeUnknownSync(SessionV1.Part)
|
||||
|
||||
/** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
|
||||
export type ShareData =
|
||||
| { type: "session"; data: SDKSession }
|
||||
| { type: "message"; data: Message }
|
||||
| { type: "part"; data: Part }
|
||||
| { type: "session_diff"; data: unknown }
|
||||
| { type: "model"; data: unknown }
|
||||
|
||||
/** Extract share ID from a share URL like https://opncd.ai/share/abc123 */
|
||||
export function parseShareUrl(url: string): string | null {
|
||||
const match = url.match(/^https?:\/\/[^/]+\/share\/([a-zA-Z0-9_-]+)$/)
|
||||
return match ? match[1] : null
|
||||
}
|
||||
|
||||
export function shouldAttachShareAuthHeaders(shareUrl: string, accountBaseUrl: string): boolean {
|
||||
try {
|
||||
return new URL(shareUrl).origin === new URL(accountBaseUrl).origin
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function formatImportFileError(file: string, error: FSUtil.Error) {
|
||||
if (error._tag === "PlatformError") {
|
||||
if (error.reason._tag === "NotFound") return `File not found: ${file}`
|
||||
@@ -49,54 +26,14 @@ export function formatImportFileError(file: string, error: FSUtil.Error) {
|
||||
return `Invalid JSON in ${file}: ${detail}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform ShareNext API response (flat array) into the nested structure for local file storage.
|
||||
*
|
||||
* The API returns a flat array: [session, message, message, part, part, ...]
|
||||
* Local storage expects: { info: session, messages: [{ info: message, parts: [part, ...] }, ...] }
|
||||
*
|
||||
* This groups parts by their messageID to reconstruct the hierarchy before writing to disk.
|
||||
*/
|
||||
export function transformShareData(shareData: ShareData[]): {
|
||||
info: SDKSession
|
||||
messages: Array<{ info: Message; parts: Part[] }>
|
||||
} | null {
|
||||
const sessionItem = shareData.find((d) => d.type === "session")
|
||||
if (!sessionItem) return null
|
||||
|
||||
const messageMap = new Map<string, Message>()
|
||||
const partMap = new Map<string, Part[]>()
|
||||
|
||||
for (const item of shareData) {
|
||||
if (item.type === "message") {
|
||||
messageMap.set(item.data.id, item.data)
|
||||
} else if (item.type === "part") {
|
||||
if (!partMap.has(item.data.messageID)) {
|
||||
partMap.set(item.data.messageID, [])
|
||||
}
|
||||
partMap.get(item.data.messageID)!.push(item.data)
|
||||
}
|
||||
}
|
||||
|
||||
if (messageMap.size === 0) return null
|
||||
|
||||
return {
|
||||
info: sessionItem.data,
|
||||
messages: Array.from(messageMap.values()).map((msg) => ({
|
||||
info: msg,
|
||||
parts: partMap.get(msg.id) ?? [],
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
type ExportData = { info: SDKSession; messages: Array<{ info: Message; parts: Part[] }> }
|
||||
|
||||
export const ImportCommand = effectCmd({
|
||||
command: "import <file>",
|
||||
describe: "import session data from JSON file or URL",
|
||||
describe: "import session data from JSON file",
|
||||
builder: (yargs) =>
|
||||
yargs.positional("file", {
|
||||
describe: "path to JSON file or share URL",
|
||||
describe: "path to JSON file",
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
}),
|
||||
@@ -108,67 +45,15 @@ export const ImportCommand = effectCmd({
|
||||
})
|
||||
|
||||
const runImport = Effect.fn("Cli.import.body")(function* (file: string, ctx: InstanceContext) {
|
||||
const share = yield* ShareNext.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const { db } = yield* Database.Service
|
||||
|
||||
let exportData: ExportData | undefined
|
||||
if (file.startsWith("http://") || file.startsWith("https://"))
|
||||
return yield* fail("Importing from share URLs is no longer supported: session sharing has been discontinued")
|
||||
|
||||
const isUrl = file.startsWith("http://") || file.startsWith("https://")
|
||||
|
||||
if (isUrl) {
|
||||
const slug = parseShareUrl(file)
|
||||
if (!slug) {
|
||||
const baseUrl = yield* Effect.orDie(share.url())
|
||||
process.stdout.write(`Invalid URL format. Expected: ${baseUrl}/share/<slug>`)
|
||||
process.stdout.write(EOL)
|
||||
return
|
||||
}
|
||||
|
||||
const baseUrl = new URL(file).origin
|
||||
const req = yield* Effect.orDie(share.request())
|
||||
const headers = shouldAttachShareAuthHeaders(file, req.baseUrl) ? req.headers : {}
|
||||
|
||||
const tryFetch = (url: string) =>
|
||||
Effect.tryPromise({
|
||||
try: () => fetch(url, { headers }),
|
||||
catch: (e) =>
|
||||
new CliError({
|
||||
message: `Failed to fetch share data: ${e instanceof Error ? e.message : String(e)}`,
|
||||
}),
|
||||
})
|
||||
|
||||
const dataPath = req.api.data(slug)
|
||||
let response = yield* tryFetch(`${baseUrl}${dataPath}`)
|
||||
|
||||
if (!response.ok && dataPath !== `/api/share/${slug}/data`) {
|
||||
response = yield* tryFetch(`${baseUrl}/api/share/${slug}/data`)
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
process.stdout.write(`Failed to fetch share data: ${response.statusText}`)
|
||||
process.stdout.write(EOL)
|
||||
return
|
||||
}
|
||||
|
||||
const shareData = yield* Effect.tryPromise({
|
||||
try: () => response.json() as Promise<ShareData[]>,
|
||||
catch: () => new CliError({ message: "Share data was not valid JSON" }),
|
||||
})
|
||||
const transformed = transformShareData(shareData)
|
||||
|
||||
if (!transformed) {
|
||||
process.stdout.write(`Share not found or empty: ${slug}`)
|
||||
process.stdout.write(EOL)
|
||||
return
|
||||
}
|
||||
|
||||
exportData = transformed
|
||||
} else {
|
||||
exportData = (yield* fs
|
||||
.readJson(file)
|
||||
.pipe(Effect.mapError((error) => new CliError({ message: formatImportFileError(file, error) })))) as ExportData
|
||||
}
|
||||
const exportData = (yield* fs
|
||||
.readJson(file)
|
||||
.pipe(Effect.mapError((error) => new CliError({ message: formatImportFileError(file, error) })))) as ExportData
|
||||
|
||||
if (!exportData) {
|
||||
process.stdout.write(`Failed to read session data`)
|
||||
|
||||
@@ -43,14 +43,12 @@ export const PrCommand = effectCmd({
|
||||
"view",
|
||||
`${prNumber}`,
|
||||
"--json",
|
||||
"headRepository,headRepositoryOwner,isCrossRepository,headRefName,body",
|
||||
"headRepository,headRepositoryOwner,isCrossRepository,headRefName",
|
||||
],
|
||||
{ nothrow: true },
|
||||
),
|
||||
)
|
||||
|
||||
let sessionId: string | undefined
|
||||
|
||||
if (prInfoResult.code === 0 && prInfoResult.text.trim()) {
|
||||
const prInfo = JSON.parse(prInfoResult.text)
|
||||
|
||||
@@ -71,26 +69,6 @@ export const PrCommand = effectCmd({
|
||||
cwd: worktree,
|
||||
})
|
||||
}
|
||||
|
||||
if (prInfo?.body) {
|
||||
const sessionMatch = prInfo.body.match(/https:\/\/opncd\.ai\/s\/([a-zA-Z0-9_-]+)/)
|
||||
if (sessionMatch) {
|
||||
const sessionUrl = sessionMatch[0]
|
||||
UI.println(`Found opencode session: ${sessionUrl}`)
|
||||
UI.println(`Importing session...`)
|
||||
|
||||
const importResult = yield* Effect.promise(() =>
|
||||
Process.text(["opencode", "import", sessionUrl], { nothrow: true }),
|
||||
)
|
||||
if (importResult.code === 0) {
|
||||
const sessionIdMatch = importResult.text.trim().match(/Imported session: ([a-zA-Z0-9_-]+)/)
|
||||
if (sessionIdMatch) {
|
||||
sessionId = sessionIdMatch[1]
|
||||
UI.println(`Session imported: ${sessionId}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UI.println(`Successfully checked out PR #${prNumber} as branch '${localBranchName}'`)
|
||||
@@ -98,10 +76,9 @@ export const PrCommand = effectCmd({
|
||||
UI.println("Starting opencode...")
|
||||
UI.println()
|
||||
|
||||
const opencodeArgs = sessionId ? ["-s", sessionId] : []
|
||||
const code = yield* Effect.promise(
|
||||
() =>
|
||||
Process.spawn(["opencode", ...opencodeArgs], {
|
||||
Process.spawn(["opencode"], {
|
||||
stdin: "inherit",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
|
||||
@@ -160,7 +160,8 @@ export const RunCommand = effectCmd({
|
||||
})
|
||||
.option("share", {
|
||||
type: "boolean",
|
||||
describe: "share the session",
|
||||
describe: "deprecated: session sharing was discontinued",
|
||||
hidden: true,
|
||||
})
|
||||
.option("model", {
|
||||
type: "string",
|
||||
@@ -532,19 +533,8 @@ export const RunCommand = effectCmd({
|
||||
}
|
||||
}
|
||||
|
||||
async function share(sdk: OpencodeClient, sessionID: string) {
|
||||
const cfg = await sdk.config.get()
|
||||
if (!cfg.data) return
|
||||
if (cfg.data.share !== "auto" && !flags.autoShare && !args.share) return
|
||||
const res = await sdk.session.share({ sessionID }).catch((error) => {
|
||||
if (error instanceof Error && error.message.includes("disabled")) {
|
||||
UI.println(UI.Style.TEXT_DANGER_BOLD + "! " + error.message)
|
||||
}
|
||||
return { error }
|
||||
})
|
||||
if (!res.error && "data" in res && res.data?.share?.url) {
|
||||
UI.println(UI.Style.TEXT_INFO_BOLD + "~ " + res.data.share.url)
|
||||
}
|
||||
async function share(_sdk: OpencodeClient, _sessionID: string) {
|
||||
if (args.share) UI.println(UI.Style.TEXT_DANGER_BOLD + "! Session sharing has been discontinued")
|
||||
}
|
||||
|
||||
async function createFreshSession(
|
||||
|
||||
@@ -10,6 +10,8 @@ import { withNetworkOptions, resolveNetworkOptionsNoConfig, hasArg } from "@/cli
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
|
||||
import type { EventSource } from "@opencode-ai/tui/context/sdk"
|
||||
import type { UpdateNotice, UpdateSource } from "@opencode-ai/tui"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { writeHeapSnapshot } from "v8"
|
||||
import { ServerAuth } from "@/server/auth"
|
||||
import { validateSession } from "../tui/validate-session"
|
||||
@@ -49,6 +51,43 @@ function createEventSource(client: RpcClient): EventSource {
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors the V2 CLI updater: the TUI process checks every 10 minutes and installs on this machine,
|
||||
// but only when the user picks an option in the /update dialog.
|
||||
function createUpdater(latest: () => Promise<UpdateNotice | undefined>): UpdateSource {
|
||||
const listeners = new Set<(notice: UpdateNotice) => void>()
|
||||
let current: UpdateNotice | undefined
|
||||
const check = () =>
|
||||
latest()
|
||||
.then((notice) => {
|
||||
if (!notice || JSON.stringify(notice) === JSON.stringify(current)) return
|
||||
current = notice
|
||||
listeners.forEach((notify) => notify(notice))
|
||||
})
|
||||
.catch(() => {})
|
||||
setTimeout(check, 1000).unref?.()
|
||||
setInterval(check, 10 * 60 * 1000).unref?.()
|
||||
return {
|
||||
subscribe: (notify) => {
|
||||
if (current) notify(current)
|
||||
listeners.add(notify)
|
||||
return () => listeners.delete(notify)
|
||||
},
|
||||
check: async () => {
|
||||
const { Installation } = await import("@/installation")
|
||||
// A failed check leaves latest unset, which /update reports as "Couldn't check for updates."
|
||||
const latest = await Installation.latest(await Installation.method()).catch(() => undefined)
|
||||
return { current: InstallationVersion, latest }
|
||||
},
|
||||
apply: async (target) => {
|
||||
const { Installation } = await import("@/installation")
|
||||
const method = await Installation.method()
|
||||
if (method === "unknown") throw new Error("Installation method not found")
|
||||
if (target.type === "major") return Installation.upgradeMajor(method)
|
||||
return Installation.upgrade(method, target.version)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function target() {
|
||||
if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
|
||||
const dist = new URL("./cli/tui/worker.js", import.meta.url)
|
||||
@@ -262,9 +301,7 @@ export const TuiThreadCommand = cmd({
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
client.call("checkUpgrade", { directory: cwd }).catch(() => {})
|
||||
}, 1000).unref?.()
|
||||
const updater = createUpdater(async () => await client.call("checkUpgrade", { directory: cwd }))
|
||||
|
||||
try {
|
||||
const { Effect } = await import("effect")
|
||||
@@ -284,6 +321,7 @@ export const TuiThreadCommand = cmd({
|
||||
fetch: transport.fetch,
|
||||
headers: transport.headers,
|
||||
events: transport.events,
|
||||
updater,
|
||||
args: {
|
||||
continue: args.continue,
|
||||
sessionID: args.session,
|
||||
|
||||
@@ -19,8 +19,12 @@ export const UpgradeCommand = {
|
||||
type: "string",
|
||||
choices: ["curl", "npm", "pnpm", "bun", "brew", "choco", "scoop"],
|
||||
})
|
||||
.option("major", {
|
||||
describe: "upgrade to OpenCode 2 and remove OpenCode 1",
|
||||
type: "boolean",
|
||||
})
|
||||
},
|
||||
handler: async (args: { target?: string; method?: string }) => {
|
||||
handler: async (args: { target?: string; method?: string; major?: boolean }) => {
|
||||
UI.empty()
|
||||
UI.println(UI.logo(" "))
|
||||
UI.empty()
|
||||
@@ -43,10 +47,36 @@ export const UpgradeCommand = {
|
||||
}
|
||||
}
|
||||
prompts.log.info("Using method: " + method)
|
||||
const target = args.target ? args.target.replace(/^v/, "") : await Installation.latest()
|
||||
|
||||
if (args.major) {
|
||||
prompts.log.info(`From ${InstallationVersion} → OpenCode 2`)
|
||||
const spinner = prompts.spinner()
|
||||
spinner.start("Installing OpenCode 2...")
|
||||
const err = await Installation.upgradeMajor(method).catch((err) => err)
|
||||
if (err) {
|
||||
spinner.stop("Upgrade failed", 1)
|
||||
if (err instanceof Error) prompts.log.error(err.message)
|
||||
prompts.log.info(`To install OpenCode 2 manually, see ${Installation.MAJOR_INSTRUCTIONS}`)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
spinner.stop("OpenCode 2 installed")
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
|
||||
const target = args.target
|
||||
? args.target.replace(/^v/, "")
|
||||
: await Installation.latest(method).catch(() => undefined)
|
||||
if (!target) {
|
||||
prompts.log.error("Couldn't check for updates")
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
|
||||
if (InstallationVersion === target) {
|
||||
prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`)
|
||||
prompts.log.info("OpenCode 2 is available. Upgrade with `opencode upgrade --major`")
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
@@ -69,6 +99,7 @@ export const UpgradeCommand = {
|
||||
return
|
||||
}
|
||||
spinner.stop("Upgrade complete")
|
||||
prompts.log.info("OpenCode 2 is available. Upgrade with `opencode upgrade --major`")
|
||||
prompts.outro("Done")
|
||||
},
|
||||
}
|
||||
@@ -58,7 +58,7 @@ export const rpc = {
|
||||
},
|
||||
async checkUpgrade(input: { directory: string }) {
|
||||
await InstanceRuntime.load({ directory: input.directory })
|
||||
await upgrade().catch(() => {})
|
||||
return upgrade().catch(() => undefined)
|
||||
},
|
||||
async reload() {
|
||||
await AppRuntime.runPromise(
|
||||
|
||||
@@ -3,51 +3,35 @@ import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Installation } from "@/installation"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
|
||||
// Checking never installs anything. Updates only happen when the user runs `/update` or `opencode upgrade`.
|
||||
export function shouldNotify(input: {
|
||||
autoupdate: boolean | "notify" | undefined
|
||||
disabled: boolean
|
||||
always: boolean
|
||||
current: string
|
||||
latest: string
|
||||
}) {
|
||||
if (input.autoupdate === false || input.disabled) return false
|
||||
if (input.always) return true
|
||||
return Installation.isUpdate(input.current, input.latest)
|
||||
}
|
||||
|
||||
// Returns undefined when checks are disabled or fail, so the TUI only suggests OpenCode 2 after a successful check.
|
||||
export async function upgrade() {
|
||||
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.getGlobal()))
|
||||
if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) return
|
||||
const method = await Installation.method()
|
||||
const latest = await Installation.latest(method).catch(() => {})
|
||||
const latest = await Installation.latest(await Installation.method()).catch(() => {})
|
||||
if (!latest) return
|
||||
|
||||
if (Flag.OPENCODE_ALWAYS_NOTIFY_UPDATE) {
|
||||
GlobalBus.emit("event", {
|
||||
directory: "global",
|
||||
payload: {
|
||||
type: Installation.Event.UpdateAvailable.type,
|
||||
properties: { version: latest },
|
||||
},
|
||||
if (
|
||||
shouldNotify({
|
||||
autoupdate: config.autoupdate,
|
||||
disabled: Flag.OPENCODE_DISABLE_AUTOUPDATE,
|
||||
always: Flag.OPENCODE_ALWAYS_NOTIFY_UPDATE,
|
||||
current: InstallationVersion,
|
||||
latest,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (InstallationVersion === latest) return
|
||||
|
||||
const kind = Installation.getReleaseType(InstallationVersion, latest)
|
||||
|
||||
if (config.autoupdate === "notify" || kind !== "patch") {
|
||||
GlobalBus.emit("event", {
|
||||
directory: "global",
|
||||
payload: {
|
||||
type: Installation.Event.UpdateAvailable.type,
|
||||
properties: { version: latest },
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (method === "unknown") return
|
||||
await Installation.upgrade(method, latest)
|
||||
.then(() =>
|
||||
GlobalBus.emit("event", {
|
||||
directory: "global",
|
||||
payload: {
|
||||
type: Installation.Event.Updated.type,
|
||||
properties: { version: latest },
|
||||
},
|
||||
}),
|
||||
)
|
||||
.catch(() => {})
|
||||
)
|
||||
return { type: "available" as const, version: latest }
|
||||
return { type: "major" as const }
|
||||
}
|
||||
@@ -44,7 +44,6 @@ import { Vcs } from "@/project/vcs"
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
import { Worktree } from "@/worktree"
|
||||
import { Installation } from "@/installation"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
import { SessionShare } from "@/share/session"
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import { memoMap } from "@opencode-ai/core/effect/memo-map"
|
||||
@@ -103,7 +102,6 @@ export const AppLayer = AppNodeBuilderV1.build(
|
||||
Workspace.node,
|
||||
Worktree.node,
|
||||
Installation.node,
|
||||
ShareNext.node,
|
||||
SessionShare.node,
|
||||
]),
|
||||
).pipe(Layer.provideMerge(AppNodeBuilderV1.build(Ripgrep.node)), Layer.provideMerge(Observability.layer))
|
||||
|
||||
@@ -5,7 +5,6 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
import { Format } from "@/format"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
import { Vcs } from "@/project/vcs"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { Config } from "@/config/config"
|
||||
@@ -13,7 +12,7 @@ import * as Observability from "@opencode-ai/core/observability"
|
||||
import { memoMap } from "@opencode-ai/core/effect/memo-map"
|
||||
|
||||
export const BootstrapLayer = AppNodeBuilder.build(
|
||||
LayerNode.group([Config.node, Plugin.node, ShareNext.node, Format.node, LSP.node, Vcs.node, Snapshot.node]),
|
||||
LayerNode.group([Config.node, Plugin.node, Format.node, LSP.node, Vcs.node, Snapshot.node]),
|
||||
).pipe(Layer.provide(Observability.layer))
|
||||
|
||||
export const BootstrapRuntime = ManagedRuntime.make(BootstrapLayer, { memoMap })
|
||||
@@ -60,25 +60,63 @@ export class UpgradeFailedError extends Schema.TaggedErrorClass<UpgradeFailedErr
|
||||
}
|
||||
}
|
||||
|
||||
// OpenCode 1 only updates within its own major version. OpenCode 2 is offered separately through
|
||||
// upgradeMajor, so package sources that already publish 2.x (such as Homebrew Core) must not be
|
||||
// reported as a normal update.
|
||||
export function isUpdate(current: string, latest: string) {
|
||||
if (!semver.valid(latest)) return false
|
||||
if (!semver.valid(current)) return semver.major(latest) === 1
|
||||
return semver.major(latest) === semver.major(current) && semver.gt(latest, current)
|
||||
}
|
||||
|
||||
// Every install method discovers OpenCode 1 releases from npm. opencode-ai only contains OpenCode 1;
|
||||
// OpenCode 2 ships as @opencode/cli. Preview builds follow their own dist-tag, source builds follow latest.
|
||||
const NPM_TAG = InstallationChannel === "local" ? "latest" : InstallationChannel
|
||||
|
||||
// Response schemas for external version APIs
|
||||
const GitHubRelease = Schema.Struct({ tag_name: Schema.String })
|
||||
const NpmPackage = Schema.Struct({ version: Schema.String })
|
||||
const BrewFormula = Schema.Struct({ versions: Schema.Struct({ stable: Schema.String }) })
|
||||
const BrewInfoV2 = Schema.Struct({
|
||||
formulae: Schema.Array(Schema.Struct({ versions: Schema.Struct({ stable: Schema.String }) })),
|
||||
})
|
||||
const ChocoPackage = Schema.Struct({
|
||||
d: Schema.Struct({ results: Schema.Array(Schema.Struct({ Version: Schema.String })) }),
|
||||
})
|
||||
const ScoopManifest = NpmPackage
|
||||
|
||||
export interface Interface {
|
||||
readonly info: () => Effect.Effect<Info>
|
||||
readonly method: () => Effect.Effect<Method>
|
||||
readonly latest: (method?: Method) => Effect.Effect<string>
|
||||
readonly upgrade: (method: Method, target: string) => Effect.Effect<void, UpgradeFailedError>
|
||||
readonly upgradeMajor: (method: Method) => Effect.Effect<void, UpgradeFailedError>
|
||||
}
|
||||
|
||||
// OpenCode 2 ships under different package names but the same `opencode` binary, so package
|
||||
// manager installs remove V1 first and restore it if the V2 install fails.
|
||||
const MAJOR_INSTALL: Partial<Record<Method, { remove: string[]; install: string[]; restore: string[] }>> = {
|
||||
npm: {
|
||||
remove: ["npm", "uninstall", "-g", "opencode-ai"],
|
||||
install: ["npm", "install", "-g", "@opencode/cli"],
|
||||
restore: ["npm", "install", "-g", `opencode-ai@${InstallationVersion}`],
|
||||
},
|
||||
pnpm: {
|
||||
remove: ["pnpm", "remove", "-g", "opencode-ai"],
|
||||
install: ["pnpm", "add", "-g", "--allow-build=@opencode/cli", "@opencode/cli"],
|
||||
restore: ["pnpm", "add", "-g", `opencode-ai@${InstallationVersion}`],
|
||||
},
|
||||
bun: {
|
||||
remove: ["bun", "remove", "-g", "opencode-ai"],
|
||||
install: ["bun", "install", "-g", "--trust", "@opencode/cli"],
|
||||
restore: ["bun", "install", "-g", `opencode-ai@${InstallationVersion}`],
|
||||
},
|
||||
yarn: {
|
||||
remove: ["yarn", "global", "remove", "opencode-ai"],
|
||||
install: ["yarn", "global", "add", "@opencode/cli"],
|
||||
restore: ["yarn", "global", "add", `opencode-ai@${InstallationVersion}`],
|
||||
},
|
||||
}
|
||||
|
||||
export const MAJOR_INSTRUCTIONS = "https://opencode.ai/v2/docs"
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Installation") {}
|
||||
|
||||
export const use = serviceUse(Service)
|
||||
@@ -164,6 +202,70 @@ const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProcess.Serv
|
||||
Effect.mapError(() => new UpgradeFailedError({ stderr: upgradeFailure("curl") })),
|
||||
)
|
||||
|
||||
const majorUpgrade = Effect.fnUntraced(function* (m: Method) {
|
||||
if (m === "curl") {
|
||||
// The V2 installer writes to the same ~/.opencode/bin/opencode path, replacing V1 in place.
|
||||
const response = yield* httpOk
|
||||
.execute(HttpClientRequest.get("https://opencode.ai/v2/install"))
|
||||
.pipe(Effect.mapError(() => new UpgradeFailedError({ stderr: upgradeFailure("curl") })))
|
||||
const body = yield* response.text.pipe(
|
||||
Effect.mapError(() => new UpgradeFailedError({ stderr: upgradeFailure("curl") })),
|
||||
)
|
||||
return yield* run([yield* upgradeScriptShell(), "-c", body])
|
||||
}
|
||||
|
||||
if (m === "brew") {
|
||||
const formula = yield* getBrewFormula()
|
||||
const env = { HOMEBREW_NO_AUTO_UPDATE: "1" }
|
||||
const removed = yield* run(["brew", "uninstall", formula], { env })
|
||||
if (removed.code !== 0) return removed
|
||||
const installed = yield* run(["brew", "install", "anomalyco/tap/opencode-v2"], { env })
|
||||
if (installed.code !== 0) yield* run(["brew", "install", formula], { env })
|
||||
return installed
|
||||
}
|
||||
|
||||
const steps = MAJOR_INSTALL[m]
|
||||
if (!steps)
|
||||
return yield* new UpgradeFailedError({
|
||||
stderr: `OpenCode 2 can't be installed automatically with ${m}. See ${MAJOR_INSTRUCTIONS}`,
|
||||
})
|
||||
const removed = yield* run(steps.remove)
|
||||
if (removed.code !== 0) return removed
|
||||
const installed = yield* run(steps.install)
|
||||
if (installed.code !== 0) yield* run(steps.restore)
|
||||
return installed
|
||||
})
|
||||
|
||||
const npmLatest = Effect.fnUntraced(function* () {
|
||||
const response = yield* httpOk.execute(
|
||||
HttpClientRequest.get(`${yield* NpmConfig.registry(process.cwd())}/opencode-ai/${NPM_TAG}`).pipe(
|
||||
HttpClientRequest.acceptJson,
|
||||
),
|
||||
)
|
||||
const data = yield* HttpClientResponse.schemaBodyJson(NpmPackage)(response)
|
||||
return data.version
|
||||
})
|
||||
|
||||
// Chocolatey can only install versions its moderated community feed has published.
|
||||
const chocoHas = Effect.fnUntraced(function* (version: string) {
|
||||
const response = yield* httpOk.execute(
|
||||
HttpClientRequest.get(
|
||||
`https://community.chocolatey.org/api/v2/Packages?$filter=Id%20eq%20%27opencode%27%20and%20Version%20eq%20%27${encodeURIComponent(version)}%27&$select=Version`,
|
||||
).pipe(HttpClientRequest.setHeaders({ Accept: "application/json;odata=verbose" })),
|
||||
)
|
||||
const data = yield* HttpClientResponse.schemaBodyJson(ChocoPackage)(response)
|
||||
return data.d.results.length > 0
|
||||
})
|
||||
|
||||
const tapVersion = Effect.fnUntraced(
|
||||
function* (formula: string) {
|
||||
const infoJson = yield* text(["brew", "info", "--json=v2", formula])
|
||||
const info = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(BrewInfoV2))(infoJson)
|
||||
return info.formulae[0].versions.stable
|
||||
},
|
||||
Effect.orElseSucceed(() => undefined),
|
||||
)
|
||||
|
||||
const result: Interface = {
|
||||
info: Effect.fn("Installation.info")(function* () {
|
||||
return {
|
||||
@@ -205,64 +307,24 @@ const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProcess.Serv
|
||||
|
||||
return "unknown" as Method
|
||||
}),
|
||||
// Reports the installed version when there's no OpenCode 1 update this install method can deliver,
|
||||
// so callers offer OpenCode 2 through the major upgrade instead.
|
||||
latest: Effect.fn("Installation.latest")(function* (installMethod?: Method) {
|
||||
const detectedMethod = installMethod || (yield* result.method())
|
||||
|
||||
if (detectedMethod === "brew") {
|
||||
const formula = yield* getBrewFormula()
|
||||
if (formula.includes("/")) {
|
||||
const infoJson = yield* text(["brew", "info", "--json=v2", formula])
|
||||
const info = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(BrewInfoV2))(infoJson)
|
||||
return info.formulae[0].versions.stable
|
||||
}
|
||||
const response = yield* httpOk.execute(
|
||||
HttpClientRequest.get("https://formulae.brew.sh/api/formula/opencode.json").pipe(
|
||||
HttpClientRequest.acceptJson,
|
||||
),
|
||||
)
|
||||
const data = yield* HttpClientResponse.schemaBodyJson(BrewFormula)(response)
|
||||
return data.versions.stable
|
||||
}
|
||||
|
||||
if (detectedMethod === "npm" || detectedMethod === "bun" || detectedMethod === "pnpm") {
|
||||
const response = yield* httpOk.execute(
|
||||
HttpClientRequest.get(
|
||||
`${yield* NpmConfig.registry(process.cwd())}/opencode-ai/${InstallationChannel}`,
|
||||
).pipe(HttpClientRequest.acceptJson),
|
||||
)
|
||||
const data = yield* HttpClientResponse.schemaBodyJson(NpmPackage)(response)
|
||||
return data.version
|
||||
}
|
||||
|
||||
if (detectedMethod === "choco") {
|
||||
const response = yield* httpOk.execute(
|
||||
HttpClientRequest.get(
|
||||
"https://community.chocolatey.org/api/v2/Packages?$filter=Id%20eq%20%27opencode%27%20and%20IsLatestVersion&$select=Version",
|
||||
).pipe(HttpClientRequest.setHeaders({ Accept: "application/json;odata=verbose" })),
|
||||
)
|
||||
const data = yield* HttpClientResponse.schemaBodyJson(ChocoPackage)(response)
|
||||
return data.d.results[0].Version
|
||||
}
|
||||
|
||||
if (detectedMethod === "scoop") {
|
||||
const response = yield* httpOk.execute(
|
||||
HttpClientRequest.get(
|
||||
"https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/opencode.json",
|
||||
).pipe(HttpClientRequest.setHeaders({ Accept: "application/json" })),
|
||||
)
|
||||
const data = yield* HttpClientResponse.schemaBodyJson(ScoopManifest)(response)
|
||||
return data.version
|
||||
}
|
||||
|
||||
const response = yield* httpOk.execute(
|
||||
HttpClientRequest.get("https://api.github.com/repos/anomalyco/opencode/releases/latest").pipe(
|
||||
HttpClientRequest.acceptJson,
|
||||
),
|
||||
)
|
||||
const data = yield* HttpClientResponse.schemaBodyJson(GitHubRelease)(response)
|
||||
return data.tag_name.replace(/^v/, "")
|
||||
const method = installMethod || (yield* result.method())
|
||||
// Homebrew Core's opencode formula is OpenCode 2, so `brew upgrade` can't deliver an OpenCode 1 update.
|
||||
if (method === "brew" && !(yield* getBrewFormula()).includes("/")) return InstallationVersion
|
||||
return yield* Effect.gen(function* () {
|
||||
const found = yield* npmLatest()
|
||||
if (!isUpdate(InstallationVersion, found)) return InstallationVersion
|
||||
if (method === "choco" && !(yield* chocoHas(found))) return InstallationVersion
|
||||
return found
|
||||
}).pipe(Effect.timeout("10 seconds"))
|
||||
}, Effect.orDie),
|
||||
upgrade: Effect.fn("Installation.upgrade")(function* (m: Method, target: string) {
|
||||
if (semver.valid(target) && semver.major(target) >= 2)
|
||||
return yield* new UpgradeFailedError({
|
||||
stderr: `OpenCode ${target} is OpenCode 2. Upgrade with \`opencode upgrade --major\` or see ${MAJOR_INSTRUCTIONS}`,
|
||||
})
|
||||
let upgradeResult: { code: number; stdout: string; stderr: string } | undefined
|
||||
switch (m) {
|
||||
case "curl":
|
||||
@@ -277,25 +339,36 @@ const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProcess.Serv
|
||||
case "bun":
|
||||
upgradeResult = yield* run(["bun", "install", "-g", `opencode-ai@${target}`])
|
||||
break
|
||||
case "yarn":
|
||||
upgradeResult = yield* run(["yarn", "global", "add", `opencode-ai@${target}`])
|
||||
break
|
||||
case "brew": {
|
||||
const formula = yield* getBrewFormula()
|
||||
const env = { HOMEBREW_NO_AUTO_UPDATE: "1" }
|
||||
if (formula.includes("/")) {
|
||||
const tap = yield* run(["brew", "tap", "anomalyco/tap"], { env })
|
||||
if (tap.code !== 0) {
|
||||
upgradeResult = tap
|
||||
if (!formula.includes("/"))
|
||||
return yield* new UpgradeFailedError({
|
||||
stderr: `Homebrew Core's opencode formula is OpenCode 2, so Homebrew can't install OpenCode ${target}. Upgrade with \`opencode upgrade --major\`, or reinstall from anomalyco/tap/opencode to stay on OpenCode 1.`,
|
||||
})
|
||||
const tap = yield* run(["brew", "tap", "anomalyco/tap"], { env })
|
||||
if (tap.code !== 0) {
|
||||
upgradeResult = tap
|
||||
break
|
||||
}
|
||||
const repo = yield* text(["brew", "--repo", "anomalyco/tap"])
|
||||
const dir = repo.trim()
|
||||
if (dir) {
|
||||
const pull = yield* run(["git", "pull", "--ff-only"], { cwd: dir, env })
|
||||
if (pull.code !== 0) {
|
||||
upgradeResult = pull
|
||||
break
|
||||
}
|
||||
const repo = yield* text(["brew", "--repo", "anomalyco/tap"])
|
||||
const dir = repo.trim()
|
||||
if (dir) {
|
||||
const pull = yield* run(["git", "pull", "--ff-only"], { cwd: dir, env })
|
||||
if (pull.code !== 0) {
|
||||
upgradeResult = pull
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// The tap is updated shortly after npm, so a just-published release may not be installable yet.
|
||||
const available = yield* tapVersion(formula)
|
||||
if (!available || semver.major(available) !== semver.major(target) || semver.lt(available, target))
|
||||
return yield* new UpgradeFailedError({
|
||||
stderr: `Homebrew can't install OpenCode ${target} yet: ${formula} is at ${available ?? "an unknown version"}. Try again in a few minutes.`,
|
||||
})
|
||||
upgradeResult = yield* run(["brew", "upgrade", formula], { env })
|
||||
break
|
||||
}
|
||||
@@ -319,6 +392,11 @@ const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProcess.Serv
|
||||
})
|
||||
yield* text([process.execPath, "--version"])
|
||||
}),
|
||||
upgradeMajor: Effect.fn("Installation.upgradeMajor")(function* (m: Method) {
|
||||
const outcome = yield* majorUpgrade(m)
|
||||
if (outcome.code !== 0) return yield* new UpgradeFailedError({ stderr: upgradeFailure(m, outcome) })
|
||||
yield* Effect.logInfo("upgraded major", { method: m, stdout: outcome.stdout, stderr: outcome.stderr })
|
||||
}),
|
||||
}
|
||||
|
||||
return Service.of(result)
|
||||
@@ -332,5 +410,7 @@ const { runPromise } = makeRuntime(Service, AppNodeBuilder.build(node))
|
||||
export const latest = (...args: Parameters<Interface["latest"]>) => runPromise((s) => s.latest(...args))
|
||||
export const method = () => runPromise((s) => s.method())
|
||||
export const upgrade = (...args: Parameters<Interface["upgrade"]>) => runPromise((s) => s.upgrade(...args))
|
||||
export const upgradeMajor = (...args: Parameters<Interface["upgradeMajor"]>) =>
|
||||
runPromise((s) => s.upgradeMajor(...args))
|
||||
|
||||
export * as Installation from "."
|
||||
@@ -6,7 +6,6 @@ import { Snapshot } from "../snapshot"
|
||||
import * as Project from "./project"
|
||||
import * as Vcs from "./vcs"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Config } from "@/config/config"
|
||||
import { Service } from "./bootstrap-service"
|
||||
@@ -25,7 +24,6 @@ const layer = Layer.effect(
|
||||
const lsp = yield* LSP.Service
|
||||
const plugin = yield* Plugin.Service
|
||||
const project = yield* Project.Service
|
||||
const shareNext = yield* ShareNext.Service
|
||||
const snapshot = yield* Snapshot.Service
|
||||
const vcs = yield* Vcs.Service
|
||||
|
||||
@@ -39,7 +37,7 @@ const layer = Layer.effect(
|
||||
// Each service self-manages its own slow work via Effect.forkScoped against
|
||||
// its per-instance state scope. We just await materialization here.
|
||||
yield* Effect.forEach(
|
||||
[lsp, shareNext, format, vcs, snapshot, project],
|
||||
[lsp, format, vcs, snapshot, project],
|
||||
(s) => s.init().pipe(Effect.catchCause((cause) => Effect.logWarning("init failed", { cause }))),
|
||||
{ concurrency: "unbounded", discard: true },
|
||||
).pipe(Effect.withSpan("InstanceBootstrap.init"))
|
||||
@@ -52,7 +50,7 @@ const layer = Layer.effect(
|
||||
export const node = makeGlobalNode({
|
||||
service: Service,
|
||||
layer: layer,
|
||||
deps: [Config.node, Format.node, LSP.node, Plugin.node, Project.node, ShareNext.node, Snapshot.node, Vcs.node],
|
||||
deps: [Config.node, Format.node, LSP.node, Plugin.node, Project.node, Snapshot.node, Vcs.node],
|
||||
})
|
||||
|
||||
export * as InstanceBootstrap from "./bootstrap"
|
||||
@@ -1616,10 +1616,13 @@ const layer = Layer.effect(
|
||||
release_date: model.release_date ?? existingModel?.release_date ?? "",
|
||||
variants: {},
|
||||
}
|
||||
// Configured reasoning options replace the catalog variants; configured variants still apply on top.
|
||||
const variants =
|
||||
existingModel?.api.npm === parsedModel.api.npm
|
||||
? (existingModel.variants ?? ProviderTransform.variants(parsedModel))
|
||||
: ProviderTransform.variants(parsedModel)
|
||||
model.reasoning_options !== undefined
|
||||
? (ProviderTransform.reasoningVariants(model, parsedModel) ?? ProviderTransform.variants(parsedModel))
|
||||
: existingModel?.api.npm === parsedModel.api.npm
|
||||
? (existingModel.variants ?? ProviderTransform.variants(parsedModel))
|
||||
: ProviderTransform.variants(parsedModel)
|
||||
const merged = mergeDeep(variants, model.variants ?? {})
|
||||
parsedModel.variants = mapValues(
|
||||
pickBy(merged, (v) => !v.disabled),
|
||||
|
||||
@@ -1714,7 +1714,10 @@ export function schema(model: Provider.Model, schema: JSONSchema7): JSONSchema7
|
||||
return schema
|
||||
}
|
||||
|
||||
export function reasoningVariants(model: ModelsDev.Model, target: Provider.Model): Provider.Model["variants"] {
|
||||
export function reasoningVariants(
|
||||
model: Pick<ModelsDev.Model, "reasoning_options">,
|
||||
target: Provider.Model,
|
||||
): Provider.Model["variants"] {
|
||||
const options = model.reasoning_options
|
||||
if (options === undefined) return
|
||||
if (options.length === 0) return {}
|
||||
|
||||
@@ -184,6 +184,12 @@ export const ptyConnectHandlers = HttpApiBuilder.group(PtyConnectApi, "pty-conne
|
||||
params: { ptyID: PtyID }
|
||||
request: HttpServerRequest.HttpServerRequest
|
||||
}) {
|
||||
if (!validOrigin(ctx.request, cors)) return HttpServerResponse.empty({ status: 403 })
|
||||
|
||||
const ticket = new URL(ctx.request.url, "http://localhost").searchParams.get(PTY_CONNECT_TICKET_QUERY)
|
||||
if (ticket && !(yield* tickets.consume({ ticket, ptyID: ctx.params.ptyID, ...(yield* ticketScope) })))
|
||||
return HttpServerResponse.empty({ status: 403 })
|
||||
|
||||
const exists = yield* pty(Pty.Service.use((service) => service.get(ctx.params.ptyID))).pipe(
|
||||
Effect.map((info) => info.status === "running"),
|
||||
Effect.catchTag("Pty.NotFoundError", () => Effect.succeed(false)),
|
||||
@@ -192,13 +198,6 @@ export const ptyConnectHandlers = HttpApiBuilder.group(PtyConnectApi, "pty-conne
|
||||
|
||||
const query = Schema.decodeUnknownOption(CursorQuery)(yield* HttpServerRequest.ParsedSearchParams)
|
||||
if (Option.isNone(query)) return HttpServerResponse.empty({ status: 400 })
|
||||
const ticket = new URL(ctx.request.url, "http://localhost").searchParams.get(PTY_CONNECT_TICKET_QUERY)
|
||||
if (ticket) {
|
||||
const valid = validOrigin(ctx.request, cors)
|
||||
? yield* tickets.consume({ ticket, ptyID: ctx.params.ptyID, ...(yield* ticketScope) })
|
||||
: false
|
||||
if (!valid) return HttpServerResponse.empty({ status: 403 })
|
||||
}
|
||||
const parsedCursor = query.value.cursor === undefined ? undefined : Number(query.value.cursor)
|
||||
const cursor =
|
||||
parsedCursor !== undefined && Number.isSafeInteger(parsedCursor) && parsedCursor >= -1
|
||||
|
||||
@@ -40,7 +40,6 @@ import { SessionStatus } from "@/session/status"
|
||||
import { SessionSummary } from "@/session/summary"
|
||||
import { Todo } from "@/session/todo"
|
||||
import { SessionShare } from "@/share/session"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
import { Skill } from "@/skill"
|
||||
import { Discovery } from "@/skill/discovery"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
@@ -258,7 +257,6 @@ const app = LayerNode.group([
|
||||
Workspace.node,
|
||||
Worktree.node,
|
||||
Installation.node,
|
||||
ShareNext.node,
|
||||
SessionShare.node,
|
||||
InstanceStore.node,
|
||||
httpClient,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Session } from "@/session/session"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { Effect, Layer, Scope, Context } from "effect"
|
||||
import { Config } from "@/config/config"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { ShareNext } from "./share-next"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
|
||||
export interface Interface {
|
||||
readonly create: (input?: Session.CreateInput) => Effect.Effect<Session.Info>
|
||||
@@ -17,32 +14,20 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Se
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const cfg = yield* Config.Service
|
||||
const session = yield* Session.Service
|
||||
const shareNext = yield* ShareNext.Service
|
||||
const scope = yield* Scope.Scope
|
||||
const flags = yield* RuntimeFlags.Service
|
||||
|
||||
const share = Effect.fn("SessionShare.share")(function* (sessionID: SessionID) {
|
||||
const conf = yield* cfg.get()
|
||||
if (conf.share === "disabled") throw new Error("Sharing is disabled in configuration")
|
||||
const result = yield* shareNext.create(sessionID)
|
||||
yield* session.setShare({ sessionID, share: { url: result.url } })
|
||||
return result
|
||||
// Session sharing was discontinued. The share endpoints stay for API compatibility: share fails and unshare only
|
||||
// clears the local link.
|
||||
const share = Effect.fn("SessionShare.share")(function* (_sessionID: SessionID) {
|
||||
return yield* Effect.fail(new Error("Session sharing has been discontinued"))
|
||||
})
|
||||
|
||||
const unshare = Effect.fn("SessionShare.unshare")(function* (sessionID: SessionID) {
|
||||
yield* shareNext.remove(sessionID)
|
||||
yield* session.setShare({ sessionID, share: undefined })
|
||||
})
|
||||
|
||||
const create = Effect.fn("SessionShare.create")(function* (input?: Session.CreateInput) {
|
||||
const result = yield* session.create(input)
|
||||
if (result.parentID) return result
|
||||
const conf = yield* cfg.get()
|
||||
if (!(flags.autoShare || conf.share === "auto")) return result
|
||||
yield* share(result.id).pipe(Effect.ignore, Effect.forkIn(scope))
|
||||
return result
|
||||
return yield* session.create(input)
|
||||
})
|
||||
|
||||
return Service.of({ create, share, unshare })
|
||||
@@ -52,7 +37,7 @@ const layer = Layer.effect(
|
||||
export const node = LayerNode.make({
|
||||
service: Service,
|
||||
layer: layer,
|
||||
deps: [Config.node, Session.node, ShareNext.node, RuntimeFlags.node],
|
||||
deps: [Session.node],
|
||||
})
|
||||
|
||||
export * as SessionShare from "./session"
|
||||
@@ -1,371 +0,0 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
|
||||
import type * as SDK from "@opencode-ai/sdk/v2"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { Effect, Exit, Layer, Option, Schema, Scope, Context, Stream } from "effect"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import { Account } from "@/account/account"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { Provider } from "@/provider/provider"
|
||||
|
||||
import { Session } from "@/session/session"
|
||||
import { MessageV2 } from "@/session/message-v2"
|
||||
import type { SessionID } from "@/session/schema"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Config } from "@/config/config"
|
||||
import { SessionShareTable } from "@opencode-ai/core/share/sql"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
|
||||
const disabled = process.env["OPENCODE_DISABLE_SHARE"] === "true" || process.env["OPENCODE_DISABLE_SHARE"] === "1"
|
||||
|
||||
export type Api = {
|
||||
create: string
|
||||
sync: (shareID: string) => string
|
||||
remove: (shareID: string) => string
|
||||
data: (shareID: string) => string
|
||||
}
|
||||
|
||||
export type Req = {
|
||||
headers: Record<string, string>
|
||||
api: Api
|
||||
baseUrl: string
|
||||
}
|
||||
|
||||
const ShareSchema = Schema.Struct({
|
||||
id: Schema.String,
|
||||
url: Schema.String,
|
||||
secret: Schema.String,
|
||||
})
|
||||
export type Share = typeof ShareSchema.Type
|
||||
|
||||
type State = {
|
||||
queue: Map<SessionID, Map<string, Data>>
|
||||
scope: Scope.Closeable
|
||||
shared: Map<SessionID, Share | null>
|
||||
}
|
||||
|
||||
type Data =
|
||||
| {
|
||||
type: "session"
|
||||
data: SDK.Session
|
||||
}
|
||||
| {
|
||||
type: "message"
|
||||
data: SDK.Message
|
||||
}
|
||||
| {
|
||||
type: "part"
|
||||
data: SDK.Part
|
||||
}
|
||||
| {
|
||||
type: "session_diff"
|
||||
data: SDK.SnapshotFileDiff[]
|
||||
}
|
||||
| {
|
||||
type: "model"
|
||||
data: SDK.Model[]
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly init: () => Effect.Effect<void, unknown>
|
||||
readonly url: () => Effect.Effect<string, unknown>
|
||||
readonly request: () => Effect.Effect<Req, unknown>
|
||||
readonly create: (sessionID: SessionID) => Effect.Effect<Share, unknown>
|
||||
readonly remove: (sessionID: SessionID) => Effect.Effect<void, unknown>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ShareNext") {}
|
||||
|
||||
export const use = serviceUse(Service)
|
||||
|
||||
function api(resource: string): Api {
|
||||
return {
|
||||
create: `/api/${resource}`,
|
||||
sync: (shareID) => `/api/${resource}/${shareID}/sync`,
|
||||
remove: (shareID) => `/api/${resource}/${shareID}`,
|
||||
data: (shareID) => `/api/${resource}/${shareID}/data`,
|
||||
}
|
||||
}
|
||||
|
||||
const legacyApi = api("share")
|
||||
const consoleApi = api("shares")
|
||||
|
||||
function key(item: Data) {
|
||||
switch (item.type) {
|
||||
case "session":
|
||||
return "session"
|
||||
case "message":
|
||||
return `message/${item.data.id}`
|
||||
case "part":
|
||||
return `part/${item.data.messageID}/${item.data.id}`
|
||||
case "session_diff":
|
||||
return "session_diff"
|
||||
case "model":
|
||||
return "model"
|
||||
}
|
||||
}
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const account = yield* Account.Service
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const cfg = yield* Config.Service
|
||||
const { db } = yield* Database.Service
|
||||
const http = yield* HttpClient.HttpClient
|
||||
const httpOk = HttpClient.filterStatusOk(http)
|
||||
const provider = yield* Provider.Service
|
||||
const session = yield* Session.Service
|
||||
|
||||
function sync(sessionID: SessionID, data: Data[]) {
|
||||
return Effect.gen(function* () {
|
||||
if (disabled) return
|
||||
const share = yield* getCached(sessionID)
|
||||
if (!share) return
|
||||
|
||||
const s = yield* InstanceState.get(state)
|
||||
const existing = s.queue.get(sessionID)
|
||||
if (existing) {
|
||||
for (const item of data) {
|
||||
existing.set(key(item), item)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const next = new Map(data.map((item) => [key(item), item]))
|
||||
s.queue.set(sessionID, next)
|
||||
yield* flush(sessionID).pipe(
|
||||
Effect.delay(1000),
|
||||
Effect.catchCause((cause) => Effect.logError("share flush failed", { sessionID: sessionID, cause: cause })),
|
||||
Effect.forkIn(s.scope),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const state: InstanceState.InstanceState<State> = yield* InstanceState.make<State>(
|
||||
Effect.fn("ShareNext.state")(function* (_ctx) {
|
||||
const cache: State = { queue: new Map(), scope: yield* Scope.make(), shared: new Map() }
|
||||
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Scope.close(cache.scope, Exit.void).pipe(
|
||||
Effect.andThen(
|
||||
Effect.sync(() => {
|
||||
cache.queue.clear()
|
||||
cache.shared.clear()
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if (disabled) return cache
|
||||
|
||||
const watch = <D extends EventV2.Definition>(
|
||||
def: D,
|
||||
fn: (data: EventV2.Data<D>) => Effect.Effect<void, unknown>,
|
||||
) =>
|
||||
events.listen((event) => {
|
||||
if (event.type !== def.type || event.location?.directory !== _ctx.directory) return Effect.void
|
||||
return fn(event.data as EventV2.Data<D>).pipe(
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.logError("share subscriber failed", { type: def.type, cause: cause }),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
yield* watch(Session.Event.Updated, (data) =>
|
||||
Effect.gen(function* () {
|
||||
const info = data.info
|
||||
yield* sync(info.id, [{ type: "session", data: structuredClone(info) as SDK.Session }])
|
||||
}),
|
||||
)
|
||||
yield* watch(MessageV2.Event.Updated, (data) =>
|
||||
Effect.gen(function* () {
|
||||
const info = data.info
|
||||
yield* sync(info.sessionID, [{ type: "message", data: structuredClone(info) as SDK.Message }])
|
||||
if (info.role !== "user") return
|
||||
const model = yield* provider.getModel(info.model.providerID, info.model.modelID)
|
||||
yield* sync(info.sessionID, [{ type: "model", data: [model] }])
|
||||
}),
|
||||
)
|
||||
yield* watch(MessageV2.Event.PartUpdated, (data) =>
|
||||
sync(data.part.sessionID, [{ type: "part", data: structuredClone(data.part) as SDK.Part }]),
|
||||
)
|
||||
yield* watch(Session.Event.Diff, (data) =>
|
||||
sync(data.sessionID, [{ type: "session_diff", data: structuredClone(data.diff) as SDK.SnapshotFileDiff[] }]),
|
||||
)
|
||||
yield* watch(Session.Event.Deleted, (data) => remove(data.sessionID))
|
||||
|
||||
return cache
|
||||
}),
|
||||
)
|
||||
|
||||
const request = Effect.fn("ShareNext.request")(function* () {
|
||||
const headers: Record<string, string> = {}
|
||||
const active = yield* account.active()
|
||||
if (Option.isNone(active) || !active.value.active_org_id) {
|
||||
const baseUrl = (yield* cfg.get()).enterprise?.url ?? "https://opncd.ai"
|
||||
return { headers, api: legacyApi, baseUrl } satisfies Req
|
||||
}
|
||||
|
||||
const token = yield* account.token(active.value.id)
|
||||
if (Option.isNone(token)) {
|
||||
throw new Error("No active account token available for sharing")
|
||||
}
|
||||
|
||||
headers.authorization = `Bearer ${token.value}`
|
||||
headers["x-org-id"] = active.value.active_org_id
|
||||
return { headers, api: consoleApi, baseUrl: active.value.url } satisfies Req
|
||||
})
|
||||
|
||||
const get = Effect.fnUntraced(function* (sessionID: SessionID) {
|
||||
const row = yield* db
|
||||
.select()
|
||||
.from(SessionShareTable)
|
||||
.where(eq(SessionShareTable.session_id, sessionID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!row) return
|
||||
return { id: row.id, secret: row.secret, url: row.url } satisfies Share
|
||||
})
|
||||
|
||||
const getCached = Effect.fnUntraced(function* (sessionID: SessionID) {
|
||||
const s = yield* InstanceState.get(state)
|
||||
if (s.shared.has(sessionID)) {
|
||||
const cached = s.shared.get(sessionID)
|
||||
return cached === null ? undefined : cached
|
||||
}
|
||||
|
||||
const share = yield* get(sessionID)
|
||||
s.shared.set(sessionID, share ?? null)
|
||||
return share
|
||||
})
|
||||
|
||||
const flush = Effect.fn("ShareNext.flush")(function* (sessionID: SessionID) {
|
||||
if (disabled) return
|
||||
const s = yield* InstanceState.get(state)
|
||||
const queued = s.queue.get(sessionID)
|
||||
if (!queued) return
|
||||
|
||||
s.queue.delete(sessionID)
|
||||
|
||||
const share = yield* getCached(sessionID)
|
||||
if (!share) return
|
||||
|
||||
const req = yield* request()
|
||||
const res = yield* HttpClientRequest.post(`${req.baseUrl}${req.api.sync(share.id)}`).pipe(
|
||||
HttpClientRequest.setHeaders(req.headers),
|
||||
HttpClientRequest.bodyJson({ secret: share.secret, data: Array.from(queued.values()) }),
|
||||
Effect.flatMap((r) => http.execute(r)),
|
||||
)
|
||||
|
||||
if (res.status >= 400) {
|
||||
yield* Effect.logWarning("failed to sync share", {
|
||||
sessionID: sessionID,
|
||||
shareID: share.id,
|
||||
status: res.status,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const full = Effect.fn("ShareNext.full")(function* (sessionID: SessionID) {
|
||||
yield* Effect.logInfo("full sync", { sessionID: sessionID })
|
||||
const info = yield* session.get(sessionID)
|
||||
const diffs = yield* session.diff(sessionID)
|
||||
const messages = yield* session.messages({ sessionID })
|
||||
const models = yield* Effect.forEach(
|
||||
Array.from(
|
||||
new Map(
|
||||
messages
|
||||
.filter((msg) => msg.info.role === "user")
|
||||
.map((msg) => (msg.info as SDK.UserMessage).model)
|
||||
.map((item) => [`${item.providerID}/${item.modelID}`, item] as const),
|
||||
).values(),
|
||||
),
|
||||
(item) => provider.getModel(ProviderV2.ID.make(item.providerID), ModelV2.ID.make(item.modelID)),
|
||||
{ concurrency: 8 },
|
||||
)
|
||||
|
||||
yield* sync(sessionID, [
|
||||
{ type: "session", data: info },
|
||||
...messages.map((item) => ({ type: "message" as const, data: item.info })),
|
||||
...messages.flatMap((item) => item.parts.map((part) => ({ type: "part" as const, data: part }))),
|
||||
{ type: "session_diff", data: diffs },
|
||||
{ type: "model", data: models },
|
||||
])
|
||||
})
|
||||
|
||||
const init = Effect.fn("ShareNext.init")(function* () {
|
||||
if (disabled) return
|
||||
yield* InstanceState.get(state)
|
||||
})
|
||||
|
||||
const url = Effect.fn("ShareNext.url")(function* () {
|
||||
return (yield* request()).baseUrl
|
||||
})
|
||||
|
||||
const create = Effect.fn("ShareNext.create")(function* (sessionID: SessionID) {
|
||||
if (disabled) return { id: "", url: "", secret: "" }
|
||||
yield* Effect.logInfo("creating share", { sessionID: sessionID })
|
||||
const req = yield* request()
|
||||
const result = yield* HttpClientRequest.post(`${req.baseUrl}${req.api.create}`).pipe(
|
||||
HttpClientRequest.setHeaders(req.headers),
|
||||
HttpClientRequest.bodyJson({ sessionID }),
|
||||
Effect.flatMap((r) => httpOk.execute(r)),
|
||||
Effect.flatMap(HttpClientResponse.schemaBodyJson(ShareSchema)),
|
||||
)
|
||||
yield* db
|
||||
.insert(SessionShareTable)
|
||||
.values({ session_id: sessionID, id: result.id, secret: result.secret, url: result.url })
|
||||
.onConflictDoUpdate({
|
||||
target: SessionShareTable.session_id,
|
||||
set: { id: result.id, secret: result.secret, url: result.url },
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
const s = yield* InstanceState.get(state)
|
||||
s.shared.set(sessionID, result)
|
||||
yield* full(sessionID).pipe(
|
||||
Effect.catchCause((cause) => Effect.logError("share full sync failed", { sessionID: sessionID, cause: cause })),
|
||||
Effect.forkIn(s.scope),
|
||||
)
|
||||
return result
|
||||
})
|
||||
|
||||
const remove = Effect.fn("ShareNext.remove")(function* (sessionID: SessionID) {
|
||||
if (disabled) return
|
||||
yield* Effect.logInfo("removing share", { sessionID: sessionID })
|
||||
const s = yield* InstanceState.get(state)
|
||||
const share = yield* getCached(sessionID)
|
||||
if (!share) {
|
||||
s.shared.delete(sessionID)
|
||||
s.queue.delete(sessionID)
|
||||
return
|
||||
}
|
||||
|
||||
const req = yield* request()
|
||||
yield* HttpClientRequest.delete(`${req.baseUrl}${req.api.remove(share.id)}`).pipe(
|
||||
HttpClientRequest.setHeaders(req.headers),
|
||||
HttpClientRequest.bodyJson({ secret: share.secret }),
|
||||
Effect.flatMap((r) => httpOk.execute(r)),
|
||||
)
|
||||
|
||||
yield* db.delete(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)).run().pipe(Effect.orDie)
|
||||
s.shared.delete(sessionID)
|
||||
s.queue.delete(sessionID)
|
||||
})
|
||||
|
||||
return Service.of({ init, url, request, create, remove })
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = LayerNode.make({
|
||||
service: Service,
|
||||
layer: layer,
|
||||
deps: [Account.node, EventV2Bridge.node, Config.node, Database.node, httpClient, Provider.node, Session.node],
|
||||
})
|
||||
|
||||
export * as ShareNext from "./share-next"
|
||||
@@ -85,7 +85,6 @@ Options:
|
||||
-c, --continue continue the last session [boolean]
|
||||
-s, --session session id to continue [string]
|
||||
--fork fork the session before continuing (requires --continue or --session) [boolean]
|
||||
--share share the session [boolean]
|
||||
-m, --model model to use in the format of provider/model [string]
|
||||
--agent agent to use [string]
|
||||
--format format: default (formatted) or json (raw JSON events)
|
||||
@@ -185,7 +184,8 @@ Options:
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]
|
||||
-m, --method installation method to use
|
||||
[string] [choices: "curl", "npm", "pnpm", "bun", "brew", "choco", "scoop"]"
|
||||
[string] [choices: "curl", "npm", "pnpm", "bun", "brew", "choco", "scoop"]
|
||||
--major upgrade to OpenCode 2 and remove OpenCode 1 [boolean]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode uninstall --help 1`] = `
|
||||
@@ -301,10 +301,10 @@ Options:
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode import --help 1`] = `
|
||||
"opencode import <file>
|
||||
|
||||
import session data from JSON file or URL
|
||||
import session data from JSON file
|
||||
|
||||
Positionals:
|
||||
file path to JSON file or share URL [string] [required]
|
||||
file path to JSON file [string] [required]
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { test, expect } from "bun:test"
|
||||
import {
|
||||
formatImportFileError,
|
||||
parseShareUrl,
|
||||
shouldAttachShareAuthHeaders,
|
||||
transformShareData,
|
||||
type ShareData,
|
||||
} from "../../src/cli/cmd/import"
|
||||
import { formatImportFileError } from "../../src/cli/cmd/import"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { PlatformError } from "effect"
|
||||
|
||||
@@ -41,50 +35,3 @@ test("formats import file errors", () => {
|
||||
),
|
||||
).toBe("Invalid JSON in test.json: Unexpected token")
|
||||
})
|
||||
|
||||
// parseShareUrl tests
|
||||
test("parses valid share URLs", () => {
|
||||
expect(parseShareUrl("https://opncd.ai/share/Jsj3hNIW")).toBe("Jsj3hNIW")
|
||||
expect(parseShareUrl("https://custom.example.com/share/abc123")).toBe("abc123")
|
||||
expect(parseShareUrl("http://localhost:3000/share/test_id-123")).toBe("test_id-123")
|
||||
})
|
||||
|
||||
test("rejects invalid URLs", () => {
|
||||
expect(parseShareUrl("https://opncd.ai/s/Jsj3hNIW")).toBeNull() // legacy format
|
||||
expect(parseShareUrl("https://opncd.ai/share/")).toBeNull()
|
||||
expect(parseShareUrl("https://opncd.ai/share/id/extra")).toBeNull()
|
||||
expect(parseShareUrl("not-a-url")).toBeNull()
|
||||
})
|
||||
|
||||
test("only attaches share auth headers for same-origin URLs", () => {
|
||||
expect(shouldAttachShareAuthHeaders("https://control.example.com/share/abc", "https://control.example.com")).toBe(
|
||||
true,
|
||||
)
|
||||
expect(shouldAttachShareAuthHeaders("https://other.example.com/share/abc", "https://control.example.com")).toBe(false)
|
||||
expect(shouldAttachShareAuthHeaders("https://control.example.com:443/share/abc", "https://control.example.com")).toBe(
|
||||
true,
|
||||
)
|
||||
expect(shouldAttachShareAuthHeaders("not-a-url", "https://control.example.com")).toBe(false)
|
||||
})
|
||||
|
||||
// transformShareData tests
|
||||
test("transforms share data to storage format", () => {
|
||||
const data: ShareData[] = [
|
||||
{ type: "session", data: { id: "sess-1", title: "Test" } as any },
|
||||
{ type: "message", data: { id: "msg-1", sessionID: "sess-1" } as any },
|
||||
{ type: "part", data: { id: "part-1", messageID: "msg-1" } as any },
|
||||
{ type: "part", data: { id: "part-2", messageID: "msg-1" } as any },
|
||||
]
|
||||
|
||||
const result = transformShareData(data)!
|
||||
|
||||
expect(result.info.id).toBe("sess-1")
|
||||
expect(result.messages).toHaveLength(1)
|
||||
expect(result.messages[0].parts).toHaveLength(2)
|
||||
})
|
||||
|
||||
test("returns null for invalid share data", () => {
|
||||
expect(transformShareData([])).toBeNull()
|
||||
expect(transformShareData([{ type: "message", data: {} as any }])).toBeNull()
|
||||
expect(transformShareData([{ type: "session", data: { id: "s" } as any }])).toBeNull() // no messages
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { shouldNotify } from "../../src/cli/upgrade"
|
||||
|
||||
const base = { autoupdate: undefined, disabled: false, always: false, current: "1.4.0", latest: "1.4.1" }
|
||||
|
||||
describe("upgrade check", () => {
|
||||
test("notifies about every newer OpenCode 1 release, including patches", () => {
|
||||
expect(shouldNotify(base)).toBe(true)
|
||||
expect(shouldNotify({ ...base, latest: "1.10.0" })).toBe(true)
|
||||
})
|
||||
|
||||
test("does not treat OpenCode 2 or older releases as updates", () => {
|
||||
expect(shouldNotify({ ...base, latest: "2.0.0" })).toBe(false)
|
||||
expect(shouldNotify({ ...base, latest: "1.3.9" })).toBe(false)
|
||||
})
|
||||
|
||||
test("treats true and notify the same", () => {
|
||||
expect(shouldNotify({ ...base, autoupdate: true })).toBe(true)
|
||||
expect(shouldNotify({ ...base, autoupdate: "notify" })).toBe(true)
|
||||
})
|
||||
|
||||
test("does not notify when already on the latest version", () => {
|
||||
expect(shouldNotify({ ...base, latest: "1.4.0" })).toBe(false)
|
||||
})
|
||||
|
||||
test("never notifies when update checks are disabled", () => {
|
||||
expect(shouldNotify({ ...base, autoupdate: false })).toBe(false)
|
||||
expect(shouldNotify({ ...base, disabled: true, always: true })).toBe(false)
|
||||
})
|
||||
|
||||
test("always-notify flag forces a notice for the current version", () => {
|
||||
expect(shouldNotify({ ...base, latest: "1.4.0", always: true })).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,19 +1,24 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
|
||||
import { Effect, Layer, Stream } from "effect"
|
||||
import { Effect, Exit, Fiber, Layer, Stream } from "effect"
|
||||
import * as TestClock from "effect/testing/TestClock"
|
||||
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { Installation } from "../../src/installation"
|
||||
import { InstallationChannel } from "@opencode-ai/core/installation/version"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
function mockHttpClient(handler: (request: HttpClientRequest.HttpClientRequest) => Response) {
|
||||
const client = HttpClient.make((request) => Effect.succeed(HttpClientResponse.fromWeb(request, handler(request))))
|
||||
// A handler that returns undefined never responds.
|
||||
function mockHttpClient(handler: (request: HttpClientRequest.HttpClientRequest) => Response | undefined) {
|
||||
const client = HttpClient.make((request) => {
|
||||
const response = handler(request)
|
||||
return response ? Effect.succeed(HttpClientResponse.fromWeb(request, response)) : Effect.never
|
||||
})
|
||||
return Layer.succeed(HttpClient.HttpClient, client)
|
||||
}
|
||||
|
||||
@@ -52,7 +57,7 @@ function jsonResponse(body: unknown) {
|
||||
}
|
||||
|
||||
function testLayer(
|
||||
httpHandler: (request: HttpClientRequest.HttpClientRequest) => Response,
|
||||
httpHandler: (request: HttpClientRequest.HttpClientRequest) => Response | undefined,
|
||||
spawnHandler?: (cmd: string, args: readonly string[]) => string | { code: number; stdout?: string; stderr?: string },
|
||||
) {
|
||||
const spawnerNode = makeGlobalNode({
|
||||
@@ -67,116 +72,126 @@ function testLayer(
|
||||
}
|
||||
|
||||
describe("installation", () => {
|
||||
describe("latest", () => {
|
||||
testEffect(testLayer(() => jsonResponse({ tag_name: "v1.2.3" }))).effect(
|
||||
"reads release version from GitHub releases",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* Installation.use.latest("unknown")
|
||||
expect(result).toBe("1.2.3")
|
||||
}),
|
||||
)
|
||||
|
||||
testEffect(testLayer(() => jsonResponse({ tag_name: "v4.0.0-beta.1" }))).effect(
|
||||
"strips v prefix from GitHub release tag",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* Installation.use.latest("curl")
|
||||
expect(result).toBe("4.0.0-beta.1")
|
||||
}),
|
||||
)
|
||||
|
||||
const npmCalls: string[] = []
|
||||
testEffect(
|
||||
testLayer((request) => {
|
||||
npmCalls.push(request.url)
|
||||
return jsonResponse({ version: "1.5.0" })
|
||||
}),
|
||||
).effect("reads npm versions via registry", () =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* Installation.use.latest("npm")
|
||||
expect(result).toBe("1.5.0")
|
||||
expect(npmCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`)
|
||||
}),
|
||||
)
|
||||
|
||||
const bunCalls: string[] = []
|
||||
testEffect(
|
||||
testLayer((request) => {
|
||||
bunCalls.push(request.url)
|
||||
return jsonResponse({ version: "1.6.0" })
|
||||
}),
|
||||
).effect("reads bun versions via registry", () =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* Installation.use.latest("bun")
|
||||
expect(result).toBe("1.6.0")
|
||||
expect(bunCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`)
|
||||
}),
|
||||
)
|
||||
|
||||
const pnpmCalls: string[] = []
|
||||
testEffect(
|
||||
testLayer((request) => {
|
||||
pnpmCalls.push(request.url)
|
||||
return jsonResponse({ version: "1.7.0" })
|
||||
}),
|
||||
).effect("reads pnpm versions via registry", () =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* Installation.use.latest("pnpm")
|
||||
expect(result).toBe("1.7.0")
|
||||
expect(pnpmCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`)
|
||||
}),
|
||||
)
|
||||
|
||||
testEffect(testLayer(() => jsonResponse({ version: "2.3.4" }))).effect("reads scoop manifest versions", () =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* Installation.use.latest("scoop")
|
||||
expect(result).toBe("2.3.4")
|
||||
}),
|
||||
)
|
||||
|
||||
testEffect(testLayer(() => jsonResponse({ d: { results: [{ Version: "3.4.5" }] } }))).effect(
|
||||
"reads chocolatey feed versions",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* Installation.use.latest("choco")
|
||||
expect(result).toBe("3.4.5")
|
||||
}),
|
||||
)
|
||||
|
||||
testEffect(
|
||||
testLayer(
|
||||
() => jsonResponse({ versions: { stable: "2.0.0" } }),
|
||||
(cmd, args) => {
|
||||
// getBrewFormula: return core formula (no tap)
|
||||
if (cmd === "brew" && args.includes("--formula") && args.includes("anomalyco/tap/opencode")) return ""
|
||||
if (cmd === "brew" && args.includes("--formula") && args.includes("opencode")) return "opencode"
|
||||
return ""
|
||||
},
|
||||
),
|
||||
).effect("reads brew formulae API versions", () =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* Installation.use.latest("brew")
|
||||
expect(result).toBe("2.0.0")
|
||||
}),
|
||||
)
|
||||
|
||||
const brewInfoJson = JSON.stringify({
|
||||
formulae: [{ versions: { stable: "2.1.0" } }],
|
||||
describe("isUpdate", () => {
|
||||
test("only counts newer releases in the installed major version", () => {
|
||||
expect(Installation.isUpdate("1.19.0", "1.19.1")).toBe(true)
|
||||
expect(Installation.isUpdate("1.19.0", "1.20.0")).toBe(true)
|
||||
expect(Installation.isUpdate("1.19.0", "1.19.0")).toBe(false)
|
||||
expect(Installation.isUpdate("1.19.0", "1.18.35")).toBe(false)
|
||||
expect(Installation.isUpdate("1.19.0", "2.0.0")).toBe(false)
|
||||
expect(Installation.isUpdate("1.9.0", "1.10.0")).toBe(true)
|
||||
})
|
||||
|
||||
test("compares preview builds within their channel", () => {
|
||||
expect(Installation.isUpdate("0.0.0-dev-202610110139", "0.0.0-dev-202610120000")).toBe(true)
|
||||
expect(Installation.isUpdate("0.0.0-dev-202610120000", "0.0.0-dev-202610110139")).toBe(false)
|
||||
})
|
||||
|
||||
test("source builds only accept OpenCode 1 releases", () => {
|
||||
expect(Installation.isUpdate("local", "1.19.0")).toBe(true)
|
||||
expect(Installation.isUpdate("local", "2.0.0")).toBe(false)
|
||||
expect(Installation.isUpdate("1.19.0", "not-a-version")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("latest", () => {
|
||||
const npm = "https://registry.npmjs.org/opencode-ai/latest"
|
||||
const choco = "https://community.chocolatey.org/api/v2/Packages"
|
||||
const tap = (cmd: string, args: readonly string[]) =>
|
||||
cmd === "brew" && args.join(" ") === "list --formula anomalyco/tap/opencode" ? "opencode" : ""
|
||||
const core = (cmd: string, args: readonly string[]) =>
|
||||
cmd === "brew" && args.join(" ") === "list --formula opencode" ? "opencode" : ""
|
||||
const registry = (version: string, calls: string[], feed: string[] = [version]) =>
|
||||
testLayer((request) => {
|
||||
calls.push(request.url)
|
||||
if (request.url.startsWith(choco))
|
||||
return jsonResponse({
|
||||
d: { results: feed.filter((v) => request.url.includes(`%27${v}%27`)).map((Version) => ({ Version })) },
|
||||
})
|
||||
return jsonResponse({ version })
|
||||
})
|
||||
|
||||
const methods = ["curl", "yarn", "unknown", "npm", "bun", "pnpm", "scoop", "choco"] as const
|
||||
for (const method of methods) {
|
||||
const calls: string[] = []
|
||||
testEffect(registry("1.19.2", calls)).effect(`reads ${method} versions from the opencode-ai npm package`, () =>
|
||||
Effect.gen(function* () {
|
||||
calls.length = 0
|
||||
expect(yield* Installation.use.latest(method)).toBe("1.19.2")
|
||||
expect(calls[0]).toBe(npm)
|
||||
expect(calls.slice(1).every((url) => url.startsWith(choco))).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
testEffect(registry("2.0.27", [])).effect(`ignores OpenCode 2 versions for ${method}`, () =>
|
||||
Effect.gen(function* () {
|
||||
expect(yield* Installation.use.latest(method)).toBe(InstallationVersion)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const tapCalls: string[] = []
|
||||
testEffect(
|
||||
testLayer(
|
||||
() => jsonResponse({}), // HTTP not used for tap formula
|
||||
(cmd, args) => {
|
||||
if (cmd === "brew" && args.includes("anomalyco/tap/opencode") && args.includes("--formula")) return "opencode"
|
||||
if (cmd === "brew" && args.includes("--json=v2")) return brewInfoJson
|
||||
return ""
|
||||
},
|
||||
),
|
||||
).effect("reads brew tap info JSON via CLI", () =>
|
||||
testLayer((request) => {
|
||||
tapCalls.push(request.url)
|
||||
return jsonResponse({ version: "1.19.2" })
|
||||
}, tap),
|
||||
).effect("reads Homebrew tap versions from the opencode-ai npm package", () =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* Installation.use.latest("brew")
|
||||
expect(result).toBe("2.1.0")
|
||||
tapCalls.length = 0
|
||||
expect(yield* Installation.use.latest("brew")).toBe("1.19.2")
|
||||
expect(tapCalls).toEqual([npm])
|
||||
}),
|
||||
)
|
||||
|
||||
const coreCalls: string[] = []
|
||||
testEffect(
|
||||
testLayer((request) => {
|
||||
coreCalls.push(request.url)
|
||||
return jsonResponse({ version: "1.19.2" })
|
||||
}, core),
|
||||
).effect("never offers an OpenCode 1 update Homebrew Core can't install", () =>
|
||||
Effect.gen(function* () {
|
||||
coreCalls.length = 0
|
||||
expect(yield* Installation.use.latest("brew")).toBe(InstallationVersion)
|
||||
expect(coreCalls).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
testEffect(registry("1.19.2", [], ["1.19.1"])).effect("waits until Chocolatey has published the npm version", () =>
|
||||
Effect.gen(function* () {
|
||||
expect(yield* Installation.use.latest("choco")).toBe(InstallationVersion)
|
||||
}),
|
||||
)
|
||||
|
||||
const errorCalls: string[] = []
|
||||
testEffect(
|
||||
testLayer((request) => {
|
||||
errorCalls.push(request.url)
|
||||
return new Response("unavailable", { status: 503 })
|
||||
}),
|
||||
).live("fails when the npm registry fails, without consulting GitHub", () =>
|
||||
Effect.gen(function* () {
|
||||
errorCalls.length = 0
|
||||
expect(Exit.isFailure(yield* Installation.use.latest("curl").pipe(Effect.exit))).toBe(true)
|
||||
expect(errorCalls.length).toBeGreaterThan(0)
|
||||
expect(errorCalls.every((url) => url === npm)).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
testEffect(testLayer(() => jsonResponse({ name: "opencode-ai" }))).effect(
|
||||
"fails when the npm registry returns no version",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
expect(Exit.isFailure(yield* Installation.use.latest("npm").pipe(Effect.exit))).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
testEffect(testLayer(() => undefined)).effect("gives up when the npm registry doesn't answer in time", () =>
|
||||
Effect.gen(function* () {
|
||||
const fiber = yield* Installation.use.latest("curl").pipe(Effect.exit, Effect.forkChild)
|
||||
yield* TestClock.adjust("10 seconds")
|
||||
expect(Exit.isFailure(yield* Fiber.join(fiber))).toBe(true)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -192,7 +207,7 @@ describe("installation", () => {
|
||||
),
|
||||
).effect("returns sanitized typed errors for failed package upgrades", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* Effect.flip(Installation.use.upgrade("npm", "9.9.9"))
|
||||
const error = yield* Effect.flip(Installation.use.upgrade("npm", "1.99.9"))
|
||||
expect(error).toBeInstanceOf(Installation.UpgradeFailedError)
|
||||
expect(error.stderr).toBe("Upgrade failed for npm (exit code 1).")
|
||||
expect(error.message).toBe(error.stderr)
|
||||
@@ -212,7 +227,7 @@ describe("installation", () => {
|
||||
),
|
||||
).effect("returns sanitized typed errors when the curl install script fails", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* Effect.flip(Installation.use.upgrade("curl", "9.9.9"))
|
||||
const error = yield* Effect.flip(Installation.use.upgrade("curl", "1.99.9"))
|
||||
expect(error).toBeInstanceOf(Installation.UpgradeFailedError)
|
||||
expect(error.stderr).toBe("Upgrade failed for curl (exit code 1).")
|
||||
expect(error.message).toBe(error.stderr)
|
||||
@@ -233,7 +248,199 @@ describe("installation", () => {
|
||||
),
|
||||
).effect("falls back to sh when bash is unavailable during curl upgrade", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* Installation.use.upgrade("curl", "9.9.9")
|
||||
yield* Installation.use.upgrade("curl", "1.99.9")
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe("upgrade per method", () => {
|
||||
const packageCalls: string[] = []
|
||||
testEffect(
|
||||
testLayer(
|
||||
() => jsonResponse({}),
|
||||
(cmd, args) => {
|
||||
packageCalls.push([cmd, ...args].join(" "))
|
||||
return ""
|
||||
},
|
||||
),
|
||||
).effect("installs the OpenCode 1 package with each package manager", () =>
|
||||
Effect.gen(function* () {
|
||||
for (const method of ["npm", "pnpm", "bun", "yarn"] as const) {
|
||||
packageCalls.length = 0
|
||||
yield* Installation.use.upgrade(method, "1.19.2")
|
||||
expect(packageCalls[0]).toContain("opencode-ai@1.19.2")
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const majorCalls: string[] = []
|
||||
testEffect(
|
||||
testLayer(
|
||||
() => jsonResponse({}),
|
||||
(cmd, args) => {
|
||||
majorCalls.push([cmd, ...args].join(" "))
|
||||
return ""
|
||||
},
|
||||
),
|
||||
).effect("refuses OpenCode 2 targets for every method", () =>
|
||||
Effect.gen(function* () {
|
||||
for (const method of ["curl", "npm", "pnpm", "bun", "yarn", "brew", "choco", "scoop"] as const) {
|
||||
majorCalls.length = 0
|
||||
const error = yield* Effect.flip(Installation.use.upgrade(method, "2.0.27"))
|
||||
expect(error.stderr).toContain("opencode upgrade --major")
|
||||
expect(majorCalls).toEqual([])
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const coreCalls: string[] = []
|
||||
testEffect(
|
||||
testLayer(
|
||||
() => jsonResponse({}),
|
||||
(cmd, args) => {
|
||||
coreCalls.push([cmd, ...args].join(" "))
|
||||
if (args.join(" ") === "list --formula opencode") return "opencode"
|
||||
return ""
|
||||
},
|
||||
),
|
||||
).effect("never runs brew upgrade for Homebrew Core, whose formula is OpenCode 2", () =>
|
||||
Effect.gen(function* () {
|
||||
coreCalls.length = 0
|
||||
const error = yield* Effect.flip(Installation.use.upgrade("brew", "1.19.2"))
|
||||
expect(error.stderr).toContain("Homebrew Core")
|
||||
expect(coreCalls.some((call) => call.startsWith("brew upgrade"))).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
const tap = (stable: string, calls: string[]) =>
|
||||
testLayer(
|
||||
() => jsonResponse({}),
|
||||
(cmd, args) => {
|
||||
calls.push([cmd, ...args].join(" "))
|
||||
if (args.join(" ") === "list --formula anomalyco/tap/opencode") return "opencode"
|
||||
if (args.join(" ") === "--repo anomalyco/tap") return "/tmp/opencode-test-tap"
|
||||
if (args.includes("--json=v2")) return JSON.stringify({ formulae: [{ versions: { stable } }] })
|
||||
return ""
|
||||
},
|
||||
)
|
||||
|
||||
const tapCalls: string[] = []
|
||||
testEffect(tap("1.19.2", tapCalls)).effect("upgrades the Homebrew tap once it has the version", () =>
|
||||
Effect.gen(function* () {
|
||||
tapCalls.length = 0
|
||||
yield* Installation.use.upgrade("brew", "1.19.2")
|
||||
expect(tapCalls).toContain("git pull --ff-only")
|
||||
expect(tapCalls).toContain("brew upgrade anomalyco/tap/opencode")
|
||||
}),
|
||||
)
|
||||
|
||||
for (const stable of ["1.19.1", "2.0.0"]) {
|
||||
const calls: string[] = []
|
||||
testEffect(tap(stable, calls)).effect(`asks to retry when the Homebrew tap is at ${stable}`, () =>
|
||||
Effect.gen(function* () {
|
||||
calls.length = 0
|
||||
const error = yield* Effect.flip(Installation.use.upgrade("brew", "1.19.2"))
|
||||
expect(error.stderr).toContain("Try again in a few minutes")
|
||||
expect(calls.some((call) => call.startsWith("brew upgrade"))).toBe(false)
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
describe("upgradeMajor", () => {
|
||||
const calls: string[] = []
|
||||
testEffect(
|
||||
testLayer(
|
||||
() => jsonResponse({}),
|
||||
(cmd, args) => {
|
||||
calls.push([cmd, ...args].join(" "))
|
||||
return ""
|
||||
},
|
||||
),
|
||||
).effect("replaces the npm package with OpenCode 2", () =>
|
||||
Effect.gen(function* () {
|
||||
calls.length = 0
|
||||
yield* Installation.use.upgradeMajor("npm")
|
||||
expect(calls.slice(0, 2)).toEqual(["npm uninstall -g opencode-ai", "npm install -g @opencode/cli"])
|
||||
}),
|
||||
)
|
||||
|
||||
const restoreCalls: string[] = []
|
||||
testEffect(
|
||||
testLayer(
|
||||
() => jsonResponse({}),
|
||||
(cmd, args) => {
|
||||
restoreCalls.push([cmd, ...args].join(" "))
|
||||
if (args.includes("@opencode/cli")) return { code: 1, stderr: "install failed" }
|
||||
return ""
|
||||
},
|
||||
),
|
||||
).effect("restores OpenCode 1 when the OpenCode 2 install fails", () =>
|
||||
Effect.gen(function* () {
|
||||
restoreCalls.length = 0
|
||||
const error = yield* Effect.flip(Installation.use.upgradeMajor("npm"))
|
||||
expect(error.stderr).toBe("Upgrade failed for npm (exit code 1).")
|
||||
expect(restoreCalls[2]).toMatch(/^npm install -g opencode-ai@/)
|
||||
}),
|
||||
)
|
||||
|
||||
const brewCalls: string[] = []
|
||||
testEffect(
|
||||
testLayer(
|
||||
() => jsonResponse({}),
|
||||
(cmd, args) => {
|
||||
brewCalls.push([cmd, ...args].join(" "))
|
||||
if (args.join(" ") === "list --formula anomalyco/tap/opencode") return "opencode"
|
||||
return ""
|
||||
},
|
||||
),
|
||||
).effect("replaces the brew formula with the OpenCode 2 formula", () =>
|
||||
Effect.gen(function* () {
|
||||
brewCalls.length = 0
|
||||
yield* Installation.use.upgradeMajor("brew")
|
||||
expect(brewCalls).toContain("brew uninstall anomalyco/tap/opencode")
|
||||
expect(brewCalls).toContain("brew install anomalyco/tap/opencode-v2")
|
||||
}),
|
||||
)
|
||||
|
||||
const curlCalls: string[] = []
|
||||
const curlUrls: string[] = []
|
||||
testEffect(
|
||||
testLayer(
|
||||
(request) => {
|
||||
curlUrls.push(request.url)
|
||||
return new Response("echo v2", { status: 200 })
|
||||
},
|
||||
(cmd, args) => {
|
||||
curlCalls.push(cmd)
|
||||
if (cmd === "bash" && args[0] === "--version") return "GNU bash"
|
||||
return ""
|
||||
},
|
||||
),
|
||||
).effect("runs the OpenCode 2 install script for curl installs", () =>
|
||||
Effect.gen(function* () {
|
||||
curlUrls.length = 0
|
||||
yield* Installation.use.upgradeMajor("curl")
|
||||
expect(curlUrls).toEqual(["https://opencode.ai/v2/install"])
|
||||
expect(curlCalls).toContain("bash")
|
||||
}),
|
||||
)
|
||||
|
||||
const chocoCalls: string[] = []
|
||||
testEffect(
|
||||
testLayer(
|
||||
() => jsonResponse({}),
|
||||
(cmd) => {
|
||||
chocoCalls.push(cmd)
|
||||
return ""
|
||||
},
|
||||
),
|
||||
).effect("leaves package managers without OpenCode 2 untouched", () =>
|
||||
Effect.gen(function* () {
|
||||
chocoCalls.length = 0
|
||||
const error = yield* Effect.flip(Installation.use.upgradeMajor("choco"))
|
||||
expect(error.stderr).toContain(Installation.MAJOR_INSTRUCTIONS)
|
||||
expect(chocoCalls).toEqual([])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -584,6 +584,33 @@ it.instance(
|
||||
},
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"model config reasoning options replace catalog variants under configured variants",
|
||||
Effect.gen(function* () {
|
||||
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = yield* list
|
||||
const models = providers[ProviderV2.ID.anthropic].models
|
||||
expect(Object.keys(models["claude-sonnet-4-6"].variants ?? {})).toEqual(["low", "high"])
|
||||
expect(models["claude-sonnet-4-6"].variants?.high).toMatchObject({ effort: "high", extra: true })
|
||||
expect(models["claude-fixed"].variants).toEqual({})
|
||||
}),
|
||||
{
|
||||
config: {
|
||||
provider: {
|
||||
anthropic: {
|
||||
models: {
|
||||
"claude-sonnet-4-6": {
|
||||
reasoning_options: [{ type: "effort", values: ["low", "high"] }],
|
||||
variants: { high: { extra: true } },
|
||||
},
|
||||
"claude-fixed": { id: "claude-sonnet-4-6", reasoning: true, reasoning_options: [] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"model config regenerates variants when overriding the provider package",
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -1656,14 +1656,7 @@ const scenarios: Scenario[] = [
|
||||
.mutating()
|
||||
.seeded((ctx) => ctx.session({ title: "Share session" }))
|
||||
.at((ctx) => ({ path: route("/session/{sessionID}/share", { sessionID: ctx.state.id }), headers: ctx.headers() }))
|
||||
.json(
|
||||
200,
|
||||
(body, ctx) => {
|
||||
object(body)
|
||||
check(body.id === ctx.state.id, "share should return the session")
|
||||
},
|
||||
"status",
|
||||
),
|
||||
.json(500, object, "status"),
|
||||
http.protected
|
||||
.delete("/session/{sessionID}/share", "session.unshare")
|
||||
.mutating()
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
import { beforeEach, describe, expect } from "bun:test"
|
||||
import { Effect, Exit, Layer, Option } from "effect"
|
||||
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
|
||||
import { AccessToken, AccountID, OrgID, RefreshToken } from "../../src/account/schema"
|
||||
import { AccountRepo } from "../../src/account/repo"
|
||||
import { EventV2Bridge } from "../../src/event-v2-bridge"
|
||||
import { Session } from "@/session/session"
|
||||
import type { SessionID } from "../../src/session/schema"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
import { SessionShareTable } from "@opencode-ai/core/share/sql"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { pollWithTimeout, testEffect } from "../lib/effect"
|
||||
|
||||
const env = LayerNode.compile(LayerNode.group([CrossSpawnSpawner.node]))
|
||||
const it = testEffect(env)
|
||||
|
||||
const json = (req: Parameters<typeof HttpClientResponse.fromWeb>[0], body: unknown, status = 200) =>
|
||||
HttpClientResponse.fromWeb(
|
||||
req,
|
||||
new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
)
|
||||
|
||||
const none = HttpClient.make(() => Effect.die("unexpected http call"))
|
||||
|
||||
function requestLayer(client: HttpClient.HttpClient) {
|
||||
const replacement = [httpClient, Layer.succeed(HttpClient.HttpClient, client)] as const
|
||||
return LayerNode.compile(LayerNode.group([ShareNext.node, AccountRepo.node]), [replacement])
|
||||
}
|
||||
|
||||
function integrationLayer(client: HttpClient.HttpClient) {
|
||||
const replacement = [httpClient, Layer.succeed(HttpClient.HttpClient, client)] as const
|
||||
return LayerNode.compile(
|
||||
LayerNode.group([
|
||||
ShareNext.node,
|
||||
EventV2Bridge.node,
|
||||
Session.node,
|
||||
SessionProjector.node,
|
||||
AccountRepo.node,
|
||||
Database.node,
|
||||
]),
|
||||
[replacement],
|
||||
)
|
||||
}
|
||||
|
||||
const share = (id: SessionID) =>
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
return yield* db
|
||||
.select()
|
||||
.from(SessionShareTable)
|
||||
.where(eq(SessionShareTable.session_id, id))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const seed = (url: string, org?: string) =>
|
||||
AccountRepo.Service.use((repo) =>
|
||||
repo.persistAccount({
|
||||
id: AccountID.make("account-1"),
|
||||
email: "user@example.com",
|
||||
url,
|
||||
accessToken: AccessToken.make("st_test_token"),
|
||||
refreshToken: RefreshToken.make("rt_test_token"),
|
||||
expiry: Date.now() + 10 * 60_000,
|
||||
orgID: org ? Option.some(OrgID.make(org)) : Option.none(),
|
||||
}),
|
||||
)
|
||||
|
||||
beforeEach(async () => {
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
describe("ShareNext", () => {
|
||||
it.live("request uses legacy share API without active org account", () =>
|
||||
provideTmpdirInstance(
|
||||
() =>
|
||||
ShareNext.Service.use((svc) =>
|
||||
Effect.gen(function* () {
|
||||
const req = yield* svc.request()
|
||||
|
||||
expect(req.api.create).toBe("/api/share")
|
||||
expect(req.api.sync("shr_123")).toBe("/api/share/shr_123/sync")
|
||||
expect(req.api.remove("shr_123")).toBe("/api/share/shr_123")
|
||||
expect(req.api.data("shr_123")).toBe("/api/share/shr_123/data")
|
||||
expect(req.baseUrl).toBe("https://legacy-share.example.com")
|
||||
expect(req.headers).toEqual({})
|
||||
}),
|
||||
).pipe(Effect.provide(requestLayer(none))),
|
||||
{ config: { enterprise: { url: "https://legacy-share.example.com" } } },
|
||||
),
|
||||
)
|
||||
|
||||
it.live("request uses default URL when no enterprise config", () =>
|
||||
provideTmpdirInstance(() =>
|
||||
ShareNext.Service.use((svc) =>
|
||||
Effect.gen(function* () {
|
||||
const req = yield* svc.request()
|
||||
|
||||
expect(req.baseUrl).toBe("https://opncd.ai")
|
||||
expect(req.api.create).toBe("/api/share")
|
||||
expect(req.headers).toEqual({})
|
||||
}),
|
||||
).pipe(Effect.provide(requestLayer(none))),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("request uses org share API with auth headers when account is active", () =>
|
||||
provideTmpdirInstance(() =>
|
||||
Effect.gen(function* () {
|
||||
yield* seed("https://control.example.com", "org-1")
|
||||
|
||||
const req = yield* ShareNext.use.request()
|
||||
|
||||
expect(req.api.create).toBe("/api/shares")
|
||||
expect(req.api.sync("shr_123")).toBe("/api/shares/shr_123/sync")
|
||||
expect(req.api.remove("shr_123")).toBe("/api/shares/shr_123")
|
||||
expect(req.api.data("shr_123")).toBe("/api/shares/shr_123/data")
|
||||
expect(req.baseUrl).toBe("https://control.example.com")
|
||||
expect(req.headers).toEqual({
|
||||
authorization: "Bearer st_test_token",
|
||||
"x-org-id": "org-1",
|
||||
})
|
||||
}).pipe(Effect.provide(requestLayer(none))),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("create posts share, persists it, and returns the result", () =>
|
||||
provideTmpdirInstance(
|
||||
() => {
|
||||
const createRequests: HttpClientRequest.HttpClientRequest[] = []
|
||||
const client = HttpClient.make((req) => {
|
||||
if (req.url.endsWith("/api/share")) {
|
||||
createRequests.push(req)
|
||||
return Effect.succeed(
|
||||
json(req, {
|
||||
id: "shr_abc",
|
||||
url: "https://legacy-share.example.com/share/abc",
|
||||
secret: "sec_123",
|
||||
}),
|
||||
)
|
||||
}
|
||||
return Effect.succeed(json(req, { ok: true }))
|
||||
})
|
||||
return Effect.gen(function* () {
|
||||
const session = yield* (yield* Session.Service).create({ title: "test" })
|
||||
|
||||
const result = yield* (yield* ShareNext.Service).create(session.id)
|
||||
|
||||
expect(result.id).toBe("shr_abc")
|
||||
expect(result.url).toBe("https://legacy-share.example.com/share/abc")
|
||||
expect(result.secret).toBe("sec_123")
|
||||
|
||||
const row = yield* share(session.id)
|
||||
expect(row?.id).toBe("shr_abc")
|
||||
expect(row?.url).toBe("https://legacy-share.example.com/share/abc")
|
||||
expect(row?.secret).toBe("sec_123")
|
||||
|
||||
expect(createRequests).toHaveLength(1)
|
||||
expect(createRequests[0].method).toBe("POST")
|
||||
expect(createRequests[0].url).toBe("https://legacy-share.example.com/api/share")
|
||||
}).pipe(Effect.provide(integrationLayer(client)))
|
||||
},
|
||||
{ config: { enterprise: { url: "https://legacy-share.example.com" } } },
|
||||
),
|
||||
)
|
||||
|
||||
it.live("remove deletes the persisted share and calls the delete endpoint", () =>
|
||||
provideTmpdirInstance(
|
||||
() => {
|
||||
const seen: HttpClientRequest.HttpClientRequest[] = []
|
||||
const client = HttpClient.make((req) => {
|
||||
seen.push(req)
|
||||
if (req.method === "POST") {
|
||||
return Effect.succeed(
|
||||
json(req, {
|
||||
id: "shr_abc",
|
||||
url: "https://legacy-share.example.com/share/abc",
|
||||
secret: "sec_123",
|
||||
}),
|
||||
)
|
||||
}
|
||||
return Effect.succeed(HttpClientResponse.fromWeb(req, new Response(null, { status: 200 })))
|
||||
})
|
||||
return Effect.gen(function* () {
|
||||
const session = yield* (yield* Session.Service).create({ title: "test" })
|
||||
const service = yield* ShareNext.Service
|
||||
|
||||
yield* service.create(session.id)
|
||||
yield* service.remove(session.id)
|
||||
|
||||
expect(yield* share(session.id)).toBeUndefined()
|
||||
expect(seen.map((req) => [req.method, req.url])).toEqual([
|
||||
["POST", "https://legacy-share.example.com/api/share"],
|
||||
["DELETE", "https://legacy-share.example.com/api/share/shr_abc"],
|
||||
])
|
||||
}).pipe(Effect.provide(integrationLayer(client)))
|
||||
},
|
||||
{ config: { enterprise: { url: "https://legacy-share.example.com" } } },
|
||||
),
|
||||
)
|
||||
|
||||
it.live("create fails on a non-ok response and does not persist a share", () =>
|
||||
provideTmpdirInstance(() => {
|
||||
const client = HttpClient.make((req) => Effect.succeed(json(req, { error: "bad" }, 500)))
|
||||
return Effect.gen(function* () {
|
||||
const session = yield* (yield* Session.Service).create({ title: "test" })
|
||||
|
||||
const exit = yield* ShareNext.Service.use((svc) => Effect.exit(svc.create(session.id)))
|
||||
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
expect(yield* share(session.id)).toBeUndefined()
|
||||
}).pipe(Effect.provide(integrationLayer(client)))
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("ShareNext coalesces rapid diff events into one delayed sync with latest data", () =>
|
||||
provideTmpdirInstance(
|
||||
() => {
|
||||
const seen: Array<{ url: string; body: string }> = []
|
||||
const client = HttpClient.make((req) => {
|
||||
if (req.url.endsWith("/sync") && req.body._tag === "Uint8Array") {
|
||||
seen.push({ url: req.url, body: new TextDecoder().decode(req.body.body) })
|
||||
}
|
||||
return Effect.succeed(json(req, { ok: true }))
|
||||
})
|
||||
|
||||
return Effect.gen(function* () {
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const share = yield* ShareNext.Service
|
||||
const session = yield* Session.Service
|
||||
|
||||
const info = yield* session.create({ title: "first" })
|
||||
yield* share.init()
|
||||
yield* Effect.sleep(50)
|
||||
const { db } = yield* Database.Service
|
||||
yield* db
|
||||
.insert(SessionShareTable)
|
||||
.values({
|
||||
session_id: info.id,
|
||||
id: "shr_abc",
|
||||
url: "https://legacy-share.example.com/share/abc",
|
||||
secret: "sec_123",
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
yield* events.publish(Session.Event.Diff, {
|
||||
sessionID: info.id,
|
||||
diff: [
|
||||
{
|
||||
file: "a.ts",
|
||||
patch:
|
||||
"Index: a.ts\n===================================================================\n--- a.ts\t\n+++ a.ts\t\n@@ -1,1 +1,1 @@\n-one\n\\ No newline at end of file\n+two\n\\ No newline at end of file\n",
|
||||
additions: 1,
|
||||
deletions: 1,
|
||||
status: "modified",
|
||||
},
|
||||
],
|
||||
})
|
||||
yield* events.publish(Session.Event.Diff, {
|
||||
sessionID: info.id,
|
||||
diff: [
|
||||
{
|
||||
file: "b.ts",
|
||||
patch:
|
||||
"Index: b.ts\n===================================================================\n--- b.ts\t\n+++ b.ts\t\n@@ -1,1 +1,1 @@\n-old\n\\ No newline at end of file\n+new\n\\ No newline at end of file\n",
|
||||
additions: 2,
|
||||
deletions: 0,
|
||||
status: "modified",
|
||||
},
|
||||
],
|
||||
})
|
||||
yield* pollWithTimeout(
|
||||
Effect.sync(() => (seen.length === 1 ? true : undefined)),
|
||||
"timed out waiting for share sync",
|
||||
"5 seconds",
|
||||
)
|
||||
|
||||
expect(seen).toHaveLength(1)
|
||||
expect(seen[0].url).toBe("https://legacy-share.example.com/api/share/shr_abc/sync")
|
||||
|
||||
const body = JSON.parse(seen[0].body) as {
|
||||
secret: string
|
||||
data: Array<{
|
||||
type: string
|
||||
data: Array<{
|
||||
file: string
|
||||
patch: string
|
||||
additions: number
|
||||
deletions: number
|
||||
status?: string
|
||||
}>
|
||||
}>
|
||||
}
|
||||
expect(body.secret).toBe("sec_123")
|
||||
expect(body.data).toHaveLength(1)
|
||||
expect(body.data[0].type).toBe("session_diff")
|
||||
expect(body.data[0].data).toEqual([
|
||||
{
|
||||
file: "b.ts",
|
||||
patch:
|
||||
"Index: b.ts\n===================================================================\n--- b.ts\t\n+++ b.ts\t\n@@ -1,1 +1,1 @@\n-old\n\\ No newline at end of file\n+new\n\\ No newline at end of file\n",
|
||||
additions: 2,
|
||||
deletions: 0,
|
||||
status: "modified",
|
||||
},
|
||||
])
|
||||
}).pipe(Effect.provide(integrationLayer(client)))
|
||||
},
|
||||
{ config: { enterprise: { url: "https://legacy-share.example.com" } } },
|
||||
),
|
||||
)
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1768,6 +1768,23 @@ export type ProviderConfig = {
|
||||
release_date?: string
|
||||
attachment?: boolean
|
||||
reasoning?: boolean
|
||||
/**
|
||||
* Reasoning controls the model accepts, used to build its variants
|
||||
*/
|
||||
reasoning_options?: Array<
|
||||
| {
|
||||
type: "effort"
|
||||
values: Array<string>
|
||||
}
|
||||
| {
|
||||
type: "toggle"
|
||||
}
|
||||
| {
|
||||
type: "budget_tokens"
|
||||
min?: number
|
||||
max?: number
|
||||
}
|
||||
>
|
||||
temperature?: boolean
|
||||
tool_call?: boolean
|
||||
interleaved?:
|
||||
@@ -1927,7 +1944,7 @@ export type Config = {
|
||||
share?: "manual" | "auto" | "disabled"
|
||||
autoshare?: boolean
|
||||
/**
|
||||
* Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications
|
||||
* Check for new versions and show an update notice. Updates are never installed automatically; run /update or `opencode upgrade`. Set to false to disable update checks. true and 'notify' behave the same
|
||||
*/
|
||||
autoupdate?: boolean | "notify"
|
||||
disabled_providers?: Array<string>
|
||||
|
||||
@@ -20821,6 +20821,59 @@
|
||||
"reasoning": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"reasoning_options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["effort"]
|
||||
},
|
||||
"values": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["type", "values"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["toggle"]
|
||||
}
|
||||
},
|
||||
"required": ["type"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["budget_tokens"]
|
||||
},
|
||||
"min": {
|
||||
"type": "number"
|
||||
},
|
||||
"max": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["type"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Reasoning controls the model accepts, used to build its variants"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -21263,7 +21316,7 @@
|
||||
"enum": ["notify"]
|
||||
}
|
||||
],
|
||||
"description": "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications"
|
||||
"description": "Check for new versions and show an update notice. Updates are never installed automatically; run /update or `opencode upgrade`. Set to false to disable update checks. true and 'notify' behave the same"
|
||||
},
|
||||
"disabled_providers": {
|
||||
"type": "array",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -140,6 +140,14 @@ export const PtyHandler = HttpApiBuilder.group(Api, "server.pty", (handlers) =>
|
||||
.handleRaw(
|
||||
"pty.connect",
|
||||
Effect.fn("PtyHandler.connect")(function* (ctx) {
|
||||
if (!isAllowedRequestOrigin(ctx.request.headers.origin, ctx.request.headers.host, cors))
|
||||
return HttpServerResponse.empty({ status: 403 })
|
||||
|
||||
const url = new URL(ctx.request.url, "http://localhost")
|
||||
const ticket = url.searchParams.get(PTY_CONNECT_TICKET_QUERY)
|
||||
if (ticket && !(yield* tickets.consume({ ticket, ptyID: ctx.params.ptyID, ...(yield* ticketScope) })))
|
||||
return HttpServerResponse.empty({ status: 403 })
|
||||
|
||||
const pty = yield* Pty.Service
|
||||
const exists = yield* pty.get(ctx.params.ptyID).pipe(
|
||||
Effect.as(true),
|
||||
@@ -147,14 +155,6 @@ export const PtyHandler = HttpApiBuilder.group(Api, "server.pty", (handlers) =>
|
||||
)
|
||||
if (!exists) return HttpServerResponse.empty({ status: 404 })
|
||||
|
||||
const url = new URL(ctx.request.url, "http://localhost")
|
||||
const ticket = url.searchParams.get(PTY_CONNECT_TICKET_QUERY)
|
||||
if (ticket) {
|
||||
const valid = isAllowedRequestOrigin(ctx.request.headers.origin, ctx.request.headers.host, cors)
|
||||
? yield* tickets.consume({ ticket, ptyID: ctx.params.ptyID, ...(yield* ticketScope) })
|
||||
: false
|
||||
if (!valid) return HttpServerResponse.empty({ status: 403 })
|
||||
}
|
||||
const parsedCursor = url.searchParams.get("cursor")
|
||||
const cursorNumber = parsedCursor === null ? undefined : Number(parsedCursor)
|
||||
const cursor =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
+20
-71
@@ -56,11 +56,10 @@ import { Session } from "./routes/session"
|
||||
import { PromptHistoryProvider } from "./component/prompt/history"
|
||||
import { FrecencyProvider } from "./component/prompt/frecency"
|
||||
import { PromptStashProvider } from "./component/prompt/stash"
|
||||
import { DialogAlert } from "./ui/dialog-alert"
|
||||
import { DialogConfirm } from "./ui/dialog-confirm"
|
||||
import { ToastProvider, useToast } from "./ui/toast"
|
||||
import { isDefaultTitle } from "./util/session"
|
||||
import { KVProvider, useKV } from "./context/kv"
|
||||
import { UpdateNotificationProvider, useUpdateNotification, type UpdateSource } from "./context/update-notification"
|
||||
import * as Model from "./util/model"
|
||||
import { ArgsProvider, useArgs, type Args } from "./context/args"
|
||||
import { openUrl } from "@opencode-ai/core/open"
|
||||
@@ -149,6 +148,7 @@ export type TuiInput = {
|
||||
headers?: RequestInit["headers"]
|
||||
events?: EventSource
|
||||
pluginHost: TuiPluginHost
|
||||
updater?: UpdateSource
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown) {
|
||||
@@ -166,23 +166,6 @@ function errorMessage(error: unknown) {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
|
||||
function isVersionGreater(left: string, right: string) {
|
||||
const parse = (value: string) => {
|
||||
const [core, prerelease] = value.replace(/^v/, "").split("-", 2)
|
||||
return { core: core.split(".").map((part) => Number.parseInt(part, 10) || 0), prerelease }
|
||||
}
|
||||
const a = parse(left)
|
||||
const b = parse(right)
|
||||
for (let index = 0; index < Math.max(a.core.length, b.core.length); index++) {
|
||||
const difference = (a.core[index] ?? 0) - (b.core[index] ?? 0)
|
||||
if (difference) return difference > 0
|
||||
}
|
||||
if (a.prerelease === b.prerelease) return false
|
||||
if (!a.prerelease) return true
|
||||
if (!b.prerelease) return false
|
||||
return a.prerelease.localeCompare(b.prerelease, undefined, { numeric: true }) > 0
|
||||
}
|
||||
|
||||
export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||
const global = yield* Global.Service
|
||||
const exit = { epilogue: undefined as string | undefined, reason: undefined as unknown }
|
||||
@@ -315,10 +298,12 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||
<PromptRefProvider>
|
||||
<EditorContextProvider>
|
||||
<LocationProvider>
|
||||
<App
|
||||
onSnapshot={input.onSnapshot}
|
||||
pluginHost={input.pluginHost}
|
||||
/>
|
||||
<UpdateNotificationProvider updater={input.updater}>
|
||||
<App
|
||||
onSnapshot={input.onSnapshot}
|
||||
pluginHost={input.pluginHost}
|
||||
/>
|
||||
</UpdateNotificationProvider>
|
||||
</LocationProvider>
|
||||
</EditorContextProvider>
|
||||
</PromptRefProvider>
|
||||
@@ -386,6 +371,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
||||
const pluginRuntime = usePluginRuntime()
|
||||
const attention = createTuiAttention({ renderer, config: tuiConfig, kv })
|
||||
const clipboard = useClipboard()
|
||||
const update = useUpdateNotification()
|
||||
|
||||
const api = createTuiApi(
|
||||
createTuiApiAdapters({
|
||||
@@ -771,6 +757,17 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
||||
},
|
||||
category: "System",
|
||||
},
|
||||
...(update.open
|
||||
? [
|
||||
{
|
||||
name: "opencode.update",
|
||||
title: "Update OpenCode",
|
||||
slashName: "update",
|
||||
run: () => update.open?.(),
|
||||
category: "System",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: "opencode.debug",
|
||||
title: "View debug info",
|
||||
@@ -1030,54 +1027,6 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
||||
})
|
||||
})
|
||||
|
||||
event.on("installation.update-available", async (evt) => {
|
||||
console.log("installation.update-available", evt)
|
||||
const version = evt.properties.version
|
||||
|
||||
const skipped = kv.get("skipped_version")
|
||||
if (skipped && !isVersionGreater(version, skipped)) return
|
||||
|
||||
const choice = await DialogConfirm.show(
|
||||
dialog,
|
||||
`Update Available`,
|
||||
`A new release v${version} is available. Would you like to update now?`,
|
||||
"skip",
|
||||
)
|
||||
|
||||
if (choice === false) {
|
||||
kv.set("skipped_version", version)
|
||||
return
|
||||
}
|
||||
|
||||
if (choice !== true) return
|
||||
|
||||
toast.show({
|
||||
variant: "info",
|
||||
message: `Updating to v${version}…`,
|
||||
duration: 30000,
|
||||
})
|
||||
|
||||
const result = await sdk.client.global.upgrade({ target: version })
|
||||
|
||||
if (result.error || !result.data?.success) {
|
||||
toast.show({
|
||||
variant: "error",
|
||||
title: "Update Failed",
|
||||
message: "Update failed",
|
||||
duration: 10000,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await DialogAlert.show(
|
||||
dialog,
|
||||
"Update Complete",
|
||||
`Successfully updated to OpenCode v${result.data.version}. Please restart the application.`,
|
||||
)
|
||||
|
||||
void exit()
|
||||
})
|
||||
|
||||
const plugin = createMemo(() => {
|
||||
if (!ready()) return
|
||||
if (route.data.type !== "plugin") return
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { createEffect, createMemo, createResource, createSignal, For, Match, Show, Switch } from "solid-js"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { isVersionGreater, type UpdateState, type UpdateTarget } from "../context/update-notification"
|
||||
import { useDialog } from "../ui/dialog"
|
||||
import { useBindings } from "../keymap"
|
||||
import { errorMessage } from "../util/error"
|
||||
import { Spinner } from "./spinner"
|
||||
import { openUrl } from "@opencode-ai/core/open"
|
||||
|
||||
export const V2_ANNOUNCEMENT_URL = "https://anoma.ly/notes/opencode-2-0"
|
||||
|
||||
type Version = { current: string; latest?: string }
|
||||
|
||||
// A newer OpenCode 1 release uses the normal update flow; OpenCode 2.0 is only offered when OpenCode 1 is current.
|
||||
export function updateTarget(version: Version): UpdateTarget {
|
||||
if (version.latest && isUpdate(version.current, version.latest)) return { type: "latest", version: version.latest }
|
||||
return { type: "major" }
|
||||
}
|
||||
|
||||
// Only a newer release in the installed major version is a normal update. OpenCode 2 has its own offer.
|
||||
export function isUpdate(current: string, latest: string) {
|
||||
const major = (value: string) => value.replace(/^v/, "").split(".")[0]
|
||||
return major(latest) === major(current) && isVersionGreater(latest, current)
|
||||
}
|
||||
|
||||
export function DialogUpdate(props: {
|
||||
check: () => Promise<Version | undefined>
|
||||
state: () => UpdateState | undefined
|
||||
install: (target: UpdateTarget) => Promise<void>
|
||||
skip: (version: string) => void
|
||||
dismiss: () => void
|
||||
restart: () => void
|
||||
}) {
|
||||
const dialog = useDialog()
|
||||
const { theme } = useTheme()
|
||||
const [error, setError] = createSignal<string>()
|
||||
const [active, setActive] = createSignal(0)
|
||||
const [check] = createResource(() =>
|
||||
props.check().catch((error: unknown) => {
|
||||
setError(errorMessage(error))
|
||||
return undefined
|
||||
}),
|
||||
)
|
||||
|
||||
const target = () => {
|
||||
const version = check()
|
||||
if (!version?.latest) return undefined
|
||||
return updateTarget(version)
|
||||
}
|
||||
const state = createMemo(() => {
|
||||
const current = props.state()
|
||||
if (current?.type === "installing" || current?.type === "installed") return current
|
||||
if (check.loading) return { type: "checking" as const }
|
||||
if (current?.type === "failed") return current
|
||||
const next = target()
|
||||
if (!next) return { type: "check-failed" as const, message: error() ?? "Couldn't check for updates." }
|
||||
if (next.type === "major") return { type: "major" as const }
|
||||
return { type: "available" as const, version: next.version }
|
||||
})
|
||||
const buttons = createMemo(() => {
|
||||
const current = state()
|
||||
if (current.type === "checking" || current.type === "installing") return []
|
||||
const next = target()
|
||||
const close = {
|
||||
label: next?.type === "major" ? "Dismiss" : "Skip",
|
||||
run: () => {
|
||||
if (current.type !== "installed" && next?.type === "major") props.dismiss()
|
||||
if (current.type !== "installed" && next?.type === "latest") props.skip(next.version)
|
||||
dialog.clear()
|
||||
},
|
||||
}
|
||||
if (current.type === "installed") return [close, { label: "Restart", run: props.restart }]
|
||||
if (!next) return [close]
|
||||
return [close, { label: next.type === "major" ? "Upgrade" : "Update", run: () => props.install(next) }]
|
||||
})
|
||||
|
||||
createEffect(() => setActive(Math.max(0, buttons().length - 1)))
|
||||
|
||||
const move = (offset: number) => {
|
||||
const count = buttons().length
|
||||
if (count) setActive((value) => (value + offset + count) % count)
|
||||
}
|
||||
|
||||
useBindings(() => ({
|
||||
bindings: [
|
||||
{ key: "return", desc: "Confirm update action", group: "Dialog", cmd: () => void buttons()[active()]?.run() },
|
||||
{ key: "left", desc: "Previous update action", group: "Dialog", cmd: () => move(-1) },
|
||||
{ key: "shift+tab", desc: "Previous update action", group: "Dialog", cmd: () => move(-1) },
|
||||
{ key: "right", desc: "Next update action", group: "Dialog", cmd: () => move(1) },
|
||||
{ key: "tab", desc: "Next update action", group: "Dialog", cmd: () => move(1) },
|
||||
],
|
||||
}))
|
||||
|
||||
return (
|
||||
<box paddingLeft={2} paddingRight={2} gap={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
||||
{state().type === "installing"
|
||||
? "Updating OpenCode"
|
||||
: state().type === "available" || state().type === "major" || state().type === "failed"
|
||||
? "Update available"
|
||||
: "Update"}
|
||||
</text>
|
||||
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
||||
esc
|
||||
</text>
|
||||
</box>
|
||||
<box paddingBottom={1} gap={1}>
|
||||
<Show when={state()} keyed>
|
||||
{(current) => (
|
||||
<Switch>
|
||||
<Match when={current.type === "checking"}>
|
||||
<Spinner color={theme.text}>Checking for updates…</Spinner>
|
||||
</Match>
|
||||
<Match when={current.type === "available"}>
|
||||
<text fg={theme.textMuted} wrapMode="word">
|
||||
An update is available. After installing, you'll be prompted to restart OpenCode.
|
||||
</text>
|
||||
</Match>
|
||||
<Match when={current.type === "installing" && current} keyed>
|
||||
{(installing) => (
|
||||
<Spinner color={theme.text}>
|
||||
{installing.target.type === "major"
|
||||
? "Installing OpenCode 2.0…"
|
||||
: `Installing OpenCode ${installing.target.version}…`}
|
||||
</Spinner>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={current.type === "installed" && current} keyed>
|
||||
{(installed) => (
|
||||
<text fg={theme.textMuted} wrapMode="word">
|
||||
{installed.target.type === "major"
|
||||
? "Update successful! A restart is required. Run `opencode` to start OpenCode 2.0."
|
||||
: "Update successful! A restart is required."}
|
||||
</text>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={current.type === "major"}>
|
||||
<box gap={1}>
|
||||
<box>
|
||||
<text fg={theme.textMuted} wrapMode="word">
|
||||
OpenCode 2.0 is the next major release.
|
||||
</text>
|
||||
<text
|
||||
fg={theme.textMuted}
|
||||
wrapMode="word"
|
||||
onMouseUp={() => {
|
||||
openUrl(V2_ANNOUNCEMENT_URL).catch(() => {})
|
||||
}}
|
||||
>
|
||||
Read more about it:{" "}
|
||||
<a href={V2_ANNOUNCEMENT_URL} style={{ fg: theme.primary }}>
|
||||
{V2_ANNOUNCEMENT_URL}
|
||||
</a>
|
||||
</text>
|
||||
</box>
|
||||
<text fg={theme.textMuted} wrapMode="word">
|
||||
It contains some breaking changes.
|
||||
</text>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={(current.type === "failed" || current.type === "check-failed") && current} keyed>
|
||||
{(failed) => (
|
||||
<text fg={theme.error} wrapMode="word">
|
||||
{failed.message}
|
||||
</text>
|
||||
)}
|
||||
</Match>
|
||||
</Switch>
|
||||
)}
|
||||
</Show>
|
||||
</box>
|
||||
<Show when={buttons().length > 0}>
|
||||
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
|
||||
<For each={buttons()}>
|
||||
{(button, index) => (
|
||||
<box
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
backgroundColor={active() === index() ? theme.primary : undefined}
|
||||
onMouseUp={() => void button.run()}
|
||||
>
|
||||
<text fg={active() === index() ? theme.selectedListItemText : theme.textMuted}>{button.label}</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { useKV } from "./kv"
|
||||
import { useExit } from "./exit"
|
||||
import { useDialog } from "../ui/dialog"
|
||||
import { errorMessage } from "../util/error"
|
||||
import { DialogUpdate } from "../component/dialog-update"
|
||||
|
||||
export type UpdateTarget = { readonly type: "latest"; readonly version: string } | { readonly type: "major" }
|
||||
|
||||
export type UpdateState =
|
||||
| { readonly type: "installing"; readonly target: UpdateTarget }
|
||||
| { readonly type: "installed"; readonly target: UpdateTarget }
|
||||
| { readonly type: "failed"; readonly target: UpdateTarget; readonly message: string }
|
||||
|
||||
// A newer OpenCode 1 release wins; OpenCode 2.0 is only suggested once OpenCode 1 is up to date.
|
||||
export type UpdateNotice = { readonly type: "available"; readonly version: string } | { readonly type: "major" }
|
||||
|
||||
// Implemented by the CLI process so checks and installs run against this machine's installation.
|
||||
export type UpdateSource = {
|
||||
readonly subscribe: (notify: (notice: UpdateNotice) => void) => () => void
|
||||
readonly check: () => Promise<{ readonly current: string; readonly latest?: string }>
|
||||
readonly apply: (target: UpdateTarget) => Promise<void>
|
||||
}
|
||||
|
||||
export const DISMISS_DURATION = 7 * 24 * 60 * 60 * 1000
|
||||
|
||||
// Skipping an OpenCode 1 version hides it until a newer one ships; dismissing the OpenCode 2.0 offer hides it for a week.
|
||||
export function visibleNotice(
|
||||
notice: UpdateNotice | undefined,
|
||||
hidden: { readonly skippedVersion?: string; readonly dismissedUntil?: number },
|
||||
now: number,
|
||||
) {
|
||||
if (!notice) return undefined
|
||||
if (notice.type === "major")
|
||||
return hidden.dismissedUntil !== undefined && now < hidden.dismissedUntil ? undefined : notice
|
||||
if (hidden.skippedVersion && !isVersionGreater(notice.version, hidden.skippedVersion)) return undefined
|
||||
return notice
|
||||
}
|
||||
|
||||
export const MAJOR_INSTRUCTIONS = "https://opencode.ai/v2/docs"
|
||||
|
||||
export const { use: useUpdateNotification, provider: UpdateNotificationProvider } = createSimpleContext({
|
||||
name: "UpdateNotification",
|
||||
init: (props: { updater?: UpdateSource }) => {
|
||||
const kv = useKV()
|
||||
const dialog = useDialog()
|
||||
const exit = useExit()
|
||||
const [available, setAvailable] = createSignal<UpdateNotice>()
|
||||
const [state, setState] = createSignal<UpdateState>()
|
||||
const [now, setNow] = createSignal(Date.now())
|
||||
const clock = setInterval(() => setNow(Date.now()), 10 * 60 * 1000)
|
||||
onCleanup(() => clearInterval(clock))
|
||||
|
||||
// Checks only announce a version. Installing always goes through /update.
|
||||
onMount(() => {
|
||||
if (!props.updater) return
|
||||
onCleanup(props.updater.subscribe(setAvailable))
|
||||
})
|
||||
|
||||
const notification = createMemo(() => {
|
||||
const current = state()
|
||||
if (current?.type === "installed") return current
|
||||
return visibleNotice(
|
||||
available(),
|
||||
{ skippedVersion: kv.get("skipped_version"), dismissedUntil: kv.get("update_dismissed_until") },
|
||||
now(),
|
||||
)
|
||||
})
|
||||
|
||||
const install = async (target: UpdateTarget) => {
|
||||
const updater = props.updater
|
||||
if (!updater || state()?.type === "installing") return
|
||||
setState({ type: "installing", target })
|
||||
await updater.apply(target).then(
|
||||
() => setState({ type: "installed", target }),
|
||||
(error: unknown) => {
|
||||
const message = errorMessage(error)
|
||||
setState({
|
||||
type: "failed",
|
||||
target,
|
||||
message:
|
||||
target.type === "major" && !message.includes(MAJOR_INSTRUCTIONS)
|
||||
? `${message}\nTo install OpenCode 2.0 manually, see ${MAJOR_INSTRUCTIONS}`
|
||||
: message,
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const open = () => {
|
||||
const updater = props.updater
|
||||
if (!updater) return
|
||||
dialog.replace(() => (
|
||||
<DialogUpdate
|
||||
check={updater.check}
|
||||
state={state}
|
||||
install={install}
|
||||
skip={(version) => kv.set("skipped_version", version)}
|
||||
dismiss={() => {
|
||||
const at = Date.now()
|
||||
kv.set("update_dismissed_until", at + DISMISS_DURATION)
|
||||
setNow(at)
|
||||
}}
|
||||
restart={() => exit()}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
return {
|
||||
notification,
|
||||
open: props.updater ? open : undefined,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export function isVersionGreater(left: string, right: string) {
|
||||
const parse = (value: string) => {
|
||||
const [core, prerelease] = value.replace(/^v/, "").split("-", 2)
|
||||
return { core: core.split(".").map((part) => Number.parseInt(part, 10) || 0), prerelease }
|
||||
}
|
||||
const a = parse(left)
|
||||
const b = parse(right)
|
||||
for (let index = 0; index < Math.max(a.core.length, b.core.length); index++) {
|
||||
const difference = (a.core[index] ?? 0) - (b.core[index] ?? 0)
|
||||
if (difference) return difference > 0
|
||||
}
|
||||
if (a.prerelease === b.prerelease) return false
|
||||
if (!a.prerelease) return true
|
||||
if (!b.prerelease) return false
|
||||
return a.prerelease.localeCompare(b.prerelease, undefined, { numeric: true }) > 0
|
||||
}
|
||||
@@ -167,7 +167,6 @@ const TIPS: Tip[] = [
|
||||
(shortcuts) => press(shortcuts.agentCycle(), "to cycle between Build and Plan agents"),
|
||||
"Use {highlight}/undo{/highlight} to revert the last message and file changes",
|
||||
"Use {highlight}/redo{/highlight} to restore previously undone messages and file changes",
|
||||
"Run {highlight}/share{/highlight} to create a public opencode.ai link",
|
||||
"Drag and drop images or PDFs into the terminal as context",
|
||||
(shortcuts) => press(shortcuts.inputPaste(), "to paste images from your clipboard into the prompt"),
|
||||
(shortcuts) => `Use ${commandText("/editor", shortcuts.editorOpen())} to compose messages in your external editor`,
|
||||
@@ -259,9 +258,6 @@ const TIPS: Tip[] = [
|
||||
'Set {highlight}"tools": {"bash": false}{/highlight} to disable specific tools',
|
||||
'Set {highlight}"mcp_*": false{/highlight} to disable all tools from an MCP server',
|
||||
"Override global tool settings per agent configuration",
|
||||
'Set {highlight}"share": "auto"{/highlight} to automatically share all sessions',
|
||||
'Set {highlight}"share": "disabled"{/highlight} to prevent any session sharing',
|
||||
"Run {highlight}/unshare{/highlight} to remove a session from public access",
|
||||
"Permission {highlight}doom_loop{/highlight} prevents infinite tool call loops",
|
||||
"Permission {highlight}external_directory{/highlight} protects files outside project",
|
||||
"Run {highlight}opencode debug config{/highlight} to troubleshoot configuration",
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { run, type TuiInput } from "./app"
|
||||
export type { UpdateNotice, UpdateSource } from "./context/update-notification"
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Prompt, type PromptRef } from "../component/prompt"
|
||||
import { createEffect, createMemo, createSignal, onMount } from "solid-js"
|
||||
import { createEffect, createMemo, createSignal, onMount, Show } from "solid-js"
|
||||
import { Logo } from "../component/logo"
|
||||
import { useSync } from "../context/sync"
|
||||
import { Toast } from "../ui/toast"
|
||||
@@ -12,6 +12,9 @@ import { useEditorContext } from "../context/editor"
|
||||
import { useTerminalDimensions } from "@opentui/solid"
|
||||
import { useTuiConfig } from "../config"
|
||||
import { HomeSessionDestinationProvider } from "./home/session-destination"
|
||||
import { useUpdateNotification } from "../context/update-notification"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useExit } from "../context/exit"
|
||||
|
||||
let once = false
|
||||
const placeholder = {
|
||||
@@ -30,6 +33,7 @@ export function Home() {
|
||||
const editor = useEditorContext()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const tuiConfig = useTuiConfig()
|
||||
const [logoWidth, setLogoWidth] = createSignal(0)
|
||||
const promptMaxWidth = createMemo(() => {
|
||||
const configured = tuiConfig.prompt?.max_width
|
||||
if (configured === "auto") return Math.max(75, Math.floor(dimensions().width * 0.7))
|
||||
@@ -72,12 +76,18 @@ export function Home() {
|
||||
<box flexGrow={1} alignItems="center" paddingLeft={2} paddingRight={2}>
|
||||
<box flexGrow={1} minHeight={0} />
|
||||
<box height={4} minHeight={0} flexShrink={1} />
|
||||
<box flexShrink={0}>
|
||||
<box
|
||||
flexShrink={0}
|
||||
onSizeChange={function () {
|
||||
setLogoWidth(this.width)
|
||||
}}
|
||||
>
|
||||
<pluginRuntime.Slot name="home_logo" mode="replace">
|
||||
<Logo />
|
||||
</pluginRuntime.Slot>
|
||||
</box>
|
||||
<box height={1} minHeight={0} flexShrink={1} />
|
||||
<UpdateNotification width={logoWidth()} />
|
||||
<box width="100%" maxWidth={promptMaxWidth()} zIndex={1000} paddingTop={1} flexShrink={0}>
|
||||
<pluginRuntime.Slot name="home_prompt" mode="replace" ref={bind}>
|
||||
<Prompt ref={bind} right={<pluginRuntime.Slot name="home_prompt_right" />} placeholders={placeholder} />
|
||||
@@ -93,3 +103,47 @@ export function Home() {
|
||||
</HomeSessionDestinationProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function UpdateNotification(props: { width: number }) {
|
||||
const update = useUpdateNotification()
|
||||
const exit = useExit()
|
||||
const { theme } = useTheme()
|
||||
const [hovered, setHovered] = createSignal(false)
|
||||
createEffect(() => {
|
||||
update.notification()
|
||||
setHovered(false)
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={update.notification()} keyed>
|
||||
{(state) => (
|
||||
<box
|
||||
flexShrink={0}
|
||||
flexDirection="row"
|
||||
justifyContent="center"
|
||||
width={props.width}
|
||||
maxWidth="100%"
|
||||
gap={1}
|
||||
backgroundColor={hovered() ? theme.backgroundElement : undefined}
|
||||
onMouseOver={() => setHovered(true)}
|
||||
onMouseOut={() => setHovered(false)}
|
||||
onMouseUp={() => {
|
||||
if (state.type === "installed") return exit()
|
||||
update.open?.()
|
||||
}}
|
||||
>
|
||||
<text fg={theme.textMuted}>
|
||||
<span style={{ fg: theme.primary }}>{state.type === "installed" ? "/exit" : "/update"}</span>
|
||||
{state.type === "available"
|
||||
? ` to install v${state.version}`
|
||||
: state.type === "major"
|
||||
? " to upgrade to OpenCode 2.0"
|
||||
: state.target.type === "major"
|
||||
? " restart to use OpenCode 2.0"
|
||||
: ` restart to use v${state.target.version}`}
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
@@ -113,12 +113,10 @@ function goUpsellKeys(action: RetryAction) {
|
||||
}
|
||||
|
||||
const sessionBindingCommands = [
|
||||
"session.share",
|
||||
"session.rename",
|
||||
"session.timeline",
|
||||
"session.fork",
|
||||
"session.compact",
|
||||
"session.unshare",
|
||||
"session.undo",
|
||||
"session.redo",
|
||||
"session.sidebar.toggle",
|
||||
@@ -463,46 +461,6 @@ export function Session() {
|
||||
}
|
||||
|
||||
const sessionCommandList = createMemo(() => [
|
||||
{
|
||||
title: session()?.share?.url ? "Copy share link" : "Share session",
|
||||
value: "session.share",
|
||||
suggested: route.type === "session",
|
||||
category: "Session",
|
||||
enabled: sync.data.config.share !== "disabled",
|
||||
slash: {
|
||||
name: "share",
|
||||
},
|
||||
run: async () => {
|
||||
const copy = (url: string) =>
|
||||
clipboard
|
||||
.write?.(url)
|
||||
.then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" }))
|
||||
.catch(() => toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }))
|
||||
const url = session()?.share?.url
|
||||
if (url) {
|
||||
await copy(url)
|
||||
dialog.clear()
|
||||
return
|
||||
}
|
||||
if (!kv.get("share_consent", false)) {
|
||||
const ok = await DialogConfirm.show(dialog, "Share Session", "Are you sure you want to share it?")
|
||||
if (ok !== true) return
|
||||
kv.set("share_consent", true)
|
||||
}
|
||||
await sdk.client.session
|
||||
.share({
|
||||
sessionID: route.sessionID,
|
||||
})
|
||||
.then((res) => copy(res.data!.share!.url))
|
||||
.catch((error) => {
|
||||
toast.show({
|
||||
message: error instanceof Error ? error.message : "Failed to share session",
|
||||
variant: "error",
|
||||
})
|
||||
})
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Rename session",
|
||||
value: "session.rename",
|
||||
@@ -584,29 +542,6 @@ export function Session() {
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Unshare session",
|
||||
value: "session.unshare",
|
||||
category: "Session",
|
||||
enabled: !!session()?.share?.url,
|
||||
slash: {
|
||||
name: "unshare",
|
||||
},
|
||||
run: async () => {
|
||||
await sdk.client.session
|
||||
.unshare({
|
||||
sessionID: route.sessionID,
|
||||
})
|
||||
.then(() => toast.show({ message: "Session unshared successfully", variant: "success" }))
|
||||
.catch((error) => {
|
||||
toast.show({
|
||||
message: error instanceof Error ? error.message : "Failed to unshare session",
|
||||
variant: "error",
|
||||
})
|
||||
})
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Undo previous message",
|
||||
value: "session.undo",
|
||||
|
||||
@@ -77,9 +77,6 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
||||
</Show>
|
||||
</text>
|
||||
</Show>
|
||||
<Show when={session()!.share?.url}>
|
||||
<text fg={theme.textMuted}>{session()!.share!.url}</text>
|
||||
</Show>
|
||||
</box>
|
||||
</pluginRuntime.Slot>
|
||||
<pluginRuntime.Slot name="sidebar_content" session_id={props.sessionID} />
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { updateTarget } from "../../src/component/dialog-update"
|
||||
import { DISMISS_DURATION, isVersionGreater, visibleNotice } from "../../src/context/update-notification"
|
||||
|
||||
describe("dialog update", () => {
|
||||
test("uses the normal OpenCode 1 update when a newer release exists", () => {
|
||||
expect(updateTarget({ current: "1.4.0", latest: "1.4.2" })).toEqual({ type: "latest", version: "1.4.2" })
|
||||
})
|
||||
|
||||
test("only offers OpenCode 2 when OpenCode 1 is up to date", () => {
|
||||
expect(updateTarget({ current: "1.4.2", latest: "1.4.2" })).toEqual({ type: "major" })
|
||||
})
|
||||
|
||||
test("never treats OpenCode 2 or an older release as a normal update", () => {
|
||||
expect(updateTarget({ current: "1.4.2", latest: "2.0.0" })).toEqual({ type: "major" })
|
||||
expect(updateTarget({ current: "1.4.2", latest: "1.4.1" })).toEqual({ type: "major" })
|
||||
expect(updateTarget({ current: "1.9.0", latest: "1.10.0" })).toEqual({ type: "latest", version: "1.10.0" })
|
||||
})
|
||||
|
||||
test("skipping an OpenCode 1 version hides it until a newer one ships", () => {
|
||||
const now = 1_000_000
|
||||
expect(visibleNotice({ type: "available", version: "1.4.2" }, {}, now)).toEqual({
|
||||
type: "available",
|
||||
version: "1.4.2",
|
||||
})
|
||||
expect(visibleNotice({ type: "available", version: "1.4.2" }, { skippedVersion: "1.4.2" }, now)).toBeUndefined()
|
||||
expect(visibleNotice({ type: "available", version: "1.4.3" }, { skippedVersion: "1.4.2" }, now)).toEqual({
|
||||
type: "available",
|
||||
version: "1.4.3",
|
||||
})
|
||||
})
|
||||
|
||||
test("dismissing the OpenCode 2.0 offer hides it for a week", () => {
|
||||
const now = 1_000_000
|
||||
const dismissedUntil = now + DISMISS_DURATION
|
||||
expect(DISMISS_DURATION).toBe(7 * 24 * 60 * 60 * 1000)
|
||||
expect(visibleNotice({ type: "major" }, { dismissedUntil }, now)).toBeUndefined()
|
||||
expect(visibleNotice({ type: "major" }, { dismissedUntil }, dismissedUntil - 1)).toBeUndefined()
|
||||
expect(visibleNotice({ type: "major" }, { dismissedUntil }, dismissedUntil)).toEqual({ type: "major" })
|
||||
})
|
||||
|
||||
test("skipping and dismissing are independent", () => {
|
||||
const now = 1_000_000
|
||||
expect(visibleNotice({ type: "major" }, { skippedVersion: "1.4.2" }, now)).toEqual({ type: "major" })
|
||||
expect(
|
||||
visibleNotice({ type: "available", version: "1.4.3" }, { dismissedUntil: now + DISMISS_DURATION }, now),
|
||||
).toEqual({ type: "available", version: "1.4.3" })
|
||||
})
|
||||
|
||||
test("compares versions for skipping", () => {
|
||||
expect(isVersionGreater("1.4.2", "1.4.2")).toBe(false)
|
||||
expect(isVersionGreater("1.4.3", "1.4.2")).toBe(true)
|
||||
expect(isVersionGreater("1.5.0", "1.10.0")).toBe(false)
|
||||
expect(isVersionGreater("1.4.2", "1.4.2-beta.1")).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "1.18.35",
|
||||
"version": "1.19.3",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -133,6 +133,7 @@ OpenCode Zen هي بوابة AI تتيح لك الوصول إلى هذه الن
|
||||
| Jev 1.13 Free | jev-1.13-free | `https://opencode.ai/zen/v1/systemone` | - |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Space Bunny Free | space-bunny-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Cat Jam Free | cat-jam-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| LongCat 2.5 Preview Free | longcat-2.5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Step 5 Preview Free | step-5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Exo Free | exo-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -220,6 +221,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
| --------------------------------- | --------------- | --------------- | --------------- | --------------- |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| Space Bunny Free | Free | Free | Free | - |
|
||||
| Cat Jam Free | Free | Free | Free | - |
|
||||
| LongCat 2.5 Preview Free | Free | Free | Free | - |
|
||||
| Step 5 Preview Free | Free | Free | Free | - |
|
||||
| Exo Free | Free | Free | Free | - |
|
||||
@@ -341,6 +343,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
- Nemotron 3.5 Lightning Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
|
||||
- Big Pickle نموذج خفي ومتاح مجانا على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
|
||||
- Space Bunny Free نموذج خفي ومتاح مجانا على OpenCode لفترة محدودة. يتبع مزوده سياسة عدم الاحتفاظ بالبيانات ولا يستخدم بياناتك لتدريب النماذج.
|
||||
- Cat Jam Free نموذج خفي ومتاح مجانا على OpenCode لفترة محدودة. لا يستخدم مزوده بياناتك لتدريب النماذج، ويحتفظ بها لمدة 30 يوما لأسباب تتعلق بالسلامة.
|
||||
- LongCat 2.5 Preview Free متاح مجانا على OpenCode لفترة محدودة. يتبع مزوده سياسة عدم الاحتفاظ بالبيانات ولا يستخدم بياناتك لتدريب النماذج.
|
||||
- Step 5 Preview Free متاح مجانا على OpenCode لفترة محدودة. يتبع مزوده سياسة عدم الاحتفاظ بالبيانات ولا يستخدم بياناتك لتدريب النماذج.
|
||||
- Muse Spark 1.3 Contributor Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
|
||||
@@ -396,6 +399,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
تتم استضافة جميع نماذجنا في الولايات المتحدة والاتحاد الأوروبي. يلتزم مزوّدونا بسياسة عدم الاحتفاظ بالبيانات ولا يستخدمون بياناتك لتدريب النماذج، مع الاستثناءات التالية:
|
||||
|
||||
- Big Pickle: خلال فترته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج.
|
||||
- Cat Jam Free: لا تُستخدم المطالبات والمدخلات الأخرى في التدريب. يُحتفظ بالبيانات لمدة 30 يوما لأسباب تتعلق بالسلامة.
|
||||
- Exo Free: خلال فترته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج.
|
||||
- MiMo-V2.6-Flash Free: خلال فترته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج.
|
||||
- MiMo-V2.5 Free: خلال فترته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج.
|
||||
|
||||
@@ -138,6 +138,7 @@ Našim modelima možete pristupiti i preko sljedećih API endpointa.
|
||||
| Jev 1.13 Free | jev-1.13-free | `https://opencode.ai/zen/v1/systemone` | - |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Space Bunny Free | space-bunny-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Cat Jam Free | cat-jam-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| LongCat 2.5 Preview Free | longcat-2.5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Step 5 Preview Free | step-5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Exo Free | exo-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -227,6 +228,7 @@ Podržavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**.
|
||||
| --------------------------------- | --------------- | --------------- | --------------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| Space Bunny Free | Free | Free | Free | - |
|
||||
| Cat Jam Free | Free | Free | Free | - |
|
||||
| LongCat 2.5 Preview Free | Free | Free | Free | - |
|
||||
| Step 5 Preview Free | Free | Free | Free | - |
|
||||
| Exo Free | Free | Free | Free | - |
|
||||
@@ -348,6 +350,7 @@ Besplatni modeli:
|
||||
- Nemotron 3.5 Lightning Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model.
|
||||
- Big Pickle je stealth model koji je besplatan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model.
|
||||
- Space Bunny Free je stealth model koji je besplatan na OpenCode ograničeno vrijeme. Pružalac usluge slijedi politiku nultog zadržavanja i ne koristi vaše podatke za treniranje modela.
|
||||
- Cat Jam Free je stealth model koji je besplatan na OpenCode ograničeno vrijeme. Pružalac usluge ne koristi vaše podatke za treniranje modela i čuva ih 30 dana iz sigurnosnih razloga.
|
||||
- LongCat 2.5 Preview Free je besplatan na OpenCode ograničeno vrijeme. Pružalac usluge slijedi politiku nultog zadržavanja i ne koristi vaše podatke za treniranje modela.
|
||||
- Step 5 Preview Free je besplatan na OpenCode ograničeno vrijeme. Pružalac usluge slijedi politiku nultog zadržavanja i ne koristi vaše podatke za treniranje modela.
|
||||
- Muse Spark 1.3 Contributor Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model.
|
||||
@@ -408,6 +411,7 @@ Svi naši modeli su hostovani u US i EU. Naši provajderi prate zero-retention p
|
||||
i ne koriste vaše podatke za treniranje modela, uz sljedeće izuzetke:
|
||||
|
||||
- Big Pickle: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljšanje modela.
|
||||
- Cat Jam Free: Promptovi i drugi ulazni podaci ne koriste se za treniranje modela. Podaci se čuvaju 30 dana iz sigurnosnih razloga.
|
||||
- Exo Free: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljšanje modela.
|
||||
- MiMo-V2.6-Flash Free: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljšanje modela.
|
||||
- MiMo-V2.5 Free: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljšanje modela.
|
||||
|
||||
@@ -651,11 +651,18 @@ To upgrade to a specific version.
|
||||
opencode upgrade v0.1.48
|
||||
```
|
||||
|
||||
To upgrade to OpenCode 2 and remove OpenCode 1.
|
||||
|
||||
```bash
|
||||
opencode upgrade --major
|
||||
```
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Short | Description |
|
||||
| -------------------------------------- | ----- | ----------------------------------------------------------------- |
|
||||
| <nobr><code>{"--method"}</code></nobr> | `-m` | The installation method that was used; curl, npm, pnpm, bun, brew |
|
||||
| <nobr><code>{"--major"}</code></nobr> | | Upgrade to OpenCode 2 and remove OpenCode 1 |
|
||||
|
||||
---
|
||||
|
||||
@@ -685,7 +692,7 @@ OpenCode can be configured using environment variables.
|
||||
| `OPENCODE_TUI_CONFIG` | string | Path to TUI config file |
|
||||
| `OPENCODE_CONFIG_DIR` | string | Path to config directory |
|
||||
| `OPENCODE_CONFIG_CONTENT` | string | Inline json config content |
|
||||
| `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Disable automatic update checks |
|
||||
| `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Disable update checks and notices |
|
||||
| `OPENCODE_DISABLE_PRUNE` | boolean | Disable pruning of old data |
|
||||
| `OPENCODE_DISABLE_TERMINAL_TITLE` | boolean | Disable automatic terminal title updates |
|
||||
| `OPENCODE_PERMISSION` | string | Inlined json permissions config |
|
||||
|
||||
@@ -645,7 +645,9 @@ Note that disabling snapshots means changes made by the agent cannot be rolled b
|
||||
|
||||
### Autoupdate
|
||||
|
||||
OpenCode will automatically download any new updates when it starts up. You can disable this with the `autoupdate` option.
|
||||
OpenCode checks for new versions in the background and shows a notice when one is available. Updates are never installed automatically. Run [`/update`](/docs/tui#update) in the TUI or [`opencode upgrade`](/docs/cli#upgrade) to install one.
|
||||
|
||||
You can turn off update checks and notices with the `autoupdate` option.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
@@ -654,8 +656,7 @@ OpenCode will automatically download any new updates when it starts up. You can
|
||||
}
|
||||
```
|
||||
|
||||
If you don't want updates but want to be notified when a new version is available, set `autoupdate` to `"notify"`.
|
||||
Notice that this only works if it was not installed using a package manager such as Homebrew.
|
||||
Setting `autoupdate` to `true` or `"notify"` keeps the default behavior: you're notified, but nothing is installed until you run `/update`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -138,6 +138,7 @@ Du kan også få adgang til vores modeller gennem følgende API-endpoints.
|
||||
| Jev 1.13 Free | jev-1.13-free | `https://opencode.ai/zen/v1/systemone` | - |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Space Bunny Free | space-bunny-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Cat Jam Free | cat-jam-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| LongCat 2.5 Preview Free | longcat-2.5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Step 5 Preview Free | step-5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Exo Free | exo-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -227,6 +228,7 @@ Vi understøtter en pay-as-you-go-model. Nedenfor er priserne **pr. 1M tokens**.
|
||||
| --------------------------------- | --------------- | --------------- | --------------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| Space Bunny Free | Free | Free | Free | - |
|
||||
| Cat Jam Free | Free | Free | Free | - |
|
||||
| LongCat 2.5 Preview Free | Free | Free | Free | - |
|
||||
| Step 5 Preview Free | Free | Free | Free | - |
|
||||
| Exo Free | Free | Free | Free | - |
|
||||
@@ -348,6 +350,7 @@ De gratis modeller:
|
||||
- Nemotron 3.5 Lightning Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen.
|
||||
- Big Pickle er en stealth-model, som er gratis på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen.
|
||||
- Space Bunny Free er en stealth-model, som er gratis på OpenCode i en begrænset periode. Udbyderen følger en nul-opbevaringspolitik og bruger ikke dine data til at træne modeller.
|
||||
- Cat Jam Free er en stealth-model, som er gratis på OpenCode i en begrænset periode. Udbyderen bruger ikke dine data til at træne modeller og opbevarer dem i 30 dage af sikkerhedshensyn.
|
||||
- LongCat 2.5 Preview Free er gratis på OpenCode i en begrænset periode. Udbyderen følger en nul-opbevaringspolitik og bruger ikke dine data til at træne modeller.
|
||||
- Step 5 Preview Free er gratis på OpenCode i en begrænset periode. Udbyderen følger en nul-opbevaringspolitik og bruger ikke dine data til at træne modeller.
|
||||
- Muse Spark 1.3 Contributor Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen.
|
||||
@@ -406,6 +409,7 @@ at opkræve dig mere end $20, hvis din saldo kommer under $5.
|
||||
Alle vores modeller hostes i US og EU. Vores udbydere følger en nul-opbevaringspolitik og bruger ikke dine data til modeltræning, med følgende undtagelser:
|
||||
|
||||
- Big Pickle: I den gratis periode kan indsamlede data blive brugt til at forbedre modellen.
|
||||
- Cat Jam Free: Prompts og andre input bruges ikke til træning. Data opbevares i 30 dage af sikkerhedshensyn.
|
||||
- Exo Free: I den gratis periode kan indsamlede data blive brugt til at forbedre modellen.
|
||||
- MiMo-V2.6-Flash Free: I den gratis periode kan indsamlede data blive brugt til at forbedre modellen.
|
||||
- MiMo-V2.5 Free: I den gratis periode kan indsamlede data blive brugt til at forbedre modellen.
|
||||
|
||||
@@ -129,6 +129,7 @@ Du kannst auch über die folgenden API-Endpunkte auf unsere Modelle zugreifen.
|
||||
| Jev 1.13 Free | jev-1.13-free | `https://opencode.ai/zen/v1/systemone` | - |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Space Bunny Free | space-bunny-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Cat Jam Free | cat-jam-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| LongCat 2.5 Preview Free | longcat-2.5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Step 5 Preview Free | step-5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Exo Free | exo-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -216,6 +217,7 @@ Wir unterstützen ein Pay-as-you-go-Modell. Unten findest du die Preise **pro 1M
|
||||
| --------------------------------- | --------------- | --------------- | --------------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| Space Bunny Free | Free | Free | Free | - |
|
||||
| Cat Jam Free | Free | Free | Free | - |
|
||||
| LongCat 2.5 Preview Free | Free | Free | Free | - |
|
||||
| Step 5 Preview Free | Free | Free | Free | - |
|
||||
| Exo Free | Free | Free | Free | - |
|
||||
@@ -337,6 +339,7 @@ Die kostenlosen Modelle:
|
||||
- Nemotron 3.5 Lightning Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern.
|
||||
- Big Pickle ist ein Stealth-Modell, das für begrenzte Zeit kostenlos auf OpenCode verfügbar ist. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern.
|
||||
- Space Bunny Free ist ein Stealth-Modell, das für begrenzte Zeit kostenlos auf OpenCode verfügbar ist. Der Anbieter befolgt eine Zero-Retention-Richtlinie und verwendet deine Daten nicht zum Trainieren von Modellen.
|
||||
- Cat Jam Free ist ein Stealth-Modell, das für begrenzte Zeit kostenlos auf OpenCode verfügbar ist. Der Anbieter verwendet deine Daten nicht zum Trainieren von Modellen und speichert sie aus Sicherheitsgründen 30 Tage lang.
|
||||
- LongCat 2.5 Preview Free ist für begrenzte Zeit kostenlos auf OpenCode verfügbar. Der Anbieter befolgt eine Zero-Retention-Richtlinie und verwendet deine Daten nicht zum Trainieren von Modellen.
|
||||
- Step 5 Preview Free ist für begrenzte Zeit kostenlos auf OpenCode verfügbar. Der Anbieter befolgt eine Zero-Retention-Richtlinie und verwendet deine Daten nicht zum Trainieren von Modellen.
|
||||
- Muse Spark 1.3 Contributor Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern.
|
||||
@@ -392,6 +395,7 @@ Angenommen, du setzt ein monatliches Nutzungslimit von $20, dann wird Zen in ein
|
||||
Alle unsere Modelle werden in den USA und der EU gehostet. Unsere Provider folgen einer Zero-Retention-Richtlinie und verwenden deine Daten nicht zum Trainieren von Modellen, mit den folgenden Ausnahmen:
|
||||
|
||||
- Big Pickle: Während des kostenlosen Zeitraums können gesammelte Daten zur Verbesserung des Modells verwendet werden.
|
||||
- Cat Jam Free: Prompts und andere Eingaben werden nicht für das Training verwendet. Daten werden aus Sicherheitsgründen 30 Tage lang gespeichert.
|
||||
- Exo Free: Während des kostenlosen Zeitraums können gesammelte Daten zur Verbesserung des Modells verwendet werden.
|
||||
- MiMo-V2.6-Flash Free: Während des kostenlosen Zeitraums können gesammelte Daten zur Verbesserung des Modells verwendet werden.
|
||||
- MiMo-V2.5 Free: Während des kostenlosen Zeitraums können gesammelte Daten zur Verbesserung des Modells verwendet werden.
|
||||
|
||||
@@ -138,6 +138,7 @@ También puedes acceder a nuestros modelos a través de los siguientes endpoints
|
||||
| Jev 1.13 Free | jev-1.13-free | `https://opencode.ai/zen/v1/systemone` | - |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Space Bunny Free | space-bunny-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Cat Jam Free | cat-jam-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| LongCat 2.5 Preview Free | longcat-2.5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Step 5 Preview Free | step-5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Exo Free | exo-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -227,6 +228,7 @@ Admitimos un modelo de pago por uso. A continuación se muestran los precios **p
|
||||
| ----------------------------------- | --------------- | --------------- | ---------------- | ------------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| Space Bunny Free | Free | Free | Free | - |
|
||||
| Cat Jam Free | Free | Free | Free | - |
|
||||
| LongCat 2.5 Preview Free | Free | Free | Free | - |
|
||||
| Step 5 Preview Free | Free | Free | Free | - |
|
||||
| Exo Free | Free | Free | Free | - |
|
||||
@@ -348,6 +350,7 @@ Los modelos gratuitos:
|
||||
- Nemotron 3.5 Lightning Free está disponible en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo.
|
||||
- Big Pickle es un modelo stealth que es gratuito en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo.
|
||||
- Space Bunny Free es un modelo stealth que es gratuito en OpenCode por tiempo limitado. Su proveedor sigue una política de retención cero y no utiliza tus datos para entrenar modelos.
|
||||
- Cat Jam Free es un modelo stealth que es gratuito en OpenCode por tiempo limitado. Su proveedor no utiliza tus datos para entrenar modelos y los conserva durante 30 días por motivos de seguridad.
|
||||
- LongCat 2.5 Preview Free es gratuito en OpenCode por tiempo limitado. Su proveedor sigue una política de retención cero y no utiliza tus datos para entrenar modelos.
|
||||
- Step 5 Preview Free es gratuito en OpenCode por tiempo limitado. Su proveedor sigue una política de retención cero y no utiliza tus datos para entrenar modelos.
|
||||
- Muse Spark 1.3 Contributor Free está disponible en OpenCode por tiempo limitado. El equipo está aprovechando este período para recopilar comentarios y mejorar el modelo.
|
||||
@@ -406,6 +409,7 @@ cobrándote más de $20 si tu saldo baja de $5.
|
||||
Todos nuestros modelos están alojados en EE. UU. y la UE. Nuestros proveedores siguen una política de zero-retention y no usan tus datos para el entrenamiento de modelos, con las siguientes excepciones:
|
||||
|
||||
- Big Pickle: Durante su período gratuito, los datos recopilados pueden usarse para mejorar el modelo.
|
||||
- Cat Jam Free: Los prompts y otras entradas no se usan para el entrenamiento. Los datos se conservan durante 30 días por motivos de seguridad.
|
||||
- Exo Free: Durante su período gratuito, los datos recopilados pueden usarse para mejorar el modelo.
|
||||
- MiMo-V2.6-Flash Free: Durante su período gratuito, los datos recopilados pueden usarse para mejorar el modelo.
|
||||
- MiMo-V2.5 Free: Durante su período gratuito, los datos recopilados pueden usarse para mejorar el modelo.
|
||||
|
||||
@@ -129,6 +129,7 @@ Vous pouvez également accéder à nos modèles via les points de terminaison AP
|
||||
| Jev 1.13 Free | jev-1.13-free | `https://opencode.ai/zen/v1/systemone` | - |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Space Bunny Free | space-bunny-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Cat Jam Free | cat-jam-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| LongCat 2.5 Preview Free | longcat-2.5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Step 5 Preview Free | step-5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Exo Free | exo-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -216,6 +217,7 @@ Nous prenons en charge un modèle de paiement à l'utilisation. Vous trouverez c
|
||||
| ----------------------------------- | --------------- | --------------- | --------------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| Space Bunny Free | Free | Free | Free | - |
|
||||
| Cat Jam Free | Free | Free | Free | - |
|
||||
| LongCat 2.5 Preview Free | Free | Free | Free | - |
|
||||
| Step 5 Preview Free | Free | Free | Free | - |
|
||||
| Exo Free | Free | Free | Free | - |
|
||||
@@ -337,6 +339,7 @@ Les modèles gratuits :
|
||||
- Nemotron 3.5 Lightning Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle.
|
||||
- Big Pickle est un modèle stealth gratuit sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle.
|
||||
- Space Bunny Free est un modèle stealth gratuit sur OpenCode pour une durée limitée. Son fournisseur applique une politique de conservation nulle et n'utilise pas vos données pour entraîner des modèles.
|
||||
- Cat Jam Free est un modèle stealth gratuit sur OpenCode pour une durée limitée. Son fournisseur n'utilise pas vos données pour entraîner des modèles et les conserve pendant 30 jours pour des raisons de sécurité.
|
||||
- LongCat 2.5 Preview Free est gratuit sur OpenCode pour une durée limitée. Son fournisseur applique une politique de conservation nulle et n'utilise pas vos données pour entraîner des modèles.
|
||||
- Step 5 Preview Free est gratuit sur OpenCode pour une durée limitée. Son fournisseur applique une politique de conservation nulle et n'utilise pas vos données pour entraîner des modèles.
|
||||
- Muse Spark 1.3 Contributor Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle.
|
||||
@@ -392,6 +395,7 @@ Par exemple, si vous définissez une limite d'utilisation mensuelle à $20, Zen
|
||||
Tous nos modèles sont hébergés aux États-Unis et dans l'UE. Nos fournisseurs suivent une politique de rétention zéro et n'utilisent pas vos données pour l'entraînement des modèles, avec les exceptions suivantes :
|
||||
|
||||
- Big Pickle : Pendant sa période gratuite, les données collectées peuvent être utilisées pour améliorer le modèle.
|
||||
- Cat Jam Free : Les prompts et autres données saisies ne sont pas utilisés pour l'entraînement. Les données sont conservées pendant 30 jours pour des raisons de sécurité.
|
||||
- Exo Free : Pendant sa période gratuite, les données collectées peuvent être utilisées pour améliorer le modèle.
|
||||
- MiMo-V2.6-Flash Free : Pendant sa période gratuite, les données collectées peuvent être utilisées pour améliorer le modèle.
|
||||
- MiMo-V2.5 Free : Pendant sa période gratuite, les données collectées peuvent être utilisées pour améliorer le modèle.
|
||||
|
||||
@@ -138,6 +138,7 @@ Puoi anche accedere ai nostri modelli tramite i seguenti endpoint API.
|
||||
| Jev 1.13 Free | jev-1.13-free | `https://opencode.ai/zen/v1/systemone` | - |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Space Bunny Free | space-bunny-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Cat Jam Free | cat-jam-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| LongCat 2.5 Preview Free | longcat-2.5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Step 5 Preview Free | step-5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Exo Free | exo-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -227,6 +228,7 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**.
|
||||
| --------------------------------- | --------------- | --------------- | --------------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| Space Bunny Free | Free | Free | Free | - |
|
||||
| Cat Jam Free | Free | Free | Free | - |
|
||||
| LongCat 2.5 Preview Free | Free | Free | Free | - |
|
||||
| Step 5 Preview Free | Free | Free | Free | - |
|
||||
| Exo Free | Free | Free | Free | - |
|
||||
@@ -348,6 +350,7 @@ I modelli gratuiti:
|
||||
- Nemotron 3.5 Lightning Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello.
|
||||
- Big Pickle è un modello stealth che è gratuito su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello.
|
||||
- Space Bunny Free è un modello stealth gratuito su OpenCode per un periodo limitato. Il suo provider segue una politica di conservazione zero e non usa i tuoi dati per addestrare modelli.
|
||||
- Cat Jam Free è un modello stealth che è gratuito su OpenCode per un periodo limitato. Il suo provider non usa i tuoi dati per addestrare modelli e li conserva per 30 giorni per motivi di sicurezza.
|
||||
- LongCat 2.5 Preview Free è gratuito su OpenCode per un periodo limitato. Il suo provider segue una politica di conservazione zero e non usa i tuoi dati per addestrare modelli.
|
||||
- Step 5 Preview Free è gratuito su OpenCode per un periodo limitato. Il suo provider segue una politica di conservazione zero e non usa i tuoi dati per addestrare modelli.
|
||||
- Muse Spark 1.3 Contributor Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello.
|
||||
@@ -406,6 +409,7 @@ per addebitarti più di $20 se il tuo saldo scende sotto $5.
|
||||
Tutti i nostri modelli sono ospitati negli Stati Uniti e nell'UE. I nostri provider seguono una policy di zero-retention e non usano i tuoi dati per l'addestramento dei modelli, con le seguenti eccezioni:
|
||||
|
||||
- Big Pickle: durante il periodo gratuito, i dati raccolti possono essere usati per migliorare il modello.
|
||||
- Cat Jam Free: i prompt e gli altri input non vengono usati per l'addestramento. I dati vengono conservati per 30 giorni per motivi di sicurezza.
|
||||
- Exo Free: durante il periodo gratuito, i dati raccolti possono essere usati per migliorare il modello.
|
||||
- MiMo-V2.6-Flash Free: durante il periodo gratuito, i dati raccolti possono essere usati per migliorare il modello.
|
||||
- MiMo-V2.5 Free: durante il periodo gratuito, i dati raccolti possono essere usati per migliorare il modello.
|
||||
|
||||
@@ -129,6 +129,7 @@ OpenCode Zen は、OpenCode のほかのプロバイダーと同じように動
|
||||
| Jev 1.13 Free | jev-1.13-free | `https://opencode.ai/zen/v1/systemone` | - |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Space Bunny Free | space-bunny-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Cat Jam Free | cat-jam-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| LongCat 2.5 Preview Free | longcat-2.5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Step 5 Preview Free | step-5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Exo Free | exo-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -216,6 +217,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
| --------------------------------- | --------------- | --------------- | --------------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| Space Bunny Free | Free | Free | Free | - |
|
||||
| Cat Jam Free | Free | Free | Free | - |
|
||||
| LongCat 2.5 Preview Free | Free | Free | Free | - |
|
||||
| Step 5 Preview Free | Free | Free | Free | - |
|
||||
| Exo Free | Free | Free | Free | - |
|
||||
@@ -337,6 +339,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
- Nemotron 3.5 Lightning Free は期間限定で OpenCode で利用できます。チームはこの期間中にフィードバックを集め、モデルを改善しています。
|
||||
- Big Pickle はステルスモデルで、期間限定で OpenCode で無料提供されています。チームはこの期間中にフィードバックを集め、モデルを改善しています。
|
||||
- Space Bunny Free はステルスモデルで、期間限定で OpenCode で無料提供されています。プロバイダーはゼロ保持ポリシーに従い、データをモデルのトレーニングに使用しません。
|
||||
- Cat Jam Free はステルスモデルで、期間限定で OpenCode で無料提供されています。プロバイダーはデータをモデルのトレーニングに使用せず、安全上の理由から 30 日間保持します。
|
||||
- LongCat 2.5 Preview Free は期間限定で OpenCode で無料提供されています。プロバイダーはゼロ保持ポリシーに従い、データをモデルのトレーニングに使用しません。
|
||||
- Step 5 Preview Free は期間限定で OpenCode で無料提供されています。プロバイダーはゼロ保持ポリシーに従い、データをモデルのトレーニングに使用しません。
|
||||
- Muse Spark 1.3 Contributor Free は期間限定で OpenCode で利用できます。チームはこの期間を活用してフィードバックを収集し、モデルを改善しています。
|
||||
@@ -392,6 +395,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
すべてのモデルは米国と EU でホストされています。私たちのプロバイダーはゼロ保持ポリシーに従っており、以下の例外を除き、モデル学習にあなたのデータを使用しません。
|
||||
|
||||
- Big Pickle: 無料提供期間中、収集されたデータがモデル改善に使われる場合があります。
|
||||
- Cat Jam Free: プロンプトやその他の入力は学習に使用されません。データは安全上の理由から 30 日間保持されます。
|
||||
- Exo Free: 無料提供期間中、収集されたデータがモデル改善に使われる場合があります。
|
||||
- MiMo-V2.6-Flash Free: 無料提供期間中、収集されたデータがモデル改善に使われる場合があります。
|
||||
- MiMo-V2.5 Free: 無料提供期間中、収集されたデータがモデル改善に使われる場合があります。
|
||||
|
||||
@@ -129,6 +129,7 @@ OpenCode Zen은 OpenCode의 다른 provider와 똑같이 작동합니다.
|
||||
| Jev 1.13 Free | jev-1.13-free | `https://opencode.ai/zen/v1/systemone` | - |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Space Bunny Free | space-bunny-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Cat Jam Free | cat-jam-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| LongCat 2.5 Preview Free | longcat-2.5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Step 5 Preview Free | step-5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Exo Free | exo-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -216,6 +217,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
| --------------------------------- | --------------- | --------------- | --------------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| Space Bunny Free | Free | Free | Free | - |
|
||||
| Cat Jam Free | Free | Free | Free | - |
|
||||
| LongCat 2.5 Preview Free | Free | Free | Free | - |
|
||||
| Step 5 Preview Free | Free | Free | Free | - |
|
||||
| Exo Free | Free | Free | Free | - |
|
||||
@@ -337,6 +339,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
- Nemotron 3.5 Lightning Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다.
|
||||
- Big Pickle은 한정된 기간 동안 OpenCode에서 무료로 제공되는 stealth model입니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다.
|
||||
- Space Bunny Free는 한정된 기간 동안 OpenCode에서 무료로 제공되는 stealth model입니다. 제공업체는 데이터 미보관 정책을 따르며 사용자의 데이터를 모델 학습에 사용하지 않습니다.
|
||||
- Cat Jam Free는 한정된 기간 동안 OpenCode에서 무료로 제공되는 stealth model입니다. 제공업체는 사용자의 데이터를 모델 학습에 사용하지 않으며, 안전상의 이유로 30일 동안 보관합니다.
|
||||
- LongCat 2.5 Preview Free는 한정된 기간 동안 OpenCode에서 무료로 제공됩니다. 제공업체는 데이터 미보관 정책을 따르며 사용자의 데이터를 모델 학습에 사용하지 않습니다.
|
||||
- Step 5 Preview Free는 한정된 기간 동안 OpenCode에서 무료로 제공됩니다. 제공업체는 데이터 미보관 정책을 따르며 사용자의 데이터를 모델 학습에 사용하지 않습니다.
|
||||
- Muse Spark 1.3 Contributor Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간을 활용해 피드백을 수집하고 모델을 개선하고 있습니다.
|
||||
@@ -392,6 +395,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
모든 모델은 미국과 EU에서 호스팅됩니다. provider는 zero-retention 정책을 따르며, 다음 예외를 제외하고는 데이터를 모델 학습에 사용하지 않습니다.
|
||||
|
||||
- Big Pickle: 무료 제공 기간에는 수집된 데이터가 모델 개선에 사용될 수 있습니다.
|
||||
- Cat Jam Free: 프롬프트와 기타 입력은 학습에 사용되지 않습니다. 데이터는 안전상의 이유로 30일 동안 보관됩니다.
|
||||
- Exo Free: 무료 제공 기간에는 수집된 데이터가 모델 개선에 사용될 수 있습니다.
|
||||
- MiMo-V2.6-Flash Free: 무료 제공 기간에는 수집된 데이터가 모델 개선에 사용될 수 있습니다.
|
||||
- MiMo-V2.5 Free: 무료 제공 기간에는 수집된 데이터가 모델 개선에 사용될 수 있습니다.
|
||||
|
||||
@@ -138,6 +138,7 @@ Du kan også få tilgang til modellene våre gjennom følgende API-endepunkter.
|
||||
| Jev 1.13 Free | jev-1.13-free | `https://opencode.ai/zen/v1/systemone` | - |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Space Bunny Free | space-bunny-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Cat Jam Free | cat-jam-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| LongCat 2.5 Preview Free | longcat-2.5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Step 5 Preview Free | step-5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Exo Free | exo-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -227,6 +228,7 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1M tokens**.
|
||||
| --------------------------------- | --------------- | --------------- | --------------- | --------------- |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| Space Bunny Free | Free | Free | Free | - |
|
||||
| Cat Jam Free | Free | Free | Free | - |
|
||||
| LongCat 2.5 Preview Free | Free | Free | Free | - |
|
||||
| Step 5 Preview Free | Free | Free | Free | - |
|
||||
| Exo Free | Free | Free | Free | - |
|
||||
@@ -348,6 +350,7 @@ Gratis-modellene:
|
||||
- Nemotron 3.5 Lightning Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen.
|
||||
- Big Pickle er en stealth-modell som er gratis på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen.
|
||||
- Space Bunny Free er en stealth-modell som er gratis på OpenCode i en begrenset periode. Leverandøren følger en nulloppbevaringspolicy og bruker ikke dataene dine til å trene modeller.
|
||||
- Cat Jam Free er en stealth-modell som er gratis på OpenCode i en begrenset periode. Leverandøren bruker ikke dataene dine til å trene modeller og lagrer dem i 30 dager av sikkerhetshensyn.
|
||||
- LongCat 2.5 Preview Free er gratis på OpenCode i en begrenset periode. Leverandøren følger en nulloppbevaringspolicy og bruker ikke dataene dine til å trene modeller.
|
||||
- Step 5 Preview Free er gratis på OpenCode i en begrenset periode. Leverandøren følger en nulloppbevaringspolicy og bruker ikke dataene dine til å trene modeller.
|
||||
- Muse Spark 1.3 Contributor Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen.
|
||||
@@ -406,6 +409,7 @@ med å belaste deg mer enn $20 hvis saldoen din går under $5.
|
||||
Alle modellene våre hostes i US og EU. Leverandørene våre følger en policy for zero-retention og bruker ikke dataene dine til modelltrening, med følgende unntak:
|
||||
|
||||
- Big Pickle: I gratisperioden kan innsamlede data brukes til å forbedre modellen.
|
||||
- Cat Jam Free: Ledetekster og andre inndata brukes ikke til trening. Data lagres i 30 dager av sikkerhetshensyn.
|
||||
- Exo Free: I gratisperioden kan innsamlede data brukes til å forbedre modellen.
|
||||
- MiMo-V2.6-Flash Free: I gratisperioden kan innsamlede data brukes til å forbedre modellen.
|
||||
- MiMo-V2.5 Free: I gratisperioden kan innsamlede data brukes til å forbedre modellen.
|
||||
|
||||
@@ -138,6 +138,7 @@ Możesz też uzyskać dostęp do naszych modeli przez poniższe endpointy API.
|
||||
| Jev 1.13 Free | jev-1.13-free | `https://opencode.ai/zen/v1/systemone` | - |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Space Bunny Free | space-bunny-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Cat Jam Free | cat-jam-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| LongCat 2.5 Preview Free | longcat-2.5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Step 5 Preview Free | step-5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Exo Free | exo-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -227,6 +228,7 @@ Obsługujemy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów*
|
||||
| --------------------------------- | --------------- | --------------- | --------------- | -------------- |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| Space Bunny Free | Free | Free | Free | - |
|
||||
| Cat Jam Free | Free | Free | Free | - |
|
||||
| LongCat 2.5 Preview Free | Free | Free | Free | - |
|
||||
| Step 5 Preview Free | Free | Free | Free | - |
|
||||
| Exo Free | Free | Free | Free | - |
|
||||
@@ -348,6 +350,7 @@ Darmowe modele:
|
||||
- Nemotron 3.5 Lightning Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu.
|
||||
- Big Pickle to stealth model, który jest darmowy w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu.
|
||||
- Space Bunny Free to stealth model, który jest darmowy w OpenCode przez ograniczony czas. Dostawca stosuje zasadę zerowego przechowywania i nie używa twoich danych do trenowania modeli.
|
||||
- Cat Jam Free to stealth model, który jest darmowy w OpenCode przez ograniczony czas. Dostawca nie używa twoich danych do trenowania modeli i przechowuje je przez 30 dni ze względów bezpieczeństwa.
|
||||
- LongCat 2.5 Preview Free jest darmowy w OpenCode przez ograniczony czas. Dostawca stosuje zasadę zerowego przechowywania i nie używa twoich danych do trenowania modeli.
|
||||
- Step 5 Preview Free jest darmowy w OpenCode przez ograniczony czas. Dostawca stosuje zasadę zerowego przechowywania i nie używa twoich danych do trenowania modeli.
|
||||
- Muse Spark 1.3 Contributor Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu.
|
||||
@@ -406,6 +409,7 @@ ostatecznie obciążyć Cię kwotą wyższą niż $20, jeśli Twoje saldo spadni
|
||||
Wszystkie nasze modele są hostowane w US i EU. Nasi dostawcy stosują politykę zero-retention i nie wykorzystują Twoich danych do trenowania modeli, z następującymi wyjątkami:
|
||||
|
||||
- Big Pickle: W czasie darmowego okresu zebrane dane mogą być wykorzystywane do ulepszania modelu.
|
||||
- Cat Jam Free: Prompty i inne dane wejściowe nie są wykorzystywane do trenowania modeli. Dane są przechowywane przez 30 dni ze względów bezpieczeństwa.
|
||||
- Exo Free: W czasie darmowego okresu zebrane dane mogą być wykorzystywane do ulepszania modelu.
|
||||
- MiMo-V2.6-Flash Free: W czasie darmowego okresu zebrane dane mogą być wykorzystywane do ulepszania modelu.
|
||||
- MiMo-V2.5 Free: W czasie darmowego okresu zebrane dane mogą być wykorzystywane do ulepszania modelu.
|
||||
|
||||
@@ -129,6 +129,7 @@ Você também pode acessar nossos modelos pelos seguintes endpoints de API.
|
||||
| Jev 1.13 Free | jev-1.13-free | `https://opencode.ai/zen/v1/systemone` | - |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Space Bunny Free | space-bunny-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Cat Jam Free | cat-jam-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| LongCat 2.5 Preview Free | longcat-2.5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Step 5 Preview Free | step-5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Exo Free | exo-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -216,6 +217,7 @@ Oferecemos um modelo pay-as-you-go. Abaixo estão os preços **por 1M tokens**.
|
||||
| --------------------------------- | --------------- | --------------- | ---------------- | ---------------- |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| Space Bunny Free | Free | Free | Free | - |
|
||||
| Cat Jam Free | Free | Free | Free | - |
|
||||
| LongCat 2.5 Preview Free | Free | Free | Free | - |
|
||||
| Step 5 Preview Free | Free | Free | Free | - |
|
||||
| Exo Free | Free | Free | Free | - |
|
||||
@@ -337,6 +339,7 @@ Os modelos gratuitos:
|
||||
- Nemotron 3.5 Lightning Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo.
|
||||
- Big Pickle é um modelo stealth que está gratuito no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo.
|
||||
- Space Bunny Free é um modelo stealth gratuito no OpenCode por tempo limitado. Seu provedor segue uma política de retenção zero e não usa seus dados para treinar modelos.
|
||||
- Cat Jam Free é um modelo stealth que está gratuito no OpenCode por tempo limitado. Seu provedor não usa seus dados para treinar modelos e os retém por 30 dias por motivos de segurança.
|
||||
- LongCat 2.5 Preview Free é gratuito no OpenCode por tempo limitado. Seu provedor segue uma política de retenção zero e não usa seus dados para treinar modelos.
|
||||
- Step 5 Preview Free é gratuito no OpenCode por tempo limitado. Seu provedor segue uma política de retenção zero e não usa seus dados para treinar modelos.
|
||||
- Muse Spark 1.3 Contributor Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo.
|
||||
@@ -392,6 +395,7 @@ Por exemplo, digamos que você defina um limite mensal de uso de $20; o Zen não
|
||||
Todos os nossos modelos são hospedados nos EUA e na UE. Nossos provedores seguem uma política de zero-retention e não usam seus dados para treinamento de modelos, com as seguintes exceções:
|
||||
|
||||
- Big Pickle: Durante seu período gratuito, os dados coletados podem ser usados para melhorar o modelo.
|
||||
- Cat Jam Free: Os prompts e outras entradas não são usados para treinamento. Os dados são retidos por 30 dias por motivos de segurança.
|
||||
- Exo Free: Durante seu período gratuito, os dados coletados podem ser usados para melhorar o modelo.
|
||||
- MiMo-V2.6-Flash Free: Durante seu período gratuito, os dados coletados podem ser usados para melhorar o modelo.
|
||||
- MiMo-V2.5 Free: Durante seu período gratuito, os dados coletados podem ser usados para melhorar o modelo.
|
||||
|
||||
@@ -138,6 +138,7 @@ OpenCode Zen работает как любой другой провайдер
|
||||
| Jev 1.13 Free | jev-1.13-free | `https://opencode.ai/zen/v1/systemone` | - |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Space Bunny Free | space-bunny-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Cat Jam Free | cat-jam-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| LongCat 2.5 Preview Free | longcat-2.5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Step 5 Preview Free | step-5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Exo Free | exo-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -227,6 +228,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
| --------------------------------- | --------------- | --------------- | --------------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| Space Bunny Free | Free | Free | Free | - |
|
||||
| Cat Jam Free | Free | Free | Free | - |
|
||||
| LongCat 2.5 Preview Free | Free | Free | Free | - |
|
||||
| Step 5 Preview Free | Free | Free | Free | - |
|
||||
| Exo Free | Free | Free | Free | - |
|
||||
@@ -348,6 +350,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
- Nemotron 3.5 Lightning Free доступна в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель.
|
||||
- Big Pickle — это скрытая модель, которая доступна бесплатно в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель.
|
||||
- Space Bunny Free — это скрытая модель, которая доступна бесплатно в OpenCode ограниченное время. Поставщик соблюдает политику нулевого хранения и не использует ваши данные для обучения моделей.
|
||||
- Cat Jam Free — это скрытая модель, которая доступна бесплатно в OpenCode ограниченное время. Поставщик не использует ваши данные для обучения моделей и хранит их 30 дней в целях безопасности.
|
||||
- LongCat 2.5 Preview Free доступна бесплатно в OpenCode ограниченное время. Поставщик соблюдает политику нулевого хранения и не использует ваши данные для обучения моделей.
|
||||
- Step 5 Preview Free доступна бесплатно в OpenCode ограниченное время. Поставщик соблюдает политику нулевого хранения и не использует ваши данные для обучения моделей.
|
||||
- Muse Spark 1.3 Contributor Free доступна в OpenCode в течение ограниченного времени. Команда использует этот период для сбора отзывов и улучшения модели.
|
||||
@@ -406,6 +409,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
Все наши модели размещены в US и EU. Наши провайдеры придерживаются политики нулевого хранения и не используют ваши данные для обучения моделей, за следующими исключениями:
|
||||
|
||||
- Big Pickle: во время бесплатного периода собранные данные могут использоваться для улучшения модели.
|
||||
- Cat Jam Free: запросы и другие входные данные не используются для обучения. Данные хранятся 30 дней в целях безопасности.
|
||||
- Exo Free: во время бесплатного периода собранные данные могут использоваться для улучшения модели.
|
||||
- MiMo-V2.6-Flash Free: во время бесплатного периода собранные данные могут использоваться для улучшения модели.
|
||||
- MiMo-V2.5 Free: во время бесплатного периода собранные данные могут использоваться для улучшения модели.
|
||||
|
||||
@@ -131,6 +131,7 @@ OpenCode Zen ทำงานเหมือน provider อื่น ๆ ใน
|
||||
| Jev 1.13 Free | jev-1.13-free | `https://opencode.ai/zen/v1/systemone` | - |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Space Bunny Free | space-bunny-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Cat Jam Free | cat-jam-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| LongCat 2.5 Preview Free | longcat-2.5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Step 5 Preview Free | step-5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Exo Free | exo-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -218,6 +219,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
| --------------------------------- | --------------- | --------------- | --------------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| Space Bunny Free | Free | Free | Free | - |
|
||||
| Cat Jam Free | Free | Free | Free | - |
|
||||
| LongCat 2.5 Preview Free | Free | Free | Free | - |
|
||||
| Step 5 Preview Free | Free | Free | Free | - |
|
||||
| Exo Free | Free | Free | Free | - |
|
||||
@@ -339,6 +341,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
- Nemotron 3.5 Lightning Free เปิดให้ใช้บน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล
|
||||
- Big Pickle เป็น stealth model ที่ใช้งานฟรีบน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล
|
||||
- Space Bunny Free เป็น stealth model ที่ใช้งานฟรีบน OpenCode ในช่วงเวลาจำกัด ผู้ให้บริการใช้นโยบายไม่เก็บรักษาข้อมูลและไม่นำข้อมูลของคุณไปใช้ฝึกโมเดล
|
||||
- Cat Jam Free เป็น stealth model ที่ใช้งานฟรีบน OpenCode ในช่วงเวลาจำกัด ผู้ให้บริการไม่นำข้อมูลของคุณไปใช้ฝึกโมเดล และเก็บรักษาข้อมูลไว้ 30 วันเพื่อเหตุผลด้านความปลอดภัย
|
||||
- LongCat 2.5 Preview Free ใช้งานฟรีบน OpenCode ในช่วงเวลาจำกัด ผู้ให้บริการใช้นโยบายไม่เก็บรักษาข้อมูลและไม่นำข้อมูลของคุณไปใช้ฝึกโมเดล
|
||||
- Step 5 Preview Free ใช้งานฟรีบน OpenCode ในช่วงเวลาจำกัด ผู้ให้บริการใช้นโยบายไม่เก็บรักษาข้อมูลและไม่นำข้อมูลของคุณไปใช้ฝึกโมเดล
|
||||
- Muse Spark 1.3 Contributor Free เปิดให้ใช้งานบน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อรวบรวมความคิดเห็นและปรับปรุงโมเดล
|
||||
@@ -394,6 +397,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
โมเดลทั้งหมดของเราโฮสต์อยู่ใน US และ EU provider ของเราปฏิบัติตามนโยบาย zero-retention และจะไม่ใช้ข้อมูลของคุณเพื่อฝึกโมเดล ยกเว้นกรณีต่อไปนี้:
|
||||
|
||||
- Big Pickle: ระหว่างช่วงที่เปิดให้ใช้ฟรี ข้อมูลที่เก็บรวบรวมอาจถูกนำไปใช้เพื่อปรับปรุงโมเดล
|
||||
- Cat Jam Free: พรอมป์ต์และข้อมูลนำเข้าอื่น ๆ จะไม่ถูกนำไปใช้ฝึกโมเดล ข้อมูลจะถูกเก็บรักษาไว้ 30 วันเพื่อเหตุผลด้านความปลอดภัย
|
||||
- Exo Free: ระหว่างช่วงที่เปิดให้ใช้ฟรี ข้อมูลที่เก็บรวบรวมอาจถูกนำไปใช้เพื่อปรับปรุงโมเดล
|
||||
- MiMo-V2.6-Flash Free: ระหว่างช่วงที่เปิดให้ใช้ฟรี ข้อมูลที่เก็บรวบรวมอาจถูกนำไปใช้เพื่อปรับปรุงโมเดล
|
||||
- MiMo-V2.5 Free: ระหว่างช่วงที่เปิดให้ใช้ฟรี ข้อมูลที่เก็บรวบรวมอาจถูกนำไปใช้เพื่อปรับปรุงโมเดล
|
||||
|
||||
@@ -129,6 +129,7 @@ Modellerimize aşağıdaki API uç noktaları aracılığıyla da erişebilirsin
|
||||
| Jev 1.13 Free | jev-1.13-free | `https://opencode.ai/zen/v1/systemone` | - |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Space Bunny Free | space-bunny-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Cat Jam Free | cat-jam-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| LongCat 2.5 Preview Free | longcat-2.5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Step 5 Preview Free | step-5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Exo Free | exo-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -216,6 +217,7 @@ Kullandıkça öde modelini destekliyoruz. Aşağıda **1M token başına** fiya
|
||||
| --------------------------------- | --------------- | --------------- | --------------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| Space Bunny Free | Free | Free | Free | - |
|
||||
| Cat Jam Free | Free | Free | Free | - |
|
||||
| LongCat 2.5 Preview Free | Free | Free | Free | - |
|
||||
| Step 5 Preview Free | Free | Free | Free | - |
|
||||
| Exo Free | Free | Free | Free | - |
|
||||
@@ -337,6 +339,7 @@ Kredi kartı ücretleri maliyet üzerinden yansıtılır (%4.4 + işlem başına
|
||||
- Nemotron 3.5 Lightning Free, sınırlı bir süre için OpenCode'da ücretsizdir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor.
|
||||
- Big Pickle, sınırlı bir süre için OpenCode'da ücretsiz olan gizli bir modeldir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor.
|
||||
- Space Bunny Free, sınırlı bir süre için OpenCode'da ücretsiz olan gizli bir modeldir. Sağlayıcısı sıfır saklama politikası uygular ve verilerinizi model eğitimi için kullanmaz.
|
||||
- Cat Jam Free, sınırlı bir süre için OpenCode'da ücretsiz olan gizli bir modeldir. Sağlayıcısı verilerinizi model eğitimi için kullanmaz ve güvenlik nedeniyle 30 gün boyunca saklar.
|
||||
- LongCat 2.5 Preview Free, sınırlı bir süre için OpenCode'da ücretsizdir. Sağlayıcısı sıfır saklama politikası uygular ve verilerinizi model eğitimi için kullanmaz.
|
||||
- Step 5 Preview Free, sınırlı bir süre için OpenCode'da ücretsizdir. Sağlayıcısı sıfır saklama politikası uygular ve verilerinizi model eğitimi için kullanmaz.
|
||||
- Muse Spark 1.3 Contributor Free, sınırlı bir süre için OpenCode'da kullanılabilir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor.
|
||||
@@ -392,6 +395,7 @@ Ayrıca tüm çalışma alanı için ve ekibinizdeki her üye için aylık kulla
|
||||
Tüm modellerimiz US ve EU'da barındırılıyor. Sağlayıcılarımız zero-retention politikası uygular ve aşağıdaki istisnalar dışında verilerinizi model eğitimi için kullanmaz:
|
||||
|
||||
- Big Pickle: Ücretsiz döneminde toplanan veriler modeli iyileştirmek için kullanılabilir.
|
||||
- Cat Jam Free: İstemler ve diğer girdiler eğitim için kullanılmaz. Veriler güvenlik nedeniyle 30 gün boyunca saklanır.
|
||||
- Exo Free: Ücretsiz döneminde toplanan veriler modeli iyileştirmek için kullanılabilir.
|
||||
- MiMo-V2.6-Flash Free: Ücretsiz döneminde toplanan veriler modeli iyileştirmek için kullanılabilir.
|
||||
- MiMo-V2.5 Free: Ücretsiz döneminde toplanan veriler modeli iyileştirmek için kullanılabilir.
|
||||
|
||||
@@ -283,6 +283,16 @@ Unshare current session. [Learn more](/docs/share#un-sharing).
|
||||
|
||||
---
|
||||
|
||||
### update
|
||||
|
||||
Check for a new version of OpenCode and install it. If a newer OpenCode 1 release is available, the dialog installs it. If OpenCode 1 is up to date, it offers to upgrade you to OpenCode 2.0 instead. OpenCode never installs updates on its own, so a notice on the home screen tells you when one is available. Choosing **Skip** on an OpenCode 1 update hides the notice until a newer version is released, and choosing **Dismiss** on the OpenCode 2.0 offer hides it for a week.
|
||||
|
||||
```bash frame="none"
|
||||
/update
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Editor setup
|
||||
|
||||
Both the `/editor` and `/export` commands use the editor specified in your `EDITOR` environment variable.
|
||||
|
||||
@@ -138,6 +138,7 @@ You can also access our models through the following API endpoints.
|
||||
| Jev 1.13 Free | jev-1.13-free | `https://opencode.ai/zen/v1/systemone` | - |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Space Bunny Free | space-bunny-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Cat Jam Free | cat-jam-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| LongCat 2.5 Preview Free | longcat-2.5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Step 5 Preview Free | step-5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Exo Free | exo-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -227,6 +228,7 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
|
||||
| --------------------------------- | --------------- | --------------- | --------------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| Space Bunny Free | Free | Free | Free | - |
|
||||
| Cat Jam Free | Free | Free | Free | - |
|
||||
| LongCat 2.5 Preview Free | Free | Free | Free | - |
|
||||
| Step 5 Preview Free | Free | Free | Free | - |
|
||||
| Exo Free | Free | Free | Free | - |
|
||||
@@ -348,6 +350,7 @@ The free models:
|
||||
- Nemotron 3.5 Lightning Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
|
||||
- Big Pickle is a stealth model that's free on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
|
||||
- Space Bunny Free is a stealth model that's free on OpenCode for a limited time. Its provider follows a zero-retention policy and does not use your data for model training.
|
||||
- Cat Jam Free is a stealth model that's free on OpenCode for a limited time. Its provider does not use your data for model training and retains it for 30 days for safety reasons.
|
||||
- LongCat 2.5 Preview Free is free on OpenCode for a limited time. Its provider follows a zero-retention policy and does not use your data for model training.
|
||||
- Step 5 Preview Free is free on OpenCode for a limited time. Its provider follows a zero-retention policy and does not use your data for model training.
|
||||
- Muse Spark 1.3 Contributor Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
|
||||
@@ -406,6 +409,7 @@ charging you more than $20 if your balance goes below $5.
|
||||
All our models are hosted in the US and EU. Our providers follow a zero-retention policy and do not use your data for model training, with the following exceptions:
|
||||
|
||||
- Big Pickle: During its free period, collected data may be used to improve the model.
|
||||
- Cat Jam Free: Prompts and other inputs are not used for training. Data is retained for 30 days for safety reasons.
|
||||
- Exo Free: During its free period, collected data may be used to improve the model.
|
||||
- MiMo-V2.6-Flash Free: During its free period, collected data may be used to improve the model.
|
||||
- MiMo-V2.5 Free: During its free period, collected data may be used to improve the model.
|
||||
|
||||
@@ -129,6 +129,7 @@ OpenCode Zen 的工作方式与 OpenCode 中的任何其他提供商相同。
|
||||
| Jev 1.13 Free | jev-1.13-free | `https://opencode.ai/zen/v1/systemone` | - |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Space Bunny Free | space-bunny-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Cat Jam Free | cat-jam-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| LongCat 2.5 Preview Free | longcat-2.5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Step 5 Preview Free | step-5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Exo Free | exo-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -216,6 +217,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
| --------------------------------- | --------------- | --------------- | --------------- | -------- |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| Space Bunny Free | Free | Free | Free | - |
|
||||
| Cat Jam Free | Free | Free | Free | - |
|
||||
| LongCat 2.5 Preview Free | Free | Free | Free | - |
|
||||
| Step 5 Preview Free | Free | Free | Free | - |
|
||||
| Exo Free | Free | Free | Free | - |
|
||||
@@ -337,6 +339,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
- Nemotron 3.5 Lightning Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。
|
||||
- Big Pickle 是一个隐身模型,目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。
|
||||
- Space Bunny Free 是一个隐身模型,目前在 OpenCode 上限时免费提供。其提供商遵循零保留策略,不会将你的数据用于模型训练。
|
||||
- Cat Jam Free 是一个隐身模型,目前在 OpenCode 上限时免费提供。其提供商不会将你的数据用于模型训练,并会出于安全原因保留 30 天。
|
||||
- LongCat 2.5 Preview Free 目前在 OpenCode 上限时免费提供。其提供商遵循零保留策略,不会将你的数据用于模型训练。
|
||||
- Step 5 Preview Free 目前在 OpenCode 上限时免费提供。其提供商遵循零保留策略,不会将你的数据用于模型训练。
|
||||
- Muse Spark 1.3 Contributor Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。
|
||||
@@ -392,6 +395,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
我们所有模型都托管在 US 和 EU。我们的提供商遵循零保留政策,不会将你的数据用于模型训练,但以下情况除外:
|
||||
|
||||
- Big Pickle:在免费期间,收集的数据可能会被用于改进模型。
|
||||
- Cat Jam Free:提示词和其他输入不会用于训练。数据会出于安全原因保留 30 天。
|
||||
- Exo Free:在免费期间,收集的数据可能会被用于改进模型。
|
||||
- MiMo-V2.6-Flash Free:在免费期间,收集的数据可能会被用于改进模型。
|
||||
- MiMo-V2.5 Free:在免费期间,收集的数据可能会被用于改进模型。
|
||||
|
||||
@@ -133,6 +133,7 @@ OpenCode Zen 的運作方式和 OpenCode 中的其他供應商一樣。
|
||||
| Jev 1.13 Free | jev-1.13-free | `https://opencode.ai/zen/v1/systemone` | - |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Space Bunny Free | space-bunny-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Cat Jam Free | cat-jam-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| LongCat 2.5 Preview Free | longcat-2.5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Step 5 Preview Free | step-5-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Exo Free | exo-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -221,6 +222,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
| --------------------------------- | --------------- | --------------- | --------------- | -------- |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| Space Bunny Free | Free | Free | Free | - |
|
||||
| Cat Jam Free | Free | Free | Free | - |
|
||||
| LongCat 2.5 Preview Free | Free | Free | Free | - |
|
||||
| Step 5 Preview Free | Free | Free | Free | - |
|
||||
| Exo Free | Free | Free | Free | - |
|
||||
@@ -342,6 +344,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
- Nemotron 3.5 Lightning Free 在 OpenCode 上限時提供。團隊正在利用這段時間收集回饋並改進模型。
|
||||
- Big Pickle 是一個隱身模型,在 OpenCode 上限時免費提供。團隊正在利用這段時間收集回饋並改進模型。
|
||||
- Space Bunny Free 是一個隱身模型,在 OpenCode 上限時免費提供。其供應商遵循零保留政策,不會將你的資料用於模型訓練。
|
||||
- Cat Jam Free 是一個隱身模型,在 OpenCode 上限時免費提供。其供應商不會將你的資料用於模型訓練,並會基於安全考量保留 30 天。
|
||||
- LongCat 2.5 Preview Free 在 OpenCode 上限時免費提供。其供應商遵循零保留政策,不會將你的資料用於模型訓練。
|
||||
- Step 5 Preview Free 在 OpenCode 上限時免費提供。其供應商遵循零保留政策,不會將你的資料用於模型訓練。
|
||||
- Muse Spark 1.3 Contributor Free 在 OpenCode 上限時提供。團隊正在利用這段時間收集回饋並改進模型。
|
||||
@@ -398,6 +401,7 @@ curl https://opencode.ai/zen/v1/systemone \
|
||||
我們所有模型都託管於美國和歐盟。我們的供應商遵循零保留政策,且不會將你的資料用於模型訓練,但以下情況除外:
|
||||
|
||||
- Big Pickle: 在免費期間,收集到的資料可能會用於改進模型。
|
||||
- Cat Jam Free: 提示詞與其他輸入不會用於訓練。資料會基於安全考量保留 30 天。
|
||||
- Exo Free: 在免費期間,收集到的資料可能會用於改進模型。
|
||||
- MiMo-V2.6-Flash Free: 在免費期間,收集到的資料可能會用於改進模型。
|
||||
- MiMo-V2.5 Free: 在免費期間,收集到的資料可能會用於改進模型。
|
||||
|
||||
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user