Merge branch 'dev' into feature/session-handoff
commit
89ca7986bf
|
|
@ -8,6 +8,8 @@
|
|||
# - Denounce with minus prefix: -username or -platform:username.
|
||||
# - Optional details after a space following the handle.
|
||||
adamdotdevin
|
||||
ariane-emory
|
||||
-florianleibert
|
||||
fwang
|
||||
iamdavidhill
|
||||
jayair
|
||||
|
|
|
|||
|
|
@ -64,10 +64,13 @@ jobs:
|
|||
|
||||
Requirements:
|
||||
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.
|
||||
3. Keep locale docs structure aligned with their corresponding English pages.
|
||||
4. Do not modify English source docs in packages/web/src/content/docs/*.mdx.
|
||||
5. If no locale updates are needed, make no changes.
|
||||
2. You MUST use the Task tool for translation work and launch subagents with subagent_type `translator` (defined in .opencode/agent/translator.md).
|
||||
3. Do not translate directly in the primary agent. Use translator subagent output as the source for locale text updates.
|
||||
4. Run translator subagent Task calls in parallel whenever file/locale translation work is independent.
|
||||
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
|
||||
if: steps.changes.outputs.has_changes == 'true'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -32,6 +32,9 @@ description: Use this when you are working on file operations like reading, writ
|
|||
- Decode tool stderr with `Bun.readableStreamToText`.
|
||||
- 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
|
||||
|
||||
- Use Bun APIs first.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
github-policies:
|
||||
runners:
|
||||
allowed_groups:
|
||||
- "blacksmith runners 01kbd5v56sg8tz7rea39b7ygpt"
|
||||
build:
|
||||
disallow_reruns: false
|
||||
branch_rulesets:
|
||||
56
bun.lock
56
bun.lock
|
|
@ -23,7 +23,7 @@
|
|||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
|
|
@ -73,7 +73,7 @@
|
|||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
|
|
@ -107,7 +107,7 @@
|
|||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
|
|
@ -134,7 +134,7 @@
|
|||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
|
|
@ -158,7 +158,7 @@
|
|||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
|
@ -182,7 +182,7 @@
|
|||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
|
|
@ -215,7 +215,7 @@
|
|||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
|
|
@ -244,7 +244,7 @@
|
|||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
|
|
@ -260,7 +260,7 @@
|
|||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
|
|
@ -301,8 +301,8 @@
|
|||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.5.4",
|
||||
"@opentui/core": "0.1.77",
|
||||
"@opentui/solid": "0.1.77",
|
||||
"@opentui/core": "0.1.79",
|
||||
"@opentui/solid": "0.1.79",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
|
|
@ -366,7 +366,7 @@
|
|||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
|
|
@ -386,7 +386,7 @@
|
|||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.90.10",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
|
|
@ -397,7 +397,7 @@
|
|||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
|
|
@ -410,7 +410,7 @@
|
|||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
|
|
@ -452,7 +452,7 @@
|
|||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
|
|
@ -463,7 +463,7 @@
|
|||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
|
@ -522,7 +522,7 @@
|
|||
"@tailwindcss/vite": "4.1.11",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/bun": "1.3.8",
|
||||
"@types/bun": "1.3.9",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "22.13.9",
|
||||
"@types/semver": "7.7.1",
|
||||
|
|
@ -1279,21 +1279,21 @@
|
|||
|
||||
"@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=="],
|
||||
|
||||
|
|
@ -1853,7 +1853,7 @@
|
|||
|
||||
"@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=="],
|
||||
|
||||
|
|
@ -2181,7 +2181,7 @@
|
|||
|
||||
"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=="],
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-cvRBvHRuunNjF07c4GVHl5rRgoTn1qfI/HdJWtOV63M=",
|
||||
"aarch64-linux": "sha256-DJUI4pMZ7wQTnyOiuDHALmZz7FZtrTbzRzCuNOShmWE=",
|
||||
"aarch64-darwin": "sha256-JnkqDwuC7lNsjafV+jOGfvs8K1xC8rk5CTOW+spjiCA=",
|
||||
"x86_64-darwin": "sha256-GBeTqq2vDn/mXplYNglrAT2xajjFVzB4ATHnMS0j7z4="
|
||||
"x86_64-linux": "sha256-pp2gb4nxiIT3VltB6Xli2wZPH32JfnMsI+BbihyU1+E=",
|
||||
"aarch64-linux": "sha256-hJwxhBICZz/pbIxQsF/sIpZTlFIgLpcAyF44O8wxMdU=",
|
||||
"aarch64-darwin": "sha256-DPONXP52XOg/ApdSnLp32a+K5XCOnDGhbTUto2Rme0g=",
|
||||
"x86_64-darwin": "sha256-KX1h5LRJSgthpbOPqWlbM/sPf8cvQrdRJvxtrz/FzBQ="
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"description": "AI-powered development tool",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.8",
|
||||
"packageManager": "bun@1.3.9",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"dev:desktop": "bun --cwd packages/desktop tauri dev",
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
"packages/slack"
|
||||
],
|
||||
"catalog": {
|
||||
"@types/bun": "1.3.8",
|
||||
"@types/bun": "1.3.9",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"ulid": "3.0.1",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { onCleanup, onMount } from "solid-js"
|
|||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { uuid } from "@/utils/uuid"
|
||||
import { getCursorPosition } from "./editor-dom"
|
||||
|
||||
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 attachment: ImageAttachmentPart = {
|
||||
type: "image",
|
||||
id: crypto.randomUUID?.() ?? Math.random().toString(16).slice(2),
|
||||
id: uuid(),
|
||||
filename: file.name,
|
||||
mime: file.type,
|
||||
dataUrl,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
|
|||
import { useParams } from "@solidjs/router"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { createScopedCache } from "@/utils/scoped-cache"
|
||||
import { uuid } from "@/utils/uuid"
|
||||
import type { SelectedLineRange } from "@/context/file"
|
||||
|
||||
export type LineComment = {
|
||||
|
|
@ -53,7 +54,7 @@ function createCommentSessionState(store: Store<CommentStore>, setStore: SetStor
|
|||
|
||||
const add = (input: Omit<LineComment, "id" | "time">) => {
|
||||
const next: LineComment = {
|
||||
id: crypto.randomUUID?.() ?? Math.random().toString(16).slice(2),
|
||||
id: uuid(),
|
||||
time: Date.now(),
|
||||
...input,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { uuid } from "@/utils/uuid"
|
||||
|
||||
type Nav = {
|
||||
id: string
|
||||
dir?: string
|
||||
|
|
@ -16,8 +18,6 @@ const key = (dir: string | undefined, to: string) => `${dir ?? ""}:${to}`
|
|||
|
||||
const now = () => performance.now()
|
||||
|
||||
const uid = () => crypto.randomUUID?.() ?? Math.random().toString(16).slice(2)
|
||||
|
||||
const navs = new Map<string, Nav>()
|
||||
const pending = 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 }) {
|
||||
if (!dev) return
|
||||
|
||||
const id = uid()
|
||||
const id = uuid()
|
||||
const start = now()
|
||||
const nav = ensure(id, { ...input, id, 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 pendingId = pending.get(k)
|
||||
if (pendingId) pending.delete(k)
|
||||
const id = pendingId ?? uid()
|
||||
const id = pendingId ?? uuid()
|
||||
|
||||
const start = now()
|
||||
const nav = ensure(id, { ...input, id, start, trigger: pendingId ? "key" : "route" })
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 697 B |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -7,18 +7,24 @@ import { useI18n } from "~/context/i18n"
|
|||
import { LocaleLinks } from "~/component/locale-links"
|
||||
import previewLogoLight from "../../asset/brand/preview-opencode-logo-light.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 previewWordmarkDark from "../../asset/brand/preview-opencode-wordmark-dark.png"
|
||||
import previewWordmarkSimpleLight from "../../asset/brand/preview-opencode-wordmark-simple-light.png"
|
||||
import previewWordmarkSimpleDark from "../../asset/brand/preview-opencode-wordmark-simple-dark.png"
|
||||
import logoLightPng from "../../asset/brand/opencode-logo-light.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 wordmarkDarkPng from "../../asset/brand/opencode-wordmark-dark.png"
|
||||
import wordmarkSimpleLightPng from "../../asset/brand/opencode-wordmark-simple-light.png"
|
||||
import wordmarkSimpleDarkPng from "../../asset/brand/opencode-wordmark-simple-dark.png"
|
||||
import logoLightSvg from "../../asset/brand/opencode-logo-light.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 wordmarkDarkSvg from "../../asset/brand/opencode-wordmark-dark.svg"
|
||||
import wordmarkSimpleLightSvg from "../../asset/brand/opencode-wordmark-simple-light.svg"
|
||||
|
|
@ -135,6 +141,60 @@ export default function Brand() {
|
|||
</button>
|
||||
</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>
|
||||
<img src={previewWordmarkLight} alt="OpenCode brand guidelines" />
|
||||
<div data-component="actions">
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ type RetryOptions = {
|
|||
excludeProviders: string[]
|
||||
retryCount: number
|
||||
}
|
||||
type BillingSource = "anonymous" | "free" | "byok" | "subscription" | "balance"
|
||||
|
||||
export async function handler(
|
||||
input: APIEvent,
|
||||
|
|
@ -51,6 +52,7 @@ export async function handler(
|
|||
type AuthInfo = Awaited<ReturnType<typeof authenticate>>
|
||||
type ModelInfo = Awaited<ReturnType<typeof validateModel>>
|
||||
type ProviderInfo = Awaited<ReturnType<typeof selectProvider>>
|
||||
type CostInfo = ReturnType<typeof calculateCost>
|
||||
|
||||
const MAX_FAILOVER_RETRIES = 3
|
||||
const MAX_429_RETRIES = 3
|
||||
|
|
@ -139,21 +141,22 @@ export async function handler(
|
|||
"llm.error.code": res.status,
|
||||
"llm.error.message": res.statusText,
|
||||
})
|
||||
}
|
||||
|
||||
// Try another provider => stop retrying if using fallback provider
|
||||
if (
|
||||
// ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
|
||||
res.status !== 404 &&
|
||||
// ie. cannot change codex model providers mid-session
|
||||
modelInfo.stickyProvider !== "strict" &&
|
||||
modelInfo.fallbackProvider &&
|
||||
providerInfo.id !== modelInfo.fallbackProvider
|
||||
) {
|
||||
return retriableRequest({
|
||||
excludeProviders: [...retry.excludeProviders, providerInfo.id],
|
||||
retryCount: retry.retryCount + 1,
|
||||
})
|
||||
}
|
||||
// Try another provider => stop retrying if using fallback provider
|
||||
if (
|
||||
res.status !== 200 &&
|
||||
// ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
|
||||
res.status !== 404 &&
|
||||
// ie. cannot change codex model providers mid-session
|
||||
modelInfo.stickyProvider !== "strict" &&
|
||||
modelInfo.fallbackProvider &&
|
||||
providerInfo.id !== modelInfo.fallbackProvider
|
||||
) {
|
||||
return retriableRequest({
|
||||
excludeProviders: [...retry.excludeProviders, providerInfo.id],
|
||||
retryCount: retry.retryCount + 1,
|
||||
})
|
||||
}
|
||||
|
||||
return { providerInfo, reqBody, res, startTimestamp }
|
||||
|
|
@ -183,18 +186,25 @@ export async function handler(
|
|||
|
||||
// Handle non-streaming response
|
||||
if (!isStream) {
|
||||
const responseConverter = createResponseConverter(providerInfo.format, opts.format)
|
||||
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.debug("RESPONSE: " + body)
|
||||
dataDumper?.provideResponse(body)
|
||||
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, {
|
||||
status: resStatus,
|
||||
statusText: res.statusText,
|
||||
|
|
@ -226,12 +236,16 @@ export async function handler(
|
|||
dataDumper?.flush()
|
||||
await rateLimiter?.track()
|
||||
const usage = usageParser.retrieve()
|
||||
let cost = "0"
|
||||
if (usage) {
|
||||
const tokensInfo = providerInfo.normalizeUsage(usage)
|
||||
await trialLimiter?.track(tokensInfo)
|
||||
const costInfo = await trackUsage(authInfo, modelInfo, providerInfo, billingSource, tokensInfo)
|
||||
await reload(authInfo, costInfo)
|
||||
const usageInfo = providerInfo.normalizeUsage(usage)
|
||||
const costInfo = calculateCost(modelInfo, usageInfo)
|
||||
await trialLimiter?.track(usageInfo)
|
||||
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()
|
||||
return
|
||||
}
|
||||
|
|
@ -283,7 +297,6 @@ export async function handler(
|
|||
return pump()
|
||||
},
|
||||
})
|
||||
|
||||
return new Response(stream, {
|
||||
status: resStatus,
|
||||
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.provider?.credentials) return "free"
|
||||
if (authInfo.provider?.credentials) return "byok"
|
||||
if (authInfo.isFree) return "free"
|
||||
if (modelInfo.allowAnonymous) return "free"
|
||||
|
||||
|
|
@ -613,13 +626,7 @@ export async function handler(
|
|||
return res
|
||||
}
|
||||
|
||||
async function trackUsage(
|
||||
authInfo: AuthInfo,
|
||||
modelInfo: ModelInfo,
|
||||
providerInfo: ProviderInfo,
|
||||
billingSource: ReturnType<typeof validateBilling>,
|
||||
usageInfo: UsageInfo,
|
||||
) {
|
||||
function calculateCost(modelInfo: ModelInfo, usageInfo: UsageInfo) {
|
||||
const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
|
||||
usageInfo
|
||||
|
||||
|
|
@ -657,6 +664,33 @@ export async function handler(
|
|||
(cacheReadCost ?? 0) +
|
||||
(cacheWrite5mCost ?? 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({
|
||||
"tokens.input": inputTokens,
|
||||
|
|
@ -677,7 +711,7 @@ export async function handler(
|
|||
if (billingSource === "anonymous") return
|
||||
authInfo = authInfo!
|
||||
|
||||
const cost = authInfo.provider?.credentials ? 0 : centsToMicroCents(totalCostInCent)
|
||||
const cost = centsToMicroCents(totalCostInCent)
|
||||
await Database.use((db) =>
|
||||
Promise.all([
|
||||
db.insert(UsageTable).values({
|
||||
|
|
@ -772,16 +806,12 @@ export async function handler(
|
|||
return { costInMicroCents: cost }
|
||||
}
|
||||
|
||||
async function reload(authInfo: AuthInfo, costInfo: Awaited<ReturnType<typeof trackUsage>>) {
|
||||
if (!authInfo) return
|
||||
if (authInfo.isFree) return
|
||||
if (authInfo.provider?.credentials) return
|
||||
if (authInfo.subscription) return
|
||||
|
||||
if (!costInfo) return
|
||||
async function reload(billingSource: BillingSource, authInfo: AuthInfo, costInfo: CostInfo) {
|
||||
if (billingSource !== "balance") return
|
||||
authInfo = authInfo!
|
||||
|
||||
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
|
||||
|
||||
const lock = await Database.use((tx) =>
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) =>
|
|||
}
|
||||
},
|
||||
retrieve: () => usage,
|
||||
buidlCostChunk: (cost: string) => `event: ping\ndata: ${JSON.stringify({ type: "ping", cost })}\n\n`,
|
||||
}
|
||||
},
|
||||
normalizeUsage: (usage: Usage) => ({
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ export const googleHelper: ProviderHelper = ({ providerModel }) => ({
|
|||
usage = json.usageMetadata
|
||||
},
|
||||
retrieve: () => usage,
|
||||
buidlCostChunk: (cost: string) => `data: ${JSON.stringify({ type: "ping", cost })}\n\n`,
|
||||
}
|
||||
},
|
||||
normalizeUsage: (usage: Usage) => {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ export const oaCompatHelper: ProviderHelper = () => ({
|
|||
usage = json.usage
|
||||
},
|
||||
retrieve: () => usage,
|
||||
buidlCostChunk: (cost: string) => `data: ${JSON.stringify({ choices: [], cost })}\n\n`,
|
||||
}
|
||||
},
|
||||
normalizeUsage: (usage: Usage) => {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export const openaiHelper: ProviderHelper = () => ({
|
|||
usage = json.response.usage
|
||||
},
|
||||
retrieve: () => usage,
|
||||
buidlCostChunk: (cost: string) => `event: ping\ndata: ${JSON.stringify({ type: "ping", cost })}\n\n`,
|
||||
}
|
||||
},
|
||||
normalizeUsage: (usage: Usage) => {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export type ProviderHelper = (input: { reqModel: string; providerModel: string }
|
|||
createUsageParser: () => {
|
||||
parse: (chunk: string) => void
|
||||
retrieve: () => any
|
||||
buidlCostChunk: (cost: string) => string
|
||||
}
|
||||
normalizeUsage: (usage: any) => UsageInfo
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -3117,6 +3117,7 @@ dependencies = [
|
|||
"tauri-plugin-window-state",
|
||||
"tauri-specta",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
|
|
@ -5631,6 +5632,18 @@ dependencies = [
|
|||
"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]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.17"
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ tracing = "0.1"
|
|||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing-appender = "0.2"
|
||||
chrono = "0.4"
|
||||
tokio-stream = { version = "0.1.18", features = ["sync"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
gtk = "0.18.2"
|
||||
|
|
|
|||
|
|
@ -1,35 +1,46 @@
|
|||
use futures::{FutureExt, Stream, StreamExt, future};
|
||||
use tauri::{AppHandle, Manager, path::BaseDirectory};
|
||||
use tauri_plugin_shell::{
|
||||
ShellExt,
|
||||
process::{Command, CommandChild, CommandEvent, TerminatedPayload},
|
||||
process::{CommandChild, CommandEvent, TerminatedPayload},
|
||||
};
|
||||
use tauri_plugin_store::StoreExt;
|
||||
use tauri_specta::Event;
|
||||
use tokio::sync::oneshot;
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::constants::{SETTINGS_STORE, WSL_ENABLED_KEY};
|
||||
|
||||
const CLI_INSTALL_DIR: &str = ".opencode/bin";
|
||||
const CLI_BINARY_NAME: &str = "opencode";
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
pub struct ServerConfig {
|
||||
pub hostname: Option<String>,
|
||||
pub port: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
pub struct Config {
|
||||
pub server: Option<ServerConfig>,
|
||||
}
|
||||
|
||||
pub async fn get_config(app: &AppHandle) -> Option<Config> {
|
||||
create_command(app, "debug config", &[])
|
||||
.output()
|
||||
let (events, _) = spawn_command(app, "debug config", &[]).ok()?;
|
||||
|
||||
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
|
||||
.inspect_err(|e| tracing::warn!("Failed to read OC config: {e}"))
|
||||
.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> {
|
||||
|
|
@ -175,7 +186,11 @@ fn shell_escape(input: &str) -> String {
|
|||
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
|
||||
.path()
|
||||
.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())),
|
||||
);
|
||||
|
||||
if cfg!(windows) {
|
||||
let cmd = if cfg!(windows) {
|
||||
if is_wsl_enabled(app) {
|
||||
tracing::info!("WSL is enabled, spawning CLI server in WSL");
|
||||
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));
|
||||
|
||||
return app
|
||||
.shell()
|
||||
app.shell()
|
||||
.command("wsl")
|
||||
.args(["-e", "bash", "-lc", &script.join("\n")]);
|
||||
.args(["-e", "bash", "-lc", &script.join("\n")])
|
||||
} else {
|
||||
let mut cmd = app
|
||||
.shell()
|
||||
|
|
@ -249,7 +263,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St
|
|||
cmd = cmd.env(key, value);
|
||||
}
|
||||
|
||||
return cmd;
|
||||
cmd
|
||||
}
|
||||
} else {
|
||||
let sidecar = get_sidecar_path(app);
|
||||
|
|
@ -268,7 +282,13 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St
|
|||
}
|
||||
|
||||
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(
|
||||
|
|
@ -286,45 +306,96 @@ pub fn serve(
|
|||
("OPENCODE_SERVER_PASSWORD", password.to_string()),
|
||||
];
|
||||
|
||||
let (mut rx, child) = create_command(
|
||||
let (events, child) = spawn_command(
|
||||
app,
|
||||
format!("--print-logs --log-level WARN serve --hostname {hostname} --port {port}").as_str(),
|
||||
&envs,
|
||||
)
|
||||
.spawn()
|
||||
.expect("Failed to spawn opencode");
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut exit_tx = Some(exit_tx);
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Stdout(line_bytes) => {
|
||||
let line = String::from_utf8_lossy(&line_bytes);
|
||||
tracing::info!(target: "sidecar", "{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);
|
||||
let mut exit_tx = Some(exit_tx);
|
||||
tokio::spawn(
|
||||
events
|
||||
.for_each(move |event| {
|
||||
match event {
|
||||
CommandEvent::Stdout(line_bytes) => {
|
||||
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!("{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)
|
||||
}
|
||||
|
||||
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),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,16 +24,17 @@ use std::{
|
|||
sync::{Arc, Mutex},
|
||||
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)))]
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use tauri_plugin_shell::process::CommandChild;
|
||||
use tauri_specta::Event;
|
||||
use tokio::{
|
||||
sync::{oneshot, watch},
|
||||
time::{sleep, timeout},
|
||||
};
|
||||
|
||||
use crate::cli::sync_cli;
|
||||
use crate::cli::{sqlite_migration::SqliteMigrationProgress, sync_cli};
|
||||
use crate::constants::*;
|
||||
use crate::server::get_saved_server_url;
|
||||
use crate::windows::{LoadingWindow, MainWindow};
|
||||
|
|
@ -122,8 +123,8 @@ async fn await_initialization(
|
|||
let mut rx = init_state.current.clone();
|
||||
|
||||
let events = async {
|
||||
let e = (*rx.borrow()).clone();
|
||||
let _ = events.send(e).unwrap();
|
||||
let e = *rx.borrow();
|
||||
let _ = events.send(e);
|
||||
|
||||
while rx.changed().await.is_ok() {
|
||||
let step = *rx.borrow_and_update();
|
||||
|
|
@ -517,7 +518,10 @@ fn make_specta_builder() -> tauri_specta::Builder<tauri::Wry> {
|
|||
wsl_path,
|
||||
resolve_app_path
|
||||
])
|
||||
.events(tauri_specta::collect_events![LoadingWindowComplete])
|
||||
.events(tauri_specta::collect_events![
|
||||
LoadingWindowComplete,
|
||||
SqliteMigrationProgress
|
||||
])
|
||||
.error_handling(tauri_specta::ErrorHandlingMode::Throw)
|
||||
}
|
||||
|
||||
|
|
@ -556,17 +560,46 @@ async fn initialize(app: AppHandle) {
|
|||
|
||||
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_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 init_tx = init_tx.clone();
|
||||
let app = app.clone();
|
||||
|
||||
async move {
|
||||
let mut sqlite_exists = sqlite_file_exists();
|
||||
|
||||
tracing::info!("Setting up server connection");
|
||||
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
|
||||
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 sqlite_enabled {
|
||||
tracing::debug!(sqlite_exists, "Checking sqlite file existence");
|
||||
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();
|
||||
}
|
||||
}
|
||||
if let Some(sqlite_done_rx) = sqlite_done {
|
||||
let _ = sqlite_done_rx.await;
|
||||
}
|
||||
|
||||
tokio::spawn(cli_health_check);
|
||||
}
|
||||
|
||||
|
|
@ -654,11 +676,11 @@ async fn initialize(app: AppHandle) {
|
|||
.is_err()
|
||||
{
|
||||
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");
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
Some(loading_window)
|
||||
} else {
|
||||
tracing::debug!("Showing main window without loading window");
|
||||
MainWindow::create(&app).expect("Failed to create main window");
|
||||
|
||||
None
|
||||
|
|
@ -667,7 +689,6 @@ async fn initialize(app: AppHandle) {
|
|||
let _ = loading_task.await;
|
||||
|
||||
tracing::info!("Loading done, completing initialisation");
|
||||
|
||||
let _ = init_tx.send(InitStep::Done);
|
||||
|
||||
if loading_window.is_some() {
|
||||
|
|
|
|||
|
|
@ -11,17 +11,11 @@ use crate::{
|
|||
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 enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for WslConfig {
|
||||
fn default() -> Self {
|
||||
Self { enabled: false }
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_default_server_url(app: AppHandle) -> Result<Option<String>, String> {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export const commands = {
|
|||
/** Events */
|
||||
export const events = {
|
||||
loadingWindowComplete: makeEvent<LoadingWindowComplete>("loading-window-complete"),
|
||||
sqliteMigrationProgress: makeEvent<SqliteMigrationProgress>("sqlite-migration-progress"),
|
||||
};
|
||||
|
||||
/* Types */
|
||||
|
|
@ -37,6 +38,8 @@ export type ServerReadyData = {
|
|||
password: string | null,
|
||||
};
|
||||
|
||||
export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { type: "Done" };
|
||||
|
||||
export type WslConfig = {
|
||||
enabled: boolean,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import "@opencode-ai/app/index.css"
|
|||
import { Font } from "@opencode-ai/ui/font"
|
||||
import { Splash } from "@opencode-ai/ui/logo"
|
||||
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 { Channel } from "@tauri-apps/api/core"
|
||||
import { Switch } from "solid-js"
|
||||
|
|
@ -57,15 +57,29 @@ render(() => {
|
|||
"This could take a couple of minutes",
|
||||
]
|
||||
const [textIndex, setTextIndex] = createSignal(0)
|
||||
const [progress, setProgress] = createSignal(0)
|
||||
|
||||
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))
|
||||
setTextIndex(1)
|
||||
await new Promise((res) => setTimeout(res, 6000))
|
||||
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>
|
||||
</Switch>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.1.59"
|
||||
version = "1.1.60"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/anomalyco/opencode"
|
||||
|
|
@ -11,26 +11,26 @@ name = "OpenCode"
|
|||
icon = "./icons/opencode.svg"
|
||||
|
||||
[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"
|
||||
args = ["acp"]
|
||||
|
||||
[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"
|
||||
args = ["acp"]
|
||||
|
||||
[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"
|
||||
args = ["acp"]
|
||||
|
||||
[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"
|
||||
args = ["acp"]
|
||||
|
||||
[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"
|
||||
args = ["acp"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
|
@ -84,8 +84,8 @@
|
|||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.5.4",
|
||||
"@opentui/core": "0.1.77",
|
||||
"@opentui/solid": "0.1.77",
|
||||
"@opentui/core": "0.1.79",
|
||||
"@opentui/solid": "0.1.79",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Clipboard } from "@tui/util/clipboard"
|
|||
import { TextAttributes } from "@opentui/core"
|
||||
import { RouteProvider, useRoute } from "@tui/context/route"
|
||||
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 { Flag } from "@/flag/flag"
|
||||
import { DialogProvider, useDialog } from "@tui/ui/dialog"
|
||||
|
|
@ -110,8 +111,17 @@ export function tui(input: {
|
|||
}) {
|
||||
// promise to prevent immediate exit
|
||||
return new Promise<void>(async (resolve) => {
|
||||
const unguard = win32InstallCtrlCGuard()
|
||||
win32DisableProcessedInput()
|
||||
|
||||
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 () => {
|
||||
unguard?.()
|
||||
await input.onExit?.()
|
||||
resolve()
|
||||
}
|
||||
|
|
@ -730,7 +740,8 @@ function ErrorComponent(props: {
|
|||
const handleExit = async () => {
|
||||
renderer.setTerminalTitle("")
|
||||
renderer.destroy()
|
||||
props.onExit()
|
||||
win32FlushInputBuffer()
|
||||
await props.onExit()
|
||||
}
|
||||
|
||||
useKeyboard((evt) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { cmd } from "../cmd"
|
||||
import { tui } from "./app"
|
||||
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
|
||||
|
||||
export const AttachCommand = cmd({
|
||||
command: "attach <url>",
|
||||
|
|
@ -26,27 +27,34 @@ export const AttachCommand = cmd({
|
|||
describe: "basic auth password (defaults to OPENCODE_SERVER_PASSWORD)",
|
||||
}),
|
||||
handler: async (args) => {
|
||||
const directory = (() => {
|
||||
if (!args.dir) return undefined
|
||||
try {
|
||||
process.chdir(args.dir)
|
||||
return process.cwd()
|
||||
} catch {
|
||||
// If the directory doesn't exist locally (remote attach), pass it through.
|
||||
return args.dir
|
||||
}
|
||||
})()
|
||||
const headers = (() => {
|
||||
const password = args.password ?? process.env.OPENCODE_SERVER_PASSWORD
|
||||
if (!password) return undefined
|
||||
const auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`
|
||||
return { Authorization: auth }
|
||||
})()
|
||||
await tui({
|
||||
url: args.url,
|
||||
args: { sessionID: args.session },
|
||||
directory,
|
||||
headers,
|
||||
})
|
||||
const unguard = win32InstallCtrlCGuard()
|
||||
try {
|
||||
win32DisableProcessedInput()
|
||||
|
||||
const directory = (() => {
|
||||
if (!args.dir) return undefined
|
||||
try {
|
||||
process.chdir(args.dir)
|
||||
return process.cwd()
|
||||
} catch {
|
||||
// If the directory doesn't exist locally (remote attach), pass it through.
|
||||
return args.dir
|
||||
}
|
||||
})()
|
||||
const headers = (() => {
|
||||
const password = args.password ?? process.env.OPENCODE_SERVER_PASSWORD
|
||||
if (!password) return undefined
|
||||
const auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`
|
||||
return { Authorization: auth }
|
||||
})()
|
||||
await tui({
|
||||
url: args.url,
|
||||
args: { sessionID: args.session },
|
||||
directory,
|
||||
headers,
|
||||
})
|
||||
} finally {
|
||||
unguard?.()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useRenderer } from "@opentui/solid"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { FormatError, FormatUnknownError } from "@/cli/error"
|
||||
import { win32FlushInputBuffer } from "../win32"
|
||||
type Exit = ((reason?: unknown) => Promise<void>) & {
|
||||
message: {
|
||||
set: (value?: string) => () => void
|
||||
|
|
@ -32,6 +33,7 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({
|
|||
// Reset window title before destroying renderer
|
||||
renderer.setTerminalTitle("")
|
||||
renderer.destroy()
|
||||
win32FlushInputBuffer()
|
||||
await input.onExit?.()
|
||||
if (reason) {
|
||||
const formatted = FormatError(reason) ?? FormatUnknownError(reason)
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ export function Session() {
|
|||
const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true)
|
||||
const [showAssistantMetadata, setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true)
|
||||
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 [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true)
|
||||
|
||||
|
|
@ -582,6 +583,15 @@ export function Session() {
|
|||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: showHeader() ? "Hide header" : "Show header",
|
||||
value: "session.toggle.header",
|
||||
category: "Session",
|
||||
onSelect: (dialog) => {
|
||||
setShowHeader((prev) => !prev)
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Page up",
|
||||
value: "session.page.up",
|
||||
|
|
@ -963,7 +973,7 @@ export function Session() {
|
|||
<box flexDirection="row">
|
||||
<box flexGrow={1} paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={1}>
|
||||
<Show when={session()}>
|
||||
<Show when={!sidebarVisible() || !wide()}>
|
||||
<Show when={showHeader() && (!sidebarVisible() || !wide())}>
|
||||
<Header />
|
||||
</Show>
|
||||
<scrollbox
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { Log } from "@/util/log"
|
|||
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
|
||||
import type { Event } from "@opencode-ai/sdk/v2"
|
||||
import type { EventSource } from "./context/sdk"
|
||||
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
|
||||
|
||||
declare global {
|
||||
const OPENCODE_WORKER_PATH: string
|
||||
|
|
@ -77,99 +78,111 @@ export const TuiThreadCommand = cmd({
|
|||
describe: "agent to use",
|
||||
}),
|
||||
handler: async (args) => {
|
||||
if (args.fork && !args.continue && !args.session) {
|
||||
UI.error("--fork requires --continue or --session")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// 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
|
||||
})
|
||||
// Keep ENABLE_PROCESSED_INPUT cleared even if other code flips it.
|
||||
// (Important when running under `bun run` wrappers on Windows.)
|
||||
const unguard = win32InstallCtrlCGuard()
|
||||
try {
|
||||
process.chdir(cwd)
|
||||
} catch (e) {
|
||||
UI.error("Failed to change directory to " + cwd)
|
||||
return
|
||||
// Must be the very first thing — disables CTRL_C_EVENT before any Worker
|
||||
// spawn or async work so the OS cannot kill the process group.
|
||||
win32DisableProcessedInput()
|
||||
|
||||
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
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -571,25 +571,28 @@ export namespace MCP {
|
|||
const clientsSnapshot = await clients()
|
||||
const defaultTimeout = cfg.experimental?.mcp_timeout
|
||||
|
||||
for (const [clientName, client] of Object.entries(clientsSnapshot)) {
|
||||
// Only include tools from connected MCPs (skip disabled ones)
|
||||
if (s.status[clientName]?.status !== "connected") {
|
||||
continue
|
||||
}
|
||||
const connectedClients = Object.entries(clientsSnapshot).filter(
|
||||
([clientName]) => s.status[clientName]?.status === "connected",
|
||||
)
|
||||
|
||||
const toolsResult = await client.listTools().catch((e) => {
|
||||
log.error("failed to get tools", { clientName, error: e.message })
|
||||
const failedStatus = {
|
||||
status: "failed" as const,
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
}
|
||||
s.status[clientName] = failedStatus
|
||||
delete s.clients[clientName]
|
||||
return undefined
|
||||
})
|
||||
if (!toolsResult) {
|
||||
continue
|
||||
}
|
||||
const toolsResults = await Promise.all(
|
||||
connectedClients.map(async ([clientName, client]) => {
|
||||
const toolsResult = await client.listTools().catch((e) => {
|
||||
log.error("failed to get tools", { clientName, error: e.message })
|
||||
const failedStatus = {
|
||||
status: "failed" as const,
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
}
|
||||
s.status[clientName] = failedStatus
|
||||
delete s.clients[clientName]
|
||||
return undefined
|
||||
})
|
||||
return { clientName, client, toolsResult }
|
||||
}),
|
||||
)
|
||||
|
||||
for (const { clientName, client, toolsResult } of toolsResults) {
|
||||
if (!toolsResult) continue
|
||||
const mcpConfig = config[clientName]
|
||||
const entry = isMcpConfigured(mcpConfig) ? mcpConfig : undefined
|
||||
const timeout = entry?.timeout ?? defaultTimeout
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export namespace ModelsDev {
|
|||
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
|
||||
options: z.record(z.string(), z.any()),
|
||||
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(),
|
||||
})
|
||||
export type Model = z.infer<typeof Model>
|
||||
|
|
|
|||
|
|
@ -629,7 +629,7 @@ export namespace Provider {
|
|||
family: model.family,
|
||||
api: {
|
||||
id: model.id,
|
||||
url: provider.api!,
|
||||
url: model.provider?.api ?? provider.api!,
|
||||
npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible",
|
||||
},
|
||||
status: model.status ?? "active",
|
||||
|
|
@ -781,7 +781,7 @@ export namespace Provider {
|
|||
existingModel?.api.npm ??
|
||||
modelsDev[providerID]?.npm ??
|
||||
"@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",
|
||||
name,
|
||||
|
|
|
|||
|
|
@ -401,6 +401,8 @@ export namespace ProviderTransform {
|
|||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/xai
|
||||
case "@ai-sdk/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":
|
||||
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,9 @@ export namespace SessionCompaction {
|
|||
|
||||
const reserved =
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export namespace LLM {
|
|||
tools: Record<string, Tool>
|
||||
retries?: number
|
||||
output?: "tool"
|
||||
toolChoice?: "auto" | "required" | "none"
|
||||
}
|
||||
|
||||
export type StreamOutput = StreamTextResult<ToolSet, unknown>
|
||||
|
|
@ -206,6 +207,7 @@ export namespace LLM {
|
|||
providerOptions: ProviderTransform.providerOptions(input.model, params.options),
|
||||
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
|
||||
tools,
|
||||
toolChoice: input.toolChoice,
|
||||
maxOutputTokens,
|
||||
abortSignal: input.abort,
|
||||
toolChoice: input.output === "tool" ? "required" : undefined,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,13 @@ import type { Provider } from "@/provider/provider"
|
|||
export namespace MessageV2 {
|
||||
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
|
||||
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(
|
||||
"ProviderAuthError",
|
||||
z.object({
|
||||
|
|
@ -39,6 +46,29 @@ export namespace MessageV2 {
|
|||
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({
|
||||
id: z.string(),
|
||||
sessionID: z.string(),
|
||||
|
|
@ -313,6 +343,7 @@ export namespace MessageV2 {
|
|||
time: z.object({
|
||||
created: z.number(),
|
||||
}),
|
||||
format: Format.optional(),
|
||||
summary: z
|
||||
.object({
|
||||
title: z.string().optional(),
|
||||
|
|
@ -365,6 +396,7 @@ export namespace MessageV2 {
|
|||
NamedError.Unknown.Schema,
|
||||
OutputLengthError.Schema,
|
||||
AbortedError.Schema,
|
||||
StructuredOutputError.Schema,
|
||||
ContextOverflowError.Schema,
|
||||
APIError.Schema,
|
||||
])
|
||||
|
|
@ -393,6 +425,7 @@ export namespace MessageV2 {
|
|||
write: z.number(),
|
||||
}),
|
||||
}),
|
||||
structured: z.any().optional(),
|
||||
variant: z.string().optional(),
|
||||
finish: z.string().optional(),
|
||||
}).meta({
|
||||
|
|
|
|||
|
|
@ -50,6 +50,16 @@ import { Truncate } from "@/tool/truncation"
|
|||
// @ts-ignore
|
||||
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 {
|
||||
const log = Log.create({ service: "session.prompt" })
|
||||
|
||||
|
|
@ -96,6 +106,7 @@ export namespace SessionPrompt {
|
|||
.describe(
|
||||
"@deprecated tools and permissions have been merged, you can set permissions on the session itself now",
|
||||
),
|
||||
format: MessageV2.Format.optional(),
|
||||
system: z.string().optional(),
|
||||
variant: z.string().optional(),
|
||||
parts: z.array(
|
||||
|
|
@ -276,6 +287,11 @@ export namespace SessionPrompt {
|
|||
|
||||
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
|
||||
const session = await Session.get(sessionID)
|
||||
while (true) {
|
||||
|
|
@ -589,6 +605,16 @@ export namespace SessionPrompt {
|
|||
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) {
|
||||
SessionSummary.summarize({
|
||||
sessionID: sessionID,
|
||||
|
|
@ -619,12 +645,19 @@ export namespace SessionPrompt {
|
|||
|
||||
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({
|
||||
user: lastUser,
|
||||
agent,
|
||||
abort,
|
||||
sessionID,
|
||||
system: [...(await SystemPrompt.environment(model)), ...(await InstructionPrompt.system())],
|
||||
system,
|
||||
messages: [
|
||||
...MessageV2.toModelMessages(sessionMessages, model),
|
||||
...(isLastStep
|
||||
|
|
@ -638,7 +671,33 @@ export namespace SessionPrompt {
|
|||
],
|
||||
tools,
|
||||
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 === "compact") {
|
||||
await SessionCompaction.create({
|
||||
|
|
@ -669,7 +728,8 @@ export namespace SessionPrompt {
|
|||
return Provider.defaultModel()
|
||||
}
|
||||
|
||||
async function resolveTools(input: {
|
||||
/** @internal Exported for testing */
|
||||
export async function resolveTools(input: {
|
||||
agent: Agent.Info
|
||||
model: Provider.Model
|
||||
session: Session.Info
|
||||
|
|
@ -849,6 +909,36 @@ export namespace SessionPrompt {
|
|||
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) {
|
||||
const agent = await Agent.get(input.agent ?? (await Agent.defaultAgent()))
|
||||
|
||||
|
|
@ -870,6 +960,7 @@ export namespace SessionPrompt {
|
|||
agent: agent.name,
|
||||
model,
|
||||
system: input.system,
|
||||
format: input.format,
|
||||
variant,
|
||||
}
|
||||
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) {
|
||||
limit = end - offset
|
||||
limit = end - (offset - 1)
|
||||
}
|
||||
}
|
||||
const args = { filePath: filepath, offset, limit }
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export const EditTool = Tool.define("edit", {
|
|||
}
|
||||
|
||||
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)
|
||||
|
|
@ -617,7 +617,7 @@ export function trimDiff(diff: string): string {
|
|||
|
||||
export function replace(content: string, oldString: string, newString: string, replaceAll = false): string {
|
||||
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
|
||||
|
|
@ -647,9 +647,9 @@ export function replace(content: string, oldString: string, newString: string, r
|
|||
}
|
||||
|
||||
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(
|
||||
"Found multiple matches for oldString. Provide more surrounding lines in oldString to identify the correct match.",
|
||||
)
|
||||
throw new Error("Found multiple matches for oldString. Provide more surrounding context to make the match unique.")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ Performs exact string replacements in files.
|
|||
|
||||
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.
|
||||
- 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.
|
||||
- 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 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.
|
||||
|
|
|
|||
|
|
@ -62,7 +62,9 @@ export const GlobTool = Tool.define("glob", {
|
|||
output.push(...files.map((f) => f.path))
|
||||
if (truncated) {
|
||||
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.)`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
for (const match of finalMatches) {
|
||||
|
|
@ -127,7 +128,9 @@ export const GrepTool = Tool.define("grep", {
|
|||
|
||||
if (truncated) {
|
||||
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) {
|
||||
|
|
@ -138,7 +141,7 @@ export const GrepTool = Tool.define("grep", {
|
|||
return {
|
||||
title: params.pattern,
|
||||
metadata: {
|
||||
matches: finalMatches.length,
|
||||
matches: totalMatches,
|
||||
truncated,
|
||||
},
|
||||
output: outputLines.join("\n"),
|
||||
|
|
|
|||
|
|
@ -17,19 +17,26 @@ const MAX_BYTES = 50 * 1024
|
|||
export const ReadTool = Tool.define("read", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The path to the file to read"),
|
||||
offset: z.coerce.number().describe("The line number to start reading from (0-based)").optional(),
|
||||
limit: z.coerce.number().describe("The number of lines to read (defaults to 2000)").optional(),
|
||||
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 (1-indexed)").optional(),
|
||||
limit: z.coerce.number().describe("The maximum number of lines to read (defaults to 2000)").optional(),
|
||||
}),
|
||||
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
|
||||
if (!path.isAbsolute(filepath)) {
|
||||
filepath = path.resolve(Instance.directory, filepath)
|
||||
}
|
||||
const title = path.relative(Instance.worktree, filepath)
|
||||
|
||||
const file = Bun.file(filepath)
|
||||
const stat = await file.stat().catch(() => undefined)
|
||||
|
||||
await assertExternalDirectory(ctx, filepath, {
|
||||
bypass: Boolean(ctx.extra?.["bypassCwdCheck"]),
|
||||
kind: stat?.isDirectory() ? "directory" : "file",
|
||||
})
|
||||
|
||||
await ctx.ask({
|
||||
|
|
@ -39,8 +46,7 @@ export const ReadTool = Tool.define("read", {
|
|||
metadata: {},
|
||||
})
|
||||
|
||||
const file = Bun.file(filepath)
|
||||
if (!(await file.exists())) {
|
||||
if (!stat) {
|
||||
const dir = path.dirname(filepath)
|
||||
const base = path.basename(filepath)
|
||||
|
||||
|
|
@ -60,6 +66,48 @@ export const ReadTool = Tool.define("read", {
|
|||
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)
|
||||
|
||||
// Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files)
|
||||
|
|
@ -75,7 +123,7 @@ export const ReadTool = Tool.define("read", {
|
|||
metadata: {
|
||||
preview: msg,
|
||||
truncated: false,
|
||||
...(instructions.length > 0 && { loaded: instructions.map((i) => i.filepath) }),
|
||||
loaded: instructions.map((i) => i.filepath),
|
||||
},
|
||||
attachments: [
|
||||
{
|
||||
|
|
@ -94,13 +142,15 @@ export const ReadTool = Tool.define("read", {
|
|||
if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`)
|
||||
|
||||
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"))
|
||||
if (start >= lines.length) throw new Error(`Offset ${offset} is out of range for this file (${lines.length} lines)`)
|
||||
|
||||
const raw: string[] = []
|
||||
let bytes = 0
|
||||
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 size = Buffer.byteLength(line, "utf-8") + (raw.length > 0 ? 1 : 0)
|
||||
if (bytes + size > MAX_BYTES) {
|
||||
|
|
@ -112,15 +162,15 @@ export const ReadTool = Tool.define("read", {
|
|||
}
|
||||
|
||||
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")
|
||||
|
||||
let output = "<file>\n"
|
||||
let output = [`<path>${filepath}</path>`, `<type>file</type>`, "<content>"].join("\n")
|
||||
output += content.join("\n")
|
||||
|
||||
const totalLines = lines.length
|
||||
const lastReadLine = offset + raw.length
|
||||
const lastReadLine = offset + raw.length - 1
|
||||
const hasMoreLines = totalLines > lastReadLine
|
||||
const truncated = hasMoreLines || truncatedByBytes
|
||||
|
||||
|
|
@ -131,7 +181,7 @@ export const ReadTool = Tool.define("read", {
|
|||
} else {
|
||||
output += `\n\n(End of file - total ${totalLines} lines)`
|
||||
}
|
||||
output += "\n</file>"
|
||||
output += "\n</content>"
|
||||
|
||||
// just warms the lsp client
|
||||
LSP.touchFile(filepath, false)
|
||||
|
|
@ -147,7 +197,7 @@ export const ReadTool = Tool.define("read", {
|
|||
metadata: {
|
||||
preview,
|
||||
truncated,
|
||||
...(instructions.length > 0 && { loaded: instructions.map((i) => i.filepath) }),
|
||||
loaded: instructions.map((i) => i.filepath),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
Reads a file from the local filesystem. You can access any file directly by using this tool.
|
||||
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.
|
||||
Read a file or directory from the local filesystem. If the path does not exist, an error is returned.
|
||||
|
||||
Usage:
|
||||
- The filePath parameter must be an absolute path, not a relative path
|
||||
- By default, it reads up to 2000 lines starting from the beginning 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
|
||||
- Any lines longer than 2000 characters will be truncated
|
||||
- Results are returned using cat -n format, with line numbers starting at 1
|
||||
- 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 read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
|
||||
- You can read image files using this tool.
|
||||
- The filePath parameter should be an absolute path.
|
||||
- By default, this tool returns up to 2000 lines from the start of the file.
|
||||
- The offset parameter is the line number to start from (1-indexed).
|
||||
- To read later sections, call this tool again with a larger offset.
|
||||
- Use the grep tool to find specific content in large files or files with long lines.
|
||||
- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.
|
||||
- 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.
|
||||
- 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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
})
|
||||
|
|
@ -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 () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
|
|
@ -232,7 +258,7 @@ describe("tool.read truncation", () => {
|
|||
test("respects offset parameter", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
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)
|
||||
},
|
||||
})
|
||||
|
|
@ -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 () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ import type {
|
|||
McpLocalConfig,
|
||||
McpRemoteConfig,
|
||||
McpStatusResponses,
|
||||
OutputFormat,
|
||||
Part as Part2,
|
||||
PartDeleteErrors,
|
||||
PartDeleteResponses,
|
||||
|
|
@ -1475,6 +1476,7 @@ export class Session extends HeyApiClient {
|
|||
tools?: {
|
||||
[key: string]: boolean
|
||||
}
|
||||
format?: OutputFormat
|
||||
system?: string
|
||||
variant?: string
|
||||
parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
|
||||
|
|
@ -1493,6 +1495,7 @@ export class Session extends HeyApiClient {
|
|||
{ in: "body", key: "agent" },
|
||||
{ in: "body", key: "noReply" },
|
||||
{ in: "body", key: "tools" },
|
||||
{ in: "body", key: "format" },
|
||||
{ in: "body", key: "system" },
|
||||
{ in: "body", key: "variant" },
|
||||
{ in: "body", key: "parts" },
|
||||
|
|
@ -1563,6 +1566,7 @@ export class Session extends HeyApiClient {
|
|||
tools?: {
|
||||
[key: string]: boolean
|
||||
}
|
||||
format?: OutputFormat
|
||||
system?: string
|
||||
variant?: string
|
||||
parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
|
||||
|
|
@ -1581,6 +1585,7 @@ export class Session extends HeyApiClient {
|
|||
{ in: "body", key: "agent" },
|
||||
{ in: "body", key: "noReply" },
|
||||
{ in: "body", key: "tools" },
|
||||
{ in: "body", key: "format" },
|
||||
{ in: "body", key: "system" },
|
||||
{ in: "body", key: "variant" },
|
||||
{ in: "body", key: "parts" },
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
file: string
|
||||
before: string
|
||||
|
|
@ -106,6 +122,7 @@ export type UserMessage = {
|
|||
time: {
|
||||
created: number
|
||||
}
|
||||
format?: OutputFormat
|
||||
summary?: {
|
||||
title?: string
|
||||
body?: string
|
||||
|
|
@ -152,6 +169,14 @@ export type MessageAbortedError = {
|
|||
}
|
||||
}
|
||||
|
||||
export type StructuredOutputError = {
|
||||
name: "StructuredOutputError"
|
||||
data: {
|
||||
message: string
|
||||
retries: number
|
||||
}
|
||||
}
|
||||
|
||||
export type ContextOverflowError = {
|
||||
name: "ContextOverflowError"
|
||||
data: {
|
||||
|
|
@ -189,6 +214,7 @@ export type AssistantMessage = {
|
|||
| UnknownError
|
||||
| MessageOutputLengthError
|
||||
| MessageAbortedError
|
||||
| StructuredOutputError
|
||||
| ContextOverflowError
|
||||
| ApiError
|
||||
parentID: string
|
||||
|
|
@ -212,6 +238,7 @@ export type AssistantMessage = {
|
|||
write: number
|
||||
}
|
||||
}
|
||||
structured?: unknown
|
||||
variant?: string
|
||||
finish?: string
|
||||
}
|
||||
|
|
@ -841,6 +868,7 @@ export type EventSessionError = {
|
|||
| UnknownError
|
||||
| MessageOutputLengthError
|
||||
| MessageAbortedError
|
||||
| StructuredOutputError
|
||||
| ContextOverflowError
|
||||
| ApiError
|
||||
}
|
||||
|
|
@ -1521,6 +1549,7 @@ export type ProviderConfig = {
|
|||
}
|
||||
provider?: {
|
||||
npm: string
|
||||
api: string
|
||||
}
|
||||
/**
|
||||
* Variant-specific configuration
|
||||
|
|
@ -3402,6 +3431,7 @@ export type SessionPromptData = {
|
|||
tools?: {
|
||||
[key: string]: boolean
|
||||
}
|
||||
format?: OutputFormat
|
||||
system?: string
|
||||
variant?: string
|
||||
parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
|
||||
|
|
@ -3589,6 +3619,7 @@ export type SessionPromptAsyncData = {
|
|||
tools?: {
|
||||
[key: string]: boolean
|
||||
}
|
||||
format?: OutputFormat
|
||||
system?: string
|
||||
variant?: string
|
||||
parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
|
||||
|
|
@ -4083,6 +4114,7 @@ export type ProviderListResponses = {
|
|||
}
|
||||
provider?: {
|
||||
npm: string
|
||||
api: string
|
||||
}
|
||||
variants?: {
|
||||
[key: string]: {
|
||||
|
|
|
|||
|
|
@ -2420,6 +2420,9 @@
|
|||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"format": {
|
||||
"$ref": "#/components/schemas/OutputFormat"
|
||||
},
|
||||
"system": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
@ -2796,6 +2799,9 @@
|
|||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"format": {
|
||||
"$ref": "#/components/schemas/OutputFormat"
|
||||
},
|
||||
"system": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
@ -3892,9 +3898,12 @@
|
|||
"properties": {
|
||||
"npm": {
|
||||
"type": "string"
|
||||
},
|
||||
"api": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["npm"]
|
||||
"required": ["npm", "api"]
|
||||
},
|
||||
"variants": {
|
||||
"type": "object",
|
||||
|
|
@ -6172,6 +6181,52 @@
|
|||
},
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -6219,6 +6274,9 @@
|
|||
},
|
||||
"required": ["created"]
|
||||
},
|
||||
"format": {
|
||||
"$ref": "#/components/schemas/OutputFormat"
|
||||
},
|
||||
"summary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -6344,6 +6402,28 @@
|
|||
},
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -6451,6 +6531,9 @@
|
|||
{
|
||||
"$ref": "#/components/schemas/MessageAbortedError"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/StructuredOutputError"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/ContextOverflowError"
|
||||
},
|
||||
|
|
@ -6522,6 +6605,7 @@
|
|||
},
|
||||
"required": ["input", "output", "reasoning", "cache"]
|
||||
},
|
||||
"structured": {},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
@ -8227,6 +8311,9 @@
|
|||
{
|
||||
"$ref": "#/components/schemas/MessageAbortedError"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/StructuredOutputError"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/ContextOverflowError"
|
||||
},
|
||||
|
|
@ -9416,9 +9503,12 @@
|
|||
"properties": {
|
||||
"npm": {
|
||||
"type": "string"
|
||||
},
|
||||
"api": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["npm"]
|
||||
"required": ["npm", "api"]
|
||||
},
|
||||
"variants": {
|
||||
"description": "Variant-specific configuration",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "1.1.59",
|
||||
"version": "1.1.60",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ description: هيّئ الوكلاء المتخصصين واستخدمهم.
|
|||
|
||||
---
|
||||
|
||||
### استخدام build
|
||||
### استخدام Build
|
||||
|
||||
_الوضع_: `primary`
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ Build هو الوكيل الأساسي **الافتراضي** مع تفعيل ج
|
|||
|
||||
---
|
||||
|
||||
### استخدام plan
|
||||
### استخدام Plan
|
||||
|
||||
_الوضع_: `primary`
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ _الوضع_: `primary`
|
|||
|
||||
---
|
||||
|
||||
### استخدام general
|
||||
### استخدام General
|
||||
|
||||
_الوضع_: `subagent`
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ _الوضع_: `subagent`
|
|||
|
||||
---
|
||||
|
||||
### استخدام explore
|
||||
### استخدام Explore
|
||||
|
||||
_الوضع_: `subagent`
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ _الوضع_: `subagent`
|
|||
|
||||
---
|
||||
|
||||
### استخدام compaction
|
||||
### استخدام Compaction
|
||||
|
||||
_الوضع_: `primary`
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ _الوضع_: `primary`
|
|||
|
||||
---
|
||||
|
||||
### استخدام title
|
||||
### استخدام Title
|
||||
|
||||
_الوضع_: `primary`
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ _الوضع_: `primary`
|
|||
|
||||
---
|
||||
|
||||
### استخدام summary
|
||||
### استخدام Summary
|
||||
|
||||
_الوضع_: `primary`
|
||||
|
||||
|
|
|
|||
|
|
@ -69,10 +69,10 @@ opencode attach [url]
|
|||
يتيح ذلك استخدام واجهة TUI مع واجهة خلفية لـ OpenCode تعمل عن بعد. على سبيل المثال:
|
||||
|
||||
```bash
|
||||
# Start the backend server for web/mobile access
|
||||
# ابدأ خادم الواجهة الخلفية للوصول عبر الويب/الجوال
|
||||
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
|
||||
```
|
||||
|
||||
|
|
@ -326,10 +326,10 @@ opencode run Explain the use of context in Go
|
|||
يمكنك أيضا الإرفاق بمثيل `opencode serve` قيد التشغيل لتجنّب زمن الإقلاع البارد لخوادم MCP في كل تشغيل:
|
||||
|
||||
```bash
|
||||
# Start a headless server in one terminal
|
||||
# ابدأ خادمًا بلا واجهة في محطة طرفية واحدة
|
||||
opencode serve
|
||||
|
||||
# In another terminal, run commands that attach to it
|
||||
# في محطة طرفية أخرى، شغّل الأوامر التي ترتبط به
|
||||
opencode run --attach http://localhost:4096 "Explain async/await in JavaScript"
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -54,11 +54,11 @@ Focus on the failing tests and suggest fixes.
|
|||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"command": {
|
||||
// This becomes the name of the command
|
||||
// يصبح هذا اسم الأمر
|
||||
"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.",
|
||||
// This is shown as the description in the TUI
|
||||
// يظهر هذا كوصف في TUI
|
||||
"description": "Run tests with coverage",
|
||||
"agent": "build",
|
||||
"model": "anthropic/claude-3-5-sonnet-20241022"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ description: استخدام ملف إعدادات OpenCode بصيغة JSON.
|
|||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
// Theme configuration
|
||||
// إعدادات السمة
|
||||
"theme": "opencode",
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"autoupdate": true,
|
||||
|
|
@ -326,7 +326,7 @@ opencode run "Hello world"
|
|||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"prompt": "You are a code reviewer. Focus on security, performance, and maintainability.",
|
||||
"tools": {
|
||||
// Disable file modification tools for review-only agent
|
||||
// تعطيل أدوات تعديل الملفات لوكيل المراجعة فقط
|
||||
"write": false,
|
||||
"edit": false,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ How is authentication handled in @packages/functions/src/api/index.ts
|
|||
|
||||
انتقل إليه باستخدام مفتاح **Tab**. سترى مؤشرا لذلك في الزاوية السفلية اليمنى.
|
||||
|
||||
```bash frame="none" title="Switch to Plan mode"
|
||||
```bash frame="none" title="التبديل إلى وضع Plan"
|
||||
<TAB>
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -278,10 +278,10 @@ opencode mcp logout my-oauth-server
|
|||
إذا فشل خادم MCP بعيد في المصادقة، يمكنك تشخيص المشكلة باستخدام:
|
||||
|
||||
```bash
|
||||
# View auth status for all OAuth-capable servers
|
||||
# عرض حالة المصادقة لجميع الخوادم القادرة على OAuth
|
||||
opencode mcp auth list
|
||||
|
||||
# Debug connection and OAuth flow for a specific server
|
||||
# تصحيح الاتصال وتدفق OAuth لخادم محدد
|
||||
opencode mcp debug my-oauth-server
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ description: إعداد الوكلاء والشهادات المخصصة.
|
|||
يتبع OpenCode متغيرات بيئة الوكيل القياسية.
|
||||
|
||||
```bash
|
||||
# HTTPS proxy (recommended)
|
||||
# وكيل HTTPS (موصى به)
|
||||
export HTTPS_PROXY=https://proxy.example.com:8080
|
||||
|
||||
# HTTP proxy (if HTTPS not available)
|
||||
# وكيل HTTP (إذا لم يتوفر HTTPS)
|
||||
export HTTP_PROXY=http://proxy.example.com:8080
|
||||
|
||||
# Bypass proxy for local server (required)
|
||||
# تجاوز الوكيل للخادم المحلي (مطلوب)
|
||||
export NO_PROXY=localhost,127.0.0.1
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ OpenCode Zen هي قائمة نماذج يوفّرها فريق OpenCode وقد
|
|||
فلا تتردد في فتح PR.
|
||||
|
||||
:::note
|
||||
ألا ترى مزوّدا هنا؟ أرسل PR.
|
||||
لم تجد المزوّد الذي تبحث عنه؟ أرسل PR.
|
||||
:::
|
||||
|
||||
---
|
||||
|
|
@ -139,13 +139,13 @@ OpenCode Zen هي قائمة نماذج يوفّرها فريق OpenCode وقد
|
|||
عيّن أحد متغيرات البيئة التالية أثناء تشغيل opencode:
|
||||
|
||||
```bash
|
||||
# Option 1: Using AWS access keys
|
||||
# الخيار 1: استخدام مفاتيح وصول AWS
|
||||
AWS_ACCESS_KEY_ID=XXX AWS_SECRET_ACCESS_KEY=YYY opencode
|
||||
|
||||
# Option 2: Using named AWS profile
|
||||
# الخيار 2: استخدام ملف تعريف AWS مسمّى
|
||||
AWS_PROFILE=my-profile opencode
|
||||
|
||||
# Option 3: Using Bedrock bearer token
|
||||
# الخيار 3: استخدام رمز Bedrock المميز (bearer token)
|
||||
AWS_BEARER_TOKEN_BEDROCK=XXX opencode
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -366,7 +366,7 @@ OPENCODE_ENABLE_EXA=1 opencode
|
|||
|
||||
---
|
||||
|
||||
### Ignore patterns
|
||||
### أنماط التجاهل
|
||||
|
||||
لتضمين ملفات يتم تجاهلها عادة، أنشئ ملف `.ignore` في جذر المشروع. يمكن لهذا الملف السماح صراحة بمسارات محددة.
|
||||
|
||||
|
|
|
|||
|
|
@ -290,12 +290,12 @@ How is auth handled in @packages/functions/src/api/index.ts?
|
|||
<Tabs>
|
||||
<TabItem label="Linux/macOS">
|
||||
```bash
|
||||
# Example for nano or vim
|
||||
# مثال لـ nano أو vim
|
||||
export EDITOR=nano
|
||||
export EDITOR=vim
|
||||
|
||||
# For GUI editors, VS Code, Cursor, VSCodium, Windsurf, Zed, etc.
|
||||
# include --wait
|
||||
# للمحررات الرسومية، VS Code، Cursor، VSCodium، Windsurf، Zed، إلخ.
|
||||
# قم بتضمين --wait
|
||||
export EDITOR="code --wait"
|
||||
```
|
||||
|
||||
|
|
@ -308,8 +308,8 @@ How is auth handled in @packages/functions/src/api/index.ts?
|
|||
```bash
|
||||
set EDITOR=notepad
|
||||
|
||||
# For GUI editors, VS Code, Cursor, VSCodium, Windsurf, Zed, etc.
|
||||
# include --wait
|
||||
# للمحررات الرسومية، VS Code، Cursor، VSCodium، Windsurf، Zed، إلخ.
|
||||
# قم بتضمين --wait
|
||||
set EDITOR=code --wait
|
||||
```
|
||||
|
||||
|
|
@ -321,8 +321,8 @@ How is auth handled in @packages/functions/src/api/index.ts?
|
|||
```powershell
|
||||
$env:EDITOR = "notepad"
|
||||
|
||||
# For GUI editors, VS Code, Cursor, VSCodium, Windsurf, Zed, etc.
|
||||
# include --wait
|
||||
# للمحررات الرسومية، VS Code، Cursor، VSCodium، Windsurf، Zed، إلخ.
|
||||
# قم بتضمين --wait
|
||||
$env:EDITOR = "code --wait"
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -113,10 +113,10 @@ OPENCODE_SERVER_PASSWORD=secret opencode web
|
|||
يمكنك إرفاق واجهة terminal (TUI) بخادم ويب قيد التشغيل:
|
||||
|
||||
```bash
|
||||
# Start the web server
|
||||
# ابدأ خادم الويب
|
||||
opencode web --port 4096
|
||||
|
||||
# In another terminal, attach the TUI
|
||||
# في محطة طرفية أخرى، اربط TUI
|
||||
opencode attach http://localhost:4096
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
## Podrška
|
||||
## Podržane funkcije
|
||||
|
||||
OpenCode radi isto kroz ACP kao i u terminalu. Podržane su sve funkcije:
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ Možete se prebacivati između agenata tokom sesije ili ih pozvati spominj
|
|||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
### 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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
## 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`.
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -233,7 +233,7 @@ Ovo je **obavezna** opcija konfiguracije.
|
|||
|
||||
---
|
||||
|
||||
### Temperature
|
||||
### Temperatura
|
||||
|
||||
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.
|
||||
|
||||
|
|
@ -306,7 +306,7 @@ Naslijeđeno polje `maxSteps` je zastarjelo. Umjesto toga koristite `steps`.
|
|||
|
||||
---
|
||||
|
||||
### Disable
|
||||
### Onemogućavanje
|
||||
|
||||
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.
|
||||
|
||||
|
|
@ -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`.
|
||||
|
||||
|
|
@ -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:
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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:
|
||||
|
||||
|
|
@ -679,7 +679,7 @@ Ova interaktivna komanda će:
|
|||
|
||||
---
|
||||
|
||||
## Slučajevi upotrebe
|
||||
## Primjeri upotrebe
|
||||
|
||||
Evo nekoliko uobičajenih slučajeva upotrebe različitih agenata.
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ OpenCode CLI po defaultu pokreće [TUI](/docs/tui) kada se pokrene bez ikakvih a
|
|||
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
|
||||
opencode run "Explain how closures work in JavaScript"
|
||||
|
|
@ -21,36 +21,36 @@ opencode run "Explain how closures work in JavaScript"
|
|||
|
||||
### tui
|
||||
|
||||
Pokrenite korisnički interfejs OpenCode terminala.
|
||||
Pokrenite OpenCode terminalski korisnički interfejs.
|
||||
|
||||
```bash
|
||||
opencode [project]
|
||||
```
|
||||
|
||||
#### Zastave
|
||||
#### Opcije
|
||||
|
||||
| Zastava | Kratko | Opis |
|
||||
| Opcija | Kratko | Opis |
|
||||
| ------------ | ------ | ------------------------------------------------------------------------ |
|
||||
| `--continue` | `-c` | Nastavite posljednju sesiju |
|
||||
| `--session` | `-s` | ID sesije za nastavak |
|
||||
| `--fork` | | Forkujte sesiju pri nastavku (koristiti sa `--continue` ili `--session`) |
|
||||
| `--prompt` | | Uputstvo za upotrebu |
|
||||
| `--model` | `-m` | Model za korištenje u obliku dobavljača/modela |
|
||||
| `--prompt` | | Prompt za upotrebu |
|
||||
| `--model` | `-m` | Model za korištenje u obliku provider/model |
|
||||
| `--agent` | | Agent za korištenje |
|
||||
| `--port` | | Port za slušanje na |
|
||||
| `--hostname` | | Slušajte ime hosta |
|
||||
| `--port` | | Port na kojem treba slušati |
|
||||
| `--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
|
||||
|
||||
Upravljajte agentima za OpenCode.
|
||||
Upravljajte OpenCode agentima.
|
||||
|
||||
```bash
|
||||
opencode agent [command]
|
||||
|
|
@ -60,13 +60,13 @@ opencode agent [command]
|
|||
|
||||
### 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
|
||||
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
|
||||
# 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
|
||||
```
|
||||
|
||||
#### 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 |
|
||||
|
||||
---
|
||||
|
|
@ -93,7 +93,7 @@ Kreirajte novog agenta s prilagođenom konfiguracijom.
|
|||
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
|
||||
|
||||
Navodi sve autentifikovane dobavljače pohranjene u datoteci akreditiva.
|
||||
Navodi sve autentifikovane dobavljače pohranjene u datoteci vjerodajnica.
|
||||
|
||||
```bash
|
||||
opencode auth list
|
||||
|
|
@ -157,7 +157,7 @@ opencode auth logout
|
|||
|
||||
### github
|
||||
|
||||
Upravljajte GitHub agentom za automatizaciju spremišta.
|
||||
Upravljajte GitHub agentom za automatizaciju repozitorija.
|
||||
|
||||
```bash
|
||||
opencode github [command]
|
||||
|
|
@ -167,7 +167,7 @@ opencode github [command]
|
|||
|
||||
#### install
|
||||
|
||||
Instalirajte GitHub agenta u svoje spremište.
|
||||
Instalirajte GitHub agenta u svoj repozitorij.
|
||||
|
||||
```bash
|
||||
opencode github install
|
||||
|
|
@ -179,24 +179,24 @@ Ovo postavlja neophodni tok rada GitHub Actions i vodi vas kroz proces konfigura
|
|||
|
||||
#### run
|
||||
|
||||
Pokrenite GitHub agent. Ovo se obično koristi u GitHub akcijama.
|
||||
Pokrenite GitHub agent. Ovo se obično koristi u GitHub Actions.
|
||||
|
||||
```bash
|
||||
opencode github run
|
||||
```
|
||||
|
||||
##### Zastave
|
||||
##### Opcije
|
||||
|
||||
| Zastava | Opis |
|
||||
| --------- | -------------------------------------------- |
|
||||
| `--event` | GitHub lažni događaj za pokretanje agenta za |
|
||||
| `--token` | GitHub token ličnog pristupa |
|
||||
| Opcija | Opis |
|
||||
| --------- | -------------------------------------- |
|
||||
| `--event` | GitHub mock event za pokretanje agenta |
|
||||
| `--token` | GitHub Personal Access Token |
|
||||
|
||||
---
|
||||
|
||||
### mcp
|
||||
|
||||
Upravljajte serverima protokola konteksta modela.
|
||||
Upravljajte Model Context Protocol (MCP) serverima.
|
||||
|
||||
```bash
|
||||
opencode mcp [command]
|
||||
|
|
@ -212,7 +212,7 @@ Dodajte MCP server svojoj konfiguraciji.
|
|||
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.
|
||||
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
|
||||
opencode mcp auth list
|
||||
|
|
@ -267,7 +267,7 @@ opencode mcp logout [name]
|
|||
|
||||
#### debug
|
||||
|
||||
Otklanjanje grešaka OAuth veze sa MCP serverom.
|
||||
Otklanjanje grešaka (debug) OAuth veze sa MCP serverom.
|
||||
|
||||
```bash
|
||||
opencode mcp debug <name>
|
||||
|
|
@ -291,11 +291,11 @@ Opciono možete proslijediti ID provajdera za filtriranje modela po tom dobavlja
|
|||
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) |
|
||||
|
||||
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
|
||||
|
||||
Pokrenite opencode u neinteraktivnom modu tako što ćete direktno proslijediti prompt.
|
||||
Pokrenite OpenCode u neinteraktivnom modu tako što ćete direktno proslijediti prompt.
|
||||
|
||||
```bash
|
||||
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"
|
||||
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"
|
||||
```
|
||||
|
||||
#### 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 |
|
||||
| `--session` | `-s` | ID sesije za nastavak |
|
||||
| `--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 |
|
||||
| `--format` | | Format: default (formatiran) ili json (sirovi JSON događaji) |
|
||||
| `--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) |
|
||||
|
||||
---
|
||||
|
||||
### 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
|
||||
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 |
|
||||
| ------------ | ---------------------------------------------------- |
|
||||
| `--port` | Port za slušanje na |
|
||||
| `--hostname` | Ime hosta za slušanje |
|
||||
| `--mdns` | Omogući mDNS otkrivanje |
|
||||
| `--cors` | Dodatni izvor(a) pretraživača koji dozvoljavaju CORS |
|
||||
| Opcija | Opis |
|
||||
| ------------ | ----------------------------------------------------- |
|
||||
| `--port` | Port na kojem treba slušati |
|
||||
| `--hostname` | Hostname na kojem treba slušati |
|
||||
| `--mdns` | Omogući mDNS otkrivanje |
|
||||
| `--cors` | Dodatni origin(i) pretraživača koji dozvoljavaju CORS |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -388,12 +388,12 @@ Navedite sve OpenCode sesije.
|
|||
opencode session list
|
||||
```
|
||||
|
||||
##### Zastave
|
||||
##### Opcije
|
||||
|
||||
| Zastava | Kratko | Opis |
|
||||
| ------------- | ------ | ----------------------------------------- |
|
||||
| `--max-count` | `-n` | Ograničenje na N najnovijih sesija |
|
||||
| `--format` | | Izlazni format: tablica ili json (tabela) |
|
||||
| Opcija | Kratko | Opis |
|
||||
| ------------- | ------ | -------------------------------------- |
|
||||
| `--max-count` | `-n` | Ograničenje na N najnovijih sesija |
|
||||
| `--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
|
||||
```
|
||||
|
||||
#### Zastave
|
||||
#### Opcije
|
||||
|
||||
| Zastava | Opis |
|
||||
| ----------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `--days` | Prikaži statistiku za zadnjih N dana (sva vremena) |
|
||||
| `--tools` | Broj alata za prikaz (svi) |
|
||||
| `--models` | Prikaži raščlambu korištenja modela (skriveno prema zadanim postavkama). Proslijedite broj za prikaz vrha N |
|
||||
| `--project` | Filtriraj po projektu (svi projekti, prazan niz: trenutni projekt) |
|
||||
| Opcija | Opis |
|
||||
| ----------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `--days` | Prikaži statistiku za zadnjih N dana (sva vremena) |
|
||||
| `--tools` | Broj alata za prikaz (svi) |
|
||||
| `--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) |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -447,22 +447,22 @@ opencode import https://opncd.ai/s/abc123
|
|||
|
||||
### web
|
||||
|
||||
Pokrenite OpenCode server bez glave sa web interfejsom.
|
||||
Pokrenite OpenCode headless server sa web interfejsom.
|
||||
|
||||
```bash
|
||||
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 |
|
||||
| ------------ | ---------------------------------------------------- |
|
||||
| `--port` | Port za slušanje na |
|
||||
| `--hostname` | Ime hosta za slušanje |
|
||||
| `--mdns` | Omogući mDNS otkrivanje |
|
||||
| `--cors` | Dodatni izvor(a) pretraživača koji dozvoljavaju CORS |
|
||||
| Opcija | Opis |
|
||||
| ------------ | ----------------------------------------------------- |
|
||||
| `--port` | Port na kojem treba slušati |
|
||||
| `--hostname` | Hostname na kojem treba slušati |
|
||||
| `--mdns` | Omogući mDNS otkrivanje |
|
||||
| `--cors` | Dodatni origin(i) pretraživača koji dozvoljavaju CORS |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -474,15 +474,15 @@ Pokrenite ACP (Agent Client Protocol) server.
|
|||
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 |
|
||||
| ------------ | ------------------- |
|
||||
| `--cwd` | Radni imenik |
|
||||
| `--port` | Port za slušanje na |
|
||||
| `--hostname` | Slušajte ime hosta |
|
||||
| Opcija | Opis |
|
||||
| ------------ | --------------------------- |
|
||||
| `--cwd` | Radni direktorij |
|
||||
| `--port` | Port na kojem treba slušati |
|
||||
| `--hostname` | Hostname na kojem slušati |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -494,12 +494,12 @@ Deinstalirajte OpenCode i uklonite sve povezane datoteke.
|
|||
opencode uninstall
|
||||
```
|
||||
|
||||
#### Zastave
|
||||
#### Opcije
|
||||
|
||||
| Zastava | Kratko | Opis |
|
||||
| Opcija | Kratko | Opis |
|
||||
| --------------- | ------ | --------------------------------------------- |
|
||||
| `--keep-config` | `-c` | Čuvajte konfiguracijske datoteke |
|
||||
| `--keep-data` | `-d` | Čuvajte podatke i snimke sesije |
|
||||
| `--keep-config` | `-c` | Sačuvajte konfiguracijske datoteke |
|
||||
| `--keep-data` | `-d` | Sačuvajte podatke i snimke sesije |
|
||||
| `--dry-run` | | Pokažite šta bi bilo uklonjeno bez uklanjanja |
|
||||
| `--force` | `-f` | Preskoči upite za potvrdu |
|
||||
|
||||
|
|
@ -507,7 +507,7 @@ opencode uninstall
|
|||
|
||||
### upgrade
|
||||
|
||||
Ažurira opencode na najnoviju verziju ili određenu verziju.
|
||||
Ažurira OpenCode na najnoviju verziju ili određenu verziju.
|
||||
|
||||
```bash
|
||||
opencode upgrade [target]
|
||||
|
|
@ -525,73 +525,76 @@ Za nadogradnju na određenu verziju.
|
|||
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 |
|
||||
|
||||
---
|
||||
|
||||
## Globalne zastave
|
||||
## Globalne opcije
|
||||
|
||||
CLI otvorenog koda uzima sljedeće globalne zastavice.
|
||||
| Zastava | Kratko | Opis
|
||||
|-------------- | ----- | ------------------------------------ |
|
||||
| `--help` | `-h` | Prikaži pomoć |
|
||||
| `--version` | `-v` | Odštampaj broj verzije |
|
||||
| `--print-logs` | | Ispis zapisnika u stderr |
|
||||
| `--log-level` | | Nivo dnevnika (DEBUG, INFO, WARN, ERROR) |
|
||||
OpenCode CLI prihvata sljedeće globalne zastavice.
|
||||
|
||||
| Opcija | Kratko | Opis |
|
||||
| -------------- | ------ | ----------------------------------------- |
|
||||
| `--help` | `-h` | Prikaži pomoć |
|
||||
| `--version` | `-v` | Ispiši broj verzije |
|
||||
| `--print-logs` | | Ispis logova u stderr |
|
||||
| `--log-level` | | Nivo logovanja (DEBUG, INFO, WARN, ERROR) |
|
||||
|
||||
---
|
||||
|
||||
## Varijable okruženja
|
||||
|
||||
OpenCode se može konfigurirati pomoću varijabli okruženja.
|
||||
| Varijabilna | Vrsta | Opis
|
||||
|------------------------------------- | ------- | ------------------------------------------------- |
|
||||
| `OPENCODE_AUTO_SHARE` | boolean | Automatski dijeli sesije |
|
||||
| `OPENCODE_GIT_BASH_PATH` | string | Putanja do Git Bash izvršne datoteke na Windows |
|
||||
| `OPENCODE_CONFIG` | string | Put do konfiguracionog fajla |
|
||||
| `OPENCODE_CONFIG_DIR` | string | Put do konfiguracijskog direktorija |
|
||||
| `OPENCODE_CONFIG_CONTENT` | string | Inline json konfiguracijski sadržaj |
|
||||
| `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Onemogući automatske provjere ažuriranja |
|
||||
| `OPENCODE_DISABLE_PRUNE` | boolean | Onemogući orezivanje starih podataka |
|
||||
| `OPENCODE_DISABLE_TERMINAL_TITLE` | boolean | Onemogući automatsko ažuriranje naslova terminala |
|
||||
| `OPENCODE_PERMISSION` | string | Umetnuta json konfiguracija dozvola |
|
||||
| `OPENCODE_DISABLE_DEFAULT_PLUGINS` | boolean | Onemogući podrazumevane dodatke |
|
||||
| `OPENCODE_DISABLE_LSP_DOWNLOAD` | boolean | Onemogući automatsko preuzimanje LSP servera |
|
||||
| `OPENCODE_ENABLE_EXPERIMENTAL_MODELS` | boolean | Omogući eksperimentalne modele |
|
||||
| `OPENCODE_DISABLE_AUTOCOMPACT` | boolean | Onemogući automatsko sažimanje konteksta |
|
||||
| `OPENCODE_DISABLE_CLAUDE_CODE` | boolean | Onemogući čitanje sa `.claude` (prompt + vještine) |
|
||||
| `OPENCODE_DISABLE_CLAUDE_CODE_PROMPT` | boolean | Onemogući čitanje `~/.claude/CLAUDE.md` |
|
||||
| `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | Onemogući učitavanje `.claude/skills` |
|
||||
| `OPENCODE_DISABLE_MODELS_FETCH` | boolean | Onemogući dohvaćanje modela iz udaljenih izvora |
|
||||
| `OPENCODE_FAKE_VCS` | string | Lažni VCS provajder za potrebe testiranja |
|
||||
| `OPENCODE_DISABLE_FILETIME_CHECK` | boolean | Onemogući provjeru vremena datoteke radi optimizacije |
|
||||
| `OPENCODE_CLIENT` | string | Identifikator klijenta (zadano na `cli`) |
|
||||
| `OPENCODE_ENABLE_EXA` | boolean | Omogući Exa alate za web pretraživanje |
|
||||
| `OPENCODE_SERVER_PASSWORD` | string | Omogući osnovnu autorizaciju za `serve`/`web` |
|
||||
| `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 |
|
||||
|
||||
| Varijabla | Tip | Opis |
|
||||
| ------------------------------------- | ------- | ------------------------------------------------------------------ |
|
||||
| `OPENCODE_AUTO_SHARE` | boolean | Automatski dijeli sesije |
|
||||
| `OPENCODE_GIT_BASH_PATH` | string | Putanja do Git Bash izvršne datoteke na Windows-u |
|
||||
| `OPENCODE_CONFIG` | string | Putanja do konfiguracijskog fajla |
|
||||
| `OPENCODE_CONFIG_DIR` | string | Putanja do konfiguracijskog direktorija |
|
||||
| `OPENCODE_CONFIG_CONTENT` | string | Inline json konfiguracijski sadržaj |
|
||||
| `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Onemogući automatske provjere ažuriranja |
|
||||
| `OPENCODE_DISABLE_PRUNE` | boolean | Onemogući brisanje (pruning) starih podataka |
|
||||
| `OPENCODE_DISABLE_TERMINAL_TITLE` | boolean | Onemogući automatsko ažuriranje naslova terminala |
|
||||
| `OPENCODE_PERMISSION` | string | Inline json konfiguracija dozvola |
|
||||
| `OPENCODE_DISABLE_DEFAULT_PLUGINS` | boolean | Onemogući podrazumijevane dodatke (plugins) |
|
||||
| `OPENCODE_DISABLE_LSP_DOWNLOAD` | boolean | Onemogući automatsko preuzimanje LSP servera |
|
||||
| `OPENCODE_ENABLE_EXPERIMENTAL_MODELS` | boolean | Omogući eksperimentalne modele |
|
||||
| `OPENCODE_DISABLE_AUTOCOMPACT` | boolean | Onemogući automatsko sažimanje konteksta |
|
||||
| `OPENCODE_DISABLE_CLAUDE_CODE` | boolean | Onemogući čitanje iz `.claude` (prompt + vještine) |
|
||||
| `OPENCODE_DISABLE_CLAUDE_CODE_PROMPT` | boolean | Onemogući čitanje `~/.claude/CLAUDE.md` |
|
||||
| `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | Onemogući učitavanje `.claude/skills` |
|
||||
| `OPENCODE_DISABLE_MODELS_FETCH` | boolean | Onemogući dohvaćanje modela iz udaljenih izvora |
|
||||
| `OPENCODE_FAKE_VCS` | string | Lažni VCS provajder za potrebe testiranja |
|
||||
| `OPENCODE_DISABLE_FILETIME_CHECK` | boolean | Onemogući provjeru vremena datoteke radi optimizacije |
|
||||
| `OPENCODE_CLIENT` | string | Identifikator klijenta (zadano na `cli`) |
|
||||
| `OPENCODE_ENABLE_EXA` | boolean | Omogući Exa alate za web pretraživanje |
|
||||
| `OPENCODE_SERVER_PASSWORD` | string | Omogući osnovnu autentifikaciju za `serve`/`web` |
|
||||
| `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
|
||||
|
||||
Ove varijable okruženja omogućavaju eksperimentalne karakteristike koje se mogu promijeniti ili ukloniti.
|
||||
| Varijabilna | Vrsta | Opis
|
||||
|----------------------------------------------- | ------- | --------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | Omogući sve eksperimentalne funkcije |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Omogući otkrivanje ikona |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Onemogući kopiranje pri odabiru u TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | broj | Zadano vremensko ograničenje za bash komande u ms |
|
||||
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | broj | Maksimalni izlazni tokeni za LLM odgovore |
|
||||
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Omogući praćenje datoteka za cijeli dir |
|
||||
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Omogući oxfmt formatter |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Omogući eksperimentalni LSP alat |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Onemogući praćenje datoteka |
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Omogući eksperimentalne Exa funkcije |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Omogući eksperimentalnu provjeru tipa LSP |
|
||||
| `OPENCODE_EXPERIMENTAL_MARKDOWN` | boolean | Omogući eksperimentalne Markdown funkcije |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Omogući režim plana |
|
||||
|
||||
| Varijabla | Tip | Opis |
|
||||
| ----------------------------------------------- | ------- | ------------------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | Omogući sve eksperimentalne funkcije |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Omogući otkrivanje ikona |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Onemogući kopiranje pri odabiru u TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Zadano vremensko ograničenje za bash naredbe u ms |
|
||||
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Maksimalni izlazni tokeni za LLM odgovore |
|
||||
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Omogući praćenje datoteka za cijeli direktorij |
|
||||
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Omogući oxfmt formatter |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Omogući eksperimentalni LSP alat |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Onemogući praćenje datoteka |
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Omogući eksperimentalne Exa funkcije |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Omogući eksperimentalnu provjeru tipa LSP |
|
||||
| `OPENCODE_EXPERIMENTAL_MARKDOWN` | boolean | Omogući eksperimentalne Markdown funkcije |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Omogući Plan mod |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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/`.
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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.
|
||||
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
|
||||
Prilagođene komande mogu nadjačati ugrađene komande.
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: Config
|
||||
title: Konfiguracija
|
||||
description: Korištenje OpenCode JSON konfiguracije.
|
||||
---
|
||||
|
||||
|
|
@ -25,10 +25,10 @@ OpenCode podržava i **JSON** i **JSONC** (JSON sa komentarima) formate.
|
|||
|
||||
## Lokacije
|
||||
|
||||
Možete postaviti svoju konfiguraciju na nekoliko različitih lokacija i one imaju a
|
||||
drugačiji redosled prioriteta.
|
||||
Možete postaviti svoju konfiguraciju na nekoliko različitih lokacija i one imaju drugačiji redoslijed prioriteta.
|
||||
|
||||
:::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.
|
||||
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):
|
||||
|
||||
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
|
||||
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
|
||||
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.
|
||||
:::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.
|
||||
|
|
@ -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.
|
||||
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.
|
||||
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.
|
||||
:::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`.
|
||||
|
||||
|
|
@ -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`
|
||||
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.
|
||||
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.
|
||||
|
||||
```bash
|
||||
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).
|
||||
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_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.
|
||||
[Ovdje saznajte više o korištenju TUI](/docs/tui).
|
||||
[Saznajte više o korištenju TUI](/docs/tui) ovdje.
|
||||
|
||||
---
|
||||
|
||||
### 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"
|
||||
{
|
||||
|
|
@ -186,15 +184,15 @@ Možete konfigurirati postavke servera za komande `opencode serve` i `opencode w
|
|||
Dostupne opcije:
|
||||
|
||||
- `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`.
|
||||
- `mdns` - Omogući otkrivanje mDNS usluge. 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.
|
||||
- `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`.
|
||||
- `hostname` - Hostname za slušanje. Kada je `mdns` omogućen i nije postavljeno ime hosta, podrazumevano je `0.0.0.0`.
|
||||
- `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 servis. Zadano je `opencode.local`. Korisno za pokretanje više instanci na istoj mreži.
|
||||
- `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.
|
||||
|
||||
---
|
||||
|
||||
### Tools
|
||||
### Alati
|
||||
|
||||
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.
|
||||
- `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`)
|
||||
- `profile` - AWS imenovan profil od `~/.aws/credentials` (podrazumevano 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.
|
||||
- `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 alias za generičku opciju `baseURL` koristeći terminologiju specifičnu za AWS. Ako su oba navedena, `endpoint` ima prednost.
|
||||
:::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.
|
||||
:::
|
||||
|
|
@ -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`.
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
|
@ -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
|
||||
- `"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`.
|
||||
|
||||
---
|
||||
|
||||
### 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"
|
||||
{
|
||||
|
|
@ -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"
|
||||
{
|
||||
|
|
@ -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`.
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
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:
|
||||
|
||||
```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`.
|
||||
|
||||
|
|
@ -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`.
|
||||
|
||||
|
|
@ -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.
|
||||
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
|
||||
o pravilima ovdje](/docs/rules).
|
||||
Ovo uzima niz putanja i glob uzoraka do datoteka instrukcija. [Saznajte više 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:
|
||||
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
|
@ -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
|
||||
`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:
|
||||
|
||||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: Custom Tools
|
||||
title: Prilagođeni alati
|
||||
description: Kreirajte alate koje LLM može pozvati u otvorenom kodu.
|
||||
---
|
||||
|
||||
|
|
@ -133,7 +133,7 @@ Koristite `context.worktree` za korijen git radnog stabla.
|
|||
|
||||
## 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.
|
||||
Prvo kreirajte alat kao Python skriptu:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: Enterprise
|
||||
title: Za preduzeća
|
||||
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.
|
||||
|
||||
|
|
@ -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>**.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
### Central Config
|
||||
### Centralna konfiguracija
|
||||
|
||||
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.
|
||||
|
||||
|
|
@ -104,17 +104,17 @@ Ovo je trenutno na našoj mapi puta. Ako ste zainteresovani, **<a href={email}>j
|
|||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
## Često postavljana pitanja
|
||||
|
||||
<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.
|
||||
|
||||
</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.
|
||||
|
||||
|
|
@ -123,21 +123,21 @@ Zatim **<a href={email}>kontaktirajte nas</a>** da razgovaramo o cijenama i opci
|
|||
</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.
|
||||
|
||||
</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.
|
||||
|
||||
</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.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: Formatters
|
||||
title: Formateri
|
||||
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.
|
||||
| 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
- **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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
- **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.
|
||||
- **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>
|
||||
|
||||
<summary>Flow configuration</summary>
|
||||
<summary>Konfiguracija toka</summary>
|
||||
|
||||
```yaml
|
||||
image: node:22-slim
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
- **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.
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
---
|
||||
title: Intro
|
||||
description: Započnite s OpenCode.
|
||||
title: Uvod
|
||||
description: Započnite sa OpenCode.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem } from "@astrojs/starlight/components"
|
||||
import config from "../../../../config.mjs"
|
||||
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.
|
||||

|
||||
Hajde da počnemo.
|
||||
|
||||
|
|
@ -18,17 +18,17 @@ Hajde da počnemo.
|
|||
Da biste koristili OpenCode u svom terminalu, trebat će vam:
|
||||
|
||||
1. Moderan emulator terminala kao što su:
|
||||
- [WezTerm](https://wezterm.org), multi-platforma
|
||||
- [Alacritty](https://alacritty.org), više platforma
|
||||
- [WezTerm](https://wezterm.org), više platformi
|
||||
- [Alacritty](https://alacritty.org), više platformi
|
||||
- [Ghostty](https://ghostty.org), Linux i macOS
|
||||
- [Kitty](https://sw.kovidgoyal.net/kitty/), Linux i macOS
|
||||
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
|
||||
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:
|
||||
|
||||
- **Korišćenje Node.js**
|
||||
- **Korištenje Node.js**
|
||||
|
||||
<Tabs>
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ Također ga možete instalirati pomoću sljedećih naredbi:
|
|||
|
||||
</Tabs>
|
||||
|
||||
- **Korišćenje Homebrew-a na macOS-u i Linux-u**
|
||||
- **Korištenje Homebrew-a na macOS-u i Linux-u**
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
- **Korišćenje Parua na Arch Linuxu**
|
||||
- **Korištenje Parua na Arch Linuxu**
|
||||
|
||||
```bash
|
||||
paru -S opencode-bin
|
||||
|
|
@ -87,16 +87,16 @@ Također ga možete instalirati pomoću sljedećih naredbi:
|
|||
#### Windows
|
||||
|
||||
:::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
|
||||
choco install opencode
|
||||
```
|
||||
|
||||
- **Upotreba Scoop-a**
|
||||
- **Korištenje Scoop-a**
|
||||
|
||||
```bash
|
||||
scoop install opencode
|
||||
|
|
@ -108,13 +108,13 @@ Za najbolje iskustvo na Windows-u preporučujemo korištenje [Windows Subsystem
|
|||
npm install -g opencode-ai
|
||||
```
|
||||
|
||||
- **Korišćenje Mise**
|
||||
- **Korištenje Mise**
|
||||
|
||||
```bash
|
||||
mise use -g github:anomalyco/opencode
|
||||
```
|
||||
|
||||
- **Korišćenje Dockera**
|
||||
- **Korištenje Dockera**
|
||||
|
||||
```bash
|
||||
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.
|
||||
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
|
||||
tim.
|
||||
To je kurirana lista modela koji su testirani i verifikovani od strane OpenCode tima.
|
||||
|
||||
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
|
||||
na čemu želite da radite.
|
||||
Sada kada ste konfigurisali provajdera, možete se kretati do projekta na kojem želite raditi.
|
||||
|
||||
```bash
|
||||
cd /path/to/project
|
||||
|
|
@ -173,26 +171,22 @@ Zatim inicijalizirajte OpenCode za projekat pokretanjem sljedeće naredbe.
|
|||
/init
|
||||
```
|
||||
|
||||
Ovo će omogućiti OpenCode da analizira vaš projekat i kreira `AGENTS.md` fajl u njemu
|
||||
korijen projekta.
|
||||
Ovo će omogućiti OpenCode da analizira vaš projekat i kreira `AGENTS.md` fajl u korijenu projekta.
|
||||
:::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
|
||||
korišteno.
|
||||
Ovo pomaže OpenCode da razumije strukturu projekta i obrasce kodiranja koji se koriste.
|
||||
|
||||
---
|
||||
|
||||
## Upotreba
|
||||
## Korištenje
|
||||
|
||||
Sada ste spremni da koristite OpenCode za rad na svom projektu. Slobodno pitajte
|
||||
bilo šta!
|
||||
Ako ste novi u korištenju agenta za AI kodiranje, evo nekoliko primjera koji bi mogli
|
||||
pomoć.
|
||||
Sada ste spremni da koristite OpenCode za rad na svom projektu. Slobodno pitajte bilo šta!
|
||||
Ako ste novi u korištenju agenta za AI kodiranje, evo nekoliko primjera koji bi mogli pomoći.
|
||||
|
||||
---
|
||||
|
||||
### Postavljajte pitanja
|
||||
### Postavljanje pitanja
|
||||
|
||||
Možete zamoliti OpenCode da vam objasni kodnu bazu.
|
||||
:::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.
|
||||
|
||||
1. **Kreirajte plan**
|
||||
OpenCode ima _Plan mod_ koji onemogućuje njegovu sposobnost da pravi promjene i
|
||||
umjesto toga predložite _kako_ će implementirati ovu funkciju.
|
||||
OpenCode ima _Plan mod_ koji onemogućuje njegovu sposobnost da pravi promjene i umjesto toga predlaže _kako_ će implementirati ovu funkciju.
|
||||
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"
|
||||
|
|
@ -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.
|
||||
|
||||
```txt frame="none"
|
||||
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.
|
||||
From this screen, the user can undelete a note or permanently delete it.
|
||||
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.
|
||||
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
|
||||
da razgovarate s njim kao da razgovarate sa mlađim programerom u svom timu.
|
||||
Ž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.
|
||||
:::tip
|
||||
Dajte OpenCode dosta konteksta i primjera koji će mu pomoći da razumije šta vi
|
||||
želim.
|
||||
Dajte OpenCode dosta konteksta i primjera koji će mu pomoći da razumije šta vi želite.
|
||||
:::
|
||||
|
||||
2. **Ponovite plan**
|
||||
Kada vam da plan, možete mu dati povratne informacije ili dodati više detalja.
|
||||
|
||||
```txt frame="none"
|
||||
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.
|
||||
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.
|
||||
```
|
||||
|
||||
:::tip
|
||||
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š
|
||||
učinite to povlačenjem i ispuštanjem slike u terminal.
|
||||
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.
|
||||
|
||||
3. **Izgradite funkciju**
|
||||
Kada se osjećate ugodno s planom, vratite se na _Build mode_ do
|
||||
ponovnim pritiskom na taster **Tab**.
|
||||
Kada se osjećate ugodno s planom, vratite se na _Build mode_ ponovnim pritiskom na taster **Tab**.
|
||||
|
||||
```bash frame="none"
|
||||
<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
|
||||
bez potrebe da prvo pregledate plan.
|
||||
Za jednostavnije promjene, možete zamoliti OpenCode da ga direktno izgradi bez potrebe da prvo pregledate plan.
|
||||
|
||||
```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
|
||||
|
|
@ -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
|
||||
```
|
||||
|
||||
Želite da budete sigurni da ste pružili dobru količinu detalja kako bi OpenCode bio ispravan
|
||||
promjene.
|
||||
Želite da budete sigurni da ste pružili dobru količinu detalja kako bi OpenCode napravio ispravne promjene.
|
||||
|
||||
---
|
||||
|
||||
### Poništi promjene
|
||||
### Poništavanje izmjena
|
||||
|
||||
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?
|
||||
```
|
||||
|
||||
Ali shvatate da to nije ono što ste želeli. Možete **poništiti** promjene
|
||||
koristeći naredbu `/undo`.
|
||||
Ali shvatate da to nije ono što ste željeli. Možete **poništiti** promjene koristeći naredbu `/undo`.
|
||||
|
||||
```bash frame="none"
|
||||
/undo
|
||||
```
|
||||
|
||||
OpenCode će sada poništiti promjene koje ste napravili i prikazati vašu originalnu poruku
|
||||
opet.
|
||||
OpenCode će sada poništiti promjene koje ste napravili i ponovo prikazati vašu originalnu poruku.
|
||||
|
||||
```txt frame="none" "@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
|
||||
tim](/docs/share).
|
||||
Razgovore koje imate sa OpenCode možete [dijeliti sa vašim timom](/docs/share).
|
||||
|
||||
```bash frame="none"
|
||||
/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.
|
||||
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
Loading…
Reference in New Issue