Merge branch 'dev' into feature/session-handoff

This commit is contained in:
Adam 2026-02-12 06:10:30 -06:00 committed by GitHub
commit 89ca7986bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
576 changed files with 15054 additions and 11725 deletions

2
.github/VOUCHED.td vendored
View file

@ -8,6 +8,8 @@
# - Denounce with minus prefix: -username or -platform:username. # - Denounce with minus prefix: -username or -platform:username.
# - Optional details after a space following the handle. # - Optional details after a space following the handle.
adamdotdevin adamdotdevin
ariane-emory
-florianleibert
fwang fwang
iamdavidhill iamdavidhill
jayair jayair

View file

@ -64,10 +64,13 @@ jobs:
Requirements: Requirements:
1. Update all relevant locale docs under packages/web/src/content/docs/<locale>/ so they reflect these English page changes. 1. Update all relevant locale docs under packages/web/src/content/docs/<locale>/ so they reflect these English page changes.
2. Preserve frontmatter keys, internal links, code blocks, and existing locale-specific metadata unless the English change requires an update. 2. You MUST use the Task tool for translation work and launch subagents with subagent_type `translator` (defined in .opencode/agent/translator.md).
3. Keep locale docs structure aligned with their corresponding English pages. 3. Do not translate directly in the primary agent. Use translator subagent output as the source for locale text updates.
4. Do not modify English source docs in packages/web/src/content/docs/*.mdx. 4. Run translator subagent Task calls in parallel whenever file/locale translation work is independent.
5. If no locale updates are needed, make no changes. 5. Preserve frontmatter keys, internal links, code blocks, and existing locale-specific metadata unless the English change requires an update.
6. Keep locale docs structure aligned with their corresponding English pages.
7. Do not modify English source docs in packages/web/src/content/docs/*.mdx.
8. If no locale updates are needed, make no changes.
- name: Commit and push locale docs updates - name: Commit and push locale docs updates
if: steps.changes.outputs.has_changes == 'true' if: steps.changes.outputs.has_changes == 'true'

54
.github/workflows/sign-cli.yml vendored Normal file
View file

@ -0,0 +1,54 @@
name: sign-cli
on:
push:
branches:
- brendan/desktop-signpath
workflow_dispatch:
permissions:
contents: read
actions: read
jobs:
sign-cli:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'anomalyco/opencode'
steps:
- uses: actions/checkout@v3
with:
fetch-tags: true
- uses: ./.github/actions/setup-bun
- name: Build
run: |
./packages/opencode/script/build.ts
- name: Upload unsigned Windows CLI
id: upload_unsigned_windows_cli
uses: actions/upload-artifact@v4
with:
name: unsigned-opencode-windows-cli
path: packages/opencode/dist/opencode-windows-x64/bin/opencode.exe
if-no-files-found: error
- name: Submit SignPath signing request
id: submit_signpath_signing_request
uses: signpath/github-action-submit-signing-request@v1
with:
api-token: ${{ secrets.SIGNPATH_API_KEY }}
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
project-slug: ${{ secrets.SIGNPATH_PROJECT_SLUG }}
signing-policy-slug: ${{ secrets.SIGNPATH_SIGNING_POLICY_SLUG }}
artifact-configuration-slug: ${{ secrets.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }}
github-artifact-id: ${{ steps.upload_unsigned_windows_cli.outputs.artifact-id }}
wait-for-completion: true
output-artifact-directory: signed-opencode-cli
- name: Upload signed Windows CLI
uses: actions/upload-artifact@v4
with:
name: signed-opencode-windows-cli
path: signed-opencode-cli/*.exe
if-no-files-found: error

View file

@ -32,6 +32,9 @@ description: Use this when you are working on file operations like reading, writ
- Decode tool stderr with `Bun.readableStreamToText`. - Decode tool stderr with `Bun.readableStreamToText`.
- For large writes, use `Bun.write(Bun.file(path), text)`. - For large writes, use `Bun.write(Bun.file(path), text)`.
NOTE: Bun.file(...).exists() will return `false` if the value is a directory.
Use Filesystem.exists(...) instead if path can be file or directory
## Quick checklist ## Quick checklist
- Use Bun APIs first. - Use Bun APIs first.

View file

@ -0,0 +1,7 @@
github-policies:
runners:
allowed_groups:
- "blacksmith runners 01kbd5v56sg8tz7rea39b7ygpt"
build:
disallow_reruns: false
branch_rulesets:

View file

@ -23,7 +23,7 @@
}, },
"packages/app": { "packages/app": {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.1.59", "version": "1.1.60",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@ -73,7 +73,7 @@
}, },
"packages/console/app": { "packages/console/app": {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.1.59", "version": "1.1.60",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
@ -107,7 +107,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.1.59", "version": "1.1.60",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1", "@jsx-email/render": "1.1.1",
@ -134,7 +134,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.1.59", "version": "1.1.60",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "2.0.0", "@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2", "@ai-sdk/openai": "2.0.2",
@ -158,7 +158,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.1.59", "version": "1.1.60",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@ -182,7 +182,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.1.59", "version": "1.1.60",
"dependencies": { "dependencies": {
"@opencode-ai/app": "workspace:*", "@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
@ -215,7 +215,7 @@
}, },
"packages/enterprise": { "packages/enterprise": {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.1.59", "version": "1.1.60",
"dependencies": { "dependencies": {
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
@ -244,7 +244,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.1.59", "version": "1.1.60",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
@ -260,7 +260,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.1.59", "version": "1.1.60",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@ -301,8 +301,8 @@
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.4", "@openrouter/ai-sdk-provider": "1.5.4",
"@opentui/core": "0.1.77", "@opentui/core": "0.1.79",
"@opentui/solid": "0.1.77", "@opentui/solid": "0.1.79",
"@parcel/watcher": "2.5.1", "@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:", "@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2", "@solid-primitives/event-bus": "1.1.2",
@ -366,7 +366,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.1.59", "version": "1.1.60",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"zod": "catalog:", "zod": "catalog:",
@ -386,7 +386,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.1.59", "version": "1.1.60",
"devDependencies": { "devDependencies": {
"@hey-api/openapi-ts": "0.90.10", "@hey-api/openapi-ts": "0.90.10",
"@tsconfig/node22": "catalog:", "@tsconfig/node22": "catalog:",
@ -397,7 +397,7 @@
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.1.59", "version": "1.1.60",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@ -410,7 +410,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.1.59", "version": "1.1.60",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@ -452,7 +452,7 @@
}, },
"packages/util": { "packages/util": {
"name": "@opencode-ai/util", "name": "@opencode-ai/util",
"version": "1.1.59", "version": "1.1.60",
"dependencies": { "dependencies": {
"zod": "catalog:", "zod": "catalog:",
}, },
@ -463,7 +463,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.1.59", "version": "1.1.60",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
@ -522,7 +522,7 @@
"@tailwindcss/vite": "4.1.11", "@tailwindcss/vite": "4.1.11",
"@tsconfig/bun": "1.0.9", "@tsconfig/bun": "1.0.9",
"@tsconfig/node22": "22.0.2", "@tsconfig/node22": "22.0.2",
"@types/bun": "1.3.8", "@types/bun": "1.3.9",
"@types/luxon": "3.7.1", "@types/luxon": "3.7.1",
"@types/node": "22.13.9", "@types/node": "22.13.9",
"@types/semver": "7.7.1", "@types/semver": "7.7.1",
@ -1279,21 +1279,21 @@
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentui/core": ["@opentui/core@0.1.77", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.77", "@opentui/core-darwin-x64": "0.1.77", "@opentui/core-linux-arm64": "0.1.77", "@opentui/core-linux-x64": "0.1.77", "@opentui/core-win32-arm64": "0.1.77", "@opentui/core-win32-x64": "0.1.77", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-lE3kabm6jdqK3AuBq+O0zZrXdxt6ulmibTc57sf+AsPny6cmwYHnWI4wD6hcreFiYoQVNVvdiJchVgPtowMlEg=="], "@opentui/core": ["@opentui/core@0.1.79", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.79", "@opentui/core-darwin-x64": "0.1.79", "@opentui/core-linux-arm64": "0.1.79", "@opentui/core-linux-x64": "0.1.79", "@opentui/core-win32-arm64": "0.1.79", "@opentui/core-win32-x64": "0.1.79", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-job/t09w8A/aHb/WuaVbimu5fIffyN+PCuVO5cYhXEg/NkOkC/WdFi80B8bwncR/DBPyLAh6oJ3EG86grOVo5g=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.77", "", { "os": "darwin", "cpu": "arm64" }, "sha512-SNqmygCMEsPCW7xWjzCZ5caBf36xaprwVdAnFijGDOuIzLA4iaDa6um8cj3TJh7awenN3NTRsuRc7OuH42UH+g=="], "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.79", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kgsGniV+DM5G1P3GideyJhvfnthNKcVCAm2mPTIr9InQ3L0gS/Feh7zgwOS/jxDvdlQbOWGKMk2Z3JApeC1MLw=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.77", "", { "os": "darwin", "cpu": "x64" }, "sha512-/8fsa03swEHTQt/9NrGm98kemlU+VuTURI/OFZiH53vPDRrOYIYoa4Jyga/H7ZMcG+iFhkq97zIe+0Kw95LGmA=="], "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.79", "", { "os": "darwin", "cpu": "x64" }, "sha512-OpyAmFqAAKQ2CeFmf/oLWcNksmP6Ryx/3R5dbKXThOudMCeQvfvInJTRbc2jTn9VFpf+Qj4BgHkJg1h90tf/EA=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.77", "", { "os": "linux", "cpu": "arm64" }, "sha512-QfUXZJPc69OvqoMu+AlLgjqXrwu4IeqcBuUWYMuH8nPTeLsVUc3CBbXdV2lv9UDxWzxzrxdS4ALPaxvmEv9lsQ=="], "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.79", "", { "os": "linux", "cpu": "arm64" }, "sha512-DCa5YaknS4bWhFt8TMEGH+qmTinyzuY8hoZbO4crtWXAxofPP7Pas76Cwxlvis/PyLffA+pPxAl1l5sUZpsvqw=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.77", "", { "os": "linux", "cpu": "x64" }, "sha512-Kmfx0yUKnPj67AoXYIgL7qQo0QVsUG5Iw8aRtv6XFzXqa5SzBPhaKkKZ9yHPjOmTalZquUs+9zcCRNKpYYuL7A=="], "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.79", "", { "os": "linux", "cpu": "x64" }, "sha512-V6xjvFfHh3NGvsuuDae1KHPRZXHMEE8XL0A/GM6v4I4OCC23kDmkK60Vn6OptQwAzwwbz0X0IX+Ut/GQU9qGgA=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.77", "", { "os": "win32", "cpu": "arm64" }, "sha512-HGTscPXc7gdd23Nh1DbzUNjog1I+5IZp95XPtLftGTpjrWs60VcetXcyJqK2rQcXNxewJK5yDyaa5QyMjfEhCQ=="], "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.79", "", { "os": "win32", "cpu": "arm64" }, "sha512-sPRKnVzOdT5szI59tte7pxwwkYA+07EQN+6miFAvkFuiLmRUngONUD8HVjL7nCnxcPFqxaU4Rvl1y40ST86g8g=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.77", "", { "os": "win32", "cpu": "x64" }, "sha512-c7GijsbvVgnlzd2murIbwuwrGbcv76KdUw6WlVv7a0vex50z6xJCpv1keGzpe0QfxrZ/6fFEFX7JnwGLno0wjA=="], "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.79", "", { "os": "win32", "cpu": "x64" }, "sha512-vmQcFTvKf9fqajnDtgU6/uAsiTGwx8//khqHVBmiTEXUsiT792Ki9l8sgNughbuldqG5iZOiF6IaAWU1H67UpA=="],
"@opentui/solid": ["@opentui/solid@0.1.77", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.77", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-JY+hUbXVV+XCk6bC8dvcwawWCEmC3Gid6GDs23AJWBgHZ3TU2kRKrgwTdltm45DOq2cZXrYCt690/yE8bP+Gxg=="], "@opentui/solid": ["@opentui/solid@0.1.79", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.79", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-c5+0jexKxb8GwRDDkQ/U6isZZqClAzHccXmYiLYmSnqdoQQp2lIGHLartL+K8lfIQrsKClzP2ZHumN6nexRfRg=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@ -1853,7 +1853,7 @@
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="], "@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
@ -2181,7 +2181,7 @@
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="], "bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
"bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="], "bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],

View file

@ -1,15 +0,0 @@
{
"keep": {
"days": true,
"amount": 14
},
"auditLog": "/home/thdxr/dev/projects/sst/opencode/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json",
"files": [
{
"date": 1759827172859,
"name": "/home/thdxr/dev/projects/sst/opencode/logs/mcp-puppeteer-2025-10-07.log",
"hash": "a3d98b26edd793411b968a0d24cfeee8332138e282023c3b83ec169d55c67f16"
}
],
"hashType": "sha256"
}

View file

@ -1,48 +0,0 @@
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.879"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.880"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.191"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.192"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.267"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.268"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.276"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.277"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.838"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.839"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.499"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.500"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"}
{"arguments":{"url":"https://google.com"},"level":"debug","message":"Tool call received","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150","tool":"puppeteer_navigate"}
{"0":"n","1":"p","2":"x","level":"info","message":"Launching browser with config:","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.488"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.489"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.815"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.816"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.934"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.935"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.154"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.155"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.426"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.427"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.715"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.716"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.063"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.064"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.567"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.568"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.937"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.938"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:37.120"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:37.121"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:52.490"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:52.491"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:39:25.524"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:39:25.525"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:40:57.126"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:40:57.127"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:42:24.175"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:42:24.176"}

View file

@ -1,8 +1,8 @@
{ {
"nodeModules": { "nodeModules": {
"x86_64-linux": "sha256-cvRBvHRuunNjF07c4GVHl5rRgoTn1qfI/HdJWtOV63M=", "x86_64-linux": "sha256-pp2gb4nxiIT3VltB6Xli2wZPH32JfnMsI+BbihyU1+E=",
"aarch64-linux": "sha256-DJUI4pMZ7wQTnyOiuDHALmZz7FZtrTbzRzCuNOShmWE=", "aarch64-linux": "sha256-hJwxhBICZz/pbIxQsF/sIpZTlFIgLpcAyF44O8wxMdU=",
"aarch64-darwin": "sha256-JnkqDwuC7lNsjafV+jOGfvs8K1xC8rk5CTOW+spjiCA=", "aarch64-darwin": "sha256-DPONXP52XOg/ApdSnLp32a+K5XCOnDGhbTUto2Rme0g=",
"x86_64-darwin": "sha256-GBeTqq2vDn/mXplYNglrAT2xajjFVzB4ATHnMS0j7z4=" "x86_64-darwin": "sha256-KX1h5LRJSgthpbOPqWlbM/sPf8cvQrdRJvxtrz/FzBQ="
} }
} }

View file

@ -4,7 +4,7 @@
"description": "AI-powered development tool", "description": "AI-powered development tool",
"private": true, "private": true,
"type": "module", "type": "module",
"packageManager": "bun@1.3.8", "packageManager": "bun@1.3.9",
"scripts": { "scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"dev:desktop": "bun --cwd packages/desktop tauri dev", "dev:desktop": "bun --cwd packages/desktop tauri dev",
@ -23,7 +23,7 @@
"packages/slack" "packages/slack"
], ],
"catalog": { "catalog": {
"@types/bun": "1.3.8", "@types/bun": "1.3.9",
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
"@hono/zod-validator": "0.4.2", "@hono/zod-validator": "0.4.2",
"ulid": "3.0.1", "ulid": "3.0.1",

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.1.59", "version": "1.1.60",
"description": "", "description": "",
"type": "module", "type": "module",
"exports": { "exports": {

View file

@ -2,6 +2,7 @@ import { onCleanup, onMount } from "solid-js"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt" import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { uuid } from "@/utils/uuid"
import { getCursorPosition } from "./editor-dom" import { getCursorPosition } from "./editor-dom"
export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"] export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
@ -31,7 +32,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
const dataUrl = reader.result as string const dataUrl = reader.result as string
const attachment: ImageAttachmentPart = { const attachment: ImageAttachmentPart = {
type: "image", type: "image",
id: crypto.randomUUID?.() ?? Math.random().toString(16).slice(2), id: uuid(),
filename: file.name, filename: file.name,
mime: file.type, mime: file.type,
dataUrl, dataUrl,

View file

@ -4,6 +4,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
import { useParams } from "@solidjs/router" import { useParams } from "@solidjs/router"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
import { createScopedCache } from "@/utils/scoped-cache" import { createScopedCache } from "@/utils/scoped-cache"
import { uuid } from "@/utils/uuid"
import type { SelectedLineRange } from "@/context/file" import type { SelectedLineRange } from "@/context/file"
export type LineComment = { export type LineComment = {
@ -53,7 +54,7 @@ function createCommentSessionState(store: Store<CommentStore>, setStore: SetStor
const add = (input: Omit<LineComment, "id" | "time">) => { const add = (input: Omit<LineComment, "id" | "time">) => {
const next: LineComment = { const next: LineComment = {
id: crypto.randomUUID?.() ?? Math.random().toString(16).slice(2), id: uuid(),
time: Date.now(), time: Date.now(),
...input, ...input,
} }

View file

@ -1,3 +1,5 @@
import { uuid } from "@/utils/uuid"
type Nav = { type Nav = {
id: string id: string
dir?: string dir?: string
@ -16,8 +18,6 @@ const key = (dir: string | undefined, to: string) => `${dir ?? ""}:${to}`
const now = () => performance.now() const now = () => performance.now()
const uid = () => crypto.randomUUID?.() ?? Math.random().toString(16).slice(2)
const navs = new Map<string, Nav>() const navs = new Map<string, Nav>()
const pending = new Map<string, string>() const pending = new Map<string, string>()
const active = new Map<string, string>() const active = new Map<string, string>()
@ -94,7 +94,7 @@ function ensure(id: string, data: Omit<Nav, "marks" | "logged" | "timer">) {
export function navStart(input: { dir?: string; from?: string; to: string; trigger?: string }) { export function navStart(input: { dir?: string; from?: string; to: string; trigger?: string }) {
if (!dev) return if (!dev) return
const id = uid() const id = uuid()
const start = now() const start = now()
const nav = ensure(id, { ...input, id, start }) const nav = ensure(id, { ...input, id, start })
nav.marks["navigate:start"] = start nav.marks["navigate:start"] = start
@ -109,7 +109,7 @@ export function navParams(input: { dir?: string; from?: string; to: string }) {
const k = key(input.dir, input.to) const k = key(input.dir, input.to)
const pendingId = pending.get(k) const pendingId = pending.get(k)
if (pendingId) pending.delete(k) if (pendingId) pending.delete(k)
const id = pendingId ?? uid() const id = pendingId ?? uuid()
const start = now() const start = now()
const nav = ensure(id, { ...input, id, start, trigger: pendingId ? "key" : "route" }) const nav = ensure(id, { ...input, id, start, trigger: pendingId ? "key" : "route" })

View file

@ -0,0 +1,78 @@
import { afterEach, describe, expect, test } from "bun:test"
import { uuid } from "./uuid"
const cryptoDescriptor = Object.getOwnPropertyDescriptor(globalThis, "crypto")
const secureDescriptor = Object.getOwnPropertyDescriptor(globalThis, "isSecureContext")
const randomDescriptor = Object.getOwnPropertyDescriptor(Math, "random")
const setCrypto = (value: Partial<Crypto>) => {
Object.defineProperty(globalThis, "crypto", {
configurable: true,
value: value as Crypto,
})
}
const setSecure = (value: boolean) => {
Object.defineProperty(globalThis, "isSecureContext", {
configurable: true,
value,
})
}
const setRandom = (value: () => number) => {
Object.defineProperty(Math, "random", {
configurable: true,
value,
})
}
afterEach(() => {
if (cryptoDescriptor) {
Object.defineProperty(globalThis, "crypto", cryptoDescriptor)
}
if (secureDescriptor) {
Object.defineProperty(globalThis, "isSecureContext", secureDescriptor)
}
if (!secureDescriptor) {
delete (globalThis as { isSecureContext?: boolean }).isSecureContext
}
if (randomDescriptor) {
Object.defineProperty(Math, "random", randomDescriptor)
}
})
describe("uuid", () => {
test("uses randomUUID in secure contexts", () => {
setCrypto({ randomUUID: () => "00000000-0000-0000-0000-000000000000" })
setSecure(true)
expect(uuid()).toBe("00000000-0000-0000-0000-000000000000")
})
test("falls back in insecure contexts", () => {
setCrypto({ randomUUID: () => "00000000-0000-0000-0000-000000000000" })
setSecure(false)
setRandom(() => 0.5)
expect(uuid()).toBe("8")
})
test("falls back when randomUUID throws", () => {
setCrypto({
randomUUID: () => {
throw new DOMException("Failed", "OperationError")
},
})
setSecure(true)
setRandom(() => 0.5)
expect(uuid()).toBe("8")
})
test("falls back when randomUUID is unavailable", () => {
setCrypto({})
setSecure(true)
setRandom(() => 0.5)
expect(uuid()).toBe("8")
})
})

View file

@ -0,0 +1,12 @@
const fallback = () => Math.random().toString(16).slice(2)
export function uuid() {
const c = globalThis.crypto
if (!c || typeof c.randomUUID !== "function") return fallback()
if (typeof globalThis.isSecureContext === "boolean" && !globalThis.isSecureContext) return fallback()
try {
return c.randomUUID()
} catch {
return fallback()
}
}

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.1.59", "version": "1.1.60",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 B

View file

@ -0,0 +1,18 @@
<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(30, 0)">
<g clip-path="url(#clip0_1401_86283)">
<mask id="mask0_1401_86283" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="240" height="300">
<path d="M240 0H0V300H240V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_1401_86283)">
<path d="M180 240H60V120H180V240Z" fill="#4B4646"/>
<path d="M180 60H60V240H180V60ZM240 300H0V0H240V300Z" fill="#F1ECEC"/>
</g>
</g>
</g>
<defs>
<clipPath id="clip0_1401_86283">
<rect width="240" height="300" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 B

View file

@ -0,0 +1,18 @@
<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(30, 0)">
<g clip-path="url(#clip0_1401_86274)">
<mask id="mask0_1401_86274" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="240" height="300">
<path d="M240 0H0V300H240V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_1401_86274)">
<path d="M180 240H60V120H180V240Z" fill="#CFCECD"/>
<path d="M180 60H60V240H180V60ZM240 300H0V0H240V300Z" fill="#211E1E"/>
</g>
</g>
</g>
<defs>
<clipPath id="clip0_1401_86274">
<rect width="240" height="300" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -7,18 +7,24 @@ import { useI18n } from "~/context/i18n"
import { LocaleLinks } from "~/component/locale-links" import { LocaleLinks } from "~/component/locale-links"
import previewLogoLight from "../../asset/brand/preview-opencode-logo-light.png" import previewLogoLight from "../../asset/brand/preview-opencode-logo-light.png"
import previewLogoDark from "../../asset/brand/preview-opencode-logo-dark.png" import previewLogoDark from "../../asset/brand/preview-opencode-logo-dark.png"
import previewLogoLightSquare from "../../asset/brand/preview-opencode-logo-light-square.png"
import previewLogoDarkSquare from "../../asset/brand/preview-opencode-logo-dark-square.png"
import previewWordmarkLight from "../../asset/brand/preview-opencode-wordmark-light.png" import previewWordmarkLight from "../../asset/brand/preview-opencode-wordmark-light.png"
import previewWordmarkDark from "../../asset/brand/preview-opencode-wordmark-dark.png" import previewWordmarkDark from "../../asset/brand/preview-opencode-wordmark-dark.png"
import previewWordmarkSimpleLight from "../../asset/brand/preview-opencode-wordmark-simple-light.png" import previewWordmarkSimpleLight from "../../asset/brand/preview-opencode-wordmark-simple-light.png"
import previewWordmarkSimpleDark from "../../asset/brand/preview-opencode-wordmark-simple-dark.png" import previewWordmarkSimpleDark from "../../asset/brand/preview-opencode-wordmark-simple-dark.png"
import logoLightPng from "../../asset/brand/opencode-logo-light.png" import logoLightPng from "../../asset/brand/opencode-logo-light.png"
import logoDarkPng from "../../asset/brand/opencode-logo-dark.png" import logoDarkPng from "../../asset/brand/opencode-logo-dark.png"
import logoLightSquarePng from "../../asset/brand/opencode-logo-light-square.png"
import logoDarkSquarePng from "../../asset/brand/opencode-logo-dark-square.png"
import wordmarkLightPng from "../../asset/brand/opencode-wordmark-light.png" import wordmarkLightPng from "../../asset/brand/opencode-wordmark-light.png"
import wordmarkDarkPng from "../../asset/brand/opencode-wordmark-dark.png" import wordmarkDarkPng from "../../asset/brand/opencode-wordmark-dark.png"
import wordmarkSimpleLightPng from "../../asset/brand/opencode-wordmark-simple-light.png" import wordmarkSimpleLightPng from "../../asset/brand/opencode-wordmark-simple-light.png"
import wordmarkSimpleDarkPng from "../../asset/brand/opencode-wordmark-simple-dark.png" import wordmarkSimpleDarkPng from "../../asset/brand/opencode-wordmark-simple-dark.png"
import logoLightSvg from "../../asset/brand/opencode-logo-light.svg" import logoLightSvg from "../../asset/brand/opencode-logo-light.svg"
import logoDarkSvg from "../../asset/brand/opencode-logo-dark.svg" import logoDarkSvg from "../../asset/brand/opencode-logo-dark.svg"
import logoLightSquareSvg from "../../asset/brand/opencode-logo-light-square.svg"
import logoDarkSquareSvg from "../../asset/brand/opencode-logo-dark-square.svg"
import wordmarkLightSvg from "../../asset/brand/opencode-wordmark-light.svg" import wordmarkLightSvg from "../../asset/brand/opencode-wordmark-light.svg"
import wordmarkDarkSvg from "../../asset/brand/opencode-wordmark-dark.svg" import wordmarkDarkSvg from "../../asset/brand/opencode-wordmark-dark.svg"
import wordmarkSimpleLightSvg from "../../asset/brand/opencode-wordmark-simple-light.svg" import wordmarkSimpleLightSvg from "../../asset/brand/opencode-wordmark-simple-light.svg"
@ -135,6 +141,60 @@ export default function Brand() {
</button> </button>
</div> </div>
</div> </div>
<div>
<img src={previewLogoLightSquare} alt="OpenCode brand guidelines" />
<div data-component="actions">
<button onClick={() => downloadFile(logoLightSquarePng, "opencode-logo-light-square.png")}>
PNG
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</button>
<button onClick={() => downloadFile(logoLightSquareSvg, "opencode-logo-light-square.svg")}>
SVG
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</button>
</div>
</div>
<div>
<img src={previewLogoDarkSquare} alt="OpenCode brand guidelines" />
<div data-component="actions">
<button onClick={() => downloadFile(logoDarkSquarePng, "opencode-logo-dark-square.png")}>
PNG
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</button>
<button onClick={() => downloadFile(logoDarkSquareSvg, "opencode-logo-dark-square.svg")}>
SVG
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</button>
</div>
</div>
<div> <div>
<img src={previewWordmarkLight} alt="OpenCode brand guidelines" /> <img src={previewWordmarkLight} alt="OpenCode brand guidelines" />
<div data-component="actions"> <div data-component="actions">

View file

@ -38,6 +38,7 @@ type RetryOptions = {
excludeProviders: string[] excludeProviders: string[]
retryCount: number retryCount: number
} }
type BillingSource = "anonymous" | "free" | "byok" | "subscription" | "balance"
export async function handler( export async function handler(
input: APIEvent, input: APIEvent,
@ -51,6 +52,7 @@ export async function handler(
type AuthInfo = Awaited<ReturnType<typeof authenticate>> type AuthInfo = Awaited<ReturnType<typeof authenticate>>
type ModelInfo = Awaited<ReturnType<typeof validateModel>> type ModelInfo = Awaited<ReturnType<typeof validateModel>>
type ProviderInfo = Awaited<ReturnType<typeof selectProvider>> type ProviderInfo = Awaited<ReturnType<typeof selectProvider>>
type CostInfo = ReturnType<typeof calculateCost>
const MAX_FAILOVER_RETRIES = 3 const MAX_FAILOVER_RETRIES = 3
const MAX_429_RETRIES = 3 const MAX_429_RETRIES = 3
@ -139,21 +141,22 @@ export async function handler(
"llm.error.code": res.status, "llm.error.code": res.status,
"llm.error.message": res.statusText, "llm.error.message": res.statusText,
}) })
}
// Try another provider => stop retrying if using fallback provider // Try another provider => stop retrying if using fallback provider
if ( if (
// ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found. res.status !== 200 &&
res.status !== 404 && // ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
// ie. cannot change codex model providers mid-session res.status !== 404 &&
modelInfo.stickyProvider !== "strict" && // ie. cannot change codex model providers mid-session
modelInfo.fallbackProvider && modelInfo.stickyProvider !== "strict" &&
providerInfo.id !== modelInfo.fallbackProvider modelInfo.fallbackProvider &&
) { providerInfo.id !== modelInfo.fallbackProvider
return retriableRequest({ ) {
excludeProviders: [...retry.excludeProviders, providerInfo.id], return retriableRequest({
retryCount: retry.retryCount + 1, excludeProviders: [...retry.excludeProviders, providerInfo.id],
}) retryCount: retry.retryCount + 1,
} })
} }
return { providerInfo, reqBody, res, startTimestamp } return { providerInfo, reqBody, res, startTimestamp }
@ -183,18 +186,25 @@ export async function handler(
// Handle non-streaming response // Handle non-streaming response
if (!isStream) { if (!isStream) {
const responseConverter = createResponseConverter(providerInfo.format, opts.format)
const json = await res.json() const json = await res.json()
const body = JSON.stringify(responseConverter(json)) const usageInfo = providerInfo.normalizeUsage(json.usage)
const costInfo = calculateCost(modelInfo, usageInfo)
await trialLimiter?.track(usageInfo)
await rateLimiter?.track()
await trackUsage(billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo)
await reload(billingSource, authInfo, costInfo)
const responseConverter = createResponseConverter(providerInfo.format, opts.format)
const body = JSON.stringify(
responseConverter({
...json,
cost: calculateOccuredCost(billingSource, costInfo),
}),
)
logger.metric({ response_length: body.length }) logger.metric({ response_length: body.length })
logger.debug("RESPONSE: " + body) logger.debug("RESPONSE: " + body)
dataDumper?.provideResponse(body) dataDumper?.provideResponse(body)
dataDumper?.flush() dataDumper?.flush()
const tokensInfo = providerInfo.normalizeUsage(json.usage)
await trialLimiter?.track(tokensInfo)
await rateLimiter?.track()
const costInfo = await trackUsage(authInfo, modelInfo, providerInfo, billingSource, tokensInfo)
await reload(authInfo, costInfo)
return new Response(body, { return new Response(body, {
status: resStatus, status: resStatus,
statusText: res.statusText, statusText: res.statusText,
@ -226,12 +236,16 @@ export async function handler(
dataDumper?.flush() dataDumper?.flush()
await rateLimiter?.track() await rateLimiter?.track()
const usage = usageParser.retrieve() const usage = usageParser.retrieve()
let cost = "0"
if (usage) { if (usage) {
const tokensInfo = providerInfo.normalizeUsage(usage) const usageInfo = providerInfo.normalizeUsage(usage)
await trialLimiter?.track(tokensInfo) const costInfo = calculateCost(modelInfo, usageInfo)
const costInfo = await trackUsage(authInfo, modelInfo, providerInfo, billingSource, tokensInfo) await trialLimiter?.track(usageInfo)
await reload(authInfo, costInfo) await trackUsage(billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo)
await reload(billingSource, authInfo, costInfo)
cost = calculateOccuredCost(billingSource, costInfo)
} }
c.enqueue(encoder.encode(usageParser.buidlCostChunk(cost)))
c.close() c.close()
return return
} }
@ -283,7 +297,6 @@ export async function handler(
return pump() return pump()
}, },
}) })
return new Response(stream, { return new Response(stream, {
status: resStatus, status: resStatus,
statusText: res.statusText, statusText: res.statusText,
@ -498,9 +511,9 @@ export async function handler(
} }
} }
function validateBilling(authInfo: AuthInfo, modelInfo: ModelInfo) { function validateBilling(authInfo: AuthInfo, modelInfo: ModelInfo): BillingSource {
if (!authInfo) return "anonymous" if (!authInfo) return "anonymous"
if (authInfo.provider?.credentials) return "free" if (authInfo.provider?.credentials) return "byok"
if (authInfo.isFree) return "free" if (authInfo.isFree) return "free"
if (modelInfo.allowAnonymous) return "free" if (modelInfo.allowAnonymous) return "free"
@ -613,13 +626,7 @@ export async function handler(
return res return res
} }
async function trackUsage( function calculateCost(modelInfo: ModelInfo, usageInfo: UsageInfo) {
authInfo: AuthInfo,
modelInfo: ModelInfo,
providerInfo: ProviderInfo,
billingSource: ReturnType<typeof validateBilling>,
usageInfo: UsageInfo,
) {
const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } = const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
usageInfo usageInfo
@ -657,6 +664,33 @@ export async function handler(
(cacheReadCost ?? 0) + (cacheReadCost ?? 0) +
(cacheWrite5mCost ?? 0) + (cacheWrite5mCost ?? 0) +
(cacheWrite1hCost ?? 0) (cacheWrite1hCost ?? 0)
return {
totalCostInCent,
inputCost,
outputCost,
reasoningCost,
cacheReadCost,
cacheWrite5mCost,
cacheWrite1hCost,
}
}
function calculateOccuredCost(billingSource: BillingSource, costInfo: CostInfo) {
return billingSource === "balance" ? (costInfo.totalCostInCent / 100).toFixed(8) : "0"
}
async function trackUsage(
billingSource: BillingSource,
authInfo: AuthInfo,
modelInfo: ModelInfo,
providerInfo: ProviderInfo,
usageInfo: UsageInfo,
costInfo: CostInfo,
) {
const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
usageInfo
const { totalCostInCent, inputCost, outputCost, reasoningCost, cacheReadCost, cacheWrite5mCost, cacheWrite1hCost } =
costInfo
logger.metric({ logger.metric({
"tokens.input": inputTokens, "tokens.input": inputTokens,
@ -677,7 +711,7 @@ export async function handler(
if (billingSource === "anonymous") return if (billingSource === "anonymous") return
authInfo = authInfo! authInfo = authInfo!
const cost = authInfo.provider?.credentials ? 0 : centsToMicroCents(totalCostInCent) const cost = centsToMicroCents(totalCostInCent)
await Database.use((db) => await Database.use((db) =>
Promise.all([ Promise.all([
db.insert(UsageTable).values({ db.insert(UsageTable).values({
@ -772,16 +806,12 @@ export async function handler(
return { costInMicroCents: cost } return { costInMicroCents: cost }
} }
async function reload(authInfo: AuthInfo, costInfo: Awaited<ReturnType<typeof trackUsage>>) { async function reload(billingSource: BillingSource, authInfo: AuthInfo, costInfo: CostInfo) {
if (!authInfo) return if (billingSource !== "balance") return
if (authInfo.isFree) return authInfo = authInfo!
if (authInfo.provider?.credentials) return
if (authInfo.subscription) return
if (!costInfo) return
const reloadTrigger = centsToMicroCents((authInfo.billing.reloadTrigger ?? Billing.RELOAD_TRIGGER) * 100) const reloadTrigger = centsToMicroCents((authInfo.billing.reloadTrigger ?? Billing.RELOAD_TRIGGER) * 100)
if (authInfo.billing.balance - costInfo.costInMicroCents >= reloadTrigger) return if (authInfo.billing.balance - costInfo.totalCostInCent >= reloadTrigger) return
if (authInfo.billing.timeReloadLockedTill && authInfo.billing.timeReloadLockedTill > new Date()) return if (authInfo.billing.timeReloadLockedTill && authInfo.billing.timeReloadLockedTill > new Date()) return
const lock = await Database.use((tx) => const lock = await Database.use((tx) =>

View file

@ -167,6 +167,7 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) =>
} }
}, },
retrieve: () => usage, retrieve: () => usage,
buidlCostChunk: (cost: string) => `event: ping\ndata: ${JSON.stringify({ type: "ping", cost })}\n\n`,
} }
}, },
normalizeUsage: (usage: Usage) => ({ normalizeUsage: (usage: Usage) => ({

View file

@ -56,6 +56,7 @@ export const googleHelper: ProviderHelper = ({ providerModel }) => ({
usage = json.usageMetadata usage = json.usageMetadata
}, },
retrieve: () => usage, retrieve: () => usage,
buidlCostChunk: (cost: string) => `data: ${JSON.stringify({ type: "ping", cost })}\n\n`,
} }
}, },
normalizeUsage: (usage: Usage) => { normalizeUsage: (usage: Usage) => {

View file

@ -54,6 +54,7 @@ export const oaCompatHelper: ProviderHelper = () => ({
usage = json.usage usage = json.usage
}, },
retrieve: () => usage, retrieve: () => usage,
buidlCostChunk: (cost: string) => `data: ${JSON.stringify({ choices: [], cost })}\n\n`,
} }
}, },
normalizeUsage: (usage: Usage) => { normalizeUsage: (usage: Usage) => {

View file

@ -43,6 +43,7 @@ export const openaiHelper: ProviderHelper = () => ({
usage = json.response.usage usage = json.response.usage
}, },
retrieve: () => usage, retrieve: () => usage,
buidlCostChunk: (cost: string) => `event: ping\ndata: ${JSON.stringify({ type: "ping", cost })}\n\n`,
} }
}, },
normalizeUsage: (usage: Usage) => { normalizeUsage: (usage: Usage) => {

View file

@ -43,6 +43,7 @@ export type ProviderHelper = (input: { reqModel: string; providerModel: string }
createUsageParser: () => { createUsageParser: () => {
parse: (chunk: string) => void parse: (chunk: string) => void
retrieve: () => any retrieve: () => any
buidlCostChunk: (cost: string) => string
} }
normalizeUsage: (usage: any) => UsageInfo normalizeUsage: (usage: any) => UsageInfo
} }

View file

@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.1.59", "version": "1.1.60",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.1.59", "version": "1.1.60",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.1.59", "version": "1.1.60",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",

View file

@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"private": true, "private": true,
"version": "1.1.59", "version": "1.1.60",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {

View file

@ -3117,6 +3117,7 @@ dependencies = [
"tauri-plugin-window-state", "tauri-plugin-window-state",
"tauri-specta", "tauri-specta",
"tokio", "tokio",
"tokio-stream",
"tracing", "tracing",
"tracing-appender", "tracing-appender",
"tracing-subscriber", "tracing-subscriber",
@ -5631,6 +5632,18 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-stream"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
"tokio-util",
]
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.17" version = "0.7.17"

View file

@ -51,6 +51,7 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = "0.2" tracing-appender = "0.2"
chrono = "0.4" chrono = "0.4"
tokio-stream = { version = "0.1.18", features = ["sync"] }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
gtk = "0.18.2" gtk = "0.18.2"

View file

@ -1,35 +1,46 @@
use futures::{FutureExt, Stream, StreamExt, future};
use tauri::{AppHandle, Manager, path::BaseDirectory}; use tauri::{AppHandle, Manager, path::BaseDirectory};
use tauri_plugin_shell::{ use tauri_plugin_shell::{
ShellExt, ShellExt,
process::{Command, CommandChild, CommandEvent, TerminatedPayload}, process::{CommandChild, CommandEvent, TerminatedPayload},
}; };
use tauri_plugin_store::StoreExt; use tauri_plugin_store::StoreExt;
use tauri_specta::Event;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use tracing::Instrument;
use crate::constants::{SETTINGS_STORE, WSL_ENABLED_KEY}; use crate::constants::{SETTINGS_STORE, WSL_ENABLED_KEY};
const CLI_INSTALL_DIR: &str = ".opencode/bin"; const CLI_INSTALL_DIR: &str = ".opencode/bin";
const CLI_BINARY_NAME: &str = "opencode"; const CLI_BINARY_NAME: &str = "opencode";
#[derive(serde::Deserialize)] #[derive(serde::Deserialize, Debug)]
pub struct ServerConfig { pub struct ServerConfig {
pub hostname: Option<String>, pub hostname: Option<String>,
pub port: Option<u32>, pub port: Option<u32>,
} }
#[derive(serde::Deserialize)] #[derive(serde::Deserialize, Debug)]
pub struct Config { pub struct Config {
pub server: Option<ServerConfig>, pub server: Option<ServerConfig>,
} }
pub async fn get_config(app: &AppHandle) -> Option<Config> { pub async fn get_config(app: &AppHandle) -> Option<Config> {
create_command(app, "debug config", &[]) let (events, _) = spawn_command(app, "debug config", &[]).ok()?;
.output()
events
.fold(String::new(), async |mut config_str, event| {
if let CommandEvent::Stdout(stdout) = event
&& let Ok(s) = str::from_utf8(&stdout)
{
config_str += s
}
config_str
})
.map(|v| serde_json::from_str::<Config>(&v))
.await .await
.inspect_err(|e| tracing::warn!("Failed to read OC config: {e}"))
.ok() .ok()
.and_then(|out| String::from_utf8(out.stdout.to_vec()).ok())
.and_then(|s| serde_json::from_str::<Config>(&s).ok())
} }
fn get_cli_install_path() -> Option<std::path::PathBuf> { fn get_cli_install_path() -> Option<std::path::PathBuf> {
@ -175,7 +186,11 @@ fn shell_escape(input: &str) -> String {
escaped escaped
} }
pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, String)]) -> Command { pub fn spawn_command(
app: &tauri::AppHandle,
args: &str,
extra_env: &[(&str, String)],
) -> Result<(impl Stream<Item = CommandEvent> + 'static, CommandChild), tauri_plugin_shell::Error> {
let state_dir = app let state_dir = app
.path() .path()
.resolve("", BaseDirectory::AppLocalData) .resolve("", BaseDirectory::AppLocalData)
@ -202,7 +217,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St
.map(|(key, value)| (key.to_string(), value.clone())), .map(|(key, value)| (key.to_string(), value.clone())),
); );
if cfg!(windows) { let cmd = if cfg!(windows) {
if is_wsl_enabled(app) { if is_wsl_enabled(app) {
tracing::info!("WSL is enabled, spawning CLI server in WSL"); tracing::info!("WSL is enabled, spawning CLI server in WSL");
let version = app.package_info().version.to_string(); let version = app.package_info().version.to_string();
@ -234,10 +249,9 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St
script.push(format!("{} exec \"$BIN\" {}", env_prefix.join(" "), args)); script.push(format!("{} exec \"$BIN\" {}", env_prefix.join(" "), args));
return app app.shell()
.shell()
.command("wsl") .command("wsl")
.args(["-e", "bash", "-lc", &script.join("\n")]); .args(["-e", "bash", "-lc", &script.join("\n")])
} else { } else {
let mut cmd = app let mut cmd = app
.shell() .shell()
@ -249,7 +263,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St
cmd = cmd.env(key, value); cmd = cmd.env(key, value);
} }
return cmd; cmd
} }
} else { } else {
let sidecar = get_sidecar_path(app); let sidecar = get_sidecar_path(app);
@ -268,7 +282,13 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St
} }
cmd cmd
} };
let (rx, child) = cmd.spawn()?;
let event_stream = tokio_stream::wrappers::ReceiverStream::new(rx);
let event_stream = sqlite_migration::logs_middleware(app.clone(), event_stream);
Ok((event_stream, child))
} }
pub fn serve( pub fn serve(
@ -286,45 +306,96 @@ pub fn serve(
("OPENCODE_SERVER_PASSWORD", password.to_string()), ("OPENCODE_SERVER_PASSWORD", password.to_string()),
]; ];
let (mut rx, child) = create_command( let (events, child) = spawn_command(
app, app,
format!("--print-logs --log-level WARN serve --hostname {hostname} --port {port}").as_str(), format!("--print-logs --log-level WARN serve --hostname {hostname} --port {port}").as_str(),
&envs, &envs,
) )
.spawn()
.expect("Failed to spawn opencode"); .expect("Failed to spawn opencode");
tokio::spawn(async move { let mut exit_tx = Some(exit_tx);
let mut exit_tx = Some(exit_tx); tokio::spawn(
while let Some(event) = rx.recv().await { events
match event { .for_each(move |event| {
CommandEvent::Stdout(line_bytes) => { match event {
let line = String::from_utf8_lossy(&line_bytes); CommandEvent::Stdout(line_bytes) => {
tracing::info!(target: "sidecar", "{line}"); let line = String::from_utf8_lossy(&line_bytes);
} tracing::info!("{line}");
CommandEvent::Stderr(line_bytes) => {
let line = String::from_utf8_lossy(&line_bytes);
tracing::info!(target: "sidecar", "{line}");
}
CommandEvent::Error(err) => {
tracing::error!(target: "sidecar", "{err}");
}
CommandEvent::Terminated(payload) => {
tracing::info!(
target: "sidecar",
code = ?payload.code,
signal = ?payload.signal,
"Sidecar terminated"
);
if let Some(tx) = exit_tx.take() {
let _ = tx.send(payload);
} }
CommandEvent::Stderr(line_bytes) => {
let line = String::from_utf8_lossy(&line_bytes);
tracing::info!("{line}");
}
CommandEvent::Error(err) => {
tracing::error!("{err}");
}
CommandEvent::Terminated(payload) => {
tracing::info!(
code = ?payload.code,
signal = ?payload.signal,
"Sidecar terminated"
);
if let Some(tx) = exit_tx.take() {
let _ = tx.send(payload);
}
}
_ => {}
} }
_ => {}
} future::ready(())
} })
}); .instrument(tracing::info_span!("sidecar")),
);
(child, exit_rx) (child, exit_rx)
} }
pub mod sqlite_migration {
use super::*;
#[derive(
tauri_specta::Event, serde::Serialize, serde::Deserialize, Clone, Copy, Debug, specta::Type,
)]
#[serde(tag = "type", content = "value")]
pub enum SqliteMigrationProgress {
InProgress(u8),
Done,
}
pub(super) fn logs_middleware(
app: AppHandle,
stream: impl Stream<Item = CommandEvent>,
) -> impl Stream<Item = CommandEvent> {
let app = app.clone();
let mut done = false;
stream.filter_map(move |event| {
if done {
return future::ready(Some(event));
}
future::ready(match &event {
CommandEvent::Stdout(stdout) => {
let Ok(s) = str::from_utf8(stdout) else {
return future::ready(None);
};
if let Some(s) = s.strip_prefix("sqlite-migration:").map(|s| s.trim()) {
if let Ok(progress) = s.parse::<u8>() {
let _ = SqliteMigrationProgress::InProgress(progress).emit(&app);
} else if s == "done" {
done = true;
let _ = SqliteMigrationProgress::Done.emit(&app);
}
None
} else {
Some(event)
}
}
_ => Some(event),
})
})
}
}

View file

@ -24,16 +24,17 @@ use std::{
sync::{Arc, Mutex}, sync::{Arc, Mutex},
time::Duration, time::Duration,
}; };
use tauri::{AppHandle, Manager, RunEvent, State, ipc::Channel}; use tauri::{AppHandle, Listener, Manager, RunEvent, State, ipc::Channel};
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))] #[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_deep_link::DeepLinkExt;
use tauri_plugin_shell::process::CommandChild; use tauri_plugin_shell::process::CommandChild;
use tauri_specta::Event;
use tokio::{ use tokio::{
sync::{oneshot, watch}, sync::{oneshot, watch},
time::{sleep, timeout}, time::{sleep, timeout},
}; };
use crate::cli::sync_cli; use crate::cli::{sqlite_migration::SqliteMigrationProgress, sync_cli};
use crate::constants::*; use crate::constants::*;
use crate::server::get_saved_server_url; use crate::server::get_saved_server_url;
use crate::windows::{LoadingWindow, MainWindow}; use crate::windows::{LoadingWindow, MainWindow};
@ -122,8 +123,8 @@ async fn await_initialization(
let mut rx = init_state.current.clone(); let mut rx = init_state.current.clone();
let events = async { let events = async {
let e = (*rx.borrow()).clone(); let e = *rx.borrow();
let _ = events.send(e).unwrap(); let _ = events.send(e);
while rx.changed().await.is_ok() { while rx.changed().await.is_ok() {
let step = *rx.borrow_and_update(); let step = *rx.borrow_and_update();
@ -517,7 +518,10 @@ fn make_specta_builder() -> tauri_specta::Builder<tauri::Wry> {
wsl_path, wsl_path,
resolve_app_path resolve_app_path
]) ])
.events(tauri_specta::collect_events![LoadingWindowComplete]) .events(tauri_specta::collect_events![
LoadingWindowComplete,
SqliteMigrationProgress
])
.error_handling(tauri_specta::ErrorHandlingMode::Throw) .error_handling(tauri_specta::ErrorHandlingMode::Throw)
} }
@ -556,17 +560,46 @@ async fn initialize(app: AppHandle) {
tracing::info!("Main and loading windows created"); tracing::info!("Main and loading windows created");
// SQLite migration handling:
// We only do this if the sqlite db doesn't exist, and we're expecting the sidecar to create it
// First, we spawn a task that listens for SqliteMigrationProgress events that can
// come from any invocation of the sidecar CLI. The progress is captured by a stdout stream interceptor.
// Then in the loading task, we wait for sqlite migration to complete before
// starting our health check against the server, otherwise long migrations could result in a timeout.
let sqlite_enabled = option_env!("OPENCODE_SQLITE").is_some(); let sqlite_enabled = option_env!("OPENCODE_SQLITE").is_some();
let sqlite_done = (sqlite_enabled && !sqlite_file_exists()).then(|| {
tracing::info!(
path = %opencode_db_path().expect("failed to get db path").display(),
"Sqlite file not found, waiting for it to be generated"
);
let (done_tx, done_rx) = oneshot::channel::<()>();
let done_tx = Arc::new(Mutex::new(Some(done_tx)));
let init_tx = init_tx.clone();
let id = SqliteMigrationProgress::listen(&app, move |e| {
let _ = init_tx.send(InitStep::SqliteWaiting);
if matches!(e.payload, SqliteMigrationProgress::Done)
&& let Some(done_tx) = done_tx.lock().unwrap().take()
{
let _ = done_tx.send(());
}
});
let app = app.clone();
tokio::spawn(done_rx.map(async move |_| {
app.unlisten(id);
}))
});
let loading_task = tokio::spawn({ let loading_task = tokio::spawn({
let init_tx = init_tx.clone();
let app = app.clone(); let app = app.clone();
async move { async move {
let mut sqlite_exists = sqlite_file_exists();
tracing::info!("Setting up server connection"); tracing::info!("Setting up server connection");
let server_connection = setup_server_connection(app.clone()).await; let server_connection = setup_server_connection(app.clone()).await;
tracing::info!("Server connection setup");
// we delay spawning this future so that the timeout is created lazily // we delay spawning this future so that the timeout is created lazily
let cli_health_check = match server_connection { let cli_health_check = match server_connection {
@ -622,23 +655,12 @@ async fn initialize(app: AppHandle) {
} }
}; };
tracing::info!("server connection started");
if let Some(cli_health_check) = cli_health_check { if let Some(cli_health_check) = cli_health_check {
if sqlite_enabled { if let Some(sqlite_done_rx) = sqlite_done {
tracing::debug!(sqlite_exists, "Checking sqlite file existence"); let _ = sqlite_done_rx.await;
if !sqlite_exists {
tracing::info!(
path = %opencode_db_path().expect("failed to get db path").display(),
"Sqlite file not found, waiting for it to be generated"
);
let _ = init_tx.send(InitStep::SqliteWaiting);
while !sqlite_exists {
sleep(Duration::from_secs(1)).await;
sqlite_exists = sqlite_file_exists();
}
}
} }
tokio::spawn(cli_health_check); tokio::spawn(cli_health_check);
} }
@ -654,11 +676,11 @@ async fn initialize(app: AppHandle) {
.is_err() .is_err()
{ {
tracing::debug!("Loading task timed out, showing loading window"); tracing::debug!("Loading task timed out, showing loading window");
let app = app.clone();
let loading_window = LoadingWindow::create(&app).expect("Failed to create loading window"); let loading_window = LoadingWindow::create(&app).expect("Failed to create loading window");
sleep(Duration::from_secs(1)).await; sleep(Duration::from_secs(1)).await;
Some(loading_window) Some(loading_window)
} else { } else {
tracing::debug!("Showing main window without loading window");
MainWindow::create(&app).expect("Failed to create main window"); MainWindow::create(&app).expect("Failed to create main window");
None None
@ -667,7 +689,6 @@ async fn initialize(app: AppHandle) {
let _ = loading_task.await; let _ = loading_task.await;
tracing::info!("Loading done, completing initialisation"); tracing::info!("Loading done, completing initialisation");
let _ = init_tx.send(InitStep::Done); let _ = init_tx.send(InitStep::Done);
if loading_window.is_some() { if loading_window.is_some() {

View file

@ -11,17 +11,11 @@ use crate::{
constants::{DEFAULT_SERVER_URL_KEY, SETTINGS_STORE, WSL_ENABLED_KEY}, constants::{DEFAULT_SERVER_URL_KEY, SETTINGS_STORE, WSL_ENABLED_KEY},
}; };
#[derive(Clone, serde::Serialize, serde::Deserialize, specta::Type, Debug)] #[derive(Clone, serde::Serialize, serde::Deserialize, specta::Type, Debug, Default)]
pub struct WslConfig { pub struct WslConfig {
pub enabled: bool, pub enabled: bool,
} }
impl Default for WslConfig {
fn default() -> Self {
Self { enabled: false }
}
}
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub fn get_default_server_url(app: AppHandle) -> Result<Option<String>, String> { pub fn get_default_server_url(app: AppHandle) -> Result<Option<String>, String> {

View file

@ -23,6 +23,7 @@ export const commands = {
/** Events */ /** Events */
export const events = { export const events = {
loadingWindowComplete: makeEvent<LoadingWindowComplete>("loading-window-complete"), loadingWindowComplete: makeEvent<LoadingWindowComplete>("loading-window-complete"),
sqliteMigrationProgress: makeEvent<SqliteMigrationProgress>("sqlite-migration-progress"),
}; };
/* Types */ /* Types */
@ -37,6 +38,8 @@ export type ServerReadyData = {
password: string | null, password: string | null,
}; };
export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { type: "Done" };
export type WslConfig = { export type WslConfig = {
enabled: boolean, enabled: boolean,
}; };

View file

@ -4,7 +4,7 @@ import "@opencode-ai/app/index.css"
import { Font } from "@opencode-ai/ui/font" import { Font } from "@opencode-ai/ui/font"
import { Splash } from "@opencode-ai/ui/logo" import { Splash } from "@opencode-ai/ui/logo"
import "./styles.css" import "./styles.css"
import { createSignal, Match, onMount } from "solid-js" import { createSignal, Match, onCleanup, onMount } from "solid-js"
import { commands, events, InitStep } from "./bindings" import { commands, events, InitStep } from "./bindings"
import { Channel } from "@tauri-apps/api/core" import { Channel } from "@tauri-apps/api/core"
import { Switch } from "solid-js" import { Switch } from "solid-js"
@ -57,15 +57,29 @@ render(() => {
"This could take a couple of minutes", "This could take a couple of minutes",
] ]
const [textIndex, setTextIndex] = createSignal(0) const [textIndex, setTextIndex] = createSignal(0)
const [progress, setProgress] = createSignal(0)
onMount(async () => { onMount(async () => {
const listener = events.sqliteMigrationProgress.listen((e) => {
if (e.payload.type === "InProgress") setProgress(e.payload.value)
})
onCleanup(() => listener.then((c) => c()))
await new Promise((res) => setTimeout(res, 3000)) await new Promise((res) => setTimeout(res, 3000))
setTextIndex(1) setTextIndex(1)
await new Promise((res) => setTimeout(res, 6000)) await new Promise((res) => setTimeout(res, 6000))
setTextIndex(2) setTextIndex(2)
}) })
return <>{textItems[textIndex()]}</> return (
<div class="flex flex-col items-center gap-1">
<span>{textItems[textIndex()]}</span>
<span>Progress: {progress()}%</span>
<div class="h-2 w-48 rounded-full border border-white relative">
<div class="bg-[#fff] h-full absolute left-0 inset-y-0" style={{ width: `${progress()}%` }} />
</div>
</div>
)
}} }}
</Match> </Match>
</Switch> </Switch>

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.1.59", "version": "1.1.60",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",

View file

@ -1,7 +1,7 @@
id = "opencode" id = "opencode"
name = "OpenCode" name = "OpenCode"
description = "The open source coding agent." description = "The open source coding agent."
version = "1.1.59" version = "1.1.60"
schema_version = 1 schema_version = 1
authors = ["Anomaly"] authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode" repository = "https://github.com/anomalyco/opencode"
@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg" icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64] [agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.59/opencode-darwin-arm64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.60/opencode-darwin-arm64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64] [agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.59/opencode-darwin-x64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.60/opencode-darwin-x64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64] [agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.59/opencode-linux-arm64.tar.gz" archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.60/opencode-linux-arm64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64] [agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.59/opencode-linux-x64.tar.gz" archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.60/opencode-linux-x64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64] [agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.59/opencode-windows-x64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.60/opencode-windows-x64.zip"
cmd = "./opencode.exe" cmd = "./opencode.exe"
args = ["acp"] args = ["acp"]

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.1.59", "version": "1.1.60",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.1.59", "version": "1.1.60",
"name": "opencode", "name": "opencode",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
@ -84,8 +84,8 @@
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.4", "@openrouter/ai-sdk-provider": "1.5.4",
"@opentui/core": "0.1.77", "@opentui/core": "0.1.79",
"@opentui/solid": "0.1.77", "@opentui/solid": "0.1.79",
"@parcel/watcher": "2.5.1", "@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:", "@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2", "@solid-primitives/event-bus": "1.1.2",

View file

@ -3,6 +3,7 @@ import { Clipboard } from "@tui/util/clipboard"
import { TextAttributes } from "@opentui/core" import { TextAttributes } from "@opentui/core"
import { RouteProvider, useRoute } from "@tui/context/route" import { RouteProvider, useRoute } from "@tui/context/route"
import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js" import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js"
import { win32DisableProcessedInput, win32FlushInputBuffer, win32InstallCtrlCGuard } from "./win32"
import { Installation } from "@/installation" import { Installation } from "@/installation"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { DialogProvider, useDialog } from "@tui/ui/dialog" import { DialogProvider, useDialog } from "@tui/ui/dialog"
@ -110,8 +111,17 @@ export function tui(input: {
}) { }) {
// promise to prevent immediate exit // promise to prevent immediate exit
return new Promise<void>(async (resolve) => { return new Promise<void>(async (resolve) => {
const unguard = win32InstallCtrlCGuard()
win32DisableProcessedInput()
const mode = await getTerminalBackgroundColor() const mode = await getTerminalBackgroundColor()
// Re-clear after getTerminalBackgroundColor() — setRawMode(false) restores
// the original console mode which re-enables ENABLE_PROCESSED_INPUT.
win32DisableProcessedInput()
const onExit = async () => { const onExit = async () => {
unguard?.()
await input.onExit?.() await input.onExit?.()
resolve() resolve()
} }
@ -730,7 +740,8 @@ function ErrorComponent(props: {
const handleExit = async () => { const handleExit = async () => {
renderer.setTerminalTitle("") renderer.setTerminalTitle("")
renderer.destroy() renderer.destroy()
props.onExit() win32FlushInputBuffer()
await props.onExit()
} }
useKeyboard((evt) => { useKeyboard((evt) => {

View file

@ -1,5 +1,6 @@
import { cmd } from "../cmd" import { cmd } from "../cmd"
import { tui } from "./app" import { tui } from "./app"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
export const AttachCommand = cmd({ export const AttachCommand = cmd({
command: "attach <url>", command: "attach <url>",
@ -26,27 +27,34 @@ export const AttachCommand = cmd({
describe: "basic auth password (defaults to OPENCODE_SERVER_PASSWORD)", describe: "basic auth password (defaults to OPENCODE_SERVER_PASSWORD)",
}), }),
handler: async (args) => { handler: async (args) => {
const directory = (() => { const unguard = win32InstallCtrlCGuard()
if (!args.dir) return undefined try {
try { win32DisableProcessedInput()
process.chdir(args.dir)
return process.cwd() const directory = (() => {
} catch { if (!args.dir) return undefined
// If the directory doesn't exist locally (remote attach), pass it through. try {
return args.dir process.chdir(args.dir)
} return process.cwd()
})() } catch {
const headers = (() => { // If the directory doesn't exist locally (remote attach), pass it through.
const password = args.password ?? process.env.OPENCODE_SERVER_PASSWORD return args.dir
if (!password) return undefined }
const auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` })()
return { Authorization: auth } const headers = (() => {
})() const password = args.password ?? process.env.OPENCODE_SERVER_PASSWORD
await tui({ if (!password) return undefined
url: args.url, const auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`
args: { sessionID: args.session }, return { Authorization: auth }
directory, })()
headers, await tui({
}) url: args.url,
args: { sessionID: args.session },
directory,
headers,
})
} finally {
unguard?.()
}
}, },
}) })

View file

@ -1,6 +1,7 @@
import { useRenderer } from "@opentui/solid" import { useRenderer } from "@opentui/solid"
import { createSimpleContext } from "./helper" import { createSimpleContext } from "./helper"
import { FormatError, FormatUnknownError } from "@/cli/error" import { FormatError, FormatUnknownError } from "@/cli/error"
import { win32FlushInputBuffer } from "../win32"
type Exit = ((reason?: unknown) => Promise<void>) & { type Exit = ((reason?: unknown) => Promise<void>) & {
message: { message: {
set: (value?: string) => () => void set: (value?: string) => () => void
@ -32,6 +33,7 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({
// Reset window title before destroying renderer // Reset window title before destroying renderer
renderer.setTerminalTitle("") renderer.setTerminalTitle("")
renderer.destroy() renderer.destroy()
win32FlushInputBuffer()
await input.onExit?.() await input.onExit?.()
if (reason) { if (reason) {
const formatted = FormatError(reason) ?? FormatUnknownError(reason) const formatted = FormatError(reason) ?? FormatUnknownError(reason)

View file

@ -149,6 +149,7 @@ export function Session() {
const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true) const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true)
const [showAssistantMetadata, setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true) const [showAssistantMetadata, setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true)
const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", false) const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", false)
const [showHeader, setShowHeader] = kv.signal("header_visible", true)
const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word") const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word")
const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true) const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true)
@ -582,6 +583,15 @@ export function Session() {
dialog.clear() dialog.clear()
}, },
}, },
{
title: showHeader() ? "Hide header" : "Show header",
value: "session.toggle.header",
category: "Session",
onSelect: (dialog) => {
setShowHeader((prev) => !prev)
dialog.clear()
},
},
{ {
title: "Page up", title: "Page up",
value: "session.page.up", value: "session.page.up",
@ -963,7 +973,7 @@ export function Session() {
<box flexDirection="row"> <box flexDirection="row">
<box flexGrow={1} paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={1}> <box flexGrow={1} paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={1}>
<Show when={session()}> <Show when={session()}>
<Show when={!sidebarVisible() || !wide()}> <Show when={showHeader() && (!sidebarVisible() || !wide())}>
<Header /> <Header />
</Show> </Show>
<scrollbox <scrollbox

View file

@ -9,6 +9,7 @@ import { Log } from "@/util/log"
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
import type { Event } from "@opencode-ai/sdk/v2" import type { Event } from "@opencode-ai/sdk/v2"
import type { EventSource } from "./context/sdk" import type { EventSource } from "./context/sdk"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
declare global { declare global {
const OPENCODE_WORKER_PATH: string const OPENCODE_WORKER_PATH: string
@ -77,99 +78,111 @@ export const TuiThreadCommand = cmd({
describe: "agent to use", describe: "agent to use",
}), }),
handler: async (args) => { handler: async (args) => {
if (args.fork && !args.continue && !args.session) { // Keep ENABLE_PROCESSED_INPUT cleared even if other code flips it.
UI.error("--fork requires --continue or --session") // (Important when running under `bun run` wrappers on Windows.)
process.exit(1) const unguard = win32InstallCtrlCGuard()
}
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
const baseCwd = process.env.PWD ?? process.cwd()
const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
const localWorker = new URL("./worker.ts", import.meta.url)
const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url)
const workerPath = await iife(async () => {
if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
if (await Bun.file(distWorker).exists()) return distWorker
return localWorker
})
try { try {
process.chdir(cwd) // Must be the very first thing — disables CTRL_C_EVENT before any Worker
} catch (e) { // spawn or async work so the OS cannot kill the process group.
UI.error("Failed to change directory to " + cwd) win32DisableProcessedInput()
return
if (args.fork && !args.continue && !args.session) {
UI.error("--fork requires --continue or --session")
process.exitCode = 1
return
}
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
const baseCwd = process.env.PWD ?? process.cwd()
const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
const localWorker = new URL("./worker.ts", import.meta.url)
const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url)
const workerPath = await iife(async () => {
if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
if (await Bun.file(distWorker).exists()) return distWorker
return localWorker
})
try {
process.chdir(cwd)
} catch (e) {
UI.error("Failed to change directory to " + cwd)
return
}
const worker = new Worker(workerPath, {
env: Object.fromEntries(
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
),
})
worker.onerror = (e) => {
Log.Default.error(e)
}
const client = Rpc.client<typeof rpc>(worker)
process.on("uncaughtException", (e) => {
Log.Default.error(e)
})
process.on("unhandledRejection", (e) => {
Log.Default.error(e)
})
process.on("SIGUSR2", async () => {
await client.call("reload", undefined)
})
const prompt = await iife(async () => {
const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined
if (!args.prompt) return piped
return piped ? piped + "\n" + args.prompt : args.prompt
})
// Check if server should be started (port or hostname explicitly set in CLI or config)
const networkOpts = await resolveNetworkOptions(args)
const shouldStartServer =
process.argv.includes("--port") ||
process.argv.includes("--hostname") ||
process.argv.includes("--mdns") ||
networkOpts.mdns ||
networkOpts.port !== 0 ||
networkOpts.hostname !== "127.0.0.1"
let url: string
let customFetch: typeof fetch | undefined
let events: EventSource | undefined
if (shouldStartServer) {
// Start HTTP server for external access
const server = await client.call("server", networkOpts)
url = server.url
} else {
// Use direct RPC communication (no HTTP)
url = "http://opencode.internal"
customFetch = createWorkerFetch(client)
events = createEventSource(client)
}
const tuiPromise = tui({
url,
fetch: customFetch,
events,
args: {
continue: args.continue,
sessionID: args.session,
agent: args.agent,
model: args.model,
prompt,
fork: args.fork,
},
onExit: async () => {
await client.call("shutdown", undefined)
},
})
setTimeout(() => {
client.call("checkUpgrade", { directory: cwd }).catch(() => {})
}, 1000)
await tuiPromise
} finally {
unguard?.()
} }
const worker = new Worker(workerPath, {
env: Object.fromEntries(
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
),
})
worker.onerror = (e) => {
Log.Default.error(e)
}
const client = Rpc.client<typeof rpc>(worker)
process.on("uncaughtException", (e) => {
Log.Default.error(e)
})
process.on("unhandledRejection", (e) => {
Log.Default.error(e)
})
process.on("SIGUSR2", async () => {
await client.call("reload", undefined)
})
const prompt = await iife(async () => {
const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined
if (!args.prompt) return piped
return piped ? piped + "\n" + args.prompt : args.prompt
})
// Check if server should be started (port or hostname explicitly set in CLI or config)
const networkOpts = await resolveNetworkOptions(args)
const shouldStartServer =
process.argv.includes("--port") ||
process.argv.includes("--hostname") ||
process.argv.includes("--mdns") ||
networkOpts.mdns ||
networkOpts.port !== 0 ||
networkOpts.hostname !== "127.0.0.1"
let url: string
let customFetch: typeof fetch | undefined
let events: EventSource | undefined
if (shouldStartServer) {
// Start HTTP server for external access
const server = await client.call("server", networkOpts)
url = server.url
} else {
// Use direct RPC communication (no HTTP)
url = "http://opencode.internal"
customFetch = createWorkerFetch(client)
events = createEventSource(client)
}
const tuiPromise = tui({
url,
fetch: customFetch,
events,
args: {
continue: args.continue,
sessionID: args.session,
agent: args.agent,
model: args.model,
prompt,
fork: args.fork,
},
onExit: async () => {
await client.call("shutdown", undefined)
},
})
setTimeout(() => {
client.call("checkUpgrade", { directory: cwd }).catch(() => {})
}, 1000)
await tuiPromise
}, },
}) })

View file

@ -0,0 +1,129 @@
import { dlopen, ptr } from "bun:ffi"
const STD_INPUT_HANDLE = -10
const ENABLE_PROCESSED_INPUT = 0x0001
const kernel = () =>
dlopen("kernel32.dll", {
GetStdHandle: { args: ["i32"], returns: "ptr" },
GetConsoleMode: { args: ["ptr", "ptr"], returns: "i32" },
SetConsoleMode: { args: ["ptr", "u32"], returns: "i32" },
FlushConsoleInputBuffer: { args: ["ptr"], returns: "i32" },
})
let k32: ReturnType<typeof kernel> | undefined
function load() {
if (process.platform !== "win32") return false
try {
k32 ??= kernel()
return true
} catch {
return false
}
}
/**
* Clear ENABLE_PROCESSED_INPUT on the console stdin handle.
*/
export function win32DisableProcessedInput() {
if (process.platform !== "win32") return
if (!process.stdin.isTTY) return
if (!load()) return
const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE)
const buf = new Uint32Array(1)
if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return
const mode = buf[0]!
if ((mode & ENABLE_PROCESSED_INPUT) === 0) return
k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT)
}
/**
* Discard any queued console input (mouse events, key presses, etc.).
*/
export function win32FlushInputBuffer() {
if (process.platform !== "win32") return
if (!process.stdin.isTTY) return
if (!load()) return
const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE)
k32!.symbols.FlushConsoleInputBuffer(handle)
}
let unhook: (() => void) | undefined
/**
* Keep ENABLE_PROCESSED_INPUT disabled.
*
* On Windows, Ctrl+C becomes a CTRL_C_EVENT (instead of stdin input) when
* ENABLE_PROCESSED_INPUT is set. Various runtimes can re-apply console modes
* (sometimes on a later tick), and the flag is console-global, not per-process.
*
* We combine:
* - A `setRawMode(...)` hook to re-clear after known raw-mode toggles.
* - A low-frequency poll as a backstop for native/external mode changes.
*/
export function win32InstallCtrlCGuard() {
if (process.platform !== "win32") return
if (!process.stdin.isTTY) return
if (!load()) return
if (unhook) return unhook
const stdin = process.stdin as any
const original = stdin.setRawMode
const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE)
const buf = new Uint32Array(1)
if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return
const initial = buf[0]!
const enforce = () => {
if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return
const mode = buf[0]!
if ((mode & ENABLE_PROCESSED_INPUT) === 0) return
k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT)
}
// Some runtimes can re-apply console modes on the next tick; enforce twice.
const later = () => {
enforce()
setImmediate(enforce)
}
let wrapped: ((mode: boolean) => unknown) | undefined
if (typeof original === "function") {
wrapped = (mode: boolean) => {
const result = original.call(stdin, mode)
later()
return result
}
stdin.setRawMode = wrapped
}
// Ensure it's cleared immediately too (covers any earlier mode changes).
later()
const interval = setInterval(enforce, 100)
interval.unref()
let done = false
unhook = () => {
if (done) return
done = true
clearInterval(interval)
if (wrapped && stdin.setRawMode === wrapped) {
stdin.setRawMode = original
}
k32!.symbols.SetConsoleMode(handle, initial)
unhook = undefined
}
return unhook
}

View file

@ -571,25 +571,28 @@ export namespace MCP {
const clientsSnapshot = await clients() const clientsSnapshot = await clients()
const defaultTimeout = cfg.experimental?.mcp_timeout const defaultTimeout = cfg.experimental?.mcp_timeout
for (const [clientName, client] of Object.entries(clientsSnapshot)) { const connectedClients = Object.entries(clientsSnapshot).filter(
// Only include tools from connected MCPs (skip disabled ones) ([clientName]) => s.status[clientName]?.status === "connected",
if (s.status[clientName]?.status !== "connected") { )
continue
}
const toolsResult = await client.listTools().catch((e) => { const toolsResults = await Promise.all(
log.error("failed to get tools", { clientName, error: e.message }) connectedClients.map(async ([clientName, client]) => {
const failedStatus = { const toolsResult = await client.listTools().catch((e) => {
status: "failed" as const, log.error("failed to get tools", { clientName, error: e.message })
error: e instanceof Error ? e.message : String(e), const failedStatus = {
} status: "failed" as const,
s.status[clientName] = failedStatus error: e instanceof Error ? e.message : String(e),
delete s.clients[clientName] }
return undefined s.status[clientName] = failedStatus
}) delete s.clients[clientName]
if (!toolsResult) { return undefined
continue })
} return { clientName, client, toolsResult }
}),
)
for (const { clientName, client, toolsResult } of toolsResults) {
if (!toolsResult) continue
const mcpConfig = config[clientName] const mcpConfig = config[clientName]
const entry = isMcpConfigured(mcpConfig) ? mcpConfig : undefined const entry = isMcpConfigured(mcpConfig) ? mcpConfig : undefined
const timeout = entry?.timeout ?? defaultTimeout const timeout = entry?.timeout ?? defaultTimeout

View file

@ -64,7 +64,7 @@ export namespace ModelsDev {
status: z.enum(["alpha", "beta", "deprecated"]).optional(), status: z.enum(["alpha", "beta", "deprecated"]).optional(),
options: z.record(z.string(), z.any()), options: z.record(z.string(), z.any()),
headers: z.record(z.string(), z.string()).optional(), headers: z.record(z.string(), z.string()).optional(),
provider: z.object({ npm: z.string() }).optional(), provider: z.object({ npm: z.string(), api: z.string() }).optional(),
variants: z.record(z.string(), z.record(z.string(), z.any())).optional(), variants: z.record(z.string(), z.record(z.string(), z.any())).optional(),
}) })
export type Model = z.infer<typeof Model> export type Model = z.infer<typeof Model>

View file

@ -629,7 +629,7 @@ export namespace Provider {
family: model.family, family: model.family,
api: { api: {
id: model.id, id: model.id,
url: provider.api!, url: model.provider?.api ?? provider.api!,
npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible", npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible",
}, },
status: model.status ?? "active", status: model.status ?? "active",
@ -781,7 +781,7 @@ export namespace Provider {
existingModel?.api.npm ?? existingModel?.api.npm ??
modelsDev[providerID]?.npm ?? modelsDev[providerID]?.npm ??
"@ai-sdk/openai-compatible", "@ai-sdk/openai-compatible",
url: provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api, url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api,
}, },
status: model.status ?? existingModel?.status ?? "active", status: model.status ?? existingModel?.status ?? "active",
name, name,

View file

@ -401,6 +401,8 @@ export namespace ProviderTransform {
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/xai // https://v5.ai-sdk.dev/providers/ai-sdk-providers/xai
case "@ai-sdk/deepinfra": case "@ai-sdk/deepinfra":
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/deepinfra // https://v5.ai-sdk.dev/providers/ai-sdk-providers/deepinfra
case "venice-ai-sdk-provider":
// https://docs.venice.ai/overview/guides/reasoning-models#reasoning-effort
case "@ai-sdk/openai-compatible": case "@ai-sdk/openai-compatible":
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }])) return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))

View file

@ -41,7 +41,9 @@ export namespace SessionCompaction {
const reserved = const reserved =
config.compaction?.reserved ?? Math.min(COMPACTION_BUFFER, ProviderTransform.maxOutputTokens(input.model)) config.compaction?.reserved ?? Math.min(COMPACTION_BUFFER, ProviderTransform.maxOutputTokens(input.model))
const usable = input.model.limit.input ? input.model.limit.input - reserved : context - reserved const usable = input.model.limit.input
? input.model.limit.input - reserved
: context - ProviderTransform.maxOutputTokens(input.model)
return count >= usable return count >= usable
} }

View file

@ -39,6 +39,7 @@ export namespace LLM {
tools: Record<string, Tool> tools: Record<string, Tool>
retries?: number retries?: number
output?: "tool" output?: "tool"
toolChoice?: "auto" | "required" | "none"
} }
export type StreamOutput = StreamTextResult<ToolSet, unknown> export type StreamOutput = StreamTextResult<ToolSet, unknown>
@ -206,6 +207,7 @@ export namespace LLM {
providerOptions: ProviderTransform.providerOptions(input.model, params.options), providerOptions: ProviderTransform.providerOptions(input.model, params.options),
activeTools: Object.keys(tools).filter((x) => x !== "invalid"), activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
tools, tools,
toolChoice: input.toolChoice,
maxOutputTokens, maxOutputTokens,
abortSignal: input.abort, abortSignal: input.abort,
toolChoice: input.output === "tool" ? "required" : undefined, toolChoice: input.output === "tool" ? "required" : undefined,

View file

@ -15,6 +15,13 @@ import type { Provider } from "@/provider/provider"
export namespace MessageV2 { export namespace MessageV2 {
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({})) export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
export const AbortedError = NamedError.create("MessageAbortedError", z.object({ message: z.string() })) export const AbortedError = NamedError.create("MessageAbortedError", z.object({ message: z.string() }))
export const StructuredOutputError = NamedError.create(
"StructuredOutputError",
z.object({
message: z.string(),
retries: z.number(),
}),
)
export const AuthError = NamedError.create( export const AuthError = NamedError.create(
"ProviderAuthError", "ProviderAuthError",
z.object({ z.object({
@ -39,6 +46,29 @@ export namespace MessageV2 {
z.object({ message: z.string(), responseBody: z.string().optional() }), z.object({ message: z.string(), responseBody: z.string().optional() }),
) )
export const OutputFormatText = z
.object({
type: z.literal("text"),
})
.meta({
ref: "OutputFormatText",
})
export const OutputFormatJsonSchema = z
.object({
type: z.literal("json_schema"),
schema: z.record(z.string(), z.any()).meta({ ref: "JSONSchema" }),
retryCount: z.number().int().min(0).default(2),
})
.meta({
ref: "OutputFormatJsonSchema",
})
export const Format = z.discriminatedUnion("type", [OutputFormatText, OutputFormatJsonSchema]).meta({
ref: "OutputFormat",
})
export type OutputFormat = z.infer<typeof Format>
const PartBase = z.object({ const PartBase = z.object({
id: z.string(), id: z.string(),
sessionID: z.string(), sessionID: z.string(),
@ -313,6 +343,7 @@ export namespace MessageV2 {
time: z.object({ time: z.object({
created: z.number(), created: z.number(),
}), }),
format: Format.optional(),
summary: z summary: z
.object({ .object({
title: z.string().optional(), title: z.string().optional(),
@ -365,6 +396,7 @@ export namespace MessageV2 {
NamedError.Unknown.Schema, NamedError.Unknown.Schema,
OutputLengthError.Schema, OutputLengthError.Schema,
AbortedError.Schema, AbortedError.Schema,
StructuredOutputError.Schema,
ContextOverflowError.Schema, ContextOverflowError.Schema,
APIError.Schema, APIError.Schema,
]) ])
@ -393,6 +425,7 @@ export namespace MessageV2 {
write: z.number(), write: z.number(),
}), }),
}), }),
structured: z.any().optional(),
variant: z.string().optional(), variant: z.string().optional(),
finish: z.string().optional(), finish: z.string().optional(),
}).meta({ }).meta({

View file

@ -50,6 +50,16 @@ import { Truncate } from "@/tool/truncation"
// @ts-ignore // @ts-ignore
globalThis.AI_SDK_LOG_WARNINGS = false globalThis.AI_SDK_LOG_WARNINGS = false
const STRUCTURED_OUTPUT_DESCRIPTION = `Use this tool to return your final response in the requested structured format.
IMPORTANT:
- You MUST call this tool exactly once at the end of your response
- The input must be valid JSON matching the required schema
- Complete all necessary research and tool calls BEFORE calling this tool
- This tool provides your final answer - no further actions are taken after calling it`
const STRUCTURED_OUTPUT_SYSTEM_PROMPT = `IMPORTANT: The user has requested structured output. You MUST use the StructuredOutput tool to provide your final response. Do NOT respond with plain text - you MUST call the StructuredOutput tool with your answer formatted according to the schema.`
export namespace SessionPrompt { export namespace SessionPrompt {
const log = Log.create({ service: "session.prompt" }) const log = Log.create({ service: "session.prompt" })
@ -96,6 +106,7 @@ export namespace SessionPrompt {
.describe( .describe(
"@deprecated tools and permissions have been merged, you can set permissions on the session itself now", "@deprecated tools and permissions have been merged, you can set permissions on the session itself now",
), ),
format: MessageV2.Format.optional(),
system: z.string().optional(), system: z.string().optional(),
variant: z.string().optional(), variant: z.string().optional(),
parts: z.array( parts: z.array(
@ -276,6 +287,11 @@ export namespace SessionPrompt {
using _ = defer(() => cancel(sessionID)) using _ = defer(() => cancel(sessionID))
// Structured output state
// Note: On session resumption, state is reset but outputFormat is preserved
// on the user message and will be retrieved from lastUser below
let structuredOutput: unknown | undefined
let step = 0 let step = 0
const session = await Session.get(sessionID) const session = await Session.get(sessionID)
while (true) { while (true) {
@ -589,6 +605,16 @@ export namespace SessionPrompt {
messages: msgs, messages: msgs,
}) })
// Inject StructuredOutput tool if JSON schema mode enabled
if (lastUser.format?.type === "json_schema") {
tools["StructuredOutput"] = createStructuredOutputTool({
schema: lastUser.format.schema,
onSuccess(output) {
structuredOutput = output
},
})
}
if (step === 1) { if (step === 1) {
SessionSummary.summarize({ SessionSummary.summarize({
sessionID: sessionID, sessionID: sessionID,
@ -619,12 +645,19 @@ export namespace SessionPrompt {
await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: sessionMessages }) await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: sessionMessages })
// Build system prompt, adding structured output instruction if needed
const system = [...(await SystemPrompt.environment(model)), ...(await InstructionPrompt.system())]
const format = lastUser.format ?? { type: "text" }
if (format.type === "json_schema") {
system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT)
}
const result = await processor.process({ const result = await processor.process({
user: lastUser, user: lastUser,
agent, agent,
abort, abort,
sessionID, sessionID,
system: [...(await SystemPrompt.environment(model)), ...(await InstructionPrompt.system())], system,
messages: [ messages: [
...MessageV2.toModelMessages(sessionMessages, model), ...MessageV2.toModelMessages(sessionMessages, model),
...(isLastStep ...(isLastStep
@ -638,7 +671,33 @@ export namespace SessionPrompt {
], ],
tools, tools,
model, model,
toolChoice: format.type === "json_schema" ? "required" : undefined,
}) })
// If structured output was captured, save it and exit immediately
// This takes priority because the StructuredOutput tool was called successfully
if (structuredOutput !== undefined) {
processor.message.structured = structuredOutput
processor.message.finish = processor.message.finish ?? "stop"
await Session.updateMessage(processor.message)
break
}
// Check if model finished (finish reason is not "tool-calls" or "unknown")
const modelFinished = processor.message.finish && !["tool-calls", "unknown"].includes(processor.message.finish)
if (modelFinished && !processor.message.error) {
if (format.type === "json_schema") {
// Model stopped without calling StructuredOutput tool
processor.message.error = new MessageV2.StructuredOutputError({
message: "Model did not produce structured output",
retries: 0,
}).toObject()
await Session.updateMessage(processor.message)
break
}
}
if (result === "stop") break if (result === "stop") break
if (result === "compact") { if (result === "compact") {
await SessionCompaction.create({ await SessionCompaction.create({
@ -669,7 +728,8 @@ export namespace SessionPrompt {
return Provider.defaultModel() return Provider.defaultModel()
} }
async function resolveTools(input: { /** @internal Exported for testing */
export async function resolveTools(input: {
agent: Agent.Info agent: Agent.Info
model: Provider.Model model: Provider.Model
session: Session.Info session: Session.Info
@ -849,6 +909,36 @@ export namespace SessionPrompt {
return tools return tools
} }
/** @internal Exported for testing */
export function createStructuredOutputTool(input: {
schema: Record<string, any>
onSuccess: (output: unknown) => void
}): AITool {
// Remove $schema property if present (not needed for tool input)
const { $schema, ...toolSchema } = input.schema
return tool({
id: "StructuredOutput" as any,
description: STRUCTURED_OUTPUT_DESCRIPTION,
inputSchema: jsonSchema(toolSchema as any),
async execute(args) {
// AI SDK validates args against inputSchema before calling execute()
input.onSuccess(args)
return {
output: "Structured output captured successfully.",
title: "Structured Output",
metadata: { valid: true },
}
},
toModelOutput(result) {
return {
type: "text",
value: result.output,
}
},
})
}
async function createUserMessage(input: PromptInput) { async function createUserMessage(input: PromptInput) {
const agent = await Agent.get(input.agent ?? (await Agent.defaultAgent())) const agent = await Agent.get(input.agent ?? (await Agent.defaultAgent()))
@ -870,6 +960,7 @@ export namespace SessionPrompt {
agent: agent.name, agent: agent.name,
model, model,
system: input.system, system: input.system,
format: input.format,
variant, variant,
} }
using _ = defer(() => InstructionPrompt.clear(info.id)) using _ = defer(() => InstructionPrompt.clear(info.id))
@ -1022,9 +1113,9 @@ export namespace SessionPrompt {
} }
} }
} }
offset = Math.max(start - 1, 0) offset = Math.max(start, 1)
if (end) { if (end) {
limit = end - offset limit = end - (offset - 1)
} }
} }
const args = { filePath: filepath, offset, limit } const args = { filePath: filepath, offset, limit }

View file

@ -38,7 +38,7 @@ export const EditTool = Tool.define("edit", {
} }
if (params.oldString === params.newString) { if (params.oldString === params.newString) {
throw new Error("oldString and newString must be different") throw new Error("No changes to apply: oldString and newString are identical.")
} }
const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath) const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
@ -617,7 +617,7 @@ export function trimDiff(diff: string): string {
export function replace(content: string, oldString: string, newString: string, replaceAll = false): string { export function replace(content: string, oldString: string, newString: string, replaceAll = false): string {
if (oldString === newString) { if (oldString === newString) {
throw new Error("oldString and newString must be different") throw new Error("No changes to apply: oldString and newString are identical.")
} }
let notFound = true let notFound = true
@ -647,9 +647,9 @@ export function replace(content: string, oldString: string, newString: string, r
} }
if (notFound) { if (notFound) {
throw new Error("oldString not found in content") throw new Error(
"Could not find oldString in the file. It must match exactly, including whitespace, indentation, and line endings.",
)
} }
throw new Error( throw new Error("Found multiple matches for oldString. Provide more surrounding context to make the match unique.")
"Found multiple matches for oldString. Provide more surrounding lines in oldString to identify the correct match.",
)
} }

View file

@ -2,9 +2,9 @@ Performs exact string replacements in files.
Usage: Usage:
- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file. - You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the oldString or newString. - When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: line number + colon + space (e.g., `1: `). Everything after that space is the actual file content to match. Never include any part of the line number prefix in the oldString or newString.
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required. - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked. - Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
- The edit will FAIL if `oldString` is not found in the file with an error "oldString not found in content". - The edit will FAIL if `oldString` is not found in the file with an error "oldString not found in content".
- The edit will FAIL if `oldString` is found multiple times in the file with an error "oldString found multiple times and requires more code context to uniquely identify the intended match". Either provide a larger string with more surrounding context to make it unique or use `replaceAll` to change every instance of `oldString`. - The edit will FAIL if `oldString` is found multiple times in the file with an error "Found multiple matches for oldString. Provide more surrounding lines in oldString to identify the correct match." Either provide a larger string with more surrounding context to make it unique or use `replaceAll` to change every instance of `oldString`.
- Use `replaceAll` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance. - Use `replaceAll` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.

View file

@ -62,7 +62,9 @@ export const GlobTool = Tool.define("glob", {
output.push(...files.map((f) => f.path)) output.push(...files.map((f) => f.path))
if (truncated) { if (truncated) {
output.push("") output.push("")
output.push("(Results are truncated. Consider using a more specific path or pattern.)") output.push(
`(Results are truncated: showing first ${limit} results. Consider using a more specific path or pattern.)`,
)
} }
} }

View file

@ -109,7 +109,8 @@ export const GrepTool = Tool.define("grep", {
} }
} }
const outputLines = [`Found ${finalMatches.length} matches`] const totalMatches = matches.length
const outputLines = [`Found ${totalMatches} matches${truncated ? ` (showing first ${limit})` : ""}`]
let currentFile = "" let currentFile = ""
for (const match of finalMatches) { for (const match of finalMatches) {
@ -127,7 +128,9 @@ export const GrepTool = Tool.define("grep", {
if (truncated) { if (truncated) {
outputLines.push("") outputLines.push("")
outputLines.push("(Results are truncated. Consider using a more specific path or pattern.)") outputLines.push(
`(Results truncated: showing ${limit} of ${totalMatches} matches (${totalMatches - limit} hidden). Consider using a more specific path or pattern.)`,
)
} }
if (hasErrors) { if (hasErrors) {
@ -138,7 +141,7 @@ export const GrepTool = Tool.define("grep", {
return { return {
title: params.pattern, title: params.pattern,
metadata: { metadata: {
matches: finalMatches.length, matches: totalMatches,
truncated, truncated,
}, },
output: outputLines.join("\n"), output: outputLines.join("\n"),

View file

@ -17,19 +17,26 @@ const MAX_BYTES = 50 * 1024
export const ReadTool = Tool.define("read", { export const ReadTool = Tool.define("read", {
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
filePath: z.string().describe("The path to the file to read"), filePath: z.string().describe("The absolute path to the file or directory to read"),
offset: z.coerce.number().describe("The line number to start reading from (0-based)").optional(), offset: z.coerce.number().describe("The line number to start reading from (1-indexed)").optional(),
limit: z.coerce.number().describe("The number of lines to read (defaults to 2000)").optional(), limit: z.coerce.number().describe("The maximum number of lines to read (defaults to 2000)").optional(),
}), }),
async execute(params, ctx) { async execute(params, ctx) {
if (params.offset !== undefined && params.offset < 1) {
throw new Error("offset must be greater than or equal to 1")
}
let filepath = params.filePath let filepath = params.filePath
if (!path.isAbsolute(filepath)) { if (!path.isAbsolute(filepath)) {
filepath = path.resolve(Instance.directory, filepath) filepath = path.resolve(Instance.directory, filepath)
} }
const title = path.relative(Instance.worktree, filepath) const title = path.relative(Instance.worktree, filepath)
const file = Bun.file(filepath)
const stat = await file.stat().catch(() => undefined)
await assertExternalDirectory(ctx, filepath, { await assertExternalDirectory(ctx, filepath, {
bypass: Boolean(ctx.extra?.["bypassCwdCheck"]), bypass: Boolean(ctx.extra?.["bypassCwdCheck"]),
kind: stat?.isDirectory() ? "directory" : "file",
}) })
await ctx.ask({ await ctx.ask({
@ -39,8 +46,7 @@ export const ReadTool = Tool.define("read", {
metadata: {}, metadata: {},
}) })
const file = Bun.file(filepath) if (!stat) {
if (!(await file.exists())) {
const dir = path.dirname(filepath) const dir = path.dirname(filepath)
const base = path.basename(filepath) const base = path.basename(filepath)
@ -60,6 +66,48 @@ export const ReadTool = Tool.define("read", {
throw new Error(`File not found: ${filepath}`) throw new Error(`File not found: ${filepath}`)
} }
if (stat.isDirectory()) {
const dirents = await fs.promises.readdir(filepath, { withFileTypes: true })
const entries = await Promise.all(
dirents.map(async (dirent) => {
if (dirent.isDirectory()) return dirent.name + "/"
if (dirent.isSymbolicLink()) {
const target = await fs.promises.stat(path.join(filepath, dirent.name)).catch(() => undefined)
if (target?.isDirectory()) return dirent.name + "/"
}
return dirent.name
}),
)
entries.sort((a, b) => a.localeCompare(b))
const limit = params.limit ?? DEFAULT_READ_LIMIT
const offset = params.offset ?? 1
const start = offset - 1
const sliced = entries.slice(start, start + limit)
const truncated = start + sliced.length < entries.length
const output = [
`<path>${filepath}</path>`,
`<type>directory</type>`,
`<entries>`,
sliced.join("\n"),
truncated
? `\n(Showing ${sliced.length} of ${entries.length} entries. Use 'offset' parameter to read beyond entry ${offset + sliced.length})`
: `\n(${entries.length} entries)`,
`</entries>`,
].join("\n")
return {
title,
output,
metadata: {
preview: sliced.slice(0, 20).join("\n"),
truncated,
loaded: [] as string[],
},
}
}
const instructions = await InstructionPrompt.resolve(ctx.messages, filepath, ctx.messageID) const instructions = await InstructionPrompt.resolve(ctx.messages, filepath, ctx.messageID)
// Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files) // Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files)
@ -75,7 +123,7 @@ export const ReadTool = Tool.define("read", {
metadata: { metadata: {
preview: msg, preview: msg,
truncated: false, truncated: false,
...(instructions.length > 0 && { loaded: instructions.map((i) => i.filepath) }), loaded: instructions.map((i) => i.filepath),
}, },
attachments: [ attachments: [
{ {
@ -94,13 +142,15 @@ export const ReadTool = Tool.define("read", {
if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`) if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`)
const limit = params.limit ?? DEFAULT_READ_LIMIT const limit = params.limit ?? DEFAULT_READ_LIMIT
const offset = params.offset || 0 const offset = params.offset ?? 1
const start = offset - 1
const lines = await file.text().then((text) => text.split("\n")) const lines = await file.text().then((text) => text.split("\n"))
if (start >= lines.length) throw new Error(`Offset ${offset} is out of range for this file (${lines.length} lines)`)
const raw: string[] = [] const raw: string[] = []
let bytes = 0 let bytes = 0
let truncatedByBytes = false let truncatedByBytes = false
for (let i = offset; i < Math.min(lines.length, offset + limit); i++) { for (let i = start; i < Math.min(lines.length, start + limit); i++) {
const line = lines[i].length > MAX_LINE_LENGTH ? lines[i].substring(0, MAX_LINE_LENGTH) + "..." : lines[i] const line = lines[i].length > MAX_LINE_LENGTH ? lines[i].substring(0, MAX_LINE_LENGTH) + "..." : lines[i]
const size = Buffer.byteLength(line, "utf-8") + (raw.length > 0 ? 1 : 0) const size = Buffer.byteLength(line, "utf-8") + (raw.length > 0 ? 1 : 0)
if (bytes + size > MAX_BYTES) { if (bytes + size > MAX_BYTES) {
@ -112,15 +162,15 @@ export const ReadTool = Tool.define("read", {
} }
const content = raw.map((line, index) => { const content = raw.map((line, index) => {
return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}` return `${index + offset}: ${line}`
}) })
const preview = raw.slice(0, 20).join("\n") const preview = raw.slice(0, 20).join("\n")
let output = "<file>\n" let output = [`<path>${filepath}</path>`, `<type>file</type>`, "<content>"].join("\n")
output += content.join("\n") output += content.join("\n")
const totalLines = lines.length const totalLines = lines.length
const lastReadLine = offset + raw.length const lastReadLine = offset + raw.length - 1
const hasMoreLines = totalLines > lastReadLine const hasMoreLines = totalLines > lastReadLine
const truncated = hasMoreLines || truncatedByBytes const truncated = hasMoreLines || truncatedByBytes
@ -131,7 +181,7 @@ export const ReadTool = Tool.define("read", {
} else { } else {
output += `\n\n(End of file - total ${totalLines} lines)` output += `\n\n(End of file - total ${totalLines} lines)`
} }
output += "\n</file>" output += "\n</content>"
// just warms the lsp client // just warms the lsp client
LSP.touchFile(filepath, false) LSP.touchFile(filepath, false)
@ -147,7 +197,7 @@ export const ReadTool = Tool.define("read", {
metadata: { metadata: {
preview, preview,
truncated, truncated,
...(instructions.length > 0 && { loaded: instructions.map((i) => i.filepath) }), loaded: instructions.map((i) => i.filepath),
}, },
} }
}, },

View file

@ -1,12 +1,14 @@
Reads a file from the local filesystem. You can access any file directly by using this tool. Read a file or directory from the local filesystem. If the path does not exist, an error is returned.
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
Usage: Usage:
- The filePath parameter must be an absolute path, not a relative path - The filePath parameter should be an absolute path.
- By default, it reads up to 2000 lines starting from the beginning of the file - By default, this tool returns up to 2000 lines from the start of the file.
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters - The offset parameter is the line number to start from (1-indexed).
- Any lines longer than 2000 characters will be truncated - To read later sections, call this tool again with a larger offset.
- Results are returned using cat -n format, with line numbers starting at 1 - Use the grep tool to find specific content in large files or files with long lines.
- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful. - If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.
- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents. - Contents are returned with each line prefixed by its line number as `<line>: <content>`. For example, if a file has contents "foo\n", you will receive "1: foo\n". For directories, entries are returned one per line (without line numbers) with a trailing `/` for subdirectories.
- You can read image files using this tool. - Any line longer than 2000 characters is truncated.
- Call this tool in parallel when you know there are multiple files you want to read.
- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.
- This tool can read image files and PDFs and return them as file attachments.

View file

@ -0,0 +1,233 @@
import { describe, expect, test } from "bun:test"
import path from "path"
import { Session } from "../../src/session"
import { SessionPrompt } from "../../src/session/prompt"
import { Log } from "../../src/util/log"
import { Instance } from "../../src/project/instance"
import { MessageV2 } from "../../src/session/message-v2"
const projectRoot = path.join(__dirname, "../..")
Log.init({ print: false })
// Skip tests if no API key is available
const hasApiKey = !!process.env.ANTHROPIC_API_KEY
// Helper to run test within Instance context
async function withInstance<T>(fn: () => Promise<T>): Promise<T> {
return Instance.provide({
directory: projectRoot,
fn,
})
}
describe("StructuredOutput Integration", () => {
test.skipIf(!hasApiKey)(
"produces structured output with simple schema",
async () => {
await withInstance(async () => {
const session = await Session.create({ title: "Structured Output Test" })
const result = await SessionPrompt.prompt({
sessionID: session.id,
parts: [
{
type: "text",
text: "What is 2 + 2? Provide a simple answer.",
},
],
format: {
type: "json_schema",
schema: {
type: "object",
properties: {
answer: { type: "number", description: "The numerical answer" },
explanation: { type: "string", description: "Brief explanation" },
},
required: ["answer"],
},
retryCount: 0,
},
})
// Verify structured output was captured (only on assistant messages)
expect(result.info.role).toBe("assistant")
if (result.info.role === "assistant") {
expect(result.info.structured).toBeDefined()
expect(typeof result.info.structured).toBe("object")
const output = result.info.structured as any
expect(output.answer).toBe(4)
// Verify no error was set
expect(result.info.error).toBeUndefined()
}
// Clean up
// Note: Not removing session to avoid race with background SessionSummary.summarize
})
},
60000,
)
test.skipIf(!hasApiKey)(
"produces structured output with nested objects",
async () => {
await withInstance(async () => {
const session = await Session.create({ title: "Nested Schema Test" })
const result = await SessionPrompt.prompt({
sessionID: session.id,
parts: [
{
type: "text",
text: "Tell me about Anthropic company in a structured format.",
},
],
format: {
type: "json_schema",
schema: {
type: "object",
properties: {
company: {
type: "object",
properties: {
name: { type: "string" },
founded: { type: "number" },
},
required: ["name", "founded"],
},
products: {
type: "array",
items: { type: "string" },
},
},
required: ["company"],
},
retryCount: 0,
},
})
// Verify structured output was captured (only on assistant messages)
expect(result.info.role).toBe("assistant")
if (result.info.role === "assistant") {
expect(result.info.structured).toBeDefined()
const output = result.info.structured as any
expect(output.company).toBeDefined()
expect(output.company.name).toBe("Anthropic")
expect(typeof output.company.founded).toBe("number")
if (output.products) {
expect(Array.isArray(output.products)).toBe(true)
}
// Verify no error was set
expect(result.info.error).toBeUndefined()
}
// Clean up
// Note: Not removing session to avoid race with background SessionSummary.summarize
})
},
60000,
)
test.skipIf(!hasApiKey)(
"works with text outputFormat (default)",
async () => {
await withInstance(async () => {
const session = await Session.create({ title: "Text Output Test" })
const result = await SessionPrompt.prompt({
sessionID: session.id,
parts: [
{
type: "text",
text: "Say hello.",
},
],
format: {
type: "text",
},
})
// Verify no structured output (text mode) and no error
expect(result.info.role).toBe("assistant")
if (result.info.role === "assistant") {
expect(result.info.structured).toBeUndefined()
expect(result.info.error).toBeUndefined()
}
// Verify we got a response with parts
expect(result.parts.length).toBeGreaterThan(0)
// Clean up
// Note: Not removing session to avoid race with background SessionSummary.summarize
})
},
60000,
)
test.skipIf(!hasApiKey)(
"stores outputFormat on user message",
async () => {
await withInstance(async () => {
const session = await Session.create({ title: "OutputFormat Storage Test" })
await SessionPrompt.prompt({
sessionID: session.id,
parts: [
{
type: "text",
text: "What is 1 + 1?",
},
],
format: {
type: "json_schema",
schema: {
type: "object",
properties: {
result: { type: "number" },
},
required: ["result"],
},
retryCount: 3,
},
})
// Get all messages from session
const messages = await Session.messages({ sessionID: session.id })
const userMessage = messages.find((m) => m.info.role === "user")
// Verify outputFormat was stored on user message
expect(userMessage).toBeDefined()
if (userMessage?.info.role === "user") {
expect(userMessage.info.format).toBeDefined()
expect(userMessage.info.format?.type).toBe("json_schema")
if (userMessage.info.format?.type === "json_schema") {
expect(userMessage.info.format.retryCount).toBe(3)
}
}
// Clean up
// Note: Not removing session to avoid race with background SessionSummary.summarize
})
},
60000,
)
test("unit test: StructuredOutputError is properly structured", () => {
const error = new MessageV2.StructuredOutputError({
message: "Failed to produce valid structured output after 3 attempts",
retries: 3,
})
expect(error.name).toBe("StructuredOutputError")
expect(error.data.message).toContain("3 attempts")
expect(error.data.retries).toBe(3)
const obj = error.toObject()
expect(obj.name).toBe("StructuredOutputError")
expect(obj.data.retries).toBe(3)
})
})

View file

@ -0,0 +1,385 @@
import { describe, expect, test } from "bun:test"
import { MessageV2 } from "../../src/session/message-v2"
import { SessionPrompt } from "../../src/session/prompt"
describe("structured-output.OutputFormat", () => {
test("parses text format", () => {
const result = MessageV2.Format.safeParse({ type: "text" })
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.type).toBe("text")
}
})
test("parses json_schema format with defaults", () => {
const result = MessageV2.Format.safeParse({
type: "json_schema",
schema: { type: "object", properties: { name: { type: "string" } } },
})
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.type).toBe("json_schema")
if (result.data.type === "json_schema") {
expect(result.data.retryCount).toBe(2) // default value
}
}
})
test("parses json_schema format with custom retryCount", () => {
const result = MessageV2.Format.safeParse({
type: "json_schema",
schema: { type: "object" },
retryCount: 5,
})
expect(result.success).toBe(true)
if (result.success && result.data.type === "json_schema") {
expect(result.data.retryCount).toBe(5)
}
})
test("rejects invalid type", () => {
const result = MessageV2.Format.safeParse({ type: "invalid" })
expect(result.success).toBe(false)
})
test("rejects json_schema without schema", () => {
const result = MessageV2.Format.safeParse({ type: "json_schema" })
expect(result.success).toBe(false)
})
test("rejects negative retryCount", () => {
const result = MessageV2.Format.safeParse({
type: "json_schema",
schema: { type: "object" },
retryCount: -1,
})
expect(result.success).toBe(false)
})
})
describe("structured-output.StructuredOutputError", () => {
test("creates error with message and retries", () => {
const error = new MessageV2.StructuredOutputError({
message: "Failed to validate",
retries: 3,
})
expect(error.name).toBe("StructuredOutputError")
expect(error.data.message).toBe("Failed to validate")
expect(error.data.retries).toBe(3)
})
test("converts to object correctly", () => {
const error = new MessageV2.StructuredOutputError({
message: "Test error",
retries: 2,
})
const obj = error.toObject()
expect(obj.name).toBe("StructuredOutputError")
expect(obj.data.message).toBe("Test error")
expect(obj.data.retries).toBe(2)
})
test("isInstance correctly identifies error", () => {
const error = new MessageV2.StructuredOutputError({
message: "Test",
retries: 1,
})
expect(MessageV2.StructuredOutputError.isInstance(error)).toBe(true)
expect(MessageV2.StructuredOutputError.isInstance({ name: "other" })).toBe(false)
})
})
describe("structured-output.UserMessage", () => {
test("user message accepts outputFormat", () => {
const result = MessageV2.User.safeParse({
id: "test-id",
sessionID: "test-session",
role: "user",
time: { created: Date.now() },
agent: "default",
model: { providerID: "anthropic", modelID: "claude-3" },
outputFormat: {
type: "json_schema",
schema: { type: "object" },
},
})
expect(result.success).toBe(true)
})
test("user message works without outputFormat (optional)", () => {
const result = MessageV2.User.safeParse({
id: "test-id",
sessionID: "test-session",
role: "user",
time: { created: Date.now() },
agent: "default",
model: { providerID: "anthropic", modelID: "claude-3" },
})
expect(result.success).toBe(true)
})
})
describe("structured-output.AssistantMessage", () => {
const baseAssistantMessage = {
id: "test-id",
sessionID: "test-session",
role: "assistant" as const,
parentID: "parent-id",
modelID: "claude-3",
providerID: "anthropic",
mode: "default",
agent: "default",
path: { cwd: "/test", root: "/test" },
cost: 0.001,
tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: Date.now() },
}
test("assistant message accepts structured", () => {
const result = MessageV2.Assistant.safeParse({
...baseAssistantMessage,
structured: { company: "Anthropic", founded: 2021 },
})
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.structured).toEqual({ company: "Anthropic", founded: 2021 })
}
})
test("assistant message works without structured_output (optional)", () => {
const result = MessageV2.Assistant.safeParse(baseAssistantMessage)
expect(result.success).toBe(true)
})
})
describe("structured-output.createStructuredOutputTool", () => {
test("creates tool with correct id", () => {
const tool = SessionPrompt.createStructuredOutputTool({
schema: { type: "object", properties: { name: { type: "string" } } },
onSuccess: () => {},
})
// AI SDK tool type doesn't expose id, but we set it internally
expect((tool as any).id).toBe("StructuredOutput")
})
test("creates tool with description", () => {
const tool = SessionPrompt.createStructuredOutputTool({
schema: { type: "object" },
onSuccess: () => {},
})
expect(tool.description).toContain("structured format")
})
test("creates tool with schema as inputSchema", () => {
const schema = {
type: "object",
properties: {
company: { type: "string" },
founded: { type: "number" },
},
required: ["company"],
}
const tool = SessionPrompt.createStructuredOutputTool({
schema,
onSuccess: () => {},
})
// AI SDK wraps schema in { jsonSchema: {...} }
expect(tool.inputSchema).toBeDefined()
const inputSchema = tool.inputSchema as any
expect(inputSchema.jsonSchema?.properties?.company).toBeDefined()
expect(inputSchema.jsonSchema?.properties?.founded).toBeDefined()
})
test("strips $schema property from inputSchema", () => {
const schema = {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: { name: { type: "string" } },
}
const tool = SessionPrompt.createStructuredOutputTool({
schema,
onSuccess: () => {},
})
// AI SDK wraps schema in { jsonSchema: {...} }
const inputSchema = tool.inputSchema as any
expect(inputSchema.jsonSchema?.$schema).toBeUndefined()
})
test("execute calls onSuccess with valid args", async () => {
let capturedOutput: unknown
const tool = SessionPrompt.createStructuredOutputTool({
schema: { type: "object", properties: { name: { type: "string" } } },
onSuccess: (output) => {
capturedOutput = output
},
})
expect(tool.execute).toBeDefined()
const testArgs = { name: "Test Company" }
const result = await tool.execute!(testArgs, {
toolCallId: "test-call-id",
messages: [],
abortSignal: undefined as any,
})
expect(capturedOutput).toEqual(testArgs)
expect(result.output).toBe("Structured output captured successfully.")
expect(result.metadata.valid).toBe(true)
})
test("AI SDK validates schema before execute - missing required field", async () => {
// Note: The AI SDK validates the input against the schema BEFORE calling execute()
// So invalid inputs never reach the tool's execute function
// This test documents the expected schema behavior
const tool = SessionPrompt.createStructuredOutputTool({
schema: {
type: "object",
properties: {
name: { type: "string" },
age: { type: "number" },
},
required: ["name", "age"],
},
onSuccess: () => {},
})
// The schema requires both 'name' and 'age'
expect(tool.inputSchema).toBeDefined()
const inputSchema = tool.inputSchema as any
expect(inputSchema.jsonSchema?.required).toContain("name")
expect(inputSchema.jsonSchema?.required).toContain("age")
})
test("AI SDK validates schema types before execute - wrong type", async () => {
// Note: The AI SDK validates the input against the schema BEFORE calling execute()
// So invalid inputs never reach the tool's execute function
// This test documents the expected schema behavior
const tool = SessionPrompt.createStructuredOutputTool({
schema: {
type: "object",
properties: {
count: { type: "number" },
},
required: ["count"],
},
onSuccess: () => {},
})
// The schema defines 'count' as a number
expect(tool.inputSchema).toBeDefined()
const inputSchema = tool.inputSchema as any
expect(inputSchema.jsonSchema?.properties?.count?.type).toBe("number")
})
test("execute handles nested objects", async () => {
let capturedOutput: unknown
const tool = SessionPrompt.createStructuredOutputTool({
schema: {
type: "object",
properties: {
user: {
type: "object",
properties: {
name: { type: "string" },
email: { type: "string" },
},
required: ["name"],
},
},
required: ["user"],
},
onSuccess: (output) => {
capturedOutput = output
},
})
// Valid nested object - AI SDK validates before calling execute()
const validResult = await tool.execute!(
{ user: { name: "John", email: "john@test.com" } },
{
toolCallId: "test-call-id",
messages: [],
abortSignal: undefined as any,
},
)
expect(capturedOutput).toEqual({ user: { name: "John", email: "john@test.com" } })
expect(validResult.metadata.valid).toBe(true)
// Verify schema has correct nested structure
const inputSchema = tool.inputSchema as any
expect(inputSchema.jsonSchema?.properties?.user?.type).toBe("object")
expect(inputSchema.jsonSchema?.properties?.user?.properties?.name?.type).toBe("string")
expect(inputSchema.jsonSchema?.properties?.user?.required).toContain("name")
})
test("execute handles arrays", async () => {
let capturedOutput: unknown
const tool = SessionPrompt.createStructuredOutputTool({
schema: {
type: "object",
properties: {
tags: {
type: "array",
items: { type: "string" },
},
},
required: ["tags"],
},
onSuccess: (output) => {
capturedOutput = output
},
})
// Valid array - AI SDK validates before calling execute()
const validResult = await tool.execute!(
{ tags: ["a", "b", "c"] },
{
toolCallId: "test-call-id",
messages: [],
abortSignal: undefined as any,
},
)
expect(capturedOutput).toEqual({ tags: ["a", "b", "c"] })
expect(validResult.metadata.valid).toBe(true)
// Verify schema has correct array structure
const inputSchema = tool.inputSchema as any
expect(inputSchema.jsonSchema?.properties?.tags?.type).toBe("array")
expect(inputSchema.jsonSchema?.properties?.tags?.items?.type).toBe("string")
})
test("toModelOutput returns text value", () => {
const tool = SessionPrompt.createStructuredOutputTool({
schema: { type: "object" },
onSuccess: () => {},
})
expect(tool.toModelOutput).toBeDefined()
const modelOutput = tool.toModelOutput!({
output: "Test output",
title: "Test",
metadata: { valid: true },
})
expect(modelOutput.type).toBe("text")
expect(modelOutput.value).toBe("Test output")
})
// Note: Retry behavior is handled by the AI SDK and the prompt loop, not the tool itself
// The tool simply calls onSuccess when execute() is called with valid args
// See prompt.ts loop() for actual retry logic
})

View file

@ -78,6 +78,32 @@ describe("tool.read external_directory permission", () => {
}) })
}) })
test("asks for directory-scoped external_directory permission when reading external directory", async () => {
await using outerTmp = await tmpdir({
init: async (dir) => {
await Bun.write(path.join(dir, "external", "a.txt"), "a")
},
})
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const read = await ReadTool.init()
const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
const testCtx = {
...ctx,
ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
requests.push(req)
},
}
await read.execute({ filePath: path.join(outerTmp.path, "external") }, testCtx)
const extDirReq = requests.find((r) => r.permission === "external_directory")
expect(extDirReq).toBeDefined()
expect(extDirReq!.patterns).toContain(path.join(outerTmp.path, "external", "*"))
},
})
})
test("asks for external_directory permission when reading relative path outside project", async () => { test("asks for external_directory permission when reading relative path outside project", async () => {
await using tmp = await tmpdir({ git: true }) await using tmp = await tmpdir({ git: true })
await Instance.provide({ await Instance.provide({
@ -232,7 +258,7 @@ describe("tool.read truncation", () => {
test("respects offset parameter", async () => { test("respects offset parameter", async () => {
await using tmp = await tmpdir({ await using tmp = await tmpdir({
init: async (dir) => { init: async (dir) => {
const lines = Array.from({ length: 20 }, (_, i) => `line${i}`).join("\n") const lines = Array.from({ length: 20 }, (_, i) => `line${i + 1}`).join("\n")
await Bun.write(path.join(dir, "offset.txt"), lines) await Bun.write(path.join(dir, "offset.txt"), lines)
}, },
}) })
@ -249,6 +275,43 @@ describe("tool.read truncation", () => {
}) })
}) })
test("throws when offset is beyond end of file", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const lines = Array.from({ length: 3 }, (_, i) => `line${i + 1}`).join("\n")
await Bun.write(path.join(dir, "short.txt"), lines)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const read = await ReadTool.init()
await expect(
read.execute({ filePath: path.join(tmp.path, "short.txt"), offset: 4, limit: 5 }, ctx),
).rejects.toThrow("Offset 4 is out of range for this file (3 lines)")
},
})
})
test("does not mark final directory page as truncated", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Promise.all(
Array.from({ length: 10 }, (_, i) => Bun.write(path.join(dir, "dir", `file-${i + 1}.txt`), `line${i}`)),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const read = await ReadTool.init()
const result = await read.execute({ filePath: path.join(tmp.path, "dir"), offset: 6, limit: 5 }, ctx)
expect(result.metadata.truncated).toBe(false)
expect(result.output).not.toContain("Showing 5 of 10 entries")
},
})
})
test("truncates long lines", async () => { test("truncates long lines", async () => {
await using tmp = await tmpdir({ await using tmp = await tmpdir({
init: async (dir) => { init: async (dir) => {

View file

@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.1.59", "version": "1.1.60",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {

View file

@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.1.59", "version": "1.1.60",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {

View file

@ -57,6 +57,7 @@ import type {
McpLocalConfig, McpLocalConfig,
McpRemoteConfig, McpRemoteConfig,
McpStatusResponses, McpStatusResponses,
OutputFormat,
Part as Part2, Part as Part2,
PartDeleteErrors, PartDeleteErrors,
PartDeleteResponses, PartDeleteResponses,
@ -1475,6 +1476,7 @@ export class Session extends HeyApiClient {
tools?: { tools?: {
[key: string]: boolean [key: string]: boolean
} }
format?: OutputFormat
system?: string system?: string
variant?: string variant?: string
parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput> parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
@ -1493,6 +1495,7 @@ export class Session extends HeyApiClient {
{ in: "body", key: "agent" }, { in: "body", key: "agent" },
{ in: "body", key: "noReply" }, { in: "body", key: "noReply" },
{ in: "body", key: "tools" }, { in: "body", key: "tools" },
{ in: "body", key: "format" },
{ in: "body", key: "system" }, { in: "body", key: "system" },
{ in: "body", key: "variant" }, { in: "body", key: "variant" },
{ in: "body", key: "parts" }, { in: "body", key: "parts" },
@ -1563,6 +1566,7 @@ export class Session extends HeyApiClient {
tools?: { tools?: {
[key: string]: boolean [key: string]: boolean
} }
format?: OutputFormat
system?: string system?: string
variant?: string variant?: string
parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput> parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
@ -1581,6 +1585,7 @@ export class Session extends HeyApiClient {
{ in: "body", key: "agent" }, { in: "body", key: "agent" },
{ in: "body", key: "noReply" }, { in: "body", key: "noReply" },
{ in: "body", key: "tools" }, { in: "body", key: "tools" },
{ in: "body", key: "format" },
{ in: "body", key: "system" }, { in: "body", key: "system" },
{ in: "body", key: "variant" }, { in: "body", key: "variant" },
{ in: "body", key: "parts" }, { in: "body", key: "parts" },

View file

@ -90,6 +90,22 @@ export type EventFileEdited = {
} }
} }
export type OutputFormatText = {
type: "text"
}
export type JsonSchema = {
[key: string]: unknown
}
export type OutputFormatJsonSchema = {
type: "json_schema"
schema: JsonSchema
retryCount?: number
}
export type OutputFormat = OutputFormatText | OutputFormatJsonSchema
export type FileDiff = { export type FileDiff = {
file: string file: string
before: string before: string
@ -106,6 +122,7 @@ export type UserMessage = {
time: { time: {
created: number created: number
} }
format?: OutputFormat
summary?: { summary?: {
title?: string title?: string
body?: string body?: string
@ -152,6 +169,14 @@ export type MessageAbortedError = {
} }
} }
export type StructuredOutputError = {
name: "StructuredOutputError"
data: {
message: string
retries: number
}
}
export type ContextOverflowError = { export type ContextOverflowError = {
name: "ContextOverflowError" name: "ContextOverflowError"
data: { data: {
@ -189,6 +214,7 @@ export type AssistantMessage = {
| UnknownError | UnknownError
| MessageOutputLengthError | MessageOutputLengthError
| MessageAbortedError | MessageAbortedError
| StructuredOutputError
| ContextOverflowError | ContextOverflowError
| ApiError | ApiError
parentID: string parentID: string
@ -212,6 +238,7 @@ export type AssistantMessage = {
write: number write: number
} }
} }
structured?: unknown
variant?: string variant?: string
finish?: string finish?: string
} }
@ -841,6 +868,7 @@ export type EventSessionError = {
| UnknownError | UnknownError
| MessageOutputLengthError | MessageOutputLengthError
| MessageAbortedError | MessageAbortedError
| StructuredOutputError
| ContextOverflowError | ContextOverflowError
| ApiError | ApiError
} }
@ -1521,6 +1549,7 @@ export type ProviderConfig = {
} }
provider?: { provider?: {
npm: string npm: string
api: string
} }
/** /**
* Variant-specific configuration * Variant-specific configuration
@ -3402,6 +3431,7 @@ export type SessionPromptData = {
tools?: { tools?: {
[key: string]: boolean [key: string]: boolean
} }
format?: OutputFormat
system?: string system?: string
variant?: string variant?: string
parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput> parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
@ -3589,6 +3619,7 @@ export type SessionPromptAsyncData = {
tools?: { tools?: {
[key: string]: boolean [key: string]: boolean
} }
format?: OutputFormat
system?: string system?: string
variant?: string variant?: string
parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput> parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
@ -4083,6 +4114,7 @@ export type ProviderListResponses = {
} }
provider?: { provider?: {
npm: string npm: string
api: string
} }
variants?: { variants?: {
[key: string]: { [key: string]: {

View file

@ -2420,6 +2420,9 @@
"type": "boolean" "type": "boolean"
} }
}, },
"format": {
"$ref": "#/components/schemas/OutputFormat"
},
"system": { "system": {
"type": "string" "type": "string"
}, },
@ -2796,6 +2799,9 @@
"type": "boolean" "type": "boolean"
} }
}, },
"format": {
"$ref": "#/components/schemas/OutputFormat"
},
"system": { "system": {
"type": "string" "type": "string"
}, },
@ -3892,9 +3898,12 @@
"properties": { "properties": {
"npm": { "npm": {
"type": "string" "type": "string"
},
"api": {
"type": "string"
} }
}, },
"required": ["npm"] "required": ["npm", "api"]
}, },
"variants": { "variants": {
"type": "object", "type": "object",
@ -6172,6 +6181,52 @@
}, },
"required": ["type", "properties"] "required": ["type", "properties"]
}, },
"OutputFormatText": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "text"
}
},
"required": ["type"]
},
"JSONSchema": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"OutputFormatJsonSchema": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "json_schema"
},
"schema": {
"$ref": "#/components/schemas/JSONSchema"
},
"retryCount": {
"default": 2,
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}
},
"required": ["type", "schema"]
},
"OutputFormat": {
"anyOf": [
{
"$ref": "#/components/schemas/OutputFormatText"
},
{
"$ref": "#/components/schemas/OutputFormatJsonSchema"
}
]
},
"FileDiff": { "FileDiff": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -6219,6 +6274,9 @@
}, },
"required": ["created"] "required": ["created"]
}, },
"format": {
"$ref": "#/components/schemas/OutputFormat"
},
"summary": { "summary": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -6344,6 +6402,28 @@
}, },
"required": ["name", "data"] "required": ["name", "data"]
}, },
"StructuredOutputError": {
"type": "object",
"properties": {
"name": {
"type": "string",
"const": "StructuredOutputError"
},
"data": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"retries": {
"type": "number"
}
},
"required": ["message", "retries"]
}
},
"required": ["name", "data"]
},
"ContextOverflowError": { "ContextOverflowError": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -6451,6 +6531,9 @@
{ {
"$ref": "#/components/schemas/MessageAbortedError" "$ref": "#/components/schemas/MessageAbortedError"
}, },
{
"$ref": "#/components/schemas/StructuredOutputError"
},
{ {
"$ref": "#/components/schemas/ContextOverflowError" "$ref": "#/components/schemas/ContextOverflowError"
}, },
@ -6522,6 +6605,7 @@
}, },
"required": ["input", "output", "reasoning", "cache"] "required": ["input", "output", "reasoning", "cache"]
}, },
"structured": {},
"variant": { "variant": {
"type": "string" "type": "string"
}, },
@ -8227,6 +8311,9 @@
{ {
"$ref": "#/components/schemas/MessageAbortedError" "$ref": "#/components/schemas/MessageAbortedError"
}, },
{
"$ref": "#/components/schemas/StructuredOutputError"
},
{ {
"$ref": "#/components/schemas/ContextOverflowError" "$ref": "#/components/schemas/ContextOverflowError"
}, },
@ -9416,9 +9503,12 @@
"properties": { "properties": {
"npm": { "npm": {
"type": "string" "type": "string"
},
"api": {
"type": "string"
} }
}, },
"required": ["npm"] "required": ["npm", "api"]
}, },
"variants": { "variants": {
"description": "Variant-specific configuration", "description": "Variant-specific configuration",

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.1.59", "version": "1.1.60",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.1.59", "version": "1.1.60",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"exports": { "exports": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/util", "name": "@opencode-ai/util",
"version": "1.1.59", "version": "1.1.60",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",

View file

@ -2,7 +2,7 @@
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"version": "1.1.59", "version": "1.1.60",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",

View file

@ -45,7 +45,7 @@ description: هيّئ الوكلاء المتخصصين واستخدمهم.
--- ---
### استخدام build ### استخدام Build
_الوضع_: `primary` _الوضع_: `primary`
@ -53,7 +53,7 @@ Build هو الوكيل الأساسي **الافتراضي** مع تفعيل ج
--- ---
### استخدام plan ### استخدام Plan
_الوضع_: `primary` _الوضع_: `primary`
@ -67,7 +67,7 @@ _الوضع_: `primary`
--- ---
### استخدام general ### استخدام General
_الوضع_: `subagent` _الوضع_: `subagent`
@ -75,7 +75,7 @@ _الوضع_: `subagent`
--- ---
### استخدام explore ### استخدام Explore
_الوضع_: `subagent` _الوضع_: `subagent`
@ -83,7 +83,7 @@ _الوضع_: `subagent`
--- ---
### استخدام compaction ### استخدام Compaction
_الوضع_: `primary` _الوضع_: `primary`
@ -91,7 +91,7 @@ _الوضع_: `primary`
--- ---
### استخدام title ### استخدام Title
_الوضع_: `primary` _الوضع_: `primary`
@ -99,7 +99,7 @@ _الوضع_: `primary`
--- ---
### استخدام summary ### استخدام Summary
_الوضع_: `primary` _الوضع_: `primary`

View file

@ -69,10 +69,10 @@ opencode attach [url]
يتيح ذلك استخدام واجهة TUI مع واجهة خلفية لـ OpenCode تعمل عن بعد. على سبيل المثال: يتيح ذلك استخدام واجهة TUI مع واجهة خلفية لـ OpenCode تعمل عن بعد. على سبيل المثال:
```bash ```bash
# Start the backend server for web/mobile access # ابدأ خادم الواجهة الخلفية للوصول عبر الويب/الجوال
opencode web --port 4096 --hostname 0.0.0.0 opencode web --port 4096 --hostname 0.0.0.0
# In another terminal, attach the TUI to the running backend # في محطة طرفية (terminal) أخرى، اربط TUI بالواجهة الخلفية قيد التشغيل
opencode attach http://10.20.30.40:4096 opencode attach http://10.20.30.40:4096
``` ```
@ -326,10 +326,10 @@ opencode run Explain the use of context in Go
يمكنك أيضا الإرفاق بمثيل `opencode serve` قيد التشغيل لتجنّب زمن الإقلاع البارد لخوادم MCP في كل تشغيل: يمكنك أيضا الإرفاق بمثيل `opencode serve` قيد التشغيل لتجنّب زمن الإقلاع البارد لخوادم MCP في كل تشغيل:
```bash ```bash
# Start a headless server in one terminal # ابدأ خادمًا بلا واجهة في محطة طرفية واحدة
opencode serve opencode serve
# In another terminal, run commands that attach to it # في محطة طرفية أخرى، شغّل الأوامر التي ترتبط به
opencode run --attach http://localhost:4096 "Explain async/await in JavaScript" opencode run --attach http://localhost:4096 "Explain async/await in JavaScript"
``` ```

View file

@ -54,11 +54,11 @@ Focus on the failing tests and suggest fixes.
{ {
"$schema": "https://opencode.ai/config.json", "$schema": "https://opencode.ai/config.json",
"command": { "command": {
// This becomes the name of the command // يصبح هذا اسم الأمر
"test": { "test": {
// This is the prompt that will be sent to the LLM // هذه هي المطالبة التي ستُرسل إلى LLM
"template": "Run the full test suite with coverage report and show any failures.\nFocus on the failing tests and suggest fixes.", "template": "Run the full test suite with coverage report and show any failures.\nFocus on the failing tests and suggest fixes.",
// This is shown as the description in the TUI // يظهر هذا كوصف في TUI
"description": "Run tests with coverage", "description": "Run tests with coverage",
"agent": "build", "agent": "build",
"model": "anthropic/claude-3-5-sonnet-20241022" "model": "anthropic/claude-3-5-sonnet-20241022"

View file

@ -14,7 +14,7 @@ description: استخدام ملف إعدادات OpenCode بصيغة JSON.
```jsonc title="opencode.jsonc" ```jsonc title="opencode.jsonc"
{ {
"$schema": "https://opencode.ai/config.json", "$schema": "https://opencode.ai/config.json",
// Theme configuration // إعدادات السمة
"theme": "opencode", "theme": "opencode",
"model": "anthropic/claude-sonnet-4-5", "model": "anthropic/claude-sonnet-4-5",
"autoupdate": true, "autoupdate": true,
@ -326,7 +326,7 @@ opencode run "Hello world"
"model": "anthropic/claude-sonnet-4-5", "model": "anthropic/claude-sonnet-4-5",
"prompt": "You are a code reviewer. Focus on security, performance, and maintainability.", "prompt": "You are a code reviewer. Focus on security, performance, and maintainability.",
"tools": { "tools": {
// Disable file modification tools for review-only agent // تعطيل أدوات تعديل الملفات لوكيل المراجعة فقط
"write": false, "write": false,
"edit": false, "edit": false,
}, },

View file

@ -221,7 +221,7 @@ How is authentication handled in @packages/functions/src/api/index.ts
انتقل إليه باستخدام مفتاح **Tab**. سترى مؤشرا لذلك في الزاوية السفلية اليمنى. انتقل إليه باستخدام مفتاح **Tab**. سترى مؤشرا لذلك في الزاوية السفلية اليمنى.
```bash frame="none" title="Switch to Plan mode" ```bash frame="none" title="التبديل إلى وضع Plan"
<TAB> <TAB>
``` ```

View file

@ -278,10 +278,10 @@ opencode mcp logout my-oauth-server
إذا فشل خادم MCP بعيد في المصادقة، يمكنك تشخيص المشكلة باستخدام: إذا فشل خادم MCP بعيد في المصادقة، يمكنك تشخيص المشكلة باستخدام:
```bash ```bash
# View auth status for all OAuth-capable servers # عرض حالة المصادقة لجميع الخوادم القادرة على OAuth
opencode mcp auth list opencode mcp auth list
# Debug connection and OAuth flow for a specific server # تصحيح الاتصال وتدفق OAuth لخادم محدد
opencode mcp debug my-oauth-server opencode mcp debug my-oauth-server
``` ```

View file

@ -12,13 +12,13 @@ description: إعداد الوكلاء والشهادات المخصصة.
يتبع OpenCode متغيرات بيئة الوكيل القياسية. يتبع OpenCode متغيرات بيئة الوكيل القياسية.
```bash ```bash
# HTTPS proxy (recommended) # وكيل HTTPS (موصى به)
export HTTPS_PROXY=https://proxy.example.com:8080 export HTTPS_PROXY=https://proxy.example.com:8080
# HTTP proxy (if HTTPS not available) # وكيل HTTP (إذا لم يتوفر HTTPS)
export HTTP_PROXY=http://proxy.example.com:8080 export HTTP_PROXY=http://proxy.example.com:8080
# Bypass proxy for local server (required) # تجاوز الوكيل للخادم المحلي (مطلوب)
export NO_PROXY=localhost,127.0.0.1 export NO_PROXY=localhost,127.0.0.1
``` ```

View file

@ -89,7 +89,7 @@ OpenCode Zen هي قائمة نماذج يوفّرها فريق OpenCode وقد
فلا تتردد في فتح PR. فلا تتردد في فتح PR.
:::note :::note
ألا ترى مزوّدا هنا؟ أرسل PR. لم تجد المزوّد الذي تبحث عنه؟ أرسل PR.
::: :::
--- ---
@ -139,13 +139,13 @@ OpenCode Zen هي قائمة نماذج يوفّرها فريق OpenCode وقد
عيّن أحد متغيرات البيئة التالية أثناء تشغيل opencode: عيّن أحد متغيرات البيئة التالية أثناء تشغيل opencode:
```bash ```bash
# Option 1: Using AWS access keys # الخيار 1: استخدام مفاتيح وصول AWS
AWS_ACCESS_KEY_ID=XXX AWS_SECRET_ACCESS_KEY=YYY opencode AWS_ACCESS_KEY_ID=XXX AWS_SECRET_ACCESS_KEY=YYY opencode
# Option 2: Using named AWS profile # الخيار 2: استخدام ملف تعريف AWS مسمّى
AWS_PROFILE=my-profile opencode AWS_PROFILE=my-profile opencode
# Option 3: Using Bedrock bearer token # الخيار 3: استخدام رمز Bedrock المميز (bearer token)
AWS_BEARER_TOKEN_BEDROCK=XXX opencode AWS_BEARER_TOKEN_BEDROCK=XXX opencode
``` ```

View file

@ -366,7 +366,7 @@ OPENCODE_ENABLE_EXA=1 opencode
--- ---
### Ignore patterns ### أنماط التجاهل
لتضمين ملفات يتم تجاهلها عادة، أنشئ ملف `.ignore` في جذر المشروع. يمكن لهذا الملف السماح صراحة بمسارات محددة. لتضمين ملفات يتم تجاهلها عادة، أنشئ ملف `.ignore` في جذر المشروع. يمكن لهذا الملف السماح صراحة بمسارات محددة.

View file

@ -290,12 +290,12 @@ How is auth handled in @packages/functions/src/api/index.ts?
<Tabs> <Tabs>
<TabItem label="Linux/macOS"> <TabItem label="Linux/macOS">
```bash ```bash
# Example for nano or vim # مثال لـ nano أو vim
export EDITOR=nano export EDITOR=nano
export EDITOR=vim export EDITOR=vim
# For GUI editors, VS Code, Cursor, VSCodium, Windsurf, Zed, etc. # للمحررات الرسومية، VS Code، Cursor، VSCodium، Windsurf، Zed، إلخ.
# include --wait # قم بتضمين --wait
export EDITOR="code --wait" export EDITOR="code --wait"
``` ```
@ -308,8 +308,8 @@ How is auth handled in @packages/functions/src/api/index.ts?
```bash ```bash
set EDITOR=notepad set EDITOR=notepad
# For GUI editors, VS Code, Cursor, VSCodium, Windsurf, Zed, etc. # للمحررات الرسومية، VS Code، Cursor، VSCodium، Windsurf، Zed، إلخ.
# include --wait # قم بتضمين --wait
set EDITOR=code --wait set EDITOR=code --wait
``` ```
@ -321,8 +321,8 @@ How is auth handled in @packages/functions/src/api/index.ts?
```powershell ```powershell
$env:EDITOR = "notepad" $env:EDITOR = "notepad"
# For GUI editors, VS Code, Cursor, VSCodium, Windsurf, Zed, etc. # للمحررات الرسومية، VS Code، Cursor، VSCodium، Windsurf، Zed، إلخ.
# include --wait # قم بتضمين --wait
$env:EDITOR = "code --wait" $env:EDITOR = "code --wait"
``` ```

View file

@ -113,10 +113,10 @@ OPENCODE_SERVER_PASSWORD=secret opencode web
يمكنك إرفاق واجهة terminal (TUI) بخادم ويب قيد التشغيل: يمكنك إرفاق واجهة terminal (TUI) بخادم ويب قيد التشغيل:
```bash ```bash
# Start the web server # ابدأ خادم الويب
opencode web --port 4096 opencode web --port 4096
# In another terminal, attach the TUI # في محطة طرفية أخرى، اربط TUI
opencode attach http://localhost:4096 opencode attach http://localhost:4096
``` ```

View file

@ -13,7 +13,7 @@ ACP je otvoreni protokol koji standardizira komunikaciju između uređivača kod
--- ---
## Konfiguriši ## Konfiguracija
Da biste koristili OpenCode putem ACP-a, konfigurirajte svoj uređivač da pokrene naredbu `opencode acp`. Da biste koristili OpenCode putem ACP-a, konfigurirajte svoj uređivač da pokrene naredbu `opencode acp`.
@ -140,7 +140,7 @@ Ova konfiguracija postavlja CodeCompanion da koristi OpenCode kao ACP chat agent
Ako trebate proslijediti varijable okruženja (kao što je `OPENCODE_API_KEY`), pogledajte [Configuring Adapters: Environment Variables](https://codecompanion.olimorris.dev/getting-started#setting-an-api-key) u dokumentaciji CodeCompanion.nvim. Ako trebate proslijediti varijable okruženja (kao što je `OPENCODE_API_KEY`), pogledajte [Configuring Adapters: Environment Variables](https://codecompanion.olimorris.dev/getting-started#setting-an-api-key) u dokumentaciji CodeCompanion.nvim.
## Podrška ## Podržane funkcije
OpenCode radi isto kroz ACP kao i u terminalu. Podržane su sve funkcije: OpenCode radi isto kroz ACP kao i u terminalu. Podržane su sve funkcije:

View file

@ -15,7 +15,7 @@ Možete se prebacivati između agenata tokom sesije ili ih pozvati spominj
## Vrste ## Vrste
Postoje dvije vrste agenata u OpenCode; primarni agenti i podagenti. Postoje dvije vrste agenata u OpenCode: primarni agenti i podagenti.
--- ---
@ -45,17 +45,17 @@ OpenCode dolazi sa dva ugrađena primarna agenta i dva ugrađena subagenta.
--- ---
### Koristi build ### Build agent
_Mode_: `primary` _Režim_: `primary`
Build je **podrazumevani** primarni agent sa svim omogućenim alatima. Ovo je standardni agent za razvojni rad gdje vam je potreban pun pristup operacijama datoteka i sistemskim komandama. Build je **podrazumevani** primarni agent sa svim omogućenim alatima. Ovo je standardni agent za razvojni rad gdje vam je potreban pun pristup operacijama datoteka i sistemskim komandama.
--- ---
### Koristi plan ### Plan agent
_Mode_: `primary` _Režim_: `primary`
Ograničeni agent dizajniran za planiranje i analizu. Koristimo sistem dozvola kako bismo vam pružili veću kontrolu i spriječili neželjene promjene. Ograničeni agent dizajniran za planiranje i analizu. Koristimo sistem dozvola kako bismo vam pružili veću kontrolu i spriječili neželjene promjene.
Prema zadanim postavkama, sve sljedeće je postavljeno na `ask`: Prema zadanim postavkama, sve sljedeće je postavljeno na `ask`:
@ -67,47 +67,47 @@ Ovaj agent je koristan kada želite da LLM analizira kod, predloži promjene ili
--- ---
### Koristi general ### General agent
_Mode_: `subagent` _Režim_: `subagent`
Agent opće namjene za istraživanje složenih pitanja i izvršavanje zadataka u više koraka. Ima potpuni pristup alatima (osim todo), tako da može mijenjati fajlove kada je to potrebno. Koristite ovo za paralelno pokretanje više jedinica rada. Agent opće namjene za istraživanje složenih pitanja i izvršavanje zadataka u više koraka. Ima potpuni pristup alatima (osim todo), tako da može mijenjati fajlove kada je to potrebno. Koristite ovo za paralelno pokretanje više jedinica rada.
--- ---
### Koristi explore ### Explore agent
_Mode_: `subagent` _Režim_: `subagent`
Brzi agent samo za čitanje za istraživanje kodnih baza. Nije moguće mijenjati fajlove. Koristite ovo kada trebate brzo pronaći datoteke po uzorku, pretražiti kod za ključne riječi ili odgovoriti na pitanja o bazi kodova. Brzi agent samo za čitanje za istraživanje kodnih baza. Nije moguće mijenjati fajlove. Koristite ovo kada trebate brzo pronaći datoteke po uzorku, pretražiti kod za ključne riječi ili odgovoriti na pitanja o bazi kodova.
--- ---
### Koristi compaction ### Compaction agent
_Mode_: `primary` _Režim_: `primary`
Skriveni sistemski agent koji sažima dugi kontekst u manji sažetak. Pokreće se automatski kada je potrebno i ne može se odabrati u korisničkom interfejsu. Skriveni sistemski agent koji sažima dugi kontekst u manji sažetak. Pokreće se automatski kada je potrebno i ne može se odabrati u korisničkom interfejsu.
--- ---
### Koristi title ### Title agent
_Mode_: `primary` _Režim_: `primary`
Skriveni sistemski agent koji generiše kratke naslove sesija. Pokreće se automatski i ne može se odabrati u korisničkom interfejsu. Skriveni sistemski agent koji generiše kratke naslove sesija. Pokreće se automatski i ne može se odabrati u korisničkom interfejsu.
--- ---
### Koristi summary ### Summary agent
_Mode_: `primary` _Režim_: `primary`
Skriveni sistemski agent koji kreira sažetke sesije. Pokreće se automatski i ne može se odabrati u korisničkom interfejsu. Skriveni sistemski agent koji kreira sažetke sesije. Pokreće se automatski i ne može se odabrati u korisničkom interfejsu.
--- ---
## Upotreba ## Korištenje
1. Za primarne agente, koristite taster **Tab** za kretanje kroz njih tokom sesije. Također možete koristiti svoju konfiguriranu vezu tipke `switch_agent`. 1. Za primarne agente, koristite taster **Tab** za kretanje kroz njih tokom sesije. Također možete koristiti svoju konfiguriranu vezu tipke `switch_agent`.
@ -215,7 +215,7 @@ Pogledajmo ove opcije konfiguracije detaljno.
--- ---
### Description ### Opis
Koristite opciju `description` da pružite kratak opis onoga što agent radi i kada ga koristiti. Koristite opciju `description` da pružite kratak opis onoga što agent radi i kada ga koristiti.
@ -233,7 +233,7 @@ Ovo je **obavezna** opcija konfiguracije.
--- ---
### Temperature ### Temperatura
Kontrolišite slučajnost i kreativnost odgovora LLM-a pomoću `temperature` konfiguracije. Kontrolišite slučajnost i kreativnost odgovora LLM-a pomoću `temperature` konfiguracije.
@ -280,7 +280,7 @@ Ako temperatura nije navedena, OpenCode koristi standardne postavke specifične
--- ---
### Max steps ### Maksimalan broj koraka
Kontrolirajte maksimalni broj iteracija agenta koje agent može izvesti prije nego što bude prisiljen da odgovori samo tekstom. Ovo omogućava korisnicima koji žele kontrolirati troškove da postave ograničenje na akcije agenta. Kontrolirajte maksimalni broj iteracija agenta koje agent može izvesti prije nego što bude prisiljen da odgovori samo tekstom. Ovo omogućava korisnicima koji žele kontrolirati troškove da postave ograničenje na akcije agenta.
@ -306,7 +306,7 @@ Naslijeđeno polje `maxSteps` je zastarjelo. Umjesto toga koristite `steps`.
--- ---
### Disable ### Onemogućavanje
Postavite na `true` da onemogućite agenta. Postavite na `true` da onemogućite agenta.
@ -322,7 +322,7 @@ Postavite na `true` da onemogućite agenta.
--- ---
### Prompt ### Upit
Navedite prilagođenu sistemsku prompt datoteku za ovog agenta sa `prompt` konfiguracijom. Datoteka s promptom treba da sadrži upute specifične za svrhu agenta. Navedite prilagođenu sistemsku prompt datoteku za ovog agenta sa `prompt` konfiguracijom. Datoteka s promptom treba da sadrži upute specifične za svrhu agenta.
@ -362,7 +362,7 @@ ID modela u vašoj OpenCode konfiguraciji koristi format `provider/model-id`. Na
--- ---
### Tools ### Alati
Kontrolirajte koji su alati dostupni u ovom agentu koristeći konfiguraciju `tools`. Možete omogućiti ili onemogućiti određene alate tako što ćete ih postaviti na `true` ili `false`. Kontrolirajte koji su alati dostupni u ovom agentu koristeći konfiguraciju `tools`. Možete omogućiti ili onemogućiti određene alate tako što ćete ih postaviti na `true` ili `false`.
@ -409,7 +409,7 @@ Također možete koristiti zamjenske znakove za kontrolu više alata odjednom. N
--- ---
### Permissions ### Dozvole
Možete konfigurirati dozvole za upravljanje radnjama koje agent može poduzeti. Trenutno se dozvole za alate `edit`, `bash` i `webfetch` mogu konfigurirati na: Možete konfigurirati dozvole za upravljanje radnjama koje agent može poduzeti. Trenutno se dozvole za alate `edit`, `bash` i `webfetch` mogu konfigurirati na:
@ -521,7 +521,7 @@ Budući da posljednje podudarno pravilo ima prednost, prvo postavite zamjenski z
--- ---
### Mode ### Način rada
Kontrolirajte način rada agenta koristeći konfiguraciju `mode`. Opcija `mode` se koristi da specificira kako se agent može koristiti. Kontrolirajte način rada agenta koristeći konfiguraciju `mode`. Opcija `mode` se koristi da specificira kako se agent može koristiti.
@ -539,7 +539,7 @@ Opcija `mode` se može postaviti na `primary`, `subagent` ili `all`. Ako `mode`
--- ---
### Hidden ### Skriveno
Sakrij podagenta iz `@` menija za automatsko dovršavanje sa `hidden: true`. Korisno za interne podagente koje bi drugi agenti trebali programski pozvati samo preko Task alata. Sakrij podagenta iz `@` menija za automatsko dovršavanje sa `hidden: true`. Korisno za interne podagente koje bi drugi agenti trebali programski pozvati samo preko Task alata.
@ -562,7 +562,7 @@ Odnosi se samo na `mode: subagent` agente.
--- ---
### Task permissions ### Dozvole zadataka
Kontrolirajte koje podagente agent može pozvati preko Task alata sa `permission.task`. Koristi glob uzorke za fleksibilno uparivanje. Kontrolirajte koje podagente agent može pozvati preko Task alata sa `permission.task`. Koristi glob uzorke za fleksibilno uparivanje.
@ -595,7 +595,7 @@ Korisnici uvijek mogu pozvati bilo kojeg subagenta direktno preko `@` menija za
--- ---
### Color ### Boja
Prilagodite vizualni izgled agenta u korisničkom sučelju s opcijom `color`. Ovo utiče na to kako se agent pojavljuje u interfejsu. Prilagodite vizualni izgled agenta u korisničkom sučelju s opcijom `color`. Ovo utiče na to kako se agent pojavljuje u interfejsu.
@ -634,7 +634,7 @@ Vrijednosti se kreću od 0.0 do 1.0. Niže vrijednosti su više fokusirane, viš
--- ---
### Additional ### Dodatno
Sve druge opcije koje navedete u konfiguraciji agenta će biti **direktno proslijeđene** dobavljaču kao opcije modela. Ovo vam omogućava da koristite karakteristike i parametre specifične za provajdera. Sve druge opcije koje navedete u konfiguraciji agenta će biti **direktno proslijeđene** dobavljaču kao opcije modela. Ovo vam omogućava da koristite karakteristike i parametre specifične za provajdera.
@ -661,7 +661,7 @@ Pokrenite `opencode models` da vidite listu dostupnih modela.
--- ---
## Kreirajte agente ## Kreiranje agenata
Možete kreirati nove agente koristeći sljedeću naredbu: Možete kreirati nove agente koristeći sljedeću naredbu:
@ -679,7 +679,7 @@ Ova interaktivna komanda će:
--- ---
## Slučajevi upotrebe ## Primjeri upotrebe
Evo nekoliko uobičajenih slučajeva upotrebe različitih agenata. Evo nekoliko uobičajenih slučajeva upotrebe različitih agenata.

View file

@ -11,7 +11,7 @@ OpenCode CLI po defaultu pokreće [TUI](/docs/tui) kada se pokrene bez ikakvih a
opencode opencode
``` ```
Ali takođe prihvata komande kao što je dokumentovano na ovoj stranici. Ovo vam omogućava programsku interakciju sa OpenCode. Ali takođe prihvata naredbe kao što je dokumentovano na ovoj stranici. Ovo vam omogućava programsku interakciju sa OpenCode.
```bash ```bash
opencode run "Explain how closures work in JavaScript" opencode run "Explain how closures work in JavaScript"
@ -21,36 +21,36 @@ opencode run "Explain how closures work in JavaScript"
### tui ### tui
Pokrenite korisnički interfejs OpenCode terminala. Pokrenite OpenCode terminalski korisnički interfejs.
```bash ```bash
opencode [project] opencode [project]
``` ```
#### Zastave #### Opcije
| Zastava | Kratko | Opis | | Opcija | Kratko | Opis |
| ------------ | ------ | ------------------------------------------------------------------------ | | ------------ | ------ | ------------------------------------------------------------------------ |
| `--continue` | `-c` | Nastavite posljednju sesiju | | `--continue` | `-c` | Nastavite posljednju sesiju |
| `--session` | `-s` | ID sesije za nastavak | | `--session` | `-s` | ID sesije za nastavak |
| `--fork` | | Forkujte sesiju pri nastavku (koristiti sa `--continue` ili `--session`) | | `--fork` | | Forkujte sesiju pri nastavku (koristiti sa `--continue` ili `--session`) |
| `--prompt` | | Uputstvo za upotrebu | | `--prompt` | | Prompt za upotrebu |
| `--model` | `-m` | Model za korištenje u obliku dobavljača/modela | | `--model` | `-m` | Model za korištenje u obliku provider/model |
| `--agent` | | Agent za korištenje | | `--agent` | | Agent za korištenje |
| `--port` | | Port za slušanje na | | `--port` | | Port na kojem treba slušati |
| `--hostname` | | Slušajte ime hosta | | `--hostname` | | Hostname na kojem treba slušati |
--- ---
## Commands ## Naredbe
OpenCode CLI takođe ima sledeće komande. OpenCode CLI takođe ima sljedeće naredbe.
--- ---
### agent ### agent
Upravljajte agentima za OpenCode. Upravljajte OpenCode agentima.
```bash ```bash
opencode agent [command] opencode agent [command]
@ -60,13 +60,13 @@ opencode agent [command]
### attach ### attach
Priključite terminal na već pokrenut OpenCode backend server pokrenut putem `serve` ili `web` komandi. Priključite terminal na već pokrenut OpenCode backend server pokrenut putem `serve` ili `web` naredbi.
```bash ```bash
opencode attach [url] opencode attach [url]
``` ```
Ovo omogućava korištenje TUI-ja sa udaljenim OpenCode backend-om. na primjer: Ovo omogućava korištenje TUI-ja sa udaljenim OpenCode backend-om. Na primjer:
```bash ```bash
# Start the backend server for web/mobile access # Start the backend server for web/mobile access
@ -76,11 +76,11 @@ opencode web --port 4096 --hostname 0.0.0.0
opencode attach http://10.20.30.40:4096 opencode attach http://10.20.30.40:4096
``` ```
#### Zastave #### Opcije
| Zastava | Kratko | Opis | | Opcija | Kratko | Opis |
| ----------- | ------ | ------------------------------------ | | ----------- | ------ | ------------------------------------ |
| `--dir` | | Radni direktorij za pokretanje TUI u | | `--dir` | | Radni direktorij za pokretanje TUI-a |
| `--session` | `-s` | ID sesije za nastavak | | `--session` | `-s` | ID sesije za nastavak |
--- ---
@ -93,7 +93,7 @@ Kreirajte novog agenta s prilagođenom konfiguracijom.
opencode agent create opencode agent create
``` ```
Ova komanda će vas voditi kroz kreiranje novog agenta sa prilagođenim sistemskim promptom i konfiguracijom alata. Ova naredba će vas voditi kroz kreiranje novog agenta sa prilagođenim sistemskim promptom i konfiguracijom alata.
--- ---
@ -131,7 +131,7 @@ Kada se OpenCode pokrene, učitava dobavljače iz datoteke vjerodajnica. I ako p
#### list #### list
Navodi sve autentifikovane dobavljače pohranjene u datoteci akreditiva. Navodi sve autentifikovane dobavljače pohranjene u datoteci vjerodajnica.
```bash ```bash
opencode auth list opencode auth list
@ -157,7 +157,7 @@ opencode auth logout
### github ### github
Upravljajte GitHub agentom za automatizaciju spremišta. Upravljajte GitHub agentom za automatizaciju repozitorija.
```bash ```bash
opencode github [command] opencode github [command]
@ -167,7 +167,7 @@ opencode github [command]
#### install #### install
Instalirajte GitHub agenta u svoje spremište. Instalirajte GitHub agenta u svoj repozitorij.
```bash ```bash
opencode github install opencode github install
@ -179,24 +179,24 @@ Ovo postavlja neophodni tok rada GitHub Actions i vodi vas kroz proces konfigura
#### run #### run
Pokrenite GitHub agent. Ovo se obično koristi u GitHub akcijama. Pokrenite GitHub agent. Ovo se obično koristi u GitHub Actions.
```bash ```bash
opencode github run opencode github run
``` ```
##### Zastave ##### Opcije
| Zastava | Opis | | Opcija | Opis |
| --------- | -------------------------------------------- | | --------- | -------------------------------------- |
| `--event` | GitHub lažni događaj za pokretanje agenta za | | `--event` | GitHub mock event za pokretanje agenta |
| `--token` | GitHub token ličnog pristupa | | `--token` | GitHub Personal Access Token |
--- ---
### mcp ### mcp
Upravljajte serverima protokola konteksta modela. Upravljajte Model Context Protocol (MCP) serverima.
```bash ```bash
opencode mcp [command] opencode mcp [command]
@ -212,7 +212,7 @@ Dodajte MCP server svojoj konfiguraciji.
opencode mcp add opencode mcp add
``` ```
Ova komanda će vas voditi kroz dodavanje lokalnog ili udaljenog MCP servera. Ova naredba će vas voditi kroz dodavanje lokalnog ili udaljenog MCP servera.
--- ---
@ -241,7 +241,7 @@ opencode mcp auth [name]
``` ```
Ako ne navedete ime servera, od vas će biti zatraženo da izaberete neki od dostupnih servera koji podržavaju OAuth. Ako ne navedete ime servera, od vas će biti zatraženo da izaberete neki od dostupnih servera koji podržavaju OAuth.
Također možete navesti servere koji podržavaju OAuth i njihov status provjere autentičnosti. Također možete navesti servere koji podržavaju OAuth i njihov status autentifikacije.
```bash ```bash
opencode mcp auth list opencode mcp auth list
@ -267,7 +267,7 @@ opencode mcp logout [name]
#### debug #### debug
Otklanjanje grešaka OAuth veze sa MCP serverom. Otklanjanje grešaka (debug) OAuth veze sa MCP serverom.
```bash ```bash
opencode mcp debug <name> opencode mcp debug <name>
@ -291,11 +291,11 @@ Opciono možete proslijediti ID provajdera za filtriranje modela po tom dobavlja
opencode models anthropic opencode models anthropic
``` ```
#### Zastave #### Opcije
| Zastava | Opis | | Opcija | Opis |
| ----------- | ------------------------------------------------------------------------ | | ----------- | ------------------------------------------------------------------------ |
| `--refresh` | Osvježite predmemoriju modela sa models.dev | | `--refresh` | Osvježite keš modela sa models.dev |
| `--verbose` | Koristite detaljniji izlaz modela (uključuje metapodatke poput troškova) | | `--verbose` | Koristite detaljniji izlaz modela (uključuje metapodatke poput troškova) |
Koristite `--refresh` zastavicu da ažurirate keširanu listu modela. Ovo je korisno kada su novi modeli dodani provajderu i želite da ih vidite u OpenCode. Koristite `--refresh` zastavicu da ažurirate keširanu listu modela. Ovo je korisno kada su novi modeli dodani provajderu i želite da ih vidite u OpenCode.
@ -308,13 +308,13 @@ opencode models --refresh
### run ### run
Pokrenite opencode u neinteraktivnom modu tako što ćete direktno proslijediti prompt. Pokrenite OpenCode u neinteraktivnom modu tako što ćete direktno proslijediti prompt.
```bash ```bash
opencode run [message..] opencode run [message..]
``` ```
Ovo je korisno za skriptiranje, automatizaciju ili kada želite brz odgovor bez pokretanja punog TUI-ja. Na primjer. Ovo je korisno za skriptiranje, automatizaciju ili kada želite brz odgovor bez pokretanja punog TUI-ja. Na primjer:
```bash "opencode run" ```bash "opencode run"
opencode run Explain the use of context in Go opencode run Explain the use of context in Go
@ -330,11 +330,11 @@ opencode serve
opencode run --attach http://localhost:4096 "Explain async/await in JavaScript" opencode run --attach http://localhost:4096 "Explain async/await in JavaScript"
``` ```
#### Zastave #### Opcije
| Zastava | Kratko | Opis | | Opcija | Kratko | Opis |
| ------------ | ------ | ------------------------------------------------------------------------ | | ------------ | ------ | ------------------------------------------------------------------------ |
| `--command` | | Naredba za pokretanje, koristite poruku za args | | `--command` | | Naredba za pokretanje, koristite poruku za argumente |
| `--continue` | `-c` | Nastavite posljednju sesiju | | `--continue` | `-c` | Nastavite posljednju sesiju |
| `--session` | `-s` | ID sesije za nastavak | | `--session` | `-s` | ID sesije za nastavak |
| `--fork` | | Forkujte sesiju pri nastavku (koristiti sa `--continue` ili `--session`) | | `--fork` | | Forkujte sesiju pri nastavku (koristiti sa `--continue` ili `--session`) |
@ -344,29 +344,29 @@ opencode run --attach http://localhost:4096 "Explain async/await in JavaScript"
| `--file` | `-f` | Fajlovi koje treba priložiti poruci | | `--file` | `-f` | Fajlovi koje treba priložiti poruci |
| `--format` | | Format: default (formatiran) ili json (sirovi JSON događaji) | | `--format` | | Format: default (formatiran) ili json (sirovi JSON događaji) |
| `--title` | | Naslov sesije (koristi skraćeni prompt ako nije navedena vrijednost) | | `--title` | | Naslov sesije (koristi skraćeni prompt ako nije navedena vrijednost) |
| `--attach` | | Priključite na pokrenuti opencode server (npr. http://localhost:4096) | | `--attach` | | Priključite na pokrenuti OpenCode server (npr. http://localhost:4096) |
| `--port` | | Port za lokalni server (zadano na nasumični port) | | `--port` | | Port za lokalni server (zadano na nasumični port) |
--- ---
### serve ### serve
Pokrenite OpenCode server bez glave za pristup API-ju. Pogledajte [server docs](/docs/server) za kompletan HTTP interfejs. Pokrenite OpenCode headless server za API pristup. Pogledajte [server docs](/docs/server) za kompletan HTTP interfejs.
```bash ```bash
opencode serve opencode serve
``` ```
Ovo pokreće HTTP server koji pruža API pristup funkcionalnosti otvorenog koda bez TUI interfejsa. Postavite `OPENCODE_SERVER_PASSWORD` da omogućite HTTP osnovnu auth (korisničko ime je zadano na `opencode`). Ovo pokreće HTTP server koji pruža API pristup funkcionalnosti OpenCode-a bez TUI interfejsa. Postavite `OPENCODE_SERVER_PASSWORD` da omogućite HTTP osnovnu auth (korisničko ime je zadano na `opencode`).
#### Zastave #### Opcije
| Zastava | Opis | | Opcija | Opis |
| ------------ | ---------------------------------------------------- | | ------------ | ----------------------------------------------------- |
| `--port` | Port za slušanje na | | `--port` | Port na kojem treba slušati |
| `--hostname` | Ime hosta za slušanje | | `--hostname` | Hostname na kojem treba slušati |
| `--mdns` | Omogući mDNS otkrivanje | | `--mdns` | Omogući mDNS otkrivanje |
| `--cors` | Dodatni izvor(a) pretraživača koji dozvoljavaju CORS | | `--cors` | Dodatni origin(i) pretraživača koji dozvoljavaju CORS |
--- ---
@ -388,12 +388,12 @@ Navedite sve OpenCode sesije.
opencode session list opencode session list
``` ```
##### Zastave ##### Opcije
| Zastava | Kratko | Opis | | Opcija | Kratko | Opis |
| ------------- | ------ | ----------------------------------------- | | ------------- | ------ | -------------------------------------- |
| `--max-count` | `-n` | Ograničenje na N najnovijih sesija | | `--max-count` | `-n` | Ograničenje na N najnovijih sesija |
| `--format` | | Izlazni format: tablica ili json (tabela) | | `--format` | | Izlazni format: table ili json (table) |
--- ---
@ -405,14 +405,14 @@ Prikaži statistiku upotrebe tokena i troškova za vaše OpenCode sesije.
opencode stats opencode stats
``` ```
#### Zastave #### Opcije
| Zastava | Opis | | Opcija | Opis |
| ----------- | ----------------------------------------------------------------------------------------------------------- | | ----------- | ---------------------------------------------------------------------------------------------------------- |
| `--days` | Prikaži statistiku za zadnjih N dana (sva vremena) | | `--days` | Prikaži statistiku za zadnjih N dana (sva vremena) |
| `--tools` | Broj alata za prikaz (svi) | | `--tools` | Broj alata za prikaz (svi) |
| `--models` | Prikaži raščlambu korištenja modela (skriveno prema zadanim postavkama). Proslijedite broj za prikaz vrha N | | `--models` | Prikaži raščlambu korištenja modela (skriveno prema zadanim postavkama). Proslijedite broj za prikaz top N |
| `--project` | Filtriraj po projektu (svi projekti, prazan niz: trenutni projekt) | | `--project` | Filtriraj po projektu (svi projekti, prazan niz: trenutni projekt) |
--- ---
@ -447,22 +447,22 @@ opencode import https://opncd.ai/s/abc123
### web ### web
Pokrenite OpenCode server bez glave sa web interfejsom. Pokrenite OpenCode headless server sa web interfejsom.
```bash ```bash
opencode web opencode web
``` ```
Ovo pokreće HTTP server i otvara web pretraživač za pristup OpenCode preko web interfejsa. Postavite `OPENCODE_SERVER_PASSWORD` da omogućite HTTP osnovnu auth (korisničko ime je zadano na `opencode`). Ovo pokreće HTTP server i otvara web pretraživač za pristup OpenCode-u preko web interfejsa. Postavite `OPENCODE_SERVER_PASSWORD` da omogućite HTTP osnovnu auth (korisničko ime je zadano na `opencode`).
#### Zastave #### Opcije
| Zastava | Opis | | Opcija | Opis |
| ------------ | ---------------------------------------------------- | | ------------ | ----------------------------------------------------- |
| `--port` | Port za slušanje na | | `--port` | Port na kojem treba slušati |
| `--hostname` | Ime hosta za slušanje | | `--hostname` | Hostname na kojem treba slušati |
| `--mdns` | Omogući mDNS otkrivanje | | `--mdns` | Omogući mDNS otkrivanje |
| `--cors` | Dodatni izvor(a) pretraživača koji dozvoljavaju CORS | | `--cors` | Dodatni origin(i) pretraživača koji dozvoljavaju CORS |
--- ---
@ -474,15 +474,15 @@ Pokrenite ACP (Agent Client Protocol) server.
opencode acp opencode acp
``` ```
Ova komanda pokreće ACP server koji komunicira preko stdin/stdout koristeći nd-JSON. Ova naredba pokreće ACP server koji komunicira preko stdin/stdout koristeći nd-JSON.
#### Zastave #### Opcije
| Zastava | Opis | | Opcija | Opis |
| ------------ | ------------------- | | ------------ | --------------------------- |
| `--cwd` | Radni imenik | | `--cwd` | Radni direktorij |
| `--port` | Port za slušanje na | | `--port` | Port na kojem treba slušati |
| `--hostname` | Slušajte ime hosta | | `--hostname` | Hostname na kojem slušati |
--- ---
@ -494,12 +494,12 @@ Deinstalirajte OpenCode i uklonite sve povezane datoteke.
opencode uninstall opencode uninstall
``` ```
#### Zastave #### Opcije
| Zastava | Kratko | Opis | | Opcija | Kratko | Opis |
| --------------- | ------ | --------------------------------------------- | | --------------- | ------ | --------------------------------------------- |
| `--keep-config` | `-c` | Čuvajte konfiguracijske datoteke | | `--keep-config` | `-c` | Sačuvajte konfiguracijske datoteke |
| `--keep-data` | `-d` | Čuvajte podatke i snimke sesije | | `--keep-data` | `-d` | Sačuvajte podatke i snimke sesije |
| `--dry-run` | | Pokažite šta bi bilo uklonjeno bez uklanjanja | | `--dry-run` | | Pokažite šta bi bilo uklonjeno bez uklanjanja |
| `--force` | `-f` | Preskoči upite za potvrdu | | `--force` | `-f` | Preskoči upite za potvrdu |
@ -507,7 +507,7 @@ opencode uninstall
### upgrade ### upgrade
Ažurira opencode na najnoviju verziju ili određenu verziju. Ažurira OpenCode na najnoviju verziju ili određenu verziju.
```bash ```bash
opencode upgrade [target] opencode upgrade [target]
@ -525,73 +525,76 @@ Za nadogradnju na određenu verziju.
opencode upgrade v0.1.48 opencode upgrade v0.1.48
``` ```
#### Zastave #### Opcije
| Zastava | Kratko | Opis | | Opcija | Kratko | Opis |
| ---------- | ------ | ------------------------------------------------------- | | ---------- | ------ | ------------------------------------------------------- |
| `--method` | `-m` | Korišteni način instalacije; curl, npm, pnpm, bun, brew | | `--method` | `-m` | Korišteni način instalacije; curl, npm, pnpm, bun, brew |
--- ---
## Globalne zastave ## Globalne opcije
CLI otvorenog koda uzima sljedeće globalne zastavice. OpenCode CLI prihvata sljedeće globalne zastavice.
| Zastava | Kratko | Opis
|-------------- | ----- | ------------------------------------ | | Opcija | Kratko | Opis |
| `--help` | `-h` | Prikaži pomoć | | -------------- | ------ | ----------------------------------------- |
| `--version` | `-v` | Odštampaj broj verzije | | `--help` | `-h` | Prikaži pomoć |
| `--print-logs` | | Ispis zapisnika u stderr | | `--version` | `-v` | Ispiši broj verzije |
| `--log-level` | | Nivo dnevnika (DEBUG, INFO, WARN, ERROR) | | `--print-logs` | | Ispis logova u stderr |
| `--log-level` | | Nivo logovanja (DEBUG, INFO, WARN, ERROR) |
--- ---
## Varijable okruženja ## Varijable okruženja
OpenCode se može konfigurirati pomoću varijabli okruženja. OpenCode se može konfigurirati pomoću varijabli okruženja.
| Varijabilna | Vrsta | Opis
|------------------------------------- | ------- | ------------------------------------------------- | | Varijabla | Tip | Opis |
| `OPENCODE_AUTO_SHARE` | boolean | Automatski dijeli sesije | | ------------------------------------- | ------- | ------------------------------------------------------------------ |
| `OPENCODE_GIT_BASH_PATH` | string | Putanja do Git Bash izvršne datoteke na Windows | | `OPENCODE_AUTO_SHARE` | boolean | Automatski dijeli sesije |
| `OPENCODE_CONFIG` | string | Put do konfiguracionog fajla | | `OPENCODE_GIT_BASH_PATH` | string | Putanja do Git Bash izvršne datoteke na Windows-u |
| `OPENCODE_CONFIG_DIR` | string | Put do konfiguracijskog direktorija | | `OPENCODE_CONFIG` | string | Putanja do konfiguracijskog fajla |
| `OPENCODE_CONFIG_CONTENT` | string | Inline json konfiguracijski sadržaj | | `OPENCODE_CONFIG_DIR` | string | Putanja do konfiguracijskog direktorija |
| `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Onemogući automatske provjere ažuriranja | | `OPENCODE_CONFIG_CONTENT` | string | Inline json konfiguracijski sadržaj |
| `OPENCODE_DISABLE_PRUNE` | boolean | Onemogući orezivanje starih podataka | | `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Onemogući automatske provjere ažuriranja |
| `OPENCODE_DISABLE_TERMINAL_TITLE` | boolean | Onemogući automatsko ažuriranje naslova terminala | | `OPENCODE_DISABLE_PRUNE` | boolean | Onemogući brisanje (pruning) starih podataka |
| `OPENCODE_PERMISSION` | string | Umetnuta json konfiguracija dozvola | | `OPENCODE_DISABLE_TERMINAL_TITLE` | boolean | Onemogući automatsko ažuriranje naslova terminala |
| `OPENCODE_DISABLE_DEFAULT_PLUGINS` | boolean | Onemogući podrazumevane dodatke | | `OPENCODE_PERMISSION` | string | Inline json konfiguracija dozvola |
| `OPENCODE_DISABLE_LSP_DOWNLOAD` | boolean | Onemogući automatsko preuzimanje LSP servera | | `OPENCODE_DISABLE_DEFAULT_PLUGINS` | boolean | Onemogući podrazumijevane dodatke (plugins) |
| `OPENCODE_ENABLE_EXPERIMENTAL_MODELS` | boolean | Omogući eksperimentalne modele | | `OPENCODE_DISABLE_LSP_DOWNLOAD` | boolean | Onemogući automatsko preuzimanje LSP servera |
| `OPENCODE_DISABLE_AUTOCOMPACT` | boolean | Onemogući automatsko sažimanje konteksta | | `OPENCODE_ENABLE_EXPERIMENTAL_MODELS` | boolean | Omogući eksperimentalne modele |
| `OPENCODE_DISABLE_CLAUDE_CODE` | boolean | Onemogući čitanje sa `.claude` (prompt + vještine) | | `OPENCODE_DISABLE_AUTOCOMPACT` | boolean | Onemogući automatsko sažimanje konteksta |
| `OPENCODE_DISABLE_CLAUDE_CODE_PROMPT` | boolean | Onemogući čitanje `~/.claude/CLAUDE.md` | | `OPENCODE_DISABLE_CLAUDE_CODE` | boolean | Onemogući čitanje iz `.claude` (prompt + vještine) |
| `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | Onemogući učitavanje `.claude/skills` | | `OPENCODE_DISABLE_CLAUDE_CODE_PROMPT` | boolean | Onemogući čitanje `~/.claude/CLAUDE.md` |
| `OPENCODE_DISABLE_MODELS_FETCH` | boolean | Onemogući dohvaćanje modela iz udaljenih izvora | | `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | Onemogući učitavanje `.claude/skills` |
| `OPENCODE_FAKE_VCS` | string | Lažni VCS provajder za potrebe testiranja | | `OPENCODE_DISABLE_MODELS_FETCH` | boolean | Onemogući dohvaćanje modela iz udaljenih izvora |
| `OPENCODE_DISABLE_FILETIME_CHECK` | boolean | Onemogući provjeru vremena datoteke radi optimizacije | | `OPENCODE_FAKE_VCS` | string | Lažni VCS provajder za potrebe testiranja |
| `OPENCODE_CLIENT` | string | Identifikator klijenta (zadano na `cli`) | | `OPENCODE_DISABLE_FILETIME_CHECK` | boolean | Onemogući provjeru vremena datoteke radi optimizacije |
| `OPENCODE_ENABLE_EXA` | boolean | Omogući Exa alate za web pretraživanje | | `OPENCODE_CLIENT` | string | Identifikator klijenta (zadano na `cli`) |
| `OPENCODE_SERVER_PASSWORD` | string | Omogući osnovnu autorizaciju za `serve`/`web` | | `OPENCODE_ENABLE_EXA` | boolean | Omogući Exa alate za web pretraživanje |
| `OPENCODE_SERVER_USERNAME` | string | Poništi osnovno korisničko ime autentifikacije (zadano `opencode`) | | `OPENCODE_SERVER_PASSWORD` | string | Omogući osnovnu autentifikaciju za `serve`/`web` |
| `OPENCODE_MODELS_URL` | string | Prilagođeni URL za dohvaćanje konfiguracije modela | | `OPENCODE_SERVER_USERNAME` | string | Poništi osnovno korisničko ime autentifikacije (zadano `opencode`) |
| `OPENCODE_MODELS_URL` | string | Prilagođeni URL za dohvaćanje konfiguracije modela |
--- ---
### Eksperimentalno ### Eksperimentalno
Ove varijable okruženja omogućavaju eksperimentalne karakteristike koje se mogu promijeniti ili ukloniti. Ove varijable okruženja omogućavaju eksperimentalne karakteristike koje se mogu promijeniti ili ukloniti.
| Varijabilna | Vrsta | Opis
|----------------------------------------------- | ------- | --------------------------------------- | | Varijabla | Tip | Opis |
| `OPENCODE_EXPERIMENTAL` | boolean | Omogući sve eksperimentalne funkcije | | ----------------------------------------------- | ------- | ------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Omogući otkrivanje ikona | | `OPENCODE_EXPERIMENTAL` | boolean | Omogući sve eksperimentalne funkcije |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Onemogući kopiranje pri odabiru u TUI | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Omogući otkrivanje ikona |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | broj | Zadano vremensko ograničenje za bash komande u ms | | `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Onemogući kopiranje pri odabiru u TUI |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | broj | Maksimalni izlazni tokeni za LLM odgovore | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Zadano vremensko ograničenje za bash naredbe u ms |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Omogući praćenje datoteka za cijeli dir | | `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Maksimalni izlazni tokeni za LLM odgovore |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Omogući oxfmt formatter | | `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Omogući praćenje datoteka za cijeli direktorij |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Omogući eksperimentalni LSP alat | | `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Omogući oxfmt formatter |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Onemogući praćenje datoteka | | `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Omogući eksperimentalni LSP alat |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Omogući eksperimentalne Exa funkcije | | `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Onemogući praćenje datoteka |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Omogući eksperimentalnu provjeru tipa LSP | | `OPENCODE_EXPERIMENTAL_EXA` | boolean | Omogući eksperimentalne Exa funkcije |
| `OPENCODE_EXPERIMENTAL_MARKDOWN` | boolean | Omogući eksperimentalne Markdown funkcije | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Omogući eksperimentalnu provjeru tipa LSP |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Omogući režim plana | | `OPENCODE_EXPERIMENTAL_MARKDOWN` | boolean | Omogući eksperimentalne Markdown funkcije |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Omogući Plan mod |

View file

@ -13,7 +13,7 @@ Prilagođene komande su dodatak ugrađenim komandama kao što su `/init`, `/undo
--- ---
## Kreirajte komandne fajlove ## Kreiranje datoteka naredbi
Kreirajte markdown fajlove u direktorijumu `commands/` da definišete prilagođene komande. Kreirajte markdown fajlove u direktorijumu `commands/` da definišete prilagođene komande.
Kreiraj `.opencode/commands/test.md`: Kreiraj `.opencode/commands/test.md`:
@ -38,7 +38,7 @@ Koristite komandu tako što ćete upisati `/` nakon čega slijedi naziv komande.
--- ---
## Konfiguriši ## Konfiguracija
Možete dodati prilagođene komande kroz OpenCode konfiguraciju ili kreiranjem markdown datoteka u direktoriju `commands/`. Možete dodati prilagođene komande kroz OpenCode konfiguraciju ili kreiranjem markdown datoteka u direktoriju `commands/`.
@ -99,7 +99,7 @@ Ime markdown datoteke postaje ime naredbe. Na primjer, `test.md` vam omogućava
--- ---
## Prompt config ## Konfiguracija upita
Promptovi za prilagođene komande podržavaju nekoliko posebnih čuvara mjesta i sintakse. Promptovi za prilagođene komande podržavaju nekoliko posebnih čuvara mjesta i sintakse.
@ -190,7 +190,7 @@ Naredbe se pokreću u korijenskom direktoriju vašeg projekta i njihov izlaz pos
--- ---
### Reference fajlova ### Reference datoteka
Uključite datoteke u svoju naredbu koristeći `@` nakon čega slijedi naziv datoteke. Uključite datoteke u svoju naredbu koristeći `@` nakon čega slijedi naziv datoteke.
@ -213,7 +213,7 @@ Pogledajmo detaljno opcije konfiguracije.
--- ---
### Template ### Šablon
Opcija `template` definira prompt koji će biti poslan LLM-u kada se naredba izvrši. Opcija `template` definira prompt koji će biti poslan LLM-u kada se naredba izvrši.
@ -269,7 +269,7 @@ Ovo je **opciona** opcija konfiguracije. Ako nije navedeno, podrazumevano je va
--- ---
### Subtask ### Podzadatak
Koristite `subtask` boolean da prisilite naredbu da pokrene [subagent](/docs/agents/#subagents) pozivanje. Koristite `subtask` boolean da prisilite naredbu da pokrene [subagent](/docs/agents/#subagents) pozivanje.
Ovo je korisno ako želite da naredba ne zagađuje vaš primarni kontekst i da će **primorati** agenta da djeluje kao subagent, Ovo je korisno ako želite da naredba ne zagađuje vaš primarni kontekst i da će **primorati** agenta da djeluje kao subagent,
@ -307,9 +307,9 @@ Ovo je **opciona** opcija konfiguracije.
--- ---
## Ugrađene ## Ugrađene naredbe
opencode uključuje nekoliko ugrađenih naredbi kao što su `/init`, `/undo`, `/redo`, `/share`, `/help`; [saznaj više](/docs/tui#commands). OpenCode uključuje nekoliko ugrađenih naredbi kao što su `/init`, `/undo`, `/redo`, `/share`, `/help`; [saznaj više](/docs/tui#commands).
:::note :::note
Prilagođene komande mogu nadjačati ugrađene komande. Prilagođene komande mogu nadjačati ugrađene komande.
::: :::

View file

@ -1,5 +1,5 @@
--- ---
title: Config title: Konfiguracija
description: Korištenje OpenCode JSON konfiguracije. description: Korištenje OpenCode JSON konfiguracije.
--- ---
@ -25,10 +25,10 @@ OpenCode podržava i **JSON** i **JSONC** (JSON sa komentarima) formate.
## Lokacije ## Lokacije
Možete postaviti svoju konfiguraciju na nekoliko različitih lokacija i one imaju a Možete postaviti svoju konfiguraciju na nekoliko različitih lokacija i one imaju drugačiji redoslijed prioriteta.
drugačiji redosled prioriteta.
:::note :::note
Konfiguracijski fajlovi su **spojeni zajedno**, a ne zamijenjeni. Konfiguracijski fajlovi se **spajaju**, ne zamjenjuju.
::: :::
Konfiguracijski fajlovi se spajaju, ne zamjenjuju. Kombiniraju se postavke sa sljedećih konfiguracijskih lokacija. Kasnije konfiguracije poništavaju prethodne samo za konfliktne ključeve. Nekonfliktne postavke iz svih konfiguracija su sačuvane. Konfiguracijski fajlovi se spajaju, ne zamjenjuju. Kombiniraju se postavke sa sljedećih konfiguracijskih lokacija. Kasnije konfiguracije poništavaju prethodne samo za konfliktne ključeve. Nekonfliktne postavke iz svih konfiguracija su sačuvane.
Na primjer, ako vaša globalna konfiguracija postavlja `theme: "opencode"` i `autoupdate: true`, a vaša projektna konfiguracija postavlja `model: "anthropic/claude-sonnet-4-5"`, konačna konfiguracija će uključivati sve tri postavke. Na primjer, ako vaša globalna konfiguracija postavlja `theme: "opencode"` i `autoupdate: true`, a vaša projektna konfiguracija postavlja `model: "anthropic/claude-sonnet-4-5"`, konačna konfiguracija će uključivati sve tri postavke.
@ -39,12 +39,13 @@ Na primjer, ako vaša globalna konfiguracija postavlja `theme: "opencode"` i `au
Izvori konfiguracije se učitavaju ovim redoslijedom (kasniji izvori poništavaju ranije): Izvori konfiguracije se učitavaju ovim redoslijedom (kasniji izvori poništavaju ranije):
1. **Udaljena konfiguracija** (od `.well-known/opencode`) - organizacione postavke 1. **Udaljena konfiguracija** (od `.well-known/opencode`) - organizacijske postavke
2. **Globalna konfiguracija** (`~/.config/opencode/opencode.json`) - korisničke postavke 2. **Globalna konfiguracija** (`~/.config/opencode/opencode.json`) - korisničke postavke
3. **Prilagođena konfiguracija** (`OPENCODE_CONFIG` env var) - prilagođena zaobilaženja 3. **Prilagođena konfiguracija** (`OPENCODE_CONFIG` env var) - prilagođena preinačenja
4. **Konfiguracija projekta** (`opencode.json` u projektu) - postavke specifične za projekat 4. **Konfiguracija projekta** (`opencode.json` u projektu) - postavke specifične za projekat
5. **`.opencode` direktoriji** - agenti, komande, dodaci 5. **`.opencode` direktoriji** - agenti, komande, dodaci
6. **Inline config** (`OPENCODE_CONFIG_CONTENT` env var) - runtime nadjačava 6. **Inline konfiguracija** (`OPENCODE_CONFIG_CONTENT` env var) - runtime preinačenja
To znači da konfiguracije projekta mogu nadjačati globalne zadane postavke, a globalne konfiguracije mogu nadjačati postavke udaljene organizacije. To znači da konfiguracije projekta mogu nadjačati globalne zadane postavke, a globalne konfiguracije mogu nadjačati postavke udaljene organizacije.
:::note :::note
Direktoriji `.opencode` i `~/.config/opencode` koriste **imena u množini** za poddirektorije: `agents/`, `commands/`, `modes/`, `plugins/`, `skills/`, `tools/` i `themes/`. Pojedinačna imena (npr. `agent/`) su također podržana za kompatibilnost unatrag. Direktoriji `.opencode` i `~/.config/opencode` koriste **imena u množini** za poddirektorije: `agents/`, `commands/`, `modes/`, `plugins/`, `skills/`, `tools/` i `themes/`. Pojedinačna imena (npr. `agent/`) su također podržana za kompatibilnost unatrag.
@ -52,9 +53,9 @@ Izvori konfiguracije se učitavaju ovim redoslijedom (kasniji izvori poništavaj
--- ---
### Daljinski ### Udaljeno (Remote)
Organizacije mogu pružiti zadanu konfiguraciju preko `.well-known/opencode` krajnje točke. Ovo se automatski preuzima kada se autentifikujete kod provajdera koji to podržava. Organizacije mogu pružiti zadanu konfiguraciju preko `.well-known/opencode` krajnje tačke. Ovo se automatski preuzima kada se autentifikujete kod provajdera koji to podržava.
Prvo se učitava udaljena konfiguracija koja služi kao osnovni sloj. Svi ostali izvori konfiguracije (globalni, projektni) mogu nadjačati ove zadane postavke. Prvo se učitava udaljena konfiguracija koja služi kao osnovni sloj. Svi ostali izvori konfiguracije (globalni, projektni) mogu nadjačati ove zadane postavke.
Na primjer, ako vaša organizacija nudi MCP servere koji su po defaultu onemogućeni: Na primjer, ako vaša organizacija nudi MCP servere koji su po defaultu onemogućeni:
@ -86,14 +87,14 @@ Možete omogućiti određene servere u vašoj lokalnoj konfiguraciji:
--- ---
### Global ### Globalno
Postavite svoju globalnu OpenCode konfiguraciju u `~/.config/opencode/opencode.json`. Koristite globalnu konfiguraciju za korisničke preferencije kao što su teme, provajderi ili veze tipki. Postavite svoju globalnu OpenCode konfiguraciju u `~/.config/opencode/opencode.json`. Koristite globalnu konfiguraciju za korisničke preferencije kao što su teme, provajderi ili veze tipki.
Globalna konfiguracija poništava zadane postavke udaljene organizacije. Globalna konfiguracija poništava zadane postavke udaljene organizacije.
--- ---
### Project ### Projekt
Dodajte `opencode.json` u korijen projekta. Konfiguracija projekta ima najveći prioritet među standardnim konfiguracijskim datotekama - ona nadjačava globalne i udaljene konfiguracije. Dodajte `opencode.json` u korijen projekta. Konfiguracija projekta ima najveći prioritet među standardnim konfiguracijskim datotekama - ona nadjačava globalne i udaljene konfiguracije.
:::tip :::tip
@ -104,7 +105,7 @@ Ovo je također sigurno provjeriti u Git i koristi istu shemu kao globalna.
--- ---
### Custom config ### Prilagođena konfiguracija
Navedite prilagođenu putanju konfiguracijske datoteke koristeći varijablu okruženja `OPENCODE_CONFIG`. Navedite prilagođenu putanju konfiguracijske datoteke koristeći varijablu okruženja `OPENCODE_CONFIG`.
@ -117,12 +118,9 @@ Prilagođena konfiguracija se učitava između globalne i projektne konfiguracij
--- ---
### Custom directory ### Prilagođeni direktorij
Navedite prilagođeni konfiguracijski direktorij koristeći `OPENCODE_CONFIG_DIR` Navedite prilagođeni konfiguracijski direktorij koristeći `OPENCODE_CONFIG_DIR` varijablu okruženja. U ovom direktoriju će se tražiti agenti, komande, modovi i dodaci baš kao standardni `.opencode` direktorij, i trebali bi pratiti istu strukturu.
varijabla okruženja. U ovom direktoriju će se tražiti agenti, komande,
modove i dodatke baš kao standardni `.opencode` direktorij, i trebali bi
prate istu strukturu.
```bash ```bash
export OPENCODE_CONFIG_DIR=/path/to/my/config-directory export OPENCODE_CONFIG_DIR=/path/to/my/config-directory
@ -133,10 +131,10 @@ Prilagođeni direktorij se učitava nakon direktorija globalne konfiguracije i `
--- ---
## Shema ## Šema
Konfiguracijski fajl ima šemu koja je definirana u [**`opencode.ai/config.json`**](https://opencode.ai/config.json). Konfiguracijski fajl ima šemu koja je definirana u [**`opencode.ai/config.json`**](https://opencode.ai/config.json).
Vaš uređivač bi trebao biti u mogućnosti da potvrdi i autodovršava na osnovu šeme. Vaš editor bi trebao biti u mogućnosti da validira i autodovršava na osnovu šeme.
--- ---
@ -162,13 +160,13 @@ Dostupne opcije:
- `scroll_acceleration.enabled` - Omogući ubrzanje skrolovanja u macOS stilu. **Ima prednost nad `scroll_speed`.** - `scroll_acceleration.enabled` - Omogući ubrzanje skrolovanja u macOS stilu. **Ima prednost nad `scroll_speed`.**
- `scroll_speed` - Prilagođeni množitelj brzine pomicanja (podrazumevano: `3`, minimalno: `1`). Zanemareno ako je `scroll_acceleration.enabled` `true`. - `scroll_speed` - Prilagođeni množitelj brzine pomicanja (podrazumevano: `3`, minimalno: `1`). Zanemareno ako je `scroll_acceleration.enabled` `true`.
- `diff_style` - Kontrola prikaza razlike. `"auto"` se prilagođava širini terminala, `"stacked"` uvijek prikazuje jednu kolonu. - `diff_style` - Kontrola prikaza razlike. `"auto"` se prilagođava širini terminala, `"stacked"` uvijek prikazuje jednu kolonu.
[Ovdje saznajte više o korištenju TUI](/docs/tui). [Saznajte više o korištenju TUI](/docs/tui) ovdje.
--- ---
### Server ### Server
Možete konfigurirati postavke servera za komande `opencode serve` i `opencode web` putem opcije `server`. Možete konfigurirati postavke servera za naredbe `opencode serve` i `opencode web` putem opcije `server`.
```json title="opencode.json" ```json title="opencode.json"
{ {
@ -186,15 +184,15 @@ Možete konfigurirati postavke servera za komande `opencode serve` i `opencode w
Dostupne opcije: Dostupne opcije:
- `port` - Port za slušanje. - `port` - Port za slušanje.
- `hostname` - Ime hosta za slušanje. Kada je `mdns` omogućen i nije postavljeno ime hosta, podrazumevano je `0.0.0.0`. - `hostname` - Hostname za slušanje. Kada je `mdns` omogućen i nije postavljeno ime hosta, podrazumevano je `0.0.0.0`.
- `mdns` - Omogući otkrivanje mDNS usluge. Ovo omogućava drugim uređajima na mreži da otkriju vaš OpenCode server. - `mdns` - Omogući mDNS otkrivanje servisa. Ovo omogućava drugim uređajima na mreži da otkriju vaš OpenCode server.
- `mdnsDomain` - Prilagođeno ime domene za mDNS uslugu. Zadano je `opencode.local`. Korisno za pokretanje više instanci na istoj mreži. - `mdnsDomain` - Prilagođeno ime domene za mDNS servis. Zadano je `opencode.local`. Korisno za pokretanje više instanci na istoj mreži.
- `cors` - Dodatni izvori koji omogućavaju CORS kada koristite HTTP server iz klijenta baziranog na pretraživaču. Vrijednosti moraju biti punog porijekla (šema + host + opcijski port), npr. `https://app.example.com`. - `cors` - Dodatni origini koji omogućavaju CORS kada koristite HTTP server iz klijenta baziranog na pretraživaču. Vrijednosti moraju biti puni origin (shema + host + opcijski port), npr. `https://app.example.com`.
[Saznajte više o serveru](/docs/server) ovdje. [Saznajte više o serveru](/docs/server) ovdje.
--- ---
### Tools ### Alati
Možete upravljati alatima koje LLM može koristiti putem opcije `tools`. Možete upravljati alatima koje LLM može koristiti putem opcije `tools`.
@ -244,7 +242,7 @@ Opcije provajdera mogu uključivati `timeout` i `setCacheKey`:
- `timeout` - Vrijeme čekanja zahtjeva u milisekundama (podrazumevano: 300000). Postavite na `false` da onemogućite. - `timeout` - Vrijeme čekanja zahtjeva u milisekundama (podrazumevano: 300000). Postavite na `false` da onemogućite.
- `setCacheKey` - Osigurajte da je ključ keš memorije uvijek postavljen za određenog provajdera. - `setCacheKey` - Osigurajte da je ključ keš memorije uvijek postavljen za određenog provajdera.
Također možete konfigurirati [lokalni modeli](/docs/models#local). [Saznajte više](/docs/models). Također možete konfigurirati [lokalne modele](/docs/models#local). [Saznajte više](/docs/models).
--- ---
@ -272,8 +270,8 @@ Amazon Bedrock podržava konfiguraciju specifičnu za AWS:
``` ```
- `region` - AWS regija za Bedrock (zadano na `AWS_REGION` env var ili `us-east-1`) - `region` - AWS regija za Bedrock (zadano na `AWS_REGION` env var ili `us-east-1`)
- `profile` - AWS imenovan profil od `~/.aws/credentials` (podrazumevano na `AWS_PROFILE` env var) - `profile` - AWS imenovani profil iz `~/.aws/credentials` (zadano na `AWS_PROFILE` env var)
- `endpoint` - URL prilagođene krajnje tačke za VPC krajnje tačke. Ovo je pseudonim za generičku opciju `baseURL` koristeći terminologiju specifičnu za AWS. Ako su oba navedena, `endpoint` ima prednost. - `endpoint` - URL prilagođene krajnje tačke za VPC krajnje tačke. Ovo je alias za generičku opciju `baseURL` koristeći terminologiju specifičnu za AWS. Ako su oba navedena, `endpoint` ima prednost.
:::note :::note
Tokeni nosioca (`AWS_BEARER_TOKEN_BEDROCK` ili `/connect`) imaju prednost nad autentifikacijom zasnovanom na profilu. Pogledajte [prednost autentifikacije](/docs/providers#authentication-precedence) za detalje. Tokeni nosioca (`AWS_BEARER_TOKEN_BEDROCK` ili `/connect`) imaju prednost nad autentifikacijom zasnovanom na profilu. Pogledajte [prednost autentifikacije](/docs/providers#authentication-precedence) za detalje.
::: :::
@ -281,7 +279,7 @@ Amazon Bedrock podržava konfiguraciju specifičnu za AWS:
--- ---
### Theme ### Tema
Možete konfigurirati temu koju želite koristiti u svojoj OpenCode konfiguraciji putem opcije `theme`. Možete konfigurirati temu koju želite koristiti u svojoj OpenCode konfiguraciji putem opcije `theme`.
@ -333,7 +331,7 @@ Možete postaviti zadanog agenta koristeći opciju `default_agent`. Ovo određuj
} }
``` ```
Zadani agent mora biti primarni agent (ne podagent). Ovo može biti ugrađeni agent kao što je `"build"` ili `"plan"`, ili [prilagođeni agent](/docs/agents) koji ste definirali. Ako navedeni agent ne postoji ili je subagent, OpenCode će se vratiti na `"build"` s upozorenjem. Zadani agent mora biti primarni agent (ne podagent). Ovo može biti ugrađeni agent kao što je `"build"` ili `"plan"`, ili [prilagođeni agent](/docs/agents) koji ste definirali. Ako navedeni agent ne postoji ili je podagent, OpenCode će se vratiti na `"build"` s upozorenjem.
Ova postavka se primjenjuje na sva sučelja: TUI, CLI (`opencode run`), desktop aplikaciju i GitHub Action. Ova postavka se primjenjuje na sva sučelja: TUI, CLI (`opencode run`), desktop aplikaciju i GitHub Action.
--- ---
@ -349,18 +347,18 @@ Možete konfigurirati funkciju [share](/docs/share) putem opcije `share`.
} }
``` ```
Ovo traje: Ovo prihvata:
- `"manual"` - Dozvoli ručno dijeljenje putem komandi (podrazumevano) - `"manual"` - Dozvoli ručno dijeljenje putem naredbi (podrazumevano)
- `"auto"` - Automatski dijelite nove razgovore - `"auto"` - Automatski dijelite nove razgovore
- `"disabled"` - Onemogući dijeljenje u potpunosti - `"disabled"` - Onemogući dijeljenje u potpunosti
Podrazumevano, dijeljenje je postavljeno na ručni način rada gdje trebate eksplicitno dijeliti razgovore pomoću naredbe `/share`. Podrazumevano, dijeljenje je postavljeno na ručni način rada gdje trebate eksplicitno dijeliti razgovore pomoću naredbe `/share`.
--- ---
### Command ### Naredbe
Možete konfigurirati prilagođene komande za ponavljanje zadataka putem opcije `command`. Možete konfigurirati prilagođene naredbe za ponavljanje zadataka putem opcije `command`.
```jsonc title="opencode.jsonc" ```jsonc title="opencode.jsonc"
{ {
@ -380,13 +378,13 @@ Možete konfigurirati prilagođene komande za ponavljanje zadataka putem opcije
} }
``` ```
Također možete definirati komande koristeći markdown fajlove u `~/.config/opencode/commands/` ili `.opencode/commands/`. [Saznajte više ovdje](/docs/commands). Također možete definirati naredbe koristeći markdown fajlove u `~/.config/opencode/commands/` ili `.opencode/commands/`. [Saznajte više ovdje](/docs/commands).
--- ---
### Keybinds ### Prečice tipki
Možete prilagoditi svoje veze ključeva putem opcije `keybinds`. Možete prilagoditi svoje veze tipki putem opcije `keybinds`.
```json title="opencode.json" ```json title="opencode.json"
{ {
@ -415,7 +413,7 @@ Imajte na umu da ovo funkcionira samo ako nije instalirano pomoću upravitelja p
--- ---
### Formatters ### Formateri
Možete konfigurirati formatere koda putem opcije `formatter`. Možete konfigurirati formatere koda putem opcije `formatter`.
@ -437,13 +435,13 @@ Možete konfigurirati formatere koda putem opcije `formatter`.
} }
``` ```
[Saznajte više o formatterima](/docs/formatters) ovdje. [Saznajte više o formaterima](/docs/formatters) ovdje.
--- ---
### Dozvole ### Dozvole
Prema zadanim postavkama, opencode **dopušta sve operacije** bez potrebe za eksplicitnim dopuštenjem. Ovo možete promijeniti koristeći opciju `permission`. Prema zadanim postavkama, OpenCode **dopušta sve operacije** bez potrebe za eksplicitnim dopuštenjem. Ovo možete promijeniti koristeći opciju `permission`.
Na primjer, da osigurate da alati `edit` i `bash` zahtijevaju odobrenje korisnika: Na primjer, da osigurate da alati `edit` i `bash` zahtijevaju odobrenje korisnika:
```json title="opencode.json" ```json title="opencode.json"
@ -456,11 +454,11 @@ Na primjer, da osigurate da alati `edit` i `bash` zahtijevaju odobrenje korisnik
} }
``` ```
[Ovdje saznajte više o ](/docs/permissions) dozvolama. [Saznajte više o dozvolama](/docs/permissions) ovdje.
--- ---
### Compaction ### Sažimanje
Možete kontrolirati ponašanje sažimanja konteksta putem opcije `compaction`. Možete kontrolirati ponašanje sažimanja konteksta putem opcije `compaction`.
@ -479,7 +477,7 @@ Možete kontrolirati ponašanje sažimanja konteksta putem opcije `compaction`.
--- ---
### Watcher ### Promatrač (Watcher)
Možete konfigurirati obrasce ignoriranja promatrača datoteka putem opcije `watcher`. Možete konfigurirati obrasce ignoriranja promatrača datoteka putem opcije `watcher`.
@ -511,7 +509,7 @@ Možete konfigurirati MCP servere koje želite koristiti putem opcije `mcp`.
--- ---
### Extras ### Dodaci
[Plugins](/docs/plugins) proširuju OpenCode sa prilagođenim alatima, kukicama i integracijama. [Plugins](/docs/plugins) proširuju OpenCode sa prilagođenim alatima, kukicama i integracijama.
Postavite datoteke dodataka u `.opencode/plugins/` ili `~/.config/opencode/plugins/`. Također možete učitati dodatke iz npm-a preko opcije `plugin`. Postavite datoteke dodataka u `.opencode/plugins/` ili `~/.config/opencode/plugins/`. Također možete učitati dodatke iz npm-a preko opcije `plugin`.
@ -538,8 +536,7 @@ Možete konfigurirati upute za model koji koristite putem opcije `instructions`.
} }
``` ```
Ovo uzima niz putanja i uzoraka globusa do datoteka instrukcija. [Saznajte više Ovo uzima niz putanja i glob uzoraka do datoteka instrukcija. [Saznajte više o pravilima ovdje](/docs/rules).
o pravilima ovdje](/docs/rules).
--- ---
@ -560,7 +557,7 @@ Možete onemogućiti dobavljače koji se automatski učitavaju preko opcije `dis
Opcija `disabled_providers` prihvata niz ID-ova provajdera. Kada je provajder onemogućen: Opcija `disabled_providers` prihvata niz ID-ova provajdera. Kada je provajder onemogućen:
- Neće se učitati čak i ako su varijable okruženja postavljene. - Neće se učitati čak i ako su varijable okruženja postavljene.
- Neće se učitati čak i ako su API ključevi konfigurirani putem `/connect` komande. - Neće se učitati čak i ako su API ključevi konfigurirani putem `/connect` naredbe.
- Modeli dobavljača se neće pojaviti na listi za odabir modela. - Modeli dobavljača se neće pojaviti na listi za odabir modela.
--- ---
@ -576,7 +573,7 @@ Možete odrediti listu dozvoljenih dobavljača putem opcije `enabled_providers`.
} }
``` ```
Ovo je korisno kada želite da ograničite OpenCode da koristi samo određene provajdere umesto da ih onemogućavate jednog po jednog. Ovo je korisno kada želite da ograničite OpenCode da koristi samo određene provajdere umjesto da ih onemogućavate jednog po jednog.
:::note :::note
`disabled_providers` ima prioritet nad `enabled_providers`. `disabled_providers` ima prioritet nad `enabled_providers`.
::: :::
@ -607,7 +604,7 @@ Možete koristiti zamjenu varijabli u vašim konfiguracijskim datotekama da bist
--- ---
### Env vars ### Varijable okruženja
Koristite `{env:VARIABLE_NAME}` za zamjenu varijabli okruženja: Koristite `{env:VARIABLE_NAME}` za zamjenu varijabli okruženja:
@ -630,7 +627,7 @@ Ako varijabla okruženja nije postavljena, bit će zamijenjena praznim nizom.
--- ---
### Fajlovi ### Datoteke
Koristite `{file:path/to/file}` da zamijenite sadržaj fajla: Koristite `{file:path/to/file}` da zamijenite sadržaj fajla:

View file

@ -1,5 +1,5 @@
--- ---
title: Custom Tools title: Prilagođeni alati
description: Kreirajte alate koje LLM može pozvati u otvorenom kodu. description: Kreirajte alate koje LLM može pozvati u otvorenom kodu.
--- ---
@ -133,7 +133,7 @@ Koristite `context.worktree` za korijen git radnog stabla.
## Primjeri ## Primjeri
### Napišite alat u Python-u ### Pisanje alata u Python-u
Možete pisati svoje alate na bilo kom jeziku koji želite. Evo primjera koji zbraja dva broja koristeći Python. Možete pisati svoje alate na bilo kom jeziku koji želite. Evo primjera koji zbraja dva broja koristeći Python.
Prvo kreirajte alat kao Python skriptu: Prvo kreirajte alat kao Python skriptu:

View file

@ -1,5 +1,5 @@
--- ---
title: Enterprise title: Za preduzeća
description: Sigurno korištenje OpenCode u vašoj organizaciji. description: Sigurno korištenje OpenCode u vašoj organizaciji.
--- ---
@ -19,7 +19,7 @@ Da započnete s OpenCode Enterprise:
--- ---
## Trial ## Proba
OpenCode je otvorenog koda i ne pohranjuje vaš kod niti kontekstualne podatke, tako da vaši developeri mogu jednostavno [započeti](/docs/) i provesti probu. OpenCode je otvorenog koda i ne pohranjuje vaš kod niti kontekstualne podatke, tako da vaši developeri mogu jednostavno [započeti](/docs/) i provesti probu.
@ -60,19 +60,19 @@ Preporučujemo da ovo onemogućite tokom probe.
--- ---
## Pricing ## Cijene
Koristimo model naplate po sjedištu za OpenCode Enterprise. Ako imate vlastiti LLM gateway, ne naplaćujemo korištene tokene. Za više detalja o cijenama i opcijama implementacije, **<a href={email}>kontaktirajte nas</a>**. Koristimo model naplate po sjedištu za OpenCode Enterprise. Ako imate vlastiti LLM gateway, ne naplaćujemo korištene tokene. Za više detalja o cijenama i opcijama implementacije, **<a href={email}>kontaktirajte nas</a>**.
--- ---
## Deployment ## Postavljanje
Nakon što završite probni period i spremni ste koristiti OpenCode u svojoj organizaciji, možete **<a href={email}>kontaktirati nas</a>** da razgovaramo o cijenama i opcijama implementacije. Nakon što završite probni period i spremni ste koristiti OpenCode u svojoj organizaciji, možete **<a href={email}>kontaktirati nas</a>** da razgovaramo o cijenama i opcijama implementacije.
--- ---
### Central Config ### Centralna konfiguracija
Možemo postaviti OpenCode da koristi jednu centralnu konfiguraciju za cijelu organizaciju. Možemo postaviti OpenCode da koristi jednu centralnu konfiguraciju za cijelu organizaciju.
@ -96,7 +96,7 @@ Također možete onemogućiti sve druge AI provajdere, čime osiguravate da svi
--- ---
### Self-hosting ### Samostalno hostovanje
Iako preporučujemo onemogućavanje share stranica kako biste osigurali da podaci nikada ne napuštaju vašu organizaciju, možemo vam pomoći i da ih samostalno hostujete na vlastitoj infrastrukturi. Iako preporučujemo onemogućavanje share stranica kako biste osigurali da podaci nikada ne napuštaju vašu organizaciju, možemo vam pomoći i da ih samostalno hostujete na vlastitoj infrastrukturi.
@ -104,17 +104,17 @@ Ovo je trenutno na našoj mapi puta. Ako ste zainteresovani, **<a href={email}>j
--- ---
## FAQ ## Često postavljana pitanja
<details> <details>
<summary>What is OpenCode Enterprise?</summary> <summary>Šta je OpenCode Enterprise?</summary>
OpenCode Enterprise je za organizacije koje žele osigurati da njihov kod i podaci nikada ne napuštaju njihovu infrastrukturu. To omogućava centralizovana konfiguracija koja se integriše s vašim SSO-om i internim AI gateway-om. OpenCode Enterprise je za organizacije koje žele osigurati da njihov kod i podaci nikada ne napuštaju njihovu infrastrukturu. To omogućava centralizovana konfiguracija koja se integriše s vašim SSO-om i internim AI gateway-om.
</details> </details>
<details> <details>
<summary>How do I get started with OpenCode Enterprise?</summary> <summary>Kako započeti s OpenCode Enterprise?</summary>
Jednostavno započnite internu probu sa svojim timom. OpenCode po defaultu ne pohranjuje vaš kod ni kontekstualne podatke, što olakšava početak. Jednostavno započnite internu probu sa svojim timom. OpenCode po defaultu ne pohranjuje vaš kod ni kontekstualne podatke, što olakšava početak.
@ -123,21 +123,21 @@ Zatim **<a href={email}>kontaktirajte nas</a>** da razgovaramo o cijenama i opci
</details> </details>
<details> <details>
<summary>How does enterprise pricing work?</summary> <summary>Kako funkcionišu enterprise cijene?</summary>
Nudimo enterprise cijene po sjedištu. Ako imate vlastiti LLM gateway, ne naplaćujemo korištene tokene. Za više detalja, **<a href={email}>kontaktirajte nas</a>** za prilagođenu ponudu prema potrebama vaše organizacije. Nudimo enterprise cijene po sjedištu. Ako imate vlastiti LLM gateway, ne naplaćujemo korištene tokene. Za više detalja, **<a href={email}>kontaktirajte nas</a>** za prilagođenu ponudu prema potrebama vaše organizacije.
</details> </details>
<details> <details>
<summary>Is my data secure with OpenCode Enterprise?</summary> <summary>Jesu li moji podaci sigurni uz OpenCode Enterprise?</summary>
Da. OpenCode ne pohranjuje vaš kod niti kontekstualne podatke. Sva obrada se odvija lokalno ili putem direktnih API poziva vašem AI provajderu. Uz centralnu konfiguraciju i SSO integraciju, vaši podaci ostaju sigurni unutar infrastrukture vaše organizacije. Da. OpenCode ne pohranjuje vaš kod niti kontekstualne podatke. Sva obrada se odvija lokalno ili putem direktnih API poziva vašem AI provajderu. Uz centralnu konfiguraciju i SSO integraciju, vaši podaci ostaju sigurni unutar infrastrukture vaše organizacije.
</details> </details>
<details> <details>
<summary>Can we use our own private NPM registry?</summary> <summary>Možemo li koristiti vlastiti privatni NPM registar?</summary>
OpenCode podržava privatne npm registre kroz Bunovu izvornu podršku za `.npmrc` datoteku. Ako vaša organizacija koristi privatni registar, kao što je JFrog Artifactory, Nexus ili slično, osigurajte da su developeri autentifikovani prije pokretanja OpenCode. OpenCode podržava privatne npm registre kroz Bunovu izvornu podršku za `.npmrc` datoteku. Ako vaša organizacija koristi privatni registar, kao što je JFrog Artifactory, Nexus ili slično, osigurajte da su developeri autentifikovani prije pokretanja OpenCode.

View file

@ -1,5 +1,5 @@
--- ---
title: Formatters title: Formateri
description: OpenCode koristi formatere specifične za jezik. description: OpenCode koristi formatere specifične za jezik.
--- ---
@ -7,7 +7,7 @@ OpenCode automatski formatira datoteke nakon što su napisane ili uređene pomo
--- ---
## Ugrađeno ## Ugrađeni
OpenCode dolazi sa nekoliko ugrađenih formatera za popularne jezike i okvire. Ispod je lista formatera, podržanih ekstenzija datoteka i naredbi ili opcija konfiguracije koje su mu potrebne. OpenCode dolazi sa nekoliko ugrađenih formatera za popularne jezike i okvire. Ispod je lista formatera, podržanih ekstenzija datoteka i naredbi ili opcija konfiguracije koje su mu potrebne.
| Formatter | Ekstenzije | Zahtjevi | Formatter | Ekstenzije | Zahtjevi
@ -49,7 +49,7 @@ Kada OpenCode piše ili uređuje datoteku, on:
--- ---
## Konfiguriši ## Konfiguracija
Možete prilagoditi formatere kroz `formatter` odjeljak u vašoj OpenCode konfiguraciji. Možete prilagoditi formatere kroz `formatter` odjeljak u vašoj OpenCode konfiguraciji.

View file

@ -7,7 +7,7 @@ OpenCode se integriše sa vašim GitHub tokovom rada. Spomenite `/opencode` ili
--- ---
## Karakteristike ## Funkcije
- **Problemi trijaže**: Zamolite OpenCode da ispita problem i objasni vam ga. - **Problemi trijaže**: Zamolite OpenCode da ispita problem i objasni vam ga.
- **Popravi i implementiraj**: Zamolite OpenCode da popravi problem ili implementira funkciju. I radit će u novoj poslovnici i dostavljati PR sa svim promjenama. - **Popravi i implementiraj**: Zamolite OpenCode da popravi problem ili implementira funkciju. I radit će u novoj poslovnici i dostavljati PR sa svim promjenama.
@ -244,7 +244,7 @@ Za `issues` događaje, `prompt` unos je **potreban** jer nema komentara za izvla
--- ---
## Prilagođene upite ## Prilagođeni upiti
Zaobiđite zadani prompt da biste prilagodili ponašanje OpenCode za vaš tok posla. Zaobiđite zadani prompt da biste prilagodili ponašanje OpenCode za vaš tok posla.

View file

@ -15,7 +15,7 @@ Ovdje koristimo CI/CD komponentu kreiranu u zajednici za OpenCode — [nagyv/git
--- ---
### Karakteristike ### Funkcije
- **Koristite prilagođenu konfiguraciju po poslu**: Konfigurirajte OpenCode s prilagođenim konfiguracijskim direktorijem, na primjer `./config/#custom-directory` da omogućite ili onemogućite funkcionalnost po OpenCode pozivanju. - **Koristite prilagođenu konfiguraciju po poslu**: Konfigurirajte OpenCode s prilagođenim konfiguracijskim direktorijem, na primjer `./config/#custom-directory` da omogućite ili onemogućite funkcionalnost po OpenCode pozivanju.
- **Minimalno podešavanje**: CI komponenta postavlja OpenCode u pozadini, samo trebate kreirati OpenCode konfiguraciju i početnu prompt. - **Minimalno podešavanje**: CI komponenta postavlja OpenCode u pozadini, samo trebate kreirati OpenCode konfiguraciju i početnu prompt.
@ -49,7 +49,7 @@ Spomenite `@opencode` u komentaru i OpenCode će izvršiti zadatke unutar vašeg
--- ---
### Karakteristike ### Funkcije
- **Problemi trijaže**: Zamolite OpenCode da ispita problem i objasni vam ga. - **Problemi trijaže**: Zamolite OpenCode da ispita problem i objasni vam ga.
- **Popravi i implementiraj**: Zamolite OpenCode da popravi problem ili implementira funkciju. - **Popravi i implementiraj**: Zamolite OpenCode da popravi problem ili implementira funkciju.
@ -74,7 +74,7 @@ Pogledajte [**GitLab dokumente**](https://docs.gitlab.com/user/duo_agent_platfor
<details> <details>
<summary>Flow configuration</summary> <summary>Konfiguracija toka</summary>
```yaml ```yaml
image: node:22-slim image: node:22-slim

View file

@ -7,7 +7,7 @@ OpenCode se integriše sa VS kodom, Cursor-om ili bilo kojim IDE-om koji podrža
--- ---
## Upotreba ## Korištenje
- **Brzo pokretanje**: Koristite `Cmd+Esc` (Mac) ili `Ctrl+Esc` (Windows/Linux) da otvorite OpenCode u prikazu podijeljenog terminala ili fokusirajte postojeću terminalsku sesiju ako je već pokrenuta. - **Brzo pokretanje**: Koristite `Cmd+Esc` (Mac) ili `Ctrl+Esc` (Windows/Linux) da otvorite OpenCode u prikazu podijeljenog terminala ili fokusirajte postojeću terminalsku sesiju ako je već pokrenuta.
- **Nova sesija**: Koristite `Cmd+Shift+Esc` (Mac) ili `Ctrl+Shift+Esc` (Windows/Linux) da započnete novu OpenCode terminalsku sesiju, čak i ako je ona već otvorena. Takođe možete kliknuti na dugme OpenCode u korisničkom sučelju. - **Nova sesija**: Koristite `Cmd+Shift+Esc` (Mac) ili `Ctrl+Shift+Esc` (Windows/Linux) da započnete novu OpenCode terminalsku sesiju, čak i ako je ona već otvorena. Takođe možete kliknuti na dugme OpenCode u korisničkom sučelju.

View file

@ -1,13 +1,13 @@
--- ---
title: Intro title: Uvod
description: Započnite s OpenCode. description: Započnite sa OpenCode.
--- ---
import { Tabs, TabItem } from "@astrojs/starlight/components" import { Tabs, TabItem } from "@astrojs/starlight/components"
import config from "../../../../config.mjs" import config from "../../../../config.mjs"
export const console = config.console export const console = config.console
[**OpenCode**](/) je agent za AI kodiranje otvorenog koda. Dostupan je kao interfejs baziran na terminalu, desktop aplikacija ili IDE ekstenzija. [**OpenCode**](/) je AI agent za kodiranje otvorenog koda. Dostupan je kao interfejs baziran na terminalu, desktop aplikacija ili IDE ekstenzija.
![OpenCode TUI sa opencode temom](../../../assets/lander/screenshot.png) ![OpenCode TUI sa opencode temom](../../../assets/lander/screenshot.png)
Hajde da počnemo. Hajde da počnemo.
@ -18,17 +18,17 @@ Hajde da počnemo.
Da biste koristili OpenCode u svom terminalu, trebat će vam: Da biste koristili OpenCode u svom terminalu, trebat će vam:
1. Moderan emulator terminala kao što su: 1. Moderan emulator terminala kao što su:
- [WezTerm](https://wezterm.org), multi-platforma - [WezTerm](https://wezterm.org), više platformi
- [Alacritty](https://alacritty.org), više platforma - [Alacritty](https://alacritty.org), više platformi
- [Ghostty](https://ghostty.org), Linux i macOS - [Ghostty](https://ghostty.org), Linux i macOS
- [Kitty](https://sw.kovidgoyal.net/kitty/), Linux i macOS - [Kitty](https://sw.kovidgoyal.net/kitty/), Linux i macOS
2. API ključevi za LLM provajdere koje želite koristiti. 2. API ključevi za LLM provajdere koje želite koristiti.
--- ---
## Instaliraj ## Instalacija
Najlakši način za instaliranje OpenCode je putem instalacione skripte. Najlakši način za instaliranje OpenCode je putem instalacijske skripte.
```bash ```bash
curl -fsSL https://opencode.ai/install | bash curl -fsSL https://opencode.ai/install | bash
@ -36,7 +36,7 @@ curl -fsSL https://opencode.ai/install | bash
Također ga možete instalirati pomoću sljedećih naredbi: Također ga možete instalirati pomoću sljedećih naredbi:
- **Korišćenje Node.js** - **Korištenje Node.js**
<Tabs> <Tabs>
@ -70,7 +70,7 @@ Također ga možete instalirati pomoću sljedećih naredbi:
</Tabs> </Tabs>
- **Korišćenje Homebrew-a na macOS-u i Linux-u** - **Korištenje Homebrew-a na macOS-u i Linux-u**
```bash ```bash
brew install anomalyco/tap/opencode brew install anomalyco/tap/opencode
@ -78,7 +78,7 @@ Također ga možete instalirati pomoću sljedećih naredbi:
> Preporučujemo korištenje OpenCode tap za najnovija izdanja. Službenu formulu `brew install opencode` održava Homebrew tim i ažurira se rjeđe. > Preporučujemo korištenje OpenCode tap za najnovija izdanja. Službenu formulu `brew install opencode` održava Homebrew tim i ažurira se rjeđe.
- **Korišćenje Parua na Arch Linuxu** - **Korištenje Parua na Arch Linuxu**
```bash ```bash
paru -S opencode-bin paru -S opencode-bin
@ -87,16 +87,16 @@ Također ga možete instalirati pomoću sljedećih naredbi:
#### Windows #### Windows
:::tip[Preporučeno: Koristite WSL] :::tip[Preporučeno: Koristite WSL]
Za najbolje iskustvo na Windows-u preporučujemo korištenje [Windows Subsystem for Linux (WSL)](/docs/windows-wsl). Pruža bolje performanse i potpunu kompatibilnost sa OpenCode karakteristikama. Za najbolje iskustvo na Windows-u preporučujemo korištenje [Windows Subsystem for Linux (WSL)](/docs/windows-wsl). Pruža bolje performanse i potpunu kompatibilnost sa OpenCode funkcijama.
::: :::
- **Upotreba Chocolatey-a** - **Korištenje Chocolatey-a**
```bash ```bash
choco install opencode choco install opencode
``` ```
- **Upotreba Scoop-a** - **Korištenje Scoop-a**
```bash ```bash
scoop install opencode scoop install opencode
@ -108,13 +108,13 @@ Za najbolje iskustvo na Windows-u preporučujemo korištenje [Windows Subsystem
npm install -g opencode-ai npm install -g opencode-ai
``` ```
- **Korišćenje Mise** - **Korištenje Mise**
```bash ```bash
mise use -g github:anomalyco/opencode mise use -g github:anomalyco/opencode
``` ```
- **Korišćenje Dockera** - **Korištenje Dockera**
```bash ```bash
docker run -it --rm ghcr.io/anomalyco/opencode docker run -it --rm ghcr.io/anomalyco/opencode
@ -125,12 +125,11 @@ Također možete preuzeti binarnu datoteku iz [Releases](https://github.com/anom
--- ---
## Konfiguriši ## Konfiguracija
Uz OpenCode možete koristiti bilo kojeg LLM provajdera tako što ćete konfigurirati njihove API ključeve. Uz OpenCode možete koristiti bilo kojeg LLM provajdera tako što ćete konfigurirati njihove API ključeve.
Ako ste tek počeli koristiti LLM provajdere, preporučujemo korištenje [OpenCode Zen](/docs/zen). Ako ste tek počeli koristiti LLM provajdere, preporučujemo korištenje [OpenCode Zen](/docs/zen).
To je kurirana lista modela koji su testirani i verifikovani od strane OpenCode To je kurirana lista modela koji su testirani i verifikovani od strane OpenCode tima.
tim.
1. Pokrenite naredbu `/connect` u TUI-u, odaberite opencode i idite na [opencode.ai/auth](https://opencode.ai/auth). 1. Pokrenite naredbu `/connect` u TUI-u, odaberite opencode i idite na [opencode.ai/auth](https://opencode.ai/auth).
@ -152,10 +151,9 @@ Alternativno, možete odabrati jednog od drugih provajdera. [Saznajte više](/do
--- ---
## Inicijaliziraj ## Inicijalizacija
Sada kada ste konfigurisali provajdera, možete se kretati do projekta koji Sada kada ste konfigurisali provajdera, možete se kretati do projekta na kojem želite raditi.
na čemu želite da radite.
```bash ```bash
cd /path/to/project cd /path/to/project
@ -173,26 +171,22 @@ Zatim inicijalizirajte OpenCode za projekat pokretanjem sljedeće naredbe.
/init /init
``` ```
Ovo će omogućiti OpenCode da analizira vaš projekat i kreira `AGENTS.md` fajl u njemu Ovo će omogućiti OpenCode da analizira vaš projekat i kreira `AGENTS.md` fajl u korijenu projekta.
korijen projekta.
:::tip :::tip
Trebali biste urezati datoteku `AGENTS.md` vašeg projekta u Git. Trebali biste komitovati datoteku `AGENTS.md` vašeg projekta u Git.
::: :::
Ovo pomaže OpenCode da razumije strukturu projekta i obrasce kodiranja Ovo pomaže OpenCode da razumije strukturu projekta i obrasce kodiranja koji se koriste.
korišteno.
--- ---
## Upotreba ## Korištenje
Sada ste spremni da koristite OpenCode za rad na svom projektu. Slobodno pitajte Sada ste spremni da koristite OpenCode za rad na svom projektu. Slobodno pitajte bilo šta!
bilo šta! Ako ste novi u korištenju agenta za AI kodiranje, evo nekoliko primjera koji bi mogli pomoći.
Ako ste novi u korištenju agenta za AI kodiranje, evo nekoliko primjera koji bi mogli
pomoć.
--- ---
### Postavljajte pitanja ### Postavljanje pitanja
Možete zamoliti OpenCode da vam objasni kodnu bazu. Možete zamoliti OpenCode da vam objasni kodnu bazu.
:::tip :::tip
@ -207,13 +201,12 @@ Ovo je korisno ako postoji dio kodne baze na kojem niste radili.
--- ---
### Dodajte karakteristike ### Dodavanje funkcija
Možete zamoliti OpenCode da vašem projektu doda nove funkcije. Iako preporučujemo da ga prvo zamolite da napravi plan. Možete zamoliti OpenCode da vašem projektu doda nove funkcije. Iako preporučujemo da ga prvo zamolite da napravi plan.
1. **Kreirajte plan** 1. **Kreirajte plan**
OpenCode ima _Plan mod_ koji onemogućuje njegovu sposobnost da pravi promjene i OpenCode ima _Plan mod_ koji onemogućuje njegovu sposobnost da pravi promjene i umjesto toga predlaže _kako_ će implementirati ovu funkciju.
umjesto toga predložite _kako_ će implementirati ovu funkciju.
Prebacite se na njega pomoću tipke **Tab**. Vidjet ćete indikator za ovo u donjem desnom uglu. Prebacite se na njega pomoću tipke **Tab**. Vidjet ćete indikator za ovo u donjem desnom uglu.
```bash frame="none" title="Switch to Plan mode" ```bash frame="none" title="Switch to Plan mode"
@ -223,35 +216,31 @@ Možete zamoliti OpenCode da vašem projektu doda nove funkcije. Iako preporuču
Hajde sada da opišemo šta želimo da uradi. Hajde sada da opišemo šta želimo da uradi.
```txt frame="none" ```txt frame="none"
When a user deletes a note, we'd like to flag it as deleted in the database. When a user deletes a note, we'd like to flag it as deleted in the database.
Then create a screen that shows all the recently deleted notes. Then create a screen that shows all the recently deleted notes.
From this screen, the user can undelete a note or permanently delete it. From this screen, the user can undelete a note or permanently delete it.
``` ```
Želite da date OpenCode dovoljno detalja da razumete šta želite. Pomaže Želite da date OpenCode dovoljno detalja da razumije šta želite. Pomaže da razgovarate s njim kao da razgovarate sa mlađim programerom u svom timu.
da razgovarate s njim kao da razgovarate sa mlađim programerom u svom timu.
:::tip :::tip
Dajte OpenCode dosta konteksta i primjera koji će mu pomoći da razumije šta vi Dajte OpenCode dosta konteksta i primjera koji će mu pomoći da razumije šta vi želite.
želim.
::: :::
2. **Ponovite plan** 2. **Ponovite plan**
Kada vam da plan, možete mu dati povratne informacije ili dodati više detalja. Kada vam da plan, možete mu dati povratne informacije ili dodati više detalja.
```txt frame="none" ```txt frame="none"
We'd like to design this new screen using a design I've used before. We'd like to design this new screen using a design I've used before.
[Image #1] Take a look at this image and use it as a reference. [Image #1] Take a look at this image and use it as a reference.
``` ```
:::tip :::tip
Prevucite i ispustite slike u terminal da biste ih dodali u prompt. Prevucite i ispustite slike u terminal da biste ih dodali u prompt.
::: :::
OpenCode može skenirati sve slike koje mu date i dodati ih u prompt. Možeš OpenCode može skenirati sve slike koje mu date i dodati ih u prompt. Možete to učiniti povlačenjem i ispuštanjem slike u terminal.
učinite to povlačenjem i ispuštanjem slike u terminal.
3. **Izgradite funkciju** 3. **Izgradite funkciju**
Kada se osjećate ugodno s planom, vratite se na _Build mode_ do Kada se osjećate ugodno s planom, vratite se na _Build mode_ ponovnim pritiskom na taster **Tab**.
ponovnim pritiskom na taster **Tab**.
```bash frame="none" ```bash frame="none"
<TAB> <TAB>
@ -265,10 +254,9 @@ I tražeći od njega da napravi promjene.
--- ---
### Napravite promjene ### Pravljenje izmjena
Za jednostavnije promjene, možete zamoliti OpenCode da ga direktno izgradi Za jednostavnije promjene, možete zamoliti OpenCode da ga direktno izgradi bez potrebe da prvo pregledate plan.
bez potrebe da prvo pregledate plan.
```txt frame="none" "@packages/functions/src/settings.ts" "@packages/functions/src/notes.ts" ```txt frame="none" "@packages/functions/src/settings.ts" "@packages/functions/src/notes.ts"
We need to add authentication to the /settings route. Take a look at how this is We need to add authentication to the /settings route. Take a look at how this is
@ -276,12 +264,11 @@ handled in the /notes route in @packages/functions/src/notes.ts and implement
the same logic in @packages/functions/src/settings.ts the same logic in @packages/functions/src/settings.ts
``` ```
Želite da budete sigurni da ste pružili dobru količinu detalja kako bi OpenCode bio ispravan Želite da budete sigurni da ste pružili dobru količinu detalja kako bi OpenCode napravio ispravne promjene.
promjene.
--- ---
### Poništi promjene ### Poništavanje izmjena
Recimo da tražite od OpenCode da izvrši neke promjene. Recimo da tražite od OpenCode da izvrši neke promjene.
@ -289,15 +276,13 @@ Recimo da tražite od OpenCode da izvrši neke promjene.
Can you refactor the function in @packages/functions/src/api/index.ts? Can you refactor the function in @packages/functions/src/api/index.ts?
``` ```
Ali shvatate da to nije ono što ste želeli. Možete **poništiti** promjene Ali shvatate da to nije ono što ste željeli. Možete **poništiti** promjene koristeći naredbu `/undo`.
koristeći naredbu `/undo`.
```bash frame="none" ```bash frame="none"
/undo /undo
``` ```
OpenCode će sada poništiti promjene koje ste napravili i prikazati vašu originalnu poruku OpenCode će sada poništiti promjene koje ste napravili i ponovo prikazati vašu originalnu poruku.
opet.
```txt frame="none" "@packages/functions/src/api/index.ts" ```txt frame="none" "@packages/functions/src/api/index.ts"
Can you refactor the function in @packages/functions/src/api/index.ts? Can you refactor the function in @packages/functions/src/api/index.ts?
@ -315,10 +300,9 @@ Ili **možete ponoviti** promjene koristeći naredbu `/redo`.
--- ---
## Dijeli ## Dijeljenje
Razgovore koje imate sa OpenCode možete [dijeliti sa vašim Razgovore koje imate sa OpenCode možete [dijeliti sa vašim timom](/docs/share).
tim](/docs/share).
```bash frame="none" ```bash frame="none"
/share /share
@ -332,7 +316,7 @@ Evo [primjer razgovora](https://opencode.ai/s/4XP1fce5) sa OpenCode.
--- ---
## Prilagodi ## Prilagođavanje
I to je to! Sada ste profesionalac u korištenju OpenCode. I to je to! Sada ste profesionalac u korištenju OpenCode.
Da biste to učinili svojim, preporučujemo [odabir teme](/docs/themes), [prilagođavanje povezivanja tipki](/docs/keybinds), [konfiguriranje formatera koda](/docs/formatters), [kreiranje prilagođenih komandi](/docs/commands), ili igranje sa [OpenCode config](/docs/config). Da biste to učinili svojim, preporučujemo [odabir teme](/docs/themes), [prilagođavanje povezivanja tipki](/docs/keybinds), [konfiguriranje formatera koda](/docs/formatters), [kreiranje prilagođenih komandi](/docs/commands), ili igranje sa [OpenCode config](/docs/config).

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