mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-11 03:14:29 +00:00
Compare commits
42 Commits
github-v1.
...
production
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24556331c8 | ||
|
|
39145b99e8 | ||
|
|
0afa6e03a8 | ||
|
|
7a3c775dc1 | ||
|
|
3ea58bb790 | ||
|
|
50c705cd2d | ||
|
|
3894c217cc | ||
|
|
66c2bb8f37 | ||
|
|
1bbbd51d48 | ||
|
|
50f3e74d05 | ||
|
|
21475a1dfd | ||
|
|
dce4c05fa9 | ||
|
|
8c56571ef9 | ||
|
|
92a77b72fb | ||
|
|
4f6b929784 | ||
|
|
55119559b3 | ||
|
|
fd5531316f | ||
|
|
fbc41475b4 | ||
|
|
a0673256db | ||
|
|
fc37337a3e | ||
|
|
80220cebe4 | ||
|
|
8bdf6fa359 | ||
|
|
1d11a0adfd | ||
|
|
1e2f664410 | ||
|
|
a3aad9c9bf | ||
|
|
eb2587844b | ||
|
|
d863a9cf4e | ||
|
|
7d5be1556a | ||
|
|
659f15aa9b | ||
|
|
d1f5b9e911 | ||
|
|
284b00ff23 | ||
|
|
2c5760742b | ||
|
|
70c794e913 | ||
|
|
3929f0b5bd | ||
|
|
6f5dfe125a | ||
|
|
27fa9dc843 | ||
|
|
1e03a55acd | ||
|
|
65c9669283 | ||
|
|
18b6257119 | ||
|
|
c607c01fb9 | ||
|
|
4c4e30cd71 | ||
|
|
19ad7ad809 |
883
.opencode/agent/translator.md
Normal file
883
.opencode/agent/translator.md
Normal file
@@ -0,0 +1,883 @@
|
||||
---
|
||||
description: Translate content for a specified locale while preserving technical terms
|
||||
mode: subagent
|
||||
model: opencode/gemini-3-pro
|
||||
---
|
||||
|
||||
You are a professional translator and localization specialist.
|
||||
|
||||
Translate the user's content into the requested target locale (language + region, e.g. fr-FR, de-DE).
|
||||
|
||||
Requirements:
|
||||
|
||||
- Preserve meaning, intent, tone, and formatting (including Markdown/MDX structure).
|
||||
- Preserve all technical terms and artifacts exactly: product/company names, API names, identifiers, code, commands/flags, file paths, URLs, versions, error messages, config keys/values, and anything inside inline code or code blocks.
|
||||
- Also preserve every term listed in the Do-Not-Translate glossary below.
|
||||
- Do not modify fenced code blocks.
|
||||
- Output ONLY the translation (no commentary).
|
||||
|
||||
If the target locale is missing, ask the user to provide it.
|
||||
|
||||
---
|
||||
|
||||
# Do-Not-Translate Terms (OpenCode Docs)
|
||||
|
||||
Generated from: `packages/web/src/content/docs/*.mdx` (default English docs)
|
||||
Generated on: 2026-02-10
|
||||
|
||||
Use this as a translation QA checklist / glossary. Preserve listed terms exactly (spelling, casing, punctuation).
|
||||
|
||||
General rules (verbatim, even if not listed below):
|
||||
|
||||
- Anything inside inline code (single backticks) or fenced code blocks (triple backticks)
|
||||
- MDX/JS code in docs: `import ... from "..."`, component tags, identifiers
|
||||
- CLI commands, flags, config keys/values, file paths, URLs/domains, and env vars
|
||||
|
||||
## Proper nouns and product names
|
||||
|
||||
Additional (not reliably captured via link text):
|
||||
|
||||
```text
|
||||
Astro
|
||||
Bun
|
||||
Chocolatey
|
||||
Cursor
|
||||
Docker
|
||||
Git
|
||||
GitHub Actions
|
||||
GitLab CI
|
||||
GNOME Terminal
|
||||
Homebrew
|
||||
Mise
|
||||
Neovim
|
||||
Node.js
|
||||
npm
|
||||
Obsidian
|
||||
opencode
|
||||
opencode-ai
|
||||
Paru
|
||||
pnpm
|
||||
ripgrep
|
||||
Scoop
|
||||
SST
|
||||
Starlight
|
||||
Visual Studio Code
|
||||
VS Code
|
||||
VSCodium
|
||||
Windsurf
|
||||
Windows Terminal
|
||||
Yarn
|
||||
Zellij
|
||||
Zed
|
||||
anomalyco
|
||||
```
|
||||
|
||||
Extracted from link labels in the English docs (review and prune as desired):
|
||||
|
||||
```text
|
||||
@openspoon/subtask2
|
||||
302.AI console
|
||||
ACP progress report
|
||||
Agent Client Protocol
|
||||
Agent Skills
|
||||
Agentic
|
||||
AGENTS.md
|
||||
AI SDK
|
||||
Alacritty
|
||||
Anthropic
|
||||
Anthropic's Data Policies
|
||||
Atom One
|
||||
Avante.nvim
|
||||
Ayu
|
||||
Azure AI Foundry
|
||||
Azure portal
|
||||
Baseten
|
||||
built-in GITHUB_TOKEN
|
||||
Bun.$
|
||||
Catppuccin
|
||||
Cerebras console
|
||||
ChatGPT Plus or Pro
|
||||
Cloudflare dashboard
|
||||
CodeCompanion.nvim
|
||||
CodeNomad
|
||||
Configuring Adapters: Environment Variables
|
||||
Context7 MCP server
|
||||
Cortecs console
|
||||
Deep Infra dashboard
|
||||
DeepSeek console
|
||||
Duo Agent Platform
|
||||
Everforest
|
||||
Fireworks AI console
|
||||
Firmware dashboard
|
||||
Ghostty
|
||||
GitLab CLI agents docs
|
||||
GitLab docs
|
||||
GitLab User Settings > Access Tokens
|
||||
Granular Rules (Object Syntax)
|
||||
Grep by Vercel
|
||||
Groq console
|
||||
Gruvbox
|
||||
Helicone
|
||||
Helicone documentation
|
||||
Helicone Header Directory
|
||||
Helicone's Model Directory
|
||||
Hugging Face Inference Providers
|
||||
Hugging Face settings
|
||||
install WSL
|
||||
IO.NET console
|
||||
JetBrains IDE
|
||||
Kanagawa
|
||||
Kitty
|
||||
MiniMax API Console
|
||||
Models.dev
|
||||
Moonshot AI console
|
||||
Nebius Token Factory console
|
||||
Nord
|
||||
OAuth
|
||||
Ollama integration docs
|
||||
OpenAI's Data Policies
|
||||
OpenChamber
|
||||
OpenCode
|
||||
OpenCode config
|
||||
OpenCode Config
|
||||
OpenCode TUI with the opencode theme
|
||||
OpenCode Web - Active Session
|
||||
OpenCode Web - New Session
|
||||
OpenCode Web - See Servers
|
||||
OpenCode Zen
|
||||
OpenCode-Obsidian
|
||||
OpenRouter dashboard
|
||||
OpenWork
|
||||
OVHcloud panel
|
||||
Pro+ subscription
|
||||
SAP BTP Cockpit
|
||||
Scaleway Console IAM settings
|
||||
Scaleway Generative APIs
|
||||
SDK documentation
|
||||
Sentry MCP server
|
||||
shell API
|
||||
Together AI console
|
||||
Tokyonight
|
||||
Unified Billing
|
||||
Venice AI console
|
||||
Vercel dashboard
|
||||
WezTerm
|
||||
Windows Subsystem for Linux (WSL)
|
||||
WSL
|
||||
WSL (Windows Subsystem for Linux)
|
||||
WSL extension
|
||||
xAI console
|
||||
Z.AI API console
|
||||
Zed
|
||||
ZenMux dashboard
|
||||
Zod
|
||||
```
|
||||
|
||||
## Acronyms and initialisms
|
||||
|
||||
```text
|
||||
ACP
|
||||
AGENTS
|
||||
AI
|
||||
AI21
|
||||
ANSI
|
||||
API
|
||||
AST
|
||||
AWS
|
||||
BTP
|
||||
CD
|
||||
CDN
|
||||
CI
|
||||
CLI
|
||||
CMD
|
||||
CORS
|
||||
DEBUG
|
||||
EKS
|
||||
ERROR
|
||||
FAQ
|
||||
GLM
|
||||
GNOME
|
||||
GPT
|
||||
HTML
|
||||
HTTP
|
||||
HTTPS
|
||||
IAM
|
||||
ID
|
||||
IDE
|
||||
INFO
|
||||
IO
|
||||
IP
|
||||
IRSA
|
||||
JS
|
||||
JSON
|
||||
JSONC
|
||||
K2
|
||||
LLM
|
||||
LM
|
||||
LSP
|
||||
M2
|
||||
MCP
|
||||
MR
|
||||
NET
|
||||
NPM
|
||||
NTLM
|
||||
OIDC
|
||||
OS
|
||||
PAT
|
||||
PATH
|
||||
PHP
|
||||
PR
|
||||
PTY
|
||||
README
|
||||
RFC
|
||||
RPC
|
||||
SAP
|
||||
SDK
|
||||
SKILL
|
||||
SSE
|
||||
SSO
|
||||
TS
|
||||
TTY
|
||||
TUI
|
||||
UI
|
||||
URL
|
||||
US
|
||||
UX
|
||||
VCS
|
||||
VPC
|
||||
VPN
|
||||
VS
|
||||
WARN
|
||||
WSL
|
||||
X11
|
||||
YAML
|
||||
```
|
||||
|
||||
## Code identifiers used in prose (CamelCase, mixedCase)
|
||||
|
||||
```text
|
||||
apiKey
|
||||
AppleScript
|
||||
AssistantMessage
|
||||
baseURL
|
||||
BurntSushi
|
||||
ChatGPT
|
||||
ClangFormat
|
||||
CodeCompanion
|
||||
CodeNomad
|
||||
DeepSeek
|
||||
DefaultV2
|
||||
FileContent
|
||||
FileDiff
|
||||
FileNode
|
||||
fineGrained
|
||||
FormatterStatus
|
||||
GitHub
|
||||
GitLab
|
||||
iTerm2
|
||||
JavaScript
|
||||
JetBrains
|
||||
macOS
|
||||
mDNS
|
||||
MiniMax
|
||||
NeuralNomadsAI
|
||||
NickvanDyke
|
||||
NoeFabris
|
||||
OpenAI
|
||||
OpenAPI
|
||||
OpenChamber
|
||||
OpenCode
|
||||
OpenRouter
|
||||
OpenTUI
|
||||
OpenWork
|
||||
ownUserPermissions
|
||||
PowerShell
|
||||
ProviderAuthAuthorization
|
||||
ProviderAuthMethod
|
||||
ProviderInitError
|
||||
SessionStatus
|
||||
TabItem
|
||||
tokenType
|
||||
ToolIDs
|
||||
ToolList
|
||||
TypeScript
|
||||
typesUrl
|
||||
UserMessage
|
||||
VcsInfo
|
||||
WebView2
|
||||
WezTerm
|
||||
xAI
|
||||
ZenMux
|
||||
```
|
||||
|
||||
## OpenCode CLI commands (as shown in docs)
|
||||
|
||||
```text
|
||||
opencode
|
||||
opencode [project]
|
||||
opencode /path/to/project
|
||||
opencode acp
|
||||
opencode agent [command]
|
||||
opencode agent create
|
||||
opencode agent list
|
||||
opencode attach [url]
|
||||
opencode attach http://10.20.30.40:4096
|
||||
opencode attach http://localhost:4096
|
||||
opencode auth [command]
|
||||
opencode auth list
|
||||
opencode auth login
|
||||
opencode auth logout
|
||||
opencode auth ls
|
||||
opencode export [sessionID]
|
||||
opencode github [command]
|
||||
opencode github install
|
||||
opencode github run
|
||||
opencode import <file>
|
||||
opencode import https://opncd.ai/s/abc123
|
||||
opencode import session.json
|
||||
opencode mcp [command]
|
||||
opencode mcp add
|
||||
opencode mcp auth [name]
|
||||
opencode mcp auth list
|
||||
opencode mcp auth ls
|
||||
opencode mcp auth my-oauth-server
|
||||
opencode mcp auth sentry
|
||||
opencode mcp debug <name>
|
||||
opencode mcp debug my-oauth-server
|
||||
opencode mcp list
|
||||
opencode mcp logout [name]
|
||||
opencode mcp logout my-oauth-server
|
||||
opencode mcp ls
|
||||
opencode models --refresh
|
||||
opencode models [provider]
|
||||
opencode models anthropic
|
||||
opencode run [message..]
|
||||
opencode run Explain the use of context in Go
|
||||
opencode serve
|
||||
opencode serve --cors http://localhost:5173 --cors https://app.example.com
|
||||
opencode serve --hostname 0.0.0.0 --port 4096
|
||||
opencode serve [--port <number>] [--hostname <string>] [--cors <origin>]
|
||||
opencode session [command]
|
||||
opencode session list
|
||||
opencode stats
|
||||
opencode uninstall
|
||||
opencode upgrade
|
||||
opencode upgrade [target]
|
||||
opencode upgrade v0.1.48
|
||||
opencode web
|
||||
opencode web --cors https://example.com
|
||||
opencode web --hostname 0.0.0.0
|
||||
opencode web --mdns
|
||||
opencode web --mdns --mdns-domain myproject.local
|
||||
opencode web --port 4096
|
||||
opencode web --port 4096 --hostname 0.0.0.0
|
||||
opencode.server.close()
|
||||
```
|
||||
|
||||
## Slash commands and routes
|
||||
|
||||
```text
|
||||
/agent
|
||||
/auth/:id
|
||||
/clear
|
||||
/command
|
||||
/config
|
||||
/config/providers
|
||||
/connect
|
||||
/continue
|
||||
/doc
|
||||
/editor
|
||||
/event
|
||||
/experimental/tool?provider=<p>&model=<m>
|
||||
/experimental/tool/ids
|
||||
/export
|
||||
/file?path=<path>
|
||||
/file/content?path=<p>
|
||||
/file/status
|
||||
/find?pattern=<pat>
|
||||
/find/file
|
||||
/find/file?query=<q>
|
||||
/find/symbol?query=<q>
|
||||
/formatter
|
||||
/global/event
|
||||
/global/health
|
||||
/help
|
||||
/init
|
||||
/instance/dispose
|
||||
/log
|
||||
/lsp
|
||||
/mcp
|
||||
/mnt/
|
||||
/mnt/c/
|
||||
/mnt/d/
|
||||
/models
|
||||
/oc
|
||||
/opencode
|
||||
/path
|
||||
/project
|
||||
/project/current
|
||||
/provider
|
||||
/provider/{id}/oauth/authorize
|
||||
/provider/{id}/oauth/callback
|
||||
/provider/auth
|
||||
/q
|
||||
/quit
|
||||
/redo
|
||||
/resume
|
||||
/session
|
||||
/session/:id
|
||||
/session/:id/abort
|
||||
/session/:id/children
|
||||
/session/:id/command
|
||||
/session/:id/diff
|
||||
/session/:id/fork
|
||||
/session/:id/init
|
||||
/session/:id/message
|
||||
/session/:id/message/:messageID
|
||||
/session/:id/permissions/:permissionID
|
||||
/session/:id/prompt_async
|
||||
/session/:id/revert
|
||||
/session/:id/share
|
||||
/session/:id/shell
|
||||
/session/:id/summarize
|
||||
/session/:id/todo
|
||||
/session/:id/unrevert
|
||||
/session/status
|
||||
/share
|
||||
/summarize
|
||||
/theme
|
||||
/tui
|
||||
/tui/append-prompt
|
||||
/tui/clear-prompt
|
||||
/tui/control/next
|
||||
/tui/control/response
|
||||
/tui/execute-command
|
||||
/tui/open-help
|
||||
/tui/open-models
|
||||
/tui/open-sessions
|
||||
/tui/open-themes
|
||||
/tui/show-toast
|
||||
/tui/submit-prompt
|
||||
/undo
|
||||
/Users/username
|
||||
/Users/username/projects/*
|
||||
/vcs
|
||||
```
|
||||
|
||||
## CLI flags and short options
|
||||
|
||||
```text
|
||||
--agent
|
||||
--attach
|
||||
--command
|
||||
--continue
|
||||
--cors
|
||||
--cwd
|
||||
--days
|
||||
--dir
|
||||
--dry-run
|
||||
--event
|
||||
--file
|
||||
--force
|
||||
--fork
|
||||
--format
|
||||
--help
|
||||
--hostname
|
||||
--hostname 0.0.0.0
|
||||
--keep-config
|
||||
--keep-data
|
||||
--log-level
|
||||
--max-count
|
||||
--mdns
|
||||
--mdns-domain
|
||||
--method
|
||||
--model
|
||||
--models
|
||||
--port
|
||||
--print-logs
|
||||
--project
|
||||
--prompt
|
||||
--refresh
|
||||
--session
|
||||
--share
|
||||
--title
|
||||
--token
|
||||
--tools
|
||||
--verbose
|
||||
--version
|
||||
--wait
|
||||
|
||||
-c
|
||||
-d
|
||||
-f
|
||||
-h
|
||||
-m
|
||||
-n
|
||||
-s
|
||||
-v
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
```text
|
||||
AI_API_URL
|
||||
AI_FLOW_CONTEXT
|
||||
AI_FLOW_EVENT
|
||||
AI_FLOW_INPUT
|
||||
AICORE_DEPLOYMENT_ID
|
||||
AICORE_RESOURCE_GROUP
|
||||
AICORE_SERVICE_KEY
|
||||
ANTHROPIC_API_KEY
|
||||
AWS_ACCESS_KEY_ID
|
||||
AWS_BEARER_TOKEN_BEDROCK
|
||||
AWS_PROFILE
|
||||
AWS_REGION
|
||||
AWS_ROLE_ARN
|
||||
AWS_SECRET_ACCESS_KEY
|
||||
AWS_WEB_IDENTITY_TOKEN_FILE
|
||||
AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
|
||||
AZURE_RESOURCE_NAME
|
||||
CI_PROJECT_DIR
|
||||
CI_SERVER_FQDN
|
||||
CI_WORKLOAD_REF
|
||||
CLOUDFLARE_ACCOUNT_ID
|
||||
CLOUDFLARE_API_TOKEN
|
||||
CLOUDFLARE_GATEWAY_ID
|
||||
CONTEXT7_API_KEY
|
||||
GITHUB_TOKEN
|
||||
GITLAB_AI_GATEWAY_URL
|
||||
GITLAB_HOST
|
||||
GITLAB_INSTANCE_URL
|
||||
GITLAB_OAUTH_CLIENT_ID
|
||||
GITLAB_TOKEN
|
||||
GITLAB_TOKEN_OPENCODE
|
||||
GOOGLE_APPLICATION_CREDENTIALS
|
||||
GOOGLE_CLOUD_PROJECT
|
||||
HTTP_PROXY
|
||||
HTTPS_PROXY
|
||||
K2_
|
||||
MY_API_KEY
|
||||
MY_ENV_VAR
|
||||
MY_MCP_CLIENT_ID
|
||||
MY_MCP_CLIENT_SECRET
|
||||
NO_PROXY
|
||||
NODE_ENV
|
||||
NODE_EXTRA_CA_CERTS
|
||||
NPM_AUTH_TOKEN
|
||||
OC_ALLOW_WAYLAND
|
||||
OPENCODE_API_KEY
|
||||
OPENCODE_AUTH_JSON
|
||||
OPENCODE_AUTO_SHARE
|
||||
OPENCODE_CLIENT
|
||||
OPENCODE_CONFIG
|
||||
OPENCODE_CONFIG_CONTENT
|
||||
OPENCODE_CONFIG_DIR
|
||||
OPENCODE_DISABLE_AUTOCOMPACT
|
||||
OPENCODE_DISABLE_AUTOUPDATE
|
||||
OPENCODE_DISABLE_CLAUDE_CODE
|
||||
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT
|
||||
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS
|
||||
OPENCODE_DISABLE_DEFAULT_PLUGINS
|
||||
OPENCODE_DISABLE_FILETIME_CHECK
|
||||
OPENCODE_DISABLE_LSP_DOWNLOAD
|
||||
OPENCODE_DISABLE_MODELS_FETCH
|
||||
OPENCODE_DISABLE_PRUNE
|
||||
OPENCODE_DISABLE_TERMINAL_TITLE
|
||||
OPENCODE_ENABLE_EXA
|
||||
OPENCODE_ENABLE_EXPERIMENTAL_MODELS
|
||||
OPENCODE_EXPERIMENTAL
|
||||
OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS
|
||||
OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT
|
||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER
|
||||
OPENCODE_EXPERIMENTAL_EXA
|
||||
OPENCODE_EXPERIMENTAL_FILEWATCHER
|
||||
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY
|
||||
OPENCODE_EXPERIMENTAL_LSP_TOOL
|
||||
OPENCODE_EXPERIMENTAL_LSP_TY
|
||||
OPENCODE_EXPERIMENTAL_MARKDOWN
|
||||
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX
|
||||
OPENCODE_EXPERIMENTAL_OXFMT
|
||||
OPENCODE_EXPERIMENTAL_PLAN_MODE
|
||||
OPENCODE_FAKE_VCS
|
||||
OPENCODE_GIT_BASH_PATH
|
||||
OPENCODE_MODEL
|
||||
OPENCODE_MODELS_URL
|
||||
OPENCODE_PERMISSION
|
||||
OPENCODE_PORT
|
||||
OPENCODE_SERVER_PASSWORD
|
||||
OPENCODE_SERVER_USERNAME
|
||||
PROJECT_ROOT
|
||||
RESOURCE_NAME
|
||||
RUST_LOG
|
||||
VARIABLE_NAME
|
||||
VERTEX_LOCATION
|
||||
XDG_CONFIG_HOME
|
||||
```
|
||||
|
||||
## Package/module identifiers
|
||||
|
||||
```text
|
||||
../../../config.mjs
|
||||
@astrojs/starlight/components
|
||||
@opencode-ai/plugin
|
||||
@opencode-ai/sdk
|
||||
path
|
||||
shescape
|
||||
zod
|
||||
|
||||
@
|
||||
@ai-sdk/anthropic
|
||||
@ai-sdk/cerebras
|
||||
@ai-sdk/google
|
||||
@ai-sdk/openai
|
||||
@ai-sdk/openai-compatible
|
||||
@File#L37-42
|
||||
@modelcontextprotocol/server-everything
|
||||
@opencode
|
||||
```
|
||||
|
||||
## GitHub owner/repo slugs referenced in docs
|
||||
|
||||
```text
|
||||
24601/opencode-zellij-namer
|
||||
angristan/opencode-wakatime
|
||||
anomalyco/opencode
|
||||
apps/opencode-agent
|
||||
athal7/opencode-devcontainers
|
||||
awesome-opencode/awesome-opencode
|
||||
backnotprop/plannotator
|
||||
ben-vargas/ai-sdk-provider-opencode-sdk
|
||||
btriapitsyn/openchamber
|
||||
BurntSushi/ripgrep
|
||||
Cluster444/agentic
|
||||
code-yeongyu/oh-my-opencode
|
||||
darrenhinde/opencode-agents
|
||||
different-ai/opencode-scheduler
|
||||
different-ai/openwork
|
||||
features/copilot
|
||||
folke/tokyonight.nvim
|
||||
franlol/opencode-md-table-formatter
|
||||
ggml-org/llama.cpp
|
||||
ghoulr/opencode-websearch-cited.git
|
||||
H2Shami/opencode-helicone-session
|
||||
hosenur/portal
|
||||
jamesmurdza/daytona
|
||||
jenslys/opencode-gemini-auth
|
||||
JRedeker/opencode-morph-fast-apply
|
||||
JRedeker/opencode-shell-strategy
|
||||
kdcokenny/ocx
|
||||
kdcokenny/opencode-background-agents
|
||||
kdcokenny/opencode-notify
|
||||
kdcokenny/opencode-workspace
|
||||
kdcokenny/opencode-worktree
|
||||
login/device
|
||||
mohak34/opencode-notifier
|
||||
morhetz/gruvbox
|
||||
mtymek/opencode-obsidian
|
||||
NeuralNomadsAI/CodeNomad
|
||||
nick-vi/opencode-type-inject
|
||||
NickvanDyke/opencode.nvim
|
||||
NoeFabris/opencode-antigravity-auth
|
||||
nordtheme/nord
|
||||
numman-ali/opencode-openai-codex-auth
|
||||
olimorris/codecompanion.nvim
|
||||
panta82/opencode-notificator
|
||||
rebelot/kanagawa.nvim
|
||||
remorses/kimaki
|
||||
sainnhe/everforest
|
||||
shekohex/opencode-google-antigravity-auth
|
||||
shekohex/opencode-pty.git
|
||||
spoons-and-mirrors/subtask2
|
||||
sudo-tee/opencode.nvim
|
||||
supermemoryai/opencode-supermemory
|
||||
Tarquinen/opencode-dynamic-context-pruning
|
||||
Th3Whit3Wolf/one-nvim
|
||||
upstash/context7
|
||||
vtemian/micode
|
||||
vtemian/octto
|
||||
yetone/avante.nvim
|
||||
zenobi-us/opencode-plugin-template
|
||||
zenobi-us/opencode-skillful
|
||||
```
|
||||
|
||||
## Paths, filenames, globs, and URLs
|
||||
|
||||
```text
|
||||
./.opencode/themes/*.json
|
||||
./<project-slug>/storage/
|
||||
./config/#custom-directory
|
||||
./global/storage/
|
||||
.agents/skills/*/SKILL.md
|
||||
.agents/skills/<name>/SKILL.md
|
||||
.clang-format
|
||||
.claude
|
||||
.claude/skills
|
||||
.claude/skills/*/SKILL.md
|
||||
.claude/skills/<name>/SKILL.md
|
||||
.env
|
||||
.github/workflows/opencode.yml
|
||||
.gitignore
|
||||
.gitlab-ci.yml
|
||||
.ignore
|
||||
.NET SDK
|
||||
.npmrc
|
||||
.ocamlformat
|
||||
.opencode
|
||||
.opencode/
|
||||
.opencode/agents/
|
||||
.opencode/commands/
|
||||
.opencode/commands/test.md
|
||||
.opencode/modes/
|
||||
.opencode/plans/*.md
|
||||
.opencode/plugins/
|
||||
.opencode/skills/<name>/SKILL.md
|
||||
.opencode/skills/git-release/SKILL.md
|
||||
.opencode/tools/
|
||||
.well-known/opencode
|
||||
{ type: "raw" \| "patch", content: string }
|
||||
{file:path/to/file}
|
||||
**/*.js
|
||||
%USERPROFILE%/intelephense/license.txt
|
||||
%USERPROFILE%\.cache\opencode
|
||||
%USERPROFILE%\.config\opencode\opencode.jsonc
|
||||
%USERPROFILE%\.config\opencode\plugins
|
||||
%USERPROFILE%\.local\share\opencode
|
||||
%USERPROFILE%\.local\share\opencode\log
|
||||
<project-root>/.opencode/themes/*.json
|
||||
<providerId>/<modelId>
|
||||
<your-project>/.opencode/plugins/
|
||||
~
|
||||
~/...
|
||||
~/.agents/skills/*/SKILL.md
|
||||
~/.agents/skills/<name>/SKILL.md
|
||||
~/.aws/credentials
|
||||
~/.bashrc
|
||||
~/.cache/opencode
|
||||
~/.cache/opencode/node_modules/
|
||||
~/.claude/CLAUDE.md
|
||||
~/.claude/skills/
|
||||
~/.claude/skills/*/SKILL.md
|
||||
~/.claude/skills/<name>/SKILL.md
|
||||
~/.config/opencode
|
||||
~/.config/opencode/AGENTS.md
|
||||
~/.config/opencode/agents/
|
||||
~/.config/opencode/commands/
|
||||
~/.config/opencode/modes/
|
||||
~/.config/opencode/opencode.json
|
||||
~/.config/opencode/opencode.jsonc
|
||||
~/.config/opencode/plugins/
|
||||
~/.config/opencode/skills/*/SKILL.md
|
||||
~/.config/opencode/skills/<name>/SKILL.md
|
||||
~/.config/opencode/themes/*.json
|
||||
~/.config/opencode/tools/
|
||||
~/.config/zed/settings.json
|
||||
~/.local/share
|
||||
~/.local/share/opencode/
|
||||
~/.local/share/opencode/auth.json
|
||||
~/.local/share/opencode/log/
|
||||
~/.local/share/opencode/mcp-auth.json
|
||||
~/.local/share/opencode/opencode.jsonc
|
||||
~/.npmrc
|
||||
~/.zshrc
|
||||
~/code/
|
||||
~/Library/Application Support
|
||||
~/projects/*
|
||||
~/projects/personal/
|
||||
${config.github}/blob/dev/packages/sdk/js/src/gen/types.gen.ts
|
||||
$HOME/intelephense/license.txt
|
||||
$HOME/projects/*
|
||||
$XDG_CONFIG_HOME/opencode/themes/*.json
|
||||
agent/
|
||||
agents/
|
||||
build/
|
||||
commands/
|
||||
dist/
|
||||
http://<wsl-ip>:4096
|
||||
http://127.0.0.1:8080/callback
|
||||
http://localhost:<port>
|
||||
http://localhost:4096
|
||||
http://localhost:4096/doc
|
||||
https://app.example.com
|
||||
https://AZURE_COGNITIVE_SERVICES_RESOURCE_NAME.cognitiveservices.azure.com/
|
||||
https://opencode.ai/zen/v1/chat/completions
|
||||
https://opencode.ai/zen/v1/messages
|
||||
https://opencode.ai/zen/v1/models/gemini-3-flash
|
||||
https://opencode.ai/zen/v1/models/gemini-3-pro
|
||||
https://opencode.ai/zen/v1/responses
|
||||
https://RESOURCE_NAME.openai.azure.com/
|
||||
laravel/pint
|
||||
log/
|
||||
model: "anthropic/claude-sonnet-4-5"
|
||||
modes/
|
||||
node_modules/
|
||||
openai/gpt-4.1
|
||||
opencode.ai/config.json
|
||||
opencode/<model-id>
|
||||
opencode/gpt-5.1-codex
|
||||
opencode/gpt-5.2-codex
|
||||
opencode/kimi-k2
|
||||
openrouter/google/gemini-2.5-flash
|
||||
opncd.ai/s/<share-id>
|
||||
packages/*/AGENTS.md
|
||||
plugins/
|
||||
project/
|
||||
provider_id/model_id
|
||||
provider/model
|
||||
provider/model-id
|
||||
rm -rf ~/.cache/opencode
|
||||
skills/
|
||||
skills/*/SKILL.md
|
||||
src/**/*.ts
|
||||
themes/
|
||||
tools/
|
||||
```
|
||||
|
||||
## Keybind strings
|
||||
|
||||
```text
|
||||
alt+b
|
||||
Alt+Ctrl+K
|
||||
alt+d
|
||||
alt+f
|
||||
Cmd+Esc
|
||||
Cmd+Option+K
|
||||
Cmd+Shift+Esc
|
||||
Cmd+Shift+G
|
||||
Cmd+Shift+P
|
||||
ctrl+a
|
||||
ctrl+b
|
||||
ctrl+d
|
||||
ctrl+e
|
||||
Ctrl+Esc
|
||||
ctrl+f
|
||||
ctrl+g
|
||||
ctrl+k
|
||||
Ctrl+Shift+Esc
|
||||
Ctrl+Shift+P
|
||||
ctrl+t
|
||||
ctrl+u
|
||||
ctrl+w
|
||||
ctrl+x
|
||||
DELETE
|
||||
Shift+Enter
|
||||
WIN+R
|
||||
```
|
||||
|
||||
## Model ID strings referenced
|
||||
|
||||
```text
|
||||
{env:OPENCODE_MODEL}
|
||||
anthropic/claude-3-5-sonnet-20241022
|
||||
anthropic/claude-haiku-4-20250514
|
||||
anthropic/claude-haiku-4-5
|
||||
anthropic/claude-sonnet-4-20250514
|
||||
anthropic/claude-sonnet-4-5
|
||||
gitlab/duo-chat-haiku-4-5
|
||||
lmstudio/google/gemma-3n-e4b
|
||||
openai/gpt-4.1
|
||||
openai/gpt-5
|
||||
opencode/gpt-5.1-codex
|
||||
opencode/gpt-5.2-codex
|
||||
opencode/kimi-k2
|
||||
openrouter/google/gemini-2.5-flash
|
||||
```
|
||||
30
bun.lock
30
bun.lock
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -73,7 +73,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"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.53",
|
||||
"version": "1.1.56",
|
||||
"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.53",
|
||||
"version": "1.1.56",
|
||||
"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.53",
|
||||
"version": "1.1.56",
|
||||
"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.53",
|
||||
"version": "1.1.56",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -215,7 +215,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -244,7 +244,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -260,7 +260,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -366,7 +366,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -386,7 +386,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.90.10",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -397,7 +397,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -410,7 +410,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -452,7 +452,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -463,7 +463,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
||||
@@ -166,14 +166,10 @@ const bucketNew = new sst.cloudflare.Bucket("ZenDataNew")
|
||||
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
||||
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
||||
|
||||
let logProcessor
|
||||
if ($app.stage === "production" || $app.stage === "frank") {
|
||||
const HONEYCOMB_API_KEY = new sst.Secret("HONEYCOMB_API_KEY")
|
||||
logProcessor = new sst.cloudflare.Worker("LogProcessor", {
|
||||
handler: "packages/console/function/src/log-processor.ts",
|
||||
link: [HONEYCOMB_API_KEY],
|
||||
})
|
||||
}
|
||||
const logProcessor = new sst.cloudflare.Worker("LogProcessor", {
|
||||
handler: "packages/console/function/src/log-processor.ts",
|
||||
link: [new sst.Secret("HONEYCOMB_API_KEY")],
|
||||
})
|
||||
|
||||
new sst.cloudflare.x.SolidStart("Console", {
|
||||
domain,
|
||||
@@ -211,7 +207,7 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
transform: {
|
||||
worker: {
|
||||
placement: { mode: "smart" },
|
||||
tailConsumers: logProcessor ? [{ service: logProcessor.nodes.worker.scriptName }] : [],
|
||||
tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
[test]
|
||||
root = "./src"
|
||||
preload = ["./happydom.ts"]
|
||||
|
||||
126
packages/app/e2e/session/session-undo-redo.spec.ts
Normal file
126
packages/app/e2e/session/session-undo-redo.spec.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import type { Page } from "@playwright/test"
|
||||
import { test, expect } from "../fixtures"
|
||||
import { withSession } from "../actions"
|
||||
import { createSdk, modKey } from "../utils"
|
||||
import { promptSelector } from "../selectors"
|
||||
|
||||
async function seedConversation(input: {
|
||||
page: Page
|
||||
sdk: ReturnType<typeof createSdk>
|
||||
sessionID: string
|
||||
token: string
|
||||
}) {
|
||||
const prompt = input.page.locator(promptSelector)
|
||||
await expect(prompt).toBeVisible()
|
||||
await prompt.click()
|
||||
await input.page.keyboard.type(`Reply with exactly: ${input.token}`)
|
||||
await input.page.keyboard.press("Enter")
|
||||
|
||||
let userMessageID: string | undefined
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const messages = await input.sdk.session
|
||||
.messages({ sessionID: input.sessionID, limit: 50 })
|
||||
.then((r) => r.data ?? [])
|
||||
const users = messages.filter((m) => m.info.role === "user")
|
||||
if (users.length === 0) return false
|
||||
|
||||
const user = users.reduce((acc, item) => (item.info.id > acc.info.id ? item : acc))
|
||||
userMessageID = user.info.id
|
||||
|
||||
const assistantText = messages
|
||||
.filter((m) => m.info.role === "assistant")
|
||||
.flatMap((m) => m.parts)
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join("\n")
|
||||
|
||||
return assistantText.includes(input.token)
|
||||
},
|
||||
{ timeout: 90_000 },
|
||||
)
|
||||
.toBe(true)
|
||||
|
||||
if (!userMessageID) throw new Error("Expected a user message id")
|
||||
return { prompt, userMessageID }
|
||||
}
|
||||
|
||||
test("slash undo sets revert and restores prior prompt", async ({ page, withProject }) => {
|
||||
test.setTimeout(120_000)
|
||||
|
||||
const token = `undo_${Date.now()}`
|
||||
|
||||
await withProject(async (project) => {
|
||||
const sdk = createSdk(project.directory)
|
||||
|
||||
await withSession(sdk, `e2e undo ${Date.now()}`, async (session) => {
|
||||
await project.gotoSession(session.id)
|
||||
|
||||
const seeded = await seedConversation({ page, sdk, sessionID: session.id, token })
|
||||
|
||||
await seeded.prompt.click()
|
||||
await page.keyboard.type("/undo")
|
||||
|
||||
const undo = page.locator('[data-slash-id="session.undo"]').first()
|
||||
await expect(undo).toBeVisible()
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
await expect
|
||||
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
|
||||
timeout: 30_000,
|
||||
})
|
||||
.toBe(seeded.userMessageID)
|
||||
|
||||
await expect(seeded.prompt).toContainText(token)
|
||||
await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`)).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test("slash redo clears revert and restores latest state", async ({ page, withProject }) => {
|
||||
test.setTimeout(120_000)
|
||||
|
||||
const token = `redo_${Date.now()}`
|
||||
|
||||
await withProject(async (project) => {
|
||||
const sdk = createSdk(project.directory)
|
||||
|
||||
await withSession(sdk, `e2e redo ${Date.now()}`, async (session) => {
|
||||
await project.gotoSession(session.id)
|
||||
|
||||
const seeded = await seedConversation({ page, sdk, sessionID: session.id, token })
|
||||
|
||||
await seeded.prompt.click()
|
||||
await page.keyboard.type("/undo")
|
||||
|
||||
const undo = page.locator('[data-slash-id="session.undo"]').first()
|
||||
await expect(undo).toBeVisible()
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
await expect
|
||||
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
|
||||
timeout: 30_000,
|
||||
})
|
||||
.toBe(seeded.userMessageID)
|
||||
|
||||
await seeded.prompt.click()
|
||||
await page.keyboard.press(`${modKey}+A`)
|
||||
await page.keyboard.press("Backspace")
|
||||
await page.keyboard.type("/redo")
|
||||
|
||||
const redo = page.locator('[data-slash-id="session.redo"]').first()
|
||||
await expect(redo).toBeVisible()
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
await expect
|
||||
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
|
||||
timeout: 30_000,
|
||||
})
|
||||
.toBeUndefined()
|
||||
|
||||
await expect(seeded.prompt).not.toContainText(token)
|
||||
await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`).first()).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
36
packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts
Normal file
36
packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { closeSidebar, hoverSessionItem } from "../actions"
|
||||
import { projectSwitchSelector, sessionItemSelector } from "../selectors"
|
||||
|
||||
test("collapsed sidebar popover stays open when archiving a session", async ({ page, slug, sdk, gotoSession }) => {
|
||||
const stamp = Date.now()
|
||||
|
||||
const one = await sdk.session.create({ title: `e2e sidebar popover archive 1 ${stamp}` }).then((r) => r.data)
|
||||
const two = await sdk.session.create({ title: `e2e sidebar popover archive 2 ${stamp}` }).then((r) => r.data)
|
||||
|
||||
if (!one?.id) throw new Error("Session create did not return an id")
|
||||
if (!two?.id) throw new Error("Session create did not return an id")
|
||||
|
||||
try {
|
||||
await gotoSession(one.id)
|
||||
await closeSidebar(page)
|
||||
|
||||
const project = page.locator(projectSwitchSelector(slug)).first()
|
||||
await expect(project).toBeVisible()
|
||||
await project.hover()
|
||||
|
||||
await expect(page.locator(sessionItemSelector(one.id)).first()).toBeVisible()
|
||||
await expect(page.locator(sessionItemSelector(two.id)).first()).toBeVisible()
|
||||
|
||||
const item = await hoverSessionItem(page, one.id)
|
||||
await item
|
||||
.getByRole("button", { name: /archive/i })
|
||||
.first()
|
||||
.click()
|
||||
|
||||
await expect(page.locator(sessionItemSelector(two.id)).first()).toBeVisible()
|
||||
} finally {
|
||||
await sdk.session.delete({ sessionID: one.id }).catch(() => undefined)
|
||||
await sdk.session.delete({ sessionID: two.id }).catch(() => undefined)
|
||||
}
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -89,7 +89,6 @@ let runner: ReturnType<typeof Bun.spawn> | undefined
|
||||
let server: { stop: () => Promise<void> | void } | undefined
|
||||
let inst: { Instance: { disposeAll: () => Promise<void> | void } } | undefined
|
||||
let cleaned = false
|
||||
let internalError = false
|
||||
|
||||
const cleanup = async () => {
|
||||
if (cleaned) return
|
||||
@@ -115,9 +114,8 @@ const shutdown = (code: number, reason: string) => {
|
||||
}
|
||||
|
||||
const reportInternalError = (reason: string, error: unknown) => {
|
||||
internalError = true
|
||||
console.error(`e2e-local internal error: ${reason}`)
|
||||
console.error(error)
|
||||
console.warn(`e2e-local ignored server error: ${reason}`)
|
||||
console.warn(error)
|
||||
}
|
||||
|
||||
process.once("SIGINT", () => shutdown(130, "SIGINT"))
|
||||
@@ -177,6 +175,4 @@ try {
|
||||
await cleanup()
|
||||
}
|
||||
|
||||
if (code === 0 && internalError) code = 1
|
||||
|
||||
process.exit(code)
|
||||
|
||||
@@ -6,6 +6,7 @@ let dirsToExpand: typeof import("./file-tree").dirsToExpand
|
||||
|
||||
beforeAll(async () => {
|
||||
mock.module("@solidjs/router", () => ({
|
||||
useNavigate: () => () => undefined,
|
||||
useParams: () => ({}),
|
||||
}))
|
||||
mock.module("@/context/file", () => ({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useFile } from "@/context/file"
|
||||
import { encodeFilePath } from "@/context/file/path"
|
||||
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
@@ -20,11 +21,7 @@ import { Dynamic } from "solid-js/web"
|
||||
import type { FileNode } from "@opencode-ai/sdk/v2"
|
||||
|
||||
function pathToFileUrl(filepath: string): string {
|
||||
const encodedPath = filepath
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/")
|
||||
return `file://${encodedPath}`
|
||||
return `file://${encodeFilePath(filepath)}`
|
||||
}
|
||||
|
||||
type Kind = "add" | "del" | "mix"
|
||||
@@ -223,12 +220,14 @@ export default function FileTree(props: {
|
||||
seen.add(item)
|
||||
}
|
||||
|
||||
return out.toSorted((a, b) => {
|
||||
out.sort((a, b) => {
|
||||
if (a.type !== b.type) {
|
||||
return a.type === "directory" ? -1 : 1
|
||||
}
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
|
||||
return out
|
||||
})
|
||||
|
||||
const Node = (
|
||||
|
||||
@@ -112,7 +112,7 @@ describe("buildRequestParts", () => {
|
||||
// Special chars should be encoded
|
||||
expect(filePart.url).toContain("file%23name.txt")
|
||||
// Should have Windows drive letter properly encoded
|
||||
expect(filePart.url).toMatch(/file:\/\/\/[A-Z]%3A/)
|
||||
expect(filePart.url).toMatch(/file:\/\/\/[A-Z]:/)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -210,7 +210,7 @@ describe("buildRequestParts", () => {
|
||||
if (filePart?.type === "file") {
|
||||
// Should handle absolute path that differs from sessionDirectory
|
||||
expect(() => new URL(filePart.url)).not.toThrow()
|
||||
expect(filePart.url).toContain("/D%3A/other/project/file.ts")
|
||||
expect(filePart.url).toContain("/D:/other/project/file.ts")
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput } from "@opencode-ai/sdk/v2/client"
|
||||
import type { FileSelection } from "@/context/file"
|
||||
import { encodeFilePath } from "@/context/file/path"
|
||||
import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt"
|
||||
import { Identifier } from "@/utils/id"
|
||||
|
||||
@@ -27,23 +28,11 @@ type BuildRequestPartsInput = {
|
||||
sessionDirectory: string
|
||||
}
|
||||
|
||||
const absolute = (directory: string, path: string) =>
|
||||
path.startsWith("/") ? path : (directory + "/" + path).replace("//", "/")
|
||||
|
||||
const encodeFilePath = (filepath: string): string => {
|
||||
// Normalize Windows paths: convert backslashes to forward slashes
|
||||
let normalized = filepath.replace(/\\/g, "/")
|
||||
|
||||
// Handle Windows absolute paths (D:/path -> /D:/path for proper file:// URLs)
|
||||
if (/^[A-Za-z]:/.test(normalized)) {
|
||||
normalized = "/" + normalized
|
||||
}
|
||||
|
||||
// Encode each path segment (preserving forward slashes as path separators)
|
||||
return normalized
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/")
|
||||
const absolute = (directory: string, path: string) => {
|
||||
if (path.startsWith("/")) return path
|
||||
if (/^[A-Za-z]:[\\/]/.test(path) || /^[A-Za-z]:$/.test(path)) return path
|
||||
if (path.startsWith("\\\\") || path.startsWith("//")) return path
|
||||
return `${directory.replace(/[\\/]+$/, "")}/${path}`
|
||||
}
|
||||
|
||||
const fileQuery = (selection: FileSelection | undefined) =>
|
||||
|
||||
@@ -166,6 +166,7 @@ export function SessionHeader() {
|
||||
})
|
||||
|
||||
const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp }))
|
||||
const [menu, setMenu] = createStore({ open: false })
|
||||
|
||||
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
|
||||
const current = createMemo(() => options().find((o) => o.id === prefs.app) ?? options()[0])
|
||||
@@ -355,7 +356,12 @@ export function SessionHeader() {
|
||||
<span class="text-12-regular text-text-strong">Open</span>
|
||||
</Button>
|
||||
<div class="self-stretch w-px bg-border-base/70" />
|
||||
<DropdownMenu gutter={6} placement="bottom-end">
|
||||
<DropdownMenu
|
||||
gutter={6}
|
||||
placement="bottom-end"
|
||||
open={menu.open}
|
||||
onOpenChange={(open) => setMenu("open", open)}
|
||||
>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="chevron-down"
|
||||
@@ -375,7 +381,13 @@ export function SessionHeader() {
|
||||
}}
|
||||
>
|
||||
{options().map((o) => (
|
||||
<DropdownMenu.RadioItem value={o.id} onSelect={() => openDir(o.id)}>
|
||||
<DropdownMenu.RadioItem
|
||||
value={o.id}
|
||||
onSelect={() => {
|
||||
setMenu("open", false)
|
||||
openDir(o.id)
|
||||
}}
|
||||
>
|
||||
<div class="flex size-5 shrink-0 items-center justify-center">
|
||||
<AppIcon id={o.icon} class={size(o.icon)} />
|
||||
</div>
|
||||
@@ -388,7 +400,12 @@ export function SessionHeader() {
|
||||
</DropdownMenu.RadioGroup>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onSelect={copyPath}>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
setMenu("open", false)
|
||||
copyPath()
|
||||
}}
|
||||
>
|
||||
<div class="flex size-5 shrink-0 items-center justify-center">
|
||||
<Icon name="copy" size="small" class="text-icon-weak" />
|
||||
</div>
|
||||
|
||||
@@ -74,7 +74,9 @@ export const Terminal = (props: TerminalProps) => {
|
||||
let handleTextareaBlur: () => void
|
||||
let disposed = false
|
||||
const cleanups: VoidFunction[] = []
|
||||
let tail = local.pty.tail ?? ""
|
||||
const start =
|
||||
typeof local.pty.cursor === "number" && Number.isSafeInteger(local.pty.cursor) ? local.pty.cursor : undefined
|
||||
let cursor = start ?? 0
|
||||
|
||||
const cleanup = () => {
|
||||
if (!cleanups.length) return
|
||||
@@ -164,13 +166,16 @@ export const Terminal = (props: TerminalProps) => {
|
||||
|
||||
const once = { value: false }
|
||||
|
||||
const url = new URL(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
|
||||
const url = new URL(sdk.url + `/pty/${local.pty.id}/connect`)
|
||||
url.searchParams.set("directory", sdk.directory)
|
||||
url.searchParams.set("cursor", String(start !== undefined ? start : local.pty.buffer ? -1 : 0))
|
||||
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
|
||||
if (window.__OPENCODE__?.serverPassword) {
|
||||
url.username = "opencode"
|
||||
url.password = window.__OPENCODE__?.serverPassword
|
||||
}
|
||||
const socket = new WebSocket(url)
|
||||
socket.binaryType = "arraybuffer"
|
||||
cleanups.push(() => {
|
||||
if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close()
|
||||
})
|
||||
@@ -185,7 +190,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
cursorStyle: "bar",
|
||||
fontSize: 14,
|
||||
fontFamily: monoFontFamily(settings.appearance.font()),
|
||||
allowTransparency: true,
|
||||
allowTransparency: false,
|
||||
convertEol: true,
|
||||
theme: terminalColors(),
|
||||
scrollback: 10_000,
|
||||
@@ -289,26 +294,6 @@ export const Terminal = (props: TerminalProps) => {
|
||||
handleResize = () => fit.fit()
|
||||
window.addEventListener("resize", handleResize)
|
||||
cleanups.push(() => window.removeEventListener("resize", handleResize))
|
||||
const limit = 16_384
|
||||
const min = 32
|
||||
const windowMs = 750
|
||||
const seed = tail.length > limit ? tail.slice(-limit) : tail
|
||||
let sync = seed.length >= min
|
||||
let syncUntil = 0
|
||||
const stopSync = () => {
|
||||
sync = false
|
||||
syncUntil = 0
|
||||
}
|
||||
|
||||
const overlap = (data: string) => {
|
||||
if (!seed) return 0
|
||||
const max = Math.min(seed.length, data.length)
|
||||
if (max < min) return 0
|
||||
for (let i = max; i >= min; i--) {
|
||||
if (seed.slice(-i) === data.slice(0, i)) return i
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const onResize = t.onResize(async (size) => {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
@@ -325,7 +310,6 @@ export const Terminal = (props: TerminalProps) => {
|
||||
})
|
||||
cleanups.push(() => disposeIfDisposable(onResize))
|
||||
const onData = t.onData((data) => {
|
||||
if (data) stopSync()
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(data)
|
||||
}
|
||||
@@ -343,7 +327,6 @@ export const Terminal = (props: TerminalProps) => {
|
||||
|
||||
const handleOpen = () => {
|
||||
local.onConnect?.()
|
||||
if (sync) syncUntil = Date.now() + windowMs
|
||||
sdk.client.pty
|
||||
.update({
|
||||
ptyID: local.pty.id,
|
||||
@@ -357,31 +340,31 @@ export const Terminal = (props: TerminalProps) => {
|
||||
socket.addEventListener("open", handleOpen)
|
||||
cleanups.push(() => socket.removeEventListener("open", handleOpen))
|
||||
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (disposed) return
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
// WebSocket control frame: 0x00 + UTF-8 JSON (currently { cursor }).
|
||||
const bytes = new Uint8Array(event.data)
|
||||
if (bytes[0] !== 0) return
|
||||
const json = decoder.decode(bytes.subarray(1))
|
||||
try {
|
||||
const meta = JSON.parse(json) as { cursor?: unknown }
|
||||
const next = meta?.cursor
|
||||
if (typeof next === "number" && Number.isSafeInteger(next) && next >= 0) {
|
||||
cursor = next
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const data = typeof event.data === "string" ? event.data : ""
|
||||
if (!data) return
|
||||
|
||||
const next = (() => {
|
||||
if (!sync) return data
|
||||
if (syncUntil && Date.now() > syncUntil) {
|
||||
stopSync()
|
||||
return data
|
||||
}
|
||||
const n = overlap(data)
|
||||
if (!n) {
|
||||
stopSync()
|
||||
return data
|
||||
}
|
||||
const trimmed = data.slice(n)
|
||||
if (trimmed) stopSync()
|
||||
return trimmed
|
||||
})()
|
||||
|
||||
if (!next) return
|
||||
|
||||
t.write(next)
|
||||
tail = next.length >= limit ? next.slice(-limit) : (tail + next).slice(-limit)
|
||||
t.write(data)
|
||||
cursor += data.length
|
||||
}
|
||||
socket.addEventListener("message", handleMessage)
|
||||
cleanups.push(() => socket.removeEventListener("message", handleMessage))
|
||||
@@ -435,7 +418,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
props.onCleanup({
|
||||
...local.pty,
|
||||
buffer,
|
||||
tail,
|
||||
cursor,
|
||||
rows: t.rows,
|
||||
cols: t.cols,
|
||||
scrollY: t.getViewportY(),
|
||||
|
||||
@@ -6,6 +6,7 @@ let createCommentSessionForTest: typeof import("./comments").createCommentSessio
|
||||
|
||||
beforeAll(async () => {
|
||||
mock.module("@solidjs/router", () => ({
|
||||
useNavigate: () => () => undefined,
|
||||
useParams: () => ({}),
|
||||
}))
|
||||
mock.module("@opencode-ai/ui/context", () => ({
|
||||
|
||||
@@ -108,7 +108,7 @@ describe("encodeFilePath", () => {
|
||||
const url = new URL(fileUrl)
|
||||
expect(url.protocol).toBe("file:")
|
||||
expect(url.pathname).toContain("README.bs.md")
|
||||
expect(result).toBe("/D%3A/dev/projects/opencode/README.bs.md")
|
||||
expect(result).toBe("/D:/dev/projects/opencode/README.bs.md")
|
||||
})
|
||||
|
||||
test("should handle mixed separator path (Windows + Unix)", () => {
|
||||
@@ -118,7 +118,7 @@ describe("encodeFilePath", () => {
|
||||
const fileUrl = `file://${result}`
|
||||
|
||||
expect(() => new URL(fileUrl)).not.toThrow()
|
||||
expect(result).toBe("/D%3A/dev/projects/opencode/README.bs.md")
|
||||
expect(result).toBe("/D:/dev/projects/opencode/README.bs.md")
|
||||
})
|
||||
|
||||
test("should handle Windows path with spaces", () => {
|
||||
@@ -146,7 +146,7 @@ describe("encodeFilePath", () => {
|
||||
const fileUrl = `file://${result}`
|
||||
|
||||
expect(() => new URL(fileUrl)).not.toThrow()
|
||||
expect(result).toBe("/C%3A/")
|
||||
expect(result).toBe("/C:/")
|
||||
})
|
||||
|
||||
test("should handle Windows relative path with backslashes", () => {
|
||||
@@ -177,7 +177,7 @@ describe("encodeFilePath", () => {
|
||||
const fileUrl = `file://${result}`
|
||||
|
||||
expect(() => new URL(fileUrl)).not.toThrow()
|
||||
expect(result).toBe("/c%3A/users/test/file.txt")
|
||||
expect(result).toBe("/c:/users/test/file.txt")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -193,7 +193,7 @@ describe("encodeFilePath", () => {
|
||||
const result = encodeFilePath(windowsPath)
|
||||
// Should convert to forward slashes and add leading /
|
||||
expect(result).not.toContain("\\")
|
||||
expect(result).toMatch(/^\/[A-Za-z]%3A\//)
|
||||
expect(result).toMatch(/^\/[A-Za-z]:\//)
|
||||
})
|
||||
|
||||
test("should handle relative paths the same on all platforms", () => {
|
||||
@@ -237,7 +237,7 @@ describe("encodeFilePath", () => {
|
||||
const result = encodeFilePath(alreadyNormalized)
|
||||
|
||||
// Should not add another leading slash
|
||||
expect(result).toBe("/D%3A/path/file.txt")
|
||||
expect(result).toBe("/D:/path/file.txt")
|
||||
expect(result).not.toContain("//D")
|
||||
})
|
||||
|
||||
@@ -246,7 +246,7 @@ describe("encodeFilePath", () => {
|
||||
const result = encodeFilePath(justDrive)
|
||||
const fileUrl = `file://${result}`
|
||||
|
||||
expect(result).toBe("/D%3A")
|
||||
expect(result).toBe("/D:")
|
||||
expect(() => new URL(fileUrl)).not.toThrow()
|
||||
})
|
||||
|
||||
@@ -256,7 +256,7 @@ describe("encodeFilePath", () => {
|
||||
const fileUrl = `file://${result}`
|
||||
|
||||
expect(() => new URL(fileUrl)).not.toThrow()
|
||||
expect(result).toBe("/C%3A/Users/test/")
|
||||
expect(result).toBe("/C:/Users/test/")
|
||||
})
|
||||
|
||||
test("should handle very long paths", () => {
|
||||
|
||||
@@ -90,9 +90,14 @@ export function encodeFilePath(filepath: string): string {
|
||||
}
|
||||
|
||||
// Encode each path segment (preserving forward slashes as path separators)
|
||||
// Keep the colon in Windows drive letters (`/C:/...`) so downstream file URL parsers
|
||||
// can reliably detect drives.
|
||||
return normalized
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.map((segment, index) => {
|
||||
if (index === 1 && /^[A-Za-z]:$/.test(segment)) return segment
|
||||
return encodeURIComponent(segment)
|
||||
})
|
||||
.join("/")
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,19 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
||||
const platform = usePlatform()
|
||||
const abort = new AbortController()
|
||||
|
||||
const auth = (() => {
|
||||
if (typeof window === "undefined") return
|
||||
const password = window.__OPENCODE__?.serverPassword
|
||||
if (!password) return
|
||||
return {
|
||||
Authorization: `Basic ${btoa(`opencode:${password}`)}`,
|
||||
}
|
||||
})()
|
||||
|
||||
const eventSdk = createOpencodeClient({
|
||||
baseUrl: server.url,
|
||||
signal: abort.signal,
|
||||
fetch: platform.fetch,
|
||||
headers: auth,
|
||||
})
|
||||
const emitter = createGlobalEmitter<{
|
||||
[key: string]: Event
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useSync } from "./sync"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { useModels } from "@/context/models"
|
||||
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
||||
|
||||
export type ModelKey = { providerID: string; modelID: string }
|
||||
|
||||
@@ -184,11 +185,27 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
models.setVisibility(model, visible)
|
||||
},
|
||||
variant: {
|
||||
current() {
|
||||
configured() {
|
||||
const a = agent.current()
|
||||
const m = current()
|
||||
if (!a || !m) return undefined
|
||||
return getConfiguredAgentVariant({
|
||||
agent: { model: a.model, variant: a.variant },
|
||||
model: { providerID: m.provider.id, modelID: m.id, variants: m.variants },
|
||||
})
|
||||
},
|
||||
selected() {
|
||||
const m = current()
|
||||
if (!m) return undefined
|
||||
return models.variant.get({ providerID: m.provider.id, modelID: m.id })
|
||||
},
|
||||
current() {
|
||||
return resolveModelVariant({
|
||||
variants: this.list(),
|
||||
selected: this.selected(),
|
||||
configured: this.configured(),
|
||||
})
|
||||
},
|
||||
list() {
|
||||
const m = current()
|
||||
if (!m) return []
|
||||
@@ -203,17 +220,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
cycle() {
|
||||
const variants = this.list()
|
||||
if (variants.length === 0) return
|
||||
const currentVariant = this.current()
|
||||
if (!currentVariant) {
|
||||
this.set(variants[0])
|
||||
return
|
||||
}
|
||||
const index = variants.indexOf(currentVariant)
|
||||
if (index === -1 || index === variants.length - 1) {
|
||||
this.set(undefined)
|
||||
return
|
||||
}
|
||||
this.set(variants[index + 1])
|
||||
this.set(
|
||||
cycleModelVariant({
|
||||
variants,
|
||||
selected: this.selected(),
|
||||
configured: this.configured(),
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
66
packages/app/src/context/model-variant.test.ts
Normal file
66
packages/app/src/context/model-variant.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
||||
|
||||
describe("model variant", () => {
|
||||
test("resolves configured agent variant when model matches", () => {
|
||||
const value = getConfiguredAgentVariant({
|
||||
agent: {
|
||||
model: { providerID: "openai", modelID: "gpt-5.2" },
|
||||
variant: "xhigh",
|
||||
},
|
||||
model: {
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5.2",
|
||||
variants: { low: {}, high: {}, xhigh: {} },
|
||||
},
|
||||
})
|
||||
|
||||
expect(value).toBe("xhigh")
|
||||
})
|
||||
|
||||
test("ignores configured variant when model does not match", () => {
|
||||
const value = getConfiguredAgentVariant({
|
||||
agent: {
|
||||
model: { providerID: "openai", modelID: "gpt-5.2" },
|
||||
variant: "xhigh",
|
||||
},
|
||||
model: {
|
||||
providerID: "anthropic",
|
||||
modelID: "claude-sonnet-4",
|
||||
variants: { low: {}, high: {}, xhigh: {} },
|
||||
},
|
||||
})
|
||||
|
||||
expect(value).toBeUndefined()
|
||||
})
|
||||
|
||||
test("prefers selected variant over configured variant", () => {
|
||||
const value = resolveModelVariant({
|
||||
variants: ["low", "high", "xhigh"],
|
||||
selected: "high",
|
||||
configured: "xhigh",
|
||||
})
|
||||
|
||||
expect(value).toBe("high")
|
||||
})
|
||||
|
||||
test("cycles from configured variant to next", () => {
|
||||
const value = cycleModelVariant({
|
||||
variants: ["low", "high", "xhigh"],
|
||||
selected: undefined,
|
||||
configured: "high",
|
||||
})
|
||||
|
||||
expect(value).toBe("xhigh")
|
||||
})
|
||||
|
||||
test("wraps from configured last variant to first", () => {
|
||||
const value = cycleModelVariant({
|
||||
variants: ["low", "high", "xhigh"],
|
||||
selected: undefined,
|
||||
configured: "xhigh",
|
||||
})
|
||||
|
||||
expect(value).toBe("low")
|
||||
})
|
||||
})
|
||||
50
packages/app/src/context/model-variant.ts
Normal file
50
packages/app/src/context/model-variant.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
type AgentModel = {
|
||||
providerID: string
|
||||
modelID: string
|
||||
}
|
||||
|
||||
type Agent = {
|
||||
model?: AgentModel
|
||||
variant?: string
|
||||
}
|
||||
|
||||
type Model = AgentModel & {
|
||||
variants?: Record<string, unknown>
|
||||
}
|
||||
|
||||
type VariantInput = {
|
||||
variants: string[]
|
||||
selected: string | undefined
|
||||
configured: string | undefined
|
||||
}
|
||||
|
||||
export function getConfiguredAgentVariant(input: { agent: Agent | undefined; model: Model | undefined }) {
|
||||
if (!input.agent?.variant) return undefined
|
||||
if (!input.agent.model) return undefined
|
||||
if (!input.model?.variants) return undefined
|
||||
if (input.agent.model.providerID !== input.model.providerID) return undefined
|
||||
if (input.agent.model.modelID !== input.model.modelID) return undefined
|
||||
if (!(input.agent.variant in input.model.variants)) return undefined
|
||||
return input.agent.variant
|
||||
}
|
||||
|
||||
export function resolveModelVariant(input: VariantInput) {
|
||||
if (input.selected && input.variants.includes(input.selected)) return input.selected
|
||||
if (input.configured && input.variants.includes(input.configured)) return input.configured
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function cycleModelVariant(input: VariantInput) {
|
||||
if (input.variants.length === 0) return undefined
|
||||
if (input.selected && input.variants.includes(input.selected)) {
|
||||
const index = input.variants.indexOf(input.selected)
|
||||
if (index === input.variants.length - 1) return undefined
|
||||
return input.variants[index + 1]
|
||||
}
|
||||
if (input.configured && input.variants.includes(input.configured)) {
|
||||
const index = input.variants.indexOf(input.configured)
|
||||
if (index === input.variants.length - 1) return input.variants[0]
|
||||
return input.variants[index + 1]
|
||||
}
|
||||
return input.variants[0]
|
||||
}
|
||||
@@ -5,6 +5,7 @@ let getLegacyTerminalStorageKeys: (dir: string, legacySessionID?: string) => str
|
||||
|
||||
beforeAll(async () => {
|
||||
mock.module("@solidjs/router", () => ({
|
||||
useNavigate: () => () => undefined,
|
||||
useParams: () => ({}),
|
||||
}))
|
||||
mock.module("@opencode-ai/ui/context", () => ({
|
||||
|
||||
@@ -13,7 +13,7 @@ export type LocalPTY = {
|
||||
cols?: number
|
||||
buffer?: string
|
||||
scrollY?: number
|
||||
tail?: string
|
||||
cursor?: number
|
||||
}
|
||||
|
||||
const WORKSPACE_KEY = "__workspace__"
|
||||
|
||||
@@ -54,6 +54,13 @@ export default function Layout(props: ParentProps) {
|
||||
navigate(`/${params.dir}/session/${sessionID}`)
|
||||
}
|
||||
|
||||
const sessionHref = (sessionID: string) => {
|
||||
if (params.dir) return `/${params.dir}/session/${sessionID}`
|
||||
return `/session/${sessionID}`
|
||||
}
|
||||
|
||||
const syncSession = (sessionID: string) => sync.session.sync(sessionID)
|
||||
|
||||
return (
|
||||
<DataProvider
|
||||
data={sync.data}
|
||||
@@ -62,6 +69,8 @@ export default function Layout(props: ParentProps) {
|
||||
onQuestionReply={replyToQuestion}
|
||||
onQuestionReject={rejectQuestion}
|
||||
onNavigateToSession={navigateToSession}
|
||||
onSessionHref={sessionHref}
|
||||
onSyncSession={syncSession}
|
||||
>
|
||||
<LocalProvider>{props.children}</LocalProvider>
|
||||
</DataProvider>
|
||||
|
||||
@@ -25,7 +25,8 @@ export default function Home() {
|
||||
const homedir = createMemo(() => sync.data.path.home)
|
||||
const recent = createMemo(() => {
|
||||
return sync.data.project
|
||||
.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
|
||||
.slice()
|
||||
.sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
|
||||
.slice(0, 5)
|
||||
})
|
||||
|
||||
|
||||
@@ -181,20 +181,6 @@ export default function Layout(props: ParentProps) {
|
||||
aim.reset()
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => ({ dir: params.dir, id: params.id }),
|
||||
() => {
|
||||
if (layout.sidebar.opened()) return
|
||||
if (!state.hoverProject) return
|
||||
aim.reset()
|
||||
setState("hoverSession", undefined)
|
||||
setState("hoverProject", undefined)
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const autoselecting = createMemo(() => {
|
||||
if (params.dir) return false
|
||||
if (!state.autoselect) return false
|
||||
@@ -1272,8 +1258,6 @@ export default function Layout(props: ParentProps) {
|
||||
),
|
||||
)
|
||||
|
||||
await globalSDK.client.instance.dispose({ directory }).catch(() => undefined)
|
||||
|
||||
setBusy(directory, false)
|
||||
dismiss()
|
||||
|
||||
@@ -1938,7 +1922,7 @@ export default function Layout(props: ParentProps) {
|
||||
direction="horizontal"
|
||||
size={layout.sidebar.width()}
|
||||
min={244}
|
||||
max={window.innerWidth * 0.3 + 64}
|
||||
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
|
||||
collapseThreshold={244}
|
||||
onResize={layout.sidebar.resize}
|
||||
onCollapse={layout.sidebar.close}
|
||||
|
||||
@@ -26,7 +26,7 @@ export const isRootVisibleSession = (session: Session, directory: string) =>
|
||||
workspaceKey(session.directory) === workspaceKey(directory) && !session.parentID && !session.time?.archived
|
||||
|
||||
export const sortedRootSessions = (store: { session: Session[]; path: { directory: string } }, now: number) =>
|
||||
store.session.filter((session) => isRootVisibleSession(session, store.path.directory)).toSorted(sortSessions(now))
|
||||
store.session.filter((session) => isRootVisibleSession(session, store.path.directory)).sort(sortSessions(now))
|
||||
|
||||
export const childMapByParent = (sessions: Session[]) => {
|
||||
const map = new Map<string, string[]>()
|
||||
|
||||
@@ -118,7 +118,7 @@ export const SortableWorkspace = (props: {
|
||||
const touch = createMediaQuery("(hover: none)")
|
||||
const showNew = createMemo(() => !loading() && (touch() || sessions().length === 0 || (active() && !params.id)))
|
||||
const loadMore = async () => {
|
||||
setWorkspaceStore("limit", (limit) => limit + 5)
|
||||
setWorkspaceStore("limit", (limit) => (limit ?? 0) + 5)
|
||||
await globalSync.project.loadSessions(props.directory)
|
||||
}
|
||||
|
||||
@@ -368,7 +368,7 @@ export const LocalWorkspace = (props: {
|
||||
const loading = createMemo(() => !booted() && sessions().length === 0)
|
||||
const hasMore = createMemo(() => workspace().store.sessionTotal > sessions().length)
|
||||
const loadMore = async () => {
|
||||
workspace().setStore("limit", (limit) => limit + 5)
|
||||
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
|
||||
await globalSync.project.loadSessions(props.project.worktree)
|
||||
}
|
||||
|
||||
|
||||
@@ -1026,10 +1026,31 @@ export default function Page() {
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div class={input.emptyClass}>
|
||||
<Mark class="w-14 opacity-10" />
|
||||
<div class="text-14-regular text-text-weak max-w-56">{language.t("session.review.empty")}</div>
|
||||
</div>
|
||||
<SessionReviewTab
|
||||
title={changesTitle()}
|
||||
empty={
|
||||
store.changes === "turn" ? (
|
||||
emptyTurn()
|
||||
) : (
|
||||
<div class={input.emptyClass}>
|
||||
<Mark class="w-14 opacity-10" />
|
||||
<div class="text-14-regular text-text-weak max-w-56">{language.t("session.review.empty")}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
diffs={reviewDiffs}
|
||||
view={view}
|
||||
diffStyle={input.diffStyle}
|
||||
onDiffStyleChange={input.onDiffStyleChange}
|
||||
onScrollRef={(el) => setTree("reviewScroll", el)}
|
||||
focusedFile={tree.activeDiff}
|
||||
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
||||
comments={comments.all()}
|
||||
focusedComment={comments.focus()}
|
||||
onFocusedCommentChange={comments.setFocus}
|
||||
onViewFile={openReviewFile}
|
||||
classes={input.classes}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
@@ -1041,7 +1062,7 @@ export default function Page() {
|
||||
diffStyle: layout.review.diffStyle(),
|
||||
onDiffStyleChange: layout.review.setDiffStyle,
|
||||
loadingClass: "px-6 py-4 text-text-weak",
|
||||
emptyClass: "h-full px-6 pb-30 flex flex-col items-center justify-center text-center gap-6",
|
||||
emptyClass: "h-full pb-30 flex flex-col items-center justify-center text-center gap-6",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1569,7 +1590,7 @@ export default function Page() {
|
||||
container: "px-4",
|
||||
},
|
||||
loadingClass: "px-4 py-4 text-text-weak",
|
||||
emptyClass: "h-full px-4 pb-30 flex flex-col items-center justify-center text-center gap-6",
|
||||
emptyClass: "h-full pb-30 flex flex-col items-center justify-center text-center gap-6",
|
||||
})}
|
||||
scroll={ui.scroll}
|
||||
onResumeScroll={resumeScroll}
|
||||
@@ -1683,7 +1704,7 @@ export default function Page() {
|
||||
direction="horizontal"
|
||||
size={layout.session.width()}
|
||||
min={450}
|
||||
max={window.innerWidth * 0.45}
|
||||
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.45}
|
||||
onResize={layout.session.resize}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
@@ -228,6 +228,7 @@ export const createScrollSpy = (input: Input) => {
|
||||
node.delete(key)
|
||||
visible.delete(key)
|
||||
dirty = true
|
||||
schedule()
|
||||
}
|
||||
|
||||
const markDirty = () => {
|
||||
|
||||
@@ -41,7 +41,7 @@ export function TerminalPanel(props: {
|
||||
direction="vertical"
|
||||
size={props.height}
|
||||
min={100}
|
||||
max={window.innerHeight * 0.6}
|
||||
max={typeof window === "undefined" ? 1000 : window.innerHeight * 0.6}
|
||||
collapseThreshold={50}
|
||||
onResize={props.resize}
|
||||
onCollapse={props.close}
|
||||
|
||||
@@ -365,48 +365,81 @@ export const useSessionCommands = (input: {
|
||||
return [
|
||||
{
|
||||
id: "session.share",
|
||||
title: input.info()?.share?.url ? "Copy share link" : input.language.t("command.session.share"),
|
||||
title: input.info()?.share?.url
|
||||
? input.language.t("session.share.copy.copyLink")
|
||||
: input.language.t("command.session.share"),
|
||||
description: input.info()?.share?.url
|
||||
? "Copy share URL to clipboard"
|
||||
? input.language.t("toast.session.share.success.description")
|
||||
: input.language.t("command.session.share.description"),
|
||||
category: input.language.t("command.category.session"),
|
||||
slash: "share",
|
||||
disabled: !input.params.id,
|
||||
onSelect: async () => {
|
||||
if (!input.params.id) return
|
||||
const copy = (url: string, existing: boolean) =>
|
||||
navigator.clipboard
|
||||
.writeText(url)
|
||||
.then(() =>
|
||||
showToast({
|
||||
title: existing
|
||||
? input.language.t("session.share.copy.copied")
|
||||
: input.language.t("toast.session.share.success.title"),
|
||||
description: input.language.t("toast.session.share.success.description"),
|
||||
variant: "success",
|
||||
}),
|
||||
)
|
||||
.catch(() =>
|
||||
showToast({
|
||||
title: input.language.t("toast.session.share.copyFailed.title"),
|
||||
variant: "error",
|
||||
}),
|
||||
)
|
||||
const url = input.info()?.share?.url
|
||||
if (url) {
|
||||
await copy(url, true)
|
||||
|
||||
const write = (value: string) => {
|
||||
const body = typeof document === "undefined" ? undefined : document.body
|
||||
if (body) {
|
||||
const textarea = document.createElement("textarea")
|
||||
textarea.value = value
|
||||
textarea.setAttribute("readonly", "")
|
||||
textarea.style.position = "fixed"
|
||||
textarea.style.opacity = "0"
|
||||
textarea.style.pointerEvents = "none"
|
||||
body.appendChild(textarea)
|
||||
textarea.select()
|
||||
const copied = document.execCommand("copy")
|
||||
body.removeChild(textarea)
|
||||
if (copied) return Promise.resolve(true)
|
||||
}
|
||||
|
||||
const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard
|
||||
if (!clipboard?.writeText) return Promise.resolve(false)
|
||||
return clipboard.writeText(value).then(
|
||||
() => true,
|
||||
() => false,
|
||||
)
|
||||
}
|
||||
|
||||
const copy = async (url: string, existing: boolean) => {
|
||||
const ok = await write(url)
|
||||
if (!ok) {
|
||||
showToast({
|
||||
title: input.language.t("toast.session.share.copyFailed.title"),
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
showToast({
|
||||
title: existing
|
||||
? input.language.t("session.share.copy.copied")
|
||||
: input.language.t("toast.session.share.success.title"),
|
||||
description: input.language.t("toast.session.share.success.description"),
|
||||
variant: "success",
|
||||
})
|
||||
}
|
||||
|
||||
const existing = input.info()?.share?.url
|
||||
if (existing) {
|
||||
await copy(existing, true)
|
||||
return
|
||||
}
|
||||
await input.sdk.client.session
|
||||
|
||||
const url = await input.sdk.client.session
|
||||
.share({ sessionID: input.params.id })
|
||||
.then((res) => copy(res.data!.share!.url, false))
|
||||
.catch(() =>
|
||||
showToast({
|
||||
title: input.language.t("toast.session.share.failed.title"),
|
||||
description: input.language.t("toast.session.share.failed.description"),
|
||||
variant: "error",
|
||||
}),
|
||||
)
|
||||
.then((res) => res.data?.share?.url)
|
||||
.catch(() => undefined)
|
||||
if (!url) {
|
||||
showToast({
|
||||
title: input.language.t("toast.session.share.failed.title"),
|
||||
description: input.language.t("toast.session.share.failed.description"),
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await copy(url, false)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -99,4 +99,9 @@ describe("persist localStorage resilience", () => {
|
||||
|
||||
expect(storage.getItem("direct-value")).toBe('{"value":5}')
|
||||
})
|
||||
|
||||
test("normalizer rejects malformed JSON payloads", () => {
|
||||
const result = persistTesting.normalize({ value: "ok" }, '{"value":"\\x"}')
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -195,6 +195,14 @@ function parse(value: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalize(defaults: unknown, raw: string, migrate?: (value: unknown) => unknown) {
|
||||
const parsed = parse(raw)
|
||||
if (parsed === undefined) return
|
||||
const migrated = migrate ? migrate(parsed) : parsed
|
||||
const merged = merge(defaults, migrated)
|
||||
return JSON.stringify(merged)
|
||||
}
|
||||
|
||||
function workspaceStorage(dir: string) {
|
||||
const head = dir.slice(0, 12) || "workspace"
|
||||
const sum = checksum(dir) ?? "0"
|
||||
@@ -291,6 +299,7 @@ function localStorageDirect(): SyncStorage {
|
||||
export const PersistTesting = {
|
||||
localStorageDirect,
|
||||
localStorageWithPrefix,
|
||||
normalize,
|
||||
}
|
||||
|
||||
export const Persist = {
|
||||
@@ -358,12 +367,11 @@ export function persisted<T>(
|
||||
getItem: (key) => {
|
||||
const raw = current.getItem(key)
|
||||
if (raw !== null) {
|
||||
const parsed = parse(raw)
|
||||
if (parsed === undefined) return raw
|
||||
|
||||
const migrated = config.migrate ? config.migrate(parsed) : parsed
|
||||
const merged = merge(defaults, migrated)
|
||||
const next = JSON.stringify(merged)
|
||||
const next = normalize(defaults, raw, config.migrate)
|
||||
if (next === undefined) {
|
||||
current.removeItem(key)
|
||||
return null
|
||||
}
|
||||
if (raw !== next) current.setItem(key, next)
|
||||
return next
|
||||
}
|
||||
@@ -372,16 +380,13 @@ export function persisted<T>(
|
||||
const legacyRaw = legacyStore.getItem(legacyKey)
|
||||
if (legacyRaw === null) continue
|
||||
|
||||
current.setItem(key, legacyRaw)
|
||||
const next = normalize(defaults, legacyRaw, config.migrate)
|
||||
if (next === undefined) {
|
||||
legacyStore.removeItem(legacyKey)
|
||||
continue
|
||||
}
|
||||
current.setItem(key, next)
|
||||
legacyStore.removeItem(legacyKey)
|
||||
|
||||
const parsed = parse(legacyRaw)
|
||||
if (parsed === undefined) return legacyRaw
|
||||
|
||||
const migrated = config.migrate ? config.migrate(parsed) : parsed
|
||||
const merged = merge(defaults, migrated)
|
||||
const next = JSON.stringify(merged)
|
||||
if (legacyRaw !== next) current.setItem(key, next)
|
||||
return next
|
||||
}
|
||||
|
||||
@@ -405,12 +410,11 @@ export function persisted<T>(
|
||||
getItem: async (key) => {
|
||||
const raw = await current.getItem(key)
|
||||
if (raw !== null) {
|
||||
const parsed = parse(raw)
|
||||
if (parsed === undefined) return raw
|
||||
|
||||
const migrated = config.migrate ? config.migrate(parsed) : parsed
|
||||
const merged = merge(defaults, migrated)
|
||||
const next = JSON.stringify(merged)
|
||||
const next = normalize(defaults, raw, config.migrate)
|
||||
if (next === undefined) {
|
||||
await current.removeItem(key).catch(() => undefined)
|
||||
return null
|
||||
}
|
||||
if (raw !== next) await current.setItem(key, next)
|
||||
return next
|
||||
}
|
||||
@@ -421,16 +425,13 @@ export function persisted<T>(
|
||||
const legacyRaw = await legacyStore.getItem(legacyKey)
|
||||
if (legacyRaw === null) continue
|
||||
|
||||
await current.setItem(key, legacyRaw)
|
||||
const next = normalize(defaults, legacyRaw, config.migrate)
|
||||
if (next === undefined) {
|
||||
await legacyStore.removeItem(legacyKey).catch(() => undefined)
|
||||
continue
|
||||
}
|
||||
await current.setItem(key, next)
|
||||
await legacyStore.removeItem(legacyKey)
|
||||
|
||||
const parsed = parse(legacyRaw)
|
||||
if (parsed === undefined) return legacyRaw
|
||||
|
||||
const migrated = config.migrate ? config.migrate(parsed) : parsed
|
||||
const merged = merge(defaults, migrated)
|
||||
const next = JSON.stringify(merged)
|
||||
if (legacyRaw !== next) await current.setItem(key, next)
|
||||
return next
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -9,8 +9,8 @@ export const config = {
|
||||
github: {
|
||||
repoUrl: "https://github.com/anomalyco/opencode",
|
||||
starsFormatted: {
|
||||
compact: "95K",
|
||||
full: "95,000",
|
||||
compact: "100K",
|
||||
full: "100,000",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -22,8 +22,8 @@ export const config = {
|
||||
|
||||
// Static stats (used on landing page)
|
||||
stats: {
|
||||
contributors: "650",
|
||||
commits: "8,500",
|
||||
contributors: "700",
|
||||
commits: "9,000",
|
||||
monthlyUsers: "2.5M",
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
export class AuthError extends Error {}
|
||||
export class CreditsError extends Error {}
|
||||
export class MonthlyLimitError extends Error {}
|
||||
export class SubscriptionError extends Error {
|
||||
export class UserLimitError extends Error {}
|
||||
export class ModelError extends Error {}
|
||||
|
||||
class LimitError extends Error {
|
||||
retryAfter?: number
|
||||
constructor(message: string, retryAfter?: number) {
|
||||
super(message)
|
||||
this.retryAfter = retryAfter
|
||||
}
|
||||
}
|
||||
export class UserLimitError extends Error {}
|
||||
export class ModelError extends Error {}
|
||||
export class RateLimitError extends Error {}
|
||||
export class FreeUsageLimitError extends LimitError {}
|
||||
export class SubscriptionUsageLimitError extends LimitError {}
|
||||
|
||||
@@ -18,10 +18,10 @@ import {
|
||||
AuthError,
|
||||
CreditsError,
|
||||
MonthlyLimitError,
|
||||
SubscriptionError,
|
||||
UserLimitError,
|
||||
ModelError,
|
||||
RateLimitError,
|
||||
FreeUsageLimitError,
|
||||
SubscriptionUsageLimitError,
|
||||
} from "./error"
|
||||
import { createBodyConverter, createStreamPartConverter, createResponseConverter, UsageInfo } from "./provider/provider"
|
||||
import { anthropicHelper } from "./provider/anthropic"
|
||||
@@ -52,7 +52,8 @@ export async function handler(
|
||||
type ModelInfo = Awaited<ReturnType<typeof validateModel>>
|
||||
type ProviderInfo = Awaited<ReturnType<typeof selectProvider>>
|
||||
|
||||
const MAX_RETRIES = 3
|
||||
const MAX_FAILOVER_RETRIES = 3
|
||||
const MAX_429_RETRIES = 3
|
||||
const FREE_WORKSPACES = [
|
||||
"wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
|
||||
"wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // opencode bench
|
||||
@@ -111,7 +112,7 @@ export async function handler(
|
||||
)
|
||||
logger.debug("REQUEST URL: " + reqUrl)
|
||||
logger.debug("REQUEST: " + reqBody.substring(0, 300) + "...")
|
||||
const res = await fetch(reqUrl, {
|
||||
const res = await fetchWith429Retry(reqUrl, {
|
||||
method: "POST",
|
||||
headers: (() => {
|
||||
const headers = new Headers(input.request.headers)
|
||||
@@ -133,20 +134,26 @@ export async function handler(
|
||||
body: reqBody,
|
||||
})
|
||||
|
||||
// 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,
|
||||
if (res.status !== 200) {
|
||||
logger.metric({
|
||||
"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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { providerInfo, reqBody, res, startTimestamp }
|
||||
@@ -304,9 +311,9 @@ export async function handler(
|
||||
{ status: 401 },
|
||||
)
|
||||
|
||||
if (error instanceof RateLimitError || error instanceof SubscriptionError) {
|
||||
if (error instanceof FreeUsageLimitError || error instanceof SubscriptionUsageLimitError) {
|
||||
const headers = new Headers()
|
||||
if (error instanceof SubscriptionError && error.retryAfter) {
|
||||
if (error.retryAfter) {
|
||||
headers.set("retry-after", String(error.retryAfter))
|
||||
}
|
||||
return new Response(
|
||||
@@ -369,7 +376,7 @@ export async function handler(
|
||||
if (provider) return provider
|
||||
}
|
||||
|
||||
if (retry.retryCount === MAX_RETRIES) {
|
||||
if (retry.retryCount === MAX_FAILOVER_RETRIES) {
|
||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
|
||||
}
|
||||
|
||||
@@ -520,7 +527,7 @@ export async function handler(
|
||||
timeUpdated: sub.timeFixedUpdated,
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionError(
|
||||
throw new SubscriptionUsageLimitError(
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
|
||||
result.resetInSec,
|
||||
)
|
||||
@@ -534,7 +541,7 @@ export async function handler(
|
||||
timeUpdated: sub.timeRollingUpdated,
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionError(
|
||||
throw new SubscriptionUsageLimitError(
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
|
||||
result.resetInSec,
|
||||
)
|
||||
@@ -597,6 +604,15 @@ export async function handler(
|
||||
providerInfo.apiKey = authInfo.provider.credentials
|
||||
}
|
||||
|
||||
async function fetchWith429Retry(url: string, options: RequestInit, retry = { count: 0 }) {
|
||||
const res = await fetch(url, options)
|
||||
if (res.status === 429 && retry.count < MAX_429_RETRIES) {
|
||||
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, retry.count) * 500))
|
||||
return fetchWith429Retry(url, options, { count: retry.count + 1 })
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
async function trackUsage(
|
||||
authInfo: AuthInfo,
|
||||
modelInfo: ModelInfo,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Database, eq, and, sql, inArray } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { IpRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
|
||||
import { RateLimitError } from "./error"
|
||||
import { FreeUsageLimitError } from "./error"
|
||||
import { logger } from "./logger"
|
||||
import { ZenData } from "@opencode-ai/console-core/model.js"
|
||||
|
||||
@@ -28,17 +28,46 @@ export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: s
|
||||
check: async () => {
|
||||
const rows = await Database.use((tx) =>
|
||||
tx
|
||||
.select({ count: IpRateLimitTable.count })
|
||||
.select({ interval: IpRateLimitTable.interval, count: IpRateLimitTable.count })
|
||||
.from(IpRateLimitTable)
|
||||
.where(and(eq(IpRateLimitTable.ip, ip), inArray(IpRateLimitTable.interval, intervals))),
|
||||
)
|
||||
const total = rows.reduce((sum, r) => sum + r.count, 0)
|
||||
logger.debug(`rate limit total: ${total}`)
|
||||
if (total >= limitValue) throw new RateLimitError(`Rate limit exceeded. Please try again later.`)
|
||||
if (total >= limitValue)
|
||||
throw new FreeUsageLimitError(
|
||||
`Rate limit exceeded. Please try again later.`,
|
||||
limit.period === "day" ? getRetryAfterDay(now) : getRetryAfterHour(rows, intervals, limitValue, now),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function getRetryAfterDay(now: number) {
|
||||
return Math.ceil((86_400_000 - (now % 86_400_000)) / 1000)
|
||||
}
|
||||
|
||||
export function getRetryAfterHour(
|
||||
rows: { interval: string; count: number }[],
|
||||
intervals: string[],
|
||||
limit: number,
|
||||
now: number,
|
||||
) {
|
||||
const counts = new Map(rows.map((r) => [r.interval, r.count]))
|
||||
// intervals are ordered newest to oldest: [current, -1h, -2h]
|
||||
// simulate dropping oldest intervals one at a time
|
||||
let running = intervals.reduce((sum, i) => sum + (counts.get(i) ?? 0), 0)
|
||||
for (let i = intervals.length - 1; i >= 0; i--) {
|
||||
running -= counts.get(intervals[i]) ?? 0
|
||||
if (running < limit) {
|
||||
// interval at index i rolls out of the window (intervals.length - i) hours from the current hour start
|
||||
const hours = intervals.length - i
|
||||
return Math.ceil((hours * 3_600_000 - (now % 3_600_000)) / 1000)
|
||||
}
|
||||
}
|
||||
return Math.ceil((3_600_000 - (now % 3_600_000)) / 1000)
|
||||
}
|
||||
|
||||
function buildYYYYMMDD(timestamp: number) {
|
||||
return new Date(timestamp)
|
||||
.toISOString()
|
||||
|
||||
92
packages/console/app/test/rateLimiter.test.ts
Normal file
92
packages/console/app/test/rateLimiter.test.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { getRetryAfterDay, getRetryAfterHour } from "../src/routes/zen/util/rateLimiter"
|
||||
|
||||
describe("getRetryAfterDay", () => {
|
||||
test("returns full day at midnight UTC", () => {
|
||||
const midnight = Date.UTC(2026, 0, 15, 0, 0, 0, 0)
|
||||
expect(getRetryAfterDay(midnight)).toBe(86_400)
|
||||
})
|
||||
|
||||
test("returns remaining seconds until next UTC day", () => {
|
||||
const noon = Date.UTC(2026, 0, 15, 12, 0, 0, 0)
|
||||
expect(getRetryAfterDay(noon)).toBe(43_200)
|
||||
})
|
||||
|
||||
test("rounds up to nearest second", () => {
|
||||
const almost = Date.UTC(2026, 0, 15, 23, 59, 59, 500)
|
||||
expect(getRetryAfterDay(almost)).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getRetryAfterHour", () => {
|
||||
// 14:30:00 UTC — 30 minutes into the current hour
|
||||
const now = Date.UTC(2026, 0, 15, 14, 30, 0, 0)
|
||||
const intervals = ["2026011514", "2026011513", "2026011512"]
|
||||
|
||||
test("waits 3 hours when all usage is in current hour", () => {
|
||||
const rows = [{ interval: "2026011514", count: 10 }]
|
||||
// only current hour has usage — it won't leave the window for 3 hours from hour start
|
||||
// 3 * 3600 - 1800 = 9000s
|
||||
expect(getRetryAfterHour(rows, intervals, 10, now)).toBe(9000)
|
||||
})
|
||||
|
||||
test("waits 1 hour when dropping oldest interval is sufficient", () => {
|
||||
const rows = [
|
||||
{ interval: "2026011514", count: 2 },
|
||||
{ interval: "2026011512", count: 10 },
|
||||
]
|
||||
// total=12, drop oldest (-2h, count=10) -> 2 < 10
|
||||
// hours = 3 - 2 = 1 -> 1 * 3600 - 1800 = 1800s
|
||||
expect(getRetryAfterHour(rows, intervals, 10, now)).toBe(1800)
|
||||
})
|
||||
|
||||
test("waits 2 hours when usage spans oldest two intervals", () => {
|
||||
const rows = [
|
||||
{ interval: "2026011513", count: 8 },
|
||||
{ interval: "2026011512", count: 5 },
|
||||
]
|
||||
// total=13, drop -2h (5) -> 8, 8 >= 8, drop -1h (8) -> 0 < 8
|
||||
// hours = 3 - 1 = 2 -> 2 * 3600 - 1800 = 5400s
|
||||
expect(getRetryAfterHour(rows, intervals, 8, now)).toBe(5400)
|
||||
})
|
||||
|
||||
test("waits 1 hour when oldest interval alone pushes over limit", () => {
|
||||
const rows = [
|
||||
{ interval: "2026011514", count: 1 },
|
||||
{ interval: "2026011513", count: 1 },
|
||||
{ interval: "2026011512", count: 10 },
|
||||
]
|
||||
// total=12, drop -2h (10) -> 2 < 10
|
||||
// hours = 3 - 2 = 1 -> 1800s
|
||||
expect(getRetryAfterHour(rows, intervals, 10, now)).toBe(1800)
|
||||
})
|
||||
|
||||
test("waits 2 hours when middle interval keeps total over limit", () => {
|
||||
const rows = [
|
||||
{ interval: "2026011514", count: 4 },
|
||||
{ interval: "2026011513", count: 4 },
|
||||
{ interval: "2026011512", count: 4 },
|
||||
]
|
||||
// total=12, drop -2h (4) -> 8, 8 >= 5, drop -1h (4) -> 4 < 5
|
||||
// hours = 3 - 1 = 2 -> 5400s
|
||||
expect(getRetryAfterHour(rows, intervals, 5, now)).toBe(5400)
|
||||
})
|
||||
|
||||
test("rounds up to nearest second", () => {
|
||||
const offset = Date.UTC(2026, 0, 15, 14, 30, 0, 500)
|
||||
const rows = [
|
||||
{ interval: "2026011514", count: 2 },
|
||||
{ interval: "2026011512", count: 10 },
|
||||
]
|
||||
// hours=1 -> 3_600_000 - 1_800_500 = 1_799_500ms -> ceil(1799.5) = 1800
|
||||
expect(getRetryAfterHour(rows, intervals, 10, offset)).toBe(1800)
|
||||
})
|
||||
|
||||
test("fallback returns time until next hour when rows are empty", () => {
|
||||
// edge case: rows empty but function called (shouldn't happen in practice)
|
||||
// loop drops all zeros, running stays 0 which is < any positive limit on first iteration
|
||||
const rows: { interval: string; count: number }[] = []
|
||||
// drop -2h (0) -> 0 < 1 -> hours = 3 - 2 = 1 -> 1800s
|
||||
expect(getRetryAfterHour(rows, intervals, 1, now)).toBe(1800)
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -19,6 +19,7 @@
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"exports": {
|
||||
"./*.js": "./src/*.ts",
|
||||
"./*": "./src/*"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -17,8 +17,7 @@ export default {
|
||||
)
|
||||
return
|
||||
|
||||
let metrics = {
|
||||
event_type: "completions",
|
||||
let data = {
|
||||
"cf.continent": event.event.request.cf?.continent,
|
||||
"cf.country": event.event.request.cf?.country,
|
||||
"cf.city": event.event.request.cf?.city,
|
||||
@@ -31,22 +30,28 @@ export default {
|
||||
status: event.event.response?.status ?? 0,
|
||||
ip: event.event.request.headers["x-real-ip"],
|
||||
}
|
||||
const time = new Date(event.eventTimestamp ?? Date.now()).toISOString()
|
||||
const events = []
|
||||
for (const log of event.logs) {
|
||||
for (const message of log.message) {
|
||||
if (!message.startsWith("_metric:")) continue
|
||||
metrics = { ...metrics, ...JSON.parse(message.slice(8)) }
|
||||
const json = JSON.parse(message.slice(8))
|
||||
data = { ...data, ...json }
|
||||
if ("llm.error.code" in json) {
|
||||
events.push({ time, data: { ...data, event_type: "llm.error" } })
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify(metrics, null, 2))
|
||||
events.push({ time, data: { ...data, event_type: "completions" } })
|
||||
console.log(JSON.stringify(data, null, 2))
|
||||
|
||||
const ret = await fetch("https://api.honeycomb.io/1/events/zen", {
|
||||
const ret = await fetch("https://api.honeycomb.io/1/batch/zen", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Honeycomb-Event-Time": (event.eventTimestamp ?? Date.now()).toString(),
|
||||
"X-Honeycomb-Team": Resource.HONEYCOMB_API_KEY.value,
|
||||
},
|
||||
body: JSON.stringify(metrics),
|
||||
body: JSON.stringify(events),
|
||||
})
|
||||
console.log(ret.status)
|
||||
console.log(await ret.text())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"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.53",
|
||||
"version": "1.1.56",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -20,9 +20,9 @@ use std::{
|
||||
env,
|
||||
net::TcpListener,
|
||||
path::PathBuf,
|
||||
process::Command,
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
process::Command,
|
||||
};
|
||||
use tauri::{AppHandle, Manager, RunEvent, State, ipc::Channel};
|
||||
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
||||
@@ -152,12 +152,12 @@ fn check_app_exists(app_name: &str) -> bool {
|
||||
{
|
||||
check_windows_app(app_name)
|
||||
}
|
||||
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
check_macos_app(app_name)
|
||||
}
|
||||
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
check_linux_app(app_name)
|
||||
@@ -165,11 +165,165 @@ fn check_app_exists(app_name: &str) -> bool {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn check_windows_app(app_name: &str) -> bool {
|
||||
fn check_windows_app(_app_name: &str) -> bool {
|
||||
// Check if command exists in PATH, including .exe
|
||||
return true;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn resolve_windows_app_path(app_name: &str) -> Option<String> {
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
// Try to find the command using 'where'
|
||||
let output = Command::new("where").arg(app_name).output().ok()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let paths = String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.map(PathBuf::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let has_ext = |path: &Path, ext: &str| {
|
||||
path.extension()
|
||||
.and_then(|v| v.to_str())
|
||||
.map(|v| v.eq_ignore_ascii_case(ext))
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
if let Some(path) = paths.iter().find(|path| has_ext(path, "exe")) {
|
||||
return Some(path.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
let resolve_cmd = |path: &Path| -> Option<String> {
|
||||
let content = std::fs::read_to_string(path).ok()?;
|
||||
|
||||
for token in content.split('"') {
|
||||
let lower = token.to_ascii_lowercase();
|
||||
if !lower.contains(".exe") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(index) = lower.find("%~dp0") {
|
||||
let base = path.parent()?;
|
||||
let suffix = &token[index + 5..];
|
||||
let mut resolved = PathBuf::from(base);
|
||||
|
||||
for part in suffix.replace('/', "\\").split('\\') {
|
||||
if part.is_empty() || part == "." {
|
||||
continue;
|
||||
}
|
||||
if part == ".." {
|
||||
let _ = resolved.pop();
|
||||
continue;
|
||||
}
|
||||
resolved.push(part);
|
||||
}
|
||||
|
||||
if resolved.exists() {
|
||||
return Some(resolved.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let resolved = PathBuf::from(token);
|
||||
if resolved.exists() {
|
||||
return Some(resolved.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
};
|
||||
|
||||
for path in &paths {
|
||||
if has_ext(path, "cmd") || has_ext(path, "bat") {
|
||||
if let Some(resolved) = resolve_cmd(path) {
|
||||
return Some(resolved);
|
||||
}
|
||||
}
|
||||
|
||||
if path.extension().is_none() {
|
||||
let cmd = path.with_extension("cmd");
|
||||
if cmd.exists() {
|
||||
if let Some(resolved) = resolve_cmd(&cmd) {
|
||||
return Some(resolved);
|
||||
}
|
||||
}
|
||||
|
||||
let bat = path.with_extension("bat");
|
||||
if bat.exists() {
|
||||
if let Some(resolved) = resolve_cmd(&bat) {
|
||||
return Some(resolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let key = app_name
|
||||
.chars()
|
||||
.filter(|v| v.is_ascii_alphanumeric())
|
||||
.flat_map(|v| v.to_lowercase())
|
||||
.collect::<String>();
|
||||
|
||||
if !key.is_empty() {
|
||||
for path in &paths {
|
||||
let dirs = [
|
||||
path.parent(),
|
||||
path.parent().and_then(|dir| dir.parent()),
|
||||
path.parent()
|
||||
.and_then(|dir| dir.parent())
|
||||
.and_then(|dir| dir.parent()),
|
||||
];
|
||||
|
||||
for dir in dirs.into_iter().flatten() {
|
||||
if let Ok(entries) = std::fs::read_dir(dir) {
|
||||
for entry in entries.flatten() {
|
||||
let candidate = entry.path();
|
||||
if !has_ext(&candidate, "exe") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(stem) = candidate.file_stem().and_then(|v| v.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let name = stem
|
||||
.chars()
|
||||
.filter(|v| v.is_ascii_alphanumeric())
|
||||
.flat_map(|v| v.to_lowercase())
|
||||
.collect::<String>();
|
||||
|
||||
if name.contains(&key) || key.contains(&name) {
|
||||
return Some(candidate.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paths.first().map(|path| path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
fn resolve_app_path(app_name: &str) -> Option<String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
resolve_windows_app_path(app_name)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
// On macOS/Linux, just return the app_name as-is since
|
||||
// the opener plugin handles them correctly
|
||||
Some(app_name.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn check_macos_app(app_name: &str) -> bool {
|
||||
// Check common installation locations
|
||||
@@ -181,13 +335,13 @@ fn check_macos_app(app_name: &str) -> bool {
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
app_locations.push(format!("{}/Applications/{}.app", home, app_name));
|
||||
}
|
||||
|
||||
|
||||
for location in app_locations {
|
||||
if std::path::Path::new(&location).exists() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Also check if command exists in PATH
|
||||
Command::new("which")
|
||||
.arg(app_name)
|
||||
@@ -251,7 +405,8 @@ pub fn run() {
|
||||
get_display_backend,
|
||||
set_display_backend,
|
||||
markdown::parse_markdown_command,
|
||||
check_app_exists
|
||||
check_app_exists,
|
||||
resolve_app_path
|
||||
])
|
||||
.events(tauri_specta::collect_events![LoadingWindowComplete])
|
||||
.error_handling(tauri_specta::ErrorHandlingMode::Throw);
|
||||
|
||||
@@ -14,6 +14,7 @@ export const commands = {
|
||||
setDisplayBackend: (backend: LinuxDisplayBackend) => __TAURI_INVOKE<null>("set_display_backend", { backend }),
|
||||
parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE<string>("parse_markdown_command", { markdown }),
|
||||
checkAppExists: (appName: string) => __TAURI_INVOKE<boolean>("check_app_exists", { appName }),
|
||||
resolveAppPath: (appName: string) => __TAURI_INVOKE<string | null>("resolve_app_path", { appName }),
|
||||
};
|
||||
|
||||
/** Events */
|
||||
|
||||
@@ -116,6 +116,15 @@ function parseRecord(value: unknown) {
|
||||
return value as Record<string, unknown>
|
||||
}
|
||||
|
||||
function parseStored(value: unknown) {
|
||||
if (typeof value !== "string") return value
|
||||
try {
|
||||
return JSON.parse(value) as unknown
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
function pickLocale(value: unknown): Locale | null {
|
||||
const direct = parseLocale(value)
|
||||
if (direct) return direct
|
||||
@@ -169,7 +178,7 @@ export function initI18n(): Promise<Locale> {
|
||||
if (!store) return state.locale
|
||||
|
||||
const raw = await store.get("language").catch(() => null)
|
||||
const value = typeof raw === "string" ? JSON.parse(raw) : raw
|
||||
const value = parseStored(raw)
|
||||
const next = pickLocale(value) ?? state.locale
|
||||
|
||||
state.locale = next
|
||||
|
||||
@@ -98,7 +98,12 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
|
||||
void shellOpen(url).catch(() => undefined)
|
||||
},
|
||||
|
||||
openPath(path: string, app?: string) {
|
||||
async openPath(path: string, app?: string) {
|
||||
const os = ostype()
|
||||
if (os === "windows" && app) {
|
||||
const resolvedApp = await commands.resolveAppPath(app)
|
||||
return openerOpenPath(path, resolvedApp || app)
|
||||
}
|
||||
return openerOpenPath(path, app)
|
||||
},
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.1.53"
|
||||
version = "1.1.56"
|
||||
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.53/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.56/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.53/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.56/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.53/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.56/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.53/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.56/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.53/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.56/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -2,4 +2,4 @@ preload = ["@opentui/solid/preload"]
|
||||
|
||||
[test]
|
||||
preload = ["./test/preload.ts"]
|
||||
timeout = 10000 # 10 seconds (default is 5000ms)
|
||||
timeout = 30000 # 30 seconds - allow time for package installation
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createMemo, createSignal } from "solid-js"
|
||||
import { useLocal } from "@tui/context/local"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { map, pipe, flatMap, entries, filter, sortBy, take } from "remeda"
|
||||
import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
|
||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
|
||||
import { useKeybind } from "../context/keybind"
|
||||
@@ -20,96 +20,51 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
const sync = useSync()
|
||||
const dialog = useDialog()
|
||||
const keybind = useKeybind()
|
||||
const [ref, setRef] = createSignal<DialogSelectRef<unknown>>()
|
||||
const [query, setQuery] = createSignal("")
|
||||
|
||||
const connected = useConnected()
|
||||
const providers = createDialogProviderOptions()
|
||||
|
||||
const showExtra = createMemo(() => {
|
||||
if (!connected()) return false
|
||||
if (props.providerID) return false
|
||||
return true
|
||||
})
|
||||
const showExtra = createMemo(() => connected() && !props.providerID)
|
||||
|
||||
const options = createMemo(() => {
|
||||
const q = query()
|
||||
const needle = q.trim()
|
||||
const needle = query().trim()
|
||||
const showSections = showExtra() && needle.length === 0
|
||||
const favorites = connected() ? local.model.favorite() : []
|
||||
const recents = local.model.recent()
|
||||
|
||||
const recentList = showSections
|
||||
? recents.filter(
|
||||
(item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID),
|
||||
)
|
||||
: []
|
||||
|
||||
const favoriteOptions = showSections
|
||||
? favorites.flatMap((item) => {
|
||||
const provider = sync.data.provider.find((x) => x.id === item.providerID)
|
||||
if (!provider) return []
|
||||
const model = provider.models[item.modelID]
|
||||
if (!model) return []
|
||||
return [
|
||||
{
|
||||
key: item,
|
||||
value: {
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
},
|
||||
title: model.name ?? item.modelID,
|
||||
description: provider.name,
|
||||
category: "Favorites",
|
||||
disabled: provider.id === "opencode" && model.id.includes("-nano"),
|
||||
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||
onSelect: () => {
|
||||
dialog.clear()
|
||||
local.model.set(
|
||||
{
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
},
|
||||
{ recent: true },
|
||||
)
|
||||
},
|
||||
function toOptions(items: typeof favorites, category: string) {
|
||||
if (!showSections) return []
|
||||
return items.flatMap((item) => {
|
||||
const provider = sync.data.provider.find((x) => x.id === item.providerID)
|
||||
if (!provider) return []
|
||||
const model = provider.models[item.modelID]
|
||||
if (!model) return []
|
||||
return [
|
||||
{
|
||||
key: item,
|
||||
value: { providerID: provider.id, modelID: model.id },
|
||||
title: model.name ?? item.modelID,
|
||||
description: provider.name,
|
||||
category,
|
||||
disabled: provider.id === "opencode" && model.id.includes("-nano"),
|
||||
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||
onSelect: () => {
|
||||
dialog.clear()
|
||||
local.model.set({ providerID: provider.id, modelID: model.id }, { recent: true })
|
||||
},
|
||||
]
|
||||
})
|
||||
: []
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const recentOptions = showSections
|
||||
? recentList.flatMap((item) => {
|
||||
const provider = sync.data.provider.find((x) => x.id === item.providerID)
|
||||
if (!provider) return []
|
||||
const model = provider.models[item.modelID]
|
||||
if (!model) return []
|
||||
return [
|
||||
{
|
||||
key: item,
|
||||
value: {
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
},
|
||||
title: model.name ?? item.modelID,
|
||||
description: provider.name,
|
||||
category: "Recent",
|
||||
disabled: provider.id === "opencode" && model.id.includes("-nano"),
|
||||
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||
onSelect: () => {
|
||||
dialog.clear()
|
||||
local.model.set(
|
||||
{
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
},
|
||||
{ recent: true },
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
: []
|
||||
const favoriteOptions = toOptions(favorites, "Favorites")
|
||||
const recentOptions = toOptions(
|
||||
recents.filter(
|
||||
(item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID),
|
||||
),
|
||||
"Recent",
|
||||
)
|
||||
|
||||
const providerOptions = pipe(
|
||||
sync.data.provider,
|
||||
@@ -123,45 +78,26 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
entries(),
|
||||
filter(([_, info]) => info.status !== "deprecated"),
|
||||
filter(([_, info]) => (props.providerID ? info.providerID === props.providerID : true)),
|
||||
map(([model, info]) => {
|
||||
const value = {
|
||||
providerID: provider.id,
|
||||
modelID: model,
|
||||
}
|
||||
return {
|
||||
value,
|
||||
title: info.name ?? model,
|
||||
description: favorites.some(
|
||||
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
|
||||
)
|
||||
? "(Favorite)"
|
||||
: undefined,
|
||||
category: connected() ? provider.name : undefined,
|
||||
disabled: provider.id === "opencode" && model.includes("-nano"),
|
||||
footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||
onSelect() {
|
||||
dialog.clear()
|
||||
local.model.set(
|
||||
{
|
||||
providerID: provider.id,
|
||||
modelID: model,
|
||||
},
|
||||
{ recent: true },
|
||||
)
|
||||
},
|
||||
}
|
||||
}),
|
||||
map(([model, info]) => ({
|
||||
value: { providerID: provider.id, modelID: model },
|
||||
title: info.name ?? model,
|
||||
description: favorites.some((item) => item.providerID === provider.id && item.modelID === model)
|
||||
? "(Favorite)"
|
||||
: undefined,
|
||||
category: connected() ? provider.name : undefined,
|
||||
disabled: provider.id === "opencode" && model.includes("-nano"),
|
||||
footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||
onSelect() {
|
||||
dialog.clear()
|
||||
local.model.set({ providerID: provider.id, modelID: model }, { recent: true })
|
||||
},
|
||||
})),
|
||||
filter((x) => {
|
||||
if (!showSections) return true
|
||||
const value = x.value
|
||||
const inFavorites = favorites.some(
|
||||
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
|
||||
)
|
||||
if (inFavorites) return false
|
||||
const inRecents = recents.some(
|
||||
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
|
||||
)
|
||||
if (inRecents) return false
|
||||
if (favorites.some((item) => item.providerID === x.value.providerID && item.modelID === x.value.modelID))
|
||||
return false
|
||||
if (recents.some((item) => item.providerID === x.value.providerID && item.modelID === x.value.modelID))
|
||||
return false
|
||||
return true
|
||||
}),
|
||||
sortBy(
|
||||
@@ -175,21 +111,19 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
const popularProviders = !connected()
|
||||
? pipe(
|
||||
providers(),
|
||||
map((option) => {
|
||||
return {
|
||||
...option,
|
||||
category: "Popular providers",
|
||||
}
|
||||
}),
|
||||
map((option) => ({
|
||||
...option,
|
||||
category: "Popular providers",
|
||||
})),
|
||||
take(6),
|
||||
)
|
||||
: []
|
||||
|
||||
// Search shows a single merged list (favorites inline)
|
||||
if (needle) {
|
||||
const filteredProviders = fuzzysort.go(needle, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj)
|
||||
const filteredPopular = fuzzysort.go(needle, popularProviders, { keys: ["title"] }).map((x) => x.obj)
|
||||
return [...filteredProviders, ...filteredPopular]
|
||||
return [
|
||||
...fuzzysort.go(needle, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj),
|
||||
...fuzzysort.go(needle, popularProviders, { keys: ["title"] }).map((x) => x.obj),
|
||||
]
|
||||
}
|
||||
|
||||
return [...favoriteOptions, ...recentOptions, ...providerOptions, ...popularProviders]
|
||||
@@ -199,13 +133,11 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
props.providerID ? sync.data.provider.find((x) => x.id === props.providerID) : null,
|
||||
)
|
||||
|
||||
const title = createMemo(() => {
|
||||
if (provider()) return provider()!.name
|
||||
return "Select model"
|
||||
})
|
||||
const title = createMemo(() => provider()?.name ?? "Select model")
|
||||
|
||||
return (
|
||||
<DialogSelect
|
||||
<DialogSelect<ReturnType<typeof options>[number]["value"]>
|
||||
options={options()}
|
||||
keybind={[
|
||||
{
|
||||
keybind: keybind.all.model_provider_list?.[0],
|
||||
@@ -223,12 +155,11 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
},
|
||||
},
|
||||
]}
|
||||
ref={setRef}
|
||||
onFilter={setQuery}
|
||||
flat={true}
|
||||
skipFilter={true}
|
||||
title={title()}
|
||||
current={local.model.current()}
|
||||
options={options()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface DialogSelectProps<T> {
|
||||
title: string
|
||||
placeholder?: string
|
||||
options: DialogSelectOption<T>[]
|
||||
flat?: boolean
|
||||
ref?: (ref: DialogSelectRef<T>) => void
|
||||
onMove?: (option: DialogSelectOption<T>) => void
|
||||
onFilter?: (query: string) => void
|
||||
@@ -100,7 +101,10 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
setStore("input", "keyboard")
|
||||
})
|
||||
|
||||
const grouped = createMemo(() => {
|
||||
const flatten = createMemo(() => props.flat && store.filter.length > 0)
|
||||
|
||||
const grouped = createMemo<[string, DialogSelectOption<T>[]][]>(() => {
|
||||
if (flatten()) return [["", filtered()]]
|
||||
const result = pipe(
|
||||
filtered(),
|
||||
groupBy((x) => x.category ?? ""),
|
||||
@@ -117,10 +121,16 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
)
|
||||
})
|
||||
|
||||
const rows = createMemo(() => {
|
||||
const headers = grouped().reduce((acc, [category], i) => {
|
||||
if (!category) return acc
|
||||
return acc + (i > 0 ? 2 : 1)
|
||||
}, 0)
|
||||
return flat().length + headers
|
||||
})
|
||||
|
||||
const dimensions = useTerminalDimensions()
|
||||
const height = createMemo(() =>
|
||||
Math.min(flat().length + grouped().length * 2 - 1, Math.floor(dimensions().height / 2) - 6),
|
||||
)
|
||||
const height = createMemo(() => Math.min(rows(), Math.floor(dimensions().height / 2) - 6))
|
||||
|
||||
const selected = createMemo(() => flat()[store.selected])
|
||||
|
||||
@@ -311,7 +321,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
>
|
||||
<Option
|
||||
title={option.title}
|
||||
footer={option.footer}
|
||||
footer={flatten() ? (option.category ?? option.footer) : option.footer}
|
||||
description={option.description !== category ? option.description : undefined}
|
||||
active={active()}
|
||||
current={current()}
|
||||
|
||||
@@ -15,6 +15,17 @@ export namespace Pty {
|
||||
|
||||
const BUFFER_LIMIT = 1024 * 1024 * 2
|
||||
const BUFFER_CHUNK = 64 * 1024
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
// WebSocket control frame: 0x00 + UTF-8 JSON (currently { cursor }).
|
||||
const meta = (cursor: number) => {
|
||||
const json = JSON.stringify({ cursor })
|
||||
const bytes = encoder.encode(json)
|
||||
const out = new Uint8Array(bytes.length + 1)
|
||||
out[0] = 0
|
||||
out.set(bytes, 1)
|
||||
return out
|
||||
}
|
||||
|
||||
const pty = lazy(async () => {
|
||||
const { spawn } = await import("bun-pty")
|
||||
@@ -68,6 +79,8 @@ export namespace Pty {
|
||||
info: Info
|
||||
process: IPty
|
||||
buffer: string
|
||||
bufferCursor: number
|
||||
cursor: number
|
||||
subscribers: Set<WSContext>
|
||||
}
|
||||
|
||||
@@ -139,23 +152,27 @@ export namespace Pty {
|
||||
info,
|
||||
process: ptyProcess,
|
||||
buffer: "",
|
||||
bufferCursor: 0,
|
||||
cursor: 0,
|
||||
subscribers: new Set(),
|
||||
}
|
||||
state().set(id, session)
|
||||
ptyProcess.onData((data) => {
|
||||
let open = false
|
||||
session.cursor += data.length
|
||||
|
||||
for (const ws of session.subscribers) {
|
||||
if (ws.readyState !== 1) {
|
||||
session.subscribers.delete(ws)
|
||||
continue
|
||||
}
|
||||
open = true
|
||||
ws.send(data)
|
||||
}
|
||||
if (open) return
|
||||
|
||||
session.buffer += data
|
||||
if (session.buffer.length <= BUFFER_LIMIT) return
|
||||
session.buffer = session.buffer.slice(-BUFFER_LIMIT)
|
||||
const excess = session.buffer.length - BUFFER_LIMIT
|
||||
session.buffer = session.buffer.slice(excess)
|
||||
session.bufferCursor += excess
|
||||
})
|
||||
ptyProcess.onExit(({ exitCode }) => {
|
||||
log.info("session exited", { id, exitCode })
|
||||
@@ -215,28 +232,47 @@ export namespace Pty {
|
||||
}
|
||||
}
|
||||
|
||||
export function connect(id: string, ws: WSContext) {
|
||||
export function connect(id: string, ws: WSContext, cursor?: number) {
|
||||
const session = state().get(id)
|
||||
if (!session) {
|
||||
ws.close()
|
||||
return
|
||||
}
|
||||
log.info("client connected to session", { id })
|
||||
session.subscribers.add(ws)
|
||||
if (session.buffer) {
|
||||
const buffer = session.buffer.length <= BUFFER_LIMIT ? session.buffer : session.buffer.slice(-BUFFER_LIMIT)
|
||||
session.buffer = ""
|
||||
|
||||
const start = session.bufferCursor
|
||||
const end = session.cursor
|
||||
|
||||
const from =
|
||||
cursor === -1 ? end : typeof cursor === "number" && Number.isSafeInteger(cursor) ? Math.max(0, cursor) : 0
|
||||
|
||||
const data = (() => {
|
||||
if (!session.buffer) return ""
|
||||
if (from >= end) return ""
|
||||
const offset = Math.max(0, from - start)
|
||||
if (offset >= session.buffer.length) return ""
|
||||
return session.buffer.slice(offset)
|
||||
})()
|
||||
|
||||
if (data) {
|
||||
try {
|
||||
for (let i = 0; i < buffer.length; i += BUFFER_CHUNK) {
|
||||
ws.send(buffer.slice(i, i + BUFFER_CHUNK))
|
||||
for (let i = 0; i < data.length; i += BUFFER_CHUNK) {
|
||||
ws.send(data.slice(i, i + BUFFER_CHUNK))
|
||||
}
|
||||
} catch {
|
||||
session.subscribers.delete(ws)
|
||||
session.buffer = buffer
|
||||
ws.close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
ws.send(meta(end))
|
||||
} catch {
|
||||
ws.close()
|
||||
return
|
||||
}
|
||||
|
||||
session.subscribers.add(ws)
|
||||
return {
|
||||
onMessage: (message: string | ArrayBuffer) => {
|
||||
session.process.write(String(message))
|
||||
|
||||
@@ -151,11 +151,18 @@ export const PtyRoutes = lazy(() =>
|
||||
validator("param", z.object({ ptyID: z.string() })),
|
||||
upgradeWebSocket((c) => {
|
||||
const id = c.req.param("ptyID")
|
||||
const cursor = (() => {
|
||||
const value = c.req.query("cursor")
|
||||
if (!value) return
|
||||
const parsed = Number(value)
|
||||
if (!Number.isSafeInteger(parsed) || parsed < -1) return
|
||||
return parsed
|
||||
})()
|
||||
let handler: ReturnType<typeof Pty.connect>
|
||||
if (!Pty.get(id)) throw new Error("Session not found")
|
||||
return {
|
||||
onOpen(_event, ws) {
|
||||
handler = Pty.connect(id, ws)
|
||||
handler = Pty.connect(id, ws, cursor)
|
||||
},
|
||||
onMessage(event) {
|
||||
handler?.onMessage(String(event.data))
|
||||
|
||||
@@ -78,6 +78,9 @@ export namespace Server {
|
||||
})
|
||||
})
|
||||
.use((c, next) => {
|
||||
// Allow CORS preflight requests to succeed without auth.
|
||||
// Browser clients sending Authorization headers will preflight with OPTIONS.
|
||||
if (c.req.method === "OPTIONS") return next()
|
||||
const password = Flag.OPENCODE_SERVER_PASSWORD
|
||||
if (!password) return next()
|
||||
const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
|
||||
@@ -107,7 +110,12 @@ export namespace Server {
|
||||
|
||||
if (input.startsWith("http://localhost:")) return input
|
||||
if (input.startsWith("http://127.0.0.1:")) return input
|
||||
if (input === "tauri://localhost" || input === "http://tauri.localhost") return input
|
||||
if (
|
||||
input === "tauri://localhost" ||
|
||||
input === "http://tauri.localhost" ||
|
||||
input === "https://tauri.localhost"
|
||||
)
|
||||
return input
|
||||
|
||||
// *.opencode.ai (https only, adjust if needed)
|
||||
if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) {
|
||||
|
||||
@@ -64,6 +64,8 @@ export namespace SessionRetry {
|
||||
|
||||
if (MessageV2.APIError.isInstance(error)) {
|
||||
if (!error.data.isRetryable) return undefined
|
||||
if (error.data.responseBody?.includes("FreeUsageLimitError"))
|
||||
return `Free usage exceeded, add credits https://opencode.ai/zen`
|
||||
return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ import { Identifier } from "@/id/id"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
|
||||
import { Log } from "@/util/log"
|
||||
import path from "path"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Storage } from "@/storage/storage"
|
||||
import { Bus } from "@/bus"
|
||||
|
||||
@@ -91,18 +89,7 @@ export namespace SessionSummary {
|
||||
)
|
||||
|
||||
async function summarizeSession(input: { sessionID: string; messages: MessageV2.WithParts[] }) {
|
||||
const files = new Set(
|
||||
input.messages
|
||||
.flatMap((x) => x.parts)
|
||||
.filter((x) => x.type === "patch")
|
||||
.flatMap((x) => x.files)
|
||||
.map((x) => path.relative(Instance.worktree, x).replaceAll("\\", "/")),
|
||||
)
|
||||
const diffs = await computeDiff({ messages: input.messages }).then((x) =>
|
||||
x.filter((x) => {
|
||||
return files.has(x.file)
|
||||
}),
|
||||
)
|
||||
const diffs = await computeDiff({ messages: input.messages })
|
||||
await Session.update(input.sessionID, (draft) => {
|
||||
draft.summary = {
|
||||
additions: diffs.reduce((sum, x) => sum + x.additions, 0),
|
||||
@@ -206,7 +193,6 @@ export namespace SessionSummary {
|
||||
for (const part of item.parts) {
|
||||
if (part.type === "step-finish" && part.snapshot) {
|
||||
to = part.snapshot
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type FileContents, File, FileOptions, LineAnnotation, type SelectedLineRange } from "@pierre/diffs"
|
||||
import { ComponentProps, createEffect, createMemo, createSignal, onCleanup, onMount, Show, splitProps } from "solid-js"
|
||||
import { Portal } from "solid-js/web"
|
||||
import { createDefaultOptions, styleVariables } from "../pierre"
|
||||
import { getWorkerPool } from "../pierre/worker"
|
||||
import { Icon } from "./icon"
|
||||
@@ -125,11 +126,9 @@ export function Code<T>(props: CodeProps<T>) {
|
||||
let wrapper!: HTMLDivElement
|
||||
let container!: HTMLDivElement
|
||||
let findInput: HTMLInputElement | undefined
|
||||
let findBar: HTMLDivElement | undefined
|
||||
let findOverlay!: HTMLDivElement
|
||||
let findOverlayFrame: number | undefined
|
||||
let findOverlayScroll: HTMLElement[] = []
|
||||
let findScroll: HTMLElement | undefined
|
||||
let observer: MutationObserver | undefined
|
||||
let renderToken = 0
|
||||
let selectionFrame: number | undefined
|
||||
@@ -159,6 +158,8 @@ export function Code<T>(props: CodeProps<T>) {
|
||||
let findMode: "highlights" | "overlay" = "overlay"
|
||||
let findHits: Range[] = []
|
||||
|
||||
const [findPos, setFindPos] = createSignal<{ top: number; right: number }>({ top: 8, right: 8 })
|
||||
|
||||
const file = createMemo(
|
||||
() =>
|
||||
new File<T>(
|
||||
@@ -291,23 +292,26 @@ export function Code<T>(props: CodeProps<T>) {
|
||||
setFindIndex(0)
|
||||
}
|
||||
|
||||
const getScrollParent = (el: HTMLElement): HTMLElement | null => {
|
||||
const getScrollParent = (el: HTMLElement): HTMLElement | undefined => {
|
||||
let parent = el.parentElement
|
||||
while (parent) {
|
||||
const style = getComputedStyle(parent)
|
||||
if (style.overflowY === "auto" || style.overflowY === "scroll") return parent
|
||||
parent = parent.parentElement
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const positionFindBar = () => {
|
||||
if (!findBar || !wrapper) return
|
||||
const scrollTop = findScroll ? findScroll.scrollTop : window.scrollY
|
||||
findBar.style.position = "absolute"
|
||||
findBar.style.top = `${scrollTop + 8}px`
|
||||
findBar.style.right = "8px"
|
||||
findBar.style.left = ""
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
const root = getScrollParent(wrapper) ?? wrapper
|
||||
const rect = root.getBoundingClientRect()
|
||||
const title = parseFloat(getComputedStyle(root).getPropertyValue("--session-title-height"))
|
||||
const header = Number.isNaN(title) ? 0 : title
|
||||
setFindPos({
|
||||
top: Math.round(rect.top) + header - 4,
|
||||
right: Math.round(window.innerWidth - rect.right) + 8,
|
||||
})
|
||||
}
|
||||
|
||||
const scanFind = (root: ShadowRoot, query: string) => {
|
||||
@@ -426,7 +430,6 @@ export function Code<T>(props: CodeProps<T>) {
|
||||
}
|
||||
if (opts?.scroll && active) {
|
||||
scrollToRange(active)
|
||||
positionFindBar()
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -435,7 +438,6 @@ export function Code<T>(props: CodeProps<T>) {
|
||||
syncOverlayScroll()
|
||||
if (opts?.scroll && active) {
|
||||
scrollToRange(active)
|
||||
positionFindBar()
|
||||
}
|
||||
scheduleOverlay()
|
||||
}
|
||||
@@ -464,14 +466,12 @@ export function Code<T>(props: CodeProps<T>) {
|
||||
return
|
||||
}
|
||||
scrollToRange(active)
|
||||
positionFindBar()
|
||||
return
|
||||
}
|
||||
|
||||
clearHighlightFind()
|
||||
syncOverlayScroll()
|
||||
scrollToRange(active)
|
||||
positionFindBar()
|
||||
scheduleOverlay()
|
||||
}
|
||||
|
||||
@@ -484,11 +484,9 @@ export function Code<T>(props: CodeProps<T>) {
|
||||
findCurrent = host
|
||||
findTarget = host
|
||||
|
||||
findScroll = getScrollParent(wrapper) ?? undefined
|
||||
if (!findOpen()) setFindOpen(true)
|
||||
requestAnimationFrame(() => {
|
||||
applyFind({ scroll: true })
|
||||
positionFindBar()
|
||||
findInput?.focus()
|
||||
findInput?.select()
|
||||
})
|
||||
@@ -514,18 +512,18 @@ export function Code<T>(props: CodeProps<T>) {
|
||||
|
||||
createEffect(() => {
|
||||
if (!findOpen()) return
|
||||
findScroll = getScrollParent(wrapper) ?? undefined
|
||||
const target = findScroll ?? window
|
||||
|
||||
const handler = () => positionFindBar()
|
||||
target.addEventListener("scroll", handler, { passive: true })
|
||||
window.addEventListener("resize", handler, { passive: true })
|
||||
handler()
|
||||
const update = () => positionFindBar()
|
||||
requestAnimationFrame(update)
|
||||
window.addEventListener("resize", update, { passive: true })
|
||||
|
||||
const root = getScrollParent(wrapper) ?? wrapper
|
||||
const observer = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(() => update())
|
||||
observer?.observe(root)
|
||||
|
||||
onCleanup(() => {
|
||||
target.removeEventListener("scroll", handler)
|
||||
window.removeEventListener("resize", handler)
|
||||
findScroll = undefined
|
||||
window.removeEventListener("resize", update)
|
||||
observer?.disconnect()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -916,6 +914,64 @@ export function Code<T>(props: CodeProps<T>) {
|
||||
pendingSelectionEnd = false
|
||||
})
|
||||
|
||||
const FindBar = (barProps: { class: string; style?: ComponentProps<"div">["style"] }) => (
|
||||
<div class={barProps.class} style={barProps.style} onPointerDown={(e) => e.stopPropagation()}>
|
||||
<Icon name="magnifying-glass" size="small" class="text-text-weak shrink-0" />
|
||||
<input
|
||||
ref={findInput}
|
||||
placeholder="Find"
|
||||
value={findQuery()}
|
||||
class="w-40 bg-transparent outline-none text-14-regular text-text-strong placeholder:text-text-weak"
|
||||
onInput={(e) => {
|
||||
setFindQuery(e.currentTarget.value)
|
||||
setFindIndex(0)
|
||||
applyFind({ reset: true, scroll: true })
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
closeFind()
|
||||
return
|
||||
}
|
||||
if (e.key !== "Enter") return
|
||||
e.preventDefault()
|
||||
stepFind(e.shiftKey ? -1 : 1)
|
||||
}}
|
||||
/>
|
||||
<div class="shrink-0 text-12-regular text-text-weak tabular-nums text-right" style={{ width: "10ch" }}>
|
||||
{findCount() ? `${findIndex() + 1}/${findCount()}` : "0/0"}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong disabled:opacity-40 disabled:pointer-events-none"
|
||||
disabled={findCount() === 0}
|
||||
aria-label="Previous match"
|
||||
onClick={() => stepFind(-1)}
|
||||
>
|
||||
<Icon name="chevron-down" size="small" class="rotate-180" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong disabled:opacity-40 disabled:pointer-events-none"
|
||||
disabled={findCount() === 0}
|
||||
aria-label="Next match"
|
||||
onClick={() => stepFind(1)}
|
||||
>
|
||||
<Icon name="chevron-down" size="small" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong"
|
||||
aria-label="Close search"
|
||||
onClick={closeFind}
|
||||
>
|
||||
<Icon name="close-small" size="small" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component="code"
|
||||
@@ -936,65 +992,15 @@ export function Code<T>(props: CodeProps<T>) {
|
||||
}}
|
||||
>
|
||||
<Show when={findOpen()}>
|
||||
<div
|
||||
ref={findBar}
|
||||
class="z-50 flex h-8 items-center gap-2 rounded-md border border-border-base bg-background-base px-3 shadow-md"
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Icon name="magnifying-glass" size="small" class="text-text-weak shrink-0" />
|
||||
<input
|
||||
ref={findInput}
|
||||
placeholder="Find"
|
||||
value={findQuery()}
|
||||
class="w-40 bg-transparent outline-none text-14-regular text-text-strong placeholder:text-text-weak"
|
||||
onInput={(e) => {
|
||||
setFindQuery(e.currentTarget.value)
|
||||
setFindIndex(0)
|
||||
applyFind({ reset: true, scroll: true })
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
closeFind()
|
||||
return
|
||||
}
|
||||
if (e.key !== "Enter") return
|
||||
e.preventDefault()
|
||||
stepFind(e.shiftKey ? -1 : 1)
|
||||
<Portal>
|
||||
<FindBar
|
||||
class="fixed z-50 flex h-8 items-center gap-2 rounded-md border border-border-base bg-background-base px-3 shadow-md"
|
||||
style={{
|
||||
top: `${findPos().top}px`,
|
||||
right: `${findPos().right}px`,
|
||||
}}
|
||||
/>
|
||||
<div class="shrink-0 text-12-regular text-text-weak tabular-nums text-right" style={{ width: "10ch" }}>
|
||||
{findCount() ? `${findIndex() + 1}/${findCount()}` : "0/0"}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong disabled:opacity-40 disabled:pointer-events-none"
|
||||
disabled={findCount() === 0}
|
||||
aria-label="Previous match"
|
||||
onClick={() => stepFind(-1)}
|
||||
>
|
||||
<Icon name="chevron-down" size="small" class="rotate-180" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong disabled:opacity-40 disabled:pointer-events-none"
|
||||
disabled={findCount() === 0}
|
||||
aria-label="Next match"
|
||||
onClick={() => stepFind(1)}
|
||||
>
|
||||
<Icon name="chevron-down" size="small" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong"
|
||||
aria-label="Close search"
|
||||
onClick={closeFind}
|
||||
>
|
||||
<Icon name="close-small" size="small" />
|
||||
</button>
|
||||
</div>
|
||||
</Portal>
|
||||
</Show>
|
||||
<div ref={container} />
|
||||
<div ref={findOverlay} class="pointer-events-none absolute inset-0 z-0" />
|
||||
|
||||
@@ -877,6 +877,74 @@ ToolRegistry.register({
|
||||
const data = useData()
|
||||
const i18n = useI18n()
|
||||
const childSessionId = () => props.metadata.sessionId as string | undefined
|
||||
|
||||
const href = createMemo(() => {
|
||||
const sessionId = childSessionId()
|
||||
if (!sessionId) return
|
||||
|
||||
const direct = data.sessionHref?.(sessionId)
|
||||
if (direct) return direct
|
||||
|
||||
if (typeof window === "undefined") return
|
||||
const path = window.location.pathname
|
||||
const idx = path.indexOf("/session")
|
||||
if (idx === -1) return
|
||||
return `${path.slice(0, idx)}/session/${sessionId}`
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const sessionId = childSessionId()
|
||||
if (!sessionId) return
|
||||
const sync = data.syncSession
|
||||
if (!sync) return
|
||||
Promise.resolve(sync(sessionId)).catch(() => undefined)
|
||||
})
|
||||
|
||||
const handleLinkClick = (e: MouseEvent) => {
|
||||
const sessionId = childSessionId()
|
||||
const url = href()
|
||||
if (!sessionId || !url) return
|
||||
|
||||
e.stopPropagation()
|
||||
|
||||
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return
|
||||
|
||||
const nav = data.navigateToSession
|
||||
if (!nav || typeof window === "undefined") return
|
||||
|
||||
e.preventDefault()
|
||||
const before = window.location.pathname + window.location.search + window.location.hash
|
||||
nav(sessionId)
|
||||
setTimeout(() => {
|
||||
const after = window.location.pathname + window.location.search + window.location.hash
|
||||
if (after === before) window.location.assign(url)
|
||||
}, 50)
|
||||
}
|
||||
|
||||
const trigger = () => (
|
||||
<div data-slot="basic-tool-tool-info-structured">
|
||||
<div data-slot="basic-tool-tool-info-main">
|
||||
<span data-slot="basic-tool-tool-title" class="capitalize">
|
||||
{i18n.t("ui.tool.agent", { type: props.input.subagent_type || props.tool })}
|
||||
</span>
|
||||
<Show when={props.input.description}>
|
||||
<Switch>
|
||||
<Match when={href()}>
|
||||
{(url) => (
|
||||
<a data-slot="basic-tool-tool-subtitle" class="clickable" href={url()} onClick={handleLinkClick}>
|
||||
{props.input.description}
|
||||
</a>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<span data-slot="basic-tool-tool-subtitle">{props.input.description}</span>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const childToolParts = createMemo(() => {
|
||||
const sessionId = childSessionId()
|
||||
if (!sessionId) return []
|
||||
@@ -924,13 +992,6 @@ ToolRegistry.register({
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubtitleClick = () => {
|
||||
const sessionId = childSessionId()
|
||||
if (sessionId && data.navigateToSession) {
|
||||
data.navigateToSession(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
const renderChildToolPart = () => {
|
||||
const toolData = childToolPart()
|
||||
if (!toolData) return null
|
||||
@@ -958,21 +1019,7 @@ ToolRegistry.register({
|
||||
<Switch>
|
||||
<Match when={childPermission()}>
|
||||
<>
|
||||
<Show
|
||||
when={childToolPart()}
|
||||
fallback={
|
||||
<BasicTool
|
||||
icon="task"
|
||||
defaultOpen={true}
|
||||
trigger={{
|
||||
title: i18n.t("ui.tool.agent", { type: props.input.subagent_type || props.tool }),
|
||||
titleClass: "capitalize",
|
||||
subtitle: props.input.description,
|
||||
}}
|
||||
onSubtitleClick={handleSubtitleClick}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Show when={childToolPart()} fallback={<BasicTool icon="task" defaultOpen={true} trigger={trigger()} />}>
|
||||
{renderChildToolPart()}
|
||||
</Show>
|
||||
<div data-component="permission-prompt">
|
||||
@@ -991,16 +1038,7 @@ ToolRegistry.register({
|
||||
</>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<BasicTool
|
||||
icon="task"
|
||||
defaultOpen={true}
|
||||
trigger={{
|
||||
title: i18n.t("ui.tool.agent", { type: props.input.subagent_type || props.tool }),
|
||||
titleClass: "capitalize",
|
||||
subtitle: props.input.description,
|
||||
}}
|
||||
onSubtitleClick={handleSubtitleClick}
|
||||
>
|
||||
<BasicTool icon="task" defaultOpen={true} trigger={trigger()}>
|
||||
<div
|
||||
ref={autoScroll.scrollRef}
|
||||
onScroll={autoScroll.handleScroll}
|
||||
|
||||
@@ -48,6 +48,10 @@ export type QuestionRejectFn = (input: { requestID: string }) => void
|
||||
|
||||
export type NavigateToSessionFn = (sessionID: string) => void
|
||||
|
||||
export type SessionHrefFn = (sessionID: string) => string
|
||||
|
||||
export type SyncSessionFn = (sessionID: string) => void | Promise<void>
|
||||
|
||||
export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
name: "Data",
|
||||
init: (props: {
|
||||
@@ -57,6 +61,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
onQuestionReply?: QuestionReplyFn
|
||||
onQuestionReject?: QuestionRejectFn
|
||||
onNavigateToSession?: NavigateToSessionFn
|
||||
onSessionHref?: SessionHrefFn
|
||||
onSyncSession?: SyncSessionFn
|
||||
}) => {
|
||||
return {
|
||||
get store() {
|
||||
@@ -69,6 +75,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
replyToQuestion: props.onQuestionReply,
|
||||
rejectQuestion: props.onQuestionReject,
|
||||
navigateToSession: props.onNavigateToSession,
|
||||
sessionHref: props.onSessionHref,
|
||||
syncSession: props.onSyncSession,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "1.1.53",
|
||||
"version": "1.1.56",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -142,13 +142,13 @@ require("codecompanion").setup({
|
||||
|
||||
## الدعم
|
||||
|
||||
يعمل OpenCode عبر ACP بالطريقة نفسها التي يعمل بها في الطرفية. جميع الميزات مدعومة:
|
||||
يعمل OpenCode عبر ACP بالطريقة نفسها التي يعمل بها في terminal. جميع الميزات مدعومة:
|
||||
|
||||
:::note
|
||||
بعض أوامر الشرطة المائلة المضمنة مثل `/undo` و`/redo` غير مدعومة حاليا.
|
||||
:::
|
||||
|
||||
- الأدوات المضمنة (عمليات الملفات، أوامر الطرفية، إلخ.)
|
||||
- الأدوات المضمنة (عمليات الملفات، أوامر terminal، إلخ.)
|
||||
- الأدوات المخصصة وأوامر الشرطة المائلة
|
||||
- خوادم MCP المضبوطة في إعدادات OpenCode
|
||||
- قواعد خاصة بالمشروع من `AGENTS.md`
|
||||
|
||||
@@ -460,7 +460,7 @@ permission:
|
||||
webfetch: deny
|
||||
---
|
||||
|
||||
حلل الشفرة فقط واقترح التغييرات.
|
||||
Only analyze code and suggest changes.
|
||||
```
|
||||
|
||||
يمكنك ضبط الأذونات لأوامر bash محددة.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: سطر الأوامر
|
||||
description: خيارات وأوامر سطر أوامر OpenCode.
|
||||
title: CLI
|
||||
description: أوامر وخيارات CLI في OpenCode.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem } from "@astrojs/starlight/components"
|
||||
|
||||
يبدأ سطر أوامر OpenCode افتراضيا واجهة [TUI](/docs/tui) عند تشغيله بدون أي معاملات.
|
||||
يبدأ CLI الخاص بـ OpenCode افتراضيا واجهة [TUI](/docs/tui) عند تشغيله بدون أي معاملات.
|
||||
|
||||
```bash
|
||||
opencode
|
||||
@@ -21,7 +21,7 @@ opencode run "Explain how closures work in JavaScript"
|
||||
|
||||
### tui
|
||||
|
||||
بدء واجهة المستخدم الطرفية (TUI) الخاصة بـ OpenCode.
|
||||
بدء واجهة المستخدم terminal (TUI) الخاصة بـ OpenCode.
|
||||
|
||||
```bash
|
||||
opencode [project]
|
||||
@@ -44,7 +44,7 @@ opencode [project]
|
||||
|
||||
## الأوامر
|
||||
|
||||
يتضمن سطر أوامر OpenCode أيضا الأوامر التالية.
|
||||
يتضمن CLI الخاص بـ OpenCode أيضا الأوامر التالية.
|
||||
|
||||
---
|
||||
|
||||
@@ -60,7 +60,7 @@ opencode agent [command]
|
||||
|
||||
### attach
|
||||
|
||||
إرفاق طرفية بخادم الواجهة الخلفية لـ OpenCode قيد التشغيل بالفعل، والذي تم تشغيله عبر الأمرين `serve` أو `web`.
|
||||
إرفاق terminal بخادم الواجهة الخلفية لـ OpenCode قيد التشغيل بالفعل، والذي تم تشغيله عبر الأمرين `serve` أو `web`.
|
||||
|
||||
```bash
|
||||
opencode attach [url]
|
||||
@@ -562,7 +562,7 @@ opencode upgrade v0.1.48
|
||||
| `OPENCODE_CONFIG_CONTENT` | string | محتوى تهيئة JSON مُضمّن |
|
||||
| `OPENCODE_DISABLE_AUTOUPDATE` | boolean | تعطيل التحقق التلقائي من التحديثات |
|
||||
| `OPENCODE_DISABLE_PRUNE` | boolean | تعطيل تنقية البيانات القديمة |
|
||||
| `OPENCODE_DISABLE_TERMINAL_TITLE` | boolean | تعطيل تحديث عنوان الطرفية تلقائيا |
|
||||
| `OPENCODE_DISABLE_TERMINAL_TITLE` | boolean | تعطيل تحديث عنوان terminal تلقائيا |
|
||||
| `OPENCODE_PERMISSION` | string | تهيئة أذونات JSON مُضمّنة |
|
||||
| `OPENCODE_DISABLE_DEFAULT_PLUGINS` | boolean | تعطيل الإضافات الافتراضية |
|
||||
| `OPENCODE_DISABLE_LSP_DOWNLOAD` | boolean | تعطيل تنزيل خوادم LSP تلقائيا |
|
||||
|
||||
@@ -160,7 +160,7 @@ with the following content: $3
|
||||
|
||||
---
|
||||
|
||||
### خرج الصدفة (Shell)
|
||||
### خرج shell
|
||||
|
||||
استخدم _!`command`_ لحقن خرج [أمر bash](/docs/tui#bash-commands) داخل مُطالبتك.
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ opencode run "Hello world"
|
||||
|
||||
- `scroll_acceleration.enabled` - تفعيل تسارع التمرير بأسلوب macOS. **له أولوية على `scroll_speed`.**
|
||||
- `scroll_speed` - مُضاعِف سرعة تمرير مخصص (الافتراضي: `3`، الحد الأدنى: `1`). يتم تجاهله إذا كان `scroll_acceleration.enabled` مساويًا لـ `true`.
|
||||
- `diff_style` - التحكم في عرض `diff`. القيمة `"auto"` تتكيف مع عرض الطرفية، و`"stacked"` تعرض عمودًا واحدًا دائمًا.
|
||||
- `diff_style` - التحكم في عرض `diff`. القيمة `"auto"` تتكيف مع عرض terminal، و`"stacked"` تعرض عمودًا واحدًا دائمًا.
|
||||
|
||||
[تعرف على المزيد حول استخدام TUI هنا](/docs/tui).
|
||||
|
||||
|
||||
@@ -52,19 +52,19 @@ description: مشاريع وتكاملات مبنية باستخدام OpenCode.
|
||||
|
||||
## المشاريع
|
||||
|
||||
| الاسم | الوصف |
|
||||
| ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- |
|
||||
| [kimaki](https://github.com/remorses/kimaki) | بوت Discord للتحكم بجلسات OpenCode، مبني على SDK |
|
||||
| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | ملحق Neovim لموجهات تراعي المحرر، مبني على API |
|
||||
| [portal](https://github.com/hosenur/portal) | واجهة ويب تركز على الجوال لـ OpenCode عبر Tailscale/VPN |
|
||||
| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | قالب لبناء ملحقات OpenCode |
|
||||
| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | واجهة Neovim لـ opencode - وكيل برمجة بالذكاء الاصطناعي يعمل في الطرفية |
|
||||
| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | موفر Vercel AI SDK لاستخدام OpenCode عبر @opencode-ai/sdk |
|
||||
| [OpenChamber](https://github.com/btriapitsyn/openchamber) | تطبيق ويب/سطح مكتب وامتداد VS Code لـ OpenCode |
|
||||
| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | ملحق Obsidian يدمج OpenCode داخل واجهة Obsidian |
|
||||
| [OpenWork](https://github.com/different-ai/openwork) | بديل مفتوح المصدر لـ Claude Cowork، مدعوم بـ OpenCode |
|
||||
| [ocx](https://github.com/kdcokenny/ocx) | مدير امتدادات OpenCode مع ملفات تعريف محمولة ومعزولة. |
|
||||
| [CodeNomad](https://github.com/NeuralNomadsAI/CodeNomad) | تطبيق عميل لسطح المكتب والويب والجوال وعن بُعد لـ OpenCode |
|
||||
| الاسم | الوصف |
|
||||
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
|
||||
| [kimaki](https://github.com/remorses/kimaki) | بوت Discord للتحكم بجلسات OpenCode، مبني على SDK |
|
||||
| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | ملحق Neovim لموجهات تراعي المحرر، مبني على API |
|
||||
| [portal](https://github.com/hosenur/portal) | واجهة ويب تركز على الجوال لـ OpenCode عبر Tailscale/VPN |
|
||||
| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | قالب لبناء ملحقات OpenCode |
|
||||
| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | واجهة Neovim لـ opencode - وكيل برمجة بالذكاء الاصطناعي يعمل في terminal |
|
||||
| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | موفر Vercel AI SDK لاستخدام OpenCode عبر @opencode-ai/sdk |
|
||||
| [OpenChamber](https://github.com/btriapitsyn/openchamber) | تطبيق ويب/سطح مكتب وامتداد VS Code لـ OpenCode |
|
||||
| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | ملحق Obsidian يدمج OpenCode داخل واجهة Obsidian |
|
||||
| [OpenWork](https://github.com/different-ai/openwork) | بديل مفتوح المصدر لـ Claude Cowork، مدعوم بـ OpenCode |
|
||||
| [ocx](https://github.com/kdcokenny/ocx) | مدير امتدادات OpenCode مع ملفات تعريف محمولة ومعزولة. |
|
||||
| [CodeNomad](https://github.com/NeuralNomadsAI/CodeNomad) | تطبيق عميل لسطح المكتب والويب والجوال وعن بُعد لـ OpenCode |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ title: IDE
|
||||
description: امتداد OpenCode لـ VS Code و Cursor وغيرها من بيئات التطوير
|
||||
---
|
||||
|
||||
يتكامل OpenCode مع VS Code و Cursor أو أي IDE يدعم الطرفية. ما عليك سوى تشغيل `opencode` في الطرفية للبدء.
|
||||
يتكامل OpenCode مع VS Code و Cursor أو أي IDE يدعم terminal. ما عليك سوى تشغيل `opencode` في terminal للبدء.
|
||||
|
||||
---
|
||||
|
||||
## الاستخدام
|
||||
|
||||
- **تشغيل سريع**: استخدم `Cmd+Esc` (Mac) أو `Ctrl+Esc` (Windows/Linux) لفتح OpenCode في عرض طرفية مقسّم، أو للتركيز على جلسة طرفية موجودة إذا كانت قيد التشغيل بالفعل.
|
||||
- **جلسة جديدة**: استخدم `Cmd+Shift+Esc` (Mac) أو `Ctrl+Shift+Esc` (Windows/Linux) لبدء جلسة طرفية جديدة لـ OpenCode حتى لو كانت هناك جلسة مفتوحة. يمكنك أيضا النقر على زر OpenCode في واجهة المستخدم.
|
||||
- **تشغيل سريع**: استخدم `Cmd+Esc` (Mac) أو `Ctrl+Esc` (Windows/Linux) لفتح OpenCode في عرض terminal مقسّم، أو للتركيز على جلسة terminal موجودة إذا كانت قيد التشغيل بالفعل.
|
||||
- **جلسة جديدة**: استخدم `Cmd+Shift+Esc` (Mac) أو `Ctrl+Shift+Esc` (Windows/Linux) لبدء جلسة terminal جديدة لـ OpenCode حتى لو كانت هناك جلسة مفتوحة. يمكنك أيضا النقر على زر OpenCode في واجهة المستخدم.
|
||||
- **وعي بالسياق**: شارك تلقائيا تحديدك الحالي أو تبويبك مع OpenCode.
|
||||
- **اختصارات الإشارة إلى الملفات**: استخدم `Cmd+Option+K` (Mac) أو `Alt+Ctrl+K` (Linux/Windows) لإدراج مراجع الملفات. مثلا: `@File#L37-42`.
|
||||
|
||||
@@ -21,7 +21,7 @@ description: امتداد OpenCode لـ VS Code و Cursor وغيرها من بي
|
||||
لتثبيت OpenCode على VS Code والتفرعات الشائعة مثل Cursor و Windsurf و VSCodium:
|
||||
|
||||
1. افتح VS Code
|
||||
2. افتح الطرفية المدمجة
|
||||
2. افتح terminal المدمجة
|
||||
3. شغّل `opencode` - سيتم تثبيت الامتداد تلقائيا
|
||||
|
||||
أما إذا كنت تريد استخدام IDE الخاص بك عند تشغيل `/editor` أو `/export` من واجهة TUI، فستحتاج إلى ضبط `export EDITOR="code --wait"`. [اعرف المزيد](/docs/tui/#editor-setup).
|
||||
@@ -38,8 +38,8 @@ description: امتداد OpenCode لـ VS Code و Cursor وغيرها من بي
|
||||
|
||||
إذا فشل تثبيت الامتداد تلقائيا:
|
||||
|
||||
- تأكد من أنك تشغّل `opencode` داخل الطرفية المدمجة.
|
||||
- تأكد من تثبيت واجهة سطر الأوامر (CLI) الخاصة بـ IDE لديك:
|
||||
- تأكد من أنك تشغّل `opencode` داخل terminal المدمجة.
|
||||
- تأكد من تثبيت CLI الخاصة بـ IDE لديك:
|
||||
- لـ VS Code: أمر `code`
|
||||
- لـ Cursor: أمر `cursor`
|
||||
- لـ Windsurf: أمر `windsurf`
|
||||
|
||||
@@ -7,9 +7,9 @@ import { Tabs, TabItem } from "@astrojs/starlight/components"
|
||||
import config from "../../../../config.mjs"
|
||||
export const console = config.console
|
||||
|
||||
[**OpenCode**](/) هو وكيل ترميز بالذكاء الاصطناعي مفتوح المصدر. يتوفر كواجهة طرفية، وتطبيق لسطح المكتب، أو إضافة لبيئة تطوير متكاملة (IDE).
|
||||
[**OpenCode**](/) هو وكيل ترميز بالذكاء الاصطناعي مفتوح المصدر. يتوفر كواجهة terminal، وتطبيق لسطح المكتب، أو إضافة لبيئة تطوير متكاملة (IDE).
|
||||
|
||||

|
||||

|
||||
|
||||
لنبدأ.
|
||||
|
||||
@@ -17,9 +17,9 @@ export const console = config.console
|
||||
|
||||
#### المتطلبات
|
||||
|
||||
لاستخدام OpenCode في الطرفية، ستحتاج إلى:
|
||||
لاستخدام OpenCode في terminal، ستحتاج إلى:
|
||||
|
||||
1. محاكي طرفية حديث مثل:
|
||||
1. محاكي terminal حديث مثل:
|
||||
- [WezTerm](https://wezterm.org) متعدد المنصات
|
||||
- [Alacritty](https://alacritty.org) متعدد المنصات
|
||||
- [Ghostty](https://ghostty.org) على Linux وmacOS
|
||||
@@ -249,10 +249,10 @@ How is authentication handled in @packages/functions/src/api/index.ts
|
||||
```
|
||||
|
||||
:::tip
|
||||
اسحب الصور وأفلِتها داخل الطرفية لإضافتها إلى الطلب.
|
||||
اسحب الصور وأفلِتها داخل terminal لإضافتها إلى الطلب.
|
||||
:::
|
||||
|
||||
يستطيع OpenCode فحص أي صور تزوده بها وإضافتها إلى الطلب. يمكنك فعل ذلك عبر سحب الصورة وإفلاتها داخل الطرفية.
|
||||
يستطيع OpenCode فحص أي صور تزوده بها وإضافتها إلى الطلب. يمكنك فعل ذلك عبر سحب الصورة وإفلاتها داخل terminal.
|
||||
|
||||
3. **ابنِ الميزة**
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ description: خصّص اختصارات لوحة المفاتيح.
|
||||
|
||||
## مفتاح القائد
|
||||
|
||||
يستخدم OpenCode مفتاح `leader` لمعظم اختصارات لوحة المفاتيح. يساعد ذلك على تجنّب التعارضات في الطرفية لديك.
|
||||
يستخدم OpenCode مفتاح `leader` لمعظم اختصارات لوحة المفاتيح. يساعد ذلك على تجنّب التعارضات في terminal لديك.
|
||||
|
||||
افتراضيا، يكون `ctrl+x` هو مفتاح القائد، وتتطلّب معظم الإجراءات أن تضغط أولا مفتاح القائد ثم الاختصار. على سبيل المثال، لبدء جلسة جديدة اضغط `ctrl+x` أولا ثم اضغط `n`.
|
||||
|
||||
@@ -154,7 +154,7 @@ description: خصّص اختصارات لوحة المفاتيح.
|
||||
|
||||
## مفتاح Shift+Enter
|
||||
|
||||
بعض الطرفيات لا ترسل مفاتيح التعديل مع Enter افتراضيا. قد تحتاج إلى ضبط طرفيتك لإرسال `Shift+Enter` كسلسلة هروب.
|
||||
بعض تطبيقات terminal لا ترسل مفاتيح التعديل مع Enter افتراضيا. قد تحتاج إلى ضبط terminal لإرسال `Shift+Enter` كسلسلة هروب.
|
||||
|
||||
### Windows Terminal
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@ description: تهيئة مزوّد LLM والنموذج.
|
||||
|
||||
عند بدء تشغيل OpenCode، يتحقق من النماذج وفق ترتيب الأولوية التالي:
|
||||
|
||||
1. خيار سطر الأوامر `--model` أو `-m`. الصيغة هي نفسها الموجودة في ملف الإعدادات: `provider_id/model_id`.
|
||||
1. خيار CLI `--model` أو `-m`. الصيغة هي نفسها الموجودة في ملف الإعدادات: `provider_id/model_id`.
|
||||
|
||||
2. قائمة النماذج في إعدادات OpenCode.
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ description: أوضاع مختلفة لحالات استخدام مختلفة.
|
||||
- `write` - لا يمكن إنشاء ملفات جديدة
|
||||
- `edit` - لا يمكن تعديل الملفات الموجودة، باستثناء الملفات الموجودة في `.opencode/plans/*.md` لتفصيل الخطة نفسها
|
||||
- `patch` - لا يمكن تطبيق التصحيحات
|
||||
- `bash` - لا يمكن تنفيذ أوامر الصدفة
|
||||
- `bash` - لا يمكن تنفيذ أوامر shell
|
||||
|
||||
يكون هذا الوضع مفيدًا عندما تريد من الذكاء الاصطناعي تحليل الشيفرة، أو اقتراح تغييرات، أو إنشاء خطط دون إجراء أي تعديلات فعلية على قاعدة الشيفرة لديك.
|
||||
|
||||
@@ -227,7 +227,7 @@ Provide constructive feedback without making direct changes.
|
||||
|
||||
| الأداة | الوصف |
|
||||
| ----------- | -------------------------- |
|
||||
| `bash` | تنفيذ أوامر الصدفة |
|
||||
| `bash` | تنفيذ أوامر shell |
|
||||
| `edit` | تعديل الملفات الموجودة |
|
||||
| `write` | إنشاء ملفات جديدة |
|
||||
| `read` | قراءة محتويات الملفات |
|
||||
|
||||
@@ -26,7 +26,7 @@ export NO_PROXY=localhost,127.0.0.1
|
||||
تتواصل واجهة TUI مع خادم HTTP محلي. يجب تجاوز الوكيل لهذا الاتصال لمنع حلقات التوجيه.
|
||||
:::
|
||||
|
||||
يمكنك إعداد منفذ الخادم واسم المضيف باستخدام [أعلام سطر الأوامر](/docs/cli#run).
|
||||
يمكنك إعداد منفذ الخادم واسم المضيف باستخدام [أعلام CLI](/docs/cli#run).
|
||||
|
||||
---
|
||||
|
||||
@@ -54,4 +54,4 @@ export HTTPS_PROXY=http://username:password@proxy.example.com:8080
|
||||
export NODE_EXTRA_CA_CERTS=/path/to/ca-cert.pem
|
||||
```
|
||||
|
||||
يعمل ذلك لكل من اتصالات الوكيل والوصول المباشر إلى واجهات برمجة التطبيقات.
|
||||
يعمل ذلك لكل من اتصالات الوكيل والوصول المباشر إلى APIs.
|
||||
|
||||
@@ -134,7 +134,7 @@ description: تحكّم في الإجراءات التي تتطلب موافقة
|
||||
- `glob` — مطابقة أسماء الملفات (يطابق نمط الـ glob)
|
||||
- `grep` — البحث في المحتوى (يطابق نمط regex)
|
||||
- `list` — سرد الملفات في دليل (يطابق مسار الدليل)
|
||||
- `bash` — تشغيل أوامر الصدفة (يطابق الأوامر المُحلَّلة مثل `git status --porcelain`)
|
||||
- `bash` — تشغيل أوامر shell (يطابق الأوامر المُحلَّلة مثل `git status --porcelain`)
|
||||
- `task` — تشغيل وكلاء فرعيين (يطابق نوع الوكيل الفرعي)
|
||||
- `skill` — تحميل مهارة (يطابق اسم المهارة)
|
||||
- `lsp` — تشغيل استعلامات LSP (حاليًا دون قواعد دقيقة)
|
||||
|
||||
@@ -192,7 +192,7 @@ export const MyPlugin: Plugin = async ({ project, client, $, directory, worktree
|
||||
|
||||
- `todo.updated`
|
||||
|
||||
#### أحداث الصدفة
|
||||
#### أحداث shell
|
||||
|
||||
- `shell.env`
|
||||
|
||||
@@ -260,7 +260,7 @@ export const EnvProtection = async ({ project, client, $, directory, worktree })
|
||||
|
||||
### حقن متغيرات البيئة
|
||||
|
||||
احقن متغيرات البيئة في جميع عمليات تنفيذ الصدفة (أدوات الذكاء الاصطناعي وطرفيات المستخدم):
|
||||
احقن متغيرات البيئة في جميع عمليات تنفيذ shell (أدوات الذكاء الاصطناعي وterminal المستخدم):
|
||||
|
||||
```javascript title=".opencode/plugins/inject-env.js"
|
||||
export const InjectEnvPlugin = async () => {
|
||||
|
||||
@@ -282,9 +282,9 @@ OpenCode Zen هي قائمة نماذج يوفّرها فريق OpenCode وقد
|
||||
|
||||
##### استخدام مفاتيح API
|
||||
|
||||
يمكنك أيضا اختيار **Create an API Key** إذا لم يكن لديك اشتراك Pro/Max. سيفتح ذلك المتصفح أيضا ويطلب منك تسجيل الدخول إلى Anthropic ويعطيك رمزا يمكنك لصقه في الطرفية.
|
||||
يمكنك أيضا اختيار **Create an API Key** إذا لم يكن لديك اشتراك Pro/Max. سيفتح ذلك المتصفح أيضا ويطلب منك تسجيل الدخول إلى Anthropic ويعطيك رمزا يمكنك لصقه في terminal.
|
||||
|
||||
أو إذا كان لديك مفتاح API بالفعل، يمكنك اختيار **Manually enter API Key** ولصقه في الطرفية.
|
||||
أو إذا كان لديك مفتاح API بالفعل، يمكنك اختيار **Manually enter API Key** ولصقه في terminal.
|
||||
|
||||
---
|
||||
|
||||
@@ -661,7 +661,7 @@ OpenCode Zen هي قائمة نماذج يوفّرها فريق OpenCode وقد
|
||||
2. انقر **Add new token**
|
||||
3. Name: `OpenCode`، Scopes: `api`
|
||||
4. انسخ الرمز (يبدأ بـ `glpat-`)
|
||||
5. أدخله في الطرفية
|
||||
5. أدخله في terminal
|
||||
|
||||
3. شغّل الأمر `/models` لعرض النماذج المتاحة.
|
||||
|
||||
@@ -1324,7 +1324,7 @@ npm install -g opencode-helicone-session
|
||||
|
||||
##### استخدام مفاتيح API
|
||||
|
||||
إذا كان لديك مفتاح API بالفعل، يمكنك اختيار **Manually enter API Key** ولصقه في الطرفية.
|
||||
إذا كان لديك مفتاح API بالفعل، يمكنك اختيار **Manually enter API Key** ولصقه في terminal.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ const client = createOpencodeClient({
|
||||
|
||||
## الأنواع
|
||||
|
||||
تتضمن SDK تعريفات TypeScript لجميع أنواع واجهات برمجة التطبيقات. استوردها مباشرة:
|
||||
تتضمن SDK تعريفات TypeScript لجميع أنواع API. استوردها مباشرة:
|
||||
|
||||
```typescript
|
||||
import type { Session, Message, Part } from "@opencode-ai/sdk"
|
||||
@@ -117,7 +117,7 @@ try {
|
||||
|
||||
---
|
||||
|
||||
## واجهات برمجة التطبيقات
|
||||
## APIs
|
||||
|
||||
توفر SDK جميع واجهات الخادم عبر عميل آمن للأنواع.
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ OPENCODE_SERVER_PASSWORD=your-password opencode serve
|
||||
|
||||
### كيف يعمل
|
||||
|
||||
عند تشغيل `opencode` يبدأ تشغيل واجهة طرفية تفاعلية (TUI) وخادما. تكون الـ TUI هي
|
||||
عند تشغيل `opencode` يبدأ تشغيل واجهة terminal تفاعلية (TUI) وخادما. تكون الـ TUI هي
|
||||
العميل الذي يتحدث إلى الخادم. يوفّر الخادم نقطة نهاية لمواصفة OpenAPI 3.1.
|
||||
وتُستخدم هذه النقطة أيضا لتوليد [SDK](/docs/sdk).
|
||||
|
||||
@@ -56,7 +56,7 @@ OPENCODE_SERVER_PASSWORD=your-password opencode serve
|
||||
|
||||
تتيح هذه البنية لـ opencode دعم عدة عملاء وتمكّنك من التفاعل مع opencode برمجيا.
|
||||
|
||||
يمكنك تشغيل `opencode serve` لبدء خادم مستقل. إذا كانت واجهة opencode الطرفية (TUI)
|
||||
يمكنك تشغيل `opencode serve` لبدء خادم مستقل. إذا كانت واجهة opencode في terminal (TUI)
|
||||
قيد التشغيل، فسيبدأ `opencode serve` خادما جديدا.
|
||||
|
||||
---
|
||||
@@ -177,7 +177,7 @@ http://<hostname>:<port>/doc
|
||||
| `GET` | `/session/:id/message/:messageID` | الحصول على تفاصيل الرسالة | يعيد `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
|
||||
| `POST` | `/session/:id/prompt_async` | إرسال رسالة بشكل غير متزامن (بدون انتظار) | المتن: مثل `/session/:id/message`، يعيد `204 No Content` |
|
||||
| `POST` | `/session/:id/command` | تنفيذ أمر شرطة مائلة (slash) | المتن: `{ messageID?, agent?, model?, command, arguments }`، يعيد `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
|
||||
| `POST` | `/session/:id/shell` | تشغيل أمر في الصدفة (shell) | المتن: `{ agent, model?, command }`، يعيد `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
|
||||
| `POST` | `/session/:id/shell` | تشغيل أمر في shell | المتن: `{ agent, model?, command }`، يعيد `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -3,19 +3,19 @@ title: السمات
|
||||
description: اختر سمة مدمجة أو عرّف سمة خاصة بك.
|
||||
---
|
||||
|
||||
مع OpenCode يمكنك الاختيار من بين عدة سمات مدمجة، أو استخدام سمة تتكيّف مع سمة طرفيتك، أو تعريف سمة مخصصة خاصة بك.
|
||||
مع OpenCode يمكنك الاختيار من بين عدة سمات مدمجة، أو استخدام سمة تتكيّف مع سمة terminal لديك، أو تعريف سمة مخصصة خاصة بك.
|
||||
|
||||
افتراضيًا، يستخدم OpenCode سمتنا `opencode`.
|
||||
|
||||
---
|
||||
|
||||
## متطلبات الطرفية
|
||||
## متطلبات terminal
|
||||
|
||||
لكي تُعرض السمات بشكل صحيح مع لوحة ألوانها الكاملة، يجب أن تدعم طرفيتك **truecolor** (ألوان 24-بت). تدعم معظم الطرفيات الحديثة ذلك افتراضيًا، لكن قد تحتاج إلى تفعيله:
|
||||
لكي تُعرض السمات بشكل صحيح مع لوحة ألوانها الكاملة، يجب أن يدعم terminal لديك **truecolor** (ألوان 24-بت). تدعم معظم تطبيقات terminal الحديثة ذلك افتراضيًا، لكن قد تحتاج إلى تفعيله:
|
||||
|
||||
- **التحقق من الدعم**: شغّل `echo $COLORTERM` - يجب أن يطبع `truecolor` أو `24bit`
|
||||
- **تفعيل truecolor**: اضبط متغير البيئة `COLORTERM=truecolor` في ملف إعدادات الصدفة
|
||||
- **توافق الطرفية**: تأكد من أن محاكي الطرفية يدعم ألوان 24-بت (معظم الطرفيات الحديثة مثل iTerm2 وAlacritty وKitty وWindows Terminal والإصدارات الحديثة من GNOME Terminal تدعم ذلك)
|
||||
- **تفعيل truecolor**: اضبط متغير البيئة `COLORTERM=truecolor` في ملف إعدادات shell
|
||||
- **توافق terminal**: تأكد من أن محاكي terminal يدعم ألوان 24-بت (معظم تطبيقات terminal الحديثة مثل iTerm2 وAlacritty وKitty وWindows Terminal والإصدارات الحديثة من GNOME Terminal تدعم ذلك)
|
||||
|
||||
بدون دعم truecolor، قد تظهر السمات بدقة ألوان أقل أو تعود إلى أقرب تقريب ضمن 256 لونًا.
|
||||
|
||||
@@ -27,7 +27,7 @@ description: اختر سمة مدمجة أو عرّف سمة خاصة بك.
|
||||
|
||||
| الاسم | الوصف |
|
||||
| ---------------------- | --------------------------------------------------------------------------- |
|
||||
| `system` | يتكيّف مع لون خلفية طرفيتك |
|
||||
| `system` | يتكيّف مع لون خلفية terminal لديك |
|
||||
| `tokyonight` | مبني على سمة [Tokyonight](https://github.com/folke/tokyonight.nvim) |
|
||||
| `everforest` | مبني على سمة [Everforest](https://github.com/sainnhe/everforest) |
|
||||
| `ayu` | مبني على السمة الداكنة [Ayu](https://github.com/ayu-theme) |
|
||||
@@ -45,17 +45,17 @@ description: اختر سمة مدمجة أو عرّف سمة خاصة بك.
|
||||
|
||||
## سمة النظام
|
||||
|
||||
صُممت سمة `system` لتتكيّف تلقائيًا مع مخطط ألوان طرفيتك. وعلى عكس السمات التقليدية التي تستخدم ألوانًا ثابتة، فإن سمة _system_:
|
||||
صُممت سمة `system` لتتكيّف تلقائيًا مع مخطط ألوان terminal لديك. وعلى عكس السمات التقليدية التي تستخدم ألوانًا ثابتة، فإن سمة _system_:
|
||||
|
||||
- **توليد تدرج رمادي**: تنشئ تدرجًا رماديًا مخصصًا اعتمادًا على لون خلفية طرفيتك، بما يضمن أفضل تباين.
|
||||
- **استخدام ألوان ANSI**: تستفيد من ألوان ANSI القياسية (0-15) لإبراز الصياغة وعناصر الواجهة، والتي تحترم لوحة ألوان طرفيتك.
|
||||
- **الحفاظ على افتراضيات الطرفية**: تستخدم `none` لألوان النص والخلفية للحفاظ على مظهر طرفيتك الأصلي.
|
||||
- **توليد تدرج رمادي**: تنشئ تدرجًا رماديًا مخصصًا اعتمادًا على لون خلفية terminal لديك، بما يضمن أفضل تباين.
|
||||
- **استخدام ألوان ANSI**: تستفيد من ألوان ANSI القياسية (0-15) لإبراز الصياغة وعناصر الواجهة، والتي تحترم لوحة ألوان terminal لديك.
|
||||
- **الحفاظ على افتراضيات terminal**: تستخدم `none` لألوان النص والخلفية للحفاظ على مظهر terminal الأصلي.
|
||||
|
||||
سمة النظام مناسبة للمستخدمين الذين:
|
||||
|
||||
- يريدون أن يطابق OpenCode مظهر طرفيتهم
|
||||
- يستخدمون مخططات ألوان مخصصة للطرفية
|
||||
- يفضلون مظهرًا متسقًا عبر جميع تطبيقات الطرفية
|
||||
- يريدون أن يطابق OpenCode مظهر terminal لديهم
|
||||
- يستخدمون مخططات ألوان مخصصة لـ terminal
|
||||
- يفضلون مظهرًا متسقًا عبر جميع تطبيقات terminal
|
||||
|
||||
---
|
||||
|
||||
@@ -119,7 +119,7 @@ vim .opencode/themes/my-theme.json
|
||||
- **ألوان ANSI**: `3` (0-255)
|
||||
- **مراجع الألوان**: `"primary"` أو تعريفات مخصصة
|
||||
- **متغيرات داكن/فاتح**: `{"dark": "#000", "light": "#fff"}`
|
||||
- **بدون لون**: `"none"` - يستخدم اللون الافتراضي للطرفية أو يكون شفافًا
|
||||
- **بدون لون**: `"none"` - يستخدم اللون الافتراضي لـ terminal أو يكون شفافًا
|
||||
|
||||
---
|
||||
|
||||
@@ -129,12 +129,12 @@ vim .opencode/themes/my-theme.json
|
||||
|
||||
---
|
||||
|
||||
### افتراضيات الطرفية
|
||||
### افتراضيات terminal
|
||||
|
||||
يمكن استخدام القيمة الخاصة `"none"` لأي لون لوراثة اللون الافتراضي للطرفية. هذا مفيد خصوصًا لإنشاء سمات تمتزج بسلاسة مع مخطط ألوان طرفيتك:
|
||||
يمكن استخدام القيمة الخاصة `"none"` لأي لون لوراثة اللون الافتراضي لـ terminal. هذا مفيد خصوصًا لإنشاء سمات تمتزج بسلاسة مع مخطط ألوان terminal لديك:
|
||||
|
||||
- `"text": "none"` - يستخدم لون المقدمة الافتراضي للطرفية
|
||||
- `"background": "none"` - يستخدم لون الخلفية الافتراضي للطرفية
|
||||
- `"text": "none"` - يستخدم لون المقدمة الافتراضي لـ terminal
|
||||
- `"background": "none"` - يستخدم لون الخلفية الافتراضي لـ terminal
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ description: إدارة الأدوات التي يمكن لـ LLM استخدام
|
||||
|
||||
### bash
|
||||
|
||||
نفذ أوامر الصدفة (Shell) في بيئة مشروعك.
|
||||
نفذ أوامر shell في بيئة مشروعك.
|
||||
|
||||
```json title="opencode.json" {4}
|
||||
{
|
||||
@@ -58,7 +58,7 @@ description: إدارة الأدوات التي يمكن لـ LLM استخدام
|
||||
}
|
||||
```
|
||||
|
||||
تتيح هذه الأداة لـ LLM تشغيل أوامر الطرفية مثل `npm install` و`git status` أو أي أمر صدفة آخر.
|
||||
تتيح هذه الأداة لـ LLM تشغيل أوامر terminal مثل `npm install` و`git status` أو أي أمر shell آخر.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ description: المشكلات الشائعة وكيفية حلها.
|
||||
|
||||
تتم تسمية ملفات السجل بطوابع زمنية (مثل `2025-01-09T123456.log`) ويتم الاحتفاظ بأحدث 10 ملفات سجل.
|
||||
|
||||
يمكنك ضبط مستوى السجل باستخدام خيار سطر الأوامر `--log-level` للحصول على معلومات تصحيح أكثر تفصيلا. على سبيل المثال: `opencode --log-level DEBUG`.
|
||||
يمكنك ضبط مستوى السجل باستخدام خيار CLI `--log-level` للحصول على معلومات تصحيح أكثر تفصيلا. على سبيل المثال: `opencode --log-level DEBUG`.
|
||||
|
||||
---
|
||||
|
||||
@@ -138,7 +138,7 @@ description: المشكلات الشائعة وكيفية حلها.
|
||||
|
||||
### Windows: مشكلات الأداء العامة
|
||||
|
||||
إذا كنت تواجه بطءا في الأداء، أو مشكلات في الوصول إلى الملفات، أو مشكلات في الطرفية على Windows، فجرّب استخدام [WSL (نظام Windows الفرعي لـ Linux)](/docs/windows-wsl). يوفر WSL بيئة Linux تعمل بسلاسة أكبر مع ميزات OpenCode.
|
||||
إذا كنت تواجه بطءا في الأداء، أو مشكلات في الوصول إلى الملفات، أو مشكلات في terminal على Windows، فجرّب استخدام [WSL (نظام Windows الفرعي لـ Linux)](/docs/windows-wsl). يوفر WSL بيئة Linux تعمل بسلاسة أكبر مع ميزات OpenCode.
|
||||
|
||||
---
|
||||
|
||||
@@ -198,7 +198,7 @@ description: المشكلات الشائعة وكيفية حلها.
|
||||
### OpenCode لا يبدأ
|
||||
|
||||
1. تحقق من السجلات بحثا عن رسائل الخطأ
|
||||
2. جرّب التشغيل مع `--print-logs` لرؤية المخرجات في الطرفية
|
||||
2. جرّب التشغيل مع `--print-logs` لرؤية المخرجات في terminal
|
||||
3. تأكد من أنك تستخدم أحدث إصدار عبر `opencode upgrade`
|
||||
|
||||
---
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: TUI
|
||||
description: استخدام واجهة المستخدم الطرفية (TUI) في OpenCode.
|
||||
description: استخدام واجهة المستخدم TUI في OpenCode.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem } from "@astrojs/starlight/components"
|
||||
|
||||
يوفّر OpenCode واجهة طرفية تفاعلية (TUI) للعمل على مشاريعك باستخدام LLM.
|
||||
يوفّر OpenCode واجهة terminal تفاعلية (TUI) للعمل على مشاريعك باستخدام LLM.
|
||||
|
||||
يؤدي تشغيل OpenCode إلى بدء واجهة TUI للدليل الحالي.
|
||||
|
||||
@@ -45,7 +45,7 @@ How is auth handled in @packages/functions/src/api/index.ts?
|
||||
|
||||
## أوامر Bash
|
||||
|
||||
ابدأ الرسالة بـ `!` لتشغيل أمر في الصدفة.
|
||||
ابدأ الرسالة بـ `!` لتشغيل أمر في shell.
|
||||
|
||||
```bash frame="none"
|
||||
!ls -la
|
||||
@@ -57,7 +57,7 @@ How is auth handled in @packages/functions/src/api/index.ts?
|
||||
|
||||
## الأوامر
|
||||
|
||||
عند استخدام واجهة OpenCode الطرفية، يمكنك كتابة `/` متبوعة باسم أمر لتنفيذ الإجراءات بسرعة. مثلا:
|
||||
عند استخدام واجهة OpenCode في terminal، يمكنك كتابة `/` متبوعة باسم أمر لتنفيذ الإجراءات بسرعة. مثلا:
|
||||
|
||||
```bash frame="none"
|
||||
/help
|
||||
@@ -288,7 +288,7 @@ How is auth handled in @packages/functions/src/api/index.ts?
|
||||
يستخدم الأمران `/editor` و`/export` المحرر المحدد في متغير البيئة `EDITOR`.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="لينكس/macOS">
|
||||
<TabItem label="Linux/macOS">
|
||||
```bash
|
||||
# Example for nano or vim
|
||||
export EDITOR=nano
|
||||
@@ -299,12 +299,12 @@ How is auth handled in @packages/functions/src/api/index.ts?
|
||||
export EDITOR="code --wait"
|
||||
```
|
||||
|
||||
لجعل ذلك دائما، أضف هذا إلى ملف تهيئة الصدفة لديك؛
|
||||
لجعل ذلك دائما، أضف هذا إلى ملف تهيئة shell لديك؛
|
||||
`~/.bashrc`، `~/.zshrc`، إلخ.
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="ويندوز (CMD)">
|
||||
<TabItem label="Windows (CMD)">
|
||||
```bash
|
||||
set EDITOR=notepad
|
||||
|
||||
@@ -317,7 +317,7 @@ How is auth handled in @packages/functions/src/api/index.ts?
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="ويندوز (PowerShell)">
|
||||
<TabItem label="Windows (PowerShell)">
|
||||
```powershell
|
||||
$env:EDITOR = "notepad"
|
||||
|
||||
@@ -346,7 +346,7 @@ How is auth handled in @packages/functions/src/api/index.ts?
|
||||
تحتاج بعض المحررات مثل VS Code إلى التشغيل مع الخيار `--wait`.
|
||||
:::
|
||||
|
||||
تحتاج بعض المحررات إلى وسائط سطر الأوامر لتعمل بوضع الحجب. يجعل الخيار `--wait` عملية المحرر تنتظر حتى يتم إغلاقها.
|
||||
تحتاج بعض المحررات إلى وسائط CLI لتعمل بوضع الحجب. يجعل الخيار `--wait` عملية المحرر تنتظر حتى يتم إغلاقها.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: الويب
|
||||
description: استخدام OpenCode في متصفحك.
|
||||
---
|
||||
|
||||
يمكن تشغيل OpenCode كتطبيق ويب داخل متصفحك، ليمنحك تجربة البرمجة القوية بالذكاء الاصطناعي نفسها دون الحاجة إلى الطرفية.
|
||||
يمكن تشغيل OpenCode كتطبيق ويب داخل متصفحك، ليمنحك تجربة البرمجة القوية بالذكاء الاصطناعي نفسها دون الحاجة إلى terminal.
|
||||
|
||||

|
||||
|
||||
@@ -22,14 +22,14 @@ opencode web
|
||||
:::
|
||||
|
||||
:::tip[مستخدمو Windows]
|
||||
لأفضل تجربة، شغّل `opencode web` من [WSL](/docs/windows-wsl) بدلا من PowerShell. يضمن ذلك وصولا صحيحا إلى نظام الملفات وتكاملا أفضل مع الطرفية.
|
||||
لأفضل تجربة، شغّل `opencode web` من [WSL](/docs/windows-wsl) بدلا من PowerShell. يضمن ذلك وصولا صحيحا إلى نظام الملفات وتكاملا أفضل مع terminal.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## الإعدادات
|
||||
|
||||
يمكنك ضبط خادم الويب باستخدام خيارات سطر الأوامر أو عبر [ملف الإعدادات](/docs/config).
|
||||
يمكنك ضبط خادم الويب باستخدام خيارات CLI أو عبر [ملف الإعدادات](/docs/config).
|
||||
|
||||
### المنفذ
|
||||
|
||||
@@ -108,9 +108,9 @@ OPENCODE_SERVER_PASSWORD=secret opencode web
|
||||
|
||||
---
|
||||
|
||||
## إرفاق طرفية
|
||||
## إرفاق terminal
|
||||
|
||||
يمكنك إرفاق واجهة طرفية (TUI) بخادم ويب قيد التشغيل:
|
||||
يمكنك إرفاق واجهة terminal (TUI) بخادم ويب قيد التشغيل:
|
||||
|
||||
```bash
|
||||
# Start the web server
|
||||
@@ -120,7 +120,7 @@ opencode web --port 4096
|
||||
opencode attach http://localhost:4096
|
||||
```
|
||||
|
||||
يتيح لك ذلك استخدام واجهة الويب والطرفية في الوقت نفسه، مع مشاركة الجلسات والحالة نفسها.
|
||||
يتيح لك ذلك استخدام واجهة الويب وterminal في الوقت نفسه، مع مشاركة الجلسات والحالة نفسها.
|
||||
|
||||
---
|
||||
|
||||
@@ -139,4 +139,4 @@ opencode attach http://localhost:4096
|
||||
}
|
||||
```
|
||||
|
||||
تكون خيارات سطر الأوامر ذات أولوية أعلى من إعدادات ملف الإعدادات.
|
||||
تكون خيارات CLI ذات أولوية أعلى من إعدادات ملف الإعدادات.
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Steps } from "@astrojs/starlight/components"
|
||||
رغم أن OpenCode يمكن تشغيله مباشرة على Windows، نوصي باستخدام [Windows Subsystem for Linux (WSL)](https://learn.microsoft.com/en-us/windows/wsl/install) للحصول على أفضل تجربة. يوفر WSL بيئة Linux تعمل بسلاسة مع ميزات OpenCode.
|
||||
|
||||
:::tip[لماذا WSL؟]
|
||||
يوفر WSL أداء أفضل لنظام الملفات، ودعمًا كاملًا للطرفية، وتوافقًا مع أدوات التطوير التي يعتمد عليها OpenCode.
|
||||
يوفر WSL أداء أفضل لنظام الملفات، ودعمًا كاملًا لـ terminal، وتوافقًا مع أدوات التطوير التي يعتمد عليها OpenCode.
|
||||
:::
|
||||
|
||||
---
|
||||
@@ -23,7 +23,7 @@ import { Steps } from "@astrojs/starlight/components"
|
||||
|
||||
2. **ثبّت OpenCode داخل WSL**
|
||||
|
||||
بعد إعداد WSL، افتح طرفية WSL وثبّت OpenCode باستخدام إحدى [طرق التثبيت](/docs/).
|
||||
بعد إعداد WSL، افتح terminal داخل WSL وثبّت OpenCode باستخدام إحدى [طرق التثبيت](/docs/).
|
||||
|
||||
```bash
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
@@ -73,7 +73,7 @@ OPENCODE_SERVER_PASSWORD=your-password opencode serve --hostname 0.0.0.0
|
||||
|
||||
لأفضل تجربة ويب على Windows:
|
||||
|
||||
1. **شغّل `opencode web` من طرفية WSL** بدلًا من PowerShell:
|
||||
1. **شغّل `opencode web` من terminal داخل WSL** بدلًا من PowerShell:
|
||||
|
||||
```bash
|
||||
opencode web --hostname 0.0.0.0
|
||||
@@ -81,7 +81,7 @@ OPENCODE_SERVER_PASSWORD=your-password opencode serve --hostname 0.0.0.0
|
||||
|
||||
2. **افتحه من متصفح Windows** عبر `http://localhost:<port>` (يعرض OpenCode الرابط)
|
||||
|
||||
تشغيل `opencode web` من WSL يضمن وصولًا صحيحًا لنظام الملفات وتكاملًا أفضل مع الطرفية، مع بقائه متاحًا من متصفح Windows.
|
||||
تشغيل `opencode web` من WSL يضمن وصولًا صحيحًا لنظام الملفات وتكاملًا أفضل مع terminal، مع بقائه متاحًا من متصفح Windows.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -12,12 +12,11 @@ Novi TUI radi kao stari jer se povezuje na isti opencode server.
|
||||
## Upgrade
|
||||
|
||||
Ne biste trebali biti automatski nadograđeni na 1.0 ako trenutno koristite prethodnu
|
||||
verzija. Međutim, neke starije verzije OpenCode-a uvijek uzimaju najnoviju verziju.
|
||||
verzija. Međutim, neke starije verzije OpenCode uvijek uzimaju najnoviju verziju.
|
||||
Za ručnu nadogradnju, pokrenite
|
||||
|
||||
```bash
|
||||
$ opencode upgrade 1.0.0
|
||||
|
||||
```
|
||||
|
||||
Za vraćanje na 0.x, pokrenite
|
||||
|
||||
@@ -149,7 +149,7 @@ Neke ugrađene komande kao što su `/undo` i `/redo` trenutno nisu podržane.
|
||||
:::
|
||||
|
||||
- Ugrađeni alati (operacije sa datotekama, naredbe terminala, itd.)
|
||||
- Prilagođeni alati i komande kosih crta
|
||||
- Prilagođeni alati i slash komande
|
||||
- MCP serveri konfigurisani u vašoj OpenCode konfiguraciji
|
||||
- Pravila specifična za projekat `AGENTS.md`
|
||||
- Prilagođeni formateri i linteri
|
||||
|
||||
@@ -4,38 +4,42 @@ description: Konfigurirajte i koristite specijalizirane agente.
|
||||
---
|
||||
|
||||
Agenti su specijalizirani AI asistenti koji se mogu konfigurirati za specifične zadatke i tokove posla. Oni vam omogućavaju da kreirate fokusirane alate sa prilagođenim upitima, modelima i pristupom alatima.
|
||||
|
||||
:::tip
|
||||
Koristite agenta plana za analizu koda i pregled prijedloga bez ikakvih promjena koda.
|
||||
:::
|
||||
|
||||
Možete se prebacivati između agenata tokom sesije ili ih pozvati spominjanjem `@`.
|
||||
|
||||
---
|
||||
|
||||
## Vrsta
|
||||
## Vrste
|
||||
|
||||
Postoje dvije vrste agenata u OpenCode-u; primarni agensi i subagensi.
|
||||
Postoje dvije vrste agenata u OpenCode; primarni agenti i podagenti.
|
||||
|
||||
---
|
||||
|
||||
### Primarni agenti
|
||||
|
||||
Primarni agenti su glavni pomoćnici s kojima direktno komunicirate. Možete se kretati kroz njih pomoću tipke **Tab** ili vašeg konfigurisanog povezivanja tipki `switch_agent`. Ovi agenti vode vaš glavni razgovor. Pristup alatima se konfiguriše putem dozvola — na primjer, Build ima omogućene sve alate dok je Plan ograničen.
|
||||
|
||||
:::tip
|
||||
Možete koristiti tipku **Tab** za prebacivanje između primarnih agenata tokom sesije.
|
||||
:::
|
||||
OpenCode dolazi sa dva ugrađena primarna agenta, **Build** i **Plan**. Hoćemo
|
||||
pogledajte ove u nastavku.
|
||||
|
||||
OpenCode dolazi sa dva ugrađena primarna agenta, **Build** i **Plan**. Pogledat ćemo ih u nastavku.
|
||||
|
||||
---
|
||||
|
||||
### Subagent
|
||||
### Subagenti
|
||||
|
||||
Subagenti su specijalizovani pomoćnici koje primarni agenti mogu pozvati za određene zadatke. Možete ih i ručno pozvati **@ spominjanjem** u svojim porukama.
|
||||
|
||||
OpenCode dolazi sa dva ugrađena subagenta, **General** i **Explore**. Ovo ćemo pogledati u nastavku.
|
||||
|
||||
---
|
||||
|
||||
## Embedded
|
||||
## Ugrađeni
|
||||
|
||||
OpenCode dolazi sa dva ugrađena primarna agenta i dva ugrađena subagenta.
|
||||
|
||||
@@ -43,54 +47,62 @@ OpenCode dolazi sa dva ugrađena primarna agenta i dva ugrađena subagenta.
|
||||
|
||||
### Koristi build
|
||||
|
||||
_Način_: `primary`
|
||||
_Mode_: `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.
|
||||
|
||||
---
|
||||
|
||||
### Koristite plan
|
||||
### Koristi plan
|
||||
|
||||
_Način_: `primary`
|
||||
Konačan agent dizajniran za planiranje i analizu. Koristimo sistem dozvola kako bismo vam pružili veću kontrolu i spriječili neželjene promjene.
|
||||
_Mode_: `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`:
|
||||
|
||||
- `file edits`: Sva upisivanja, zakrpe i uređivanja
|
||||
- `bash`: Sve bash komande
|
||||
Ovaj agent je koristan kada želite da LLM analizira kod, predloži promjene ili kreira planove bez stvarnih modifikacija vaše baze koda.
|
||||
|
||||
Ovaj agent je koristan kada želite da LLM analizira kod, predloži promjene ili kreira planove bez stvarnih modifikacija vaše baze koda.
|
||||
|
||||
---
|
||||
|
||||
### Upotreba općenito
|
||||
### Koristi general
|
||||
|
||||
_Način_: `subagent`
|
||||
Agent opće namjene za istraživanje složenih pitanja i izvršavanje zadataka u više koraka. Ima potpuni pristup alatima (osim zadataka), tako da može mijenjati fajl kada je to potrebno. Koristite ovo za paralelno pokretanje više jedinica rada.
|
||||
_Mode_: `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.
|
||||
|
||||
---
|
||||
|
||||
### Koristite explore
|
||||
### Koristi explore
|
||||
|
||||
_Mode_: `subagent`
|
||||
|
||||
_Način_: `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.
|
||||
|
||||
---
|
||||
|
||||
### Koristite zbijanje
|
||||
### Koristi compaction
|
||||
|
||||
_Mode_: `primary`
|
||||
|
||||
_Način_: `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.
|
||||
|
||||
---
|
||||
|
||||
### Koristite naslov
|
||||
### Koristi title
|
||||
|
||||
_Mode_: `primary`
|
||||
|
||||
_Način_: `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 sažetak
|
||||
### Koristi summary
|
||||
|
||||
_Mode_: `primary`
|
||||
|
||||
_Način_: `primary`
|
||||
Skriveni sistemski agent koji kreira sažetke sesije. Pokreće se automatski i ne može se odabrati u korisničkom interfejsu.
|
||||
|
||||
---
|
||||
@@ -98,25 +110,24 @@ Skriveni sistemski agent koji kreira sažetke sesije. Pokreće se automatski i n
|
||||
## Upotreba
|
||||
|
||||
1. Za primarne agente, koristite taster **Tab** za kretanje kroz njih tokom sesije. Također možete koristiti svoju konfiguriranu vezu tipke `switch_agent`.
|
||||
|
||||
2. Subagenti se mogu pozvati:
|
||||
- **Automatski** od strane primarnih agenata za specijalizovane zadatke na osnovu njihovih opisa.
|
||||
- Ručno **@ spominjanjem** subagenta u vašoj poruci. Na primjer.
|
||||
|
||||
- **Automatski** od strane primarnih agenata za specijalizovane zadatke na osnovu njihovih opisa.
|
||||
- Ručno **@ spominjanjem** subagenta u vašoj poruci. Na primjer.
|
||||
|
||||
```txt frame="none"
|
||||
```txt frame="none"
|
||||
@general help me search for this function
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
3. **Navigacija između sesija**: Kada subagenti kreiraju vlastite podređene sesije, možete se kretati između roditeljske sesije i svih podređenih sesija koristeći:
|
||||
- **\<Leader>+Right** (ili vaša konfigurirana `session_child_cycle` veza) za kretanje naprijed kroz roditelj → dijete1 → dijete2 → ... → roditelj
|
||||
- **\<Leader>+Left** (ili vaše konfigurirano povezivanje tipki `session_child_cycle_reverse`) za kretanje unazad kroz roditelj ← dijete1 ← dijete2 ← ... ← roditelj
|
||||
|
||||
- **\<Leader>+Desno** (ili vaša konfigurirana `session_child_cycle` veza) za petlju naprijed kroz roditelj → dijete1 → dijete2 → ... → roditelj
|
||||
- **\<Leader>+Levo** (ili vaše konfigurirano povezivanje tipki `session_child_cycle_reverse`) za kretanje unazad kroz roditelj ← dijete1 ← dijete2 ← ... ← roditelj
|
||||
Ovo vam omogućava neprimetno prebacivanje između glavnog razgovora i rada specijalizovanog podagenta.
|
||||
Ovo vam omogućava neprimetno prebacivanje između glavnog razgovora i rada specijalizovanog podagenta.
|
||||
|
||||
---
|
||||
|
||||
## Konfiguriši
|
||||
## Konfiguracija
|
||||
|
||||
Možete prilagoditi ugrađene agente ili kreirati vlastite kroz konfiguraciju. Agenti se mogu konfigurisati na dva načina:
|
||||
|
||||
@@ -167,10 +178,10 @@ Konfigurirajte agente u svom konfiguracijskom fajlu `opencode.json`:
|
||||
|
||||
### Markdown
|
||||
|
||||
Također možete definirati agente koristeći mardown datoteke. Stavite ih u:
|
||||
Također možete definirati agente koristeći markdown datoteke. Stavite ih u:
|
||||
|
||||
- Globalno: `~/.config/opencode/agents/`
|
||||
- Po projektu: `.opencode/agents/
|
||||
- Po projektu: `.opencode/agents/`
|
||||
|
||||
```markdown title="~/.config/opencode/agents/review.md"
|
||||
---
|
||||
@@ -184,16 +195,17 @@ tools:
|
||||
bash: false
|
||||
---
|
||||
|
||||
Nalazite se u načinu pregleda koda. Fokusirajte se na:
|
||||
You are in code review mode. Focus on:
|
||||
|
||||
- Kvalitet koda i najbolje prakse
|
||||
- Potencijalne greške i rubni slučajevi
|
||||
- Implikacije na performanse
|
||||
- Sigurnosna pitanja
|
||||
Dajte konstruktivne povratne informacije bez direktnih promjena.
|
||||
- Code quality and best practices
|
||||
- Potential bugs and edge cases
|
||||
- Performance implications
|
||||
- Security considerations
|
||||
|
||||
Provide constructive feedback without making direct changes.
|
||||
```
|
||||
|
||||
Ime marginalne datoteke postaje ime agenta. Na primjer, `review.md` kreira `review` agenta.
|
||||
Ime markdown datoteke postaje ime agenta. Na primjer, `review.md` kreira `review` agenta.
|
||||
|
||||
---
|
||||
|
||||
@@ -203,7 +215,7 @@ Pogledajmo ove opcije konfiguracije detaljno.
|
||||
|
||||
---
|
||||
|
||||
### Opis
|
||||
### Description
|
||||
|
||||
Koristite opciju `description` da pružite kratak opis onoga što agent radi i kada ga koristiti.
|
||||
|
||||
@@ -221,27 +233,30 @@ Ovo je **obavezna** opcija konfiguracije.
|
||||
|
||||
---
|
||||
|
||||
### Temperatura
|
||||
### Temperature
|
||||
|
||||
Kontrolišite slučajnost i kreativnost odgovora LLM-a pomoću `temperature` konfiguracije.
|
||||
|
||||
Niže vrijednosti čine odgovore fokusiranijim i determinističkim, dok više vrijednosti povećavaju kreativnost i varijabilnost.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"agent": {
|
||||
"agent": {
|
||||
"plan": {
|
||||
"temperatura": 0,1 },
|
||||
"kreativno": {
|
||||
"temperatura": 0,8 }
|
||||
"temperature": 0.1
|
||||
},
|
||||
"creative": {
|
||||
"temperature": 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Vrijednosti temperature se obično kreću od 0,0 do 1,0:
|
||||
Vrijednosti temperature se obično kreću od 0.0 do 1.0:
|
||||
|
||||
- **0,0-0,2**: Vrlo fokusirani i deterministički odgovori, idealni za analizu i planiranje koda
|
||||
- **0,3-0,5**: Uravnoteženi odgovori sa malo kreativnosti, dobro za opšte razvojne zadatke
|
||||
- **0,6-1,0**: kreativniji i raznovrsniji odgovori, korisni za razmišljanje i istraživanje
|
||||
- **0.0-0.2**: Vrlo fokusirani i deterministički odgovori, idealni za analizu i planiranje koda
|
||||
- **0.3-0.5**: Uravnoteženi odgovori sa malo kreativnosti, dobro za opšte razvojne zadatke
|
||||
- **0.6-1.0**: Kreativniji i raznovrsniji odgovori, korisni za razmišljanje i istraživanje
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
@@ -261,35 +276,37 @@ Vrijednosti temperature se obično kreću od 0,0 do 1,0:
|
||||
}
|
||||
```
|
||||
|
||||
Ako temperatura nije navedena, OpenCode koristi standardne postavke specifične za model; obično 0 za većinu modela, 0,55 za Qwen modele.
|
||||
Ako temperatura nije navedena, OpenCode koristi standardne postavke specifične za model; obično 0 za većinu modela, 0.55 za Qwen modele.
|
||||
|
||||
---
|
||||
|
||||
### Maks. stepenice
|
||||
### Max steps
|
||||
|
||||
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.
|
||||
|
||||
Ako ovo nije postavljeno, agent će nastaviti iterirati sve dok model ne odluči da se zaustavi ili korisnik ne prekine sesiju.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"agent": {
|
||||
"brzo mislilac": {
|
||||
"opis": "Brzo razmišljanje s ograničenim iteracijama",
|
||||
"prompt": "Vi brzo mislite. Riješite probleme minimalnim koracima.",
|
||||
"koraci": 5
|
||||
"quick-thinker": {
|
||||
"description": "Fast reasoning with limited iterations",
|
||||
"prompt": "You are a quick thinker. Solve problems with minimal steps.",
|
||||
"steps": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Kada se dostigne ograničenje, agent prima poseban sistemski prompt koji ga upućuje da odgovori sa rezimeom svog rada i preporučenim preostalim zadacima.
|
||||
|
||||
:::caution
|
||||
Naslijeđeno polje `maxSteps` je zastarjelo. Umjesto toga koristite `steps`.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
### Onemogući
|
||||
### Disable
|
||||
|
||||
Postavite na `true` da onemogućite agenta.
|
||||
|
||||
@@ -312,7 +329,7 @@ Navedite prilagođenu sistemsku prompt datoteku za ovog agenta sa `prompt` konfi
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"agent": {
|
||||
"recenzija": {
|
||||
"review": {
|
||||
"prompt": "{file:./prompts/code-review.txt}"
|
||||
}
|
||||
}
|
||||
@@ -326,6 +343,7 @@ Ova putanja je relativna u odnosu na mjesto gdje se nalazi konfiguracijski fajl.
|
||||
### Model
|
||||
|
||||
Koristite `model` konfiguraciju da nadjačate model za ovog agenta. Korisno za korištenje različitih modela optimiziranih za različite zadatke. Na primjer, brži model za planiranje, sposobniji model za implementaciju.
|
||||
|
||||
:::tip
|
||||
Ako ne navedete model, primarni agenti koriste [model globalno konfiguriran](/docs/config#models) dok će podagenti koristiti model primarnog agenta koji je pozvao subagenta.
|
||||
:::
|
||||
@@ -344,7 +362,7 @@ ID modela u vašoj OpenCode konfiguraciji koristi format `provider/model-id`. Na
|
||||
|
||||
---
|
||||
|
||||
### Uvijek
|
||||
### Tools
|
||||
|
||||
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`.
|
||||
|
||||
@@ -369,13 +387,14 @@ Kontrolirajte koji su alati dostupni u ovom agentu koristeći konfiguraciju `too
|
||||
:::note
|
||||
Konfiguracija specifična za agenta poništava globalnu konfiguraciju.
|
||||
:::
|
||||
|
||||
Također možete koristiti zamjenske znakove za kontrolu više alata odjednom. Na primjer, da onemogućite sve alate sa MCP servera:
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"agent": {
|
||||
"plan": {
|
||||
"readonly": {
|
||||
"tools": {
|
||||
"mymcp_*": false,
|
||||
"write": false,
|
||||
@@ -390,7 +409,7 @@ Također možete koristiti zamjenske znakove za kontrolu više alata odjednom. N
|
||||
|
||||
---
|
||||
|
||||
### Dozvole
|
||||
### Permissions
|
||||
|
||||
Možete konfigurirati dozvole za upravljanje radnjama koje agent može poduzeti. Trenutno se dozvole za alate `edit`, `bash` i `webfetch` mogu konfigurirati na:
|
||||
|
||||
@@ -462,7 +481,7 @@ Možete postaviti dozvole za određene bash komande.
|
||||
}
|
||||
```
|
||||
|
||||
Ovo može poprimiti oblik lopte.
|
||||
Ovo može koristiti glob uzorak.
|
||||
|
||||
```json title="opencode.json" {7}
|
||||
{
|
||||
@@ -520,7 +539,7 @@ Opcija `mode` se može postaviti na `primary`, `subagent` ili `all`. Ako `mode`
|
||||
|
||||
---
|
||||
|
||||
### Skriveno
|
||||
### Hidden
|
||||
|
||||
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.
|
||||
|
||||
@@ -535,14 +554,15 @@ Sakrij podagenta iz `@` menija za automatsko dovršavanje sa `hidden: true`. Kor
|
||||
}
|
||||
```
|
||||
|
||||
Ovo utiče samo na vidljivost korisnika u meniju za automatsko dovršavanje. Model i dalje može pozvati skrivene agente putem alata Zadatak ako dozvole dozvoljavaju.
|
||||
Ovo utiče samo na vidljivost korisnika u meniju za automatsko dovršavanje. Skriveni agenti se i dalje mogu pozvati od strane modela putem alata Task ako dozvole to dozvoljavaju.
|
||||
|
||||
:::note
|
||||
Odnosi se samo na `mode: subagent` agente.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
### Dozvole za zadatak
|
||||
### Task permissions
|
||||
|
||||
Kontrolirajte koje podagente agent može pozvati preko Task alata sa `permission.task`. Koristi glob uzorke za fleksibilno uparivanje.
|
||||
|
||||
@@ -564,28 +584,31 @@ Kontrolirajte koje podagente agent može pozvati preko Task alata sa `permission
|
||||
```
|
||||
|
||||
Kada se postavi na `deny`, subagent se u potpunosti uklanja iz opisa alata za zadatak, tako da ga model neće pokušati pozvati.
|
||||
|
||||
:::tip
|
||||
Pravila se procjenjuju po redoslijedu i **pobjeđuje **poslednje odgovarajuće pravilo\*_. U gornjem primjeru, `orchestrator-planner` odgovara i `_`(odbije) i`orchestrator-_`(dozvoli), ali pošto`orchestrator-_`dolazi nakon`\*`, rezultat je `allow`.
|
||||
Pravila se procjenjuju po redoslijedu i **posljednje odgovarajuće pravilo pobjeđuje**. U gornjem primjeru, `orchestrator-planner` odgovara i `*` (deny) i `orchestrator-*` (allow), ali pošto `orchestrator-*` dolazi nakon `*`, rezultat je `allow`.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
Korisnici uvijek mogu pozvati bilo kojeg subagenta direktno preko `@` menija za autodovršavanje, čak i ako bi dozvole za zadatak agenta to uskratile.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
### Boja
|
||||
### Color
|
||||
|
||||
Prilagodite vizualni izgled agenta u korisničkom sučelju s opcijom `color`. Ovo utiče na to kako se agent pojavljuje u interfejsu.
|
||||
|
||||
Koristite važeću heksadecimalnu boju (npr. `#FF5733`) ili boju teme: `primary`, `secondary`, `accent`, `success`, `warning`, `error`, `info`.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"agent": {
|
||||
"kreativno": {
|
||||
"boja": "#ff6b6b"
|
||||
"creative": {
|
||||
"color": "#ff6b6b"
|
||||
},
|
||||
"code-reviewer": {
|
||||
"boja": "akcent"
|
||||
"color": "accent"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -593,7 +616,7 @@ Koristite važeću heksadecimalnu boju (npr. `#FF5733`) ili boju teme: `primary`
|
||||
|
||||
---
|
||||
|
||||
### Leglo P
|
||||
### Top P
|
||||
|
||||
Kontrolirajte raznolikost odgovora s opcijom `top_p`. Alternativa temperaturi za kontrolu nasumice.
|
||||
|
||||
@@ -607,29 +630,31 @@ Kontrolirajte raznolikost odgovora s opcijom `top_p`. Alternativa temperaturi za
|
||||
}
|
||||
```
|
||||
|
||||
Vrijednosti se kreću od 0,0 do 1,0. Niže vrijednosti su više fokusirane, više vrijednosti raznovrsnije.
|
||||
Vrijednosti se kreću od 0.0 do 1.0. Niže vrijednosti su više fokusirane, više vrijednosti raznovrsnije.
|
||||
|
||||
---
|
||||
|
||||
### Dodatni
|
||||
### Additional
|
||||
|
||||
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.
|
||||
|
||||
Na primjer, sa OpenAI-jevim modelima rezonovanja, možete kontrolisati napor rasuđivanja:
|
||||
|
||||
```json title="opencode.json" {6,7}
|
||||
{
|
||||
"agent": {
|
||||
"duboki mislilac": {
|
||||
"opis": "Agent koji koristi veliki napor u razmišljanju za složene probleme",
|
||||
"deep-thinker": {
|
||||
"description": "Agent that uses high reasoning effort for complex problems",
|
||||
"model": "openai/gpt-5",
|
||||
"reasoningEffort": "visoko",
|
||||
"textVerbosity": "niska"
|
||||
"reasoningEffort": "high",
|
||||
"textVerbosity": "low"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ove dodatne opcije su specifične za model i dobavljača. U dokumentaciji vašeg provajdera provjerite dostupne parametre.
|
||||
|
||||
:::tip
|
||||
Pokrenite `opencode models` da vidite listu dostupnih modela.
|
||||
:::
|
||||
@@ -659,9 +684,9 @@ Ova interaktivna komanda će:
|
||||
Evo nekoliko uobičajenih slučajeva upotrebe različitih agenata.
|
||||
|
||||
- **Build agent**: Potpuni razvojni rad sa svim omogućenim alatima
|
||||
- **Agent za plan**: Analiza i planiranje bez unošenja promjena
|
||||
- **Agent za pregled**: Pregled koda sa pristupom samo za čitanje plus alati za dokumentaciju
|
||||
- **Agent za otklanjanje grešaka**: Fokusiran na istragu sa omogućenim bash i alatima za čitanje
|
||||
- **Plan agent**: Analiza i planiranje bez unošenja promjena
|
||||
- **Review agent**: Code review sa pristupom samo za čitanje plus alati za dokumentaciju
|
||||
- **Debug agent**: Fokusiran na istragu sa omogućenim bash i alatima za čitanje
|
||||
- **Docs agent**: Pisanje dokumentacije sa operacijama datoteka, ali bez sistemskih naredbi
|
||||
|
||||
---
|
||||
@@ -669,6 +694,7 @@ Evo nekoliko uobičajenih slučajeva upotrebe različitih agenata.
|
||||
## Primjeri
|
||||
|
||||
Evo nekoliko primjera agenata koji bi vam mogli biti korisni.
|
||||
|
||||
:::tip
|
||||
Imate li agenta kojeg biste željeli podijeliti? [Pošalji PR](https://github.com/anomalyco/opencode).
|
||||
:::
|
||||
@@ -685,13 +711,14 @@ tools:
|
||||
bash: false
|
||||
---
|
||||
|
||||
Vi ste tehnički pisac. Kreirajte jasnu, sveobuhvatnu dokumentaciju.
|
||||
Fokusirajte se na:
|
||||
You are a technical writer. Create clear, comprehensive documentation.
|
||||
|
||||
- Jasna objašnjenja
|
||||
- Pravilna struktura
|
||||
- Primjeri kodova
|
||||
- Jezik prilagođen korisniku
|
||||
Focus on:
|
||||
|
||||
- Clear explanations
|
||||
- Proper structure
|
||||
- Code examples
|
||||
- User-friendly language
|
||||
```
|
||||
|
||||
---
|
||||
@@ -717,7 +744,3 @@ Look for:
|
||||
- Dependency vulnerabilities
|
||||
- Configuration security issues
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
@@ -9,10 +9,9 @@ OpenCode CLI po defaultu pokreće [TUI](/docs/tui) kada se pokrene bez ikakvih a
|
||||
|
||||
```bash
|
||||
opencode
|
||||
|
||||
```
|
||||
|
||||
Ali takođe prihvata komande kao što je dokumentovano na ovoj stranici. Ovo vam omogućava programsku interakciju sa OpenCodeom.
|
||||
Ali takođe prihvata komande kao što je dokumentovano na ovoj stranici. Ovo vam omogućava programsku interakciju sa OpenCode.
|
||||
|
||||
```bash
|
||||
opencode run "Explain how closures work in JavaScript"
|
||||
@@ -26,7 +25,6 @@ Pokrenite korisnički interfejs OpenCode terminala.
|
||||
|
||||
```bash
|
||||
opencode [project]
|
||||
|
||||
```
|
||||
|
||||
#### Zastave
|
||||
@@ -44,7 +42,7 @@ opencode [project]
|
||||
|
||||
---
|
||||
|
||||
## komandante
|
||||
## Commands
|
||||
|
||||
OpenCode CLI takođe ima sledeće komande.
|
||||
|
||||
@@ -60,13 +58,12 @@ opencode agent [command]
|
||||
|
||||
---
|
||||
|
||||
### prilog
|
||||
### attach
|
||||
|
||||
Priključite terminal na već pokrenut OpenCode backend server pokrenut putem `serve` ili `web` komandi.
|
||||
|
||||
```bash
|
||||
opencode attach [url]
|
||||
|
||||
```
|
||||
|
||||
Ovo omogućava korištenje TUI-ja sa udaljenim OpenCode backend-om. na primjer:
|
||||
@@ -88,20 +85,19 @@ opencode attach http://10.20.30.40:4096
|
||||
|
||||
---
|
||||
|
||||
#### kreiraj
|
||||
#### create
|
||||
|
||||
Kreirajte novog agenta s prilagođenom konfiguracijom.
|
||||
|
||||
```bash
|
||||
opencode agent create
|
||||
|
||||
```
|
||||
|
||||
Ova komanda će vas voditi kroz kreiranje novog agenta sa prilagođenim sistemskim promptom i konfiguracijom alata.
|
||||
|
||||
---
|
||||
|
||||
#### lista
|
||||
#### list
|
||||
|
||||
Navedite sve dostupne agente.
|
||||
|
||||
@@ -117,12 +113,11 @@ Naredba za upravljanje vjerodajnicama i prijavom za provajdere.
|
||||
|
||||
```bash
|
||||
opencode auth [command]
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### aplikacija
|
||||
#### login
|
||||
|
||||
OpenCode pokreće lista provajdera na [Models.dev](https://models.dev), tako da možete koristiti `opencode auth login` da konfigurirate API ključeve za bilo kojeg provajdera kojeg želite koristiti. Ovo je pohranjeno u `~/.local/share/opencode/auth.json`.
|
||||
|
||||
@@ -134,13 +129,12 @@ Kada se OpenCode pokrene, učitava dobavljače iz datoteke vjerodajnica. I ako p
|
||||
|
||||
---
|
||||
|
||||
#### lista
|
||||
#### list
|
||||
|
||||
Navodi sve autentifikovane dobavljače pohranjene u datoteci akreditiva.
|
||||
|
||||
```bash
|
||||
opencode auth lista
|
||||
|
||||
opencode auth list
|
||||
```
|
||||
|
||||
Ili kratka verzija.
|
||||
@@ -151,13 +145,12 @@ opencode auth ls
|
||||
|
||||
---
|
||||
|
||||
#### odjava
|
||||
#### logout
|
||||
|
||||
Odjavljuje vas s provajdera tako što ga briše iz datoteke vjerodajnica.
|
||||
|
||||
```bash
|
||||
opencode auth logout
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
@@ -172,20 +165,19 @@ opencode github [command]
|
||||
|
||||
---
|
||||
|
||||
#### instaliraj
|
||||
#### install
|
||||
|
||||
Instalirajte GitHub agenta u svoje spremište.
|
||||
|
||||
```bash
|
||||
opencode github instalacija
|
||||
|
||||
opencode github install
|
||||
```
|
||||
|
||||
Ovo postavlja neophodni tok rada GitHub Actions i vodi vas kroz proces konfiguracije. [Saznajte više](/docs/github).
|
||||
|
||||
---
|
||||
|
||||
#### trči
|
||||
#### run
|
||||
|
||||
Pokrenite GitHub agent. Ovo se obično koristi u GitHub akcijama.
|
||||
|
||||
@@ -208,12 +200,11 @@ Upravljajte serverima protokola konteksta modela.
|
||||
|
||||
```bash
|
||||
opencode mcp [command]
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### dodaj
|
||||
#### add
|
||||
|
||||
Dodajte MCP server svojoj konfiguraciji.
|
||||
|
||||
@@ -225,13 +216,12 @@ Ova komanda će vas voditi kroz dodavanje lokalnog ili udaljenog MCP servera.
|
||||
|
||||
---
|
||||
|
||||
#### lista
|
||||
#### list
|
||||
|
||||
Navedite sve konfigurirane MCP servere i njihov status veze.
|
||||
|
||||
```bash
|
||||
opencode mcp lista
|
||||
|
||||
opencode mcp list
|
||||
```
|
||||
|
||||
Ili koristite kratku verziju.
|
||||
@@ -248,7 +238,6 @@ Autentifikujte se sa MCP serverom koji je omogućen za OAuth.
|
||||
|
||||
```bash
|
||||
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.
|
||||
@@ -262,12 +251,11 @@ Ili koristite kratku verziju.
|
||||
|
||||
```bash
|
||||
opencode mcp auth ls
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### odjava
|
||||
#### logout
|
||||
|
||||
Uklonite OAuth vjerodajnice za MCP server.
|
||||
|
||||
@@ -277,18 +265,17 @@ opencode mcp logout [name]
|
||||
|
||||
---
|
||||
|
||||
#### otklanjanje grešaka
|
||||
#### debug
|
||||
|
||||
Otklanjanje grešaka OAuth veze sa MCP serverom.
|
||||
|
||||
```bash
|
||||
opencode mcp debug <name>
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### model
|
||||
### models
|
||||
|
||||
Navedite sve dostupne modele konfiguriranih provajdera.
|
||||
|
||||
@@ -302,7 +289,6 @@ Opciono možete proslijediti ID provajdera za filtriranje modela po tom dobavlja
|
||||
|
||||
```bash
|
||||
opencode models anthropic
|
||||
|
||||
```
|
||||
|
||||
#### Zastave
|
||||
@@ -312,7 +298,7 @@ opencode models anthropic
|
||||
| `--refresh` | Osvježite predmemoriju 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-u.
|
||||
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.
|
||||
|
||||
```bash
|
||||
opencode models --refresh
|
||||
@@ -320,13 +306,12 @@ opencode models --refresh
|
||||
|
||||
---
|
||||
|
||||
### trči
|
||||
### run
|
||||
|
||||
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.
|
||||
@@ -340,9 +325,9 @@ Također možete priključiti pokrenutu `opencode serve` instancu kako biste izb
|
||||
```bash
|
||||
# Start a headless server in one terminal
|
||||
opencode serve
|
||||
# U drugom terminalu, pokrenite komande koje se vezuju za njega
|
||||
opencode run --attach http://localhost:4096 "Objasni async/await u JavaScriptu"
|
||||
|
||||
# In another terminal, run commands that attach to it
|
||||
opencode run --attach http://localhost:4096 "Explain async/await in JavaScript"
|
||||
```
|
||||
|
||||
#### Zastave
|
||||
@@ -364,7 +349,7 @@ opencode run --attach http://localhost:4096 "Objasni async/await u JavaScriptu"
|
||||
|
||||
---
|
||||
|
||||
### poslužiti
|
||||
### serve
|
||||
|
||||
Pokrenite OpenCode server bez glave za pristup API-ju. Pogledajte [server docs](/docs/server) za kompletan HTTP interfejs.
|
||||
|
||||
@@ -385,18 +370,17 @@ Ovo pokreće HTTP server koji pruža API pristup funkcionalnosti otvorenog koda
|
||||
|
||||
---
|
||||
|
||||
### sesija
|
||||
### session
|
||||
|
||||
Upravljajte OpenCode sesijama.
|
||||
|
||||
```bash
|
||||
opencode sesija [naredba]
|
||||
|
||||
opencode session [command]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### lista
|
||||
#### list
|
||||
|
||||
Navedite sve OpenCode sesije.
|
||||
|
||||
@@ -413,13 +397,12 @@ opencode session list
|
||||
|
||||
---
|
||||
|
||||
### statistika
|
||||
### stats
|
||||
|
||||
Prikaži statistiku upotrebe tokena i troškova za vaše OpenCode sesije.
|
||||
|
||||
```bash
|
||||
opencode stats
|
||||
|
||||
```
|
||||
|
||||
#### Zastave
|
||||
@@ -433,7 +416,7 @@ opencode stats
|
||||
|
||||
---
|
||||
|
||||
### izvoz
|
||||
### export
|
||||
|
||||
Izvezite podatke sesije kao JSON.
|
||||
|
||||
@@ -445,13 +428,12 @@ Ako ne unesete ID sesije, od vas će biti zatraženo da odaberete neku od dostup
|
||||
|
||||
---
|
||||
|
||||
### uvoz
|
||||
### import
|
||||
|
||||
Uvezite podatke sesije iz JSON datoteke ili OpenCode dijeljenog URL-a.
|
||||
|
||||
```bash
|
||||
opencode import <file>
|
||||
|
||||
```
|
||||
|
||||
Možete uvesti iz lokalne datoteke ili OpenCode dijeljenog URL-a.
|
||||
@@ -469,10 +451,9 @@ Pokrenite OpenCode server bez glave sa web interfejsom.
|
||||
|
||||
```bash
|
||||
opencode web
|
||||
|
||||
```
|
||||
|
||||
Ovo pokreće HTTP server i otvara web pretraživač za pristup OpenCodeu 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 preko web interfejsa. Postavite `OPENCODE_SERVER_PASSWORD` da omogućite HTTP osnovnu auth (korisničko ime je zadano na `opencode`).
|
||||
|
||||
#### Zastave
|
||||
|
||||
@@ -505,13 +486,12 @@ Ova komanda pokreće ACP server koji komunicira preko stdin/stdout koristeći nd
|
||||
|
||||
---
|
||||
|
||||
### deinstaliraj
|
||||
### uninstall
|
||||
|
||||
Deinstalirajte OpenCode i uklonite sve povezane datoteke.
|
||||
|
||||
```bash
|
||||
opencode uninstall
|
||||
|
||||
```
|
||||
|
||||
#### Zastave
|
||||
@@ -537,7 +517,6 @@ Za nadogradnju na najnoviju verziju.
|
||||
|
||||
```bash
|
||||
opencode upgrade
|
||||
|
||||
```
|
||||
|
||||
Za nadogradnju na određenu verziju.
|
||||
@@ -592,7 +571,7 @@ OpenCode se može konfigurirati pomoću varijabli okruženja.
|
||||
| `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 `OPENCODE_GIT_BASH_PATH`/`OPENCODE_CONFIG` |
|
||||
| `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 |
|
||||
|
||||
@@ -614,5 +593,5 @@ Ove varijable okruženja omogućavaju eksperimentalne karakteristike koje se mog
|
||||
| `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 funkcije smanjenja |
|
||||
| `OPENCODE_EXPERIMENTAL_MARKDOWN` | boolean | Omogući eksperimentalne Markdown funkcije |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Omogući režim plana |
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: komandant
|
||||
title: Komande
|
||||
description: Kreirajte prilagođene komande za zadatke koji se ponavljaju.
|
||||
---
|
||||
|
||||
@@ -7,7 +7,6 @@ Prilagođene komande vam omogućavaju da odredite prompt koji želite da pokrene
|
||||
|
||||
```bash frame="none"
|
||||
/my-command
|
||||
|
||||
```
|
||||
|
||||
Prilagođene komande su dodatak ugrađenim komandama kao što su `/init`, `/undo`, `/redo`, `/share`, `/help`. [Saznajte više](/docs/tui#commands).
|
||||
@@ -16,7 +15,7 @@ Prilagođene komande su dodatak ugrađenim komandama kao što su `/init`, `/undo
|
||||
|
||||
## Kreirajte komandne fajlove
|
||||
|
||||
Kreirajte mardown fajlove u direktorijumu `commands/` da definišete prilagođene komande.
|
||||
Kreirajte markdown fajlove u direktorijumu `commands/` da definišete prilagođene komande.
|
||||
Kreiraj `.opencode/commands/test.md`:
|
||||
|
||||
```md title=".opencode/commands/test.md"
|
||||
@@ -35,7 +34,6 @@ Koristite komandu tako što ćete upisati `/` nakon čega slijedi naziv komande.
|
||||
|
||||
```bash frame="none"
|
||||
"/test"
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
@@ -71,17 +69,16 @@ Sada možete pokrenuti ovu naredbu u TUI:
|
||||
|
||||
```bash frame="none"
|
||||
/test
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Markdown
|
||||
|
||||
Također možete definirati komande koristeći mardown datoteke. Stavite ih u:
|
||||
Također možete definirati komande koristeći markdown datoteke. Stavite ih u:
|
||||
|
||||
- Globalno: `~/.config/opencode/commands/`
|
||||
- Po projektu: `.opencode/commands/
|
||||
- Po projektu: `.opencode/commands/`
|
||||
|
||||
```markdown title="~/.config/opencode/commands/test.md"
|
||||
---
|
||||
@@ -94,12 +91,10 @@ Run the full test suite with coverage report and show any failures.
|
||||
Focus on the failing tests and suggest fixes.
|
||||
```
|
||||
|
||||
Ime datoteke za označavanje postaje ime naredbe. Na primjer, `test.md` dozvoljava
|
||||
pokrećeš:
|
||||
Ime markdown datoteke postaje ime naredbe. Na primjer, `test.md` vam omogućava da pokrenete:
|
||||
|
||||
```bash frame="none"
|
||||
/test
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
@@ -126,8 +121,7 @@ Include proper typing and basic structure.
|
||||
Pokrenite naredbu s argumentima:
|
||||
|
||||
```bash frame="none"
|
||||
/componentButton
|
||||
|
||||
/component Button
|
||||
```
|
||||
|
||||
I `$ARGUMENTS` će biti zamijenjen sa `Button`.
|
||||
@@ -137,7 +131,8 @@ Također možete pristupiti pojedinačnim argumentima koristeći pozicione param
|
||||
- `$2` - Drugi argument
|
||||
- `$3` - Treći argument
|
||||
- I tako dalje...
|
||||
na primjer:
|
||||
|
||||
Na primjer:
|
||||
|
||||
```md title=".opencode/commands/create-file.md"
|
||||
---
|
||||
@@ -152,7 +147,6 @@ Pokrenite naredbu:
|
||||
|
||||
```bash frame="none"
|
||||
/create-file config.json src "{ \"key\": \"value\" }"
|
||||
|
||||
```
|
||||
|
||||
Ovo zamjenjuje:
|
||||
@@ -163,9 +157,9 @@ Ovo zamjenjuje:
|
||||
|
||||
---
|
||||
|
||||
### Izlaz ljuske
|
||||
### Shell izlaz
|
||||
|
||||
Koristite _!`command`_ da ubacite [bash command](/docs/tui#bash-commands) izlaz u svoj prompt.
|
||||
Koristite _!`command`_ da ubacite izlaz [bash command](/docs/tui#bash-commands) u svoj prompt.
|
||||
Na primjer, da kreirate prilagođenu naredbu koja analizira pokrivenost testom:
|
||||
|
||||
```md title=".opencode/commands/analyze-coverage.md"
|
||||
@@ -186,9 +180,10 @@ Ili da vidite nedavne promjene:
|
||||
description: Review recent changes
|
||||
---
|
||||
|
||||
Nedavna git urezivanja:
|
||||
Recent git commits:
|
||||
!`git log --oneline -10`
|
||||
Pregledajte ove promjene i predložite bilo kakva poboljšanja.
|
||||
|
||||
Review these changes and suggest any improvements.
|
||||
```
|
||||
|
||||
Naredbe se pokreću u korijenskom direktoriju vašeg projekta i njihov izlaz postaje dio prompta.
|
||||
@@ -226,7 +221,7 @@ Opcija `template` definira prompt koji će biti poslan LLM-u kada se naredba izv
|
||||
{
|
||||
"command": {
|
||||
"test": {
|
||||
"template": "Pokrenite kompletan testni paket sa izvještajem o pokrivenosti i pokažite sve greške.\nFokusirajte se na neuspjele testove i predložite popravke."
|
||||
"template": "Run the full test suite with coverage report and show any failures.\nFocus on the failing tests and suggest fixes."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,7 +269,7 @@ Ovo je **opciona** opcija konfiguracije. Ako nije navedeno, podrazumevano je va
|
||||
|
||||
---
|
||||
|
||||
### Subdatatak
|
||||
### Subtask
|
||||
|
||||
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,
|
||||
@@ -312,14 +307,10 @@ Ovo je **opciona** opcija konfiguracije.
|
||||
|
||||
---
|
||||
|
||||
## Embedded
|
||||
## Ugrađene
|
||||
|
||||
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.
|
||||
:::
|
||||
Ako definirate prilagođenu naredbu s istim imenom, ona će nadjačati ugrađenu naredbu.
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
@@ -14,7 +14,7 @@ OpenCode podržava i **JSON** i **JSONC** (JSON sa komentarima) formate.
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
// Konfiguracija teme
|
||||
// Theme configuration
|
||||
"theme": "opencode",
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"autoupdate": true,
|
||||
@@ -93,7 +93,7 @@ Globalna konfiguracija poništava zadane postavke udaljene organizacije.
|
||||
|
||||
---
|
||||
|
||||
### Nakon projekta
|
||||
### Project
|
||||
|
||||
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 +104,7 @@ Ovo je također sigurno provjeriti u Git i koristi istu shemu kao globalna.
|
||||
|
||||
---
|
||||
|
||||
### Custom track
|
||||
### Custom config
|
||||
|
||||
Navedite prilagođenu putanju konfiguracijske datoteke koristeći varijablu okruženja `OPENCODE_CONFIG`.
|
||||
|
||||
@@ -117,7 +117,7 @@ Prilagođena konfiguracija se učitava između globalne i projektne konfiguracij
|
||||
|
||||
---
|
||||
|
||||
### Prilagođeni imenik
|
||||
### Custom directory
|
||||
|
||||
Navedite prilagođeni konfiguracijski direktorij koristeći `OPENCODE_CONFIG_DIR`
|
||||
varijabla okruženja. U ovom direktoriju će se tražiti agenti, komande,
|
||||
@@ -127,7 +127,6 @@ prate istu strukturu.
|
||||
```bash
|
||||
export OPENCODE_CONFIG_DIR=/path/to/my/config-directory
|
||||
opencode run "Hello world"
|
||||
|
||||
```
|
||||
|
||||
Prilagođeni direktorij se učitava nakon direktorija globalne konfiguracije i `.opencode`, tako da **može nadjačati** njihove postavke.
|
||||
@@ -195,7 +194,7 @@ Dostupne opcije:
|
||||
|
||||
---
|
||||
|
||||
### Uvijek
|
||||
### Tools
|
||||
|
||||
Možete upravljati alatima koje LLM može koristiti putem opcije `tools`.
|
||||
|
||||
@@ -282,7 +281,7 @@ Amazon Bedrock podržava konfiguraciju specifičnu za AWS:
|
||||
|
||||
---
|
||||
|
||||
### Domaći
|
||||
### Theme
|
||||
|
||||
Možete konfigurirati temu koju želite koristiti u svojoj OpenCode konfiguraciji putem opcije `theme`.
|
||||
|
||||
@@ -319,7 +318,7 @@ Možete konfigurirati specijalizirane agente za određene zadatke putem opcije `
|
||||
}
|
||||
```
|
||||
|
||||
Također možete definirati agente koristeći mardown datoteke u `~/.config/opencode/agents/` ili `.opencode/agents/`. [Saznajte više ovdje](/docs/agents).
|
||||
Također možete definirati agente koristeći markdown datoteke u `~/.config/opencode/agents/` ili `.opencode/agents/`. [Saznajte više ovdje](/docs/agents).
|
||||
|
||||
---
|
||||
|
||||
@@ -359,7 +358,7 @@ Ovo traje:
|
||||
|
||||
---
|
||||
|
||||
### komandante
|
||||
### Command
|
||||
|
||||
Možete konfigurirati prilagođene komande za ponavljanje zadataka putem opcije `command`.
|
||||
|
||||
@@ -381,7 +380,7 @@ Možete konfigurirati prilagođene komande za ponavljanje zadataka putem opcije
|
||||
}
|
||||
```
|
||||
|
||||
Također možete definirati komande koristeći mardown fajlove u `~/.config/opencode/commands/` ili `.opencode/commands/`. [Saznajte više ovdje](/docs/commands).
|
||||
Također možete definirati komande koristeći markdown fajlove u `~/.config/opencode/commands/` ili `.opencode/commands/`. [Saznajte više ovdje](/docs/commands).
|
||||
|
||||
---
|
||||
|
||||
@@ -416,7 +415,7 @@ Imajte na umu da ovo funkcionira samo ako nije instalirano pomoću upravitelja p
|
||||
|
||||
---
|
||||
|
||||
### Trenerke
|
||||
### Formatters
|
||||
|
||||
Možete konfigurirati formatere koda putem opcije `formatter`.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Ekosistem
|
||||
description: Projekti i integracije izgrađeni uz OpenCode.
|
||||
---
|
||||
|
||||
Kolekcija projekata zajednice izgrađenih na OpenCode-u.
|
||||
Kolekcija projekata zajednice izgrađenih na OpenCode.
|
||||
:::note
|
||||
Želite li na ovu listu dodati svoj OpenCode projekat? Pošaljite PR.
|
||||
:::
|
||||
@@ -17,16 +17,16 @@ Također možete pogledati [awesome-opencode](https://github.com/awesome-opencod
|
||||
| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | --- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | Automatski pokrenite OpenCode sesije u izoliranim Daytona sandboxovima uz git sinhronizaciju i preglede uživo |
|
||||
| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatski ubacite Helicone zaglavlja sesije za grupisanje zahtjeva |
|
||||
| [opencode-type-iject](https://github.com/nick-vi/opencode-type-inject) | Automatski ubaci TypeScript/Svelte tipove u čitanje datoteka pomoću alata za pretraživanje |
|
||||
| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Automatski ubaci TypeScript/Svelte tipove u čitanje datoteka pomoću alata za pretraživanje |
|
||||
| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Koristite svoju ChatGPT Plus/Pro pretplatu umjesto API kredita |
|
||||
| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Koristite svoj postojeći Gemini plan umjesto API naplate |
|
||||
| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Koristite besplatne modele Antigravity umjesto API naplate |
|
||||
| [opencodentigravity-auth](https://github.com/NoeFabris/opencodentigravity-auth) | Koristite besplatne modele Antigravity umjesto API naplate |
|
||||
| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | Izolacija devcontainer-a s više grana s plitkim klonovima i automatski dodijeljenim portovima |
|
||||
| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth dodatak, s podrškom za Google pretraživanje i robusnijim API rukovanjem |
|
||||
| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimizirajte korištenje tokena smanjenjem izlaza zastarjelih alata |
|
||||
| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Dodajte podršku za izvorno web pretraživanje za podržane provajdere sa stilom utemeljenim na Googleu |
|
||||
| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Omogućuje AI agentima da pokreću pozadinske procese u PTY-u, šalju im interaktivni ulaz. |
|
||||
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Upute za neinteraktivne naredbe ljuske - sprječava visi od TTY ovisnih operacija | | [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Pratite upotrebu OpenCodea sa Wakatime |
|
||||
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Upute za neinteraktivne naredbe ljuske - sprječava visi od TTY ovisnih operacija | | [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Pratite upotrebu OpenCode sa Wakatime |
|
||||
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Očistite tabele umanjenja vrijednosti koje su izradili LLM |
|
||||
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x brže uređivanje koda s Morph Fast Apply API-jem i markerima za lijeno uređivanje |
|
||||
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Pozadinski agenti, unapred izgrađeni LSP/AST/MCP alati, kurirani agenti, kompatibilni sa Claude Code |
|
||||
@@ -38,7 +38,7 @@ Također možete pogledati [awesome-opencode](https://github.com/awesome-opencod
|
||||
| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interaktivni pregled plana s vizualnim napomenama i privatnim/offline dijeljenjem |
|
||||
| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Proširite opencode /komande u moćan sistem orkestracije sa granularnom kontrolom toka |
|
||||
| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | Planirajte ponavljajuće poslove koristeći launchd (Mac) ili systemd (Linux) sa cron sintaksom | | [micode](https://github.com/vtemian/micode) | Strukturirana Brainstorm → Plan → Implementacija toka rada uz kontinuitet sesije |
|
||||
| [oktobar](https://github.com/vtemian/octto) | Interaktivno korisničko sučelje pretraživača za AI brainstorming sa obrascima za više pitanja |
|
||||
| [octto](https://github.com/vtemian/octto) | Interaktivno korisničko sučelje pretraživača za AI brainstorming sa obrascima za više pitanja |
|
||||
| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Pozadinski agenti u stilu Claudea s asinhroniziranim delegiranjem i postojanošću konteksta |
|
||||
| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | Notifikacije izvornog OS-a za OpenCode – znajte kada se zadaci dovrše |
|
||||
| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | Uvezeni višeagentni orkestracijski pojas – 16 komponenti, jedna instalacija |
|
||||
@@ -48,25 +48,25 @@ Također možete pogledati [awesome-opencode](https://github.com/awesome-opencod
|
||||
|
||||
## Projekti
|
||||
|
||||
| Ime | Opis |
|
||||
| ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
|
||||
| [kimaki](https://github.com/remorses/kimaki) | Discord bot za kontrolu OpenCode sesija, izgrađen na SDK |
|
||||
| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | Neovim dodatak za upite svjestan uređivača, izgrađen na API |
|
||||
| [portal](https://github.com/hosenur/portal) | Mobilni korisnički interfejs za OpenCode preko Tailscale/VPN |
|
||||
| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | Predložak za izgradnju OpenCode dodataka |
|
||||
| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | Neovim frontend za opencode - terminal baziran AI agent za kodiranje |
|
||||
| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | Vercel AI SDK dobavljač za korištenje OpenCodea putem @opencode-ai/sdk |
|
||||
| [OpenChamber](https://github.com/btriapitsyn/openchamber) | Web / Desktop App i VS Code Extension za OpenCode |
|
||||
| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | Obsidian dodatak koji ugrađuje OpenCode u Obsidian-ov UI |
|
||||
| [OpenWork](https://github.com/different-ai/openwork) | Alternativa otvorenog koda Claudeu Coworku, pokretana pomoću OpenCode |
|
||||
| [ocx](https://github.com/kdcokenny/ocx) | OpenCode menadžer ekstenzija sa prenosivim, izolovanim profilima. |
|
||||
| [CodeNomad](https://github.com/NeuralNomadsAI/CodeNomad) | Desktop, Web, Mobile i Remote Client aplikacija za OpenCode |
|
||||
| Ime | Opis |
|
||||
| ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- |
|
||||
| [kimaki](https://github.com/remorses/kimaki) | Discord bot za kontrolu OpenCode sesija, izgrađen na SDK |
|
||||
| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | Neovim dodatak za upite svjestan uređivača, izgrađen na API |
|
||||
| [portal](https://github.com/hosenur/portal) | Mobilni korisnički interfejs za OpenCode preko Tailscale/VPN |
|
||||
| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | Predložak za izgradnju OpenCode dodataka |
|
||||
| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | Neovim frontend za opencode - terminal baziran AI agent za kodiranje |
|
||||
| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | Vercel AI SDK dobavljač za korištenje OpenCode putem @opencodei/sdk |
|
||||
| [OpenChamber](https://github.com/btriapitsyn/openchamber) | Web / Desktop App i VS Code Extension za OpenCode |
|
||||
| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | Obsidian dodatak koji ugrađuje OpenCode u Obsidian-ov UI |
|
||||
| [OpenWork](https://github.com/different-ai/openwork) | Alternativa otvorenog koda Claudeu Coworku, pokretana pomoću OpenCode |
|
||||
| [ocx](https://github.com/kdcokenny/ocx) | OpenCode menadžer ekstenzija sa prenosivim, izolovanim profilima. |
|
||||
| [CodeNomad](https://github.com/NeuralNomadsAI/CodeNomad) | Desktop, Web, Mobile i Remote Client aplikacija za OpenCode |
|
||||
|
||||
---
|
||||
|
||||
## Agenti
|
||||
|
||||
| Ime | Opis |
|
||||
| ----------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| [Agentic](https://github.com/Cluster444/agentic) | Modularni AI agenti i komande za strukturirani razvoj |
|
||||
| [opencode-agents](https://github.com/darrenhinde/opencode-agents) | Konfiguracije, upiti, agenti i dodaci za poboljšane tokove rada |
|
||||
| Ime | Opis |
|
||||
| ------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| [Agentic](https://github.com/Cluster444/agentic) | Modularni AI agenti i komande za strukturirani razvoj |
|
||||
| [opencodegents](https://github.com/darrenhinde/opencodegents) | Konfiguracije, upiti, agenti i dodaci za poboljšane tokove rada |
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user