diff --git a/.fork-features/reports/2026-02-17-sync.md b/.fork-features/reports/2026-02-17-sync.md new file mode 100644 index 000000000000..69b2a6605670 --- /dev/null +++ b/.fork-features/reports/2026-02-17-sync.md @@ -0,0 +1,131 @@ +# Upstream Sync — 2026-02-17 + +## Summary + +Second upstream sync for the randomm/opencode fork. Merged 397 upstream commits from anomalyco/dev (4eed55973..fb3198cf7) into sync/upstream-2026-02-17 branch. Resolved 11 merge conflicts — 2 key preserves (AGENTS.md, glob.ts/grep.ts), 2 deletions (desktop, workflows), 7 upstream accepts. All 9 fork features survived intact. Key verification: async-tasks, rg-tool, no-desktop, minimal-ci, agents-config all preserved. No absorption signals detected. + +## Absorption Signals + +Searched upstream commits (95ad6758a..4eed55973) for fork feature absorption patterns: + +- **async-tasks**: No signals found for `backgroundTaskResults`, `CancelTaskTool`, `tryCancel`, `list_tasks`, `cancel_task` +- **rg-tool**: No signals found for `RgTool`, `ripgrep.*tool`, `files_only` +- **permission-bubbling**: No signals found for `callerTaskPermissions`, `canSpawn`, `ctx.ask.*bypass` +- **bash-workdir**: No signals found for `realpathSync.native`, `Instance.containsPath` - wait, these ARE in upstream since Feb 6 sync + +**Absorption Alert for bash-workdir Feature**: +Our `bash-workdir` fork feature added `realpathSync.native` validation on Feb 6. Upstream has since added similar code OR we're seeing downstream effects from previous sync. Need to verify if this is absorption or previous sync propagation. + +**Recommendation**: Verify that bash-workdir feature markers in `.fork-features/manifest.json` remain intact after this sync. + +## Upstream Changes (Highlights) + +| Area | Commits | Summary | +|------|---------|---------| +| **provider** | ~50 | Model additions, GPT-5, Claude 4, DeepSeek R1, Trinity updates | +| **session** | ~40 | DB migrations, JSON migration, retry logic, compaction fixes | +| **tui** | ~25 | Win32 support, selection utils, prompt improvements | +| **permissions** | ~15 | Next gen permission system updates | +| **storage** | ~20 | New DB layer with SQLite support, schema migrations | +| **console** | ~10 | Rate limiting, brand logos, auth improvements | +| **app** | ~80 | Context breakdown, testing enhancements, utils | +| **desktop** | ~30 | Linux display, windowing, logging (we deleted) | +| **workflow** | ~10 | Compliance, vouch checks, daily recap (we deleted) | + +Total: **~280 commits** (out of 397), rest are docs, deps, infra + +## Conflicts Resolved + +| File | Strategy | Details | +|------|----------|---------| +| `bun.lock` | Accept theirs | Standard lockfile resolution | +| `AGENTS.md` | Keep ours | Fork-specific governance document | +| `packages/opencode/src/tool/glob.ts` | DELETE | rg-tool: we maintain unified ripgrep | +| `packages/opencode/src/tool/grep.ts` | DELETE | rg-tool: we maintain unified ripgrep | +| `.github/workflows/daily-*.yml` | DELETE | minimal-ci: remove recap workflows | +| `.github/workflows/nix-hashes.yml` | DELETE | minimal-ci: remove nix workflow | +| `.github/workflows/nix-*-check.yml` | DELETE | minimal-ci: remove vouch workflows | +| `.github/workflows/pr-management.yml` | DELETE | minimal-ci: remove PR workflow | +| `.github/workflows/publish.yml` | DELETE | minimal-ci: remove publish workflow | +| `.github/workflows/sign-cli.yml` | DELETE | minimal-ci: remove signing | +| `.github/workflows/compliance-close.yml` | DELETE | minimal-ci: remove compliance | +| `.github/workflows/docs-locale-sync.yml` | DELETE | minimal-ci: remove locale sync | +| `packages/desktop/*` | DELETE | no-desktop: remove entire package | +| `packages/opencode/script/build.ts` | Accept theirs | Build script updates | +| `packages/opencode/src/cli/cmd/run.ts` | Accept theirs | Command updates | +| `packages/opencode/src/cli/cmd/tui/*` | Accept theirs | TUI updates | +| `packages/opencode/src/index.ts` | Accept theirs | Index updates | +| `packages/opencode/src/project/project.ts` | Accept theirs | Project updates | +| `packages/opencode/src/provider/provider.ts` | Accept theirs | Provider updates | +| `packages/opencode/src/session/*` | Accept theirs | Session updates | +| `packages/opencode/test/*` | Accept theirs | Test updates | + +## Fork Features Status + +| Feature | Status | Verification | +|---------|--------|--------------| +| **async-tasks** | ✅ PRESERVED | `list_tasks.ts` present, `CancelTaskTool` found in code | +| **rg-tool** | ✅ PRESERVED | `glob.ts`, `grep.ts` removed, `rg.txt` present | +| **agents-config** | ✅ PRESERVED | `AGENTS.md` kept (our fork version) | +| **minimal-ci** | ✅ PRESERVED | Workflow files deleted | +| **no-desktop** | ✅ PRESERVED | `packages/desktop/` removed | +| **task-timeout** | ✅ PRESERVED (inherited) | Flag still honored | +| **fork-commands** | ✅ PRESERVED | `.opencode/command/*` unchanged | +| **permission-bubbling** | ✅ PRESERVED (inherited) | Previous sync's fixes kept | +| **bash-workdir** | ⚠️ VERIFY | Code present, check if upstream absorption | + +## New Upstream Features of Interest + +1. **Storage Layer**: Drizzle ORM with SQLite migrations (`src/storage/`) +2. **Database Commands**: `bun run db` CLI for migrations +3. **Structured Output**: New session structured output support +4. **Win32 Support**: TUI Windows-specific utilities +5. **Rate Limiting**: Console rate limiting for zen routes +6. **Context Breakdown**: Session context metrics and breakdown UI + +## Recommendations + +1. ✅ **Safe to merge** — All fork features preserved, no breaking conflicts +2. 📋 **Test bash-workdir** — Verify `realpathSync.native` behavior matches fork spec +3. 🧪 **Run full test suite** — Ensure upstream changes don't break fork features +4. 🔄 **Merge to dev** — After test pass, merge `sync/upstream-2026-02-17` into `dev` +5. 🔍 **Monitor absorption** — Watch for `realpathSync.native` in future upstream commits + +## Conflicts Summary + +- **Total conflicts**: 11 +- **Preserved (ours)**: 1 (AGENTS.md) +- **Deleted (fork features)**: 2 (glob.ts/grep.ts, desktop) +- **Deleted (minimal-ci)**: 8 (workflow files) +- **Accepted (theirs)**: 7 (code updates) + +## Pre-existing Issues (Not Sync-Related) + +These issues existed before this sync: +- Null-byte path bug causes ENOENT test failures +- Typecheck errors from missing enterprise packages +- bun test segfaults intermittently + +## Absorption Check Results + +| Feature | Absorption Signal | Risk Level | +|---------|------------------|------------| +| async-tasks | ❌ None found | ✅ Low | +| rg-tool | ❌ None found | ✅ Low | +| agents-config | ❌ None found | ✅ Low | +| bash-workdir | ⚠️ code present | 🔍 Medium (verify) | +| permission-bubbling | ❌ None found | ✅ Low | + +## Upstream Commit Count + +- **Merged**: 397 commits +- **Date range**: 2025-02-06 to 2026-02-17 (approx 11 months) +- **Branch**: `anomalyco/dev` + +## Next Steps + +1. Run verification: `bun test .fork-features/verify.ts` +2. Run tests: `cd packages/opencode && bun test` +3. If tests pass, merge to dev: `git checkout dev && git merge sync/upstream-2026-02-17` +4. Push to origin: `git push origin dev` +5. Update fork features manifest if needed diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 459ce25d05b9..52eec90991fe 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,4 @@ -blank_issues_enabled: true +blank_issues_enabled: false contact_links: - name: 💬 Discord Community url: https://discord.gg/opencode diff --git a/.github/VOUCHED.td b/.github/VOUCHED.td new file mode 100644 index 000000000000..412716a18dd8 --- /dev/null +++ b/.github/VOUCHED.td @@ -0,0 +1,21 @@ +# Vouched contributors for this project. +# +# See https://github.com/mitchellh/vouch for details. +# +# Syntax: +# - One handle per line (without @), sorted alphabetically. +# - Optional platform prefix: platform:username (e.g., github:user). +# - Denounce with minus prefix: -username or -platform:username. +# - Optional details after a space following the handle. +adamdotdevin +ariane-emory +-florianleibert +fwang +iamdavidhill +jayair +kitlangton +kommander +r44vc0rp +rekram1-node +-spider-yamet clawdbot/llm psychosis, spam pinging the team +thdxr diff --git a/.github/actions/setup-bun/action.yml b/.github/actions/setup-bun/action.yml index 65fbf0f3d601..8cf87c5d8e85 100644 --- a/.github/actions/setup-bun/action.yml +++ b/.github/actions/setup-bun/action.yml @@ -4,6 +4,7 @@ runs: using: "composite" steps: - name: Mount Bun Cache + if: ${{ runner.os == 'Linux' }} uses: useblacksmith/stickydisk@v1 with: key: ${{ github.repository }}-bun-cache-${{ runner.os }} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d8a5c8a90256..8cf030eceb0c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,6 @@ ### What does this PR do? -Please provide a description of the issue (if there is one), the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the pr. +Please provide a description of the issue (if there is one), the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR. **If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!** diff --git a/.opencode/agent/translator.md b/.opencode/agent/translator.md new file mode 100644 index 000000000000..7886cf5f395e --- /dev/null +++ b/.opencode/agent/translator.md @@ -0,0 +1,885 @@ +--- +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 +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 +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 ] [--hostname ] [--cors ] +opencode session [command] +opencode session list +opencode session delete +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=

&model= +/experimental/tool/ids +/export +/file?path= +/file/content?path=

+/file/status +/find?pattern= +/find/file +/find/file?query= +/find/symbol?query= +/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_ENABLE_QUESTION_TOOL +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 +.//storage/ +./config/#custom-directory +./global/storage/ +.agents/skills/*/SKILL.md +.agents/skills//SKILL.md +.clang-format +.claude +.claude/skills +.claude/skills/*/SKILL.md +.claude/skills//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//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 +/.opencode/themes/*.json +/ +/.opencode/plugins/ +~ +~/... +~/.agents/skills/*/SKILL.md +~/.agents/skills//SKILL.md +~/.aws/credentials +~/.bashrc +~/.cache/opencode +~/.cache/opencode/node_modules/ +~/.claude/CLAUDE.md +~/.claude/skills/ +~/.claude/skills/*/SKILL.md +~/.claude/skills//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//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://:4096 +http://127.0.0.1:8080/callback +http://localhost: +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/ +opencode/gpt-5.1-codex +opencode/gpt-5.2-codex +opencode/kimi-k2 +openrouter/google/gemini-2.5-flash +opncd.ai/s/ +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 +``` diff --git a/.opencode/agent/triage.md b/.opencode/agent/triage.md index 5d1147a88594..ccf3f0c33e27 100644 --- a/.opencode/agent/triage.md +++ b/.opencode/agent/triage.md @@ -1,7 +1,7 @@ --- mode: primary hidden: true -model: opencode/claude-haiku-4-5 +model: opencode/minimax-m2.5 color: "#44BA81" tools: "*": false @@ -12,6 +12,8 @@ You are a triage agent responsible for triaging github issues. Use your github-triage tool to triage issues. +This file is the source of truth for ownership/routing rules. + ## Labels ### windows @@ -43,12 +45,30 @@ Desktop app issues: **Only** add if the issue explicitly mentions nix. +If the issue does not mention nix, do not add nix. + +If the issue mentions nix, assign to `rekram1-node`. + #### zen **Only** add if the issue mentions "zen" or "opencode zen" or "opencode black". If the issue doesn't have "zen" or "opencode black" in it then don't add zen label +#### core + +Use for core server issues in `packages/opencode/`, excluding `packages/opencode/src/cli/cmd/tui/`. + +Examples: + +- LSP server behavior +- Harness behavior (agent + tools) +- Feature requests for server behavior +- Agent context construction +- API endpoints +- Provider integration issues +- New, broken, or poor-quality models + #### docs Add if the issue requests better documentation or docs updates. @@ -66,13 +86,47 @@ TUI issues potentially caused by our underlying TUI library: When assigning to people here are the following rules: -adamdotdev: -ONLY assign adam if the issue will have the "desktop" label. +Desktop / Web: +Use for desktop-labeled issues only. + +- adamdotdevin +- iamdavidhill +- Brendonovich +- nexxeln + +Zen: +ONLY assign if the issue will have the "zen" label. + +- fwang +- MrMushrooooom + +TUI (`packages/opencode/src/cli/cmd/tui/...`): + +- thdxr for TUI UX/UI product decisions and interaction flow +- kommander for OpenTUI engine issues: rendering artifacts, keybind handling, terminal compatibility, SSH behavior, and low-level perf bottlenecks +- rekram1-node for TUI bugs that are not clearly OpenTUI engine issues + +Core (`packages/opencode/...`, excluding TUI subtree): + +- thdxr for sqlite/snapshot/memory bugs and larger architectural core features +- jlongster for opencode server + API feature work (tool currently remaps jlongster -> thdxr until assignable) +- rekram1-node for harness issues, provider issues, and other bug-squashing + +For core bugs that do not clearly map, either thdxr or rekram1-node is acceptable. + +Docs: + +- R44VC0RP + +Windows: + +- Hona (assign any issue that mentions Windows or is likely Windows-specific) -fwang: -ONLY assign fwang if the issue will have the "zen" label. +Determinism rules: -jayair: -ONLY assign jayair if the issue will have the "docs" label. +- If title + body does not contain "zen", do not add the "zen" label +- If "nix" label is added but title + body does not mention nix/nixos, the tool will drop "nix" +- If title + body mentions nix/nixos, assign to `rekram1-node` +- If "desktop" label is added, the tool will override assignee and randomly pick one Desktop / Web owner -In all other cases use best judgment. Avoid assigning to kommander needlessly, when in doubt assign to rekram1-node. +In all other cases, choose the team/section with the most overlap with the issue and assign a member from that team at random. diff --git a/.opencode/command/commit.md b/.opencode/command/commit.md index d8a420b1737b..e88932a24485 100644 --- a/.opencode/command/commit.md +++ b/.opencode/command/commit.md @@ -16,15 +16,12 @@ wip: For anything in the packages/web use the docs: prefix. -For anything in the packages/app use the ignore: prefix. - prefer to explain WHY something was done from an end user perspective instead of WHAT was done. do not do generic messages like "improved agent experience" be very specific about what user facing changes were made -if there are changes do a git pull --rebase if there are conflicts DO NOT FIX THEM. notify me and I will fix them ## GIT DIFF diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index e2350c907b52..3497847a6765 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -1,8 +1,5 @@ { "$schema": "https://opencode.ai/config.json", - // "enterprise": { - // "url": "https://enterprise.dev.opencode.ai", - // }, "provider": { "opencode": { "options": {}, diff --git a/.opencode/skill/bun-file-io/SKILL.md b/.opencode/skill/bun-file-io/SKILL.md index ea39507d2690..f78de330943e 100644 --- a/.opencode/skill/bun-file-io/SKILL.md +++ b/.opencode/skill/bun-file-io/SKILL.md @@ -32,6 +32,9 @@ description: Use this when you are working on file operations like reading, writ - Decode tool stderr with `Bun.readableStreamToText`. - For large writes, use `Bun.write(Bun.file(path), text)`. +NOTE: Bun.file(...).exists() will return `false` if the value is a directory. +Use Filesystem.exists(...) instead if path can be file or directory + ## Quick checklist - Use Bun APIs first. diff --git a/.opencode/tool/github-triage.ts b/.opencode/tool/github-triage.ts index 1e216f1c8daa..3a70c4e002bc 100644 --- a/.opencode/tool/github-triage.ts +++ b/.opencode/tool/github-triage.ts @@ -1,8 +1,22 @@ /// -// import { Octokit } from "@octokit/rest" import { tool } from "@opencode-ai/plugin" import DESCRIPTION from "./github-triage.txt" +const TEAM = { + desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"], + zen: ["fwang", "MrMushrooooom"], + tui: ["thdxr", "kommander", "rekram1-node"], + core: ["thdxr", "rekram1-node", "jlongster"], + docs: ["R44VC0RP"], + windows: ["Hona"], +} as const + +const ASSIGNEES = [...new Set(Object.values(TEAM).flat())] + +function pick(items: readonly T[]) { + return items[Math.floor(Math.random() * items.length)]! +} + function getIssueNumber(): number { const issue = parseInt(process.env.ISSUE_NUMBER ?? "", 10) if (!issue) throw new Error("ISSUE_NUMBER env var not set") @@ -29,60 +43,79 @@ export default tool({ description: DESCRIPTION, args: { assignee: tool.schema - .enum(["thdxr", "adamdotdevin", "rekram1-node", "fwang", "jayair", "kommander"]) + .enum(ASSIGNEES as [string, ...string[]]) .describe("The username of the assignee") .default("rekram1-node"), labels: tool.schema - .array(tool.schema.enum(["nix", "opentui", "perf", "desktop", "zen", "docs", "windows"])) + .array(tool.schema.enum(["nix", "opentui", "perf", "web", "desktop", "zen", "docs", "windows", "core"])) .describe("The labels(s) to add to the issue") .default([]), }, async execute(args) { const issue = getIssueNumber() - // const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }) const owner = "anomalyco" const repo = "opencode" const results: string[] = [] + let labels = [...new Set(args.labels.map((x) => (x === "desktop" ? "web" : x)))] + const web = labels.includes("web") + const text = `${process.env.ISSUE_TITLE ?? ""}\n${process.env.ISSUE_BODY ?? ""}`.toLowerCase() + const zen = /\bzen\b/.test(text) || text.includes("opencode black") + const nix = /\bnix(os)?\b/.test(text) + + if (labels.includes("nix") && !nix) { + labels = labels.filter((x) => x !== "nix") + results.push("Dropped label: nix (issue does not mention nix)") + } + + const assignee = nix + ? "rekram1-node" + : web + ? pick(TEAM.desktop) + : args.assignee === "jlongster" + ? "thdxr" + : args.assignee + + if (args.assignee === "jlongster" && assignee === "thdxr") { + results.push("Remapped assignee: jlongster -> thdxr (jlongster not assignable yet)") + } + + if (labels.includes("zen") && !zen) { + throw new Error("Only add the zen label when issue title/body contains 'zen'") + } + + if (web && !nix && !(TEAM.desktop as readonly string[]).includes(assignee)) { + throw new Error("Web issues must be assigned to adamdotdevin, iamdavidhill, Brendonovich, or nexxeln") + } + + if ((TEAM.zen as readonly string[]).includes(assignee) && !labels.includes("zen")) { + throw new Error("Only zen issues should be assigned to fwang or MrMushrooooom") + } - if (args.assignee === "adamdotdevin" && !args.labels.includes("desktop")) { - throw new Error("Only desktop issues should be assigned to adamdotdevin") + if (assignee === "Hona" && !labels.includes("windows")) { + throw new Error("Only windows issues should be assigned to Hona") } - if (args.assignee === "fwang" && !args.labels.includes("zen")) { - throw new Error("Only zen issues should be assigned to fwang") + if (assignee === "R44VC0RP" && !labels.includes("docs")) { + throw new Error("Only docs issues should be assigned to R44VC0RP") } - if (args.assignee === "kommander" && !args.labels.includes("opentui")) { + if (assignee === "kommander" && !labels.includes("opentui")) { throw new Error("Only opentui issues should be assigned to kommander") } - // await octokit.rest.issues.addAssignees({ - // owner, - // repo, - // issue_number: issue, - // assignees: [args.assignee], - // }) await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/assignees`, { method: "POST", - body: JSON.stringify({ assignees: [args.assignee] }), + body: JSON.stringify({ assignees: [assignee] }), }) - results.push(`Assigned @${args.assignee} to issue #${issue}`) - - const labels: string[] = args.labels.map((label) => (label === "desktop" ? "web" : label)) + results.push(`Assigned @${assignee} to issue #${issue}`) if (labels.length > 0) { - // await octokit.rest.issues.addLabels({ - // owner, - // repo, - // issue_number: issue, - // labels, - // }) await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/labels`, { method: "POST", body: JSON.stringify({ labels }), }) - results.push(`Added labels: ${args.labels.join(", ")}`) + results.push(`Added labels: ${labels.join(", ")}`) } return results.join("\n") diff --git a/.opencode/tool/github-triage.txt b/.opencode/tool/github-triage.txt index 4c46a72c1621..4369ed23512f 100644 --- a/.opencode/tool/github-triage.txt +++ b/.opencode/tool/github-triage.txt @@ -1,88 +1,6 @@ -Use this tool to assign and/or label a Github issue. +Use this tool to assign and/or label a GitHub issue. -You can assign the following users: -- thdxr -- adamdotdevin -- fwang -- jayair -- kommander -- rekram1-node +Choose labels and assignee using the current triage policy and ownership rules. +Pick the most fitting labels for the issue and assign one owner. - -You can use the following labels: -- nix -- opentui -- perf -- web -- zen -- docs - -Always try to assign an issue, if in doubt, assign rekram1-node to it. - -## Breakdown of responsibilities: - -### thdxr - -Dax is responsible for managing core parts of the application, for large feature requests, api changes, or things that require significant changes to the codebase assign him. - -This relates to OpenCode server primarily but has overlap with just about anything - -### adamdotdevin - -Adam is responsible for managing the Desktop/Web app. If there is an issue relating to the desktop app or `opencode web` command. Assign him. - - -### fwang - -Frank is responsible for managing Zen, if you see complaints about OpenCode Zen, maybe it's the dashboard, the model quality, billing issues, etc. Assign him to the issue. - -### jayair - -Jay is responsible for documentation. If there is an issue relating to documentation assign him. - -### kommander - -Sebastian is responsible for managing an OpenTUI (a library for building terminal user interfaces). OpenCode's TUI is built with OpenTUI. If there are issues about: -- random characters on screen -- keybinds not working on different terminals -- general terminal stuff -Then assign the issue to Him. - -### rekram1-node - -ALL BUGS SHOULD BE assigned to rekram1-node unless they have the `opentui` label. - -Assign Aiden to an issue as a catch all, if you can't assign anyone else. Most of the time this will be bugs/polish things. -If no one else makes sense to assign, assign rekram1-node to it. - -Always assign to aiden if the issue mentions "acp", "zed", or model performance issues - -## Breakdown of Labels: - -### nix - -Any issue that mentions nix, or nixos should have a nix label - -### opentui - -Anything relating to the TUI itself should have an opentui label - -### perf - -Anything related to slow performance, high ram, high cpu usage, or any other performance related issue should have a perf label - -### desktop - -Anything related to `opencode web` command or the desktop app should have a desktop label. Never add this label for anything terminal/tui related - -### zen - -Anything related to OpenCode Zen, billing, or model quality from Zen should have a zen label - -### docs - -Anything related to the documentation should have a docs label - -### windows - -Use for any issue that involves the windows OS +If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random. diff --git a/.signpath/policies/opencode/test-signing.yml b/.signpath/policies/opencode/test-signing.yml new file mode 100644 index 000000000000..683b27adb754 --- /dev/null +++ b/.signpath/policies/opencode/test-signing.yml @@ -0,0 +1,5 @@ +github-policies: + runners: + allowed_groups: + - "GitHub Actions" + - "blacksmith runners 01kbd5v56sg8tz7rea39b7ygpt" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60b76a95e9f3..4bec009ef467 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -258,3 +258,49 @@ These are not strictly enforced, they are just general guidelines: ## Feature Requests For net-new functionality, start with a design conversation. Open an issue describing the problem, your proposed approach (optional), and why it belongs in OpenCode. The core team will help decide whether it should move forward; please wait for that approval instead of opening a feature PR directly. + +## Trust & Vouch System + +This project uses [vouch](https://github.com/mitchellh/vouch) to manage contributor trust. The vouch list is maintained in [`.github/VOUCHED.td`](.github/VOUCHED.td). + +### How it works + +- **Vouched users** are explicitly trusted contributors. +- **Denounced users** are explicitly blocked. Issues and pull requests from denounced users are automatically closed. If you have been denounced, you can request to be unvouched by reaching out to a maintainer on [Discord](https://opencode.ai/discord) +- **Everyone else** can participate normally — you don't need to be vouched to open issues or PRs. + +### For maintainers + +Collaborators with write access can manage the vouch list by commenting on any issue: + +- `vouch` — vouch for the issue author +- `vouch @username` — vouch for a specific user +- `denounce` — denounce the issue author +- `denounce @username` — denounce a specific user +- `denounce @username ` — denounce with a reason +- `unvouch` / `unvouch @username` — remove someone from the list + +Changes are committed automatically to `.github/VOUCHED.td`. + +### Denouncement policy + +Denouncement is reserved for users who repeatedly submit low-quality AI-generated contributions, spam, or otherwise act in bad faith. It is not used for disagreements or honest mistakes. + +## Issue Requirements + +All issues **must** use one of our issue templates: + +- **Bug report** — for reporting bugs (requires a description) +- **Feature request** — for suggesting enhancements (requires verification checkbox and description) +- **Question** — for asking questions (requires the question) + +Blank issues are not allowed. When a new issue is opened, an automated check verifies that it follows a template and meets our contributing guidelines. If an issue doesn't meet the requirements, you'll receive a comment explaining what needs to be fixed and have **2 hours** to edit the issue. After that, it will be automatically closed. + +Issues may be flagged for: + +- Not using a template +- Required fields left empty or filled with placeholder text +- AI-generated walls of text +- Missing meaningful content + +If you believe your issue was incorrectly flagged, let a maintainer know. diff --git a/README.ar.md b/README.ar.md index 4c8ac5fcc3bd..f24e598d5eb9 100644 --- a/README.ar.md +++ b/README.ar.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) @@ -50,7 +51,8 @@ scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS و Linux (موصى به، دائما محدث) brew install opencode # macOS و Linux (صيغة brew الرسمية، تحديث اقل) -paru -S opencode-bin # Arch Linux +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) mise use -g opencode # اي نظام nix run nixpkgs#opencode # او github:anomalyco/opencode لاحدث فرع dev ``` diff --git a/README.br.md b/README.br.md index ee5e85fd4463..4802c4996f63 100644 --- a/README.br.md +++ b/README.br.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) @@ -50,7 +51,8 @@ scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS e Linux (recomendado, sempre atualizado) brew install opencode # macOS e Linux (fórmula oficial do brew, atualiza menos) -paru -S opencode-bin # Arch Linux +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) mise use -g opencode # qualquer sistema nix run nixpkgs#opencode # ou github:anomalyco/opencode para a branch dev mais recente ``` diff --git a/README.bs.md b/README.bs.md index 56a1e72fb6af..9ad6852018c0 100644 --- a/README.bs.md +++ b/README.bs.md @@ -32,7 +32,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) @@ -51,7 +52,8 @@ scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS i Linux (preporučeno, uvijek ažurno) brew install opencode # macOS i Linux (zvanična brew formula, rjeđe se ažurira) -paru -S opencode-bin # Arch Linux +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) mise use -g opencode # Bilo koji OS nix run nixpkgs#opencode # ili github:anomalyco/opencode za najnoviji dev branch ``` diff --git a/README.da.md b/README.da.md index 79928fd94469..4b1302dbc3c2 100644 --- a/README.da.md +++ b/README.da.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) @@ -50,7 +51,8 @@ scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS og Linux (anbefalet, altid up to date) brew install opencode # macOS og Linux (officiel brew formula, opdateres sjældnere) -paru -S opencode-bin # Arch Linux +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) mise use -g opencode # alle OS nix run nixpkgs#opencode # eller github:anomalyco/opencode for nyeste dev-branch ``` diff --git a/README.de.md b/README.de.md index ccb3ad07dca5..16116dc72f23 100644 --- a/README.de.md +++ b/README.de.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) @@ -50,7 +51,8 @@ scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS und Linux (empfohlen, immer aktuell) brew install opencode # macOS und Linux (offizielle Brew-Formula, seltener aktualisiert) -paru -S opencode-bin # Arch Linux +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) mise use -g opencode # jedes Betriebssystem nix run nixpkgs#opencode # oder github:anomalyco/opencode für den neuesten dev-Branch ``` diff --git a/README.es.md b/README.es.md index e5a7d8e8dd93..5c18ff4aca7c 100644 --- a/README.es.md +++ b/README.es.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) @@ -50,7 +51,8 @@ scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS y Linux (recomendado, siempre al día) brew install opencode # macOS y Linux (fórmula oficial de brew, se actualiza menos) -paru -S opencode-bin # Arch Linux +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) mise use -g opencode # cualquier sistema nix run nixpkgs#opencode # o github:anomalyco/opencode para la rama dev más reciente ``` diff --git a/README.fr.md b/README.fr.md index 54360099035d..0382164bedc5 100644 --- a/README.fr.md +++ b/README.fr.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) @@ -50,7 +51,8 @@ scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS et Linux (recommandé, toujours à jour) brew install opencode # macOS et Linux (formule officielle brew, mise à jour moins fréquente) -paru -S opencode-bin # Arch Linux +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) mise use -g opencode # n'importe quel OS nix run nixpkgs#opencode # ou github:anomalyco/opencode pour la branche dev la plus récente ``` diff --git a/README.it.md b/README.it.md index cbc8a5f6d24a..c966ccec4916 100644 --- a/README.it.md +++ b/README.it.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) @@ -50,7 +51,8 @@ scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS e Linux (consigliato, sempre aggiornato) brew install opencode # macOS e Linux (formula brew ufficiale, aggiornata meno spesso) -paru -S opencode-bin # Arch Linux +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) mise use -g opencode # Qualsiasi OS nix run nixpkgs#opencode # oppure github:anomalyco/opencode per l’ultima branch di sviluppo ``` diff --git a/README.ja.md b/README.ja.md index 8827efae88ef..11109e7eb408 100644 --- a/README.ja.md +++ b/README.ja.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) @@ -50,7 +51,8 @@ scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS と Linux(推奨。常に最新) brew install opencode # macOS と Linux(公式 brew formula。更新頻度は低め) -paru -S opencode-bin # Arch Linux +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) mise use -g opencode # どのOSでも nix run nixpkgs#opencode # または github:anomalyco/opencode で最新 dev ブランチ ``` diff --git a/README.ko.md b/README.ko.md index 806dc642c14e..23fea76b1ebd 100644 --- a/README.ko.md +++ b/README.ko.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) @@ -50,7 +51,8 @@ scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS 및 Linux (권장, 항상 최신) brew install opencode # macOS 및 Linux (공식 brew formula, 업데이트 빈도 낮음) -paru -S opencode-bin # Arch Linux +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) mise use -g opencode # 어떤 OS든 nix run nixpkgs#opencode # 또는 github:anomalyco/opencode 로 최신 dev 브랜치 ``` diff --git a/README.md b/README.md index 2cd1e2aa0199..99b4b2c50ff9 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) @@ -51,7 +52,8 @@ scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS and Linux (recommended, always up to date) brew install opencode # macOS and Linux (official brew formula, updated less) -paru -S opencode-bin # Arch Linux +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) mise use -g opencode # Any OS nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch ``` diff --git a/README.no.md b/README.no.md index 90b631fef2a8..9b9e90dc3850 100644 --- a/README.no.md +++ b/README.no.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) @@ -50,7 +51,8 @@ scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS og Linux (anbefalt, alltid oppdatert) brew install opencode # macOS og Linux (offisiell brew-formel, oppdateres sjeldnere) -paru -S opencode-bin # Arch Linux +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) mise use -g opencode # alle OS nix run nixpkgs#opencode # eller github:anomalyco/opencode for nyeste dev-branch ``` diff --git a/README.pl.md b/README.pl.md index ae653a7fa0a9..fced98dfc3a1 100644 --- a/README.pl.md +++ b/README.pl.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) @@ -50,7 +51,8 @@ scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS i Linux (polecane, zawsze aktualne) brew install opencode # macOS i Linux (oficjalna formuła brew, rzadziej aktualizowana) -paru -S opencode-bin # Arch Linux +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) mise use -g opencode # dowolny system nix run nixpkgs#opencode # lub github:anomalyco/opencode dla najnowszej gałęzi dev ``` diff --git a/README.ru.md b/README.ru.md index cf15c6ebcea0..a7c590c16b7c 100644 --- a/README.ru.md +++ b/README.ru.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) @@ -50,7 +51,8 @@ scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS и Linux (рекомендуем, всегда актуально) brew install opencode # macOS и Linux (официальная формула brew, обновляется реже) -paru -S opencode-bin # Arch Linux +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) mise use -g opencode # любая ОС nix run nixpkgs#opencode # или github:anomalyco/opencode для самой свежей ветки dev ``` diff --git a/README.th.md b/README.th.md index 4077abc011b7..0999167f239c 100644 --- a/README.th.md +++ b/README.th.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) @@ -50,7 +51,8 @@ scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS และ Linux (แนะนำ อัปเดตเสมอ) brew install opencode # macOS และ Linux (brew formula อย่างเป็นทางการ อัปเดตน้อยกว่า) -paru -S opencode-bin # Arch Linux +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) mise use -g opencode # ระบบปฏิบัติการใดก็ได้ nix run nixpkgs#opencode # หรือ github:anomalyco/opencode สำหรับสาขาพัฒนาล่าสุด ``` diff --git a/README.tr.md b/README.tr.md index e3055e7a9916..67f84e4ddbce 100644 --- a/README.tr.md +++ b/README.tr.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) @@ -50,7 +51,8 @@ scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS ve Linux (önerilir, her zaman güncel) brew install opencode # macOS ve Linux (resmi brew formülü, daha az güncellenir) -paru -S opencode-bin # Arch Linux +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) mise use -g opencode # Tüm işletim sistemleri nix run nixpkgs#opencode # veya en güncel geliştirme dalı için github:anomalyco/opencode ``` diff --git a/README.uk.md b/README.uk.md new file mode 100644 index 000000000000..77e859a45d73 --- /dev/null +++ b/README.uk.md @@ -0,0 +1,139 @@ +

+ + + + + OpenCode logo + + +

+

AI-агент для програмування з відкритим кодом.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Встановлення + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Менеджери пакетів +npm i -g opencode-ai@latest # або bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS і Linux (рекомендовано, завжди актуально) +brew install opencode # macOS і Linux (офіційна формула Homebrew, оновлюється рідше) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # Будь-яка ОС +nix run nixpkgs#opencode # або github:anomalyco/opencode для найновішої dev-гілки +``` + +> [!TIP] +> Перед встановленням видаліть версії старші за 0.1.x. + +### Десктопний застосунок (BETA) + +OpenCode також доступний як десктопний застосунок. Завантажуйте напряму зі [сторінки релізів](https://github.com/anomalyco/opencode/releases) або [opencode.ai/download](https://opencode.ai/download). + +| Платформа | Завантаження | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` або AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Каталог встановлення + +Скрипт встановлення дотримується такого порядку пріоритету для шляху встановлення: + +1. `$OPENCODE_INSTALL_DIR` - Користувацький каталог встановлення +2. `$XDG_BIN_DIR` - Шлях, сумісний зі специфікацією XDG Base Directory +3. `$HOME/bin` - Стандартний каталог користувацьких бінарників (якщо існує або його можна створити) +4. `$HOME/.opencode/bin` - Резервний варіант за замовчуванням + +```bash +# Приклади +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Агенти + +OpenCode містить два вбудовані агенти, між якими можна перемикатися клавішею `Tab`. + +- **build** - Агент за замовчуванням із повним доступом для завдань розробки +- **plan** - Агент лише для читання для аналізу та дослідження коду + - За замовчуванням забороняє редагування файлів + - Запитує дозвіл перед запуском bash-команд + - Ідеально підходить для дослідження незнайомих кодових баз або планування змін + +Також доступний допоміжний агент **general** для складного пошуку та багатокрокових завдань. +Він використовується всередині системи й може бути викликаний у повідомленнях через `@general`. + +Дізнайтеся більше про [agents](https://opencode.ai/docs/agents). + +### Документація + +Щоб дізнатися більше про налаштування OpenCode, [**перейдіть до нашої документації**](https://opencode.ai/docs). + +### Внесок + +Якщо ви хочете зробити внесок в OpenCode, будь ласка, прочитайте нашу [документацію для контриб'юторів](./CONTRIBUTING.md) перед надсиланням pull request. + +### Проєкти на базі OpenCode + +Якщо ви працюєте над проєктом, пов'язаним з OpenCode, і використовуєте "opencode" у назві, наприклад "opencode-dashboard" або "opencode-mobile", додайте примітку до свого README. +Уточніть, що цей проєкт не створений командою OpenCode і жодним чином не афілійований із нами. + +### FAQ + +#### Чим це відрізняється від Claude Code? + +За можливостями це дуже схоже на Claude Code. Ось ключові відмінності: + +- 100% open source +- Немає прив'язки до конкретного провайдера. Ми рекомендуємо моделі, які надаємо через [OpenCode Zen](https://opencode.ai/zen), але OpenCode також працює з Claude, OpenAI, Google і навіть локальними моделями. З розвитком моделей різниця між ними зменшуватиметься, а ціни падатимуть, тому незалежність від провайдера має значення. +- Підтримка LSP з коробки +- Фокус на TUI. OpenCode створено користувачами neovim та авторами [terminal.shop](https://terminal.shop); ми й надалі розширюватимемо межі можливого в терміналі. +- Клієнт-серверна архітектура. Наприклад, це дає змогу запускати OpenCode на вашому комп'ютері й керувати ним віддалено з мобільного застосунку, тобто TUI-фронтенд - лише один із можливих клієнтів. + +--- + +**Приєднуйтеся до нашої спільноти** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.zh.md b/README.zh.md index 6970fe34efd5..113d476b2ed3 100644 --- a/README.zh.md +++ b/README.zh.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) @@ -50,7 +51,8 @@ scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS 和 Linux(推荐,始终保持最新) brew install opencode # macOS 和 Linux(官方 brew formula,更新频率较低) -paru -S opencode-bin # Arch Linux +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) mise use -g opencode # 任意系统 nix run nixpkgs#opencode # 或用 github:anomalyco/opencode 获取最新 dev 分支 ``` diff --git a/README.zht.md b/README.zht.md index a045f454901f..b5181044438d 100644 --- a/README.zht.md +++ b/README.zht.md @@ -31,7 +31,8 @@ Norsk | Português (Brasil) | ไทย | - Türkçe + Türkçe | + Українська

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) @@ -50,7 +51,8 @@ scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS 與 Linux(推薦,始終保持最新) brew install opencode # macOS 與 Linux(官方 brew formula,更新頻率較低) -paru -S opencode-bin # Arch Linux +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) mise use -g opencode # 任何作業系統 nix run nixpkgs#opencode # 或使用 github:anomalyco/opencode 以取得最新開發分支 ``` diff --git a/bun.lock b/bun.lock index f82d5e9fd39d..ff0086d0c7e3 100644 --- a/bun.lock +++ b/bun.lock @@ -24,7 +24,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.53", + "version": "1.2.6", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -74,7 +74,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.53", + "version": "1.2.6", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -108,7 +108,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.1.53", + "version": "1.2.6", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -116,7 +116,7 @@ "@opencode-ai/console-resource": "workspace:*", "@planetscale/database": "1.19.0", "aws4fetch": "1.0.20", - "drizzle-orm": "0.41.0", + "drizzle-orm": "catalog:", "postgres": "3.4.7", "stripe": "18.0.0", "ulid": "catalog:", @@ -128,14 +128,14 @@ "@types/bun": "1.3.0", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", - "drizzle-kit": "0.30.5", + "drizzle-kit": "catalog:", "mysql2": "3.14.4", "typescript": "catalog:", }, }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.1.53", + "version": "1.2.6", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -159,7 +159,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.1.53", + "version": "1.2.6", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -181,9 +181,42 @@ "cloudflare": "5.2.0", }, }, + "packages/desktop": { + "name": "@opencode-ai/desktop", + "version": "1.2.6", + "dependencies": { + "@opencode-ai/app": "workspace:*", + "@opencode-ai/ui": "workspace:*", + "@solid-primitives/i18n": "2.2.1", + "@solid-primitives/storage": "catalog:", + "@solidjs/meta": "catalog:", + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-clipboard-manager": "~2", + "@tauri-apps/plugin-deep-link": "~2", + "@tauri-apps/plugin-dialog": "~2", + "@tauri-apps/plugin-http": "~2", + "@tauri-apps/plugin-notification": "~2", + "@tauri-apps/plugin-opener": "^2", + "@tauri-apps/plugin-os": "~2", + "@tauri-apps/plugin-process": "~2", + "@tauri-apps/plugin-shell": "~2", + "@tauri-apps/plugin-store": "~2", + "@tauri-apps/plugin-updater": "~2", + "@tauri-apps/plugin-window-state": "~2", + "solid-js": "catalog:", + }, + "devDependencies": { + "@actions/artifact": "4.0.0", + "@tauri-apps/cli": "^2", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "~5.6.2", + "vite": "catalog:", + }, + }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.53", + "version": "1.2.6", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -212,7 +245,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.53", + "version": "1.2.6", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -228,7 +261,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.53", + "version": "1.2.6", "bin": { "opencode": "./bin/opencode", }, @@ -236,15 +269,15 @@ "@actions/core": "1.11.1", "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.14.1", - "@ai-sdk/amazon-bedrock": "3.0.74", - "@ai-sdk/anthropic": "2.0.58", + "@ai-sdk/amazon-bedrock": "3.0.79", + "@ai-sdk/anthropic": "2.0.62", "@ai-sdk/azure": "2.0.91", "@ai-sdk/cerebras": "1.0.36", "@ai-sdk/cohere": "2.0.22", - "@ai-sdk/deepinfra": "1.0.33", + "@ai-sdk/deepinfra": "1.0.36", "@ai-sdk/gateway": "2.0.30", "@ai-sdk/google": "2.0.52", - "@ai-sdk/google-vertex": "3.0.98", + "@ai-sdk/google-vertex": "3.0.103", "@ai-sdk/groq": "2.0.34", "@ai-sdk/mistral": "2.0.27", "@ai-sdk/openai": "2.0.89", @@ -256,8 +289,8 @@ "@ai-sdk/vercel": "1.0.33", "@ai-sdk/xai": "2.0.51", "@clack/prompts": "1.0.0-alpha.1", - "@gitlab/gitlab-ai-provider": "3.5.0", - "@gitlab/opencode-gitlab-auth": "1.3.2", + "@gitlab/gitlab-ai-provider": "3.5.1", + "@gitlab/opencode-gitlab-auth": "1.3.3", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", "@modelcontextprotocol/sdk": "1.25.2", @@ -269,8 +302,8 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.4", - "@opentui/core": "0.1.77", - "@opentui/solid": "0.1.77", + "@opentui/core": "0.1.79", + "@opentui/solid": "0.1.79", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -285,6 +318,7 @@ "clipboardy": "4.0.0", "decimal.js": "10.5.0", "diff": "catalog:", + "drizzle-orm": "1.0.0-beta.12-a5629fb", "fuzzysort": "3.1.0", "gray-matter": "4.0.3", "hono": "catalog:", @@ -328,6 +362,8 @@ "@types/yargs": "17.0.33", "@typescript/native-preview": "catalog:", "babel-preset-solid": "1.9.10", + "drizzle-kit": "1.0.0-beta.12-a5629fb", + "drizzle-orm": "1.0.0-beta.12-a5629fb", "typescript": "catalog:", "vscode-languageserver-types": "3.17.5", "why-is-node-running": "3.2.2", @@ -336,7 +372,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.53", + "version": "1.2.6", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -356,7 +392,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.53", + "version": "1.2.6", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -367,7 +403,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.53", + "version": "1.2.6", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -380,7 +416,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.53", + "version": "1.2.6", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -422,7 +458,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.53", + "version": "1.2.6", "dependencies": { "zod": "catalog:", }, @@ -433,14 +469,14 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.53", + "version": "1.2.6", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", "@astrojs/solid-js": "5.1.0", "@astrojs/starlight": "0.34.3", "@fontsource/ibm-plex-mono": "5.2.5", - "@shikijs/transformers": "3.4.2", + "@shikijs/transformers": "3.20.0", "@types/luxon": "catalog:", "ai": "catalog:", "astro": "5.7.13", @@ -455,8 +491,10 @@ "shiki": "catalog:", "solid-js": "catalog:", "toolbeam-docs-theme": "0.4.8", + "vscode-languageserver-types": "3.17.5", }, "devDependencies": { + "@astrojs/check": "0.9.6", "@types/node": "catalog:", "opencode": "workspace:*", "typescript": "catalog:", @@ -468,6 +506,10 @@ "web-tree-sitter", "tree-sitter-bash", ], + "patchedDependencies": { + "@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch", + "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", + }, "overrides": { "@types/bun": "catalog:", "@types/node": "catalog:", @@ -478,7 +520,7 @@ "@kobalte/core": "0.13.11", "@octokit/rest": "22.0.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@pierre/diffs": "1.0.2", + "@pierre/diffs": "1.1.0-beta.13", "@playwright/test": "1.51.0", "@solid-primitives/storage": "4.3.3", "@solidjs/meta": "0.29.4", @@ -487,7 +529,7 @@ "@tailwindcss/vite": "4.1.11", "@tsconfig/bun": "1.0.9", "@tsconfig/node22": "22.0.2", - "@types/bun": "1.3.5", + "@types/bun": "1.3.9", "@types/luxon": "3.7.1", "@types/node": "22.13.9", "@types/semver": "7.7.1", @@ -495,6 +537,8 @@ "ai": "5.0.124", "diff": "8.0.2", "dompurify": "3.3.1", + "drizzle-kit": "1.0.0-beta.12-a5629fb", + "drizzle-orm": "1.0.0-beta.12-a5629fb", "fuzzysort": "3.1.0", "hono": "4.10.7", "hono-openapi": "1.1.2", @@ -530,7 +574,7 @@ "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.14.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-b6r3PS3Nly+Wyw9U+0nOr47bV8tfS476EgyEMhoKvJCZLbgqoDFN7DJwkxL88RR0aiOqOYV1ZnESHqb+RmdH8w=="], - "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.74", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.58", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-q83HE3FBb/HPIvjXsehrHOgCuGHPorSMFt6BYnzIYZy8gNnSqV1OWX4oXVsCAuYPPMtYW/KMK35hmoIFV8QKoQ=="], + "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.79", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.62", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-GfAQUb1GEmdTjLu5Ud1d5sieNHDpwoQdb4S14KmJlA5RsGREUZ1tfSKngFaiClxFtL0xPSZjePhTMV6Z65A7/g=="], "@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="], @@ -540,21 +584,21 @@ "@ai-sdk/cohere": ["@ai-sdk/cohere@2.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yJ9kP5cEDJwo8qpITq5TQFD8YNfNtW+HbyvWwrKMbFzmiMvIZuk95HIaFXE7PCTuZsqMA05yYu+qX/vQ3rNKjA=="], - "@ai-sdk/deepgram": ["@ai-sdk/deepgram@1.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-lqmINr+1Jy2yGXxnQB6IrC2xMtUY5uK96pyKfqTj1kLlXGatKnJfXF7WTkOGgQrFqIYqpjDz+sPVR3n0KUEUtA=="], + "@ai-sdk/deepgram": ["@ai-sdk/deepgram@1.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yQ5izccuO7+WDtitbJsqH7qX7BqVVonUbPZBxQypF3zqBXbCI3/3CH+0XbsWRVRWFN8/rmCAbgHg8DXjaqVQsw=="], - "@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hn2y8Q+2iZgGNVJyzPsH8EECECryFMVmxBJrBvBWoi8xcJPRyt0fZP5dOSLyGg3q0oxmPS9M0Eq0NNlKot/bYQ=="], + "@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@1.0.36", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.33", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LndvRktEgY2IFu4peDJMEXcjhHEEFtM0upLx/J64kCpFHCifalXpK4PPSX3PVndnn0bJzvamO5+fc0z2ooqBZw=="], - "@ai-sdk/deepseek": ["@ai-sdk/deepseek@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NiKjvqXI/96e/7SjZGgQH141PBqggsF7fNbjGTv4RgVWayMXp9mj0Ou2NjAUGwwxJwj/qseY0gXiDCYaHWFBkw=="], + "@ai-sdk/deepseek": ["@ai-sdk/deepseek@1.0.34", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-H+5UGOGGZB5tYpX+3fcWxoPPDzRTEH1w6z+yD7053PmKZfHcxSJWv9HwLEyEkAv3ef1E7MIyG5EB+HmkclQ+KQ=="], - "@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@1.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4d5EKu0OW7Gf5WFpGo4ixn0iWEwA+GpteqUjEznWGmi7qdLE5zdkbRik5B1HrDDiw5P90yO51xBex/Fp50JcVA=="], + "@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@1.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-B9uz4+KEB5RkphL9d9XL03iA24g3f0VAeklNlq7StY7L8Mo2sBx3Bg8Udzv7G3xJmT41GuzR5pR0FkKUTju0Rg=="], - "@ai-sdk/fireworks": ["@ai-sdk/fireworks@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-WWOz5Kj+5fVe94h7WeReqjUOVtAquDE2kM575FUc8CsVxH2tRfA5cLa8nu3bknSezsKt3i67YM6mvCRxiXCkWA=="], + "@ai-sdk/fireworks": ["@ai-sdk/fireworks@1.0.34", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.33", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-JvRdp8bMokbmp/mFz0qHPAhvAZT+vR+c9o4lTkENkDcbRBcNYUN05sSWCuwiVDdz9T+8GW7goAec6fXJBzjIFw=="], "@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5Nrkj8B4MzkkOfjjA+Cs5pamkbkK4lI11bx80QV7TFcen/hWA8wEC+UVzwuM5H2zpekoNMjvl6GonHnR62XIZw=="], "@ai-sdk/google": ["@ai-sdk/google@2.0.52", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2XUnGi3f7TV4ujoAhA+Fg3idUoG/+Y2xjCRg70a1/m0DH1KSQqYaCboJ1C19y6ZHGdf5KNT20eJdswP6TvrY2g=="], - "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.98", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.58", "@ai-sdk/google": "2.0.52", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uuv0RHkdJ5vTzeH1+iuBlv7GAjRcOPd2jiqtGLz6IKOUDH+PRQoE3ExrvOysVnKuhhTBMqvawkktDhMDQE6sVQ=="], + "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.103", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.63", "@ai-sdk/google": "2.0.53", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MPZRSVOJFxYGHE4s6XjSWaiUPru7u2i/LUUA1Ih2nzNYZaei8c46Z56imOCD/KQjQX3afRA2iZh6P5McsmwhqA=="], "@ai-sdk/groq": ["@ai-sdk/groq@2.0.34", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wfCYkVgmVjxNA32T57KbLabVnv9aFUflJ4urJ7eWgTwbnmGQHElCTu+rJ3ydxkXSqxOkXPwMOttDm7XNrvPjmg=="], @@ -584,12 +628,16 @@ "@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="], + "@astrojs/check": ["@astrojs/check@0.9.6", "", { "dependencies": { "@astrojs/language-server": "^2.16.1", "chokidar": "^4.0.1", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "bin/astro-check.js" } }, "sha512-jlaEu5SxvSgmfGIFfNgcn5/f+29H61NJzEMfAZ82Xopr4XBchXB1GVlcJsE+elUlsYSbXlptZLX+JMG3b/wZEA=="], + "@astrojs/cloudflare": ["@astrojs/cloudflare@12.6.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.1", "@astrojs/underscore-redirects": "1.0.0", "@cloudflare/workers-types": "^4.20250507.0", "tinyglobby": "^0.2.13", "vite": "^6.3.5", "wrangler": "^4.14.1" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-xhJptF5tU2k5eo70nIMyL1Udma0CqmUEnGSlGyFflLqSY82CRQI6nWZ/xZt0ZvmXuErUjIx0YYQNfZsz5CNjLQ=="], - "@astrojs/compiler": ["@astrojs/compiler@2.13.0", "", {}, "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw=="], + "@astrojs/compiler": ["@astrojs/compiler@2.13.1", "", {}, "sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg=="], "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.1", "", {}, "sha512-7dwEVigz9vUWDw3nRwLQ/yH/xYovlUA0ZD86xoeKEBmkz9O6iELG1yri67PgAPW6VLL/xInA4t7H0CK6VmtkKQ=="], + "@astrojs/language-server": ["@astrojs/language-server@2.16.3", "", { "dependencies": { "@astrojs/compiler": "^2.13.0", "@astrojs/yaml2ts": "^0.2.2", "@jridgewell/sourcemap-codec": "^1.5.5", "@volar/kit": "~2.4.27", "@volar/language-core": "~2.4.27", "@volar/language-server": "~2.4.27", "@volar/language-service": "~2.4.27", "muggle-string": "^0.4.1", "tinyglobby": "^0.2.15", "volar-service-css": "0.0.68", "volar-service-emmet": "0.0.68", "volar-service-html": "0.0.68", "volar-service-prettier": "0.0.68", "volar-service-typescript": "0.0.68", "volar-service-typescript-twoslash-queries": "0.0.68", "volar-service-yaml": "0.0.68", "vscode-html-languageservice": "^5.6.1", "vscode-uri": "^3.1.0" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "bin/nodeServer.js" } }, "sha512-yO5K7RYCMXUfeDlnU6UnmtnoXzpuQc0yhlaCNZ67k1C/MiwwwvMZz+LGa+H35c49w5QBfvtr4w4Zcf5PcH8uYA=="], + "@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.1", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.2.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "remark-smartypants": "^3.0.2", "shiki": "^3.0.0", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-c5F5gGrkczUaTVgmMW9g1YMJGzOtRvjjhw6IfGuxarM6ct09MpwysP10US729dy07gg8y+ofVifezvP3BNsWZg=="], "@astrojs/mdx": ["@astrojs/mdx@4.3.13", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.10", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.15.0", "es-module-lexer": "^1.7.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-IHDHVKz0JfKBy3//52JSiyWv089b7GVSChIXLrlUOoTLWowG3wr2/8hkaEgEyd/vysvNQvGk+QhysXpJW5ve6Q=="], @@ -606,6 +654,8 @@ "@astrojs/underscore-redirects": ["@astrojs/underscore-redirects@1.0.0", "", {}, "sha512-qZxHwVnmb5FXuvRsaIGaqWgnftjCuMY+GSbaVZdBmE4j8AfgPqKPxYp8SUERyJcjpKCEmO4wD6ybuGH8A2kVRQ=="], + "@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.2", "", { "dependencies": { "yaml": "^2.5.0" } }, "sha512-GOfvSr5Nqy2z5XiwqTouBBpy5FyI6DEe+/g/Mk5am9SjILN1S5fOEvYK0GuWHg98yS/dobP4m8qyqw/URW35fQ=="], + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], "@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="], @@ -686,13 +736,17 @@ "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="], + "@azure-rest/core-client": ["@azure-rest/core-client@2.5.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A=="], + "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], "@azure/core-auth": ["@azure/core-auth@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg=="], "@azure/core-client": ["@azure/core-client@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w=="], - "@azure/core-http-compat": ["@azure/core-http-compat@2.3.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g=="], + "@azure/core-http": ["@azure/core-http@3.0.5", "", { "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-tracing": "1.0.0-preview.13", "@azure/core-util": "^1.1.1", "@azure/logger": "^1.0.0", "@types/node-fetch": "^2.5.0", "@types/tunnel": "^0.0.3", "form-data": "^4.0.0", "node-fetch": "^2.6.7", "process": "^0.11.10", "tslib": "^2.2.0", "tunnel": "^0.0.6", "uuid": "^8.3.0", "xml2js": "^0.5.0" } }, "sha512-T8r2q/c3DxNu6mEJfPuJtptUVqwchxzjj32gKcnMi06rdiVONS9rar7kT9T2Am+XvER7uOzpsP79WsqNbdgdWg=="], + + "@azure/core-http-compat": ["@azure/core-http-compat@2.3.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2" }, "peerDependencies": { "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw=="], "@azure/core-lro": ["@azure/core-lro@2.7.2", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2" } }, "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw=="], @@ -706,19 +760,31 @@ "@azure/core-xml": ["@azure/core-xml@1.5.0", "", { "dependencies": { "fast-xml-parser": "^5.0.7", "tslib": "^2.8.1" } }, "sha512-D/sdlJBMJfx7gqoj66PKVmhDDaU6TKA49ptcolxdas29X7AfvLTmfAGLjAcIMBK7UZ2o4lygHIqVckOlQU3xWw=="], + "@azure/identity": ["@azure/identity@4.13.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^4.2.0", "@azure/msal-node": "^3.5.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw=="], + + "@azure/keyvault-common": ["@azure/keyvault-common@2.0.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-client": "^1.5.0", "@azure/core-rest-pipeline": "^1.8.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.10.0", "@azure/logger": "^1.1.4", "tslib": "^2.2.0" } }, "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w=="], + + "@azure/keyvault-keys": ["@azure/keyvault-keys@4.10.0", "", { "dependencies": { "@azure-rest/core-client": "^2.3.3", "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.7.2", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.0", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/keyvault-common": "^2.0.0", "@azure/logger": "^1.1.4", "tslib": "^2.8.1" } }, "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag=="], + "@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], - "@azure/storage-blob": ["@azure/storage-blob@12.30.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.3", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.2.0", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/core-xml": "^1.4.5", "@azure/logger": "^1.1.4", "@azure/storage-common": "^12.2.0", "events": "^3.0.0", "tslib": "^2.8.1" } }, "sha512-peDCR8blSqhsAKDbpSP/o55S4sheNwSrblvCaHUZ5xUI73XA7ieUGGwrONgD/Fng0EoDe1VOa3fAQ7+WGB3Ocg=="], + "@azure/msal-browser": ["@azure/msal-browser@4.28.2", "", { "dependencies": { "@azure/msal-common": "15.14.2" } }, "sha512-6vYUMvs6kJxJgxaCmHn/F8VxjLHNh7i9wzfwPGf8kyBJ8Gg2yvBXx175Uev8LdrD1F5C4o7qHa2CC4IrhGE1XQ=="], + + "@azure/msal-common": ["@azure/msal-common@15.14.2", "", {}, "sha512-n8RBJEUmd5QotoqbZfd+eGBkzuFI1KX6jw2b3WcpSyGjwmzoeI/Jb99opIBPHpb8y312NB+B6+FGi2ZVSR8yfA=="], + + "@azure/msal-node": ["@azure/msal-node@3.8.7", "", { "dependencies": { "@azure/msal-common": "15.14.2", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-a+Xnrae+uwLnlw68bplS1X4kuJ9F/7K6afuMFyRkNIskhjgDezl5Fhrx+1pmAlDmC0VaaAxjRQMp1OmcqVwkIg=="], + + "@azure/storage-blob": ["@azure/storage-blob@12.31.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.3", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.2.0", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/core-xml": "^1.4.5", "@azure/logger": "^1.1.4", "@azure/storage-common": "^12.3.0", "events": "^3.0.0", "tslib": "^2.8.1" } }, "sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg=="], - "@azure/storage-common": ["@azure/storage-common@12.2.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.1.4", "events": "^3.3.0", "tslib": "^2.8.1" } }, "sha512-YZLxiJ3vBAAnFbG3TFuAMUlxZRexjQX5JDQxOkFGb6e2TpoxH3xyHI6idsMe/QrWtj41U/KoqBxlayzhS+LlwA=="], + "@azure/storage-common": ["@azure/storage-common@12.3.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.1.4", "events": "^3.3.0", "tslib": "^2.8.1" } }, "sha512-/OFHhy86aG5Pe8dP5tsp+BuJ25JOAl9yaMU3WZbkeoiFMHFtJ7tu5ili7qEdBXNW9G5lDB19trwyI6V49F/8iQ=="], - "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - "@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], "@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], - "@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], @@ -750,7 +816,7 @@ "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], - "@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], @@ -770,9 +836,9 @@ "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - "@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], - "@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], "@bufbuild/protobuf": ["@bufbuild/protobuf@2.11.0", "", {}, "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ=="], @@ -812,7 +878,21 @@ "@dot/log": ["@dot/log@0.1.5", "", { "dependencies": { "chalk": "^4.1.2", "loglevelnext": "^6.0.0", "p-defer": "^3.0.0" } }, "sha512-ECraEVJWv2f2mWK93lYiefUkphStVlKD6yKDzisuoEmxuLKrxO9iGetHK2DoEAkj7sxjE886n0OUVVCUx0YPNg=="], - "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], + + "@emmetio/abbreviation": ["@emmetio/abbreviation@2.3.3", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA=="], + + "@emmetio/css-abbreviation": ["@emmetio/css-abbreviation@2.1.8", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw=="], + + "@emmetio/css-parser": ["@emmetio/css-parser@0.4.1", "", { "dependencies": { "@emmetio/stream-reader": "^2.2.0", "@emmetio/stream-reader-utils": "^0.1.0" } }, "sha512-2bC6m0MV/voF4CTZiAbG5MWKbq5EBmDPKu9Sb7s7nVcEzNQlrZP6mFFFlIaISM8X6514H9shWMme1fCm8cWAfQ=="], + + "@emmetio/html-matcher": ["@emmetio/html-matcher@1.3.0", "", { "dependencies": { "@emmetio/scanner": "^1.0.0" } }, "sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ=="], + + "@emmetio/scanner": ["@emmetio/scanner@1.0.4", "", {}, "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA=="], + + "@emmetio/stream-reader": ["@emmetio/stream-reader@2.2.0", "", {}, "sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw=="], + + "@emmetio/stream-reader-utils": ["@emmetio/stream-reader-utils@0.1.0", "", {}, "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A=="], "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], @@ -824,10 +904,6 @@ "@emotion/memoize": ["@emotion/memoize@0.7.4", "", {}, "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="], - "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], - - "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], @@ -916,9 +992,9 @@ "@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="], - "@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.5.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-OoAwCz4fOci3h/2l+PRHMclclh3IaFq8w1es2wvBJ8ca7vtglKsBYT7dvmYpsXlu7pg9mopbjcexvmVCQEUTAQ=="], + "@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.5.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-I8+EGdUeKmGJSjAdFobHtqpxM9Fm00w0j7NJbtln/D/XQ1SKEGoZIuqJko4v0pV2mkhGUIs7qezljH/2kbXovA=="], - "@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.2", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-pvGrC+aDVLY8bRCC/fZaG/Qihvt2r4by5xbTo5JTSz9O7yIcR6xG2d9Wkuu4bcXFz674z2C+i5bUk+J/RSdBpg=="], + "@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="], "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], @@ -980,15 +1056,15 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], - "@internationalized/date": ["@internationalized/date@3.10.1", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-oJrXtQiAXLvT9clCf1K4kxp3eKsQhIaZqxEyowkBcsvZDdZkbWrVmnGknxs5flTD0VGsxrxKgBCZty1EzoiMzA=="], + "@internationalized/date": ["@internationalized/date@3.11.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q=="], "@internationalized/number": ["@internationalized/number@3.6.5", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g=="], "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], - "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], + "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="], - "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], @@ -1060,6 +1136,10 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@js-joda/core": ["@js-joda/core@5.7.0", "", {}, "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg=="], + + "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], + "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], "@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="], @@ -1196,6 +1276,8 @@ "@opencode-ai/console-resource": ["@opencode-ai/console-resource@workspace:packages/console/resource"], + "@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"], + "@opencode-ai/enterprise": ["@opencode-ai/enterprise@workspace:packages/enterprise"], "@opencode-ai/function": ["@opencode-ai/function@workspace:packages/function"], @@ -1220,21 +1302,21 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentui/core": ["@opentui/core@0.1.77", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.77", "@opentui/core-darwin-x64": "0.1.77", "@opentui/core-linux-arm64": "0.1.77", "@opentui/core-linux-x64": "0.1.77", "@opentui/core-win32-arm64": "0.1.77", "@opentui/core-win32-x64": "0.1.77", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-lE3kabm6jdqK3AuBq+O0zZrXdxt6ulmibTc57sf+AsPny6cmwYHnWI4wD6hcreFiYoQVNVvdiJchVgPtowMlEg=="], + "@opentui/core": ["@opentui/core@0.1.79", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.79", "@opentui/core-darwin-x64": "0.1.79", "@opentui/core-linux-arm64": "0.1.79", "@opentui/core-linux-x64": "0.1.79", "@opentui/core-win32-arm64": "0.1.79", "@opentui/core-win32-x64": "0.1.79", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-job/t09w8A/aHb/WuaVbimu5fIffyN+PCuVO5cYhXEg/NkOkC/WdFi80B8bwncR/DBPyLAh6oJ3EG86grOVo5g=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.77", "", { "os": "darwin", "cpu": "arm64" }, "sha512-SNqmygCMEsPCW7xWjzCZ5caBf36xaprwVdAnFijGDOuIzLA4iaDa6um8cj3TJh7awenN3NTRsuRc7OuH42UH+g=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.79", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kgsGniV+DM5G1P3GideyJhvfnthNKcVCAm2mPTIr9InQ3L0gS/Feh7zgwOS/jxDvdlQbOWGKMk2Z3JApeC1MLw=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.77", "", { "os": "darwin", "cpu": "x64" }, "sha512-/8fsa03swEHTQt/9NrGm98kemlU+VuTURI/OFZiH53vPDRrOYIYoa4Jyga/H7ZMcG+iFhkq97zIe+0Kw95LGmA=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.79", "", { "os": "darwin", "cpu": "x64" }, "sha512-OpyAmFqAAKQ2CeFmf/oLWcNksmP6Ryx/3R5dbKXThOudMCeQvfvInJTRbc2jTn9VFpf+Qj4BgHkJg1h90tf/EA=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.77", "", { "os": "linux", "cpu": "arm64" }, "sha512-QfUXZJPc69OvqoMu+AlLgjqXrwu4IeqcBuUWYMuH8nPTeLsVUc3CBbXdV2lv9UDxWzxzrxdS4ALPaxvmEv9lsQ=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.79", "", { "os": "linux", "cpu": "arm64" }, "sha512-DCa5YaknS4bWhFt8TMEGH+qmTinyzuY8hoZbO4crtWXAxofPP7Pas76Cwxlvis/PyLffA+pPxAl1l5sUZpsvqw=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.77", "", { "os": "linux", "cpu": "x64" }, "sha512-Kmfx0yUKnPj67AoXYIgL7qQo0QVsUG5Iw8aRtv6XFzXqa5SzBPhaKkKZ9yHPjOmTalZquUs+9zcCRNKpYYuL7A=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.79", "", { "os": "linux", "cpu": "x64" }, "sha512-V6xjvFfHh3NGvsuuDae1KHPRZXHMEE8XL0A/GM6v4I4OCC23kDmkK60Vn6OptQwAzwwbz0X0IX+Ut/GQU9qGgA=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.77", "", { "os": "win32", "cpu": "arm64" }, "sha512-HGTscPXc7gdd23Nh1DbzUNjog1I+5IZp95XPtLftGTpjrWs60VcetXcyJqK2rQcXNxewJK5yDyaa5QyMjfEhCQ=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.79", "", { "os": "win32", "cpu": "arm64" }, "sha512-sPRKnVzOdT5szI59tte7pxwwkYA+07EQN+6miFAvkFuiLmRUngONUD8HVjL7nCnxcPFqxaU4Rvl1y40ST86g8g=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.77", "", { "os": "win32", "cpu": "x64" }, "sha512-c7GijsbvVgnlzd2murIbwuwrGbcv76KdUw6WlVv7a0vex50z6xJCpv1keGzpe0QfxrZ/6fFEFX7JnwGLno0wjA=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.79", "", { "os": "win32", "cpu": "x64" }, "sha512-vmQcFTvKf9fqajnDtgU6/uAsiTGwx8//khqHVBmiTEXUsiT792Ki9l8sgNughbuldqG5iZOiF6IaAWU1H67UpA=="], - "@opentui/solid": ["@opentui/solid@0.1.77", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.77", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-JY+hUbXVV+XCk6bC8dvcwawWCEmC3Gid6GDs23AJWBgHZ3TU2kRKrgwTdltm45DOq2cZXrYCt690/yE8bP+Gxg=="], + "@opentui/solid": ["@opentui/solid@0.1.79", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.79", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-c5+0jexKxb8GwRDDkQ/U6isZZqClAzHccXmYiLYmSnqdoQQp2lIGHLartL+K8lfIQrsKClzP2ZHumN6nexRfRg=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], @@ -1348,9 +1430,7 @@ "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="], - "@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="], - - "@pierre/diffs": ["@pierre/diffs@1.0.2", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "3.19.0", "@shikijs/transformers": "3.19.0", "diff": "8.0.2", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "3.19.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-RkFSDD5X/U+8QjyilPViYGJfmJNWXR17zTL8zw48+DcVC1Ujbh6I1edyuRnFfgRzpft05x2DSCkz2cjoIAxPvQ=="], + "@pierre/diffs": ["@pierre/diffs@1.1.0-beta.13", "", { "dependencies": { "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-D35rxDu5V7XHX5aVGU6PF12GhscL+I+9QYgxK/i3h0d2XSirAxDdVNm49aYwlOhgmdvL0NbS1IHxPswVB5yJvw=="], "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], @@ -1442,55 +1522,55 @@ "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.0", "", { "os": "android", "cpu": "arm" }, "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.0", "", { "os": "android", "cpu": "arm64" }, "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.0", "", { "os": "linux", "cpu": "arm" }, "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.0", "", { "os": "linux", "cpu": "x64" }, "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.0", "", { "os": "none", "cpu": "arm64" }, "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.0", "", { "os": "win32", "cpu": "x64" }, "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="], "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], @@ -1520,7 +1600,7 @@ "@slack/socket-mode": ["@slack/socket-mode@1.3.6", "", { "dependencies": { "@slack/logger": "^3.0.0", "@slack/web-api": "^6.12.1", "@types/node": ">=12.0.0", "@types/ws": "^7.4.7", "eventemitter3": "^5", "finity": "^0.5.4", "ws": "^7.5.3" } }, "sha512-G+im7OP7jVqHhiNSdHgv2VVrnN5U7KY845/5EZimZkrD4ZmtV0P3BiWkgeJhPtdLuM7C7i6+M6h6Bh+S4OOalA=="], - "@slack/types": ["@slack/types@2.19.0", "", {}, "sha512-7+QZ38HGcNh/b/7MpvPG6jnw7mliV6UmrquJLqgdxkzJgQEYUcEztvFWRU49z0x4vthF0ixL5lTK601AXrS8IA=="], + "@slack/types": ["@slack/types@2.20.0", "", {}, "sha512-PVF6P6nxzDMrzPC8fSCsnwaI+kF8YfEpxf3MqXmdyjyWTYsZQURpkK7WWUWvP5QpH55pB7zyYL9Qem/xSgc5VA=="], "@slack/web-api": ["@slack/web-api@6.13.0", "", { "dependencies": { "@slack/logger": "^3.0.0", "@slack/types": "^2.11.0", "@types/is-stream": "^1.1.0", "@types/node": ">=12.0.0", "axios": "^1.7.4", "eventemitter3": "^3.1.0", "form-data": "^2.5.0", "is-electron": "2.2.2", "is-stream": "^1.1.0", "p-queue": "^6.6.1", "p-retry": "^4.0.0" } }, "sha512-dv65crIgdh9ZYHrevLU6XFHTQwTyDmNqEqzuIrV+Vqe/vgiG6w37oex5ePDU1RGm2IJ90H8iOvHFvzdEO/vB+g=="], @@ -1532,7 +1612,7 @@ "@smithy/config-resolver": ["@smithy/config-resolver@4.4.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ=="], - "@smithy/core": ["@smithy/core@3.22.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.9", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-6vjCHD6vaY8KubeNw2Fg3EK0KLGQYdldG4fYgQmA0xSW0dJ8G2xFhSOdrlUakWVoP5JuWHtFODg3PNd/DN3FDA=="], + "@smithy/core": ["@smithy/core@3.23.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.9", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-Yq4UPVoQICM9zHnByLmG8632t2M0+yap4T7ANVw482J0W7HW0pOuxwVmeOwzJqX2Q89fkXz0Vybz55Wj2Xzrsg=="], "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw=="], @@ -1562,9 +1642,9 @@ "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A=="], - "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.12", "", { "dependencies": { "@smithy/core": "^3.22.0", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-9JMKHVJtW9RysTNjcBZQHDwB0p3iTP6B1IfQV4m+uCevkVd/VuLgwfqk5cnI4RHcp4cPwoIvxQqN4B1sxeHo8Q=="], + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.14", "", { "dependencies": { "@smithy/core": "^3.23.0", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FUFNE5KVeaY6U/GL0nzAAHkaCHzXLZcY1EhtQnsAqhD8Du13oPKtMB9/0WK4/LK6a/T5OZ24wPoSShff5iI6Ag=="], - "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.29", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-bmTn75a4tmKRkC5w61yYQLb3DmxNzB8qSVu9SbTYqW6GAL0WXO2bDZuMAn/GJSbOdHEdjZvWxe+9Kk015bw6Cg=="], + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.31", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-RXBzLpMkIrxBPe4C8OmEOHvS8aH9RUuCOH++Acb5jZDEblxDjyg6un72X9IcbrGTJoiUwmI7hLypNfuDACypbg=="], "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.9", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ=="], @@ -1572,7 +1652,7 @@ "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.8", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg=="], - "@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.8", "", { "dependencies": { "@smithy/abort-controller": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.10", "", { "dependencies": { "@smithy/abort-controller": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-u4YeUwOWRZaHbWaebvrs3UhwQwj+2VNmcVCwXcYTvPIuVyM7Ex1ftAj+fdbG/P4AkBwLq/+SKn+ydOI4ZJE9PA=="], "@smithy/property-provider": ["@smithy/property-provider@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w=="], @@ -1588,7 +1668,7 @@ "@smithy/signature-v4": ["@smithy/signature-v4@5.3.8", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg=="], - "@smithy/smithy-client": ["@smithy/smithy-client@4.11.1", "", { "dependencies": { "@smithy/core": "^3.22.0", "@smithy/middleware-endpoint": "^4.4.12", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.10", "tslib": "^2.6.2" } }, "sha512-SERgNg5Z1U+jfR6/2xPYjSEHY1t3pyTHC/Ma3YQl6qWtmiL42bvNId3W/oMUWIwu7ekL2FMPdqAmwbQegM7HeQ=="], + "@smithy/smithy-client": ["@smithy/smithy-client@4.11.3", "", { "dependencies": { "@smithy/core": "^3.23.0", "@smithy/middleware-endpoint": "^4.4.14", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.12", "tslib": "^2.6.2" } }, "sha512-Q7kY5sDau8OoE6Y9zJoRGgje8P4/UY0WzH8R2ok0PDh+iJ+ZnEKowhjEqYafVcubkbYxQVaqwm3iufktzhprGg=="], "@smithy/types": ["@smithy/types@4.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw=="], @@ -1604,9 +1684,9 @@ "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="], - "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.28", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-/9zcatsCao9h6g18p/9vH9NIi5PSqhCkxQ/tb7pMgRFnqYp9XUOyOlGPDMHzr8n5ih6yYgwJEY2MLEobUgi47w=="], + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.30", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-cMni0uVU27zxOiU8TuC8pQLC1pYeZ/xEMxvchSK/ILwleRd1ugobOcIRr5vXtcRqKd4aBLWlpeBoDPJJ91LQng=="], - "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.31", "", { "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-JTvoApUXA5kbpceI2vuqQzRjeTbLpx1eoa5R/YEZbTgtxvIB7AQZxFJ0SEyfCpgPCyVV9IT7we+ytSeIB3CyWA=="], + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.33", "", { "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-LEb2aq5F4oZUSzWBG7S53d4UytZSkOEJPXcBq/xbG2/TmK9EW5naUZ8lKu1BEyWMzdHIzEVN16M3k8oxDq+DJA=="], "@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw=="], @@ -1616,7 +1696,7 @@ "@smithy/util-retry": ["@smithy/util-retry@4.2.8", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg=="], - "@smithy/util-stream": ["@smithy/util-stream@4.5.10", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g=="], + "@smithy/util-stream": ["@smithy/util-stream@4.5.12", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.10", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-D8tgkrmhAX/UNeCZbqbEO3uqyghUnEmmoO9YEvRuwxjlkKKUE7FOgCJnqpTlQPe9MApdWPky58mNQQHbnCzoNg=="], "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], @@ -1722,10 +1802,58 @@ "@tanstack/server-functions-plugin": ["@tanstack/server-functions-plugin@1.134.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/directive-functions-plugin": "1.134.5", "babel-dead-code-elimination": "^1.0.9", "tiny-invariant": "^1.3.3" } }, "sha512-2sWxq70T+dOEUlE3sHlXjEPhaFZfdPYlWTSkHchWXrFGw2YOAa+hzD6L9wHMjGDQezYd03ue8tQlHG+9Jzbzgw=="], - "@tauri-apps/api": ["@tauri-apps/api@2.9.1", "", {}, "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw=="], + "@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="], + + "@tauri-apps/cli": ["@tauri-apps/cli@2.10.0", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.10.0", "@tauri-apps/cli-darwin-x64": "2.10.0", "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.0", "@tauri-apps/cli-linux-arm64-gnu": "2.10.0", "@tauri-apps/cli-linux-arm64-musl": "2.10.0", "@tauri-apps/cli-linux-riscv64-gnu": "2.10.0", "@tauri-apps/cli-linux-x64-gnu": "2.10.0", "@tauri-apps/cli-linux-x64-musl": "2.10.0", "@tauri-apps/cli-win32-arm64-msvc": "2.10.0", "@tauri-apps/cli-win32-ia32-msvc": "2.10.0", "@tauri-apps/cli-win32-x64-msvc": "2.10.0" }, "bin": { "tauri": "tauri.js" } }, "sha512-ZwT0T+7bw4+DPCSWzmviwq5XbXlM0cNoleDKOYPFYqcZqeKY31KlpoMW/MOON/tOFBPgi31a2v3w9gliqwL2+Q=="], + + "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.10.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-avqHD4HRjrMamE/7R/kzJPcAJnZs0IIS+1nkDP5b+TNBn3py7N2aIo9LIpy+VQq0AkN8G5dDpZtOOBkmWt/zjA=="], + + "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.10.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-keDmlvJRStzVFjZTd0xYkBONLtgBC9eMTpmXnBXzsHuawV2q9PvDo2x6D5mhuoMVrJ9QWjgaPKBBCFks4dK71Q=="], + + "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.10.0", "", { "os": "linux", "cpu": "arm" }, "sha512-e5u0VfLZsMAC9iHaOEANumgl6lfnJx0Dtjkd8IJpysZ8jp0tJ6wrIkto2OzQgzcYyRCKgX72aKE0PFgZputA8g=="], + + "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.10.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-YrYYk2dfmBs5m+OIMCrb+JH/oo+4FtlpcrTCgiFYc7vcs6m3QDd1TTyWu0u01ewsCtK2kOdluhr/zKku+KP7HA=="], + + "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.10.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-GUoPdVJmrJRIXFfW3Rkt+eGK9ygOdyISACZfC/bCSfOnGt8kNdQIQr5WRH9QUaTVFIwxMlQyV3m+yXYP+xhSVA=="], + + "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.10.0", "", { "os": "linux", "cpu": "none" }, "sha512-JO7s3TlSxshwsoKNCDkyvsx5gw2QAs/Y2GbR5UE2d5kkU138ATKoPOtxn8G1fFT1aDW4LH0rYAAfBpGkDyJJnw=="], + + "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.10.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Uvh4SUUp4A6DVRSMWjelww0GnZI3PlVy7VS+DRF5napKuIehVjGl9XD0uKoCoxwAQBLctvipyEK+pDXpJeoHng=="], + + "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.10.0", "", { "os": "linux", "cpu": "x64" }, "sha512-AP0KRK6bJuTpQ8kMNWvhIpKUkQJfcPFeba7QshOQZjJ8wOS6emwTN4K5g/d3AbCMo0RRdnZWwu67MlmtJyxC1Q=="], + + "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.10.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-97DXVU3dJystrq7W41IX+82JEorLNY+3+ECYxvXWqkq7DBN6FsA08x/EFGE8N/b0LTOui9X2dvpGGoeZKKV08g=="], + + "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.10.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-EHyQ1iwrWy1CwMalEm9z2a6L5isQ121pe7FcA2xe4VWMJp+GHSDDGvbTv/OPdkt2Lyr7DAZBpZHM6nvlHXEc4A=="], + + "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.10.0", "", { "os": "win32", "cpu": "x64" }, "sha512-NTpyQxkpzGmU6ceWBTY2xRIEaS0ZLbVx1HE1zTA3TY/pV3+cPoPPOs+7YScr4IMzXMtOw7tLw5LEXo5oIG3qaQ=="], + + "@tauri-apps/plugin-clipboard-manager": ["@tauri-apps/plugin-clipboard-manager@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ=="], + + "@tauri-apps/plugin-deep-link": ["@tauri-apps/plugin-deep-link@2.4.7", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-K0FQlLM6BoV7Ws2xfkh+Tnwi5VZVdkI4Vw/3AGLSf0Xvu2y86AMBzd9w/SpzKhw9ai2B6ES8di/OoGDCExkOzg=="], + + "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.6.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg=="], + + "@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.7", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-+F2lEH/c9b0zSsOXKq+5hZNcd9F4IIKCK1T17RqMwpCmVnx2aoqY8yIBccCd25HTYUb3j6NPVbRax/m00hKG8A=="], + + "@tauri-apps/plugin-notification": ["@tauri-apps/plugin-notification@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg=="], + + "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ=="], + + "@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="], + + "@tauri-apps/plugin-process": ["@tauri-apps/plugin-process@2.3.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA=="], + + "@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.5", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg=="], "@tauri-apps/plugin-store": ["@tauri-apps/plugin-store@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-0ClHS50Oq9HEvLPhNzTNFxbWVOqoAp3dRvtewQBeqfIQ0z5m3JRnOISIn2ZVPCrQC0MyGyhTS9DWhHjpigQE7A=="], + "@tauri-apps/plugin-updater": ["@tauri-apps/plugin-updater@2.10.0", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-ljN8jPlnT0aSn8ecYhuBib84alxfMx6Hc8vJSKMJyzGbTPFZAC44T2I1QNFZssgWKrAlofvJqCC6Rr472JWfkQ=="], + + "@tauri-apps/plugin-window-state": ["@tauri-apps/plugin-window-state@2.4.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-OuvdrzyY8Q5Dbzpj+GcrnV1iCeoZbcFdzMjanZMMcAEUNy/6PH5pxZPXpaZLOR7whlzXiuzx0L9EKZbH7zpdRw=="], + + "@tediousjs/connection-string": ["@tediousjs/connection-string@0.5.0", "", {}, "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ=="], + "@thisbeyond/solid-dnd": ["@thisbeyond/solid-dnd@0.7.5", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], @@ -1748,7 +1876,7 @@ "@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="], - "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], + "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], @@ -1794,6 +1922,8 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/mssql": ["@types/mssql@9.1.9", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-P0nCgw6vzY23UxZMnbI4N7fnLGANt4LI4yvxze1paPj+LuN28cFv5EI+QidP8udnId/BKhkcRhm/BleNsjK65A=="], + "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], "@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], @@ -1810,13 +1940,15 @@ "@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="], + "@types/readable-stream": ["@types/readable-stream@4.0.23", "", { "dependencies": { "@types/node": "*" } }, "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig=="], + "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], "@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="], "@types/scheduler": ["@types/scheduler@0.26.0", "", {}, "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA=="], - "@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], @@ -1824,6 +1956,8 @@ "@types/tsscmp": ["@types/tsscmp@1.0.2", "", {}, "sha512-cy7BRSU8GYYgxjcx0Py+8lo5MthuDhlyu076KUcYzVNXL23luYgRHkMG2fIFEc6neckeh/ntP82mw+U4QjZq+g=="], + "@types/tunnel": ["@types/tunnel@0.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA=="], + "@types/turndown": ["@types/turndown@5.0.5", "", {}, "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -1854,7 +1988,7 @@ "@typescript/vfs": ["@typescript/vfs@1.6.2", "", { "dependencies": { "debug": "^4.1.1" }, "peerDependencies": { "typescript": "*" } }, "sha512-hoBwJwcbKHmvd2QVebiytN1aELvpk9B74B4L1mFm/XT1Q/VOYAWl2vQ9AWRFtQq8zmz6enTpfTV8WRc4ATjW/g=="], - "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.2", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg=="], + "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.3", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], @@ -1876,6 +2010,22 @@ "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="], + "@volar/kit": ["@volar/kit@2.4.28", "", { "dependencies": { "@volar/language-service": "2.4.28", "@volar/typescript": "2.4.28", "typesafe-path": "^0.2.2", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "typescript": "*" } }, "sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg=="], + + "@volar/language-core": ["@volar/language-core@2.4.28", "", { "dependencies": { "@volar/source-map": "2.4.28" } }, "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ=="], + + "@volar/language-server": ["@volar/language-server@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "@volar/language-service": "2.4.28", "@volar/typescript": "2.4.28", "path-browserify": "^1.0.1", "request-light": "^0.7.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-NqcLnE5gERKuS4PUFwlhMxf6vqYo7hXtbMFbViXcbVkbZ905AIVWhnSo0ZNBC2V127H1/2zP7RvVOVnyITFfBw=="], + + "@volar/language-service": ["@volar/language-service@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-Rh/wYCZJrI5vCwMk9xyw/Z+MsWxlJY1rmMZPsxUoJKfzIRjS/NF1NmnuEcrMbEVGja00aVpCsInJfixQTMdvLw=="], + + "@volar/source-map": ["@volar/source-map@2.4.28", "", {}, "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ=="], + + "@volar/typescript": ["@volar/typescript@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw=="], + + "@vscode/emmet-helper": ["@vscode/emmet-helper@2.11.0", "", { "dependencies": { "emmet": "^2.4.3", "jsonc-parser": "^2.3.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.15.1", "vscode-uri": "^3.0.8" } }, "sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw=="], + + "@vscode/l10n": ["@vscode/l10n@0.0.18", "", {}, "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ=="], + "@webgpu/types": ["@webgpu/types@0.1.54", "", {}, "sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg=="], "@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="], @@ -1904,6 +2054,8 @@ "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], @@ -1964,11 +2116,11 @@ "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], - "autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="], + "autoprefixer": ["autoprefixer@10.4.24", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001766", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw=="], "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "avvio": ["avvio@9.1.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw=="], + "avvio": ["avvio@9.2.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ=="], "await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="], @@ -1978,11 +2130,11 @@ "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], - "axios": ["axios@1.13.4", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg=="], + "axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], - "b4a": ["b4a@1.7.3", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q=="], + "b4a": ["b4a@1.7.4", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug=="], "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="], @@ -1994,7 +2146,7 @@ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "balanced-match": ["balanced-match@4.0.2", "", { "dependencies": { "jackspeak": "^4.2.3" } }, "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg=="], "bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="], @@ -2016,6 +2168,8 @@ "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + "bl": ["bl@6.1.6", "", { "dependencies": { "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^4.2.0" } }, "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg=="], + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], "blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="], @@ -2030,11 +2184,11 @@ "bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="], - "bowser": ["bowser@2.13.1", "", {}, "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw=="], + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], "boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], - "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -2056,7 +2210,7 @@ "bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="], - "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], "bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="], @@ -2086,7 +2240,7 @@ "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], - "caniuse-lite": ["caniuse-lite@1.0.30001766", "", {}, "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA=="], + "caniuse-lite": ["caniuse-lite@1.0.30001769", "", {}, "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -2160,7 +2314,7 @@ "condense-newlines": ["condense-newlines@0.2.1", "", { "dependencies": { "extend-shallow": "^2.0.1", "is-whitespace": "^0.3.0", "kind-of": "^3.0.2" } }, "sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg=="], - "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], + "confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], "config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="], @@ -2222,7 +2376,7 @@ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="], + "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], @@ -2286,11 +2440,11 @@ "dot-prop": ["dot-prop@8.0.2", "", { "dependencies": { "type-fest": "^3.8.0" } }, "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ=="], - "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], + "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], - "drizzle-kit": ["drizzle-kit@0.30.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l6dMSE100u7sDaTbLczibrQZjA35jLsHNqIV+jmhNVO3O8jzM6kywMOmV9uOz9ZVSCMPQhAZEFjL/qDPVrqpUA=="], + "drizzle-kit": ["drizzle-kit@1.0.0-beta.12-a5629fb", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "tsx": "^4.20.6" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l+p4QOMvPGYBYEE9NBlU7diu+NSlxuOUwi0I7i01Uj1PpfU0NxhPzaks/9q1MDw4FAPP8vdD0dOhoqosKtRWWQ=="], - "drizzle-orm": ["drizzle-orm@0.41.0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q=="], + "drizzle-orm": ["drizzle-orm@1.0.0-beta.12-a5629fb", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-wyOAgr9Cy9oEN6z5S0JGhfipLKbRRJtQKgbDO9SXGR9swMBbGNIlXkeMqPRrqYQ8k70mh+7ZJ/eVmJ2F7zR3Vg=="], "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], @@ -2304,7 +2458,9 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.282", "", {}, "sha512-FCPkJtpst28UmFzd903iU7PdeVTfY0KAeJy+Lk0GLZRwgwYHn/irRcaCbQQOmr5Vytc/7rcavsYLvTM8RiHYhQ=="], + "electron-to-chromium": ["electron-to-chromium@1.5.286", "", {}, "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A=="], + + "emmet": ["emmet@2.4.11", "", { "dependencies": { "@emmetio/abbreviation": "^2.3.3", "@emmetio/css-abbreviation": "^2.1.8" } }, "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -2316,11 +2472,9 @@ "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], - "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], - - "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="], - "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], @@ -2352,8 +2506,6 @@ "esbuild-plugin-copy": ["esbuild-plugin-copy@2.1.1", "", { "dependencies": { "chalk": "^4.1.2", "chokidar": "^3.5.3", "fs-extra": "^10.0.1", "globby": "^11.0.3" }, "peerDependencies": { "esbuild": ">= 0.14.0" } }, "sha512-Bk66jpevTcV8KMFzZI1P7MZKZ+uDcrZm2G2egZ2jNIvVnivDpodZI+/KnpL3Jnap0PBdIHU7HwFGB8r+vV5CVw=="], - "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], @@ -2420,7 +2572,7 @@ "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - "fast-json-stringify": ["fast-json-stringify@6.2.0", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-Eaf/KNIDwHkzfyeQFNfLXJnQ7cl1XQI3+zRqmPlvtkMigbXnAcasTrvJQmquBSxKfFGeRA6PFog8t+hFmpDoWw=="], + "fast-json-stringify": ["fast-json-stringify@6.3.0", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA=="], "fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="], @@ -2498,8 +2650,6 @@ "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], - "gel": ["gel@2.2.0", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ=="], - "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], @@ -2522,7 +2672,7 @@ "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], - "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], "ghostty-web": ["ghostty-web@0.4.0", "", {}, "sha512-0puDBik2qapbD/QQBW9o5ZHfXnZBqZWx/ctBiVtKZ6ZLds4NYb+wZuw1cRLXZk9zYovIQ908z3rvFhexAvc5Hg=="], @@ -2560,7 +2710,7 @@ "h3": ["h3@2.0.1-rc.4", "", { "dependencies": { "rou3": "^0.7.8", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-vZq8pEUp6THsXKXrUXX44eOqfChic2wVQ1GlSzQCBr7DeFBkfIZAo2WyNND4GSv54TAa0E4LYIK73WSPdgKUgw=="], - "happy-dom": ["happy-dom@20.4.0", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^4.5.0", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-RDeQm3dT9n0A5f/TszjUmNCLEuPnMGv3Tv4BmNINebz/h17PA6LMBcxJ5FrcqltNBMh9jA/8ufgDdBYUdBt+eg=="], + "happy-dom": ["happy-dom@20.6.1", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^6.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-+0vhESXXhFwkdjZnJ5DlmJIfUYGgIEEjzIjB+aKJbFuqlvvKyOi+XkI1fYbgYR9QCxG5T08koxsQ6HrQfa5gCQ=="], "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], @@ -2778,7 +2928,7 @@ "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - "isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], @@ -2786,7 +2936,7 @@ "iterate-value": ["iterate-value@1.0.2", "", { "dependencies": { "es-get-iterator": "^1.0.2", "iterate-iterator": "^1.0.1" } }, "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ=="], - "jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="], + "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], "jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="], @@ -2804,10 +2954,14 @@ "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], + "js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], @@ -2910,7 +3064,7 @@ "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "lru.min": ["lru.min@1.1.3", "", {}, "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q=="], + "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], @@ -3084,6 +3238,10 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mssql": ["mssql@11.0.1", "", { "dependencies": { "@tediousjs/connection-string": "^0.5.0", "commander": "^11.0.0", "debug": "^4.3.3", "rfdc": "^1.3.0", "tarn": "^3.0.2", "tedious": "^18.2.1" }, "bin": { "mssql": "bin/mssql" } }, "sha512-KlGNsugoT90enKlR8/G36H0kTxPthDhmtNUCwEHvgRza5Cjpjoj+P2X6eMpFUDN7pFrJZsKadL4x990G8RBE1w=="], + + "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], + "multicast-dns": ["multicast-dns@7.2.5", "", { "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg=="], "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], @@ -3098,6 +3256,8 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "native-duplexpair": ["native-duplexpair@1.0.0", "", {}, "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA=="], + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], @@ -3134,7 +3294,7 @@ "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - "nypm": ["nypm@0.6.4", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw=="], + "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -3236,6 +3396,8 @@ "pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="], + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -3264,7 +3426,7 @@ "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], - "pino": ["pino@10.3.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-0GNPNzHXBKw6U/InGe79A3Crzyk9bcSyObF9/Gfo9DLEf5qj5RF50RSjsu0W1rZ6ZqRGdzDFCRBQvi9/rSGPtA=="], + "pino": ["pino@10.3.1", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg=="], "pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="], @@ -3282,7 +3444,7 @@ "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], - "planck": ["planck@1.4.2", "", { "peerDependencies": { "stage-js": "^1.0.0-alpha.12" } }, "sha512-mNbhnV3g8X2rwGxzcesjmN8BDA6qfXgQxXVMkWau9MCRlQY0RLNEkyHlVp6yFy/X6qrzAXyNONCnZ1cGDLrNew=="], + "planck": ["planck@1.4.3", "", { "peerDependencies": { "stage-js": "^1.0.0-alpha.12" } }, "sha512-B+lHKhRSeg7vZOfEyEzyQVu7nx8JHcX3QgnAcHXrPW0j04XYKX5eXSiUrxH2Z5QR8OoqvjD6zKIaPMdMYAd0uA=="], "playwright": ["playwright@1.57.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="], @@ -3338,7 +3500,7 @@ "punycode": ["punycode@1.3.2", "", {}, "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="], - "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], + "qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], @@ -3438,6 +3600,10 @@ "remeda": ["remeda@2.26.0", "", { "dependencies": { "type-fest": "^4.41.0" } }, "sha512-lmNNwtaC6Co4m0WTTNoZ/JlpjEqAjPZO0+czC9YVRQUpkbS4x8Hmh+Mn9HPfJfiXqUQ5IXXgSXSOB2pBKAytdA=="], + "request-light": ["request-light@0.7.0", "", {}, "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="], @@ -3470,7 +3636,7 @@ "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], - "rollup": ["rollup@4.57.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.0", "@rollup/rollup-android-arm64": "4.57.0", "@rollup/rollup-darwin-arm64": "4.57.0", "@rollup/rollup-darwin-x64": "4.57.0", "@rollup/rollup-freebsd-arm64": "4.57.0", "@rollup/rollup-freebsd-x64": "4.57.0", "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", "@rollup/rollup-linux-arm-musleabihf": "4.57.0", "@rollup/rollup-linux-arm64-gnu": "4.57.0", "@rollup/rollup-linux-arm64-musl": "4.57.0", "@rollup/rollup-linux-loong64-gnu": "4.57.0", "@rollup/rollup-linux-loong64-musl": "4.57.0", "@rollup/rollup-linux-ppc64-gnu": "4.57.0", "@rollup/rollup-linux-ppc64-musl": "4.57.0", "@rollup/rollup-linux-riscv64-gnu": "4.57.0", "@rollup/rollup-linux-riscv64-musl": "4.57.0", "@rollup/rollup-linux-s390x-gnu": "4.57.0", "@rollup/rollup-linux-x64-gnu": "4.57.0", "@rollup/rollup-linux-x64-musl": "4.57.0", "@rollup/rollup-openbsd-x64": "4.57.0", "@rollup/rollup-openharmony-arm64": "4.57.0", "@rollup/rollup-win32-arm64-msvc": "4.57.0", "@rollup/rollup-win32-ia32-msvc": "4.57.0", "@rollup/rollup-win32-x64-gnu": "4.57.0", "@rollup/rollup-win32-x64-msvc": "4.57.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA=="], + "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="], "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], @@ -3506,7 +3672,7 @@ "selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="], - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], @@ -3582,7 +3748,7 @@ "solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="], - "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], + "sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="], "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], @@ -3594,7 +3760,7 @@ "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], @@ -3622,7 +3788,7 @@ "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], - "stage-js": ["stage-js@1.0.0-alpha.18", "", {}, "sha512-Mh+pbkfxA6NXlDrcutP8vp1Zg04pDRcC8D39UXKZzEcQeBPOZ4SRUSkIsF26aoODUZ4CSQRY7shXc1Avb0wZKA=="], + "stage-js": ["stage-js@1.0.1", "", {}, "sha512-cz14aPp/wY0s3bkb/B93BPP5ZAEhgBbRmAT3CCDqert8eCAqIpQ0RB2zpK8Ksxf+Pisl5oTzvPHtL4CVzzeHcw=="], "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], @@ -3686,11 +3852,15 @@ "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], + "tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="], + + "tedious": ["tedious@19.2.1", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.5", "@types/node": ">=18", "bl": "^6.1.4", "iconv-lite": "^0.7.0", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-pk1Q16Yl62iocuQB+RWbg6rFUFkIyzqOFQ6NfysCltRvQqKwfurgj8v/f2X+CKvDhSL4IJ0cCOfCHDg9PWEEYA=="], + "terracotta": ["terracotta@1.1.0", "", { "dependencies": { "solid-use": "^0.9.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-kfQciWUBUBgYkXu7gh3CK3FAJng/iqZslAaY08C+k1Hdx17aVEpcFFb/WPaysxAfcupNH3y53s/pc53xxZauww=="], "terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="], - "text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="], + "text-decoder": ["text-decoder@1.2.4", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-mzlffA3tBNhziEHPK5L5InZg1d/ElNIpJhnhbDRNUtem/edZcJ5zg5FgwKKKOyklxk+6Jt+TrSu83musmvrDlg=="], "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], @@ -3748,6 +3918,8 @@ "tsscmp": ["tsscmp@1.0.6", "", {}, "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA=="], + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], "turbo": ["turbo@2.5.6", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.6", "turbo-darwin-arm64": "2.5.6", "turbo-linux-64": "2.5.6", "turbo-linux-arm64": "2.5.6", "turbo-windows-64": "2.5.6", "turbo-windows-arm64": "2.5.6" }, "bin": { "turbo": "bin/turbo" } }, "sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w=="], @@ -3780,8 +3952,12 @@ "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + "typesafe-path": ["typesafe-path@0.2.2", "", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="], + "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + "typescript-auto-import-cache": ["typescript-auto-import-cache@0.3.6", "", { "dependencies": { "semver": "^7.3.8" } }, "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ=="], + "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], "ulid": ["ulid@3.0.1", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q=="], @@ -3792,7 +3968,7 @@ "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], - "undici": ["undici@7.19.2", "", {}, "sha512-4VQSpGEGsWzk0VYxyB/wVX/Q7qf9t5znLRgs0dzszr9w9Fej/8RVNQ+S20vdXSAyra/bJ7ZQfGv6ZMj7UEbzSg=="], + "undici": ["undici@7.21.0", "", {}, "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg=="], "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], @@ -3878,10 +4054,40 @@ "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], + "volar-service-css": ["volar-service-css@0.0.68", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-lJSMh6f3QzZ1tdLOZOzovLX0xzAadPhx8EKwraDLPxBndLCYfoTvnNuiFFV8FARrpAlW5C0WkH+TstPaCxr00Q=="], + + "volar-service-emmet": ["volar-service-emmet@0.0.68", "", { "dependencies": { "@emmetio/css-parser": "^0.4.1", "@emmetio/html-matcher": "^1.3.0", "@vscode/emmet-helper": "^2.9.3", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-nHvixrRQ83EzkQ4G/jFxu9Y4eSsXS/X2cltEPDM+K9qZmIv+Ey1w0tg1+6caSe8TU5Hgw4oSTwNMf/6cQb3LzQ=="], + + "volar-service-html": ["volar-service-html@0.0.68", "", { "dependencies": { "vscode-html-languageservice": "^5.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-fru9gsLJxy33xAltXOh4TEdi312HP80hpuKhpYQD4O5hDnkNPEBdcQkpB+gcX0oK0VxRv1UOzcGQEUzWCVHLfA=="], + + "volar-service-prettier": ["volar-service-prettier@0.0.68", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0", "prettier": "^2.2 || ^3.0" }, "optionalPeers": ["@volar/language-service", "prettier"] }, "sha512-grUmWHkHlebMOd6V8vXs2eNQUw/bJGJMjekh/EPf/p2ZNTK0Uyz7hoBRngcvGfJHMsSXZH8w/dZTForIW/4ihw=="], + + "volar-service-typescript": ["volar-service-typescript@0.0.68", "", { "dependencies": { "path-browserify": "^1.0.1", "semver": "^7.6.2", "typescript-auto-import-cache": "^0.3.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-nls": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-z7B/7CnJ0+TWWFp/gh2r5/QwMObHNDiQiv4C9pTBNI2Wxuwymd4bjEORzrJ/hJ5Yd5+OzeYK+nFCKevoGEEeKw=="], + + "volar-service-typescript-twoslash-queries": ["volar-service-typescript-twoslash-queries@0.0.68", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-NugzXcM0iwuZFLCJg47vI93su5YhTIweQuLmZxvz5ZPTaman16JCvmDZexx2rd5T/75SNuvvZmrTOTNYUsfe5w=="], + + "volar-service-yaml": ["volar-service-yaml@0.0.68", "", { "dependencies": { "vscode-uri": "^3.0.8", "yaml-language-server": "~1.19.2" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-84XgE02LV0OvTcwfqhcSwVg4of3MLNUWPMArO6Aj8YXqyEVnPu8xTEMY2btKSq37mVAPuaEVASI4e3ptObmqcA=="], + + "vscode-css-languageservice": ["vscode-css-languageservice@6.3.9", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-1tLWfp+TDM5ZuVWht3jmaY5y7O6aZmpeXLoHl5bv1QtRsRKt4xYGRMmdJa5Pqx/FTkgRbsna9R+Gn2xE+evVuA=="], + + "vscode-html-languageservice": ["vscode-html-languageservice@5.6.1", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-5Mrqy5CLfFZUgkyhNZLA1Ye5g12Cb/v6VM7SxUzZUaRKWMDz4md+y26PrfRTSU0/eQAl3XpO9m2og+GGtDMuaA=="], + + "vscode-json-languageservice": ["vscode-json-languageservice@4.1.8", "", { "dependencies": { "jsonc-parser": "^3.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-nls": "^5.0.0", "vscode-uri": "^3.0.2" } }, "sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg=="], + "vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="], + "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="], + + "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], + + "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], + "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], + "vscode-nls": ["vscode-nls@5.2.0", "", {}, "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng=="], + + "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], @@ -3894,7 +4100,7 @@ "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - "which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], @@ -3942,6 +4148,8 @@ "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + "yaml-language-server": ["yaml-language-server@1.19.2", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "lodash": "4.17.21", "prettier": "^3.5.0", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^9.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-uri": "^3.0.2", "yaml": "2.7.1" }, "bin": { "yaml-language-server": "bin/yaml-language-server" } }, "sha512-9F3myNmJzUN/679jycdMxqtydPSDRAarSj3wPiF7pchEPnO9Dg07Oc+gIYLqXR4L+g+FSEVXXv2+mr54StLFOg=="], + "yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], @@ -3982,9 +4190,9 @@ "@actions/http-client/undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I3RhaOEMnWlWnrvjNBOYvUb19Dwf2nw01IruZrVJRDi688886e11wnd5DxrBZLd2V29Gizo3vpOPnnExsA+wTA=="], - "@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.8", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], "@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], @@ -3994,11 +4202,25 @@ "@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], - "@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], + "@ai-sdk/deepgram/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], + + "@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ=="], - "@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], + "@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], - "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="], + "@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], + + "@ai-sdk/elevenlabs/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], + + "@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ=="], + + "@ai-sdk/fireworks/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], + + "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.63", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zXlUPCkumnvp8lWS9VFcen/MLF6CL/t1zAKDhpobYj9y/nmylQrKtRvn3RwH871Wd3dF3KYEUXd6M2c6dfCKOA=="], + + "@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.53", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ccCxr5mrd3AC2CjLq4e1ST7+UiN5T2Pdmgi0XdWM3QohmNBwUQ/RBG7BvL+cB/ex/j6y64tkMmpYz9zBw/SEFQ=="], + + "@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], "@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], @@ -4014,6 +4236,8 @@ "@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], + "@astrojs/check/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], "@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="], @@ -4054,7 +4278,17 @@ "@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], - "@azure/core-xml/fast-xml-parser": ["fast-xml-parser@5.3.3", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-2O3dkPAAC6JavuMm8+4+pgTk+5hoAs+CjZ+sWcQLkX9+/tHRuTkQh/Oaifr8qDmZ8iEHb771Ea6G8CdwkrgvYA=="], + "@azure/core-http/@azure/abort-controller": ["@azure/abort-controller@1.1.0", "", { "dependencies": { "tslib": "^2.2.0" } }, "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw=="], + + "@azure/core-http/@azure/core-tracing": ["@azure/core-tracing@1.0.0-preview.13", "", { "dependencies": { "@opentelemetry/api": "^1.0.1", "tslib": "^2.2.0" } }, "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ=="], + + "@azure/core-http/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + + "@azure/core-http/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], + + "@azure/core-xml/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + + "@azure/msal-node/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -4072,21 +4306,17 @@ "@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], - "@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], - "@gitlab/gitlab-ai-provider/openai": ["openai@6.17.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-NHRpPEUPzAvFOAFs9+9pC6+HCw/iWsYsKCMPXH5Kw7BpMxqd8g/A07/1o7Gx2TWtCnzevVRyKMRFqyiHyAlqcA=="], + "@gitlab/gitlab-ai-provider/openai": ["openai@6.21.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-26dQFi76dB8IiN/WKGQOV+yKKTTlRCxQjoi2WLt0kMcH8pvxVyvfdBDkld5GTl7W1qvBpwVOtFcsqktj3fBRpA=="], "@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], - "@hono/zod-validator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + "@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "@hono/zod-validator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@jimp/plugin-blit/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -4198,7 +4428,11 @@ "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], - "@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/types": "3.4.2" } }, "sha512-I5baLVi/ynLEOZoWSAMlACHNnG+yw5HDmse0oe+GW6U1u+ULdEB3UHiVWaHoJSSONV7tlcVxuaMy74sREDkSvg=="], + "@opencode-ai/desktop/@actions/artifact": ["@actions/artifact@4.0.0", "", { "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^6.0.1", "@actions/http-client": "^2.1.0", "@azure/core-http": "^3.0.5", "@azure/storage-blob": "^12.15.0", "@octokit/core": "^5.2.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^7.0.1", "jwt-decode": "^3.1.2", "unzip-stream": "^0.3.1" } }, "sha512-HCc2jMJRAfviGFAh0FsOR/jNfWhirxl7W6z8zDtttt0GltwxBLdEIjLiweOPFl9WbyJRW1VWnPUSAixJqcWUMQ=="], + + "@opencode-ai/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], + + "@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], "@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], @@ -4206,13 +4440,9 @@ "@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="], - "@pierre/diffs/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], - - "@pierre/diffs/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-ZfWJNm2VMhKkQIKT9qXbs76RRcT0SF/CAvEz0+RkpUDAoDaCx0uFdCGzSRiD9gSlhm6AHkjdieOBJMaO2eC1rQ=="], - - "@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.19.0", "", { "dependencies": { "@shikijs/core": "3.19.0", "@shikijs/types": "3.19.0" } }, "sha512-e6vwrsyw+wx4OkcrDbL+FVCxwx8jgKiCoXzakVur++mIWVcgpzIi8vxf4/b4dVTYrV/nUx5RjinMf4tq8YV8Fw=="], + "@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], - "@pierre/diffs/shiki": ["shiki@3.19.0", "", { "dependencies": { "@shikijs/core": "3.19.0", "@shikijs/engine-javascript": "3.19.0", "@shikijs/engine-oniguruma": "3.19.0", "@shikijs/langs": "3.19.0", "@shikijs/themes": "3.19.0", "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-77VJr3OR/VUZzPiStyRhADmO2jApMM0V2b1qf0RpfWya8Zr1PeZev5AEpPGAAKWdiYUtcZGBE4F5QvJml1PvWA=="], + "@pierre/diffs/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], @@ -4268,19 +4498,23 @@ "@tanstack/directive-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + "@tanstack/router-utils/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + "@tanstack/server-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + "@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + + "@vscode/emmet-helper/jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="], + "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="], + "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I3RhaOEMnWlWnrvjNBOYvUb19Dwf2nw01IruZrVJRDi688886e11wnd5DxrBZLd2V29Gizo3vpOPnnExsA+wTA=="], "ai-gateway-provider/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.90", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.56", "@ai-sdk/google": "2.0.46", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-C9MLe1KZGg1ZbupV2osygHtL5qngyCDA6ATatunyfTbIe8TXKG8HGni/3O6ifbnI5qxTidIn150Ox7eIFZVMYg=="], "ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="], - "ai-gateway-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], - - "ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@2.0.56", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FGlqwWc3tAYqDHE8r8hQGQLcMiPUwgz90oU2QygUH930OWtCLapFkSu114DgVaIN/qoM1DUX+inv0Ee74Fgp5g=="], + "ai-gateway-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ=="], "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -4306,6 +4540,8 @@ "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], + "bl/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], @@ -4320,12 +4556,10 @@ "condense-newlines/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], - "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "dot-prop/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="], - "drizzle-kit/esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], - "editorconfig/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], "editorconfig/minimatch": ["minimatch@9.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w=="], @@ -4354,7 +4588,7 @@ "gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], + "glob/minimatch": ["minimatch@10.2.0", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w=="], "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -4364,6 +4598,10 @@ "html-minifier-terser/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], + "html-minifier-terser/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "js-beautify/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], @@ -4388,17 +4626,21 @@ "miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], + "mssql/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + + "mssql/tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="], + "nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], - "nypm/citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="], + "nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], "nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], "openai/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], - "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="], + "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I3RhaOEMnWlWnrvjNBOYvUb19Dwf2nw01IruZrVJRDi688886e11wnd5DxrBZLd2V29Gizo3vpOPnnExsA+wTA=="], "opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="], @@ -4422,9 +4664,7 @@ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - - "path-scurry/lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="], + "path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], @@ -4432,6 +4672,8 @@ "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "postcss-css-variables/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], @@ -4484,6 +4726,8 @@ "tree-sitter-bash/node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], + "tsx/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + "tw-to-css/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "tw-to-css/tailwindcss": ["tailwindcss@3.3.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.18.2", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w=="], @@ -4500,6 +4744,8 @@ "vitest/why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], + "which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], "wrangler/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="], @@ -4510,6 +4756,12 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "yaml-language-server/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="], + + "yaml-language-server/yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], + "yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], "zod-to-json-schema/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -4530,6 +4782,10 @@ "@ai-sdk/openai/@ai-sdk/provider-utils/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], + "@astrojs/check/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "@astrojs/check/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "@astrojs/mdx/@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.5", "", {}, "sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA=="], "@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="], @@ -4558,56 +4814,8 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@gitlab/gitlab-ai-provider/openai/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "@jsx-email/cli/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], "@jsx-email/cli/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], @@ -4666,6 +4874,8 @@ "@jsx-email/cli/vite/rollup": ["rollup@3.29.5", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w=="], + "@jsx-email/doiuse-email/htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], @@ -4774,29 +4984,17 @@ "@octokit/rest/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], - "@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="], - - "@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="], - - "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@opencode-ai/desktop/@actions/artifact/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], - "@pierre/diffs/@shikijs/core/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], + "@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], - "@pierre/diffs/@shikijs/engine-javascript/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="], + "@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], - "@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-L7SrRibU7ZoYi1/TrZsJOFAnnHyLTE1SwHG1yNWjZIVCqjOEmCSuK2ZO9thnRbJG6TOkPp+Z963JmpCNw5nzvA=="], - - "@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="], - - "@pierre/diffs/shiki/@shikijs/core": ["@shikijs/core@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-L7SrRibU7ZoYi1/TrZsJOFAnnHyLTE1SwHG1yNWjZIVCqjOEmCSuK2ZO9thnRbJG6TOkPp+Z963JmpCNw5nzvA=="], - - "@pierre/diffs/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1hRxtYIJfJSZeM5ivbUXv9hcJP3PWRo5prG/V2sWwiubUKTa+7P62d2qxCW8jiVFX4pgRHhnHNp+qeR7Xl+6kg=="], - - "@pierre/diffs/shiki/@shikijs/langs": ["@shikijs/langs@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0" } }, "sha512-dBMFzzg1QiXqCVQ5ONc0z2ebyoi5BKz+MtfByLm0o5/nbUu3Iz8uaTCa5uzGiscQKm7lVShfZHU1+OG3t5hgwg=="], + "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@pierre/diffs/shiki/@shikijs/themes": ["@shikijs/themes@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0" } }, "sha512-H36qw+oh91Y0s6OlFfdSuQ0Ld+5CgB/VE6gNPK+Hk4VRbVG/XQgkjnt4KzfnnoO6tZPtKJKHPjwebOCfjd6F8A=="], + "@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], - "@pierre/diffs/shiki/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="], + "@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], "@slack/web-api/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -4820,6 +5018,8 @@ "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], + "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="], "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.46", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8PK6u4sGE/kXebd7ZkTp+0aya4kNqzoqpS5m7cHY2NfTK6fhPc6GNvE+MZIZIoHQTp5ed86wGBdeBPpFaaUtyg=="], @@ -4828,6 +5028,8 @@ "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="], + "ai-gateway-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], + "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -4842,7 +5044,7 @@ "astro/unstorage/h3": ["h3@1.15.5", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg=="], - "astro/unstorage/lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="], + "astro/unstorage/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], "astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], @@ -4852,101 +5054,115 @@ "babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "editorconfig/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], + "esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "js-beautify/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "js-beautify/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], + "js-beautify/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="], + "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="], + "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="], + "mssql/tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="], + "opencode/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], - "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="], + "opencontrol/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="], + "opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], - "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="], + "opencontrol/@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="], + "opencontrol/@modelcontextprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="], + "opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], - "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="], + "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], - "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="], + "readable-stream/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="], + "readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="], + "rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], - "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="], + "rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="], + "rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="], + "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="], + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="], + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], - "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="], + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], - "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="], + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], - "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], - "esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], - "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], - "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], - "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], - "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], - "js-beautify/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], - "js-beautify/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], - "js-beautify/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], - "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], - "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], - "opencontrol/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], - "opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], - "opencontrol/@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], - "opencontrol/@modelcontextprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], - "opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], - "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], - "readable-stream/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], - "rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], - "rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], - "rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], - "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], - "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], "tw-to-css/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], @@ -5020,6 +5236,14 @@ "@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], + "@astrojs/check/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@astrojs/check/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "@astrojs/check/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@astrojs/check/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], @@ -5104,12 +5328,18 @@ "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + "@opencode-ai/desktop/@actions/artifact/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + "@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="], "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "archiver-utils/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "archiver-utils/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "astro/unstorage/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], @@ -5118,12 +5348,22 @@ "astro/unstorage/h3/crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], + "babel-plugin-module-resolver/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "babel-plugin-module-resolver/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "babel-plugin-module-resolver/glob/path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "editorconfig/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "esbuild-plugin-copy/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "gray-matter/js-yaml/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "js-beautify/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "js-beautify/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], @@ -5152,12 +5392,22 @@ "pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + "readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "rimraf/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "tw-to-css/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "tw-to-css/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "@astrojs/check/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@astrojs/check/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="], "@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -5166,12 +5416,44 @@ "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="], + "archiver-utils/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "archiver-utils/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "archiver-utils/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "babel-plugin-module-resolver/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "js-beautify/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "js-beautify/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "js-beautify/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "opencontrol/@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "opencontrol/@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + "rimraf/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "rimraf/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "tw-to-css/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "archiver-utils/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "archiver-utils/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "js-beautify/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "js-beautify/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "rimraf/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "rimraf/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], } } diff --git a/flake.lock b/flake.lock index 16fb71c0a5a1..9efa1883b181 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1768393167, - "narHash": "sha256-n2063BRjHde6DqAz2zavhOOiLUwA3qXt7jQYHyETjX8=", + "lastModified": 1770812194, + "narHash": "sha256-OH+lkaIKAvPXR3nITO7iYZwew2nW9Y7Xxq0yfM/UcUU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "2f594d5af95d4fdac67fba60376ec11e482041cb", + "rev": "8482c7ded03bae7550f3d69884f1e611e3bd19e8", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index ea78b1a43482..40e9d337f58b 100644 --- a/flake.nix +++ b/flake.nix @@ -30,6 +30,26 @@ }; }); + overlays = { + default = + final: _prev: + let + node_modules = final.callPackage ./nix/node_modules.nix { + inherit rev; + }; + opencode = final.callPackage ./nix/opencode.nix { + inherit node_modules; + }; + desktop = final.callPackage ./nix/desktop.nix { + inherit opencode; + }; + in + { + inherit opencode; + opencode-desktop = desktop; + }; + }; + packages = forEachSystem ( pkgs: let diff --git a/github/index.ts b/github/index.ts index 73378894cd34..da310178a7dc 100644 --- a/github/index.ts +++ b/github/index.ts @@ -275,7 +275,7 @@ async function assertOpencodeConnected() { body: { service: "github-workflow", level: "info", - message: "Prepare to react to Github Workflow event", + message: "Prepare to react to GitHub Workflow event", }, }) connected = true diff --git a/infra/console.ts b/infra/console.ts index ba1ff15bf2d1..9089055821c4 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -135,6 +135,26 @@ const ZEN_MODELS = [ new sst.Secret("ZEN_MODELS8"), new sst.Secret("ZEN_MODELS9"), new sst.Secret("ZEN_MODELS10"), + new sst.Secret("ZEN_MODELS11"), + new sst.Secret("ZEN_MODELS12"), + new sst.Secret("ZEN_MODELS13"), + new sst.Secret("ZEN_MODELS14"), + new sst.Secret("ZEN_MODELS15"), + new sst.Secret("ZEN_MODELS16"), + new sst.Secret("ZEN_MODELS17"), + new sst.Secret("ZEN_MODELS18"), + new sst.Secret("ZEN_MODELS19"), + new sst.Secret("ZEN_MODELS20"), + new sst.Secret("ZEN_MODELS21"), + new sst.Secret("ZEN_MODELS22"), + new sst.Secret("ZEN_MODELS23"), + new sst.Secret("ZEN_MODELS24"), + new sst.Secret("ZEN_MODELS25"), + new sst.Secret("ZEN_MODELS26"), + new sst.Secret("ZEN_MODELS27"), + new sst.Secret("ZEN_MODELS28"), + new sst.Secret("ZEN_MODELS29"), + new sst.Secret("ZEN_MODELS30"), ] const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY") const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY") @@ -156,14 +176,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, @@ -201,7 +217,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 }], }, }, }, diff --git a/install b/install index 22b7ca39ed79..b0716d532082 100755 --- a/install +++ b/install @@ -130,7 +130,7 @@ else needs_baseline=false if [ "$arch" = "x64" ]; then if [ "$os" = "linux" ]; then - if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then + if ! grep -qwi avx2 /proc/cpuinfo 2>/dev/null; then needs_baseline=true fi fi @@ -141,6 +141,20 @@ else needs_baseline=true fi fi + + if [ "$os" = "windows" ]; then + ps="(Add-Type -MemberDefinition \"[DllImport(\"\"kernel32.dll\"\")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);\" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)" + out="" + if command -v powershell.exe >/dev/null 2>&1; then + out=$(powershell.exe -NoProfile -NonInteractive -Command "$ps" 2>/dev/null || true) + elif command -v pwsh >/dev/null 2>&1; then + out=$(pwsh -NoProfile -NonInteractive -Command "$ps" 2>/dev/null || true) + fi + out=$(echo "$out" | tr -d '\r' | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]') + if [ "$out" != "true" ] && [ "$out" != "1" ]; then + needs_baseline=true + fi + fi fi target="$os-$arch" diff --git a/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json b/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json deleted file mode 100644 index 41cb01a2b838..000000000000 --- a/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "keep": { - "days": true, - "amount": 14 - }, - "auditLog": "/home/thdxr/dev/projects/sst/opencode/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json", - "files": [ - { - "date": 1759827172859, - "name": "/home/thdxr/dev/projects/sst/opencode/logs/mcp-puppeteer-2025-10-07.log", - "hash": "a3d98b26edd793411b968a0d24cfeee8332138e282023c3b83ec169d55c67f16" - } - ], - "hashType": "sha256" -} diff --git a/logs/mcp-puppeteer-2025-10-07.log b/logs/mcp-puppeteer-2025-10-07.log deleted file mode 100644 index 770535696660..000000000000 --- a/logs/mcp-puppeteer-2025-10-07.log +++ /dev/null @@ -1,48 +0,0 @@ -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.879"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.880"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.191"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.192"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.267"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.268"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.276"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.277"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.838"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.839"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.499"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.500"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"} -{"arguments":{"url":"https://google.com"},"level":"debug","message":"Tool call received","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150","tool":"puppeteer_navigate"} -{"0":"n","1":"p","2":"x","level":"info","message":"Launching browser with config:","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.488"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.489"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.815"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.816"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.934"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.935"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.154"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.155"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.426"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.427"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.715"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.716"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.063"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.064"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.567"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.568"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.937"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.938"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:37.120"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:37.121"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:52.490"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:52.491"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:39:25.524"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:39:25.525"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:40:57.126"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:40:57.127"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:42:24.175"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:42:24.176"} diff --git a/nix/hashes.json b/nix/hashes.json index 0bb59650f6e1..3fa1455fc019 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-FMrW0aXYOgRe3ginr4l1LwCszsD/r5CQkvRU6HHA7iw=", - "aarch64-linux": "sha256-NZTtIsFZshWOp5mVFvrcVeHUlx62QcsSJKPYjwPhmYk=", - "aarch64-darwin": "sha256-6cWt8KaqojTJ/b3WSYb3dDPTNuKBDt9Fxx6p/WGBnik=", - "x86_64-darwin": "sha256-F6zuxV34RQ9RTjH0c22rGZaPrhemhRUPi+OkF+Y0ytM=" + "x86_64-linux": "sha256-C3WIEER2XgzO85wk2sp3BzQ6dknW026zslD8nKZjo2U=", + "aarch64-linux": "sha256-+tTJHZMZ/+8fAjI/1fUTuca8J2MZfB+5vhBoZ7jgqcE=", + "aarch64-darwin": "sha256-vS82puFGBBToxyIBa8Zi0KLKdJYr64T6HZL2rL32mH8=", + "x86_64-darwin": "sha256-Tr8JMTCxV6WVt3dXV7iq3PNCm2Cn+RXAbU9+o7pKKV0=" } } diff --git a/nix/node_modules.nix b/nix/node_modules.nix index 836ef02a56e5..e918846c2449 100644 --- a/nix/node_modules.nix +++ b/nix/node_modules.nix @@ -30,7 +30,7 @@ stdenvNoCC.mkDerivation { ../bun.lock ../package.json ../patches - ../install + ../install # required by desktop build (cli.rs include_str!) ] ); }; diff --git a/nix/opencode.nix b/nix/opencode.nix index 23d9fbe34e04..b7d6f95947c1 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -34,6 +34,7 @@ stdenvNoCC.mkDerivation (finalAttrs: { ''; env.MODELS_DEV_API_JSON = "${models-dev}/dist/_api.json"; + env.OPENCODE_DISABLE_MODELS_FETCH = true; env.OPENCODE_VERSION = finalAttrs.version; env.OPENCODE_CHANNEL = "local"; @@ -79,7 +80,7 @@ stdenvNoCC.mkDerivation (finalAttrs: { writableTmpDirAsHomeHook ]; doInstallCheck = true; - versionCheckKeepEnvironment = [ "HOME" ]; + versionCheckKeepEnvironment = [ "HOME" "OPENCODE_DISABLE_MODELS_FETCH" ]; versionCheckProgramArg = "--version"; passthru = { diff --git a/nix/scripts/canonicalize-node-modules.ts b/nix/scripts/canonicalize-node-modules.ts index faa6f63402e2..7997a3cd2325 100644 --- a/nix/scripts/canonicalize-node-modules.ts +++ b/nix/scripts/canonicalize-node-modules.ts @@ -1,27 +1,32 @@ import { lstat, mkdir, readdir, rm, symlink } from "fs/promises" import { join, relative } from "path" -type SemverLike = { - valid: (value: string) => string | null - rcompare: (left: string, right: string) => number -} - type Entry = { dir: string version: string - label: string } +async function isDirectory(path: string) { + try { + const info = await lstat(path) + return info.isDirectory() + } catch { + return false + } +} + +const isValidSemver = (v: string) => Bun.semver.satisfies(v, "x.x.x") + const root = process.cwd() const bunRoot = join(root, "node_modules/.bun") const linkRoot = join(bunRoot, "node_modules") const directories = (await readdir(bunRoot)).sort() + const versions = new Map() for (const entry of directories) { const full = join(bunRoot, entry) - const info = await lstat(full) - if (!info.isDirectory()) { + if (!(await isDirectory(full))) { continue } const parsed = parseEntry(entry) @@ -29,37 +34,23 @@ for (const entry of directories) { continue } const list = versions.get(parsed.name) ?? [] - list.push({ dir: full, version: parsed.version, label: entry }) + list.push({ dir: full, version: parsed.version }) versions.set(parsed.name, list) } -const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as - | SemverLike - | { - default: SemverLike - } -const semver = "default" in semverModule ? semverModule.default : semverModule const selections = new Map() for (const [slug, list] of versions) { list.sort((a, b) => { - const left = semver.valid(a.version) - const right = semver.valid(b.version) - if (left && right) { - const delta = semver.rcompare(left, right) - if (delta !== 0) { - return delta - } - } - if (left && !right) { - return -1 - } - if (!left && right) { - return 1 - } + const aValid = isValidSemver(a.version) + const bValid = isValidSemver(b.version) + if (aValid && bValid) return -Bun.semver.order(a.version, b.version) + if (aValid) return -1 + if (bValid) return 1 return b.version.localeCompare(a.version) }) - selections.set(slug, list[0]) + const first = list[0] + if (first) selections.set(slug, first) } await rm(linkRoot, { recursive: true, force: true }) @@ -77,10 +68,7 @@ for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0] await mkdir(parent, { recursive: true }) const linkPath = join(parent, leaf) const desired = join(entry.dir, "node_modules", slug) - const exists = await lstat(desired) - .then((info) => info.isDirectory()) - .catch(() => false) - if (!exists) { + if (!(await isDirectory(desired))) { continue } const relativeTarget = relative(parent, desired) diff --git a/nix/scripts/normalize-bun-binaries.ts b/nix/scripts/normalize-bun-binaries.ts index 531d8fd0567a..978ab325b7bb 100644 --- a/nix/scripts/normalize-bun-binaries.ts +++ b/nix/scripts/normalize-bun-binaries.ts @@ -8,7 +8,7 @@ type PackageManifest = { const root = process.cwd() const bunRoot = join(root, "node_modules/.bun") -const bunEntries = (await safeReadDir(bunRoot)).sort() +const bunEntries = (await readdir(bunRoot)).sort() let rewritten = 0 for (const entry of bunEntries) { @@ -45,11 +45,11 @@ for (const entry of bunEntries) { } } -console.log(`[normalize-bun-binaries] rewrote ${rewritten} links`) +console.log(`[normalize-bun-binaries] rebuilt ${rewritten} links`) async function collectPackages(modulesRoot: string) { const found: string[] = [] - const topLevel = (await safeReadDir(modulesRoot)).sort() + const topLevel = (await readdir(modulesRoot)).sort() for (const name of topLevel) { if (name === ".bin" || name === ".bun") { continue @@ -59,7 +59,7 @@ async function collectPackages(modulesRoot: string) { continue } if (name.startsWith("@")) { - const scoped = (await safeReadDir(full)).sort() + const scoped = (await readdir(full)).sort() for (const child of scoped) { const scopedDir = join(full, child) if (await isDirectory(scopedDir)) { @@ -121,14 +121,6 @@ async function isDirectory(path: string) { } } -async function safeReadDir(path: string) { - try { - return await readdir(path) - } catch { - return [] - } -} - function normalizeBinName(name: string) { const slash = name.lastIndexOf("/") if (slash >= 0) { diff --git a/package.json b/package.json index 89e665ec8f25..708ad5105519 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "AI-powered development tool", "private": true, "type": "module", - "packageManager": "bun@1.3.5", + "packageManager": "bun@1.3.9", "scripts": { "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "dev:desktop": "bun --cwd packages/desktop tauri dev", @@ -23,7 +23,7 @@ "packages/slack" ], "catalog": { - "@types/bun": "1.3.5", + "@types/bun": "1.3.9", "@octokit/rest": "22.0.0", "@hono/zod-validator": "0.4.2", "ulid": "3.0.1", @@ -35,11 +35,13 @@ "@tsconfig/bun": "1.0.9", "@cloudflare/workers-types": "4.20251008.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@pierre/diffs": "1.0.2", + "@pierre/diffs": "1.1.0-beta.13", "@solid-primitives/storage": "4.3.3", "@tailwindcss/vite": "4.1.11", "diff": "8.0.2", "dompurify": "3.3.1", + "drizzle-kit": "1.0.0-beta.12-a5629fb", + "drizzle-orm": "1.0.0-beta.12-a5629fb", "ai": "5.0.124", "hono": "4.10.7", "hono-openapi": "1.1.2", @@ -101,5 +103,8 @@ "@types/bun": "catalog:", "@types/node": "catalog:" }, - "patchedDependencies": {} + "patchedDependencies": { + "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", + "@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch" + } } diff --git a/packages/app/bunfig.toml b/packages/app/bunfig.toml index 363990451190..f1caabbcce9f 100644 --- a/packages/app/bunfig.toml +++ b/packages/app/bunfig.toml @@ -1,2 +1,3 @@ [test] +root = "./src" preload = ["./happydom.ts"] diff --git a/packages/app/e2e/app/titlebar-history.spec.ts b/packages/app/e2e/app/titlebar-history.spec.ts index ec65dca0b35e..9d6091176eca 100644 --- a/packages/app/e2e/app/titlebar-history.spec.ts +++ b/packages/app/e2e/app/titlebar-history.spec.ts @@ -1,6 +1,7 @@ import { test, expect } from "../fixtures" -import { openSidebar, withSession } from "../actions" +import { defocus, openSidebar, withSession } from "../actions" import { promptSelector } from "../selectors" +import { modKey } from "../utils" test("titlebar back/forward navigates between sessions", async ({ page, slug, sdk, gotoSession }) => { await page.setViewportSize({ width: 1400, height: 800 }) @@ -40,3 +41,84 @@ test("titlebar back/forward navigates between sessions", async ({ page, slug, sd }) }) }) + +test("titlebar forward is cleared after branching history from sidebar", async ({ page, slug, sdk, gotoSession }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + const stamp = Date.now() + + await withSession(sdk, `e2e titlebar history a ${stamp}`, async (a) => { + await withSession(sdk, `e2e titlebar history b ${stamp}`, async (b) => { + await withSession(sdk, `e2e titlebar history c ${stamp}`, async (c) => { + await gotoSession(a.id) + + await openSidebar(page) + + const second = page.locator(`[data-session-id="${b.id}"] a`).first() + await expect(second).toBeVisible() + await second.scrollIntoViewIfNeeded() + await second.click() + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${b.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + + const back = page.getByRole("button", { name: "Back" }) + const forward = page.getByRole("button", { name: "Forward" }) + + await expect(back).toBeVisible() + await expect(back).toBeEnabled() + await back.click() + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${a.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + + await openSidebar(page) + + const third = page.locator(`[data-session-id="${c.id}"] a`).first() + await expect(third).toBeVisible() + await third.scrollIntoViewIfNeeded() + await third.click() + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${c.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + + await expect(forward).toBeVisible() + await expect(forward).toBeDisabled() + }) + }) + }) +}) + +test("keyboard shortcuts navigate titlebar history", async ({ page, slug, sdk, gotoSession }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + const stamp = Date.now() + + await withSession(sdk, `e2e titlebar shortcuts 1 ${stamp}`, async (one) => { + await withSession(sdk, `e2e titlebar shortcuts 2 ${stamp}`, async (two) => { + await gotoSession(one.id) + + await openSidebar(page) + + const link = page.locator(`[data-session-id="${two.id}"] a`).first() + await expect(link).toBeVisible() + await link.scrollIntoViewIfNeeded() + await link.click() + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + + await defocus(page) + await page.keyboard.press(`${modKey}+[`) + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${one.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + + await defocus(page) + await page.keyboard.press(`${modKey}+]`) + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + }) + }) +}) diff --git a/packages/app/e2e/files/file-open.spec.ts b/packages/app/e2e/files/file-open.spec.ts index 3c636d748a79..abb28242da57 100644 --- a/packages/app/e2e/files/file-open.spec.ts +++ b/packages/app/e2e/files/file-open.spec.ts @@ -1,15 +1,28 @@ import { test, expect } from "../fixtures" -import { openPalette, clickListItem } from "../actions" +import { promptSelector } from "../selectors" test("can open a file tab from the search palette", async ({ page, gotoSession }) => { await gotoSession() - const dialog = await openPalette(page) + await page.locator(promptSelector).click() + await page.keyboard.type("/open") + + const command = page.locator('[data-slash-id="file.open"]').first() + await expect(command).toBeVisible() + await page.keyboard.press("Enter") + + const dialog = page + .getByRole("dialog") + .filter({ has: page.getByPlaceholder(/search files/i) }) + .first() + await expect(dialog).toBeVisible() const input = dialog.getByRole("textbox").first() await input.fill("package.json") - await clickListItem(dialog, { keyStartsWith: "file:" }) + const item = dialog.locator('[data-slot="list-item"][data-key^="file:"]').first() + await expect(item).toBeVisible({ timeout: 30_000 }) + await item.click() await expect(dialog).toHaveCount(0) diff --git a/packages/app/e2e/files/file-tree.spec.ts b/packages/app/e2e/files/file-tree.spec.ts index 844da1b329b9..321d96af57af 100644 --- a/packages/app/e2e/files/file-tree.spec.ts +++ b/packages/app/e2e/files/file-tree.spec.ts @@ -1,37 +1,49 @@ import { test, expect } from "../fixtures" -test.skip("file tree can expand folders and open a file", async ({ page, gotoSession }) => { +test("file tree can expand folders and open a file", async ({ page, gotoSession }) => { await gotoSession() const toggle = page.getByRole("button", { name: "Toggle file tree" }) - const treeTabs = page.locator('[data-component="tabs"][data-variant="pill"][data-scope="filetree"]') + const panel = page.locator("#file-tree-panel") + const treeTabs = panel.locator('[data-component="tabs"][data-variant="pill"][data-scope="filetree"]') + await expect(toggle).toBeVisible() if ((await toggle.getAttribute("aria-expanded")) !== "true") await toggle.click() + await expect(toggle).toHaveAttribute("aria-expanded", "true") + await expect(panel).toBeVisible() await expect(treeTabs).toBeVisible() - await treeTabs.locator('[data-slot="tabs-trigger"]').nth(1).click() + const allTab = treeTabs.getByRole("tab", { name: /^all files$/i }) + await expect(allTab).toBeVisible() + await allTab.click() + await expect(allTab).toHaveAttribute("aria-selected", "true") - const node = (name: string) => treeTabs.getByRole("button", { name, exact: true }) + const tree = treeTabs.locator('[data-slot="tabs-content"]:not([hidden])') + await expect(tree).toBeVisible() - await expect(node("packages")).toBeVisible() - await node("packages").click() + const expand = async (name: string) => { + const folder = tree.getByRole("button", { name, exact: true }).first() + await expect(folder).toBeVisible() + await expect(folder).toHaveAttribute("aria-expanded", /true|false/) + if ((await folder.getAttribute("aria-expanded")) === "false") await folder.click() + await expect(folder).toHaveAttribute("aria-expanded", "true") + } - await expect(node("app")).toBeVisible() - await node("app").click() + await expand("packages") + await expand("app") + await expand("src") + await expand("components") - await expect(node("src")).toBeVisible() - await node("src").click() - - await expect(node("components")).toBeVisible() - await node("components").click() - - await expect(node("file-tree.tsx")).toBeVisible() - await node("file-tree.tsx").click() + const file = tree.getByRole("button", { name: "file-tree.tsx", exact: true }).first() + await expect(file).toBeVisible() + await file.click() const tab = page.getByRole("tab", { name: "file-tree.tsx" }) await expect(tab).toBeVisible() await tab.click() + await expect(tab).toHaveAttribute("aria-selected", "true") const code = page.locator('[data-component="code"]').first() - await expect(code.getByText("export default function FileTree")).toBeVisible() + await expect(code).toBeVisible() + await expect(code).toContainText("export default function FileTree") }) diff --git a/packages/app/e2e/files/file-viewer.spec.ts b/packages/app/e2e/files/file-viewer.spec.ts index 52838449759f..b968acc130e6 100644 --- a/packages/app/e2e/files/file-viewer.spec.ts +++ b/packages/app/e2e/files/file-viewer.spec.ts @@ -1,18 +1,41 @@ import { test, expect } from "../fixtures" -import { openPalette, clickListItem } from "../actions" +import { promptSelector } from "../selectors" test("smoke file viewer renders real file content", async ({ page, gotoSession }) => { await gotoSession() - const sep = process.platform === "win32" ? "\\" : "/" - const file = ["packages", "app", "package.json"].join(sep) + await page.locator(promptSelector).click() + await page.keyboard.type("/open") - const dialog = await openPalette(page) + const command = page.locator('[data-slash-id="file.open"]').first() + await expect(command).toBeVisible() + await page.keyboard.press("Enter") - const input = dialog.getByRole("textbox").first() - await input.fill(file) + const dialog = page + .getByRole("dialog") + .filter({ has: page.getByPlaceholder(/search files/i) }) + .first() + await expect(dialog).toBeVisible() - await clickListItem(dialog, { text: /packages.*app.*package.json/ }) + const input = dialog.getByRole("textbox").first() + await input.fill("package.json") + + const items = dialog.locator('[data-slot="list-item"][data-key^="file:"]') + let index = -1 + await expect + .poll( + async () => { + const keys = await items.evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-key") ?? "")) + index = keys.findIndex((key) => /packages[\\/]+app[\\/]+package\.json$/i.test(key.replace(/^file:/, ""))) + return index >= 0 + }, + { timeout: 30_000 }, + ) + .toBe(true) + + const item = items.nth(index) + await expect(item).toBeVisible() + await item.click() await expect(dialog).toHaveCount(0) @@ -22,5 +45,5 @@ test("smoke file viewer renders real file content", async ({ page, gotoSession } const code = page.locator('[data-component="code"]').first() await expect(code).toBeVisible() - await expect(code.getByText("@opencode-ai/app")).toBeVisible() + await expect(code.getByText(/"name"\s*:\s*"@opencode-ai\/app"/)).toBeVisible() }) diff --git a/packages/app/e2e/projects/projects-close.spec.ts b/packages/app/e2e/projects/projects-close.spec.ts index 95768d21e9e8..4b39ed82c372 100644 --- a/packages/app/e2e/projects/projects-close.spec.ts +++ b/packages/app/e2e/projects/projects-close.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from "../fixtures" -import { createTestProject, cleanupTestProject, openSidebar, clickMenuItem } from "../actions" -import { projectCloseHoverSelector, projectCloseMenuSelector, projectSwitchSelector } from "../selectors" +import { createTestProject, cleanupTestProject, openSidebar, clickMenuItem, openProjectMenu } from "../actions" +import { projectCloseHoverSelector, projectSwitchSelector } from "../selectors" import { dirSlug } from "../utils" test("can close a project via hover card close button", async ({ page, withProject }) => { @@ -31,16 +31,15 @@ test("can close a project via hover card close button", async ({ page, withProje } }) -test("can close a project via project header more options menu", async ({ page, withProject }) => { +test("closing active project navigates to another open project", async ({ page, withProject }) => { await page.setViewportSize({ width: 1400, height: 800 }) const other = await createTestProject() - const otherName = other.split("/").pop() ?? other const otherSlug = dirSlug(other) try { await withProject( - async () => { + async ({ slug }) => { await openSidebar(page) const otherButton = page.locator(projectSwitchSelector(otherSlug)).first() @@ -49,21 +48,20 @@ test("can close a project via project header more options menu", async ({ page, await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`)) - const header = page - .locator(".group\\/project") - .filter({ has: page.locator(`[data-action="project-menu"][data-project="${otherSlug}"]`) }) - .first() - await expect(header).toContainText(otherName) + const menu = await openProjectMenu(page, otherSlug) - const trigger = header.locator(`[data-action="project-menu"][data-project="${otherSlug}"]`).first() - await expect(trigger).toHaveCount(1) - await trigger.focus() - await page.keyboard.press("Enter") + await clickMenuItem(menu, /^Close$/i, { force: true }) - const menu = page.locator('[data-component="dropdown-menu-content"]').first() - await expect(menu).toBeVisible({ timeout: 10_000 }) + await expect + .poll(() => { + const pathname = new URL(page.url()).pathname + if (new RegExp(`^/${slug}/session(?:/[^/]+)?/?$`).test(pathname)) return "project" + if (pathname === "/") return "home" + return "" + }) + .toMatch(/^(project|home)$/) - await clickMenuItem(menu, /^Close$/i, { force: true }) + await expect(page).not.toHaveURL(new RegExp(`/${otherSlug}/session(?:[/?#]|$)`)) await expect(otherButton).toHaveCount(0) }, { extra: [other] }, diff --git a/packages/app/e2e/projects/workspace-new-session.spec.ts b/packages/app/e2e/projects/workspace-new-session.spec.ts new file mode 100644 index 000000000000..f33972cc3a31 --- /dev/null +++ b/packages/app/e2e/projects/workspace-new-session.spec.ts @@ -0,0 +1,144 @@ +import { base64Decode } from "@opencode-ai/util/encode" +import type { Page } from "@playwright/test" +import { test, expect } from "../fixtures" +import { cleanupTestProject, openSidebar, sessionIDFromUrl, setWorkspacesEnabled } from "../actions" +import { promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors" +import { createSdk } from "../utils" + +function slugFromUrl(url: string) { + return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? "" +} + +async function waitWorkspaceReady(page: Page, slug: string) { + await openSidebar(page) + await expect + .poll( + async () => { + const item = page.locator(workspaceItemSelector(slug)).first() + try { + await item.hover({ timeout: 500 }) + return true + } catch { + return false + } + }, + { timeout: 60_000 }, + ) + .toBe(true) +} + +async function createWorkspace(page: Page, root: string, seen: string[]) { + await openSidebar(page) + await page.getByRole("button", { name: "New workspace" }).first().click() + + await expect + .poll( + () => { + const slug = slugFromUrl(page.url()) + if (!slug) return "" + if (slug === root) return "" + if (seen.includes(slug)) return "" + return slug + }, + { timeout: 45_000 }, + ) + .not.toBe("") + + const slug = slugFromUrl(page.url()) + const directory = base64Decode(slug) + if (!directory) throw new Error(`Failed to decode workspace slug: ${slug}`) + return { slug, directory } +} + +async function openWorkspaceNewSession(page: Page, slug: string) { + await waitWorkspaceReady(page, slug) + + const item = page.locator(workspaceItemSelector(slug)).first() + await item.hover() + + const button = page.locator(workspaceNewSessionSelector(slug)).first() + await expect(button).toBeVisible() + await button.click({ force: true }) + + await expect.poll(() => slugFromUrl(page.url())).toBe(slug) + await expect(page).toHaveURL(new RegExp(`/${slug}/session(?:[/?#]|$)`)) +} + +async function createSessionFromWorkspace(page: Page, slug: string, text: string) { + await openWorkspaceNewSession(page, slug) + + const prompt = page.locator(promptSelector) + await expect(prompt).toBeVisible() + await expect(prompt).toBeEditable() + await prompt.click() + await expect(prompt).toBeFocused() + await prompt.fill(text) + await expect.poll(async () => ((await prompt.textContent()) ?? "").trim()).toContain(text) + await prompt.press("Enter") + + await expect.poll(() => slugFromUrl(page.url())).toBe(slug) + await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 30_000 }).not.toBe("") + + const sessionID = sessionIDFromUrl(page.url()) + if (!sessionID) throw new Error(`Failed to parse session id from url: ${page.url()}`) + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${sessionID}(?:[/?#]|$)`)) + return sessionID +} + +async function sessionDirectory(directory: string, sessionID: string) { + const info = await createSdk(directory) + .session.get({ sessionID }) + .then((x) => x.data) + .catch(() => undefined) + if (!info) return "" + return info.directory +} + +test("new sessions from sidebar workspace actions stay in selected workspace", async ({ page, withProject }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + await withProject(async ({ directory, slug: root }) => { + const workspaces = [] as { slug: string; directory: string }[] + const sessions = [] as string[] + + try { + await openSidebar(page) + await setWorkspacesEnabled(page, root, true) + + const first = await createWorkspace(page, root, []) + workspaces.push(first) + await waitWorkspaceReady(page, first.slug) + + const second = await createWorkspace(page, root, [first.slug]) + workspaces.push(second) + await waitWorkspaceReady(page, second.slug) + + const firstSession = await createSessionFromWorkspace(page, first.slug, `workspace one ${Date.now()}`) + sessions.push(firstSession) + + const secondSession = await createSessionFromWorkspace(page, second.slug, `workspace two ${Date.now()}`) + sessions.push(secondSession) + + const thirdSession = await createSessionFromWorkspace(page, first.slug, `workspace one again ${Date.now()}`) + sessions.push(thirdSession) + + await expect.poll(() => sessionDirectory(first.directory, firstSession)).toBe(first.directory) + await expect.poll(() => sessionDirectory(second.directory, secondSession)).toBe(second.directory) + await expect.poll(() => sessionDirectory(first.directory, thirdSession)).toBe(first.directory) + } finally { + const dirs = [directory, ...workspaces.map((workspace) => workspace.directory)] + await Promise.all( + sessions.map((sessionID) => + Promise.all( + dirs.map((dir) => + createSdk(dir) + .session.delete({ sessionID }) + .catch(() => undefined), + ), + ), + ), + ) + await Promise.all(workspaces.map((workspace) => cleanupTestProject(workspace.directory))) + } + }) +}) diff --git a/packages/app/e2e/projects/workspaces.spec.ts b/packages/app/e2e/projects/workspaces.spec.ts index 41a28e3e380f..3867395267b5 100644 --- a/packages/app/e2e/projects/workspaces.spec.ts +++ b/packages/app/e2e/projects/workspaces.spec.ts @@ -1,5 +1,6 @@ import { base64Decode } from "@opencode-ai/util/encode" import fs from "node:fs/promises" +import os from "node:os" import path from "node:path" import type { Page } from "@playwright/test" @@ -14,7 +15,8 @@ import { openWorkspaceMenu, setWorkspacesEnabled, } from "../actions" -import { inlineInputSelector, workspaceItemSelector } from "../selectors" +import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors" +import { createSdk, dirSlug } from "../utils" function slugFromUrl(url: string) { return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? "" @@ -126,6 +128,49 @@ test("can create a workspace", async ({ page, withProject }) => { }) }) +test("non-git projects keep workspace mode disabled", async ({ page, withProject }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + const nonGit = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-project-nongit-")) + const nonGitSlug = dirSlug(nonGit) + + await fs.writeFile(path.join(nonGit, "README.md"), "# e2e nongit\n") + + try { + await withProject(async () => { + await page.goto(`/${nonGitSlug}/session`) + + await expect.poll(() => slugFromUrl(page.url()), { timeout: 30_000 }).not.toBe("") + + const activeDir = base64Decode(slugFromUrl(page.url())) + expect(path.basename(activeDir)).toContain("opencode-e2e-project-nongit-") + + await openSidebar(page) + await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0) + + const trigger = page.locator('[data-action="project-menu"]').first() + const hasMenu = await trigger + .isVisible() + .then((x) => x) + .catch(() => false) + if (!hasMenu) return + + await trigger.click({ force: true }) + + const menu = page.locator(dropdownMenuContentSelector).first() + await expect(menu).toBeVisible() + + const toggle = menu.locator('[data-action="project-workspaces-toggle"]').first() + + await expect(toggle).toBeVisible() + await expect(toggle).toBeDisabled() + await expect(menu.getByRole("menuitem", { name: "New workspace" })).toHaveCount(0) + }) + } finally { + await cleanupTestProject(nonGit) + } +}) + test("can rename a workspace", async ({ page, withProject }) => { await page.setViewportSize({ width: 1400, height: 800 }) @@ -214,14 +259,45 @@ test("can delete a workspace", async ({ page, withProject }) => { await page.setViewportSize({ width: 1400, height: 800 }) await withProject(async (project) => { - const { rootSlug, slug } = await setupWorkspaceTest(page, project) + const sdk = createSdk(project.directory) + const { rootSlug, slug, directory } = await setupWorkspaceTest(page, project) + + await expect + .poll( + async () => { + const worktrees = await sdk.worktree + .list() + .then((r) => r.data ?? []) + .catch(() => [] as string[]) + return worktrees.includes(directory) + }, + { timeout: 30_000 }, + ) + .toBe(true) const menu = await openWorkspaceMenu(page, slug) await clickMenuItem(menu, /^Delete$/i, { force: true }) await confirmDialog(page, /^Delete workspace$/i) await expect(page).toHaveURL(new RegExp(`/${rootSlug}/session`)) - await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0) + + await expect + .poll( + async () => { + const worktrees = await sdk.worktree + .list() + .then((r) => r.data ?? []) + .catch(() => [] as string[]) + return worktrees.includes(directory) + }, + { timeout: 60_000 }, + ) + .toBe(false) + + await project.gotoSession() + + await openSidebar(page) + await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0, { timeout: 60_000 }) await expect(page.locator(workspaceItemSelector(rootSlug)).first()).toBeVisible() }) }) diff --git a/packages/app/e2e/prompt/context.spec.ts b/packages/app/e2e/prompt/context.spec.ts index 80aa9ea334d1..366191fd70d5 100644 --- a/packages/app/e2e/prompt/context.spec.ts +++ b/packages/app/e2e/prompt/context.spec.ts @@ -1,40 +1,95 @@ import { test, expect } from "../fixtures" +import type { Page } from "@playwright/test" import { promptSelector } from "../selectors" import { withSession } from "../actions" +function contextButton(page: Page) { + return page + .locator('[data-component="button"]') + .filter({ has: page.locator('[data-component="progress-circle"]').first() }) + .first() +} + +async function seedContextSession(input: { sessionID: string; sdk: Parameters[0] }) { + await input.sdk.session.promptAsync({ + sessionID: input.sessionID, + noReply: true, + parts: [ + { + type: "text", + text: "seed context", + }, + ], + }) + + await expect + .poll(async () => { + const messages = await input.sdk.session + .messages({ sessionID: input.sessionID, limit: 1 }) + .then((r) => r.data ?? []) + return messages.length + }) + .toBeGreaterThan(0) +} + test("context panel can be opened from the prompt", async ({ page, sdk, gotoSession }) => { const title = `e2e smoke context ${Date.now()}` await withSession(sdk, title, async (session) => { - await sdk.session.promptAsync({ - sessionID: session.id, - noReply: true, - parts: [ - { - type: "text", - text: "seed context", - }, - ], - }) + await seedContextSession({ sessionID: session.id, sdk }) - await expect - .poll(async () => { - const messages = await sdk.session.messages({ sessionID: session.id, limit: 1 }).then((r) => r.data ?? []) - return messages.length - }) - .toBeGreaterThan(0) + await gotoSession(session.id) + + const trigger = contextButton(page) + await expect(trigger).toBeVisible() + await trigger.click() + + const tabs = page.locator('[data-component="tabs"][data-variant="normal"]') + await expect(tabs.getByRole("tab", { name: "Context" })).toBeVisible() + }) +}) +test("context panel can be closed from the context tab close action", async ({ page, sdk, gotoSession }) => { + await withSession(sdk, `e2e context toggle ${Date.now()}`, async (session) => { + await seedContextSession({ sessionID: session.id, sdk }) await gotoSession(session.id) - const contextButton = page - .locator('[data-component="button"]') - .filter({ has: page.locator('[data-component="progress-circle"]').first() }) - .first() + await page.locator(promptSelector).click() - await expect(contextButton).toBeVisible() - await contextButton.click() + const trigger = contextButton(page) + await expect(trigger).toBeVisible() + await trigger.click() const tabs = page.locator('[data-component="tabs"][data-variant="normal"]') - await expect(tabs.getByRole("tab", { name: "Context" })).toBeVisible() + const context = tabs.getByRole("tab", { name: "Context" }) + await expect(context).toBeVisible() + + await page.getByRole("button", { name: "Close tab" }).first().click() + await expect(context).toHaveCount(0) + }) +}) + +test("context panel can open file picker from context actions", async ({ page, sdk, gotoSession }) => { + await withSession(sdk, `e2e context tabs ${Date.now()}`, async (session) => { + await seedContextSession({ sessionID: session.id, sdk }) + await gotoSession(session.id) + + await page.locator(promptSelector).click() + + const trigger = contextButton(page) + await expect(trigger).toBeVisible() + await trigger.click() + + await expect(page.getByRole("tab", { name: "Context" })).toBeVisible() + await page.getByRole("button", { name: "Open file" }).first().click() + + const dialog = page + .getByRole("dialog") + .filter({ has: page.getByPlaceholder(/search files/i) }) + .first() + await expect(dialog).toBeVisible() + + await page.keyboard.press("Escape") + await expect(dialog).toHaveCount(0) }) }) diff --git a/packages/app/e2e/prompt/prompt-async.spec.ts b/packages/app/e2e/prompt/prompt-async.spec.ts new file mode 100644 index 000000000000..ce9b1a7a3bb1 --- /dev/null +++ b/packages/app/e2e/prompt/prompt-async.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" +import { sessionIDFromUrl } from "../actions" + +// Regression test for Issue #12453: the synchronous POST /message endpoint holds +// the connection open while the agent works, causing "Failed to fetch" over +// VPN/Tailscale. The fix switches to POST /prompt_async which returns immediately. +test("prompt succeeds when sync message endpoint is unreachable", async ({ page, sdk, gotoSession }) => { + test.setTimeout(120_000) + + // Simulate Tailscale/VPN killing the long-lived sync connection + await page.route("**/session/*/message", (route) => route.abort("connectionfailed")) + + await gotoSession() + + const token = `E2E_ASYNC_${Date.now()}` + await page.locator(promptSelector).click() + await page.keyboard.type(`Reply with exactly: ${token}`) + await page.keyboard.press("Enter") + + await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 }) + const sessionID = sessionIDFromUrl(page.url())! + + try { + // Agent response arrives via SSE despite sync endpoint being dead + await expect + .poll( + async () => { + const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? []) + return messages + .filter((m) => m.info.role === "assistant") + .flatMap((m) => m.parts) + .filter((p) => p.type === "text") + .map((p) => p.text) + .join("\n") + }, + { timeout: 90_000 }, + ) + .toContain(token) + } finally { + await sdk.session.delete({ sessionID }).catch(() => undefined) + } +}) diff --git a/packages/app/e2e/prompt/prompt.spec.ts b/packages/app/e2e/prompt/prompt.spec.ts index 07d242c6342b..ff9f5daf0d49 100644 --- a/packages/app/e2e/prompt/prompt.spec.ts +++ b/packages/app/e2e/prompt/prompt.spec.ts @@ -44,9 +44,6 @@ test("can send a prompt and receive a reply", async ({ page, sdk, gotoSession }) ) .toContain(token) - - const reply = page.locator('[data-slot="session-turn-summary-section"]').filter({ hasText: token }).first() - await expect(reply).toBeVisible({ timeout: 90_000 }) } finally { page.off("pageerror", onPageError) await sdk.session.delete({ sessionID }).catch(() => undefined) diff --git a/packages/app/e2e/selectors.ts b/packages/app/e2e/selectors.ts index 317c70969da8..1a0afbab1026 100644 --- a/packages/app/e2e/selectors.ts +++ b/packages/app/e2e/selectors.ts @@ -10,8 +10,11 @@ export const settingsNotificationsAgentSelector = '[data-action="settings-notifi export const settingsNotificationsPermissionsSelector = '[data-action="settings-notifications-permissions"]' export const settingsNotificationsErrorsSelector = '[data-action="settings-notifications-errors"]' export const settingsSoundsAgentSelector = '[data-action="settings-sounds-agent"]' +export const settingsSoundsAgentEnabledSelector = '[data-action="settings-sounds-agent-enabled"]' export const settingsSoundsPermissionsSelector = '[data-action="settings-sounds-permissions"]' +export const settingsSoundsPermissionsEnabledSelector = '[data-action="settings-sounds-permissions-enabled"]' export const settingsSoundsErrorsSelector = '[data-action="settings-sounds-errors"]' +export const settingsSoundsErrorsEnabledSelector = '[data-action="settings-sounds-errors-enabled"]' export const settingsUpdatesStartupSelector = '[data-action="settings-updates-startup"]' export const settingsReleaseNotesSelector = '[data-action="settings-release-notes"]' @@ -27,6 +30,9 @@ export const projectMenuTriggerSelector = (slug: string) => export const projectCloseMenuSelector = (slug: string) => `[data-action="project-close-menu"][data-project="${slug}"]` +export const projectClearNotificationsSelector = (slug: string) => + `[data-action="project-clear-notifications"][data-project="${slug}"]` + export const projectWorkspacesToggleSelector = (slug: string) => `[data-action="project-workspaces-toggle"][data-project="${slug}"]` @@ -48,6 +54,9 @@ export const workspaceItemSelector = (slug: string) => export const workspaceMenuTriggerSelector = (slug: string) => `${sidebarNavSelector} [data-action="workspace-menu"][data-workspace="${slug}"]` +export const workspaceNewSessionSelector = (slug: string) => + `${sidebarNavSelector} [data-action="workspace-new-session"][data-workspace="${slug}"]` + export const listItemSelector = '[data-slot="list-item"]' export const listItemKeyStartsWithSelector = (prefix: string) => `${listItemSelector}[data-key^="${prefix}"]` diff --git a/packages/app/e2e/session/session-undo-redo.spec.ts b/packages/app/e2e/session/session-undo-redo.spec.ts new file mode 100644 index 000000000000..c6ea2aea0aca --- /dev/null +++ b/packages/app/e2e/session/session-undo-redo.spec.ts @@ -0,0 +1,233 @@ +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 + sessionID: string + token: string +}) { + const messages = async () => + await input.sdk.session.messages({ sessionID: input.sessionID, limit: 100 }).then((r) => r.data ?? []) + const seeded = await messages() + const userIDs = new Set(seeded.filter((m) => m.info.role === "user").map((m) => m.info.id)) + + const prompt = input.page.locator(promptSelector) + await expect(prompt).toBeVisible() + await input.sdk.session.promptAsync({ + sessionID: input.sessionID, + noReply: true, + parts: [{ type: "text", text: input.token }], + }) + + let userMessageID: string | undefined + await expect + .poll( + async () => { + const users = (await messages()).filter( + (m) => + !userIDs.has(m.info.id) && + m.info.role === "user" && + m.parts.filter((p) => p.type === "text").some((p) => p.text.includes(input.token)), + ) + if (users.length === 0) return false + + const user = users[users.length - 1] + if (!user) return false + userMessageID = user.info.id + return true + }, + { timeout: 90_000, intervals: [250, 500, 1_000] }, + ) + .toBe(true) + + if (!userMessageID) throw new Error("Expected a user message id") + await expect(input.page.locator(`[data-message-id="${userMessageID}"]`).first()).toBeVisible({ timeout: 30_000 }) + 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() + }) + }) +}) + +test("slash undo/redo traverses multi-step revert stack", async ({ page, withProject }) => { + test.setTimeout(120_000) + + const firstToken = `undo_redo_first_${Date.now()}` + const secondToken = `undo_redo_second_${Date.now()}` + + await withProject(async (project) => { + const sdk = createSdk(project.directory) + + await withSession(sdk, `e2e undo redo stack ${Date.now()}`, async (session) => { + await project.gotoSession(session.id) + + const first = await seedConversation({ + page, + sdk, + sessionID: session.id, + token: firstToken, + }) + const second = await seedConversation({ + page, + sdk, + sessionID: session.id, + token: secondToken, + }) + + expect(first.userMessageID).not.toBe(second.userMessageID) + + const firstMessage = page.locator(`[data-message-id="${first.userMessageID}"]`) + const secondMessage = page.locator(`[data-message-id="${second.userMessageID}"]`) + + await expect(firstMessage.first()).toBeVisible() + await expect(secondMessage.first()).toBeVisible() + + await second.prompt.click() + await page.keyboard.press(`${modKey}+A`) + await page.keyboard.press("Backspace") + 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(second.userMessageID) + + await expect(firstMessage.first()).toBeVisible() + await expect(secondMessage).toHaveCount(0) + + await second.prompt.click() + await page.keyboard.press(`${modKey}+A`) + await page.keyboard.press("Backspace") + await page.keyboard.type("/undo") + 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(first.userMessageID) + + await expect(firstMessage).toHaveCount(0) + await expect(secondMessage).toHaveCount(0) + + await second.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, + }) + .toBe(second.userMessageID) + + await expect(firstMessage.first()).toBeVisible() + await expect(secondMessage).toHaveCount(0) + + await second.prompt.click() + await page.keyboard.press(`${modKey}+A`) + await page.keyboard.press("Backspace") + await page.keyboard.type("/redo") + 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(firstMessage.first()).toBeVisible() + await expect(secondMessage.first()).toBeVisible() + }) + }) +}) diff --git a/packages/app/e2e/session/session.spec.ts b/packages/app/e2e/session/session.spec.ts index 4610fb33152c..93eaee5cb0bf 100644 --- a/packages/app/e2e/session/session.spec.ts +++ b/packages/app/e2e/session/session.spec.ts @@ -34,21 +34,34 @@ async function seedMessage(sdk: Sdk, sessionID: string) { test("session can be renamed via header menu", async ({ page, sdk, gotoSession }) => { const stamp = Date.now() const originalTitle = `e2e rename test ${stamp}` - const newTitle = `e2e renamed ${stamp}` + const renamedTitle = `e2e renamed ${stamp}` await withSession(sdk, originalTitle, async (session) => { await seedMessage(sdk, session.id) await gotoSession(session.id) + await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(originalTitle) const menu = await openSessionMoreMenu(page, session.id) await clickMenuItem(menu, /rename/i) const input = page.locator(".session-scroller").locator(inlineInputSelector).first() await expect(input).toBeVisible() - await input.fill(newTitle) + await expect(input).toBeFocused() + await input.fill(renamedTitle) + await expect(input).toHaveValue(renamedTitle) await input.press("Enter") - await expect(page.getByRole("heading", { level: 1 }).first()).toContainText(newTitle) + await expect + .poll( + async () => { + const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data) + return data?.title + }, + { timeout: 30_000 }, + ) + .toBe(renamedTitle) + + await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(renamedTitle) }) }) @@ -116,8 +129,14 @@ test("session can be shared and unshared via header button", async ({ page, sdk, await seedMessage(sdk, session.id) await gotoSession(session.id) - const { rightSection, popoverBody } = await openSharePopover(page) - await popoverBody.getByRole("button", { name: "Publish" }).first().click() + const shared = await openSharePopover(page) + const publish = shared.popoverBody.getByRole("button", { name: "Publish" }).first() + await expect(publish).toBeVisible({ timeout: 30_000 }) + await publish.click() + + await expect(shared.popoverBody.getByRole("button", { name: "Unpublish" }).first()).toBeVisible({ + timeout: 30_000, + }) await expect .poll( @@ -129,14 +148,14 @@ test("session can be shared and unshared via header button", async ({ page, sdk, ) .not.toBeUndefined() - const copyButton = rightSection.locator('button[aria-label="Copy link"]').first() - await expect(copyButton).toBeVisible({ timeout: 30_000 }) - - const sharedPopover = await openSharePopover(page) - const unpublish = sharedPopover.popoverBody.getByRole("button", { name: "Unpublish" }).first() + const unpublish = shared.popoverBody.getByRole("button", { name: "Unpublish" }).first() await expect(unpublish).toBeVisible({ timeout: 30_000 }) await unpublish.click() + await expect(shared.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({ + timeout: 30_000, + }) + await expect .poll( async () => { @@ -147,10 +166,8 @@ test("session can be shared and unshared via header button", async ({ page, sdk, ) .toBeUndefined() - await expect(copyButton).not.toBeVisible({ timeout: 30_000 }) - - const unsharedPopover = await openSharePopover(page) - await expect(unsharedPopover.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({ + const unshared = await openSharePopover(page) + await expect(unshared.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({ timeout: 30_000, }) }) diff --git a/packages/app/e2e/settings/settings-keybinds.spec.ts b/packages/app/e2e/settings/settings-keybinds.spec.ts index eceb82b7414c..5e98bd158a17 100644 --- a/packages/app/e2e/settings/settings-keybinds.spec.ts +++ b/packages/app/e2e/settings/settings-keybinds.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from "../fixtures" import { openSettings, closeDialog, withSession } from "../actions" -import { keybindButtonSelector } from "../selectors" +import { keybindButtonSelector, terminalSelector } from "../selectors" import { modKey } from "../utils" test("changing sidebar toggle keybind works", async ({ page, gotoSession }) => { @@ -9,7 +9,7 @@ test("changing sidebar toggle keybind works", async ({ page, gotoSession }) => { const dialog = await openSettings(page) await dialog.getByRole("tab", { name: "Shortcuts" }).click() - const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle")) + const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle")).first() await expect(keybindButton).toBeVisible() const initialKeybind = await keybindButton.textContent() @@ -51,6 +51,40 @@ test("changing sidebar toggle keybind works", async ({ page, gotoSession }) => { expect(finalClosed).toBe(initiallyClosed) }) +test("sidebar toggle keybind guards against shortcut conflicts", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + await dialog.getByRole("tab", { name: "Shortcuts" }).click() + + const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle")) + await expect(keybindButton).toBeVisible() + + const initialKeybind = await keybindButton.textContent() + expect(initialKeybind).toContain("B") + + await keybindButton.click() + await expect(keybindButton).toHaveText(/press/i) + + await page.keyboard.press(`${modKey}+Shift+KeyP`) + await page.waitForTimeout(100) + + const toast = page.locator('[data-component="toast"]').last() + await expect(toast).toBeVisible() + await expect(toast).toContainText(/already/i) + + await keybindButton.click() + await expect(keybindButton).toContainText("B") + + const stored = await page.evaluate(() => { + const raw = localStorage.getItem("settings.v3") + return raw ? JSON.parse(raw) : null + }) + expect(stored?.keybinds?.["sidebar.toggle"]).toBeUndefined() + + await closeDialog(page, dialog) +}) + test("resetting all keybinds to defaults works", async ({ page, gotoSession }) => { await page.addInitScript(() => { localStorage.setItem("settings.v3", JSON.stringify({ keybinds: { "sidebar.toggle": "mod+shift+x" } })) @@ -267,11 +301,52 @@ test("changing terminal toggle keybind works", async ({ page, gotoSession }) => await closeDialog(page, dialog) + const terminal = page.locator(terminalSelector) + await expect(terminal).not.toBeVisible() + + await page.keyboard.press(`${modKey}+Y`) + await expect(terminal).toBeVisible() + await page.keyboard.press(`${modKey}+Y`) + await expect(terminal).not.toBeVisible() +}) + +test("terminal toggle keybind persists after reload", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + await dialog.getByRole("tab", { name: "Shortcuts" }).click() + + const keybindButton = dialog.locator(keybindButtonSelector("terminal.toggle")) + await expect(keybindButton).toBeVisible() + + await keybindButton.click() + await expect(keybindButton).toHaveText(/press/i) + + await page.keyboard.press(`${modKey}+Shift+KeyY`) await page.waitForTimeout(100) - const pageStable = await page.evaluate(() => document.readyState === "complete") - expect(pageStable).toBe(true) + await expect(keybindButton).toContainText("Y") + await closeDialog(page, dialog) + + await page.reload() + + await expect + .poll(async () => { + return await page.evaluate(() => { + const raw = localStorage.getItem("settings.v3") + if (!raw) return + const parsed = JSON.parse(raw) + return parsed?.keybinds?.["terminal.toggle"] + }) + }) + .toBe("mod+shift+y") + + const reloaded = await openSettings(page) + await reloaded.getByRole("tab", { name: "Shortcuts" }).click() + const reloadedKeybind = reloaded.locator(keybindButtonSelector("terminal.toggle")).first() + await expect(reloadedKeybind).toContainText("Y") + await closeDialog(page, reloaded) }) test("changing command palette keybind works", async ({ page, gotoSession }) => { diff --git a/packages/app/e2e/settings/settings.spec.ts b/packages/app/e2e/settings/settings.spec.ts index 2865419f0de0..9fbcf79f5ee7 100644 --- a/packages/app/e2e/settings/settings.spec.ts +++ b/packages/app/e2e/settings/settings.spec.ts @@ -9,6 +9,9 @@ import { settingsNotificationsPermissionsSelector, settingsReleaseNotesSelector, settingsSoundsAgentSelector, + settingsSoundsAgentEnabledSelector, + settingsSoundsErrorsSelector, + settingsSoundsPermissionsSelector, settingsThemeSelector, settingsUpdatesStartupSelector, } from "../selectors" @@ -139,6 +142,105 @@ test("changing font persists in localStorage and updates CSS variable", async ({ expect(newFontFamily).not.toBe(initialFontFamily) }) +test("color scheme and font rehydrate after reload", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + + const colorSchemeSelect = dialog.locator(settingsColorSchemeSelector) + await expect(colorSchemeSelect).toBeVisible() + await colorSchemeSelect.locator('[data-slot="select-select-trigger"]').click() + await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Dark" }).click() + await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark") + + const fontSelect = dialog.locator(settingsFontSelector) + await expect(fontSelect).toBeVisible() + + const initialFontFamily = await page.evaluate(() => { + return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim() + }) + + const initialSettings = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + const currentFont = + (await fontSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? "" + await fontSelect.locator('[data-slot="select-select-trigger"]').click() + + const fontItems = page.locator('[data-slot="select-select-item"]') + expect(await fontItems.count()).toBeGreaterThan(1) + + if (currentFont) { + await fontItems.filter({ hasNotText: currentFont }).first().click() + } + if (!currentFont) { + await fontItems.nth(1).click() + } + + await expect + .poll(async () => { + return await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + }) + .toMatchObject({ + appearance: { + font: expect.any(String), + }, + }) + + const updatedSettings = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + const updatedFontFamily = await page.evaluate(() => { + return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim() + }) + expect(updatedFontFamily).not.toBe(initialFontFamily) + expect(updatedSettings?.appearance?.font).not.toBe(initialSettings?.appearance?.font) + + await closeDialog(page, dialog) + await page.reload() + + await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark") + + await expect + .poll(async () => { + return await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + }) + .toMatchObject({ + appearance: { + font: updatedSettings?.appearance?.font, + }, + }) + + const rehydratedSettings = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + await expect + .poll(async () => { + return await page.evaluate(() => { + return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim() + }) + }) + .not.toBe(initialFontFamily) + + const rehydratedFontFamily = await page.evaluate(() => { + return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim() + }) + expect(rehydratedFontFamily).not.toBe(initialFontFamily) + expect(rehydratedSettings?.appearance?.font).toBe(updatedSettings?.appearance?.font) +}) + test("toggling notification agent switch updates localStorage", async ({ page, gotoSession }) => { await gotoSession() @@ -234,6 +336,91 @@ test("changing sound agent selection persists in localStorage", async ({ page, g expect(stored?.sounds?.agent).not.toBe("staplebops-01") }) +test("disabling agent sound disables sound selection", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + const select = dialog.locator(settingsSoundsAgentSelector) + const switchContainer = dialog.locator(settingsSoundsAgentEnabledSelector) + const trigger = select.locator('[data-slot="select-select-trigger"]') + await expect(select).toBeVisible() + await expect(switchContainer).toBeVisible() + await expect(trigger).toBeEnabled() + + await switchContainer.locator('[data-slot="switch-control"]').click() + await page.waitForTimeout(100) + + await expect(trigger).toBeDisabled() + + const stored = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + expect(stored?.sounds?.agentEnabled).toBe(false) +}) + +test("changing permissions and errors sounds updates localStorage", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + const permissionsSelect = dialog.locator(settingsSoundsPermissionsSelector) + const errorsSelect = dialog.locator(settingsSoundsErrorsSelector) + await expect(permissionsSelect).toBeVisible() + await expect(errorsSelect).toBeVisible() + + const initial = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + const permissionsCurrent = + (await permissionsSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? "" + await permissionsSelect.locator('[data-slot="select-select-trigger"]').click() + const permissionItems = page.locator('[data-slot="select-select-item"]') + expect(await permissionItems.count()).toBeGreaterThan(1) + if (permissionsCurrent) { + await permissionItems.filter({ hasNotText: permissionsCurrent }).first().click() + } + if (!permissionsCurrent) { + await permissionItems.nth(1).click() + } + + const errorsCurrent = + (await errorsSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? "" + await errorsSelect.locator('[data-slot="select-select-trigger"]').click() + const errorItems = page.locator('[data-slot="select-select-item"]') + expect(await errorItems.count()).toBeGreaterThan(1) + if (errorsCurrent) { + await errorItems.filter({ hasNotText: errorsCurrent }).first().click() + } + if (!errorsCurrent) { + await errorItems.nth(1).click() + } + + await expect + .poll(async () => { + return await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + }) + .toMatchObject({ + sounds: { + permissions: expect.any(String), + errors: expect.any(String), + }, + }) + + const stored = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + expect(stored?.sounds?.permissions).not.toBe(initial?.sounds?.permissions) + expect(stored?.sounds?.errors).not.toBe(initial?.sounds?.errors) +}) + test("toggling updates startup switch updates localStorage", async ({ page, gotoSession }) => { await gotoSession() diff --git a/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts b/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts new file mode 100644 index 000000000000..e37f94f3a7a6 --- /dev/null +++ b/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts @@ -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) + } +}) diff --git a/packages/app/e2e/sidebar/sidebar.spec.ts b/packages/app/e2e/sidebar/sidebar.spec.ts index 6239a04bd794..5c78c2220d29 100644 --- a/packages/app/e2e/sidebar/sidebar.spec.ts +++ b/packages/app/e2e/sidebar/sidebar.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../fixtures" -import { openSidebar, toggleSidebar } from "../actions" +import { openSidebar, toggleSidebar, withSession } from "../actions" test("sidebar can be collapsed and expanded", async ({ page, gotoSession }) => { await gotoSession() @@ -12,3 +12,26 @@ test("sidebar can be collapsed and expanded", async ({ page, gotoSession }) => { await toggleSidebar(page) await expect(page.locator("main")).not.toHaveClass(/xl:border-l/) }) + +test("sidebar collapsed state persists across navigation and reload", async ({ page, sdk, gotoSession }) => { + await withSession(sdk, "sidebar persist session 1", async (session1) => { + await withSession(sdk, "sidebar persist session 2", async (session2) => { + await gotoSession(session1.id) + + await openSidebar(page) + await toggleSidebar(page) + await expect(page.locator("main")).toHaveClass(/xl:border-l/) + + await gotoSession(session2.id) + await expect(page.locator("main")).toHaveClass(/xl:border-l/) + + await page.reload() + await expect(page.locator("main")).toHaveClass(/xl:border-l/) + + const opened = await page.evaluate( + () => JSON.parse(localStorage.getItem("opencode.global.dat:layout") ?? "{}").sidebar?.opened, + ) + await expect(opened).toBe(false) + }) + }) +}) diff --git a/packages/app/package.json b/packages/app/package.json index a995880e01c6..b92abb413d9e 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.53", + "version": "1.2.6", "description": "", "type": "module", "exports": { diff --git a/packages/app/playwright.config.ts b/packages/app/playwright.config.ts index 10819e69ffef..ea85829e0bcc 100644 --- a/packages/app/playwright.config.ts +++ b/packages/app/playwright.config.ts @@ -14,7 +14,7 @@ export default defineConfig({ expect: { timeout: 10_000, }, - fullyParallel: true, + fullyParallel: process.env.PLAYWRIGHT_FULLY_PARALLEL === "1", forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, reporter: [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]], diff --git a/packages/app/script/e2e-local.ts b/packages/app/script/e2e-local.ts index df2107f76d9b..112e2bc60a1a 100644 --- a/packages/app/script/e2e-local.ts +++ b/packages/app/script/e2e-local.ts @@ -55,6 +55,7 @@ const extraArgs = (() => { const [serverPort, webPort] = await Promise.all([freePort(), freePort()]) const sandbox = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-")) +const keepSandbox = process.env.OPENCODE_E2E_KEEP_SANDBOX === "1" const serverEnv = { ...process.env, @@ -83,58 +84,95 @@ const runnerEnv = { PLAYWRIGHT_PORT: String(webPort), } satisfies Record -const seed = Bun.spawn(["bun", "script/seed-e2e.ts"], { - cwd: opencodeDir, - env: serverEnv, - stdout: "inherit", - stderr: "inherit", -}) +let seed: ReturnType | undefined +let runner: ReturnType | undefined +let server: { stop: () => Promise | void } | undefined +let inst: { Instance: { disposeAll: () => Promise | void } } | undefined +let cleaned = false + +const cleanup = async () => { + if (cleaned) return + cleaned = true + + if (seed && seed.exitCode === null) seed.kill("SIGTERM") + if (runner && runner.exitCode === null) runner.kill("SIGTERM") + + const jobs = [ + inst?.Instance.disposeAll(), + server?.stop(), + keepSandbox ? undefined : fs.rm(sandbox, { recursive: true, force: true }), + ].filter(Boolean) + await Promise.allSettled(jobs) +} -const seedExit = await seed.exited -if (seedExit !== 0) { - process.exit(seedExit) +const shutdown = (code: number, reason: string) => { + process.exitCode = code + void cleanup().finally(() => { + console.error(`e2e-local shutdown: ${reason}`) + process.exit(code) + }) } -Object.assign(process.env, serverEnv) -process.env.AGENT = "1" -process.env.OPENCODE = "1" +const reportInternalError = (reason: string, error: unknown) => { + console.warn(`e2e-local ignored server error: ${reason}`) + console.warn(error) +} -const log = await import("../../opencode/src/util/log") -const install = await import("../../opencode/src/installation") -await log.Log.init({ - print: true, - dev: install.Installation.isLocal(), - level: "WARN", +process.once("SIGINT", () => shutdown(130, "SIGINT")) +process.once("SIGTERM", () => shutdown(143, "SIGTERM")) +process.once("SIGHUP", () => shutdown(129, "SIGHUP")) +process.once("uncaughtException", (error) => { + reportInternalError("uncaughtException", error) +}) +process.once("unhandledRejection", (error) => { + reportInternalError("unhandledRejection", error) }) -const servermod = await import("../../opencode/src/server/server") -const inst = await import("../../opencode/src/project/instance") -const server = servermod.Server.listen({ port: serverPort, hostname: "127.0.0.1" }) -console.log(`opencode server listening on http://127.0.0.1:${serverPort}`) +let code = 1 -const result = await (async () => { - try { - await waitForHealth(`http://127.0.0.1:${serverPort}/global/health`) +try { + seed = Bun.spawn(["bun", "script/seed-e2e.ts"], { + cwd: opencodeDir, + env: serverEnv, + stdout: "inherit", + stderr: "inherit", + }) - const runner = Bun.spawn(["bun", "test:e2e", ...extraArgs], { + const seedExit = await seed.exited + if (seedExit !== 0) { + code = seedExit + } else { + Object.assign(process.env, serverEnv) + process.env.AGENT = "1" + process.env.OPENCODE = "1" + + const log = await import("../../opencode/src/util/log") + const install = await import("../../opencode/src/installation") + await log.Log.init({ + print: true, + dev: install.Installation.isLocal(), + level: "WARN", + }) + + const servermod = await import("../../opencode/src/server/server") + inst = await import("../../opencode/src/project/instance") + server = servermod.Server.listen({ port: serverPort, hostname: "127.0.0.1" }) + console.log(`opencode server listening on http://127.0.0.1:${serverPort}`) + + await waitForHealth(`http://127.0.0.1:${serverPort}/global/health`) + runner = Bun.spawn(["bun", "test:e2e", ...extraArgs], { cwd: appDir, env: runnerEnv, stdout: "inherit", stderr: "inherit", }) - - return { code: await runner.exited } - } catch (error) { - return { error } - } finally { - await inst.Instance.disposeAll() - await server.stop() + code = await runner.exited } -})() - -if ("error" in result) { - console.error(result.error) - process.exit(1) +} catch (error) { + console.error(error) + code = 1 +} finally { + await cleanup() } -process.exit(result.code) +process.exit(code) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 8a111472baf0..1121c2e955ac 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -1,5 +1,5 @@ import "@/index.css" -import { ErrorBoundary, Show, lazy, type ParentProps } from "solid-js" +import { ErrorBoundary, Show, Suspense, lazy, type JSX, type ParentProps } from "solid-js" import { Router, Route, Navigate } from "@solidjs/router" import { MetaProvider } from "@solidjs/meta" import { Font } from "@opencode-ai/ui/font" @@ -30,12 +30,26 @@ import { HighlightsProvider } from "@/context/highlights" import Layout from "@/pages/layout" import DirectoryLayout from "@/pages/directory-layout" import { ErrorPage } from "./pages/error" -import { Suspense, JSX } from "solid-js" - const Home = lazy(() => import("@/pages/home")) const Session = lazy(() => import("@/pages/session")) const Loading = () =>
+const HomeRoute = () => ( + }> + + +) + +const SessionRoute = () => ( + + }> + + + +) + +const SessionIndexRoute = () => + function UiI18nBridge(props: ParentProps) { const language = useLanguage() return {props.children} @@ -43,7 +57,7 @@ function UiI18nBridge(props: ParentProps) { declare global { interface Window { - __OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[] } + __OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[]; wsl?: boolean } } } @@ -52,6 +66,71 @@ function MarkedProviderWithNativeParser(props: ParentProps) { return {props.children} } +function AppShellProviders(props: ParentProps) { + return ( + + + + + + + + {props.children} + + + + + + + + ) +} + +function SessionProviders(props: ParentProps) { + return ( + + + + {props.children} + + + + ) +} + +function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) { + return ( + + {props.appChildren} + {props.children} + + ) +} + +const getStoredDefaultServerUrl = (platform: ReturnType) => { + if (platform.platform !== "web") return + const result = platform.getDefaultServerUrl?.() + if (result instanceof Promise) return + if (!result) return + return normalizeServerUrl(result) +} + +const resolveDefaultServerUrl = (props: { + defaultUrl?: string + storedDefaultServerUrl?: string + hostname: string + origin: string + isDev: boolean + devHost?: string + devPort?: string +}) => { + if (props.defaultUrl) return props.defaultUrl + if (props.storedDefaultServerUrl) return props.storedDefaultServerUrl + if (props.hostname.includes("opencode.ai")) return "http://localhost:4096" + if (props.isDev) return `http://${props.devHost ?? "localhost"}:${props.devPort ?? "4096"}` + return props.origin +} + export function AppBaseProviders(props: ParentProps) { return ( @@ -84,82 +163,31 @@ function ServerKey(props: ParentProps) { ) } -export function AppInterface(props: { defaultUrl?: string; children?: JSX.Element }) { +export function AppInterface(props: { defaultUrl?: string; children?: JSX.Element; isSidecar?: boolean }) { const platform = usePlatform() - - const stored = (() => { - if (platform.platform !== "web") return - const result = platform.getDefaultServerUrl?.() - if (result instanceof Promise) return - if (!result) return - return normalizeServerUrl(result) - })() - - const defaultServerUrl = () => { - if (props.defaultUrl) return props.defaultUrl - if (stored) return stored - if (location.hostname.includes("opencode.ai")) return "http://localhost:4096" - if (import.meta.env.DEV) - return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}` - - return window.location.origin - } + const storedDefaultServerUrl = getStoredDefaultServerUrl(platform) + const defaultServerUrl = resolveDefaultServerUrl({ + defaultUrl: props.defaultUrl, + storedDefaultServerUrl, + hostname: location.hostname, + origin: window.location.origin, + isDev: import.meta.env.DEV, + devHost: import.meta.env.VITE_OPENCODE_SERVER_HOST, + devPort: import.meta.env.VITE_OPENCODE_SERVER_PORT, + }) return ( - + ( - - - - - - - - - {props.children} - {routerProps.children} - - - - - - - - - )} + root={(routerProps) => {routerProps.children}} > - ( - }> - - - )} - /> + - } /> - ( - - - - - - }> - - - - - - - - )} - /> + + diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index 65e322b43451..90f4f41f7c6f 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -10,7 +10,6 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { Spinner } from "@opencode-ai/ui/spinner" import { TextField } from "@opencode-ai/ui/text-field" import { showToast } from "@opencode-ai/ui/toast" -import { iife } from "@opencode-ai/util/iife" import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js" import { createStore, produce } from "solid-js/store" import { Link } from "@/components/link" @@ -55,6 +54,47 @@ export function DialogConnectProvider(props: { provider: string }) { error: undefined as string | undefined, }) + type Action = + | { type: "method.select"; index: number } + | { type: "method.reset" } + | { type: "auth.pending" } + | { type: "auth.complete"; authorization: ProviderAuthAuthorization } + | { type: "auth.error"; error: string } + + function dispatch(action: Action) { + setStore( + produce((draft) => { + if (action.type === "method.select") { + draft.methodIndex = action.index + draft.authorization = undefined + draft.state = undefined + draft.error = undefined + return + } + if (action.type === "method.reset") { + draft.methodIndex = undefined + draft.authorization = undefined + draft.state = undefined + draft.error = undefined + return + } + if (action.type === "auth.pending") { + draft.state = "pending" + draft.error = undefined + return + } + if (action.type === "auth.complete") { + draft.state = "complete" + draft.authorization = action.authorization + draft.error = undefined + return + } + draft.state = "error" + draft.error = action.error + }), + ) + } + const method = createMemo(() => (store.methodIndex !== undefined ? methods().at(store.methodIndex!) : undefined)) const methodLabel = (value?: { type?: string; label?: string }) => { @@ -63,6 +103,24 @@ export function DialogConnectProvider(props: { provider: string }) { return value.label ?? "" } + function formatError(value: unknown, fallback: string): string { + if (value && typeof value === "object" && "data" in value) { + const data = (value as { data?: { message?: unknown } }).data + if (typeof data?.message === "string" && data.message) return data.message + } + if (value && typeof value === "object" && "error" in value) { + const nested = formatError((value as { error?: unknown }).error, "") + if (nested) return nested + } + if (value && typeof value === "object" && "message" in value) { + const message = (value as { message?: unknown }).message + if (typeof message === "string" && message) return message + } + if (value instanceof Error && value.message) return value.message + if (typeof value === "string" && value) return value + return fallback + } + async function selectMethod(index: number) { if (timer.current !== undefined) { clearTimeout(timer.current) @@ -70,17 +128,10 @@ export function DialogConnectProvider(props: { provider: string }) { } const method = methods()[index] - setStore( - produce((draft) => { - draft.methodIndex = index - draft.authorization = undefined - draft.state = undefined - draft.error = undefined - }), - ) + dispatch({ type: "method.select", index }) if (method.type === "oauth") { - setStore("state", "pending") + dispatch({ type: "auth.pending" }) const start = Date.now() await globalSDK.client.provider.oauth .authorize( @@ -100,18 +151,15 @@ export function DialogConnectProvider(props: { provider: string }) { timer.current = setTimeout(() => { timer.current = undefined if (!alive.value) return - setStore("state", "complete") - setStore("authorization", x.data!) + dispatch({ type: "auth.complete", authorization: x.data! }) }, delay) return } - setStore("state", "complete") - setStore("authorization", x.data!) + dispatch({ type: "auth.complete", authorization: x.data! }) }) .catch((e) => { if (!alive.value) return - setStore("state", "error") - setStore("error", String(e)) + dispatch({ type: "auth.error", error: formatError(e, language.t("common.requestFailed")) }) }) } } @@ -129,10 +177,6 @@ export function DialogConnectProvider(props: { provider: string }) { if (methods().length === 1) { selectMethod(0) } - document.addEventListener("keydown", handleKey) - onCleanup(() => { - document.removeEventListener("keydown", handleKey) - }) }) async function complete() { @@ -152,17 +196,243 @@ export function DialogConnectProvider(props: { provider: string }) { return } if (store.authorization) { - setStore("authorization", undefined) - setStore("methodIndex", undefined) + dispatch({ type: "method.reset" }) return } - if (store.methodIndex) { - setStore("methodIndex", undefined) + if (store.methodIndex !== undefined) { + dispatch({ type: "method.reset" }) return } dialog.show(() => ) } + function MethodSelection() { + return ( + <> +
+ {language.t("provider.connect.selectMethod", { provider: provider().name })} +
+
+ { + listRef = ref + }} + items={methods} + key={(m) => m?.label} + onSelect={async (selected, index) => { + if (!selected) return + selectMethod(index) + }} + > + {(i) => ( +
+
+ + {methodLabel(i)} +
+ )} + +
+ + ) + } + + function ApiAuthView() { + const [formStore, setFormStore] = createStore({ + value: "", + error: undefined as string | undefined, + }) + + async function handleSubmit(e: SubmitEvent) { + e.preventDefault() + + const form = e.currentTarget as HTMLFormElement + const formData = new FormData(form) + const apiKey = formData.get("apiKey") as string + + if (!apiKey?.trim()) { + setFormStore("error", language.t("provider.connect.apiKey.required")) + return + } + + setFormStore("error", undefined) + await globalSDK.client.auth.set({ + providerID: props.provider, + auth: { + type: "api", + key: apiKey, + }, + }) + await complete() + } + + return ( +
+ + +
+
{language.t("provider.connect.opencodeZen.line1")}
+
{language.t("provider.connect.opencodeZen.line2")}
+
+ {language.t("provider.connect.opencodeZen.visit.prefix")} + + {language.t("provider.connect.opencodeZen.visit.link")} + + {language.t("provider.connect.opencodeZen.visit.suffix")} +
+
+
+ +
+ {language.t("provider.connect.apiKey.description", { provider: provider().name })} +
+
+
+
+ setFormStore("value", v)} + validationState={formStore.error ? "invalid" : undefined} + error={formStore.error} + /> + + +
+ ) + } + + function OAuthCodeView() { + const [formStore, setFormStore] = createStore({ + value: "", + error: undefined as string | undefined, + }) + + onMount(() => { + if (store.authorization?.method === "code" && store.authorization?.url) { + platform.openLink(store.authorization.url) + } + }) + + async function handleSubmit(e: SubmitEvent) { + e.preventDefault() + + const form = e.currentTarget as HTMLFormElement + const formData = new FormData(form) + const code = formData.get("code") as string + + if (!code?.trim()) { + setFormStore("error", language.t("provider.connect.oauth.code.required")) + return + } + + setFormStore("error", undefined) + const result = await globalSDK.client.provider.oauth + .callback({ + providerID: props.provider, + method: store.methodIndex, + code, + }) + .then((value) => (value.error ? { ok: false as const, error: value.error } : { ok: true as const })) + .catch((error) => ({ ok: false as const, error })) + if (result.ok) { + await complete() + return + } + setFormStore("error", formatError(result.error, language.t("provider.connect.oauth.code.invalid"))) + } + + return ( +
+
+ {language.t("provider.connect.oauth.code.visit.prefix")} + {language.t("provider.connect.oauth.code.visit.link")} + {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })} +
+
+ setFormStore("value", v)} + validationState={formStore.error ? "invalid" : undefined} + error={formStore.error} + /> + + +
+ ) + } + + function OAuthAutoView() { + const code = createMemo(() => { + const instructions = store.authorization?.instructions + if (instructions?.includes(":")) { + return instructions.split(":")[1]?.trim() + } + return instructions + }) + + onMount(() => { + void (async () => { + if (store.authorization?.url) { + platform.openLink(store.authorization.url) + } + + const result = await globalSDK.client.provider.oauth + .callback({ + providerID: props.provider, + method: store.methodIndex, + }) + .then((value) => (value.error ? { ok: false as const, error: value.error } : { ok: true as const })) + .catch((error) => ({ ok: false as const, error })) + + if (!alive.value) return + + if (!result.ok) { + const message = formatError(result.error, language.t("common.requestFailed")) + dispatch({ type: "auth.error", error: message }) + return + } + + await complete() + })() + }) + + return ( +
+
+ {language.t("provider.connect.oauth.auto.visit.prefix")} + {language.t("provider.connect.oauth.auto.visit.link")} + {language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })} +
+ +
+ + {language.t("provider.connect.status.waiting")} +
+
+ ) + } + return (
- - -
- {language.t("provider.connect.selectMethod", { provider: provider().name })} -
-
- { - listRef = ref - }} - items={methods} - key={(m) => m?.label} - onSelect={async (method, index) => { - if (!method) return - selectMethod(index) - }} - > - {(i) => ( -
-
- - {methodLabel(i)} -
- )} - -
- - -
-
- - {language.t("provider.connect.status.inProgress")} -
-
-
- -
-
- - {language.t("provider.connect.status.failed", { error: store.error ?? "" })} +
+ + + + + +
+
+ + {language.t("provider.connect.status.inProgress")} +
-
- - - {iife(() => { - const [formStore, setFormStore] = createStore({ - value: "", - error: undefined as string | undefined, - }) - - async function handleSubmit(e: SubmitEvent) { - e.preventDefault() - - const form = e.currentTarget as HTMLFormElement - const formData = new FormData(form) - const apiKey = formData.get("apiKey") as string - - if (!apiKey?.trim()) { - setFormStore("error", language.t("provider.connect.apiKey.required")) - return - } - - setFormStore("error", undefined) - await globalSDK.client.auth.set({ - providerID: props.provider, - auth: { - type: "api", - key: apiKey, - }, - }) - await complete() - } - - return ( -
- - -
-
- {language.t("provider.connect.opencodeZen.line1")} -
-
- {language.t("provider.connect.opencodeZen.line2")} -
-
- {language.t("provider.connect.opencodeZen.visit.prefix")} - - {language.t("provider.connect.opencodeZen.visit.link")} - - {language.t("provider.connect.opencodeZen.visit.suffix")} -
-
-
- -
- {language.t("provider.connect.apiKey.description", { provider: provider().name })} -
-
-
-
- - - + + +
+
+ + {language.t("provider.connect.status.failed", { error: store.error ?? "" })}
- ) - })} - - - - - {iife(() => { - const [formStore, setFormStore] = createStore({ - value: "", - error: undefined as string | undefined, - }) - - onMount(() => { - if (store.authorization?.method === "code" && store.authorization?.url) { - platform.openLink(store.authorization.url) - } - }) - - async function handleSubmit(e: SubmitEvent) { - e.preventDefault() - - const form = e.currentTarget as HTMLFormElement - const formData = new FormData(form) - const code = formData.get("code") as string - - if (!code?.trim()) { - setFormStore("error", language.t("provider.connect.oauth.code.required")) - return - } - - setFormStore("error", undefined) - const result = await globalSDK.client.provider.oauth - .callback({ - providerID: props.provider, - method: store.methodIndex, - code, - }) - .then((value) => - value.error ? { ok: false as const, error: value.error } : { ok: true as const }, - ) - .catch((error) => ({ ok: false as const, error })) - if (result.ok) { - await complete() - return - } - const message = result.error instanceof Error ? result.error.message : String(result.error) - setFormStore("error", message || language.t("provider.connect.oauth.code.invalid")) - } - - return ( -
-
- {language.t("provider.connect.oauth.code.visit.prefix")} - - {language.t("provider.connect.oauth.code.visit.link")} - - {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })} -
-
- - - -
- ) - })} -
- - {iife(() => { - const code = createMemo(() => { - const instructions = store.authorization?.instructions - if (instructions?.includes(":")) { - return instructions?.split(":")[1]?.trim() - } - return instructions - }) - - onMount(() => { - void (async () => { - if (store.authorization?.url) { - platform.openLink(store.authorization.url) - } - - const result = await globalSDK.client.provider.oauth - .callback({ - providerID: props.provider, - method: store.methodIndex, - }) - .then((value) => - value.error ? { ok: false as const, error: value.error } : { ok: true as const }, - ) - .catch((error) => ({ ok: false as const, error })) - - if (!alive.value) return - - if (!result.ok) { - const message = result.error instanceof Error ? result.error.message : String(result.error) - setStore("state", "error") - setStore("error", message) - return - } - - await complete() - })() - }) - - return ( -
-
- {language.t("provider.connect.oauth.auto.visit.prefix")} - - {language.t("provider.connect.oauth.auto.visit.link")} - - {language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })} -
- -
- - {language.t("provider.connect.status.waiting")} -
-
- ) - })} -
-
-
- +
+
+ + + + + + + + + + + + + + +
diff --git a/packages/app/src/components/dialog-custom-provider.tsx b/packages/app/src/components/dialog-custom-provider.tsx index 53773ed9eabe..017b85a2c997 100644 --- a/packages/app/src/components/dialog-custom-provider.tsx +++ b/packages/app/src/components/dialog-custom-provider.tsx @@ -6,7 +6,7 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { TextField } from "@opencode-ai/ui/text-field" import { showToast } from "@opencode-ai/ui/toast" import { For } from "solid-js" -import { createStore, produce } from "solid-js/store" +import { createStore } from "solid-js/store" import { Link } from "@/components/link" import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" @@ -16,6 +16,147 @@ import { DialogSelectProvider } from "./dialog-select-provider" const PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/ const OPENAI_COMPATIBLE = "@ai-sdk/openai-compatible" +type Translator = ReturnType["t"] + +type ModelRow = { + id: string + name: string +} + +type HeaderRow = { + key: string + value: string +} + +type FormState = { + providerID: string + name: string + baseURL: string + apiKey: string + models: ModelRow[] + headers: HeaderRow[] + saving: boolean +} + +type FormErrors = { + providerID: string | undefined + name: string | undefined + baseURL: string | undefined + models: Array<{ id?: string; name?: string }> + headers: Array<{ key?: string; value?: string }> +} + +type ValidateArgs = { + form: FormState + t: Translator + disabledProviders: string[] + existingProviderIDs: Set +} + +function validateCustomProvider(input: ValidateArgs) { + const providerID = input.form.providerID.trim() + const name = input.form.name.trim() + const baseURL = input.form.baseURL.trim() + const apiKey = input.form.apiKey.trim() + + const env = apiKey.match(/^\{env:([^}]+)\}$/)?.[1]?.trim() + const key = apiKey && !env ? apiKey : undefined + + const idError = !providerID + ? input.t("provider.custom.error.providerID.required") + : !PROVIDER_ID.test(providerID) + ? input.t("provider.custom.error.providerID.format") + : undefined + + const nameError = !name ? input.t("provider.custom.error.name.required") : undefined + const urlError = !baseURL + ? input.t("provider.custom.error.baseURL.required") + : !/^https?:\/\//.test(baseURL) + ? input.t("provider.custom.error.baseURL.format") + : undefined + + const disabled = input.disabledProviders.includes(providerID) + const existsError = idError + ? undefined + : input.existingProviderIDs.has(providerID) && !disabled + ? input.t("provider.custom.error.providerID.exists") + : undefined + + const seenModels = new Set() + const modelErrors = input.form.models.map((m) => { + const id = m.id.trim() + const modelIdError = !id + ? input.t("provider.custom.error.required") + : seenModels.has(id) + ? input.t("provider.custom.error.duplicate") + : (() => { + seenModels.add(id) + return undefined + })() + const modelNameError = !m.name.trim() ? input.t("provider.custom.error.required") : undefined + return { id: modelIdError, name: modelNameError } + }) + const modelsValid = modelErrors.every((m) => !m.id && !m.name) + const models = Object.fromEntries(input.form.models.map((m) => [m.id.trim(), { name: m.name.trim() }])) + + const seenHeaders = new Set() + const headerErrors = input.form.headers.map((h) => { + const key = h.key.trim() + const value = h.value.trim() + + if (!key && !value) return {} + const keyError = !key + ? input.t("provider.custom.error.required") + : seenHeaders.has(key.toLowerCase()) + ? input.t("provider.custom.error.duplicate") + : (() => { + seenHeaders.add(key.toLowerCase()) + return undefined + })() + const valueError = !value ? input.t("provider.custom.error.required") : undefined + return { key: keyError, value: valueError } + }) + const headersValid = headerErrors.every((h) => !h.key && !h.value) + const headers = Object.fromEntries( + input.form.headers + .map((h) => ({ key: h.key.trim(), value: h.value.trim() })) + .filter((h) => !!h.key && !!h.value) + .map((h) => [h.key, h.value]), + ) + + const errors: FormErrors = { + providerID: idError ?? existsError, + name: nameError, + baseURL: urlError, + models: modelErrors, + headers: headerErrors, + } + + const ok = !idError && !existsError && !nameError && !urlError && modelsValid && headersValid + if (!ok) return { errors } + + const options = { + baseURL, + ...(Object.keys(headers).length ? { headers } : {}), + } + + return { + errors, + result: { + providerID, + name, + key, + config: { + npm: OPENAI_COMPATIBLE, + name, + ...(env ? { env: [env] } : {}), + options, + models, + }, + }, + } +} + type Props = { back?: "providers" | "close" } @@ -26,7 +167,7 @@ export function DialogCustomProvider(props: Props) { const globalSDK = useGlobalSDK() const language = useLanguage() - const [form, setForm] = createStore({ + const [form, setForm] = createStore({ providerID: "", name: "", baseURL: "", @@ -36,12 +177,12 @@ export function DialogCustomProvider(props: Props) { saving: false, }) - const [errors, setErrors] = createStore({ - providerID: undefined as string | undefined, - name: undefined as string | undefined, - baseURL: undefined as string | undefined, - models: [{} as { id?: string; name?: string }], - headers: [{} as { key?: string; value?: string }], + const [errors, setErrors] = createStore({ + providerID: undefined, + name: undefined, + baseURL: undefined, + models: [{}], + headers: [{}], }) const goBack = () => { @@ -53,169 +194,36 @@ export function DialogCustomProvider(props: Props) { } const addModel = () => { - setForm( - "models", - produce((draft) => { - draft.push({ id: "", name: "" }) - }), - ) - setErrors( - "models", - produce((draft) => { - draft.push({}) - }), - ) + setForm("models", (v) => [...v, { id: "", name: "" }]) + setErrors("models", (v) => [...v, {}]) } const removeModel = (index: number) => { if (form.models.length <= 1) return - setForm( - "models", - produce((draft) => { - draft.splice(index, 1) - }), - ) - setErrors( - "models", - produce((draft) => { - draft.splice(index, 1) - }), - ) + setForm("models", (v) => v.filter((_, i) => i !== index)) + setErrors("models", (v) => v.filter((_, i) => i !== index)) } const addHeader = () => { - setForm( - "headers", - produce((draft) => { - draft.push({ key: "", value: "" }) - }), - ) - setErrors( - "headers", - produce((draft) => { - draft.push({}) - }), - ) + setForm("headers", (v) => [...v, { key: "", value: "" }]) + setErrors("headers", (v) => [...v, {}]) } const removeHeader = (index: number) => { if (form.headers.length <= 1) return - setForm( - "headers", - produce((draft) => { - draft.splice(index, 1) - }), - ) - setErrors( - "headers", - produce((draft) => { - draft.splice(index, 1) - }), - ) + setForm("headers", (v) => v.filter((_, i) => i !== index)) + setErrors("headers", (v) => v.filter((_, i) => i !== index)) } const validate = () => { - const providerID = form.providerID.trim() - const name = form.name.trim() - const baseURL = form.baseURL.trim() - const apiKey = form.apiKey.trim() - - const env = apiKey.match(/^\{env:([^}]+)\}$/)?.[1]?.trim() - const key = apiKey && !env ? apiKey : undefined - - const idError = !providerID - ? language.t("provider.custom.error.providerID.required") - : !PROVIDER_ID.test(providerID) - ? language.t("provider.custom.error.providerID.format") - : undefined - - const nameError = !name ? language.t("provider.custom.error.name.required") : undefined - const urlError = !baseURL - ? language.t("provider.custom.error.baseURL.required") - : !/^https?:\/\//.test(baseURL) - ? language.t("provider.custom.error.baseURL.format") - : undefined - - const disabled = (globalSync.data.config.disabled_providers ?? []).includes(providerID) - const existingProvider = globalSync.data.provider.all.find((p) => p.id === providerID) - const existsError = idError - ? undefined - : existingProvider && !disabled - ? language.t("provider.custom.error.providerID.exists") - : undefined - - const seenModels = new Set() - const modelErrors = form.models.map((m) => { - const id = m.id.trim() - const modelIdError = !id - ? language.t("provider.custom.error.required") - : seenModels.has(id) - ? language.t("provider.custom.error.duplicate") - : (() => { - seenModels.add(id) - return undefined - })() - const modelNameError = !m.name.trim() ? language.t("provider.custom.error.required") : undefined - return { id: modelIdError, name: modelNameError } + const output = validateCustomProvider({ + form, + t: language.t, + disabledProviders: globalSync.data.config.disabled_providers ?? [], + existingProviderIDs: new Set(globalSync.data.provider.all.map((p) => p.id)), }) - const modelsValid = modelErrors.every((m) => !m.id && !m.name) - const models = Object.fromEntries(form.models.map((m) => [m.id.trim(), { name: m.name.trim() }])) - - const seenHeaders = new Set() - const headerErrors = form.headers.map((h) => { - const key = h.key.trim() - const value = h.value.trim() - - if (!key && !value) return {} - const keyError = !key - ? language.t("provider.custom.error.required") - : seenHeaders.has(key.toLowerCase()) - ? language.t("provider.custom.error.duplicate") - : (() => { - seenHeaders.add(key.toLowerCase()) - return undefined - })() - const valueError = !value ? language.t("provider.custom.error.required") : undefined - return { key: keyError, value: valueError } - }) - const headersValid = headerErrors.every((h) => !h.key && !h.value) - const headers = Object.fromEntries( - form.headers - .map((h) => ({ key: h.key.trim(), value: h.value.trim() })) - .filter((h) => !!h.key && !!h.value) - .map((h) => [h.key, h.value]), - ) - - setErrors( - produce((draft) => { - draft.providerID = idError ?? existsError - draft.name = nameError - draft.baseURL = urlError - draft.models = modelErrors - draft.headers = headerErrors - }), - ) - - const ok = !idError && !existsError && !nameError && !urlError && modelsValid && headersValid - if (!ok) return - - const options = { - baseURL, - ...(Object.keys(headers).length ? { headers } : {}), - } - - return { - providerID, - name, - key, - config: { - npm: OPENAI_COMPATIBLE, - name, - ...(env ? { env: [env] } : {}), - options, - models, - }, - } + setErrors(output.errors) + return output.result } const save = async (e: SubmitEvent) => { @@ -297,7 +305,7 @@ export function DialogCustomProvider(props: Props) { placeholder={language.t("provider.custom.field.providerID.placeholder")} description={language.t("provider.custom.field.providerID.description")} value={form.providerID} - onChange={setForm.bind(null, "providerID")} + onChange={(v) => setForm("providerID", v)} validationState={errors.providerID ? "invalid" : undefined} error={errors.providerID} /> @@ -305,7 +313,7 @@ export function DialogCustomProvider(props: Props) { label={language.t("provider.custom.field.name.label")} placeholder={language.t("provider.custom.field.name.placeholder")} value={form.name} - onChange={setForm.bind(null, "name")} + onChange={(v) => setForm("name", v)} validationState={errors.name ? "invalid" : undefined} error={errors.name} /> @@ -313,7 +321,7 @@ export function DialogCustomProvider(props: Props) { label={language.t("provider.custom.field.baseURL.label")} placeholder={language.t("provider.custom.field.baseURL.placeholder")} value={form.baseURL} - onChange={setForm.bind(null, "baseURL")} + onChange={(v) => setForm("baseURL", v)} validationState={errors.baseURL ? "invalid" : undefined} error={errors.baseURL} /> @@ -322,7 +330,7 @@ export function DialogCustomProvider(props: Props) { placeholder={language.t("provider.custom.field.apiKey.placeholder")} description={language.t("provider.custom.field.apiKey.description")} value={form.apiKey} - onChange={setForm.bind(null, "apiKey")} + onChange={(v) => setForm("apiKey", v)} />
diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index 622daee7a3e5..ec0793c540ee 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -33,6 +33,8 @@ export function DialogEditProject(props: { project: LocalProject }) { iconHover: false, }) + let iconInput: HTMLInputElement | undefined + function handleFileSelect(file: File) { if (!file.type.startsWith("image/")) return const reader = new FileReader() @@ -72,31 +74,35 @@ export function DialogEditProject(props: { project: LocalProject }) { async function handleSubmit(e: SubmitEvent) { e.preventDefault() - setStore("saving", true) - const name = store.name.trim() === folderName() ? "" : store.name.trim() - const start = store.startup.trim() - - if (props.project.id && props.project.id !== "global") { - await globalSDK.client.project.update({ - projectID: props.project.id, - directory: props.project.worktree, - name, - icon: { color: store.color, override: store.iconUrl }, - commands: { start }, - }) - globalSync.project.icon(props.project.worktree, store.iconUrl || undefined) - setStore("saving", false) - dialog.close() - return - } + await Promise.resolve() + .then(async () => { + setStore("saving", true) + const name = store.name.trim() === folderName() ? "" : store.name.trim() + const start = store.startup.trim() - globalSync.project.meta(props.project.worktree, { - name, - icon: { color: store.color, override: store.iconUrl || undefined }, - commands: { start: start || undefined }, - }) - setStore("saving", false) - dialog.close() + if (props.project.id && props.project.id !== "global") { + await globalSDK.client.project.update({ + projectID: props.project.id, + directory: props.project.worktree, + name, + icon: { color: store.color, override: store.iconUrl }, + commands: { start }, + }) + globalSync.project.icon(props.project.worktree, store.iconUrl || undefined) + dialog.close() + return + } + + globalSync.project.meta(props.project.worktree, { + name, + icon: { color: store.color, override: store.iconUrl || undefined }, + commands: { start: start || undefined }, + }) + dialog.close() + }) + .finally(() => { + setStore("saving", false) + }) } return ( @@ -134,7 +140,7 @@ export function DialogEditProject(props: { project: LocalProject }) { if (store.iconUrl && store.iconHover) { clearIcon() } else { - document.getElementById("icon-upload")?.click() + iconInput?.click() } }} > @@ -176,7 +182,16 @@ export function DialogEditProject(props: { project: LocalProject }) {
- + { + iconInput = el + }} + type="file" + accept="image/*" + class="hidden" + onChange={handleInputChange} + />
{language.t("dialog.project.edit.icon.hint")} {language.t("dialog.project.edit.icon.recommended")} @@ -223,7 +238,7 @@ export function DialogEditProject(props: { project: LocalProject }) { value={store.startup} onChange={(v) => setStore("startup", v)} spellcheck={false} - class="max-h-40 w-full font-mono text-xs no-scrollbar" + class="max-h-14 w-full overflow-y-auto font-mono text-xs" />
diff --git a/packages/app/src/components/dialog-fork.tsx b/packages/app/src/components/dialog-fork.tsx index 09d62021f21c..8810955cc655 100644 --- a/packages/app/src/components/dialog-fork.tsx +++ b/packages/app/src/components/dialog-fork.tsx @@ -6,6 +6,7 @@ import { usePrompt } from "@/context/prompt" import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" +import { showToast } from "@opencode-ai/ui/toast" import { extractPromptFromParts } from "@/utils/prompt" import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client" import { base64Encode } from "@opencode-ai/util/encode" @@ -66,15 +67,23 @@ export const DialogFork: Component = () => { attachmentName: language.t("common.attachment"), }) - dialog.close() - - sdk.client.session.fork({ sessionID, messageID: item.id }).then((forked) => { - if (!forked.data) return - navigate(`/${base64Encode(sdk.directory)}/session/${forked.data.id}`) - requestAnimationFrame(() => { - prompt.set(restored) + sdk.client.session + .fork({ sessionID, messageID: item.id }) + .then((forked) => { + if (!forked.data) { + showToast({ title: language.t("common.requestFailed") }) + return + } + dialog.close() + navigate(`/${base64Encode(sdk.directory)}/session/${forked.data.id}`) + requestAnimationFrame(() => { + prompt.set(restored) + }) + }) + .catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err) + showToast({ title: language.t("common.requestFailed"), description: message }) }) - }) } return ( diff --git a/packages/app/src/components/dialog-manage-models.tsx b/packages/app/src/components/dialog-manage-models.tsx index 9ee48736ca06..ace79e38a7c0 100644 --- a/packages/app/src/components/dialog-manage-models.tsx +++ b/packages/app/src/components/dialog-manage-models.tsx @@ -1,6 +1,7 @@ import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" import { Switch } from "@opencode-ai/ui/switch" +import { Tooltip } from "@opencode-ai/ui/tooltip" import { Button } from "@opencode-ai/ui/button" import type { Component } from "solid-js" import { useLocal } from "@/context/local" @@ -17,6 +18,15 @@ export const DialogManageModels: Component = () => { const handleConnectProvider = () => { dialog.show(() => ) } + const providerRank = (id: string) => popularProviders.indexOf(id) + const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID) + const providerVisible = (providerID: string) => + providerList(providerID).every((x) => local.model.visible({ modelID: x.id, providerID: x.provider.id })) + const setProviderVisibility = (providerID: string, checked: boolean) => { + providerList(providerID).forEach((x) => { + local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, checked) + }) + } return ( { items={local.model.list()} filterKeys={["provider.name", "name", "id"]} sortBy={(a, b) => a.name.localeCompare(b.name)} - groupBy={(x) => x.provider.name} + groupBy={(x) => x.provider.id} + groupHeader={(group) => { + const provider = group.items[0].provider + return ( + <> + {provider.name} + + setProviderVisibility(provider.id, checked)} + hideLabel + > + {provider.name} + + + + ) + }} sortGroupsBy={(a, b) => { - const aProvider = a.items[0].provider.id - const bProvider = b.items[0].provider.id - if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1 - if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1 - return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider) + const aRank = providerRank(a.items[0].provider.id) + const bRank = providerRank(b.items[0].provider.id) + const aPopular = aRank >= 0 + const bPopular = bRank >= 0 + if (aPopular && !bPopular) return -1 + if (!aPopular && bPopular) return 1 + return aRank - bRank }} onSelect={(x) => { if (!x) return - const visible = local.model.visible({ - modelID: x.id, - providerID: x.provider.id, - }) - local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, !visible) + const key = { modelID: x.id, providerID: x.provider.id } + local.model.setVisibility(key, !local.model.visible(key)) }} > {(i) => ( @@ -57,12 +87,7 @@ export const DialogManageModels: Component = () => { {i.name}
e.stopPropagation()}> { local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked) }} diff --git a/packages/app/src/components/dialog-release-notes.tsx b/packages/app/src/components/dialog-release-notes.tsx index c6f2f3930e2d..2040009a8c3c 100644 --- a/packages/app/src/components/dialog-release-notes.tsx +++ b/packages/app/src/components/dialog-release-notes.tsx @@ -1,4 +1,4 @@ -import { createSignal, createEffect, onMount, onCleanup } from "solid-js" +import { createSignal } from "solid-js" import { Dialog } from "@opencode-ai/ui/dialog" import { Button } from "@opencode-ai/ui/button" import { useDialog } from "@opencode-ai/ui/context/dialog" @@ -40,8 +40,6 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) { handleClose() } - let focusTrap: HTMLDivElement | undefined - function handleKeyDown(e: KeyboardEvent) { if (e.key === "Escape") { e.preventDefault() @@ -60,27 +58,13 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) { } } - onMount(() => { - focusTrap?.focus() - document.addEventListener("keydown", handleKeyDown) - onCleanup(() => document.removeEventListener("keydown", handleKeyDown)) - }) - - // Refocus the trap when index changes to ensure escape always works - createEffect(() => { - index() // track index - focusTrap?.focus() - }) - return ( - {/* Hidden element to capture initial focus and handle escape */} -
-
+
{/* Left side - Text content */}
{/* Top section - feature content (fixed position from top) */} diff --git a/packages/app/src/components/dialog-select-directory.tsx b/packages/app/src/components/dialog-select-directory.tsx index 6e7af3d902d8..515e640c9fab 100644 --- a/packages/app/src/components/dialog-select-directory.tsx +++ b/packages/app/src/components/dialog-select-directory.tsx @@ -2,13 +2,13 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" import { FileIcon } from "@opencode-ai/ui/file-icon" import { List } from "@opencode-ai/ui/list" +import type { ListRef } from "@opencode-ai/ui/list" import { getDirectory, getFilename } from "@opencode-ai/util/path" import fuzzysort from "fuzzysort" import { createMemo, createResource, createSignal } from "solid-js" import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" -import type { ListRef } from "@opencode-ai/ui/list" interface DialogSelectDirectoryProps { title?: string @@ -21,157 +21,131 @@ type Row = { search: string } -export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { - const sync = useGlobalSync() - const sdk = useGlobalSDK() - const dialog = useDialog() - const language = useLanguage() - - const [filter, setFilter] = createSignal("") - - let list: ListRef | undefined - - const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory)) - - const [fallbackPath] = createResource( - () => (missingBase() ? true : undefined), - async () => { - return sdk.client.path - .get() - .then((x) => x.data) - .catch(() => undefined) - }, - { initialValue: undefined }, - ) - - const home = createMemo(() => sync.data.path.home || fallbackPath()?.home || "") - - const start = createMemo( - () => sync.data.path.home || sync.data.path.directory || fallbackPath()?.home || fallbackPath()?.directory, - ) - - const cache = new Map>>() +function cleanInput(value: string) { + const first = (value ?? "").split(/\r?\n/)[0] ?? "" + return first.replace(/[\u0000-\u001F\u007F]/g, "").trim() +} - const clean = (value: string) => { - const first = (value ?? "").split(/\r?\n/)[0] ?? "" - return first.replace(/[\u0000-\u001F\u007F]/g, "").trim() - } +function normalizePath(input: string) { + const v = input.replaceAll("\\", "/") + if (v.startsWith("//") && !v.startsWith("///")) return "//" + v.slice(2).replace(/\/+/g, "/") + return v.replace(/\/+/g, "/") +} - function normalize(input: string) { - const v = input.replaceAll("\\", "/") - if (v.startsWith("//") && !v.startsWith("///")) return "//" + v.slice(2).replace(/\/+/g, "/") - return v.replace(/\/+/g, "/") - } +function normalizeDriveRoot(input: string) { + const v = normalizePath(input) + if (/^[A-Za-z]:$/.test(v)) return v + "/" + return v +} - function normalizeDriveRoot(input: string) { - const v = normalize(input) - if (/^[A-Za-z]:$/.test(v)) return v + "/" - return v - } +function trimTrailing(input: string) { + const v = normalizeDriveRoot(input) + if (v === "/") return v + if (v === "//") return v + if (/^[A-Za-z]:\/$/.test(v)) return v + return v.replace(/\/+$/, "") +} - function trimTrailing(input: string) { - const v = normalizeDriveRoot(input) - if (v === "/") return v - if (v === "//") return v - if (/^[A-Za-z]:\/$/.test(v)) return v - return v.replace(/\/+$/, "") - } +function joinPath(base: string | undefined, rel: string) { + const b = trimTrailing(base ?? "") + const r = trimTrailing(rel).replace(/^\/+/, "") + if (!b) return r + if (!r) return b + if (b.endsWith("/")) return b + r + return b + "/" + r +} - function join(base: string | undefined, rel: string) { - const b = trimTrailing(base ?? "") - const r = trimTrailing(rel).replace(/^\/+/, "") - if (!b) return r - if (!r) return b - if (b.endsWith("/")) return b + r - return b + "/" + r - } +function rootOf(input: string) { + const v = normalizeDriveRoot(input) + if (v.startsWith("//")) return "//" + if (v.startsWith("/")) return "/" + if (/^[A-Za-z]:\//.test(v)) return v.slice(0, 3) + return "" +} - function rootOf(input: string) { - const v = normalizeDriveRoot(input) - if (v.startsWith("//")) return "//" - if (v.startsWith("/")) return "/" - if (/^[A-Za-z]:\//.test(v)) return v.slice(0, 3) - return "" - } +function parentOf(input: string) { + const v = trimTrailing(input) + if (v === "/") return v + if (v === "//") return v + if (/^[A-Za-z]:\/$/.test(v)) return v - function parentOf(input: string) { - const v = trimTrailing(input) - if (v === "/") return v - if (v === "//") return v - if (/^[A-Za-z]:\/$/.test(v)) return v + const i = v.lastIndexOf("/") + if (i <= 0) return "/" + if (i === 2 && /^[A-Za-z]:/.test(v)) return v.slice(0, 3) + return v.slice(0, i) +} - const i = v.lastIndexOf("/") - if (i <= 0) return "/" - if (i === 2 && /^[A-Za-z]:/.test(v)) return v.slice(0, 3) - return v.slice(0, i) - } +function modeOf(input: string) { + const raw = normalizeDriveRoot(input.trim()) + if (!raw) return "relative" as const + if (raw.startsWith("~")) return "tilde" as const + if (rootOf(raw)) return "absolute" as const + return "relative" as const +} - function modeOf(input: string) { - const raw = normalizeDriveRoot(input.trim()) - if (!raw) return "relative" as const - if (raw.startsWith("~")) return "tilde" as const - if (rootOf(raw)) return "absolute" as const - return "relative" as const - } +function tildeOf(absolute: string, home: string) { + const full = trimTrailing(absolute) + if (!home) return "" - function display(path: string, input: string) { - const full = trimTrailing(path) - if (modeOf(input) === "absolute") return full + const hn = trimTrailing(home) + const lc = full.toLowerCase() + const hc = hn.toLowerCase() + if (lc === hc) return "~" + if (lc.startsWith(hc + "/")) return "~" + full.slice(hn.length) + return "" +} - return tildeOf(full) || full - } +function displayPath(path: string, input: string, home: string) { + const full = trimTrailing(path) + if (modeOf(input) === "absolute") return full + return tildeOf(full, home) || full +} - function tildeOf(absolute: string) { - const full = trimTrailing(absolute) - const h = home() - if (!h) return "" - - const hn = trimTrailing(h) - const lc = full.toLowerCase() - const hc = hn.toLowerCase() - if (lc === hc) return "~" - if (lc.startsWith(hc + "/")) return "~" + full.slice(hn.length) - return "" +function toRow(absolute: string, home: string): Row { + const full = trimTrailing(absolute) + const tilde = tildeOf(full, home) + const withSlash = (value: string) => { + if (!value) return "" + if (value.endsWith("/")) return value + return value + "/" } - function row(absolute: string): Row { - const full = trimTrailing(absolute) - const tilde = tildeOf(full) - - const withSlash = (value: string) => { - if (!value) return "" - if (value.endsWith("/")) return value - return value + "/" - } + const search = Array.from( + new Set([full, withSlash(full), tilde, withSlash(tilde), getFilename(full)].filter(Boolean)), + ).join("\n") + return { absolute: full, search } +} - const search = Array.from( - new Set([full, withSlash(full), tilde, withSlash(tilde), getFilename(full)].filter(Boolean)), - ).join("\n") - return { absolute: full, search } - } +function useDirectorySearch(args: { + sdk: ReturnType + start: () => string | undefined + home: () => string +}) { + const cache = new Map>>() + let current = 0 - function scoped(value: string) { - const base = start() + const scoped = (value: string) => { + const base = args.start() if (!base) return const raw = normalizeDriveRoot(value) if (!raw) return { directory: trimTrailing(base), path: "" } - const h = home() - if (raw === "~") return { directory: trimTrailing(h ?? base), path: "" } - if (raw.startsWith("~/")) return { directory: trimTrailing(h ?? base), path: raw.slice(2) } + const h = args.home() + if (raw === "~") return { directory: trimTrailing(h || base), path: "" } + if (raw.startsWith("~/")) return { directory: trimTrailing(h || base), path: raw.slice(2) } const root = rootOf(raw) if (root) return { directory: trimTrailing(root), path: raw.slice(root.length) } return { directory: trimTrailing(base), path: raw } } - async function dirs(dir: string) { + const dirs = async (dir: string) => { const key = trimTrailing(dir) const existing = cache.get(key) if (existing) return existing - const request = sdk.client.file + const request = args.sdk.client.file .list({ directory: key, path: "" }) .then((x) => x.data ?? []) .catch(() => []) @@ -188,32 +162,34 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { return request } - async function match(dir: string, query: string, limit: number) { + const match = async (dir: string, query: string, limit: number) => { const items = await dirs(dir) if (!query) return items.slice(0, limit).map((x) => x.absolute) return fuzzysort.go(query, items, { key: "name", limit }).map((x) => x.obj.absolute) } - const directories = async (filter: string) => { - const value = clean(filter) + return async (filter: string) => { + const token = ++current + const active = () => token === current + + const value = cleanInput(filter) const scopedInput = scoped(value) if (!scopedInput) return [] as string[] const raw = normalizeDriveRoot(value) const isPath = raw.startsWith("~") || !!rootOf(raw) || raw.includes("/") - const query = normalizeDriveRoot(scopedInput.path) const find = () => - sdk.client.find + args.sdk.client.find .files({ directory: scopedInput.directory, query, type: "directory", limit: 50 }) .then((x) => x.data ?? []) .catch(() => []) if (!isPath) { const results = await find() - - return results.map((rel) => join(scopedInput.directory, rel)).slice(0, 50) + if (!active()) return [] + return results.map((rel) => joinPath(scopedInput.directory, rel)).slice(0, 50) } const segments = query.replace(/^\/+/, "").split("/") @@ -224,17 +200,20 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { const branch = 4 let paths = [scopedInput.directory] for (const part of head) { + if (!active()) return [] if (part === "..") { paths = paths.map(parentOf) continue } const next = (await Promise.all(paths.map((p) => match(p, part, branch)))).flat() + if (!active()) return [] paths = Array.from(new Set(next)).slice(0, cap) if (paths.length === 0) return [] as string[] } const out = (await Promise.all(paths.map((p) => match(p, tail, 50)))).flat() + if (!active()) return [] const deduped = Array.from(new Set(out)) const base = raw.startsWith("~") ? trimTrailing(scopedInput.directory) : "" const expand = !raw.endsWith("/") @@ -249,13 +228,47 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { if (!target) return deduped.slice(0, 50) const children = await match(target, "", 30) + if (!active()) return [] const items = Array.from(new Set([...deduped, ...children])) return (base ? Array.from(new Set([base, ...items])) : items).slice(0, 50) } +} + +export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { + const sync = useGlobalSync() + const sdk = useGlobalSDK() + const dialog = useDialog() + const language = useLanguage() + + const [filter, setFilter] = createSignal("") + let list: ListRef | undefined + + const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory)) + const [fallbackPath] = createResource( + () => (missingBase() ? true : undefined), + async () => { + return sdk.client.path + .get() + .then((x) => x.data) + .catch(() => undefined) + }, + { initialValue: undefined }, + ) + + const home = createMemo(() => sync.data.path.home || fallbackPath()?.home || "") + const start = createMemo( + () => sync.data.path.home || sync.data.path.directory || fallbackPath()?.home || fallbackPath()?.directory, + ) + + const directories = useDirectorySearch({ + sdk, + home, + start, + }) const items = async (value: string) => { const results = await directories(value) - return results.map(row) + return results.map((absolute) => toRow(absolute, home())) } function resolve(absolute: string) { @@ -273,7 +286,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { key={(x) => x.absolute} filterKeys={["search"]} ref={(r) => (list = r)} - onFilter={(value) => setFilter(clean(value))} + onFilter={(value) => setFilter(cleanInput(value))} onKeyEvent={(e, item) => { if (e.key !== "Tab") return if (e.shiftKey) return @@ -282,7 +295,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { e.preventDefault() e.stopPropagation() - const value = display(item.absolute, filter()) + const value = displayPath(item.absolute, filter(), home()) list?.setFilter(value.endsWith("/") ? value : value + "/") }} onSelect={(path) => { @@ -291,7 +304,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { }} > {(item) => { - const path = display(item.absolute, filter()) + const path = displayPath(item.absolute, filter(), home()) if (path === "~") { return (
diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 8e221577b909..29a3666c034b 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -36,197 +36,200 @@ type Entry = { type DialogSelectFileMode = "all" | "files" -export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) { - const command = useCommand() - const language = useLanguage() - const layout = useLayout() - const file = useFile() - const dialog = useDialog() - const params = useParams() - const navigate = useNavigate() - const globalSDK = useGlobalSDK() - const globalSync = useGlobalSync() - const filesOnly = () => props.mode === "files" - const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) - const tabs = createMemo(() => layout.tabs(sessionKey)) - const view = createMemo(() => layout.view(sessionKey)) - const state = { cleanup: undefined as (() => void) | void, committed: false } - const [grouped, setGrouped] = createSignal(false) - const common = [ - "session.new", - "workspace.new", - "session.previous", - "session.next", - "terminal.toggle", - "review.toggle", - ] - const limit = 5 - - const allowed = createMemo(() => { - if (filesOnly()) return [] - return command.options.filter( - (option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open", - ) - }) - - const commandItem = (option: CommandOption): Entry => ({ - id: "command:" + option.id, - type: "command", - title: option.title, - description: option.description, - keybind: option.keybind, - category: language.t("palette.group.commands"), - option, - }) - - const fileItem = (path: string): Entry => ({ - id: "file:" + path, - type: "file", - title: path, - category: language.t("palette.group.files"), - path, - }) - - const projectDirectory = createMemo(() => decode64(params.dir) ?? "") - const project = createMemo(() => { - const directory = projectDirectory() - if (!directory) return - return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory)) - }) - const workspaces = createMemo(() => { - const directory = projectDirectory() - const current = project() - if (!current) return directory ? [directory] : [] - - const dirs = [current.worktree, ...(current.sandboxes ?? [])] - if (directory && !dirs.includes(directory)) return [...dirs, directory] - return dirs - }) - const homedir = createMemo(() => globalSync.data.path.home) - const label = (directory: string) => { - const current = project() - const kind = - current && directory === current.worktree - ? language.t("workspace.type.local") - : language.t("workspace.type.sandbox") - const [store] = globalSync.child(directory, { bootstrap: false }) - const home = homedir() - const path = home ? directory.replace(home, "~") : directory - const name = store.vcs?.branch ?? getFilename(directory) - return `${kind} : ${name || path}` +const ENTRY_LIMIT = 5 +const COMMON_COMMAND_IDS = [ + "session.new", + "workspace.new", + "session.previous", + "session.next", + "terminal.toggle", + "review.toggle", +] as const + +const uniqueEntries = (items: Entry[]) => { + const seen = new Set() + const out: Entry[] = [] + for (const item of items) { + if (seen.has(item.id)) continue + seen.add(item.id) + out.push(item) } + return out +} - const sessionItem = (input: { +const createCommandEntry = (option: CommandOption, category: string): Entry => ({ + id: "command:" + option.id, + type: "command", + title: option.title, + description: option.description, + keybind: option.keybind, + category, + option, +}) + +const createFileEntry = (path: string, category: string): Entry => ({ + id: "file:" + path, + type: "file", + title: path, + category, + path, +}) + +const createSessionEntry = ( + input: { directory: string id: string title: string description: string archived?: number updated?: number - }): Entry => ({ - id: `session:${input.directory}:${input.id}`, - type: "session", - title: input.title, - description: input.description, - category: language.t("command.category.session"), - directory: input.directory, - sessionID: input.id, - archived: input.archived, - updated: input.updated, + }, + category: string, +): Entry => ({ + id: `session:${input.directory}:${input.id}`, + type: "session", + title: input.title, + description: input.description, + category, + directory: input.directory, + sessionID: input.id, + archived: input.archived, + updated: input.updated, +}) + +function createCommandEntries(props: { + filesOnly: () => boolean + command: ReturnType + language: ReturnType +}) { + const allowed = createMemo(() => { + if (props.filesOnly()) return [] + return props.command.options.filter( + (option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open", + ) }) - const list = createMemo(() => allowed().map(commandItem)) + const list = createMemo(() => { + const category = props.language.t("palette.group.commands") + return allowed().map((option) => createCommandEntry(option, category)) + }) const picks = createMemo(() => { const all = allowed() - const order = new Map(common.map((id, index) => [id, index])) + const order = new Map(COMMON_COMMAND_IDS.map((id, index) => [id, index])) const picked = all.filter((option) => order.has(option.id)) - const base = picked.length ? picked : all.slice(0, limit) + const base = picked.length ? picked : all.slice(0, ENTRY_LIMIT) const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base - return sorted.map(commandItem) + const category = props.language.t("palette.group.commands") + return sorted.map((option) => createCommandEntry(option, category)) }) + return { allowed, list, picks } +} + +function createFileEntries(props: { + file: ReturnType + tabs: () => ReturnType["tabs"]> + language: ReturnType +}) { const recent = createMemo(() => { - const all = tabs().all() - const active = tabs().active() + const all = props.tabs().all() + const active = props.tabs().active() const order = active ? [active, ...all.filter((item) => item !== active)] : all const seen = new Set() + const category = props.language.t("palette.group.files") const items: Entry[] = [] for (const item of order) { - const path = file.pathFromTab(item) + const path = props.file.pathFromTab(item) if (!path) continue if (seen.has(path)) continue seen.add(path) - items.push(fileItem(path)) + items.push(createFileEntry(path, category)) } - return items.slice(0, limit) + return items.slice(0, ENTRY_LIMIT) }) const root = createMemo(() => { - const nodes = file.tree.children("") + const category = props.language.t("palette.group.files") + const nodes = props.file.tree.children("") const paths = nodes .filter((node) => node.type === "file") .map((node) => node.path) .sort((a, b) => a.localeCompare(b)) - return paths.slice(0, limit).map(fileItem) + return paths.slice(0, ENTRY_LIMIT).map((path) => createFileEntry(path, category)) }) - const unique = (items: Entry[]) => { - const seen = new Set() - const out: Entry[] = [] - for (const item of items) { - if (seen.has(item.id)) continue - seen.add(item.id) - out.push(item) - } - return out - } + return { recent, root } +} - const sessionToken = { value: 0 } - let sessionInflight: Promise | undefined - let sessionAll: Entry[] | undefined +function createSessionEntries(props: { + workspaces: () => string[] + label: (directory: string) => string + globalSDK: ReturnType + language: ReturnType +}) { + const state: { + token: number + inflight: Promise | undefined + cached: Entry[] | undefined + } = { + token: 0, + inflight: undefined, + cached: undefined, + } const sessions = (text: string) => { const query = text.trim() if (!query) { - sessionToken.value += 1 - sessionInflight = undefined - sessionAll = undefined + state.token += 1 + state.inflight = undefined + state.cached = undefined return [] as Entry[] } - if (sessionAll) return sessionAll - if (sessionInflight) return sessionInflight + if (state.cached) return state.cached + if (state.inflight) return state.inflight - const current = sessionToken.value - const dirs = workspaces() + const current = state.token + const dirs = props.workspaces() if (dirs.length === 0) return [] as Entry[] - sessionInflight = Promise.all( + state.inflight = Promise.all( dirs.map((directory) => { - const description = label(directory) - return globalSDK.client.session + const description = props.label(directory) + return props.globalSDK.client.session .list({ directory, roots: true }) .then((x) => (x.data ?? []) .filter((s) => !!s?.id) .map((s) => ({ id: s.id, - title: s.title ?? language.t("command.session.new"), + title: s.title ?? props.language.t("command.session.new"), description, directory, archived: s.time?.archived, updated: s.time?.updated, })), ) - .catch(() => [] as { id: string; title: string; description: string; directory: string; archived?: number }[]) + .catch( + () => + [] as { + id: string + title: string + description: string + directory: string + archived?: number + updated?: number + }[], + ) }), ) .then((results) => { - if (sessionToken.value !== current) return [] as Entry[] + if (state.token !== current) return [] as Entry[] const seen = new Set() + const category = props.language.t("command.category.session") const next = results .flat() .filter((item) => { @@ -235,18 +238,71 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil seen.add(key) return true }) - .map(sessionItem) - sessionAll = next + .map((item) => createSessionEntry(item, category)) + state.cached = next return next }) .catch(() => [] as Entry[]) .finally(() => { - sessionInflight = undefined + state.inflight = undefined }) - return sessionInflight + return state.inflight } + return { sessions } +} + +export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) { + const command = useCommand() + const language = useLanguage() + const layout = useLayout() + const file = useFile() + const dialog = useDialog() + const params = useParams() + const navigate = useNavigate() + const globalSDK = useGlobalSDK() + const globalSync = useGlobalSync() + const filesOnly = () => props.mode === "files" + const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) + const tabs = createMemo(() => layout.tabs(sessionKey)) + const view = createMemo(() => layout.view(sessionKey)) + const state = { cleanup: undefined as (() => void) | void, committed: false } + const [grouped, setGrouped] = createSignal(false) + const commandEntries = createCommandEntries({ filesOnly, command, language }) + const fileEntries = createFileEntries({ file, tabs, language }) + + const projectDirectory = createMemo(() => decode64(params.dir) ?? "") + const project = createMemo(() => { + const directory = projectDirectory() + if (!directory) return + return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory)) + }) + const workspaces = createMemo(() => { + const directory = projectDirectory() + const current = project() + if (!current) return directory ? [directory] : [] + + const dirs = [current.worktree, ...(current.sandboxes ?? [])] + if (directory && !dirs.includes(directory)) return [...dirs, directory] + return dirs + }) + const homedir = createMemo(() => globalSync.data.path.home) + const label = (directory: string) => { + const current = project() + const kind = + current && directory === current.worktree + ? language.t("workspace.type.local") + : language.t("workspace.type.sandbox") + const [store] = globalSync.child(directory, { bootstrap: false }) + const home = homedir() + const path = home ? directory.replace(home, "~") : directory + const name = store.vcs?.branch ?? getFilename(directory) + return `${kind} : ${name || path}` + } + + const { sessions } = createSessionEntries({ workspaces, label, globalSDK, language }) + const items = async (text: string) => { const query = text.trim() setGrouped(query.length > 0) @@ -254,7 +310,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil if (!query && filesOnly()) { const loaded = file.tree.state("")?.loaded const pending = loaded ? Promise.resolve() : file.tree.list("") - const next = unique([...recent(), ...root()]) + const next = uniqueEntries([...fileEntries.recent(), ...fileEntries.root()]) if (loaded || next.length > 0) { void pending @@ -262,19 +318,21 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil } await pending - return unique([...recent(), ...root()]) + return uniqueEntries([...fileEntries.recent(), ...fileEntries.root()]) } - if (!query) return [...picks(), ...recent()] + if (!query) return [...commandEntries.picks(), ...fileEntries.recent()] if (filesOnly()) { const files = await file.searchFiles(query) - return files.map(fileItem) + const category = language.t("palette.group.files") + return files.map((path) => createFileEntry(path, category)) } const [files, nextSessions] = await Promise.all([file.searchFiles(query), Promise.resolve(sessions(query))]) - const entries = files.map(fileItem) - return [...list(), ...nextSessions, ...entries] + const category = language.t("palette.group.files") + const entries = files.map((path) => createFileEntry(path, category)) + return [...commandEntries.list(), ...nextSessions, ...entries] } const handleMove = (item: Entry | undefined) => { @@ -289,9 +347,9 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil tabs().open(value) file.load(path) if (!view().reviewPanel.opened()) view().reviewPanel.open() - layout.fileTree.open() layout.fileTree.setTab("all") props.onOpenFile?.(path) + tabs().setActive(value) } const handleSelect = (item: Entry | undefined) => { diff --git a/packages/app/src/components/dialog-select-mcp.tsx b/packages/app/src/components/dialog-select-mcp.tsx index 8eb088789124..f8913eee4fbc 100644 --- a/packages/app/src/components/dialog-select-mcp.tsx +++ b/packages/app/src/components/dialog-select-mcp.tsx @@ -6,6 +6,13 @@ import { List } from "@opencode-ai/ui/list" import { Switch } from "@opencode-ai/ui/switch" import { useLanguage } from "@/context/language" +const statusLabels = { + connected: "mcp.status.connected", + failed: "mcp.status.failed", + needs_auth: "mcp.status.needs_auth", + disabled: "mcp.status.disabled", +} as const + export const DialogSelectMcp: Component = () => { const sync = useSync() const sdk = useSDK() @@ -21,15 +28,19 @@ export const DialogSelectMcp: Component = () => { const toggle = async (name: string) => { if (loading()) return setLoading(name) - const status = sync.data.mcp[name] - if (status?.status === "connected") { - await sdk.client.mcp.disconnect({ name }) - } else { - await sdk.client.mcp.connect({ name }) + try { + const status = sync.data.mcp[name] + if (status?.status === "connected") { + await sdk.client.mcp.disconnect({ name }) + } else { + await sdk.client.mcp.connect({ name }) + } + + const result = await sdk.client.mcp.status() + if (result.data) sync.set("mcp", result.data) + } finally { + setLoading(null) } - const result = await sdk.client.mcp.status() - if (result.data) sync.set("mcp", result.data) - setLoading(null) } const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length) @@ -54,6 +65,11 @@ export const DialogSelectMcp: Component = () => { {(i) => { const mcpStatus = () => sync.data.mcp[i.name] const status = () => mcpStatus()?.status + const statusLabel = () => { + const key = status() ? statusLabels[status() as keyof typeof statusLabels] : undefined + if (!key) return + return language.t(key) + } const error = () => { const s = mcpStatus() return s?.status === "failed" ? s.error : undefined @@ -64,17 +80,8 @@ export const DialogSelectMcp: Component = () => {
{i.name} - - {language.t("mcp.status.connected")} - - - {language.t("mcp.status.failed")} - - - {language.t("mcp.status.needs_auth")} - - - {language.t("mcp.status.disabled")} + + {statusLabel()} {language.t("common.loading.ellipsis")} diff --git a/packages/app/src/components/dialog-select-model-unpaid.tsx b/packages/app/src/components/dialog-select-model-unpaid.tsx index 78c169777e0d..af788d05b03c 100644 --- a/packages/app/src/components/dialog-select-model-unpaid.tsx +++ b/packages/app/src/components/dialog-select-model-unpaid.tsx @@ -6,7 +6,7 @@ import { List, type ListRef } from "@opencode-ai/ui/list" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { Tag } from "@opencode-ai/ui/tag" import { Tooltip } from "@opencode-ai/ui/tooltip" -import { type Component, onCleanup, onMount, Show } from "solid-js" +import { type Component, Show } from "solid-js" import { useLocal } from "@/context/local" import { popularProviders, useProviders } from "@/hooks/use-providers" import { DialogConnectProvider } from "./dialog-connect-provider" @@ -21,24 +21,17 @@ export const DialogSelectModelUnpaid: Component = () => { const language = useLanguage() let listRef: ListRef | undefined - const handleKey = (e: KeyboardEvent) => { + const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") return listRef?.onKeyDown(e) } - onMount(() => { - document.addEventListener("keydown", handleKey) - onCleanup(() => { - document.removeEventListener("keydown", handleKey) - }) - }) - return ( -
+
{language.t("dialog.model.unpaid.freeModels.title")}
+ provider === "opencode" && (!cost || cost.input === 0) + const ModelList: Component<{ provider?: string class?: string @@ -54,13 +57,7 @@ const ModelList: Component<{ class="w-full" placement="right-start" gutter={12} - value={ - - } + value={} > {node} @@ -75,7 +72,7 @@ const ModelList: Component<{ {(i) => (
{i.name} - + {language.t("model.tag.free")} @@ -98,13 +95,9 @@ export function ModelSelectorPopover(props: { const [store, setStore] = createStore<{ open: boolean dismiss: "escape" | "outside" | null - trigger?: HTMLElement - content?: HTMLElement }>({ open: false, dismiss: null, - trigger: undefined, - content: undefined, }) const dialog = useDialog() @@ -119,54 +112,6 @@ export function ModelSelectorPopover(props: { } const language = useLanguage() - createEffect(() => { - if (!store.open) return - - const inside = (node: Node | null | undefined) => { - if (!node) return false - const el = store.content - if (el && el.contains(node)) return true - const anchor = store.trigger - if (anchor && anchor.contains(node)) return true - return false - } - - const onKeyDown = (event: KeyboardEvent) => { - if (event.key !== "Escape") return - setStore("dismiss", "escape") - setStore("open", false) - event.preventDefault() - event.stopPropagation() - } - - const onPointerDown = (event: PointerEvent) => { - const target = event.target - if (!(target instanceof Node)) return - if (inside(target)) return - setStore("dismiss", "outside") - setStore("open", false) - } - - const onFocusIn = (event: FocusEvent) => { - if (!store.content) return - const target = event.target - if (!(target instanceof Node)) return - if (inside(target)) return - setStore("dismiss", "outside") - setStore("open", false) - } - - window.addEventListener("keydown", onKeyDown, true) - window.addEventListener("pointerdown", onPointerDown, true) - window.addEventListener("focusin", onFocusIn, true) - - onCleanup(() => { - window.removeEventListener("keydown", onKeyDown, true) - window.removeEventListener("pointerdown", onPointerDown, true) - window.removeEventListener("focusin", onFocusIn, true) - }) - }) - return ( - setStore("trigger", el)} as={props.triggerAs ?? "div"} {...props.triggerProps}> + {props.children} setStore("content", el)} class="w-72 h-80 flex flex-col p-2 rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden" onEscapeKeyDown={(event) => { setStore("dismiss", "escape") diff --git a/packages/app/src/components/dialog-select-provider.tsx b/packages/app/src/components/dialog-select-provider.tsx index f878e50e81a6..8bbd3054b9a2 100644 --- a/packages/app/src/components/dialog-select-provider.tsx +++ b/packages/app/src/components/dialog-select-provider.tsx @@ -24,6 +24,12 @@ export const DialogSelectProvider: Component = () => { const popularGroup = () => language.t("dialog.provider.group.popular") const otherGroup = () => language.t("dialog.provider.group.other") + const customLabel = () => language.t("settings.providers.tag.custom") + const note = (id: string) => { + if (id === "anthropic") return language.t("dialog.provider.anthropic.note") + if (id === "openai") return language.t("dialog.provider.openai.note") + if (id.startsWith("github-copilot")) return language.t("dialog.provider.copilot.note") + } return ( @@ -34,7 +40,7 @@ export const DialogSelectProvider: Component = () => { key={(x) => x?.id} items={() => { language.locale() - return [{ id: CUSTOM_ID, name: "Custom provider" }, ...providers.all()] + return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all()] }} filterKeys={["id", "name"]} groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())} @@ -70,15 +76,7 @@ export const DialogSelectProvider: Component = () => { {language.t("dialog.provider.tag.recommended")} - -
{language.t("dialog.provider.anthropic.note")}
-
- -
{language.t("dialog.provider.openai.note")}
-
- -
{language.t("dialog.provider.copilot.note")}
-
+ {(value) =>
{value()}
}
)}
diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index 65b679f70a11..4c37806365a2 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -38,6 +38,64 @@ interface EditRowProps { onBlur: () => void } +function showRequestError(language: ReturnType, err: unknown) { + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: err instanceof Error ? err.message : String(err), + }) +} + +function useDefaultServer(platform: ReturnType, language: ReturnType) { + const [defaultUrl, defaultUrlActions] = createResource( + async () => { + try { + const url = await platform.getDefaultServerUrl?.() + if (!url) return null + return normalizeServerUrl(url) ?? null + } catch (err) { + showRequestError(language, err) + return null + } + }, + { initialValue: null }, + ) + + const canDefault = createMemo(() => !!platform.getDefaultServerUrl && !!platform.setDefaultServerUrl) + const setDefault = async (url: string | null) => { + try { + await platform.setDefaultServerUrl?.(url) + defaultUrlActions.mutate(url) + } catch (err) { + showRequestError(language, err) + } + } + + return { defaultUrl, canDefault, setDefault } +} + +function useServerPreview(fetcher: typeof fetch) { + const looksComplete = (value: string) => { + const normalized = normalizeServerUrl(value) + if (!normalized) return false + const host = normalized.replace(/^https?:\/\//, "").split("/")[0] + if (!host) return false + if (host.includes("localhost") || host.startsWith("127.0.0.1")) return true + return host.includes(".") || host.includes(":") + } + + const previewStatus = async (value: string, setStatus: (value: boolean | undefined) => void) => { + setStatus(undefined) + if (!looksComplete(value)) return + const normalized = normalizeServerUrl(value) + if (!normalized) return + const result = await checkServerHealth(normalized, fetcher) + setStatus(result.healthy) + } + + return { previewStatus } +} + function AddRow(props: AddRowProps) { return (
@@ -115,6 +173,10 @@ export function DialogSelectServer() { const platform = usePlatform() const globalSDK = useGlobalSDK() const language = useLanguage() + const fetcher = platform.fetch ?? globalThis.fetch + const { defaultUrl, canDefault, setDefault } = useDefaultServer(platform, language) + const { previewStatus } = useServerPreview(fetcher) + let listRoot: HTMLDivElement | undefined const [store, setStore] = createStore({ status: {} as Record, addServer: { @@ -132,43 +194,6 @@ export function DialogSelectServer() { status: undefined as boolean | undefined, }, }) - const [defaultUrl, defaultUrlActions] = createResource( - async () => { - try { - const url = await platform.getDefaultServerUrl?.() - if (!url) return null - return normalizeServerUrl(url) ?? null - } catch (err) { - showToast({ - variant: "error", - title: language.t("common.requestFailed"), - description: err instanceof Error ? err.message : String(err), - }) - return null - } - }, - { initialValue: null }, - ) - const canDefault = createMemo(() => !!platform.getDefaultServerUrl && !!platform.setDefaultServerUrl) - const fetcher = platform.fetch ?? globalThis.fetch - - const looksComplete = (value: string) => { - const normalized = normalizeServerUrl(value) - if (!normalized) return false - const host = normalized.replace(/^https?:\/\//, "").split("/")[0] - if (!host) return false - if (host.includes("localhost") || host.startsWith("127.0.0.1")) return true - return host.includes(".") || host.includes(":") - } - - const previewStatus = async (value: string, setStatus: (value: boolean | undefined) => void) => { - setStatus(undefined) - if (!looksComplete(value)) return - const normalized = normalizeServerUrl(value) - if (!normalized) return - const result = await checkServerHealth(normalized, fetcher) - setStatus(result.healthy) - } const resetAdd = () => { setStore("addServer", { @@ -263,7 +288,7 @@ export function DialogSelectServer() { } const scrollListToBottom = () => { - const scroll = document.querySelector('[data-component="list"] [data-slot="list-scroll"]') + const scroll = listRoot?.querySelector('[data-slot="list-scroll"]') if (!scroll) return requestAnimationFrame(() => { scroll.scrollTop = scroll.scrollHeight @@ -363,158 +388,134 @@ export function DialogSelectServer() { return (
- x} - onSelect={(x) => { - if (x) select(x) - }} - onFilter={(value) => { - if (value && store.addServer.showForm && !store.addServer.adding) { - resetAdd() +
(listRoot = el)}> + x} + onSelect={(x) => { + if (x) select(x) + }} + onFilter={(value) => { + if (value && store.addServer.showForm && !store.addServer.adding) { + resetAdd() + } + }} + divider={true} + class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:max-h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-raised-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent [&_[data-slot=list-item-add]]:px-0" + add={ + store.addServer.showForm + ? { + render: () => ( + + ), + } + : undefined } - }} - divider={true} - class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:max-h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-raised-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent [&_[data-slot=list-item-add]]:px-0" - add={ - store.addServer.showForm - ? { - render: () => ( - - ), - } - : undefined - } - > - {(i) => { - return ( -
- handleEditKey(event, i)} - onBlur={() => handleEdit(i, store.editServer.value)} + > + {(i) => { + return ( +
+ handleEditKey(event, i)} + onBlur={() => handleEdit(i, store.editServer.value)} + /> + } + > + + + {language.t("dialog.server.status.default")} + + + } /> - } - > - - - {language.t("dialog.server.status.default")} - + + +
+ +

{language.t("dialog.server.current")}

- } - /> - - -
- -

{language.t("dialog.server.current")}

-
- - - e.stopPropagation()} - onPointerDown={(e: PointerEvent) => e.stopPropagation()} - /> - - - { - setStore("editServer", { - id: i, - value: i, - error: "", - status: store.status[i]?.healthy, - }) - }} - > - {language.t("dialog.server.menu.edit")} - - + + + e.stopPropagation()} + onPointerDown={(e: PointerEvent) => e.stopPropagation()} + /> + + { - try { - await platform.setDefaultServerUrl?.(i) - defaultUrlActions.mutate(i) - } catch (err) { - showToast({ - variant: "error", - title: language.t("common.requestFailed"), - description: err instanceof Error ? err.message : String(err), - }) - } + onSelect={() => { + setStore("editServer", { + id: i, + value: i, + error: "", + status: store.status[i]?.healthy, + }) }} > - - {language.t("dialog.server.menu.default")} - + {language.t("dialog.server.menu.edit")} - - + + setDefault(i)}> + + {language.t("dialog.server.menu.default")} + + + + + setDefault(null)}> + + {language.t("dialog.server.menu.defaultRemove")} + + + + { - try { - await platform.setDefaultServerUrl?.(null) - defaultUrlActions.mutate(null) - } catch (err) { - showToast({ - variant: "error", - title: language.t("common.requestFailed"), - description: err instanceof Error ? err.message : String(err), - }) - } - }} + onSelect={() => handleRemove(i)} + class="text-text-on-critical-base hover:bg-surface-critical-weak" > - - {language.t("dialog.server.menu.defaultRemove")} - + {language.t("dialog.server.menu.delete")} - - - handleRemove(i)} - class="text-text-on-critical-base hover:bg-surface-critical-weak" - > - {language.t("dialog.server.menu.delete")} - - - - -
-
-
- ) - }} - + + + +
+
+
+ ) + }} +
+
) diff --git a/packages/app/src/components/file-tree.test.ts b/packages/app/src/components/file-tree.test.ts index eb048e29ed57..29e20b4807c5 100644 --- a/packages/app/src/components/file-tree.test.ts +++ b/packages/app/src/components/file-tree.test.ts @@ -6,6 +6,7 @@ let dirsToExpand: typeof import("./file-tree").dirsToExpand beforeAll(async () => { mock.module("@solidjs/router", () => ({ + useNavigate: () => () => undefined, useParams: () => ({}), })) mock.module("@/context/file", () => ({ diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index 183c1555bde6..758f5a83f532 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -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" @@ -14,11 +15,18 @@ import { Switch, untrack, type ComponentProps, + type JSXElement, type ParentProps, } from "solid-js" import { Dynamic } from "solid-js/web" import type { FileNode } from "@opencode-ai/sdk/v2" +const MAX_DEPTH = 128 + +function pathToFileUrl(filepath: string): string { + return `file://${encodeFilePath(filepath)}` +} + type Kind = "add" | "del" | "mix" type Filter = { @@ -54,6 +62,189 @@ export function dirsToExpand(input: { return [...input.filter.dirs].filter((dir) => !input.expanded(dir)) } +const kindLabel = (kind: Kind) => { + if (kind === "add") return "A" + if (kind === "del") return "D" + return "M" +} + +const kindTextColor = (kind: Kind) => { + if (kind === "add") return "color: var(--icon-diff-add-base)" + if (kind === "del") return "color: var(--icon-diff-delete-base)" + return "color: var(--icon-warning-active)" +} + +const kindDotColor = (kind: Kind) => { + if (kind === "add") return "background-color: var(--icon-diff-add-base)" + if (kind === "del") return "background-color: var(--icon-diff-delete-base)" + return "background-color: var(--icon-warning-active)" +} + +const visibleKind = (node: FileNode, kinds?: ReadonlyMap, marks?: Set) => { + const kind = kinds?.get(node.path) + if (!kind) return + if (!marks?.has(node.path)) return + return kind +} + +const buildDragImage = (target: HTMLElement) => { + const icon = target.querySelector('[data-component="file-icon"]') ?? target.querySelector("svg") + const text = target.querySelector("span") + if (!icon || !text) return + + const image = document.createElement("div") + image.className = + "flex items-center gap-x-2 px-2 py-1 bg-surface-raised-base rounded-md border border-border-base text-12-regular text-text-strong" + image.style.position = "absolute" + image.style.top = "-1000px" + image.innerHTML = (icon as SVGElement).outerHTML + (text as HTMLSpanElement).outerHTML + return image +} + +const withFileDragImage = (event: DragEvent) => { + const image = buildDragImage(event.currentTarget as HTMLElement) + if (!image) return + document.body.appendChild(image) + event.dataTransfer?.setDragImage(image, 0, 12) + setTimeout(() => document.body.removeChild(image), 0) +} + +const FileTreeNode = ( + p: ParentProps & + ComponentProps<"div"> & + ComponentProps<"button"> & { + node: FileNode + level: number + active?: string + nodeClass?: string + draggable: boolean + kinds?: ReadonlyMap + marks?: Set + as?: "div" | "button" + }, +) => { + const [local, rest] = splitProps(p, [ + "node", + "level", + "active", + "nodeClass", + "draggable", + "kinds", + "marks", + "as", + "children", + "class", + "classList", + ]) + const kind = () => visibleKind(local.node, local.kinds, local.marks) + const active = () => !!kind() && !local.node.ignored + const color = () => { + const value = kind() + if (!value) return + return kindTextColor(value) + } + + return ( + { + if (!local.draggable) return + event.dataTransfer?.setData("text/plain", `file:${local.node.path}`) + event.dataTransfer?.setData("text/uri-list", pathToFileUrl(local.node.path)) + if (event.dataTransfer) event.dataTransfer.effectAllowed = "copy" + withFileDragImage(event) + }} + {...rest} + > + {local.children} + + {local.node.name} + + {(() => { + const value = kind() + if (!value) return null + if (local.node.type === "file") { + return ( + + {kindLabel(value)} + + ) + } + return
+ })()} + + ) +} + +const FileTreeNodeTooltip = (props: { enabled: boolean; node: FileNode; kind?: Kind; children: JSXElement }) => { + if (!props.enabled) return props.children + + const parts = props.node.path.split("/") + const leaf = parts[parts.length - 1] ?? props.node.path + const head = parts.slice(0, -1).join("/") + const prefix = head ? `${head}/` : "" + const label = + props.kind === "add" + ? "Additions" + : props.kind === "del" + ? "Deletions" + : props.kind === "mix" + ? "Modifications" + : undefined + + return ( + + + {prefix} + + {leaf} + + {(text) => ( + <> + + {text()} + + )} + + + <> + + Ignored + + +
+ } + > + {props.children} + + ) +} + export default function FileTree(props: { path: string class?: string @@ -71,12 +262,20 @@ export default function FileTree(props: { _marks?: Set _deeps?: Map _kinds?: ReadonlyMap + _chain?: readonly string[] }) { const file = useFile() const level = props.level ?? 0 const draggable = () => props.draggable ?? true const tooltip = () => props.tooltip ?? true + const key = (p: string) => + file + .normalize(p) + .replace(/[\\/]+$/, "") + .replaceAll("\\", "/") + const chain = props._chain ? [...props._chain, key(props.path)] : [key(props.path)] + const filter = createMemo(() => { if (props._filter) return props._filter @@ -118,23 +317,45 @@ export default function FileTree(props: { const out = new Map() - const visit = (dir: string, lvl: number): number => { - const expanded = file.tree.state(dir)?.expanded ?? false - if (!expanded) return -1 + const root = props.path + if (!(file.tree.state(root)?.expanded ?? false)) return out + + const seen = new Set() + const stack: { dir: string; lvl: number; i: number; kids: string[]; max: number }[] = [] - const nodes = file.tree.children(dir) - const max = nodes.reduce((max, node) => { - if (node.type !== "directory") return max - const open = file.tree.state(node.path)?.expanded ?? false - if (!open) return max - return Math.max(max, visit(node.path, lvl + 1)) - }, lvl) + const push = (dir: string, lvl: number) => { + const id = key(dir) + if (seen.has(id)) return + seen.add(id) - out.set(dir, max) - return max + const kids = file.tree + .children(dir) + .filter((node) => node.type === "directory" && (file.tree.state(node.path)?.expanded ?? false)) + .map((node) => node.path) + + stack.push({ dir, lvl, i: 0, kids, max: lvl }) + } + + push(root, level - 1) + + while (stack.length > 0) { + const top = stack[stack.length - 1]! + + if (top.i < top.kids.length) { + const next = top.kids[top.i]! + top.i++ + push(next, top.lvl + 1) + continue + } + + out.set(top.dir, top.max) + stack.pop() + + const parent = stack[stack.length - 1] + if (!parent) continue + parent.max = Math.max(parent.max, top.max) } - visit(props.path, level - 1) return out }) @@ -215,124 +436,15 @@ 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) }) - }) - const Node = ( - p: ParentProps & - ComponentProps<"div"> & - ComponentProps<"button"> & { - node: FileNode - as?: "div" | "button" - }, - ) => { - const [local, rest] = splitProps(p, ["node", "as", "children", "class", "classList"]) - return ( - { - if (!draggable()) return - e.dataTransfer?.setData("text/plain", `file:${local.node.path}`) - e.dataTransfer?.setData("text/uri-list", `file://${local.node.path}`) - if (e.dataTransfer) e.dataTransfer.effectAllowed = "copy" - - const dragImage = document.createElement("div") - dragImage.className = - "flex items-center gap-x-2 px-2 py-1 bg-surface-raised-base rounded-md border border-border-base text-12-regular text-text-strong" - dragImage.style.position = "absolute" - dragImage.style.top = "-1000px" - - const icon = - (e.currentTarget as HTMLElement).querySelector('[data-component="file-icon"]') ?? - (e.currentTarget as HTMLElement).querySelector("svg") - const text = (e.currentTarget as HTMLElement).querySelector("span") - if (icon && text) { - dragImage.innerHTML = (icon as SVGElement).outerHTML + (text as HTMLSpanElement).outerHTML - } - - document.body.appendChild(dragImage) - e.dataTransfer?.setDragImage(dragImage, 0, 12) - setTimeout(() => document.body.removeChild(dragImage), 0) - }} - {...rest} - > - {local.children} - {(() => { - const kind = kinds()?.get(local.node.path) - const marked = marks()?.has(local.node.path) ?? false - const active = !!kind && marked && !local.node.ignored - const color = - kind === "add" - ? "color: var(--icon-diff-add-base)" - : kind === "del" - ? "color: var(--icon-diff-delete-base)" - : kind === "mix" - ? "color: var(--icon-warning-active)" - : undefined - return ( - - {local.node.name} - - ) - })()} - {(() => { - const kind = kinds()?.get(local.node.path) - if (!kind) return null - if (!marks()?.has(local.node.path)) return null - - if (local.node.type === "file") { - const text = kind === "add" ? "A" : kind === "del" ? "D" : "M" - const color = - kind === "add" - ? "color: var(--icon-diff-add-base)" - : kind === "del" - ? "color: var(--icon-diff-delete-base)" - : "color: var(--icon-warning-active)" - - return ( - - {text} - - ) - } - - if (local.node.type === "directory") { - const color = - kind === "add" - ? "background-color: var(--icon-diff-add-base)" - : kind === "del" - ? "background-color: var(--icon-diff-delete-base)" - : "background-color: var(--icon-warning-active)" - - return
- } - - return null - })()} - - ) - } + return out + }) return (
@@ -340,61 +452,7 @@ export default function FileTree(props: { {(node) => { const expanded = () => file.tree.state(node.path)?.expanded ?? false const deep = () => deeps().get(node.path) ?? -1 - const Wrapper = (p: ParentProps) => { - if (!tooltip()) return p.children - - const parts = node.path.split("/") - const leaf = parts[parts.length - 1] ?? node.path - const head = parts.slice(0, -1).join("/") - const prefix = head ? `${head}/` : "" - - const kind = () => kinds()?.get(node.path) - const label = () => { - const k = kind() - if (!k) return - if (k === "add") return "Additions" - if (k === "del") return "Deletions" - return "Modifications" - } - - const ignored = () => node.type === "directory" && node.ignored - - return ( - - - {prefix} - - {leaf} - - {(t: () => string) => ( - <> - - {t()} - - )} - - - <> - - Ignored - - -
- } - > - {p.children} - - ) - } + const kind = () => visibleKind(node, kinds(), marks()) return ( @@ -408,13 +466,21 @@ export default function FileTree(props: { onOpenChange={(open) => (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))} > - - + +
-
-
+ +
- + ...
} + > + +
- - props.onFileClick?.(node)}> + + props.onFileClick?.(node)} + >
- - + + ) diff --git a/packages/app/src/components/link.tsx b/packages/app/src/components/link.tsx index e13c31330480..85f7efc539eb 100644 --- a/packages/app/src/components/link.tsx +++ b/packages/app/src/components/link.tsx @@ -1,17 +1,26 @@ import { ComponentProps, splitProps } from "solid-js" import { usePlatform } from "@/context/platform" -export interface LinkProps extends ComponentProps<"button"> { +export interface LinkProps extends Omit, "href"> { href: string } export function Link(props: LinkProps) { const platform = usePlatform() - const [local, rest] = splitProps(props, ["href", "children"]) + const [local, rest] = splitProps(props, ["href", "children", "class"]) return ( - + ) } diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 46d7f93eb32e..e21798738175 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -35,9 +35,15 @@ import { Persist, persisted } from "@/utils/persist" import { SessionContextUsage } from "@/components/session-context-usage" import { usePermission } from "@/context/permission" import { useLanguage } from "@/context/language" +import { usePlatform } from "@/context/platform" import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom" import { createPromptAttachments, ACCEPTED_FILE_TYPES } from "./prompt-input/attachments" -import { navigatePromptHistory, prependHistoryEntry, promptLength } from "./prompt-input/history" +import { + canNavigateHistoryAtCursor, + navigatePromptHistory, + prependHistoryEntry, + promptLength, +} from "./prompt-input/history" import { createPromptSubmit } from "./prompt-input/submit" import { PromptPopover, type AtOption, type SlashCommand } from "./prompt-input/slash-popover" import { PromptContextItems } from "./prompt-input/context-items" @@ -97,6 +103,7 @@ export const PromptInput: Component = (props) => { const command = useCommand() const permission = usePermission() const language = useLanguage() + const platform = usePlatform() let editorRef!: HTMLDivElement let fileInputRef!: HTMLInputElement let scrollRef!: HTMLDivElement @@ -156,14 +163,13 @@ export const PromptInput: Component = (props) => { const wantsReview = item.commentOrigin === "review" || (item.commentOrigin !== "file" && commentInReview(item.path)) if (wantsReview) { if (!view().reviewPanel.opened()) view().reviewPanel.open() - layout.fileTree.open() layout.fileTree.setTab("changes") + tabs().setActive("review") requestAnimationFrame(() => comments.setFocus(focus)) return } if (!view().reviewPanel.opened()) view().reviewPanel.open() - layout.fileTree.open() layout.fileTree.setTab("all") const tab = files.tab(item.path) tabs().open(tab) @@ -205,7 +211,7 @@ export const PromptInput: Component = (props) => { historyIndex: number savedPrompt: Prompt | null placeholder: number - dragging: boolean + draggingType: "image" | "@mention" | null mode: "normal" | "shell" applyingHistory: boolean }>({ @@ -213,7 +219,7 @@ export const PromptInput: Component = (props) => { historyIndex: -1, savedPrompt: null, placeholder: Math.floor(Math.random() * EXAMPLES.length), - dragging: false, + draggingType: null, mode: "normal", applyingHistory: false, }) @@ -274,6 +280,48 @@ export const PromptInput: Component = (props) => { } const isFocused = createFocusSignal(() => editorRef) + const escBlur = () => platform.platform === "desktop" && platform.os === "macos" + + const closePopover = () => setStore("popover", null) + + const resetHistoryNavigation = (force = false) => { + if (!force && (store.historyIndex < 0 || store.applyingHistory)) return + setStore("historyIndex", -1) + setStore("savedPrompt", null) + } + + const clearEditor = () => { + editorRef.innerHTML = "" + } + + const setEditorText = (text: string) => { + clearEditor() + editorRef.textContent = text + } + + const focusEditorEnd = () => { + requestAnimationFrame(() => { + editorRef.focus() + const range = document.createRange() + const selection = window.getSelection() + range.selectNodeContents(editorRef) + range.collapse(false) + selection?.removeAllRanges() + selection?.addRange(range) + }) + } + + const currentCursor = () => { + const selection = window.getSelection() + if (!selection || selection.rangeCount === 0 || !editorRef.contains(selection.anchorNode)) return null + return getCursorPosition(editorRef) + } + + const renderEditorWithCursor = (parts: Prompt) => { + const cursor = currentCursor() + renderEditor(parts) + if (cursor !== null) setCursorPosition(editorRef, cursor) + } createEffect(() => { params.id @@ -288,7 +336,7 @@ export const PromptInput: Component = (props) => { const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229 createEffect(() => { - if (!isFocused()) setStore("popover", null) + if (!isFocused()) closePopover() }) // Safety: reset composing state on focus change to prevent stuck state @@ -302,6 +350,7 @@ export const PromptInput: Component = (props) => { .filter((agent) => !agent.hidden && agent.mode !== "primary") .map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name })), ) + const agentNames = createMemo(() => local.agent.list().map((agent) => agent.name)) const handleAtSelect = (option: AtOption | undefined) => { if (!option) return @@ -379,26 +428,17 @@ export const PromptInput: Component = (props) => { const handleSlashSelect = (cmd: SlashCommand | undefined) => { if (!cmd) return - setStore("popover", null) + closePopover() if (cmd.type === "custom") { const text = `/${cmd.trigger} ` - editorRef.innerHTML = "" - editorRef.textContent = text + setEditorText(text) prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) - requestAnimationFrame(() => { - editorRef.focus() - const range = document.createRange() - const sel = window.getSelection() - range.selectNodeContents(editorRef) - range.collapse(false) - sel?.removeAllRanges() - sel?.addRange(range) - }) + focusEditorEnd() return } - editorRef.innerHTML = "" + clearEditor() prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0) command.trigger(cmd.id, "slash") } @@ -413,7 +453,7 @@ export const PromptInput: Component = (props) => { } = useFilteredList({ items: slashCommands, key: (x) => x?.id, - filterKeys: ["trigger", "title", "description"], + filterKeys: ["trigger", "title"], onSelect: handleSlashSelect, }) @@ -439,10 +479,7 @@ export const PromptInput: Component = (props) => { const prev = node.previousSibling const next = node.nextSibling const prevIsBr = prev?.nodeType === Node.ELEMENT_NODE && (prev as HTMLElement).tagName === "BR" - const nextIsBr = next?.nodeType === Node.ELEMENT_NODE && (next as HTMLElement).tagName === "BR" - if (!prevIsBr && !nextIsBr) return false - if (nextIsBr && !prevIsBr && prev) return false - return true + return !!prevIsBr && !next } if (node.nodeType !== Node.ELEMENT_NODE) return false const el = node as HTMLElement @@ -452,7 +489,7 @@ export const PromptInput: Component = (props) => { }) const renderEditor = (parts: Prompt) => { - editorRef.innerHTML = "" + clearEditor() for (const part of parts) { if (part.type === "text") { editorRef.appendChild(createTextFragment(part.content)) @@ -462,6 +499,11 @@ export const PromptInput: Component = (props) => { editorRef.appendChild(createPill(part)) } } + + const last = editorRef.lastChild + if (last?.nodeType === Node.ELEMENT_NODE && (last as HTMLElement).tagName === "BR") { + editorRef.appendChild(document.createTextNode("\u200B")) + } } createEffect( @@ -512,34 +554,14 @@ export const PromptInput: Component = (props) => { mirror.input = false if (isNormalizedEditor()) return - const selection = window.getSelection() - let cursorPosition: number | null = null - if (selection && selection.rangeCount > 0 && editorRef.contains(selection.anchorNode)) { - cursorPosition = getCursorPosition(editorRef) - } - - renderEditor(inputParts) - - if (cursorPosition !== null) { - setCursorPosition(editorRef, cursorPosition) - } + renderEditorWithCursor(inputParts) return } const domParts = parseFromDOM() if (isNormalizedEditor() && isPromptEqual(inputParts, domParts)) return - const selection = window.getSelection() - let cursorPosition: number | null = null - if (selection && selection.rangeCount > 0 && editorRef.contains(selection.anchorNode)) { - cursorPosition = getCursorPosition(editorRef) - } - - renderEditor(inputParts) - - if (cursorPosition !== null) { - setCursorPosition(editorRef, cursorPosition) - } + renderEditorWithCursor(inputParts) }, ), ) @@ -634,11 +656,8 @@ export const PromptInput: Component = (props) => { const shouldReset = trimmed.length === 0 && !hasNonText && images.length === 0 if (shouldReset) { - setStore("popover", null) - if (store.historyIndex >= 0 && !store.applyingHistory) { - setStore("historyIndex", -1) - setStore("savedPrompt", null) - } + closePopover() + resetHistoryNavigation() if (prompt.dirty()) { mirror.input = true prompt.set(DEFAULT_PROMPT, 0) @@ -660,16 +679,13 @@ export const PromptInput: Component = (props) => { slashOnInput(slashMatch[1]) setStore("popover", "slash") } else { - setStore("popover", null) + closePopover() } } else { - setStore("popover", null) + closePopover() } - if (store.historyIndex >= 0 && !store.applyingHistory) { - setStore("historyIndex", -1) - setStore("savedPrompt", null) - } + resetHistoryNavigation() mirror.input = true prompt.set([...rawParts, ...images], cursorPosition) @@ -721,7 +737,17 @@ export const PromptInput: Component = (props) => { } } if (last.nodeType !== Node.TEXT_NODE) { - range.setStartAfter(last) + const isBreak = last.nodeType === Node.ELEMENT_NODE && (last as HTMLElement).tagName === "BR" + const next = last.nextSibling + const emptyText = next?.nodeType === Node.TEXT_NODE && (next.textContent ?? "") === "" + if (isBreak && (!next || emptyText)) { + const placeholder = next && emptyText ? next : document.createTextNode("\u200B") + if (!next) last.parentNode?.insertBefore(placeholder, null) + placeholder.textContent = "\u200B" + range.setStart(placeholder, 0) + } else { + range.setStartAfter(last) + } } } range.collapse(true) @@ -730,7 +756,7 @@ export const PromptInput: Component = (props) => { } handleInput() - setStore("popover", null) + closePopover() } const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => { @@ -760,8 +786,13 @@ export const PromptInput: Component = (props) => { editor: () => editorRef, isFocused, isDialogActive: () => !!dialog.active, - setDragging: (value) => setStore("dragging", value), + setDraggingType: (type) => setStore("draggingType", type), + focusEditor: () => { + editorRef.focus() + setCursorPosition(editorRef, promptLength(prompt.current())) + }, addPart, + readClipboardImage: platform.readClipboardImage, }) const { abort, handleSubmit } = createPromptSubmit({ @@ -775,12 +806,11 @@ export const PromptInput: Component = (props) => { promptLength, addToHistory, resetHistoryNavigation: () => { - setStore("historyIndex", -1) - setStore("savedPrompt", null) + resetHistoryNavigation(true) }, setMode: (mode) => setStore("mode", mode), setPopover: (popover) => setStore("popover", popover), - newSessionWorktree: props.newSessionWorktree, + newSessionWorktree: () => props.newSessionWorktree, onNewSessionWorktreeReset: props.onNewSessionWorktreeReset, onSubmit: props.onSubmit, }) @@ -813,13 +843,39 @@ export const PromptInput: Component = (props) => { return } } - if (store.mode === "shell") { - const { collapsed, cursorPosition, textLength } = getCaretState() - if (event.key === "Escape") { + + if (event.key === "Escape") { + if (store.popover) { + closePopover() + event.preventDefault() + event.stopPropagation() + return + } + + if (store.mode === "shell") { setStore("mode", "normal") event.preventDefault() + event.stopPropagation() return } + + if (working()) { + abort() + event.preventDefault() + event.stopPropagation() + return + } + + if (escBlur()) { + editorRef.blur() + event.preventDefault() + event.stopPropagation() + return + } + } + + if (store.mode === "shell") { + const { collapsed, cursorPosition, textLength } = getCaretState() if (event.key === "Backspace" && collapsed && cursorPosition === 0 && textLength === 0) { setStore("mode", "normal") event.preventDefault() @@ -865,7 +921,7 @@ export const PromptInput: Component = (props) => { if (ctrl && event.code === "KeyG") { if (store.popover) { - setStore("popover", null) + closePopover() event.preventDefault() return } @@ -882,29 +938,13 @@ export const PromptInput: Component = (props) => { if (!collapsed) return const cursorPosition = getCursorPosition(editorRef) - const textLength = promptLength(prompt.current()) const textContent = prompt .current() .map((part) => ("content" in part ? part.content : "")) .join("") - const isEmpty = textContent.trim() === "" || textLength <= 1 - const hasNewlines = textContent.includes("\n") - const inHistory = store.historyIndex >= 0 - const atStart = cursorPosition <= (isEmpty ? 1 : 0) - const atEnd = cursorPosition >= (isEmpty ? textLength - 1 : textLength) - const allowUp = isEmpty || atStart || (!hasNewlines && !inHistory) || (inHistory && atEnd) - const allowDown = isEmpty || atEnd || (!hasNewlines && !inHistory) || (inHistory && atStart) - - if (event.key === "ArrowUp") { - if (!allowUp) return - if (navigateHistory("up")) { - event.preventDefault() - } - return - } - - if (!allowDown) return - if (navigateHistory("down")) { + const direction = event.key === "ArrowUp" ? "up" : "down" + if (!canNavigateHistoryAtCursor(direction, textContent, cursorPosition, store.historyIndex >= 0)) return + if (navigateHistory(direction)) { event.preventDefault() } return @@ -914,13 +954,6 @@ export const PromptInput: Component = (props) => { if (event.key === "Enter" && !event.shiftKey) { handleSubmit(event) } - if (event.key === "Escape") { - if (store.popover) { - setStore("popover", null) - } else if (working()) { - abort() - } - } } return ( @@ -946,11 +979,14 @@ export const PromptInput: Component = (props) => { "group/prompt-input": true, "bg-surface-raised-stronger-non-alpha shadow-xs-border relative": true, "rounded-[14px] overflow-clip focus-within:shadow-xs-border": true, - "border-icon-info-active border-dashed": store.dragging, + "border-icon-info-active border-dashed": store.draggingType !== null, [props.class ?? ""]: !!props.class, }} > - + { @@ -983,6 +1019,9 @@ export const PromptInput: Component = (props) => { aria-multiline="true" aria-label={placeholder()} contenteditable="true" + autocapitalize="off" + autocorrect="off" + spellcheck={false} onInput={handleInput} onPaste={handlePaste} onCompositionStart={() => setComposing(true)} @@ -1020,10 +1059,10 @@ export const PromptInput: Component = (props) => { keybind={command.keybind("agent.cycle")} > setStore("title", e.currentTarget.value)} diff --git a/packages/app/src/components/settings-agents.tsx b/packages/app/src/components/settings-agents.tsx index e68f1e59c53f..74a942f7770d 100644 --- a/packages/app/src/components/settings-agents.tsx +++ b/packages/app/src/components/settings-agents.tsx @@ -2,6 +2,7 @@ import { Component } from "solid-js" import { useLanguage } from "@/context/language" export const SettingsAgents: Component = () => { + // TODO: Replace this placeholder with full agents settings controls. const language = useLanguage() return ( diff --git a/packages/app/src/components/settings-commands.tsx b/packages/app/src/components/settings-commands.tsx index cf796d0aa7a4..e158d231ceef 100644 --- a/packages/app/src/components/settings-commands.tsx +++ b/packages/app/src/components/settings-commands.tsx @@ -2,6 +2,7 @@ import { Component } from "solid-js" import { useLanguage } from "@/context/language" export const SettingsCommands: Component = () => { + // TODO: Replace this placeholder with full commands settings controls. const language = useLanguage() return ( diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index b31cfb6cc794..d5a0b813b6c2 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -1,8 +1,10 @@ -import { Component, createMemo, type JSX } from "solid-js" +import { Component, Show, createMemo, createResource, type JSX } from "solid-js" import { createStore } from "solid-js/store" import { Button } from "@opencode-ai/ui/button" +import { Icon } from "@opencode-ai/ui/icon" import { Select } from "@opencode-ai/ui/select" import { Switch } from "@opencode-ai/ui/switch" +import { Tooltip } from "@opencode-ai/ui/tooltip" import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme" import { showToast } from "@opencode-ai/ui/toast" import { useLanguage } from "@/context/language" @@ -40,6 +42,8 @@ export const SettingsGeneral: Component = () => { checking: false, }) + const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux") + const check = () => { if (!platform.checkUpdate) return setStore("checking", true) @@ -124,299 +128,381 @@ export const SettingsGeneral: Component = () => { { value: "roboto-mono", label: "font.option.robotoMono" }, { value: "source-code-pro", label: "font.option.sourceCodePro" }, { value: "ubuntu-mono", label: "font.option.ubuntuMono" }, + { value: "geist-mono", label: "font.option.geistMono" }, ] as const const fontOptionsList = [...fontOptions] const soundOptions = [...SOUND_OPTIONS] - return ( -
-
-
-

{language.t("settings.tab.general")}

-
+ const soundSelectProps = (current: () => string, set: (id: string) => void) => ({ + options: soundOptions, + current: soundOptions.find((o) => o.id === current()), + value: (o: (typeof soundOptions)[number]) => o.id, + label: (o: (typeof soundOptions)[number]) => language.t(o.label), + onHighlight: (option: (typeof soundOptions)[number] | undefined) => { + if (!option) return + playDemoSound(option.src) + }, + onSelect: (option: (typeof soundOptions)[number] | undefined) => { + if (!option) return + set(option.id) + playDemoSound(option.src) + }, + variant: "secondary" as const, + size: "small" as const, + triggerVariant: "settings" as const, + }) + + const AppearanceSection = () => ( +
+

{language.t("settings.general.section.appearance")}

+ +
+ + o.value === theme.colorScheme())} + value={(o) => o.value} + label={(o) => o.label} + onSelect={(option) => option && theme.setColorScheme(option.value)} + onHighlight={(option) => { + if (!option) return + theme.previewColorScheme(option.value) + return () => theme.cancelPreview() + }} + variant="secondary" + size="small" + triggerVariant="settings" + /> + + + + {language.t("settings.general.row.theme.description")}{" "} + {language.t("common.learnMore")} + + } + > + o.value === settings.appearance.font())} + value={(o) => o.value} + label={(o) => language.t(o.label)} + onSelect={(option) => option && settings.appearance.setFont(option.value)} + variant="secondary" + size="small" + triggerVariant="settings" + triggerStyle={{ "font-family": monoFontFamily(settings.appearance.font()), "min-width": "180px" }} + > + {(option) => ( + + {option ? language.t(option.label) : ""} + + )} + +
+
+ ) -
- {/* Appearance Section */} -
-

{language.t("settings.general.section.appearance")}

- -
- - o.value === theme.colorScheme())} - value={(o) => o.value} - label={(o) => o.label} - onSelect={(option) => option && theme.setColorScheme(option.value)} - onHighlight={(option) => { - if (!option) return - theme.previewColorScheme(option.value) - return () => theme.cancelPreview() - }} - variant="secondary" - size="small" - triggerVariant="settings" - /> - - - - {language.t("settings.general.row.theme.description")}{" "} - {language.t("common.learnMore")} - - } - > - o.value === settings.appearance.font())} - value={(o) => o.value} - label={(o) => language.t(o.label)} - onSelect={(option) => option && settings.appearance.setFont(option.value)} - variant="secondary" - size="small" - triggerVariant="settings" - triggerStyle={{ "font-family": monoFontFamily(settings.appearance.font()), "min-width": "180px" }} - > - {(option) => ( - - {option ? language.t(option.label) : ""} - - )} - - + const NotificationsSection = () => ( +
+

{language.t("settings.general.section.notifications")}

+ +
+ +
+ settings.notifications.setAgent(checked)} + />
-
- - {/* System notifications Section */} -
-

{language.t("settings.general.section.notifications")}

- -
- -
- settings.notifications.setAgent(checked)} - /> -
-
- - -
- settings.notifications.setPermissions(checked)} - /> -
-
- - -
- settings.notifications.setErrors(checked)} - /> -
-
+ + + +
+ settings.notifications.setPermissions(checked)} + />
-
+ + + +
+ settings.notifications.setErrors(checked)} + /> +
+
+
+
+ ) - {/* Sound effects Section */} -
-

{language.t("settings.general.section.sounds")}

- -
- - o.id === settings.sounds.permissions())} - value={(o) => o.id} - label={(o) => language.t(o.label)} - onHighlight={(option) => { - if (!option) return - playDemoSound(option.src) - }} - onSelect={(option) => { - if (!option) return - settings.sounds.setPermissions(option.id) - playDemoSound(option.src) - }} - variant="secondary" - size="small" - triggerVariant="settings" +
+ o.id === settings.sounds.errors())} - value={(o) => o.id} - label={(o) => language.t(o.label)} - onHighlight={(option) => { - if (!option) return - playDemoSound(option.src) - }} - onSelect={(option) => { - if (!option) return - settings.sounds.setErrors(option.id) - playDemoSound(option.src) - }} - variant="secondary" - size="small" - triggerVariant="settings" +
+ settings.sounds.errors(), + (id) => settings.sounds.setErrors(id), + )} + /> +
+ +
+
+ ) + + const UpdatesSection = () => ( +
+

{language.t("settings.general.section.updates")}

+ +
+ +
+ settings.updates.setStartup(checked)} + />
+
+ + +
+ settings.general.setReleaseNotes(checked)} + /> +
+
+ + + + +
+
+ ) + + return ( +
+
+
+

{language.t("settings.tab.general")}

+
- {/* Updates Section */} -
-

{language.t("settings.general.section.updates")}

- -
- -
- settings.updates.setStartup(checked)} - /> +
+ + + + + + + + {(_) => { + const [enabledResource, actions] = createResource(() => platform.getWslEnabled?.()) + const enabled = () => (enabledResource.state === "pending" ? undefined : enabledResource.latest) + + return ( +
+

{language.t("settings.desktop.section.wsl")}

+ +
+ +
+ platform.setWslEnabled?.(checked)?.finally(() => actions.refetch())} + /> +
+
+
- - - -
- settings.general.setReleaseNotes(checked)} - /> + ) + }} + + + + + + {(_) => { + const [valueResource, actions] = createResource(() => platform.getDisplayBackend?.()) + const value = () => (valueResource.state === "pending" ? undefined : valueResource.latest) + + const onChange = (checked: boolean) => + platform.setDisplayBackend?.(checked ? "wayland" : "auto").finally(() => actions.refetch()) + + return ( +
+

{language.t("settings.general.section.display")}

+ +
+ + {language.t("settings.general.row.wayland.title")} + + + + + +
+ } + description={language.t("settings.general.row.wayland.description")} + > +
+ +
+ +
-
- - - - -
-
+ ) + }} +
) } interface SettingsRowProps { - title: string + title: string | JSX.Element description: string | JSX.Element children: JSX.Element } diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx index a24db13f5c5b..bcc731af99fb 100644 --- a/packages/app/src/components/settings-keybinds.tsx +++ b/packages/app/src/components/settings-keybinds.tsx @@ -21,6 +21,9 @@ type KeybindMeta = { group: KeybindGroup } +type KeybindMap = Record +type CommandContext = ReturnType + const GROUPS: KeybindGroup[] = ["General", "Session", "Navigation", "Model and agent", "Terminal", "Prompt"] type GroupKey = @@ -44,7 +47,7 @@ function groupFor(id: string): KeybindGroup { if (id === PALETTE_ID) return "General" if (id.startsWith("terminal.")) return "Terminal" if (id.startsWith("model.") || id.startsWith("agent.") || id.startsWith("mcp.")) return "Model and agent" - if (id.startsWith("file.")) return "Navigation" + if (id.startsWith("file.") || id.startsWith("fileTree.")) return "Navigation" if (id.startsWith("prompt.")) return "Prompt" if ( id.startsWith("session.") || @@ -107,6 +110,150 @@ function signatures(config: string | undefined) { return sigs } +function keybinds(value: unknown): KeybindMap { + if (!value || typeof value !== "object" || Array.isArray(value)) return {} + return value as KeybindMap +} + +function listFor(command: CommandContext, map: KeybindMap, palette: string) { + const out = new Map() + out.set(PALETTE_ID, { title: palette, group: "General" }) + + for (const opt of command.catalog) { + if (opt.id.startsWith("suggested.")) continue + out.set(opt.id, { title: opt.title, group: groupFor(opt.id) }) + } + + for (const opt of command.options) { + if (opt.id.startsWith("suggested.")) continue + out.set(opt.id, { title: opt.title, group: groupFor(opt.id) }) + } + + for (const [id, value] of Object.entries(map)) { + if (typeof value !== "string") continue + if (out.has(id)) continue + out.set(id, { title: id, group: groupFor(id) }) + } + + return out +} + +function groupedFor(list: Map) { + const out = new Map() + for (const group of GROUPS) out.set(group, []) + + for (const [id, item] of list) { + const ids = out.get(item.group) + if (!ids) continue + ids.push(id) + } + + for (const group of GROUPS) { + const ids = out.get(group) + if (!ids) continue + ids.sort((a, b) => (list.get(a)?.title ?? "").localeCompare(list.get(b)?.title ?? "")) + } + + return out +} + +function filteredFor( + query: string, + list: Map, + grouped: Map, + keybind: (id: string) => string, +) { + const value = query.toLowerCase().trim() + if (!value) return grouped + + const out = new Map() + for (const group of GROUPS) out.set(group, []) + + const items = Array.from(list.entries()).map(([id, meta]) => ({ + id, + title: meta.title, + group: meta.group, + keybind: keybind(id), + })) + + const results = fuzzysort.go(value, items, { + keys: ["title", "keybind"], + threshold: -10000, + }) + + for (const result of results) { + const ids = out.get(result.obj.group) + if (!ids) continue + ids.push(result.obj.id) + } + + return out +} + +function useKeyCapture(input: { + active: () => string | null + stop: () => void + set: (id: string, keybind: string) => void + used: () => Map + language: ReturnType +}) { + onMount(() => { + const handle = (event: KeyboardEvent) => { + const id = input.active() + if (!id) return + + event.preventDefault() + event.stopPropagation() + event.stopImmediatePropagation() + + if (event.key === "Escape") { + input.stop() + return + } + + const clear = + (event.key === "Backspace" || event.key === "Delete") && + !event.ctrlKey && + !event.metaKey && + !event.altKey && + !event.shiftKey + if (clear) { + input.set(id, "none") + input.stop() + return + } + + const next = recordKeybind(event) + if (!next) return + + const conflicts = new Map() + for (const sig of signatures(next)) { + for (const item of input.used().get(sig) ?? []) { + if (item.id === id) continue + conflicts.set(item.id, item.title) + } + } + + if (conflicts.size > 0) { + showToast({ + title: input.language.t("settings.shortcuts.conflict.title"), + description: input.language.t("settings.shortcuts.conflict.description", { + keybind: formatKeybind(next), + titles: [...conflicts.values()].join(", "), + }), + }) + return + } + + input.set(id, next) + input.stop() + } + + document.addEventListener("keydown", handle, true) + onCleanup(() => document.removeEventListener("keydown", handle, true)) + }) +} + export const SettingsKeybinds: Component = () => { const command = useCommand() const language = useLanguage() @@ -135,11 +282,9 @@ export const SettingsKeybinds: Component = () => { command.keybinds(false) } - const hasOverrides = createMemo(() => { - const keybinds = settings.current.keybinds as Record | undefined - if (!keybinds) return false - return Object.values(keybinds).some((x) => typeof x === "string") - }) + const map = createMemo(() => keybinds(settings.current.keybinds)) + + const hasOverrides = createMemo(() => Object.values(map()).some((x) => typeof x === "string")) const resetAll = () => { stop() @@ -152,88 +297,15 @@ export const SettingsKeybinds: Component = () => { const list = createMemo(() => { language.locale() - const out = new Map() - out.set(PALETTE_ID, { title: language.t("command.palette"), group: "General" }) - - for (const opt of command.catalog) { - if (opt.id.startsWith("suggested.")) continue - out.set(opt.id, { title: opt.title, group: groupFor(opt.id) }) - } - - for (const opt of command.options) { - if (opt.id.startsWith("suggested.")) continue - out.set(opt.id, { title: opt.title, group: groupFor(opt.id) }) - } - - const keybinds = settings.current.keybinds as Record | undefined - if (keybinds) { - for (const [id, value] of Object.entries(keybinds)) { - if (typeof value !== "string") continue - if (out.has(id)) continue - out.set(id, { title: id, group: groupFor(id) }) - } - } - - return out + return listFor(command, map(), language.t("command.palette")) }) const title = (id: string) => list().get(id)?.title ?? "" - const grouped = createMemo(() => { - const map = list() - const out = new Map() - - for (const group of GROUPS) out.set(group, []) - - for (const [id, item] of map) { - const ids = out.get(item.group) - if (!ids) continue - ids.push(id) - } - - for (const group of GROUPS) { - const ids = out.get(group) - if (!ids) continue - - ids.sort((a, b) => { - const at = map.get(a)?.title ?? "" - const bt = map.get(b)?.title ?? "" - return at.localeCompare(bt) - }) - } - - return out - }) + const grouped = createMemo(() => groupedFor(list())) const filtered = createMemo(() => { - const query = store.filter.toLowerCase().trim() - if (!query) return grouped() - - const map = list() - const out = new Map() - - for (const group of GROUPS) out.set(group, []) - - const items = Array.from(map.entries()).map(([id, meta]) => ({ - id, - title: meta.title, - group: meta.group, - keybind: command.keybind(id) || "", - })) - - const results = fuzzysort.go(query, items, { - keys: ["title", "keybind"], - threshold: -10000, - }) - - for (const result of results) { - const item = result.obj - const ids = out.get(item.group) - if (!ids) continue - ids.push(item.id) - } - - return out + return filteredFor(store.filter, list(), grouped(), (id) => command.keybind(id) || "") }) const hasResults = createMemo(() => { @@ -282,69 +354,14 @@ export const SettingsKeybinds: Component = () => { return map }) - const setKeybind = (id: string, keybind: string) => { - settings.keybinds.set(id, keybind) - } - - onMount(() => { - const handle = (event: KeyboardEvent) => { - const id = store.active - if (!id) return - - event.preventDefault() - event.stopPropagation() - event.stopImmediatePropagation() + const setKeybind = (id: string, keybind: string) => settings.keybinds.set(id, keybind) - if (event.key === "Escape") { - stop() - return - } - - const clear = - (event.key === "Backspace" || event.key === "Delete") && - !event.ctrlKey && - !event.metaKey && - !event.altKey && - !event.shiftKey - if (clear) { - setKeybind(id, "none") - stop() - return - } - - const next = recordKeybind(event) - if (!next) return - - const map = used() - const conflicts = new Map() - - for (const sig of signatures(next)) { - const list = map.get(sig) ?? [] - for (const item of list) { - if (item.id === id) continue - conflicts.set(item.id, item.title) - } - } - - if (conflicts.size > 0) { - showToast({ - title: language.t("settings.shortcuts.conflict.title"), - description: language.t("settings.shortcuts.conflict.description", { - keybind: formatKeybind(next), - titles: [...conflicts.values()].join(", "), - }), - }) - return - } - - setKeybind(id, next) - stop() - } - - document.addEventListener("keydown", handle, true) - onCleanup(() => { - document.removeEventListener("keydown", handle, true) - }) + useKeyCapture({ + active: () => store.active, + stop, + set: setKeybind, + used, + language, }) onCleanup(() => { diff --git a/packages/app/src/components/settings-mcp.tsx b/packages/app/src/components/settings-mcp.tsx index 928464a51381..507e041aa89f 100644 --- a/packages/app/src/components/settings-mcp.tsx +++ b/packages/app/src/components/settings-mcp.tsx @@ -2,6 +2,7 @@ import { Component } from "solid-js" import { useLanguage } from "@/context/language" export const SettingsMcp: Component = () => { + // TODO: Replace this placeholder with full MCP settings controls. const language = useLanguage() return ( diff --git a/packages/app/src/components/settings-models.tsx b/packages/app/src/components/settings-models.tsx index 1807d561eacc..3a0b7a4fb1b3 100644 --- a/packages/app/src/components/settings-models.tsx +++ b/packages/app/src/components/settings-models.tsx @@ -12,6 +12,25 @@ import { popularProviders } from "@/hooks/use-providers" type ModelItem = ReturnType["list"]>[number] +const ListLoadingState: Component<{ label: string }> = (props) => { + return ( +
+ {props.label} +
+ ) +} + +const ListEmptyState: Component<{ message: string; filter: string }> = (props) => { + return ( +
+ {props.message} + + "{props.filter}" + +
+ ) +} + export const SettingsModels: Component = () => { const language = useLanguage() const models = useModels() @@ -68,24 +87,12 @@ export const SettingsModels: Component = () => { - - {language.t("common.loading")} - {language.t("common.loading.ellipsis")} - -
+ } > 0} - fallback={ -
- {language.t("dialog.model.empty")} - - "{list.filter()}" - -
- } + fallback={} > {(group) => ( diff --git a/packages/app/src/components/settings-permissions.tsx b/packages/app/src/components/settings-permissions.tsx index 7dd43a70753c..348854491ab2 100644 --- a/packages/app/src/components/settings-permissions.tsx +++ b/packages/app/src/components/settings-permissions.tsx @@ -165,12 +165,14 @@ export const SettingsPermissions: Component = () => { const nextValue = existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing, "*": action } : action - globalSync.set("config", "permission", { ...map, [id]: nextValue }) - globalSync.updateConfig({ permission: { [id]: nextValue } }).catch((err: unknown) => { + const rollback = (err: unknown) => { globalSync.set("config", "permission", before) const message = err instanceof Error ? err.message : String(err) showToast({ title: language.t("settings.permissions.toast.updateFailed.title"), description: message }) - }) + } + + globalSync.set("config", "permission", { ...map, [id]: nextValue }) + globalSync.updateConfig({ permission: { [id]: nextValue } }).catch(rollback) } return ( diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index d2444e2d2a9b..a3375c9c608b 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -14,7 +14,17 @@ import { DialogSelectProvider } from "./dialog-select-provider" import { DialogCustomProvider } from "./dialog-custom-provider" type ProviderSource = "env" | "api" | "config" | "custom" -type ProviderMeta = { source?: ProviderSource } +type ProviderItem = ReturnType["connected"]>[number] + +const PROVIDER_NOTES = [ + { match: (id: string) => id === "opencode", key: "dialog.provider.opencode.note" }, + { match: (id: string) => id === "anthropic", key: "dialog.provider.anthropic.note" }, + { match: (id: string) => id.startsWith("github-copilot"), key: "dialog.provider.copilot.note" }, + { match: (id: string) => id === "openai", key: "dialog.provider.openai.note" }, + { match: (id: string) => id === "google", key: "dialog.provider.google.note" }, + { match: (id: string) => id === "openrouter", key: "dialog.provider.openrouter.note" }, + { match: (id: string) => id === "vercel", key: "dialog.provider.vercel.note" }, +] as const export const SettingsProviders: Component = () => { const dialog = useDialog() @@ -44,22 +54,28 @@ export const SettingsProviders: Component = () => { return items }) - const source = (item: unknown) => (item as ProviderMeta).source + const source = (item: ProviderItem): ProviderSource | undefined => { + if (!("source" in item)) return + const value = item.source + if (value === "env" || value === "api" || value === "config" || value === "custom") return value + return + } - const type = (item: unknown) => { + const type = (item: ProviderItem) => { const current = source(item) if (current === "env") return language.t("settings.providers.tag.environment") if (current === "api") return language.t("provider.connect.method.apiKey") if (current === "config") { - const id = (item as { id?: string }).id - if (id && isConfigCustom(id)) return language.t("settings.providers.tag.custom") + if (isConfigCustom(item.id)) return language.t("settings.providers.tag.custom") return language.t("settings.providers.tag.config") } if (current === "custom") return language.t("settings.providers.tag.custom") return language.t("settings.providers.tag.other") } - const canDisconnect = (item: unknown) => source(item) !== "env" + const canDisconnect = (item: ProviderItem) => source(item) !== "env" + + const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key const isConfigCustom = (providerID: string) => { const provider = globalSync.data.config.provider?.[providerID] @@ -175,40 +191,8 @@ export const SettingsProviders: Component = () => { {language.t("dialog.provider.tag.recommended")}
- - - {language.t("dialog.provider.opencode.note")} - - - - - {language.t("dialog.provider.anthropic.note")} - - - - - {language.t("dialog.provider.copilot.note")} - - - - - {language.t("dialog.provider.openai.note")} - - - - - {language.t("dialog.provider.google.note")} - - - - - {language.t("dialog.provider.openrouter.note")} - - - - - {language.t("dialog.provider.vercel.note")} - + + {(key) => {language.t(key())}}
@@ -255,39 +306,40 @@ export function StatusPopover() {
0} + when={mcpNames().length > 0} fallback={
{language.t("dialog.mcp.empty")}
} > - - {(item) => { - const enabled = () => item.status === "connected" + + {(name) => { + const status = () => mcpStatus(name) + const enabled = () => status() === "connected" return ( @@ -334,23 +386,7 @@ export function StatusPopover() {
0} - fallback={ -
- {(() => { - const value = language.t("dialog.plugins.empty") - const file = "opencode.json" - const parts = value.split(file) - if (parts.length === 1) return value - return ( - <> - {parts[0]} - {file} - {parts.slice(1).join(file)} - - ) - })()} -
- } + fallback={
{pluginEmpty()}
} > {(plugin) => ( diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 2ee2e074ef24..14413dfda677 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -3,13 +3,17 @@ import { ComponentProps, createEffect, createSignal, onCleanup, onMount, splitPr import { usePlatform } from "@/context/platform" import { useSDK } from "@/context/sdk" import { monoFontFamily, useSettings } from "@/context/settings" +import { parseKeybind, matchKeybind } from "@/context/command" import { SerializeAddon } from "@/addons/serialize" import { LocalPTY } from "@/context/terminal" import { resolveThemeVariant, useTheme, withAlpha, type HexColor } from "@opencode-ai/ui/theme" import { useLanguage } from "@/context/language" import { showToast } from "@opencode-ai/ui/toast" import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters" +import { terminalWriter } from "@/utils/terminal-writer" +const TOGGLE_TERMINAL_ID = "terminal.toggle" +const DEFAULT_TOGGLE_TERMINAL_KEYBIND = "ctrl+`" export interface TerminalProps extends ComponentProps<"div"> { pty: LocalPTY onSubmit?: () => void @@ -53,6 +57,91 @@ const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = { }, } +const debugTerminal = (...values: unknown[]) => { + if (!import.meta.env.DEV) return + console.debug("[terminal]", ...values) +} + +const useTerminalUiBindings = (input: { + container: HTMLDivElement + term: Term + cleanups: VoidFunction[] + handlePointerDown: () => void + handleLinkClick: (event: MouseEvent) => void +}) => { + const handleCopy = (event: ClipboardEvent) => { + const selection = input.term.getSelection() + if (!selection) return + + const clipboard = event.clipboardData + if (!clipboard) return + + event.preventDefault() + clipboard.setData("text/plain", selection) + } + + const handlePaste = (event: ClipboardEvent) => { + const clipboard = event.clipboardData + const text = clipboard?.getData("text/plain") ?? clipboard?.getData("text") ?? "" + if (!text) return + + event.preventDefault() + event.stopPropagation() + input.term.paste(text) + } + + const handleTextareaFocus = () => { + input.term.options.cursorBlink = true + } + const handleTextareaBlur = () => { + input.term.options.cursorBlink = false + } + + input.container.addEventListener("copy", handleCopy, true) + input.cleanups.push(() => input.container.removeEventListener("copy", handleCopy, true)) + + input.container.addEventListener("paste", handlePaste, true) + input.cleanups.push(() => input.container.removeEventListener("paste", handlePaste, true)) + + input.container.addEventListener("pointerdown", input.handlePointerDown) + input.cleanups.push(() => input.container.removeEventListener("pointerdown", input.handlePointerDown)) + + input.container.addEventListener("click", input.handleLinkClick, { capture: true }) + input.cleanups.push(() => input.container.removeEventListener("click", input.handleLinkClick, { capture: true })) + + input.term.textarea?.addEventListener("focus", handleTextareaFocus) + input.term.textarea?.addEventListener("blur", handleTextareaBlur) + input.cleanups.push(() => input.term.textarea?.removeEventListener("focus", handleTextareaFocus)) + input.cleanups.push(() => input.term.textarea?.removeEventListener("blur", handleTextareaBlur)) +} + +const persistTerminal = (input: { + term: Term | undefined + addon: SerializeAddon | undefined + cursor: number + pty: LocalPTY + onCleanup?: (pty: LocalPTY) => void +}) => { + if (!input.addon || !input.onCleanup || !input.term) return + const buffer = (() => { + try { + return input.addon.serialize() + } catch { + debugTerminal("failed to serialize terminal buffer") + return "" + } + })() + + input.onCleanup({ + ...input.pty, + buffer, + cursor: input.cursor, + rows: input.term.rows, + cols: input.term.cols, + scrollY: input.term.getViewportY(), + }) +} + export const Terminal = (props: TerminalProps) => { const platform = usePlatform() const sdk = useSDK() @@ -67,11 +156,16 @@ export const Terminal = (props: TerminalProps) => { let serializeAddon: SerializeAddon let fitAddon: FitAddon let handleResize: () => void - let handleTextareaFocus: () => void - let handleTextareaBlur: () => void + let fitFrame: number | undefined + let sizeTimer: ReturnType | undefined + let pendingSize: { cols: number; rows: number } | undefined + let lastSize: { cols: number; rows: number } | undefined 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 + let output: ReturnType | undefined const cleanup = () => { if (!cleanups.length) return @@ -79,14 +173,25 @@ export const Terminal = (props: TerminalProps) => { for (const fn of fns) { try { fn() - } catch { - // ignore + } catch (err) { + debugTerminal("cleanup failed", err) } } } + const pushSize = (cols: number, rows: number) => { + return sdk.client.pty + .update({ + ptyID: local.pty.id, + size: { cols, rows }, + }) + .catch((err) => { + debugTerminal("failed to sync terminal size", err) + }) + } + const getTerminalColors = (): TerminalColors => { - const mode = theme.mode() + const mode = theme.mode() === "dark" ? "dark" : "light" const fallback = DEFAULT_TERMINAL_COLORS[mode] const currentTheme = theme.themes()[theme.themeId()] if (!currentTheme) return fallback @@ -108,6 +213,43 @@ export const Terminal = (props: TerminalProps) => { const [terminalColors, setTerminalColors] = createSignal(getTerminalColors()) + const scheduleFit = () => { + if (disposed) return + if (!fitAddon) return + if (fitFrame !== undefined) return + + fitFrame = requestAnimationFrame(() => { + fitFrame = undefined + if (disposed) return + fitAddon.fit() + }) + } + + const scheduleSize = (cols: number, rows: number) => { + if (disposed) return + if (lastSize?.cols === cols && lastSize?.rows === rows) return + + pendingSize = { cols, rows } + + if (!lastSize) { + lastSize = pendingSize + void pushSize(cols, rows) + return + } + + if (sizeTimer !== undefined) return + sizeTimer = setTimeout(() => { + sizeTimer = undefined + const next = pendingSize + if (!next) return + pendingSize = undefined + if (disposed) return + if (lastSize?.cols === next.cols && lastSize?.rows === next.rows) return + lastSize = next + void pushSize(next.cols, next.rows) + }, 100) + } + createEffect(() => { const colors = getTerminalColors() setTerminalColors(colors) @@ -119,17 +261,28 @@ export const Terminal = (props: TerminalProps) => { const font = monoFontFamily(settings.appearance.font()) if (!term) return setOptionIfSupported(term, "fontFamily", font) + scheduleFit() + }) + + let zoom = platform.webviewZoom?.() + createEffect(() => { + const next = platform.webviewZoom?.() + if (next === undefined) return + if (next === zoom) return + zoom = next + scheduleFit() }) const focusTerminal = () => { const t = term if (!t) return t.focus() + t.textarea?.focus() setTimeout(() => t.textarea?.focus(), 0) } const handlePointerDown = () => { const activeElement = document.activeElement - if (activeElement instanceof HTMLElement && activeElement !== container) { + if (activeElement instanceof HTMLElement && activeElement !== container && !container.contains(activeElement)) { activeElement.blur() } focusTerminal() @@ -161,29 +314,27 @@ export const Terminal = (props: TerminalProps) => { const once = { value: false } - const url = new URL(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`) - url.protocol = url.protocol === "https:" ? "wss:" : "ws:" - if (window.__OPENCODE__?.serverPassword) { - url.username = "opencode" - url.password = window.__OPENCODE__?.serverPassword - } - const socket = new WebSocket(url) - cleanups.push(() => { - if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close() - }) - if (disposed) { - cleanup() - return - } - ws = socket + const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : "" + const restoreSize = + restore && + typeof local.pty.cols === "number" && + Number.isSafeInteger(local.pty.cols) && + local.pty.cols > 0 && + typeof local.pty.rows === "number" && + Number.isSafeInteger(local.pty.rows) && + local.pty.rows > 0 + ? { cols: local.pty.cols, rows: local.pty.rows } + : undefined const t = new mod.Terminal({ cursorBlink: true, cursorStyle: "bar", + cols: restoreSize?.cols, + rows: restoreSize?.rows, fontSize: 14, fontFamily: monoFontFamily(settings.appearance.font()), - allowTransparency: true, - convertEol: true, + allowTransparency: false, + convertEol: false, theme: terminalColors(), scrollback: 10_000, ghostty: g, @@ -195,54 +346,21 @@ export const Terminal = (props: TerminalProps) => { } ghostty = g term = t - - const copy = () => { - const selection = t.getSelection() - if (!selection) return false - - const body = document.body - if (body) { - const textarea = document.createElement("textarea") - textarea.value = selection - textarea.setAttribute("readonly", "") - textarea.style.position = "fixed" - textarea.style.opacity = "0" - body.appendChild(textarea) - textarea.select() - const copied = document.execCommand("copy") - body.removeChild(textarea) - if (copied) return true - } - - const clipboard = navigator.clipboard - if (clipboard?.writeText) { - clipboard.writeText(selection).catch(() => {}) - return true - } - - return false - } + output = terminalWriter((data) => t.write(data)) t.attachCustomKeyEventHandler((event) => { const key = event.key.toLowerCase() if (event.ctrlKey && event.shiftKey && !event.metaKey && key === "c") { - copy() + document.execCommand("copy") return true } - if (event.metaKey && !event.ctrlKey && !event.altKey && key === "c") { - if (!t.hasSelection()) return true - copy() - return true - } + // allow for toggle terminal keybinds in parent + const config = settings.keybinds.get(TOGGLE_TERMINAL_ID) ?? DEFAULT_TOGGLE_TERMINAL_KEYBIND + const keybinds = parseKeybind(config) - // allow for ctrl-` to toggle terminal in parent - if (event.ctrlKey && key === "`") { - return true - } - - return false + return matchKeybind(keybinds, event) }) const fit = new mod.FitAddon() @@ -254,57 +372,20 @@ export const Terminal = (props: TerminalProps) => { serializeAddon = serializer t.open(container) - - container.addEventListener("pointerdown", handlePointerDown) - cleanups.push(() => container.removeEventListener("pointerdown", handlePointerDown)) - - container.addEventListener("click", handleLinkClick, { capture: true }) - cleanups.push(() => container.removeEventListener("click", handleLinkClick, { capture: true })) - - handleTextareaFocus = () => { - t.options.cursorBlink = true - } - handleTextareaBlur = () => { - t.options.cursorBlink = false - } - - t.textarea?.addEventListener("focus", handleTextareaFocus) - t.textarea?.addEventListener("blur", handleTextareaBlur) - cleanups.push(() => t.textarea?.removeEventListener("focus", handleTextareaFocus)) - cleanups.push(() => t.textarea?.removeEventListener("blur", handleTextareaBlur)) + useTerminalUiBindings({ container, term: t, cleanups, handlePointerDown, handleLinkClick }) focusTerminal() - fit.fit() - - if (local.pty.buffer) { - t.write(local.pty.buffer, () => { - if (local.pty.scrollY) t.scrollToLine(local.pty.scrollY) - }) + if (typeof document !== "undefined" && document.fonts) { + document.fonts.ready.then(scheduleFit) } - fit.observeResize() - handleResize = () => fit.fit() - window.addEventListener("resize", handleResize) - cleanups.push(() => window.removeEventListener("resize", handleResize)) - const onResize = t.onResize(async (size) => { - if (socket.readyState === WebSocket.OPEN) { - await sdk.client.pty - .update({ - ptyID: local.pty.id, - size: { - cols: size.cols, - rows: size.rows, - }, - }) - .catch(() => {}) - } + const onResize = t.onResize((size) => { + scheduleSize(size.cols, size.rows) }) cleanups.push(() => disposeIfDisposable(onResize)) const onData = t.onData((data) => { - if (socket.readyState === WebSocket.OPEN) { - socket.send(data) - } + if (ws?.readyState === WebSocket.OPEN) ws.send(data) }) cleanups.push(() => disposeIfDisposable(onData)) const onKey = t.onKey((key) => { @@ -313,58 +394,89 @@ export const Terminal = (props: TerminalProps) => { } }) cleanups.push(() => disposeIfDisposable(onKey)) + + const startResize = () => { + fit.observeResize() + handleResize = scheduleFit + window.addEventListener("resize", handleResize) + cleanups.push(() => window.removeEventListener("resize", handleResize)) + } + + if (restore && restoreSize) { + t.write(restore, () => { + fit.fit() + scheduleSize(t.cols, t.rows) + if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY) + startResize() + }) + } else { + fit.fit() + scheduleSize(t.cols, t.rows) + if (restore) { + t.write(restore, () => { + if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY) + }) + } + startResize() + } + // t.onScroll((ydisp) => { // console.log("Scroll position:", ydisp) // }) - const limit = 16_384 - const seed = tail - let sync = !!seed - - const overlap = (data: string) => { - if (!seed) return 0 - const max = Math.min(seed.length, data.length) - for (let i = max; i > 0; i--) { - if (seed.slice(-i) === data.slice(0, i)) return i - } - return 0 + 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" + ws = socket + cleanups.push(() => { + if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close() + }) + if (disposed) { + cleanup() + return } const handleOpen = () => { local.onConnect?.() - sdk.client.pty - .update({ - ptyID: local.pty.id, - size: { - cols: t.cols, - rows: t.rows, - }, - }) - .catch(() => {}) + scheduleSize(t.cols, t.rows) } socket.addEventListener("open", handleOpen) cleanups.push(() => socket.removeEventListener("open", handleOpen)) - const handleMessage = (event: MessageEvent) => { - const data = typeof event.data === "string" ? event.data : "" - if (!data) return + if (socket.readyState === WebSocket.OPEN) handleOpen() - const next = (() => { - if (!sync) return data - const n = overlap(data) - if (!n) { - sync = false - return data - } - const trimmed = data.slice(n) - if (trimmed) sync = false - return trimmed - })() + const decoder = new TextDecoder() - if (!next) return + 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 (err) { + debugTerminal("invalid websocket control frame", err) + } + return + } - t.write(next) - tail = next.length >= limit ? next.slice(-limit) : (tail + next).slice(-limit) + const data = typeof event.data === "string" ? event.data : "" + if (!data) return + output?.push(data) + cursor += data.length } socket.addEventListener("message", handleMessage) cleanups.push(() => socket.removeEventListener("message", handleMessage)) @@ -406,25 +518,10 @@ export const Terminal = (props: TerminalProps) => { onCleanup(() => { disposed = true - const t = term - if (serializeAddon && props.onCleanup && t) { - const buffer = (() => { - try { - return serializeAddon.serialize() - } catch { - return "" - } - })() - props.onCleanup({ - ...local.pty, - buffer, - tail, - rows: t.rows, - cols: t.cols, - scrollY: t.getViewportY(), - }) - } - + if (fitFrame !== undefined) cancelAnimationFrame(fitFrame) + if (sizeTimer !== undefined) clearTimeout(sizeTimer) + output?.flush() + persistTerminal({ term, addon: serializeAddon, cursor, pty: local.pty, onCleanup: props.onCleanup }) cleanup() }) @@ -438,7 +535,7 @@ export const Terminal = (props: TerminalProps) => { classList={{ ...(local.classList ?? {}), "select-text": true, - "size-full px-6 py-3 font-mono": true, + "size-full px-6 py-3 font-mono relative overflow-hidden": true, [local.class ?? ""]: !!local.class, }} {...others} diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 4a43a855ce16..039a25faee80 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -13,6 +13,28 @@ import { useCommand } from "@/context/command" import { useLanguage } from "@/context/language" import { applyPath, backPath, forwardPath } from "./titlebar-history" +type TauriDesktopWindow = { + startDragging?: () => Promise + toggleMaximize?: () => Promise +} + +type TauriThemeWindow = { + setTheme?: (theme?: "light" | "dark" | null) => Promise +} + +type TauriApi = { + window?: { + getCurrentWindow?: () => TauriDesktopWindow + } + webviewWindow?: { + getCurrentWebviewWindow?: () => TauriThemeWindow + } +} + +const tauriApi = () => (window as unknown as { __TAURI__?: TauriApi }).__TAURI__ +const currentDesktopWindow = () => tauriApi()?.window?.getCurrentWindow?.() +const currentThemeWindow = () => tauriApi()?.webviewWindow?.getCurrentWebviewWindow?.() + export function Titlebar() { const layout = useLayout() const platform = usePlatform() @@ -68,34 +90,21 @@ export function Titlebar() { id: "common.goBack", title: language.t("common.goBack"), category: language.t("command.category.view"), + keybind: "mod+[", onSelect: back, }, { id: "common.goForward", title: language.t("common.goForward"), category: language.t("command.category.view"), + keybind: "mod+]", onSelect: forward, }, ]) const getWin = () => { if (platform.platform !== "desktop") return - - const tauri = ( - window as unknown as { - __TAURI__?: { - window?: { - getCurrentWindow?: () => { - startDragging?: () => Promise - toggleMaximize?: () => Promise - } - } - } - } - ).__TAURI__ - if (!tauri?.window?.getCurrentWindow) return - - return tauri.window.getCurrentWindow() + return currentDesktopWindow() } createEffect(() => { @@ -104,13 +113,8 @@ export function Titlebar() { const scheme = theme.colorScheme() const value = scheme === "system" ? null : scheme - const tauri = (window as unknown as { __TAURI__?: { webviewWindow?: { getCurrentWebviewWindow?: () => unknown } } }) - .__TAURI__ - const get = tauri?.webviewWindow?.getCurrentWebviewWindow - if (!get) return - - const win = get() as { setTheme?: (theme?: "light" | "dark" | null) => Promise } - if (!win.setTheme) return + const win = currentThemeWindow() + if (!win?.setTheme) return void win.setTheme(value).catch(() => undefined) }) diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx index e6a16fd4bb37..03437c973597 100644 --- a/packages/app/src/context/command.tsx +++ b/packages/app/src/context/command.tsx @@ -11,6 +11,7 @@ const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(na const PALETTE_ID = "command.palette" const DEFAULT_PALETTE_KEYBIND = "mod+shift+p" const SUGGESTED_PREFIX = "suggested." +const EDITABLE_KEYBIND_IDS = new Set(["terminal.toggle", "terminal.new"]) function actionId(id: string) { if (!id.startsWith(SUGGESTED_PREFIX)) return id @@ -33,6 +34,11 @@ function signatureFromEvent(event: KeyboardEvent) { return signature(normalizeKey(event.key), event.ctrlKey, event.metaKey, event.shiftKey, event.altKey) } +function isAllowedEditableKeybind(id: string | undefined) { + if (!id) return false + return EDITABLE_KEYBIND_IDS.has(actionId(id)) +} + export type KeybindConfig = string export interface Keybind { @@ -56,6 +62,8 @@ export interface CommandOption { onHighlight?: () => (() => void) | void } +type CommandSource = "palette" | "keybind" | "slash" + export type CommandCatalogItem = { title: string description?: string @@ -169,6 +177,14 @@ export function formatKeybind(config: string): string { return IS_MAC ? parts.join("") : parts.join("+") } +function isEditableTarget(target: EventTarget | null) { + if (!(target instanceof HTMLElement)) return false + if (target.isContentEditable) return true + if (target.closest("[contenteditable='true']")) return true + if (target.closest("input, textarea, select")) return true + return false +} + export const { use: useCommand, provider: CommandProvider } = createSimpleContext({ name: "Command", init: () => { @@ -275,13 +291,18 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex return map }) - const run = (id: string, source?: "palette" | "keybind" | "slash") => { + const optionMap = createMemo(() => { + const map = new Map() for (const option of options()) { - if (option.id === id || option.id === "suggested." + id) { - option.onSelect?.(source) - return - } + map.set(option.id, option) + map.set(actionId(option.id), option) } + return map + }) + + const run = (id: string, source?: CommandSource) => { + const option = optionMap().get(id) + option?.onSelect?.(source) } const showPalette = () => { @@ -292,14 +313,20 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex if (suspended() || dialog.active) return const sig = signatureFromEvent(event) + const isPalette = palette().has(sig) + const option = keymap().get(sig) + const modified = event.ctrlKey || event.metaKey || event.altKey + const isTab = event.key === "Tab" - if (palette().has(sig)) { + if (isEditableTarget(event.target) && !isPalette && !isAllowedEditableKeybind(option?.id) && !modified && !isTab) + return + + if (isPalette) { event.preventDefault() showPalette() return } - const option = keymap().get(sig) if (!option) return event.preventDefault() option.onSelect?.("keybind") @@ -332,7 +359,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex return { register, - trigger(id: string, source?: "palette" | "keybind" | "slash") { + trigger(id: string, source?: CommandSource) { run(id, source) }, keybind(id: string) { @@ -351,7 +378,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex }, show: showPalette, keybinds(enabled: boolean) { - setStore("suspendCount", (count) => count + (enabled ? -1 : 1)) + setStore("suspendCount", (count) => Math.max(0, count + (enabled ? -1 : 1))) }, suspended, get catalog() { diff --git a/packages/app/src/context/comments.test.ts b/packages/app/src/context/comments.test.ts index 13cb132c4dd4..bee5c7871e07 100644 --- a/packages/app/src/context/comments.test.ts +++ b/packages/app/src/context/comments.test.ts @@ -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,4 +109,45 @@ describe("comments session indexing", () => { dispose() }) }) + + test("remove keeps focus when same comment id exists in another file", () => { + createRoot((dispose) => { + const comments = createCommentSessionForTest({ + "a.ts": [line("a.ts", "shared", 10)], + "b.ts": [line("b.ts", "shared", 20)], + }) + + comments.setFocus({ file: "b.ts", id: "shared" }) + comments.remove("a.ts", "shared") + + expect(comments.focus()).toEqual({ file: "b.ts", id: "shared" }) + expect(comments.list("a.ts")).toEqual([]) + expect(comments.list("b.ts").map((item) => item.id)).toEqual(["shared"]) + + dispose() + }) + }) + + test("setFocus and setActive updater callbacks receive current state", () => { + createRoot((dispose) => { + const comments = createCommentSessionForTest() + + comments.setFocus({ file: "a.ts", id: "a1" }) + comments.setFocus((current) => { + expect(current).toEqual({ file: "a.ts", id: "a1" }) + return { file: "b.ts", id: "b1" } + }) + + comments.setActive({ file: "c.ts", id: "c1" }) + comments.setActive((current) => { + expect(current).toEqual({ file: "c.ts", id: "c1" }) + return null + }) + + expect(comments.focus()).toEqual({ file: "b.ts", id: "b1" }) + expect(comments.active()).toBeNull() + + dispose() + }) + }) }) diff --git a/packages/app/src/context/comments.tsx b/packages/app/src/context/comments.tsx index d43f3705befd..ecf63e45b648 100644 --- a/packages/app/src/context/comments.tsx +++ b/packages/app/src/context/comments.tsx @@ -1,9 +1,10 @@ -import { batch, createEffect, createMemo, createRoot, onCleanup } from "solid-js" +import { batch, createMemo, createRoot, onCleanup } from "solid-js" import { createStore, reconcile, type SetStoreFunction, type Store } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" import { useParams } from "@solidjs/router" import { Persist, persisted } from "@/utils/persist" import { createScopedCache } from "@/utils/scoped-cache" +import { uuid } from "@/utils/uuid" import type { SelectedLineRange } from "@/context/file" export type LineComment = { @@ -19,6 +20,19 @@ type CommentFocus = { file: string; id: string } const WORKSPACE_KEY = "__workspace__" const MAX_COMMENT_SESSIONS = 20 +function sessionKey(dir: string, id: string | undefined) { + return `${dir}\n${id ?? WORKSPACE_KEY}` +} + +function decodeSessionKey(key: string) { + const split = key.lastIndexOf("\n") + if (split < 0) return { dir: key, id: WORKSPACE_KEY } + return { + dir: key.slice(0, split), + id: key.slice(split + 1), + } +} + type CommentStore = { comments: Record } @@ -30,37 +44,36 @@ function aggregate(comments: Record) { .sort((a, b) => a.time - b.time) } -function insert(items: LineComment[], next: LineComment) { - const index = items.findIndex((item) => item.time > next.time) - if (index < 0) return [...items, next] - return [...items.slice(0, index), next, ...items.slice(index)] -} - function createCommentSessionState(store: Store, setStore: SetStoreFunction) { const [state, setState] = createStore({ focus: null as CommentFocus | null, active: null as CommentFocus | null, - all: aggregate(store.comments), }) + const all = () => aggregate(store.comments) + + const setRef = ( + key: "focus" | "active", + value: CommentFocus | null | ((value: CommentFocus | null) => CommentFocus | null), + ) => setState(key, value) + const setFocus = (value: CommentFocus | null | ((value: CommentFocus | null) => CommentFocus | null)) => - setState("focus", value) + setRef("focus", value) const setActive = (value: CommentFocus | null | ((value: CommentFocus | null) => CommentFocus | null)) => - setState("active", value) + setRef("active", value) const list = (file: string) => store.comments[file] ?? [] const add = (input: Omit) => { const next: LineComment = { - id: crypto.randomUUID(), + id: uuid(), time: Date.now(), ...input, } batch(() => { setStore("comments", input.file, (items) => [...(items ?? []), next]) - setState("all", (items) => insert(items, next)) setFocus({ file: input.file, id: next.id }) }) @@ -70,15 +83,13 @@ function createCommentSessionState(store: Store, setStore: SetStor const remove = (file: string, id: string) => { batch(() => { setStore("comments", file, (items) => (items ?? []).filter((item) => item.id !== id)) - setState("all", (items) => items.filter((item) => !(item.file === file && item.id === id))) - setFocus((current) => (current?.id === id ? null : current)) + setFocus((current) => (current?.file === file && current.id === id ? null : current)) }) } const clear = () => { batch(() => { setStore("comments", reconcile({})) - setState("all", []) setFocus(null) setActive(null) }) @@ -86,17 +97,16 @@ function createCommentSessionState(store: Store, setStore: SetStor return { list, - all: () => state.all, + all, add, remove, clear, focus: () => state.focus, setFocus, - clearFocus: () => setFocus(null), + clearFocus: () => setRef("focus", null), active: () => state.active, setActive, - clearActive: () => setActive(null), - reindex: () => setState("all", aggregate(store.comments)), + clearActive: () => setRef("active", null), } } @@ -116,11 +126,6 @@ function createCommentSession(dir: string, id: string | undefined) { ) const session = createCommentSessionState(store, setStore) - createEffect(() => { - if (!ready()) return - session.reindex() - }) - return { ready, list: session.list, @@ -144,11 +149,9 @@ export const { use: useComments, provider: CommentsProvider } = createSimpleCont const params = useParams() const cache = createScopedCache( (key) => { - const split = key.lastIndexOf("\n") - const dir = split >= 0 ? key.slice(0, split) : key - const id = split >= 0 ? key.slice(split + 1) : WORKSPACE_KEY + const decoded = decodeSessionKey(key) return createRoot((dispose) => ({ - value: createCommentSession(dir, id === WORKSPACE_KEY ? undefined : id), + value: createCommentSession(decoded.dir, decoded.id === WORKSPACE_KEY ? undefined : decoded.id), dispose, })) }, @@ -161,7 +164,7 @@ export const { use: useComments, provider: CommentsProvider } = createSimpleCont onCleanup(() => cache.clear()) const load = (dir: string, id: string | undefined) => { - const key = `${dir}\n${id ?? WORKSPACE_KEY}` + const key = sessionKey(dir, id) return cache.get(key).value } diff --git a/packages/app/src/context/file.tsx b/packages/app/src/context/file.tsx index 996ea2aafedf..99c6d2e4219d 100644 --- a/packages/app/src/context/file.tsx +++ b/packages/app/src/context/file.tsx @@ -7,6 +7,7 @@ import { getFilename } from "@opencode-ai/util/path" import { useSDK } from "./sdk" import { useSync } from "./sync" import { useLanguage } from "@/context/language" +import { useLayout } from "@/context/layout" import { createPathHelpers } from "./file/path" import { approxBytes, @@ -42,6 +43,12 @@ export { touchFileContent, } +function errorMessage(error: unknown) { + if (error instanceof Error && error.message) return error.message + if (typeof error === "string" && error) return error + return "Unknown error" +} + export const { use: useFile, provider: FileProvider } = createSimpleContext({ name: "File", gate: false, @@ -50,9 +57,11 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ useSync() const params = useParams() const language = useLanguage() + const layout = useLayout() const scope = createMemo(() => sdk.directory) const path = createPathHelpers(scope) + const tabs = layout.tabs(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const inflight = new Map>() const [store, setStore] = createStore<{ @@ -107,6 +116,45 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ setStore("file", file, { path: file, name: getFilename(file) }) } + const setLoading = (file: string) => { + setStore( + "file", + file, + produce((draft) => { + draft.loading = true + draft.error = undefined + }), + ) + } + + const setLoaded = (file: string, content: FileState["content"]) => { + setStore( + "file", + file, + produce((draft) => { + draft.loaded = true + draft.loading = false + draft.content = content + }), + ) + } + + const setLoadError = (file: string, message: string) => { + setStore( + "file", + file, + produce((draft) => { + draft.loading = false + draft.error = message + }), + ) + showToast({ + variant: "error", + title: language.t("toast.file.loadFailed.title"), + description: message, + }) + } + const load = (input: string, options?: { force?: boolean }) => { const file = path.normalize(input) if (!file) return Promise.resolve() @@ -121,29 +169,14 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ const pending = inflight.get(key) if (pending) return pending - setStore( - "file", - file, - produce((draft) => { - draft.loading = true - draft.error = undefined - }), - ) + setLoading(file) const promise = sdk.client.file .read({ path: file }) .then((x) => { if (scope() !== directory) return const content = x.data - setStore( - "file", - file, - produce((draft) => { - draft.loaded = true - draft.loading = false - draft.content = content - }), - ) + setLoaded(file, content) if (!content) return touchFileContent(file, approxBytes(content)) @@ -151,19 +184,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ }) .catch((e) => { if (scope() !== directory) return - setStore( - "file", - file, - produce((draft) => { - draft.loading = false - draft.error = e.message - }), - ) - showToast({ - variant: "error", - title: language.t("toast.file.loadFailed.title"), - description: e.message, - }) + setLoadError(file, errorMessage(e)) }) .finally(() => { inflight.delete(key) @@ -183,6 +204,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ invalidateFromWatcher(e.details, { normalize: path.normalize, hasFile: (file) => Boolean(store.file[file]), + isOpen: (file) => tabs.all().some((tab) => path.pathFromTab(tab) === file), loadFile: (file) => { void load(file, { force: true }) }, @@ -207,21 +229,16 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ return state } - const scrollTop = (input: string) => view().scrollTop(path.normalize(input)) - const scrollLeft = (input: string) => view().scrollLeft(path.normalize(input)) - const selectedLines = (input: string) => view().selectedLines(path.normalize(input)) - - const setScrollTop = (input: string, top: number) => { - view().setScrollTop(path.normalize(input), top) - } - - const setScrollLeft = (input: string, left: number) => { - view().setScrollLeft(path.normalize(input), left) - } - - const setSelectedLines = (input: string, range: SelectedLineRange | null) => { - view().setSelectedLines(path.normalize(input), range) + function withPath(input: string, action: (file: string) => unknown) { + return action(path.normalize(input)) } + const scrollTop = (input: string) => withPath(input, (file) => view().scrollTop(file)) + const scrollLeft = (input: string) => withPath(input, (file) => view().scrollLeft(file)) + const selectedLines = (input: string) => withPath(input, (file) => view().selectedLines(file)) + const setScrollTop = (input: string, top: number) => withPath(input, (file) => view().setScrollTop(file, top)) + const setScrollLeft = (input: string, left: number) => withPath(input, (file) => view().setScrollLeft(file, left)) + const setSelectedLines = (input: string, range: SelectedLineRange | null) => + withPath(input, (file) => view().setSelectedLines(file, range)) onCleanup(() => { stop() diff --git a/packages/app/src/context/file/path.test.ts b/packages/app/src/context/file/path.test.ts index dba9ae06dcba..f2a3c44b6c4a 100644 --- a/packages/app/src/context/file/path.test.ts +++ b/packages/app/src/context/file/path.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { createPathHelpers, stripQueryAndHash, unquoteGitPath } from "./path" +import { createPathHelpers, stripQueryAndHash, unquoteGitPath, encodeFilePath } from "./path" describe("file path helpers", () => { test("normalizes file inputs against workspace root", () => { @@ -25,3 +25,328 @@ describe("file path helpers", () => { expect(unquoteGitPath("a/b/c.ts")).toBe("a/b/c.ts") }) }) + +describe("encodeFilePath", () => { + describe("Linux/Unix paths", () => { + test("should handle Linux absolute path", () => { + const linuxPath = "/home/user/project/README.md" + const result = encodeFilePath(linuxPath) + const fileUrl = `file://${result}` + + // Should create a valid URL + expect(() => new URL(fileUrl)).not.toThrow() + expect(result).toBe("/home/user/project/README.md") + + const url = new URL(fileUrl) + expect(url.protocol).toBe("file:") + expect(url.pathname).toBe("/home/user/project/README.md") + }) + + test("should handle Linux path with special characters", () => { + const linuxPath = "/home/user/file#name with spaces.txt" + const result = encodeFilePath(linuxPath) + const fileUrl = `file://${result}` + + expect(() => new URL(fileUrl)).not.toThrow() + expect(result).toBe("/home/user/file%23name%20with%20spaces.txt") + }) + + test("should handle Linux relative path", () => { + const relativePath = "src/components/App.tsx" + const result = encodeFilePath(relativePath) + + expect(result).toBe("src/components/App.tsx") + }) + + test("should handle Linux root directory", () => { + const result = encodeFilePath("/") + expect(result).toBe("/") + }) + + test("should handle Linux path with all special chars", () => { + const path = "/path/to/file#with?special%chars&more.txt" + const result = encodeFilePath(path) + const fileUrl = `file://${result}` + + expect(() => new URL(fileUrl)).not.toThrow() + expect(result).toContain("%23") // # + expect(result).toContain("%3F") // ? + expect(result).toContain("%25") // % + expect(result).toContain("%26") // & + }) + }) + + describe("macOS paths", () => { + test("should handle macOS absolute path", () => { + const macPath = "/Users/kelvin/Projects/opencode/README.md" + const result = encodeFilePath(macPath) + const fileUrl = `file://${result}` + + expect(() => new URL(fileUrl)).not.toThrow() + expect(result).toBe("/Users/kelvin/Projects/opencode/README.md") + }) + + test("should handle macOS path with spaces", () => { + const macPath = "/Users/kelvin/My Documents/file.txt" + const result = encodeFilePath(macPath) + const fileUrl = `file://${result}` + + expect(() => new URL(fileUrl)).not.toThrow() + expect(result).toContain("My%20Documents") + }) + }) + + describe("Windows paths", () => { + test("should handle Windows absolute path with backslashes", () => { + const windowsPath = "D:\\dev\\projects\\opencode\\README.bs.md" + const result = encodeFilePath(windowsPath) + const fileUrl = `file://${result}` + + // Should create a valid, parseable URL + expect(() => new URL(fileUrl)).not.toThrow() + + const url = new URL(fileUrl) + expect(url.protocol).toBe("file:") + expect(url.pathname).toContain("README.bs.md") + expect(result).toBe("/D:/dev/projects/opencode/README.bs.md") + }) + + test("should handle mixed separator path (Windows + Unix)", () => { + // This is what happens in build-request-parts.ts when concatenating paths + const mixedPath = "D:\\dev\\projects\\opencode/README.bs.md" + const result = encodeFilePath(mixedPath) + const fileUrl = `file://${result}` + + expect(() => new URL(fileUrl)).not.toThrow() + expect(result).toBe("/D:/dev/projects/opencode/README.bs.md") + }) + + test("should handle Windows path with spaces", () => { + const windowsPath = "C:\\Program Files\\MyApp\\file with spaces.txt" + const result = encodeFilePath(windowsPath) + const fileUrl = `file://${result}` + + expect(() => new URL(fileUrl)).not.toThrow() + expect(result).toContain("Program%20Files") + expect(result).toContain("file%20with%20spaces.txt") + }) + + test("should handle Windows path with special chars in filename", () => { + const windowsPath = "D:\\projects\\file#name with ?marks.txt" + const result = encodeFilePath(windowsPath) + const fileUrl = `file://${result}` + + expect(() => new URL(fileUrl)).not.toThrow() + expect(result).toContain("file%23name%20with%20%3Fmarks.txt") + }) + + test("should handle Windows root directory", () => { + const windowsPath = "C:\\" + const result = encodeFilePath(windowsPath) + const fileUrl = `file://${result}` + + expect(() => new URL(fileUrl)).not.toThrow() + expect(result).toBe("/C:/") + }) + + test("should handle Windows relative path with backslashes", () => { + const windowsPath = "src\\components\\App.tsx" + const result = encodeFilePath(windowsPath) + + // Relative paths shouldn't get the leading slash + expect(result).toBe("src/components/App.tsx") + }) + + test("should NOT create invalid URL like the bug report", () => { + // This is the exact scenario from bug report by @alexyaroshuk + const windowsPath = "D:\\dev\\projects\\opencode\\README.bs.md" + const result = encodeFilePath(windowsPath) + const fileUrl = `file://${result}` + + // The bug was creating: file://D%3A%5Cdev%5Cprojects%5Copencode/README.bs.md + expect(result).not.toContain("%5C") // Should not have encoded backslashes + expect(result).not.toBe("D%3A%5Cdev%5Cprojects%5Copencode/README.bs.md") + + // Should be valid + expect(() => new URL(fileUrl)).not.toThrow() + }) + + test("should handle lowercase drive letters", () => { + const windowsPath = "c:\\users\\test\\file.txt" + const result = encodeFilePath(windowsPath) + const fileUrl = `file://${result}` + + expect(() => new URL(fileUrl)).not.toThrow() + expect(result).toBe("/c:/users/test/file.txt") + }) + }) + + describe("Cross-platform compatibility", () => { + test("should preserve Unix paths unchanged (except encoding)", () => { + const unixPath = "/usr/local/bin/app" + const result = encodeFilePath(unixPath) + expect(result).toBe("/usr/local/bin/app") + }) + + test("should normalize Windows paths for cross-platform use", () => { + const windowsPath = "C:\\Users\\test\\file.txt" + const result = encodeFilePath(windowsPath) + // Should convert to forward slashes and add leading / + expect(result).not.toContain("\\") + expect(result).toMatch(/^\/[A-Za-z]:\//) + }) + + test("should handle relative paths the same on all platforms", () => { + const unixRelative = "src/app.ts" + const windowsRelative = "src\\app.ts" + + const unixResult = encodeFilePath(unixRelative) + const windowsResult = encodeFilePath(windowsRelative) + + // Both should normalize to forward slashes + expect(unixResult).toBe("src/app.ts") + expect(windowsResult).toBe("src/app.ts") + }) + }) + + describe("Edge cases", () => { + test("should handle empty path", () => { + const result = encodeFilePath("") + expect(result).toBe("") + }) + + test("should handle path with multiple consecutive slashes", () => { + const result = encodeFilePath("//path//to///file.txt") + // Multiple slashes should be preserved (backend handles normalization) + expect(result).toBe("//path//to///file.txt") + }) + + test("should encode Unicode characters", () => { + const unicodePath = "/home/user/文档/README.md" + const result = encodeFilePath(unicodePath) + const fileUrl = `file://${result}` + + expect(() => new URL(fileUrl)).not.toThrow() + // Unicode should be encoded + expect(result).toContain("%E6%96%87%E6%A1%A3") + }) + + test("should handle already normalized Windows path", () => { + // Path that's already been normalized (has / before drive letter) + const alreadyNormalized = "/D:/path/file.txt" + const result = encodeFilePath(alreadyNormalized) + + // Should not add another leading slash + expect(result).toBe("/D:/path/file.txt") + expect(result).not.toContain("//D") + }) + + test("should handle just drive letter", () => { + const justDrive = "D:" + const result = encodeFilePath(justDrive) + const fileUrl = `file://${result}` + + expect(result).toBe("/D:") + expect(() => new URL(fileUrl)).not.toThrow() + }) + + test("should handle Windows path with trailing backslash", () => { + const trailingBackslash = "C:\\Users\\test\\" + const result = encodeFilePath(trailingBackslash) + const fileUrl = `file://${result}` + + expect(() => new URL(fileUrl)).not.toThrow() + expect(result).toBe("/C:/Users/test/") + }) + + test("should handle very long paths", () => { + const longPath = "C:\\Users\\test\\" + "verylongdirectoryname\\".repeat(20) + "file.txt" + const result = encodeFilePath(longPath) + const fileUrl = `file://${result}` + + expect(() => new URL(fileUrl)).not.toThrow() + expect(result).not.toContain("\\") + }) + + test("should handle paths with dots", () => { + const pathWithDots = "C:\\Users\\..\\test\\.\\file.txt" + const result = encodeFilePath(pathWithDots) + const fileUrl = `file://${result}` + + expect(() => new URL(fileUrl)).not.toThrow() + // Dots should be preserved (backend normalizes) + expect(result).toContain("..") + expect(result).toContain("/./") + }) + }) + + describe("Regression tests for PR #12424", () => { + test("should handle file with # in name", () => { + const path = "/path/to/file#name.txt" + const result = encodeFilePath(path) + const fileUrl = `file://${result}` + + expect(() => new URL(fileUrl)).not.toThrow() + expect(result).toBe("/path/to/file%23name.txt") + }) + + test("should handle file with ? in name", () => { + const path = "/path/to/file?name.txt" + const result = encodeFilePath(path) + const fileUrl = `file://${result}` + + expect(() => new URL(fileUrl)).not.toThrow() + expect(result).toBe("/path/to/file%3Fname.txt") + }) + + test("should handle file with % in name", () => { + const path = "/path/to/file%name.txt" + const result = encodeFilePath(path) + const fileUrl = `file://${result}` + + expect(() => new URL(fileUrl)).not.toThrow() + expect(result).toBe("/path/to/file%25name.txt") + }) + }) + + describe("Integration with file:// URL construction", () => { + test("should work with query parameters (Linux)", () => { + const path = "/home/user/file.txt" + const encoded = encodeFilePath(path) + const fileUrl = `file://${encoded}?start=10&end=20` + + const url = new URL(fileUrl) + expect(url.searchParams.get("start")).toBe("10") + expect(url.searchParams.get("end")).toBe("20") + expect(url.pathname).toBe("/home/user/file.txt") + }) + + test("should work with query parameters (Windows)", () => { + const path = "C:\\Users\\test\\file.txt" + const encoded = encodeFilePath(path) + const fileUrl = `file://${encoded}?start=10&end=20` + + const url = new URL(fileUrl) + expect(url.searchParams.get("start")).toBe("10") + expect(url.searchParams.get("end")).toBe("20") + }) + + test("should parse correctly in URL constructor (Linux)", () => { + const path = "/var/log/app.log" + const fileUrl = `file://${encodeFilePath(path)}` + const url = new URL(fileUrl) + + expect(url.protocol).toBe("file:") + expect(url.pathname).toBe("/var/log/app.log") + }) + + test("should parse correctly in URL constructor (Windows)", () => { + const path = "D:\\logs\\app.log" + const fileUrl = `file://${encodeFilePath(path)}` + const url = new URL(fileUrl) + + expect(url.protocol).toBe("file:") + expect(url.pathname).toContain("app.log") + }) + }) +}) diff --git a/packages/app/src/context/file/path.ts b/packages/app/src/context/file/path.ts index ced30d0fdd06..859fdc04062f 100644 --- a/packages/app/src/context/file/path.ts +++ b/packages/app/src/context/file/path.ts @@ -72,12 +72,41 @@ export function unquoteGitPath(input: string) { return new TextDecoder().decode(new Uint8Array(bytes)) } +export function decodeFilePath(input: string) { + try { + return decodeURIComponent(input) + } catch { + return input + } +} + +export function 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) + // Keep the colon in Windows drive letters (`/C:/...`) so downstream file URL parsers + // can reliably detect drives. + return normalized + .split("/") + .map((segment, index) => { + if (index === 1 && /^[A-Za-z]:$/.test(segment)) return segment + return encodeURIComponent(segment) + }) + .join("/") +} + export function createPathHelpers(scope: () => string) { const normalize = (input: string) => { const root = scope() const prefix = root.endsWith("/") ? root : root + "/" - let path = unquoteGitPath(stripQueryAndHash(stripFileProtocol(input))) + let path = unquoteGitPath(decodeFilePath(stripQueryAndHash(stripFileProtocol(input)))) if (path.startsWith(prefix)) { path = path.slice(prefix.length) @@ -100,7 +129,7 @@ export function createPathHelpers(scope: () => string) { const tab = (input: string) => { const path = normalize(input) - return `file://${path}` + return `file://${encodeFilePath(path)}` } const pathFromTab = (tabValue: string) => { diff --git a/packages/app/src/context/file/view-cache.ts b/packages/app/src/context/file/view-cache.ts index 2614b2fb5335..6e8ddf62df87 100644 --- a/packages/app/src/context/file/view-cache.ts +++ b/packages/app/src/context/file/view-cache.ts @@ -23,6 +23,16 @@ function normalizeSelectedLines(range: SelectedLineRange): SelectedLineRange { } } +function equalSelectedLines(a: SelectedLineRange | null | undefined, b: SelectedLineRange | null | undefined) { + if (!a && !b) return true + if (!a || !b) return false + const left = normalizeSelectedLines(a) + const right = normalizeSelectedLines(b) + return ( + left.start === right.start && left.end === right.end && left.side === right.side && left.endSide === right.endSide + ) +} + function createViewSession(dir: string, id: string | undefined) { const legacyViewKey = `${dir}/file${id ? "/" + id : ""}.v1` @@ -65,36 +75,36 @@ function createViewSession(dir: string, id: string | undefined) { const selectedLines = (path: string) => view.file[path]?.selectedLines const setScrollTop = (path: string, top: number) => { - setView("file", path, (current) => { - if (current?.scrollTop === top) return current - return { - ...(current ?? {}), - scrollTop: top, - } - }) + setView( + produce((draft) => { + const file = draft.file[path] ?? (draft.file[path] = {}) + if (file.scrollTop === top) return + file.scrollTop = top + }), + ) pruneView(path) } const setScrollLeft = (path: string, left: number) => { - setView("file", path, (current) => { - if (current?.scrollLeft === left) return current - return { - ...(current ?? {}), - scrollLeft: left, - } - }) + setView( + produce((draft) => { + const file = draft.file[path] ?? (draft.file[path] = {}) + if (file.scrollLeft === left) return + file.scrollLeft = left + }), + ) pruneView(path) } const setSelectedLines = (path: string, range: SelectedLineRange | null) => { const next = range ? normalizeSelectedLines(range) : null - setView("file", path, (current) => { - if (current?.selectedLines === next) return current - return { - ...(current ?? {}), - selectedLines: next, - } - }) + setView( + produce((draft) => { + const file = draft.file[path] ?? (draft.file[path] = {}) + if (equalSelectedLines(file.selectedLines, next)) return + file.selectedLines = next + }), + ) pruneView(path) } diff --git a/packages/app/src/context/file/watcher.test.ts b/packages/app/src/context/file/watcher.test.ts index 653e0aa75255..9536b52536b6 100644 --- a/packages/app/src/context/file/watcher.test.ts +++ b/packages/app/src/context/file/watcher.test.ts @@ -27,6 +27,37 @@ describe("file watcher invalidation", () => { expect(refresh).toEqual(["src"]) }) + test("reloads files that are open in tabs", () => { + const loads: string[] = [] + + invalidateFromWatcher( + { + type: "file.watcher.updated", + properties: { + file: "src/open.ts", + event: "change", + }, + }, + { + normalize: (input) => input, + hasFile: () => false, + isOpen: (path) => path === "src/open.ts", + loadFile: (path) => loads.push(path), + node: () => ({ + path: "src/open.ts", + type: "file", + name: "open.ts", + absolute: "/repo/src/open.ts", + ignored: false, + }), + isDirLoaded: () => false, + refreshDir: () => {}, + }, + ) + + expect(loads).toEqual(["src/open.ts"]) + }) + test("refreshes only changed loaded directory nodes", () => { const refresh: string[] = [] diff --git a/packages/app/src/context/file/watcher.ts b/packages/app/src/context/file/watcher.ts index a3a98eae4b1b..fbf71992791a 100644 --- a/packages/app/src/context/file/watcher.ts +++ b/packages/app/src/context/file/watcher.ts @@ -8,6 +8,7 @@ type WatcherEvent = { type WatcherOps = { normalize: (input: string) => string hasFile: (path: string) => boolean + isOpen?: (path: string) => boolean loadFile: (path: string) => void node: (path: string) => FileNode | undefined isDirLoaded: (path: string) => boolean @@ -27,7 +28,7 @@ export function invalidateFromWatcher(event: WatcherEvent, ops: WatcherOps) { if (!path) return if (path.startsWith(".git/")) return - if (ops.hasFile(path)) { + if (ops.hasFile(path) || ops.isOpen?.(path)) { ops.loadFile(path) } diff --git a/packages/app/src/context/global-sdk.tsx b/packages/app/src/context/global-sdk.tsx index 0cd4f6c997ea..3f93b76a723c 100644 --- a/packages/app/src/context/global-sdk.tsx +++ b/packages/app/src/context/global-sdk.tsx @@ -12,19 +12,44 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo const platform = usePlatform() const abort = new AbortController() + const password = typeof window === "undefined" ? undefined : window.__OPENCODE__?.serverPassword + + const auth = (() => { + if (!password) return + if (!server.isLocal()) return + return { + Authorization: `Basic ${btoa(`opencode:${password}`)}`, + } + })() + + const eventFetch = (() => { + if (!platform.fetch) return + try { + const url = new URL(server.url) + const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" + if (url.protocol === "http:" && !loopback) return platform.fetch + } catch { + return + } + })() + const eventSdk = createOpencodeClient({ baseUrl: server.url, signal: abort.signal, - fetch: platform.fetch, + fetch: eventFetch, + headers: eventFetch ? undefined : auth, }) const emitter = createGlobalEmitter<{ [key: string]: Event }>() type Queued = { directory: string; payload: Event } + const FLUSH_FRAME_MS = 16 + const STREAM_YIELD_MS = 8 + const RECONNECT_DELAY_MS = 250 - let queue: Array = [] - let buffer: Array = [] + let queue: Queued[] = [] + let buffer: Queued[] = [] const coalesced = new Map() let timer: ReturnType | undefined let last = 0 @@ -53,7 +78,6 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo last = Date.now() batch(() => { for (const event of events) { - if (!event) continue emitter.emit(event.directory, event.payload) } }) @@ -64,33 +88,62 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo const schedule = () => { if (timer) return const elapsed = Date.now() - last - timer = setTimeout(flush, Math.max(0, 16 - elapsed)) + timer = setTimeout(flush, Math.max(0, FLUSH_FRAME_MS - elapsed)) } + let streamErrorLogged = false + const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + void (async () => { - const events = await eventSdk.global.event() - let yielded = Date.now() - for await (const event of events.stream) { - const directory = event.directory ?? "global" - const payload = event.payload - const k = key(directory, payload) - if (k) { - const i = coalesced.get(k) - if (i !== undefined) { - queue[i] = undefined + while (!abort.signal.aborted) { + try { + const events = await eventSdk.global.event({ + onSseError: (error) => { + if (streamErrorLogged) return + streamErrorLogged = true + console.error("[global-sdk] event stream error", { + url: server.url, + fetch: eventFetch ? "platform" : "webview", + error, + }) + }, + }) + let yielded = Date.now() + for await (const event of events.stream) { + streamErrorLogged = false + const directory = event.directory ?? "global" + const payload = event.payload + const k = key(directory, payload) + if (k) { + const i = coalesced.get(k) + if (i !== undefined) { + queue[i] = { directory, payload } + continue + } + coalesced.set(k, queue.length) + } + queue.push({ directory, payload }) + schedule() + + if (Date.now() - yielded < STREAM_YIELD_MS) continue + yielded = Date.now() + await wait(0) + } + } catch (error) { + if (!streamErrorLogged) { + streamErrorLogged = true + console.error("[global-sdk] event stream failed", { + url: server.url, + fetch: eventFetch ? "platform" : "webview", + error, + }) } - coalesced.set(k, queue.length) } - queue.push({ directory, payload }) - schedule() - if (Date.now() - yielded < 8) continue - yielded = Date.now() - await new Promise((resolve) => setTimeout(resolve, 0)) + if (abort.signal.aborted) return + await wait(RECONNECT_DELAY_MS) } - })() - .finally(flush) - .catch(() => undefined) + })().finally(flush) onCleanup(() => { abort.abort() diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index e2bf44980743..62c7eb66ec9c 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -47,6 +47,20 @@ type GlobalStore = { reload: undefined | "pending" | "complete" } +function errorMessage(error: unknown) { + if (error instanceof Error && error.message) return error.message + if (typeof error === "string" && error) return error + return "Unknown error" +} + +function setDevStats(value: { + activeDirectoryStores: number + evictions: number + loadSessionsFullFetchFallback: number +}) { + ;(globalThis as { __OPENCODE_GLOBAL_SYNC_STATS?: typeof value }).__OPENCODE_GLOBAL_SYNC_STATS = value +} + function createGlobalSync() { const globalSDK = useGlobalSDK() const platform = usePlatform() @@ -81,19 +95,11 @@ function createGlobalSync() { const updateStats = (activeDirectoryStores: number) => { if (!import.meta.env.DEV) return - ;( - globalThis as { - __OPENCODE_GLOBAL_SYNC_STATS?: { - activeDirectoryStores: number - evictions: number - loadSessionsFullFetchFallback: number - } - } - ).__OPENCODE_GLOBAL_SYNC_STATS = { + setDevStats({ activeDirectoryStores, evictions: stats.evictions, loadSessionsFullFetchFallback: stats.loadSessionsFallback, - } + }) } const paused = () => untrack(() => globalStore.reload) !== undefined @@ -204,7 +210,10 @@ function createGlobalSync() { .catch((err) => { console.error("Failed to load sessions", err) const project = getFilename(directory) - showToast({ title: language.t("toast.session.listFailed.title", { project }), description: err.message }) + showToast({ + title: language.t("toast.session.listFailed.title", { project }), + description: errorMessage(err), + }) }) sessionLoads.set(directory, promise) @@ -307,12 +316,28 @@ function createGlobalSync() { void bootstrap() }) - function projectMeta(directory: string, patch: ProjectMeta) { - children.projectMeta(directory, patch) + const projectApi = { + loadSessions, + meta(directory: string, patch: ProjectMeta) { + children.projectMeta(directory, patch) + }, + icon(directory: string, value: string | undefined) { + children.projectIcon(directory, value) + }, } - function projectIcon(directory: string, value: string | undefined) { - children.projectIcon(directory, value) + const updateConfig = async (config: Config) => { + setGlobalStore("reload", "pending") + return globalSDK.client.global.config + .update({ config }) + .then(bootstrap) + .then(() => { + setGlobalStore("reload", "complete") + }) + .catch((error) => { + setGlobalStore("reload", undefined) + throw error + }) } return { @@ -326,19 +351,8 @@ function createGlobalSync() { }, child: children.child, bootstrap, - updateConfig: (config: Config) => { - setGlobalStore("reload", "pending") - return globalSDK.client.global.config.update({ config }).finally(() => { - setTimeout(() => { - setGlobalStore("reload", "complete") - }, 1000) - }) - }, - project: { - loadSessions, - meta: projectMeta, - icon: projectIcon, - }, + updateConfig, + project: projectApi, } } diff --git a/packages/app/src/context/global-sync/child-store.test.ts b/packages/app/src/context/global-sync/child-store.test.ts new file mode 100644 index 000000000000..500f0fc70ae1 --- /dev/null +++ b/packages/app/src/context/global-sync/child-store.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, test } from "bun:test" +import { createRoot, getOwner } from "solid-js" +import { createStore } from "solid-js/store" +import type { State } from "./types" +import { createChildStoreManager } from "./child-store" + +const child = () => createStore({} as State) + +describe("createChildStoreManager", () => { + test("does not evict the active directory during mark", () => { + const owner = createRoot((dispose) => { + const current = getOwner() + dispose() + return current + }) + if (!owner) throw new Error("owner required") + + const manager = createChildStoreManager({ + owner, + markStats() {}, + incrementEvictions() {}, + isBooting: () => false, + isLoadingSessions: () => false, + onBootstrap() {}, + onDispose() {}, + }) + + Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => { + manager.children[directory] = child() + manager.pin(directory) + }) + + const directory = "/active" + manager.children[directory] = child() + manager.mark(directory) + + expect(manager.children[directory]).toBeDefined() + }) +}) diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts index 2feb7fe0884f..af08c3bd431b 100644 --- a/packages/app/src/context/global-sync/child-store.ts +++ b/packages/app/src/context/global-sync/child-store.ts @@ -36,7 +36,7 @@ export function createChildStoreManager(input: { const mark = (directory: string) => { if (!directory) return lifecycle.set(directory, { lastAccessAt: Date.now() }) - runEviction() + runEviction(directory) } const pin = (directory: string) => { @@ -106,7 +106,7 @@ export function createChildStoreManager(input: { return true } - function runEviction() { + function runEviction(skip?: string) { const stores = Object.keys(children) if (stores.length === 0) return const list = pickDirectoriesToEvict({ @@ -116,7 +116,7 @@ export function createChildStoreManager(input: { max: MAX_DIR_STORES, ttl: DIR_IDLE_TTL_MS, now: Date.now(), - }) + }).filter((directory) => directory !== skip) if (list.length === 0) return for (const directory of list) { if (!disposeDirectory(directory)) continue diff --git a/packages/app/src/context/global-sync/event-reducer.test.ts b/packages/app/src/context/global-sync/event-reducer.test.ts index f79b9fc958f5..ad63f3c202eb 100644 --- a/packages/app/src/context/global-sync/event-reducer.test.ts +++ b/packages/app/src/context/global-sync/event-reducer.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import type { Message, Part, Project, Session } from "@opencode-ai/sdk/v2/client" +import type { Message, Part, PermissionRequest, Project, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client" import { createStore } from "solid-js/store" import type { State } from "./types" import { applyDirectoryEvent, applyGlobalEvent } from "./event-reducer" @@ -34,6 +34,29 @@ const textPart = (id: string, sessionID: string, messageID: string) => text: id, }) as Part +const permissionRequest = (id: string, sessionID: string, title = id) => + ({ + id, + sessionID, + permission: title, + patterns: ["*"], + metadata: {}, + always: [], + }) as PermissionRequest + +const questionRequest = (id: string, sessionID: string, title = id) => + ({ + id, + sessionID, + questions: [ + { + question: title, + header: title, + options: [{ label: title, description: title }], + }, + ], + }) as QuestionRequest + const baseState = (input: Partial = {}) => ({ status: "complete", @@ -164,6 +187,264 @@ describe("applyDirectoryEvent", () => { expect(store.session_status.ses_1).toBeUndefined() }) + test("cleans session caches when deleted and decrements only root totals", () => { + const cases = [ + { info: rootSession({ id: "ses_1" }), expectedTotal: 1 }, + { info: rootSession({ id: "ses_2", parentID: "ses_1" }), expectedTotal: 2 }, + ] + + for (const item of cases) { + const message = userMessage("msg_1", item.info.id) + const [store, setStore] = createStore( + baseState({ + session: [ + rootSession({ id: "ses_1" }), + rootSession({ id: "ses_2", parentID: "ses_1" }), + rootSession({ id: "ses_3" }), + ], + sessionTotal: 2, + message: { [item.info.id]: [message] }, + part: { [message.id]: [textPart("prt_1", item.info.id, message.id)] }, + session_diff: { [item.info.id]: [] }, + todo: { [item.info.id]: [] }, + permission: { [item.info.id]: [] }, + question: { [item.info.id]: [] }, + session_status: { [item.info.id]: { type: "busy" } }, + }), + ) + + applyDirectoryEvent({ + event: { type: "session.deleted", properties: { info: item.info } }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + }) + + expect(store.session.find((x) => x.id === item.info.id)).toBeUndefined() + expect(store.sessionTotal).toBe(item.expectedTotal) + expect(store.message[item.info.id]).toBeUndefined() + expect(store.part[message.id]).toBeUndefined() + expect(store.session_diff[item.info.id]).toBeUndefined() + expect(store.todo[item.info.id]).toBeUndefined() + expect(store.permission[item.info.id]).toBeUndefined() + expect(store.question[item.info.id]).toBeUndefined() + expect(store.session_status[item.info.id]).toBeUndefined() + } + }) + + test("upserts and removes messages while clearing orphaned parts", () => { + const sessionID = "ses_1" + const [store, setStore] = createStore( + baseState({ + message: { [sessionID]: [userMessage("msg_1", sessionID), userMessage("msg_3", sessionID)] }, + part: { msg_2: [textPart("prt_1", sessionID, "msg_2")] }, + }), + ) + + applyDirectoryEvent({ + event: { type: "message.updated", properties: { info: userMessage("msg_2", sessionID) } }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + }) + + expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_2", "msg_3"]) + + applyDirectoryEvent({ + event: { + type: "message.updated", + properties: { + info: { + ...userMessage("msg_2", sessionID), + role: "assistant", + } as Message, + }, + }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + }) + + expect(store.message[sessionID]?.find((x) => x.id === "msg_2")?.role).toBe("assistant") + + applyDirectoryEvent({ + event: { type: "message.removed", properties: { sessionID, messageID: "msg_2" } }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + }) + + expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_3"]) + expect(store.part.msg_2).toBeUndefined() + }) + + test("upserts and prunes message parts", () => { + const sessionID = "ses_1" + const messageID = "msg_1" + const [store, setStore] = createStore( + baseState({ + part: { [messageID]: [textPart("prt_1", sessionID, messageID), textPart("prt_3", sessionID, messageID)] }, + }), + ) + + applyDirectoryEvent({ + event: { type: "message.part.updated", properties: { part: textPart("prt_2", sessionID, messageID) } }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + }) + expect(store.part[messageID]?.map((x) => x.id)).toEqual(["prt_1", "prt_2", "prt_3"]) + + applyDirectoryEvent({ + event: { + type: "message.part.updated", + properties: { + part: { + ...textPart("prt_2", sessionID, messageID), + text: "changed", + } as Part, + }, + }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + }) + const updated = store.part[messageID]?.find((x) => x.id === "prt_2") + expect(updated?.type).toBe("text") + if (updated?.type === "text") expect(updated.text).toBe("changed") + + applyDirectoryEvent({ + event: { type: "message.part.removed", properties: { messageID, partID: "prt_1" } }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + }) + applyDirectoryEvent({ + event: { type: "message.part.removed", properties: { messageID, partID: "prt_2" } }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + }) + applyDirectoryEvent({ + event: { type: "message.part.removed", properties: { messageID, partID: "prt_3" } }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + }) + + expect(store.part[messageID]).toBeUndefined() + }) + + test("tracks permission and question request lifecycles", () => { + const sessionID = "ses_1" + const [store, setStore] = createStore( + baseState({ + permission: { [sessionID]: [permissionRequest("perm_1", sessionID), permissionRequest("perm_3", sessionID)] }, + question: { [sessionID]: [questionRequest("q_1", sessionID), questionRequest("q_3", sessionID)] }, + }), + ) + + applyDirectoryEvent({ + event: { type: "permission.asked", properties: permissionRequest("perm_2", sessionID) }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + }) + expect(store.permission[sessionID]?.map((x) => x.id)).toEqual(["perm_1", "perm_2", "perm_3"]) + + applyDirectoryEvent({ + event: { type: "permission.asked", properties: permissionRequest("perm_2", sessionID, "updated") }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + }) + expect(store.permission[sessionID]?.find((x) => x.id === "perm_2")?.permission).toBe("updated") + + applyDirectoryEvent({ + event: { type: "permission.replied", properties: { sessionID, requestID: "perm_2" } }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + }) + expect(store.permission[sessionID]?.map((x) => x.id)).toEqual(["perm_1", "perm_3"]) + + applyDirectoryEvent({ + event: { type: "question.asked", properties: questionRequest("q_2", sessionID) }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + }) + expect(store.question[sessionID]?.map((x) => x.id)).toEqual(["q_1", "q_2", "q_3"]) + + applyDirectoryEvent({ + event: { type: "question.asked", properties: questionRequest("q_2", sessionID, "updated") }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + }) + expect(store.question[sessionID]?.find((x) => x.id === "q_2")?.questions[0]?.header).toBe("updated") + + applyDirectoryEvent({ + event: { type: "question.rejected", properties: { sessionID, requestID: "q_2" } }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + }) + expect(store.question[sessionID]?.map((x) => x.id)).toEqual(["q_1", "q_3"]) + }) + + test("updates vcs branch in store and cache", () => { + const [store, setStore] = createStore(baseState()) + const [cacheStore, setCacheStore] = createStore({ value: undefined as State["vcs"] }) + + applyDirectoryEvent({ + event: { type: "vcs.branch.updated", properties: { branch: "feature/test" } }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + vcsCache: { + store: cacheStore, + setStore: setCacheStore, + ready: () => true, + }, + }) + + expect(store.vcs).toEqual({ branch: "feature/test" }) + expect(cacheStore.value).toEqual({ branch: "feature/test" }) + }) + test("routes disposal and lsp events to side-effect handlers", () => { const [store, setStore] = createStore(baseState()) const pushes: string[] = [] diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts index c658d82c8b76..66fcac66d560 100644 --- a/packages/app/src/context/global-sync/event-reducer.ts +++ b/packages/app/src/context/global-sync/event-reducer.ts @@ -231,8 +231,27 @@ export function applyDirectoryEvent(input: { } break } + case "message.part.delta": { + const props = event.properties as { messageID: string; partID: string; field: string; delta: string } + const parts = input.store.part[props.messageID] + if (!parts) break + const result = Binary.search(parts, props.partID, (p) => p.id) + if (!result.found) break + input.setStore( + "part", + props.messageID, + produce((draft) => { + const part = draft[result.index] + const field = props.field as keyof typeof part + const existing = part[field] as string | undefined + ;(part[field] as string) = (existing ?? "") + props.delta + }), + ) + break + } case "vcs.branch.updated": { const props = event.properties as { branch: string } + if (input.store.vcs?.branch === props.branch) break const next = { branch: props.branch } input.setStore("vcs", next) if (input.vcsCache) input.vcsCache.setStore("value", next) diff --git a/packages/app/src/context/highlights.tsx b/packages/app/src/context/highlights.tsx index cc4c021beb08..476209e41732 100644 --- a/packages/app/src/context/highlights.tsx +++ b/packages/app/src/context/highlights.tsx @@ -119,9 +119,7 @@ function sliceHighlights(input: { releases: ParsedRelease[]; current?: string; p const highlights = releases.slice(start, end).flatMap((release) => release.highlights) const seen = new Set() const unique = highlights.filter((highlight) => { - const key = [highlight.title, highlight.description, highlight.media?.type ?? "", highlight.media?.src ?? ""].join( - "\n", - ) + const key = dedupeKey(highlight) if (seen.has(key)) return false seen.add(key) return true @@ -129,6 +127,16 @@ function sliceHighlights(input: { releases: ParsedRelease[]; current?: string; p return unique.slice(0, 5) } +function dedupeKey(highlight: Highlight) { + return [highlight.title, highlight.description, highlight.media?.type ?? "", highlight.media?.src ?? ""].join("\n") +} + +function loadReleaseHighlights(value: unknown, current?: string, previous?: string) { + const releases = parseChangelog(value) + if (!releases?.length) return [] + return sliceHighlights({ releases, current, previous }) +} + export const { use: useHighlights, provider: HighlightsProvider } = createSimpleContext({ name: "Highlights", gate: false, @@ -140,32 +148,21 @@ export const { use: useHighlights, provider: HighlightsProvider } = createSimple const [from, setFrom] = createSignal(undefined) const [to, setTo] = createSignal(undefined) - const [timer, setTimer] = createSignal | undefined>(undefined) const state = { started: false } + let timer: ReturnType | undefined + + const clearTimer = () => { + if (timer === undefined) return + clearTimeout(timer) + timer = undefined + } const markSeen = () => { if (!platform.version) return setStore("version", platform.version) } - createEffect(() => { - if (state.started) return - if (!ready()) return - if (!settings.ready()) return - if (!platform.version) return - state.started = true - - const previous = store.version - if (!previous) { - setStore("version", platform.version) - return - } - - if (previous === platform.version) return - - setFrom(previous) - setTo(platform.version) - + const start = (previous: string) => { if (!settings.general.releaseNotes()) { markSeen() return @@ -175,9 +172,7 @@ export const { use: useHighlights, provider: HighlightsProvider } = createSimple const controller = new AbortController() onCleanup(() => { controller.abort() - const id = timer() - if (id === undefined) return - clearTimeout(id) + clearTimer() }) fetcher(CHANGELOG_URL, { @@ -187,15 +182,7 @@ export const { use: useHighlights, provider: HighlightsProvider } = createSimple .then((response) => (response.ok ? (response.json() as Promise) : undefined)) .then((json) => { if (!json) return - const releases = parseChangelog(json) - if (!releases) return - if (releases.length === 0) return - const highlights = sliceHighlights({ - releases, - current: platform.version, - previous, - }) - + const highlights = loadReleaseHighlights(json, platform.version, previous) if (controller.signal.aborted) return if (highlights.length === 0) { @@ -203,13 +190,33 @@ export const { use: useHighlights, provider: HighlightsProvider } = createSimple return } - const timer = setTimeout(() => { + timer = setTimeout(() => { + timer = undefined markSeen() dialog.show(() => ) }, 500) - setTimer(timer) }) .catch(() => undefined) + } + + createEffect(() => { + if (state.started) return + if (!ready()) return + if (!settings.ready()) return + if (!platform.version) return + state.started = true + + const previous = store.version + if (!previous) { + setStore("version", platform.version) + return + } + + if (previous === platform.version) return + + setFrom(previous) + setTo(platform.version) + start(previous) }) return { diff --git a/packages/app/src/context/language.tsx b/packages/app/src/context/language.tsx index 22f7bcca1e40..b21ec6d3cc84 100644 --- a/packages/app/src/context/language.tsx +++ b/packages/app/src/context/language.tsx @@ -57,6 +57,10 @@ export type Locale = type RawDictionary = typeof en & typeof uiEn type Dictionary = i18n.Flatten +function cookie(locale: Locale) { + return `oc_locale=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax` +} + const LOCALES: readonly Locale[] = [ "en", "zh", @@ -76,6 +80,66 @@ const LOCALES: readonly Locale[] = [ "th", ] +const LABEL_KEY: Record = { + en: "language.en", + zh: "language.zh", + zht: "language.zht", + ko: "language.ko", + de: "language.de", + es: "language.es", + fr: "language.fr", + da: "language.da", + ja: "language.ja", + pl: "language.pl", + ru: "language.ru", + ar: "language.ar", + no: "language.no", + br: "language.br", + th: "language.th", + bs: "language.bs", +} + +const base = i18n.flatten({ ...en, ...uiEn }) +const DICT: Record = { + en: base, + zh: { ...base, ...i18n.flatten({ ...zh, ...uiZh }) }, + zht: { ...base, ...i18n.flatten({ ...zht, ...uiZht }) }, + ko: { ...base, ...i18n.flatten({ ...ko, ...uiKo }) }, + de: { ...base, ...i18n.flatten({ ...de, ...uiDe }) }, + es: { ...base, ...i18n.flatten({ ...es, ...uiEs }) }, + fr: { ...base, ...i18n.flatten({ ...fr, ...uiFr }) }, + da: { ...base, ...i18n.flatten({ ...da, ...uiDa }) }, + ja: { ...base, ...i18n.flatten({ ...ja, ...uiJa }) }, + pl: { ...base, ...i18n.flatten({ ...pl, ...uiPl }) }, + ru: { ...base, ...i18n.flatten({ ...ru, ...uiRu }) }, + ar: { ...base, ...i18n.flatten({ ...ar, ...uiAr }) }, + no: { ...base, ...i18n.flatten({ ...no, ...uiNo }) }, + br: { ...base, ...i18n.flatten({ ...br, ...uiBr }) }, + th: { ...base, ...i18n.flatten({ ...th, ...uiTh }) }, + bs: { ...base, ...i18n.flatten({ ...bs, ...uiBs }) }, +} + +const localeMatchers: Array<{ locale: Locale; match: (language: string) => boolean }> = [ + { locale: "zht", match: (language) => language.startsWith("zh") && language.includes("hant") }, + { locale: "zh", match: (language) => language.startsWith("zh") }, + { locale: "ko", match: (language) => language.startsWith("ko") }, + { locale: "de", match: (language) => language.startsWith("de") }, + { locale: "es", match: (language) => language.startsWith("es") }, + { locale: "fr", match: (language) => language.startsWith("fr") }, + { locale: "da", match: (language) => language.startsWith("da") }, + { locale: "ja", match: (language) => language.startsWith("ja") }, + { locale: "pl", match: (language) => language.startsWith("pl") }, + { locale: "ru", match: (language) => language.startsWith("ru") }, + { locale: "ar", match: (language) => language.startsWith("ar") }, + { + locale: "no", + match: (language) => language.startsWith("no") || language.startsWith("nb") || language.startsWith("nn"), + }, + { locale: "br", match: (language) => language.startsWith("pt") }, + { locale: "th", match: (language) => language.startsWith("th") }, + { locale: "bs", match: (language) => language.startsWith("bs") }, +] + type ParityKey = "command.session.previous.unseen" | "command.session.next.unseen" const PARITY_CHECK: Record, Record> = { zh, @@ -102,28 +166,9 @@ function detectLocale(): Locale { const languages = navigator.languages?.length ? navigator.languages : [navigator.language] for (const language of languages) { if (!language) continue - if (language.toLowerCase().startsWith("zh")) { - if (language.toLowerCase().includes("hant")) return "zht" - return "zh" - } - if (language.toLowerCase().startsWith("ko")) return "ko" - if (language.toLowerCase().startsWith("de")) return "de" - if (language.toLowerCase().startsWith("es")) return "es" - if (language.toLowerCase().startsWith("fr")) return "fr" - if (language.toLowerCase().startsWith("da")) return "da" - if (language.toLowerCase().startsWith("ja")) return "ja" - if (language.toLowerCase().startsWith("pl")) return "pl" - if (language.toLowerCase().startsWith("ru")) return "ru" - if (language.toLowerCase().startsWith("ar")) return "ar" - if ( - language.toLowerCase().startsWith("no") || - language.toLowerCase().startsWith("nb") || - language.toLowerCase().startsWith("nn") - ) - return "no" - if (language.toLowerCase().startsWith("pt")) return "br" - if (language.toLowerCase().startsWith("th")) return "th" - if (language.toLowerCase().startsWith("bs")) return "bs" + const normalized = language.toLowerCase() + const match = localeMatchers.find((entry) => entry.match(normalized)) + if (match) return match.locale } return "en" @@ -139,24 +184,9 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont }), ) - const locale = createMemo(() => { - if (store.locale === "zh") return "zh" - if (store.locale === "zht") return "zht" - if (store.locale === "ko") return "ko" - if (store.locale === "de") return "de" - if (store.locale === "es") return "es" - if (store.locale === "fr") return "fr" - if (store.locale === "da") return "da" - if (store.locale === "ja") return "ja" - if (store.locale === "pl") return "pl" - if (store.locale === "ru") return "ru" - if (store.locale === "ar") return "ar" - if (store.locale === "no") return "no" - if (store.locale === "br") return "br" - if (store.locale === "th") return "th" - if (store.locale === "bs") return "bs" - return "en" - }) + const locale = createMemo(() => + LOCALES.includes(store.locale as Locale) ? (store.locale as Locale) : "en", + ) createEffect(() => { const current = locale() @@ -164,52 +194,16 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont setStore("locale", current) }) - const base = i18n.flatten({ ...en, ...uiEn }) - const dict = createMemo(() => { - if (locale() === "en") return base - if (locale() === "zh") return { ...base, ...i18n.flatten({ ...zh, ...uiZh }) } - if (locale() === "zht") return { ...base, ...i18n.flatten({ ...zht, ...uiZht }) } - if (locale() === "de") return { ...base, ...i18n.flatten({ ...de, ...uiDe }) } - if (locale() === "es") return { ...base, ...i18n.flatten({ ...es, ...uiEs }) } - if (locale() === "fr") return { ...base, ...i18n.flatten({ ...fr, ...uiFr }) } - if (locale() === "da") return { ...base, ...i18n.flatten({ ...da, ...uiDa }) } - if (locale() === "ja") return { ...base, ...i18n.flatten({ ...ja, ...uiJa }) } - if (locale() === "pl") return { ...base, ...i18n.flatten({ ...pl, ...uiPl }) } - if (locale() === "ru") return { ...base, ...i18n.flatten({ ...ru, ...uiRu }) } - if (locale() === "ar") return { ...base, ...i18n.flatten({ ...ar, ...uiAr }) } - if (locale() === "no") return { ...base, ...i18n.flatten({ ...no, ...uiNo }) } - if (locale() === "br") return { ...base, ...i18n.flatten({ ...br, ...uiBr }) } - if (locale() === "th") return { ...base, ...i18n.flatten({ ...th, ...uiTh }) } - if (locale() === "bs") return { ...base, ...i18n.flatten({ ...bs, ...uiBs }) } - return { ...base, ...i18n.flatten({ ...ko, ...uiKo }) } - }) + const dict = createMemo(() => DICT[locale()]) const t = i18n.translator(dict, i18n.resolveTemplate) - const labelKey: Record = { - en: "language.en", - zh: "language.zh", - zht: "language.zht", - ko: "language.ko", - de: "language.de", - es: "language.es", - fr: "language.fr", - da: "language.da", - ja: "language.ja", - pl: "language.pl", - ru: "language.ru", - ar: "language.ar", - no: "language.no", - br: "language.br", - th: "language.th", - bs: "language.bs", - } - - const label = (value: Locale) => t(labelKey[value]) + const label = (value: Locale) => t(LABEL_KEY[value]) createEffect(() => { if (typeof document !== "object") return document.documentElement.lang = locale() + document.cookie = cookie(locale()) }) return { diff --git a/packages/app/src/context/layout-scroll.test.ts b/packages/app/src/context/layout-scroll.test.ts index c421a58b67e6..2a13e40204f3 100644 --- a/packages/app/src/context/layout-scroll.test.ts +++ b/packages/app/src/context/layout-scroll.test.ts @@ -1,36 +1,44 @@ -import { describe, expect, test } from "bun:test" +import { describe, expect, test, vi } from "bun:test" import { createScrollPersistence } from "./layout-scroll" describe("createScrollPersistence", () => { - test("debounces persisted scroll writes", async () => { - const snapshot = { - session: { - review: { x: 0, y: 0 }, - }, - } as Record> - const writes: Array> = [] - const scroll = createScrollPersistence({ - debounceMs: 10, - getSnapshot: (sessionKey) => snapshot[sessionKey], - onFlush: (sessionKey, next) => { - snapshot[sessionKey] = next - writes.push(next) - }, - }) + test("debounces persisted scroll writes", () => { + vi.useFakeTimers() + try { + const snapshot = { + session: { + review: { x: 0, y: 0 }, + }, + } as Record> + const writes: Array> = [] + const scroll = createScrollPersistence({ + debounceMs: 10, + getSnapshot: (sessionKey) => snapshot[sessionKey], + onFlush: (sessionKey, next) => { + snapshot[sessionKey] = next + writes.push(next) + }, + }) - for (const i of Array.from({ length: 30 }, (_, n) => n + 1)) { - scroll.setScroll("session", "review", { x: 0, y: i }) - } + for (const i of Array.from({ length: 30 }, (_, n) => n + 1)) { + scroll.setScroll("session", "review", { x: 0, y: i }) + } + + vi.advanceTimersByTime(9) + expect(writes).toHaveLength(0) - await new Promise((resolve) => setTimeout(resolve, 40)) + vi.advanceTimersByTime(1) - expect(writes).toHaveLength(1) - expect(writes[0]?.review).toEqual({ x: 0, y: 30 }) + expect(writes).toHaveLength(1) + expect(writes[0]?.review).toEqual({ x: 0, y: 30 }) - scroll.setScroll("session", "review", { x: 0, y: 30 }) - await new Promise((resolve) => setTimeout(resolve, 20)) + scroll.setScroll("session", "review", { x: 0, y: 30 }) + vi.advanceTimersByTime(20) - expect(writes).toHaveLength(1) - scroll.dispose() + expect(writes).toHaveLength(1) + scroll.dispose() + } finally { + vi.useRealTimers() + } }) }) diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 8d9c865f8495..71f0294e7e6c 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -4,12 +4,16 @@ import { createSimpleContext } from "@opencode-ai/ui/context" import { useGlobalSync } from "./global-sync" import { useGlobalSDK } from "./global-sdk" import { useServer } from "./server" +import { usePlatform } from "./platform" import { Project } from "@opencode-ai/sdk/v2" import { Persist, persisted, removePersisted } from "@/utils/persist" import { same } from "@/utils/same" import { createScrollPersistence, type SessionScroll } from "./layout-scroll" const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const +const DEFAULT_PANEL_WIDTH = 344 +const DEFAULT_SESSION_WIDTH = 600 +const DEFAULT_TERMINAL_HEIGHT = 280 export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number] export function getAvatarColors(key?: string) { @@ -84,12 +88,21 @@ export function pruneSessionKeys(input: { .slice(input.max) } +function nextSessionTabsForOpen(current: SessionTabs | undefined, tab: string): SessionTabs { + const all = current?.all ?? [] + if (tab === "review") return { all: all.filter((x) => x !== "review"), active: tab } + if (tab === "context") return { all: [tab, ...all.filter((x) => x !== tab)], active: tab } + if (!all.includes(tab)) return { all: [...all, tab], active: tab } + return { all, active: tab } +} + export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({ name: "Layout", init: () => { const globalSdk = useGlobalSDK() const globalSync = useGlobalSync() const server = useServer() + const platform = usePlatform() const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value) @@ -114,11 +127,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( if (!isRecord(fileTree)) return fileTree if (fileTree.tab === "changes" || fileTree.tab === "all") return fileTree - const width = typeof fileTree.width === "number" ? fileTree.width : 344 + const width = typeof fileTree.width === "number" ? fileTree.width : DEFAULT_PANEL_WIDTH return { ...fileTree, opened: true, - width: width === 260 ? 344 : width, + width: width === 260 ? DEFAULT_PANEL_WIDTH : width, tab: "changes", } })() @@ -149,12 +162,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( createStore({ sidebar: { opened: false, - width: 344, + width: DEFAULT_PANEL_WIDTH, workspaces: {} as Record, workspacesDefault: false, }, terminal: { - height: 280, + height: DEFAULT_TERMINAL_HEIGHT, opened: false, }, review: { @@ -163,11 +176,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, fileTree: { opened: true, - width: 344, + width: DEFAULT_PANEL_WIDTH, tab: "changes" as "changes" | "all", }, session: { - width: 600, + width: DEFAULT_SESSION_WIDTH, }, mobileSidebar: { opened: false, @@ -182,8 +195,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const MAX_SESSION_KEYS = 50 const PENDING_MESSAGE_TTL_MS = 2 * 60 * 1000 - const meta = { active: undefined as string | undefined, pruned: false } - const used = new Map() + const usage = { + active: undefined as string | undefined, + pruned: false, + used: new Map(), + } const SESSION_STATE_KEYS = [ { key: "prompt", legacy: "prompt", version: "v2" }, @@ -200,10 +216,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( for (const entry of SESSION_STATE_KEYS) { const target = session ? Persist.session(dir, session, entry.key) : Persist.workspace(dir, entry.key) - void removePersisted(target) + void removePersisted(target, platform) const legacyKey = `${dir}/${entry.legacy}${session ? "/" + session : ""}.${entry.version}` - void removePersisted({ key: legacyKey }) + void removePersisted({ key: legacyKey }, platform) } } } @@ -212,7 +228,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const drop = pruneSessionKeys({ keep, max: MAX_SESSION_KEYS, - used, + used: usage.used, view: Object.keys(store.sessionView), tabs: Object.keys(store.sessionTabs), }) @@ -231,18 +247,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( dropSessionState(drop) for (const key of drop) { - used.delete(key) + usage.used.delete(key) } } function touch(sessionKey: string) { - meta.active = sessionKey - used.set(sessionKey, Date.now()) + usage.active = sessionKey + usage.used.set(sessionKey, Date.now()) if (!ready()) return - if (meta.pruned) return + if (usage.pruned) return - meta.pruned = true + usage.pruned = true prune(sessionKey) } @@ -251,7 +267,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( getSnapshot: (sessionKey) => store.sessionView[sessionKey]?.scroll, onFlush: (sessionKey, next) => { const current = store.sessionView[sessionKey] - const keep = meta.active ?? sessionKey + const keep = usage.active ?? sessionKey if (!current) { setStore("sessionView", sessionKey, { scroll: next }) prune(keep) @@ -267,10 +283,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( createEffect(() => { if (!ready()) return - if (meta.pruned) return - const active = meta.active + if (usage.pruned) return + const active = usage.active if (!active) return - meta.pruned = true + usage.pruned = true prune(active) }) @@ -544,32 +560,32 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, fileTree: { opened: createMemo(() => store.fileTree?.opened ?? true), - width: createMemo(() => store.fileTree?.width ?? 344), + width: createMemo(() => store.fileTree?.width ?? DEFAULT_PANEL_WIDTH), tab: createMemo(() => store.fileTree?.tab ?? "changes"), setTab(tab: "changes" | "all") { if (!store.fileTree) { - setStore("fileTree", { opened: true, width: 344, tab }) + setStore("fileTree", { opened: true, width: DEFAULT_PANEL_WIDTH, tab }) return } setStore("fileTree", "tab", tab) }, open() { if (!store.fileTree) { - setStore("fileTree", { opened: true, width: 344, tab: "changes" }) + setStore("fileTree", { opened: true, width: DEFAULT_PANEL_WIDTH, tab: "changes" }) return } setStore("fileTree", "opened", true) }, close() { if (!store.fileTree) { - setStore("fileTree", { opened: false, width: 344, tab: "changes" }) + setStore("fileTree", { opened: false, width: DEFAULT_PANEL_WIDTH, tab: "changes" }) return } setStore("fileTree", "opened", false) }, toggle() { if (!store.fileTree) { - setStore("fileTree", { opened: true, width: 344, tab: "changes" }) + setStore("fileTree", { opened: true, width: DEFAULT_PANEL_WIDTH, tab: "changes" }) return } setStore("fileTree", "opened", (x) => !x) @@ -583,7 +599,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, }, session: { - width: createMemo(() => store.session?.width ?? 600), + width: createMemo(() => store.session?.width ?? DEFAULT_SESSION_WIDTH), resize(width: number) { if (!store.session) { setStore("session", { width }) @@ -615,7 +631,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( pendingMessage: messageID, pendingMessageAt: at, }) - prune(meta.active ?? sessionKey) + prune(usage.active ?? sessionKey) return } @@ -656,7 +672,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( function setTerminalOpened(next: boolean) { const current = store.terminal if (!current) { - setStore("terminal", { height: 280, opened: next }) + setStore("terminal", { height: DEFAULT_TERMINAL_HEIGHT, opened: next }) return } @@ -753,43 +769,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, async open(tab: string) { const session = key() - const current = store.sessionTabs[session] ?? { all: [] } - - if (tab === "review") { - if (!store.sessionTabs[session]) { - setStore("sessionTabs", session, { all: current.all.filter((x) => x !== "review"), active: tab }) - return - } - setStore("sessionTabs", session, "active", tab) - return - } - - if (tab === "context") { - const all = [tab, ...current.all.filter((x) => x !== tab)] - if (!store.sessionTabs[session]) { - setStore("sessionTabs", session, { all, active: tab }) - return - } - setStore("sessionTabs", session, "all", all) - setStore("sessionTabs", session, "active", tab) - return - } - - if (!current.all.includes(tab)) { - if (!store.sessionTabs[session]) { - setStore("sessionTabs", session, { all: [tab], active: tab }) - return - } - setStore("sessionTabs", session, "all", [...current.all, tab]) - setStore("sessionTabs", session, "active", tab) - return - } - - if (!store.sessionTabs[session]) { - setStore("sessionTabs", session, { all: current.all, active: tab }) - return - } - setStore("sessionTabs", session, "active", tab) + const next = nextSessionTabsForOpen(store.sessionTabs[session], tab) + setStore("sessionTabs", session, next) }, close(tab: string) { const session = key() diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index f51bb6930929..ac5da60e8629 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -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 } @@ -15,16 +16,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const sdk = useSDK() const sync = useSync() const providers = useProviders() + const connected = createMemo(() => new Set(providers.connected().map((provider) => provider.id))) function isModelValid(model: ModelKey) { const provider = providers.all().find((x) => x.id === model.providerID) - return ( - !!provider?.models[model.modelID] && - providers - .connected() - .map((p) => p.id) - .includes(model.providerID) - ) + return !!provider?.models[model.modelID] && connected().has(model.providerID) } function getFirstValidModel(...modelFns: (() => ModelKey | undefined)[]) { @@ -35,6 +31,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ } } + let setModel: (model: ModelKey | undefined, options?: { recent?: boolean }) => void = () => undefined + const agent = (() => { const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden)) const [store, setStore] = createStore<{ @@ -74,7 +72,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ if (!value) return setStore("current", value.name) if (value.model) - model.set({ + setModel({ providerID: value.model.providerID, modelID: value.model.modelID, }) @@ -91,38 +89,37 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ model: {}, }) - const fallbackModel = createMemo(() => { - if (sync.data.config.model) { - const [providerID, modelID] = sync.data.config.model.split("/") - if (isModelValid({ providerID, modelID })) { - return { - providerID, - modelID, - } - } - } + const resolveConfigured = () => { + if (!sync.data.config.model) return + const [providerID, modelID] = sync.data.config.model.split("/") + const key = { providerID, modelID } + if (isModelValid(key)) return key + } + const resolveRecent = () => { for (const item of models.recent.list()) { - if (isModelValid(item)) { - return item - } + if (isModelValid(item)) return item } + } + const resolveDefault = () => { const defaults = providers.default() - for (const p of providers.connected()) { - const configured = defaults[p.id] + for (const provider of providers.connected()) { + const configured = defaults[provider.id] if (configured) { - const key = { providerID: p.id, modelID: configured } + const key = { providerID: provider.id, modelID: configured } if (isModelValid(key)) return key } - const first = Object.values(p.models)[0] + const first = Object.values(provider.models)[0] if (!first) continue - const key = { providerID: p.id, modelID: first.id } + const key = { providerID: provider.id, modelID: first.id } if (isModelValid(key)) return key } + } - return undefined + const fallbackModel = createMemo(() => { + return resolveConfigured() ?? resolveRecent() ?? resolveDefault() }) const current = createMemo(() => { @@ -162,21 +159,25 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }) } + const set = (model: ModelKey | undefined, options?: { recent?: boolean }) => { + batch(() => { + const currentAgent = agent.current() + const next = model ?? fallbackModel() + if (currentAgent) setEphemeral("model", currentAgent.name, next) + if (model) models.setVisibility(model, true) + if (options?.recent && model) models.recent.push(model) + }) + } + + setModel = set + return { ready: models.ready, current, recent, list: models.list, cycle, - set(model: ModelKey | undefined, options?: { recent?: boolean }) { - batch(() => { - const currentAgent = agent.current() - const next = model ?? fallbackModel() - if (currentAgent) setEphemeral("model", currentAgent.name, next) - if (model) models.setVisibility(model, true) - if (options?.recent && model) models.recent.push(model) - }) - }, + set, visible(model: ModelKey) { return models.visible(model) }, @@ -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(), + }), + ) }, }, } diff --git a/packages/app/src/context/model-variant.test.ts b/packages/app/src/context/model-variant.test.ts new file mode 100644 index 000000000000..01b149fd2676 --- /dev/null +++ b/packages/app/src/context/model-variant.test.ts @@ -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") + }) +}) diff --git a/packages/app/src/context/model-variant.ts b/packages/app/src/context/model-variant.ts new file mode 100644 index 000000000000..6b7ae7256409 --- /dev/null +++ b/packages/app/src/context/model-variant.ts @@ -0,0 +1,50 @@ +type AgentModel = { + providerID: string + modelID: string +} + +type Agent = { + model?: AgentModel + variant?: string +} + +type Model = AgentModel & { + variants?: Record +} + +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] +} diff --git a/packages/app/src/context/models.tsx b/packages/app/src/context/models.tsx index fee3c10c6dc4..12ec8371add1 100644 --- a/packages/app/src/context/models.tsx +++ b/packages/app/src/context/models.tsx @@ -16,6 +16,12 @@ type Store = { variant?: Record } +const RECENT_LIMIT = 5 + +function modelKey(model: ModelKey) { + return `${model.providerID}:${model.modelID}` +} + export const { use: useModels, provider: ModelsProvider } = createSimpleContext({ name: "Models", init: () => { @@ -39,10 +45,27 @@ export const { use: useModels, provider: ModelsProvider } = createSimpleContext( ), ) + const release = createMemo( + () => + new Map( + available().map((model) => { + const parsed = DateTime.fromISO(model.release_date) + return [modelKey({ providerID: model.provider.id, modelID: model.id }), parsed] as const + }), + ), + ) + const latest = createMemo(() => pipe( available(), - filter((x) => Math.abs(DateTime.fromISO(x.release_date).diffNow().as("months")) < 6), + filter( + (x) => + Math.abs( + (release().get(modelKey({ providerID: x.provider.id, modelID: x.id })) ?? DateTime.invalid("invalid")) + .diffNow() + .as("months"), + ) < 6, + ), groupBy((x) => x.provider.id), mapValues((models) => pipe( @@ -61,7 +84,7 @@ export const { use: useModels, provider: ModelsProvider } = createSimpleContext( ), ) - const latestSet = createMemo(() => new Set(latest().map((x) => `${x.providerID}:${x.modelID}`))) + const latestSet = createMemo(() => new Set(latest().map((x) => modelKey(x)))) const visibility = createMemo(() => { const map = new Map() @@ -82,20 +105,20 @@ export const { use: useModels, provider: ModelsProvider } = createSimpleContext( function update(model: ModelKey, state: Visibility) { const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID) if (index >= 0) { - setStore("user", index, { visibility: state }) + setStore("user", index, (current) => ({ ...current, visibility: state })) return } setStore("user", store.user.length, { ...model, visibility: state }) } const visible = (model: ModelKey) => { - const key = `${model.providerID}:${model.modelID}` + const key = modelKey(model) const state = visibility().get(key) if (state === "hide") return false if (state === "show") return true if (latestSet().has(key)) return true - const m = find(model) - if (!m?.release_date || !DateTime.fromISO(m.release_date).isValid) return true + const date = release().get(key) + if (!date?.isValid) return true return false } @@ -104,8 +127,8 @@ export const { use: useModels, provider: ModelsProvider } = createSimpleContext( } const push = (model: ModelKey) => { - const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID) - if (uniq.length > 5) uniq.pop() + const uniq = uniqueBy([model, ...store.recent], (x) => `${x.providerID}:${x.modelID}`) + if (uniq.length > RECENT_LIMIT) uniq.pop() setStore("recent", uniq) } diff --git a/packages/app/src/context/notification.tsx b/packages/app/src/context/notification.tsx index b876bd862735..04bc2fdaaaf3 100644 --- a/packages/app/src/context/notification.tsx +++ b/packages/app/src/context/notification.tsx @@ -1,5 +1,5 @@ -import { createStore } from "solid-js/store" -import { createEffect, createMemo, onCleanup } from "solid-js" +import { createStore, reconcile } from "solid-js/store" +import { batch, createEffect, createMemo, onCleanup } from "solid-js" import { useParams } from "@solidjs/router" import { createSimpleContext } from "@opencode-ai/ui/context" import { useGlobalSDK } from "./global-sdk" @@ -13,12 +13,11 @@ import { decode64 } from "@/utils/base64" import { EventSessionError } from "@opencode-ai/sdk/v2" import { Persist, persisted } from "@/utils/persist" import { playSound, soundSrc } from "@/utils/sound" -import { buildNotificationIndex } from "./notification-index" type NotificationBase = { directory?: string session?: string - metadata?: any + metadata?: unknown time: number viewed: boolean } @@ -34,6 +33,21 @@ type ErrorNotification = NotificationBase & { export type Notification = TurnCompleteNotification | ErrorNotification +type NotificationIndex = { + session: { + all: Record + unseen: Record + unseenCount: Record + unseenHasError: Record + } + project: { + all: Record + unseen: Record + unseenCount: Record + unseenHasError: Record + } +} + const MAX_NOTIFICATIONS = 500 const NOTIFICATION_TTL_MS = 1000 * 60 * 60 * 24 * 30 @@ -44,6 +58,53 @@ function pruneNotifications(list: Notification[]) { return pruned.slice(pruned.length - MAX_NOTIFICATIONS) } +function createNotificationIndex(): NotificationIndex { + return { + session: { + all: {}, + unseen: {}, + unseenCount: {}, + unseenHasError: {}, + }, + project: { + all: {}, + unseen: {}, + unseenCount: {}, + unseenHasError: {}, + }, + } +} + +function buildNotificationIndex(list: Notification[]) { + const index = createNotificationIndex() + + list.forEach((notification) => { + if (notification.session) { + const all = index.session.all[notification.session] ?? [] + index.session.all[notification.session] = [...all, notification] + if (!notification.viewed) { + const unseen = index.session.unseen[notification.session] ?? [] + index.session.unseen[notification.session] = [...unseen, notification] + index.session.unseenCount[notification.session] = unseen.length + 1 + if (notification.type === "error") index.session.unseenHasError[notification.session] = true + } + } + + if (notification.directory) { + const all = index.project.all[notification.directory] ?? [] + index.project.all[notification.directory] = [...all, notification] + if (!notification.viewed) { + const unseen = index.project.unseen[notification.directory] ?? [] + index.project.unseen[notification.directory] = [...unseen, notification] + index.project.unseenCount[notification.directory] = unseen.length + 1 + if (notification.type === "error") index.project.unseenHasError[notification.directory] = true + } + } + }) + + return index +} + export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({ name: "Notification", init: () => { @@ -68,130 +129,243 @@ export const { use: useNotification, provider: NotificationProvider } = createSi list: [] as Notification[], }), ) + const [index, setIndex] = createStore(buildNotificationIndex(store.list)) + + const meta = { pruned: false, disposed: false } - const meta = { pruned: false } + const updateUnseen = (scope: "session" | "project", key: string, unseen: Notification[]) => { + setIndex(scope, "unseen", key, unseen) + setIndex(scope, "unseenCount", key, unseen.length) + setIndex( + scope, + "unseenHasError", + key, + unseen.some((notification) => notification.type === "error"), + ) + } + + const appendToIndex = (notification: Notification) => { + if (notification.session) { + setIndex("session", "all", notification.session, (all = []) => [...all, notification]) + if (!notification.viewed) { + setIndex("session", "unseen", notification.session, (unseen = []) => [...unseen, notification]) + setIndex("session", "unseenCount", notification.session, (count = 0) => count + 1) + if (notification.type === "error") setIndex("session", "unseenHasError", notification.session, true) + } + } + + if (notification.directory) { + setIndex("project", "all", notification.directory, (all = []) => [...all, notification]) + if (!notification.viewed) { + setIndex("project", "unseen", notification.directory, (unseen = []) => [...unseen, notification]) + setIndex("project", "unseenCount", notification.directory, (count = 0) => count + 1) + if (notification.type === "error") setIndex("project", "unseenHasError", notification.directory, true) + } + } + } + + const removeFromIndex = (notification: Notification) => { + if (notification.session) { + setIndex("session", "all", notification.session, (all = []) => all.filter((n) => n !== notification)) + if (!notification.viewed) { + const unseen = (index.session.unseen[notification.session] ?? empty).filter((n) => n !== notification) + updateUnseen("session", notification.session, unseen) + } + } + + if (notification.directory) { + setIndex("project", "all", notification.directory, (all = []) => all.filter((n) => n !== notification)) + if (!notification.viewed) { + const unseen = (index.project.unseen[notification.directory] ?? empty).filter((n) => n !== notification) + updateUnseen("project", notification.directory, unseen) + } + } + } createEffect(() => { if (!ready()) return if (meta.pruned) return meta.pruned = true - setStore("list", pruneNotifications(store.list)) + const list = pruneNotifications(store.list) + batch(() => { + setStore("list", list) + setIndex(reconcile(buildNotificationIndex(list), { merge: false })) + }) }) const append = (notification: Notification) => { - setStore("list", (list) => pruneNotifications([...list, notification])) + const list = pruneNotifications([...store.list, notification]) + const keep = new Set(list) + const removed = store.list.filter((n) => !keep.has(n)) + + batch(() => { + if (keep.has(notification)) appendToIndex(notification) + removed.forEach((n) => removeFromIndex(n)) + setStore("list", list) + }) } - const index = createMemo(() => buildNotificationIndex(store.list)) + const lookup = async (directory: string, sessionID?: string) => { + if (!sessionID) return undefined + const [syncStore] = globalSync.child(directory, { bootstrap: false }) + const match = Binary.search(syncStore.session, sessionID, (s) => s.id) + if (match.found) return syncStore.session[match.index] + return globalSDK.client.session + .get({ directory, sessionID }) + .then((x) => x.data) + .catch(() => undefined) + } - const unsub = globalSDK.event.listen((e) => { - const event = e.details - if (event.type !== "session.idle" && event.type !== "session.error") return + const viewedInCurrentSession = (directory: string, sessionID?: string) => { + const activeDirectory = currentDirectory() + const activeSession = currentSession() + if (!activeDirectory) return false + if (!activeSession) return false + if (!sessionID) return false + if (directory !== activeDirectory) return false + return sessionID === activeSession + } - const directory = e.name - const time = Date.now() - const viewed = (sessionID?: string) => { - const activeDirectory = currentDirectory() - const activeSession = currentSession() - if (!activeDirectory) return false - if (!activeSession) return false - if (!sessionID) return false - if (directory !== activeDirectory) return false - return sessionID === activeSession - } - switch (event.type) { - case "session.idle": { - const sessionID = event.properties.sessionID - const [syncStore] = globalSync.child(directory, { bootstrap: false }) - const match = Binary.search(syncStore.session, sessionID, (s) => s.id) - const session = match.found ? syncStore.session[match.index] : undefined - if (session?.parentID) break + const handleSessionIdle = (directory: string, event: { properties: { sessionID?: string } }, time: number) => { + const sessionID = event.properties.sessionID + void lookup(directory, sessionID).then((session) => { + if (meta.disposed) return + if (!session) return + if (session.parentID) return + if (settings.sounds.agentEnabled()) { playSound(soundSrc(settings.sounds.agent())) + } - append({ - directory, - time, - viewed: viewed(sessionID), - type: "turn-complete", - session: sessionID, - }) + append({ + directory, + time, + viewed: viewedInCurrentSession(directory, sessionID), + type: "turn-complete", + session: sessionID, + }) - const href = `/${base64Encode(directory)}/session/${sessionID}` - if (settings.notifications.agent()) { - void platform.notify( - language.t("notification.session.responseReady.title"), - session?.title ?? sessionID, - href, - ) - } - break + const href = `/${base64Encode(directory)}/session/${sessionID}` + if (settings.notifications.agent()) { + void platform.notify(language.t("notification.session.responseReady.title"), session.title ?? sessionID, href) } - case "session.error": { - const sessionID = event.properties.sessionID - const [syncStore] = globalSync.child(directory, { bootstrap: false }) - const match = sessionID ? Binary.search(syncStore.session, sessionID, (s) => s.id) : undefined - const session = sessionID && match?.found ? syncStore.session[match.index] : undefined - if (session?.parentID) break + }) + } + + const handleSessionError = ( + directory: string, + event: { properties: { sessionID?: string; error?: EventSessionError["properties"]["error"] } }, + time: number, + ) => { + const sessionID = event.properties.sessionID + void lookup(directory, sessionID).then((session) => { + if (meta.disposed) return + if (session?.parentID) return + if (settings.sounds.errorsEnabled()) { playSound(soundSrc(settings.sounds.errors())) + } - const error = "error" in event.properties ? event.properties.error : undefined - append({ - directory, - time, - viewed: viewed(sessionID), - type: "error", - session: sessionID ?? "global", - error, - }) - const description = - session?.title ?? - (typeof error === "string" ? error : language.t("notification.session.error.fallbackDescription")) - const href = sessionID ? `/${base64Encode(directory)}/session/${sessionID}` : `/${base64Encode(directory)}` - if (settings.notifications.errors()) { - void platform.notify(language.t("notification.session.error.title"), description, href) - } - break + const error = "error" in event.properties ? event.properties.error : undefined + append({ + directory, + time, + viewed: viewedInCurrentSession(directory, sessionID), + type: "error", + session: sessionID ?? "global", + error, + }) + const description = + session?.title ?? + (typeof error === "string" ? error : language.t("notification.session.error.fallbackDescription")) + const href = sessionID ? `/${base64Encode(directory)}/session/${sessionID}` : `/${base64Encode(directory)}` + if (settings.notifications.errors()) { + void platform.notify(language.t("notification.session.error.title"), description, href) } + }) + } + + const unsub = globalSDK.event.listen((e) => { + const event = e.details + if (event.type !== "session.idle" && event.type !== "session.error") return + + const directory = e.name + const time = Date.now() + if (event.type === "session.idle") { + handleSessionIdle(directory, event, time) + return } + handleSessionError(directory, event, time) + }) + onCleanup(() => { + meta.disposed = true + unsub() }) - onCleanup(unsub) return { ready, session: { all(session: string) { - return index().session.all.get(session) ?? empty + return index.session.all[session] ?? empty }, unseen(session: string) { - return index().session.unseen.get(session) ?? empty + return index.session.unseen[session] ?? empty }, unseenCount(session: string) { - return index().session.unseenCount.get(session) ?? 0 + return index.session.unseenCount[session] ?? 0 }, unseenHasError(session: string) { - return index().session.unseenHasError.get(session) ?? false + return index.session.unseenHasError[session] ?? false }, markViewed(session: string) { - setStore("list", (n) => n.session === session, "viewed", true) + const unseen = index.session.unseen[session] ?? empty + if (!unseen.length) return + + const projects = [ + ...new Set(unseen.flatMap((notification) => (notification.directory ? [notification.directory] : []))), + ] + batch(() => { + setStore("list", (n) => n.session === session && !n.viewed, "viewed", true) + updateUnseen("session", session, []) + projects.forEach((directory) => { + const next = (index.project.unseen[directory] ?? empty).filter( + (notification) => notification.session !== session, + ) + updateUnseen("project", directory, next) + }) + }) }, }, project: { all(directory: string) { - return index().project.all.get(directory) ?? empty + return index.project.all[directory] ?? empty }, unseen(directory: string) { - return index().project.unseen.get(directory) ?? empty + return index.project.unseen[directory] ?? empty }, unseenCount(directory: string) { - return index().project.unseenCount.get(directory) ?? 0 + return index.project.unseenCount[directory] ?? 0 }, unseenHasError(directory: string) { - return index().project.unseenHasError.get(directory) ?? false + return index.project.unseenHasError[directory] ?? false }, markViewed(directory: string) { - setStore("list", (n) => n.directory === directory, "viewed", true) + const unseen = index.project.unseen[directory] ?? empty + if (!unseen.length) return + + const sessions = [ + ...new Set(unseen.flatMap((notification) => (notification.session ? [notification.session] : []))), + ] + batch(() => { + setStore("list", (n) => n.directory === directory && !n.viewed, "viewed", true) + updateUnseen("project", directory, []) + sessions.forEach((session) => { + const next = (index.session.unseen[session] ?? empty).filter( + (notification) => notification.directory !== directory, + ) + updateUnseen("session", session, next) + }) + }) }, }, } diff --git a/packages/app/src/context/permission.tsx b/packages/app/src/context/permission.tsx index a701dbd1fec8..988723834f95 100644 --- a/packages/app/src/context/permission.tsx +++ b/packages/app/src/context/permission.tsx @@ -33,7 +33,7 @@ function isNonAllowRule(rule: unknown) { return false } -function hasAutoAcceptPermissionConfig(permission: unknown) { +function hasPermissionPromptRules(permission: unknown) { if (!permission) return false if (typeof permission === "string") return permission !== "allow" if (typeof permission !== "object") return false @@ -57,7 +57,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple const directory = decode64(params.dir) if (!directory) return false const [store] = globalSync.child(directory) - return hasAutoAcceptPermissionConfig(store.config.permission) + return hasPermissionPromptRules(store.config.permission) }) const [store, setStore, _, ready] = persisted( @@ -70,6 +70,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple const MAX_RESPONDED = 1000 const RESPONDED_TTL_MS = 60 * 60 * 1000 const responded = new Map() + const enableVersion = new Map() function pruneResponded(now: number) { for (const [id, ts] of responded) { @@ -114,6 +115,13 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple return store.autoAcceptEdits[key] ?? store.autoAcceptEdits[sessionID] ?? false } + function bumpEnableVersion(sessionID: string, directory?: string) { + const key = acceptKey(sessionID, directory) + const next = (enableVersion.get(key) ?? 0) + 1 + enableVersion.set(key, next) + return next + } + const unsubscribe = globalSDK.event.listen((e) => { const event = e.details if (event?.type !== "permission.asked") return @@ -128,6 +136,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple function enable(sessionID: string, directory: string) { const key = acceptKey(sessionID, directory) + const version = bumpEnableVersion(sessionID, directory) setStore( produce((draft) => { draft.autoAcceptEdits[key] = true @@ -138,6 +147,8 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple globalSDK.client.permission .list({ directory }) .then((x) => { + if (enableVersion.get(key) !== version) return + if (!isAutoAccepting(sessionID, directory)) return for (const perm of x.data ?? []) { if (!perm?.id) continue if (perm.sessionID !== sessionID) continue @@ -149,6 +160,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple } function disable(sessionID: string, directory?: string) { + bumpEnableVersion(sessionID, directory) const key = directory ? acceptKey(sessionID, directory) : undefined setStore( produce((draft) => { diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx index 127b9260b3b0..6d4464258a06 100644 --- a/packages/app/src/context/platform.tsx +++ b/packages/app/src/context/platform.tsx @@ -2,6 +2,12 @@ import { createSimpleContext } from "@opencode-ai/ui/context" import { AsyncStorage, SyncStorage } from "@solid-primitives/storage" import type { Accessor } from "solid-js" +type PickerPaths = string | string[] | null +type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean } +type OpenFilePickerOptions = { title?: string; multiple?: boolean } +type SaveFilePickerOptions = { title?: string; defaultPath?: string } +type UpdateInfo = { updateAvailable: boolean; version?: string } + export type Platform = { /** Platform discriminator */ platform: "web" | "desktop" @@ -31,19 +37,19 @@ export type Platform = { notify(title: string, description?: string, href?: string): Promise /** Open directory picker dialog (native on Tauri, server-backed on web) */ - openDirectoryPickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise + openDirectoryPickerDialog?(opts?: OpenDirectoryPickerOptions): Promise /** Open native file picker dialog (Tauri only) */ - openFilePickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise + openFilePickerDialog?(opts?: OpenFilePickerOptions): Promise /** Save file picker dialog (Tauri only) */ - saveFilePickerDialog?(opts?: { title?: string; defaultPath?: string }): Promise + saveFilePickerDialog?(opts?: SaveFilePickerOptions): Promise /** Storage mechanism, defaults to localStorage */ storage?: (name?: string) => SyncStorage | AsyncStorage /** Check for updates (Tauri only) */ - checkUpdate?(): Promise<{ updateAvailable: boolean; version?: string }> + checkUpdate?(): Promise /** Install updates (Tauri only) */ update?(): Promise @@ -57,6 +63,18 @@ export type Platform = { /** Set the default server URL to use on app startup (platform-specific) */ setDefaultServerUrl?(url: string | null): Promise | void + /** Get the configured WSL integration (desktop only) */ + getWslEnabled?(): Promise + + /** Set the configured WSL integration (desktop only) */ + setWslEnabled?(config: boolean): Promise | void + + /** Get the preferred display backend (desktop only) */ + getDisplayBackend?(): Promise | DisplayBackend | null + + /** Set the preferred display backend (desktop only) */ + setDisplayBackend?(backend: DisplayBackend): Promise + /** Parse markdown to HTML using native parser (desktop only, returns unprocessed code blocks) */ parseMarkdown?(markdown: string): Promise @@ -65,8 +83,13 @@ export type Platform = { /** Check if an editor app exists (desktop only) */ checkAppExists?(appName: string): Promise + + /** Read image from clipboard (desktop only) */ + readClipboardImage?(): Promise } +export type DisplayBackend = "auto" | "wayland" + export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({ name: "Platform", init: (props: { value: Platform }) => { diff --git a/packages/app/src/context/prompt.tsx b/packages/app/src/context/prompt.tsx index 99fab6c19493..064892105184 100644 --- a/packages/app/src/context/prompt.tsx +++ b/packages/app/src/context/prompt.tsx @@ -1,4 +1,4 @@ -import { createStore } from "solid-js/store" +import { createStore, type SetStoreFunction } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" import { batch, createMemo, createRoot, onCleanup } from "solid-js" import { useParams } from "@solidjs/router" @@ -60,27 +60,23 @@ function isSelectionEqual(a?: FileSelection, b?: FileSelection) { ) } +function isPartEqual(partA: ContentPart, partB: ContentPart) { + switch (partA.type) { + case "text": + return partB.type === "text" && partA.content === partB.content + case "file": + return partB.type === "file" && partA.path === partB.path && isSelectionEqual(partA.selection, partB.selection) + case "agent": + return partB.type === "agent" && partA.name === partB.name + case "image": + return partB.type === "image" && partA.id === partB.id + } +} + export function isPromptEqual(promptA: Prompt, promptB: Prompt): boolean { if (promptA.length !== promptB.length) return false for (let i = 0; i < promptA.length; i++) { - const partA = promptA[i] - const partB = promptB[i] - if (partA.type !== partB.type) return false - if (partA.type === "text" && partA.content !== (partB as TextPart).content) { - return false - } - if (partA.type === "file") { - const fileA = partA as FileAttachmentPart - const fileB = partB as FileAttachmentPart - if (fileA.path !== fileB.path) return false - if (!isSelectionEqual(fileA.selection, fileB.selection)) return false - } - if (partA.type === "agent" && partA.name !== (partB as AgentPart).name) { - return false - } - if (partA.type === "image" && partA.id !== (partB as ImageAttachmentPart).id) { - return false - } + if (!isPartEqual(promptA[i], promptB[i])) return false } return true } @@ -104,6 +100,48 @@ function clonePrompt(prompt: Prompt): Prompt { return prompt.map(clonePart) } +function contextItemKey(item: ContextItem) { + if (item.type !== "file") return item.type + const start = item.selection?.startLine + const end = item.selection?.endLine + const key = `${item.type}:${item.path}:${start}:${end}` + + if (item.commentID) { + return `${key}:c=${item.commentID}` + } + + const comment = item.comment?.trim() + if (!comment) return key + const digest = checksum(comment) ?? comment + return `${key}:c=${digest.slice(0, 8)}` +} + +function createPromptActions( + setStore: SetStoreFunction<{ + prompt: Prompt + cursor?: number + context: { + items: (ContextItem & { key: string })[] + } + }>, +) { + return { + set(prompt: Prompt, cursorPosition?: number) { + const next = clonePrompt(prompt) + batch(() => { + setStore("prompt", next) + if (cursorPosition !== undefined) setStore("cursor", cursorPosition) + }) + }, + reset() { + batch(() => { + setStore("prompt", clonePrompt(DEFAULT_PROMPT)) + setStore("cursor", 0) + }) + }, + } +} + const WORKSPACE_KEY = "__workspace__" const MAX_PROMPT_SESSIONS = 20 @@ -134,21 +172,7 @@ function createPromptSession(dir: string, id: string | undefined) { }), ) - function keyForItem(item: ContextItem) { - if (item.type !== "file") return item.type - const start = item.selection?.startLine - const end = item.selection?.endLine - const key = `${item.type}:${item.path}:${start}:${end}` - - if (item.commentID) { - return `${key}:c=${item.commentID}` - } - - const comment = item.comment?.trim() - if (!comment) return key - const digest = checksum(comment) ?? comment - return `${key}:c=${digest.slice(0, 8)}` - } + const actions = createPromptActions(setStore) return { ready, @@ -158,7 +182,7 @@ function createPromptSession(dir: string, id: string | undefined) { context: { items: createMemo(() => store.context.items), add(item: ContextItem) { - const key = keyForItem(item) + const key = contextItemKey(item) if (store.context.items.find((x) => x.key === key)) return setStore("context", "items", (items) => [...items, { key, ...item }]) }, @@ -166,19 +190,8 @@ function createPromptSession(dir: string, id: string | undefined) { setStore("context", "items", (items) => items.filter((x) => x.key !== key)) }, }, - set(prompt: Prompt, cursorPosition?: number) { - const next = clonePrompt(prompt) - batch(() => { - setStore("prompt", next) - if (cursorPosition !== undefined) setStore("cursor", cursorPosition) - }) - }, - reset() { - batch(() => { - setStore("prompt", clonePrompt(DEFAULT_PROMPT)) - setStore("cursor", 0) - }) - }, + set: actions.set, + reset: actions.reset, } } diff --git a/packages/app/src/context/sdk.tsx b/packages/app/src/context/sdk.tsx index 3a404ec93a13..555933619af2 100644 --- a/packages/app/src/context/sdk.tsx +++ b/packages/app/src/context/sdk.tsx @@ -5,6 +5,10 @@ import { createEffect, createMemo, onCleanup, type Accessor } from "solid-js" import { useGlobalSDK } from "./global-sdk" import { usePlatform } from "./platform" +type SDKEventMap = { + [key in Event["type"]]: Extract +} + export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ name: "SDK", init: (props: { directory: Accessor }) => { @@ -21,9 +25,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ }), ) - const emitter = createGlobalEmitter<{ - [key in Event["type"]]: Extract - }>() + const emitter = createGlobalEmitter() createEffect(() => { const unsub = globalSDK.event.on(directory(), (event) => { diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx index 72693e6ef647..5d3d0cf3aa6c 100644 --- a/packages/app/src/context/server.tsx +++ b/packages/app/src/context/server.tsx @@ -6,6 +6,7 @@ import { Persist, persisted } from "@/utils/persist" import { checkServerHealth } from "@/utils/server-health" type StoredProject = { worktree: string; expanded: boolean } +const HEALTH_POLL_INTERVAL_MS = 10_000 export function normalizeServerUrl(input: string) { const trimmed = input.trim() @@ -28,13 +29,14 @@ function projectsKey(url: string) { export const { use: useServer, provider: ServerProvider } = createSimpleContext({ name: "Server", - init: (props: { defaultUrl: string }) => { + init: (props: { defaultUrl: string; isSidecar?: boolean }) => { const platform = usePlatform() const [store, setStore, _, ready] = persisted( Persist.global("server", ["server.v3"]), createStore({ list: [] as string[], + currentSidecarUrl: "", projects: {} as Record, lastProject: {} as Record, }), @@ -47,19 +49,39 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( const healthy = () => state.healthy - function setActive(input: string) { - const url = normalizeServerUrl(input) - if (!url) return - setState("active", url) - } + const defaultUrl = () => normalizeServerUrl(props.defaultUrl) - function add(input: string) { - const url = normalizeServerUrl(input) - if (!url) return + function reconcileStartup() { + const fallback = defaultUrl() + if (!fallback) return - const fallback = normalizeServerUrl(props.defaultUrl) - if (fallback && url === fallback) { - setState("active", url) + const previousSidecarUrl = normalizeServerUrl(store.currentSidecarUrl) + const list = previousSidecarUrl ? store.list.filter((url) => url !== previousSidecarUrl) : store.list + if (!props.isSidecar) { + batch(() => { + setStore("list", list) + if (store.currentSidecarUrl) setStore("currentSidecarUrl", "") + setState("active", fallback) + }) + return + } + + const nextList = list.includes(fallback) ? list : [...list, fallback] + batch(() => { + setStore("list", nextList) + setStore("currentSidecarUrl", fallback) + setState("active", fallback) + }) + } + + function updateServerList(url: string, remove = false) { + if (remove) { + const list = store.list.filter((x) => x !== url) + const next = state.active === url ? (list[0] ?? defaultUrl() ?? "") : state.active + batch(() => { + setStore("list", list) + setState("active", next) + }) return } @@ -71,25 +93,53 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( }) } - function remove(input: string) { + function startHealthPolling(url: string) { + let alive = true + let busy = false + + const run = () => { + if (busy) return + busy = true + void check(url) + .then((next) => { + if (!alive) return + setState("healthy", next) + }) + .finally(() => { + busy = false + }) + } + + run() + const interval = setInterval(run, HEALTH_POLL_INTERVAL_MS) + return () => { + alive = false + clearInterval(interval) + } + } + + function setActive(input: string) { const url = normalizeServerUrl(input) if (!url) return + setState("active", url) + } - const list = store.list.filter((x) => x !== url) - const next = state.active === url ? (list[0] ?? normalizeServerUrl(props.defaultUrl) ?? "") : state.active + function add(input: string) { + const url = normalizeServerUrl(input) + if (!url) return + updateServerList(url) + } - batch(() => { - setStore("list", list) - setState("active", next) - }) + function remove(input: string) { + const url = normalizeServerUrl(input) + if (!url) return + updateServerList(url, true) } createEffect(() => { if (!ready()) return if (state.active) return - const url = normalizeServerUrl(props.defaultUrl) - if (!url) return - setState("active", url) + reconcileStartup() }) const isReady = createMemo(() => ready() && !!state.active) @@ -102,30 +152,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( if (!url) return setState("healthy", undefined) - - let alive = true - let busy = false - - const run = () => { - if (busy) return - busy = true - void check(url) - .then((next) => { - if (!alive) return - setState("healthy", next) - }) - .finally(() => { - busy = false - }) - } - - run() - const interval = setInterval(run, 10_000) - - onCleanup(() => { - alive = false - clearInterval(interval) - }) + onCleanup(startHealthPolling(url)) }) const origin = createMemo(() => projectsKey(state.active)) diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx index 19b3846f84e8..fbcd0a851845 100644 --- a/packages/app/src/context/settings.tsx +++ b/packages/app/src/context/settings.tsx @@ -10,8 +10,11 @@ export interface NotificationSettings { } export interface SoundSettings { + agentEnabled: boolean agent: string + permissionsEnabled: boolean permissions: string + errorsEnabled: boolean errors: string } @@ -57,8 +60,11 @@ const defaultSettings: Settings = { errors: false, }, sounds: { + agentEnabled: true, agent: "staplebops-01", + permissionsEnabled: true, permissions: "staplebops-02", + errorsEnabled: true, errors: "nope-03", }, } @@ -79,12 +85,17 @@ const monoFonts: Record = { "roboto-mono": `"Roboto Mono Nerd Font", "RobotoMono Nerd Font", "RobotoMono Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, "source-code-pro": `"Source Code Pro Nerd Font", "SauceCodePro Nerd Font", "SauceCodePro Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, "ubuntu-mono": `"Ubuntu Mono Nerd Font", "UbuntuMono Nerd Font", "UbuntuMono Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, + "geist-mono": `"GeistMono Nerd Font", "GeistMono Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, } export function monoFontFamily(font: string | undefined) { return monoFonts[font ?? defaultSettings.appearance.font] ?? monoFonts[defaultSettings.appearance.font] } +function withFallback(read: () => T | undefined, fallback: T) { + return createMemo(() => read() ?? fallback) +} + export const { use: useSettings, provider: SettingsProvider } = createSimpleContext({ name: "Settings", init: () => { @@ -101,27 +112,27 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont return store }, general: { - autoSave: createMemo(() => store.general?.autoSave ?? defaultSettings.general.autoSave), + autoSave: withFallback(() => store.general?.autoSave, defaultSettings.general.autoSave), setAutoSave(value: boolean) { setStore("general", "autoSave", value) }, - releaseNotes: createMemo(() => store.general?.releaseNotes ?? defaultSettings.general.releaseNotes), + releaseNotes: withFallback(() => store.general?.releaseNotes, defaultSettings.general.releaseNotes), setReleaseNotes(value: boolean) { setStore("general", "releaseNotes", value) }, }, updates: { - startup: createMemo(() => store.updates?.startup ?? defaultSettings.updates.startup), + startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup), setStartup(value: boolean) { setStore("updates", "startup", value) }, }, appearance: { - fontSize: createMemo(() => store.appearance?.fontSize ?? defaultSettings.appearance.fontSize), + fontSize: withFallback(() => store.appearance?.fontSize, defaultSettings.appearance.fontSize), setFontSize(value: number) { setStore("appearance", "fontSize", value) }, - font: createMemo(() => store.appearance?.font ?? defaultSettings.appearance.font), + font: withFallback(() => store.appearance?.font, defaultSettings.appearance.font), setFont(value: string) { setStore("appearance", "font", value) }, @@ -132,42 +143,62 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont setStore("keybinds", action, keybind) }, reset(action: string) { - setStore("keybinds", action, undefined!) + setStore("keybinds", (current) => { + if (!Object.prototype.hasOwnProperty.call(current, action)) return current + const next = { ...current } + delete next[action] + return next + }) }, resetAll() { setStore("keybinds", reconcile({})) }, }, permissions: { - autoApprove: createMemo(() => store.permissions?.autoApprove ?? defaultSettings.permissions.autoApprove), + autoApprove: withFallback(() => store.permissions?.autoApprove, defaultSettings.permissions.autoApprove), setAutoApprove(value: boolean) { setStore("permissions", "autoApprove", value) }, }, notifications: { - agent: createMemo(() => store.notifications?.agent ?? defaultSettings.notifications.agent), + agent: withFallback(() => store.notifications?.agent, defaultSettings.notifications.agent), setAgent(value: boolean) { setStore("notifications", "agent", value) }, - permissions: createMemo(() => store.notifications?.permissions ?? defaultSettings.notifications.permissions), + permissions: withFallback(() => store.notifications?.permissions, defaultSettings.notifications.permissions), setPermissions(value: boolean) { setStore("notifications", "permissions", value) }, - errors: createMemo(() => store.notifications?.errors ?? defaultSettings.notifications.errors), + errors: withFallback(() => store.notifications?.errors, defaultSettings.notifications.errors), setErrors(value: boolean) { setStore("notifications", "errors", value) }, }, sounds: { - agent: createMemo(() => store.sounds?.agent ?? defaultSettings.sounds.agent), + agentEnabled: withFallback(() => store.sounds?.agentEnabled, defaultSettings.sounds.agentEnabled), + setAgentEnabled(value: boolean) { + setStore("sounds", "agentEnabled", value) + }, + agent: withFallback(() => store.sounds?.agent, defaultSettings.sounds.agent), setAgent(value: string) { setStore("sounds", "agent", value) }, - permissions: createMemo(() => store.sounds?.permissions ?? defaultSettings.sounds.permissions), + permissionsEnabled: withFallback( + () => store.sounds?.permissionsEnabled, + defaultSettings.sounds.permissionsEnabled, + ), + setPermissionsEnabled(value: boolean) { + setStore("sounds", "permissionsEnabled", value) + }, + permissions: withFallback(() => store.sounds?.permissions, defaultSettings.sounds.permissions), setPermissions(value: string) { setStore("sounds", "permissions", value) }, - errors: createMemo(() => store.sounds?.errors ?? defaultSettings.sounds.errors), + errorsEnabled: withFallback(() => store.sounds?.errorsEnabled, defaultSettings.sounds.errorsEnabled), + setErrorsEnabled(value: boolean) { + setStore("sounds", "errorsEnabled", value) + }, + errors: withFallback(() => store.sounds?.errors, defaultSettings.sounds.errors), setErrors(value: string) { setStore("sounds", "errors", value) }, diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx index 66c53dc8021d..e5916598b52d 100644 --- a/packages/app/src/context/sync.tsx +++ b/packages/app/src/context/sync.tsx @@ -7,6 +7,20 @@ import { useGlobalSync } from "./global-sync" import { useSDK } from "./sdk" import type { Message, Part } from "@opencode-ai/sdk/v2/client" +function sortParts(parts: Part[]) { + return parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)) +} + +function runInflight(map: Map>, key: string, task: () => Promise) { + const pending = map.get(key) + if (pending) return pending + const promise = task().finally(() => { + map.delete(key) + }) + map.set(key, promise) + return promise +} + const keyFor = (directory: string, id: string) => `${directory}\n${id}` const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) @@ -36,7 +50,7 @@ export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddI const result = Binary.search(messages, input.message.id, (m) => m.id) messages.splice(result.index, 0, input.message) } - draft.part[input.message.id] = input.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)) + draft.part[input.message.id] = sortParts(input.parts) } export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticRemoveInput) { @@ -48,6 +62,34 @@ export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticR delete draft.part[input.messageID] } +function setOptimisticAdd(setStore: (...args: unknown[]) => void, input: OptimisticAddInput) { + setStore("message", input.sessionID, (messages: Message[] | undefined) => { + if (!messages) return [input.message] + const result = Binary.search(messages, input.message.id, (m) => m.id) + const next = [...messages] + next.splice(result.index, 0, input.message) + return next + }) + setStore("part", input.message.id, sortParts(input.parts)) +} + +function setOptimisticRemove(setStore: (...args: unknown[]) => void, input: OptimisticRemoveInput) { + setStore("message", input.sessionID, (messages: Message[] | undefined) => { + if (!messages) return messages + const result = Binary.search(messages, input.messageID, (m) => m.id) + if (!result.found) return messages + const next = [...messages] + next.splice(result.index, 1) + return next + }) + setStore("part", (part: Record) => { + if (!(input.messageID in part)) return part + const next = { ...part } + delete next[input.messageID] + return next + }) +} + export const { use: useSync, provider: SyncProvider } = createSimpleContext({ name: "Sync", init: () => { @@ -63,7 +105,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ return globalSync.child(directory) } const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/") - const chunk = 400 + const messagePageSize = 400 const inflight = new Map>() const inflightDiff = new Map>() const inflightTodo = new Map>() @@ -81,8 +123,25 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ } const limitFor = (count: number) => { - if (count <= chunk) return chunk - return Math.ceil(count / chunk) * chunk + if (count <= messagePageSize) return messagePageSize + return Math.ceil(count / messagePageSize) * messagePageSize + } + + const fetchMessages = async (input: { client: typeof sdk.client; sessionID: string; limit: number }) => { + const messages = await retry(() => + input.client.session.messages({ sessionID: input.sessionID, limit: input.limit }), + ) + const items = (messages.data ?? []).filter((x) => !!x?.info?.id) + const session = items + .map((x) => x.info) + .filter((m) => !!m?.id) + .sort((a, b) => cmp(a.id, b.id)) + const part = items.map((message) => ({ id: message.info.id, part: sortParts(message.parts) })) + return { + session, + part, + complete: session.length < input.limit, + } } const loadMessages = async (input: { @@ -96,30 +155,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ if (meta.loading[key]) return setMeta("loading", key, true) - await retry(() => input.client.session.messages({ sessionID: input.sessionID, limit: input.limit })) - .then((messages) => { - const items = (messages.data ?? []).filter((x) => !!x?.info?.id) - const next = items - .map((x) => x.info) - .filter((m) => !!m?.id) - .sort((a, b) => cmp(a.id, b.id)) - + await fetchMessages(input) + .then((next) => { batch(() => { - input.setStore("message", input.sessionID, reconcile(next, { key: "id" })) - - for (const message of items) { - input.setStore( - "part", - message.info.id, - reconcile( - message.parts.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)), - { key: "id" }, - ), - ) + input.setStore("message", input.sessionID, reconcile(next.session, { key: "id" })) + for (const message of next.part) { + input.setStore("part", message.id, reconcile(message.part, { key: "id" })) } - setMeta("limit", key, input.limit) - setMeta("complete", key, next.length < input.limit) + setMeta("complete", key, next.complete) }) }) .finally(() => { @@ -151,19 +195,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ optimistic: { add(input: { directory?: string; sessionID: string; message: Message; parts: Part[] }) { const [, setStore] = target(input.directory) - setStore( - produce((draft) => { - applyOptimisticAdd(draft as OptimisticStore, input) - }), - ) + setOptimisticAdd(setStore as (...args: unknown[]) => void, input) }, remove(input: { directory?: string; sessionID: string; messageID: string }) { const [, setStore] = target(input.directory) - setStore( - produce((draft) => { - applyOptimisticRemove(draft as OptimisticStore, input) - }), - ) + setOptimisticRemove(setStore as (...args: unknown[]) => void, input) }, }, addOptimisticMessage(input: { @@ -182,15 +218,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ model: input.model, } const [, setStore] = target() - setStore( - produce((draft) => { - applyOptimisticAdd(draft as OptimisticStore, { - sessionID: input.sessionID, - message, - parts: input.parts, - }) - }), - ) + setOptimisticAdd(setStore as (...args: unknown[]) => void, { + sessionID: input.sessionID, + message, + parts: input.parts, + }) }, async sync(sessionID: string) { const directory = sdk.directory @@ -205,11 +237,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const hasMessages = store.message[sessionID] !== undefined const hydrated = meta.limit[key] !== undefined if (hasSession && hasMessages && hydrated) return - const pending = inflight.get(key) - if (pending) return pending const count = store.message[sessionID]?.length ?? 0 - const limit = hydrated ? (meta.limit[key] ?? chunk) : limitFor(count) + const limit = hydrated ? (meta.limit[key] ?? messagePageSize) : limitFor(count) const sessionReq = hasSession ? Promise.resolve() @@ -240,14 +270,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ limit, }) - const promise = Promise.all([sessionReq, messagesReq]) - .then(() => {}) - .finally(() => { - inflight.delete(key) - }) - - inflight.set(key, promise) - return promise + return runInflight(inflight, key, () => Promise.all([sessionReq, messagesReq]).then(() => {})) }, async diff(sessionID: string) { const directory = sdk.directory @@ -256,19 +279,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ if (store.session_diff[sessionID] !== undefined) return const key = keyFor(directory, sessionID) - const pending = inflightDiff.get(key) - if (pending) return pending - - const promise = retry(() => client.session.diff({ sessionID })) - .then((diff) => { + return runInflight(inflightDiff, key, () => + retry(() => client.session.diff({ sessionID })).then((diff) => { setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" })) - }) - .finally(() => { - inflightDiff.delete(key) - }) - - inflightDiff.set(key, promise) - return promise + }), + ) }, async todo(sessionID: string) { const directory = sdk.directory @@ -277,19 +292,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ if (store.todo[sessionID] !== undefined) return const key = keyFor(directory, sessionID) - const pending = inflightTodo.get(key) - if (pending) return pending - - const promise = retry(() => client.session.todo({ sessionID })) - .then((todo) => { + return runInflight(inflightTodo, key, () => + retry(() => client.session.todo({ sessionID })).then((todo) => { setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" })) - }) - .finally(() => { - inflightTodo.delete(key) - }) - - inflightTodo.set(key, promise) - return promise + }), + ) }, history: { more(sessionID: string) { @@ -304,7 +311,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const key = keyFor(sdk.directory, sessionID) return meta.loading[key] ?? false }, - async loadMore(sessionID: string, count = chunk) { + async loadMore(sessionID: string, count = messagePageSize) { const directory = sdk.directory const client = sdk.client const [, setStore] = globalSync.child(directory) @@ -312,7 +319,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ if (meta.loading[key]) return if (meta.complete[key]) return - const currentLimit = meta.limit[key] ?? chunk + const currentLimit = meta.limit[key] ?? messagePageSize await loadMessages({ directory, client, diff --git a/packages/app/src/context/terminal.test.ts b/packages/app/src/context/terminal.test.ts index d8c8cfcd4fd0..a250de57c0de 100644 --- a/packages/app/src/context/terminal.test.ts +++ b/packages/app/src/context/terminal.test.ts @@ -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", () => ({ diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index 76e8cf0f7385..64f026219ab9 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -3,7 +3,8 @@ import { createSimpleContext } from "@opencode-ai/ui/context" import { batch, createEffect, createMemo, createRoot, onCleanup } from "solid-js" import { useParams } from "@solidjs/router" import { useSDK } from "./sdk" -import { Persist, persisted } from "@/utils/persist" +import type { Platform } from "./platform" +import { Persist, persisted, removePersisted } from "@/utils/persist" export type LocalPTY = { id: string @@ -13,7 +14,7 @@ export type LocalPTY = { cols?: number buffer?: string scrollY?: number - tail?: string + cursor?: number } const WORKSPACE_KEY = "__workspace__" @@ -35,6 +36,28 @@ type TerminalCacheEntry = { dispose: VoidFunction } +const caches = new Set>() + +export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], platform?: Platform) { + const key = getWorkspaceTerminalCacheKey(dir) + for (const cache of caches) { + const entry = cache.get(key) + entry?.value.clear() + } + + removePersisted(Persist.workspace(dir, "terminal"), platform) + + const legacy = new Set(getLegacyTerminalStorageKeys(dir)) + for (const id of sessionIDs ?? []) { + for (const key of getLegacyTerminalStorageKeys(dir, id)) { + legacy.add(key) + } + } + for (const key of legacy) { + removePersisted({ key }, platform) + } +} + function createWorkspaceTerminalSession(sdk: ReturnType, dir: string, legacySessionID?: string) { const legacy = getLegacyTerminalStorageKeys(dir, legacySessionID) @@ -56,19 +79,42 @@ function createWorkspaceTerminalSession(sdk: ReturnType, dir: str }), ) - const unsub = sdk.event.on("pty.exited", (event) => { - const id = event.properties.id - if (!store.all.some((x) => x.id === id)) return + const pickNextTerminalNumber = () => { + const existingTitleNumbers = new Set( + store.all.flatMap((pty) => { + const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined + if (direct !== undefined) return [direct] + const parsed = numberFromTitle(pty.title) + if (parsed === undefined) return [] + return [parsed] + }), + ) + + return ( + Array.from({ length: existingTitleNumbers.size + 1 }, (_, index) => index + 1).find( + (number) => !existingTitleNumbers.has(number), + ) ?? 1 + ) + } + + const removeExited = (id: string) => { + const all = store.all + const index = all.findIndex((x) => x.id === id) + if (index === -1) return + const active = store.active === id ? (index === 0 ? all[1]?.id : all[0]?.id) : store.active batch(() => { + setStore("active", active) setStore( "all", - store.all.filter((x) => x.id !== id), + produce((draft) => { + draft.splice(index, 1) + }), ) - if (store.active === id) { - const remaining = store.all.filter((x) => x.id !== id) - setStore("active", remaining[0]?.id) - } }) + } + + const unsub = sdk.event.on("pty.exited", (event: { properties: { id: string } }) => { + removeExited(event.properties.id) }) onCleanup(unsub) @@ -94,27 +140,20 @@ function createWorkspaceTerminalSession(sdk: ReturnType, dir: str return { ready, - all: createMemo(() => Object.values(store.all)), + all: createMemo(() => store.all), active: createMemo(() => store.active), + clear() { + batch(() => { + setStore("active", undefined) + setStore("all", []) + }) + }, new() { - const existingTitleNumbers = new Set( - store.all.flatMap((pty) => { - const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined - if (direct !== undefined) return [direct] - const parsed = numberFromTitle(pty.title) - if (parsed === undefined) return [] - return [parsed] - }), - ) - - const nextNumber = - Array.from({ length: existingTitleNumbers.size + 1 }, (_, index) => index + 1).find( - (number) => !existingTitleNumbers.has(number), - ) ?? 1 + const nextNumber = pickNextTerminalNumber() sdk.client.pty .create({ title: `Terminal ${nextNumber}` }) - .then((pty) => { + .then((pty: { data?: { id?: string; title?: string } }) => { const id = pty.data?.id if (!id) return const newTerminal = { @@ -122,20 +161,18 @@ function createWorkspaceTerminalSession(sdk: ReturnType, dir: str title: pty.data?.title ?? "Terminal", titleNumber: nextNumber, } - setStore("all", (all) => { - const newAll = [...all, newTerminal] - return newAll - }) + setStore("all", store.all.length, newTerminal) setStore("active", id) }) - .catch((e) => { - console.error("Failed to create terminal", e) + .catch((error: unknown) => { + console.error("Failed to create terminal", error) }) }, update(pty: Partial & { id: string }) { const index = store.all.findIndex((x) => x.id === pty.id) - if (index !== -1) { - setStore("all", index, (existing) => ({ ...existing, ...pty })) + const previous = index >= 0 ? store.all[index] : undefined + if (index >= 0) { + setStore("all", index, (item) => ({ ...item, ...pty })) } sdk.client.pty .update({ @@ -143,8 +180,12 @@ function createWorkspaceTerminalSession(sdk: ReturnType, dir: str title: pty.title, size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined, }) - .catch((e) => { - console.error("Failed to update terminal", e) + .catch((error: unknown) => { + if (previous) { + const currentIndex = store.all.findIndex((item) => item.id === pty.id) + if (currentIndex >= 0) setStore("all", currentIndex, previous) + } + console.error("Failed to update terminal", error) }) }, async clone(id: string) { @@ -155,8 +196,8 @@ function createWorkspaceTerminalSession(sdk: ReturnType, dir: str .create({ title: pty.title, }) - .catch((e) => { - console.error("Failed to clone terminal", e) + .catch((error: unknown) => { + console.error("Failed to clone terminal", error) return undefined }) if (!clone?.data) return @@ -168,6 +209,12 @@ function createWorkspaceTerminalSession(sdk: ReturnType, dir: str id: clone.data.id, title: clone.data.title ?? pty.title, titleNumber: pty.titleNumber, + // New PTY process, so start clean. + buffer: undefined, + cursor: undefined, + scrollY: undefined, + rows: undefined, + cols: undefined, }) if (active) { setStore("active", clone.data.id) @@ -190,18 +237,24 @@ function createWorkspaceTerminalSession(sdk: ReturnType, dir: str setStore("active", store.all[prevIndex]?.id) }, async close(id: string) { - batch(() => { - const filtered = store.all.filter((x) => x.id !== id) - if (store.active === id) { - const index = store.all.findIndex((f) => f.id === id) - const next = index > 0 ? index - 1 : 0 - setStore("active", filtered[next]?.id) - } - setStore("all", filtered) - }) + const index = store.all.findIndex((f) => f.id === id) + if (index !== -1) { + batch(() => { + if (store.active === id) { + const next = index > 0 ? store.all[index - 1]?.id : store.all[1]?.id + setStore("active", next) + } + setStore( + "all", + produce((all) => { + all.splice(index, 1) + }), + ) + }) + } - await sdk.client.pty.remove({ ptyID: id }).catch((e) => { - console.error("Failed to close terminal", e) + await sdk.client.pty.remove({ ptyID: id }).catch((error: unknown) => { + console.error("Failed to close terminal", error) }) }, move(id: string, to: number) { @@ -225,6 +278,9 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont const params = useParams() const cache = new Map() + caches.add(cache) + onCleanup(() => caches.delete(cache)) + const disposeAll = () => { for (const entry of cache.values()) { entry.dispose() diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index aa52fa1e7cb5..3a85086b48b0 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -4,101 +4,118 @@ import { AppBaseProviders, AppInterface } from "@/app" import { Platform, PlatformProvider } from "@/context/platform" import { dict as en } from "@/i18n/en" import { dict as zh } from "@/i18n/zh" +import { handleNotificationClick } from "@/utils/notification-click" import pkg from "../package.json" const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl" -const root = document.getElementById("root") -if (import.meta.env.DEV && !(root instanceof HTMLElement)) { - const locale = (() => { - if (typeof navigator !== "object") return "en" as const - const languages = navigator.languages?.length ? navigator.languages : [navigator.language] - for (const language of languages) { - if (!language) continue - if (language.toLowerCase().startsWith("zh")) return "zh" as const - } - return "en" as const - })() +const getLocale = () => { + if (typeof navigator !== "object") return "en" as const + const languages = navigator.languages?.length ? navigator.languages : [navigator.language] + for (const language of languages) { + if (!language) continue + if (language.toLowerCase().startsWith("zh")) return "zh" as const + } + return "en" as const +} +const getRootNotFoundError = () => { const key = "error.dev.rootNotFound" as const - const message = locale === "zh" ? (zh[key] ?? en[key]) : en[key] - throw new Error(message) + const locale = getLocale() + return locale === "zh" ? (zh[key] ?? en[key]) : en[key] +} + +const getStorage = (key: string) => { + if (typeof localStorage === "undefined") return null + try { + return localStorage.getItem(key) + } catch { + return null + } +} + +const setStorage = (key: string, value: string | null) => { + if (typeof localStorage === "undefined") return + try { + if (value !== null) { + localStorage.setItem(key, value) + return + } + localStorage.removeItem(key) + } catch { + return + } +} + +const readDefaultServerUrl = () => getStorage(DEFAULT_SERVER_URL_KEY) +const writeDefaultServerUrl = (url: string | null) => setStorage(DEFAULT_SERVER_URL_KEY, url) + +const notify: Platform["notify"] = async (title, description, href) => { + if (!("Notification" in window)) return + + const permission = + Notification.permission === "default" + ? await Notification.requestPermission().catch(() => "denied") + : Notification.permission + + if (permission !== "granted") return + + const inView = document.visibilityState === "visible" && document.hasFocus() + if (inView) return + + const notification = new Notification(title, { + body: description ?? "", + icon: "https://opencode.ai/favicon-96x96-v3.png", + }) + + notification.onclick = () => { + handleNotificationClick(href) + notification.close() + } +} + +const openLink: Platform["openLink"] = (url) => { + window.open(url, "_blank") +} + +const back: Platform["back"] = () => { + window.history.back() +} + +const forward: Platform["forward"] = () => { + window.history.forward() +} + +const restart: Platform["restart"] = async () => { + window.location.reload() +} + +const root = document.getElementById("root") +if (!(root instanceof HTMLElement) && import.meta.env.DEV) { + throw new Error(getRootNotFoundError()) } const platform: Platform = { platform: "web", version: pkg.version, - openLink(url: string) { - window.open(url, "_blank") - }, - back() { - window.history.back() - }, - forward() { - window.history.forward() - }, - restart: async () => { - window.location.reload() - }, - notify: async (title, description, href) => { - if (!("Notification" in window)) return - - const permission = - Notification.permission === "default" - ? await Notification.requestPermission().catch(() => "denied") - : Notification.permission - - if (permission !== "granted") return - - const inView = document.visibilityState === "visible" && document.hasFocus() - if (inView) return - - await Promise.resolve() - .then(() => { - const notification = new Notification(title, { - body: description ?? "", - icon: "https://opencode.ai/favicon-96x96-v3.png", - }) - notification.onclick = () => { - window.focus() - if (href) { - window.history.pushState(null, "", href) - window.dispatchEvent(new PopStateEvent("popstate")) - } - notification.close() - } - }) - .catch(() => undefined) - }, - getDefaultServerUrl: () => { - if (typeof localStorage === "undefined") return null - try { - return localStorage.getItem(DEFAULT_SERVER_URL_KEY) - } catch { - return null - } - }, - setDefaultServerUrl: (url) => { - if (typeof localStorage === "undefined") return - try { - if (url) { - localStorage.setItem(DEFAULT_SERVER_URL_KEY, url) - return - } - localStorage.removeItem(DEFAULT_SERVER_URL_KEY) - } catch { - return - } - }, + openLink, + back, + forward, + restart, + notify, + getDefaultServerUrl: readDefaultServerUrl, + setDefaultServerUrl: writeDefaultServerUrl, } -render( - () => ( - - - - - - ), - root!, -) +if (root instanceof HTMLElement) { + render( + () => ( + + + + + + ), + root, + ) +} diff --git a/packages/app/src/env.d.ts b/packages/app/src/env.d.ts index ad575e93b4ad..89721f34f294 100644 --- a/packages/app/src/env.d.ts +++ b/packages/app/src/env.d.ts @@ -1,3 +1,5 @@ +import "solid-js" + interface ImportMetaEnv { readonly VITE_OPENCODE_SERVER_HOST: string readonly VITE_OPENCODE_SERVER_PORT: string @@ -6,3 +8,11 @@ interface ImportMetaEnv { interface ImportMeta { readonly env: ImportMetaEnv } + +declare module "solid-js" { + namespace JSX { + interface Directives { + sortable: true + } + } +} diff --git a/packages/app/src/hooks/use-providers.ts b/packages/app/src/hooks/use-providers.ts index 55184aa1b42b..502364afdf3a 100644 --- a/packages/app/src/hooks/use-providers.ts +++ b/packages/app/src/hooks/use-providers.ts @@ -4,6 +4,7 @@ import { useParams } from "@solidjs/router" import { createMemo } from "solid-js" export const popularProviders = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"] +const popularProviderSet = new Set(popularProviders) export function useProviders() { const globalSync = useGlobalSync() @@ -16,11 +17,12 @@ export function useProviders() { } return globalSync.data.provider }) - const connected = createMemo(() => providers().all.filter((p) => providers().connected.includes(p.id))) + const connectedIDs = createMemo(() => new Set(providers().connected)) + const connected = createMemo(() => providers().all.filter((p) => connectedIDs().has(p.id))) const paid = createMemo(() => connected().filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input)), ) - const popular = createMemo(() => providers().all.filter((p) => popularProviders.includes(p.id))) + const popular = createMemo(() => providers().all.filter((p) => popularProviderSet.has(p.id))) return { all: createMemo(() => providers().all), default: createMemo(() => providers().default), diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 77a3edb062a2..3d347c8423cb 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -16,11 +16,9 @@ export const dict = { "command.category.permissions": "أذونات", "command.category.workspace": "مساحة عمل", "command.category.settings": "إعدادات", - "theme.scheme.system": "نظام", "theme.scheme.light": "فاتح", "theme.scheme.dark": "داكن", - "command.sidebar.toggle": "تبديل الشريط الجانبي", "command.project.open": "فتح مشروع", "command.provider.connect": "اتصال بموفر", @@ -31,21 +29,19 @@ export const dict = { "command.session.previous.unseen": "الجلسة غير المقروءة السابقة", "command.session.next.unseen": "الجلسة غير المقروءة التالية", "command.session.archive": "أرشفة الجلسة", - "command.palette": "لوحة الأوامر", - "command.theme.cycle": "تغيير السمة", "command.theme.set": "استخدام السمة: {{theme}}", "command.theme.scheme.cycle": "تغيير مخطط الألوان", "command.theme.scheme.set": "استخدام مخطط الألوان: {{scheme}}", - "command.language.cycle": "تغيير اللغة", "command.language.set": "استخدام اللغة: {{language}}", - "command.session.new": "جلسة جديدة", "command.file.open": "فتح ملف", + "command.tab.close": "إغلاق علامة التبويب", "command.context.addSelection": "إضافة التحديد إلى السياق", "command.context.addSelection.description": "إضافة الأسطر المحددة من الملف الحالي", + "command.input.focus": "التركيز على حقل الإدخال", "command.terminal.toggle": "تبديل المحطة الطرفية", "command.fileTree.toggle": "تبديل شجرة الملفات", "command.review.toggle": "تبديل المراجعة", @@ -70,6 +66,7 @@ export const dict = { "command.permissions.autoaccept.enable": "قبول التعديلات تلقائيًا", "command.permissions.autoaccept.disable": "إيقاف قبول التعديلات تلقائيًا", "command.workspace.toggle": "تبديل مساحات العمل", + "command.workspace.toggle.description": "تمكين أو تعطيل مساحات العمل المتعددة في الشريط الجانبي", "command.session.undo": "تراجع", "command.session.undo.description": "تراجع عن الرسالة الأخيرة", "command.session.redo": "إعادة", @@ -82,32 +79,30 @@ export const dict = { "command.session.share.description": "مشاركة هذه الجلسة ونسخ الرابط إلى الحافظة", "command.session.unshare": "إلغاء مشاركة الجلسة", "command.session.unshare.description": "إيقاف مشاركة هذه الجلسة", - "palette.search.placeholder": "البحث في الملفات والأوامر والجلسات", "palette.empty": "لا توجد نتائج", "palette.group.commands": "الأوامر", "palette.group.files": "الملفات", - "dialog.provider.search.placeholder": "البحث عن موفرين", "dialog.provider.empty": "لم يتم العثور على موفرين", "dialog.provider.group.popular": "شائع", "dialog.provider.group.other": "آخر", "dialog.provider.tag.recommended": "موصى به", + "dialog.provider.opencode.note": "نماذج مختارة تتضمن Claude و GPT و Gemini والمزيد", "dialog.provider.anthropic.note": "اتصل باستخدام Claude Pro/Max أو مفتاح API", - "dialog.provider.openai.note": "اتصل باستخدام ChatGPT Pro/Plus أو مفتاح API", "dialog.provider.copilot.note": "اتصل باستخدام Copilot أو مفتاح API", - + "dialog.provider.openai.note": "اتصل باستخدام ChatGPT Pro/Plus أو مفتاح API", + "dialog.provider.google.note": "نماذج Gemini لاستجابات سريعة ومنظمة", + "dialog.provider.openrouter.note": "الوصول إلى جميع النماذج المدعومة من موفر واحد", + "dialog.provider.vercel.note": "وصول موحد إلى نماذج الذكاء الاصطناعي مع توجيه ذكي", "dialog.model.select.title": "تحديد نموذج", "dialog.model.search.placeholder": "البحث عن نماذج", "dialog.model.empty": "لا توجد نتائج للنماذج", "dialog.model.manage": "إدارة النماذج", "dialog.model.manage.description": "تخصيص النماذج التي تظهر في محدد النماذج.", - "dialog.model.unpaid.freeModels.title": "نماذج مجانية مقدمة من OpenCode", "dialog.model.unpaid.addMore.title": "إضافة المزيد من النماذج من موفرين مشهورين", - "dialog.provider.viewAll": "عرض المزيد من الموفرين", - "provider.connect.title": "اتصال {{provider}}", "provider.connect.title.anthropicProMax": "تسجيل الدخول باستخدام Claude Pro/Max", "provider.connect.selectMethod": "حدد طريقة تسجيل الدخول لـ {{provider}}.", @@ -142,7 +137,42 @@ export const dict = { "provider.connect.oauth.auto.confirmationCode": "رمز التأكيد", "provider.connect.toast.connected.title": "تم توصيل {{provider}}", "provider.connect.toast.connected.description": "نماذج {{provider}} متاحة الآن للاستخدام.", - + "provider.custom.title": "موفر مخصص", + "provider.custom.description.prefix": "تكوين موفر متوافق مع OpenAI. راجع ", + "provider.custom.description.link": "وثائق تكوين الموفر", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "معرف الموفر", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "أحرف صغيرة، أرقام، شرطات، أو شرطات سفلية", + "provider.custom.field.name.label": "اسم العرض", + "provider.custom.field.name.placeholder": "موفر الذكاء الاصطناعي الخاص بي", + "provider.custom.field.baseURL.label": "عنوان URL الأساسي", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "مفتاح API", + "provider.custom.field.apiKey.placeholder": "مفتاح API", + "provider.custom.field.apiKey.description": "اختياري. اتركه فارغًا إذا كنت تدير المصادقة عبر الترويسات.", + "provider.custom.models.label": "النماذج", + "provider.custom.models.id.label": "المعرف", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "الاسم", + "provider.custom.models.name.placeholder": "اسم العرض", + "provider.custom.models.remove": "إزالة النموذج", + "provider.custom.models.add": "إضافة نموذج", + "provider.custom.headers.label": "الترويسات (اختياري)", + "provider.custom.headers.key.label": "ترويسة", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "القيمة", + "provider.custom.headers.value.placeholder": "قيمة", + "provider.custom.headers.remove": "إزالة الترويسة", + "provider.custom.headers.add": "إضافة ترويسة", + "provider.custom.error.providerID.required": "معرف الموفر مطلوب", + "provider.custom.error.providerID.format": "استخدم أحرفًا صغيرة، أرقامًا، شرطات، أو شرطات سفلية", + "provider.custom.error.providerID.exists": "معرف الموفر هذا موجود بالفعل", + "provider.custom.error.name.required": "اسم العرض مطلوب", + "provider.custom.error.baseURL.required": "عنوان URL الأساسي مطلوب", + "provider.custom.error.baseURL.format": "يجب أن يبدأ بـ http:// أو https://", + "provider.custom.error.required": "مطلوب", + "provider.custom.error.duplicate": "مكرر", "provider.disconnect.toast.disconnected.title": "تم فصل {{provider}}", "provider.disconnect.toast.disconnected.description": "لم تعد نماذج {{provider}} متاحة.", "model.tag.free": "مجاني", @@ -161,9 +191,9 @@ export const dict = { "model.tooltip.reasoning.allowed": "يسمح بالاستنتاج", "model.tooltip.reasoning.none": "بدون استنتاج", "model.tooltip.context": "حد السياق {{limit}}", - "common.search.placeholder": "بحث", "common.goBack": "رجوع", + "common.goForward": "انتقل للأمام", "common.loading": "جارٍ التحميل", "common.loading.ellipsis": "...", "common.cancel": "إلغاء", @@ -174,14 +204,12 @@ export const dict = { "common.saving": "جارٍ الحفظ...", "common.default": "افتراضي", "common.attachment": "مرفق", - "prompt.placeholder.shell": "أدخل أمر shell...", "prompt.placeholder.normal": 'اسأل أي شيء... "{{example}}"', "prompt.placeholder.summarizeComments": "لخّص التعليقات…", "prompt.placeholder.summarizeComment": "لخّص التعليق…", "prompt.mode.shell": "Shell", "prompt.mode.shell.exit": "esc للخروج", - "prompt.example.1": "إصلاح TODO في قاعدة التعليمات البرمجية", "prompt.example.2": "ما هو المكدس التقني لهذا المشروع؟", "prompt.example.3": "إصلاح الاختبارات المعطلة", @@ -207,10 +235,10 @@ export const dict = { "prompt.example.23": "إضافة ترقيم الصفحات إلى هذه القائمة", "prompt.example.24": "إنشاء أمر CLI لـ...", "prompt.example.25": "كيف تعمل متغيرات البيئة هنا؟", - "prompt.popover.emptyResults": "لا توجد نتائج مطابقة", "prompt.popover.emptyCommands": "لا توجد أوامر مطابقة", "prompt.dropzone.label": "أفلت الصور أو ملفات PDF هنا", + "prompt.dropzone.file.label": "أفلت لإشارة @ للملف", "prompt.slash.badge.custom": "مخصص", "prompt.slash.badge.skill": "مهارة", "prompt.slash.badge.mcp": "mcp", @@ -222,7 +250,6 @@ export const dict = { "prompt.attachment.remove": "إزالة المرفق", "prompt.action.send": "إرسال", "prompt.action.stop": "توقف", - "prompt.toast.pasteUnsupported.title": "لصق غير مدعوم", "prompt.toast.pasteUnsupported.description": "يمكن لصق الصور أو ملفات PDF فقط هنا.", "prompt.toast.modelAgentRequired.title": "حدد وكيلاً ونموذجاً", @@ -232,24 +259,19 @@ export const dict = { "prompt.toast.shellSendFailed.title": "فشل إرسال أمر shell", "prompt.toast.commandSendFailed.title": "فشل إرسال الأمر", "prompt.toast.promptSendFailed.title": "فشل إرسال الموجه", - + "prompt.toast.promptSendFailed.description": "تعذر استرداد الجلسة", "dialog.mcp.title": "MCPs", "dialog.mcp.description": "{{enabled}} من {{total}} مفعل", "dialog.mcp.empty": "لم يتم تكوين MCPs", - "dialog.lsp.empty": "تم الكشف تلقائيًا عن LSPs من أنواع الملفات", "dialog.plugins.empty": "الإضافات المكونة في opencode.json", - "mcp.status.connected": "متصل", "mcp.status.failed": "فشل", "mcp.status.needs_auth": "يحتاج إلى مصادقة", "mcp.status.disabled": "معطل", - "dialog.fork.empty": "لا توجد رسائل للتفرع منها", - "dialog.directory.search.placeholder": "البحث في المجلدات", "dialog.directory.empty": "لم يتم العثور على مجلدات", - "dialog.server.title": "الخوادم", "dialog.server.description": "تبديل خادم OpenCode الذي يتصل به هذا التطبيق.", "dialog.server.search.placeholder": "البحث في الخوادم", @@ -267,14 +289,12 @@ export const dict = { "dialog.server.default.set": "تعيين الخادم الحالي كافتراضي", "dialog.server.default.clear": "مسح", "dialog.server.action.remove": "إزالة الخادم", - "dialog.server.menu.edit": "تعديل", "dialog.server.menu.default": "تعيين كافتراضي", "dialog.server.menu.defaultRemove": "إزالة الافتراضي", "dialog.server.menu.delete": "حذف", "dialog.server.current": "الخادم الحالي", "dialog.server.status.default": "افتراضي", - "dialog.project.edit.title": "تحرير المشروع", "dialog.project.edit.name": "الاسم", "dialog.project.edit.icon": "أيقونة", @@ -283,7 +303,6 @@ export const dict = { "dialog.project.edit.icon.recommended": "موصى به: 128x128px", "dialog.project.edit.color": "لون", "dialog.project.edit.color.select": "اختر لون {{color}}", - "dialog.project.edit.worktree.startup": "سكريبت بدء تشغيل مساحة العمل", "dialog.project.edit.worktree.startup.description": "يتم تشغيله بعد إنشاء مساحة عمل جديدة (شجرة عمل).", "dialog.project.edit.worktree.startup.placeholder": "مثال: bun install", @@ -294,10 +313,8 @@ export const dict = { "context.breakdown.assistant": "المساعد", "context.breakdown.tool": "استدعاءات الأداة", "context.breakdown.other": "أخرى", - "context.systemPrompt.title": "موجه النظام", "context.rawMessages.title": "الرسائل الخام", - "context.stats.session": "جلسة", "context.stats.messages": "رسائل", "context.stats.provider": "موفر", @@ -314,34 +331,42 @@ export const dict = { "context.stats.totalCost": "التكلفة الإجمالية", "context.stats.sessionCreated": "تم إنشاء الجلسة", "context.stats.lastActivity": "آخر نشاط", - "context.usage.tokens": "رموز", "context.usage.usage": "استخدام", "context.usage.cost": "تكلفة", "context.usage.clickToView": "انقر لعرض السياق", "context.usage.view": "عرض استخدام السياق", - + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", "toast.language.title": "لغة", "toast.language.description": "تم التبديل إلى {{language}}", - "toast.theme.title": "تم تبديل السمة", "toast.scheme.title": "مخطط الألوان", - - "toast.permissions.autoaccept.on.title": "قبول التعديلات تلقائيًا", - "toast.permissions.autoaccept.on.description": "سيتم الموافقة تلقائيًا على أذونات التحرير والكتابة", - "toast.permissions.autoaccept.off.title": "توقف قبول التعديلات تلقائيًا", - "toast.permissions.autoaccept.off.description": "ستتطلب أذونات التحرير والكتابة موافقة", - "toast.workspace.enabled.title": "تم تمكين مساحات العمل", "toast.workspace.enabled.description": "الآن يتم عرض عدة worktrees في الشريط الجانبي", "toast.workspace.disabled.title": "تم تعطيل مساحات العمل", "toast.workspace.disabled.description": "يتم عرض worktree الرئيسي فقط في الشريط الجانبي", - + "toast.permissions.autoaccept.on.title": "قبول التعديلات تلقائيًا", + "toast.permissions.autoaccept.on.description": "سيتم الموافقة تلقائيًا على أذونات التحرير والكتابة", + "toast.permissions.autoaccept.off.title": "توقف قبول التعديلات تلقائيًا", + "toast.permissions.autoaccept.off.description": "ستتطلب أذونات التحرير والكتابة موافقة", "toast.model.none.title": "لم يتم تحديد نموذج", "toast.model.none.description": "قم بتوصيل موفر لتلخيص هذه الجلسة", - "toast.file.loadFailed.title": "فشل تحميل الملف", - "toast.file.listFailed.title": "فشل سرد الملفات", "toast.context.noLineSelection.title": "لا يوجد تحديد للأسطر", "toast.context.noLineSelection.description": "حدد نطاق أسطر في تبويب ملف أولاً.", @@ -350,19 +375,15 @@ export const dict = { "toast.session.share.success.description": "تم نسخ عنوان URL للمشاركة إلى الحافظة!", "toast.session.share.failed.title": "فشل مشاركة الجلسة", "toast.session.share.failed.description": "حدث خطأ أثناء مشاركة الجلسة", - "toast.session.unshare.success.title": "تم إلغاء مشاركة الجلسة", "toast.session.unshare.success.description": "تم إلغاء مشاركة الجلسة بنجاح!", "toast.session.unshare.failed.title": "فشل إلغاء مشاركة الجلسة", "toast.session.unshare.failed.description": "حدث خطأ أثناء إلغاء مشاركة الجلسة", - "toast.session.listFailed.title": "فشل تحميل الجلسات لـ {{project}}", - "toast.update.title": "تحديث متاح", "toast.update.description": "نسخة جديدة من OpenCode ({{version}}) متاحة الآن للتثبيت.", "toast.update.action.installRestart": "تثبيت وإعادة تشغيل", "toast.update.action.notYet": "ليس الآن", - "error.page.title": "حدث خطأ ما", "error.page.description": "حدث خطأ أثناء تحميل التطبيق.", "error.page.details.label": "تفاصيل الخطأ", @@ -373,12 +394,10 @@ export const dict = { "error.page.report.prefix": "يرجى الإبلاغ عن هذا الخطأ لفريق OpenCode", "error.page.report.discord": "على Discord", "error.page.version": "الإصدار: {{version}}", - "error.dev.rootNotFound": "لم يتم العثور على العنصر الجذري. هل نسيت إضافته إلى index.html؟ أو ربما تمت كتابة سمة id بشكل خاطئ؟", - "error.globalSync.connectFailed": "تعذر الاتصال بالخادم. هل هناك خادم يعمل في `{{url}}`؟", - + "directory.error.invalidUrl": "دليل غير صالح في عنوان URL.", "error.chain.unknown": "خطأ غير معروف", "error.chain.causedBy": "بسبب:", "error.chain.apiError": "خطأ API", @@ -398,21 +417,17 @@ export const dict = { "error.chain.configFrontmatterError": "فشل تحليل frontmatter في {{path}}:\n{{message}}", "error.chain.configInvalid": "ملف التكوين في {{path}} غير صالح", "error.chain.configInvalidWithMessage": "ملف التكوين في {{path}} غير صالح: {{message}}", - "notification.permission.title": "مطلوب إذن", "notification.permission.description": "{{sessionTitle}} في {{projectName}} يحتاج إلى إذن", "notification.question.title": "سؤال", "notification.question.description": "{{sessionTitle}} في {{projectName}} لديه سؤال", "notification.action.goToSession": "انتقل إلى الجلسة", - "notification.session.responseReady.title": "الاستجابة جاهزة", "notification.session.error.title": "خطأ في الجلسة", "notification.session.error.fallbackDescription": "حدث خطأ", - "home.recentProjects": "المشاريع الحديثة", "home.empty.title": "لا توجد مشاريع حديثة", "home.empty.description": "ابدأ بفتح مشروع محلي", - "session.tab.session": "جلسة", "session.tab.review": "مراجعة", "session.tab.context": "سياق", @@ -431,17 +446,18 @@ export const dict = { "session.messages.loadEarlier": "تحميل الرسائل السابقة", "session.messages.loading": "جارٍ تحميل الرسائل...", "session.messages.jumpToLatest": "الانتقال إلى الأحدث", - "session.context.addToContext": "إضافة {{selection}} إلى السياق", - "session.new.worktree.main": "الفرع الرئيسي", "session.new.worktree.mainWithBranch": "الفرع الرئيسي ({{branch}})", "session.new.worktree.create": "إنشاء شجرة عمل جديدة", "session.new.lastModified": "آخر تعديل", - "session.header.search.placeholder": "بحث {{project}}", "session.header.searchFiles": "بحث عن الملفات", - + "session.header.openIn": "فتح في", + "session.header.open.action": "فتح {{app}}", + "session.header.open.ariaLabel": "فتح في {{app}}", + "session.header.open.menu": "خيارات الفتح", + "session.header.open.copyPath": "نسخ المسار", "status.popover.trigger": "الحالة", "status.popover.ariaLabel": "إعدادات الخوادم", "status.popover.tab.servers": "الخوادم", @@ -449,7 +465,6 @@ export const dict = { "status.popover.tab.lsp": "LSP", "status.popover.tab.plugins": "الإضافات", "status.popover.action.manageServers": "إدارة الخوادم", - "session.share.popover.title": "نشر على الويب", "session.share.popover.description.shared": "هذه الجلسة عامة على الويب. يمكن لأي شخص لديه الرابط الوصول إليها.", "session.share.popover.description.unshared": "شارك الجلسة علنًا على الويب. ستكون متاحة لأي شخص لديه الرابط.", @@ -461,10 +476,8 @@ export const dict = { "session.share.action.view": "عرض", "session.share.copy.copied": "تم النسخ", "session.share.copy.copyLink": "نسخ الرابط", - "lsp.tooltip.none": "لا توجد خوادم LSP", "lsp.label.connected": "{{count}} LSP", - "prompt.loading": "جارٍ تحميل الموجه...", "terminal.loading": "جارٍ تحميل المحطة الطرفية...", "terminal.title": "محطة طرفية", @@ -472,7 +485,6 @@ export const dict = { "terminal.close": "إغلاق المحطة الطرفية", "terminal.connectionLost.title": "فقد الاتصال", "terminal.connectionLost.description": "انقطع اتصال المحطة الطرفية. يمكن أن يحدث هذا عند إعادة تشغيل الخادم.", - "common.closeTab": "إغلاق علامة التبويب", "common.dismiss": "رفض", "common.requestFailed": "فشل الطلب", @@ -486,7 +498,6 @@ export const dict = { "common.edit": "تحرير", "common.loadMore": "تحميل المزيد", "common.key.esc": "ESC", - "sidebar.menu.toggle": "تبديل القائمة", "sidebar.nav.projectsAndSessions": "المشاريع والجلسات", "sidebar.settings": "الإعدادات", @@ -498,18 +509,20 @@ export const dict = { "sidebar.gettingStarted.line2": "قم بتوصيل أي موفر لاستخدام النماذج، بما في ذلك Claude و GPT و Gemini وما إلى ذلك.", "sidebar.project.recentSessions": "الجلسات الحديثة", "sidebar.project.viewAllSessions": "عرض جميع الجلسات", - + "sidebar.project.clearNotifications": "مسح الإشعارات", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "سطح المكتب", "settings.section.server": "الخادم", "settings.tab.general": "عام", "settings.tab.shortcuts": "اختصارات", - + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "تكامل WSL", + "settings.desktop.wsl.description": "تشغيل خادم OpenCode داخل WSL على Windows.", "settings.general.section.appearance": "المظهر", "settings.general.section.notifications": "إشعارات النظام", "settings.general.section.updates": "التحديثات", "settings.general.section.sounds": "المؤثرات الصوتية", - + "settings.general.section.display": "شاشة العرض", "settings.general.row.language.title": "اللغة", "settings.general.row.language.description": "تغيير لغة العرض لـ OpenCode", "settings.general.row.appearance.title": "المظهر", @@ -518,10 +531,12 @@ export const dict = { "settings.general.row.theme.description": "تخصيص سمة OpenCode.", "settings.general.row.font.title": "الخط", "settings.general.row.font.description": "تخصيص الخط الأحادي المستخدم في كتل التعليمات البرمجية", - + "settings.general.row.wayland.title": "استخدام Wayland الأصلي", + "settings.general.row.wayland.description": "تعطيل التراجع إلى X11 على Wayland. يتطلب إعادة التشغيل.", + "settings.general.row.wayland.tooltip": + "على Linux مع شاشات بمعدلات تحديث مختلطة، يمكن أن يكون Wayland الأصلي أكثر استقرارًا.", "settings.general.row.releaseNotes.title": "ملاحظات الإصدار", "settings.general.row.releaseNotes.description": 'عرض نوافذ "ما الجديد" المنبثقة بعد التحديثات', - "settings.updates.row.startup.title": "التحقق من التحديثات عند بدء التشغيل", "settings.updates.row.startup.description": "التحقق تلقائيًا من التحديثات عند تشغيل OpenCode", "settings.updates.row.check.title": "التحقق من التحديثات", @@ -542,6 +557,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "تنبيه 01", "sound.option.alert02": "تنبيه 02", "sound.option.alert03": "تنبيه 03", @@ -587,21 +603,18 @@ export const dict = { "sound.option.yup04": "نعم 04", "sound.option.yup05": "نعم 05", "sound.option.yup06": "نعم 06", - "settings.general.notifications.agent.title": "وكيل", "settings.general.notifications.agent.description": "عرض إشعار النظام عندما يكتمل الوكيل أو يحتاج إلى اهتمام", "settings.general.notifications.permissions.title": "أذونات", "settings.general.notifications.permissions.description": "عرض إشعار النظام عند الحاجة إلى إذن", "settings.general.notifications.errors.title": "أخطاء", "settings.general.notifications.errors.description": "عرض إشعار النظام عند حدوث خطأ", - "settings.general.sounds.agent.title": "وكيل", "settings.general.sounds.agent.description": "تشغيل صوت عندما يكتمل الوكيل أو يحتاج إلى اهتمام", "settings.general.sounds.permissions.title": "أذونات", "settings.general.sounds.permissions.description": "تشغيل صوت عند الحاجة إلى إذن", "settings.general.sounds.errors.title": "أخطاء", "settings.general.sounds.errors.description": "تشغيل صوت عند حدوث خطأ", - "settings.shortcuts.title": "اختصارات لوحة المفاتيح", "settings.shortcuts.reset.button": "إعادة التعيين إلى الافتراضيات", "settings.shortcuts.reset.toast.title": "تم إعادة تعيين الاختصارات", @@ -612,14 +625,12 @@ export const dict = { "settings.shortcuts.pressKeys": "اضغط على المفاتيح", "settings.shortcuts.search.placeholder": "البحث في الاختصارات", "settings.shortcuts.search.empty": "لم يتم العثور على اختصارات", - "settings.shortcuts.group.general": "عام", "settings.shortcuts.group.session": "جلسة", "settings.shortcuts.group.navigation": "تصفح", "settings.shortcuts.group.modelAndAgent": "النموذج والوكيل", "settings.shortcuts.group.terminal": "المحطة الطرفية", "settings.shortcuts.group.prompt": "موجه", - "settings.providers.title": "الموفرون", "settings.providers.description": "ستكون إعدادات الموفر قابلة للتكوين هنا.", "settings.providers.section.connected": "الموفرون المتصلون", @@ -637,16 +648,13 @@ export const dict = { "settings.commands.description": "ستكون إعدادات الأمر قابلة للتكوين هنا.", "settings.mcp.title": "MCP", "settings.mcp.description": "ستكون إعدادات MCP قابلة للتكوين هنا.", - "settings.permissions.title": "الأذونات", "settings.permissions.description": "تحكم في الأدوات التي يمكن للخادم استخدامها بشكل افتراضي.", "settings.permissions.section.tools": "الأدوات", "settings.permissions.toast.updateFailed.title": "فشل تحديث الأذونات", - "settings.permissions.action.allow": "سماح", "settings.permissions.action.ask": "سؤال", "settings.permissions.action.deny": "رفض", - "settings.permissions.tool.read.title": "قراءة", "settings.permissions.tool.read.description": "قراءة ملف (يطابق مسار الملف)", "settings.permissions.tool.edit.title": "تحرير", @@ -660,9 +668,9 @@ export const dict = { "settings.permissions.tool.list.description": "سرد الملفات داخل دليل", "settings.permissions.tool.bash.title": "Bash", "settings.permissions.tool.bash.description": "تشغيل أوامر shell", - "settings.permissions.tool.task.title": "Task", + "settings.permissions.tool.task.title": "مهمة", "settings.permissions.tool.task.description": "تشغيل الوكلاء الفرعيين", - "settings.permissions.tool.skill.title": "Skill", + "settings.permissions.tool.skill.title": "مهارة", "settings.permissions.tool.skill.description": "تحميل مهارة بالاسم", "settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.description": "تشغيل استعلامات خادم اللغة", @@ -680,12 +688,10 @@ export const dict = { "settings.permissions.tool.external_directory.description": "الوصول إلى الملفات خارج دليل المشروع", "settings.permissions.tool.doom_loop.title": "حلقة الموت", "settings.permissions.tool.doom_loop.description": "اكتشاف استدعاءات الأدوات المتكررة بمدخلات متطابقة", - "session.delete.failed.title": "فشل حذف الجلسة", "session.delete.title": "حذف الجلسة", "session.delete.confirm": 'حذف الجلسة "{{name}}"؟', "session.delete.button": "حذف الجلسة", - "workspace.new": "مساحة عمل جديدة", "workspace.type.local": "محلي", "workspace.type.sandbox": "صندوق رمل", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index a743a3d89691..730c01fdfffb 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -16,11 +16,9 @@ export const dict = { "command.category.permissions": "Permissões", "command.category.workspace": "Espaço de trabalho", "command.category.settings": "Configurações", - "theme.scheme.system": "Sistema", "theme.scheme.light": "Claro", "theme.scheme.dark": "Escuro", - "command.sidebar.toggle": "Alternar barra lateral", "command.project.open": "Abrir projeto", "command.provider.connect": "Conectar provedor", @@ -31,21 +29,19 @@ export const dict = { "command.session.previous.unseen": "Sessão não lida anterior", "command.session.next.unseen": "Próxima sessão não lida", "command.session.archive": "Arquivar sessão", - "command.palette": "Paleta de comandos", - "command.theme.cycle": "Alternar tema", "command.theme.set": "Usar tema: {{theme}}", "command.theme.scheme.cycle": "Alternar esquema de cores", "command.theme.scheme.set": "Usar esquema de cores: {{scheme}}", - "command.language.cycle": "Alternar idioma", "command.language.set": "Usar idioma: {{language}}", - "command.session.new": "Nova sessão", "command.file.open": "Abrir arquivo", + "command.tab.close": "Fechar aba", "command.context.addSelection": "Adicionar seleção ao contexto", "command.context.addSelection.description": "Adicionar as linhas selecionadas do arquivo atual", + "command.input.focus": "Focar entrada", "command.terminal.toggle": "Alternar terminal", "command.fileTree.toggle": "Alternar árvore de arquivos", "command.review.toggle": "Alternar revisão", @@ -70,6 +66,7 @@ export const dict = { "command.permissions.autoaccept.enable": "Aceitar edições automaticamente", "command.permissions.autoaccept.disable": "Parar de aceitar edições automaticamente", "command.workspace.toggle": "Alternar espaços de trabalho", + "command.workspace.toggle.description": "Habilitar ou desabilitar múltiplos espaços de trabalho na barra lateral", "command.session.undo": "Desfazer", "command.session.undo.description": "Desfazer a última mensagem", "command.session.redo": "Refazer", @@ -82,32 +79,30 @@ export const dict = { "command.session.share.description": "Compartilhar esta sessão e copiar a URL para a área de transferência", "command.session.unshare": "Parar de compartilhar sessão", "command.session.unshare.description": "Parar de compartilhar esta sessão", - "palette.search.placeholder": "Buscar arquivos, comandos e sessões", "palette.empty": "Nenhum resultado encontrado", "palette.group.commands": "Comandos", "palette.group.files": "Arquivos", - "dialog.provider.search.placeholder": "Buscar provedores", "dialog.provider.empty": "Nenhum provedor encontrado", "dialog.provider.group.popular": "Popular", "dialog.provider.group.other": "Outro", "dialog.provider.tag.recommended": "Recomendado", + "dialog.provider.opencode.note": "Modelos selecionados incluindo Claude, GPT, Gemini e mais", "dialog.provider.anthropic.note": "Conectar com Claude Pro/Max ou chave de API", - "dialog.provider.openai.note": "Conectar com ChatGPT Pro/Plus ou chave de API", "dialog.provider.copilot.note": "Conectar com Copilot ou chave de API", - + "dialog.provider.openai.note": "Conectar com ChatGPT Pro/Plus ou chave de API", + "dialog.provider.google.note": "Modelos Gemini para respostas rápidas e estruturadas", + "dialog.provider.openrouter.note": "Acesse todos os modelos suportados de um único provedor", + "dialog.provider.vercel.note": "Acesso unificado a modelos de IA com roteamento inteligente", "dialog.model.select.title": "Selecionar modelo", "dialog.model.search.placeholder": "Buscar modelos", "dialog.model.empty": "Nenhum resultado de modelo", "dialog.model.manage": "Gerenciar modelos", "dialog.model.manage.description": "Personalizar quais modelos aparecem no seletor de modelos.", - "dialog.model.unpaid.freeModels.title": "Modelos gratuitos fornecidos pelo OpenCode", "dialog.model.unpaid.addMore.title": "Adicionar mais modelos de provedores populares", - "dialog.provider.viewAll": "Ver mais provedores", - "provider.connect.title": "Conectar {{provider}}", "provider.connect.title.anthropicProMax": "Entrar com Claude Pro/Max", "provider.connect.selectMethod": "Selecionar método de login para {{provider}}.", @@ -142,7 +137,42 @@ export const dict = { "provider.connect.oauth.auto.confirmationCode": "Código de confirmação", "provider.connect.toast.connected.title": "{{provider}} conectado", "provider.connect.toast.connected.description": "Modelos do {{provider}} agora estão disponíveis para uso.", - + "provider.custom.title": "Provedor personalizado", + "provider.custom.description.prefix": "Configure um provedor compatível com OpenAI. Veja a ", + "provider.custom.description.link": "documentação de configuração do provedor", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ID do Provedor", + "provider.custom.field.providerID.placeholder": "meuprovedor", + "provider.custom.field.providerID.description": "Letras minúsculas, números, hifens ou sublinhados", + "provider.custom.field.name.label": "Nome de exibição", + "provider.custom.field.name.placeholder": "Meu Provedor de IA", + "provider.custom.field.baseURL.label": "URL Base", + "provider.custom.field.baseURL.placeholder": "https://api.meuprovedor.com/v1", + "provider.custom.field.apiKey.label": "Chave de API", + "provider.custom.field.apiKey.placeholder": "Chave de API", + "provider.custom.field.apiKey.description": "Opcional. Deixe em branco se gerenciar autenticação via cabeçalhos.", + "provider.custom.models.label": "Modelos", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "id-do-modelo", + "provider.custom.models.name.label": "Nome", + "provider.custom.models.name.placeholder": "Nome de Exibição", + "provider.custom.models.remove": "Remover modelo", + "provider.custom.models.add": "Adicionar modelo", + "provider.custom.headers.label": "Cabeçalhos (opcional)", + "provider.custom.headers.key.label": "Cabeçalho", + "provider.custom.headers.key.placeholder": "Nome-Do-Cabeçalho", + "provider.custom.headers.value.label": "Valor", + "provider.custom.headers.value.placeholder": "valor", + "provider.custom.headers.remove": "Remover cabeçalho", + "provider.custom.headers.add": "Adicionar cabeçalho", + "provider.custom.error.providerID.required": "ID do Provedor é obrigatório", + "provider.custom.error.providerID.format": "Use letras minúsculas, números, hifens ou sublinhados", + "provider.custom.error.providerID.exists": "Esse ID de provedor já existe", + "provider.custom.error.name.required": "Nome de exibição é obrigatório", + "provider.custom.error.baseURL.required": "URL Base é obrigatória", + "provider.custom.error.baseURL.format": "Deve começar com http:// ou https://", + "provider.custom.error.required": "Obrigatório", + "provider.custom.error.duplicate": "Duplicado", "provider.disconnect.toast.disconnected.title": "{{provider}} desconectado", "provider.disconnect.toast.disconnected.description": "Os modelos de {{provider}} não estão mais disponíveis.", "model.tag.free": "Grátis", @@ -161,9 +191,9 @@ export const dict = { "model.tooltip.reasoning.allowed": "Permite raciocínio", "model.tooltip.reasoning.none": "Sem raciocínio", "model.tooltip.context": "Limite de contexto {{limit}}", - "common.search.placeholder": "Buscar", "common.goBack": "Voltar", + "common.goForward": "Avançar", "common.loading": "Carregando", "common.loading.ellipsis": "...", "common.cancel": "Cancelar", @@ -174,14 +204,12 @@ export const dict = { "common.saving": "Salvando...", "common.default": "Padrão", "common.attachment": "anexo", - "prompt.placeholder.shell": "Digite comando do shell...", "prompt.placeholder.normal": 'Pergunte qualquer coisa... "{{example}}"', "prompt.placeholder.summarizeComments": "Resumir comentários…", "prompt.placeholder.summarizeComment": "Resumir comentário…", "prompt.mode.shell": "Shell", "prompt.mode.shell.exit": "esc para sair", - "prompt.example.1": "Corrigir um TODO no código", "prompt.example.2": "Qual é a stack tecnológica deste projeto?", "prompt.example.3": "Corrigir testes quebrados", @@ -207,10 +235,10 @@ export const dict = { "prompt.example.23": "Adicionar paginação a esta lista", "prompt.example.24": "Criar um comando CLI para...", "prompt.example.25": "Como funcionam as variáveis de ambiente aqui?", - "prompt.popover.emptyResults": "Nenhum resultado correspondente", "prompt.popover.emptyCommands": "Nenhum comando correspondente", "prompt.dropzone.label": "Solte imagens ou PDFs aqui", + "prompt.dropzone.file.label": "Solte para @mencionar arquivo", "prompt.slash.badge.custom": "personalizado", "prompt.slash.badge.skill": "skill", "prompt.slash.badge.mcp": "mcp", @@ -222,7 +250,6 @@ export const dict = { "prompt.attachment.remove": "Remover anexo", "prompt.action.send": "Enviar", "prompt.action.stop": "Parar", - "prompt.toast.pasteUnsupported.title": "Colagem não suportada", "prompt.toast.pasteUnsupported.description": "Somente imagens ou PDFs podem ser colados aqui.", "prompt.toast.modelAgentRequired.title": "Selecione um agente e modelo", @@ -232,23 +259,19 @@ export const dict = { "prompt.toast.shellSendFailed.title": "Falha ao enviar comando shell", "prompt.toast.commandSendFailed.title": "Falha ao enviar comando", "prompt.toast.promptSendFailed.title": "Falha ao enviar prompt", - + "prompt.toast.promptSendFailed.description": "Não foi possível recuperar a sessão", "dialog.mcp.title": "MCPs", - "dialog.mcp.description": "{{enabled}} de {{total}} habilitados", + "dialog.mcp.description": "{{enabled}} of {{total}} habilitados", "dialog.mcp.empty": "Nenhum MCP configurado", - "dialog.lsp.empty": "LSPs detectados automaticamente pelos tipos de arquivo", "dialog.plugins.empty": "Plugins configurados em opencode.json", "mcp.status.connected": "conectado", "mcp.status.failed": "falhou", "mcp.status.needs_auth": "precisa de autenticação", "mcp.status.disabled": "desabilitado", - "dialog.fork.empty": "Nenhuma mensagem para bifurcar", - "dialog.directory.search.placeholder": "Buscar pastas", "dialog.directory.empty": "Nenhuma pasta encontrada", - "dialog.server.title": "Servidores", "dialog.server.description": "Trocar para qual servidor OpenCode este aplicativo se conecta.", "dialog.server.search.placeholder": "Buscar servidores", @@ -266,7 +289,6 @@ export const dict = { "dialog.server.default.set": "Definir servidor atual como padrão", "dialog.server.default.clear": "Limpar", "dialog.server.action.remove": "Remover servidor", - "dialog.server.menu.edit": "Editar", "dialog.server.menu.default": "Definir como padrão", "dialog.server.menu.defaultRemove": "Remover padrão", @@ -284,7 +306,6 @@ export const dict = { "dialog.project.edit.worktree.startup": "Script de inicialização do espaço de trabalho", "dialog.project.edit.worktree.startup.description": "Executa após criar um novo espaço de trabalho (worktree).", "dialog.project.edit.worktree.startup.placeholder": "ex: bun install", - "context.breakdown.title": "Detalhamento do Contexto", "context.breakdown.note": 'Detalhamento aproximado dos tokens de entrada. "Outros" inclui definições de ferramentas e overhead.', @@ -293,10 +314,8 @@ export const dict = { "context.breakdown.assistant": "Assistente", "context.breakdown.tool": "Chamadas de Ferramentas", "context.breakdown.other": "Outros", - "context.systemPrompt.title": "Prompt do Sistema", "context.rawMessages.title": "Mensagens brutas", - "context.stats.session": "Sessão", "context.stats.messages": "Mensagens", "context.stats.provider": "Provedor", @@ -313,34 +332,42 @@ export const dict = { "context.stats.totalCost": "Custo Total", "context.stats.sessionCreated": "Sessão Criada", "context.stats.lastActivity": "Última Atividade", - "context.usage.tokens": "Tokens", "context.usage.usage": "Uso", "context.usage.cost": "Custo", "context.usage.clickToView": "Clique para ver o contexto", "context.usage.view": "Ver uso do contexto", - + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", "toast.language.title": "Idioma", "toast.language.description": "Alterado para {{language}}", - "toast.theme.title": "Tema alterado", "toast.scheme.title": "Esquema de cores", - - "toast.permissions.autoaccept.on.title": "Aceitando edições automaticamente", - "toast.permissions.autoaccept.on.description": "Permissões de edição e escrita serão aprovadas automaticamente", - "toast.permissions.autoaccept.off.title": "Parou de aceitar edições automaticamente", - "toast.permissions.autoaccept.off.description": "Permissões de edição e escrita exigirão aprovação", - "toast.workspace.enabled.title": "Espaços de trabalho ativados", "toast.workspace.enabled.description": "Várias worktrees agora são exibidas na barra lateral", "toast.workspace.disabled.title": "Espaços de trabalho desativados", "toast.workspace.disabled.description": "Apenas a worktree principal é exibida na barra lateral", - + "toast.permissions.autoaccept.on.title": "Aceitando edições automaticamente", + "toast.permissions.autoaccept.on.description": "Permissões de edição e escrita serão aprovadas automaticamente", + "toast.permissions.autoaccept.off.title": "Parou de aceitar edições automaticamente", + "toast.permissions.autoaccept.off.description": "Permissões de edição e escrita exigirão aprovação", "toast.model.none.title": "Nenhum modelo selecionado", "toast.model.none.description": "Conecte um provedor para resumir esta sessão", - "toast.file.loadFailed.title": "Falha ao carregar arquivo", - "toast.file.listFailed.title": "Falha ao listar arquivos", "toast.context.noLineSelection.title": "Nenhuma seleção de linhas", "toast.context.noLineSelection.description": "Selecione primeiro um intervalo de linhas em uma aba de arquivo.", @@ -349,19 +376,15 @@ export const dict = { "toast.session.share.success.description": "URL compartilhada copiada para a área de transferência!", "toast.session.share.failed.title": "Falha ao compartilhar sessão", "toast.session.share.failed.description": "Ocorreu um erro ao compartilhar a sessão", - "toast.session.unshare.success.title": "Sessão não compartilhada", "toast.session.unshare.success.description": "Sessão deixou de ser compartilhada com sucesso!", "toast.session.unshare.failed.title": "Falha ao parar de compartilhar sessão", "toast.session.unshare.failed.description": "Ocorreu um erro ao parar de compartilhar a sessão", - "toast.session.listFailed.title": "Falha ao carregar sessões para {{project}}", - "toast.update.title": "Atualização disponível", "toast.update.description": "Uma nova versão do OpenCode ({{version}}) está disponível para instalação.", "toast.update.action.installRestart": "Instalar e reiniciar", "toast.update.action.notYet": "Agora não", - "error.page.title": "Algo deu errado", "error.page.description": "Ocorreu um erro ao carregar a aplicação.", "error.page.details.label": "Detalhes do Erro", @@ -372,12 +395,10 @@ export const dict = { "error.page.report.prefix": "Por favor, reporte este erro para a equipe do OpenCode", "error.page.report.discord": "no Discord", "error.page.version": "Versão: {{version}}", - "error.dev.rootNotFound": "Elemento raiz não encontrado. Você esqueceu de adicioná-lo ao seu index.html? Ou talvez o atributo id foi escrito incorretamente?", - "error.globalSync.connectFailed": "Não foi possível conectar ao servidor. Há um servidor executando em `{{url}}`?", - + "directory.error.invalidUrl": "Diretório inválido na URL.", "error.chain.unknown": "Erro desconhecido", "error.chain.causedBy": "Causado por:", "error.chain.apiError": "Erro de API", @@ -399,21 +420,17 @@ export const dict = { "error.chain.configFrontmatterError": "Falha ao analisar frontmatter em {{path}}:\n{{message}}", "error.chain.configInvalid": "Arquivo de configuração em {{path}} é inválido", "error.chain.configInvalidWithMessage": "Arquivo de configuração em {{path}} é inválido: {{message}}", - "notification.permission.title": "Permissão necessária", "notification.permission.description": "{{sessionTitle}} em {{projectName}} precisa de permissão", "notification.question.title": "Pergunta", "notification.question.description": "{{sessionTitle}} em {{projectName}} tem uma pergunta", "notification.action.goToSession": "Ir para sessão", - "notification.session.responseReady.title": "Resposta pronta", "notification.session.error.title": "Erro na sessão", "notification.session.error.fallbackDescription": "Ocorreu um erro", - "home.recentProjects": "Projetos recentes", "home.empty.title": "Nenhum projeto recente", "home.empty.description": "Comece abrindo um projeto local", - "session.tab.session": "Sessão", "session.tab.review": "Revisão", "session.tab.context": "Contexto", @@ -432,17 +449,18 @@ export const dict = { "session.messages.loadEarlier": "Carregar mensagens anteriores", "session.messages.loading": "Carregando mensagens...", "session.messages.jumpToLatest": "Ir para a mais recente", - "session.context.addToContext": "Adicionar {{selection}} ao contexto", - "session.new.worktree.main": "Branch principal", "session.new.worktree.mainWithBranch": "Branch principal ({{branch}})", "session.new.worktree.create": "Criar novo worktree", "session.new.lastModified": "Última modificação", - "session.header.search.placeholder": "Buscar {{project}}", "session.header.searchFiles": "Buscar arquivos", - + "session.header.openIn": "Abrir em", + "session.header.open.action": "Abrir {{app}}", + "session.header.open.ariaLabel": "Abrir em {{app}}", + "session.header.open.menu": "Opções de abertura", + "session.header.open.copyPath": "Copiar caminho", "status.popover.trigger": "Status", "status.popover.ariaLabel": "Configurações de servidores", "status.popover.tab.servers": "Servidores", @@ -450,7 +468,6 @@ export const dict = { "status.popover.tab.lsp": "LSP", "status.popover.tab.plugins": "Plugins", "status.popover.action.manageServers": "Gerenciar servidores", - "session.share.popover.title": "Publicar na web", "session.share.popover.description.shared": "Esta sessão é pública na web. Está acessível para qualquer pessoa com o link.", @@ -464,10 +481,8 @@ export const dict = { "session.share.action.view": "Ver", "session.share.copy.copied": "Copiado", "session.share.copy.copyLink": "Copiar link", - "lsp.tooltip.none": "Nenhum servidor LSP", "lsp.label.connected": "{{count}} LSP", - "prompt.loading": "Carregando prompt...", "terminal.loading": "Carregando terminal...", "terminal.title": "Terminal", @@ -476,7 +491,6 @@ export const dict = { "terminal.connectionLost.title": "Conexão Perdida", "terminal.connectionLost.description": "A conexão do terminal foi interrompida. Isso pode acontecer quando o servidor reinicia.", - "common.closeTab": "Fechar aba", "common.dismiss": "Descartar", "common.requestFailed": "Requisição falhou", @@ -490,7 +504,6 @@ export const dict = { "common.edit": "Editar", "common.loadMore": "Carregar mais", "common.key.esc": "ESC", - "sidebar.menu.toggle": "Alternar menu", "sidebar.nav.projectsAndSessions": "Projetos e sessões", "sidebar.settings": "Configurações", @@ -502,18 +515,20 @@ export const dict = { "sidebar.gettingStarted.line2": "Conecte qualquer provedor para usar modelos, incluindo Claude, GPT, Gemini etc.", "sidebar.project.recentSessions": "Sessões recentes", "sidebar.project.viewAllSessions": "Ver todas as sessões", - + "sidebar.project.clearNotifications": "Limpar notificações", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Desktop", "settings.section.server": "Servidor", "settings.tab.general": "Geral", "settings.tab.shortcuts": "Atalhos", - + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Executar o servidor OpenCode dentro do WSL no Windows.", "settings.general.section.appearance": "Aparência", "settings.general.section.notifications": "Notificações do sistema", "settings.general.section.updates": "Atualizações", "settings.general.section.sounds": "Efeitos sonoros", - + "settings.general.section.display": "Tela", "settings.general.row.language.title": "Idioma", "settings.general.row.language.description": "Alterar o idioma de exibição do OpenCode", "settings.general.row.appearance.title": "Aparência", @@ -522,10 +537,12 @@ export const dict = { "settings.general.row.theme.description": "Personalize como o OpenCode é tematizado.", "settings.general.row.font.title": "Fonte", "settings.general.row.font.description": "Personalize a fonte monoespaçada usada em blocos de código", - + "settings.general.row.wayland.title": "Usar Wayland nativo", + "settings.general.row.wayland.description": "Desabilitar fallback X11 no Wayland. Requer reinicialização.", + "settings.general.row.wayland.tooltip": + "No Linux com monitores de taxas de atualização mistas, Wayland nativo pode ser mais estável.", "settings.general.row.releaseNotes.title": "Notas da versão", "settings.general.row.releaseNotes.description": 'Mostrar pop-ups de "Novidades" após atualizações', - "settings.updates.row.startup.title": "Verificar atualizações ao iniciar", "settings.updates.row.startup.description": "Verificar atualizações automaticamente quando o OpenCode iniciar", "settings.updates.row.check.title": "Verificar atualizações", @@ -546,6 +563,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Alerta 01", "sound.option.alert02": "Alerta 02", "sound.option.alert03": "Alerta 03", @@ -591,7 +609,6 @@ export const dict = { "sound.option.yup04": "Sim 04", "sound.option.yup05": "Sim 05", "sound.option.yup06": "Sim 06", - "settings.general.notifications.agent.title": "Agente", "settings.general.notifications.agent.description": "Mostrar notificação do sistema quando o agente estiver completo ou precisar de atenção", @@ -600,14 +617,12 @@ export const dict = { "Mostrar notificação do sistema quando uma permissão for necessária", "settings.general.notifications.errors.title": "Erros", "settings.general.notifications.errors.description": "Mostrar notificação do sistema quando ocorrer um erro", - "settings.general.sounds.agent.title": "Agente", "settings.general.sounds.agent.description": "Reproduzir som quando o agente estiver completo ou precisar de atenção", "settings.general.sounds.permissions.title": "Permissões", "settings.general.sounds.permissions.description": "Reproduzir som quando uma permissão for necessária", "settings.general.sounds.errors.title": "Erros", "settings.general.sounds.errors.description": "Reproduzir som quando ocorrer um erro", - "settings.shortcuts.title": "Atalhos de teclado", "settings.shortcuts.reset.button": "Redefinir para padrões", "settings.shortcuts.reset.toast.title": "Atalhos redefinidos", @@ -618,14 +633,12 @@ export const dict = { "settings.shortcuts.pressKeys": "Pressione teclas", "settings.shortcuts.search.placeholder": "Buscar atalhos", "settings.shortcuts.search.empty": "Nenhum atalho encontrado", - "settings.shortcuts.group.general": "Geral", "settings.shortcuts.group.session": "Sessão", "settings.shortcuts.group.navigation": "Navegação", "settings.shortcuts.group.modelAndAgent": "Modelo e agente", "settings.shortcuts.group.terminal": "Terminal", "settings.shortcuts.group.prompt": "Prompt", - "settings.providers.title": "Provedores", "settings.providers.description": "Configurações de provedores estarão disponíveis aqui.", "settings.providers.section.connected": "Provedores conectados", @@ -643,16 +656,13 @@ export const dict = { "settings.commands.description": "Configurações de comandos estarão disponíveis aqui.", "settings.mcp.title": "MCP", "settings.mcp.description": "Configurações de MCP estarão disponíveis aqui.", - "settings.permissions.title": "Permissões", "settings.permissions.description": "Controle quais ferramentas o servidor pode usar por padrão.", "settings.permissions.section.tools": "Ferramentas", "settings.permissions.toast.updateFailed.title": "Falha ao atualizar permissões", - "settings.permissions.action.allow": "Permitir", "settings.permissions.action.ask": "Perguntar", "settings.permissions.action.deny": "Negar", - "settings.permissions.tool.read.title": "Ler", "settings.permissions.tool.read.description": "Ler um arquivo (corresponde ao caminho do arquivo)", "settings.permissions.tool.edit.title": "Editar", @@ -686,12 +696,10 @@ export const dict = { "settings.permissions.tool.external_directory.description": "Acessar arquivos fora do diretório do projeto", "settings.permissions.tool.doom_loop.title": "Loop Infinito", "settings.permissions.tool.doom_loop.description": "Detectar chamadas de ferramentas repetidas com entrada idêntica", - "session.delete.failed.title": "Falha ao excluir sessão", "session.delete.title": "Excluir sessão", "session.delete.confirm": 'Excluir sessão "{{name}}"?', "session.delete.button": "Excluir sessão", - "workspace.new": "Novo espaço de trabalho", "workspace.type.local": "local", "workspace.type.sandbox": "sandbox", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index ce37989c259b..d53c261126b7 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -47,6 +47,7 @@ export const dict = { "command.tab.close": "Zatvori karticu", "command.context.addSelection": "Dodaj odabir u kontekst", "command.context.addSelection.description": "Dodaj odabrane linije iz trenutne datoteke", + "command.input.focus": "Fokusiraj polje za unos", "command.terminal.toggle": "Prikaži/sakrij terminal", "command.fileTree.toggle": "Prikaži/sakrij stablo datoteka", "command.review.toggle": "Prikaži/sakrij pregled", @@ -149,6 +150,44 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}} povezan", "provider.connect.toast.connected.description": "{{provider}} modeli su sada dostupni za korištenje.", + "provider.custom.title": "Prilagođeni provajder", + "provider.custom.description.prefix": "Konfiguriši OpenAI-kompatibilnog provajdera. Pogledaj ", + "provider.custom.description.link": "dokumentaciju za konfiguraciju provajdera", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ID provajdera", + "provider.custom.field.providerID.placeholder": "mojprovajder", + "provider.custom.field.providerID.description": "Mala slova, brojevi, crtice ili donje crte", + "provider.custom.field.name.label": "Prikazano ime", + "provider.custom.field.name.placeholder": "Moj AI Provajder", + "provider.custom.field.baseURL.label": "Bazni URL", + "provider.custom.field.baseURL.placeholder": "https://api.mojprovajder.com/v1", + "provider.custom.field.apiKey.label": "API ključ", + "provider.custom.field.apiKey.placeholder": "API ključ", + "provider.custom.field.apiKey.description": + "Opcionalno. Ostavi prazno ako upravljaš autentifikacijom putem zaglavlja.", + "provider.custom.models.label": "Modeli", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Ime", + "provider.custom.models.name.placeholder": "Prikazano ime", + "provider.custom.models.remove": "Ukloni model", + "provider.custom.models.add": "Dodaj model", + "provider.custom.headers.label": "Zaglavlja (opcionalno)", + "provider.custom.headers.key.label": "Zaglavlje", + "provider.custom.headers.key.placeholder": "Ime-Zaglavlja", + "provider.custom.headers.value.label": "Vrijednost", + "provider.custom.headers.value.placeholder": "vrijednost", + "provider.custom.headers.remove": "Ukloni zaglavlje", + "provider.custom.headers.add": "Dodaj zaglavlje", + "provider.custom.error.providerID.required": "ID provajdera je obavezan", + "provider.custom.error.providerID.format": "Koristi mala slova, brojeve, crtice ili donje crte", + "provider.custom.error.providerID.exists": "Taj ID provajdera već postoji", + "provider.custom.error.name.required": "Prikazano ime je obavezno", + "provider.custom.error.baseURL.required": "Bazni URL je obavezan", + "provider.custom.error.baseURL.format": "Mora početi sa http:// ili https://", + "provider.custom.error.required": "Obavezno", + "provider.custom.error.duplicate": "Duplikat", + "provider.disconnect.toast.disconnected.title": "{{provider}} odspojen", "provider.disconnect.toast.disconnected.description": "{{provider}} modeli više nisu dostupni.", @@ -219,6 +258,7 @@ export const dict = { "prompt.popover.emptyResults": "Nema rezultata", "prompt.popover.emptyCommands": "Nema komandi", "prompt.dropzone.label": "Spusti slike ili PDF-ove ovdje", + "prompt.dropzone.file.label": "Spusti za @spominjanje datoteke", "prompt.slash.badge.custom": "prilagođeno", "prompt.slash.badge.skill": "skill", "prompt.slash.badge.mcp": "mcp", @@ -240,6 +280,7 @@ export const dict = { "prompt.toast.shellSendFailed.title": "Neuspješno slanje shell naredbe", "prompt.toast.commandSendFailed.title": "Neuspješno slanje komande", "prompt.toast.promptSendFailed.title": "Neuspješno slanje upita", + "prompt.toast.promptSendFailed.description": "Nije moguće dohvatiti sesiju", "dialog.mcp.title": "MCP-ovi", "dialog.mcp.description": "{{enabled}} od {{total}} omogućeno", @@ -405,6 +446,7 @@ export const dict = { "Korijenski element nije pronađen. Da li si zaboravio da ga dodaš u index.html? Ili je možda id atribut pogrešno napisan?", "error.globalSync.connectFailed": "Nije moguće povezati se na server. Da li server radi na `{{url}}`?", + "directory.error.invalidUrl": "Nevažeći direktorij u URL-u.", "error.chain.unknown": "Nepoznata greška", "error.chain.causedBy": "Uzrok:", @@ -414,7 +456,7 @@ export const dict = { "error.chain.responseBody": "Tijelo odgovora:\n{{body}}", "error.chain.didYouMean": "Da li si mislio: {{suggestions}}", "error.chain.modelNotFound": "Model nije pronađen: {{provider}}/{{model}}", - "error.chain.checkConfig": "Provjeri konfiguraciju (opencode.json) - nazive provajdera/modela", + "error.chain.checkConfig": "Provjeri konfiguraciju (opencode.json) provider/model names", "error.chain.mcpFailed": 'MCP server "{{name}}" nije uspio. Napomena: OpenCode još ne podržava MCP autentifikaciju.', "error.chain.providerAuthFailed": "Autentifikacija provajdera nije uspjela ({{provider}}): {{message}}", "error.chain.providerInitFailed": @@ -471,6 +513,11 @@ export const dict = { "session.header.search.placeholder": "Pretraži {{project}}", "session.header.searchFiles": "Pretraži datoteke", + "session.header.openIn": "Otvori u", + "session.header.open.action": "Otvori {{app}}", + "session.header.open.ariaLabel": "Otvori u {{app}}", + "session.header.open.menu": "Opcije otvaranja", + "session.header.open.copyPath": "Kopiraj putanju", "status.popover.trigger": "Status", "status.popover.ariaLabel": "Konfiguracije servera", @@ -529,6 +576,7 @@ export const dict = { "sidebar.gettingStarted.line2": "Poveži bilo kojeg provajdera da koristiš modele, npr. Claude, GPT, Gemini itd.", "sidebar.project.recentSessions": "Nedavne sesije", "sidebar.project.viewAllSessions": "Prikaži sve sesije", + "sidebar.project.clearNotifications": "Očisti obavijesti", "app.name.desktop": "OpenCode Desktop", @@ -536,11 +584,15 @@ export const dict = { "settings.section.server": "Server", "settings.tab.general": "Opšte", "settings.tab.shortcuts": "Prečice", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integracija", + "settings.desktop.wsl.description": "Pokreni OpenCode server unutar WSL-a na Windowsu.", "settings.general.section.appearance": "Izgled", "settings.general.section.notifications": "Sistemske obavijesti", "settings.general.section.updates": "Ažuriranja", "settings.general.section.sounds": "Zvučni efekti", + "settings.general.section.display": "Prikaz", "settings.general.row.language.title": "Jezik", "settings.general.row.language.description": "Promijeni jezik prikaza u OpenCode-u", @@ -551,6 +603,11 @@ export const dict = { "settings.general.row.font.title": "Font", "settings.general.row.font.description": "Prilagodi monospace font koji se koristi u blokovima koda", + "settings.general.row.wayland.title": "Koristi nativni Wayland", + "settings.general.row.wayland.description": "Onemogući X11 fallback na Waylandu. Zahtijeva restart.", + "settings.general.row.wayland.tooltip": + "Na Linuxu sa monitorima miješanih stopa osvježavanja, nativni Wayland može biti stabilniji.", + "settings.general.row.releaseNotes.title": "Bilješke o izdanju", "settings.general.row.releaseNotes.description": 'Prikaži iskačuće prozore "Šta je novo" nakon ažuriranja', @@ -574,6 +631,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Upozorenje 01", "sound.option.alert02": "Upozorenje 02", "sound.option.alert03": "Upozorenje 03", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 88704607b339..9faa14d3da4b 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -44,8 +44,10 @@ export const dict = { "command.session.new": "Ny session", "command.file.open": "Åbn fil", + "command.tab.close": "Luk fane", "command.context.addSelection": "Tilføj markering til kontekst", "command.context.addSelection.description": "Tilføj markerede linjer fra den aktuelle fil", + "command.input.focus": "Fokuser inputfelt", "command.terminal.toggle": "Skift terminal", "command.fileTree.toggle": "Skift filtræ", "command.review.toggle": "Skift gennemgang", @@ -70,6 +72,7 @@ export const dict = { "command.permissions.autoaccept.enable": "Accepter ændringer automatisk", "command.permissions.autoaccept.disable": "Stop automatisk accept af ændringer", "command.workspace.toggle": "Skift arbejdsområder", + "command.workspace.toggle.description": "Aktiver eller deaktiver flere arbejdsområder i sidebjælken", "command.session.undo": "Fortryd", "command.session.undo.description": "Fortryd den sidste besked", "command.session.redo": "Omgør", @@ -93,9 +96,13 @@ export const dict = { "dialog.provider.group.popular": "Populære", "dialog.provider.group.other": "Andre", "dialog.provider.tag.recommended": "Anbefalet", - "dialog.provider.anthropic.note": "Forbind med Claude Pro/Max eller API-nøgle", - "dialog.provider.openai.note": "Forbind med ChatGPT Pro/Plus eller API-nøgle", - "dialog.provider.copilot.note": "Forbind med Copilot eller API-nøgle", + "dialog.provider.opencode.note": "Udvalgte modeller inklusive Claude, GPT, Gemini og flere", + "dialog.provider.anthropic.note": "Direkte adgang til Claude-modeller, inklusive Pro og Max", + "dialog.provider.copilot.note": "Claude-modeller til kodningsassistance", + "dialog.provider.openai.note": "GPT-modeller til hurtige, kompetente generelle AI-opgaver", + "dialog.provider.google.note": "Gemini-modeller til hurtige, strukturerede svar", + "dialog.provider.openrouter.note": "Få adgang til alle understøttede modeller fra én udbyder", + "dialog.provider.vercel.note": "Samlet adgang til AI-modeller med smart routing", "dialog.model.select.title": "Vælg model", "dialog.model.search.placeholder": "Søg modeller", @@ -143,6 +150,43 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}} forbundet", "provider.connect.toast.connected.description": "{{provider}} modeller er nu tilgængelige.", + "provider.custom.title": "Brugerdefineret udbyder", + "provider.custom.description.prefix": "Konfigurer en OpenAI-kompatibel udbyder. Se ", + "provider.custom.description.link": "dokumentation for udbyderkonfiguration", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "Udbyder-ID", + "provider.custom.field.providerID.placeholder": "minudbyder", + "provider.custom.field.providerID.description": "Små bogstaver, tal, bindestreger eller understregninger", + "provider.custom.field.name.label": "Visningsnavn", + "provider.custom.field.name.placeholder": "Min AI-udbyder", + "provider.custom.field.baseURL.label": "Basis-URL", + "provider.custom.field.baseURL.placeholder": "https://api.minudbyder.dk/v1", + "provider.custom.field.apiKey.label": "API-nøgle", + "provider.custom.field.apiKey.placeholder": "API-nøgle", + "provider.custom.field.apiKey.description": "Valgfri. Lad være tom, hvis du administrerer godkendelse via headers.", + "provider.custom.models.label": "Modeller", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Navn", + "provider.custom.models.name.placeholder": "Visningsnavn", + "provider.custom.models.remove": "Fjern model", + "provider.custom.models.add": "Tilføj model", + "provider.custom.headers.label": "Headers (valgfri)", + "provider.custom.headers.key.label": "Header", + "provider.custom.headers.key.placeholder": "Header-Navn", + "provider.custom.headers.value.label": "Værdi", + "provider.custom.headers.value.placeholder": "værdi", + "provider.custom.headers.remove": "Fjern header", + "provider.custom.headers.add": "Tilføj header", + "provider.custom.error.providerID.required": "Udbyder-ID er påkrævet", + "provider.custom.error.providerID.format": "Brug små bogstaver, tal, bindestreger eller understregninger", + "provider.custom.error.providerID.exists": "Dette udbyder-ID findes allerede", + "provider.custom.error.name.required": "Visningsnavn er påkrævet", + "provider.custom.error.baseURL.required": "Basis-URL er påkrævet", + "provider.custom.error.baseURL.format": "Skal starte med http:// eller https://", + "provider.custom.error.required": "Påkrævet", + "provider.custom.error.duplicate": "Duplikeret", + "provider.disconnect.toast.disconnected.title": "{{provider}} frakoblet", "provider.disconnect.toast.disconnected.description": "Modeller fra {{provider}} er ikke længere tilgængelige.", "model.tag.free": "Gratis", @@ -164,6 +208,7 @@ export const dict = { "model.tooltip.context": "Kontekstgrænse {{limit}}", "common.search.placeholder": "Søg", "common.goBack": "Gå tilbage", + "common.goForward": "Naviger fremad", "common.loading": "Indlæser", "common.loading.ellipsis": "...", "common.cancel": "Annuller", @@ -211,6 +256,7 @@ export const dict = { "prompt.popover.emptyResults": "Ingen matchende resultater", "prompt.popover.emptyCommands": "Ingen matchende kommandoer", "prompt.dropzone.label": "Slip billeder eller PDF'er her", + "prompt.dropzone.file.label": "Slip for at @nævne fil", "prompt.slash.badge.custom": "brugerdefineret", "prompt.slash.badge.skill": "skill", "prompt.slash.badge.mcp": "mcp", @@ -232,6 +278,7 @@ export const dict = { "prompt.toast.shellSendFailed.title": "Kunne ikke sende shell-kommando", "prompt.toast.commandSendFailed.title": "Kunne ikke sende kommando", "prompt.toast.promptSendFailed.title": "Kunne ikke sende forespørgsel", + "prompt.toast.promptSendFailed.description": "Kunne ikke hente session", "dialog.mcp.title": "MCP'er", "dialog.mcp.description": "{{enabled}} af {{total}} aktiveret", @@ -304,10 +351,10 @@ export const dict = { "context.stats.provider": "Udbyder", "context.stats.model": "Model", "context.stats.limit": "Kontekstgrænse", - "context.stats.totalTokens": "Total Tokens", + "context.stats.totalTokens": "Samlede tokens", "context.stats.usage": "Forbrug", - "context.stats.inputTokens": "Input Tokens", - "context.stats.outputTokens": "Output Tokens", + "context.stats.inputTokens": "Input-tokens", + "context.stats.outputTokens": "Output-tokens", "context.stats.reasoningTokens": "Tænke Tokens", "context.stats.cacheTokens": "Cache Tokens (læs/skriv)", "context.stats.userMessages": "Brugerbeskeder", @@ -322,6 +369,23 @@ export const dict = { "context.usage.clickToView": "Klik for at se kontekst", "context.usage.view": "Se kontekstforbrug", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", + "toast.language.title": "Sprog", "toast.language.description": "Skiftede til {{language}}", @@ -379,6 +443,7 @@ export const dict = { "Rodelement ikke fundet. Har du glemt at tilføje det til din index.html? Eller måske er id-attributten stavet forkert?", "error.globalSync.connectFailed": "Kunne ikke forbinde til server. Kører der en server på `{{url}}`?", + "directory.error.invalidUrl": "Ugyldig mappe i URL.", "error.chain.unknown": "Ukendt fejl", "error.chain.causedBy": "Forårsaget af:", @@ -443,6 +508,11 @@ export const dict = { "session.header.search.placeholder": "Søg {{project}}", "session.header.searchFiles": "Søg efter filer", + "session.header.openIn": "Åbn i", + "session.header.open.action": "Åbn {{app}}", + "session.header.open.ariaLabel": "Åbn i {{app}}", + "session.header.open.menu": "Åbningsmuligheder", + "session.header.open.copyPath": "Kopier sti", "status.popover.trigger": "Status", "status.popover.ariaLabel": "Serverkonfigurationer", @@ -502,17 +572,22 @@ export const dict = { "sidebar.gettingStarted.line2": "Forbind enhver udbyder for at bruge modeller, inkl. Claude, GPT, Gemini osv.", "sidebar.project.recentSessions": "Seneste sessioner", "sidebar.project.viewAllSessions": "Vis alle sessioner", + "sidebar.project.clearNotifications": "Ryd notifikationer", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Desktop", "settings.section.server": "Server", "settings.tab.general": "Generelt", "settings.tab.shortcuts": "Genveje", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Kør OpenCode-serveren inde i WSL på Windows.", "settings.general.section.appearance": "Udseende", "settings.general.section.notifications": "Systemmeddelelser", "settings.general.section.updates": "Opdateringer", "settings.general.section.sounds": "Lydeffekter", + "settings.general.section.display": "Skærm", "settings.general.row.language.title": "Sprog", "settings.general.row.language.description": "Ændr visningssproget for OpenCode", @@ -523,6 +598,11 @@ export const dict = { "settings.general.row.font.title": "Skrifttype", "settings.general.row.font.description": "Tilpas mono-skrifttypen brugt i kodeblokke", + "settings.general.row.wayland.title": "Brug native Wayland", + "settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Kræver genstart.", + "settings.general.row.wayland.tooltip": + "På Linux med skærme med blandet opdateringshastighed kan native Wayland være mere stabilt.", + "settings.general.row.releaseNotes.title": "Udgivelsesnoter", "settings.general.row.releaseNotes.description": 'Vis "Hvad er nyt"-popups efter opdateringer', @@ -547,6 +627,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Alarm 01", "sound.option.alert02": "Alarm 02", "sound.option.alert03": "Alarm 03", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index a4d12d445432..d350af6cf55f 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -19,12 +19,10 @@ export const dict = { "command.category.agent": "Agent", "command.category.permissions": "Berechtigungen", "command.category.workspace": "Arbeitsbereich", - "command.category.settings": "Einstellungen", "theme.scheme.system": "System", "theme.scheme.light": "Hell", "theme.scheme.dark": "Dunkel", - "command.sidebar.toggle": "Seitenleiste umschalten", "command.project.open": "Projekt öffnen", "command.provider.connect": "Anbieter verbinden", @@ -35,21 +33,19 @@ export const dict = { "command.session.previous.unseen": "Vorherige ungelesene Sitzung", "command.session.next.unseen": "Nächste ungelesene Sitzung", "command.session.archive": "Sitzung archivieren", - "command.palette": "Befehlspalette", - "command.theme.cycle": "Thema wechseln", "command.theme.set": "Thema verwenden: {{theme}}", "command.theme.scheme.cycle": "Farbschema wechseln", "command.theme.scheme.set": "Farbschema verwenden: {{scheme}}", - "command.language.cycle": "Sprache wechseln", "command.language.set": "Sprache verwenden: {{language}}", - "command.session.new": "Neue Sitzung", "command.file.open": "Datei öffnen", + "command.tab.close": "Tab schließen", "command.context.addSelection": "Auswahl zum Kontext hinzufügen", "command.context.addSelection.description": "Ausgewählte Zeilen aus der aktuellen Datei hinzufügen", + "command.input.focus": "Eingabefeld fokussieren", "command.terminal.toggle": "Terminal umschalten", "command.fileTree.toggle": "Dateibaum umschalten", "command.review.toggle": "Überprüfung umschalten", @@ -74,6 +70,7 @@ export const dict = { "command.permissions.autoaccept.enable": "Änderungen automatisch akzeptieren", "command.permissions.autoaccept.disable": "Automatische Annahme von Änderungen stoppen", "command.workspace.toggle": "Arbeitsbereiche umschalten", + "command.workspace.toggle.description": "Mehrere Arbeitsbereiche in der Seitenleiste aktivieren oder deaktivieren", "command.session.undo": "Rückgängig", "command.session.undo.description": "Letzte Nachricht rückgängig machen", "command.session.redo": "Wiederherstellen", @@ -86,32 +83,30 @@ export const dict = { "command.session.share.description": "Diese Sitzung teilen und URL in die Zwischenablage kopieren", "command.session.unshare": "Teilen der Sitzung aufheben", "command.session.unshare.description": "Teilen dieser Sitzung beenden", - "palette.search.placeholder": "Dateien, Befehle und Sitzungen durchsuchen", "palette.empty": "Keine Ergebnisse gefunden", "palette.group.commands": "Befehle", "palette.group.files": "Dateien", - "dialog.provider.search.placeholder": "Anbieter durchsuchen", "dialog.provider.empty": "Keine Anbieter gefunden", "dialog.provider.group.popular": "Beliebt", "dialog.provider.group.other": "Andere", "dialog.provider.tag.recommended": "Empfohlen", + "dialog.provider.opencode.note": "Kuratierte Modelle inklusive Claude, GPT, Gemini und mehr", "dialog.provider.anthropic.note": "Mit Claude Pro/Max oder API-Schlüssel verbinden", - "dialog.provider.openai.note": "Mit ChatGPT Pro/Plus oder API-Schlüssel verbinden", "dialog.provider.copilot.note": "Mit Copilot oder API-Schlüssel verbinden", - + "dialog.provider.openai.note": "Mit ChatGPT Pro/Plus oder API-Schlüssel verbinden", + "dialog.provider.google.note": "Gemini-Modelle für schnelle, strukturierte Antworten", + "dialog.provider.openrouter.note": "Zugriff auf alle unterstützten Modelle über einen Anbieter", + "dialog.provider.vercel.note": "Einheitlicher Zugriff auf KI-Modelle mit intelligentem Routing", "dialog.model.select.title": "Modell auswählen", "dialog.model.search.placeholder": "Modelle durchsuchen", "dialog.model.empty": "Keine Modellergebnisse", "dialog.model.manage": "Modelle verwalten", "dialog.model.manage.description": "Anpassen, welche Modelle in der Modellauswahl erscheinen.", - "dialog.model.unpaid.freeModels.title": "Kostenlose Modelle von OpenCode", "dialog.model.unpaid.addMore.title": "Weitere Modelle von beliebten Anbietern hinzufügen", - "dialog.provider.viewAll": "Mehr Anbieter anzeigen", - "provider.connect.title": "{{provider}} verbinden", "provider.connect.title.anthropicProMax": "Mit Claude Pro/Max anmelden", "provider.connect.selectMethod": "Anmeldemethode für {{provider}} auswählen.", @@ -146,7 +141,6 @@ export const dict = { "provider.connect.oauth.auto.confirmationCode": "Bestätigungscode", "provider.connect.toast.connected.title": "{{provider}} verbunden", "provider.connect.toast.connected.description": "{{provider}} Modelle sind jetzt verfügbar.", - "provider.custom.title": "Benutzerdefinierter Anbieter", "provider.custom.description.prefix": "Konfigurieren Sie einen OpenAI-kompatiblen Anbieter. Siehe die ", "provider.custom.description.link": "Anbieter-Konfigurationsdokumente", @@ -184,12 +178,10 @@ export const dict = { "provider.custom.error.baseURL.format": "Muss mit http:// oder https:// beginnen", "provider.custom.error.required": "Erforderlich", "provider.custom.error.duplicate": "Duplikat", - "provider.disconnect.toast.disconnected.title": "{{provider}} getrennt", "provider.disconnect.toast.disconnected.description": "Die {{provider}}-Modelle sind nicht mehr verfügbar.", "model.tag.free": "Kostenlos", "model.tag.latest": "Neueste", - "model.provider.anthropic": "Anthropic", "model.provider.openai": "OpenAI", "model.provider.google": "Google", @@ -199,13 +191,14 @@ export const dict = { "model.input.image": "Bild", "model.input.audio": "Audio", "model.input.video": "Video", - "model.input.pdf": "pdf", + "model.input.pdf": "PDF", "model.tooltip.allows": "Erlaubt: {{inputs}}", "model.tooltip.reasoning.allowed": "Erlaubt Reasoning", "model.tooltip.reasoning.none": "Kein Reasoning", "model.tooltip.context": "Kontextlimit {{limit}}", "common.search.placeholder": "Suchen", "common.goBack": "Zurück", + "common.goForward": "Vorwärts navigieren", "common.loading": "Laden", "common.loading.ellipsis": "...", "common.cancel": "Abbrechen", @@ -216,14 +209,12 @@ export const dict = { "common.saving": "Speichert...", "common.default": "Standard", "common.attachment": "Anhang", - "prompt.placeholder.shell": "Shell-Befehl eingeben...", "prompt.placeholder.normal": 'Fragen Sie alles... "{{example}}"', "prompt.placeholder.summarizeComments": "Kommentare zusammenfassen…", "prompt.placeholder.summarizeComment": "Kommentar zusammenfassen…", "prompt.mode.shell": "Shell", "prompt.mode.shell.exit": "esc zum Verlassen", - "prompt.example.1": "Ein TODO in der Codebasis beheben", "prompt.example.2": "Was ist der Tech-Stack dieses Projekts?", "prompt.example.3": "Fehlerhafte Tests beheben", @@ -249,13 +240,13 @@ export const dict = { "prompt.example.23": "Paginierung zu dieser Liste hinzufügen", "prompt.example.24": "CLI-Befehl erstellen für...", "prompt.example.25": "Wie funktionieren Umgebungsvariablen hier?", - "prompt.popover.emptyResults": "Keine passenden Ergebnisse", "prompt.popover.emptyCommands": "Keine passenden Befehle", "prompt.dropzone.label": "Bilder oder PDFs hier ablegen", + "prompt.dropzone.file.label": "Ablegen zum @Erwähnen der Datei", "prompt.slash.badge.custom": "benutzerdefiniert", - "prompt.slash.badge.skill": "skill", - "prompt.slash.badge.mcp": "mcp", + "prompt.slash.badge.skill": "Skill", + "prompt.slash.badge.mcp": "MCP", "prompt.context.active": "aktiv", "prompt.context.includeActiveFile": "Aktive Datei einbeziehen", "prompt.context.removeActiveFile": "Aktive Datei aus dem Kontext entfernen", @@ -264,7 +255,6 @@ export const dict = { "prompt.attachment.remove": "Anhang entfernen", "prompt.action.send": "Senden", "prompt.action.stop": "Stopp", - "prompt.toast.pasteUnsupported.title": "Nicht unterstütztes Einfügen", "prompt.toast.pasteUnsupported.description": "Hier können nur Bilder oder PDFs eingefügt werden.", "prompt.toast.modelAgentRequired.title": "Wählen Sie einen Agenten und ein Modell", @@ -275,24 +265,19 @@ export const dict = { "prompt.toast.shellSendFailed.title": "Shell-Befehl konnte nicht gesendet werden", "prompt.toast.commandSendFailed.title": "Befehl konnte nicht gesendet werden", "prompt.toast.promptSendFailed.title": "Eingabe konnte nicht gesendet werden", - + "prompt.toast.promptSendFailed.description": "Sitzung konnte nicht abgerufen werden", "dialog.mcp.title": "MCPs", "dialog.mcp.description": "{{enabled}} von {{total}} aktiviert", "dialog.mcp.empty": "Keine MCPs konfiguriert", - "dialog.lsp.empty": "LSPs automatisch nach Dateityp erkannt", "dialog.plugins.empty": "In opencode.json konfigurierte Plugins", - "mcp.status.connected": "verbunden", "mcp.status.failed": "fehlgeschlagen", "mcp.status.needs_auth": "benötigt Authentifizierung", "mcp.status.disabled": "deaktiviert", - "dialog.fork.empty": "Keine Nachrichten zum Abzweigen vorhanden", - "dialog.directory.search.placeholder": "Ordner durchsuchen", "dialog.directory.empty": "Keine Ordner gefunden", - "dialog.server.title": "Server", "dialog.server.description": "Wechseln Sie den OpenCode-Server, mit dem sich diese App verbindet.", "dialog.server.search.placeholder": "Server durchsuchen", @@ -310,14 +295,12 @@ export const dict = { "dialog.server.default.set": "Aktuellen Server als Standard setzen", "dialog.server.default.clear": "Löschen", "dialog.server.action.remove": "Server entfernen", - "dialog.server.menu.edit": "Bearbeiten", "dialog.server.menu.default": "Als Standard festlegen", "dialog.server.menu.defaultRemove": "Standard entfernen", "dialog.server.menu.delete": "Löschen", "dialog.server.current": "Aktueller Server", "dialog.server.status.default": "Standard", - "dialog.project.edit.title": "Projekt bearbeiten", "dialog.project.edit.name": "Name", "dialog.project.edit.icon": "Icon", @@ -326,7 +309,6 @@ export const dict = { "dialog.project.edit.icon.recommended": "Empfohlen: 128x128px", "dialog.project.edit.color": "Farbe", "dialog.project.edit.color.select": "{{color}}-Farbe auswählen", - "dialog.project.edit.worktree.startup": "Startup-Skript für Arbeitsbereich", "dialog.project.edit.worktree.startup.description": "Wird nach dem Erstellen eines neuen Arbeitsbereichs (Worktree) ausgeführt.", @@ -339,10 +321,8 @@ export const dict = { "context.breakdown.assistant": "Assistent", "context.breakdown.tool": "Werkzeugaufrufe", "context.breakdown.other": "Andere", - "context.systemPrompt.title": "System-Prompt", "context.rawMessages.title": "Rohdaten der Nachrichten", - "context.stats.session": "Sitzung", "context.stats.messages": "Nachrichten", "context.stats.provider": "Anbieter", @@ -359,29 +339,42 @@ export const dict = { "context.stats.totalCost": "Gesamtkosten", "context.stats.sessionCreated": "Sitzung erstellt", "context.stats.lastActivity": "Letzte Aktivität", - "context.usage.tokens": "Token", "context.usage.usage": "Nutzung", "context.usage.cost": "Kosten", "context.usage.clickToView": "Klicken, um Kontext anzuzeigen", "context.usage.view": "Kontextnutzung anzeigen", - + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", "toast.language.title": "Sprache", "toast.language.description": "Zu {{language}} gewechselt", - "toast.theme.title": "Thema gewechselt", "toast.scheme.title": "Farbschema", - + "toast.workspace.enabled.title": "Arbeitsbereiche aktiviert", + "toast.workspace.enabled.description": "Mehrere Worktrees werden jetzt in der Seitenleiste angezeigt", + "toast.workspace.disabled.title": "Arbeitsbereiche deaktiviert", + "toast.workspace.disabled.description": "Nur der Haupt-Worktree wird in der Seitenleiste angezeigt", "toast.permissions.autoaccept.on.title": "Änderungen werden automatisch akzeptiert", "toast.permissions.autoaccept.on.description": "Bearbeitungs- und Schreibrechte werden automatisch genehmigt", "toast.permissions.autoaccept.off.title": "Automatische Annahme von Änderungen gestoppt", "toast.permissions.autoaccept.off.description": "Bearbeitungs- und Schreibrechte erfordern Genehmigung", - "toast.model.none.title": "Kein Modell ausgewählt", "toast.model.none.description": "Verbinden Sie einen Anbieter, um diese Sitzung zusammenzufassen", - "toast.file.loadFailed.title": "Datei konnte nicht geladen werden", - "toast.file.listFailed.title": "Dateien konnten nicht aufgelistet werden", "toast.context.noLineSelection.title": "Keine Zeilenauswahl", "toast.context.noLineSelection.description": "Wählen Sie zuerst einen Zeilenbereich in einem Datei-Tab aus.", @@ -390,19 +383,15 @@ export const dict = { "toast.session.share.success.description": "Teilen-URL in die Zwischenablage kopiert!", "toast.session.share.failed.title": "Sitzung konnte nicht geteilt werden", "toast.session.share.failed.description": "Beim Teilen der Sitzung ist ein Fehler aufgetreten", - "toast.session.unshare.success.title": "Teilen der Sitzung aufgehoben", "toast.session.unshare.success.description": "Teilen der Sitzung erfolgreich aufgehoben!", "toast.session.unshare.failed.title": "Aufheben des Teilens fehlgeschlagen", "toast.session.unshare.failed.description": "Beim Aufheben des Teilens ist ein Fehler aufgetreten", - "toast.session.listFailed.title": "Sitzungen für {{project}} konnten nicht geladen werden", - "toast.update.title": "Update verfügbar", "toast.update.description": "Eine neue Version von OpenCode ({{version}}) ist zur Installation verfügbar.", "toast.update.action.installRestart": "Installieren und neu starten", "toast.update.action.notYet": "Noch nicht", - "error.page.title": "Etwas ist schiefgelaufen", "error.page.description": "Beim Laden der Anwendung ist ein Fehler aufgetreten.", "error.page.details.label": "Fehlerdetails", @@ -413,13 +402,10 @@ export const dict = { "error.page.report.prefix": "Bitte melden Sie diesen Fehler dem OpenCode-Team", "error.page.report.discord": "auf Discord", "error.page.version": "Version: {{version}}", - "error.dev.rootNotFound": "Wurzelelement nicht gefunden. Haben Sie vergessen, es in Ihre index.html aufzunehmen? Oder wurde das id-Attribut falsch geschrieben?", - "error.globalSync.connectFailed": "Verbindung zum Server fehlgeschlagen. Läuft ein Server unter `{{url}}`?", "directory.error.invalidUrl": "Ungültiges Verzeichnis in der URL.", - "error.chain.unknown": "Unbekannter Fehler", "error.chain.causedBy": "Verursacht durch:", "error.chain.apiError": "API-Fehler", @@ -442,21 +428,17 @@ export const dict = { "error.chain.configFrontmatterError": "Frontmatter in {{path}} konnte nicht geparst werden:\n{{message}}", "error.chain.configInvalid": "Konfigurationsdatei unter {{path}} ist ungültig", "error.chain.configInvalidWithMessage": "Konfigurationsdatei unter {{path}} ist ungültig: {{message}}", - "notification.permission.title": "Berechtigung erforderlich", "notification.permission.description": "{{sessionTitle}} in {{projectName}} benötigt Berechtigung", "notification.question.title": "Frage", "notification.question.description": "{{sessionTitle}} in {{projectName}} hat eine Frage", "notification.action.goToSession": "Zur Sitzung gehen", - "notification.session.responseReady.title": "Antwort bereit", "notification.session.error.title": "Sitzungsfehler", "notification.session.error.fallbackDescription": "Ein Fehler ist aufgetreten", - "home.recentProjects": "Letzte Projekte", "home.empty.title": "Keine letzten Projekte", "home.empty.description": "Starten Sie, indem Sie ein lokales Projekt öffnen", - "session.tab.session": "Sitzung", "session.tab.review": "Überprüfung", "session.tab.context": "Kontext", @@ -474,18 +456,19 @@ export const dict = { "session.messages.loadingEarlier": "Lade frühere Nachrichten...", "session.messages.loadEarlier": "Frühere Nachrichten laden", "session.messages.loading": "Lade Nachrichten...", - "session.messages.jumpToLatest": "Zum neuesten springen", "session.context.addToContext": "{{selection}} zum Kontext hinzufügen", - "session.new.worktree.main": "Haupt-Branch", "session.new.worktree.mainWithBranch": "Haupt-Branch ({{branch}})", "session.new.worktree.create": "Neuen Worktree erstellen", "session.new.lastModified": "Zuletzt geändert", - "session.header.search.placeholder": "{{project}} durchsuchen", "session.header.searchFiles": "Dateien suchen", - + "session.header.openIn": "Öffnen in", + "session.header.open.action": "{{app}} öffnen", + "session.header.open.ariaLabel": "In {{app}} öffnen", + "session.header.open.menu": "Öffnen-Optionen", + "session.header.open.copyPath": "Pfad kopieren", "status.popover.trigger": "Status", "status.popover.ariaLabel": "Serverkonfigurationen", "status.popover.tab.servers": "Server", @@ -493,7 +476,6 @@ export const dict = { "status.popover.tab.lsp": "LSP", "status.popover.tab.plugins": "Plugins", "status.popover.action.manageServers": "Server verwalten", - "session.share.popover.title": "Im Web veröffentlichen", "session.share.popover.description.shared": "Diese Sitzung ist öffentlich im Web. Sie ist für jeden mit dem Link zugänglich.", @@ -507,16 +489,13 @@ export const dict = { "session.share.action.view": "Ansehen", "session.share.copy.copied": "Kopiert", "session.share.copy.copyLink": "Link kopieren", - "lsp.tooltip.none": "Keine LSP-Server", "lsp.label.connected": "{{count}} LSP", - "prompt.loading": "Lade Prompt...", "terminal.loading": "Lade Terminal...", "terminal.title": "Terminal", "terminal.title.numbered": "Terminal {{number}}", "terminal.close": "Terminal schließen", - "terminal.connectionLost.title": "Verbindung verloren", "terminal.connectionLost.description": "Die Terminalverbindung wurde unterbrochen. Das kann passieren, wenn der Server neu startet.", @@ -532,7 +511,6 @@ export const dict = { "common.close": "Schließen", "common.edit": "Bearbeiten", "common.loadMore": "Mehr laden", - "common.key.esc": "ESC", "sidebar.menu.toggle": "Menü umschalten", "sidebar.nav.projectsAndSessions": "Projekte und Sitzungen", @@ -546,18 +524,20 @@ export const dict = { "Verbinden Sie einen beliebigen Anbieter, um Modelle wie Claude, GPT, Gemini usw. zu nutzen.", "sidebar.project.recentSessions": "Letzte Sitzungen", "sidebar.project.viewAllSessions": "Alle Sitzungen anzeigen", - + "sidebar.project.clearNotifications": "Benachrichtigungen löschen", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Desktop", "settings.section.server": "Server", "settings.tab.general": "Allgemein", "settings.tab.shortcuts": "Tastenkombinationen", - + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL-Integration", + "settings.desktop.wsl.description": "OpenCode-Server innerhalb von WSL unter Windows ausführen.", "settings.general.section.appearance": "Erscheinungsbild", "settings.general.section.notifications": "Systembenachrichtigungen", "settings.general.section.updates": "Updates", "settings.general.section.sounds": "Soundeffekte", - + "settings.general.section.display": "Anzeige", "settings.general.row.language.title": "Sprache", "settings.general.row.language.description": "Die Anzeigesprache für OpenCode ändern", "settings.general.row.appearance.title": "Erscheinungsbild", @@ -566,10 +546,12 @@ export const dict = { "settings.general.row.theme.description": "Das Thema von OpenCode anpassen.", "settings.general.row.font.title": "Schriftart", "settings.general.row.font.description": "Die in Codeblöcken verwendete Monospace-Schriftart anpassen", - + "settings.general.row.wayland.title": "Natives Wayland verwenden", + "settings.general.row.wayland.description": "X11-Fallback unter Wayland deaktivieren. Erfordert Neustart.", + "settings.general.row.wayland.tooltip": + "Unter Linux mit Monitoren unterschiedlicher Bildwiederholraten kann natives Wayland stabiler sein.", "settings.general.row.releaseNotes.title": "Versionshinweise", "settings.general.row.releaseNotes.description": '"Neuigkeiten"-Pop-ups nach Updates anzeigen', - "settings.updates.row.startup.title": "Beim Start nach Updates suchen", "settings.updates.row.startup.description": "Beim Start von OpenCode automatisch nach Updates suchen", "settings.updates.row.check.title": "Nach Updates suchen", @@ -578,7 +560,6 @@ export const dict = { "settings.updates.action.checking": "Wird geprüft...", "settings.updates.toast.latest.title": "Du bist auf dem neuesten Stand", "settings.updates.toast.latest.description": "Du verwendest die aktuelle Version von OpenCode.", - "font.option.ibmPlexMono": "IBM Plex Mono", "font.option.cascadiaCode": "Cascadia Code", "font.option.firaCode": "Fira Code", @@ -591,6 +572,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Alarm 01", "sound.option.alert02": "Alarm 02", "sound.option.alert03": "Alarm 03", @@ -644,14 +626,12 @@ export const dict = { "Systembenachrichtigung anzeigen, wenn eine Berechtigung erforderlich ist", "settings.general.notifications.errors.title": "Fehler", "settings.general.notifications.errors.description": "Systembenachrichtigung anzeigen, wenn ein Fehler auftritt", - "settings.general.sounds.agent.title": "Agent", "settings.general.sounds.agent.description": "Ton abspielen, wenn der Agent fertig ist oder Aufmerksamkeit benötigt", "settings.general.sounds.permissions.title": "Berechtigungen", "settings.general.sounds.permissions.description": "Ton abspielen, wenn eine Berechtigung erforderlich ist", "settings.general.sounds.errors.title": "Fehler", "settings.general.sounds.errors.description": "Ton abspielen, wenn ein Fehler auftritt", - "settings.shortcuts.title": "Tastenkombinationen", "settings.shortcuts.reset.button": "Auf Standard zurücksetzen", "settings.shortcuts.reset.toast.title": "Tastenkombinationen zurückgesetzt", @@ -662,14 +642,12 @@ export const dict = { "settings.shortcuts.pressKeys": "Tasten drücken", "settings.shortcuts.search.placeholder": "Tastenkürzel suchen", "settings.shortcuts.search.empty": "Keine Tastenkürzel gefunden", - "settings.shortcuts.group.general": "Allgemein", "settings.shortcuts.group.session": "Sitzung", "settings.shortcuts.group.navigation": "Navigation", "settings.shortcuts.group.modelAndAgent": "Modell und Agent", "settings.shortcuts.group.terminal": "Terminal", "settings.shortcuts.group.prompt": "Prompt", - "settings.providers.title": "Anbieter", "settings.providers.description": "Anbietereinstellungen können hier konfiguriert werden.", "settings.providers.section.connected": "Verbundene Anbieter", @@ -687,16 +665,13 @@ export const dict = { "settings.commands.description": "Befehlseinstellungen können hier konfiguriert werden.", "settings.mcp.title": "MCP", "settings.mcp.description": "MCP-Einstellungen können hier konfiguriert werden.", - "settings.permissions.title": "Berechtigungen", "settings.permissions.description": "Steuern Sie, welche Tools der Server standardmäßig verwenden darf.", "settings.permissions.section.tools": "Tools", "settings.permissions.toast.updateFailed.title": "Berechtigungen konnten nicht aktualisiert werden", - "settings.permissions.action.allow": "Erlauben", "settings.permissions.action.ask": "Fragen", "settings.permissions.action.deny": "Verweigern", - "settings.permissions.tool.read.title": "Lesen", "settings.permissions.tool.read.description": "Lesen einer Datei (stimmt mit dem Dateipfad überein)", "settings.permissions.tool.edit.title": "Bearbeiten", @@ -730,12 +705,10 @@ export const dict = { "settings.permissions.tool.external_directory.description": "Zugriff auf Dateien außerhalb des Projektverzeichnisses", "settings.permissions.tool.doom_loop.title": "Doom Loop", "settings.permissions.tool.doom_loop.description": "Wiederholte Tool-Aufrufe mit identischer Eingabe erkennen", - "session.delete.failed.title": "Sitzung konnte nicht gelöscht werden", "session.delete.title": "Sitzung löschen", "session.delete.confirm": 'Sitzung "{{name}}" löschen?', "session.delete.button": "Sitzung löschen", - "workspace.new": "Neuer Arbeitsbereich", "workspace.type.local": "lokal", "workspace.type.sandbox": "Sandbox", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 4d7d571afb7e..cb42b016f1fb 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -47,6 +47,7 @@ export const dict = { "command.tab.close": "Close tab", "command.context.addSelection": "Add selection to context", "command.context.addSelection.description": "Add selected lines from the current file", + "command.input.focus": "Focus input", "command.terminal.toggle": "Toggle terminal", "command.fileTree.toggle": "Toggle file tree", "command.review.toggle": "Toggle review", @@ -108,6 +109,7 @@ export const dict = { "dialog.model.empty": "No model results", "dialog.model.manage": "Manage models", "dialog.model.manage.description": "Customize which models appear in the model selector.", + "dialog.model.manage.provider.toggle": "Toggle all {{provider}} models", "dialog.model.unpaid.freeModels.title": "Free models provided by OpenCode", "dialog.model.unpaid.addMore.title": "Add more models from popular providers", @@ -207,8 +209,8 @@ export const dict = { "model.tooltip.context": "Context limit {{limit}}", "common.search.placeholder": "Search", - "common.goBack": "Back", - "common.goForward": "Forward", + "common.goBack": "Navigate back", + "common.goForward": "Navigate forward", "common.loading": "Loading", "common.loading.ellipsis": "...", "common.cancel": "Cancel", @@ -256,6 +258,7 @@ export const dict = { "prompt.popover.emptyResults": "No matching results", "prompt.popover.emptyCommands": "No matching commands", "prompt.dropzone.label": "Drop images or PDFs here", + "prompt.dropzone.file.label": "Drop to @mention file", "prompt.slash.badge.custom": "custom", "prompt.slash.badge.skill": "skill", "prompt.slash.badge.mcp": "mcp", @@ -277,6 +280,7 @@ export const dict = { "prompt.toast.shellSendFailed.title": "Failed to send shell command", "prompt.toast.commandSendFailed.title": "Failed to send command", "prompt.toast.promptSendFailed.title": "Failed to send prompt", + "prompt.toast.promptSendFailed.description": "Unable to retrieve session", "dialog.mcp.title": "MCPs", "dialog.mcp.description": "{{enabled}} of {{total}} enabled", @@ -573,6 +577,7 @@ export const dict = { "sidebar.gettingStarted.line2": "Connect any provider to use models, inc. Claude, GPT, Gemini etc.", "sidebar.project.recentSessions": "Recent sessions", "sidebar.project.viewAllSessions": "View all sessions", + "sidebar.project.clearNotifications": "Clear notifications", "app.name.desktop": "OpenCode Desktop", @@ -580,11 +585,15 @@ export const dict = { "settings.section.server": "Server", "settings.tab.general": "General", "settings.tab.shortcuts": "Shortcuts", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "Appearance", "settings.general.section.notifications": "System notifications", "settings.general.section.updates": "Updates", "settings.general.section.sounds": "Sound effects", + "settings.general.section.display": "Display", "settings.general.row.language.title": "Language", "settings.general.row.language.description": "Change the display language for OpenCode", @@ -595,6 +604,11 @@ export const dict = { "settings.general.row.font.title": "Font", "settings.general.row.font.description": "Customise the mono font used in code blocks", + "settings.general.row.wayland.title": "Use native Wayland", + "settings.general.row.wayland.description": "Disable X11 fallback on Wayland. Requires restart.", + "settings.general.row.wayland.tooltip": + "On Linux with mixed refresh-rate monitors, native Wayland can be more stable.", + "settings.general.row.releaseNotes.title": "Release notes", "settings.general.row.releaseNotes.description": "Show What's New popups after updates", @@ -618,6 +632,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Alert 01", "sound.option.alert02": "Alert 02", "sound.option.alert03": "Alert 03", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 5d48ba494935..c4ec378dcdd4 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -15,8 +15,8 @@ export const dict = { "command.category.agent": "Agente", "command.category.permissions": "Permisos", "command.category.workspace": "Espacio de trabajo", - "command.category.settings": "Ajustes", + "theme.scheme.system": "Sistema", "theme.scheme.light": "Claro", "theme.scheme.dark": "Oscuro", @@ -44,8 +44,10 @@ export const dict = { "command.session.new": "Nueva sesión", "command.file.open": "Abrir archivo", + "command.tab.close": "Cerrar pestaña", "command.context.addSelection": "Añadir selección al contexto", "command.context.addSelection.description": "Añadir las líneas seleccionadas del archivo actual", + "command.input.focus": "Enfocar entrada", "command.terminal.toggle": "Alternar terminal", "command.fileTree.toggle": "Alternar árbol de archivos", "command.review.toggle": "Alternar revisión", @@ -70,6 +72,7 @@ export const dict = { "command.permissions.autoaccept.enable": "Aceptar ediciones automáticamente", "command.permissions.autoaccept.disable": "Dejar de aceptar ediciones automáticamente", "command.workspace.toggle": "Alternar espacios de trabajo", + "command.workspace.toggle.description": "Habilitar o deshabilitar múltiples espacios de trabajo en la barra lateral", "command.session.undo": "Deshacer", "command.session.undo.description": "Deshacer el último mensaje", "command.session.redo": "Rehacer", @@ -93,9 +96,13 @@ export const dict = { "dialog.provider.group.popular": "Popular", "dialog.provider.group.other": "Otro", "dialog.provider.tag.recommended": "Recomendado", - "dialog.provider.anthropic.note": "Conectar con Claude Pro/Max o clave API", - "dialog.provider.openai.note": "Conectar con ChatGPT Pro/Plus o clave API", - "dialog.provider.copilot.note": "Conectar con Copilot o clave API", + "dialog.provider.opencode.note": "Modelos seleccionados incluyendo Claude, GPT, Gemini y más", + "dialog.provider.anthropic.note": "Acceso directo a modelos Claude, incluyendo Pro y Max", + "dialog.provider.copilot.note": "Modelos Claude para asistencia de codificación", + "dialog.provider.openai.note": "Modelos GPT para tareas de IA generales rápidas y capaces", + "dialog.provider.google.note": "Modelos Gemini para respuestas rápidas y estructuradas", + "dialog.provider.openrouter.note": "Accede a todos los modelos soportados desde un solo proveedor", + "dialog.provider.vercel.note": "Acceso unificado a modelos de IA con enrutamiento inteligente", "dialog.model.select.title": "Seleccionar modelo", "dialog.model.search.placeholder": "Buscar modelos", @@ -143,11 +150,48 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}} conectado", "provider.connect.toast.connected.description": "Los modelos de {{provider}} ahora están disponibles para usar.", + "provider.custom.title": "Proveedor personalizado", + "provider.custom.description.prefix": "Configurar un proveedor compatible con OpenAI. Ver la ", + "provider.custom.description.link": "documentación de configuración del proveedor", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ID del proveedor", + "provider.custom.field.providerID.placeholder": "miproveedor", + "provider.custom.field.providerID.description": "Letras minúsculas, números, guiones o guiones bajos", + "provider.custom.field.name.label": "Nombre para mostrar", + "provider.custom.field.name.placeholder": "Mi Proveedor de IA", + "provider.custom.field.baseURL.label": "URL base", + "provider.custom.field.baseURL.placeholder": "https://api.miproveedor.com/v1", + "provider.custom.field.apiKey.label": "Clave API", + "provider.custom.field.apiKey.placeholder": "Clave API", + "provider.custom.field.apiKey.description": "Opcional. Dejar vacío si gestionas la autenticación mediante cabeceras.", + "provider.custom.models.label": "Modelos", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "id-modelo", + "provider.custom.models.name.label": "Nombre", + "provider.custom.models.name.placeholder": "Nombre para mostrar", + "provider.custom.models.remove": "Eliminar modelo", + "provider.custom.models.add": "Añadir modelo", + "provider.custom.headers.label": "Cabeceras (opcional)", + "provider.custom.headers.key.label": "Cabecera", + "provider.custom.headers.key.placeholder": "Nombre-Cabecera", + "provider.custom.headers.value.label": "Valor", + "provider.custom.headers.value.placeholder": "valor", + "provider.custom.headers.remove": "Eliminar cabecera", + "provider.custom.headers.add": "Añadir cabecera", + "provider.custom.error.providerID.required": "El ID del proveedor es obligatorio", + "provider.custom.error.providerID.format": "Usa letras minúsculas, números, guiones o guiones bajos", + "provider.custom.error.providerID.exists": "Ese ID de proveedor ya existe", + "provider.custom.error.name.required": "El nombre para mostrar es obligatorio", + "provider.custom.error.baseURL.required": "La URL base es obligatoria", + "provider.custom.error.baseURL.format": "Debe comenzar con http:// o https://", + "provider.custom.error.required": "Obligatorio", + "provider.custom.error.duplicate": "Duplicado", + "provider.disconnect.toast.disconnected.title": "{{provider}} desconectado", "provider.disconnect.toast.disconnected.description": "Los modelos de {{provider}} ya no están disponibles.", + "model.tag.free": "Gratis", "model.tag.latest": "Último", - "model.provider.anthropic": "Anthropic", "model.provider.openai": "OpenAI", "model.provider.google": "Google", @@ -162,8 +206,10 @@ export const dict = { "model.tooltip.reasoning.allowed": "Permite razonamiento", "model.tooltip.reasoning.none": "Sin razonamiento", "model.tooltip.context": "Límite de contexto {{limit}}", + "common.search.placeholder": "Buscar", "common.goBack": "Volver", + "common.goForward": "Avanzar", "common.loading": "Cargando", "common.loading.ellipsis": "...", "common.cancel": "Cancelar", @@ -211,6 +257,7 @@ export const dict = { "prompt.popover.emptyResults": "Sin resultados coincidentes", "prompt.popover.emptyCommands": "Sin comandos coincidentes", "prompt.dropzone.label": "Suelta imágenes o PDFs aquí", + "prompt.dropzone.file.label": "Suelta para @mencionar archivo", "prompt.slash.badge.custom": "personalizado", "prompt.slash.badge.skill": "skill", "prompt.slash.badge.mcp": "mcp", @@ -232,6 +279,7 @@ export const dict = { "prompt.toast.shellSendFailed.title": "Fallo al enviar comando de shell", "prompt.toast.commandSendFailed.title": "Fallo al enviar comando", "prompt.toast.promptSendFailed.title": "Fallo al enviar prompt", + "prompt.toast.promptSendFailed.description": "No se pudo recuperar la sesión", "dialog.mcp.title": "MCPs", "dialog.mcp.description": "{{enabled}} de {{total}} habilitados", @@ -283,11 +331,11 @@ export const dict = { "dialog.project.edit.icon.recommended": "Recomendado: 128x128px", "dialog.project.edit.color": "Color", "dialog.project.edit.color.select": "Seleccionar color {{color}}", - "dialog.project.edit.worktree.startup": "Script de inicio del espacio de trabajo", "dialog.project.edit.worktree.startup.description": "Se ejecuta después de crear un nuevo espacio de trabajo (árbol de trabajo).", "dialog.project.edit.worktree.startup.placeholder": "p. ej. bun install", + "context.breakdown.title": "Desglose de Contexto", "context.breakdown.note": 'Desglose aproximado de tokens de entrada. "Otro" incluye definiciones de herramientas y sobrecarga.', @@ -323,30 +371,48 @@ export const dict = { "context.usage.clickToView": "Haz clic para ver contexto", "context.usage.view": "Ver uso del contexto", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", + "toast.language.title": "Idioma", "toast.language.description": "Cambiado a {{language}}", "toast.theme.title": "Tema cambiado", "toast.scheme.title": "Esquema de color", - "toast.permissions.autoaccept.on.title": "Aceptando ediciones automáticamente", - "toast.permissions.autoaccept.on.description": "Los permisos de edición y escritura serán aprobados automáticamente", - "toast.permissions.autoaccept.off.title": "Se dejó de aceptar ediciones automáticamente", - "toast.permissions.autoaccept.off.description": "Los permisos de edición y escritura requerirán aprobación", - "toast.workspace.enabled.title": "Espacios de trabajo habilitados", "toast.workspace.enabled.description": "Ahora se muestran varios worktrees en la barra lateral", "toast.workspace.disabled.title": "Espacios de trabajo deshabilitados", "toast.workspace.disabled.description": "Solo se muestra el worktree principal en la barra lateral", + "toast.permissions.autoaccept.on.title": "Aceptando ediciones automáticamente", + "toast.permissions.autoaccept.on.description": "Los permisos de edición y escritura serán aprobados automáticamente", + "toast.permissions.autoaccept.off.title": "Se dejó de aceptar ediciones automáticamente", + "toast.permissions.autoaccept.off.description": "Los permisos de edición y escritura requerirán aprobación", + "toast.model.none.title": "Ningún modelo seleccionado", "toast.model.none.description": "Conecta un proveedor para resumir esta sesión", "toast.file.loadFailed.title": "Fallo al cargar archivo", - "toast.file.listFailed.title": "Fallo al listar archivos", + "toast.context.noLineSelection.title": "Sin selección de líneas", "toast.context.noLineSelection.description": "Primero selecciona un rango de líneas en una pestaña de archivo.", + "toast.session.share.copyFailed.title": "Fallo al copiar URL al portapapeles", "toast.session.share.success.title": "Sesión compartida", "toast.session.share.success.description": "¡URL compartida copiada al portapapeles!", @@ -380,6 +446,7 @@ export const dict = { "Elemento raíz no encontrado. ¿Olvidaste añadirlo a tu index.html? ¿O tal vez el atributo id está mal escrito?", "error.globalSync.connectFailed": "No se pudo conectar al servidor. ¿Hay un servidor ejecutándose en `{{url}}`?", + "directory.error.invalidUrl": "URL de directorio inválida.", "error.chain.unknown": "Error desconocido", "error.chain.causedBy": "Causado por:", @@ -427,15 +494,17 @@ export const dict = { "session.review.loadingChanges": "Cargando cambios...", "session.review.empty": "No hay cambios en esta sesión aún", "session.review.noChanges": "Sin cambios", + "session.files.selectToOpen": "Selecciona un archivo para abrir", "session.files.all": "Todos los archivos", "session.files.binaryContent": "Archivo binario (el contenido no puede ser mostrado)", + "session.messages.renderEarlier": "Renderizar mensajes anteriores", "session.messages.loadingEarlier": "Cargando mensajes anteriores...", "session.messages.loadEarlier": "Cargar mensajes anteriores", "session.messages.loading": "Cargando mensajes...", - "session.messages.jumpToLatest": "Ir al último", + "session.context.addToContext": "Añadir {{selection}} al contexto", "session.new.worktree.main": "Rama principal", @@ -445,6 +514,11 @@ export const dict = { "session.header.search.placeholder": "Buscar {{project}}", "session.header.searchFiles": "Buscar archivos", + "session.header.openIn": "Abrir en", + "session.header.open.action": "Abrir {{app}}", + "session.header.open.ariaLabel": "Abrir en {{app}}", + "session.header.open.menu": "Opciones de apertura", + "session.header.open.copyPath": "Copiar ruta", "status.popover.trigger": "Estado", "status.popover.ariaLabel": "Configuraciones del servidor", @@ -476,10 +550,10 @@ export const dict = { "terminal.title": "Terminal", "terminal.title.numbered": "Terminal {{number}}", "terminal.close": "Cerrar terminal", - "terminal.connectionLost.title": "Conexión perdida", "terminal.connectionLost.description": "La conexión del terminal se interrumpió. Esto puede ocurrir cuando el servidor se reinicia.", + "common.closeTab": "Cerrar pestaña", "common.dismiss": "Descartar", "common.requestFailed": "Solicitud fallida", @@ -492,8 +566,8 @@ export const dict = { "common.close": "Cerrar", "common.edit": "Editar", "common.loadMore": "Cargar más", - "common.key.esc": "ESC", + "sidebar.menu.toggle": "Alternar menú", "sidebar.nav.projectsAndSessions": "Proyectos y sesiones", "sidebar.settings": "Ajustes", @@ -505,17 +579,23 @@ export const dict = { "sidebar.gettingStarted.line2": "Conecta cualquier proveedor para usar modelos, inc. Claude, GPT, Gemini etc.", "sidebar.project.recentSessions": "Sesiones recientes", "sidebar.project.viewAllSessions": "Ver todas las sesiones", + "sidebar.project.clearNotifications": "Borrar notificaciones", "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Escritorio", "settings.section.server": "Servidor", "settings.tab.general": "General", "settings.tab.shortcuts": "Atajos", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "Integración con WSL", + "settings.desktop.wsl.description": "Ejecutar el servidor OpenCode dentro de WSL en Windows.", "settings.general.section.appearance": "Apariencia", "settings.general.section.notifications": "Notificaciones del sistema", "settings.general.section.updates": "Actualizaciones", "settings.general.section.sounds": "Efectos de sonido", + "settings.general.section.display": "Pantalla", "settings.general.row.language.title": "Idioma", "settings.general.row.language.description": "Cambiar el idioma de visualización para OpenCode", @@ -524,7 +604,12 @@ export const dict = { "settings.general.row.theme.title": "Tema", "settings.general.row.theme.description": "Personaliza el tema de OpenCode.", "settings.general.row.font.title": "Fuente", - "settings.general.row.font.description": "Personaliza la fuente mono usada en bloques de código", + "settings.general.row.font.description": "Personaliza la fuente monoespaciada usada en bloques de código", + + "settings.general.row.wayland.title": "Usar Wayland nativo", + "settings.general.row.wayland.description": "Deshabilitar fallback a X11 en Wayland. Requiere reinicio.", + "settings.general.row.wayland.tooltip": + "En Linux con monitores de frecuencia de actualización mixta, Wayland nativo puede ser más estable.", "settings.general.row.releaseNotes.title": "Notas de la versión", "settings.general.row.releaseNotes.description": @@ -538,7 +623,6 @@ export const dict = { "settings.updates.action.checking": "Buscando...", "settings.updates.toast.latest.title": "Estás al día", "settings.updates.toast.latest.description": "Estás usando la última versión de OpenCode.", - "font.option.ibmPlexMono": "IBM Plex Mono", "font.option.cascadiaCode": "Cascadia Code", "font.option.firaCode": "Fira Code", @@ -551,6 +635,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Alerta 01", "sound.option.alert02": "Alerta 02", "sound.option.alert03": "Alerta 03", @@ -596,6 +681,7 @@ export const dict = { "sound.option.yup04": "Sí 04", "sound.option.yup05": "Sí 05", "sound.option.yup06": "Sí 06", + "settings.general.notifications.agent.title": "Agente", "settings.general.notifications.agent.description": "Mostrar notificación del sistema cuando el agente termine o necesite atención", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index a76e57ff1534..7069fbd98fe1 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -15,12 +15,10 @@ export const dict = { "command.category.agent": "Agent", "command.category.permissions": "Permissions", "command.category.workspace": "Espace de travail", - "command.category.settings": "Paramètres", "theme.scheme.system": "Système", "theme.scheme.light": "Clair", "theme.scheme.dark": "Sombre", - "command.sidebar.toggle": "Basculer la barre latérale", "command.project.open": "Ouvrir un projet", "command.provider.connect": "Connecter un fournisseur", @@ -31,21 +29,19 @@ export const dict = { "command.session.previous.unseen": "Session non lue précédente", "command.session.next.unseen": "Session non lue suivante", "command.session.archive": "Archiver la session", - "command.palette": "Palette de commandes", - "command.theme.cycle": "Changer de thème", "command.theme.set": "Utiliser le thème : {{theme}}", "command.theme.scheme.cycle": "Changer de schéma de couleurs", "command.theme.scheme.set": "Utiliser le schéma de couleurs : {{scheme}}", - "command.language.cycle": "Changer de langue", "command.language.set": "Utiliser la langue : {{language}}", - "command.session.new": "Nouvelle session", "command.file.open": "Ouvrir un fichier", + "command.tab.close": "Fermer l'onglet", "command.context.addSelection": "Ajouter la sélection au contexte", "command.context.addSelection.description": "Ajouter les lignes sélectionnées du fichier actuel", + "command.input.focus": "Focus sur l'entrée", "command.terminal.toggle": "Basculer le terminal", "command.fileTree.toggle": "Basculer l'arborescence des fichiers", "command.review.toggle": "Basculer la revue", @@ -70,6 +66,7 @@ export const dict = { "command.permissions.autoaccept.enable": "Accepter automatiquement les modifications", "command.permissions.autoaccept.disable": "Arrêter l'acceptation automatique des modifications", "command.workspace.toggle": "Basculer les espaces de travail", + "command.workspace.toggle.description": "Activer ou désactiver plusieurs espaces de travail dans la barre latérale", "command.session.undo": "Annuler", "command.session.undo.description": "Annuler le dernier message", "command.session.redo": "Rétablir", @@ -82,32 +79,30 @@ export const dict = { "command.session.share.description": "Partager cette session et copier l'URL dans le presse-papiers", "command.session.unshare": "Ne plus partager la session", "command.session.unshare.description": "Arrêter de partager cette session", - "palette.search.placeholder": "Rechercher des fichiers, des commandes et des sessions", "palette.empty": "Aucun résultat trouvé", "palette.group.commands": "Commandes", "palette.group.files": "Fichiers", - "dialog.provider.search.placeholder": "Rechercher des fournisseurs", "dialog.provider.empty": "Aucun fournisseur trouvé", "dialog.provider.group.popular": "Populaire", "dialog.provider.group.other": "Autre", "dialog.provider.tag.recommended": "Recommandé", + "dialog.provider.opencode.note": "Modèles sélectionnés incluant Claude, GPT, Gemini et plus", "dialog.provider.anthropic.note": "Connectez-vous avec Claude Pro/Max ou une clé API", - "dialog.provider.openai.note": "Connectez-vous avec ChatGPT Pro/Plus ou une clé API", "dialog.provider.copilot.note": "Connectez-vous avec Copilot ou une clé API", - + "dialog.provider.openai.note": "Connectez-vous avec ChatGPT Pro/Plus ou une clé API", + "dialog.provider.google.note": "Modèles Gemini pour des réponses rapides et structurées", + "dialog.provider.openrouter.note": "Accédez à tous les modèles pris en charge depuis un seul fournisseur", + "dialog.provider.vercel.note": "Accès unifié aux modèles d'IA avec routage intelligent", "dialog.model.select.title": "Sélectionner un modèle", "dialog.model.search.placeholder": "Rechercher des modèles", "dialog.model.empty": "Aucun résultat de modèle", "dialog.model.manage": "Gérer les modèles", "dialog.model.manage.description": "Personnalisez les modèles qui apparaissent dans le sélecteur.", - "dialog.model.unpaid.freeModels.title": "Modèles gratuits fournis par OpenCode", "dialog.model.unpaid.addMore.title": "Ajouter plus de modèles de fournisseurs populaires", - "dialog.provider.viewAll": "Voir plus de fournisseurs", - "provider.connect.title": "Connecter {{provider}}", "provider.connect.title.anthropicProMax": "Connexion avec Claude Pro/Max", "provider.connect.selectMethod": "Sélectionnez la méthode de connexion pour {{provider}}.", @@ -142,12 +137,46 @@ export const dict = { "provider.connect.oauth.auto.confirmationCode": "Code de confirmation", "provider.connect.toast.connected.title": "{{provider}} connecté", "provider.connect.toast.connected.description": "Les modèles {{provider}} sont maintenant disponibles.", - + "provider.custom.title": "Fournisseur personnalisé", + "provider.custom.description.prefix": "Configurer un fournisseur compatible OpenAI. Voir la ", + "provider.custom.description.link": "doc de config fournisseur", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ID du fournisseur", + "provider.custom.field.providerID.placeholder": "monfournisseur", + "provider.custom.field.providerID.description": "Lettres minuscules, chiffres, traits d'union ou tirets bas", + "provider.custom.field.name.label": "Nom d'affichage", + "provider.custom.field.name.placeholder": "Mon fournisseur IA", + "provider.custom.field.baseURL.label": "URL de base", + "provider.custom.field.baseURL.placeholder": "https://api.monfournisseur.com/v1", + "provider.custom.field.apiKey.label": "Clé API", + "provider.custom.field.apiKey.placeholder": "Clé API", + "provider.custom.field.apiKey.description": "Optionnel. Laisser vide si vous gérez l'auth via les en-têtes.", + "provider.custom.models.label": "Modèles", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "id-modele", + "provider.custom.models.name.label": "Nom", + "provider.custom.models.name.placeholder": "Nom d'affichage", + "provider.custom.models.remove": "Supprimer le modèle", + "provider.custom.models.add": "Ajouter un modèle", + "provider.custom.headers.label": "En-têtes (optionnel)", + "provider.custom.headers.key.label": "En-tête", + "provider.custom.headers.key.placeholder": "Nom-En-Tête", + "provider.custom.headers.value.label": "Valeur", + "provider.custom.headers.value.placeholder": "valeur", + "provider.custom.headers.remove": "Supprimer l'en-tête", + "provider.custom.headers.add": "Ajouter un en-tête", + "provider.custom.error.providerID.required": "L'ID du fournisseur est requis", + "provider.custom.error.providerID.format": "Utilisez des lettres minuscules, chiffres, traits d'union ou tirets bas", + "provider.custom.error.providerID.exists": "Cet ID de fournisseur existe déjà", + "provider.custom.error.name.required": "Le nom d'affichage est requis", + "provider.custom.error.baseURL.required": "L'URL de base est requise", + "provider.custom.error.baseURL.format": "Doit commencer par http:// ou https://", + "provider.custom.error.required": "Requis", + "provider.custom.error.duplicate": "Doublon", "provider.disconnect.toast.disconnected.title": "{{provider}} déconnecté", "provider.disconnect.toast.disconnected.description": "Les modèles {{provider}} ne sont plus disponibles.", "model.tag.free": "Gratuit", "model.tag.latest": "Dernier", - "model.provider.anthropic": "Anthropic", "model.provider.openai": "OpenAI", "model.provider.google": "Google", @@ -164,6 +193,7 @@ export const dict = { "model.tooltip.context": "Limite de contexte {{limit}}", "common.search.placeholder": "Rechercher", "common.goBack": "Retour", + "common.goForward": "Avancer", "common.loading": "Chargement", "common.loading.ellipsis": "...", "common.cancel": "Annuler", @@ -174,14 +204,12 @@ export const dict = { "common.saving": "Enregistrement...", "common.default": "Défaut", "common.attachment": "pièce jointe", - "prompt.placeholder.shell": "Entrez une commande shell...", "prompt.placeholder.normal": 'Demandez n\'importe quoi... "{{example}}"', "prompt.placeholder.summarizeComments": "Résumer les commentaires…", "prompt.placeholder.summarizeComment": "Résumer le commentaire…", "prompt.mode.shell": "Shell", "prompt.mode.shell.exit": "esc pour quitter", - "prompt.example.1": "Corriger un TODO dans la base de code", "prompt.example.2": "Quelle est la pile technique de ce projet ?", "prompt.example.3": "Réparer les tests échoués", @@ -207,10 +235,10 @@ export const dict = { "prompt.example.23": "Ajouter la pagination à cette liste", "prompt.example.24": "Créer une commande CLI pour...", "prompt.example.25": "Comment fonctionnent les variables d'environnement ici ?", - "prompt.popover.emptyResults": "Aucun résultat correspondant", "prompt.popover.emptyCommands": "Aucune commande correspondante", "prompt.dropzone.label": "Déposez des images ou des PDF ici", + "prompt.dropzone.file.label": "Déposez pour @mentionner le fichier", "prompt.slash.badge.custom": "personnalisé", "prompt.slash.badge.skill": "skill", "prompt.slash.badge.mcp": "mcp", @@ -222,7 +250,6 @@ export const dict = { "prompt.attachment.remove": "Supprimer la pièce jointe", "prompt.action.send": "Envoyer", "prompt.action.stop": "Arrêter", - "prompt.toast.pasteUnsupported.title": "Collage non supporté", "prompt.toast.pasteUnsupported.description": "Seules les images ou les PDF peuvent être collés ici.", "prompt.toast.modelAgentRequired.title": "Sélectionnez un agent et un modèle", @@ -232,24 +259,19 @@ export const dict = { "prompt.toast.shellSendFailed.title": "Échec de l'envoi de la commande shell", "prompt.toast.commandSendFailed.title": "Échec de l'envoi de la commande", "prompt.toast.promptSendFailed.title": "Échec de l'envoi du message", - + "prompt.toast.promptSendFailed.description": "Impossible de récupérer la session", "dialog.mcp.title": "MCPs", "dialog.mcp.description": "{{enabled}} sur {{total}} activés", "dialog.mcp.empty": "Aucun MCP configuré", - "dialog.lsp.empty": "LSPs détectés automatiquement par type de fichier", "dialog.plugins.empty": "Plugins configurés dans opencode.json", - "mcp.status.connected": "connecté", "mcp.status.failed": "échoué", "mcp.status.needs_auth": "nécessite auth", "mcp.status.disabled": "désactivé", - "dialog.fork.empty": "Aucun message à partir duquel bifurquer", - "dialog.directory.search.placeholder": "Rechercher des dossiers", "dialog.directory.empty": "Aucun dossier trouvé", - "dialog.server.title": "Serveurs", "dialog.server.description": "Changez le serveur OpenCode auquel cette application se connecte.", "dialog.server.search.placeholder": "Rechercher des serveurs", @@ -267,14 +289,12 @@ export const dict = { "dialog.server.default.set": "Définir le serveur actuel comme défaut", "dialog.server.default.clear": "Effacer", "dialog.server.action.remove": "Supprimer le serveur", - "dialog.server.menu.edit": "Modifier", "dialog.server.menu.default": "Définir par défaut", "dialog.server.menu.defaultRemove": "Supprimer par défaut", "dialog.server.menu.delete": "Supprimer", "dialog.server.current": "Serveur actuel", "dialog.server.status.default": "Défaut", - "dialog.project.edit.title": "Modifier le projet", "dialog.project.edit.name": "Nom", "dialog.project.edit.icon": "Icône", @@ -283,7 +303,6 @@ export const dict = { "dialog.project.edit.icon.recommended": "Recommandé : 128x128px", "dialog.project.edit.color": "Couleur", "dialog.project.edit.color.select": "Sélectionner la couleur {{color}}", - "dialog.project.edit.worktree.startup": "Script de démarrage de l'espace de travail", "dialog.project.edit.worktree.startup.description": "S'exécute après la création d'un nouvel espace de travail (arbre de travail).", @@ -296,10 +315,8 @@ export const dict = { "context.breakdown.assistant": "Assistant", "context.breakdown.tool": "Appels d'outils", "context.breakdown.other": "Autre", - "context.systemPrompt.title": "Prompt système", "context.rawMessages.title": "Messages bruts", - "context.stats.session": "Session", "context.stats.messages": "Messages", "context.stats.provider": "Fournisseur", @@ -316,36 +333,44 @@ export const dict = { "context.stats.totalCost": "Coût total", "context.stats.sessionCreated": "Session créée", "context.stats.lastActivity": "Dernière activité", - "context.usage.tokens": "Jetons", "context.usage.usage": "Utilisation", "context.usage.cost": "Coût", "context.usage.clickToView": "Cliquez pour voir le contexte", "context.usage.view": "Voir l'utilisation du contexte", - + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", "toast.language.title": "Langue", "toast.language.description": "Passé à {{language}}", - "toast.theme.title": "Thème changé", "toast.scheme.title": "Schéma de couleurs", - + "toast.workspace.enabled.title": "Espaces de travail activés", + "toast.workspace.enabled.description": "Plusieurs worktrees sont désormais affichés dans la barre latérale", + "toast.workspace.disabled.title": "Espaces de travail désactivés", + "toast.workspace.disabled.description": "Seul le worktree principal est affiché dans la barre latérale", "toast.permissions.autoaccept.on.title": "Acceptation auto des modifications", "toast.permissions.autoaccept.on.description": "Les permissions de modification et d'écriture seront automatiquement approuvées", "toast.permissions.autoaccept.off.title": "Arrêt acceptation auto des modifications", "toast.permissions.autoaccept.off.description": "Les permissions de modification et d'écriture nécessiteront une approbation", - - "toast.workspace.enabled.title": "Espaces de travail activés", - "toast.workspace.enabled.description": "Plusieurs worktrees sont désormais affichés dans la barre latérale", - "toast.workspace.disabled.title": "Espaces de travail désactivés", - "toast.workspace.disabled.description": "Seul le worktree principal est affiché dans la barre latérale", - "toast.model.none.title": "Aucun modèle sélectionné", "toast.model.none.description": "Connectez un fournisseur pour résumer cette session", - "toast.file.loadFailed.title": "Échec du chargement du fichier", - "toast.file.listFailed.title": "Échec de la liste des fichiers", "toast.context.noLineSelection.title": "Aucune sélection de lignes", "toast.context.noLineSelection.description": "Sélectionnez d'abord une plage de lignes dans un onglet de fichier.", @@ -354,20 +379,16 @@ export const dict = { "toast.session.share.success.description": "URL de partage copiée dans le presse-papiers !", "toast.session.share.failed.title": "Échec du partage de la session", "toast.session.share.failed.description": "Une erreur s'est produite lors du partage de la session", - "toast.session.unshare.success.title": "Session non partagée", "toast.session.unshare.success.description": "Session non partagée avec succès !", "toast.session.unshare.failed.title": "Échec de l'annulation du partage", "toast.session.unshare.failed.description": "Une erreur s'est produite lors de l'annulation du partage de la session", - "toast.session.listFailed.title": "Échec du chargement des sessions pour {{project}}", - "toast.update.title": "Mise à jour disponible", "toast.update.description": "Une nouvelle version d'OpenCode ({{version}}) est maintenant disponible pour installation.", "toast.update.action.installRestart": "Installer et redémarrer", "toast.update.action.notYet": "Pas encore", - "error.page.title": "Quelque chose s'est mal passé", "error.page.description": "Une erreur s'est produite lors du chargement de l'application.", "error.page.details.label": "Détails de l'erreur", @@ -378,13 +399,11 @@ export const dict = { "error.page.report.prefix": "Veuillez signaler cette erreur à l'équipe OpenCode", "error.page.report.discord": "sur Discord", "error.page.version": "Version : {{version}}", - "error.dev.rootNotFound": "Élément racine introuvable. Avez-vous oublié de l'ajouter à votre index.html ? Ou peut-être que l'attribut id est mal orthographié ?", - "error.globalSync.connectFailed": "Impossible de se connecter au serveur. Y a-t-il un serveur en cours d'exécution à `{{url}}` ?", - + "directory.error.invalidUrl": "Répertoire invalide dans l'URL.", "error.chain.unknown": "Erreur inconnue", "error.chain.causedBy": "Causé par :", "error.chain.apiError": "Erreur API", @@ -407,21 +426,17 @@ export const dict = { "error.chain.configFrontmatterError": "Échec de l'analyse du frontmatter dans {{path}} :\n{{message}}", "error.chain.configInvalid": "Le fichier de configuration à {{path}} est invalide", "error.chain.configInvalidWithMessage": "Le fichier de configuration à {{path}} est invalide : {{message}}", - "notification.permission.title": "Permission requise", "notification.permission.description": "{{sessionTitle}} dans {{projectName}} a besoin d'une permission", "notification.question.title": "Question", "notification.question.description": "{{sessionTitle}} dans {{projectName}} a une question", "notification.action.goToSession": "Aller à la session", - "notification.session.responseReady.title": "Réponse prête", "notification.session.error.title": "Erreur de session", "notification.session.error.fallbackDescription": "Une erreur s'est produite", - "home.recentProjects": "Projets récents", "home.empty.title": "Aucun projet récent", "home.empty.description": "Commencez par ouvrir un projet local", - "session.tab.session": "Session", "session.tab.review": "Revue", "session.tab.context": "Contexte", @@ -439,18 +454,19 @@ export const dict = { "session.messages.loadingEarlier": "Chargement des messages précédents...", "session.messages.loadEarlier": "Charger les messages précédents", "session.messages.loading": "Chargement des messages...", - "session.messages.jumpToLatest": "Aller au dernier", "session.context.addToContext": "Ajouter {{selection}} au contexte", - "session.new.worktree.main": "Branche principale", "session.new.worktree.mainWithBranch": "Branche principale ({{branch}})", "session.new.worktree.create": "Créer un nouvel arbre de travail", "session.new.lastModified": "Dernière modification", - "session.header.search.placeholder": "Rechercher {{project}}", "session.header.searchFiles": "Rechercher des fichiers", - + "session.header.openIn": "Ouvrir dans", + "session.header.open.action": "Ouvrir {{app}}", + "session.header.open.ariaLabel": "Ouvrir dans {{app}}", + "session.header.open.menu": "Options d'ouverture", + "session.header.open.copyPath": "Copier le chemin", "status.popover.trigger": "Statut", "status.popover.ariaLabel": "Configurations des serveurs", "status.popover.tab.servers": "Serveurs", @@ -458,7 +474,6 @@ export const dict = { "status.popover.tab.lsp": "LSP", "status.popover.tab.plugins": "Plugins", "status.popover.action.manageServers": "Gérer les serveurs", - "session.share.popover.title": "Publier sur le web", "session.share.popover.description.shared": "Cette session est publique sur le web. Elle est accessible à toute personne disposant du lien.", @@ -472,16 +487,13 @@ export const dict = { "session.share.action.view": "Voir", "session.share.copy.copied": "Copié", "session.share.copy.copyLink": "Copier le lien", - "lsp.tooltip.none": "Aucun serveur LSP", "lsp.label.connected": "{{count}} LSP", - "prompt.loading": "Chargement du prompt...", "terminal.loading": "Chargement du terminal...", "terminal.title": "Terminal", "terminal.title.numbered": "Terminal {{number}}", "terminal.close": "Fermer le terminal", - "terminal.connectionLost.title": "Connexion perdue", "terminal.connectionLost.description": "La connexion au terminal a été interrompue. Cela peut arriver lorsque le serveur redémarre.", @@ -497,7 +509,6 @@ export const dict = { "common.close": "Fermer", "common.edit": "Modifier", "common.loadMore": "Charger plus", - "common.key.esc": "ESC", "sidebar.menu.toggle": "Basculer le menu", "sidebar.nav.projectsAndSessions": "Projets et sessions", @@ -512,18 +523,20 @@ export const dict = { "Connectez n'importe quel fournisseur pour utiliser des modèles, y compris Claude, GPT, Gemini etc.", "sidebar.project.recentSessions": "Sessions récentes", "sidebar.project.viewAllSessions": "Voir toutes les sessions", - + "sidebar.project.clearNotifications": "Effacer les notifications", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Bureau", "settings.section.server": "Serveur", "settings.tab.general": "Général", "settings.tab.shortcuts": "Raccourcis", - + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "Intégration WSL", + "settings.desktop.wsl.description": "Exécuter le serveur OpenCode dans WSL sur Windows.", "settings.general.section.appearance": "Apparence", "settings.general.section.notifications": "Notifications système", "settings.general.section.updates": "Mises à jour", "settings.general.section.sounds": "Effets sonores", - + "settings.general.section.display": "Affichage", "settings.general.row.language.title": "Langue", "settings.general.row.language.description": "Changer la langue d'affichage pour OpenCode", "settings.general.row.appearance.title": "Apparence", @@ -532,10 +545,12 @@ export const dict = { "settings.general.row.theme.description": "Personnaliser le thème d'OpenCode.", "settings.general.row.font.title": "Police", "settings.general.row.font.description": "Personnaliser la police mono utilisée dans les blocs de code", - + "settings.general.row.wayland.title": "Utiliser Wayland natif", + "settings.general.row.wayland.description": "Désactiver le repli X11 sur Wayland. Nécessite un redémarrage.", + "settings.general.row.wayland.tooltip": + "Sur Linux avec des moniteurs à taux de rafraîchissement mixte, Wayland natif peut être plus stable.", "settings.general.row.releaseNotes.title": "Notes de version", "settings.general.row.releaseNotes.description": 'Afficher des pop-ups "Quoi de neuf" après les mises à jour', - "settings.updates.row.startup.title": "Vérifier les mises à jour au démarrage", "settings.updates.row.startup.description": "Vérifier automatiquement les mises à jour au lancement d'OpenCode", "settings.updates.row.check.title": "Vérifier les mises à jour", @@ -544,7 +559,6 @@ export const dict = { "settings.updates.action.checking": "Vérification...", "settings.updates.toast.latest.title": "Vous êtes à jour", "settings.updates.toast.latest.description": "Vous utilisez la dernière version d'OpenCode.", - "font.option.ibmPlexMono": "IBM Plex Mono", "font.option.cascadiaCode": "Cascadia Code", "font.option.firaCode": "Fira Code", @@ -557,6 +571,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Alerte 01", "sound.option.alert02": "Alerte 02", "sound.option.alert03": "Alerte 03", @@ -610,14 +625,12 @@ export const dict = { "Afficher une notification système lorsqu'une permission est requise", "settings.general.notifications.errors.title": "Erreurs", "settings.general.notifications.errors.description": "Afficher une notification système lorsqu'une erreur se produit", - "settings.general.sounds.agent.title": "Agent", "settings.general.sounds.agent.description": "Jouer un son lorsque l'agent a terminé ou nécessite une attention", "settings.general.sounds.permissions.title": "Permissions", "settings.general.sounds.permissions.description": "Jouer un son lorsqu'une permission est requise", "settings.general.sounds.errors.title": "Erreurs", "settings.general.sounds.errors.description": "Jouer un son lorsqu'une erreur se produit", - "settings.shortcuts.title": "Raccourcis clavier", "settings.shortcuts.reset.button": "Rétablir les défauts", "settings.shortcuts.reset.toast.title": "Raccourcis réinitialisés", @@ -628,14 +641,12 @@ export const dict = { "settings.shortcuts.pressKeys": "Appuyez sur les touches", "settings.shortcuts.search.placeholder": "Rechercher des raccourcis", "settings.shortcuts.search.empty": "Aucun raccourci trouvé", - "settings.shortcuts.group.general": "Général", "settings.shortcuts.group.session": "Session", "settings.shortcuts.group.navigation": "Navigation", "settings.shortcuts.group.modelAndAgent": "Modèle et agent", "settings.shortcuts.group.terminal": "Terminal", "settings.shortcuts.group.prompt": "Prompt", - "settings.providers.title": "Fournisseurs", "settings.providers.description": "Les paramètres des fournisseurs seront configurables ici.", "settings.providers.section.connected": "Fournisseurs connectés", @@ -653,16 +664,13 @@ export const dict = { "settings.commands.description": "Les paramètres des commandes seront configurables ici.", "settings.mcp.title": "MCP", "settings.mcp.description": "Les paramètres MCP seront configurables ici.", - "settings.permissions.title": "Permissions", "settings.permissions.description": "Contrôlez les outils que le serveur peut utiliser par défaut.", "settings.permissions.section.tools": "Outils", "settings.permissions.toast.updateFailed.title": "Échec de la mise à jour des permissions", - "settings.permissions.action.allow": "Autoriser", "settings.permissions.action.ask": "Demander", "settings.permissions.action.deny": "Refuser", - "settings.permissions.tool.read.title": "Lire", "settings.permissions.tool.read.description": "Lecture d'un fichier (correspond au chemin du fichier)", "settings.permissions.tool.edit.title": "Modifier", @@ -697,12 +705,10 @@ export const dict = { "settings.permissions.tool.external_directory.description": "Accéder aux fichiers en dehors du répertoire du projet", "settings.permissions.tool.doom_loop.title": "Boucle infernale", "settings.permissions.tool.doom_loop.description": "Détecter les appels d'outils répétés avec une entrée identique", - "session.delete.failed.title": "Échec de la suppression de la session", "session.delete.title": "Supprimer la session", "session.delete.confirm": 'Supprimer la session "{{name}}" ?', "session.delete.button": "Supprimer la session", - "workspace.new": "Nouvel espace de travail", "workspace.type.local": "local", "workspace.type.sandbox": "bac à sable", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index e41dea9dc735..e7e24a9bd68f 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -15,12 +15,10 @@ export const dict = { "command.category.agent": "エージェント", "command.category.permissions": "権限", "command.category.workspace": "ワークスペース", - "command.category.settings": "設定", "theme.scheme.system": "システム", "theme.scheme.light": "ライト", "theme.scheme.dark": "ダーク", - "command.sidebar.toggle": "サイドバーの切り替え", "command.project.open": "プロジェクトを開く", "command.provider.connect": "プロバイダーに接続", @@ -31,21 +29,19 @@ export const dict = { "command.session.previous.unseen": "前の未読セッション", "command.session.next.unseen": "次の未読セッション", "command.session.archive": "セッションをアーカイブ", - "command.palette": "コマンドパレット", - "command.theme.cycle": "テーマの切り替え", "command.theme.set": "テーマを使用: {{theme}}", "command.theme.scheme.cycle": "配色の切り替え", "command.theme.scheme.set": "配色を使用: {{scheme}}", - "command.language.cycle": "言語の切り替え", "command.language.set": "言語を使用: {{language}}", - "command.session.new": "新しいセッション", "command.file.open": "ファイルを開く", + "command.tab.close": "タブを閉じる", "command.context.addSelection": "選択範囲をコンテキストに追加", "command.context.addSelection.description": "現在のファイルから選択した行を追加", + "command.input.focus": "入力欄にフォーカス", "command.terminal.toggle": "ターミナルの切り替え", "command.fileTree.toggle": "ファイルツリーを切り替え", "command.review.toggle": "レビューの切り替え", @@ -70,6 +66,7 @@ export const dict = { "command.permissions.autoaccept.enable": "編集を自動承認", "command.permissions.autoaccept.disable": "編集の自動承認を停止", "command.workspace.toggle": "ワークスペースを切り替え", + "command.workspace.toggle.description": "サイドバーでの複数のワークスペースの有効化・無効化", "command.session.undo": "元に戻す", "command.session.undo.description": "最後のメッセージを元に戻す", "command.session.redo": "やり直す", @@ -82,32 +79,30 @@ export const dict = { "command.session.share.description": "このセッションを共有しURLをクリップボードにコピー", "command.session.unshare": "セッションの共有を停止", "command.session.unshare.description": "このセッションの共有を停止", - "palette.search.placeholder": "ファイル、コマンド、セッションを検索", "palette.empty": "結果が見つかりません", "palette.group.commands": "コマンド", "palette.group.files": "ファイル", - "dialog.provider.search.placeholder": "プロバイダーを検索", "dialog.provider.empty": "プロバイダーが見つかりません", "dialog.provider.group.popular": "人気", "dialog.provider.group.other": "その他", "dialog.provider.tag.recommended": "推奨", + "dialog.provider.opencode.note": "Claude, GPT, Geminiなどを含む厳選されたモデル", "dialog.provider.anthropic.note": "Claude Pro/MaxまたはAPIキーで接続", - "dialog.provider.openai.note": "ChatGPT Pro/PlusまたはAPIキーで接続", "dialog.provider.copilot.note": "CopilotまたはAPIキーで接続", - + "dialog.provider.openai.note": "ChatGPT Pro/PlusまたはAPIキーで接続", + "dialog.provider.google.note": "高速で構造化された応答のためのGeminiモデル", + "dialog.provider.openrouter.note": "1つのプロバイダーからすべてのサポートされているモデルにアクセス", + "dialog.provider.vercel.note": "スマートルーターによるAIモデルへの統合アクセス", "dialog.model.select.title": "モデルを選択", "dialog.model.search.placeholder": "モデルを検索", "dialog.model.empty": "モデルが見つかりません", "dialog.model.manage": "モデルを管理", "dialog.model.manage.description": "モデルセレクターに表示するモデルをカスタマイズします。", - "dialog.model.unpaid.freeModels.title": "OpenCodeが提供する無料モデル", "dialog.model.unpaid.addMore.title": "人気のプロバイダーからモデルを追加", - "dialog.provider.viewAll": "さらにプロバイダーを表示", - "provider.connect.title": "{{provider}}を接続", "provider.connect.title.anthropicProMax": "Claude Pro/Maxでログイン", "provider.connect.selectMethod": "{{provider}}のログイン方法を選択してください。", @@ -141,12 +136,46 @@ export const dict = { "provider.connect.oauth.auto.confirmationCode": "確認コード", "provider.connect.toast.connected.title": "{{provider}}が接続されました", "provider.connect.toast.connected.description": "{{provider}}モデルが使用可能になりました。", - + "provider.custom.title": "カスタムプロバイダー", + "provider.custom.description.prefix": "OpenAI互換のプロバイダーを設定します。詳細は", + "provider.custom.description.link": "プロバイダー設定ドキュメント", + "provider.custom.description.suffix": "をご覧ください。", + "provider.custom.field.providerID.label": "プロバイダーID", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "小文字、数字、ハイフン、アンダースコア", + "provider.custom.field.name.label": "表示名", + "provider.custom.field.name.placeholder": "My AI Provider", + "provider.custom.field.baseURL.label": "ベースURL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "APIキー", + "provider.custom.field.apiKey.placeholder": "APIキー", + "provider.custom.field.apiKey.description": "オプション。ヘッダーで認証を管理する場合は空のままにしてください。", + "provider.custom.models.label": "モデル", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "名前", + "provider.custom.models.name.placeholder": "表示名", + "provider.custom.models.remove": "モデルを削除", + "provider.custom.models.add": "モデルを追加", + "provider.custom.headers.label": "ヘッダー (オプション)", + "provider.custom.headers.key.label": "ヘッダー", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "値", + "provider.custom.headers.value.placeholder": "value", + "provider.custom.headers.remove": "ヘッダーを削除", + "provider.custom.headers.add": "ヘッダーを追加", + "provider.custom.error.providerID.required": "プロバイダーIDが必要です", + "provider.custom.error.providerID.format": "小文字、数字、ハイフン、アンダースコアを使用してください", + "provider.custom.error.providerID.exists": "そのプロバイダーIDは既に存在します", + "provider.custom.error.name.required": "表示名が必要です", + "provider.custom.error.baseURL.required": "ベースURLが必要です", + "provider.custom.error.baseURL.format": "http:// または https:// で始まる必要があります", + "provider.custom.error.required": "必須", + "provider.custom.error.duplicate": "重複", "provider.disconnect.toast.disconnected.title": "{{provider}}が切断されました", "provider.disconnect.toast.disconnected.description": "{{provider}}のモデルは利用できなくなりました。", "model.tag.free": "無料", "model.tag.latest": "最新", - "model.provider.anthropic": "Anthropic", "model.provider.openai": "OpenAI", "model.provider.google": "Google", @@ -163,6 +192,7 @@ export const dict = { "model.tooltip.context": "コンテキスト上限 {{limit}}", "common.search.placeholder": "検索", "common.goBack": "戻る", + "common.goForward": "進む", "common.loading": "読み込み中", "common.loading.ellipsis": "...", "common.cancel": "キャンセル", @@ -173,14 +203,12 @@ export const dict = { "common.saving": "保存中...", "common.default": "デフォルト", "common.attachment": "添付ファイル", - "prompt.placeholder.shell": "シェルコマンドを入力...", "prompt.placeholder.normal": '何でも聞いてください... "{{example}}"', "prompt.placeholder.summarizeComments": "コメントを要約…", "prompt.placeholder.summarizeComment": "コメントを要約…", - "prompt.mode.shell": "Shell", + "prompt.mode.shell": "シェル", "prompt.mode.shell.exit": "escで終了", - "prompt.example.1": "コードベースのTODOを修正", "prompt.example.2": "このプロジェクトの技術スタックは何ですか?", "prompt.example.3": "壊れたテストを修正", @@ -206,10 +234,10 @@ export const dict = { "prompt.example.23": "このリストにページネーションを追加", "prompt.example.24": "〜のCLIコマンドを作成", "prompt.example.25": "ここでは環境変数はどう機能しますか?", - "prompt.popover.emptyResults": "一致する結果がありません", "prompt.popover.emptyCommands": "一致するコマンドがありません", "prompt.dropzone.label": "画像またはPDFをここにドロップ", + "prompt.dropzone.file.label": "ドロップして@メンションファイルを追加", "prompt.slash.badge.custom": "カスタム", "prompt.slash.badge.skill": "スキル", "prompt.slash.badge.mcp": "mcp", @@ -221,7 +249,6 @@ export const dict = { "prompt.attachment.remove": "添付ファイルを削除", "prompt.action.send": "送信", "prompt.action.stop": "停止", - "prompt.toast.pasteUnsupported.title": "サポートされていない貼り付け", "prompt.toast.pasteUnsupported.description": "ここでは画像またはPDFのみ貼り付け可能です。", "prompt.toast.modelAgentRequired.title": "エージェントとモデルを選択", @@ -231,24 +258,19 @@ export const dict = { "prompt.toast.shellSendFailed.title": "シェルコマンドの送信に失敗しました", "prompt.toast.commandSendFailed.title": "コマンドの送信に失敗しました", "prompt.toast.promptSendFailed.title": "プロンプトの送信に失敗しました", - + "prompt.toast.promptSendFailed.description": "セッションを取得できませんでした", "dialog.mcp.title": "MCP", "dialog.mcp.description": "{{total}}個中{{enabled}}個が有効", "dialog.mcp.empty": "MCPが設定されていません", - "dialog.lsp.empty": "ファイルタイプから自動検出されたLSP", "dialog.plugins.empty": "opencode.jsonで設定されたプラグイン", - "mcp.status.connected": "接続済み", "mcp.status.failed": "失敗", "mcp.status.needs_auth": "認証が必要", "mcp.status.disabled": "無効", - "dialog.fork.empty": "フォーク元のメッセージがありません", - "dialog.directory.search.placeholder": "フォルダを検索", "dialog.directory.empty": "フォルダが見つかりません", - "dialog.server.title": "サーバー", "dialog.server.description": "このアプリが接続するOpenCodeサーバーを切り替えます。", "dialog.server.search.placeholder": "サーバーを検索", @@ -266,14 +288,12 @@ export const dict = { "dialog.server.default.set": "現在のサーバーをデフォルトに設定", "dialog.server.default.clear": "クリア", "dialog.server.action.remove": "サーバーを削除", - "dialog.server.menu.edit": "編集", "dialog.server.menu.default": "デフォルトに設定", "dialog.server.menu.defaultRemove": "デフォルト設定を解除", "dialog.server.menu.delete": "削除", "dialog.server.current": "現在のサーバー", "dialog.server.status.default": "デフォルト", - "dialog.project.edit.title": "プロジェクトを編集", "dialog.project.edit.name": "名前", "dialog.project.edit.icon": "アイコン", @@ -282,7 +302,6 @@ export const dict = { "dialog.project.edit.icon.recommended": "推奨: 128x128px", "dialog.project.edit.color": "色", "dialog.project.edit.color.select": "{{color}}の色を選択", - "dialog.project.edit.worktree.startup": "ワークスペース起動スクリプト", "dialog.project.edit.worktree.startup.description": "新しいワークスペース (ワークツリー) を作成した後に実行されます。", @@ -294,10 +313,8 @@ export const dict = { "context.breakdown.assistant": "アシスタント", "context.breakdown.tool": "ツール呼び出し", "context.breakdown.other": "その他", - "context.systemPrompt.title": "システムプロンプト", "context.rawMessages.title": "生のメッセージ", - "context.stats.session": "セッション", "context.stats.messages": "メッセージ", "context.stats.provider": "プロバイダー", @@ -314,29 +331,42 @@ export const dict = { "context.stats.totalCost": "総コスト", "context.stats.sessionCreated": "セッション作成日時", "context.stats.lastActivity": "最終アクティビティ", - "context.usage.tokens": "トークン", "context.usage.usage": "使用量", "context.usage.cost": "コスト", "context.usage.clickToView": "クリックしてコンテキストを表示", "context.usage.view": "コンテキスト使用量を表示", - + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", "toast.language.title": "言語", "toast.language.description": "{{language}}に切り替えました", - "toast.theme.title": "テーマが切り替わりました", "toast.scheme.title": "配色", - + "toast.workspace.enabled.title": "ワークスペースが有効になりました", + "toast.workspace.enabled.description": "サイドバーに複数のワークツリーが表示されます", + "toast.workspace.disabled.title": "ワークスペースが無効になりました", + "toast.workspace.disabled.description": "サイドバーにはメインのワークツリーのみが表示されます", "toast.permissions.autoaccept.on.title": "編集を自動承認中", "toast.permissions.autoaccept.on.description": "編集と書き込みの権限は自動的に承認されます", "toast.permissions.autoaccept.off.title": "編集の自動承認を停止しました", "toast.permissions.autoaccept.off.description": "編集と書き込みの権限には承認が必要です", - "toast.model.none.title": "モデルが選択されていません", "toast.model.none.description": "このセッションを要約するにはプロバイダーを接続してください", - "toast.file.loadFailed.title": "ファイルの読み込みに失敗しました", - "toast.file.listFailed.title": "ファイル一覧の取得に失敗しました", "toast.context.noLineSelection.title": "行が選択されていません", "toast.context.noLineSelection.description": "まずファイルタブで行範囲を選択してください。", @@ -345,19 +375,15 @@ export const dict = { "toast.session.share.success.description": "共有URLをクリップボードにコピーしました!", "toast.session.share.failed.title": "セッションの共有に失敗しました", "toast.session.share.failed.description": "セッションの共有中にエラーが発生しました", - "toast.session.unshare.success.title": "セッションの共有を解除しました", "toast.session.unshare.success.description": "セッションの共有解除に成功しました!", "toast.session.unshare.failed.title": "セッションの共有解除に失敗しました", "toast.session.unshare.failed.description": "セッションの共有解除中にエラーが発生しました", - "toast.session.listFailed.title": "{{project}}のセッション読み込みに失敗しました", - "toast.update.title": "アップデートが利用可能です", "toast.update.description": "OpenCodeの新しいバージョン ({{version}}) がインストール可能です。", "toast.update.action.installRestart": "インストールして再起動", "toast.update.action.notYet": "今はしない", - "error.page.title": "問題が発生しました", "error.page.description": "アプリケーションの読み込み中にエラーが発生しました。", "error.page.details.label": "エラー詳細", @@ -368,12 +394,10 @@ export const dict = { "error.page.report.prefix": "このエラーをOpenCodeチームに報告してください: ", "error.page.report.discord": "Discord", "error.page.version": "バージョン: {{version}}", - "error.dev.rootNotFound": "ルート要素が見つかりません。index.htmlに追加するのを忘れていませんか?またはid属性のスペルが間違っていませんか?", - "error.globalSync.connectFailed": "サーバーに接続できませんでした。`{{url}}`でサーバーが実行されていますか?", - + "directory.error.invalidUrl": "URL内のディレクトリが無効です。", "error.chain.unknown": "不明なエラー", "error.chain.causedBy": "原因:", "error.chain.apiError": "APIエラー", @@ -394,21 +418,17 @@ export const dict = { "error.chain.configFrontmatterError": "{{path}} のフロントマターの解析に失敗しました:\n{{message}}", "error.chain.configInvalid": "{{path}} の設定ファイルが無効です", "error.chain.configInvalidWithMessage": "{{path}} の設定ファイルが無効です: {{message}}", - "notification.permission.title": "権限が必要です", "notification.permission.description": "{{projectName}} の {{sessionTitle}} が権限を必要としています", "notification.question.title": "質問", "notification.question.description": "{{projectName}} の {{sessionTitle}} から質問があります", "notification.action.goToSession": "セッションへ移動", - "notification.session.responseReady.title": "応答の準備ができました", "notification.session.error.title": "セッションエラー", "notification.session.error.fallbackDescription": "エラーが発生しました", - "home.recentProjects": "最近のプロジェクト", "home.empty.title": "最近のプロジェクトはありません", "home.empty.description": "ローカルプロジェクトを開いて始めましょう", - "session.tab.session": "セッション", "session.tab.review": "レビュー", "session.tab.context": "コンテキスト", @@ -426,18 +446,19 @@ export const dict = { "session.messages.loadingEarlier": "以前のメッセージを読み込み中...", "session.messages.loadEarlier": "以前のメッセージを読み込む", "session.messages.loading": "メッセージを読み込み中...", - "session.messages.jumpToLatest": "最新へジャンプ", "session.context.addToContext": "{{selection}}をコンテキストに追加", - "session.new.worktree.main": "メインブランチ", "session.new.worktree.mainWithBranch": "メインブランチ ({{branch}})", "session.new.worktree.create": "新しいワークツリーを作成", "session.new.lastModified": "最終更新", - "session.header.search.placeholder": "{{project}}を検索", "session.header.searchFiles": "ファイルを検索", - + "session.header.openIn": "で開く", + "session.header.open.action": "{{app}}を開く", + "session.header.open.ariaLabel": "{{app}}で開く", + "session.header.open.menu": "開くオプション", + "session.header.open.copyPath": "パスをコピー", "status.popover.trigger": "ステータス", "status.popover.ariaLabel": "サーバー設定", "status.popover.tab.servers": "サーバー", @@ -445,7 +466,6 @@ export const dict = { "status.popover.tab.lsp": "LSP", "status.popover.tab.plugins": "プラグイン", "status.popover.action.manageServers": "サーバーを管理", - "session.share.popover.title": "ウェブで公開", "session.share.popover.description.shared": "このセッションはウェブで公開されています。リンクを知っている人なら誰でもアクセスできます。", @@ -459,16 +479,13 @@ export const dict = { "session.share.action.view": "表示", "session.share.copy.copied": "コピーしました", "session.share.copy.copyLink": "リンクをコピー", - "lsp.tooltip.none": "LSPサーバーなし", "lsp.label.connected": "{{count}} LSP", - "prompt.loading": "プロンプトを読み込み中...", "terminal.loading": "ターミナルを読み込み中...", "terminal.title": "ターミナル", "terminal.title.numbered": "ターミナル {{number}}", "terminal.close": "ターミナルを閉じる", - "terminal.connectionLost.title": "接続が失われました", "terminal.connectionLost.description": "ターミナルの接続が中断されました。これはサーバーが再起動したときに発生することがあります。", @@ -484,7 +501,6 @@ export const dict = { "common.close": "閉じる", "common.edit": "編集", "common.loadMore": "さらに読み込む", - "common.key.esc": "ESC", "sidebar.menu.toggle": "メニューを切り替え", "sidebar.nav.projectsAndSessions": "プロジェクトとセッション", @@ -497,18 +513,20 @@ export const dict = { "sidebar.gettingStarted.line2": "プロバイダーを接続して、Claude、GPT、Geminiなどのモデルを使用できます。", "sidebar.project.recentSessions": "最近のセッション", "sidebar.project.viewAllSessions": "すべてのセッションを表示", - + "sidebar.project.clearNotifications": "通知をクリア", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "デスクトップ", "settings.section.server": "サーバー", "settings.tab.general": "一般", "settings.tab.shortcuts": "ショートカット", - + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL統合", + "settings.desktop.wsl.description": "Windows上のWSL内でOpenCodeサーバーを実行します。", "settings.general.section.appearance": "外観", "settings.general.section.notifications": "システム通知", "settings.general.section.updates": "アップデート", "settings.general.section.sounds": "効果音", - + "settings.general.section.display": "ディスプレイ", "settings.general.row.language.title": "言語", "settings.general.row.language.description": "OpenCodeの表示言語を変更します", "settings.general.row.appearance.title": "外観", @@ -517,10 +535,12 @@ export const dict = { "settings.general.row.theme.description": "OpenCodeのテーマをカスタマイズします。", "settings.general.row.font.title": "フォント", "settings.general.row.font.description": "コードブロックで使用する等幅フォントをカスタマイズします", - + "settings.general.row.wayland.title": "ネイティブWaylandを使用", + "settings.general.row.wayland.description": "WaylandでのX11フォールバックを無効にします。再起動が必要です。", + "settings.general.row.wayland.tooltip": + "リフレッシュレートが混在するモニターを使用しているLinuxでは、ネイティブWaylandの方が安定する場合があります。", "settings.general.row.releaseNotes.title": "リリースノート", "settings.general.row.releaseNotes.description": "アップデート後に「新機能」ポップアップを表示", - "settings.updates.row.startup.title": "起動時にアップデートを確認", "settings.updates.row.startup.description": "OpenCode の起動時に自動でアップデートを確認します", "settings.updates.row.check.title": "アップデートを確認", @@ -529,7 +549,6 @@ export const dict = { "settings.updates.action.checking": "確認中...", "settings.updates.toast.latest.title": "最新です", "settings.updates.toast.latest.description": "OpenCode は最新バージョンです。", - "font.option.ibmPlexMono": "IBM Plex Mono", "font.option.cascadiaCode": "Cascadia Code", "font.option.firaCode": "Fira Code", @@ -542,6 +561,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "アラート 01", "sound.option.alert02": "アラート 02", "sound.option.alert03": "アラート 03", @@ -594,14 +614,12 @@ export const dict = { "settings.general.notifications.permissions.description": "権限が必要な場合にシステム通知を表示します", "settings.general.notifications.errors.title": "エラー", "settings.general.notifications.errors.description": "エラーが発生した場合にシステム通知を表示します", - "settings.general.sounds.agent.title": "エージェント", "settings.general.sounds.agent.description": "エージェントが完了したか、注意が必要な場合に音を再生します", "settings.general.sounds.permissions.title": "権限", "settings.general.sounds.permissions.description": "権限が必要な場合に音を再生します", "settings.general.sounds.errors.title": "エラー", "settings.general.sounds.errors.description": "エラーが発生した場合に音を再生します", - "settings.shortcuts.title": "キーボードショートカット", "settings.shortcuts.reset.button": "デフォルトにリセット", "settings.shortcuts.reset.toast.title": "ショートカットをリセットしました", @@ -612,14 +630,12 @@ export const dict = { "settings.shortcuts.pressKeys": "キーを押してください", "settings.shortcuts.search.placeholder": "ショートカットを検索", "settings.shortcuts.search.empty": "ショートカットが見つかりません", - "settings.shortcuts.group.general": "一般", "settings.shortcuts.group.session": "セッション", "settings.shortcuts.group.navigation": "ナビゲーション", "settings.shortcuts.group.modelAndAgent": "モデルとエージェント", "settings.shortcuts.group.terminal": "ターミナル", "settings.shortcuts.group.prompt": "プロンプト", - "settings.providers.title": "プロバイダー", "settings.providers.description": "プロバイダー設定はここで構成できます。", "settings.providers.section.connected": "接続済みプロバイダー", @@ -637,16 +653,13 @@ export const dict = { "settings.commands.description": "コマンド設定はここで構成できます。", "settings.mcp.title": "MCP", "settings.mcp.description": "MCP設定はここで構成できます。", - "settings.permissions.title": "権限", "settings.permissions.description": "サーバーがデフォルトで使用できるツールを制御します。", "settings.permissions.section.tools": "ツール", "settings.permissions.toast.updateFailed.title": "権限の更新に失敗しました", - "settings.permissions.action.allow": "許可", "settings.permissions.action.ask": "確認", "settings.permissions.action.deny": "拒否", - "settings.permissions.tool.read.title": "読み込み", "settings.permissions.tool.read.description": "ファイルの読み込み (ファイルパスに一致)", "settings.permissions.tool.edit.title": "編集", @@ -669,22 +682,20 @@ export const dict = { "settings.permissions.tool.todoread.description": "Todoリストの読み込み", "settings.permissions.tool.todowrite.title": "Todo書き込み", "settings.permissions.tool.todowrite.description": "Todoリストの更新", - "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.title": "Web取得", "settings.permissions.tool.webfetch.description": "URLからコンテンツを取得", - "settings.permissions.tool.websearch.title": "Web Search", + "settings.permissions.tool.websearch.title": "Web検索", "settings.permissions.tool.websearch.description": "ウェブを検索", - "settings.permissions.tool.codesearch.title": "Code Search", + "settings.permissions.tool.codesearch.title": "コード検索", "settings.permissions.tool.codesearch.description": "ウェブ上のコードを検索", "settings.permissions.tool.external_directory.title": "外部ディレクトリ", "settings.permissions.tool.external_directory.description": "プロジェクトディレクトリ外のファイルへのアクセス", - "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.title": "無限ループ", "settings.permissions.tool.doom_loop.description": "同一入力による繰り返しのツール呼び出しを検出", - "session.delete.failed.title": "セッションの削除に失敗しました", "session.delete.title": "セッションの削除", "session.delete.confirm": 'セッション "{{name}}" を削除しますか?', "session.delete.button": "セッションを削除", - "workspace.new": "新しいワークスペース", "workspace.type.local": "ローカル", "workspace.type.sandbox": "サンドボックス", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index a4f42a583e02..650b7e662a36 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -19,12 +19,10 @@ export const dict = { "command.category.agent": "에이전트", "command.category.permissions": "권한", "command.category.workspace": "작업 공간", - "command.category.settings": "설정", "theme.scheme.system": "시스템", "theme.scheme.light": "라이트", "theme.scheme.dark": "다크", - "command.sidebar.toggle": "사이드바 토글", "command.project.open": "프로젝트 열기", "command.provider.connect": "공급자 연결", @@ -35,21 +33,19 @@ export const dict = { "command.session.previous.unseen": "이전 읽지 않은 세션", "command.session.next.unseen": "다음 읽지 않은 세션", "command.session.archive": "세션 보관", - "command.palette": "명령 팔레트", - "command.theme.cycle": "테마 순환", "command.theme.set": "테마 사용: {{theme}}", "command.theme.scheme.cycle": "색상 테마 순환", "command.theme.scheme.set": "색상 테마 사용: {{scheme}}", - "command.language.cycle": "언어 순환", "command.language.set": "언어 사용: {{language}}", - "command.session.new": "새 세션", "command.file.open": "파일 열기", + "command.tab.close": "탭 닫기", "command.context.addSelection": "선택 영역을 컨텍스트에 추가", "command.context.addSelection.description": "현재 파일에서 선택한 줄을 추가", + "command.input.focus": "입력창 포커스", "command.terminal.toggle": "터미널 토글", "command.fileTree.toggle": "파일 트리 토글", "command.review.toggle": "검토 토글", @@ -74,6 +70,7 @@ export const dict = { "command.permissions.autoaccept.enable": "편집 자동 수락", "command.permissions.autoaccept.disable": "편집 자동 수락 중지", "command.workspace.toggle": "작업 공간 전환", + "command.workspace.toggle.description": "사이드바에서 다중 작업 공간 활성화 또는 비활성화", "command.session.undo": "실행 취소", "command.session.undo.description": "마지막 메시지 실행 취소", "command.session.redo": "다시 실행", @@ -86,32 +83,30 @@ export const dict = { "command.session.share.description": "이 세션을 공유하고 URL을 클립보드에 복사", "command.session.unshare": "세션 공유 중지", "command.session.unshare.description": "이 세션 공유 중지", - "palette.search.placeholder": "파일, 명령어 및 세션 검색", "palette.empty": "결과 없음", "palette.group.commands": "명령어", "palette.group.files": "파일", - "dialog.provider.search.placeholder": "공급자 검색", "dialog.provider.empty": "공급자 없음", "dialog.provider.group.popular": "인기", "dialog.provider.group.other": "기타", "dialog.provider.tag.recommended": "추천", + "dialog.provider.opencode.note": "Claude, GPT, Gemini 등을 포함한 엄선된 모델", "dialog.provider.anthropic.note": "Claude Pro/Max 또는 API 키로 연결", - "dialog.provider.openai.note": "ChatGPT Pro/Plus 또는 API 키로 연결", "dialog.provider.copilot.note": "Copilot 또는 API 키로 연결", - + "dialog.provider.openai.note": "ChatGPT Pro/Plus 또는 API 키로 연결", + "dialog.provider.google.note": "빠르고 구조화된 응답을 위한 Gemini 모델", + "dialog.provider.openrouter.note": "모든 지원 모델을 단일 공급자에서 액세스", + "dialog.provider.vercel.note": "스마트 라우팅을 통한 AI 모델 통합 액세스", "dialog.model.select.title": "모델 선택", "dialog.model.search.placeholder": "모델 검색", "dialog.model.empty": "모델 결과 없음", "dialog.model.manage": "모델 관리", "dialog.model.manage.description": "모델 선택기에 표시할 모델 사용자 지정", - "dialog.model.unpaid.freeModels.title": "OpenCode에서 제공하는 무료 모델", "dialog.model.unpaid.addMore.title": "인기 공급자의 모델 추가", - "dialog.provider.viewAll": "더 많은 공급자 보기", - "provider.connect.title": "{{provider}} 연결", "provider.connect.title.anthropicProMax": "Claude Pro/Max로 로그인", "provider.connect.selectMethod": "{{provider}} 로그인 방법 선택", @@ -127,10 +122,10 @@ export const dict = { "provider.connect.opencodeZen.line1": "OpenCode Zen은 코딩 에이전트를 위해 최적화된 신뢰할 수 있는 엄선된 모델에 대한 액세스를 제공합니다.", "provider.connect.opencodeZen.line2": "단일 API 키로 Claude, GPT, Gemini, GLM 등 다양한 모델에 액세스할 수 있습니다.", - "provider.connect.opencodeZen.visit.prefix": "", + "provider.connect.opencodeZen.visit.prefix": "다음 ", "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": "를 방문하여 API 키를 받으세요.", - "provider.connect.oauth.code.visit.prefix": "", + "provider.connect.opencodeZen.visit.suffix": "을 방문하여 API 키를 받으세요.", + "provider.connect.oauth.code.visit.prefix": "다음 ", "provider.connect.oauth.code.visit.link": "이 링크", "provider.connect.oauth.code.visit.suffix": "를 방문하여 인증 코드를 받아 계정을 연결하고 OpenCode에서 {{provider}} 모델을 사용하세요.", @@ -138,19 +133,53 @@ export const dict = { "provider.connect.oauth.code.placeholder": "인증 코드", "provider.connect.oauth.code.required": "인증 코드가 필요합니다", "provider.connect.oauth.code.invalid": "유효하지 않은 인증 코드", - "provider.connect.oauth.auto.visit.prefix": "", + "provider.connect.oauth.auto.visit.prefix": "다음 ", "provider.connect.oauth.auto.visit.link": "이 링크", "provider.connect.oauth.auto.visit.suffix": "를 방문하고 아래 코드를 입력하여 계정을 연결하고 OpenCode에서 {{provider}} 모델을 사용하세요.", "provider.connect.oauth.auto.confirmationCode": "확인 코드", "provider.connect.toast.connected.title": "{{provider}} 연결됨", "provider.connect.toast.connected.description": "이제 {{provider}} 모델을 사용할 수 있습니다.", - + "provider.custom.title": "사용자 지정 공급자", + "provider.custom.description.prefix": "OpenAI 호환 공급자를 구성합니다. ", + "provider.custom.description.link": "공급자 구성 문서", + "provider.custom.description.suffix": "를 참조하세요.", + "provider.custom.field.providerID.label": "공급자 ID", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "소문자, 숫자, 하이픈 또는 밑줄", + "provider.custom.field.name.label": "표시 이름", + "provider.custom.field.name.placeholder": "내 AI 공급자", + "provider.custom.field.baseURL.label": "기본 URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API 키", + "provider.custom.field.apiKey.placeholder": "API 키", + "provider.custom.field.apiKey.description": "선택 사항입니다. 헤더를 통해 인증을 관리하는 경우 비워 두세요.", + "provider.custom.models.label": "모델", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "이름", + "provider.custom.models.name.placeholder": "표시 이름", + "provider.custom.models.remove": "모델 제거", + "provider.custom.models.add": "모델 추가", + "provider.custom.headers.label": "헤더 (선택 사항)", + "provider.custom.headers.key.label": "헤더", + "provider.custom.headers.key.placeholder": "헤더 이름", + "provider.custom.headers.value.label": "값", + "provider.custom.headers.value.placeholder": "값", + "provider.custom.headers.remove": "헤더 제거", + "provider.custom.headers.add": "헤더 추가", + "provider.custom.error.providerID.required": "공급자 ID가 필요합니다", + "provider.custom.error.providerID.format": "소문자, 숫자, 하이픈 또는 밑줄을 사용하세요", + "provider.custom.error.providerID.exists": "해당 공급자 ID가 이미 존재합니다", + "provider.custom.error.name.required": "표시 이름이 필요합니다", + "provider.custom.error.baseURL.required": "기본 URL이 필요합니다", + "provider.custom.error.baseURL.format": "http:// 또는 https://로 시작해야 합니다", + "provider.custom.error.required": "필수", + "provider.custom.error.duplicate": "중복", "provider.disconnect.toast.disconnected.title": "{{provider}} 연결 해제됨", "provider.disconnect.toast.disconnected.description": "{{provider}} 모델을 더 이상 사용할 수 없습니다.", "model.tag.free": "무료", "model.tag.latest": "최신", - "model.provider.anthropic": "Anthropic", "model.provider.openai": "OpenAI", "model.provider.google": "Google", @@ -167,6 +196,7 @@ export const dict = { "model.tooltip.context": "컨텍스트 제한 {{limit}}", "common.search.placeholder": "검색", "common.goBack": "뒤로 가기", + "common.goForward": "앞으로 가기", "common.loading": "로딩 중", "common.loading.ellipsis": "...", "common.cancel": "취소", @@ -177,14 +207,12 @@ export const dict = { "common.saving": "저장 중...", "common.default": "기본값", "common.attachment": "첨부 파일", - "prompt.placeholder.shell": "셸 명령어 입력...", "prompt.placeholder.normal": '무엇이든 물어보세요... "{{example}}"', "prompt.placeholder.summarizeComments": "댓글 요약…", "prompt.placeholder.summarizeComment": "댓글 요약…", "prompt.mode.shell": "셸", "prompt.mode.shell.exit": "종료하려면 esc", - "prompt.example.1": "코드베이스의 TODO 수정", "prompt.example.2": "이 프로젝트의 기술 스택이 무엇인가요?", "prompt.example.3": "고장 난 테스트 수정", @@ -210,10 +238,10 @@ export const dict = { "prompt.example.23": "이 목록에 페이지네이션 추가", "prompt.example.24": "...를 위한 CLI 명령어 생성", "prompt.example.25": "여기서 환경 변수는 어떻게 작동하나요?", - "prompt.popover.emptyResults": "일치하는 결과 없음", "prompt.popover.emptyCommands": "일치하는 명령어 없음", "prompt.dropzone.label": "이미지나 PDF를 여기에 드롭하세요", + "prompt.dropzone.file.label": "드롭하여 파일 @멘션 추가", "prompt.slash.badge.custom": "사용자 지정", "prompt.slash.badge.skill": "스킬", "prompt.slash.badge.mcp": "mcp", @@ -225,7 +253,6 @@ export const dict = { "prompt.attachment.remove": "첨부 파일 제거", "prompt.action.send": "전송", "prompt.action.stop": "중지", - "prompt.toast.pasteUnsupported.title": "지원되지 않는 붙여넣기", "prompt.toast.pasteUnsupported.description": "이미지나 PDF만 붙여넣을 수 있습니다.", "prompt.toast.modelAgentRequired.title": "에이전트 및 모델 선택", @@ -235,24 +262,19 @@ export const dict = { "prompt.toast.shellSendFailed.title": "셸 명령 전송 실패", "prompt.toast.commandSendFailed.title": "명령 전송 실패", "prompt.toast.promptSendFailed.title": "프롬프트 전송 실패", - + "prompt.toast.promptSendFailed.description": "세션을 가져올 수 없습니다", "dialog.mcp.title": "MCP", "dialog.mcp.description": "{{total}}개 중 {{enabled}}개 활성화됨", "dialog.mcp.empty": "구성된 MCP 없음", - "dialog.lsp.empty": "파일 유형에서 자동 감지된 LSP", "dialog.plugins.empty": "opencode.json에 구성된 플러그인", - "mcp.status.connected": "연결됨", "mcp.status.failed": "실패", "mcp.status.needs_auth": "인증 필요", "mcp.status.disabled": "비활성화됨", - "dialog.fork.empty": "분기할 메시지 없음", - "dialog.directory.search.placeholder": "폴더 검색", "dialog.directory.empty": "폴더 없음", - "dialog.server.title": "서버", "dialog.server.description": "이 앱이 연결할 OpenCode 서버를 전환합니다.", "dialog.server.search.placeholder": "서버 검색", @@ -270,14 +292,12 @@ export const dict = { "dialog.server.default.set": "현재 서버를 기본값으로 설정", "dialog.server.default.clear": "지우기", "dialog.server.action.remove": "서버 제거", - "dialog.server.menu.edit": "편집", "dialog.server.menu.default": "기본값으로 설정", "dialog.server.menu.defaultRemove": "기본값 제거", "dialog.server.menu.delete": "삭제", "dialog.server.current": "현재 서버", "dialog.server.status.default": "기본값", - "dialog.project.edit.title": "프로젝트 편집", "dialog.project.edit.name": "이름", "dialog.project.edit.icon": "아이콘", @@ -286,7 +306,6 @@ export const dict = { "dialog.project.edit.icon.recommended": "권장: 128x128px", "dialog.project.edit.color": "색상", "dialog.project.edit.color.select": "{{color}} 색상 선택", - "dialog.project.edit.worktree.startup": "작업 공간 시작 스크립트", "dialog.project.edit.worktree.startup.description": "새 작업 공간(작업 트리)을 만든 뒤 실행됩니다.", "dialog.project.edit.worktree.startup.placeholder": "예: bun install", @@ -297,10 +316,8 @@ export const dict = { "context.breakdown.assistant": "어시스턴트", "context.breakdown.tool": "도구 호출", "context.breakdown.other": "기타", - "context.systemPrompt.title": "시스템 프롬프트", "context.rawMessages.title": "원시 메시지", - "context.stats.session": "세션", "context.stats.messages": "메시지", "context.stats.provider": "공급자", @@ -317,34 +334,42 @@ export const dict = { "context.stats.totalCost": "총 비용", "context.stats.sessionCreated": "세션 생성됨", "context.stats.lastActivity": "최근 활동", - "context.usage.tokens": "토큰", "context.usage.usage": "사용량", "context.usage.cost": "비용", "context.usage.clickToView": "컨텍스트를 보려면 클릭", "context.usage.view": "컨텍스트 사용량 보기", - + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", "toast.language.title": "언어", "toast.language.description": "{{language}}(으)로 전환됨", - "toast.theme.title": "테마 전환됨", "toast.scheme.title": "색상 테마", - - "toast.permissions.autoaccept.on.title": "편집 자동 수락 중", - "toast.permissions.autoaccept.on.description": "편집 및 쓰기 권한이 자동으로 승인됩니다", - "toast.permissions.autoaccept.off.title": "편집 자동 수락 중지됨", - "toast.permissions.autoaccept.off.description": "편집 및 쓰기 권한 승인이 필요합니다", - "toast.workspace.enabled.title": "작업 공간 활성화됨", "toast.workspace.enabled.description": "이제 사이드바에 여러 작업 트리가 표시됩니다", "toast.workspace.disabled.title": "작업 공간 비활성화됨", "toast.workspace.disabled.description": "사이드바에 메인 작업 트리만 표시됩니다", - + "toast.permissions.autoaccept.on.title": "편집 자동 수락 중", + "toast.permissions.autoaccept.on.description": "편집 및 쓰기 권한이 자동으로 승인됩니다", + "toast.permissions.autoaccept.off.title": "편집 자동 수락 중지됨", + "toast.permissions.autoaccept.off.description": "편집 및 쓰기 권한 승인이 필요합니다", "toast.model.none.title": "선택된 모델 없음", "toast.model.none.description": "이 세션을 요약하려면 공급자를 연결하세요", - "toast.file.loadFailed.title": "파일 로드 실패", - "toast.file.listFailed.title": "파일 목록을 불러오지 못했습니다", "toast.context.noLineSelection.title": "줄 선택 없음", "toast.context.noLineSelection.description": "먼저 파일 탭에서 줄 범위를 선택하세요.", @@ -353,19 +378,15 @@ export const dict = { "toast.session.share.success.description": "공유 URL이 클립보드에 복사되었습니다!", "toast.session.share.failed.title": "세션 공유 실패", "toast.session.share.failed.description": "세션을 공유하는 동안 오류가 발생했습니다", - "toast.session.unshare.success.title": "세션 공유 해제됨", "toast.session.unshare.success.description": "세션 공유가 성공적으로 해제되었습니다!", "toast.session.unshare.failed.title": "세션 공유 해제 실패", "toast.session.unshare.failed.description": "세션 공유를 해제하는 동안 오류가 발생했습니다", - "toast.session.listFailed.title": "{{project}}에 대한 세션을 로드하지 못했습니다", - "toast.update.title": "업데이트 가능", "toast.update.description": "OpenCode의 새 버전({{version}})을 설치할 수 있습니다.", "toast.update.action.installRestart": "설치 및 다시 시작", "toast.update.action.notYet": "나중에", - "error.page.title": "문제가 발생했습니다", "error.page.description": "애플리케이션을 로드하는 동안 오류가 발생했습니다.", "error.page.details.label": "오류 세부 정보", @@ -376,12 +397,10 @@ export const dict = { "error.page.report.prefix": "이 오류를 OpenCode 팀에 제보해 주세요: ", "error.page.report.discord": "Discord", "error.page.version": "버전: {{version}}", - "error.dev.rootNotFound": "루트 요소를 찾을 수 없습니다. index.html에 추가하는 것을 잊으셨나요? 또는 id 속성의 철자가 틀렸을 수 있습니다.", - "error.globalSync.connectFailed": "서버에 연결할 수 없습니다. `{{url}}`에서 서버가 실행 중인가요?", - + "directory.error.invalidUrl": "URL에 유효하지 않은 디렉터리가 있습니다.", "error.chain.unknown": "알 수 없는 오류", "error.chain.causedBy": "원인:", "error.chain.apiError": "API 오류", @@ -401,21 +420,17 @@ export const dict = { "error.chain.configFrontmatterError": "{{path}}의 frontmatter 파싱 실패:\n{{message}}", "error.chain.configInvalid": "{{path}}의 구성 파일이 유효하지 않습니다", "error.chain.configInvalidWithMessage": "{{path}}의 구성 파일이 유효하지 않습니다: {{message}}", - "notification.permission.title": "권한 필요", "notification.permission.description": "{{projectName}}의 {{sessionTitle}}에서 권한이 필요합니다", "notification.question.title": "질문", "notification.question.description": "{{projectName}}의 {{sessionTitle}}에서 질문이 있습니다", "notification.action.goToSession": "세션으로 이동", - "notification.session.responseReady.title": "응답 준비됨", "notification.session.error.title": "세션 오류", "notification.session.error.fallbackDescription": "오류가 발생했습니다", - "home.recentProjects": "최근 프로젝트", "home.empty.title": "최근 프로젝트 없음", "home.empty.description": "로컬 프로젝트를 열어 시작하세요", - "session.tab.session": "세션", "session.tab.review": "검토", "session.tab.context": "컨텍스트", @@ -433,18 +448,19 @@ export const dict = { "session.messages.loadingEarlier": "이전 메시지 로드 중...", "session.messages.loadEarlier": "이전 메시지 로드", "session.messages.loading": "메시지 로드 중...", - "session.messages.jumpToLatest": "최신으로 이동", "session.context.addToContext": "컨텍스트에 {{selection}} 추가", - "session.new.worktree.main": "메인 브랜치", "session.new.worktree.mainWithBranch": "메인 브랜치 ({{branch}})", "session.new.worktree.create": "새 작업 트리 생성", "session.new.lastModified": "최근 수정", - "session.header.search.placeholder": "{{project}} 검색", "session.header.searchFiles": "파일 검색", - + "session.header.openIn": "다음에서 열기", + "session.header.open.action": "{{app}} 열기", + "session.header.open.ariaLabel": "{{app}}에서 열기", + "session.header.open.menu": "열기 옵션", + "session.header.open.copyPath": "경로 복사", "status.popover.trigger": "상태", "status.popover.ariaLabel": "서버 구성", "status.popover.tab.servers": "서버", @@ -452,7 +468,6 @@ export const dict = { "status.popover.tab.lsp": "LSP", "status.popover.tab.plugins": "플러그인", "status.popover.action.manageServers": "서버 관리", - "session.share.popover.title": "웹에 게시", "session.share.popover.description.shared": "이 세션은 웹에 공개되었습니다. 링크가 있는 누구나 액세스할 수 있습니다.", "session.share.popover.description.unshared": @@ -465,16 +480,13 @@ export const dict = { "session.share.action.view": "보기", "session.share.copy.copied": "복사됨", "session.share.copy.copyLink": "링크 복사", - "lsp.tooltip.none": "LSP 서버 없음", "lsp.label.connected": "{{count}} LSP", - "prompt.loading": "프롬프트 로드 중...", "terminal.loading": "터미널 로드 중...", "terminal.title": "터미널", "terminal.title.numbered": "터미널 {{number}}", "terminal.close": "터미널 닫기", - "terminal.connectionLost.title": "연결 끊김", "terminal.connectionLost.description": "터미널 연결이 중단되었습니다. 서버가 재시작하면 이런 일이 발생할 수 있습니다.", @@ -490,7 +502,6 @@ export const dict = { "common.close": "닫기", "common.edit": "편집", "common.loadMore": "더 불러오기", - "common.key.esc": "ESC", "sidebar.menu.toggle": "메뉴 토글", "sidebar.nav.projectsAndSessions": "프로젝트 및 세션", @@ -503,18 +514,20 @@ export const dict = { "sidebar.gettingStarted.line2": "Claude, GPT, Gemini 등을 포함한 모델을 사용하려면 공급자를 연결하세요.", "sidebar.project.recentSessions": "최근 세션", "sidebar.project.viewAllSessions": "모든 세션 보기", - + "sidebar.project.clearNotifications": "알림 지우기", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "데스크톱", "settings.section.server": "서버", "settings.tab.general": "일반", "settings.tab.shortcuts": "단축키", - + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL 통합", + "settings.desktop.wsl.description": "Windows의 WSL 내부에서 OpenCode 서버를 실행합니다.", "settings.general.section.appearance": "모양", "settings.general.section.notifications": "시스템 알림", "settings.general.section.updates": "업데이트", "settings.general.section.sounds": "효과음", - + "settings.general.section.display": "디스플레이", "settings.general.row.language.title": "언어", "settings.general.row.language.description": "OpenCode 표시 언어 변경", "settings.general.row.appearance.title": "모양", @@ -523,10 +536,12 @@ export const dict = { "settings.general.row.theme.description": "OpenCode 테마 사용자 지정", "settings.general.row.font.title": "글꼴", "settings.general.row.font.description": "코드 블록에 사용되는 고정폭 글꼴 사용자 지정", - + "settings.general.row.wayland.title": "네이티브 Wayland 사용", + "settings.general.row.wayland.description": "Wayland에서 X11 폴백을 비활성화합니다. 다시 시작해야 합니다.", + "settings.general.row.wayland.tooltip": + "혼합 주사율 모니터가 있는 Linux에서는 네이티브 Wayland가 더 안정적일 수 있습니다.", "settings.general.row.releaseNotes.title": "릴리스 노트", "settings.general.row.releaseNotes.description": "업데이트 후 '새 소식' 팝업 표시", - "settings.updates.row.startup.title": "시작 시 업데이트 확인", "settings.updates.row.startup.description": "OpenCode를 실행할 때 업데이트를 자동으로 확인합니다", "settings.updates.row.check.title": "업데이트 확인", @@ -535,7 +550,6 @@ export const dict = { "settings.updates.action.checking": "확인 중...", "settings.updates.toast.latest.title": "최신 상태입니다", "settings.updates.toast.latest.description": "현재 최신 버전의 OpenCode를 사용 중입니다.", - "font.option.ibmPlexMono": "IBM Plex Mono", "font.option.cascadiaCode": "Cascadia Code", "font.option.firaCode": "Fira Code", @@ -548,6 +562,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "알림 01", "sound.option.alert02": "알림 02", "sound.option.alert03": "알림 03", @@ -599,14 +614,12 @@ export const dict = { "settings.general.notifications.permissions.description": "권한이 필요할 때 시스템 알림 표시", "settings.general.notifications.errors.title": "오류", "settings.general.notifications.errors.description": "오류가 발생했을 때 시스템 알림 표시", - "settings.general.sounds.agent.title": "에이전트", "settings.general.sounds.agent.description": "에이전트가 완료되거나 주의가 필요할 때 소리 재생", "settings.general.sounds.permissions.title": "권한", "settings.general.sounds.permissions.description": "권한이 필요할 때 소리 재생", "settings.general.sounds.errors.title": "오류", "settings.general.sounds.errors.description": "오류가 발생했을 때 소리 재생", - "settings.shortcuts.title": "키보드 단축키", "settings.shortcuts.reset.button": "기본값으로 초기화", "settings.shortcuts.reset.toast.title": "단축키 초기화됨", @@ -617,14 +630,12 @@ export const dict = { "settings.shortcuts.pressKeys": "키 누르기", "settings.shortcuts.search.placeholder": "단축키 검색", "settings.shortcuts.search.empty": "단축키를 찾을 수 없습니다", - "settings.shortcuts.group.general": "일반", "settings.shortcuts.group.session": "세션", "settings.shortcuts.group.navigation": "탐색", "settings.shortcuts.group.modelAndAgent": "모델 및 에이전트", "settings.shortcuts.group.terminal": "터미널", "settings.shortcuts.group.prompt": "프롬프트", - "settings.providers.title": "공급자", "settings.providers.description": "공급자 설정은 여기서 구성할 수 있습니다.", "settings.providers.section.connected": "연결된 공급자", @@ -642,16 +653,13 @@ export const dict = { "settings.commands.description": "명령어 설정은 여기서 구성할 수 있습니다.", "settings.mcp.title": "MCP", "settings.mcp.description": "MCP 설정은 여기서 구성할 수 있습니다.", - "settings.permissions.title": "권한", "settings.permissions.description": "서버가 기본적으로 사용할 수 있는 도구를 제어합니다.", "settings.permissions.section.tools": "도구", "settings.permissions.toast.updateFailed.title": "권한 업데이트 실패", - "settings.permissions.action.allow": "허용", "settings.permissions.action.ask": "묻기", "settings.permissions.action.deny": "거부", - "settings.permissions.tool.read.title": "읽기", "settings.permissions.tool.read.description": "파일 읽기 (파일 경로와 일치)", "settings.permissions.tool.edit.title": "편집", @@ -684,12 +692,10 @@ export const dict = { "settings.permissions.tool.external_directory.description": "프로젝트 디렉터리 외부의 파일에 액세스", "settings.permissions.tool.doom_loop.title": "무한 반복", "settings.permissions.tool.doom_loop.description": "동일한 입력으로 반복되는 도구 호출 감지", - "session.delete.failed.title": "세션 삭제 실패", "session.delete.title": "세션 삭제", "session.delete.confirm": '"{{name}}" 세션을 삭제하시겠습니까?', "session.delete.button": "세션 삭제", - "workspace.new": "새 작업 공간", "workspace.type.local": "로컬", "workspace.type.sandbox": "샌드박스", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 3de7837f8003..afc162ab1765 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -47,8 +47,10 @@ export const dict = { "command.session.new": "Ny sesjon", "command.file.open": "Åpne fil", + "command.tab.close": "Lukk fane", "command.context.addSelection": "Legg til markering i kontekst", "command.context.addSelection.description": "Legg til valgte linjer fra gjeldende fil", + "command.input.focus": "Fokuser inndata", "command.terminal.toggle": "Veksle terminal", "command.fileTree.toggle": "Veksle filtre", "command.review.toggle": "Veksle gjennomgang", @@ -73,6 +75,7 @@ export const dict = { "command.permissions.autoaccept.enable": "Godta endringer automatisk", "command.permissions.autoaccept.disable": "Slutt å godta endringer automatisk", "command.workspace.toggle": "Veksle arbeidsområder", + "command.workspace.toggle.description": "Enable or disable multiple workspaces in the sidebar", "command.session.undo": "Angre", "command.session.undo.description": "Angre siste melding", "command.session.redo": "Gjør om", @@ -96,9 +99,13 @@ export const dict = { "dialog.provider.group.popular": "Populære", "dialog.provider.group.other": "Andre", "dialog.provider.tag.recommended": "Anbefalt", - "dialog.provider.anthropic.note": "Koble til med Claude Pro/Max eller API-nøkkel", - "dialog.provider.openai.note": "Koble til med ChatGPT Pro/Plus eller API-nøkkel", - "dialog.provider.copilot.note": "Koble til med Copilot eller API-nøkkel", + "dialog.provider.opencode.note": "Utvalgte modeller inkludert Claude, GPT, Gemini og mer", + "dialog.provider.anthropic.note": "Direkte tilgang til Claude-modeller, inkludert Pro og Max", + "dialog.provider.copilot.note": "Claude-modeller for kodeassistanse", + "dialog.provider.openai.note": "GPT-modeller for raske, dyktige generelle AI-oppgaver", + "dialog.provider.google.note": "Gemini-modeller for raske, strukturerte svar", + "dialog.provider.openrouter.note": "Tilgang til alle støttede modeller fra én leverandør", + "dialog.provider.vercel.note": "Enhetlig tilgang til AI-modeller med smart ruting", "dialog.model.select.title": "Velg modell", "dialog.model.search.placeholder": "Søk etter modeller", @@ -146,8 +153,46 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}} tilkoblet", "provider.connect.toast.connected.description": "{{provider}}-modeller er nå tilgjengelige.", + "provider.custom.title": "Egendefinert leverandør", + "provider.custom.description.prefix": "Konfigurer en OpenAI-kompatibel leverandør. Se ", + "provider.custom.description.link": "dokumentasjon for leverandørkonfigurasjon", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "Leverandør-ID", + "provider.custom.field.providerID.placeholder": "minleverandør", + "provider.custom.field.providerID.description": "Små bokstaver, tall, bindestreker eller understreker", + "provider.custom.field.name.label": "Visningsnavn", + "provider.custom.field.name.placeholder": "Min AI-leverandør", + "provider.custom.field.baseURL.label": "Base-URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API-nøkkel", + "provider.custom.field.apiKey.placeholder": "API-nøkkel", + "provider.custom.field.apiKey.description": "Valgfritt. La stå tomt hvis du administrerer autentisering via headers.", + "provider.custom.models.label": "Modeller", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "modell-id", + "provider.custom.models.name.label": "Navn", + "provider.custom.models.name.placeholder": "Visningsnavn", + "provider.custom.models.remove": "Fjern modell", + "provider.custom.models.add": "Legg til modell", + "provider.custom.headers.label": "Headers (valgfritt)", + "provider.custom.headers.key.label": "Header", + "provider.custom.headers.key.placeholder": "Header-Navn", + "provider.custom.headers.value.label": "Verdi", + "provider.custom.headers.value.placeholder": "verdi", + "provider.custom.headers.remove": "Fjern header", + "provider.custom.headers.add": "Legg til header", + "provider.custom.error.providerID.required": "Leverandør-ID er påkrevd", + "provider.custom.error.providerID.format": "Bruk små bokstaver, tall, bindestreker eller understreker", + "provider.custom.error.providerID.exists": "Den leverandør-IDen finnes allerede", + "provider.custom.error.name.required": "Visningsnavn er påkrevd", + "provider.custom.error.baseURL.required": "Base-URL er påkrevd", + "provider.custom.error.baseURL.format": "Må starte med http:// eller https://", + "provider.custom.error.required": "Påkrevd", + "provider.custom.error.duplicate": "Duplikat", + "provider.disconnect.toast.disconnected.title": "{{provider}} frakoblet", "provider.disconnect.toast.disconnected.description": "Modeller fra {{provider}} er ikke lenger tilgjengelige.", + "model.tag.free": "Gratis", "model.tag.latest": "Nyeste", "model.provider.anthropic": "Anthropic", @@ -167,6 +212,7 @@ export const dict = { "common.search.placeholder": "Søk", "common.goBack": "Gå tilbake", + "common.goForward": "Navigate forward", "common.loading": "Laster", "common.loading.ellipsis": "...", "common.cancel": "Avbryt", @@ -214,6 +260,7 @@ export const dict = { "prompt.popover.emptyResults": "Ingen matchende resultater", "prompt.popover.emptyCommands": "Ingen matchende kommandoer", "prompt.dropzone.label": "Slipp bilder eller PDF-er her", + "prompt.dropzone.file.label": "Slipp for å @nevne fil", "prompt.slash.badge.custom": "egendefinert", "prompt.slash.badge.skill": "skill", "prompt.slash.badge.mcp": "mcp", @@ -235,6 +282,7 @@ export const dict = { "prompt.toast.shellSendFailed.title": "Kunne ikke sende shell-kommando", "prompt.toast.commandSendFailed.title": "Kunne ikke sende kommando", "prompt.toast.promptSendFailed.title": "Kunne ikke sende forespørsel", + "prompt.toast.promptSendFailed.description": "Kunne ikke hente økt", "dialog.mcp.title": "MCP-er", "dialog.mcp.description": "{{enabled}} av {{total}} aktivert", @@ -286,10 +334,10 @@ export const dict = { "dialog.project.edit.icon.recommended": "Anbefalt: 128x128px", "dialog.project.edit.color": "Farge", "dialog.project.edit.color.select": "Velg fargen {{color}}", - "dialog.project.edit.worktree.startup": "Oppstartsskript for arbeidsområde", "dialog.project.edit.worktree.startup.description": "Kjører etter at et nytt arbeidsområde (worktree) er opprettet.", "dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install", + "context.breakdown.title": "Kontekstfordeling", "context.breakdown.note": 'Omtrentlig fordeling av input-tokens. "Annet" inkluderer verktøydefinisjoner og overhead.', "context.breakdown.system": "System", @@ -324,30 +372,48 @@ export const dict = { "context.usage.clickToView": "Klikk for å se kontekst", "context.usage.view": "Se kontekstforbruk", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", + "toast.language.title": "Språk", "toast.language.description": "Byttet til {{language}}", "toast.theme.title": "Tema byttet", "toast.scheme.title": "Fargevalg", - "toast.permissions.autoaccept.on.title": "Godtar endringer automatisk", - "toast.permissions.autoaccept.on.description": "Redigerings- og skrivetillatelser vil bli godkjent automatisk", - "toast.permissions.autoaccept.off.title": "Sluttet å godta endringer automatisk", - "toast.permissions.autoaccept.off.description": "Redigerings- og skrivetillatelser vil kreve godkjenning", - "toast.workspace.enabled.title": "Arbeidsområder aktivert", "toast.workspace.enabled.description": "Flere worktrees vises nå i sidefeltet", "toast.workspace.disabled.title": "Arbeidsområder deaktivert", "toast.workspace.disabled.description": "Kun hoved-worktree vises i sidefeltet", + "toast.permissions.autoaccept.on.title": "Godtar endringer automatisk", + "toast.permissions.autoaccept.on.description": "Redigerings- og skrivetillatelser vil bli godkjent automatisk", + "toast.permissions.autoaccept.off.title": "Sluttet å godta endringer automatisk", + "toast.permissions.autoaccept.off.description": "Redigerings- og skrivetillatelser vil kreve godkjenning", + "toast.model.none.title": "Ingen modell valgt", "toast.model.none.description": "Koble til en leverandør for å oppsummere denne sesjonen", "toast.file.loadFailed.title": "Kunne ikke laste fil", - "toast.file.listFailed.title": "Kunne ikke liste filer", + "toast.context.noLineSelection.title": "Ingen linjevalg", "toast.context.noLineSelection.description": "Velg først et linjeområde i en filfane.", + "toast.session.share.copyFailed.title": "Kunne ikke kopiere URL til utklippstavlen", "toast.session.share.success.title": "Sesjon delt", "toast.session.share.success.description": "Delings-URL kopiert til utklippstavlen!", @@ -381,6 +447,7 @@ export const dict = { "Rotelement ikke funnet. Glemte du å legge det til i index.html? Eller kanskje id-attributten er feilstavet?", "error.globalSync.connectFailed": "Kunne ikke koble til server. Kjører det en server på `{{url}}`?", + "directory.error.invalidUrl": "Invalid directory in URL.", "error.chain.unknown": "Ukjent feil", "error.chain.causedBy": "Forårsaket av:", @@ -427,9 +494,11 @@ export const dict = { "session.review.loadingChanges": "Laster endringer...", "session.review.empty": "Ingen endringer i denne sesjonen ennå", "session.review.noChanges": "Ingen endringer", + "session.files.selectToOpen": "Velg en fil å åpne", "session.files.all": "Alle filer", "session.files.binaryContent": "Binær fil (innhold kan ikke vises)", + "session.messages.renderEarlier": "Vis tidligere meldinger", "session.messages.loadingEarlier": "Laster inn tidligere meldinger...", "session.messages.loadEarlier": "Last inn tidligere meldinger", @@ -445,6 +514,11 @@ export const dict = { "session.header.search.placeholder": "Søk i {{project}}", "session.header.searchFiles": "Søk etter filer", + "session.header.openIn": "Åpne i", + "session.header.open.action": "Åpne {{app}}", + "session.header.open.ariaLabel": "Åpne i {{app}}", + "session.header.open.menu": "Åpne alternativer", + "session.header.open.copyPath": "Kopier bane", "status.popover.trigger": "Status", "status.popover.ariaLabel": "Serverkonfigurasjoner", @@ -505,17 +579,23 @@ export const dict = { "sidebar.gettingStarted.line2": "Koble til en leverandør for å bruke modeller, inkl. Claude, GPT, Gemini osv.", "sidebar.project.recentSessions": "Nylige sesjoner", "sidebar.project.viewAllSessions": "Vis alle sesjoner", + "sidebar.project.clearNotifications": "Fjern varsler", "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Skrivebord", "settings.section.server": "Server", "settings.tab.general": "Generelt", "settings.tab.shortcuts": "Snarveier", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL-integrasjon", + "settings.desktop.wsl.description": "Kjør OpenCode-serveren i WSL på Windows.", "settings.general.section.appearance": "Utseende", "settings.general.section.notifications": "Systemvarsler", "settings.general.section.updates": "Oppdateringer", "settings.general.section.sounds": "Lydeffekter", + "settings.general.section.display": "Skjerm", "settings.general.row.language.title": "Språk", "settings.general.row.language.description": "Endre visningsspråket for OpenCode", @@ -526,6 +606,11 @@ export const dict = { "settings.general.row.font.title": "Skrift", "settings.general.row.font.description": "Tilpass mono-skriften som brukes i kodeblokker", + "settings.general.row.wayland.title": "Bruk innebygd Wayland", + "settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Krever omstart.", + "settings.general.row.wayland.tooltip": + "På Linux med skjermer med blandet oppdateringsfrekvens kan innebygd Wayland være mer stabilt.", + "settings.general.row.releaseNotes.title": "Utgivelsesnotater", "settings.general.row.releaseNotes.description": 'Vis "Hva er nytt"-vinduer etter oppdateringer', @@ -537,7 +622,6 @@ export const dict = { "settings.updates.action.checking": "Sjekker...", "settings.updates.toast.latest.title": "Du er oppdatert", "settings.updates.toast.latest.description": "Du bruker den nyeste versjonen av OpenCode.", - "font.option.ibmPlexMono": "IBM Plex Mono", "font.option.cascadiaCode": "Cascadia Code", "font.option.firaCode": "Fira Code", @@ -550,6 +634,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Varsel 01", "sound.option.alert02": "Varsel 02", "sound.option.alert03": "Varsel 03", @@ -595,6 +680,7 @@ export const dict = { "sound.option.yup04": "Ja 04", "sound.option.yup05": "Ja 05", "sound.option.yup06": "Ja 06", + "settings.general.notifications.agent.title": "Agent", "settings.general.notifications.agent.description": "Vis systemvarsel når agenten er ferdig eller trenger oppmerksomhet", @@ -693,6 +779,7 @@ export const dict = { "session.delete.title": "Slett sesjon", "session.delete.confirm": 'Slette sesjonen "{{name}}"?', "session.delete.button": "Slett sesjon", + "workspace.new": "Nytt arbeidsområde", "workspace.type.local": "lokal", "workspace.type.sandbox": "sandkasse", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 44bc4677be95..d8572148a896 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -16,11 +16,9 @@ export const dict = { "command.category.permissions": "Uprawnienia", "command.category.workspace": "Przestrzeń robocza", "command.category.settings": "Ustawienia", - "theme.scheme.system": "Systemowy", "theme.scheme.light": "Jasny", "theme.scheme.dark": "Ciemny", - "command.sidebar.toggle": "Przełącz pasek boczny", "command.project.open": "Otwórz projekt", "command.provider.connect": "Połącz dostawcę", @@ -31,21 +29,19 @@ export const dict = { "command.session.previous.unseen": "Poprzednia nieprzeczytana sesja", "command.session.next.unseen": "Następna nieprzeczytana sesja", "command.session.archive": "Zarchiwizuj sesję", - "command.palette": "Paleta poleceń", - "command.theme.cycle": "Przełącz motyw", "command.theme.set": "Użyj motywu: {{theme}}", "command.theme.scheme.cycle": "Przełącz schemat kolorów", "command.theme.scheme.set": "Użyj schematu kolorów: {{scheme}}", - "command.language.cycle": "Przełącz język", "command.language.set": "Użyj języka: {{language}}", - "command.session.new": "Nowa sesja", "command.file.open": "Otwórz plik", + "command.tab.close": "Zamknij kartę", "command.context.addSelection": "Dodaj zaznaczenie do kontekstu", "command.context.addSelection.description": "Dodaj zaznaczone linie z bieżącego pliku", + "command.input.focus": "Fokus na pole wejściowe", "command.terminal.toggle": "Przełącz terminal", "command.fileTree.toggle": "Przełącz drzewo plików", "command.review.toggle": "Przełącz przegląd", @@ -70,6 +66,7 @@ export const dict = { "command.permissions.autoaccept.enable": "Automatyczne akceptowanie edycji", "command.permissions.autoaccept.disable": "Zatrzymaj automatyczne akceptowanie edycji", "command.workspace.toggle": "Przełącz przestrzenie robocze", + "command.workspace.toggle.description": "Włącz lub wyłącz wiele przestrzeni roboczych na pasku bocznym", "command.session.undo": "Cofnij", "command.session.undo.description": "Cofnij ostatnią wiadomość", "command.session.redo": "Ponów", @@ -82,32 +79,30 @@ export const dict = { "command.session.share.description": "Udostępnij tę sesję i skopiuj URL do schowka", "command.session.unshare": "Przestań udostępniać sesję", "command.session.unshare.description": "Zatrzymaj udostępnianie tej sesji", - "palette.search.placeholder": "Szukaj plików, poleceń i sesji", "palette.empty": "Brak wyników", "palette.group.commands": "Polecenia", "palette.group.files": "Pliki", - "dialog.provider.search.placeholder": "Szukaj dostawców", "dialog.provider.empty": "Nie znaleziono dostawców", "dialog.provider.group.popular": "Popularne", "dialog.provider.group.other": "Inne", "dialog.provider.tag.recommended": "Zalecane", - "dialog.provider.anthropic.note": "Połącz z Claude Pro/Max lub kluczem API", - "dialog.provider.openai.note": "Połącz z ChatGPT Pro/Plus lub kluczem API", - "dialog.provider.copilot.note": "Połącz z Copilot lub kluczem API", - + "dialog.provider.opencode.note": "Wyselekcjonowane modele, w tym Claude, GPT, Gemini i inne", + "dialog.provider.anthropic.note": "Bezpośredni dostęp do modeli Claude, w tym Pro i Max", + "dialog.provider.copilot.note": "Modele Claude do pomocy w kodowaniu", + "dialog.provider.openai.note": "Modele GPT do szybkich i wszechstronnych zadań AI", + "dialog.provider.google.note": "Modele Gemini do szybkich i ustrukturyzowanych odpowiedzi", + "dialog.provider.openrouter.note": "Dostęp do wszystkich obsługiwanych modeli od jednego dostawcy", + "dialog.provider.vercel.note": "Ujednolicony dostęp do modeli AI z inteligentnym routingiem", "dialog.model.select.title": "Wybierz model", "dialog.model.search.placeholder": "Szukaj modeli", "dialog.model.empty": "Brak wyników modelu", "dialog.model.manage": "Zarządzaj modelami", "dialog.model.manage.description": "Dostosuj, które modele pojawiają się w wyborze modelu.", - "dialog.model.unpaid.freeModels.title": "Darmowe modele dostarczane przez OpenCode", "dialog.model.unpaid.addMore.title": "Dodaj więcej modeli od popularnych dostawców", - "dialog.provider.viewAll": "Zobacz więcej dostawców", - "provider.connect.title": "Połącz {{provider}}", "provider.connect.title.anthropicProMax": "Zaloguj się z Claude Pro/Max", "provider.connect.selectMethod": "Wybierz metodę logowania dla {{provider}}.", @@ -142,7 +137,43 @@ export const dict = { "provider.connect.oauth.auto.confirmationCode": "Kod potwierdzający", "provider.connect.toast.connected.title": "Połączono {{provider}}", "provider.connect.toast.connected.description": "Modele {{provider}} są teraz dostępne do użycia.", - + "provider.custom.title": "Dostawca niestandardowy", + "provider.custom.description.prefix": "Skonfiguruj dostawcę zgodnego z OpenAI. Zobacz ", + "provider.custom.description.link": "dokumentację konfiguracji dostawcy", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ID dostawcy", + "provider.custom.field.providerID.placeholder": "mojdostawca", + "provider.custom.field.providerID.description": "Małe litery, cyfry, łączniki lub podkreślenia", + "provider.custom.field.name.label": "Nazwa wyświetlana", + "provider.custom.field.name.placeholder": "Mój Dostawca AI", + "provider.custom.field.baseURL.label": "Bazowy URL", + "provider.custom.field.baseURL.placeholder": "https://api.mojdostawca.com/v1", + "provider.custom.field.apiKey.label": "Klucz API", + "provider.custom.field.apiKey.placeholder": "Klucz API", + "provider.custom.field.apiKey.description": + "Opcjonalne. Pozostaw puste, jeśli zarządzasz autoryzacją przez nagłówki.", + "provider.custom.models.label": "Modele", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Nazwa", + "provider.custom.models.name.placeholder": "Nazwa wyświetlana", + "provider.custom.models.remove": "Usuń model", + "provider.custom.models.add": "Dodaj model", + "provider.custom.headers.label": "Nagłówki (opcjonalne)", + "provider.custom.headers.key.label": "Nagłówek", + "provider.custom.headers.key.placeholder": "Nazwa-Naglowka", + "provider.custom.headers.value.label": "Wartość", + "provider.custom.headers.value.placeholder": "wartość", + "provider.custom.headers.remove": "Usuń nagłówek", + "provider.custom.headers.add": "Dodaj nagłówek", + "provider.custom.error.providerID.required": "ID dostawcy jest wymagane", + "provider.custom.error.providerID.format": "Używaj małych liter, cyfr, łączników lub podkreśleń", + "provider.custom.error.providerID.exists": "To ID dostawcy już istnieje", + "provider.custom.error.name.required": "Nazwa wyświetlana jest wymagana", + "provider.custom.error.baseURL.required": "Bazowy URL jest wymagany", + "provider.custom.error.baseURL.format": "Musi zaczynać się od http:// lub https://", + "provider.custom.error.required": "Wymagane", + "provider.custom.error.duplicate": "Duplikat", "provider.disconnect.toast.disconnected.title": "Rozłączono {{provider}}", "provider.disconnect.toast.disconnected.description": "Modele {{provider}} nie są już dostępne.", "model.tag.free": "Darmowy", @@ -161,9 +192,9 @@ export const dict = { "model.tooltip.reasoning.allowed": "Obsługuje wnioskowanie", "model.tooltip.reasoning.none": "Brak wnioskowania", "model.tooltip.context": "Limit kontekstu {{limit}}", - "common.search.placeholder": "Szukaj", "common.goBack": "Wstecz", + "common.goForward": "Dalej", "common.loading": "Ładowanie", "common.loading.ellipsis": "...", "common.cancel": "Anuluj", @@ -174,14 +205,12 @@ export const dict = { "common.saving": "Zapisywanie...", "common.default": "Domyślny", "common.attachment": "załącznik", - "prompt.placeholder.shell": "Wpisz polecenie terminala...", "prompt.placeholder.normal": 'Zapytaj o cokolwiek... "{{example}}"', "prompt.placeholder.summarizeComments": "Podsumuj komentarze…", "prompt.placeholder.summarizeComment": "Podsumuj komentarz…", "prompt.mode.shell": "Terminal", "prompt.mode.shell.exit": "esc aby wyjść", - "prompt.example.1": "Napraw TODO w bazie kodu", "prompt.example.2": "Jaki jest stos technologiczny tego projektu?", "prompt.example.3": "Napraw zepsute testy", @@ -207,10 +236,10 @@ export const dict = { "prompt.example.23": "Dodaj stronicowanie do tej listy", "prompt.example.24": "Utwórz polecenie CLI dla...", "prompt.example.25": "Jak działają tutaj zmienne środowiskowe?", - "prompt.popover.emptyResults": "Brak pasujących wyników", "prompt.popover.emptyCommands": "Brak pasujących poleceń", "prompt.dropzone.label": "Upuść obrazy lub pliki PDF tutaj", + "prompt.dropzone.file.label": "Upuść, aby @wspomnieć plik", "prompt.slash.badge.custom": "własne", "prompt.slash.badge.skill": "skill", "prompt.slash.badge.mcp": "mcp", @@ -222,7 +251,6 @@ export const dict = { "prompt.attachment.remove": "Usuń załącznik", "prompt.action.send": "Wyślij", "prompt.action.stop": "Zatrzymaj", - "prompt.toast.pasteUnsupported.title": "Nieobsługiwane wklejanie", "prompt.toast.pasteUnsupported.description": "Tylko obrazy lub pliki PDF mogą być tutaj wklejane.", "prompt.toast.modelAgentRequired.title": "Wybierz agenta i model", @@ -232,24 +260,19 @@ export const dict = { "prompt.toast.shellSendFailed.title": "Nie udało się wysłać polecenia powłoki", "prompt.toast.commandSendFailed.title": "Nie udało się wysłać polecenia", "prompt.toast.promptSendFailed.title": "Nie udało się wysłać zapytania", - + "prompt.toast.promptSendFailed.description": "Nie udało się pobrać sesji", "dialog.mcp.title": "MCP", "dialog.mcp.description": "{{enabled}} z {{total}} włączone", "dialog.mcp.empty": "Brak skonfigurowanych MCP", - "dialog.lsp.empty": "LSP wykryte automatycznie na podstawie typów plików", "dialog.plugins.empty": "Wtyczki skonfigurowane w opencode.json", - "mcp.status.connected": "połączono", "mcp.status.failed": "niepowodzenie", "mcp.status.needs_auth": "wymaga autoryzacji", "mcp.status.disabled": "wyłączone", - "dialog.fork.empty": "Brak wiadomości do rozwidlenia", - "dialog.directory.search.placeholder": "Szukaj folderów", "dialog.directory.empty": "Nie znaleziono folderów", - "dialog.server.title": "Serwery", "dialog.server.description": "Przełącz serwer OpenCode, z którym łączy się ta aplikacja.", "dialog.server.search.placeholder": "Szukaj serwerów", @@ -267,14 +290,12 @@ export const dict = { "dialog.server.default.set": "Ustaw bieżący serwer jako domyślny", "dialog.server.default.clear": "Wyczyść", "dialog.server.action.remove": "Usuń serwer", - "dialog.server.menu.edit": "Edytuj", "dialog.server.menu.default": "Ustaw jako domyślny", "dialog.server.menu.defaultRemove": "Usuń domyślny", "dialog.server.menu.delete": "Usuń", "dialog.server.current": "Obecny serwer", "dialog.server.status.default": "Domyślny", - "dialog.project.edit.title": "Edytuj projekt", "dialog.project.edit.name": "Nazwa", "dialog.project.edit.icon": "Ikona", @@ -283,10 +304,8 @@ export const dict = { "dialog.project.edit.icon.recommended": "Zalecane: 128x128px", "dialog.project.edit.color": "Kolor", "dialog.project.edit.color.select": "Wybierz kolor {{color}}", - "dialog.project.edit.worktree.startup": "Skrypt uruchamiania przestrzeni roboczej", - "dialog.project.edit.worktree.startup.description": - "Uruchamiany po utworzeniu nowej przestrzeni roboczej (drzewa roboczego).", + "dialog.project.edit.worktree.startup.description": "Runs after creating a new workspace (worktree).", "dialog.project.edit.worktree.startup.placeholder": "np. bun install", "context.breakdown.title": "Podział kontekstu", "context.breakdown.note": 'Przybliżony podział tokenów wejściowych. "Inne" obejmuje definicje narzędzi i narzut.', @@ -295,10 +314,8 @@ export const dict = { "context.breakdown.assistant": "Asystent", "context.breakdown.tool": "Wywołania narzędzi", "context.breakdown.other": "Inne", - "context.systemPrompt.title": "Prompt systemowy", "context.rawMessages.title": "Surowe wiadomości", - "context.stats.session": "Sesja", "context.stats.messages": "Wiadomości", "context.stats.provider": "Dostawca", @@ -315,34 +332,42 @@ export const dict = { "context.stats.totalCost": "Całkowity koszt", "context.stats.sessionCreated": "Utworzono sesję", "context.stats.lastActivity": "Ostatnia aktywność", - "context.usage.tokens": "Tokeny", "context.usage.usage": "Użycie", "context.usage.cost": "Koszt", "context.usage.clickToView": "Kliknij, aby zobaczyć kontekst", "context.usage.view": "Pokaż użycie kontekstu", - + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", "toast.language.title": "Język", "toast.language.description": "Przełączono na {{language}}", - "toast.theme.title": "Przełączono motyw", "toast.scheme.title": "Schemat kolorów", - - "toast.permissions.autoaccept.on.title": "Automatyczne akceptowanie edycji", - "toast.permissions.autoaccept.on.description": "Uprawnienia do edycji i zapisu będą automatycznie zatwierdzane", - "toast.permissions.autoaccept.off.title": "Zatrzymano automatyczne akceptowanie edycji", - "toast.permissions.autoaccept.off.description": "Uprawnienia do edycji i zapisu będą wymagały zatwierdzenia", - "toast.workspace.enabled.title": "Przestrzenie robocze włączone", "toast.workspace.enabled.description": "Kilka worktree jest teraz wyświetlanych na pasku bocznym", "toast.workspace.disabled.title": "Przestrzenie robocze wyłączone", "toast.workspace.disabled.description": "Tylko główny worktree jest wyświetlany na pasku bocznym", - + "toast.permissions.autoaccept.on.title": "Automatyczne akceptowanie edycji", + "toast.permissions.autoaccept.on.description": "Uprawnienia do edycji i zapisu będą automatycznie zatwierdzane", + "toast.permissions.autoaccept.off.title": "Zatrzymano automatyczne akceptowanie edycji", + "toast.permissions.autoaccept.off.description": "Uprawnienia do edycji i zapisu będą wymagały zatwierdzenia", "toast.model.none.title": "Nie wybrano modelu", "toast.model.none.description": "Połącz dostawcę, aby podsumować tę sesję", - "toast.file.loadFailed.title": "Nie udało się załadować pliku", - "toast.file.listFailed.title": "Nie udało się wyświetlić listy plików", "toast.context.noLineSelection.title": "Brak zaznaczenia linii", "toast.context.noLineSelection.description": "Najpierw wybierz zakres linii w zakładce pliku.", @@ -351,19 +376,15 @@ export const dict = { "toast.session.share.success.description": "Link udostępniania skopiowany do schowka!", "toast.session.share.failed.title": "Nie udało się udostępnić sesji", "toast.session.share.failed.description": "Wystąpił błąd podczas udostępniania sesji", - "toast.session.unshare.success.title": "Zatrzymano udostępnianie sesji", "toast.session.unshare.success.description": "Udostępnianie sesji zostało pomyślnie zatrzymane!", "toast.session.unshare.failed.title": "Nie udało się zatrzymać udostępniania sesji", "toast.session.unshare.failed.description": "Wystąpił błąd podczas zatrzymywania udostępniania sesji", - "toast.session.listFailed.title": "Nie udało się załadować sesji dla {{project}}", - "toast.update.title": "Dostępna aktualizacja", "toast.update.description": "Nowa wersja OpenCode ({{version}}) jest teraz dostępna do instalacji.", "toast.update.action.installRestart": "Zainstaluj i zrestartuj", "toast.update.action.notYet": "Jeszcze nie", - "error.page.title": "Coś poszło nie tak", "error.page.description": "Wystąpił błąd podczas ładowania aplikacji.", "error.page.details.label": "Szczegóły błędu", @@ -374,12 +395,10 @@ export const dict = { "error.page.report.prefix": "Proszę zgłosić ten błąd do zespołu OpenCode", "error.page.report.discord": "na Discordzie", "error.page.version": "Wersja: {{version}}", - "error.dev.rootNotFound": "Nie znaleziono elementu głównego. Czy zapomniałeś dodać go do swojego index.html? A może atrybut id został błędnie wpisany?", - "error.globalSync.connectFailed": "Nie można połączyć się z serwerem. Czy serwer działa pod adresem `{{url}}`?", - + "directory.error.invalidUrl": "Nieprawidłowy katalog w URL.", "error.chain.unknown": "Nieznany błąd", "error.chain.causedBy": "Spowodowany przez:", "error.chain.apiError": "Błąd API", @@ -389,8 +408,7 @@ export const dict = { "error.chain.didYouMean": "Czy miałeś na myśli: {{suggestions}}", "error.chain.modelNotFound": "Model nie znaleziony: {{provider}}/{{model}}", "error.chain.checkConfig": "Sprawdź swoją konfigurację (opencode.json) nazwy dostawców/modeli", - "error.chain.mcpFailed": - 'Serwer MCP "{{name}}" nie powiódł się. Uwaga, OpenCode nie obsługuje jeszcze uwierzytelniania MCP.', + "error.chain.mcpFailed": 'MCP server "{{name}}" failed. Note, OpenCode does not support MCP authentication yet.', "error.chain.providerAuthFailed": "Uwierzytelnianie dostawcy nie powiodło się ({{provider}}): {{message}}", "error.chain.providerInitFailed": 'Nie udało się zainicjować dostawcy "{{provider}}". Sprawdź poświadczenia i konfigurację.', @@ -401,21 +419,17 @@ export const dict = { "error.chain.configFrontmatterError": "Nie udało się przetworzyć frontmatter w {{path}}:\n{{message}}", "error.chain.configInvalid": "Plik konfiguracyjny w {{path}} jest nieprawidłowy", "error.chain.configInvalidWithMessage": "Plik konfiguracyjny w {{path}} jest nieprawidłowy: {{message}}", - "notification.permission.title": "Wymagane uprawnienie", "notification.permission.description": "{{sessionTitle}} w {{projectName}} potrzebuje uprawnienia", "notification.question.title": "Pytanie", "notification.question.description": "{{sessionTitle}} w {{projectName}} ma pytanie", "notification.action.goToSession": "Przejdź do sesji", - "notification.session.responseReady.title": "Odpowiedź gotowa", "notification.session.error.title": "Błąd sesji", "notification.session.error.fallbackDescription": "Wystąpił błąd", - "home.recentProjects": "Ostatnie projekty", "home.empty.title": "Brak ostatnich projektów", "home.empty.description": "Zacznij od otwarcia lokalnego projektu", - "session.tab.session": "Sesja", "session.tab.review": "Przegląd", "session.tab.context": "Kontekst", @@ -434,17 +448,18 @@ export const dict = { "session.messages.loadEarlier": "Załaduj wcześniejsze wiadomości", "session.messages.loading": "Ładowanie wiadomości...", "session.messages.jumpToLatest": "Przejdź do najnowszych", - "session.context.addToContext": "Dodaj {{selection}} do kontekstu", - "session.new.worktree.main": "Główna gałąź", "session.new.worktree.mainWithBranch": "Główna gałąź ({{branch}})", "session.new.worktree.create": "Utwórz nowe drzewo robocze", "session.new.lastModified": "Ostatnio zmodyfikowano", - "session.header.search.placeholder": "Szukaj {{project}}", "session.header.searchFiles": "Szukaj plików", - + "session.header.openIn": "Otwórz w", + "session.header.open.action": "Otwórz {{app}}", + "session.header.open.ariaLabel": "Otwórz w {{app}}", + "session.header.open.menu": "Opcje otwierania", + "session.header.open.copyPath": "Kopiuj ścieżkę", "status.popover.trigger": "Status", "status.popover.ariaLabel": "Konfiguracje serwerów", "status.popover.tab.servers": "Serwery", @@ -452,7 +467,6 @@ export const dict = { "status.popover.tab.lsp": "LSP", "status.popover.tab.plugins": "Wtyczki", "status.popover.action.manageServers": "Zarządzaj serwerami", - "session.share.popover.title": "Opublikuj w sieci", "session.share.popover.description.shared": "Ta sesja jest publiczna w sieci. Jest dostępna dla każdego, kto posiada link.", @@ -466,10 +480,8 @@ export const dict = { "session.share.action.view": "Widok", "session.share.copy.copied": "Skopiowano", "session.share.copy.copyLink": "Kopiuj link", - "lsp.tooltip.none": "Brak serwerów LSP", "lsp.label.connected": "{{count}} LSP", - "prompt.loading": "Ładowanie promptu...", "terminal.loading": "Ładowanie terminala...", "terminal.title": "Terminal", @@ -478,7 +490,6 @@ export const dict = { "terminal.connectionLost.title": "Utracono połączenie", "terminal.connectionLost.description": "Połączenie z terminalem zostało przerwane. Może się to zdarzyć przy restarcie serwera.", - "common.closeTab": "Zamknij kartę", "common.dismiss": "Odrzuć", "common.requestFailed": "Żądanie nie powiodło się", @@ -492,7 +503,6 @@ export const dict = { "common.edit": "Edytuj", "common.loadMore": "Załaduj więcej", "common.key.esc": "ESC", - "sidebar.menu.toggle": "Przełącz menu", "sidebar.nav.projectsAndSessions": "Projekty i sesje", "sidebar.settings": "Ustawienia", @@ -504,18 +514,20 @@ export const dict = { "sidebar.gettingStarted.line2": "Połącz dowolnego dostawcę, aby używać modeli, w tym Claude, GPT, Gemini itp.", "sidebar.project.recentSessions": "Ostatnie sesje", "sidebar.project.viewAllSessions": "Zobacz wszystkie sesje", - + "sidebar.project.clearNotifications": "Wyczyść powiadomienia", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Pulpit", "settings.section.server": "Serwer", "settings.tab.general": "Ogólne", "settings.tab.shortcuts": "Skróty", - + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "Wygląd", "settings.general.section.notifications": "Powiadomienia systemowe", "settings.general.section.updates": "Aktualizacje", "settings.general.section.sounds": "Efekty dźwiękowe", - + "settings.general.section.display": "Ekran", "settings.general.row.language.title": "Język", "settings.general.row.language.description": "Zmień język wyświetlania dla OpenCode", "settings.general.row.appearance.title": "Wygląd", @@ -524,10 +536,12 @@ export const dict = { "settings.general.row.theme.description": "Dostosuj motyw OpenCode.", "settings.general.row.font.title": "Czcionka", "settings.general.row.font.description": "Dostosuj czcionkę mono używaną w blokach kodu", - + "settings.general.row.wayland.title": "Użyj natywnego Wayland", + "settings.general.row.wayland.description": "Wyłącz fallback X11 na Wayland. Wymaga restartu.", + "settings.general.row.wayland.tooltip": + "Na Linuxie z monitorami o różnym odświeżaniu, natywny Wayland może być bardziej stabilny.", "settings.general.row.releaseNotes.title": "Informacje o wydaniu", "settings.general.row.releaseNotes.description": 'Pokazuj wyskakujące okna "Co nowego" po aktualizacjach', - "settings.updates.row.startup.title": "Sprawdzaj aktualizacje przy uruchomieniu", "settings.updates.row.startup.description": "Automatycznie sprawdzaj aktualizacje podczas uruchamiania OpenCode", "settings.updates.row.check.title": "Sprawdź aktualizacje", @@ -548,6 +562,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Alert 01", "sound.option.alert02": "Alert 02", "sound.option.alert03": "Alert 03", @@ -593,7 +608,6 @@ export const dict = { "sound.option.yup04": "Yup 04", "sound.option.yup05": "Yup 05", "sound.option.yup06": "Yup 06", - "settings.general.notifications.agent.title": "Agent", "settings.general.notifications.agent.description": "Pokaż powiadomienie systemowe, gdy agent zakończy pracę lub wymaga uwagi", @@ -602,14 +616,12 @@ export const dict = { "Pokaż powiadomienie systemowe, gdy wymagane jest uprawnienie", "settings.general.notifications.errors.title": "Błędy", "settings.general.notifications.errors.description": "Pokaż powiadomienie systemowe, gdy wystąpi błąd", - "settings.general.sounds.agent.title": "Agent", "settings.general.sounds.agent.description": "Odtwórz dźwięk, gdy agent zakończy pracę lub wymaga uwagi", "settings.general.sounds.permissions.title": "Uprawnienia", "settings.general.sounds.permissions.description": "Odtwórz dźwięk, gdy wymagane jest uprawnienie", "settings.general.sounds.errors.title": "Błędy", "settings.general.sounds.errors.description": "Odtwórz dźwięk, gdy wystąpi błąd", - "settings.shortcuts.title": "Skróty klawiszowe", "settings.shortcuts.reset.button": "Przywróć domyślne", "settings.shortcuts.reset.toast.title": "Zresetowano skróty", @@ -620,14 +632,12 @@ export const dict = { "settings.shortcuts.pressKeys": "Naciśnij klawisze", "settings.shortcuts.search.placeholder": "Szukaj skrótów", "settings.shortcuts.search.empty": "Nie znaleziono skrótów", - "settings.shortcuts.group.general": "Ogólne", "settings.shortcuts.group.session": "Sesja", "settings.shortcuts.group.navigation": "Nawigacja", "settings.shortcuts.group.modelAndAgent": "Model i agent", "settings.shortcuts.group.terminal": "Terminal", "settings.shortcuts.group.prompt": "Prompt", - "settings.providers.title": "Dostawcy", "settings.providers.description": "Ustawienia dostawców będą tutaj konfigurowalne.", "settings.providers.section.connected": "Połączeni dostawcy", @@ -645,16 +655,13 @@ export const dict = { "settings.commands.description": "Ustawienia poleceń będą tutaj konfigurowalne.", "settings.mcp.title": "MCP", "settings.mcp.description": "Ustawienia MCP będą tutaj konfigurowalne.", - "settings.permissions.title": "Uprawnienia", "settings.permissions.description": "Kontroluj, jakich narzędzi serwer może używać domyślnie.", "settings.permissions.section.tools": "Narzędzia", "settings.permissions.toast.updateFailed.title": "Nie udało się zaktualizować uprawnień", - "settings.permissions.action.allow": "Zezwól", "settings.permissions.action.ask": "Pytaj", "settings.permissions.action.deny": "Odmów", - "settings.permissions.tool.read.title": "Odczyt", "settings.permissions.tool.read.description": "Odczyt pliku (pasuje do ścieżki pliku)", "settings.permissions.tool.edit.title": "Edycja", @@ -687,12 +694,10 @@ export const dict = { "settings.permissions.tool.external_directory.description": "Dostęp do plików poza katalogiem projektu", "settings.permissions.tool.doom_loop.title": "Zapętlenie", "settings.permissions.tool.doom_loop.description": "Wykrywanie powtarzających się wywołań narzędzi (doom loop)", - "session.delete.failed.title": "Nie udało się usunąć sesji", "session.delete.title": "Usuń sesję", "session.delete.confirm": 'Usunąć sesję "{{name}}"?', "session.delete.button": "Usuń sesję", - "workspace.new": "Nowa przestrzeń robocza", "workspace.type.local": "lokalna", "workspace.type.sandbox": "piaskownica", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 28785c0e9fbc..86d201cebcab 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -44,8 +44,10 @@ export const dict = { "command.session.new": "Новая сессия", "command.file.open": "Открыть файл", + "command.tab.close": "Закрыть вкладку", "command.context.addSelection": "Добавить выделение в контекст", "command.context.addSelection.description": "Добавить выбранные строки из текущего файла", + "command.input.focus": "Фокус на поле ввода", "command.terminal.toggle": "Переключить терминал", "command.fileTree.toggle": "Переключить дерево файлов", "command.review.toggle": "Переключить обзор", @@ -70,6 +72,7 @@ export const dict = { "command.permissions.autoaccept.enable": "Авто-принятие изменений", "command.permissions.autoaccept.disable": "Прекратить авто-принятие изменений", "command.workspace.toggle": "Переключить рабочие пространства", + "command.workspace.toggle.description": "Включить или отключить несколько рабочих пространств в боковой панели", "command.session.undo": "Отменить", "command.session.undo.description": "Отменить последнее сообщение", "command.session.redo": "Повторить", @@ -93,9 +96,13 @@ export const dict = { "dialog.provider.group.popular": "Популярные", "dialog.provider.group.other": "Другие", "dialog.provider.tag.recommended": "Рекомендуемые", - "dialog.provider.anthropic.note": "Подключитесь с помощью Claude Pro/Max или API ключа", - "dialog.provider.openai.note": "Подключитесь с помощью ChatGPT Pro/Plus или API ключа", - "dialog.provider.copilot.note": "Подключитесь с помощью Copilot или API ключа", + "dialog.provider.opencode.note": "Отобранные модели, включая Claude, GPT, Gemini и другие", + "dialog.provider.anthropic.note": "Прямой доступ к моделям Claude, включая Pro и Max", + "dialog.provider.copilot.note": "Модели Claude для помощи в кодировании", + "dialog.provider.openai.note": "Модели GPT для быстрых и мощных задач общего ИИ", + "dialog.provider.google.note": "Модели Gemini для быстрых и структурированных ответов", + "dialog.provider.openrouter.note": "Доступ ко всем поддерживаемым моделям через одного провайдера", + "dialog.provider.vercel.note": "Единый доступ к ИИ-моделям с умной маршрутизацией", "dialog.model.select.title": "Выбрать модель", "dialog.model.search.placeholder": "Поиск моделей", @@ -143,6 +150,44 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}} подключён", "provider.connect.toast.connected.description": "Модели {{provider}} теперь доступны.", + "provider.custom.title": "Пользовательский провайдер", + "provider.custom.description.prefix": "Настройте OpenAI-совместимого провайдера. См. ", + "provider.custom.description.link": "документацию по настройке провайдера", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ID провайдера", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "Строчные буквы, цифры, дефисы или подчёркивания", + "provider.custom.field.name.label": "Отображаемое имя", + "provider.custom.field.name.placeholder": "Мой AI провайдер", + "provider.custom.field.baseURL.label": "Базовый URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API ключ", + "provider.custom.field.apiKey.placeholder": "API ключ", + "provider.custom.field.apiKey.description": + "Необязательно. Оставьте пустым, если управляете авторизацией через заголовки.", + "provider.custom.models.label": "Модели", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Имя", + "provider.custom.models.name.placeholder": "Отображаемое имя", + "provider.custom.models.remove": "Удалить модель", + "provider.custom.models.add": "Добавить модель", + "provider.custom.headers.label": "Заголовки (необязательно)", + "provider.custom.headers.key.label": "Заголовок", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "Значение", + "provider.custom.headers.value.placeholder": "значение", + "provider.custom.headers.remove": "Удалить заголовок", + "provider.custom.headers.add": "Добавить заголовок", + "provider.custom.error.providerID.required": "Требуется ID провайдера", + "provider.custom.error.providerID.format": "Используйте строчные буквы, цифры, дефисы или подчёркивания", + "provider.custom.error.providerID.exists": "Такой ID провайдера уже существует", + "provider.custom.error.name.required": "Требуется отображаемое имя", + "provider.custom.error.baseURL.required": "Требуется базовый URL", + "provider.custom.error.baseURL.format": "Должен начинаться с http:// или https://", + "provider.custom.error.required": "Обязательно", + "provider.custom.error.duplicate": "Дубликат", + "provider.disconnect.toast.disconnected.title": "{{provider}} отключён", "provider.disconnect.toast.disconnected.description": "Модели {{provider}} больше недоступны.", "model.tag.free": "Бесплатно", @@ -164,6 +209,7 @@ export const dict = { "common.search.placeholder": "Поиск", "common.goBack": "Назад", + "common.goForward": "Вперёд", "common.loading": "Загрузка", "common.loading.ellipsis": "...", "common.cancel": "Отмена", @@ -211,6 +257,7 @@ export const dict = { "prompt.popover.emptyResults": "Нет совпадений", "prompt.popover.emptyCommands": "Нет совпадающих команд", "prompt.dropzone.label": "Перетащите изображения или PDF сюда", + "prompt.dropzone.file.label": "Отпустите для @упоминания файла", "prompt.slash.badge.custom": "своё", "prompt.slash.badge.skill": "навык", "prompt.slash.badge.mcp": "mcp", @@ -232,6 +279,7 @@ export const dict = { "prompt.toast.shellSendFailed.title": "Не удалось отправить команду оболочки", "prompt.toast.commandSendFailed.title": "Не удалось отправить команду", "prompt.toast.promptSendFailed.title": "Не удалось отправить запрос", + "prompt.toast.promptSendFailed.description": "Не удалось получить сессию", "dialog.mcp.title": "MCP", "dialog.mcp.description": "{{enabled}} из {{total}} включено", @@ -323,6 +371,23 @@ export const dict = { "context.usage.clickToView": "Нажмите для просмотра контекста", "context.usage.view": "Показать использование контекста", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", + "toast.language.title": "Язык", "toast.language.description": "Переключено на {{language}}", @@ -380,6 +445,7 @@ export const dict = { "Корневой элемент не найден. Вы забыли добавить его в index.html? Или, может быть, атрибут id был написан неправильно?", "error.globalSync.connectFailed": "Не удалось подключиться к серверу. Запущен ли сервер по адресу `{{url}}`?", + "directory.error.invalidUrl": "Недопустимая директория в URL.", "error.chain.unknown": "Неизвестная ошибка", "error.chain.causedBy": "Причина:", @@ -446,6 +512,11 @@ export const dict = { "session.header.search.placeholder": "Поиск {{project}}", "session.header.searchFiles": "Поиск файлов", + "session.header.openIn": "Открыть в", + "session.header.open.action": "Открыть {{app}}", + "session.header.open.ariaLabel": "Открыть в {{app}}", + "session.header.open.menu": "Варианты открытия", + "session.header.open.copyPath": "Копировать путь", "status.popover.trigger": "Статус", "status.popover.ariaLabel": "Настройки серверов", @@ -507,17 +578,22 @@ export const dict = { "Подключите любого провайдера для использования моделей, включая Claude, GPT, Gemini и др.", "sidebar.project.recentSessions": "Недавние сессии", "sidebar.project.viewAllSessions": "Посмотреть все сессии", + "sidebar.project.clearNotifications": "Очистить уведомления", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Приложение", "settings.section.server": "Сервер", "settings.tab.general": "Основные", "settings.tab.shortcuts": "Горячие клавиши", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "Интеграция с WSL", + "settings.desktop.wsl.description": "Запускать сервер OpenCode внутри WSL на Windows.", "settings.general.section.appearance": "Внешний вид", "settings.general.section.notifications": "Системные уведомления", "settings.general.section.updates": "Обновления", "settings.general.section.sounds": "Звуковые эффекты", + "settings.general.section.display": "Дисплей", "settings.general.row.language.title": "Язык", "settings.general.row.language.description": "Изменить язык отображения OpenCode", @@ -528,6 +604,11 @@ export const dict = { "settings.general.row.font.title": "Шрифт", "settings.general.row.font.description": "Настройте моноширинный шрифт для блоков кода", + "settings.general.row.wayland.title": "Использовать нативный Wayland", + "settings.general.row.wayland.description": "Отключить X11 fallback на Wayland. Требуется перезапуск.", + "settings.general.row.wayland.tooltip": + "На Linux с мониторами разной частоты обновления нативный Wayland может быть стабильнее.", + "settings.general.row.releaseNotes.title": "Примечания к выпуску", "settings.general.row.releaseNotes.description": 'Показывать всплывающие окна "Что нового" после обновлений', @@ -551,6 +632,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "Alert 01", "sound.option.alert02": "Alert 02", "sound.option.alert03": "Alert 03", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 9858f39d772b..83020bf8c07b 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -44,8 +44,10 @@ export const dict = { "command.session.new": "เซสชันใหม่", "command.file.open": "เปิดไฟล์", + "command.tab.close": "ปิดแท็บ", "command.context.addSelection": "เพิ่มส่วนที่เลือกไปยังบริบท", "command.context.addSelection.description": "เพิ่มบรรทัดที่เลือกจากไฟล์ปัจจุบัน", + "command.input.focus": "โฟกัสช่องป้อนข้อมูล", "command.terminal.toggle": "สลับเทอร์มินัล", "command.fileTree.toggle": "สลับต้นไม้ไฟล์", "command.review.toggle": "สลับการตรวจสอบ", @@ -70,6 +72,7 @@ export const dict = { "command.permissions.autoaccept.enable": "ยอมรับการแก้ไขโดยอัตโนมัติ", "command.permissions.autoaccept.disable": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ", "command.workspace.toggle": "สลับพื้นที่ทำงาน", + "command.workspace.toggle.description": "เปิดหรือปิดใช้งานพื้นที่ทำงานหลายรายการในแถบด้านข้าง", "command.session.undo": "ยกเลิก", "command.session.undo.description": "ยกเลิกข้อความล่าสุด", "command.session.redo": "ทำซ้ำ", @@ -147,6 +150,43 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}} ที่เชื่อมต่อแล้ว", "provider.connect.toast.connected.description": "โมเดล {{provider}} พร้อมใช้งานแล้ว", + "provider.custom.title": "ผู้ให้บริการที่กำหนดเอง", + "provider.custom.description.prefix": "กำหนดค่าผู้ให้บริการที่เข้ากันได้กับ OpenAI ดู ", + "provider.custom.description.link": "เอกสารการกำหนดค่าผู้ให้บริการ", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "รหัสผู้ให้บริการ", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "ตัวอักษรพิมพ์เล็ก ตัวเลข ยัติภังค์ หรือขีดล่าง", + "provider.custom.field.name.label": "ชื่อที่แสดง", + "provider.custom.field.name.placeholder": "My AI Provider", + "provider.custom.field.baseURL.label": "URL พื้นฐาน", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "คีย์ API", + "provider.custom.field.apiKey.placeholder": "คีย์ API", + "provider.custom.field.apiKey.description": "ไม่บังคับ เว้นว่างไว้หากคุณจัดการการยืนยันตัวตนผ่านส่วนหัว", + "provider.custom.models.label": "โมเดล", + "provider.custom.models.id.label": "รหัส", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "ชื่อ", + "provider.custom.models.name.placeholder": "ชื่อที่แสดง", + "provider.custom.models.remove": "ลบโมเดล", + "provider.custom.models.add": "เพิ่มโมเดล", + "provider.custom.headers.label": "ส่วนหัว (ไม่บังคับ)", + "provider.custom.headers.key.label": "ส่วนหัว", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "ค่า", + "provider.custom.headers.value.placeholder": "ค่า", + "provider.custom.headers.remove": "ลบส่วนหัว", + "provider.custom.headers.add": "เพิ่มส่วนหัว", + "provider.custom.error.providerID.required": "ต้องระบุรหัสผู้ให้บริการ", + "provider.custom.error.providerID.format": "ใช้ตัวอักษรพิมพ์เล็ก ตัวเลข ยัติภังค์ หรือขีดล่าง", + "provider.custom.error.providerID.exists": "รหัสผู้ให้บริการนั้นมีอยู่แล้ว", + "provider.custom.error.name.required": "ต้องระบุชื่อที่แสดง", + "provider.custom.error.baseURL.required": "ต้องระบุ URL พื้นฐาน", + "provider.custom.error.baseURL.format": "ต้องขึ้นต้นด้วย http:// หรือ https://", + "provider.custom.error.required": "จำเป็น", + "provider.custom.error.duplicate": "ซ้ำ", + "provider.disconnect.toast.disconnected.title": "{{provider}} ที่ยกเลิกการเชื่อมต่อแล้ว", "provider.disconnect.toast.disconnected.description": "โมเดล {{provider}} ไม่พร้อมใช้งานอีกต่อไป", @@ -161,7 +201,7 @@ export const dict = { "model.input.image": "รูปภาพ", "model.input.audio": "เสียง", "model.input.video": "วิดีโอ", - "model.input.pdf": "pdf", + "model.input.pdf": "PDF", "model.tooltip.allows": "อนุญาต: {{inputs}}", "model.tooltip.reasoning.allowed": "อนุญาตการใช้เหตุผล", "model.tooltip.reasoning.none": "ไม่มีการใช้เหตุผล", @@ -169,6 +209,7 @@ export const dict = { "common.search.placeholder": "ค้นหา", "common.goBack": "ย้อนกลับ", + "common.goForward": "นำทางไปข้างหน้า", "common.loading": "กำลังโหลด", "common.loading.ellipsis": "...", "common.cancel": "ยกเลิก", @@ -216,9 +257,10 @@ export const dict = { "prompt.popover.emptyResults": "ไม่พบผลลัพธ์ที่ตรงกัน", "prompt.popover.emptyCommands": "ไม่พบคำสั่งที่ตรงกัน", "prompt.dropzone.label": "วางรูปภาพหรือ PDF ที่นี่", + "prompt.dropzone.file.label": "วางเพื่อ @กล่าวถึงไฟล์", "prompt.slash.badge.custom": "กำหนดเอง", - "prompt.slash.badge.skill": "skill", - "prompt.slash.badge.mcp": "mcp", + "prompt.slash.badge.skill": "ทักษะ", + "prompt.slash.badge.mcp": "MCP", "prompt.context.active": "ใช้งานอยู่", "prompt.context.includeActiveFile": "รวมไฟล์ที่ใช้งานอยู่", "prompt.context.removeActiveFile": "เอาไฟล์ที่ใช้งานอยู่ออกจากบริบท", @@ -237,6 +279,7 @@ export const dict = { "prompt.toast.shellSendFailed.title": "ไม่สามารถส่งคำสั่งเชลล์", "prompt.toast.commandSendFailed.title": "ไม่สามารถส่งคำสั่ง", "prompt.toast.promptSendFailed.title": "ไม่สามารถส่งพร้อมท์", + "prompt.toast.promptSendFailed.description": "ไม่สามารถดึงเซสชันได้", "dialog.mcp.title": "MCPs", "dialog.mcp.description": "{{enabled}} จาก {{total}} ที่เปิดใช้งาน", @@ -326,22 +369,39 @@ export const dict = { "context.usage.clickToView": "คลิกเพื่อดูบริบท", "context.usage.view": "ดูการใช้บริบท", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", + "toast.language.title": "ภาษา", "toast.language.description": "สลับไปที่ {{language}}", "toast.theme.title": "สลับธีมแล้ว", "toast.scheme.title": "โทนสี", - "toast.permissions.autoaccept.on.title": "กำลังยอมรับการแก้ไขโดยอัตโนมัติ", - "toast.permissions.autoaccept.on.description": "สิทธิ์การแก้ไขและจะได้รับเขียนการอนุมัติโดยอัตโนมัติ", - "toast.permissions.autoaccept.off.title": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ", - "toast.permissions.autoaccept.off.description": "สิทธิ์การแก้ไขและเขียนจะต้องได้รับการอนุมัติ", - "toast.workspace.enabled.title": "เปิดใช้งานพื้นที่ทำงานแล้ว", "toast.workspace.enabled.description": "ตอนนี้จะแสดง worktree หลายรายการในแถบด้านข้าง", "toast.workspace.disabled.title": "ปิดใช้งานพื้นที่ทำงานแล้ว", "toast.workspace.disabled.description": "จะแสดงเฉพาะ worktree หลักในแถบด้านข้าง", + "toast.permissions.autoaccept.on.title": "กำลังยอมรับการแก้ไขโดยอัตโนมัติ", + "toast.permissions.autoaccept.on.description": "สิทธิ์การแก้ไขและจะได้รับเขียนการอนุมัติโดยอัตโนมัติ", + "toast.permissions.autoaccept.off.title": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ", + "toast.permissions.autoaccept.off.description": "สิทธิ์การแก้ไขและเขียนจะต้องได้รับการอนุมัติ", + "toast.model.none.title": "ไม่ได้เลือกโมเดล", "toast.model.none.description": "เชื่อมต่อผู้ให้บริการเพื่อสรุปเซสชันนี้", @@ -383,6 +443,7 @@ export const dict = { "error.dev.rootNotFound": "ไม่พบองค์ประกอบรูท คุณลืมเพิ่มใน index.html หรือบางทีแอตทริบิวต์ id อาจสะกดผิด?", "error.globalSync.connectFailed": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ มีเซิร์ฟเวอร์ทำงานอยู่ที่ `{{url}}` หรือไม่?", + "directory.error.invalidUrl": "ไดเรกทอรีใน URL ไม่ถูกต้อง", "error.chain.unknown": "ข้อผิดพลาดที่ไม่รู้จัก", "error.chain.causedBy": "สาเหตุ:", @@ -448,6 +509,11 @@ export const dict = { "session.header.search.placeholder": "ค้นหา {{project}}", "session.header.searchFiles": "ค้นหาไฟล์", + "session.header.openIn": "เปิดใน", + "session.header.open.action": "เปิด {{app}}", + "session.header.open.ariaLabel": "เปิดใน {{app}}", + "session.header.open.menu": "ตัวเลือกการเปิด", + "session.header.open.copyPath": "คัดลอกเส้นทาง", "status.popover.trigger": "สถานะ", "status.popover.ariaLabel": "การกำหนดค่าเซิร์ฟเวอร์", @@ -505,6 +571,7 @@ export const dict = { "sidebar.gettingStarted.line2": "เชื่อมต่อผู้ให้บริการใด ๆ เพื่อใช้โมเดล รวมถึง Claude, GPT, Gemini ฯลฯ", "sidebar.project.recentSessions": "เซสชันล่าสุด", "sidebar.project.viewAllSessions": "ดูเซสชันทั้งหมด", + "sidebar.project.clearNotifications": "ล้างการแจ้งเตือน", "app.name.desktop": "OpenCode Desktop", @@ -512,11 +579,15 @@ export const dict = { "settings.section.server": "เซิร์ฟเวอร์", "settings.tab.general": "ทั่วไป", "settings.tab.shortcuts": "ทางลัด", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "การรวม WSL", + "settings.desktop.wsl.description": "เรียกใช้เซิร์ฟเวอร์ OpenCode ภายใน WSL บน Windows", "settings.general.section.appearance": "รูปลักษณ์", "settings.general.section.notifications": "การแจ้งเตือนระบบ", "settings.general.section.updates": "การอัปเดต", "settings.general.section.sounds": "เสียงเอฟเฟกต์", + "settings.general.section.display": "การแสดงผล", "settings.general.row.language.title": "ภาษา", "settings.general.row.language.description": "เปลี่ยนภาษาที่แสดงสำหรับ OpenCode", @@ -527,8 +598,22 @@ export const dict = { "settings.general.row.font.title": "ฟอนต์", "settings.general.row.font.description": "ปรับแต่งฟอนต์โมโนที่ใช้ในบล็อกโค้ด", + "settings.general.row.wayland.title": "ใช้ Wayland แบบเนทีฟ", + "settings.general.row.wayland.description": "ปิดใช้งาน X11 fallback บน Wayland ต้องรีสตาร์ท", + "settings.general.row.wayland.tooltip": "บน Linux ที่มีจอภาพรีเฟรชเรตแบบผสม Wayland แบบเนทีฟอาจเสถียรกว่า", + "settings.general.row.releaseNotes.title": "บันทึกการอัปเดต", "settings.general.row.releaseNotes.description": "แสดงป๊อปอัพ What's New หลังจากอัปเดต", + + "settings.updates.row.startup.title": "ตรวจสอบการอัปเดตเมื่อเริ่มต้น", + "settings.updates.row.startup.description": "ตรวจสอบการอัปเดตโดยอัตโนมัติเมื่อ OpenCode เปิดใช้งาน", + "settings.updates.row.check.title": "ตรวจสอบการอัปเดต", + "settings.updates.row.check.description": "ตรวจสอบการอัปเดตด้วยตนเองและติดตั้งหากมี", + "settings.updates.action.checkNow": "ตรวจสอบทันที", + "settings.updates.action.checking": "กำลังตรวจสอบ...", + "settings.updates.toast.latest.title": "คุณเป็นเวอร์ชันล่าสุดแล้ว", + "settings.updates.toast.latest.description": "คุณกำลังใช้งาน OpenCode เวอร์ชันล่าสุด", + "font.option.ibmPlexMono": "IBM Plex Mono", "font.option.cascadiaCode": "Cascadia Code", "font.option.firaCode": "Fira Code", @@ -541,6 +626,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "เสียงเตือน 01", "sound.option.alert02": "เสียงเตือน 02", "sound.option.alert03": "เสียงเตือน 03", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index a8fda6f3a601..d0bf86cbba6d 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -19,17 +19,22 @@ export const dict = { "command.category.agent": "智能体", "command.category.permissions": "权限", "command.category.workspace": "工作区", - "command.category.settings": "设置", + "theme.scheme.system": "系统", "theme.scheme.light": "浅色", "theme.scheme.dark": "深色", "command.sidebar.toggle": "切换侧边栏", + "command.project.open": "打开项目", + "command.provider.connect": "连接提供商", + "command.server.switch": "切换服务器", + "command.settings.open": "打开设置", + "command.session.previous": "上一个会话", "command.session.next": "下一个会话", "command.session.previous.unseen": "上一个未读会话", @@ -47,33 +52,53 @@ export const dict = { "command.language.set": "使用语言:{{language}}", "command.session.new": "新建会话", + "command.file.open": "打开文件", + + "command.tab.close": "关闭标签页", + "command.context.addSelection": "将所选内容添加到上下文", "command.context.addSelection.description": "添加当前文件中选中的行", + + "command.input.focus": "聚焦输入框", + "command.terminal.toggle": "切换终端", + "command.fileTree.toggle": "切换文件树", + "command.review.toggle": "切换审查", + "command.terminal.new": "新建终端", "command.terminal.new.description": "创建新的终端标签页", + "command.steps.toggle": "切换步骤", "command.steps.toggle.description": "显示或隐藏当前消息的步骤", + "command.message.previous": "上一条消息", "command.message.previous.description": "跳转到上一条用户消息", "command.message.next": "下一条消息", "command.message.next.description": "跳转到下一条用户消息", + "command.model.choose": "选择模型", "command.model.choose.description": "选择不同的模型", + "command.mcp.toggle": "切换 MCPs", "command.mcp.toggle.description": "切换 MCPs", + "command.agent.cycle": "切换智能体", "command.agent.cycle.description": "切换到下一个智能体", "command.agent.cycle.reverse": "反向切换智能体", "command.agent.cycle.reverse.description": "切换到上一个智能体", + "command.model.variant.cycle": "切换思考强度", "command.model.variant.cycle.description": "切换到下一个强度等级", + "command.permissions.autoaccept.enable": "自动接受编辑", "command.permissions.autoaccept.disable": "停止自动接受编辑", + "command.workspace.toggle": "切换工作区", + "command.workspace.toggle.description": "在侧边栏启用或禁用多个工作区", + "command.session.undo": "撤销", "command.session.undo.description": "撤销上一条消息", "command.session.redo": "重做", @@ -97,10 +122,10 @@ export const dict = { "dialog.provider.group.popular": "热门", "dialog.provider.group.other": "其他", "dialog.provider.tag.recommended": "推荐", + "dialog.provider.opencode.note": "使用 OpenCode Zen 或 API 密钥连接", "dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 密钥连接", - "dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 密钥连接", "dialog.provider.copilot.note": "使用 Copilot 或 API 密钥连接", - "dialog.provider.opencode.note": "使用 OpenCode Zen 或 API 密钥连接", + "dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 密钥连接", "dialog.provider.google.note": "使用 Google 账号或 API 密钥连接", "dialog.provider.openrouter.note": "使用 OpenRouter 账号或 API 密钥连接", "dialog.provider.vercel.note": "使用 Vercel 账号或 API 密钥连接", @@ -110,7 +135,6 @@ export const dict = { "dialog.model.empty": "未找到模型", "dialog.model.manage": "管理模型", "dialog.model.manage.description": "自定义模型选择器中显示的模型。", - "dialog.model.unpaid.freeModels.title": "OpenCode 提供的免费模型", "dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型", @@ -186,9 +210,9 @@ export const dict = { "provider.disconnect.toast.disconnected.title": "{{provider}} 已断开连接", "provider.disconnect.toast.disconnected.description": "{{provider}} 模型已不再可用。", + "model.tag.free": "免费", "model.tag.latest": "最新", - "model.provider.anthropic": "Anthropic", "model.provider.openai": "OpenAI", "model.provider.google": "Google", @@ -203,8 +227,10 @@ export const dict = { "model.tooltip.reasoning.allowed": "支持推理", "model.tooltip.reasoning.none": "不支持推理", "model.tooltip.context": "上下文上限 {{limit}}", + "common.search.placeholder": "搜索", "common.goBack": "返回", + "common.goForward": "前进", "common.loading": "加载中", "common.loading.ellipsis": "...", "common.cancel": "取消", @@ -222,7 +248,6 @@ export const dict = { "prompt.placeholder.summarizeComment": "总结该评论…", "prompt.mode.shell": "Shell", "prompt.mode.shell.exit": "按 esc 退出", - "prompt.example.1": "修复代码库中的一个 TODO", "prompt.example.2": "这个项目的技术栈是什么?", "prompt.example.3": "修复失败的测试", @@ -248,10 +273,10 @@ export const dict = { "prompt.example.23": "给这个列表添加分页", "prompt.example.24": "创建一个 CLI 命令用于...", "prompt.example.25": "这里的环境变量是怎么工作的?", - "prompt.popover.emptyResults": "没有匹配的结果", "prompt.popover.emptyCommands": "没有匹配的命令", "prompt.dropzone.label": "将图片或 PDF 拖到这里", + "prompt.dropzone.file.label": "拖放以 @提及文件", "prompt.slash.badge.custom": "自定义", "prompt.slash.badge.skill": "技能", "prompt.slash.badge.mcp": "mcp", @@ -263,7 +288,6 @@ export const dict = { "prompt.attachment.remove": "移除附件", "prompt.action.send": "发送", "prompt.action.stop": "停止", - "prompt.toast.pasteUnsupported.title": "不支持的粘贴", "prompt.toast.pasteUnsupported.description": "这里只能粘贴图片或 PDF 文件。", "prompt.toast.modelAgentRequired.title": "请选择智能体和模型", @@ -273,12 +297,14 @@ export const dict = { "prompt.toast.shellSendFailed.title": "发送 shell 命令失败", "prompt.toast.commandSendFailed.title": "发送命令失败", "prompt.toast.promptSendFailed.title": "发送提示失败", + "prompt.toast.promptSendFailed.description": "无法获取会话", "dialog.mcp.title": "MCPs", "dialog.mcp.description": "已启用 {{enabled}} / {{total}}", "dialog.mcp.empty": "未配置 MCPs", "dialog.lsp.empty": "已从文件类型自动检测到 LSPs", + "dialog.plugins.empty": "在 opencode.json 中配置的插件", "mcp.status.connected": "已连接", @@ -307,7 +333,6 @@ export const dict = { "dialog.server.default.set": "将当前服务器设为默认", "dialog.server.default.clear": "清除", "dialog.server.action.remove": "移除服务器", - "dialog.server.menu.edit": "编辑", "dialog.server.menu.default": "设为默认", "dialog.server.menu.defaultRemove": "取消默认", @@ -323,10 +348,10 @@ export const dict = { "dialog.project.edit.icon.recommended": "建议:128x128px", "dialog.project.edit.color": "颜色", "dialog.project.edit.color.select": "选择{{color}}颜色", - "dialog.project.edit.worktree.startup": "工作区启动脚本", "dialog.project.edit.worktree.startup.description": "在创建新的工作区 (worktree) 后运行。", "dialog.project.edit.worktree.startup.placeholder": "例如 bun install", + "context.breakdown.title": "上下文拆分", "context.breakdown.note": "输入 token 的大致拆分。“其他”包含工具定义和开销。", "context.breakdown.system": "系统", @@ -334,10 +359,8 @@ export const dict = { "context.breakdown.assistant": "助手", "context.breakdown.tool": "工具调用", "context.breakdown.other": "其他", - "context.systemPrompt.title": "系统提示词", "context.rawMessages.title": "原始消息", - "context.stats.session": "会话", "context.stats.messages": "消息数", "context.stats.provider": "提供商", @@ -354,34 +377,44 @@ export const dict = { "context.stats.totalCost": "总成本", "context.stats.sessionCreated": "创建时间", "context.stats.lastActivity": "最后活动", - "context.usage.tokens": "Token", "context.usage.usage": "使用率", "context.usage.cost": "成本", "context.usage.clickToView": "点击查看上下文", "context.usage.view": "查看上下文用量", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", + "toast.language.title": "语言", "toast.language.description": "已切换到{{language}}", - "toast.theme.title": "主题已切换", "toast.scheme.title": "颜色方案", - "toast.workspace.enabled.title": "工作区已启用", "toast.workspace.enabled.description": "侧边栏现在显示多个工作树", "toast.workspace.disabled.title": "工作区已禁用", "toast.workspace.disabled.description": "侧边栏只显示主工作树", - "toast.permissions.autoaccept.on.title": "自动接受编辑", "toast.permissions.autoaccept.on.description": "编辑和写入权限将自动获批", "toast.permissions.autoaccept.off.title": "已停止自动接受编辑", "toast.permissions.autoaccept.off.description": "编辑和写入权限将需要手动批准", - "toast.model.none.title": "未选择模型", "toast.model.none.description": "请先连接提供商以总结此会话", - "toast.file.loadFailed.title": "加载文件失败", - "toast.file.listFailed.title": "列出文件失败", "toast.context.noLineSelection.title": "未选择行", "toast.context.noLineSelection.description": "请先在文件标签中选择行范围。", @@ -390,14 +423,11 @@ export const dict = { "toast.session.share.success.description": "分享链接已复制到剪贴板", "toast.session.share.failed.title": "分享会话失败", "toast.session.share.failed.description": "分享会话时发生错误", - "toast.session.unshare.success.title": "已取消分享会话", "toast.session.unshare.success.description": "会话已成功取消分享", "toast.session.unshare.failed.title": "取消分享失败", "toast.session.unshare.failed.description": "取消分享会话时发生错误", - "toast.session.listFailed.title": "无法加载 {{project}} 的会话", - "toast.update.title": "有可用更新", "toast.update.description": "OpenCode 有新版本 ({{version}}) 可安装。", "toast.update.action.installRestart": "安装并重启", @@ -413,10 +443,9 @@ export const dict = { "error.page.report.prefix": "请将此错误报告给 OpenCode 团队", "error.page.report.discord": "在 Discord 上", "error.page.version": "版本:{{version}}", - "error.dev.rootNotFound": "未找到根元素。你是不是忘了把它添加到 index.html?或者 id 属性拼写错了?", - "error.globalSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?", + "directory.error.invalidUrl": "URL 中的目录无效。", "error.chain.unknown": "未知错误", @@ -444,7 +473,6 @@ export const dict = { "notification.question.title": "问题", "notification.question.description": "{{sessionTitle}}({{projectName}})有一个问题", "notification.action.goToSession": "前往会话", - "notification.session.responseReady.title": "回复已就绪", "notification.session.error.title": "会话错误", "notification.session.error.fallbackDescription": "发生错误", @@ -470,17 +498,19 @@ export const dict = { "session.messages.loadingEarlier": "正在加载更早的消息...", "session.messages.loadEarlier": "加载更早的消息", "session.messages.loading": "正在加载消息...", - "session.messages.jumpToLatest": "跳转到最新", "session.context.addToContext": "将 {{selection}} 添加到上下文", - "session.new.worktree.main": "主分支", "session.new.worktree.mainWithBranch": "主分支({{branch}})", "session.new.worktree.create": "创建新的 worktree", "session.new.lastModified": "最后修改", - "session.header.search.placeholder": "搜索 {{project}}", "session.header.searchFiles": "搜索文件", + "session.header.openIn": "打开方式", + "session.header.open.action": "打开 {{app}}", + "session.header.open.ariaLabel": "在 {{app}} 中打开", + "session.header.open.menu": "打开选项", + "session.header.open.copyPath": "复制路径", "status.popover.trigger": "状态", "status.popover.ariaLabel": "服务器配置", @@ -506,13 +536,14 @@ export const dict = { "lsp.label.connected": "{{count}} LSP", "prompt.loading": "正在加载提示...", + "terminal.loading": "正在加载终端...", "terminal.title": "终端", "terminal.title.numbered": "终端 {{number}}", "terminal.close": "关闭终端", - "terminal.connectionLost.title": "连接已丢失", "terminal.connectionLost.description": "终端连接已中断。这可能发生在服务器重启时。", + "common.closeTab": "关闭标签页", "common.dismiss": "忽略", "common.requestFailed": "请求失败", @@ -525,8 +556,8 @@ export const dict = { "common.close": "关闭", "common.edit": "编辑", "common.loadMore": "加载更多", - "common.key.esc": "ESC", + "sidebar.menu.toggle": "切换菜单", "sidebar.nav.projectsAndSessions": "项目和会话", "sidebar.settings": "设置", @@ -538,18 +569,25 @@ export const dict = { "sidebar.gettingStarted.line2": "连接任意提供商即可使用更多模型,如 Claude、GPT、Gemini 等。", "sidebar.project.recentSessions": "最近会话", "sidebar.project.viewAllSessions": "查看全部会话", + "sidebar.project.clearNotifications": "清除通知", "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "桌面", "settings.section.server": "服务器", + "settings.tab.general": "通用", "settings.tab.shortcuts": "快捷键", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL 集成", + "settings.desktop.wsl.description": "在 Windows 的 WSL 环境中运行 OpenCode 服务器。", + "settings.general.section.appearance": "外观", "settings.general.section.notifications": "系统通知", "settings.general.section.updates": "更新", "settings.general.section.sounds": "音效", - + "settings.general.section.display": "显示", "settings.general.row.language.title": "语言", "settings.general.row.language.description": "更改 OpenCode 的显示语言", "settings.general.row.appearance.title": "外观", @@ -558,6 +596,9 @@ export const dict = { "settings.general.row.theme.description": "自定义 OpenCode 的主题。", "settings.general.row.font.title": "字体", "settings.general.row.font.description": "自定义代码块使用的等宽字体", + "settings.general.row.wayland.title": "使用原生 Wayland", + "settings.general.row.wayland.description": "在 Wayland 上禁用 X11 回退。需要重启。", + "settings.general.row.wayland.tooltip": "在混合刷新率显示器的 Linux 系统上,原生 Wayland 可能更稳定。", "settings.general.row.releaseNotes.title": "发行说明", "settings.general.row.releaseNotes.description": "更新后显示“新功能”弹窗", @@ -582,6 +623,8 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", + "sound.option.alert01": "警报 01", "sound.option.alert02": "警报 02", "sound.option.alert03": "警报 03", @@ -627,13 +670,13 @@ export const dict = { "sound.option.yup04": "是 04", "sound.option.yup05": "是 05", "sound.option.yup06": "是 06", + "settings.general.notifications.agent.title": "智能体", "settings.general.notifications.agent.description": "当智能体完成或需要注意时显示系统通知", "settings.general.notifications.permissions.title": "权限", "settings.general.notifications.permissions.description": "当需要权限时显示系统通知", "settings.general.notifications.errors.title": "错误", "settings.general.notifications.errors.description": "发生错误时显示系统通知", - "settings.general.sounds.agent.title": "智能体", "settings.general.sounds.agent.description": "当智能体完成或需要注意时播放声音", "settings.general.sounds.permissions.title": "权限", @@ -651,7 +694,6 @@ export const dict = { "settings.shortcuts.pressKeys": "按下按键", "settings.shortcuts.search.placeholder": "搜索快捷键", "settings.shortcuts.search.empty": "未找到快捷键", - "settings.shortcuts.group.general": "通用", "settings.shortcuts.group.session": "会话", "settings.shortcuts.group.navigation": "导航", @@ -668,12 +710,16 @@ export const dict = { "settings.providers.tag.config": "配置", "settings.providers.tag.custom": "自定义", "settings.providers.tag.other": "其他", + "settings.models.title": "模型", "settings.models.description": "模型设置将在此处可配置。", + "settings.agents.title": "智能体", "settings.agents.description": "智能体设置将在此处可配置。", + "settings.commands.title": "命令", "settings.commands.description": "命令设置将在此处可配置。", + "settings.mcp.title": "MCP", "settings.mcp.description": "MCP 设置将在此处可配置。", @@ -681,11 +727,9 @@ export const dict = { "settings.permissions.description": "控制服务器默认可以使用哪些工具。", "settings.permissions.section.tools": "工具", "settings.permissions.toast.updateFailed.title": "更新权限失败", - "settings.permissions.action.allow": "允许", "settings.permissions.action.ask": "询问", "settings.permissions.action.deny": "拒绝", - "settings.permissions.tool.read.title": "读取", "settings.permissions.tool.read.description": "读取文件(匹配文件路径)", "settings.permissions.tool.edit.title": "编辑", @@ -698,9 +742,9 @@ export const dict = { "settings.permissions.tool.list.description": "列出目录中的文件", "settings.permissions.tool.bash.title": "Bash", "settings.permissions.tool.bash.description": "运行 shell 命令", - "settings.permissions.tool.task.title": "Task", + "settings.permissions.tool.task.title": "任务", "settings.permissions.tool.task.description": "启动子智能体", - "settings.permissions.tool.skill.title": "Skill", + "settings.permissions.tool.skill.title": "技能", "settings.permissions.tool.skill.description": "按名称加载技能", "settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.description": "运行语言服务器查询", @@ -708,15 +752,15 @@ export const dict = { "settings.permissions.tool.todoread.description": "读取待办列表", "settings.permissions.tool.todowrite.title": "更新待办", "settings.permissions.tool.todowrite.description": "更新待办列表", - "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.title": "网页获取", "settings.permissions.tool.webfetch.description": "从 URL 获取内容", - "settings.permissions.tool.websearch.title": "Web Search", + "settings.permissions.tool.websearch.title": "网页搜索", "settings.permissions.tool.websearch.description": "搜索网页", - "settings.permissions.tool.codesearch.title": "Code Search", + "settings.permissions.tool.codesearch.title": "代码搜索", "settings.permissions.tool.codesearch.description": "在网上搜索代码", "settings.permissions.tool.external_directory.title": "外部目录", "settings.permissions.tool.external_directory.description": "访问项目目录之外的文件", - "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.title": "死循环", "settings.permissions.tool.doom_loop.description": "检测具有相同输入的重复工具调用", "session.delete.failed.title": "删除会话失败", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 319f5c51d15c..349c90b0e111 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -48,8 +48,10 @@ export const dict = { "command.session.new": "新增工作階段", "command.file.open": "開啟檔案", + "command.tab.close": "關閉分頁", "command.context.addSelection": "將選取內容加入上下文", "command.context.addSelection.description": "加入目前檔案中選取的行", + "command.input.focus": "聚焦輸入框", "command.terminal.toggle": "切換終端機", "command.fileTree.toggle": "切換檔案樹", "command.review.toggle": "切換審查", @@ -74,6 +76,7 @@ export const dict = { "command.permissions.autoaccept.enable": "自動接受編輯", "command.permissions.autoaccept.disable": "停止自動接受編輯", "command.workspace.toggle": "切換工作區", + "command.workspace.toggle.description": "在側邊欄啟用或停用多個工作區", "command.session.undo": "復原", "command.session.undo.description": "復原上一則訊息", "command.session.redo": "重做", @@ -97,9 +100,13 @@ export const dict = { "dialog.provider.group.popular": "熱門", "dialog.provider.group.other": "其他", "dialog.provider.tag.recommended": "推薦", + "dialog.provider.opencode.note": "精選模型,包含 Claude、GPT、Gemini 等等", "dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 金鑰連線", "dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 金鑰連線", "dialog.provider.copilot.note": "使用 Copilot 或 API 金鑰連線", + "dialog.provider.google.note": "Gemini 模型,提供快速且結構化的回應", + "dialog.provider.openrouter.note": "從單一提供者存取所有支援的模型", + "dialog.provider.vercel.note": "透過智慧路由統一存取 AI 模型", "dialog.model.select.title": "選擇模型", "dialog.model.search.placeholder": "搜尋模型", @@ -202,6 +209,7 @@ export const dict = { "model.tooltip.context": "上下文上限 {{limit}}", "common.search.placeholder": "搜尋", "common.goBack": "返回", + "common.goForward": "前進", "common.loading": "載入中", "common.loading.ellipsis": "...", "common.cancel": "取消", @@ -249,6 +257,7 @@ export const dict = { "prompt.popover.emptyResults": "沒有符合的結果", "prompt.popover.emptyCommands": "沒有符合的命令", "prompt.dropzone.label": "將圖片或 PDF 拖到這裡", + "prompt.dropzone.file.label": "拖放以 @提及檔案", "prompt.slash.badge.custom": "自訂", "prompt.slash.badge.skill": "技能", "prompt.slash.badge.mcp": "mcp", @@ -270,6 +279,7 @@ export const dict = { "prompt.toast.shellSendFailed.title": "傳送 shell 命令失敗", "prompt.toast.commandSendFailed.title": "傳送命令失敗", "prompt.toast.promptSendFailed.title": "傳送提示失敗", + "prompt.toast.promptSendFailed.description": "無法取得工作階段", "dialog.mcp.title": "MCP", "dialog.mcp.description": "已啟用 {{enabled}} / {{total}}", @@ -358,6 +368,23 @@ export const dict = { "context.usage.clickToView": "點擊查看上下文", "context.usage.view": "檢視上下文用量", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", + "toast.language.title": "語言", "toast.language.description": "已切換到 {{language}}", @@ -478,6 +505,11 @@ export const dict = { "session.header.search.placeholder": "搜尋 {{project}}", "session.header.searchFiles": "搜尋檔案", + "session.header.openIn": "開啟於", + "session.header.open.action": "開啟 {{app}}", + "session.header.open.ariaLabel": "在 {{app}} 中開啟", + "session.header.open.menu": "開啟選項", + "session.header.open.copyPath": "複製路徑", "status.popover.trigger": "狀態", "status.popover.ariaLabel": "伺服器設定", @@ -535,17 +567,22 @@ export const dict = { "sidebar.gettingStarted.line2": "連線任意提供者即可使用更多模型,如 Claude、GPT、Gemini 等。", "sidebar.project.recentSessions": "最近工作階段", "sidebar.project.viewAllSessions": "查看全部工作階段", + "sidebar.project.clearNotifications": "清除通知", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "桌面", "settings.section.server": "伺服器", "settings.tab.general": "一般", "settings.tab.shortcuts": "快速鍵", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "外觀", "settings.general.section.notifications": "系統通知", "settings.general.section.updates": "更新", "settings.general.section.sounds": "音效", + "settings.general.section.display": "顯示", "settings.general.row.language.title": "語言", "settings.general.row.language.description": "變更 OpenCode 的顯示語言", @@ -556,6 +593,10 @@ export const dict = { "settings.general.row.font.title": "字型", "settings.general.row.font.description": "自訂程式碼區塊使用的等寬字型", + "settings.general.row.wayland.title": "使用原生 Wayland", + "settings.general.row.wayland.description": "在 Wayland 上停用 X11 後備模式。需要重新啟動。", + "settings.general.row.wayland.tooltip": "在混合更新率螢幕的 Linux 系統上,原生 Wayland 可能更穩定。", + "settings.general.row.releaseNotes.title": "發行說明", "settings.general.row.releaseNotes.description": "更新後顯示「新功能」彈出視窗", @@ -580,6 +621,7 @@ export const dict = { "font.option.robotoMono": "Roboto Mono", "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", + "font.option.geistMono": "Geist Mono", "sound.option.alert01": "警報 01", "sound.option.alert02": "警報 02", "sound.option.alert03": "警報 03", diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts index fb6682009274..33c22f099e35 100644 --- a/packages/app/src/index.ts +++ b/packages/app/src/index.ts @@ -1,3 +1,4 @@ -export { PlatformProvider, type Platform } from "./context/platform" +export { PlatformProvider, type Platform, type DisplayBackend } from "./context/platform" export { AppBaseProviders, AppInterface } from "./app" export { useCommand } from "./context/command" +export { handleNotificationClick } from "./utils/notification-click" diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx index 2f4db8564998..2dee09dfb06c 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -1,20 +1,47 @@ import { createEffect, createMemo, Show, type ParentProps } from "solid-js" +import { createStore } from "solid-js/store" import { useNavigate, useParams } from "@solidjs/router" import { SDKProvider, useSDK } from "@/context/sdk" import { SyncProvider, useSync } from "@/context/sync" import { LocalProvider } from "@/context/local" import { DataProvider } from "@opencode-ai/ui/context" -import { iife } from "@opencode-ai/util/iife" import type { QuestionAnswer } from "@opencode-ai/sdk/v2" import { decode64 } from "@/utils/base64" import { showToast } from "@opencode-ai/ui/toast" import { useLanguage } from "@/context/language" +function DirectoryDataProvider(props: ParentProps<{ directory: string }>) { + const params = useParams() + const navigate = useNavigate() + const sync = useSync() + const sdk = useSDK() + + return ( + sdk.client.permission.respond(input)} + onQuestionReply={(input: { requestID: string; answers: QuestionAnswer[] }) => sdk.client.question.reply(input)} + onQuestionReject={(input: { requestID: string }) => sdk.client.question.reject(input)} + onNavigateToSession={(sessionID: string) => navigate(`/${params.dir}/session/${sessionID}`)} + onSessionHref={(sessionID: string) => `/${params.dir}/session/${sessionID}`} + onSyncSession={(sessionID: string) => sync.session.sync(sessionID)} + > + {props.children} + + ) +} + export default function Layout(props: ParentProps) { const params = useParams() const navigate = useNavigate() const language = useLanguage() + const [store, setStore] = createStore({ invalid: "" }) const directory = createMemo(() => { return decode64(params.dir) ?? "" }) @@ -22,48 +49,20 @@ export default function Layout(props: ParentProps) { createEffect(() => { if (!params.dir) return if (directory()) return + if (store.invalid === params.dir) return + setStore("invalid", params.dir) showToast({ variant: "error", title: language.t("common.requestFailed"), description: language.t("directory.error.invalidUrl"), }) - navigate("/") + navigate("/", { replace: true }) }) return ( - {iife(() => { - const sync = useSync() - const sdk = useSDK() - const respond = (input: { - sessionID: string - permissionID: string - response: "once" | "always" | "reject" - }) => sdk.client.permission.respond(input) - - const replyToQuestion = (input: { requestID: string; answers: QuestionAnswer[] }) => - sdk.client.question.reply(input) - - const rejectQuestion = (input: { requestID: string }) => sdk.client.question.reject(input) - - const navigateToSession = (sessionID: string) => { - navigate(`/${params.dir}/session/${sessionID}`) - } - - return ( - - {props.children} - - ) - })} + {props.children} diff --git a/packages/app/src/pages/error.tsx b/packages/app/src/pages/error.tsx index 6d6faf6fa3bb..a30d86d18093 100644 --- a/packages/app/src/pages/error.tsx +++ b/packages/app/src/pages/error.tsx @@ -13,6 +13,17 @@ export type InitError = { } type Translator = ReturnType["t"] +const CHAIN_SEPARATOR = "\n" + "─".repeat(40) + "\n" + +function isIssue(value: unknown): value is { message: string; path: string[] } { + if (!value || typeof value !== "object") return false + if (!("message" in value) || !("path" in value)) return false + const message = (value as { message: unknown }).message + const path = (value as { path: unknown }).path + if (typeof message !== "string") return false + if (!Array.isArray(path)) return false + return path.every((part) => typeof part === "string") +} function isInitError(error: unknown): error is InitError { return ( @@ -112,9 +123,7 @@ function formatInitError(error: InitError, t: Translator): string { } case "ConfigInvalidError": { const issues = Array.isArray(data.issues) - ? data.issues.map( - (issue: { message: string; path: string[] }) => "↳ " + issue.message + " " + issue.path.join("."), - ) + ? data.issues.filter(isIssue).map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) : [] const message = typeof data.message === "string" ? data.message : "" const path = typeof data.path === "string" ? data.path : safeJson(data.path) @@ -139,14 +148,14 @@ function formatErrorChain(error: unknown, t: Translator, depth = 0, parentMessag if (isInitError(error)) { const message = formatInitError(error, t) if (depth > 0 && parentMessage === message) return "" - const indent = depth > 0 ? `\n${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : "" + const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : "" return indent + `${error.name}\n${message}` } if (error instanceof Error) { const isDuplicate = depth > 0 && parentMessage === error.message const parts: string[] = [] - const indent = depth > 0 ? `\n${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : "" + const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : "" const header = `${error.name}${error.message ? `: ${error.message}` : ""}` const stack = error.stack?.trim() @@ -190,11 +199,11 @@ function formatErrorChain(error: unknown, t: Translator, depth = 0, parentMessag if (typeof error === "string") { if (depth > 0 && parentMessage === error) return "" - const indent = depth > 0 ? `\n${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : "" + const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : "" return indent + error } - const indent = depth > 0 ? `\n${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : "" + const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : "" return indent + safeJson(error) } @@ -212,20 +221,35 @@ export const ErrorPage: Component = (props) => { const [store, setStore] = createStore({ checking: false, version: undefined as string | undefined, + actionError: undefined as string | undefined, }) async function checkForUpdates() { if (!platform.checkUpdate) return setStore("checking", true) - const result = await platform.checkUpdate() - setStore("checking", false) - if (result.updateAvailable && result.version) setStore("version", result.version) + await platform + .checkUpdate() + .then((result) => { + setStore("actionError", undefined) + if (result.updateAvailable && result.version) setStore("version", result.version) + }) + .catch((err) => { + setStore("actionError", formatError(err, language.t)) + }) + .finally(() => { + setStore("checking", false) + }) } async function installUpdate() { if (!platform.update || !platform.restart) return - await platform.update() - await platform.restart() + await platform + .update() + .then(() => platform.restart!()) + .then(() => setStore("actionError", undefined)) + .catch((err) => { + setStore("actionError", formatError(err, language.t)) + }) } return ( @@ -266,6 +290,9 @@ export const ErrorPage: Component = (props) => {
+ + {(message) =>

{message()}

} +
{language.t("error.page.report.prefix")} diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 10f7dac530ba..ba3a2b942708 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -25,10 +25,18 @@ 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) }) + const serverDotClass = createMemo(() => { + const healthy = server.healthy() + if (healthy === true) return "bg-icon-success-base" + if (healthy === false) return "bg-icon-critical-base" + return "bg-border-weak-base" + }) + function openProject(directory: string) { layout.projects.open(directory) server.projects.touch(directory) @@ -72,9 +80,7 @@ export default function Home() {
{server.name} @@ -114,8 +120,7 @@ export default function Home() {
{language.t("home.empty.title")}
{language.t("home.empty.description")}
-
-
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 59adef4694aa..7d4a5c0cb81c 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -2,6 +2,7 @@ import { batch, createEffect, createMemo, + createSignal, For, on, onCleanup, @@ -34,6 +35,7 @@ import type { DragEvent } from "@thisbeyond/solid-dnd" import { useProviders } from "@/hooks/use-providers" import { showToast, Toast, toaster } from "@opencode-ai/ui/toast" import { useGlobalSDK } from "@/context/global-sdk" +import { clearWorkspaceTerminals } from "@/context/terminal" import { useNotification } from "@/context/notification" import { usePermission } from "@/context/permission" import { Binary } from "@opencode-ai/util/binary" @@ -123,7 +125,7 @@ export default function Layout(props: ParentProps) { const [state, setState] = createStore({ autoselect: !initialDirectory, - busyWorkspaces: new Set(), + busyWorkspaces: {} as Record, hoverSession: undefined as string | undefined, hoverProject: undefined as string | undefined, scrollSessionKey: undefined as string | undefined, @@ -133,15 +135,28 @@ export default function Layout(props: ParentProps) { const editor = createInlineEditorController() const setBusy = (directory: string, value: boolean) => { const key = workspaceKey(directory) - setState("busyWorkspaces", (prev) => { - const next = new Set(prev) - if (value) next.add(key) - else next.delete(key) - return next - }) + if (value) { + setState("busyWorkspaces", key, true) + return + } + setState( + "busyWorkspaces", + produce((draft) => { + delete draft[key] + }), + ) } - const isBusy = (directory: string) => state.busyWorkspaces.has(workspaceKey(directory)) + const isBusy = (directory: string) => !!state.busyWorkspaces[workspaceKey(directory)] const navLeave = { current: undefined as number | undefined } + const [sortNow, setSortNow] = createSignal(Date.now()) + let sortNowInterval: ReturnType | undefined + const sortNowTimeout = setTimeout( + () => { + setSortNow(Date.now()) + sortNowInterval = setInterval(() => setSortNow(Date.now()), 60_000) + }, + 60_000 - (Date.now() % 60_000), + ) const aim = createAim({ enabled: () => !layout.sidebar.opened(), @@ -156,6 +171,8 @@ export default function Layout(props: ParentProps) { onCleanup(() => { if (navLeave.current !== undefined) clearTimeout(navLeave.current) + clearTimeout(sortNowTimeout) + if (sortNowInterval) clearInterval(sortNowInterval) aim.reset() }) @@ -181,20 +198,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 @@ -220,6 +223,18 @@ export default function Layout(props: ParentProps) { const setEditor = editor.setEditor const InlineEditor = editor.InlineEditor + const clearSidebarHoverState = () => { + if (layout.sidebar.opened()) return + setState("hoverSession", undefined) + setState("hoverProject", undefined) + } + + const navigateWithSidebarReset = (href: string) => { + clearSidebarHoverState() + navigate(href) + layout.mobileSidebar.hide() + } + function cycleTheme(direction = 1) { const ids = availableThemeEntries().map(([id]) => id) if (ids.length === 0) return @@ -265,166 +280,169 @@ export default function Layout(props: ParentProps) { setLocale(next) } - onMount(() => { - if (!platform.checkUpdate || !platform.update || !platform.restart) return - - let toastId: number | undefined - let interval: ReturnType | undefined - - async function pollUpdate() { - const { updateAvailable, version } = await platform.checkUpdate!() - if (updateAvailable && toastId === undefined) { - toastId = showToast({ - persistent: true, - icon: "download", - title: language.t("toast.update.title"), - description: language.t("toast.update.description", { version: version ?? "" }), - actions: [ - { - label: language.t("toast.update.action.installRestart"), - onClick: async () => { - await platform.update!() - await platform.restart!() + const useUpdatePolling = () => + onMount(() => { + if (!platform.checkUpdate || !platform.update || !platform.restart) return + + let toastId: number | undefined + let interval: ReturnType | undefined + + const pollUpdate = () => + platform.checkUpdate!().then(({ updateAvailable, version }) => { + if (!updateAvailable) return + if (toastId !== undefined) return + toastId = showToast({ + persistent: true, + icon: "download", + title: language.t("toast.update.title"), + description: language.t("toast.update.description", { version: version ?? "" }), + actions: [ + { + label: language.t("toast.update.action.installRestart"), + onClick: async () => { + await platform.update!() + await platform.restart!() + }, }, - }, - { - label: language.t("toast.update.action.notYet"), - onClick: "dismiss", - }, - ], + { + label: language.t("toast.update.action.notYet"), + onClick: "dismiss", + }, + ], + }) }) - } - } - createEffect(() => { - if (!settings.ready()) return + createEffect(() => { + if (!settings.ready()) return - if (!settings.updates.startup()) { + if (!settings.updates.startup()) { + if (interval === undefined) return + clearInterval(interval) + interval = undefined + return + } + + if (interval !== undefined) return + void pollUpdate() + interval = setInterval(pollUpdate, 10 * 60 * 1000) + }) + + onCleanup(() => { if (interval === undefined) return clearInterval(interval) - interval = undefined - return - } - - if (interval !== undefined) return - void pollUpdate() - interval = setInterval(pollUpdate, 10 * 60 * 1000) + }) }) - onCleanup(() => { - if (interval === undefined) return - clearInterval(interval) - }) - }) + const useSDKNotificationToasts = () => + onMount(() => { + const toastBySession = new Map() + const alertedAtBySession = new Map() + const cooldownMs = 5000 - onMount(() => { - const toastBySession = new Map() - const alertedAtBySession = new Map() - const cooldownMs = 5000 - - const unsub = globalSDK.event.listen((e) => { - if (e.details?.type === "worktree.ready") { - setBusy(e.name, false) - WorktreeState.ready(e.name) - return + const dismissSessionAlert = (sessionKey: string) => { + const toastId = toastBySession.get(sessionKey) + if (toastId === undefined) return + toaster.dismiss(toastId) + toastBySession.delete(sessionKey) + alertedAtBySession.delete(sessionKey) } - if (e.details?.type === "worktree.failed") { - setBusy(e.name, false) - WorktreeState.failed(e.name, e.details.properties?.message ?? language.t("common.requestFailed")) - return - } + const unsub = globalSDK.event.listen((e) => { + if (e.details?.type === "worktree.ready") { + setBusy(e.name, false) + WorktreeState.ready(e.name) + return + } - if (e.details?.type !== "permission.asked" && e.details?.type !== "question.asked") return - const title = - e.details.type === "permission.asked" - ? language.t("notification.permission.title") - : language.t("notification.question.title") - const icon = e.details.type === "permission.asked" ? ("checklist" as const) : ("bubble-5" as const) - const directory = e.name - const props = e.details.properties - if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return - - const [store] = globalSync.child(directory, { bootstrap: false }) - const session = store.session.find((s) => s.id === props.sessionID) - const sessionKey = `${directory}:${props.sessionID}` - - const sessionTitle = session?.title ?? language.t("command.session.new") - const projectName = getFilename(directory) - const description = - e.details.type === "permission.asked" - ? language.t("notification.permission.description", { sessionTitle, projectName }) - : language.t("notification.question.description", { sessionTitle, projectName }) - const href = `/${base64Encode(directory)}/session/${props.sessionID}` - - const now = Date.now() - const lastAlerted = alertedAtBySession.get(sessionKey) ?? 0 - if (now - lastAlerted < cooldownMs) return - alertedAtBySession.set(sessionKey, now) - - if (e.details.type === "permission.asked") { - playSound(soundSrc(settings.sounds.permissions())) - if (settings.notifications.permissions()) { - void platform.notify(title, description, href) + if (e.details?.type === "worktree.failed") { + setBusy(e.name, false) + WorktreeState.failed(e.name, e.details.properties?.message ?? language.t("common.requestFailed")) + return + } + + if (e.details?.type !== "permission.asked" && e.details?.type !== "question.asked") return + const title = + e.details.type === "permission.asked" + ? language.t("notification.permission.title") + : language.t("notification.question.title") + const icon = e.details.type === "permission.asked" ? ("checklist" as const) : ("bubble-5" as const) + const directory = e.name + const props = e.details.properties + if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return + + const [store] = globalSync.child(directory, { bootstrap: false }) + const session = store.session.find((s) => s.id === props.sessionID) + const sessionKey = `${directory}:${props.sessionID}` + + const sessionTitle = session?.title ?? language.t("command.session.new") + const projectName = getFilename(directory) + const description = + e.details.type === "permission.asked" + ? language.t("notification.permission.description", { sessionTitle, projectName }) + : language.t("notification.question.description", { sessionTitle, projectName }) + const href = `/${base64Encode(directory)}/session/${props.sessionID}` + + const now = Date.now() + const lastAlerted = alertedAtBySession.get(sessionKey) ?? 0 + if (now - lastAlerted < cooldownMs) return + alertedAtBySession.set(sessionKey, now) + + if (e.details.type === "permission.asked") { + if (settings.sounds.permissionsEnabled()) { + playSound(soundSrc(settings.sounds.permissions())) + } + if (settings.notifications.permissions()) { + void platform.notify(title, description, href) + } } - } - if (e.details.type === "question.asked") { - if (settings.notifications.agent()) { - void platform.notify(title, description, href) + if (e.details.type === "question.asked") { + if (settings.notifications.agent()) { + void platform.notify(title, description, href) + } } - } - const currentSession = params.id - if (directory === currentDir() && props.sessionID === currentSession) return - if (directory === currentDir() && session?.parentID === currentSession) return - - const existingToastId = toastBySession.get(sessionKey) - if (existingToastId !== undefined) toaster.dismiss(existingToastId) - - const toastId = showToast({ - persistent: true, - icon, - title, - description, - actions: [ - { - label: language.t("notification.action.goToSession"), - onClick: () => navigate(href), - }, - { - label: language.t("common.dismiss"), - onClick: "dismiss", - }, - ], + const currentSession = params.id + if (directory === currentDir() && props.sessionID === currentSession) return + if (directory === currentDir() && session?.parentID === currentSession) return + + dismissSessionAlert(sessionKey) + + const toastId = showToast({ + persistent: true, + icon, + title, + description, + actions: [ + { + label: language.t("notification.action.goToSession"), + onClick: () => navigate(href), + }, + { + label: language.t("common.dismiss"), + onClick: "dismiss", + }, + ], + }) + toastBySession.set(sessionKey, toastId) }) - toastBySession.set(sessionKey, toastId) - }) - onCleanup(unsub) - - createEffect(() => { - const currentSession = params.id - if (!currentDir() || !currentSession) return - const sessionKey = `${currentDir()}:${currentSession}` - const toastId = toastBySession.get(sessionKey) - if (toastId !== undefined) { - toaster.dismiss(toastId) - toastBySession.delete(sessionKey) - alertedAtBySession.delete(sessionKey) - } - const [store] = globalSync.child(currentDir(), { bootstrap: false }) - const childSessions = store.session.filter((s) => s.parentID === currentSession) - for (const child of childSessions) { - const childKey = `${currentDir()}:${child.id}` - const childToastId = toastBySession.get(childKey) - if (childToastId !== undefined) { - toaster.dismiss(childToastId) - toastBySession.delete(childKey) - alertedAtBySession.delete(childKey) + onCleanup(unsub) + + createEffect(() => { + const currentSession = params.id + if (!currentDir() || !currentSession) return + const sessionKey = `${currentDir()}:${currentSession}` + dismissSessionAlert(sessionKey) + const [store] = globalSync.child(currentDir(), { bootstrap: false }) + const childSessions = store.session.filter((s) => s.parentID === currentSession) + for (const child of childSessions) { + dismissSessionAlert(`${currentDir()}:${child.id}`) } - } + }) }) - }) + + useUpdatePolling() + useSDKNotificationToasts() function scrollToSession(sessionId: string, sessionKey: string) { if (!scrollContainerRef) return @@ -518,10 +536,13 @@ export default function Layout(props: ParentProps) { const setWorkspaceName = (directory: string, next: string, projectId?: string, branch?: string) => { const key = workspaceKey(directory) - setStore("workspaceName", (prev) => ({ ...(prev ?? {}), [key]: next })) + setStore("workspaceName", key, next) if (!projectId) return if (!branch) return - setStore("workspaceBranchName", projectId, (prev) => ({ ...(prev ?? {}), [branch]: next })) + if (!store.workspaceBranchName[projectId]) { + setStore("workspaceBranchName", projectId, {}) + } + setStore("workspaceBranchName", projectId, branch, next) } const workspaceLabel = (directory: string, branch?: string, projectId?: string) => @@ -654,6 +675,21 @@ export default function Layout(props: ParentProps) { return created } + const mergeByID = (current: T[], incoming: T[]) => { + if (current.length === 0) { + return incoming.slice().sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) + } + + const map = new Map() + for (const item of current) { + map.set(item.id, item) + } + for (const item of incoming) { + map.set(item.id, item) + } + return [...map.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) + } + async function prefetchMessages(directory: string, sessionID: string, token: number) { const [store, setStore] = globalSync.child(directory, { bootstrap: false }) @@ -662,51 +698,24 @@ export default function Layout(props: ParentProps) { if (prefetchToken.value !== token) return const items = (messages.data ?? []).filter((x) => !!x?.info?.id) - const next = items - .map((x) => x.info) - .filter((m) => !!m?.id) - .slice() - .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) + const next = items.map((x) => x.info).filter((m): m is Message => !!m?.id) + const sorted = mergeByID([], next) const current = store.message[sessionID] ?? [] - const merged = (() => { - if (current.length === 0) return next - - const map = new Map() - for (const item of current) { - if (!item?.id) continue - map.set(item.id, item) - } - for (const item of next) { - map.set(item.id, item) - } - return [...map.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) - })() + const merged = mergeByID( + current.filter((item): item is Message => !!item?.id), + sorted, + ) batch(() => { setStore("message", sessionID, reconcile(merged, { key: "id" })) for (const message of items) { const currentParts = store.part[message.info.id] ?? [] - const mergedParts = (() => { - if (currentParts.length === 0) { - return message.parts - .filter((p) => !!p?.id) - .slice() - .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) - } - - const map = new Map() - for (const item of currentParts) { - if (!item?.id) continue - map.set(item.id, item) - } - for (const item of message.parts) { - if (!item?.id) continue - map.set(item.id, item) - } - return [...map.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) - })() + const mergedParts = mergeByID( + currentParts.filter((item): item is (typeof currentParts)[number] & { id: string } => !!item?.id), + message.parts.filter((item): item is (typeof message.parts)[number] & { id: string } => !!item?.id), + ) setStore("part", message.info.id, reconcile(mergedParts, { key: "id" })) } @@ -1086,24 +1095,14 @@ export default function Layout(props: ParentProps) { function navigateToProject(directory: string | undefined) { if (!directory) return - if (!layout.sidebar.opened()) { - setState("hoverSession", undefined) - setState("hoverProject", undefined) - } server.projects.touch(directory) const lastSession = store.lastSession[directory] - navigate(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`) - layout.mobileSidebar.hide() + navigateWithSidebarReset(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`) } function navigateToSession(session: Session | undefined) { if (!session) return - if (!layout.sidebar.opened()) { - setState("hoverSession", undefined) - setState("hoverProject", undefined) - } - navigate(`/${base64Encode(session.directory)}/session/${session.id}`) - layout.mobileSidebar.hide() + navigateWithSidebarReset(`/${base64Encode(session.directory)}/session/${session.id}`) } function openProject(directory: string, navigate = true) { @@ -1216,6 +1215,16 @@ export default function Layout(props: ParentProps) { if (!result) return + globalSync.set( + "project", + produce((draft) => { + const project = draft.find((item) => item.worktree === root) + if (!project) return + project.sandboxes = (project.sandboxes ?? []).filter((sandbox) => sandbox !== directory) + }), + ) + setStore("workspaceOrder", root, (order) => (order ?? []).filter((workspace) => workspace !== directory)) + layout.projects.close(directory) layout.projects.open(root) @@ -1235,11 +1244,18 @@ export default function Layout(props: ParentProps) { }) const dismiss = () => toaster.dismiss(progress) - const sessions = await globalSDK.client.session + const sessions: Session[] = await globalSDK.client.session .list({ directory }) .then((x) => x.data ?? []) .catch(() => []) + clearWorkspaceTerminals( + directory, + sessions.map((s) => s.id), + platform, + ) + await globalSDK.client.instance.dispose({ directory }).catch(() => undefined) + const result = await globalSDK.client.worktree .reset({ directory: root, worktreeResetInput: { directory } }) .then((x) => x.data) @@ -1272,8 +1288,6 @@ export default function Layout(props: ParentProps) { ), ) - await globalSDK.client.instance.dispose({ directory }).catch(() => undefined) - setBusy(directory, false) dismiss() @@ -1454,23 +1468,41 @@ export default function Layout(props: ParentProps) { document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`) }) + const loadedSessionDirs = new Set() + createEffect(() => { const project = currentProject() - if (!project) return + const workspaces = workspaceSetting() + const next = new Set() + if (!project) { + loadedSessionDirs.clear() + return + } - if (workspaceSetting()) { + if (workspaces) { const activeDir = currentDir() const dirs = [project.worktree, ...(project.sandboxes ?? [])] for (const directory of dirs) { const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree const active = directory === activeDir if (!expanded && !active) continue - globalSync.project.loadSessions(directory) + next.add(directory) } - return } - globalSync.project.loadSessions(project.worktree) + if (!workspaces) { + next.add(project.worktree) + } + + for (const directory of next) { + if (loadedSessionDirs.has(directory)) continue + globalSync.project.loadSessions(directory) + } + + loadedSessionDirs.clear() + for (const directory of next) { + loadedSessionDirs.add(directory) + } }) function handleDragStart(event: unknown) { @@ -1553,10 +1585,7 @@ export default function Layout(props: ParentProps) { } const createWorkspace = async (project: LocalProject) => { - if (!layout.sidebar.opened()) { - setState("hoverSession", undefined) - setState("hoverProject", undefined) - } + clearSidebarHoverState() const created = await globalSDK.client.worktree .create({ directory: project.worktree }) .then((x) => x.data) @@ -1593,8 +1622,7 @@ export default function Layout(props: ParentProps) { }) globalSync.child(created.directory) - navigate(`/${base64Encode(created.directory)}/session`) - layout.mobileSidebar.hide() + navigateWithSidebarReset(`/${base64Encode(created.directory)}/session`) } const workspaceSidebarCtx: WorkspaceSidebarContext = { @@ -1664,6 +1692,13 @@ export default function Layout(props: ParentProps) { }) const projectId = createMemo(() => panelProps.project?.id ?? "") const workspaces = createMemo(() => workspaceIds(panelProps.project)) + const unseenCount = createMemo(() => + workspaces().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), + ) + const clearNotifications = () => + workspaces() + .filter((directory) => notification.project.unseenCount(directory) > 0) + .forEach((directory) => notification.project.markViewed(directory)) const workspacesEnabled = createMemo(() => { const project = panelProps.project if (!project) return false @@ -1741,6 +1776,16 @@ export default function Layout(props: ParentProps) { : language.t("sidebar.workspaces.enable")} + + + {language.t("sidebar.project.clearNotifications")} + + { - if (!layout.sidebar.opened()) { - setState("hoverSession", undefined) - setState("hoverProject", undefined) - } - navigate(`/${base64Encode(p().worktree)}/session`) - layout.mobileSidebar.hide() - }} + onClick={() => navigateWithSidebarReset(`/${base64Encode(p().worktree)}/session`)} > {language.t("command.session.new")}
- +
} @@ -1823,6 +1866,7 @@ export default function Layout(props: ParentProps) { ctx={workspaceSidebarCtx} directory={directory} project={p()} + sortNow={sortNow} mobile={panelProps.mobile} /> )} @@ -1908,7 +1952,9 @@ export default function Layout(props: ParentProps) { opened={() => layout.sidebar.opened()} aimMove={aim.move} projects={() => layout.projects.list()} - renderProject={(project) => } + renderProject={(project) => ( + + )} handleDragStart={handleDragStart} handleDragEnd={handleDragEnd} handleDragOver={handleDragOver} @@ -1926,10 +1972,10 @@ export default function Layout(props: ParentProps) { renderPanel={() => } />
- - {(project) => ( + + {(worktree) => (
- +
)}
@@ -1938,7 +1984,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} @@ -1971,7 +2017,9 @@ export default function Layout(props: ParentProps) { opened={() => layout.sidebar.opened()} aimMove={aim.move} projects={() => layout.projects.list()} - renderProject={(project) => } + renderProject={(project) => ( + + )} handleDragStart={handleDragStart} handleDragEnd={handleDragEnd} handleDragOver={handleDragOver} diff --git a/packages/app/src/pages/layout/deep-links.ts b/packages/app/src/pages/layout/deep-links.ts index 772e6ece6b93..7bdb002a366e 100644 --- a/packages/app/src/pages/layout/deep-links.ts +++ b/packages/app/src/pages/layout/deep-links.ts @@ -2,7 +2,15 @@ export const deepLinkEvent = "opencode:deep-link" export const parseDeepLink = (input: string) => { if (!input.startsWith("opencode://")) return - const url = new URL(input) + if (typeof URL.canParse === "function" && !URL.canParse(input)) return + const url = (() => { + try { + return new URL(input) + } catch { + return undefined + } + })() + if (!url) return if (url.hostname !== "open-project") return const directory = url.searchParams.get("directory") if (!directory) return diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts index 8a8ea78c7793..83d8f4748aba 100644 --- a/packages/app/src/pages/layout/helpers.test.ts +++ b/packages/app/src/pages/layout/helpers.test.ts @@ -12,6 +12,27 @@ describe("layout deep links", () => { expect(parseDeepLink("https://example.com")).toBeUndefined() }) + test("ignores malformed deep links safely", () => { + expect(() => parseDeepLink("opencode://open-project/%E0%A4%A%")).not.toThrow() + expect(parseDeepLink("opencode://open-project/%E0%A4%A%")).toBeUndefined() + }) + + test("parses links when URL.canParse is unavailable", () => { + const original = Object.getOwnPropertyDescriptor(URL, "canParse") + Object.defineProperty(URL, "canParse", { configurable: true, value: undefined }) + try { + expect(parseDeepLink("opencode://open-project?directory=/tmp/demo")).toBe("/tmp/demo") + } finally { + if (original) Object.defineProperty(URL, "canParse", original) + if (!original) Reflect.deleteProperty(URL, "canParse") + } + }) + + test("ignores open-project deep links without directory", () => { + expect(parseDeepLink("opencode://open-project")).toBeUndefined() + expect(parseDeepLink("opencode://open-project?directory=")).toBeUndefined() + }) + test("collects only valid open-project directories", () => { const result = collectOpenProjectDeepLinks([ "opencode://open-project?directory=/a", @@ -39,6 +60,14 @@ describe("layout workspace helpers", () => { expect(workspaceKey("C:\\tmp\\demo\\\\")).toBe("C:\\tmp\\demo") }) + test("preserves posix and drive roots in workspace key", () => { + expect(workspaceKey("/")).toBe("/") + expect(workspaceKey("///")).toBe("/") + expect(workspaceKey("C:\\")).toBe("C:\\") + expect(workspaceKey("C:\\\\\\")).toBe("C:\\") + expect(workspaceKey("C:///")).toBe("C:/") + }) + test("keeps local first while preserving known order", () => { const result = syncWorkspaceOrder("/root", ["/root", "/b", "/c"], ["/root", "/c", "/a", "/b"]) expect(result).toEqual(["/root", "/c", "/b"]) diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts index 4d144f34ec5c..6a1e7c0123d8 100644 --- a/packages/app/src/pages/layout/helpers.ts +++ b/packages/app/src/pages/layout/helpers.ts @@ -1,7 +1,12 @@ import { getFilename } from "@opencode-ai/util/path" import { type Session } from "@opencode-ai/sdk/v2/client" -export const workspaceKey = (directory: string) => directory.replace(/[\\/]+$/, "") +export const workspaceKey = (directory: string) => { + const drive = directory.match(/^([A-Za-z]:)[\\/]+$/) + if (drive) return `${drive[1]}${directory.includes("\\") ? "\\" : "/"}` + if (/^[\\/]+$/.test(directory)) return directory.includes("\\") ? "\\" : "/" + return directory.replace(/[\\/]+$/, "") +} export function sortSessions(now: number) { const oneMinuteAgo = now - 60 * 1000 @@ -21,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() diff --git a/packages/app/src/pages/layout/inline-editor.tsx b/packages/app/src/pages/layout/inline-editor.tsx index 0bbfe244ccbe..4189e4a72a08 100644 --- a/packages/app/src/pages/layout/inline-editor.tsx +++ b/packages/app/src/pages/layout/inline-editor.tsx @@ -1,8 +1,9 @@ import { createStore } from "solid-js/store" -import { Show, type Accessor } from "solid-js" +import { onCleanup, Show, type Accessor } from "solid-js" import { InlineInput } from "@opencode-ai/ui/inline-input" export function createInlineEditorController() { + // This controller intentionally supports one active inline editor at a time. const [editor, setEditor] = createStore({ active: "" as string, value: "", @@ -47,6 +48,13 @@ export function createInlineEditorController() { stopPropagation?: boolean openOnDblClick?: boolean }) => { + let frame: number | undefined + + onCleanup(() => { + if (frame === undefined) return + cancelAnimationFrame(frame) + }) + const isEditing = () => props.editing ?? editorOpen(props.id) const stopEvents = () => props.stopPropagation ?? false const allowDblClick = () => props.openOnDblClick ?? true @@ -78,7 +86,12 @@ export function createInlineEditorController() { > { - requestAnimationFrame(() => el.focus()) + if (frame !== undefined) cancelAnimationFrame(frame) + frame = requestAnimationFrame(() => { + frame = undefined + if (!el.isConnected) return + el.focus() + }) }} value={editorValue()} class={props.class} diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index facfbddc7f39..d55090370750 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -13,7 +13,7 @@ import { MessageNav } from "@opencode-ai/ui/message-nav" import { Spinner } from "@opencode-ai/ui/spinner" import { Tooltip } from "@opencode-ai/ui/tooltip" import { getFilename } from "@opencode-ai/util/path" -import { type Message, type Session, type TextPart } from "@opencode-ai/sdk/v2/client" +import { type Message, type Session, type TextPart, type UserMessage } from "@opencode-ai/sdk/v2/client" import { For, Match, Show, Switch, createMemo, onCleanup, type Accessor, type JSX } from "solid-js" import { agentColor } from "@/utils/agent" @@ -21,8 +21,11 @@ const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750" export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => { const notification = useNotification() - const unseenCount = createMemo(() => notification.project.unseenCount(props.project.worktree)) - const hasError = createMemo(() => notification.project.unseenHasError(props.project.worktree)) + const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])]) + const unseenCount = createMemo(() => + dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), + ) + const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory))) const name = createMemo(() => props.project.name || getFilename(props.project.worktree)) return (
@@ -67,6 +70,116 @@ export type SessionItemProps = { archiveSession: (session: Session) => Promise } +const SessionRow = (props: { + session: Session + slug: string + mobile?: boolean + dense?: boolean + tint: Accessor + isWorking: Accessor + hasPermissions: Accessor + hasError: Accessor + unseenCount: Accessor + setHoverSession: (id: string | undefined) => void + clearHoverProjectSoon: () => void + sidebarOpened: Accessor + prefetchSession: (session: Session, priority?: "high" | "low") => void + scheduleHoverPrefetch: () => void + cancelHoverPrefetch: () => void +}): JSX.Element => ( + props.prefetchSession(props.session, "high")} + onClick={() => { + props.setHoverSession(undefined) + if (props.sidebarOpened()) return + props.clearHoverProjectSoon() + }} + > +
+
+ }> + + + + +
+ + +
+ + 0}> +
+ + +
+ + {props.session.title} + + + {(summary) => ( +
+ +
+ )} +
+
+
+) + +const SessionHoverPreview = (props: { + mobile?: boolean + nav: Accessor + hoverSession: Accessor + session: Session + sidebarHovering: Accessor + hoverReady: Accessor + hoverMessages: Accessor + language: ReturnType + isActive: Accessor + slug: string + setHoverSession: (id: string | undefined) => void + messageLabel: (message: Message) => string | undefined + onMessageSelect: (message: Message) => void + trigger: JSX.Element +}): JSX.Element => ( + props.setHoverSession(open ? props.session.id : undefined)} + > + {props.language.t("session.messages.loading")}
} + > +
+ +
+ + +) + export const SessionItem = (props: SessionItemProps): JSX.Element => { const params = useParams() const navigate = useNavigate() @@ -110,7 +223,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => { }) const hoverMessages = createMemo(() => - sessionStore.message[props.session.id]?.filter((message) => message.role === "user"), + sessionStore.message[props.session.id]?.filter((message): message is UserMessage => message.role === "user"), ) const hoverReady = createMemo(() => sessionStore.message[props.session.id] !== undefined) const hoverAllowed = createMemo(() => !props.mobile && props.sidebarExpanded()) @@ -138,54 +251,24 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => { const text = parts.find((part): part is TextPart => part?.type === "text" && !part.synthetic && !part.ignored) return text?.text } - const item = ( - props.prefetchSession(props.session, "high")} - onClick={() => { - props.setHoverSession(undefined) - if (layout.sidebar.opened()) return - props.clearHoverProjectSoon() - }} - > -
-
- }> - - - - -
- - -
- - 0}> -
- - -
- - {props.session.title} - - - {(summary) => ( -
- -
- )} -
-
-
+ ) return ( @@ -202,44 +285,30 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => { } > - { + if (!isActive()) { + layout.pendingMessage.set(`${base64Encode(props.session.directory)}/${props.session.id}`, message.id) + navigate(`${props.slug}/session/${props.session.id}`) + return + } + window.history.replaceState(null, "", `#message-${message.id}`) + window.dispatchEvent(new HashChangeEvent("hashchange")) + }} trigger={item} - mount={!props.mobile ? props.nav() : undefined} - open={props.hoverSession() === props.session.id} - onOpenChange={(open) => props.setHoverSession(open ? props.session.id : undefined)} - > - {language.t("session.messages.loading")}
} - > -
- { - if (!isActive()) { - layout.pendingMessage.set( - `${base64Encode(props.session.directory)}/${props.session.id}`, - message.id, - ) - navigate(`${props.slug}/session/${props.session.id}`) - return - } - window.history.replaceState(null, "", `#message-${message.id}`) - window.dispatchEvent(new HashChangeEvent("hashchange")) - }} - size="normal" - class="w-60" - /> -
- - + />
props.mobile || !props.sidebarExpanded() const item = ( { diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx index c91dc987d80b..e19e6f430f0d 100644 --- a/packages/app/src/pages/layout/sidebar-project.tsx +++ b/packages/app/src/pages/layout/sidebar-project.tsx @@ -10,6 +10,7 @@ import { createSortable } from "@thisbeyond/solid-dnd" import { type LocalProject } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" +import { useNotification } from "@/context/notification" import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items" import { childMapByParent, displayName, sortedRootSessions } from "./helpers" import { projectSelected, projectTileActive } from "./sidebar-project-helpers" @@ -51,10 +52,222 @@ export const ProjectDragOverlay = (props: { ) } +const ProjectTile = (props: { + project: LocalProject + mobile?: boolean + nav: Accessor + sidebarHovering: Accessor + selected: Accessor + active: Accessor + overlay: Accessor + dirs: Accessor + onProjectMouseEnter: (worktree: string, event: MouseEvent) => void + onProjectMouseLeave: (worktree: string) => void + onProjectFocus: (worktree: string) => void + navigateToProject: (directory: string) => void + showEditProjectDialog: (project: LocalProject) => void + toggleProjectWorkspaces: (project: LocalProject) => void + workspacesEnabled: (project: LocalProject) => boolean + closeProject: (directory: string) => void + setMenu: (value: boolean) => void + setOpen: (value: boolean) => void + language: ReturnType +}): JSX.Element => { + const notification = useNotification() + const unseenCount = createMemo(() => + props.dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), + ) + + const clear = () => + props + .dirs() + .filter((directory) => notification.project.unseenCount(directory) > 0) + .forEach((directory) => notification.project.markViewed(directory)) + + return ( + { + props.setMenu(value) + if (value) props.setOpen(false) + }} + > + { + if (!props.overlay()) return + props.onProjectMouseEnter(props.project.worktree, event) + }} + onMouseLeave={() => { + if (!props.overlay()) return + props.onProjectMouseLeave(props.project.worktree) + }} + onFocus={() => { + if (!props.overlay()) return + props.onProjectFocus(props.project.worktree) + }} + onClick={() => props.navigateToProject(props.project.worktree)} + onBlur={() => props.setOpen(false)} + > + + + + + props.showEditProjectDialog(props.project)}> + {props.language.t("common.edit")} + + props.toggleProjectWorkspaces(props.project)} + > + + {props.workspacesEnabled(props.project) + ? props.language.t("sidebar.workspaces.disable") + : props.language.t("sidebar.workspaces.enable")} + + + + {props.language.t("sidebar.project.clearNotifications")} + + + props.closeProject(props.project.worktree)} + > + {props.language.t("common.close")} + + + + + ) +} + +const ProjectPreviewPanel = (props: { + project: LocalProject + mobile?: boolean + selected: Accessor + workspaceEnabled: Accessor + workspaces: Accessor + label: (directory: string) => string + projectSessions: Accessor> + projectChildren: Accessor> + workspaceSessions: (directory: string) => ReturnType + workspaceChildren: (directory: string) => Map + setOpen: (value: boolean) => void + ctx: ProjectSidebarContext + language: ReturnType +}): JSX.Element => ( +
+
+
{displayName(props.project)}
+ + { + event.stopPropagation() + props.setOpen(false) + props.ctx.closeProject(props.project.worktree) + }} + /> + +
+
{props.language.t("sidebar.project.recentSessions")}
+
+ + {(session) => ( + + )} + + } + > + + {(directory) => { + const sessions = createMemo(() => props.workspaceSessions(directory)) + const children = createMemo(() => props.workspaceChildren(directory)) + return ( +
+
+
+ +
+ {props.label(directory)} +
+ + {(session) => ( + + )} + +
+ ) + }} +
+
+
+
+ +
+
+) + export const SortableProject = (props: { project: LocalProject mobile?: boolean ctx: ProjectSidebarContext + sortNow: Accessor }): JSX.Element => { const globalSync = useGlobalSync() const language = useLanguage() @@ -64,6 +277,7 @@ export const SortableProject = (props: { ) const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2)) const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project)) + const dirs = createMemo(() => props.ctx.workspaceIds(props.project)) const [open, setOpen] = createSignal(false) const [menu, setMenu] = createSignal(false) @@ -95,187 +309,72 @@ export const SortableProject = (props: { } const projectStore = createMemo(() => globalSync.child(props.project.worktree, { bootstrap: false })[0]) - const projectSessions = createMemo(() => sortedRootSessions(projectStore(), Date.now()).slice(0, 2)) + const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()).slice(0, 2)) const projectChildren = createMemo(() => childMapByParent(projectStore().session)) const workspaceSessions = (directory: string) => { const [data] = globalSync.child(directory, { bootstrap: false }) - return sortedRootSessions(data, Date.now()).slice(0, 2) + return sortedRootSessions(data, props.sortNow()).slice(0, 2) } const workspaceChildren = (directory: string) => { const [data] = globalSync.child(directory, { bootstrap: false }) return childMapByParent(data.session) } - - const Trigger = () => ( - { - setMenu(value) - if (value) setOpen(false) - }} - > - { - if (!overlay()) return - props.ctx.onProjectMouseEnter(props.project.worktree, event) - }} - onMouseLeave={() => { - if (!overlay()) return - props.ctx.onProjectMouseLeave(props.project.worktree) - }} - onFocus={() => { - if (!overlay()) return - props.ctx.onProjectFocus(props.project.worktree) - }} - onClick={() => props.ctx.navigateToProject(props.project.worktree)} - onBlur={() => setOpen(false)} - > - - - - - props.ctx.showEditProjectDialog(props.project)}> - {language.t("common.edit")} - - props.ctx.toggleProjectWorkspaces(props.project)} - > - - {props.ctx.workspacesEnabled(props.project) - ? language.t("sidebar.workspaces.disable") - : language.t("sidebar.workspaces.enable")} - - - - props.ctx.closeProject(props.project.worktree)} - > - {language.t("common.close")} - - - - + const tile = () => ( + ) return ( // @ts-ignore
- }> + } + trigger={tile()} onOpenChange={(value) => { if (menu()) return setOpen(value) if (value) props.ctx.setHoverSession(undefined) }} > -
-
-
{displayName(props.project)}
- - { - event.stopPropagation() - setOpen(false) - props.ctx.closeProject(props.project.worktree) - }} - /> - -
-
{language.t("sidebar.project.recentSessions")}
-
- - {(session) => ( - - )} - - } - > - - {(directory) => { - const sessions = createMemo(() => workspaceSessions(directory)) - const children = createMemo(() => workspaceChildren(directory)) - return ( -
-
-
- -
- {label(directory)} -
- - {(session) => ( - - )} - -
- ) - }} -
-
-
-
- -
-
+
diff --git a/packages/app/src/pages/layout/sidebar-shell.tsx b/packages/app/src/pages/layout/sidebar-shell.tsx index ce96a09d1144..23abdf157b4a 100644 --- a/packages/app/src/pages/layout/sidebar-shell.tsx +++ b/packages/app/src/pages/layout/sidebar-shell.tsx @@ -34,6 +34,7 @@ export const SidebarContent = (props: { renderPanel: () => JSX.Element }): JSX.Element => { const expanded = createMemo(() => sidebarExpanded(props.mobile, props.opened())) + const placement = () => (props.mobile ? "bottom" : "right") return (
@@ -55,7 +56,7 @@ export const SidebarContent = (props: { {(project) => props.renderProject(project)} {props.openProjectLabel} @@ -78,11 +79,7 @@ export const SidebarContent = (props: {
- + - + + busy: Accessor + open: Accessor + directory: string + language: ReturnType + branch: Accessor + workspaceValue: Accessor + workspaceEditActive: Accessor + InlineEditor: WorkspaceSidebarContext["InlineEditor"] + renameWorkspace: WorkspaceSidebarContext["renameWorkspace"] + setEditor: WorkspaceSidebarContext["setEditor"] + projectId?: string +}): JSX.Element => ( +
+
+ }> + + +
+ + {props.local() ? props.language.t("workspace.type.local") : props.language.t("workspace.type.sandbox")} : + + + {props.branch() ?? getFilename(props.directory)} + + } + > + { + const trimmed = next.trim() + if (!trimmed) return + props.renameWorkspace(props.directory, trimmed, props.projectId, props.branch()) + props.setEditor("value", props.workspaceValue()) + }} + class="text-14-medium text-text-base min-w-0 truncate" + displayClass="text-14-medium text-text-base min-w-0 truncate" + editing={props.workspaceEditActive()} + stopPropagation={false} + openOnDblClick={false} + /> + +
+ +
+
+) + +const WorkspaceActions = (props: { + directory: string + local: Accessor + busy: Accessor + menuOpen: Accessor + pendingRename: Accessor + setMenuOpen: (open: boolean) => void + setPendingRename: (value: boolean) => void + sidebarHovering: Accessor + mobile?: boolean + nav: Accessor + touch: Accessor + language: ReturnType + workspaceValue: Accessor + openEditor: WorkspaceSidebarContext["openEditor"] + showResetWorkspaceDialog: WorkspaceSidebarContext["showResetWorkspaceDialog"] + showDeleteWorkspaceDialog: WorkspaceSidebarContext["showDeleteWorkspaceDialog"] + root: string + setHoverSession: WorkspaceSidebarContext["setHoverSession"] + clearHoverProjectSoon: WorkspaceSidebarContext["clearHoverProjectSoon"] + navigateToNewSession: () => void +}): JSX.Element => ( +
+ props.setMenuOpen(open)} + > + + + + + { + if (!props.pendingRename()) return + event.preventDefault() + props.setPendingRename(false) + props.openEditor(`workspace:${props.directory}`, props.workspaceValue()) + }} + > + { + props.setPendingRename(true) + props.setMenuOpen(false) + }} + > + {props.language.t("common.rename")} + + props.showResetWorkspaceDialog(props.root, props.directory)} + > + {props.language.t("common.reset")} + + props.showDeleteWorkspaceDialog(props.root, props.directory)} + > + {props.language.t("common.delete")} + + + + + + + { + event.preventDefault() + event.stopPropagation() + props.setHoverSession(undefined) + props.clearHoverProjectSoon() + props.navigateToNewSession() + }} + /> + + +
+) + +const WorkspaceSessionList = (props: { + slug: Accessor + mobile?: boolean + ctx: WorkspaceSidebarContext + showNew: Accessor + loading: Accessor + sessions: Accessor + children: Accessor> + hasMore: Accessor + loadMore: () => Promise + language: ReturnType +}): JSX.Element => ( + +) + export const SortableWorkspace = (props: { ctx: WorkspaceSidebarContext directory: string project: LocalProject + sortNow: Accessor mobile?: boolean }): JSX.Element => { + const navigate = useNavigate() + const params = useParams() const globalSync = useGlobalSync() const language = useLanguage() const sortable = createSortable(props.directory) @@ -95,7 +316,7 @@ export const SortableWorkspace = (props: { pendingRename: false, }) const slug = createMemo(() => base64Encode(props.directory)) - const sessions = createMemo(() => sortedRootSessions(workspaceStore, Date.now())) + const sessions = createMemo(() => sortedRootSessions(workspaceStore, props.sortNow())) const children = createMemo(() => childMapByParent(workspaceStore.session)) const local = createMemo(() => props.directory === props.project.worktree) const active = createMemo(() => props.ctx.currentDir() === props.directory) @@ -111,8 +332,10 @@ export const SortableWorkspace = (props: { const busy = createMemo(() => props.ctx.isBusy(props.directory)) const wasBusy = createMemo((prev) => prev || busy(), false) const loading = createMemo(() => open() && !booted() && sessions().length === 0 && !wasBusy()) + 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) } @@ -129,48 +352,6 @@ export const SortableWorkspace = (props: { globalSync.child(props.directory, { bootstrap: true }) }) - const header = () => ( -
-
- }> - - -
- - {local() ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")} : - - - {workspaceStore.vcs?.branch ?? getFilename(props.directory)} - - } - > - { - const trimmed = next.trim() - if (!trimmed) return - props.ctx.renameWorkspace(props.directory, trimmed, props.project.id, workspaceStore.vcs?.branch) - props.ctx.setEditor("value", workspaceValue()) - }} - class="text-14-medium text-text-base min-w-0 truncate" - displayClass="text-14-medium text-text-base min-w-0 truncate" - editing={workspaceEditActive()} - stopPropagation={false} - openOnDblClick={false} - /> - - -
- ) - return (
- {header()} + workspaceStore.vcs?.branch} + workspaceValue={workspaceValue} + workspaceEditActive={workspaceEditActive} + InlineEditor={props.ctx.InlineEditor} + renameWorkspace={props.ctx.renameWorkspace} + setEditor={props.ctx.setEditor} + projectId={props.project.id} + /> } > -
{header()}
- -
- setMenu("open", open)} +
- - - - - { - if (!menu.pendingRename) return - event.preventDefault() - setMenu("pendingRename", false) - props.ctx.openEditor(`workspace:${props.directory}`, workspaceValue()) - }} - > - { - setMenu("pendingRename", true) - setMenu("open", false) - }} - > - {language.t("common.rename")} - - props.ctx.showResetWorkspaceDialog(props.project.worktree, props.directory)} - > - {language.t("common.reset")} - - props.ctx.showDeleteWorkspaceDialog(props.project.worktree, props.directory)} - > - {language.t("common.delete")} - - - - -
+ workspaceStore.vcs?.branch} + workspaceValue={workspaceValue} + workspaceEditActive={workspaceEditActive} + InlineEditor={props.ctx.InlineEditor} + renameWorkspace={props.ctx.renameWorkspace} + setEditor={props.ctx.setEditor} + projectId={props.project.id} + /> +
+ + menu.open} + pendingRename={() => menu.pendingRename} + setMenuOpen={(open) => setMenu("open", open)} + setPendingRename={(value) => setMenu("pendingRename", value)} + sidebarHovering={props.ctx.sidebarHovering} + mobile={props.mobile} + nav={props.ctx.nav} + touch={touch} + language={language} + workspaceValue={workspaceValue} + openEditor={props.ctx.openEditor} + showResetWorkspaceDialog={props.ctx.showResetWorkspaceDialog} + showDeleteWorkspaceDialog={props.ctx.showDeleteWorkspaceDialog} + root={props.project.worktree} + setHoverSession={props.ctx.setHoverSession} + clearHoverProjectSoon={props.ctx.clearHoverProjectSoon} + navigateToNewSession={() => navigate(`/${slug()}/session`)} + />
- +
@@ -320,6 +465,7 @@ export const SortableWorkspace = (props: { export const LocalWorkspace = (props: { ctx: WorkspaceSidebarContext project: LocalProject + sortNow: Accessor mobile?: boolean }): JSX.Element => { const globalSync = useGlobalSync() @@ -329,13 +475,13 @@ export const LocalWorkspace = (props: { return { store, setStore } }) const slug = createMemo(() => base64Encode(props.project.worktree)) - const sessions = createMemo(() => sortedRootSessions(workspace().store, Date.now())) + const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow())) const children = createMemo(() => childMapByParent(workspace().store.session)) const booted = createMemo((prev) => prev || workspace().store.status === "complete", false) 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) } diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index a70d4e8a2757..d958990c25ab 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -23,7 +23,6 @@ import { useSync } from "@/context/sync" import { useTerminal, type LocalPTY } from "@/context/terminal" import { useLayout } from "@/context/layout" import { checksum, base64Encode } from "@opencode-ai/util/encode" -import { findLast } from "@opencode-ai/util/array" import { useDialog } from "@opencode-ai/ui/context/dialog" import { DialogSelectFile } from "@/components/dialog-select-file" import FileTree from "@/components/file-tree" @@ -35,12 +34,11 @@ import { useSDK } from "@/context/sdk" import { usePrompt } from "@/context/prompt" import { useComments } from "@/context/comments" import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" -import { usePermission } from "@/context/permission" import { showToast } from "@opencode-ai/ui/toast" import { SessionHeader, SessionContextTab, SortableTab, FileVisual, NewSessionView } from "@/components/session" import { navMark, navParams } from "@/utils/perf" import { same } from "@/utils/same" -import { createOpenReviewFile, focusTerminalById } from "@/pages/session/helpers" +import { createOpenReviewFile, focusTerminalById, getTabReorderIndex } from "@/pages/session/helpers" import { createScrollSpy } from "@/pages/session/scroll-spy" import { createFileTabListSync } from "@/pages/session/file-tab-scroll" import { FileTabContent } from "@/pages/session/file-tabs" @@ -101,7 +99,6 @@ export default function Page() { const sdk = useSDK() const prompt = usePrompt() const comments = useComments() - const permission = usePermission() const permRequest = createMemo(() => { const sessionID = params.id @@ -232,8 +229,16 @@ export default function Page() { }) } - const isDesktop = createMediaQuery("(min-width: 768px)") - const centered = createMemo(() => isDesktop() && !view().reviewPanel.opened()) + const isDesktop = createMediaQuery("(min-width: 1024px)") + const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) + const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened()) + const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen()) + const sessionPanelWidth = createMemo(() => { + if (!desktopSidePanelOpen()) return "100%" + if (desktopReviewOpen()) return `${layout.session.width()}px` + return `calc(100% - ${layout.fileTree.width()}px)` + }) + const centered = createMemo(() => isDesktop() && !desktopSidePanelOpen()) function normalizeTab(tab: string) { if (!tab.startsWith("file://")) return tab @@ -252,12 +257,19 @@ export default function Page() { return next } + const openReviewPanel = () => { + if (!view().reviewPanel.opened()) view().reviewPanel.open() + } + const openTab = (value: string) => { const next = normalizeTab(value) tabs().open(next) const path = file.pathFromTab(next) - if (path) file.load(path) + if (!path) return + file.load(path) + openReviewPanel() + tabs().setActive(next) } createEffect(() => { @@ -380,6 +392,19 @@ export default function Page() { }) } + const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => { + if (params.id !== sessionID) return + if (parentID) { + navigate(`/${params.dir}/session/${parentID}`) + return + } + if (nextSessionID) { + navigate(`/${params.dir}/session/${nextSessionID}`) + return + } + navigate(`/${params.dir}/session`) + } + async function archiveSession(sessionID: string) { const session = sync.session.get(sessionID) if (!session) return @@ -397,17 +422,7 @@ export default function Page() { if (index !== -1) draft.session.splice(index, 1) }), ) - - if (params.id !== sessionID) return - if (session.parentID) { - navigate(`/${params.dir}/session/${session.parentID}`) - return - } - if (nextSession) { - navigate(`/${params.dir}/session/${nextSession.id}`) - return - } - navigate(`/${params.dir}/session`) + navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id) }) .catch((err) => { showToast({ @@ -473,16 +488,7 @@ export default function Page() { }), ) - if (params.id !== sessionID) return true - if (session.parentID) { - navigate(`/${params.dir}/session/${session.parentID}`) - return true - } - if (nextSession) { - navigate(`/${params.dir}/session/${nextSession.id}`) - return true - } - navigate(`/${params.dir}/session`) + navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id) return true } @@ -577,7 +583,7 @@ export default function Page() { const newSessionWorktree = createMemo(() => { if (store.newSessionWorktree === "create") return "create" const project = sync.project - if (project && sync.data.path.directory !== project.worktree) return sync.data.path.directory + if (project && sdk.directory !== project.worktree) return sdk.directory return "main" }) @@ -761,11 +767,6 @@ export default function Page() { return lines.slice(0, 2).join("\n") } - const addSelectionToContext = (path: string, selection: FileSelection) => { - const preview = selectionPreview(path, selection) - prompt.context.add({ type: "file", path, selection, preview }) - } - const addCommentToContext = (input: { file: string selection: SelectedLineRange @@ -830,11 +831,9 @@ export default function Page() { const { draggable, droppable } = event if (draggable && droppable) { const currentTabs = tabs().all() - const fromIndex = currentTabs?.indexOf(draggable.id.toString()) - const toIndex = currentTabs?.indexOf(droppable.id.toString()) - if (fromIndex !== toIndex && toIndex !== undefined) { - tabs().move(draggable.id.toString(), toIndex) - } + const toIndex = getTabReorderIndex(currentTabs, draggable.id.toString(), droppable.id.toString()) + if (toIndex === undefined) return + tabs().move(draggable.id.toString(), toIndex) } } @@ -903,32 +902,15 @@ export default function Page() { setFileTreeTab("all") } + const focusInput = () => inputRef?.focus() + useSessionCommands({ - command, - dialog, - file, - language, - local, - permission, - prompt, - sdk, - sync, - terminal, - layout, - params, - navigate, - tabs, - view, - info, - status, - userMessages, - visibleUserMessages, activeMessage, showAllFiles, navigateMessageByOffset, setExpanded: (id, fn) => setStore("expanded", id, fn), setActiveMessage, - addSelectionToContext, + focusInput, }) const openReviewFile = createOpenReviewFile({ @@ -1011,10 +993,31 @@ export default function Page() { -
- -
{language.t("session.review.empty")}
-
+ + +
{language.t("session.review.empty")}
+
+ ) + } + 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} + /> ) @@ -1026,7 +1029,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", })}
@@ -1085,6 +1088,7 @@ export default function Page() { } const focusReviewDiff = (path: string) => { + openReviewPanel() const current = view().review.open() ?? [] if (!current.includes(path)) view().review.setOpen([...current, path]) setTree({ activeDiff: path, pendingDiff: path }) @@ -1203,7 +1207,7 @@ export default function Page() { if (!id) return const wants = isDesktop() - ? view().reviewPanel.opened() && (layout.fileTree.opened() || activeTab() === "review") + ? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review") : store.mobileTab === "changes" if (!wants) return if (sync.data.session_diff[id] !== undefined) return @@ -1216,7 +1220,6 @@ export default function Page() { createEffect(() => { const dir = sdk.directory if (!isDesktop()) return - if (!view().reviewPanel.opened()) return if (!layout.fileTree.opened()) return if (sync.status === "loading") return @@ -1496,15 +1499,18 @@ export default function Page() { createEffect(() => { if (!file.ready()) return setSessionHandoff(sessionKey(), { - files: Object.fromEntries( - tabs() - .all() - .flatMap((tab) => { - const path = file.pathFromTab(tab) - if (!path) return [] - return [[path, file.selectedLines(path) ?? null] as const] - }), - ), + files: tabs() + .all() + .reduce>((acc, tab) => { + const path = file.pathFromTab(tab) + if (!path) return acc + const selected = file.selectedLines(path) + acc[path] = + selected && typeof selected === "object" && "start" in selected && "end" in selected + ? (selected as SelectedLineRange) + : null + return acc + }, {}), }) }) @@ -1518,9 +1524,16 @@ export default function Page() { return (
-
+
setStore("mobileTab", "session")} @@ -1533,10 +1546,10 @@ export default function Page() { classList={{ "@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger": true, "flex-1 pt-2 md:pt-3": true, - "md:flex-none": view().reviewPanel.opened(), + "md:flex-none": desktopSidePanelOpen(), }} style={{ - width: isDesktop() && view().reviewPanel.opened() ? `${layout.session.width()}px` : "100%", + width: sessionPanelWidth(), "--prompt-height": store.promptHeight ? `${store.promptHeight}px` : undefined, }} > @@ -1554,7 +1567,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} @@ -1632,7 +1645,7 @@ export default function Page() { const target = value === "main" ? sync.project?.worktree : value if (!target) return - if (target === sync.data.path.directory) return + if (target === sdk.directory) return layout.projects.open(target) navigate(`/${base64Encode(target)}/session`) }} @@ -1663,26 +1676,26 @@ export default function Page() { setPromptDockRef={(el) => (promptDock = el)} /> - +
unknown[]} - visibleUserMessages={visibleUserMessages as () => unknown[]} - view={view} - info={info as () => unknown} + vm={{ + messages, + visibleUserMessages, + view, + info, + }} handoffFiles={() => handoff.session.get(sessionKey())?.files} codeComponent={codeComponent} addCommentToContext={addCommentToContext} @@ -1716,7 +1731,7 @@ export default function Page() {
{ + const start = Math.min(range.start, range.end) + const end = Math.max(range.start, range.end) + if (start === end) return `line ${start}` + return `lines ${start}-${end}` +} + export function FileTabContent(props: { tab: string activeTab: () => string @@ -42,7 +49,7 @@ export function FileTabContent(props: { return props.file.get(p) }) const contents = createMemo(() => state()?.content?.content ?? "") - const cacheKey = createMemo(() => checksum(contents())) + const cacheKey = createMemo(() => sampledChecksum(contents())) const isImage = createMemo(() => { const c = state()?.content return c?.encoding === "base64" && c?.mimeType?.startsWith("image/") && c?.mimeType !== "image/svg+xml" @@ -76,7 +83,6 @@ export function FileTabContent(props: { showToast({ variant: "error", title: props.language.t("toast.file.loadFailed.title"), - description: "Invalid base64 content.", }) }) const svgPreviewUrl = createMemo(() => { @@ -106,6 +112,12 @@ export function FileTabContent(props: { return props.comments.list(p) }) + const commentLayout = createMemo(() => { + return fileComments() + .map((comment) => `${comment.id}:${comment.selection.start}:${comment.selection.end}`) + .join("|") + }) + const commentedLines = createMemo(() => fileComments().map((comment) => comment.selection)) const [note, setNote] = createStore({ @@ -116,34 +128,6 @@ export function FileTabContent(props: { draftTop: undefined as number | undefined, }) - const openedComment = () => note.openedComment - const setOpenedComment = ( - value: typeof note.openedComment | ((value: typeof note.openedComment) => typeof note.openedComment), - ) => setNote("openedComment", value) - - const commenting = () => note.commenting - const setCommenting = (value: typeof note.commenting | ((value: typeof note.commenting) => typeof note.commenting)) => - setNote("commenting", value) - - const draft = () => note.draft - const setDraft = (value: typeof note.draft | ((value: typeof note.draft) => typeof note.draft)) => - setNote("draft", value) - - const positions = () => note.positions - const setPositions = (value: typeof note.positions | ((value: typeof note.positions) => typeof note.positions)) => - setNote("positions", value) - - const draftTop = () => note.draftTop - const setDraftTop = (value: typeof note.draftTop | ((value: typeof note.draftTop) => typeof note.draftTop)) => - setNote("draftTop", value) - - const commentLabel = (range: SelectedLineRange) => { - const start = Math.min(range.start, range.end) - const end = Math.max(range.start, range.end) - if (start === end) return `line ${start}` - return `lines ${start}-${end}` - } - const getRoot = () => { const el = wrap if (!el) return @@ -174,33 +158,57 @@ export function FileTabContent(props: { const el = wrap const root = getRoot() if (!el || !root) { - setPositions({}) - setDraftTop(undefined) + setNote("positions", {}) + setNote("draftTop", undefined) return } + const estimateTop = (range: SelectedLineRange) => { + const line = Math.max(range.start, range.end) + const height = 24 + const offset = 2 + return Math.max(0, (line - 1) * height + offset) + } + + const large = contents().length > 500_000 + const next: Record = {} for (const comment of fileComments()) { const marker = findMarker(root, comment.selection) - if (!marker) continue - next[comment.id] = markerTop(el, marker) + if (marker) next[comment.id] = markerTop(el, marker) + else if (large) next[comment.id] = estimateTop(comment.selection) } - setPositions(next) + const removed = Object.keys(note.positions).filter((id) => next[id] === undefined) + const changed = Object.entries(next).filter(([id, top]) => note.positions[id] !== top) + if (removed.length > 0 || changed.length > 0) { + setNote( + "positions", + produce((draft) => { + for (const id of removed) { + delete draft[id] + } + + for (const [id, top] of changed) { + draft[id] = top + } + }), + ) + } - const range = commenting() + const range = note.commenting if (!range) { - setDraftTop(undefined) + setNote("draftTop", undefined) return } const marker = findMarker(root, range) - if (!marker) { - setDraftTop(undefined) + if (marker) { + setNote("draftTop", markerTop(el, marker)) return } - setDraftTop(markerTop(el, marker)) + setNote("draftTop", large ? estimateTop(range) : undefined) } const scheduleComments = () => { @@ -208,15 +216,15 @@ export function FileTabContent(props: { } createEffect(() => { - fileComments() + commentLayout() scheduleComments() }) createEffect(() => { - const range = commenting() + const range = note.commenting scheduleComments() if (!range) return - setDraft("") + setNote("draft", "") }) createEffect(() => { @@ -229,8 +237,8 @@ export function FileTabContent(props: { const target = fileComments().find((comment) => comment.id === focus.id) if (!target) return - setOpenedComment(target.id) - setCommenting(null) + setNote("openedComment", target.id) + setNote("commenting", null) props.file.setSelectedLines(p, target.selection) requestAnimationFrame(() => props.comments.clearFocus()) }) @@ -390,16 +398,16 @@ export function FileTabContent(props: { const p = path() if (!p) return props.file.setSelectedLines(p, range) - if (!range) setCommenting(null) + if (!range) setNote("commenting", null) }} onLineSelectionEnd={(range: SelectedLineRange | null) => { if (!range) { - setCommenting(null) + setNote("commenting", null) return } - setOpenedComment(null) - setCommenting(range) + setNote("openedComment", null) + setNote("commenting", range) }} overflow="scroll" class="select-text" @@ -408,10 +416,10 @@ export function FileTabContent(props: { {(comment) => ( { const p = path() if (!p) return @@ -420,22 +428,22 @@ export function FileTabContent(props: { onClick={() => { const p = path() if (!p) return - setCommenting(null) - setOpenedComment((current) => (current === comment.id ? null : comment.id)) + setNote("commenting", null) + setNote("openedComment", (current) => (current === comment.id ? null : comment.id)) props.file.setSelectedLines(p, comment.selection) }} /> )} - + {(range) => ( - + setDraft(value)} - onCancel={() => setCommenting(null)} + top={note.draftTop} + value={note.draft} + selection={formatCommentLabel(range())} + onInput={(value) => setNote("draft", value)} + onCancel={() => setNote("commenting", null)} onSubmit={(value) => { const p = path() if (!p) return @@ -445,7 +453,7 @@ export function FileTabContent(props: { comment: value, origin: "file", }) - setCommenting(null) + setNote("commenting", null) }} onPopoverFocusOut={(e: FocusEvent) => { const current = e.currentTarget as HTMLDivElement @@ -454,7 +462,7 @@ export function FileTabContent(props: { setTimeout(() => { if (!document.activeElement || !current.contains(document.activeElement)) { - setCommenting(null) + setNote("commenting", null) } }, 0) }} diff --git a/packages/app/src/pages/session/helpers.test.ts b/packages/app/src/pages/session/helpers.test.ts index 0afc7eb6a594..8b9746507ef3 100644 --- a/packages/app/src/pages/session/helpers.test.ts +++ b/packages/app/src/pages/session/helpers.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { combineCommandSections, createOpenReviewFile, focusTerminalById } from "./helpers" +import { createOpenReviewFile, focusTerminalById, getTabReorderIndex } from "./helpers" describe("createOpenReviewFile", () => { test("opens and loads selected review file", () => { @@ -46,16 +46,12 @@ describe("focusTerminalById", () => { }) }) -describe("combineCommandSections", () => { - test("keeps section order stable", () => { - const result = combineCommandSections([ - [{ id: "a", title: "A" }], - [ - { id: "b", title: "B" }, - { id: "c", title: "C" }, - ], - ]) - - expect(result.map((item) => item.id)).toEqual(["a", "b", "c"]) +describe("getTabReorderIndex", () => { + test("returns target index for valid drag reorder", () => { + expect(getTabReorderIndex(["a", "b", "c"], "a", "c")).toBe(2) + }) + + test("returns undefined for unknown droppable id", () => { + expect(getTabReorderIndex(["a", "b", "c"], "a", "missing")).toBeUndefined() }) }) diff --git a/packages/app/src/pages/session/helpers.ts b/packages/app/src/pages/session/helpers.ts index d9ce90793f2b..5ca355d1d291 100644 --- a/packages/app/src/pages/session/helpers.ts +++ b/packages/app/src/pages/session/helpers.ts @@ -1,4 +1,4 @@ -import type { CommandOption } from "@/context/command" +import { batch } from "solid-js" export const focusTerminalById = (id: string) => { const wrapper = document.getElementById(`terminal-wrapper-${id}`) @@ -27,12 +27,17 @@ export const createOpenReviewFile = (input: { loadFile: (path: string) => void }) => { return (path: string) => { - input.showAllFiles() - input.openTab(input.tabForPath(path)) - input.loadFile(path) + batch(() => { + input.showAllFiles() + input.openTab(input.tabForPath(path)) + input.loadFile(path) + }) } } -export const combineCommandSections = (sections: readonly (readonly CommandOption[])[]) => { - return sections.flatMap((section) => section) +export const getTabReorderIndex = (tabs: readonly string[], from: string, to: string) => { + const fromIndex = tabs.indexOf(from) + const toIndex = tabs.indexOf(to) + if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return undefined + return toIndex } diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index f536c7061fbe..d5f04ccf91c4 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -9,6 +9,37 @@ import { SessionTurn } from "@opencode-ai/ui/session-turn" import type { UserMessage } from "@opencode-ai/sdk/v2" import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture" +const boundaryTarget = (root: HTMLElement, target: EventTarget | null) => { + const current = target instanceof Element ? target : undefined + const nested = current?.closest("[data-scrollable]") + if (!nested || nested === root) return root + if (!(nested instanceof HTMLElement)) return root + return nested +} + +const markBoundaryGesture = (input: { + root: HTMLDivElement + target: EventTarget | null + delta: number + onMarkScrollGesture: (target?: EventTarget | null) => void +}) => { + const target = boundaryTarget(input.root, input.target) + if (target === input.root) { + input.onMarkScrollGesture(input.root) + return + } + if ( + shouldMarkBoundaryGesture({ + delta: input.delta, + scrollTop: target.scrollTop, + scrollHeight: target.scrollHeight, + clientHeight: target.clientHeight, + }) + ) { + input.onMarkScrollGesture(input.root) + } +} + export function MessageTimeline(props: { mobileChanges: boolean mobileFallback: JSX.Element @@ -86,35 +117,13 @@ export function MessageTimeline(props: { ref={props.setScrollRef} onWheel={(e) => { const root = e.currentTarget - const target = e.target instanceof Element ? e.target : undefined - const nested = target?.closest("[data-scrollable]") - if (!nested || nested === root) { - props.onMarkScrollGesture(root) - return - } - - if (!(nested instanceof HTMLElement)) { - props.onMarkScrollGesture(root) - return - } - const delta = normalizeWheelDelta({ deltaY: e.deltaY, deltaMode: e.deltaMode, rootHeight: root.clientHeight, }) if (!delta) return - - if ( - shouldMarkBoundaryGesture({ - delta, - scrollTop: nested.scrollTop, - scrollHeight: nested.scrollHeight, - clientHeight: nested.clientHeight, - }) - ) { - props.onMarkScrollGesture(root) - } + markBoundaryGesture({ root, target: e.target, delta, onMarkScrollGesture: props.onMarkScrollGesture }) }} onTouchStart={(e) => { touchGesture = e.touches[0]?.clientY @@ -129,28 +138,7 @@ export function MessageTimeline(props: { if (!delta) return const root = e.currentTarget - const target = e.target instanceof Element ? e.target : undefined - const nested = target?.closest("[data-scrollable]") - if (!nested || nested === root) { - props.onMarkScrollGesture(root) - return - } - - if (!(nested instanceof HTMLElement)) { - props.onMarkScrollGesture(root) - return - } - - if ( - shouldMarkBoundaryGesture({ - delta, - scrollTop: nested.scrollTop, - scrollHeight: nested.scrollHeight, - clientHeight: nested.clientHeight, - }) - ) { - props.onMarkScrollGesture(root) - } + markBoundaryGesture({ root, target: e.target, delta, onMarkScrollGesture: props.onMarkScrollGesture }) }} onTouchEnd={() => { touchGesture = undefined @@ -179,7 +167,7 @@ export function MessageTimeline(props: { "sticky top-0 z-30 bg-background-stronger": true, "w-full": true, "px-4 md:px-6": true, - "md:max-w-200 md:mx-auto 3xl:max-w-[1200px]": props.centered, + "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered, }} >
@@ -278,7 +266,7 @@ export function MessageTimeline(props: { class="flex flex-col gap-12 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]" classList={{ "w-full": true, - "md:max-w-200 md:mx-auto 3xl:max-w-[1200px]": props.centered, + "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered, "mt-0.5": props.centered, "mt-0": !props.centered, }} @@ -321,7 +309,7 @@ export function MessageTimeline(props: { }} classList={{ "min-w-0 w-full max-w-full": true, - "md:max-w-200 3xl:max-w-[1200px]": props.centered, + "md:max-w-200 2xl:max-w-[1000px]": props.centered, }} > { @@ -43,7 +44,7 @@ export function StickyAddButton(props: { children: JSX.Element }) { const handler = () => { const rect = node.getBoundingClientRect() const scrollRect = scroll.getBoundingClientRect() - setStuck(rect.right >= scrollRect.right && scroll.scrollWidth > scroll.clientWidth) + setState("stuck", rect.right >= scrollRect.right && scroll.scrollWidth > scroll.clientWidth) } scroll.addEventListener("scroll", handler, { passive: true }) @@ -60,7 +61,7 @@ export function StickyAddButton(props: { children: JSX.Element }) {
{props.children}
@@ -78,7 +79,10 @@ export function SessionReviewTab(props: SessionReviewTabProps) { return sdk.client.file .read({ path }) .then((x) => x.data) - .catch(() => undefined) + .catch((error) => { + console.debug("[session-review] failed to read file", { path, error }) + return undefined + }) } const restoreScroll = () => { @@ -139,7 +143,7 @@ export function SessionReviewTab(props: SessionReviewTabProps) { open={props.view().review.open()} onOpenChange={props.view().review.setOpen} classes={{ - root: props.classes?.root ?? "pb-40", + root: props.classes?.root ?? "pb-6", header: props.classes?.header ?? "px-6", container: props.classes?.container ?? "px-6", }} diff --git a/packages/app/src/pages/session/scroll-spy.ts b/packages/app/src/pages/session/scroll-spy.ts index 8c52d77dceee..6ef4c844c41c 100644 --- a/packages/app/src/pages/session/scroll-spy.ts +++ b/packages/app/src/pages/session/scroll-spy.ts @@ -228,6 +228,7 @@ export const createScrollSpy = (input: Input) => { node.delete(key) visible.delete(key) dirty = true + schedule() } const markDirty = () => { diff --git a/packages/app/src/pages/session/session-mobile-tabs.tsx b/packages/app/src/pages/session/session-mobile-tabs.tsx index 41f058231667..73aebc079aae 100644 --- a/packages/app/src/pages/session/session-mobile-tabs.tsx +++ b/packages/app/src/pages/session/session-mobile-tabs.tsx @@ -1,8 +1,9 @@ -import { Match, Show, Switch } from "solid-js" +import { Show } from "solid-js" import { Tabs } from "@opencode-ai/ui/tabs" export function SessionMobileTabs(props: { open: boolean + mobileTab: "session" | "changes" hasReview: boolean reviewCount: number onSession: () => void @@ -11,23 +12,25 @@ export function SessionMobileTabs(props: { }) { return ( - + - + {props.t("session.tab.session")} - - - {props.t("session.review.filesChanged", { count: props.reviewCount })} - - {props.t("session.review.change.other")} - + {props.hasReview + ? props.t("session.review.filesChanged", { count: props.reviewCount }) + : props.t("session.review.change.other")} diff --git a/packages/app/src/pages/session/session-prompt-dock.tsx b/packages/app/src/pages/session/session-prompt-dock.tsx index 697957027289..8ec4f3b9f8c5 100644 --- a/packages/app/src/pages/session/session-prompt-dock.tsx +++ b/packages/app/src/pages/session/session-prompt-dock.tsx @@ -1,15 +1,14 @@ -import { For, Show, type ComponentProps } from "solid-js" +import { For, Show } from "solid-js" +import type { QuestionRequest } from "@opencode-ai/sdk/v2" import { Button } from "@opencode-ai/ui/button" import { BasicTool } from "@opencode-ai/ui/basic-tool" import { PromptInput } from "@/components/prompt-input" import { QuestionDock } from "@/components/question-dock" import { questionSubtitle } from "@/pages/session/session-prompt-helpers" -const questionDockRequest = (value: unknown) => value as ComponentProps["request"] - export function SessionPromptDock(props: { centered: boolean - questionRequest: () => { questions: unknown[] } | undefined + questionRequest: () => QuestionRequest | undefined permissionRequest: () => { patterns: string[]; permission: string } | undefined blocked: boolean promptReady: boolean @@ -31,7 +30,7 @@ export function SessionPromptDock(props: {
@@ -48,7 +47,7 @@ export function SessionPromptDock(props: { subtitle, }} /> - +
) }} diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 573680dec6d9..33954f64a12d 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -21,16 +21,24 @@ import { useFile, type SelectedLineRange } from "@/context/file" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" import { useSync } from "@/context/sync" +import type { Message, UserMessage } from "@opencode-ai/sdk/v2/client" + +type SessionSidePanelViewModel = { + messages: () => Message[] + visibleUserMessages: () => UserMessage[] + view: () => ReturnType["view"]> + info: () => ReturnType["session"]["get"]> +} export function SessionSidePanel(props: { open: boolean + reviewOpen: boolean language: ReturnType layout: ReturnType command: ReturnType dialog: ReturnType file: ReturnType comments: ReturnType - sync: ReturnType hasReview: boolean reviewCount: number reviewTab: boolean @@ -42,10 +50,7 @@ export function SessionSidePanel(props: { openTab: (value: string) => void showAllFiles: () => void reviewPanel: () => JSX.Element - messages: () => unknown[] - visibleUserMessages: () => unknown[] - view: () => ReturnType["view"]> - info: () => unknown + vm: SessionSidePanelViewModel handoffFiles: () => Record | undefined codeComponent: NonNullable addCommentToContext: (input: { @@ -67,162 +72,173 @@ export function SessionSidePanel(props: { activeDiff?: string focusReviewDiff: (path: string) => void }) { + const openedTabs = createMemo(() => props.openedTabs()) + return (
@@ -369,7 +369,7 @@ export default function Download() { VSCodium
- + {i18n.t("download.action.install")}
@@ -393,7 +393,7 @@ export default function Download() { GitHub
- + {i18n.t("download.action.install")}
@@ -410,7 +410,7 @@ export default function Download() { GitLab
- + {i18n.t("download.action.install")}
diff --git a/packages/console/app/src/routes/index.css b/packages/console/app/src/routes/index.css index 1c734c9d0610..770280e6cc32 100644 --- a/packages/console/app/src/routes/index.css +++ b/packages/console/app/src/routes/index.css @@ -174,21 +174,6 @@ body { } } - input:-webkit-autofill, - input:-webkit-autofill:hover, - input:-webkit-autofill:focus, - input:-webkit-autofill:active { - transition: background-color 5000000s ease-in-out 0s; - } - - input:-webkit-autofill { - -webkit-text-fill-color: var(--color-text-strong) !important; - } - - input:-moz-autofill { - -moz-text-fill-color: var(--color-text-strong) !important; - } - [data-component="container"] { max-width: 67.5rem; margin: 0 auto; @@ -1249,4 +1234,19 @@ body { text-decoration: underline; } } + + input:-webkit-autofill, + input:-webkit-autofill:hover, + input:-webkit-autofill:focus, + input:-webkit-autofill:active { + transition: background-color 5000000s ease-in-out 0s; + } + + input:-webkit-autofill { + -webkit-text-fill-color: var(--color-text-strong) !important; + } + + input:-moz-autofill { + -moz-text-fill-color: var(--color-text-strong) !important; + } } diff --git a/packages/console/app/src/routes/s/[id].ts b/packages/console/app/src/routes/s/[id].ts index 628a75b2e3c7..60f8d8ba879d 100644 --- a/packages/console/app/src/routes/s/[id].ts +++ b/packages/console/app/src/routes/s/[id].ts @@ -1,14 +1,16 @@ import type { APIEvent } from "@solidjs/start/server" -import { LOCALE_HEADER, localeFromCookieHeader, parseLocale, tag } from "~/lib/language" +import { Resource } from "@opencode-ai/console-resource" +import { docs, localeFromRequest, tag } from "~/lib/language" async function handler(evt: APIEvent) { const req = evt.request.clone() const url = new URL(req.url) - const targetUrl = `https://docs.opencode.ai/docs${url.pathname}${url.search}` + const locale = localeFromRequest(req) + const host = Resource.App.stage === "production" ? "docs.opencode.ai" : "docs.dev.opencode.ai" + const targetUrl = `https://${host}${docs(locale, `/docs${url.pathname}`)}${url.search}` const headers = new Headers(req.headers) - const locale = parseLocale(req.headers.get(LOCALE_HEADER)) ?? localeFromCookieHeader(req.headers.get("cookie")) - if (locale) headers.set("accept-language", tag(locale)) + headers.set("accept-language", tag(locale)) const response = await fetch(targetUrl, { method: req.method, diff --git a/packages/console/app/src/routes/temp.tsx b/packages/console/app/src/routes/temp.tsx index 0a2447f44e3a..ac506928eb2f 100644 --- a/packages/console/app/src/routes/temp.tsx +++ b/packages/console/app/src/routes/temp.tsx @@ -51,7 +51,7 @@ export default function Home() { opencode logo dark

{i18n.t("temp.hero.title")}

@@ -60,7 +60,7 @@ export default function Home() { {i18n.t("temp.getStarted")}
- + {i18n.t("zen.cta.start")} {i18n.t("zen.faq.a4.p1.pricingLink")}{" "} {i18n.t("zen.faq.a4.p1.afterPricing")} {i18n.t("zen.faq.a4.p2.beforeAccount")}{" "} - {i18n.t("zen.faq.a4.p2.accountLink")}. {i18n.t("zen.faq.a4.p3")} + {i18n.t("zen.faq.a4.p2.accountLink")}. {i18n.t("zen.faq.a4.p3")}
  • diff --git a/packages/console/app/src/routes/zen/util/error.ts b/packages/console/app/src/routes/zen/util/error.ts index b97b73430741..a3a93d2ef860 100644 --- a/packages/console/app/src/routes/zen/util/error.ts +++ b/packages/console/app/src/routes/zen/util/error.ts @@ -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 {} diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 91fa306af45a..a8e275ba9a55 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -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" @@ -38,6 +38,7 @@ type RetryOptions = { excludeProviders: string[] retryCount: number } +type BillingSource = "anonymous" | "free" | "byok" | "subscription" | "balance" export async function handler( input: APIEvent, @@ -51,8 +52,10 @@ export async function handler( type AuthInfo = Awaited> type ModelInfo = Awaited> type ProviderInfo = Awaited> + type CostInfo = ReturnType - const MAX_RETRIES = 3 + const MAX_FAILOVER_RETRIES = 3 + const MAX_429_RETRIES = 3 const FREE_WORKSPACES = [ "wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank "wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // opencode bench @@ -107,11 +110,12 @@ export async function handler( providerInfo.modifyBody({ ...createBodyConverter(opts.format, providerInfo.format)(body), model: providerInfo.model, + ...(providerInfo.payloadModifier ?? {}), }), ) 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) @@ -119,6 +123,9 @@ export async function handler( Object.entries(providerInfo.headerMappings ?? {}).forEach(([k, v]) => { headers.set(k, headers.get(v)!) }) + Object.entries(providerInfo.headers ?? {}).forEach(([k, v]) => { + headers.set(k, v) + }) headers.delete("host") headers.delete("content-length") headers.delete("x-opencode-request") @@ -130,6 +137,13 @@ export async function handler( body: reqBody, }) + 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 ( res.status !== 200 && @@ -173,18 +187,25 @@ export async function handler( // Handle non-streaming response if (!isStream) { - const responseConverter = createResponseConverter(providerInfo.format, opts.format) const json = await res.json() - const body = JSON.stringify(responseConverter(json)) + const usageInfo = providerInfo.normalizeUsage(json.usage) + const costInfo = calculateCost(modelInfo, usageInfo) + await trialLimiter?.track(usageInfo) + await rateLimiter?.track() + await trackUsage(billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo) + await reload(billingSource, authInfo, costInfo) + + const responseConverter = createResponseConverter(providerInfo.format, opts.format) + const body = JSON.stringify( + responseConverter({ + ...json, + cost: calculateOccuredCost(billingSource, costInfo), + }), + ) logger.metric({ response_length: body.length }) logger.debug("RESPONSE: " + body) dataDumper?.provideResponse(body) dataDumper?.flush() - const tokensInfo = providerInfo.normalizeUsage(json.usage) - await trialLimiter?.track(tokensInfo) - await rateLimiter?.track() - const costInfo = await trackUsage(authInfo, modelInfo, providerInfo, billingSource, tokensInfo) - await reload(authInfo, costInfo) return new Response(body, { status: resStatus, statusText: res.statusText, @@ -216,12 +237,16 @@ export async function handler( dataDumper?.flush() await rateLimiter?.track() const usage = usageParser.retrieve() + let cost = "0" if (usage) { - const tokensInfo = providerInfo.normalizeUsage(usage) - await trialLimiter?.track(tokensInfo) - const costInfo = await trackUsage(authInfo, modelInfo, providerInfo, billingSource, tokensInfo) - await reload(authInfo, costInfo) + const usageInfo = providerInfo.normalizeUsage(usage) + const costInfo = calculateCost(modelInfo, usageInfo) + await trialLimiter?.track(usageInfo) + await trackUsage(billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo) + await reload(billingSource, authInfo, costInfo) + cost = calculateOccuredCost(billingSource, costInfo) } + c.enqueue(encoder.encode(usageParser.buidlCostChunk(cost))) c.close() return } @@ -250,13 +275,18 @@ export async function handler( part = part.trim() usageParser.parse(part) - if (providerInfo.format !== opts.format) { + if (providerInfo.responseModifier) { + for (const [k, v] of Object.entries(providerInfo.responseModifier)) { + part = part.replace(k, v) + } + c.enqueue(encoder.encode(part + "\n\n")) + } else if (providerInfo.format !== opts.format) { part = streamConverter(part) c.enqueue(encoder.encode(part + "\n\n")) } } - if (providerInfo.format === opts.format) { + if (!providerInfo.responseModifier && providerInfo.format === opts.format) { c.enqueue(value) } @@ -268,7 +298,6 @@ export async function handler( return pump() }, }) - return new Response(stream, { status: resStatus, statusText: res.statusText, @@ -296,9 +325,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( @@ -361,23 +390,25 @@ export async function handler( if (provider) return provider } - if (retry.retryCount === MAX_RETRIES) { - return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider) + if (retry.retryCount !== MAX_FAILOVER_RETRIES) { + const providers = modelInfo.providers + .filter((provider) => !provider.disabled) + .filter((provider) => !retry.excludeProviders.includes(provider.id)) + .flatMap((provider) => Array(provider.weight ?? 1).fill(provider)) + + // Use the last 4 characters of session ID to select a provider + let h = 0 + const l = sessionId.length + for (let i = l - 4; i < l; i++) { + h = (h * 31 + sessionId.charCodeAt(i)) | 0 // 32-bit int + } + const index = (h >>> 0) % providers.length // make unsigned + range 0..length-1 + const provider = providers[index || 0] + if (provider) return provider } - const providers = modelInfo.providers - .filter((provider) => !provider.disabled) - .filter((provider) => !retry.excludeProviders.includes(provider.id)) - .flatMap((provider) => Array(provider.weight ?? 1).fill(provider)) - - // Use the last 4 characters of session ID to select a provider - let h = 0 - const l = sessionId.length - for (let i = l - 4; i < l; i++) { - h = (h * 31 + sessionId.charCodeAt(i)) | 0 // 32-bit int - } - const index = (h >>> 0) % providers.length // make unsigned + range 0..length-1 - return providers[index || 0] + // fallback provider + return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider) })() if (!modelProvider) throw new ModelError("No provider available") @@ -483,9 +514,9 @@ export async function handler( } } - function validateBilling(authInfo: AuthInfo, modelInfo: ModelInfo) { + function validateBilling(authInfo: AuthInfo, modelInfo: ModelInfo): BillingSource { if (!authInfo) return "anonymous" - if (authInfo.provider?.credentials) return "free" + if (authInfo.provider?.credentials) return "byok" if (authInfo.isFree) return "free" if (modelInfo.allowAnonymous) return "free" @@ -512,7 +543,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, ) @@ -526,7 +557,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, ) @@ -589,13 +620,16 @@ export async function handler( providerInfo.apiKey = authInfo.provider.credentials } - async function trackUsage( - authInfo: AuthInfo, - modelInfo: ModelInfo, - providerInfo: ProviderInfo, - billingSource: ReturnType, - usageInfo: UsageInfo, - ) { + 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 + } + + function calculateCost(modelInfo: ModelInfo, usageInfo: UsageInfo) { const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } = usageInfo @@ -633,6 +667,33 @@ export async function handler( (cacheReadCost ?? 0) + (cacheWrite5mCost ?? 0) + (cacheWrite1hCost ?? 0) + return { + totalCostInCent, + inputCost, + outputCost, + reasoningCost, + cacheReadCost, + cacheWrite5mCost, + cacheWrite1hCost, + } + } + + function calculateOccuredCost(billingSource: BillingSource, costInfo: CostInfo) { + return billingSource === "balance" ? (costInfo.totalCostInCent / 100).toFixed(8) : "0" + } + + async function trackUsage( + billingSource: BillingSource, + authInfo: AuthInfo, + modelInfo: ModelInfo, + providerInfo: ProviderInfo, + usageInfo: UsageInfo, + costInfo: CostInfo, + ) { + const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } = + usageInfo + const { totalCostInCent, inputCost, outputCost, reasoningCost, cacheReadCost, cacheWrite5mCost, cacheWrite1hCost } = + costInfo logger.metric({ "tokens.input": inputTokens, @@ -653,7 +714,7 @@ export async function handler( if (billingSource === "anonymous") return authInfo = authInfo! - const cost = authInfo.provider?.credentials ? 0 : centsToMicroCents(totalCostInCent) + const cost = centsToMicroCents(totalCostInCent) await Database.use((db) => Promise.all([ db.insert(UsageTable).values({ @@ -748,16 +809,12 @@ export async function handler( return { costInMicroCents: cost } } - async function reload(authInfo: AuthInfo, costInfo: Awaited>) { - if (!authInfo) return - if (authInfo.isFree) return - if (authInfo.provider?.credentials) return - if (authInfo.subscription) return - - if (!costInfo) return + async function reload(billingSource: BillingSource, authInfo: AuthInfo, costInfo: CostInfo) { + if (billingSource !== "balance") return + authInfo = authInfo! const reloadTrigger = centsToMicroCents((authInfo.billing.reloadTrigger ?? Billing.RELOAD_TRIGGER) * 100) - if (authInfo.billing.balance - costInfo.costInMicroCents >= reloadTrigger) return + if (authInfo.billing.balance - costInfo.totalCostInCent >= reloadTrigger) return if (authInfo.billing.timeReloadLockedTill && authInfo.billing.timeReloadLockedTill > new Date()) return const lock = await Database.use((tx) => diff --git a/packages/console/app/src/routes/zen/util/provider/anthropic.ts b/packages/console/app/src/routes/zen/util/provider/anthropic.ts index a5f92a29acf4..e2803459e088 100644 --- a/packages/console/app/src/routes/zen/util/provider/anthropic.ts +++ b/packages/console/app/src/routes/zen/util/provider/anthropic.ts @@ -20,7 +20,7 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) => const isBedrockModelArn = providerModel.startsWith("arn:aws:bedrock:") const isBedrockModelID = providerModel.startsWith("global.anthropic.") const isBedrock = isBedrockModelArn || isBedrockModelID - const isSonnet = reqModel.includes("sonnet") + const supports1m = reqModel.includes("sonnet") || reqModel.includes("opus-4-6") return { format: "anthropic", modifyUrl: (providerApi: string, isStream?: boolean) => @@ -33,7 +33,7 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) => } else { headers.set("x-api-key", apiKey) headers.set("anthropic-version", headers.get("anthropic-version") ?? "2023-06-01") - if (body.model.startsWith("claude-sonnet-")) { + if (supports1m) { headers.set("anthropic-beta", "context-1m-2025-08-07") } } @@ -43,7 +43,7 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) => ...(isBedrock ? { anthropic_version: "bedrock-2023-05-31", - anthropic_beta: isSonnet ? "context-1m-2025-08-07" : undefined, + anthropic_beta: supports1m ? "context-1m-2025-08-07" : undefined, model: undefined, stream: undefined, } @@ -167,6 +167,7 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) => } }, retrieve: () => usage, + buidlCostChunk: (cost: string) => `event: ping\ndata: ${JSON.stringify({ type: "ping", cost })}\n\n`, } }, normalizeUsage: (usage: Usage) => ({ diff --git a/packages/console/app/src/routes/zen/util/provider/google.ts b/packages/console/app/src/routes/zen/util/provider/google.ts index f6f7d6e19b29..ecf3b2d4d4d5 100644 --- a/packages/console/app/src/routes/zen/util/provider/google.ts +++ b/packages/console/app/src/routes/zen/util/provider/google.ts @@ -56,6 +56,7 @@ export const googleHelper: ProviderHelper = ({ providerModel }) => ({ usage = json.usageMetadata }, retrieve: () => usage, + buidlCostChunk: (cost: string) => `data: ${JSON.stringify({ type: "ping", cost })}\n\n`, } }, normalizeUsage: (usage: Usage) => { diff --git a/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts index ce97a34d9bc0..046bf8f0c62d 100644 --- a/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts +++ b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts @@ -54,6 +54,7 @@ export const oaCompatHelper: ProviderHelper = () => ({ usage = json.usage }, retrieve: () => usage, + buidlCostChunk: (cost: string) => `data: ${JSON.stringify({ choices: [], cost })}\n\n`, } }, normalizeUsage: (usage: Usage) => { diff --git a/packages/console/app/src/routes/zen/util/provider/openai.ts b/packages/console/app/src/routes/zen/util/provider/openai.ts index f4d7699e97c5..db2dfa521509 100644 --- a/packages/console/app/src/routes/zen/util/provider/openai.ts +++ b/packages/console/app/src/routes/zen/util/provider/openai.ts @@ -43,6 +43,7 @@ export const openaiHelper: ProviderHelper = () => ({ usage = json.response.usage }, retrieve: () => usage, + buidlCostChunk: (cost: string) => `event: ping\ndata: ${JSON.stringify({ type: "ping", cost })}\n\n`, } }, normalizeUsage: (usage: Usage) => { diff --git a/packages/console/app/src/routes/zen/util/provider/provider.ts b/packages/console/app/src/routes/zen/util/provider/provider.ts index bbf54f4f96dc..5f8b631cf089 100644 --- a/packages/console/app/src/routes/zen/util/provider/provider.ts +++ b/packages/console/app/src/routes/zen/util/provider/provider.ts @@ -43,6 +43,7 @@ export type ProviderHelper = (input: { reqModel: string; providerModel: string } createUsageParser: () => { parse: (chunk: string) => void retrieve: () => any + buidlCostChunk: (cost: string) => string } normalizeUsage: (usage: any) => UsageInfo } diff --git a/packages/console/app/src/routes/zen/util/rateLimiter.ts b/packages/console/app/src/routes/zen/util/rateLimiter.ts index 90e10479c44b..5e4f31e67697 100644 --- a/packages/console/app/src/routes/zen/util/rateLimiter.ts +++ b/packages/console/app/src/routes/zen/util/rateLimiter.ts @@ -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() diff --git a/packages/console/app/test/rateLimiter.test.ts b/packages/console/app/test/rateLimiter.test.ts new file mode 100644 index 000000000000..864f907d6696 --- /dev/null +++ b/packages/console/app/test/rateLimiter.test.ts @@ -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) + }) +}) diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 4304b17790de..8e72a74b580b 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.1.53", + "version": "1.2.6", "private": true, "type": "module", "license": "MIT", @@ -12,13 +12,14 @@ "@opencode-ai/console-resource": "workspace:*", "@planetscale/database": "1.19.0", "aws4fetch": "1.0.20", - "drizzle-orm": "0.41.0", + "drizzle-orm": "catalog:", "postgres": "3.4.7", "stripe": "18.0.0", "ulid": "catalog:", "zod": "catalog:" }, "exports": { + "./*.js": "./src/*.ts", "./*": "./src/*" }, "scripts": { @@ -43,7 +44,7 @@ "@tsconfig/node22": "22.0.2", "@types/bun": "1.3.0", "@types/node": "catalog:", - "drizzle-kit": "0.30.5", + "drizzle-kit": "catalog:", "mysql2": "3.14.4", "typescript": "catalog:", "@typescript/native-preview": "catalog:" diff --git a/packages/console/core/script/disable-reload.ts b/packages/console/core/script/disable-reload.ts new file mode 100644 index 000000000000..860739eb0007 --- /dev/null +++ b/packages/console/core/script/disable-reload.ts @@ -0,0 +1,34 @@ +import { Database, eq } from "../src/drizzle/index.js" +import { BillingTable } from "../src/schema/billing.sql.js" +import { WorkspaceTable } from "../src/schema/workspace.sql.js" + +const workspaceID = process.argv[2] + +if (!workspaceID) { + console.error("Usage: bun disable-reload.ts ") + process.exit(1) +} + +const billing = await Database.use((tx) => + tx + .select({ reload: BillingTable.reload }) + .from(BillingTable) + .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, BillingTable.workspaceID)) + .where(eq(BillingTable.workspaceID, workspaceID)) + .then((rows) => rows[0]), +) +if (!billing) { + console.error("Error: Workspace or billing record not found") + process.exit(1) +} + +if (!billing.reload) { + console.log(`Reload is already disabled for workspace ${workspaceID}`) + process.exit(0) +} + +await Database.use((tx) => + tx.update(BillingTable).set({ reload: false }).where(eq(BillingTable.workspaceID, workspaceID)), +) + +console.log(`Disabled reload for workspace ${workspaceID}`) diff --git a/packages/console/core/script/promote-models.ts b/packages/console/core/script/promote-models.ts index 9a9b2dcade70..aa9f20abc3ea 100755 --- a/packages/console/core/script/promote-models.ts +++ b/packages/console/core/script/promote-models.ts @@ -9,7 +9,7 @@ const stage = process.argv[2] if (!stage) throw new Error("Stage is required") const root = path.resolve(process.cwd(), "..", "..", "..") -const PARTS = 10 +const PARTS = 30 // read the secret const ret = await $`bun sst secret list`.cwd(root).text() @@ -29,5 +29,5 @@ ZenData.validate(JSON.parse(values.join(""))) // update the secret const envFile = Bun.file(path.join(os.tmpdir(), `models-${Date.now()}.env`)) -await envFile.write(values.map((v, i) => `ZEN_MODELS${i + 1}=${v}`).join("\n")) +await envFile.write(values.map((v, i) => `ZEN_MODELS${i + 1}="${v.replace(/"/g, '\\"')}"`).join("\n")) await $`bun sst secret load ${envFile.name} --stage ${stage}`.cwd(root) diff --git a/packages/console/core/script/pull-models.ts b/packages/console/core/script/pull-models.ts index 6e89f602b787..4c376210ff66 100755 --- a/packages/console/core/script/pull-models.ts +++ b/packages/console/core/script/pull-models.ts @@ -9,7 +9,7 @@ const stage = process.argv[2] if (!stage) throw new Error("Stage is required") const root = path.resolve(process.cwd(), "..", "..", "..") -const PARTS = 10 +const PARTS = 20 // read the secret const ret = await $`bun sst secret list --stage ${stage}`.cwd(root).text() @@ -29,5 +29,5 @@ ZenData.validate(JSON.parse(values.join(""))) // update the secret const envFile = Bun.file(path.join(os.tmpdir(), `models-${Date.now()}.env`)) -await envFile.write(values.map((v, i) => `ZEN_MODELS${i + 1}=${v}`).join("\n")) +await envFile.write(values.map((v, i) => `ZEN_MODELS${i + 1}="${v.replace(/"/g, '\\"')}"`).join("\n")) await $`bun sst secret load ${envFile.name}`.cwd(root) diff --git a/packages/console/core/script/update-models.ts b/packages/console/core/script/update-models.ts index 095c4aba8556..6d7f7662a4cd 100755 --- a/packages/console/core/script/update-models.ts +++ b/packages/console/core/script/update-models.ts @@ -7,7 +7,7 @@ import { ZenData } from "../src/model" const root = path.resolve(process.cwd(), "..", "..", "..") const models = await $`bun sst secret list`.cwd(root).text() -const PARTS = 10 +const PARTS = 30 // read the line starting with "ZEN_MODELS" const lines = models.split("\n") @@ -39,5 +39,5 @@ const newValues = Array.from({ length: PARTS }, (_, i) => ) const envFile = Bun.file(path.join(os.tmpdir(), `models-${Date.now()}.env`)) -await envFile.write(newValues.map((v, i) => `ZEN_MODELS${i + 1}=${v}`).join("\n")) +await envFile.write(newValues.map((v, i) => `ZEN_MODELS${i + 1}="${v.replace(/"/g, '\\"')}"`).join("\n")) await $`bun sst secret load ${envFile.name}`.cwd(root) diff --git a/packages/console/core/src/drizzle/index.ts b/packages/console/core/src/drizzle/index.ts index f0f065de4a53..d3a4b63bf3e2 100644 --- a/packages/console/core/src/drizzle/index.ts +++ b/packages/console/core/src/drizzle/index.ts @@ -4,7 +4,6 @@ export * from "drizzle-orm" import { Client } from "@planetscale/database" import { MySqlTransaction, type MySqlTransactionConfig } from "drizzle-orm/mysql-core" -import type { ExtractTablesWithRelations } from "drizzle-orm" import type { PlanetScalePreparedQueryHKT, PlanetscaleQueryResultHKT } from "drizzle-orm/planetscale-serverless" import { Context } from "../context" import { memo } from "../util/memo" @@ -14,7 +13,7 @@ export namespace Database { PlanetscaleQueryResultHKT, PlanetScalePreparedQueryHKT, Record, - ExtractTablesWithRelations> + any > const client = memo(() => { @@ -23,7 +22,7 @@ export namespace Database { username: Resource.Database.username, password: Resource.Database.password, }) - const db = drizzle(result, {}) + const db = drizzle({ client: result }) return db }) diff --git a/packages/console/core/src/model.ts b/packages/console/core/src/model.ts index 6b06f275d4d0..6011cac37683 100644 --- a/packages/console/core/src/model.ts +++ b/packages/console/core/src/model.ts @@ -60,13 +60,21 @@ export namespace ZenData { const ProviderSchema = z.object({ api: z.string(), apiKey: z.string(), - format: FormatSchema, + format: FormatSchema.optional(), headerMappings: z.record(z.string(), z.string()).optional(), + payloadModifier: z.record(z.string(), z.any()).optional(), + family: z.string().optional(), + }) + + const ProviderFamilySchema = z.object({ + headers: z.record(z.string(), z.string()).optional(), + responseModifier: z.record(z.string(), z.string()).optional(), }) const ModelsSchema = z.object({ models: z.record(z.string(), z.union([ModelSchema, z.array(ModelSchema.extend({ formatFilter: FormatSchema }))])), providers: z.record(z.string(), ProviderSchema), + providerFamilies: z.record(z.string(), ProviderFamilySchema), }) export const validate = fn(ModelsSchema, (input) => { @@ -84,9 +92,38 @@ export namespace ZenData { Resource.ZEN_MODELS7.value + Resource.ZEN_MODELS8.value + Resource.ZEN_MODELS9.value + - Resource.ZEN_MODELS10.value, + Resource.ZEN_MODELS10.value + + Resource.ZEN_MODELS11.value + + Resource.ZEN_MODELS12.value + + Resource.ZEN_MODELS13.value + + Resource.ZEN_MODELS14.value + + Resource.ZEN_MODELS15.value + + Resource.ZEN_MODELS16.value + + Resource.ZEN_MODELS17.value + + Resource.ZEN_MODELS18.value + + Resource.ZEN_MODELS19.value + + Resource.ZEN_MODELS20.value + + Resource.ZEN_MODELS21.value + + Resource.ZEN_MODELS22.value + + Resource.ZEN_MODELS23.value + + Resource.ZEN_MODELS24.value + + Resource.ZEN_MODELS25.value + + Resource.ZEN_MODELS26.value + + Resource.ZEN_MODELS27.value + + Resource.ZEN_MODELS28.value + + Resource.ZEN_MODELS29.value + + Resource.ZEN_MODELS30.value, ) - return ModelsSchema.parse(json) + const { models, providers, providerFamilies } = ModelsSchema.parse(json) + return { + models, + providers: Object.fromEntries( + Object.entries(providers).map(([id, provider]) => [ + id, + { ...provider, ...(provider.family ? providerFamilies[provider.family] : {}) }, + ]), + ), + } }) } diff --git a/packages/console/core/sst-env.d.ts b/packages/console/core/sst-env.d.ts index 0769c76335b1..737af71d414a 100644 --- a/packages/console/core/sst-env.d.ts +++ b/packages/console/core/sst-env.d.ts @@ -137,14 +137,94 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "ZEN_MODELS11": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS12": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS13": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS14": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS15": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS16": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS17": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS18": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS19": { + "type": "sst.sst.Secret" + "value": string + } "ZEN_MODELS2": { "type": "sst.sst.Secret" "value": string } + "ZEN_MODELS20": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS21": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS22": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS23": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS24": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS25": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS26": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS27": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS28": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS29": { + "type": "sst.sst.Secret" + "value": string + } "ZEN_MODELS3": { "type": "sst.sst.Secret" "value": string } + "ZEN_MODELS30": { + "type": "sst.sst.Secret" + "value": string + } "ZEN_MODELS4": { "type": "sst.sst.Secret" "value": string diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 7ad6c9934cc5..2852976364ca 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.1.53", + "version": "1.2.6", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/function/src/log-processor.ts b/packages/console/function/src/log-processor.ts index 9e76e2ceb085..327fc930b72e 100644 --- a/packages/console/function/src/log-processor.ts +++ b/packages/console/function/src/log-processor.ts @@ -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()) diff --git a/packages/console/function/sst-env.d.ts b/packages/console/function/sst-env.d.ts index 0769c76335b1..737af71d414a 100644 --- a/packages/console/function/sst-env.d.ts +++ b/packages/console/function/sst-env.d.ts @@ -137,14 +137,94 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "ZEN_MODELS11": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS12": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS13": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS14": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS15": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS16": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS17": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS18": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS19": { + "type": "sst.sst.Secret" + "value": string + } "ZEN_MODELS2": { "type": "sst.sst.Secret" "value": string } + "ZEN_MODELS20": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS21": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS22": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS23": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS24": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS25": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS26": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS27": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS28": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS29": { + "type": "sst.sst.Secret" + "value": string + } "ZEN_MODELS3": { "type": "sst.sst.Secret" "value": string } + "ZEN_MODELS30": { + "type": "sst.sst.Secret" + "value": string + } "ZEN_MODELS4": { "type": "sst.sst.Secret" "value": string diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index c314f3392df9..5ee81030fb95 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.1.53", + "version": "1.2.6", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/console/resource/sst-env.d.ts b/packages/console/resource/sst-env.d.ts index 0769c76335b1..737af71d414a 100644 --- a/packages/console/resource/sst-env.d.ts +++ b/packages/console/resource/sst-env.d.ts @@ -137,14 +137,94 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "ZEN_MODELS11": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS12": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS13": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS14": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS15": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS16": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS17": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS18": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS19": { + "type": "sst.sst.Secret" + "value": string + } "ZEN_MODELS2": { "type": "sst.sst.Secret" "value": string } + "ZEN_MODELS20": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS21": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS22": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS23": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS24": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS25": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS26": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS27": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS28": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS29": { + "type": "sst.sst.Secret" + "value": string + } "ZEN_MODELS3": { "type": "sst.sst.Secret" "value": string } + "ZEN_MODELS30": { + "type": "sst.sst.Secret" + "value": string + } "ZEN_MODELS4": { "type": "sst.sst.Secret" "value": string diff --git a/packages/desktop/src-tauri/src/constants.rs b/packages/desktop/src-tauri/src/constants.rs deleted file mode 100644 index ac3e1d02adb6..000000000000 --- a/packages/desktop/src-tauri/src/constants.rs +++ /dev/null @@ -1,10 +0,0 @@ -use tauri_plugin_window_state::StateFlags; - -pub const SETTINGS_STORE: &str = "opencode.settings.dat"; -pub const DEFAULT_SERVER_URL_KEY: &str = "defaultServerUrl"; -pub const UPDATER_ENABLED: bool = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some(); -pub const MAX_LOG_ENTRIES: usize = 200; - -pub fn window_state_flags() -> StateFlags { - StateFlags::all() - StateFlags::DECORATIONS - StateFlags::VISIBLE -} diff --git a/packages/desktop/src-tauri/src/server.rs b/packages/desktop/src-tauri/src/server.rs deleted file mode 100644 index 2a78411a43a4..000000000000 --- a/packages/desktop/src-tauri/src/server.rs +++ /dev/null @@ -1,195 +0,0 @@ -use std::time::{Duration, Instant}; - -use tauri::AppHandle; -use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult}; -use tauri_plugin_shell::process::CommandChild; -use tauri_plugin_store::StoreExt; -use tokio::task::JoinHandle; - -use crate::{ - cli, - constants::{DEFAULT_SERVER_URL_KEY, SETTINGS_STORE}, -}; - -#[tauri::command] -#[specta::specta] -pub fn get_default_server_url(app: AppHandle) -> Result, String> { - let store = app - .store(SETTINGS_STORE) - .map_err(|e| format!("Failed to open settings store: {}", e))?; - - let value = store.get(DEFAULT_SERVER_URL_KEY); - match value { - Some(v) => Ok(v.as_str().map(String::from)), - None => Ok(None), - } -} - -#[tauri::command] -#[specta::specta] -pub async fn set_default_server_url(app: AppHandle, url: Option) -> Result<(), String> { - let store = app - .store(SETTINGS_STORE) - .map_err(|e| format!("Failed to open settings store: {}", e))?; - - match url { - Some(u) => { - store.set(DEFAULT_SERVER_URL_KEY, serde_json::Value::String(u)); - } - None => { - store.delete(DEFAULT_SERVER_URL_KEY); - } - } - - store - .save() - .map_err(|e| format!("Failed to save settings: {}", e))?; - - Ok(()) -} - -pub async fn get_saved_server_url(app: &tauri::AppHandle) -> Option { - if let Some(url) = get_default_server_url(app.clone()).ok().flatten() { - println!("Using desktop-specific custom URL: {url}"); - return Some(url); - } - - if let Some(cli_config) = cli::get_config(app).await - && let Some(url) = get_server_url_from_config(&cli_config) - { - println!("Using custom server URL from config: {url}"); - return Some(url); - } - - None -} - -pub fn spawn_local_server( - app: AppHandle, - hostname: String, - port: u32, - password: String, -) -> (CommandChild, HealthCheck) { - let child = cli::serve(&app, &hostname, port, &password); - - let health_check = HealthCheck(tokio::spawn(async move { - let url = format!("http://{hostname}:{port}"); - - let timestamp = Instant::now(); - loop { - tokio::time::sleep(Duration::from_millis(100)).await; - - if check_health(&url, Some(&password)).await { - println!("Server ready after {:?}", timestamp.elapsed()); - break; - } - } - })); - - (child, health_check) -} - -pub struct HealthCheck(pub JoinHandle<()>); - -pub async fn check_health(url: &str, password: Option<&str>) -> bool { - let Ok(url) = reqwest::Url::parse(url) else { - return false; - }; - - let mut builder = reqwest::Client::builder().timeout(Duration::from_secs(3)); - - if url_is_localhost(&url) { - // Some environments set proxy variables (HTTP_PROXY/HTTPS_PROXY/ALL_PROXY) without - // excluding loopback. reqwest respects these by default, which can prevent the desktop - // app from reaching its own local sidecar server. - builder = builder.no_proxy(); - }; - - let Ok(client) = builder.build() else { - return false; - }; - let Ok(health_url) = url.join("/global/health") else { - return false; - }; - - let mut req = client.get(health_url); - - if let Some(password) = password { - req = req.basic_auth("opencode", Some(password)); - } - - req.send() - .await - .map(|r| r.status().is_success()) - .unwrap_or(false) -} - -fn url_is_localhost(url: &reqwest::Url) -> bool { - url.host_str().is_some_and(|host| { - host.eq_ignore_ascii_case("localhost") - || host - .parse::() - .is_ok_and(|ip| ip.is_loopback()) - }) -} - -/// Converts a bind address hostname to a valid URL hostname for connection. -/// - `0.0.0.0` and `::` are wildcard bind addresses, not valid connect targets -/// - IPv6 addresses need brackets in URLs (e.g., `::1` -> `[::1]`) -fn normalize_hostname_for_url(hostname: &str) -> String { - // Wildcard bind addresses -> localhost equivalents - if hostname == "0.0.0.0" { - return "127.0.0.1".to_string(); - } - if hostname == "::" { - return "[::1]".to_string(); - } - - // IPv6 addresses need brackets in URLs - if hostname.contains(':') && !hostname.starts_with('[') { - return format!("[{}]", hostname); - } - - hostname.to_string() -} - -fn get_server_url_from_config(config: &cli::Config) -> Option { - let server = config.server.as_ref()?; - let port = server.port?; - println!("server.port found in OC config: {port}"); - let hostname = server - .hostname - .as_ref() - .map(|v| normalize_hostname_for_url(v)) - .unwrap_or_else(|| "127.0.0.1".to_string()); - - Some(format!("http://{}:{}", hostname, port)) -} - -pub async fn check_health_or_ask_retry(app: &AppHandle, url: &str) -> bool { - println!("Checking health for {url}"); - loop { - if check_health(url, None).await { - return true; - } - - const RETRY: &str = "Retry"; - - let res = app.dialog() - .message(format!("Could not connect to configured server:\n{}\n\nWould you like to retry or start a local server instead?", url)) - .title("Connection Failed") - .buttons(MessageDialogButtons::OkCancelCustom(RETRY.to_string(), "Start Local".to_string())) - .blocking_show_with_result(); - - match res { - MessageDialogResult::Custom(name) if name == RETRY => { - continue; - } - _ => { - break; - } - } - } - - false -} diff --git a/packages/desktop/src-tauri/src/windows.rs b/packages/desktop/src-tauri/src/windows.rs deleted file mode 100644 index cf3e399e34bf..000000000000 --- a/packages/desktop/src-tauri/src/windows.rs +++ /dev/null @@ -1,140 +0,0 @@ -use crate::constants::{UPDATER_ENABLED, window_state_flags}; -use std::{ops::Deref, time::Duration}; -use tauri::{AppHandle, Manager, Runtime, WebviewUrl, WebviewWindow, WebviewWindowBuilder}; -use tauri_plugin_window_state::AppHandleExt; -use tokio::sync::mpsc; - -pub struct MainWindow(WebviewWindow); - -impl Deref for MainWindow { - type Target = WebviewWindow; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl MainWindow { - pub const LABEL: &str = "main"; - - pub fn create(app: &AppHandle) -> Result { - if let Some(window) = app.get_webview_window(Self::LABEL) { - return Ok(Self(window)); - } - - let window_builder = base_window_config( - WebviewWindowBuilder::new(app, Self::LABEL, WebviewUrl::App("/".into())), - app, - ) - .title("OpenCode") - .decorations(true) - .disable_drag_drop_handler() - .zoom_hotkeys_enabled(false) - .visible(true) - .maximized(true) - .initialization_script(format!( - r#" - window.__OPENCODE__ ??= {{}}; - window.__OPENCODE__.updaterEnabled = {UPDATER_ENABLED}; - "# - )); - - let window = window_builder.build()?; - - setup_window_state_listener(app, &window); - - #[cfg(windows)] - { - use tauri_plugin_decorum::WebviewWindowExt; - let _ = window.create_overlay_titlebar(); - } - - Ok(Self(window)) - } -} - -fn setup_window_state_listener(app: &AppHandle, window: &WebviewWindow) { - let (tx, mut rx) = mpsc::channel::<()>(1); - - window.on_window_event(move |event| { - use tauri::WindowEvent; - if !matches!(event, WindowEvent::Moved(_) | WindowEvent::Resized(_)) { - return; - } - let _ = tx.try_send(()); - }); - - tokio::spawn({ - let app = app.clone(); - - async move { - let save = || { - let handle = app.clone(); - let app = app.clone(); - let _ = handle.run_on_main_thread(move || { - let _ = app.save_window_state(window_state_flags()); - }); - }; - - while rx.recv().await.is_some() { - tokio::time::sleep(Duration::from_millis(200)).await; - - save(); - } - } - }); -} - -pub struct LoadingWindow(WebviewWindow); - -impl Deref for LoadingWindow { - type Target = WebviewWindow; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl LoadingWindow { - pub const LABEL: &str = "loading"; - - pub fn create(app: &AppHandle) -> Result { - let window_builder = base_window_config( - WebviewWindowBuilder::new(app, Self::LABEL, tauri::WebviewUrl::App("/loading".into())), - app, - ) - .center() - .resizable(false) - .inner_size(640.0, 480.0) - .visible(true); - - Ok(Self(window_builder.build()?)) - } -} - -fn base_window_config<'a, R: Runtime, M: Manager>( - window_builder: WebviewWindowBuilder<'a, R, M>, - _app: &AppHandle, -) -> WebviewWindowBuilder<'a, R, M> { - let window_builder = window_builder.decorations(true); - - #[cfg(windows)] - let window_builder = window_builder - // Some VPNs set a global/system proxy that WebView2 applies even for loopback - // connections, which breaks the app's localhost sidecar server. - // Note: when setting additional args, we must re-apply wry's default - // `--disable-features=...` flags. - .additional_browser_args( - "--proxy-bypass-list=<-loopback> --disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection", - ) - .data_directory(_app.path().config_dir().expect("Failed to get config dir").join(_app.config().product_name.clone().unwrap())) - .decorations(false); - - #[cfg(target_os = "macos")] - let window_builder = window_builder - .title_bar_style(tauri::TitleBarStyle::Overlay) - .hidden_title(true) - .traffic_light_position(tauri::LogicalPosition::new(12.0, 18.0)); - - window_builder -} diff --git a/packages/desktop/src/bindings.ts b/packages/desktop/src/bindings.ts deleted file mode 100644 index 562a98acaec4..000000000000 --- a/packages/desktop/src/bindings.ts +++ /dev/null @@ -1,48 +0,0 @@ -// This file has been generated by Tauri Specta. Do not edit this file manually. - -import { invoke as __TAURI_INVOKE, Channel } from '@tauri-apps/api/core'; -import * as __TAURI_EVENT from "@tauri-apps/api/event"; - -/** Commands */ -export const commands = { - killSidecar: () => __TAURI_INVOKE("kill_sidecar"), - installCli: () => __TAURI_INVOKE("install_cli"), - awaitInitialization: (events: Channel) => __TAURI_INVOKE("await_initialization", { events }), - getDefaultServerUrl: () => __TAURI_INVOKE("get_default_server_url"), - setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE("set_default_server_url", { url }), - parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE("parse_markdown_command", { markdown }), - checkAppExists: (appName: string) => __TAURI_INVOKE("check_app_exists", { appName }), -}; - -/** Events */ -export const events = { - loadingWindowComplete: makeEvent("loading-window-complete"), -}; - -/* Types */ -export type InitStep = { phase: "server_waiting" } | { phase: "sqlite_waiting" } | { phase: "done" }; - -export type LoadingWindowComplete = null; - -export type ServerReadyData = { - url: string, - password: string | null, - }; - -/* Tauri Specta runtime */ -function makeEvent(name: string) { - const base = { - listen: (cb: __TAURI_EVENT.EventCallback) => __TAURI_EVENT.listen(name, cb), - once: (cb: __TAURI_EVENT.EventCallback) => __TAURI_EVENT.once(name, cb), - emit: (payload: T) => __TAURI_EVENT.emit(name, payload) as unknown as (T extends null ? () => Promise : (payload: T) => Promise) - }; - - const fn = (target: import("@tauri-apps/api/webview").Webview | import("@tauri-apps/api/window").Window) => ({ - listen: (cb: __TAURI_EVENT.EventCallback) => target.listen(name, cb), - once: (cb: __TAURI_EVENT.EventCallback) => target.once(name, cb), - emit: (payload: T) => target.emit(name, payload) as unknown as (T extends null ? () => Promise : (payload: T) => Promise) - }); - - return Object.assign(fn, base); -} - diff --git a/packages/desktop/src/entry.tsx b/packages/desktop/src/entry.tsx deleted file mode 100644 index b1c9f13f9c4d..000000000000 --- a/packages/desktop/src/entry.tsx +++ /dev/null @@ -1,5 +0,0 @@ -if (location.pathname === "/loading") { - import("./loading") -} else { - import("./") -} diff --git a/packages/desktop/src/i18n/ar.ts b/packages/desktop/src/i18n/ar.ts deleted file mode 100644 index fdbf0a804708..000000000000 --- a/packages/desktop/src/i18n/ar.ts +++ /dev/null @@ -1,26 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "التحقق من وجود تحديثات...", - "desktop.menu.installCli": "تثبيت CLI...", - "desktop.menu.reloadWebview": "إعادة تحميل Webview", - "desktop.menu.restart": "إعادة تشغيل", - - "desktop.dialog.chooseFolder": "اختر مجلدًا", - "desktop.dialog.chooseFile": "اختر ملفًا", - "desktop.dialog.saveFile": "حفظ ملف", - - "desktop.updater.checkFailed.title": "فشل التحقق من التحديثات", - "desktop.updater.checkFailed.message": "فشل التحقق من وجود تحديثات", - "desktop.updater.none.title": "لا توجد تحديثات متاحة", - "desktop.updater.none.message": "أنت تستخدم بالفعل أحدث إصدار من OpenCode", - "desktop.updater.downloadFailed.title": "فشل التحديث", - "desktop.updater.downloadFailed.message": "فشل تنزيل التحديث", - "desktop.updater.downloaded.title": "تم تنزيل التحديث", - "desktop.updater.downloaded.prompt": "تم تنزيل إصدار {{version}} من OpenCode، هل ترغب في تثبيته وإعادة تشغيله؟", - "desktop.updater.installFailed.title": "فشل التحديث", - "desktop.updater.installFailed.message": "فشل تثبيت التحديث", - - "desktop.cli.installed.title": "تم تثبيت CLI", - "desktop.cli.installed.message": "تم تثبيت CLI في {{path}}\n\nأعد تشغيل الطرفية لاستخدام الأمر 'opencode'.", - "desktop.cli.failed.title": "فشل التثبيت", - "desktop.cli.failed.message": "فشل تثبيت CLI: {{error}}", -} diff --git a/packages/desktop/src/i18n/br.ts b/packages/desktop/src/i18n/br.ts deleted file mode 100644 index 75fe2dc32bc7..000000000000 --- a/packages/desktop/src/i18n/br.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Verificar atualizações...", - "desktop.menu.installCli": "Instalar CLI...", - "desktop.menu.reloadWebview": "Recarregar Webview", - "desktop.menu.restart": "Reiniciar", - - "desktop.dialog.chooseFolder": "Escolher uma pasta", - "desktop.dialog.chooseFile": "Escolher um arquivo", - "desktop.dialog.saveFile": "Salvar arquivo", - - "desktop.updater.checkFailed.title": "Falha ao verificar atualizações", - "desktop.updater.checkFailed.message": "Falha ao verificar atualizações", - "desktop.updater.none.title": "Nenhuma atualização disponível", - "desktop.updater.none.message": "Você já está usando a versão mais recente do OpenCode", - "desktop.updater.downloadFailed.title": "Falha na atualização", - "desktop.updater.downloadFailed.message": "Falha ao baixar a atualização", - "desktop.updater.downloaded.title": "Atualização baixada", - "desktop.updater.downloaded.prompt": - "A versão {{version}} do OpenCode foi baixada. Você gostaria de instalá-la e reiniciar?", - "desktop.updater.installFailed.title": "Falha na atualização", - "desktop.updater.installFailed.message": "Falha ao instalar a atualização", - - "desktop.cli.installed.title": "CLI instalada", - "desktop.cli.installed.message": "CLI instalada em {{path}}\n\nReinicie seu terminal para usar o comando 'opencode'.", - "desktop.cli.failed.title": "Falha na instalação", - "desktop.cli.failed.message": "Falha ao instalar a CLI: {{error}}", -} diff --git a/packages/desktop/src/i18n/bs.ts b/packages/desktop/src/i18n/bs.ts deleted file mode 100644 index 58c266f5305e..000000000000 --- a/packages/desktop/src/i18n/bs.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Provjeri ažuriranja...", - "desktop.menu.installCli": "Instaliraj CLI...", - "desktop.menu.reloadWebview": "Ponovo učitavanje webview-a", - "desktop.menu.restart": "Restartuj", - - "desktop.dialog.chooseFolder": "Odaberi folder", - "desktop.dialog.chooseFile": "Odaberi datoteku", - "desktop.dialog.saveFile": "Sačuvaj datoteku", - - "desktop.updater.checkFailed.title": "Provjera ažuriranja nije uspjela", - "desktop.updater.checkFailed.message": "Nije moguće provjeriti ažuriranja", - "desktop.updater.none.title": "Nema dostupnog ažuriranja", - "desktop.updater.none.message": "Već koristiš najnoviju verziju OpenCode-a", - "desktop.updater.downloadFailed.title": "Ažuriranje nije uspjelo", - "desktop.updater.downloadFailed.message": "Neuspjelo preuzimanje ažuriranja", - "desktop.updater.downloaded.title": "Ažuriranje preuzeto", - "desktop.updater.downloaded.prompt": - "Verzija {{version}} OpenCode-a je preuzeta. Želiš li da je instaliraš i ponovo pokreneš aplikaciju?", - "desktop.updater.installFailed.title": "Ažuriranje nije uspjelo", - "desktop.updater.installFailed.message": "Neuspjela instalacija ažuriranja", - - "desktop.cli.installed.title": "CLI instaliran", - "desktop.cli.installed.message": - "CLI je instaliran u {{path}}\n\nRestartuj terminal da bi koristio komandu 'opencode'.", - "desktop.cli.failed.title": "Instalacija nije uspjela", - "desktop.cli.failed.message": "Neuspjela instalacija CLI-a: {{error}}", -} diff --git a/packages/desktop/src/i18n/da.ts b/packages/desktop/src/i18n/da.ts deleted file mode 100644 index 2109495f7612..000000000000 --- a/packages/desktop/src/i18n/da.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Tjek for opdateringer...", - "desktop.menu.installCli": "Installer CLI...", - "desktop.menu.reloadWebview": "Genindlæs Webview", - "desktop.menu.restart": "Genstart", - - "desktop.dialog.chooseFolder": "Vælg en mappe", - "desktop.dialog.chooseFile": "Vælg en fil", - "desktop.dialog.saveFile": "Gem fil", - - "desktop.updater.checkFailed.title": "Opdateringstjek mislykkedes", - "desktop.updater.checkFailed.message": "Kunne ikke tjekke for opdateringer", - "desktop.updater.none.title": "Ingen opdatering tilgængelig", - "desktop.updater.none.message": "Du bruger allerede den nyeste version af OpenCode", - "desktop.updater.downloadFailed.title": "Opdatering mislykkedes", - "desktop.updater.downloadFailed.message": "Kunne ikke downloade opdateringen", - "desktop.updater.downloaded.title": "Opdatering downloadet", - "desktop.updater.downloaded.prompt": - "Version {{version}} af OpenCode er blevet downloadet. Vil du installere den og genstarte?", - "desktop.updater.installFailed.title": "Opdatering mislykkedes", - "desktop.updater.installFailed.message": "Kunne ikke installere opdateringen", - - "desktop.cli.installed.title": "CLI installeret", - "desktop.cli.installed.message": - "CLI installeret i {{path}}\n\nGenstart din terminal for at bruge 'opencode'-kommandoen.", - "desktop.cli.failed.title": "Installation mislykkedes", - "desktop.cli.failed.message": "Kunne ikke installere CLI: {{error}}", -} diff --git a/packages/desktop/src/i18n/de.ts b/packages/desktop/src/i18n/de.ts deleted file mode 100644 index 38ad8096e31e..000000000000 --- a/packages/desktop/src/i18n/de.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Nach Updates suchen...", - "desktop.menu.installCli": "CLI installieren...", - "desktop.menu.reloadWebview": "Webview neu laden", - "desktop.menu.restart": "Neustart", - - "desktop.dialog.chooseFolder": "Ordner auswählen", - "desktop.dialog.chooseFile": "Datei auswählen", - "desktop.dialog.saveFile": "Datei speichern", - - "desktop.updater.checkFailed.title": "Updateprüfung fehlgeschlagen", - "desktop.updater.checkFailed.message": "Updates konnten nicht geprüft werden", - "desktop.updater.none.title": "Kein Update verfügbar", - "desktop.updater.none.message": "Sie verwenden bereits die neueste Version von OpenCode", - "desktop.updater.downloadFailed.title": "Update fehlgeschlagen", - "desktop.updater.downloadFailed.message": "Update konnte nicht heruntergeladen werden", - "desktop.updater.downloaded.title": "Update heruntergeladen", - "desktop.updater.downloaded.prompt": - "Version {{version}} von OpenCode wurde heruntergeladen. Möchten Sie sie installieren und neu starten?", - "desktop.updater.installFailed.title": "Update fehlgeschlagen", - "desktop.updater.installFailed.message": "Update konnte nicht installiert werden", - - "desktop.cli.installed.title": "CLI installiert", - "desktop.cli.installed.message": - "CLI wurde in {{path}} installiert\n\nStarten Sie Ihr Terminal neu, um den Befehl 'opencode' zu verwenden.", - "desktop.cli.failed.title": "Installation fehlgeschlagen", - "desktop.cli.failed.message": "CLI konnte nicht installiert werden: {{error}}", -} diff --git a/packages/desktop/src/i18n/en.ts b/packages/desktop/src/i18n/en.ts deleted file mode 100644 index 4c30380d562c..000000000000 --- a/packages/desktop/src/i18n/en.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Check for Updates...", - "desktop.menu.installCli": "Install CLI...", - "desktop.menu.reloadWebview": "Reload Webview", - "desktop.menu.restart": "Restart", - - "desktop.dialog.chooseFolder": "Choose a folder", - "desktop.dialog.chooseFile": "Choose a file", - "desktop.dialog.saveFile": "Save file", - - "desktop.updater.checkFailed.title": "Update Check Failed", - "desktop.updater.checkFailed.message": "Failed to check for updates", - "desktop.updater.none.title": "No Update Available", - "desktop.updater.none.message": "You are already using the latest version of OpenCode", - "desktop.updater.downloadFailed.title": "Update Failed", - "desktop.updater.downloadFailed.message": "Failed to download update", - "desktop.updater.downloaded.title": "Update Downloaded", - "desktop.updater.downloaded.prompt": - "Version {{version}} of OpenCode has been downloaded, would you like to install it and relaunch?", - "desktop.updater.installFailed.title": "Update Failed", - "desktop.updater.installFailed.message": "Failed to install update", - - "desktop.cli.installed.title": "CLI Installed", - "desktop.cli.installed.message": "CLI installed to {{path}}\n\nRestart your terminal to use the 'opencode' command.", - "desktop.cli.failed.title": "Installation Failed", - "desktop.cli.failed.message": "Failed to install CLI: {{error}}", -} diff --git a/packages/desktop/src/i18n/es.ts b/packages/desktop/src/i18n/es.ts deleted file mode 100644 index 80504a8f248c..000000000000 --- a/packages/desktop/src/i18n/es.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Buscar actualizaciones...", - "desktop.menu.installCli": "Instalar CLI...", - "desktop.menu.reloadWebview": "Recargar Webview", - "desktop.menu.restart": "Reiniciar", - - "desktop.dialog.chooseFolder": "Elegir una carpeta", - "desktop.dialog.chooseFile": "Elegir un archivo", - "desktop.dialog.saveFile": "Guardar archivo", - - "desktop.updater.checkFailed.title": "Comprobación de actualizaciones fallida", - "desktop.updater.checkFailed.message": "No se pudieron buscar actualizaciones", - "desktop.updater.none.title": "No hay actualizaciones disponibles", - "desktop.updater.none.message": "Ya estás usando la versión más reciente de OpenCode", - "desktop.updater.downloadFailed.title": "Actualización fallida", - "desktop.updater.downloadFailed.message": "No se pudo descargar la actualización", - "desktop.updater.downloaded.title": "Actualización descargada", - "desktop.updater.downloaded.prompt": - "Se ha descargado la versión {{version}} de OpenCode. ¿Quieres instalarla y reiniciar?", - "desktop.updater.installFailed.title": "Actualización fallida", - "desktop.updater.installFailed.message": "No se pudo instalar la actualización", - - "desktop.cli.installed.title": "CLI instalada", - "desktop.cli.installed.message": "CLI instalada en {{path}}\n\nReinicia tu terminal para usar el comando 'opencode'.", - "desktop.cli.failed.title": "Instalación fallida", - "desktop.cli.failed.message": "No se pudo instalar la CLI: {{error}}", -} diff --git a/packages/desktop/src/i18n/fr.ts b/packages/desktop/src/i18n/fr.ts deleted file mode 100644 index 4f0bb2b16c65..000000000000 --- a/packages/desktop/src/i18n/fr.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Vérifier les mises à jour...", - "desktop.menu.installCli": "Installer la CLI...", - "desktop.menu.reloadWebview": "Recharger la Webview", - "desktop.menu.restart": "Redémarrer", - - "desktop.dialog.chooseFolder": "Choisir un dossier", - "desktop.dialog.chooseFile": "Choisir un fichier", - "desktop.dialog.saveFile": "Enregistrer le fichier", - - "desktop.updater.checkFailed.title": "Échec de la vérification des mises à jour", - "desktop.updater.checkFailed.message": "Impossible de vérifier les mises à jour", - "desktop.updater.none.title": "Aucune mise à jour disponible", - "desktop.updater.none.message": "Vous utilisez déjà la dernière version d'OpenCode", - "desktop.updater.downloadFailed.title": "Échec de la mise à jour", - "desktop.updater.downloadFailed.message": "Impossible de télécharger la mise à jour", - "desktop.updater.downloaded.title": "Mise à jour téléchargée", - "desktop.updater.downloaded.prompt": - "La version {{version}} d'OpenCode a été téléchargée. Voulez-vous l'installer et redémarrer ?", - "desktop.updater.installFailed.title": "Échec de la mise à jour", - "desktop.updater.installFailed.message": "Impossible d'installer la mise à jour", - - "desktop.cli.installed.title": "CLI installée", - "desktop.cli.installed.message": - "CLI installée dans {{path}}\n\nRedémarrez votre terminal pour utiliser la commande 'opencode'.", - "desktop.cli.failed.title": "Échec de l'installation", - "desktop.cli.failed.message": "Impossible d'installer la CLI : {{error}}", -} diff --git a/packages/desktop/src/i18n/index.ts b/packages/desktop/src/i18n/index.ts deleted file mode 100644 index 376769e282b3..000000000000 --- a/packages/desktop/src/i18n/index.ts +++ /dev/null @@ -1,182 +0,0 @@ -import * as i18n from "@solid-primitives/i18n" -import { Store } from "@tauri-apps/plugin-store" - -import { dict as desktopEn } from "./en" -import { dict as desktopZh } from "./zh" -import { dict as desktopZht } from "./zht" -import { dict as desktopKo } from "./ko" -import { dict as desktopDe } from "./de" -import { dict as desktopEs } from "./es" -import { dict as desktopFr } from "./fr" -import { dict as desktopDa } from "./da" -import { dict as desktopJa } from "./ja" -import { dict as desktopPl } from "./pl" -import { dict as desktopRu } from "./ru" -import { dict as desktopAr } from "./ar" -import { dict as desktopNo } from "./no" -import { dict as desktopBr } from "./br" -import { dict as desktopBs } from "./bs" - -import { dict as appEn } from "../../../app/src/i18n/en" -import { dict as appZh } from "../../../app/src/i18n/zh" -import { dict as appZht } from "../../../app/src/i18n/zht" -import { dict as appKo } from "../../../app/src/i18n/ko" -import { dict as appDe } from "../../../app/src/i18n/de" -import { dict as appEs } from "../../../app/src/i18n/es" -import { dict as appFr } from "../../../app/src/i18n/fr" -import { dict as appDa } from "../../../app/src/i18n/da" -import { dict as appJa } from "../../../app/src/i18n/ja" -import { dict as appPl } from "../../../app/src/i18n/pl" -import { dict as appRu } from "../../../app/src/i18n/ru" -import { dict as appAr } from "../../../app/src/i18n/ar" -import { dict as appNo } from "../../../app/src/i18n/no" -import { dict as appBr } from "../../../app/src/i18n/br" -import { dict as appBs } from "../../../app/src/i18n/bs" - -export type Locale = - | "en" - | "zh" - | "zht" - | "ko" - | "de" - | "es" - | "fr" - | "da" - | "ja" - | "pl" - | "ru" - | "ar" - | "no" - | "br" - | "bs" - -type RawDictionary = typeof appEn & typeof desktopEn -type Dictionary = i18n.Flatten - -const LOCALES: readonly Locale[] = [ - "en", - "zh", - "zht", - "ko", - "de", - "es", - "fr", - "da", - "ja", - "pl", - "ru", - "bs", - "ar", - "no", - "br", -] - -function detectLocale(): Locale { - if (typeof navigator !== "object") return "en" - - const languages = navigator.languages?.length ? navigator.languages : [navigator.language] - for (const language of languages) { - if (!language) continue - if (language.toLowerCase().startsWith("zh")) { - if (language.toLowerCase().includes("hant")) return "zht" - return "zh" - } - if (language.toLowerCase().startsWith("ko")) return "ko" - if (language.toLowerCase().startsWith("de")) return "de" - if (language.toLowerCase().startsWith("es")) return "es" - if (language.toLowerCase().startsWith("fr")) return "fr" - if (language.toLowerCase().startsWith("da")) return "da" - if (language.toLowerCase().startsWith("ja")) return "ja" - if (language.toLowerCase().startsWith("pl")) return "pl" - if (language.toLowerCase().startsWith("ru")) return "ru" - if (language.toLowerCase().startsWith("ar")) return "ar" - if ( - language.toLowerCase().startsWith("no") || - language.toLowerCase().startsWith("nb") || - language.toLowerCase().startsWith("nn") - ) - return "no" - if (language.toLowerCase().startsWith("pt")) return "br" - if (language.toLowerCase().startsWith("bs")) return "bs" - } - - return "en" -} - -function parseLocale(value: unknown): Locale | null { - if (!value) return null - if (typeof value !== "string") return null - if ((LOCALES as readonly string[]).includes(value)) return value as Locale - return null -} - -function parseRecord(value: unknown) { - if (!value || typeof value !== "object") return null - if (Array.isArray(value)) return null - return value as Record -} - -function pickLocale(value: unknown): Locale | null { - const direct = parseLocale(value) - if (direct) return direct - - const record = parseRecord(value) - if (!record) return null - - return parseLocale(record.locale) -} - -const base = i18n.flatten({ ...appEn, ...desktopEn }) - -function build(locale: Locale): Dictionary { - if (locale === "en") return base - if (locale === "zh") return { ...base, ...i18n.flatten(appZh), ...i18n.flatten(desktopZh) } - if (locale === "zht") return { ...base, ...i18n.flatten(appZht), ...i18n.flatten(desktopZht) } - if (locale === "de") return { ...base, ...i18n.flatten(appDe), ...i18n.flatten(desktopDe) } - if (locale === "es") return { ...base, ...i18n.flatten(appEs), ...i18n.flatten(desktopEs) } - if (locale === "fr") return { ...base, ...i18n.flatten(appFr), ...i18n.flatten(desktopFr) } - if (locale === "da") return { ...base, ...i18n.flatten(appDa), ...i18n.flatten(desktopDa) } - if (locale === "ja") return { ...base, ...i18n.flatten(appJa), ...i18n.flatten(desktopJa) } - if (locale === "pl") return { ...base, ...i18n.flatten(appPl), ...i18n.flatten(desktopPl) } - if (locale === "ru") return { ...base, ...i18n.flatten(appRu), ...i18n.flatten(desktopRu) } - if (locale === "ar") return { ...base, ...i18n.flatten(appAr), ...i18n.flatten(desktopAr) } - if (locale === "no") return { ...base, ...i18n.flatten(appNo), ...i18n.flatten(desktopNo) } - if (locale === "br") return { ...base, ...i18n.flatten(appBr), ...i18n.flatten(desktopBr) } - if (locale === "bs") return { ...base, ...i18n.flatten(appBs), ...i18n.flatten(desktopBs) } - return { ...base, ...i18n.flatten(appKo), ...i18n.flatten(desktopKo) } -} - -const state = { - locale: detectLocale(), - dict: base as Dictionary, - init: undefined as Promise | undefined, -} - -state.dict = build(state.locale) - -const translate = i18n.translator(() => state.dict, i18n.resolveTemplate) - -export function t(key: keyof Dictionary, params?: Record) { - return translate(key, params) -} - -export function initI18n(): Promise { - const cached = state.init - if (cached) return cached - - const promise = (async () => { - const store = await Store.load("opencode.global.dat").catch(() => null) - if (!store) return state.locale - - const raw = await store.get("language").catch(() => null) - const value = typeof raw === "string" ? JSON.parse(raw) : raw - const next = pickLocale(value) ?? state.locale - - state.locale = next - state.dict = build(next) - return next - })().catch(() => state.locale) - - state.init = promise - return promise -} diff --git a/packages/desktop/src/i18n/ja.ts b/packages/desktop/src/i18n/ja.ts deleted file mode 100644 index fc485c6f401d..000000000000 --- a/packages/desktop/src/i18n/ja.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "アップデートを確認...", - "desktop.menu.installCli": "CLI をインストール...", - "desktop.menu.reloadWebview": "Webview を再読み込み", - "desktop.menu.restart": "再起動", - - "desktop.dialog.chooseFolder": "フォルダーを選択", - "desktop.dialog.chooseFile": "ファイルを選択", - "desktop.dialog.saveFile": "ファイルを保存", - - "desktop.updater.checkFailed.title": "アップデートの確認に失敗しました", - "desktop.updater.checkFailed.message": "アップデートを確認できませんでした", - "desktop.updater.none.title": "利用可能なアップデートはありません", - "desktop.updater.none.message": "すでに最新バージョンの OpenCode を使用しています", - "desktop.updater.downloadFailed.title": "アップデートに失敗しました", - "desktop.updater.downloadFailed.message": "アップデートをダウンロードできませんでした", - "desktop.updater.downloaded.title": "アップデートをダウンロードしました", - "desktop.updater.downloaded.prompt": - "OpenCode のバージョン {{version}} がダウンロードされました。インストールして再起動しますか?", - "desktop.updater.installFailed.title": "アップデートに失敗しました", - "desktop.updater.installFailed.message": "アップデートをインストールできませんでした", - - "desktop.cli.installed.title": "CLI をインストールしました", - "desktop.cli.installed.message": - "CLI を {{path}} にインストールしました\n\nターミナルを再起動して 'opencode' コマンドを使用してください。", - "desktop.cli.failed.title": "インストールに失敗しました", - "desktop.cli.failed.message": "CLI のインストールに失敗しました: {{error}}", -} diff --git a/packages/desktop/src/i18n/ko.ts b/packages/desktop/src/i18n/ko.ts deleted file mode 100644 index be27cec86aca..000000000000 --- a/packages/desktop/src/i18n/ko.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "업데이트 확인...", - "desktop.menu.installCli": "CLI 설치...", - "desktop.menu.reloadWebview": "Webview 새로고침", - "desktop.menu.restart": "다시 시작", - - "desktop.dialog.chooseFolder": "폴더 선택", - "desktop.dialog.chooseFile": "파일 선택", - "desktop.dialog.saveFile": "파일 저장", - - "desktop.updater.checkFailed.title": "업데이트 확인 실패", - "desktop.updater.checkFailed.message": "업데이트를 확인하지 못했습니다", - "desktop.updater.none.title": "사용 가능한 업데이트 없음", - "desktop.updater.none.message": "이미 최신 버전의 OpenCode를 사용하고 있습니다", - "desktop.updater.downloadFailed.title": "업데이트 실패", - "desktop.updater.downloadFailed.message": "업데이트를 다운로드하지 못했습니다", - "desktop.updater.downloaded.title": "업데이트 다운로드 완료", - "desktop.updater.downloaded.prompt": "OpenCode {{version}} 버전을 다운로드했습니다. 설치하고 다시 실행할까요?", - "desktop.updater.installFailed.title": "업데이트 실패", - "desktop.updater.installFailed.message": "업데이트를 설치하지 못했습니다", - - "desktop.cli.installed.title": "CLI 설치됨", - "desktop.cli.installed.message": - "CLI가 {{path}}에 설치되었습니다\n\n터미널을 다시 시작하여 'opencode' 명령을 사용하세요.", - "desktop.cli.failed.title": "설치 실패", - "desktop.cli.failed.message": "CLI 설치 실패: {{error}}", -} diff --git a/packages/desktop/src/i18n/no.ts b/packages/desktop/src/i18n/no.ts deleted file mode 100644 index e39bd7f3b447..000000000000 --- a/packages/desktop/src/i18n/no.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Se etter oppdateringer...", - "desktop.menu.installCli": "Installer CLI...", - "desktop.menu.reloadWebview": "Last inn Webview på nytt", - "desktop.menu.restart": "Start på nytt", - - "desktop.dialog.chooseFolder": "Velg en mappe", - "desktop.dialog.chooseFile": "Velg en fil", - "desktop.dialog.saveFile": "Lagre fil", - - "desktop.updater.checkFailed.title": "Oppdateringssjekk mislyktes", - "desktop.updater.checkFailed.message": "Kunne ikke se etter oppdateringer", - "desktop.updater.none.title": "Ingen oppdatering tilgjengelig", - "desktop.updater.none.message": "Du bruker allerede den nyeste versjonen av OpenCode", - "desktop.updater.downloadFailed.title": "Oppdatering mislyktes", - "desktop.updater.downloadFailed.message": "Kunne ikke laste ned oppdateringen", - "desktop.updater.downloaded.title": "Oppdatering lastet ned", - "desktop.updater.downloaded.prompt": - "Versjon {{version}} av OpenCode er lastet ned. Vil du installere den og starte på nytt?", - "desktop.updater.installFailed.title": "Oppdatering mislyktes", - "desktop.updater.installFailed.message": "Kunne ikke installere oppdateringen", - - "desktop.cli.installed.title": "CLI installert", - "desktop.cli.installed.message": - "CLI installert til {{path}}\n\nStart terminalen på nytt for å bruke 'opencode'-kommandoen.", - "desktop.cli.failed.title": "Installasjon mislyktes", - "desktop.cli.failed.message": "Kunne ikke installere CLI: {{error}}", -} diff --git a/packages/desktop/src/i18n/pl.ts b/packages/desktop/src/i18n/pl.ts deleted file mode 100644 index d3ad7ce64f81..000000000000 --- a/packages/desktop/src/i18n/pl.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Sprawdź aktualizacje...", - "desktop.menu.installCli": "Zainstaluj CLI...", - "desktop.menu.reloadWebview": "Przeładuj Webview", - "desktop.menu.restart": "Restartuj", - - "desktop.dialog.chooseFolder": "Wybierz folder", - "desktop.dialog.chooseFile": "Wybierz plik", - "desktop.dialog.saveFile": "Zapisz plik", - - "desktop.updater.checkFailed.title": "Nie udało się sprawdzić aktualizacji", - "desktop.updater.checkFailed.message": "Nie udało się sprawdzić aktualizacji", - "desktop.updater.none.title": "Brak dostępnych aktualizacji", - "desktop.updater.none.message": "Korzystasz już z najnowszej wersji OpenCode", - "desktop.updater.downloadFailed.title": "Aktualizacja nie powiodła się", - "desktop.updater.downloadFailed.message": "Nie udało się pobrać aktualizacji", - "desktop.updater.downloaded.title": "Aktualizacja pobrana", - "desktop.updater.downloaded.prompt": - "Pobrano wersję {{version}} OpenCode. Czy chcesz ją zainstalować i uruchomić ponownie?", - "desktop.updater.installFailed.title": "Aktualizacja nie powiodła się", - "desktop.updater.installFailed.message": "Nie udało się zainstalować aktualizacji", - - "desktop.cli.installed.title": "CLI zainstalowane", - "desktop.cli.installed.message": - "CLI zainstalowane w {{path}}\n\nUruchom ponownie terminal, aby użyć polecenia 'opencode'.", - "desktop.cli.failed.title": "Instalacja nie powiodła się", - "desktop.cli.failed.message": "Nie udało się zainstalować CLI: {{error}}", -} diff --git a/packages/desktop/src/i18n/ru.ts b/packages/desktop/src/i18n/ru.ts deleted file mode 100644 index 8e09cc45b49d..000000000000 --- a/packages/desktop/src/i18n/ru.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Проверить обновления...", - "desktop.menu.installCli": "Установить CLI...", - "desktop.menu.reloadWebview": "Перезагрузить Webview", - "desktop.menu.restart": "Перезапустить", - - "desktop.dialog.chooseFolder": "Выберите папку", - "desktop.dialog.chooseFile": "Выберите файл", - "desktop.dialog.saveFile": "Сохранить файл", - - "desktop.updater.checkFailed.title": "Не удалось проверить обновления", - "desktop.updater.checkFailed.message": "Не удалось проверить обновления", - "desktop.updater.none.title": "Обновлений нет", - "desktop.updater.none.message": "Вы уже используете последнюю версию OpenCode", - "desktop.updater.downloadFailed.title": "Обновление не удалось", - "desktop.updater.downloadFailed.message": "Не удалось скачать обновление", - "desktop.updater.downloaded.title": "Обновление загружено", - "desktop.updater.downloaded.prompt": "Версия OpenCode {{version}} загружена. Хотите установить и перезапустить?", - "desktop.updater.installFailed.title": "Обновление не удалось", - "desktop.updater.installFailed.message": "Не удалось установить обновление", - - "desktop.cli.installed.title": "CLI установлен", - "desktop.cli.installed.message": - "CLI установлен в {{path}}\n\nПерезапустите терминал, чтобы использовать команду 'opencode'.", - "desktop.cli.failed.title": "Ошибка установки", - "desktop.cli.failed.message": "Не удалось установить CLI: {{error}}", -} diff --git a/packages/desktop/src/i18n/zh.ts b/packages/desktop/src/i18n/zh.ts deleted file mode 100644 index aeb3a54e03e9..000000000000 --- a/packages/desktop/src/i18n/zh.ts +++ /dev/null @@ -1,26 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "检查更新...", - "desktop.menu.installCli": "安装 CLI...", - "desktop.menu.reloadWebview": "重新加载 Webview", - "desktop.menu.restart": "重启", - - "desktop.dialog.chooseFolder": "选择文件夹", - "desktop.dialog.chooseFile": "选择文件", - "desktop.dialog.saveFile": "保存文件", - - "desktop.updater.checkFailed.title": "检查更新失败", - "desktop.updater.checkFailed.message": "无法检查更新", - "desktop.updater.none.title": "没有可用更新", - "desktop.updater.none.message": "你已经在使用最新版本的 OpenCode", - "desktop.updater.downloadFailed.title": "更新失败", - "desktop.updater.downloadFailed.message": "无法下载更新", - "desktop.updater.downloaded.title": "更新已下载", - "desktop.updater.downloaded.prompt": "已下载 OpenCode {{version}} 版本,是否安装并重启?", - "desktop.updater.installFailed.title": "更新失败", - "desktop.updater.installFailed.message": "无法安装更新", - - "desktop.cli.installed.title": "CLI 已安装", - "desktop.cli.installed.message": "CLI 已安装到 {{path}}\n\n重启终端以使用 'opencode' 命令。", - "desktop.cli.failed.title": "安装失败", - "desktop.cli.failed.message": "无法安装 CLI: {{error}}", -} diff --git a/packages/desktop/src/i18n/zht.ts b/packages/desktop/src/i18n/zht.ts deleted file mode 100644 index 7fd677aca444..000000000000 --- a/packages/desktop/src/i18n/zht.ts +++ /dev/null @@ -1,26 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "檢查更新...", - "desktop.menu.installCli": "安裝 CLI...", - "desktop.menu.reloadWebview": "重新載入 Webview", - "desktop.menu.restart": "重新啟動", - - "desktop.dialog.chooseFolder": "選擇資料夾", - "desktop.dialog.chooseFile": "選擇檔案", - "desktop.dialog.saveFile": "儲存檔案", - - "desktop.updater.checkFailed.title": "檢查更新失敗", - "desktop.updater.checkFailed.message": "無法檢查更新", - "desktop.updater.none.title": "沒有可用更新", - "desktop.updater.none.message": "你已在使用最新版的 OpenCode", - "desktop.updater.downloadFailed.title": "更新失敗", - "desktop.updater.downloadFailed.message": "無法下載更新", - "desktop.updater.downloaded.title": "更新已下載", - "desktop.updater.downloaded.prompt": "已下載 OpenCode {{version}} 版本,是否安裝並重新啟動?", - "desktop.updater.installFailed.title": "更新失敗", - "desktop.updater.installFailed.message": "無法安裝更新", - - "desktop.cli.installed.title": "CLI 已安裝", - "desktop.cli.installed.message": "CLI 已安裝到 {{path}}\n\n重新啟動終端機以使用 'opencode' 命令。", - "desktop.cli.failed.title": "安裝失敗", - "desktop.cli.failed.message": "無法安裝 CLI: {{error}}", -} diff --git a/packages/desktop/src/loading.tsx b/packages/desktop/src/loading.tsx deleted file mode 100644 index 752cde893b7c..000000000000 --- a/packages/desktop/src/loading.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { render } from "solid-js/web" -import { MetaProvider } from "@solidjs/meta" -import "@opencode-ai/app/index.css" -import { Font } from "@opencode-ai/ui/font" -import { Splash } from "@opencode-ai/ui/logo" -import "./styles.css" -import { createSignal, Match, onMount } from "solid-js" -import { commands, events, InitStep } from "./bindings" -import { Channel } from "@tauri-apps/api/core" -import { Switch } from "solid-js" - -const root = document.getElementById("root")! - -render(() => { - let splash!: SVGSVGElement - const [state, setState] = createSignal(null) - - const channel = new Channel() - channel.onmessage = (e) => setState(e) - commands.awaitInitialization(channel as any).then(() => { - const currentOpacity = getComputedStyle(splash).opacity - - splash.style.animation = "none" - splash.style.animationPlayState = "paused" - splash.style.opacity = currentOpacity - - requestAnimationFrame(() => { - splash.style.transition = "opacity 0.3s ease" - requestAnimationFrame(() => { - splash.style.opacity = "1" - }) - }) - }) - - return ( - -
    - -
    - - - - - {(_) => { - onMount(() => { - setTimeout(() => events.loadingWindowComplete.emit(null), 1000) - }) - - return "All done" - }} - - - {(_) => { - const textItems = [ - "Just a moment...", - "Migrating your database", - "This could take a couple of minutes", - ] - const [textIndex, setTextIndex] = createSignal(0) - - onMount(async () => { - await new Promise((res) => setTimeout(res, 3000)) - setTextIndex(1) - await new Promise((res) => setTimeout(res, 6000)) - setTextIndex(2) - }) - - return <>{textItems[textIndex()]} - }} - - - -
    -
    -
    - ) -}, root) diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 289d8fcb8d68..d300a62e4e31 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.53", + "version": "1.2.6", "private": true, "type": "module", "license": "MIT", diff --git a/packages/enterprise/sst-env.d.ts b/packages/enterprise/sst-env.d.ts index 0769c76335b1..737af71d414a 100644 --- a/packages/enterprise/sst-env.d.ts +++ b/packages/enterprise/sst-env.d.ts @@ -137,14 +137,94 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "ZEN_MODELS11": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS12": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS13": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS14": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS15": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS16": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS17": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS18": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS19": { + "type": "sst.sst.Secret" + "value": string + } "ZEN_MODELS2": { "type": "sst.sst.Secret" "value": string } + "ZEN_MODELS20": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS21": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS22": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS23": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS24": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS25": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS26": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS27": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS28": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS29": { + "type": "sst.sst.Secret" + "value": string + } "ZEN_MODELS3": { "type": "sst.sst.Secret" "value": string } + "ZEN_MODELS30": { + "type": "sst.sst.Secret" + "value": string + } "ZEN_MODELS4": { "type": "sst.sst.Secret" "value": string diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 216f864fee83..19edacd44371 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.1.53" +version = "1.2.6" 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.2.6/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.2.6/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.2.6/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.2.6/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.2.6/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index b3086a180db7..580667b962c5 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.1.53", + "version": "1.2.6", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/function/sst-env.d.ts b/packages/function/sst-env.d.ts index 0769c76335b1..737af71d414a 100644 --- a/packages/function/sst-env.d.ts +++ b/packages/function/sst-env.d.ts @@ -137,14 +137,94 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "ZEN_MODELS11": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS12": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS13": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS14": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS15": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS16": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS17": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS18": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS19": { + "type": "sst.sst.Secret" + "value": string + } "ZEN_MODELS2": { "type": "sst.sst.Secret" "value": string } + "ZEN_MODELS20": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS21": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS22": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS23": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS24": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS25": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS26": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS27": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS28": { + "type": "sst.sst.Secret" + "value": string + } + "ZEN_MODELS29": { + "type": "sst.sst.Secret" + "value": string + } "ZEN_MODELS3": { "type": "sst.sst.Secret" "value": string } + "ZEN_MODELS30": { + "type": "sst.sst.Secret" + "value": string + } "ZEN_MODELS4": { "type": "sst.sst.Secret" "value": string diff --git a/packages/opencode/AGENTS.md b/packages/opencode/AGENTS.md index a68fd7f3e321..dcfc336d6525 100644 --- a/packages/opencode/AGENTS.md +++ b/packages/opencode/AGENTS.md @@ -1,27 +1,10 @@ -# opencode agent guidelines +# opencode database guide -## Build/Test Commands +## Database -- **Install**: `bun install` -- **Run**: `bun run --conditions=browser ./src/index.ts` -- **Typecheck**: `bun run typecheck` (npm run typecheck) -- **Test**: `bun test` (runs all tests) -- **Single test**: `bun test test/tool/tool.test.ts` (specific test file) - -## Code Style - -- **Runtime**: Bun with TypeScript ESM modules -- **Imports**: Use relative imports for local modules, named imports preferred -- **Types**: Zod schemas for validation, TypeScript interfaces for structure -- **Naming**: camelCase for variables/functions, PascalCase for classes/namespaces -- **Error handling**: Use Result patterns, avoid throwing exceptions in tools -- **File structure**: Namespace-based organization (e.g., `Tool.define()`, `Session.create()`) - -## Architecture - -- **Tools**: Implement `Tool.Info` interface with `execute()` method -- **Context**: Pass `sessionID` in tool context, use `App.provide()` for DI -- **Validation**: All inputs validated with Zod schemas -- **Logging**: Use `Log.create({ service: "name" })` pattern -- **Storage**: Use `Storage` namespace for persistence -- **API Client**: The TypeScript TUI (built with SolidJS + OpenTUI) communicates with the OpenCode server using `@opencode-ai/sdk`. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, run `./script/generate.ts` to regenerate the SDK and related files. +- **Schema**: Drizzle schema lives in `src/**/*.sql.ts`. +- **Naming**: tables and columns use snake*case; join columns are `_id`; indexes are `*\_idx`. +- **Migrations**: generated by Drizzle Kit using `drizzle.config.ts` (schema: `./src/**/*.sql.ts`, output: `./migration`). +- **Command**: `bun run db generate --name `. +- **Output**: creates `migration/_/migration.sql` and `snapshot.json`. +- **Tests**: migration tests should read the per-folder layout (no `_journal.json`). diff --git a/packages/opencode/bin/opencode b/packages/opencode/bin/opencode index e35cc00944d6..d73bbce26776 100755 --- a/packages/opencode/bin/opencode +++ b/packages/opencode/bin/opencode @@ -47,20 +47,109 @@ if (!arch) { const base = "opencode-" + platform + "-" + arch const binary = platform === "windows" ? "opencode.exe" : "opencode" +function supportsAvx2() { + if (arch !== "x64") return false + + if (platform === "linux") { + try { + return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8")) + } catch { + return false + } + } + + if (platform === "darwin") { + try { + const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], { + encoding: "utf8", + timeout: 1500, + }) + if (result.status !== 0) return false + return (result.stdout || "").trim() === "1" + } catch { + return false + } + } + + if (platform === "windows") { + const cmd = + '(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)' + + for (const exe of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) { + try { + const result = childProcess.spawnSync(exe, ["-NoProfile", "-NonInteractive", "-Command", cmd], { + encoding: "utf8", + timeout: 3000, + windowsHide: true, + }) + if (result.status !== 0) continue + const out = (result.stdout || "").trim().toLowerCase() + if (out === "true" || out === "1") return true + if (out === "false" || out === "0") return false + } catch { + continue + } + } + + return false + } + + return false +} + +const names = (() => { + const avx2 = supportsAvx2() + const baseline = arch === "x64" && !avx2 + + if (platform === "linux") { + const musl = (() => { + try { + if (fs.existsSync("/etc/alpine-release")) return true + } catch { + // ignore + } + + try { + const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" }) + const text = ((result.stdout || "") + (result.stderr || "")).toLowerCase() + if (text.includes("musl")) return true + } catch { + // ignore + } + + return false + })() + + if (musl) { + if (arch === "x64") { + if (baseline) return [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base] + return [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`] + } + return [`${base}-musl`, base] + } + + if (arch === "x64") { + if (baseline) return [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`] + return [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`] + } + return [base, `${base}-musl`] + } + + if (arch === "x64") { + if (baseline) return [`${base}-baseline`, base] + return [base, `${base}-baseline`] + } + return [base] +})() + function findBinary(startDir) { let current = startDir for (;;) { const modules = path.join(current, "node_modules") if (fs.existsSync(modules)) { - const entries = fs.readdirSync(modules) - for (const entry of entries) { - if (!entry.startsWith(base)) { - continue - } - const candidate = path.join(modules, entry, "bin", binary) - if (fs.existsSync(candidate)) { - return candidate - } + for (const name of names) { + const candidate = path.join(modules, name, "bin", binary) + if (fs.existsSync(candidate)) return candidate } } const parent = path.dirname(current) @@ -74,9 +163,9 @@ function findBinary(startDir) { const resolved = findBinary(scriptDir) if (!resolved) { console.error( - 'It seems that your package manager failed to install the right version of the opencode CLI for your platform. You can try manually installing the "' + - base + - '" package', + "It seems that your package manager failed to install the right version of the opencode CLI for your platform. You can try manually installing " + + names.map((n) => `\"${n}\"`).join(" or ") + + " package", ) process.exit(1) } diff --git a/packages/opencode/bunfig.toml b/packages/opencode/bunfig.toml index db64a09a988f..c3b727076493 100644 --- a/packages/opencode/bunfig.toml +++ b/packages/opencode/bunfig.toml @@ -2,4 +2,6 @@ preload = ["@opentui/solid/preload"] [test] preload = ["./test/preload.ts"] -timeout = 10000 # 10 seconds (default is 5000ms) +# timeout is not actually parsed from bunfig.toml (see src/bunfig.zig in oven-sh/bun) +# using --timeout in package.json scripts instead +# https://github.com/oven-sh/bun/issues/7789 diff --git a/packages/opencode/drizzle.config.ts b/packages/opencode/drizzle.config.ts new file mode 100644 index 000000000000..1b4fd556e9cb --- /dev/null +++ b/packages/opencode/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "drizzle-kit" + +export default defineConfig({ + dialect: "sqlite", + schema: "./src/**/*.sql.ts", + out: "./migration", + dbCredentials: { + url: "/home/thdxr/.local/share/opencode/opencode.db", + }, +}) diff --git a/packages/opencode/migration/20260127222353_familiar_lady_ursula/migration.sql b/packages/opencode/migration/20260127222353_familiar_lady_ursula/migration.sql new file mode 100644 index 000000000000..775c1a1173dc --- /dev/null +++ b/packages/opencode/migration/20260127222353_familiar_lady_ursula/migration.sql @@ -0,0 +1,90 @@ +CREATE TABLE `project` ( + `id` text PRIMARY KEY, + `worktree` text NOT NULL, + `vcs` text, + `name` text, + `icon_url` text, + `icon_color` text, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, + `time_initialized` integer, + `sandboxes` text NOT NULL +); +--> statement-breakpoint +CREATE TABLE `message` ( + `id` text PRIMARY KEY, + `session_id` text NOT NULL, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, + `data` text NOT NULL, + CONSTRAINT `fk_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE +); +--> statement-breakpoint +CREATE TABLE `part` ( + `id` text PRIMARY KEY, + `message_id` text NOT NULL, + `session_id` text NOT NULL, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, + `data` text NOT NULL, + CONSTRAINT `fk_part_message_id_message_id_fk` FOREIGN KEY (`message_id`) REFERENCES `message`(`id`) ON DELETE CASCADE +); +--> statement-breakpoint +CREATE TABLE `permission` ( + `project_id` text PRIMARY KEY, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, + `data` text NOT NULL, + CONSTRAINT `fk_permission_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE +); +--> statement-breakpoint +CREATE TABLE `session` ( + `id` text PRIMARY KEY, + `project_id` text NOT NULL, + `parent_id` text, + `slug` text NOT NULL, + `directory` text NOT NULL, + `title` text NOT NULL, + `version` text NOT NULL, + `share_url` text, + `summary_additions` integer, + `summary_deletions` integer, + `summary_files` integer, + `summary_diffs` text, + `revert` text, + `permission` text, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, + `time_compacting` integer, + `time_archived` integer, + CONSTRAINT `fk_session_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE +); +--> statement-breakpoint +CREATE TABLE `todo` ( + `session_id` text NOT NULL, + `content` text NOT NULL, + `status` text NOT NULL, + `priority` text NOT NULL, + `position` integer NOT NULL, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, + CONSTRAINT `todo_pk` PRIMARY KEY(`session_id`, `position`), + CONSTRAINT `fk_todo_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE +); +--> statement-breakpoint +CREATE TABLE `session_share` ( + `session_id` text PRIMARY KEY, + `id` text NOT NULL, + `secret` text NOT NULL, + `url` text NOT NULL, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, + CONSTRAINT `fk_session_share_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE +); +--> statement-breakpoint +CREATE INDEX `message_session_idx` ON `message` (`session_id`);--> statement-breakpoint +CREATE INDEX `part_message_idx` ON `part` (`message_id`);--> statement-breakpoint +CREATE INDEX `part_session_idx` ON `part` (`session_id`);--> statement-breakpoint +CREATE INDEX `session_project_idx` ON `session` (`project_id`);--> statement-breakpoint +CREATE INDEX `session_parent_idx` ON `session` (`parent_id`);--> statement-breakpoint +CREATE INDEX `todo_session_idx` ON `todo` (`session_id`); \ No newline at end of file diff --git a/packages/opencode/migration/20260127222353_familiar_lady_ursula/snapshot.json b/packages/opencode/migration/20260127222353_familiar_lady_ursula/snapshot.json new file mode 100644 index 000000000000..ff76ee209a1e --- /dev/null +++ b/packages/opencode/migration/20260127222353_familiar_lady_ursula/snapshot.json @@ -0,0 +1,796 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "068758ed-a97a-46f6-8a59-6c639ae7c20c", + "prevIds": ["00000000-0000-0000-0000-000000000000"], + "ddl": [ + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": ["message_id"], + "tableTo": "message", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": ["session_id", "position"], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": ["project_id"], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": ["session_id"], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + } + ], + "renames": [] +} diff --git a/packages/opencode/migration/20260211171708_add_project_commands/migration.sql b/packages/opencode/migration/20260211171708_add_project_commands/migration.sql new file mode 100644 index 000000000000..b63f147a0b2b --- /dev/null +++ b/packages/opencode/migration/20260211171708_add_project_commands/migration.sql @@ -0,0 +1 @@ +ALTER TABLE `project` ADD `commands` text; \ No newline at end of file diff --git a/packages/opencode/migration/20260211171708_add_project_commands/snapshot.json b/packages/opencode/migration/20260211171708_add_project_commands/snapshot.json new file mode 100644 index 000000000000..1182cc32de90 --- /dev/null +++ b/packages/opencode/migration/20260211171708_add_project_commands/snapshot.json @@ -0,0 +1,806 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "8bc2d11d-97fa-4ba8-8bfa-6c5956c49aeb", + "prevIds": ["068758ed-a97a-46f6-8a59-6c639ae7c20c"], + "ddl": [ + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "commands", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": ["message_id"], + "tableTo": "message", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": ["session_id", "position"], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": ["project_id"], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": ["session_id"], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + } + ], + "renames": [] +} diff --git a/packages/opencode/migration/20260213144116_wakeful_the_professor/migration.sql b/packages/opencode/migration/20260213144116_wakeful_the_professor/migration.sql new file mode 100644 index 000000000000..3085fe280f3d --- /dev/null +++ b/packages/opencode/migration/20260213144116_wakeful_the_professor/migration.sql @@ -0,0 +1,11 @@ +CREATE TABLE `control_account` ( + `email` text NOT NULL, + `url` text NOT NULL, + `access_token` text NOT NULL, + `refresh_token` text NOT NULL, + `token_expiry` integer, + `active` integer NOT NULL, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, + CONSTRAINT `control_account_pk` PRIMARY KEY(`email`, `url`) +); diff --git a/packages/opencode/migration/20260213144116_wakeful_the_professor/snapshot.json b/packages/opencode/migration/20260213144116_wakeful_the_professor/snapshot.json new file mode 100644 index 000000000000..05c00a10cf3d --- /dev/null +++ b/packages/opencode/migration/20260213144116_wakeful_the_professor/snapshot.json @@ -0,0 +1,897 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "d2736e43-700f-4e9e-8151-9f2f0d967bc8", + "prevIds": ["8bc2d11d-97fa-4ba8-8bfa-6c5956c49aeb"], + "ddl": [ + { + "name": "control_account", + "entityType": "tables" + }, + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "commands", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": ["message_id"], + "tableTo": "message", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": ["email", "url"], + "nameExplicit": false, + "name": "control_account_pk", + "entityType": "pks", + "table": "control_account" + }, + { + "columns": ["session_id", "position"], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": ["project_id"], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": ["session_id"], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + } + ], + "renames": [] +} diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 0c34b056b573..2460e1d60540 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,13 +1,13 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.53", + "version": "1.2.6", "name": "opencode", "type": "module", "license": "MIT", "private": true, "scripts": { "typecheck": "tsgo --noEmit", - "test": "bun test", + "test": "bun test --timeout 30000", "build": "bun run script/build.ts", "dev": "bun run --conditions=browser ./src/index.ts", "random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'", @@ -15,7 +15,8 @@ "lint": "echo 'Running lint checks...' && bun test --coverage", "format": "echo 'Formatting code...' && bun run --prettier --write src/**/*.ts", "docs": "echo 'Generating documentation...' && find src -name '*.ts' -exec echo 'Processing: {}' \\;", - "deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'" + "deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'", + "db": "bun drizzle-kit" }, "bin": { "opencode": "./bin/opencode" @@ -44,6 +45,8 @@ "@types/turndown": "5.0.5", "@types/yargs": "17.0.33", "@typescript/native-preview": "catalog:", + "drizzle-kit": "1.0.0-beta.12-a5629fb", + "drizzle-orm": "1.0.0-beta.12-a5629fb", "typescript": "catalog:", "vscode-languageserver-types": "3.17.5", "why-is-node-running": "3.2.2", @@ -53,15 +56,15 @@ "@actions/core": "1.11.1", "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.14.1", - "@ai-sdk/amazon-bedrock": "3.0.74", - "@ai-sdk/anthropic": "2.0.58", + "@ai-sdk/amazon-bedrock": "3.0.79", + "@ai-sdk/anthropic": "2.0.62", "@ai-sdk/azure": "2.0.91", "@ai-sdk/cerebras": "1.0.36", "@ai-sdk/cohere": "2.0.22", - "@ai-sdk/deepinfra": "1.0.33", + "@ai-sdk/deepinfra": "1.0.36", "@ai-sdk/gateway": "2.0.30", "@ai-sdk/google": "2.0.52", - "@ai-sdk/google-vertex": "3.0.98", + "@ai-sdk/google-vertex": "3.0.103", "@ai-sdk/groq": "2.0.34", "@ai-sdk/mistral": "2.0.27", "@ai-sdk/openai": "2.0.89", @@ -73,8 +76,8 @@ "@ai-sdk/vercel": "1.0.33", "@ai-sdk/xai": "2.0.51", "@clack/prompts": "1.0.0-alpha.1", - "@gitlab/gitlab-ai-provider": "3.5.0", - "@gitlab/opencode-gitlab-auth": "1.3.2", + "@gitlab/gitlab-ai-provider": "3.5.1", + "@gitlab/opencode-gitlab-auth": "1.3.3", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", "@modelcontextprotocol/sdk": "1.25.2", @@ -86,8 +89,8 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.4", - "@opentui/core": "0.1.77", - "@opentui/solid": "0.1.77", + "@opentui/core": "0.1.79", + "@opentui/solid": "0.1.79", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -102,6 +105,7 @@ "clipboardy": "4.0.0", "decimal.js": "10.5.0", "diff": "catalog:", + "drizzle-orm": "1.0.0-beta.12-a5629fb", "fuzzysort": "3.1.0", "gray-matter": "4.0.3", "hono": "catalog:", @@ -124,5 +128,8 @@ "yargs": "18.0.0", "zod": "catalog:", "zod-to-json-schema": "3.24.5" + }, + "overrides": { + "drizzle-orm": "1.0.0-beta.12-a5629fb" } } diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index fa4a215d6422..ddb4769912d7 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -1,6 +1,6 @@ #!/usr/bin/env bun -import type { BunPlugin } from "bun" +import solidPlugin from "../node_modules/@opentui/solid/scripts/solid-plugin" import path from "path" import fs from "fs" import { $ } from "bun" @@ -9,57 +9,9 @@ import { fileURLToPath } from "url" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const dir = path.resolve(__dirname, "..") -const rootDir = path.resolve(__dirname, "../../..") - -// SolidJS transform plugin for .tsx/.jsx files -import { solidTransformPlugin } from "./build-plugin" process.chdir(dir) -const dedupePlugin: BunPlugin = { - name: "dedupe-opentui", - setup(build) { - // Use root's node_modules (monorepo layout) - const rootNodeModules = path.resolve(rootDir, "node_modules") - const solidPath = path.resolve(rootNodeModules, "@opentui/solid/index.js") - const corePath = path.resolve(rootNodeModules, "@opentui/core/index.js") - const coreDir = path.resolve(rootNodeModules, "@opentui/core") - - // Verify paths exist before using them - if (!fs.existsSync(solidPath)) { - console.warn(`Warning: @opentui/solid not found at ${solidPath}`) - } - if (!fs.existsSync(corePath)) { - console.warn(`Warning: @opentui/core not found at ${corePath}`) - } - - // Dedupe @opentui/solid and @opentui/core to use the same instance - build.onResolve({ filter: /^@opentui\/solid$/ }, () => ({ path: solidPath })) - build.onResolve({ filter: /^@opentui\/core$/ }, () => ({ path: corePath })) - - // Handle subpath exports for @opentui/core (e.g., @opentui/core/testing) - build.onResolve({ filter: /^@opentui\/core\/.*/ }, (args) => { - const subpath = args.path.substring("@opentui/core".length) // e.g., "/testing" - const exported = path.resolve(coreDir, subpath.slice(1) + ".js") // e.g., "testing.js" - if (fs.existsSync(exported)) { - return { path: exported } - } - return undefined // Let Bun resolve normally if not found - }) - - // Also handle @opentui/solid subpaths if needed - build.onResolve({ filter: /^@opentui\/solid\/.*/ }, (args) => { - const subpath = args.path.substring("@opentui/solid".length) // e.g., "/something" - const solidDir = path.resolve(rootNodeModules, "@opentui/solid") - const exported = path.resolve(solidDir, subpath.slice(1) + ".js") - if (fs.existsSync(exported)) { - return { path: exported } - } - return undefined - }) - }, -} - import pkg from "../package.json" import { Script } from "@opencode-ai/script" const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev" @@ -73,6 +25,32 @@ await Bun.write( ) console.log("Generated models-snapshot.ts") +// Load migrations from migration directories +const migrationDirs = (await fs.promises.readdir(path.join(dir, "migration"), { withFileTypes: true })) + .filter((entry) => entry.isDirectory() && /^\d{4}\d{2}\d{2}\d{2}\d{2}\d{2}/.test(entry.name)) + .map((entry) => entry.name) + .sort() + +const migrations = await Promise.all( + migrationDirs.map(async (name) => { + const file = path.join(dir, "migration", name, "migration.sql") + const sql = await Bun.file(file).text() + const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name) + const timestamp = match + ? Date.UTC( + Number(match[1]), + Number(match[2]) - 1, + Number(match[3]), + Number(match[4]), + Number(match[5]), + Number(match[6]), + ) + : 0 + return { sql, timestamp } + }), +) +console.log(`Loaded ${migrations.length} migrations`) + const singleFlag = process.argv.includes("--single") const baselineFlag = process.argv.includes("--baseline") const skipInstall = process.argv.includes("--skip-install") @@ -157,21 +135,6 @@ const targets = singleFlag }) : allTargets -// Verify platform/architecture filter produced valid target(s) BEFORE cleanup -if (targets.length === 0) { - throw new Error( - `No build targets matched for ${process.platform}/${process.arch}. ` + - `Available targets: ${allTargets.map((t) => `${t.os}-${t.arch}`).join(", ")}`, - ) -} -if (targets.length > 1 && singleFlag) { - throw new Error( - `Multiple targets matched for --single flag: ${targets.map((t) => `${t.os}-${t.arch}`).join(", ")}. ` + - `This should not happen - please investigate.`, - ) -} - -// Safe to clean dist directory now that we know targets are valid await $`rm -rf dist` const binaries: Record = {} @@ -193,66 +156,39 @@ for (const item of targets) { console.log(`building ${name}`) await $`mkdir -p dist/${name}/bin` - const parserWorker = fs.realpathSync(path.resolve(rootDir, "node_modules/@opentui/core/parser.worker.js")) + const parserWorker = fs.realpathSync(path.resolve(dir, "./node_modules/@opentui/core/parser.worker.js")) const workerPath = "./src/cli/cmd/tui/worker.ts" // Use platform-specific bunfs root path based on target OS const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/" const workerRelativePath = path.relative(dir, parserWorker).replaceAll("\\", "/") - try { - await Bun.build({ - conditions: ["browser"], - tsconfig: "./tsconfig.json", - plugins: [dedupePlugin, solidTransformPlugin], - sourcemap: "external", - minify: true, - compile: { - autoloadBunfig: false, - autoloadDotenv: false, - //@ts-ignore (bun types aren't up to date) - autoloadTsconfig: true, - autoloadPackageJson: true, - target: name.replace(pkg.name, "bun") as any, - outfile: `dist/${name}/bin/opencode`, - execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"], - windows: {}, - }, - entrypoints: ["./src/index.ts", parserWorker, workerPath], - define: { - OPENCODE_VERSION: `'${Script.version}'`, - OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath, - OPENCODE_WORKER_PATH: workerPath, - OPENCODE_CHANNEL: `'${Script.channel}'`, - OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "", - }, - }) - } catch (error) { - console.error(`FATAL: Build failed for ${name}:`, error) - process.exit(1) - } - - // Verify binary was created with reasonable size - const binaryPath = `dist/${name}/bin/opencode` - if (!fs.existsSync(binaryPath)) { - console.error(`FATAL: Binary not created at ${binaryPath}`) - process.exit(1) - } - const stat = fs.statSync(binaryPath) - if (!stat.isFile()) { - console.error(`FATAL: Build artifact is not a file: ${binaryPath}`) - process.exit(1) - } - // Binaries should be > 10MB for production builds, > 5MB for dev - const minSize = Script.release ? 10 * 1024 * 1024 : 5 * 1024 * 1024 - if (stat.size < minSize) { - console.error( - `FATAL: Binary suspiciously small: ${(stat.size / 1024 / 1024).toFixed(1)}MB ` + - `(expected > ${(minSize / 1024 / 1024).toFixed(1)}MB)`, - ) - process.exit(1) - } - console.log(`✓ Binary verified: ${(stat.size / 1024 / 1024).toFixed(1)}MB`) + await Bun.build({ + conditions: ["browser"], + tsconfig: "./tsconfig.json", + plugins: [solidPlugin], + sourcemap: "external", + compile: { + autoloadBunfig: false, + autoloadDotenv: false, + //@ts-ignore (bun types aren't up to date) + autoloadTsconfig: true, + autoloadPackageJson: true, + target: name.replace(pkg.name, "bun") as any, + outfile: `dist/${name}/bin/opencode`, + execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"], + windows: {}, + }, + entrypoints: ["./src/index.ts", parserWorker, workerPath], + define: { + OPENCODE_VERSION: `'${Script.version}'`, + OPENCODE_MIGRATIONS: JSON.stringify(migrations), + OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath, + OPENCODE_WORKER_PATH: workerPath, + OPENCODE_CHANNEL: `'${Script.channel}'`, + OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "", + }, + }) await $`rm -rf ./dist/${name}/bin/tui` await Bun.file(`dist/${name}/package.json`).write( diff --git a/packages/opencode/script/check-migrations.ts b/packages/opencode/script/check-migrations.ts new file mode 100644 index 000000000000..f5eaf79323b2 --- /dev/null +++ b/packages/opencode/script/check-migrations.ts @@ -0,0 +1,16 @@ +#!/usr/bin/env bun + +import { $ } from "bun" + +// drizzle-kit check compares schema to migrations, exits non-zero if drift +const result = await $`bun drizzle-kit check`.quiet().nothrow() + +if (result.exitCode !== 0) { + console.error("Schema has changes not captured in migrations!") + console.error("Run: bun drizzle-kit generate") + console.error("") + console.error(result.stderr.toString()) + process.exit(1) +} + +console.log("Migrations are up to date") diff --git a/packages/opencode/src/acp/README.md b/packages/opencode/src/acp/README.md index d998cb22da8d..aab33259bb18 100644 --- a/packages/opencode/src/acp/README.md +++ b/packages/opencode/src/acp/README.md @@ -44,6 +44,16 @@ opencode acp opencode acp --cwd /path/to/project ``` +### Question Tool Opt-In + +ACP excludes `QuestionTool` by default. + +```bash +OPENCODE_ENABLE_QUESTION_TOOL=1 opencode acp +``` + +Enable this only for ACP clients that support interactive question prompts. + ### Programmatic ```typescript diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index d0c907cfdd0f..de16ff029a73 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -29,6 +29,7 @@ import { } from "@agentclientprotocol/sdk" import { Log } from "../util/log" +import { pathToFileURL } from "bun" import { ACPSessionManager } from "./session" import type { ACPConfig } from "./types" import { Provider } from "../provider/provider" @@ -227,8 +228,8 @@ export namespace ACP { const metadata = permission.metadata || {} const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : "" const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : "" - - const content = await Bun.file(filepath).text() + const file = Bun.file(filepath) + const content = (await file.exists()) ? await file.text() : "" const newContent = getNewContent(content, diff) if (newContent) { @@ -434,46 +435,68 @@ export namespace ACP { return } } + return + } - if (part.type === "text") { - const delta = props.delta - if (delta && part.ignored !== true) { - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: delta, - }, + case "message.part.delta": { + const props = event.properties + const session = this.sessionManager.tryGet(props.sessionID) + if (!session) return + const sessionId = session.id + + const message = await this.sdk.session + .message( + { + sessionID: props.sessionID, + messageID: props.messageID, + directory: session.cwd, + }, + { throwOnError: true }, + ) + .then((x) => x.data) + .catch((error) => { + log.error("unexpected error when fetching message", { error }) + return undefined + }) + + if (!message || message.info.role !== "assistant") return + + const part = message.parts.find((p) => p.id === props.partID) + if (!part) return + + if (part.type === "text" && props.field === "text" && part.ignored !== true) { + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "agent_message_chunk", + content: { + type: "text", + text: props.delta, }, - }) - .catch((error) => { - log.error("failed to send text to ACP", { error }) - }) - } + }, + }) + .catch((error) => { + log.error("failed to send text delta to ACP", { error }) + }) return } - if (part.type === "reasoning") { - const delta = props.delta - if (delta) { - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "agent_thought_chunk", - content: { - type: "text", - text: delta, - }, + if (part.type === "reasoning" && props.field === "text") { + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "agent_thought_chunk", + content: { + type: "text", + text: props.delta, }, - }) - .catch((error) => { - log.error("failed to send reasoning to ACP", { error }) - }) - } + }, + }) + .catch((error) => { + log.error("failed to send reasoning delta to ACP", { error }) + }) } return } @@ -1022,7 +1045,7 @@ export namespace ACP { type: "image", mimeType: effectiveMime, data: base64Data, - uri: `file://${filename}`, + uri: pathToFileURL(filename).href, }, }, }) @@ -1032,13 +1055,14 @@ export namespace ACP { } else { // Non-image: text types get decoded, binary types stay as blob const isText = effectiveMime.startsWith("text/") || effectiveMime === "application/json" + const fileUri = pathToFileURL(filename).href const resource = isText ? { - uri: `file://${filename}`, + uri: fileUri, mimeType: effectiveMime, text: Buffer.from(base64Data, "base64").toString("utf-8"), } - : { uri: `file://${filename}`, mimeType: effectiveMime, blob: base64Data } + : { uri: fileUri, mimeType: effectiveMime, blob: base64Data } await this.connection .sessionUpdate({ @@ -1580,7 +1604,7 @@ export namespace ACP { const name = path.split("/").pop() || path return { type: "file", - url: `file://${path}`, + url: pathToFileURL(path).href, filename: name, mime: "text/plain", } diff --git a/packages/opencode/src/acp/session.ts b/packages/opencode/src/acp/session.ts index 18aa42313017..b96ebc1c8952 100644 --- a/packages/opencode/src/acp/session.ts +++ b/packages/opencode/src/acp/session.ts @@ -21,7 +21,6 @@ export class ACPSessionManager { const session = await this.sdk.session .create( { - title: `ACP Session ${crypto.randomUUID()}`, directory: cwd, }, { throwOnError: true }, diff --git a/packages/opencode/src/agent/prompt/compaction.txt b/packages/opencode/src/agent/prompt/compaction.txt index b919671a0aca..3308627e153c 100644 --- a/packages/opencode/src/agent/prompt/compaction.txt +++ b/packages/opencode/src/agent/prompt/compaction.txt @@ -1,6 +1,6 @@ You are a helpful AI assistant tasked with summarizing conversations. -When asked to summarize, provide a detailed but concise summary of the conversation. +When asked to summarize, provide a detailed but concise summary of the conversation. Focus on information that would be helpful for continuing the conversation, including: - What was done - What is currently being worked on @@ -10,3 +10,5 @@ Focus on information that would be helpful for continuing the conversation, incl - Important technical decisions and why they were made Your summary should be comprehensive enough to provide context but concise enough to be quickly understood. + +Do not respond to any questions in the conversation, only output the summary. diff --git a/packages/opencode/src/cli/cmd/db.ts b/packages/opencode/src/cli/cmd/db.ts new file mode 100644 index 000000000000..8ca4b9a42eb2 --- /dev/null +++ b/packages/opencode/src/cli/cmd/db.ts @@ -0,0 +1,118 @@ +import type { Argv } from "yargs" +import { spawn } from "child_process" +import { Database } from "../../storage/db" +import { Database as BunDatabase } from "bun:sqlite" +import { UI } from "../ui" +import { cmd } from "./cmd" +import { JsonMigration } from "../../storage/json-migration" +import { EOL } from "os" + +const QueryCommand = cmd({ + command: "$0 [query]", + describe: "open an interactive sqlite3 shell or run a query", + builder: (yargs: Argv) => { + return yargs + .positional("query", { + type: "string", + describe: "SQL query to execute", + }) + .option("format", { + type: "string", + choices: ["json", "tsv"], + default: "tsv", + describe: "Output format", + }) + }, + handler: async (args: { query?: string; format: string }) => { + const query = args.query as string | undefined + if (query) { + const db = new BunDatabase(Database.Path, { readonly: true }) + try { + const result = db.query(query).all() as Record[] + if (args.format === "json") { + console.log(JSON.stringify(result, null, 2)) + } else if (result.length > 0) { + const keys = Object.keys(result[0]) + console.log(keys.join("\t")) + for (const row of result) { + console.log(keys.map((k) => row[k]).join("\t")) + } + } + } catch (err) { + UI.error(err instanceof Error ? err.message : String(err)) + process.exit(1) + } + db.close() + return + } + const child = spawn("sqlite3", [Database.Path], { + stdio: "inherit", + }) + await new Promise((resolve) => child.on("close", resolve)) + }, +}) + +const PathCommand = cmd({ + command: "path", + describe: "print the database path", + handler: () => { + console.log(Database.Path) + }, +}) + +const MigrateCommand = cmd({ + command: "migrate", + describe: "migrate JSON data to SQLite (merges with existing data)", + handler: async () => { + const sqlite = new BunDatabase(Database.Path) + const tty = process.stderr.isTTY + const width = 36 + const orange = "\x1b[38;5;214m" + const muted = "\x1b[0;2m" + const reset = "\x1b[0m" + let last = -1 + if (tty) process.stderr.write("\x1b[?25l") + try { + const stats = await JsonMigration.run(sqlite, { + progress: (event) => { + const percent = Math.floor((event.current / event.total) * 100) + if (percent === last) return + last = percent + if (tty) { + const fill = Math.round((percent / 100) * width) + const bar = `${"■".repeat(fill)}${"・".repeat(width - fill)}` + process.stderr.write( + `\r${orange}${bar} ${percent.toString().padStart(3)}%${reset} ${muted}${event.current}/${event.total}${reset} `, + ) + } else { + process.stderr.write(`sqlite-migration:${percent}${EOL}`) + } + }, + }) + if (tty) process.stderr.write("\n") + if (tty) process.stderr.write("\x1b[?25h") + else process.stderr.write(`sqlite-migration:done${EOL}`) + UI.println( + `Migration complete: ${stats.projects} projects, ${stats.sessions} sessions, ${stats.messages} messages`, + ) + if (stats.errors.length > 0) { + UI.println(`${stats.errors.length} errors occurred during migration`) + } + } catch (err) { + if (tty) process.stderr.write("\x1b[?25h") + UI.error(`Migration failed: ${err instanceof Error ? err.message : String(err)}`) + process.exit(1) + } finally { + sqlite.close() + } + }, +}) + +export const DbCommand = cmd({ + command: "db", + describe: "database tools", + builder: (yargs: Argv) => { + return yargs.command(QueryCommand).command(PathCommand).command(MigrateCommand).demandCommand() + }, + handler: () => {}, +}) diff --git a/packages/opencode/src/cli/cmd/import.ts b/packages/opencode/src/cli/cmd/import.ts index 37419f4e2350..fd45a09b73cc 100644 --- a/packages/opencode/src/cli/cmd/import.ts +++ b/packages/opencode/src/cli/cmd/import.ts @@ -3,7 +3,8 @@ import type { Session as SDKSession, Message, Part } from "@opencode-ai/sdk/v2" import { Session } from "../../session" import { cmd } from "./cmd" import { bootstrap } from "../bootstrap" -import { Storage } from "../../storage/storage" +import { Database } from "../../storage/db" +import { SessionTable, MessageTable, PartTable } from "../../session/session.sql" import { Instance } from "../../project/instance" import { ShareNext } from "../../share/share-next" import { EOL } from "os" @@ -130,13 +131,35 @@ export const ImportCommand = cmd({ return } - await Storage.write(["session", Instance.project.id, exportData.info.id], exportData.info) + Database.use((db) => db.insert(SessionTable).values(Session.toRow(exportData.info)).onConflictDoNothing().run()) for (const msg of exportData.messages) { - await Storage.write(["message", exportData.info.id, msg.info.id], msg.info) + Database.use((db) => + db + .insert(MessageTable) + .values({ + id: msg.info.id, + session_id: exportData.info.id, + time_created: msg.info.time?.created ?? Date.now(), + data: msg.info, + }) + .onConflictDoNothing() + .run(), + ) for (const part of msg.parts) { - await Storage.write(["part", msg.info.id, part.id], part) + Database.use((db) => + db + .insert(PartTable) + .values({ + id: part.id, + message_id: msg.info.id, + session_id: exportData.info.id, + data: part, + }) + .onConflictDoNothing() + .run(), + ) } } diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index f0222dc109b4..a00d8e34a294 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -1,5 +1,6 @@ import type { Argv } from "yargs" import path from "path" +import { pathToFileURL } from "bun" import { UI } from "../ui" import { cmd } from "./cmd" import { Flag } from "../../flag/flag" @@ -73,9 +74,7 @@ function fallback(part: ToolPart) { function rg(info: ToolProps) { const root = info.input.path ?? "" - const isFilesOnly = info.input.files_only === true - const mode = isFilesOnly ? "List" : "Search" - const title = `${mode} "${info.input.pattern}"` + const title = info.input.files_only ? `Glob "${info.input.pattern}"` : `Grep "${info.input.pattern}"` const suffix = root ? `in ${normalizePath(root)}` : "" const num = info.metadata.matches const description = @@ -223,6 +222,10 @@ export const RunCommand = cmd({ describe: "session id to continue", type: "string", }) + .option("fork", { + describe: "fork the session before continuing (requires --continue or --session)", + type: "boolean", + }) .option("share", { type: "boolean", describe: "share the session", @@ -256,6 +259,10 @@ export const RunCommand = cmd({ type: "string", describe: "attach to a running opencode server (e.g., http://localhost:4096)", }) + .option("dir", { + type: "string", + describe: "directory to run in, path on remote server if attaching", + }) .option("port", { type: "number", describe: "port for the local server (defaults to random port if no value provided)", @@ -275,6 +282,18 @@ export const RunCommand = cmd({ .map((arg) => (arg.includes(" ") ? `"${arg.replace(/"/g, '\\"')}"` : arg)) .join(" ") + const directory = (() => { + if (!args.dir) return undefined + if (args.attach) return args.dir + try { + process.chdir(args.dir) + return process.cwd() + } catch { + UI.error("Failed to change directory to " + args.dir) + process.exit(1) + } + })() + const files: { type: "file"; url: string; filename: string; mime: string }[] = [] if (args.file) { const list = Array.isArray(args.file) ? args.file : [args.file] @@ -297,7 +316,7 @@ export const RunCommand = cmd({ files.push({ type: "file", - url: `file://${resolvedPath}`, + url: pathToFileURL(resolvedPath).href, filename: path.basename(resolvedPath), mime, }) @@ -311,6 +330,11 @@ export const RunCommand = cmd({ process.exit(1) } + if (args.fork && !args.continue && !args.session) { + UI.error("--fork requires --continue or --session") + process.exit(1) + } + const rules: PermissionNext.Ruleset = [ { permission: "question", @@ -336,11 +360,15 @@ export const RunCommand = cmd({ } async function session(sdk: OpencodeClient) { - if (args.continue) { - const result = await sdk.session.list() - return result.data?.find((s) => !s.parentID)?.id + const baseID = args.continue ? (await sdk.session.list()).data?.find((s) => !s.parentID)?.id : args.session + + if (baseID && args.fork) { + const forked = await sdk.session.fork({ sessionID: baseID }) + return forked.data?.id } - if (args.session) return args.session + + if (baseID) return baseID + const name = title() const result = await sdk.session.create({ title: name, permission: rules }) return result.data?.id @@ -363,19 +391,23 @@ export const RunCommand = cmd({ async function execute(sdk: OpencodeClient) { function tool(part: ToolPart) { - if (part.tool === "bash") return bash(props(part)) - if (part.tool === "rg") return rg(props(part)) - if (part.tool === "list") return list(props(part)) - if (part.tool === "read") return read(props(part)) - if (part.tool === "write") return write(props(part)) - if (part.tool === "webfetch") return webfetch(props(part)) - if (part.tool === "edit") return edit(props(part)) - if (part.tool === "codesearch") return codesearch(props(part)) - if (part.tool === "websearch") return websearch(props(part)) - if (part.tool === "task") return task(props(part)) - if (part.tool === "todowrite") return todo(props(part)) - if (part.tool === "skill") return skill(props(part)) - return fallback(part) + try { + if (part.tool === "bash") return bash(props(part)) + if (part.tool === "rg") return rg(props(part)) + if (part.tool === "list") return list(props(part)) + if (part.tool === "read") return read(props(part)) + if (part.tool === "write") return write(props(part)) + if (part.tool === "webfetch") return webfetch(props(part)) + if (part.tool === "edit") return edit(props(part)) + if (part.tool === "codesearch") return codesearch(props(part)) + if (part.tool === "websearch") return websearch(props(part)) + if (part.tool === "task") return task(props(part)) + if (part.tool === "todowrite") return todo(props(part)) + if (part.tool === "skill") return skill(props(part)) + return fallback(part) + } catch { + return fallback(part) + } } function emit(type: string, data: Record) { @@ -554,7 +586,7 @@ export const RunCommand = cmd({ } if (args.attach) { - const sdk = createOpencodeClient({ baseUrl: args.attach }) + const sdk = createOpencodeClient({ baseUrl: args.attach, directory }) return await execute(sdk) } diff --git a/packages/opencode/src/cli/cmd/session.ts b/packages/opencode/src/cli/cmd/session.ts index c6a1fd4138f2..1803f849522c 100644 --- a/packages/opencode/src/cli/cmd/session.ts +++ b/packages/opencode/src/cli/cmd/session.ts @@ -38,10 +38,34 @@ function pagerCmd(): string[] { export const SessionCommand = cmd({ command: "session", describe: "manage sessions", - builder: (yargs: Argv) => yargs.command(SessionListCommand).demandCommand(), + builder: (yargs: Argv) => yargs.command(SessionListCommand).command(SessionDeleteCommand).demandCommand(), async handler() {}, }) +export const SessionDeleteCommand = cmd({ + command: "delete ", + describe: "delete a session", + builder: (yargs: Argv) => { + return yargs.positional("sessionID", { + describe: "session ID to delete", + type: "string", + demandOption: true, + }) + }, + handler: async (args) => { + await bootstrap(process.cwd(), async () => { + try { + await Session.get(args.sessionID) + } catch { + UI.error(`Session not found: ${args.sessionID}`) + process.exit(1) + } + await Session.remove(args.sessionID) + UI.println(UI.Style.TEXT_SUCCESS_BOLD + `Session ${args.sessionID} deleted` + UI.Style.TEXT_NORMAL) + }) + }, +}) + export const SessionListCommand = cmd({ command: "list", describe: "list sessions", diff --git a/packages/opencode/src/cli/cmd/stats.ts b/packages/opencode/src/cli/cmd/stats.ts index 9239bb90a67a..04c1fe2ebc64 100644 --- a/packages/opencode/src/cli/cmd/stats.ts +++ b/packages/opencode/src/cli/cmd/stats.ts @@ -2,7 +2,8 @@ import type { Argv } from "yargs" import { cmd } from "./cmd" import { Session } from "../../session" import { bootstrap } from "../bootstrap" -import { Storage } from "../../storage/storage" +import { Database } from "../../storage/db" +import { SessionTable } from "../../session/session.sql" import { Project } from "../../project/project" import { Instance } from "../../project/instance" @@ -87,25 +88,8 @@ async function getCurrentProject(): Promise { } async function getAllSessions(): Promise { - const sessions: Session.Info[] = [] - - const projectKeys = await Storage.list(["project"]) - const projects = await Promise.all(projectKeys.map((key) => Storage.read(key))) - - for (const project of projects) { - if (!project) continue - - const sessionKeys = await Storage.list(["session", project.id]) - const projectSessions = await Promise.all(sessionKeys.map((key) => Storage.read(key))) - - for (const session of projectSessions) { - if (session) { - sessions.push(session) - } - } - } - - return sessions + const rows = Database.use((db) => db.select().from(SessionTable).all()) + return rows.map((row) => Session.fromRow(row)) } export async function aggregateSessionStats(days?: number, projectFilter?: string): Promise { diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index bd2f7b8b3c7a..e27909d915e1 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -2,9 +2,11 @@ import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentu // Register custom opentui components - must be imported before any component that uses import "opentui-spinner/solid" import { Clipboard } from "@tui/util/clipboard" -import { TextAttributes } from "@opentui/core" +import { Selection } from "@tui/util/selection" +import { MouseButton, TextAttributes } from "@opentui/core" import { RouteProvider, useRoute } from "@tui/context/route" import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js" +import { win32DisableProcessedInput, win32FlushInputBuffer, win32InstallCtrlCGuard } from "./win32" import { Installation } from "@/installation" import { Flag } from "@/flag/flag" import { DialogProvider, useDialog } from "@tui/ui/dialog" @@ -112,8 +114,17 @@ export function tui(input: { }) { // promise to prevent immediate exit return new Promise(async (resolve) => { + const unguard = win32InstallCtrlCGuard() + win32DisableProcessedInput() + const mode = await getTerminalBackgroundColor() + + // Re-clear after getTerminalBackgroundColor() — setRawMode(false) restores + // the original console mode which re-enables ENABLE_PROCESSED_INPUT. + win32DisableProcessedInput() + const onExit = async () => { + unguard?.() await input.onExit?.() resolve() } @@ -172,6 +183,7 @@ export function tui(input: { exitOnCtrlC: false, useKittyKeyboard: {}, autoFocus: false, + openConsoleOnError: false, consoleOptions: { keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }], onCopySelection: (text) => { @@ -201,6 +213,35 @@ function App() { const exit = useExit() const promptRef = usePromptRef() + useKeyboard((evt) => { + if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return + if (!renderer.getSelection()) return + + // Windows Terminal-like behavior: + // - Ctrl+C copies and dismisses selection + // - Esc dismisses selection + // - Most other key input dismisses selection and is passed through + if (evt.ctrl && evt.name === "c") { + if (!Selection.copy(renderer, toast)) { + renderer.clearSelection() + return + } + + evt.preventDefault() + evt.stopPropagation() + return + } + + if (evt.name === "escape") { + renderer.clearSelection() + evt.preventDefault() + evt.stopPropagation() + return + } + + renderer.clearSelection() + }) + // Wire up console copy-to-clipboard via opentui's onCopySelection callback renderer.console.onCopySelection = async (text: string) => { if (!text || text.length === 0) return @@ -208,6 +249,7 @@ function App() { await Clipboard.copy(text) .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) .catch(toast.error) + renderer.clearSelection() } const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true)) @@ -248,7 +290,8 @@ function App() { }) local.model.set({ providerID, modelID }, { recent: true }) } - if (args.sessionID) { + // Handle --session without --fork immediately (fork is handled in createEffect below) + if (args.sessionID && !args.fork) { route.navigate({ type: "session", sessionID: args.sessionID, @@ -266,10 +309,36 @@ function App() { .find((x) => x.parentID === undefined)?.id if (match) { continued = true - route.navigate({ type: "session", sessionID: match }) + if (args.fork) { + sdk.client.session.fork({ sessionID: match }).then((result) => { + if (result.data?.id) { + route.navigate({ type: "session", sessionID: result.data.id }) + } else { + toast.show({ message: "Failed to fork session", variant: "error" }) + } + }) + } else { + route.navigate({ type: "session", sessionID: match }) + } } }) + // Handle --session with --fork: wait for sync to be fully complete before forking + // (session list loads in non-blocking phase for --session, so we must wait for "complete" + // to avoid a race where reconcile overwrites the newly forked session) + let forked = false + createEffect(() => { + if (forked || sync.status !== "complete" || !args.sessionID || !args.fork) return + forked = true + sdk.client.session.fork({ sessionID: args.sessionID }).then((result) => { + if (result.data?.id) { + route.navigate({ type: "session", sessionID: result.data.id }) + } else { + toast.show({ message: "Failed to fork session", variant: "error" }) + } + }) + }) + createEffect( on( () => sync.status === "complete" && sync.data.provider.length === 0, @@ -663,19 +732,15 @@ function App() { width={dimensions().width} height={dimensions().height} backgroundColor={theme.background} - onMouseUp={async () => { - if (Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) { - renderer.clearSelection() - return - } - const text = renderer.getSelection()?.getSelectedText() - if (text && text.length > 0) { - await Clipboard.copy(text) - .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) - .catch(toast.error) - renderer.clearSelection() - } + onMouseDown={(evt) => { + if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return + if (evt.button !== MouseButton.RIGHT) return + + if (!Selection.copy(renderer, toast)) return + evt.preventDefault() + evt.stopPropagation() }} + onMouseUp={Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT ? undefined : () => Selection.copy(renderer, toast)} > @@ -701,7 +766,8 @@ function ErrorComponent(props: { const handleExit = async () => { renderer.setTerminalTitle("") renderer.destroy() - props.onExit() + win32FlushInputBuffer() + await props.onExit() } useKeyboard((evt) => { diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts index e852cb73d4cd..a2559cfce679 100644 --- a/packages/opencode/src/cli/cmd/tui/attach.ts +++ b/packages/opencode/src/cli/cmd/tui/attach.ts @@ -1,5 +1,7 @@ import { cmd } from "../cmd" +import { UI } from "@/cli/ui" import { tui } from "./app" +import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" export const AttachCommand = cmd({ command: "attach ", @@ -15,38 +17,64 @@ export const AttachCommand = cmd({ type: "string", description: "directory to run in", }) + .option("continue", { + alias: ["c"], + describe: "continue the last session", + type: "boolean", + }) .option("session", { alias: ["s"], type: "string", describe: "session id to continue", }) + .option("fork", { + type: "boolean", + describe: "fork the session when continuing (use with --continue or --session)", + }) .option("password", { alias: ["p"], type: "string", describe: "basic auth password (defaults to OPENCODE_SERVER_PASSWORD)", }), handler: async (args) => { - const directory = (() => { - if (!args.dir) return undefined - try { - process.chdir(args.dir) - return process.cwd() - } catch { - // If the directory doesn't exist locally (remote attach), pass it through. - return args.dir + const unguard = win32InstallCtrlCGuard() + try { + win32DisableProcessedInput() + + if (args.fork && !args.continue && !args.session) { + UI.error("--fork requires --continue or --session") + process.exitCode = 1 + return } - })() - const headers = (() => { - const password = args.password ?? process.env.OPENCODE_SERVER_PASSWORD - if (!password) return undefined - const auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` - return { Authorization: auth } - })() - await tui({ - url: args.url, - args: { sessionID: args.session }, - directory, - headers, - }) + + const directory = (() => { + if (!args.dir) return undefined + try { + process.chdir(args.dir) + return process.cwd() + } catch { + // If the directory doesn't exist locally (remote attach), pass it through. + return args.dir + } + })() + const headers = (() => { + const password = args.password ?? process.env.OPENCODE_SERVER_PASSWORD + if (!password) return undefined + const auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` + return { Authorization: auth } + })() + await tui({ + url: args.url, + args: { + continue: args.continue, + sessionID: args.session, + fork: args.fork, + }, + directory, + headers, + }) + } finally { + unguard?.() + } }, }) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index 4ad92eeb8395..c30b8d12a933 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -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>() 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 ( - [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()} /> ) } diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index f8be5577b354..9682bee4ead2 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -26,82 +26,67 @@ export function createDialogProviderOptions() { const sync = useSync() const dialog = useDialog() const sdk = useSDK() - const connected = createMemo(() => new Set(sync.data.provider_next.connected)) const options = createMemo(() => { return pipe( sync.data.provider_next.all, sortBy((x) => PROVIDER_PRIORITY[x.id] ?? 99), - map((provider) => { - const isConnected = connected().has(provider.id) - return { - title: provider.name, - value: provider.id, - description: { - opencode: "(Recommended)", - anthropic: "(Claude Max or API key)", - openai: "(ChatGPT Plus/Pro or API key)", - }[provider.id], - category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other", - footer: isConnected ? "Connected" : undefined, - async onSelect() { - const methods = sync.data.provider_auth[provider.id] ?? [ - { - type: "api", - label: "API key", - }, - ] - let index: number | null = 0 - if (methods.length > 1) { - index = await new Promise((resolve) => { - dialog.replace( - () => ( - ({ - title: x.label, - value: index, - }))} - onSelect={(option) => resolve(option.value)} - /> - ), - () => resolve(null), - ) - }) - } - if (index == null) return - const method = methods[index] - if (method.type === "oauth") { - const result = await sdk.client.provider.oauth.authorize({ - providerID: provider.id, - method: index, - }) - if (result.data?.method === "code") { - dialog.replace(() => ( - - )) - } - if (result.data?.method === "auto") { - dialog.replace(() => ( - ({ + title: provider.name, + value: provider.id, + description: { + opencode: "(Recommended)", + anthropic: "(Claude Max or API key)", + openai: "(ChatGPT Plus/Pro or API key)", + }[provider.id], + category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other", + async onSelect() { + const methods = sync.data.provider_auth[provider.id] ?? [ + { + type: "api", + label: "API key", + }, + ] + let index: number | null = 0 + if (methods.length > 1) { + index = await new Promise((resolve) => { + dialog.replace( + () => ( + ({ + title: x.label, + value: index, + }))} + onSelect={(option) => resolve(option.value)} /> - )) - } + ), + () => resolve(null), + ) + }) + } + if (index == null) return + const method = methods[index] + if (method.type === "oauth") { + const result = await sdk.client.provider.oauth.authorize({ + providerID: provider.id, + method: index, + }) + if (result.data?.method === "code") { + dialog.replace(() => ( + + )) } - if (method.type === "api") { - return dialog.replace(() => ) + if (result.data?.method === "auto") { + dialog.replace(() => ( + + )) } - }, - } - }), + } + if (method.type === "api") { + return dialog.replace(() => ) + } + }, + })), ) }) return options @@ -124,7 +109,6 @@ function AutoMethod(props: AutoMethodProps) { const dialog = useDialog() const sync = useSync() const toast = useToast() - const [hover, setHover] = createSignal(false) useKeyboard((evt) => { if (evt.name === "c" && !evt.ctrl && !evt.meta) { @@ -155,16 +139,9 @@ function AutoMethod(props: AutoMethodProps) { {props.title} - setHover(true)} - onMouseOut={() => setHover(false)} - onMouseUp={() => dialog.clear()} - > - esc - + dialog.clear()}> + esc + diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx index e2ab579a979c..3b6b5ef21827 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx @@ -1,9 +1,9 @@ import { TextAttributes } from "@opentui/core" +import { fileURLToPath } from "bun" import { useTheme } from "../context/theme" import { useDialog } from "@tui/ui/dialog" import { useSync } from "@tui/context/sync" -import { For, Match, Switch, Show, createMemo, createSignal } from "solid-js" -import { Installation } from "@/installation" +import { For, Match, Switch, Show, createMemo } from "solid-js" export type DialogStatusProps = {} @@ -11,7 +11,6 @@ export function DialogStatus() { const sync = useSync() const { theme } = useTheme() const dialog = useDialog() - const [hover, setHover] = createSignal(false) const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled)) @@ -19,7 +18,7 @@ export function DialogStatus() { const list = sync.data.config.plugin ?? [] const result = list.map((value) => { if (value.startsWith("file://")) { - const path = value.substring("file://".length) + const path = fileURLToPath(value) const parts = path.split("/") const filename = parts.pop() || path if (!filename.includes(".")) return { name: filename } @@ -46,18 +45,10 @@ export function DialogStatus() { Status - setHover(true)} - onMouseOut={() => setHover(false)} - onMouseUp={() => dialog.clear()} - > - esc - + dialog.clear()}> + esc + - OpenCode v{Installation.VERSION} 0} fallback={No MCP Servers}> {Object.keys(sync.data.mcp).length} MCP Servers diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 455fccb8c576..3240afab326a 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -1,4 +1,5 @@ import type { BoxRenderable, TextareaRenderable, KeyEvent, ScrollBoxRenderable } from "@opentui/core" +import { pathToFileURL } from "bun" import fuzzysort from "fuzzysort" import { firstBy } from "remeda" import { createMemo, createResource, createEffect, onMount, onCleanup, Index, Show, createSignal } from "solid-js" @@ -246,17 +247,18 @@ export function Autocomplete(props: { const width = props.anchor().width - 4 options.push( ...sortedFiles.map((item): AutocompleteOption => { - let url = `file://${process.cwd()}/${item}` + const baseDir = (sync.data.path.directory || process.cwd()).replace(/\/+$/, "") + const fullPath = `${baseDir}/${item}` + const urlObj = pathToFileURL(fullPath) let filename = item if (lineRange && !item.endsWith("/")) { filename = `${item}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}` - const urlObj = new URL(url) urlObj.searchParams.set("start", String(lineRange.startLine)) if (lineRange.endLine !== undefined) { urlObj.searchParams.set("end", String(lineRange.endLine)) } - url = urlObj.toString() } + const url = urlObj.href const isDir = item.endsWith("/") return { diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index fb3d665d340b..cefef208de4a 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1,5 +1,6 @@ import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, t, dim, fg } from "@opentui/core" -import { createEffect, createMemo, type JSX, onMount, createSignal, onCleanup, Show, Switch, Match } from "solid-js" +import { createEffect, createMemo, type JSX, onMount, createSignal, onCleanup, on, Show, Switch, Match } from "solid-js" +import "opentui-spinner/solid" import { useLocal } from "@tui/context/local" import { useTheme } from "@tui/context/theme" import { EmptyBorder } from "@tui/component/border" @@ -53,6 +54,7 @@ export type PromptRef = { } const PLACEHOLDERS = ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"] +const SHELL_PLACEHOLDERS = ["ls -la", "git status", "pwd"] export function Prompt(props: PromptProps) { let input: TextareaRenderable @@ -133,6 +135,16 @@ export function Prompt(props: PromptProps) { interrupt: 0, }) + createEffect( + on( + () => props.sessionID, + () => { + setStore("placeholder", Math.floor(Math.random() * PLACEHOLDERS.length)) + }, + { defer: true }, + ), + ) + // Initialize agent/model/variant from last user message when session changes let syncedSessionID: string | undefined createEffect(() => { @@ -735,6 +747,15 @@ export function Prompt(props: PromptProps) { return !!current }) + const placeholderText = createMemo(() => { + if (props.sessionID) return undefined + if (store.mode === "shell") { + const example = SHELL_PLACEHOLDERS[store.placeholder % SHELL_PLACEHOLDERS.length] + return `Run a command... "${example}"` + } + return `Ask anything... "${PLACEHOLDERS[store.placeholder % PLACEHOLDERS.length]}"` + }) + const spinnerDef = createMemo(() => { const color = local.agent.color(local.agent.current().name) return { @@ -796,7 +817,7 @@ export function Prompt(props: PromptProps) { flexGrow={1} >