diff --git a/.github/VOUCHED.td b/.github/VOUCHED.td index 412716a18dd8..4be318d51892 100644 --- a/.github/VOUCHED.td +++ b/.github/VOUCHED.td @@ -8,6 +8,7 @@ # - Denounce with minus prefix: -username or -platform:username. # - Optional details after a space following the handle. adamdotdevin +-agusbasari29 AI PR slop ariane-emory -florianleibert fwang diff --git a/.github/workflows/docs-locale-sync.yml b/.github/workflows/docs-locale-sync.yml index 8cd0cc52e2a2..1aafc5d1e3b1 100644 --- a/.github/workflows/docs-locale-sync.yml +++ b/.github/workflows/docs-locale-sync.yml @@ -12,13 +12,14 @@ jobs: if: github.actor != 'opencode-agent[bot]' runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: - id-token: write contents: write steps: - name: Checkout repository uses: actions/checkout@v4 with: + persist-credentials: false fetch-depth: 0 + ref: ${{ github.ref_name }} - name: Setup Bun uses: ./.github/actions/setup-bun @@ -51,9 +52,54 @@ jobs: uses: sst/opencode/github@latest env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + GITHUB_TOKEN: ${{ steps.committer.outputs.token }} + OPENCODE_CONFIG_CONTENT: | + { + "permission": { + "*": "deny", + "read": { + "*": "deny", + "packages/web/src/content/docs": "allow", + "packages/web/src/content/docs/*": "allow", + "packages/web/src/content/docs/*.mdx": "allow", + "packages/web/src/content/docs/*/*.mdx": "allow", + ".opencode": "allow", + ".opencode/agent": "allow", + ".opencode/agent/glossary": "allow", + ".opencode/agent/translator.md": "allow", + ".opencode/agent/glossary/*.md": "allow" + }, + "edit": { + "*": "deny", + "packages/web/src/content/docs/*/*.mdx": "allow" + }, + "glob": { + "*": "deny", + "packages/web/src/content/docs*": "allow", + ".opencode/agent/glossary*": "allow" + }, + "task": { + "*": "deny", + "translator": "allow" + } + }, + "agent": { + "translator": { + "permission": { + "*": "deny", + "read": { + "*": "deny", + ".opencode/agent/translator.md": "allow", + ".opencode/agent/glossary/*.md": "allow" + } + } + } + } + } with: - model: opencode/gpt-5.2 + model: opencode/gpt-5.3-codex agent: docs + use_github_token: true prompt: | Update localized docs to match the latest English docs changes. @@ -67,10 +113,11 @@ jobs: 2. You MUST use the Task tool for translation work and launch subagents with subagent_type `translator` (defined in .opencode/agent/translator.md). 3. Do not translate directly in the primary agent. Use translator subagent output as the source for locale text updates. 4. Run translator subagent Task calls in parallel whenever file/locale translation work is independent. - 5. Preserve frontmatter keys, internal links, code blocks, and existing locale-specific metadata unless the English change requires an update. - 6. Keep locale docs structure aligned with their corresponding English pages. - 7. Do not modify English source docs in packages/web/src/content/docs/*.mdx. - 8. If no locale updates are needed, make no changes. + 5. Use only the minimum tools needed for this task (read/glob, file edits, and translator Task). Do not use shell, web, search, or GitHub tools for translation work. + 6. Preserve frontmatter keys, internal links, code blocks, and existing locale-specific metadata unless the English change requires an update. + 7. Keep locale docs structure aligned with their corresponding English pages. + 8. Do not modify English source docs in packages/web/src/content/docs/*.mdx. + 9. If no locale updates are needed, make no changes. - name: Commit and push locale docs updates if: steps.changes.outputs.has_changes == 'true' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 431581f5966e..8d4c9038a7e4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -41,6 +41,13 @@ jobs: - uses: ./.github/actions/setup-bun + - name: Setup git committer + id: committer + uses: ./.github/actions/setup-git-committer + with: + opencode-app-id: ${{ vars.OPENCODE_APP_ID }} + opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} + - name: Install OpenCode if: inputs.bump || inputs.version run: bun i -g opencode-ai @@ -49,14 +56,16 @@ jobs: run: | ./script/version.ts env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ steps.committer.outputs.token }} OPENCODE_BUMP: ${{ inputs.bump }} OPENCODE_VERSION: ${{ inputs.version }} OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + GH_REPO: ${{ (github.ref_name == 'beta' && 'anomalyco/opencode-beta') || github.repository }} outputs: version: ${{ steps.version.outputs.version }} release: ${{ steps.version.outputs.release }} tag: ${{ steps.version.outputs.tag }} + repo: ${{ steps.version.outputs.repo }} build-cli: needs: version @@ -69,6 +78,13 @@ jobs: - uses: ./.github/actions/setup-bun + - name: Setup git committer + id: committer + uses: ./.github/actions/setup-git-committer + with: + opencode-app-id: ${{ vars.OPENCODE_APP_ID }} + opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} + - name: Build id: build run: | @@ -76,7 +92,8 @@ jobs: env: OPENCODE_VERSION: ${{ needs.version.outputs.version }} OPENCODE_RELEASE: ${{ needs.version.outputs.release }} - GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ needs.version.outputs.repo }} + GH_TOKEN: ${{ steps.committer.outputs.token }} - uses: actions/upload-artifact@v4 with: @@ -189,6 +206,13 @@ jobs: if: contains(matrix.settings.host, 'ubuntu') run: cargo tauri --version + - name: Setup git committer + id: committer + uses: ./.github/actions/setup-git-committer + with: + opencode-app-id: ${{ vars.OPENCODE_APP_ID }} + opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} + - name: Build and upload artifacts uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a timeout-minutes: 60 @@ -196,14 +220,16 @@ jobs: projectPath: packages/desktop uploadWorkflowArtifacts: true tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }} - args: --target ${{ matrix.settings.target }} --config ./src-tauri/tauri.prod.conf.json --verbose + args: --target ${{ matrix.settings.target }} --config ${{ (github.ref_name == 'beta' && './src-tauri/tauri.beta.conf.json') || './src-tauri/tauri.prod.conf.json' }} --verbose updaterJsonPreferNsis: true releaseId: ${{ needs.version.outputs.release }} tagName: ${{ needs.version.outputs.tag }} releaseDraft: true releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext] + repo: ${{ (github.ref_name == 'beta' && 'opencode-beta') || '' }} + releaseCommitish: ${{ github.sha }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.committer.outputs.token }} TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} @@ -280,4 +306,5 @@ jobs: OPENCODE_RELEASE: ${{ needs.version.outputs.release }} AUR_KEY: ${{ secrets.AUR_KEY }} GITHUB_TOKEN: ${{ steps.committer.outputs.token }} + GH_REPO: ${{ needs.version.outputs.repo }} NPM_CONFIG_PROVENANCE: false diff --git a/.opencode/agent/glossary/README.md b/.opencode/agent/glossary/README.md new file mode 100644 index 000000000000..983900381ca9 --- /dev/null +++ b/.opencode/agent/glossary/README.md @@ -0,0 +1,63 @@ +# Locale Glossaries + +Use this folder for locale-specific translation guidance that supplements `.opencode/agent/translator.md`. + +The global glossary in `translator.md` remains the source of truth for shared do-not-translate terms (commands, code, paths, product names, etc.). These locale files capture community learnings about phrasing and terminology preferences. + +## File Naming + +- One file per locale +- Use lowercase locale slugs that match docs locales when possible (for example, `zh-cn.md`, `zh-tw.md`) +- If only language-level guidance exists, use the language code (for example, `fr.md`) +- Some repo locale slugs may be aliases/non-BCP47 for consistency (for example, `br` for Brazilian Portuguese / `pt-BR`) + +## What To Put In A Locale File + +- **Sources**: PRs/issues/discussions that motivated the guidance +- **Do Not Translate (Locale Additions)**: locale-specific terms or casing decisions +- **Preferred Terms**: recurring UI/docs words with preferred translations +- **Guidance**: tone, style, and consistency notes +- **Avoid** (optional): common literal translations or wording we should avoid +- If the repo uses a locale alias slug, document the alias in **Guidance** (for example, prose may mention `pt-BR` while config/examples use `br`) + +Prefer guidance that is: + +- Repeated across multiple docs/screens +- Easy to apply consistently +- Backed by a community contribution or review discussion + +## Template + +```md +# Glossary + +## Sources + +- PR #12345: https://github.com/anomalyco/opencode/pull/12345 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing) + +## Preferred Terms + +| English | Preferred | Notes | +| ------- | --------- | --------- | +| prompt | ... | preferred | +| session | ... | preferred | + +## Guidance + +- Prefer natural phrasing over literal translation + +## Avoid + +- Avoid ... when ... +``` + +## Contribution Notes + +- Mark entries as preferred when they may evolve +- Keep examples short +- Add or update the `Sources` section whenever you add a new rule +- Prefer PR-backed guidance over invented term mappings; start with general guidance if no term-level corrections exist yet diff --git a/.opencode/agent/glossary/ar.md b/.opencode/agent/glossary/ar.md new file mode 100644 index 000000000000..37355522a0a5 --- /dev/null +++ b/.opencode/agent/glossary/ar.md @@ -0,0 +1,28 @@ +# ar Glossary + +## Sources + +- PR #9947: https://github.com/anomalyco/opencode/pull/9947 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +No PR-backed term mappings yet. Add entries here when review PRs introduce repeated wording corrections. + +## Guidance + +- Prefer natural Arabic phrasing over literal translation +- Keep tone clear and direct in UI labels and docs prose +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths +- For RTL text, treat code, commands, and paths as LTR artifacts and keep their character order unchanged + +## Avoid + +- Avoid translating product and protocol names that are fixed identifiers +- Avoid mixing multiple Arabic terms for the same recurring UI action once a preferred term is established diff --git a/.opencode/agent/glossary/br.md b/.opencode/agent/glossary/br.md new file mode 100644 index 000000000000..fd3e7251cd90 --- /dev/null +++ b/.opencode/agent/glossary/br.md @@ -0,0 +1,34 @@ +# br Glossary + +## Sources + +- PR #10086: https://github.com/anomalyco/opencode/pull/10086 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Locale code `br` in repo config, code, and paths (repo alias for Brazilian Portuguese) + +## Preferred Terms + +These are PR-backed locale naming preferences and may evolve. + +| English / Context | Preferred | Notes | +| ---------------------------------------- | ------------------------------ | ------------------------------------------------------------- | +| Brazilian Portuguese (prose locale name) | `pt-BR` | Use standard locale naming in prose when helpful | +| Repo locale slug (code/config) | `br` | PR #10086 uses `br` for consistency/simplicity | +| Browser locale detection | `pt`, `pt-br`, `pt-BR` -> `br` | Preserve this mapping in docs/examples about locale detection | + +## Guidance + +- This file covers Brazilian Portuguese (`pt-BR`), but the repo locale code is `br` +- Use natural Brazilian Portuguese phrasing over literal translation +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths +- Keep repo locale identifiers as implemented in code/config (`br`) even when prose mentions `pt-BR` + +## Avoid + +- Avoid changing repo locale code references from `br` to `pt-br` in code snippets, paths, or config examples +- Avoid mixing Portuguese variants when a Brazilian Portuguese form is established diff --git a/.opencode/agent/glossary/bs.md b/.opencode/agent/glossary/bs.md new file mode 100644 index 000000000000..aa3bd96f6f94 --- /dev/null +++ b/.opencode/agent/glossary/bs.md @@ -0,0 +1,33 @@ +# bs Glossary + +## Sources + +- PR #12283: https://github.com/anomalyco/opencode/pull/12283 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +These are PR-backed locale naming preferences and may evolve. + +| English / Context | Preferred | Notes | +| ---------------------------------- | ---------- | ------------------------------------------------- | +| Bosnian language label (UI) | `Bosanski` | PR #12283 tested switching language to `Bosanski` | +| Repo locale slug (code/config) | `bs` | Preserve in code, config, paths, and examples | +| Browser locale detection (Bosnian) | `bs` | PR #12283 added `bs` locale auto-detection | + +## Guidance + +- Use natural Bosnian phrasing over literal translation +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths +- Keep repo locale references as `bs` in code/config, and use `Bosanski` for the user-facing language name when applicable + +## Avoid + +- Avoid changing repo locale references from `bs` to another slug in code snippets or config examples +- Avoid translating product and protocol names that are fixed identifiers diff --git a/.opencode/agent/glossary/da.md b/.opencode/agent/glossary/da.md new file mode 100644 index 000000000000..e63222170109 --- /dev/null +++ b/.opencode/agent/glossary/da.md @@ -0,0 +1,27 @@ +# da Glossary + +## Sources + +- PR #9821: https://github.com/anomalyco/opencode/pull/9821 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +No PR-backed term mappings yet. Add entries here when review PRs introduce repeated wording corrections. + +## Guidance + +- Prefer natural Danish phrasing over literal translation +- Keep tone clear and direct in UI labels and docs prose +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths + +## Avoid + +- Avoid translating product and protocol names that are fixed identifiers +- Avoid mixing multiple Danish terms for the same recurring UI action once a preferred term is established diff --git a/.opencode/agent/glossary/de.md b/.opencode/agent/glossary/de.md new file mode 100644 index 000000000000..0d2c49faceae --- /dev/null +++ b/.opencode/agent/glossary/de.md @@ -0,0 +1,27 @@ +# de Glossary + +## Sources + +- PR #9817: https://github.com/anomalyco/opencode/pull/9817 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +No PR-backed term mappings yet. Add entries here when review PRs introduce repeated wording corrections. + +## Guidance + +- Prefer natural German phrasing over literal translation +- Keep tone clear and direct in UI labels and docs prose +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths + +## Avoid + +- Avoid translating product and protocol names that are fixed identifiers +- Avoid mixing multiple German terms for the same recurring UI action once a preferred term is established diff --git a/.opencode/agent/glossary/es.md b/.opencode/agent/glossary/es.md new file mode 100644 index 000000000000..dc9b977ecffa --- /dev/null +++ b/.opencode/agent/glossary/es.md @@ -0,0 +1,27 @@ +# es Glossary + +## Sources + +- PR #9817: https://github.com/anomalyco/opencode/pull/9817 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +No PR-backed term mappings yet. Add entries here when review PRs introduce repeated wording corrections. + +## Guidance + +- Prefer natural Spanish phrasing over literal translation +- Keep tone clear and direct in UI labels and docs prose +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths + +## Avoid + +- Avoid translating product and protocol names that are fixed identifiers +- Avoid mixing multiple Spanish terms for the same recurring UI action once a preferred term is established diff --git a/.opencode/agent/glossary/fr.md b/.opencode/agent/glossary/fr.md new file mode 100644 index 000000000000..074c4de110a0 --- /dev/null +++ b/.opencode/agent/glossary/fr.md @@ -0,0 +1,27 @@ +# fr Glossary + +## Sources + +- PR #9821: https://github.com/anomalyco/opencode/pull/9821 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +No PR-backed term mappings yet. Add entries here when review PRs introduce repeated wording corrections. + +## Guidance + +- Prefer natural French phrasing over literal translation +- Keep tone clear and direct in UI labels and docs prose +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths + +## Avoid + +- Avoid translating product and protocol names that are fixed identifiers +- Avoid mixing multiple French terms for the same recurring UI action once a preferred term is established diff --git a/.opencode/agent/glossary/ja.md b/.opencode/agent/glossary/ja.md new file mode 100644 index 000000000000..f0159ca96690 --- /dev/null +++ b/.opencode/agent/glossary/ja.md @@ -0,0 +1,33 @@ +# ja Glossary + +## Sources + +- PR #9821: https://github.com/anomalyco/opencode/pull/9821 +- PR #13160: https://github.com/anomalyco/opencode/pull/13160 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +These are PR-backed wording preferences and may evolve. + +| English / Context | Preferred | Notes | +| --------------------------- | ----------------------- | ------------------------------------- | +| WSL integration (UI label) | `WSL連携` | PR #13160 prefers this over `WSL統合` | +| WSL integration description | `WindowsのWSL環境で...` | PR #13160 improved phrasing naturally | + +## Guidance + +- Prefer natural Japanese phrasing over literal translation +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths +- In WSL integration text, follow PR #13160 wording direction for more natural Japanese phrasing + +## Avoid + +- Avoid `WSL統合` in the WSL integration UI context where `WSL連携` is the reviewed wording +- Avoid translating product and protocol names that are fixed identifiers diff --git a/.opencode/agent/glossary/ko.md b/.opencode/agent/glossary/ko.md new file mode 100644 index 000000000000..71385c8a10ac --- /dev/null +++ b/.opencode/agent/glossary/ko.md @@ -0,0 +1,27 @@ +# ko Glossary + +## Sources + +- PR #9817: https://github.com/anomalyco/opencode/pull/9817 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +No PR-backed term mappings yet. Add entries here when review PRs introduce repeated wording corrections. + +## Guidance + +- Prefer natural Korean phrasing over literal translation +- Keep tone clear and direct in UI labels and docs prose +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths + +## Avoid + +- Avoid translating product and protocol names that are fixed identifiers +- Avoid mixing multiple Korean terms for the same recurring UI action once a preferred term is established diff --git a/.opencode/agent/glossary/no.md b/.opencode/agent/glossary/no.md new file mode 100644 index 000000000000..d7159dca4107 --- /dev/null +++ b/.opencode/agent/glossary/no.md @@ -0,0 +1,38 @@ +# no Glossary + +## Sources + +- PR #10018: https://github.com/anomalyco/opencode/pull/10018 +- PR #12935: https://github.com/anomalyco/opencode/pull/12935 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Sound names (PR #10018 notes these were intentionally left untranslated) + +## Preferred Terms + +These are PR-backed corrections and may evolve. + +| English / Context | Preferred | Notes | +| ----------------------------------- | ------------ | ----------------------------- | +| Save (data persistence action) | `Lagre` | Prefer over `Spare` | +| Disabled (feature/state) | `deaktivert` | Prefer over `funksjonshemmet` | +| API keys | `API Nøkler` | Prefer over `API Taster` | +| Cost (noun) | `Kostnad` | Prefer over verb form `Koste` | +| Show/View (imperative button label) | `Vis` | Prefer over `Utsikt` | + +## Guidance + +- Prefer natural Norwegian Bokmal (Bokmål) wording over literal translation +- Keep tone clear and practical in UI labels +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths +- Keep recurring UI terms consistent once a preferred term is chosen + +## Avoid + +- Avoid `Spare` for save actions in persistence contexts +- Avoid `funksjonshemmet` for disabled feature states +- Avoid `API Taster`, `Koste`, and `Utsikt` in the corrected contexts above diff --git a/.opencode/agent/glossary/pl.md b/.opencode/agent/glossary/pl.md new file mode 100644 index 000000000000..e9bad7a51567 --- /dev/null +++ b/.opencode/agent/glossary/pl.md @@ -0,0 +1,27 @@ +# pl Glossary + +## Sources + +- PR #9884: https://github.com/anomalyco/opencode/pull/9884 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +No PR-backed term mappings yet. Add entries here when review PRs introduce repeated wording corrections. + +## Guidance + +- Prefer natural Polish phrasing over literal translation +- Keep tone clear and direct in UI labels and docs prose +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths + +## Avoid + +- Avoid translating product and protocol names that are fixed identifiers +- Avoid mixing multiple Polish terms for the same recurring UI action once a preferred term is established diff --git a/.opencode/agent/glossary/ru.md b/.opencode/agent/glossary/ru.md new file mode 100644 index 000000000000..6fee0f94c06f --- /dev/null +++ b/.opencode/agent/glossary/ru.md @@ -0,0 +1,27 @@ +# ru Glossary + +## Sources + +- PR #9882: https://github.com/anomalyco/opencode/pull/9882 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +No PR-backed term mappings yet. Add entries here when review PRs introduce repeated wording corrections. + +## Guidance + +- Prefer natural Russian phrasing over literal translation +- Keep tone clear and direct in UI labels and docs prose +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths + +## Avoid + +- Avoid translating product and protocol names that are fixed identifiers +- Avoid mixing multiple Russian terms for the same recurring UI action once a preferred term is established diff --git a/.opencode/agent/glossary/th.md b/.opencode/agent/glossary/th.md new file mode 100644 index 000000000000..7b5a31d16bfc --- /dev/null +++ b/.opencode/agent/glossary/th.md @@ -0,0 +1,34 @@ +# th Glossary + +## Sources + +- PR #10809: https://github.com/anomalyco/opencode/pull/10809 +- PR #11496: https://github.com/anomalyco/opencode/pull/11496 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +These are PR-backed preferences and may evolve. + +| English / Context | Preferred | Notes | +| ------------------------------------- | --------------------- | -------------------------------------------------------------------------------- | +| Thai language label in language lists | `ไทย` | PR #10809 standardized this across locales | +| Language names in language pickers | Native names (static) | PR #11496: keep names like `English`, `Deutsch`, `ไทย` consistent across locales | + +## Guidance + +- Prefer natural Thai phrasing over literal translation +- Keep tone short and clear for buttons and labels +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths +- Keep language names static/native in language pickers instead of translating them per current locale (PR #11496) + +## Avoid + +- Avoid translating language names differently per current locale in language lists +- Avoid changing `ไทย` to another display form for the Thai language option unless the product standard changes diff --git a/.opencode/agent/glossary/zh-cn.md b/.opencode/agent/glossary/zh-cn.md new file mode 100644 index 000000000000..054e94b7e83a --- /dev/null +++ b/.opencode/agent/glossary/zh-cn.md @@ -0,0 +1,42 @@ +# zh-cn Glossary + +## Sources + +- PR #13942: https://github.com/anomalyco/opencode/pull/13942 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only when it is part of commands, package names, paths, or code) +- `OpenCode Zen` +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- `Model Context Protocol` (prefer the English expansion when introducing `MCP`) + +## Preferred Terms + +These are preferred terms for docs/UI prose and may evolve. + +| English | Preferred | Notes | +| ----------------------- | --------- | ------------------------------------------- | +| prompt | 提示词 | Keep `--prompt` unchanged in flags/code | +| session | 会话 | | +| provider | 提供商 | | +| share link / shared URL | 分享链接 | Prefer `分享` for user-facing share actions | +| headless (server) | 无界面 | Docs wording | +| authentication | 认证 | Prefer in auth/OAuth contexts | +| cache | 缓存 | | +| keybind / shortcut | 快捷键 | User-facing docs wording | +| workflow | 工作流 | e.g. GitHub Actions workflow | + +## Guidance + +- Prefer natural, concise phrasing over literal translation +- Keep the tone direct and friendly (PR #13942 consistently moved wording in this direction) +- Preserve technical artifacts exactly: commands, flags, code, inline code, URLs, file paths, model IDs +- Keep enum-like values in English when they are literals (for example, `default`, `json`) +- Prefer consistent terminology across pages once a term is chosen (`会话`, `提供商`, `提示词`, etc.) + +## Avoid + +- Avoid `opencode` in prose when referring to the product name; use `OpenCode` +- Avoid mixing alternative terms for the same concept across docs when a preferred term is already established diff --git a/.opencode/agent/glossary/zh-tw.md b/.opencode/agent/glossary/zh-tw.md new file mode 100644 index 000000000000..283660e12198 --- /dev/null +++ b/.opencode/agent/glossary/zh-tw.md @@ -0,0 +1,42 @@ +# zh-tw Glossary + +## Sources + +- PR #13942: https://github.com/anomalyco/opencode/pull/13942 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only when it is part of commands, package names, paths, or code) +- `OpenCode Zen` +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- `Model Context Protocol` (prefer the English expansion when introducing `MCP`) + +## Preferred Terms + +These are preferred terms for docs/UI prose and may evolve. + +| English | Preferred | Notes | +| ----------------------- | --------- | ------------------------------------------- | +| prompt | 提示詞 | Keep `--prompt` unchanged in flags/code | +| session | 工作階段 | | +| provider | 供應商 | | +| share link / shared URL | 分享連結 | Prefer `分享` for user-facing share actions | +| headless (server) | 無介面 | Docs wording | +| authentication | 認證 | Prefer in auth/OAuth contexts | +| cache | 快取 | | +| keybind / shortcut | 快捷鍵 | User-facing docs wording | +| workflow | 工作流程 | e.g. GitHub Actions workflow | + +## Guidance + +- Prefer natural, concise phrasing over literal translation +- Keep the tone direct and friendly (PR #13942 consistently moved wording in this direction) +- Preserve technical artifacts exactly: commands, flags, code, inline code, URLs, file paths, model IDs +- Keep enum-like values in English when they are literals (for example, `default`, `json`) +- Prefer consistent terminology across pages once a term is chosen (`工作階段`, `供應商`, `提示詞`, etc.) + +## Avoid + +- Avoid `opencode` in prose when referring to the product name; use `OpenCode` +- Avoid mixing alternative terms for the same concept across docs when a preferred term is already established diff --git a/.opencode/agent/translator.md b/.opencode/agent/translator.md index 7886cf5f395e..f0b3f8e9270b 100644 --- a/.opencode/agent/translator.md +++ b/.opencode/agent/translator.md @@ -1,7 +1,7 @@ --- description: Translate content for a specified locale while preserving technical terms mode: subagent -model: opencode/gemini-3-pro +model: opencode/gemini-3.1-pro --- You are a professional translator and localization specialist. @@ -13,10 +13,25 @@ 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. +- Also apply locale-specific guidance from `.opencode/agent/glossary/.md` when available (for example, `zh-cn.md`). - Do not modify fenced code blocks. - Output ONLY the translation (no commentary). If the target locale is missing, ask the user to provide it. +If no locale-specific glossary exists, use the global glossary only. + +--- + +# Locale-Specific Glossaries + +When a locale glossary exists, use it to: + +- Apply preferred wording for recurring UI/docs terms in that locale +- Preserve locale-specific do-not-translate terms and casing decisions +- Prefer natural phrasing over literal translation when the locale file calls it out +- If the repo uses a locale alias slug, apply that file too (for example, `pt-BR` maps to `br.md` in this repo) + +Locale guidance does not override code/command preservation rules or the global Do-Not-Translate glossary below. --- diff --git a/.opencode/tool/github-triage.ts b/.opencode/tool/github-triage.ts index ed80f49d5413..8ad0212ad074 100644 --- a/.opencode/tool/github-triage.ts +++ b/.opencode/tool/github-triage.ts @@ -5,8 +5,16 @@ 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"], + tui: [ + "thdxr", + "kommander", + // "rekram1-node" (on vacation) + ], + core: [ + "thdxr", + // "rekram1-node", (on vacation) + "jlongster", + ], docs: ["R44VC0RP"], windows: ["Hona"], } as const @@ -42,10 +50,7 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) { export default tool({ description: DESCRIPTION, args: { - assignee: tool.schema - .enum(ASSIGNEES as [string, ...string[]]) - .describe("The username of the assignee") - .default("rekram1-node"), + assignee: tool.schema.enum(ASSIGNEES as [string, ...string[]]).describe("The username of the assignee"), labels: tool.schema .array(tool.schema.enum(["nix", "opentui", "perf", "web", "desktop", "zen", "docs", "windows", "core"])) .describe("The labels(s) to add to the issue") @@ -68,7 +73,8 @@ export default tool({ results.push("Dropped label: nix (issue does not mention nix)") } - const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee + // const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee + const assignee = web ? pick(TEAM.desktop) : args.assignee if (labels.includes("zen") && !zen) { throw new Error("Only add the zen label when issue title/body contains 'zen'") diff --git a/.opencode/tool/github-triage.txt b/.opencode/tool/github-triage.txt index 4369ed23512f..1a2d69bdb5b9 100644 --- a/.opencode/tool/github-triage.txt +++ b/.opencode/tool/github-triage.txt @@ -4,3 +4,5 @@ Choose labels and assignee using the current triage policy and ownership rules. Pick the most fitting labels for the issue and assign one owner. If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random. + +(Note: rekram1-node is on vacation, do not assign issues to him.) diff --git a/README.ar.md b/README.ar.md index f24e598d5eb9..aeb2f04b72c1 100644 --- a/README.ar.md +++ b/README.ar.md @@ -32,7 +32,8 @@ Português (Brasil) | ไทย | Türkçe | - Українська + Українська | + বাংলা

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.bn.md b/README.bn.md new file mode 100644 index 000000000000..a3ffdc0d80e9 --- /dev/null +++ b/README.bn.md @@ -0,0 +1,139 @@ +

+ + + + + OpenCode logo + + +

+

ওপেন সোর্স এআই কোডিং এজেন্ট।

+

+ 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) + +--- + +### ইনস্টলেশন (Installation) + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Package managers +npm i -g opencode-ai@latest # or bun/pnpm/yarn +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) +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 +``` + +> [!TIP] +> ইনস্টল করার আগে ০.১.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`, or AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### ইনস্টলেশন ডিরেক্টরি (Installation Directory) + +ইনস্টল স্ক্রিপ্টটি ইনস্টলেশন পাতের জন্য নিম্নলিখিত অগ্রাধিকার ক্রম মেনে চলে: + +1. `$OPENCODE_INSTALL_DIR` - কাস্টম ইনস্টলেশন ডিরেক্টরি +2. `$XDG_BIN_DIR` - XDG বেস ডিরেক্টরি স্পেসিফিকেশন সমর্থিত পাথ +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 +``` + +### এজেন্টস (Agents) + +OpenCode এ দুটি বিল্ট-ইন এজেন্ট রয়েছে যা আপনি `Tab` কি(key) দিয়ে পরিবর্তন করতে পারবেন। + +- **build** - ডিফল্ট, ডেভেলপমেন্টের কাজের জন্য সম্পূর্ণ অ্যাক্সেসযুক্ত এজেন্ট +- **plan** - বিশ্লেষণ এবং কোড এক্সপ্লোরেশনের জন্য রিড-ওনলি এজেন্ট + - ডিফল্টভাবে ফাইল এডিট করতে দেয় না + - ব্যাশ কমান্ড চালানোর আগে অনুমতি চায় + - অপরিচিত কোডবেস এক্সপ্লোর করা বা পরিবর্তনের পরিকল্পনা করার জন্য আদর্শ + +এছাড়াও জটিল অনুসন্ধান এবং মাল্টিস্টেপ টাস্কের জন্য একটি **general** সাবএজেন্ট অন্তর্ভুক্ত রয়েছে। +এটি অভ্যন্তরীণভাবে ব্যবহৃত হয় এবং মেসেজে `@general` লিখে ব্যবহার করা যেতে পারে। + +এজেন্টদের সম্পর্কে আরও জানুন: [docs](https://opencode.ai/docs/agents)। + +### ডকুমেন্টেশন (Documentation) + +কিভাবে OpenCode কনফিগার করবেন সে সম্পর্কে আরও তথ্যের জন্য, [**আমাদের ডকস দেখুন**](https://opencode.ai/docs)। + +### অবদান (Contributing) + +আপনি যদি OpenCode এ অবদান রাখতে চান, অনুগ্রহ করে একটি পুল রিকোয়েস্ট সাবমিট করার আগে আমাদের [কন্ট্রিবিউটিং ডকস](./CONTRIBUTING.md) পড়ে নিন। + +### OpenCode এর উপর বিল্ডিং (Building on OpenCode) + +আপনি যদি এমন প্রজেক্টে কাজ করেন যা OpenCode এর সাথে সম্পর্কিত এবং প্রজেক্টের নামের অংশ হিসেবে "opencode" ব্যবহার করেন, উদাহরণস্বরূপ "opencode-dashboard" বা "opencode-mobile", তবে দয়া করে আপনার README তে একটি নোট যোগ করে স্পষ্ট করুন যে এই প্রজেক্টটি OpenCode দল দ্বারা তৈরি হয়নি এবং আমাদের সাথে এর কোনো সরাসরি সম্পর্ক নেই। + +### সচরাচর জিজ্ঞাসিত প্রশ্নাবলী (FAQ) + +#### এটি ক্লড কোড (Claude Code) থেকে কীভাবে আলাদা? + +ক্যাপাবিলিটির দিক থেকে এটি ক্লড কোডের (Claude Code) মতই। এখানে মূল পার্থক্যগুলো দেওয়া হলো: + +- ১০০% ওপেন সোর্স +- কোনো প্রোভাইডারের সাথে আবদ্ধ নয়। যদিও আমরা [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.br.md b/README.br.md index 4802c4996f63..6044dad6c0a0 100644 --- a/README.br.md +++ b/README.br.md @@ -32,7 +32,8 @@ Português (Brasil) | ไทย | Türkçe | - Українська + Українська | + বাংলা

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.bs.md b/README.bs.md index 9ad6852018c0..ef54a8369573 100644 --- a/README.bs.md +++ b/README.bs.md @@ -33,7 +33,8 @@ Português (Brasil) | ไทย | Türkçe | - Українська + Українська | + বাংলা

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.da.md b/README.da.md index 4b1302dbc3c2..3ffbbe820291 100644 --- a/README.da.md +++ b/README.da.md @@ -32,7 +32,8 @@ Português (Brasil) | ไทย | Türkçe | - Українська + Українська | + বাংলা

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.de.md b/README.de.md index 16116dc72f23..64c6628b45ee 100644 --- a/README.de.md +++ b/README.de.md @@ -32,7 +32,8 @@ Português (Brasil) | ไทย | Türkçe | - Українська + Українська | + বাংলা

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.es.md b/README.es.md index 5c18ff4aca7c..875c8b0832a1 100644 --- a/README.es.md +++ b/README.es.md @@ -32,7 +32,8 @@ Português (Brasil) | ไทย | Türkçe | - Українська + Українська | + বাংলা

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.fr.md b/README.fr.md index 0382164bedc5..254d38577b19 100644 --- a/README.fr.md +++ b/README.fr.md @@ -32,7 +32,8 @@ Português (Brasil) | ไทย | Türkçe | - Українська + Українська | + বাংলা

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.it.md b/README.it.md index c966ccec4916..b1f75c2d2c7c 100644 --- a/README.it.md +++ b/README.it.md @@ -32,7 +32,8 @@ Português (Brasil) | ไทย | Türkçe | - Українська + Українська | + বাংলা

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.ja.md b/README.ja.md index 11109e7eb408..31d11dcf1a1d 100644 --- a/README.ja.md +++ b/README.ja.md @@ -32,7 +32,8 @@ Português (Brasil) | ไทย | Türkçe | - Українська + Українська | + বাংলা

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.ko.md b/README.ko.md index 23fea76b1ebd..5f9de2cf3f73 100644 --- a/README.ko.md +++ b/README.ko.md @@ -32,7 +32,8 @@ Português (Brasil) | ไทย | Türkçe | - Українська + Українська | + বাংলা

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.md b/README.md index 99b4b2c50ff9..620415c96173 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,8 @@ Português (Brasil) | ไทย | Türkçe | - Українська + Українська | + বাংলা

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.no.md b/README.no.md index 9b9e90dc3850..125e18125746 100644 --- a/README.no.md +++ b/README.no.md @@ -32,7 +32,8 @@ Português (Brasil) | ไทย | Türkçe | - Українська + Українська | + বাংলা

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.pl.md b/README.pl.md index fced98dfc3a1..61ef5870c135 100644 --- a/README.pl.md +++ b/README.pl.md @@ -32,7 +32,8 @@ Português (Brasil) | ไทย | Türkçe | - Українська + Українська | + বাংলা

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.ru.md b/README.ru.md index a7c590c16b7c..fed1101c0ec3 100644 --- a/README.ru.md +++ b/README.ru.md @@ -32,7 +32,8 @@ Português (Brasil) | ไทย | Türkçe | - Українська + Українська | + বাংলা

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.th.md b/README.th.md index 0999167f239c..01d68dd8dcc5 100644 --- a/README.th.md +++ b/README.th.md @@ -32,7 +32,8 @@ Português (Brasil) | ไทย | Türkçe | - Українська + Українська | + বাংলা

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.tr.md b/README.tr.md index 67f84e4ddbce..bfb18e1b43b2 100644 --- a/README.tr.md +++ b/README.tr.md @@ -32,7 +32,8 @@ Português (Brasil) | ไทย | Türkçe | - Українська + Українська | + বাংলা

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.uk.md b/README.uk.md index 77e859a45d73..ed20fbf23686 100644 --- a/README.uk.md +++ b/README.uk.md @@ -33,7 +33,8 @@ Português (Brasil) | ไทย | Türkçe | - Українська + Українська | + বাংলা

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.zh.md b/README.zh.md index 113d476b2ed3..c6d1c1d11f0f 100644 --- a/README.zh.md +++ b/README.zh.md @@ -32,7 +32,8 @@ Português (Brasil) | ไทย | Türkçe | - Українська + Українська | + বাংলা

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/README.zht.md b/README.zht.md index b5181044438d..0dd44a9f0fc3 100644 --- a/README.zht.md +++ b/README.zht.md @@ -32,7 +32,8 @@ Português (Brasil) | ไทย | Türkçe | - Українська + Українська | + বাংলা

[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) diff --git a/bun.lock b/bun.lock index 2240f3055889..04da112cf791 100644 --- a/bun.lock +++ b/bun.lock @@ -25,7 +25,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.2.9", + "version": "1.2.10", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -75,7 +75,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.2.9", + "version": "1.2.10", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -109,7 +109,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.2.9", + "version": "1.2.10", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -136,7 +136,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.2.9", + "version": "1.2.10", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -160,7 +160,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.2.9", + "version": "1.2.10", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -184,7 +184,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.2.9", + "version": "1.2.10", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -217,7 +217,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.2.9", + "version": "1.2.10", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -246,7 +246,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.2.9", + "version": "1.2.10", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -262,7 +262,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.2.9", + "version": "1.2.10", "bin": { "opencode": "./bin/opencode", }, @@ -376,7 +376,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.2.9", + "version": "1.2.10", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -396,7 +396,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.2.9", + "version": "1.2.10", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -407,7 +407,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.2.9", + "version": "1.2.10", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -420,7 +420,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.2.9", + "version": "1.2.10", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -462,7 +462,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.2.9", + "version": "1.2.10", "dependencies": { "zod": "catalog:", }, @@ -473,7 +473,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.2.9", + "version": "1.2.10", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/github/action.yml b/github/action.yml index 8652bb8c1517..3d983a160995 100644 --- a/github/action.yml +++ b/github/action.yml @@ -30,6 +30,10 @@ inputs: description: "Comma-separated list of trigger phrases (case-insensitive). Defaults to '/opencode,/oc'" required: false + variant: + description: "Model variant for provider-specific reasoning effort (e.g., high, max, minimal)" + required: false + oidc_base_url: description: "Base URL for OIDC token exchange API. Only required when running a custom GitHub App install. Defaults to https://api.opencode.ai" required: false @@ -71,4 +75,5 @@ runs: PROMPT: ${{ inputs.prompt }} USE_GITHUB_TOKEN: ${{ inputs.use_github_token }} MENTIONS: ${{ inputs.mentions }} + VARIANT: ${{ inputs.variant }} OIDC_BASE_URL: ${{ inputs.oidc_base_url }} diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts index d42c0fcebb9e..a7ccba61752b 100644 --- a/packages/app/e2e/actions.ts +++ b/packages/app/e2e/actions.ts @@ -225,7 +225,7 @@ export async function hoverSessionItem(page: Page, sessionID: string) { export async function openSessionMoreMenu(page: Page, sessionID: string) { await expect(page).toHaveURL(new RegExp(`/session/${sessionID}(?:[/?#]|$)`)) - const scroller = page.locator(".session-scroller").first() + const scroller = page.locator(".scroll-view__viewport").first() await expect(scroller).toBeVisible() await expect(scroller.getByRole("heading", { level: 1 }).first()).toBeVisible({ timeout: 30_000 }) diff --git a/packages/app/e2e/selectors.ts b/packages/app/e2e/selectors.ts index be0bc057176a..5fad2c06b528 100644 --- a/packages/app/e2e/selectors.ts +++ b/packages/app/e2e/selectors.ts @@ -20,11 +20,8 @@ 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"]' diff --git a/packages/app/e2e/session/session.spec.ts b/packages/app/e2e/session/session.spec.ts index 93eaee5cb0bf..68d992949964 100644 --- a/packages/app/e2e/session/session.spec.ts +++ b/packages/app/e2e/session/session.spec.ts @@ -44,7 +44,7 @@ test("session can be renamed via header menu", async ({ page, sdk, gotoSession } const menu = await openSessionMoreMenu(page, session.id) await clickMenuItem(menu, /rename/i) - const input = page.locator(".session-scroller").locator(inlineInputSelector).first() + const input = page.locator(".scroll-view__viewport").locator(inlineInputSelector).first() await expect(input).toBeVisible() await expect(input).toBeFocused() await input.fill(renamedTitle) diff --git a/packages/app/e2e/settings/settings.spec.ts b/packages/app/e2e/settings/settings.spec.ts index 9fbcf79f5ee7..c2a8522eb051 100644 --- a/packages/app/e2e/settings/settings.spec.ts +++ b/packages/app/e2e/settings/settings.spec.ts @@ -9,7 +9,6 @@ import { settingsNotificationsPermissionsSelector, settingsReleaseNotesSelector, settingsSoundsAgentSelector, - settingsSoundsAgentEnabledSelector, settingsSoundsErrorsSelector, settingsSoundsPermissionsSelector, settingsThemeSelector, @@ -336,21 +335,19 @@ 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 }) => { +test("selecting none disables agent sound", 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() + await trigger.click() + const items = page.locator('[data-slot="select-select-item"]') + await expect(items.first()).toBeVisible() + await items.first().click() const stored = await page.evaluate((key) => { const raw = localStorage.getItem(key) diff --git a/packages/app/e2e/terminal/terminal-init.spec.ts b/packages/app/e2e/terminal/terminal-init.spec.ts index 87934b66e381..18991bf76364 100644 --- a/packages/app/e2e/terminal/terminal-init.spec.ts +++ b/packages/app/e2e/terminal/terminal-init.spec.ts @@ -6,6 +6,7 @@ test("smoke terminal mounts and can create a second tab", async ({ page, gotoSes await gotoSession() const terminals = page.locator(terminalSelector) + const tabs = page.locator('#terminal-panel [data-slot="tabs-trigger"]') const opened = await terminals.first().isVisible() if (!opened) { @@ -21,6 +22,7 @@ test("smoke terminal mounts and can create a second tab", async ({ page, gotoSes await page.locator(promptSelector).click() await page.keyboard.press("Control+Alt+T") - await expect(terminals).toHaveCount(2) - await expect(terminals.nth(1).locator("textarea")).toHaveCount(1) + await expect(tabs).toHaveCount(2) + await expect(terminals).toHaveCount(1) + await expect(terminals.first().locator("textarea")).toHaveCount(1) }) diff --git a/packages/app/package.json b/packages/app/package.json index 385205a0c191..b9397b0f40de 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.2.9", + "version": "1.2.10", "description": "", "type": "module", "exports": { diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index b1c608ffcc99..adfd592f8d01 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -89,6 +89,8 @@ const EXAMPLES = [ "prompt.example.25", ] as const +const NON_EMPTY_TEXT = /[^\s\u200B]/ + export const PromptInput: Component = (props) => { const sdk = useSDK() const sync = useSync() @@ -636,7 +638,9 @@ export const PromptInput: Component = (props) => { let buffer = "" const flushText = () => { - const content = buffer.replace(/\r\n?/g, "\n").replace(/\u200B/g, "") + let content = buffer + if (content.includes("\r")) content = content.replace(/\r\n?/g, "\n") + if (content.includes("\u200B")) content = content.replace(/\u200B/g, "") buffer = "" if (!content) return parts.push({ type: "text", content, start: position, end: position + content.length }) @@ -714,10 +718,12 @@ export const PromptInput: Component = (props) => { const rawParts = parseFromDOM() const images = imageAttachments() const cursorPosition = getCursorPosition(editorRef) - const rawText = rawParts.map((p) => ("content" in p ? p.content : "")).join("") - const trimmed = rawText.replace(/\u200B/g, "").trim() + const rawText = + rawParts.length === 1 && rawParts[0]?.type === "text" + ? rawParts[0].content + : rawParts.map((p) => ("content" in p ? p.content : "")).join("") const hasNonText = rawParts.some((part) => part.type !== "text") - const shouldReset = trimmed.length === 0 && !hasNonText && images.length === 0 + const shouldReset = !NON_EMPTY_TEXT.test(rawText) && !hasNonText && images.length === 0 if (shouldReset) { closePopover() @@ -757,19 +763,31 @@ export const PromptInput: Component = (props) => { } const addPart = (part: ContentPart) => { + if (part.type === "image") return false + const selection = window.getSelection() - if (!selection || selection.rangeCount === 0) return + if (!selection) return false - const cursorPosition = getCursorPosition(editorRef) - const currentPrompt = prompt.current() - const rawText = currentPrompt.map((p) => ("content" in p ? p.content : "")).join("") - const textBeforeCursor = rawText.substring(0, cursorPosition) - const atMatch = textBeforeCursor.match(/@(\S*)$/) + if (selection.rangeCount === 0 || !editorRef.contains(selection.anchorNode)) { + editorRef.focus() + const cursor = prompt.cursor() ?? promptLength(prompt.current()) + setCursorPosition(editorRef, cursor) + } + + if (selection.rangeCount === 0) return false + const range = selection.getRangeAt(0) + if (!editorRef.contains(range.startContainer)) return false if (part.type === "file" || part.type === "agent") { + const cursorPosition = getCursorPosition(editorRef) + const rawText = prompt + .current() + .map((p) => ("content" in p ? p.content : "")) + .join("") + const textBeforeCursor = rawText.substring(0, cursorPosition) + const atMatch = textBeforeCursor.match(/@(\S*)$/) const pill = createPill(part) const gap = document.createTextNode(" ") - const range = selection.getRangeAt(0) if (atMatch) { const start = atMatch.index ?? cursorPosition - atMatch[0].length @@ -784,8 +802,9 @@ export const PromptInput: Component = (props) => { range.collapse(true) selection.removeAllRanges() selection.addRange(range) - } else if (part.type === "text") { - const range = selection.getRangeAt(0) + } + + if (part.type === "text") { const fragment = createTextFragment(part.content) const last = fragment.lastChild range.deleteContents() @@ -821,6 +840,7 @@ export const PromptInput: Component = (props) => { handleInput() closePopover() + return true } const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => { diff --git a/packages/app/src/components/prompt-input/attachments.ts b/packages/app/src/components/prompt-input/attachments.ts index 9ea2e62a65f0..a9e4e496512c 100644 --- a/packages/app/src/components/prompt-input/attachments.ts +++ b/packages/app/src/components/prompt-input/attachments.ts @@ -7,6 +7,19 @@ import { getCursorPosition } from "./editor-dom" export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"] export const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"] +const LARGE_PASTE_CHARS = 8000 +const LARGE_PASTE_BREAKS = 120 + +function largePaste(text: string) { + if (text.length >= LARGE_PASTE_CHARS) return true + let breaks = 0 + for (const char of text) { + if (char !== "\n") continue + breaks += 1 + if (breaks >= LARGE_PASTE_BREAKS) return true + } + return false +} type PromptAttachmentsInput = { editor: () => HTMLDivElement | undefined @@ -14,7 +27,7 @@ type PromptAttachmentsInput = { isDialogActive: () => boolean setDraggingType: (type: "image" | "@mention" | null) => void focusEditor: () => void - addPart: (part: ContentPart) => void + addPart: (part: ContentPart) => boolean readClipboardImage?: () => Promise } @@ -89,6 +102,13 @@ export function createPromptAttachments(input: PromptAttachmentsInput) { } if (!plainText) return + + if (largePaste(plainText)) { + if (input.addPart({ type: "text", content: plainText, start: 0, end: 0 })) return + input.focusEditor() + if (input.addPart({ type: "text", content: plainText, start: 0, end: 0 })) return + } + const inserted = typeof document.execCommand === "function" && document.execCommand("insertText", false, plainText) if (inserted) return diff --git a/packages/app/src/components/prompt-input/editor-dom.test.ts b/packages/app/src/components/prompt-input/editor-dom.test.ts index 15e759f44ac0..3088522a59f6 100644 --- a/packages/app/src/components/prompt-input/editor-dom.test.ts +++ b/packages/app/src/components/prompt-input/editor-dom.test.ts @@ -24,6 +24,28 @@ describe("prompt-input editor dom", () => { expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR") }) + test("createTextFragment avoids break-node explosion for large multiline content", () => { + const content = Array.from({ length: 220 }, () => "line").join("\n") + const fragment = createTextFragment(content) + const container = document.createElement("div") + container.appendChild(fragment) + + expect(container.childNodes.length).toBe(1) + expect(container.childNodes[0]?.nodeType).toBe(Node.TEXT_NODE) + expect(container.textContent).toBe(content) + }) + + test("createTextFragment keeps terminal break in large multiline fallback", () => { + const content = `${Array.from({ length: 220 }, () => "line").join("\n")}\n` + const fragment = createTextFragment(content) + const container = document.createElement("div") + container.appendChild(fragment) + + expect(container.childNodes.length).toBe(2) + expect(container.childNodes[0]?.textContent).toBe(content.slice(0, -1)) + expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR") + }) + test("length helpers treat breaks as one char and ignore zero-width chars", () => { const container = document.createElement("div") container.appendChild(document.createTextNode("ab\u200B")) diff --git a/packages/app/src/components/prompt-input/editor-dom.ts b/packages/app/src/components/prompt-input/editor-dom.ts index 4850a26ecef9..8575140d7d54 100644 --- a/packages/app/src/components/prompt-input/editor-dom.ts +++ b/packages/app/src/components/prompt-input/editor-dom.ts @@ -1,5 +1,20 @@ +const MAX_BREAKS = 200 + export function createTextFragment(content: string): DocumentFragment { const fragment = document.createDocumentFragment() + let breaks = 0 + for (const char of content) { + if (char !== "\n") continue + breaks += 1 + if (breaks > MAX_BREAKS) { + const tail = content.endsWith("\n") + const text = tail ? content.slice(0, -1) : content + if (text) fragment.appendChild(document.createTextNode(text)) + if (tail) fragment.appendChild(document.createElement("br")) + return fragment + } + } + const segments = content.split("\n") segments.forEach((segment, index) => { if (segment) { diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index 162e016c6f4a..1ea97c395c43 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -11,6 +11,7 @@ import { Accordion } from "@opencode-ai/ui/accordion" import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header" import { Code } from "@opencode-ai/ui/code" import { Markdown } from "@opencode-ai/ui/markdown" +import { ScrollView } from "@opencode-ai/ui/scroll-view" import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client" import { useLanguage } from "@/context/language" import { getSessionContextMetrics } from "./session-context-metrics" @@ -268,9 +269,9 @@ export function SessionContextTab() { }) return ( -
{ + { scroll = el restoreScroll() }} @@ -336,6 +337,6 @@ export function SessionContextTab() {
- + ) } diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index ae8fc200f2d0..825d1dab6cff 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -452,7 +452,10 @@ export function SessionHeader() { variant: "ghost", class: "rounded-md h-[24px] px-3 border border-border-weak-base bg-surface-panel shadow-none data-[expanded]:bg-surface-base-active", - classList: { "rounded-r-none": share.shareUrl() !== undefined }, + classList: { + "rounded-r-none": share.shareUrl() !== undefined, + "border-r-0": share.shareUrl() !== undefined, + }, style: { scale: 1 }, }} trigger={{language.t("session.share.action.share")}} diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index df71fd77e868..cf993840dc8f 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -20,12 +20,17 @@ let demoSoundState = { // To prevent audio from overlapping/playing very quickly when navigating the settings menus, // delay the playback by 100ms during quick selection changes and pause existing sounds. -const playDemoSound = (src: string) => { +const stopDemoSound = () => { if (demoSoundState.cleanup) { demoSoundState.cleanup() } - clearTimeout(demoSoundState.timeout) + demoSoundState.cleanup = undefined +} + +const playDemoSound = (src: string | undefined) => { + stopDemoSound() + if (!src) return demoSoundState.timeout = setTimeout(() => { demoSoundState.cleanup = playSound(src) @@ -132,11 +137,17 @@ export const SettingsGeneral: Component = () => { ] as const const fontOptionsList = [...fontOptions] - const soundOptions = [...SOUND_OPTIONS] + const noneSound = { id: "none", label: "sound.option.none", src: undefined } as const + const soundOptions = [noneSound, ...SOUND_OPTIONS] - const soundSelectProps = (current: () => string, set: (id: string) => void) => ({ + const soundSelectProps = ( + enabled: () => boolean, + current: () => string, + setEnabled: (value: boolean) => void, + set: (id: string) => void, + ) => ({ options: soundOptions, - current: soundOptions.find((o) => o.id === current()), + current: enabled() ? (soundOptions.find((o) => o.id === current()) ?? noneSound) : noneSound, value: (o: (typeof soundOptions)[number]) => o.id, label: (o: (typeof soundOptions)[number]) => language.t(o.label), onHighlight: (option: (typeof soundOptions)[number] | undefined) => { @@ -145,6 +156,12 @@ export const SettingsGeneral: Component = () => { }, onSelect: (option: (typeof soundOptions)[number] | undefined) => { if (!option) return + if (option.id === "none") { + setEnabled(false) + stopDemoSound() + return + } + setEnabled(true) set(option.id) playDemoSound(option.src) }, @@ -250,6 +267,18 @@ export const SettingsGeneral: Component = () => { )} + + +
+ settings.general.setShowReasoningSummaries(checked)} + /> +
+
) @@ -307,66 +336,45 @@ export const SettingsGeneral: Component = () => { title={language.t("settings.general.sounds.agent.title")} description={language.t("settings.general.sounds.agent.description")} > -
-
- settings.sounds.setAgentEnabled(checked)} - /> -
- settings.sounds.agentEnabled(), + () => settings.sounds.agent(), + (value) => settings.sounds.setAgentEnabled(value), + (id) => settings.sounds.setAgent(id), + )} + /> -
-
- settings.sounds.setPermissionsEnabled(checked)} - /> -
- settings.sounds.permissionsEnabled(), + () => settings.sounds.permissions(), + (value) => settings.sounds.setPermissionsEnabled(value), + (id) => settings.sounds.setPermissions(id), + )} + /> -
-
- settings.sounds.setErrorsEnabled(checked)} - /> -
- settings.sounds.errorsEnabled(), + () => settings.sounds.errors(), + (value) => settings.sounds.setErrorsEnabled(value), + (id) => settings.sounds.setErrors(id), + )} + />
diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index bd7ab24475a6..ce811463fc67 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -540,7 +540,7 @@ export const Terminal = (props: TerminalProps) => { disposed = true if (fitFrame !== undefined) cancelAnimationFrame(fitFrame) if (sizeTimer !== undefined) clearTimeout(sizeTimer) - if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) ws.close() + if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) ws.close(1000) const finalize = () => { persistTerminal({ term, addon: serializeAddon, cursor, pty: local.pty, onCleanup: props.onCleanup }) diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx index fbcd0a851845..d279a7f321bb 100644 --- a/packages/app/src/context/settings.tsx +++ b/packages/app/src/context/settings.tsx @@ -22,6 +22,7 @@ export interface Settings { general: { autoSave: boolean releaseNotes: boolean + showReasoningSummaries: boolean } updates: { startup: boolean @@ -42,6 +43,7 @@ const defaultSettings: Settings = { general: { autoSave: true, releaseNotes: true, + showReasoningSummaries: false, }, updates: { startup: true, @@ -120,6 +122,13 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont setReleaseNotes(value: boolean) { setStore("general", "releaseNotes", value) }, + showReasoningSummaries: withFallback( + () => store.general?.showReasoningSummaries, + defaultSettings.general.showReasoningSummaries, + ), + setShowReasoningSummaries(value: boolean) { + setStore("general", "showReasoningSummaries", value) + }, }, updates: { startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup), diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 69a3a86cb282..e860a7e5d564 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -565,6 +565,7 @@ export const dict = { "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", "font.option.geistMono": "Geist Mono", + "sound.option.none": "بلا", "sound.option.alert01": "تنبيه 01", "sound.option.alert02": "تنبيه 02", "sound.option.alert03": "تنبيه 03", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 1c37317a3755..e96a0195df8d 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -571,6 +571,7 @@ export const dict = { "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", "font.option.geistMono": "Geist Mono", + "sound.option.none": "Nenhum", "sound.option.alert01": "Alerta 01", "sound.option.alert02": "Alerta 02", "sound.option.alert03": "Alerta 03", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 59bab1eb8b33..1852afcd14c2 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -639,6 +639,7 @@ export const dict = { "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", "font.option.geistMono": "Geist Mono", + "sound.option.none": "Nijedan", "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 ce33ceec316f..c5d2dc25f1fb 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -635,6 +635,7 @@ export const dict = { "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", "font.option.geistMono": "Geist Mono", + "sound.option.none": "Ingen", "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 cf3416be2def..34a80ee4c5f9 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -580,6 +580,7 @@ export const dict = { "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", "font.option.geistMono": "Geist Mono", + "sound.option.none": "Keine", "sound.option.alert01": "Alarm 01", "sound.option.alert02": "Alarm 02", "sound.option.alert03": "Alarm 03", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 8837dcbad033..7ba82066c785 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -610,6 +610,8 @@ export const dict = { "settings.general.row.theme.description": "Customise how OpenCode is themed.", "settings.general.row.font.title": "Font", "settings.general.row.font.description": "Customise the mono font used in code blocks", + "settings.general.row.reasoningSummaries.title": "Show reasoning summaries", + "settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline", "settings.general.row.wayland.title": "Use native Wayland", "settings.general.row.wayland.description": "Disable X11 fallback on Wayland. Requires restart.", @@ -640,6 +642,7 @@ export const dict = { "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", "font.option.geistMono": "Geist Mono", + "sound.option.none": "None", "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 d741bb138b79..28988bba1e14 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -643,6 +643,7 @@ export const dict = { "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", "font.option.geistMono": "Geist Mono", + "sound.option.none": "Ninguno", "sound.option.alert01": "Alerta 01", "sound.option.alert02": "Alerta 02", "sound.option.alert03": "Alerta 03", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 686539df4d8c..643c5e821134 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -579,6 +579,7 @@ export const dict = { "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", "font.option.geistMono": "Geist Mono", + "sound.option.none": "Aucun", "sound.option.alert01": "Alerte 01", "sound.option.alert02": "Alerte 02", "sound.option.alert03": "Alerte 03", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 288351c8be94..5f6e92402525 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -569,6 +569,7 @@ export const dict = { "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", "font.option.geistMono": "Geist Mono", + "sound.option.none": "なし", "sound.option.alert01": "アラート 01", "sound.option.alert02": "アラート 02", "sound.option.alert03": "アラート 03", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 72a46ca7e636..d5a0b090b93d 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -570,6 +570,7 @@ export const dict = { "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", "font.option.geistMono": "Geist Mono", + "sound.option.none": "없음", "sound.option.alert01": "알림 01", "sound.option.alert02": "알림 02", "sound.option.alert03": "알림 03", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index c099fe61f9ba..10a8c1042fa0 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -642,6 +642,7 @@ export const dict = { "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", "font.option.geistMono": "Geist Mono", + "sound.option.none": "Ingen", "sound.option.alert01": "Varsel 01", "sound.option.alert02": "Varsel 02", "sound.option.alert03": "Varsel 03", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 67c9dda2ac11..9038fd1ad2f9 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -570,6 +570,7 @@ export const dict = { "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", "font.option.geistMono": "Geist Mono", + "sound.option.none": "Brak", "sound.option.alert01": "Alert 01", "sound.option.alert02": "Alert 02", "sound.option.alert03": "Alert 03", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 57ef82fd6637..69fee5c89a4f 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -640,6 +640,7 @@ export const dict = { "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", "font.option.geistMono": "Geist Mono", + "sound.option.none": "Нет", "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 e67db0465173..d66c8f6075b8 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -634,6 +634,7 @@ export const dict = { "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", "font.option.geistMono": "Geist Mono", + "sound.option.none": "ไม่มี", "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 42740fa771b2..46daeb701ff4 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -633,6 +633,7 @@ export const dict = { "font.option.ubuntuMono": "Ubuntu Mono", "font.option.geistMono": "Geist Mono", + "sound.option.none": "无", "sound.option.alert01": "警报 01", "sound.option.alert02": "警报 02", "sound.option.alert03": "警报 03", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index f47fdede8fc5..bbb00727b708 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -629,6 +629,7 @@ export const dict = { "font.option.sourceCodePro": "Source Code Pro", "font.option.ubuntuMono": "Ubuntu Mono", "font.option.geistMono": "Geist Mono", + "sound.option.none": "無", "sound.option.alert01": "警報 01", "sound.option.alert02": "警報 02", "sound.option.alert03": "警報 03", diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 1a922d725703..a3f4b7164b4b 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -943,15 +943,12 @@ export default function Page() { if (next === dockHeight) return const el = scroller - const stick = el ? el.scrollHeight - el.clientHeight - el.scrollTop < 10 : false + const delta = next - dockHeight + const stick = el ? el.scrollHeight - el.clientHeight - el.scrollTop < 10 + Math.max(0, delta) : false dockHeight = next - if (stick && el) { - requestAnimationFrame(() => { - el.scrollTo({ top: el.scrollHeight, behavior: "auto" }) - }) - } + if (stick) autoScroll.forceScrollToBottom() if (el) scheduleScrollState(el) scrollSpy.markDirty() diff --git a/packages/app/src/pages/session/composer/session-question-dock.tsx b/packages/app/src/pages/session/composer/session-question-dock.tsx index 1ccac937c394..fd2ced3dc814 100644 --- a/packages/app/src/pages/session/composer/session-question-dock.tsx +++ b/packages/app/src/pages/session/composer/session-question-dock.tsx @@ -62,7 +62,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit const measure = () => { if (!root) return - const scroller = document.querySelector(".session-scroller") + const scroller = document.querySelector(".scroll-view__viewport") const head = scroller instanceof HTMLElement ? scroller.firstElementChild : undefined const top = head instanceof HTMLElement && head.classList.contains("sticky") ? head.getBoundingClientRect().bottom : 0 @@ -95,7 +95,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit window.addEventListener("resize", update) const dock = root?.closest('[data-component="session-prompt-dock"]') - const scroller = document.querySelector(".session-scroller") + const scroller = document.querySelector(".scroll-view__viewport") const observer = new ResizeObserver(update) if (dock instanceof HTMLElement) observer.observe(dock) if (scroller instanceof HTMLElement) observer.observe(scroller) diff --git a/packages/app/src/pages/session/file-tabs.tsx b/packages/app/src/pages/session/file-tabs.tsx index ebc1f5922768..032756cabd8d 100644 --- a/packages/app/src/pages/session/file-tabs.tsx +++ b/packages/app/src/pages/session/file-tabs.tsx @@ -9,6 +9,7 @@ import { showToast } from "@opencode-ai/ui/toast" import { LineComment as LineCommentView, LineCommentEditor } from "@opencode-ai/ui/line-comment" import { Mark } from "@opencode-ai/ui/logo" import { Tabs } from "@opencode-ai/ui/tabs" +import { ScrollView } from "@opencode-ai/ui/scroll-view" import { useLayout } from "@/context/layout" import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file" import { useComments } from "@/context/comments" @@ -509,51 +510,52 @@ export function FileTabContent(props: { tab: string }) { ) return ( - { - scroll = el - restoreScroll() - }} - onScroll={handleScroll} - > - - -
- {path()} requestAnimationFrame(restoreScroll)} - /> -
-
- -
- {renderCode(svgContent() ?? "", "")} - -
- {path()} + + { + scroll = el + restoreScroll() + }} + onScroll={handleScroll as any} + > + + +
+ {path()} requestAnimationFrame(restoreScroll)} + /> +
+
+ +
+ {renderCode(svgContent() ?? "", "")} + +
+ {path()} +
+
+
+
+ +
+ +
+
{path()?.split("/").pop()}
+
{language.t("session.files.binaryContent")}
- -
-
- -
- -
-
{path()?.split("/").pop()}
-
{language.t("session.files.binaryContent")}
-
-
- {renderCode(contents(), "pb-40")} - -
{language.t("common.loading")}...
-
- {(err) =>
{err()}
}
-
+ + {renderCode(contents(), "pb-40")} + +
{language.t("common.loading")}...
+
+ {(err) =>
{err()}
}
+ +
) } diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index 567ef5fc8765..b13ccb474ac3 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -8,12 +8,14 @@ import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { Dialog } from "@opencode-ai/ui/dialog" import { InlineInput } from "@opencode-ai/ui/inline-input" import { SessionTurn } from "@opencode-ai/ui/session-turn" +import { ScrollView } from "@opencode-ai/ui/scroll-view" import type { UserMessage } from "@opencode-ai/sdk/v2" import { showToast } from "@opencode-ai/ui/toast" import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture" import { SessionContextUsage } from "@/components/session-context-usage" import { useDialog } from "@opencode-ai/ui/context/dialog" import { useLanguage } from "@/context/language" +import { useSettings } from "@/context/settings" import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" @@ -80,6 +82,7 @@ export function MessageTimeline(props: { const navigate = useNavigate() const sdk = useSDK() const sync = useSync() + const settings = useSettings() const dialog = useDialog() const language = useLanguage() @@ -320,8 +323,8 @@ export function MessageTimeline(props: {
-
{ const root = e.currentTarget const delta = normalizeWheelDelta({ @@ -365,7 +368,7 @@ export function MessageTimeline(props: { if (props.isDesktop) props.onScrollSpyScroll() }} onClick={props.onAutoScrollInteraction} - class="relative min-w-0 w-full h-full overflow-y-auto session-scroller" + class="relative min-w-0 w-full h-full" style={{ "--session-title-height": showHeader() ? "40px" : "0px", "--sticky-accordion-top": showHeader() ? "48px" : "0px", @@ -535,6 +538,7 @@ export function MessageTimeline(props: { sessionID={sessionID() ?? ""} messageID={message.id} lastUserMessageID={props.lastUserMessageID} + showReasoningSummaries={settings.general.showReasoningSummaries()} classes={{ root: "min-w-0 w-full relative", content: "flex flex-col justify-between !overflow-visible", @@ -545,7 +549,7 @@ export function MessageTimeline(props: { )}
-
+
) diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx index 3a9f63949a41..9349e993768d 100644 --- a/packages/app/src/pages/session/review-tab.tsx +++ b/packages/app/src/pages/session/review-tab.tsx @@ -143,9 +143,9 @@ export function SessionReviewTab(props: SessionReviewTabProps) { open={props.view().review.open()} onOpenChange={props.view().review.setOpen} classes={{ - root: props.classes?.root ?? "pb-6", + root: props.classes?.root ?? "pb-6 pr-3", header: props.classes?.header ?? "px-3", - container: props.classes?.container ?? "px-3", + container: props.classes?.container ?? "pl-3", }} diffs={props.diffs()} diffStyle={props.diffStyle} diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx index 33421c3869ab..27ea4e6f317b 100644 --- a/packages/app/src/pages/session/terminal-panel.tsx +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -67,11 +67,11 @@ export function TerminalPanel() { on( () => terminal.active(), (activeId) => { - if (!activeId || !opened()) return + if (!activeId || !open()) return if (document.activeElement instanceof HTMLElement) { document.activeElement.blur() } - focusTerminalById(activeId) + setTimeout(() => focusTerminalById(activeId), 0) }, ), ) @@ -209,21 +209,17 @@ export function TerminalPanel() {
- - {(pty) => ( -
- - terminal.clone(pty.id)} /> - -
+ + {(id) => ( + + {(pty) => ( +
+ terminal.clone(id)} /> +
+ )} +
)} -
+
diff --git a/packages/console/app/package.json b/packages/console/app/package.json index acea3fd6a466..904aeadd8e06 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.2.9", + "version": "1.2.10", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/app/src/routes/download/[platform].ts b/packages/console/app/src/routes/download/[channel]/[platform].ts similarity index 70% rename from packages/console/app/src/routes/download/[platform].ts rename to packages/console/app/src/routes/download/[channel]/[platform].ts index 2c30a803623d..9a52842639ad 100644 --- a/packages/console/app/src/routes/download/[platform].ts +++ b/packages/console/app/src/routes/download/[channel]/[platform].ts @@ -1,5 +1,5 @@ -import { APIEvent } from "@solidjs/start" -import { DownloadPlatform } from "./types" +import type { APIEvent } from "@solidjs/start" +import type { DownloadPlatform } from "../types" const assetNames: Record = { "darwin-aarch64-dmg": "opencode-desktop-darwin-aarch64.dmg", @@ -17,17 +17,20 @@ const downloadNames: Record = { "windows-x64-nsis": "OpenCode Desktop Installer.exe", } satisfies { [K in DownloadPlatform]?: string } -export async function GET({ params: { platform } }: APIEvent) { +export async function GET({ params: { platform, channel } }: APIEvent) { const assetName = assetNames[platform] if (!assetName) return new Response("Not Found", { status: 404 }) - const resp = await fetch(`https://github.com/anomalyco/opencode/releases/latest/download/${assetName}`, { - cf: { - // in case gh releases has rate limits - cacheTtl: 60 * 5, - cacheEverything: true, - }, - } as any) + const resp = await fetch( + `https://github.com/anomalyco/${channel === "stable" ? "opencode" : "opencode-beta"}/releases/latest/download/${assetName}`, + { + cf: { + // in case gh releases has rate limits + cacheTtl: 60 * 5, + cacheEverything: true, + }, + } as any, + ) const downloadName = downloadNames[platform] diff --git a/packages/console/app/src/routes/download/index.tsx b/packages/console/app/src/routes/download/index.tsx index e5e4e9750219..0278d8622bf4 100644 --- a/packages/console/app/src/routes/download/index.tsx +++ b/packages/console/app/src/routes/download/index.tsx @@ -1,18 +1,18 @@ import "./index.css" -import { Title, Meta } from "@solidjs/meta" -import { A, createAsync, query } from "@solidjs/router" -import { Header } from "~/component/header" -import { Footer } from "~/component/footer" -import { IconCopy, IconCheck } from "~/component/icon" +import { Meta, Title } from "@solidjs/meta" +import { A } from "@solidjs/router" +import { createSignal, type JSX, onMount, Show } from "solid-js" import { Faq } from "~/component/faq" -import desktopAppIcon from "../../asset/lander/opencode-desktop-icon.png" +import { Footer } from "~/component/footer" +import { Header } from "~/component/header" +import { IconCheck, IconCopy } from "~/component/icon" import { Legal } from "~/component/legal" +import { LocaleLinks } from "~/component/locale-links" import { config } from "~/config" -import { createSignal, onMount, Show, JSX } from "solid-js" -import { DownloadPlatform } from "./types" import { useI18n } from "~/context/i18n" import { useLanguage } from "~/context/language" -import { LocaleLinks } from "~/component/locale-links" +import desktopAppIcon from "../../asset/lander/opencode-desktop-icon.png" +import type { DownloadPlatform } from "./types" type OS = "macOS" | "Windows" | "Linux" | null @@ -40,8 +40,8 @@ function getDownloadPlatform(os: OS): DownloadPlatform { } } -function getDownloadHref(platform: DownloadPlatform) { - return `/download/${platform}` +function getDownloadHref(platform: DownloadPlatform, channel: "stable" | "beta" = "stable") { + return `/download/${channel}/${platform}` } function IconDownload(props: JSX.SvgSVGAttributes) { diff --git a/packages/console/app/src/routes/workspace/[id]/model-section.tsx b/packages/console/app/src/routes/workspace/[id]/model-section.tsx index 97f95278a160..a4b64889cad7 100644 --- a/packages/console/app/src/routes/workspace/[id]/model-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/model-section.tsx @@ -36,7 +36,7 @@ const getModelsInfo = query(async (workspaceID: string) => { "use server" return withActor(async () => { return { - all: Object.entries(ZenData.list().models) + all: Object.entries(ZenData.list("full").models) .filter(([id, _model]) => !["claude-3-5-haiku"].includes(id)) .filter(([id, _model]) => !id.startsWith("alpha-")) .sort(([idA, modelA], [idB, modelB]) => { diff --git a/packages/console/app/src/routes/zen/lite/v1/chat/completions.ts b/packages/console/app/src/routes/zen/lite/v1/chat/completions.ts new file mode 100644 index 000000000000..9a57e893fb4f --- /dev/null +++ b/packages/console/app/src/routes/zen/lite/v1/chat/completions.ts @@ -0,0 +1,12 @@ +import type { APIEvent } from "@solidjs/start/server" +import { handler } from "~/routes/zen/util/handler" + +export function POST(input: APIEvent) { + return handler(input, { + format: "oa-compat", + modelList: "lite", + parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1], + parseModel: (url: string, body: any) => body.model, + parseIsStream: (url: string, body: any) => !!body.stream, + }) +} diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index a8e275ba9a55..5f2b51c21e95 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -44,6 +44,7 @@ export async function handler( input: APIEvent, opts: { format: ZenData.Format + modelList: "lite" | "full" parseApiKey: (headers: Headers) => string | undefined parseModel: (url: string, body: any) => string parseIsStream: (url: string, body: any) => boolean @@ -77,7 +78,7 @@ export async function handler( request: requestId, client: ocClient, }) - const zenData = ZenData.list() + const zenData = ZenData.list(opts.modelList) const modelInfo = validateModel(zenData, model) const dataDumper = createDataDumper(sessionId, requestId, projectId) const trialLimiter = createTrialLimiter(modelInfo.trial, ip, ocClient) @@ -107,11 +108,14 @@ export async function handler( const startTimestamp = Date.now() const reqUrl = providerInfo.modifyUrl(providerInfo.api, isStream) const reqBody = JSON.stringify( - providerInfo.modifyBody({ - ...createBodyConverter(opts.format, providerInfo.format)(body), - model: providerInfo.model, - ...(providerInfo.payloadModifier ?? {}), - }), + providerInfo.modifyBody( + { + ...createBodyConverter(opts.format, providerInfo.format)(body), + model: providerInfo.model, + ...(providerInfo.payloadModifier ?? {}), + }, + authInfo?.workspaceID, + ), ) logger.debug("REQUEST URL: " + reqUrl) logger.debug("REQUEST: " + reqBody.substring(0, 300) + "...") 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 db2dfa521509..596b38cc5a4b 100644 --- a/packages/console/app/src/routes/zen/util/provider/openai.ts +++ b/packages/console/app/src/routes/zen/util/provider/openai.ts @@ -18,9 +18,10 @@ export const openaiHelper: ProviderHelper = () => ({ modifyHeaders: (headers: Headers, body: Record, apiKey: string) => { headers.set("authorization", `Bearer ${apiKey}`) }, - modifyBody: (body: Record) => { - return body - }, + modifyBody: (body: Record, workspaceID?: string) => ({ + ...body, + ...(workspaceID ? { safety_identifier: workspaceID } : {}), + }), createBinaryStreamDecoder: () => undefined, streamSeparator: "\n\n", createUsageParser: () => { 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 5f8b631cf089..1f9492845f8a 100644 --- a/packages/console/app/src/routes/zen/util/provider/provider.ts +++ b/packages/console/app/src/routes/zen/util/provider/provider.ts @@ -37,7 +37,7 @@ export type ProviderHelper = (input: { reqModel: string; providerModel: string } format: ZenData.Format modifyUrl: (providerApi: string, isStream?: boolean) => string modifyHeaders: (headers: Headers, body: Record, apiKey: string) => void - modifyBody: (body: Record) => Record + modifyBody: (body: Record, workspaceID?: string) => Record createBinaryStreamDecoder: () => ((chunk: Uint8Array) => Uint8Array | undefined) | undefined streamSeparator: string createUsageParser: () => { diff --git a/packages/console/app/src/routes/zen/v1/chat/completions.ts b/packages/console/app/src/routes/zen/v1/chat/completions.ts index 65545912935a..e9e05197e2e2 100644 --- a/packages/console/app/src/routes/zen/v1/chat/completions.ts +++ b/packages/console/app/src/routes/zen/v1/chat/completions.ts @@ -4,6 +4,7 @@ import { handler } from "~/routes/zen/util/handler" export function POST(input: APIEvent) { return handler(input, { format: "oa-compat", + modelList: "full", parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1], parseModel: (url: string, body: any) => body.model, parseIsStream: (url: string, body: any) => !!body.stream, diff --git a/packages/console/app/src/routes/zen/v1/messages.ts b/packages/console/app/src/routes/zen/v1/messages.ts index 54d223f95a46..9c09315a6e89 100644 --- a/packages/console/app/src/routes/zen/v1/messages.ts +++ b/packages/console/app/src/routes/zen/v1/messages.ts @@ -4,6 +4,7 @@ import { handler } from "~/routes/zen/util/handler" export function POST(input: APIEvent) { return handler(input, { format: "anthropic", + modelList: "full", parseApiKey: (headers: Headers) => headers.get("x-api-key") ?? undefined, parseModel: (url: string, body: any) => body.model, parseIsStream: (url: string, body: any) => !!body.stream, diff --git a/packages/console/app/src/routes/zen/v1/models.ts b/packages/console/app/src/routes/zen/v1/models.ts index ee2b3ab5416e..f9c14ededdc0 100644 --- a/packages/console/app/src/routes/zen/v1/models.ts +++ b/packages/console/app/src/routes/zen/v1/models.ts @@ -17,7 +17,7 @@ export async function OPTIONS(input: APIEvent) { } export async function GET(input: APIEvent) { - const zenData = ZenData.list() + const zenData = ZenData.list("full") const disabledModels = await authenticate() return new Response( diff --git a/packages/console/app/src/routes/zen/v1/models/[model].ts b/packages/console/app/src/routes/zen/v1/models/[model].ts index b20378e379d4..a4edd5861a35 100644 --- a/packages/console/app/src/routes/zen/v1/models/[model].ts +++ b/packages/console/app/src/routes/zen/v1/models/[model].ts @@ -4,6 +4,7 @@ import { handler } from "~/routes/zen/util/handler" export function POST(input: APIEvent) { return handler(input, { format: "google", + modelList: "full", parseApiKey: (headers: Headers) => headers.get("x-goog-api-key") ?? undefined, parseModel: (url: string, body: any) => url.split("/").pop()?.split(":")?.[0] ?? "", parseIsStream: (url: string, body: any) => diff --git a/packages/console/app/src/routes/zen/v1/responses.ts b/packages/console/app/src/routes/zen/v1/responses.ts index a82a667cc7d0..cae625cf6fa9 100644 --- a/packages/console/app/src/routes/zen/v1/responses.ts +++ b/packages/console/app/src/routes/zen/v1/responses.ts @@ -4,6 +4,7 @@ import { handler } from "~/routes/zen/util/handler" export function POST(input: APIEvent) { return handler(input, { format: "openai", + modelList: "full", parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1], parseModel: (url: string, body: any) => body.model, parseIsStream: (url: string, body: any) => !!body.stream, diff --git a/packages/console/core/package.json b/packages/console/core/package.json index f5b0b3965bf5..a99f1ec32329 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.2.9", + "version": "1.2.10", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/core/src/model.ts b/packages/console/core/src/model.ts index 6011cac37683..e868b176e8a5 100644 --- a/packages/console/core/src/model.ts +++ b/packages/console/core/src/model.ts @@ -73,6 +73,7 @@ export namespace ZenData { const ModelsSchema = z.object({ models: z.record(z.string(), z.union([ModelSchema, z.array(ModelSchema.extend({ formatFilter: FormatSchema }))])), + liteModels: z.record(z.string(), ModelSchema), providers: z.record(z.string(), ProviderSchema), providerFamilies: z.record(z.string(), ProviderFamilySchema), }) @@ -81,7 +82,7 @@ export namespace ZenData { return input }) - export const list = fn(z.void(), () => { + export const list = fn(z.enum(["lite", "full"]), (modelList) => { const json = JSON.parse( Resource.ZEN_MODELS1.value + Resource.ZEN_MODELS2.value + @@ -114,9 +115,9 @@ export namespace ZenData { Resource.ZEN_MODELS29.value + Resource.ZEN_MODELS30.value, ) - const { models, providers, providerFamilies } = ModelsSchema.parse(json) + const { models, liteModels, providers, providerFamilies } = ModelsSchema.parse(json) return { - models, + models: modelList === "lite" ? liteModels : models, providers: Object.fromEntries( Object.entries(providers).map(([id, provider]) => [ id, diff --git a/packages/console/function/package.json b/packages/console/function/package.json index d0545858342e..386ee19df23f 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.2.9", + "version": "1.2.10", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index bcadba0005ec..7a08244bb629 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.2.9", + "version": "1.2.10", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 2eb532807e44..dc25cb020373 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.2.9", + "version": "1.2.10", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/desktop/src-tauri/icons/README.md b/packages/desktop/src-tauri/icons/README.md index db86593cc372..fa219a77ef1c 100644 --- a/packages/desktop/src-tauri/icons/README.md +++ b/packages/desktop/src-tauri/icons/README.md @@ -3,8 +3,8 @@ Here's the process I've been using to create icons: - Save source image as `app-icon.png` in `packages/desktop` -- `cd` to `src-tauri` -- Run `bun tauri icons -o icons/{environment}` +- `cd` to `packages/desktop` +- Run `bun tauri icon -o src-tauri/icons/{environment}` - Use [Image2Icon](https://img2icnsapp.com/)'s 'Big Sur Icon' preset to generate an `icon.icns` file and place it in the appropriate icons folder The Image2Icon step is necessary as the `icon.icns` generated by `app-icon.png` does not apply the shadow/padding expected by macOS, diff --git a/packages/desktop/src-tauri/icons/beta/128x128.png b/packages/desktop/src-tauri/icons/beta/128x128.png new file mode 100644 index 000000000000..751e80f1fda6 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/128x128.png differ diff --git a/packages/desktop/src-tauri/icons/beta/128x128@2x.png b/packages/desktop/src-tauri/icons/beta/128x128@2x.png new file mode 100644 index 000000000000..fe330df419aa Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/128x128@2x.png differ diff --git a/packages/desktop/src-tauri/icons/beta/32x32.png b/packages/desktop/src-tauri/icons/beta/32x32.png new file mode 100644 index 000000000000..2703048eed1f Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/32x32.png differ diff --git a/packages/desktop/src-tauri/icons/beta/64x64.png b/packages/desktop/src-tauri/icons/beta/64x64.png new file mode 100644 index 000000000000..ecd7fe3142c8 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/64x64.png differ diff --git a/packages/desktop/src-tauri/icons/beta/Square107x107Logo.png b/packages/desktop/src-tauri/icons/beta/Square107x107Logo.png new file mode 100644 index 000000000000..e6ea73f4da2f Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/Square107x107Logo.png differ diff --git a/packages/desktop/src-tauri/icons/beta/Square142x142Logo.png b/packages/desktop/src-tauri/icons/beta/Square142x142Logo.png new file mode 100644 index 000000000000..74ae729c42bc Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/Square142x142Logo.png differ diff --git a/packages/desktop/src-tauri/icons/beta/Square150x150Logo.png b/packages/desktop/src-tauri/icons/beta/Square150x150Logo.png new file mode 100644 index 000000000000..0b109b8f4ae3 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/Square150x150Logo.png differ diff --git a/packages/desktop/src-tauri/icons/beta/Square284x284Logo.png b/packages/desktop/src-tauri/icons/beta/Square284x284Logo.png new file mode 100644 index 000000000000..0261ded42c77 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/Square284x284Logo.png differ diff --git a/packages/desktop/src-tauri/icons/beta/Square30x30Logo.png b/packages/desktop/src-tauri/icons/beta/Square30x30Logo.png new file mode 100644 index 000000000000..34158f10a497 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/Square30x30Logo.png differ diff --git a/packages/desktop/src-tauri/icons/beta/Square310x310Logo.png b/packages/desktop/src-tauri/icons/beta/Square310x310Logo.png new file mode 100644 index 000000000000..f18bfada4cdd Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/Square310x310Logo.png differ diff --git a/packages/desktop/src-tauri/icons/beta/Square44x44Logo.png b/packages/desktop/src-tauri/icons/beta/Square44x44Logo.png new file mode 100644 index 000000000000..6d1cc06c086c Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/Square44x44Logo.png differ diff --git a/packages/desktop/src-tauri/icons/beta/Square71x71Logo.png b/packages/desktop/src-tauri/icons/beta/Square71x71Logo.png new file mode 100644 index 000000000000..a26084dc2fc3 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/Square71x71Logo.png differ diff --git a/packages/desktop/src-tauri/icons/beta/Square89x89Logo.png b/packages/desktop/src-tauri/icons/beta/Square89x89Logo.png new file mode 100644 index 000000000000..58b0eb6053c8 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/Square89x89Logo.png differ diff --git a/packages/desktop/src-tauri/icons/beta/StoreLogo.png b/packages/desktop/src-tauri/icons/beta/StoreLogo.png new file mode 100644 index 000000000000..648fd2114d7f Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/StoreLogo.png differ diff --git a/packages/desktop/src-tauri/icons/beta/android/mipmap-anydpi-v26/ic_launcher.xml b/packages/desktop/src-tauri/icons/beta/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000000..2ffbf24b6898 --- /dev/null +++ b/packages/desktop/src-tauri/icons/beta/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/packages/desktop/src-tauri/icons/beta/android/mipmap-hdpi/ic_launcher.png b/packages/desktop/src-tauri/icons/beta/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000000..39d1dd0d5197 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/android/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/desktop/src-tauri/icons/beta/android/mipmap-hdpi/ic_launcher_foreground.png b/packages/desktop/src-tauri/icons/beta/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 000000000000..84908e71c1fc Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/packages/desktop/src-tauri/icons/beta/android/mipmap-hdpi/ic_launcher_round.png b/packages/desktop/src-tauri/icons/beta/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000000..a6b8cb61624f Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/packages/desktop/src-tauri/icons/beta/android/mipmap-mdpi/ic_launcher.png b/packages/desktop/src-tauri/icons/beta/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000000..6522e0fba8ad Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/android/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/desktop/src-tauri/icons/beta/android/mipmap-mdpi/ic_launcher_foreground.png b/packages/desktop/src-tauri/icons/beta/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 000000000000..b3449bd4f3f4 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/packages/desktop/src-tauri/icons/beta/android/mipmap-mdpi/ic_launcher_round.png b/packages/desktop/src-tauri/icons/beta/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000000..7aa97d827619 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/packages/desktop/src-tauri/icons/beta/android/mipmap-xhdpi/ic_launcher.png b/packages/desktop/src-tauri/icons/beta/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000000..82bc9d22a694 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/desktop/src-tauri/icons/beta/android/mipmap-xhdpi/ic_launcher_foreground.png b/packages/desktop/src-tauri/icons/beta/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000000..6b031ce8515a Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/packages/desktop/src-tauri/icons/beta/android/mipmap-xhdpi/ic_launcher_round.png b/packages/desktop/src-tauri/icons/beta/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000000..34859de5ef06 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/packages/desktop/src-tauri/icons/beta/android/mipmap-xxhdpi/ic_launcher.png b/packages/desktop/src-tauri/icons/beta/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000000..4cdb71d62b64 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/desktop/src-tauri/icons/beta/android/mipmap-xxhdpi/ic_launcher_foreground.png b/packages/desktop/src-tauri/icons/beta/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000000..a64be6ada1d8 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/packages/desktop/src-tauri/icons/beta/android/mipmap-xxhdpi/ic_launcher_round.png b/packages/desktop/src-tauri/icons/beta/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000000..2de3c27342a7 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/packages/desktop/src-tauri/icons/beta/android/mipmap-xxxhdpi/ic_launcher.png b/packages/desktop/src-tauri/icons/beta/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000000..0ead288664db Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/desktop/src-tauri/icons/beta/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/packages/desktop/src-tauri/icons/beta/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000000..bdd1748258a7 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/packages/desktop/src-tauri/icons/beta/android/mipmap-xxxhdpi/ic_launcher_round.png b/packages/desktop/src-tauri/icons/beta/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000000..69f74758ecfe Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/packages/desktop/src-tauri/icons/beta/android/values/ic_launcher_background.xml b/packages/desktop/src-tauri/icons/beta/android/values/ic_launcher_background.xml new file mode 100644 index 000000000000..ea9c223a6cba --- /dev/null +++ b/packages/desktop/src-tauri/icons/beta/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #fff + \ No newline at end of file diff --git a/packages/desktop/src-tauri/icons/beta/icon.icns b/packages/desktop/src-tauri/icons/beta/icon.icns new file mode 100644 index 000000000000..f98de5da88b1 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/icon.icns differ diff --git a/packages/desktop/src-tauri/icons/beta/icon.ico b/packages/desktop/src-tauri/icons/beta/icon.ico new file mode 100644 index 000000000000..df8588c8e4cd Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/icon.ico differ diff --git a/packages/desktop/src-tauri/icons/beta/icon.png b/packages/desktop/src-tauri/icons/beta/icon.png new file mode 100644 index 000000000000..5313049562ad Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/icon.png differ diff --git a/packages/desktop/src-tauri/icons/beta/ios/AppIcon-20x20@1x.png b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-20x20@1x.png new file mode 100644 index 000000000000..e8ebb28efe1e Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-20x20@1x.png differ diff --git a/packages/desktop/src-tauri/icons/beta/ios/AppIcon-20x20@2x-1.png b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 000000000000..50c8015dea46 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-20x20@2x-1.png differ diff --git a/packages/desktop/src-tauri/icons/beta/ios/AppIcon-20x20@2x.png b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-20x20@2x.png new file mode 100644 index 000000000000..50c8015dea46 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-20x20@2x.png differ diff --git a/packages/desktop/src-tauri/icons/beta/ios/AppIcon-20x20@3x.png b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-20x20@3x.png new file mode 100644 index 000000000000..6e290dbc6899 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-20x20@3x.png differ diff --git a/packages/desktop/src-tauri/icons/beta/ios/AppIcon-29x29@1x.png b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-29x29@1x.png new file mode 100644 index 000000000000..4ef554b4de3a Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-29x29@1x.png differ diff --git a/packages/desktop/src-tauri/icons/beta/ios/AppIcon-29x29@2x-1.png b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 000000000000..b9ddfd47c884 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-29x29@2x-1.png differ diff --git a/packages/desktop/src-tauri/icons/beta/ios/AppIcon-29x29@2x.png b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-29x29@2x.png new file mode 100644 index 000000000000..b9ddfd47c884 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-29x29@2x.png differ diff --git a/packages/desktop/src-tauri/icons/beta/ios/AppIcon-29x29@3x.png b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-29x29@3x.png new file mode 100644 index 000000000000..052322d68216 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-29x29@3x.png differ diff --git a/packages/desktop/src-tauri/icons/beta/ios/AppIcon-40x40@1x.png b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-40x40@1x.png new file mode 100644 index 000000000000..50c8015dea46 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-40x40@1x.png differ diff --git a/packages/desktop/src-tauri/icons/beta/ios/AppIcon-40x40@2x-1.png b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 000000000000..9317b25001cf Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-40x40@2x-1.png differ diff --git a/packages/desktop/src-tauri/icons/beta/ios/AppIcon-40x40@2x.png b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-40x40@2x.png new file mode 100644 index 000000000000..9317b25001cf Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-40x40@2x.png differ diff --git a/packages/desktop/src-tauri/icons/beta/ios/AppIcon-40x40@3x.png b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-40x40@3x.png new file mode 100644 index 000000000000..6b921a17e342 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-40x40@3x.png differ diff --git a/packages/desktop/src-tauri/icons/beta/ios/AppIcon-512@2x.png b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-512@2x.png new file mode 100644 index 000000000000..b83131d64bcf Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-512@2x.png differ diff --git a/packages/desktop/src-tauri/icons/beta/ios/AppIcon-60x60@2x.png b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-60x60@2x.png new file mode 100644 index 000000000000..6b921a17e342 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-60x60@2x.png differ diff --git a/packages/desktop/src-tauri/icons/beta/ios/AppIcon-60x60@3x.png b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-60x60@3x.png new file mode 100644 index 000000000000..685004995cc7 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-60x60@3x.png differ diff --git a/packages/desktop/src-tauri/icons/beta/ios/AppIcon-76x76@1x.png b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-76x76@1x.png new file mode 100644 index 000000000000..1ffceb752a5b Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-76x76@1x.png differ diff --git a/packages/desktop/src-tauri/icons/beta/ios/AppIcon-76x76@2x.png b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-76x76@2x.png new file mode 100644 index 000000000000..81c4178c9120 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-76x76@2x.png differ diff --git a/packages/desktop/src-tauri/icons/beta/ios/AppIcon-83.5x83.5@2x.png b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 000000000000..d5453adffbd9 Binary files /dev/null and b/packages/desktop/src-tauri/icons/beta/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs index 130958bf7ceb..acab0fa7034c 100644 --- a/packages/desktop/src-tauri/src/cli.rs +++ b/packages/desktop/src-tauri/src/cli.rs @@ -320,7 +320,7 @@ pub fn spawn_command( }; let mut cmd = Command::new(shell); - cmd.args(["-l", "-c", &line]); + cmd.args(["-il", "-c", &line]); for (key, value) in envs { cmd.env(key, value); diff --git a/packages/desktop/src-tauri/tauri.beta.conf.json b/packages/desktop/src-tauri/tauri.beta.conf.json new file mode 100644 index 000000000000..4dd7879933c6 --- /dev/null +++ b/packages/desktop/src-tauri/tauri.beta.conf.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "OpenCode Beta", + "identifier": "ai.opencode.desktop.beta", + "bundle": { + "createUpdaterArtifacts": true, + "icon": [ + "icons/beta/32x32.png", + "icons/beta/128x128.png", + "icons/beta/128x128@2x.png", + "icons/beta/icon.icns", + "icons/beta/icon.ico" + ], + "windows": { + "nsis": { + "installerIcon": "icons/beta/icon.ico" + } + }, + "linux": { + "rpm": { + "compression": { + "type": "none" + } + } + } + }, + "plugins": { + "updater": { + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEYwMDM5Nzg5OUMzOUExMDQKUldRRW9UbWNpWmNEOENYT01CV0lhOXR1UFhpaXJsK1Z3aU9lZnNtNzE0TDROWVMwVW9XQnFOelkK", + "endpoints": ["https://github.com/anomalyco/opencode-beta/releases/latest/download/latest.json"] + } + } +} diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 4a28e1b49d7d..983fe3945605 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -491,34 +491,19 @@ render(() => { // Gate component that waits for the server to be ready function ServerGate(props: { children: (data: ServerReadyData) => JSX.Element }) { const [serverData] = createResource(() => commands.awaitInitialization(new Channel() as any)) + if (serverData.state === "errored") throw serverData.error return ( - -
-

Failed to start server

-

- {String(serverData.error ?? "Unknown error")} -

-
+
+
} > - - -
-
- } - > - {(data) => props.children(data())} -
+ {(data) => props.children(data())} ) } diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 04d42b55f60c..fae66ab31a87 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.2.9", + "version": "1.2.10", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index d4c308fc5076..a112d793fd76 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.2.9" +version = "1.2.10" 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.2.9/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.10/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.9/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.10/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.9/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.10/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.2.9/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.10/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.2.9/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.10/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index b8b3f45f22f1..c67be670961b 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.2.9", + "version": "1.2.10", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/bin/opencode b/packages/opencode/bin/opencode index d73bbce26776..a7674ce2f875 100755 --- a/packages/opencode/bin/opencode +++ b/packages/opencode/bin/opencode @@ -25,6 +25,12 @@ if (envPath) { const scriptPath = fs.realpathSync(__filename) const scriptDir = path.dirname(scriptPath) +// +const cached = path.join(scriptDir, ".opencode") +if (fs.existsSync(cached)) { + run(cached) +} + const platformMap = { darwin: "darwin", linux: "linux", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index f9e66d61d5e7..f8912737375e 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.2.9", + "version": "1.2.10", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index ddb4769912d7..34e80d71a081 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -1,10 +1,10 @@ #!/usr/bin/env bun -import solidPlugin from "../node_modules/@opentui/solid/scripts/solid-plugin" -import path from "path" -import fs from "fs" import { $ } from "bun" +import fs from "fs" +import path from "path" import { fileURLToPath } from "url" +import solidPlugin from "../node_modules/@opentui/solid/scripts/solid-plugin" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -12,8 +12,9 @@ const dir = path.resolve(__dirname, "..") process.chdir(dir) -import pkg from "../package.json" import { Script } from "@opencode-ai/script" +import pkg from "../package.json" + const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev" // Fetch and generate models.dev snapshot const modelsData = process.env.MODELS_DEV_API_JSON @@ -26,7 +27,11 @@ 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 })) +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() @@ -171,7 +176,6 @@ for (const item of targets) { 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, @@ -214,7 +218,7 @@ if (Script.release) { await $`zip -r ../../${key}.zip *`.cwd(`dist/${key}/bin`) } } - await $`gh release upload v${Script.version} ./dist/*.zip ./dist/*.tar.gz --clobber` + await $`gh release upload v${Script.version} ./dist/*.zip ./dist/*.tar.gz --clobber --repo ${process.env.GH_REPO}` } export { binaries } diff --git a/packages/opencode/script/postinstall.mjs b/packages/opencode/script/postinstall.mjs index e8b5e995ccfa..98f23e16fb5d 100644 --- a/packages/opencode/script/postinstall.mjs +++ b/packages/opencode/script/postinstall.mjs @@ -109,8 +109,14 @@ async function main() { // On non-Windows platforms, just verify the binary package exists // Don't replace the wrapper script - it handles binary execution const { binaryPath } = findBinary() - console.log(`Platform binary verified at: ${binaryPath}`) - console.log("Wrapper script will handle binary execution") + const target = path.join(__dirname, "bin", ".opencode") + if (fs.existsSync(target)) fs.unlinkSync(target) + try { + fs.linkSync(binaryPath, target) + } catch { + fs.copyFileSync(binaryPath, target) + } + fs.chmodSync(target, 0o755) } catch (error) { console.error("Failed to setup opencode binary:", error.message) process.exit(1) diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index 9e28ea16cf74..672e73d49a97 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -450,6 +450,7 @@ export const GithubRunCommand = cmd({ const isWorkflowDispatchEvent = context.eventName === "workflow_dispatch" const { providerID, modelID } = normalizeModel() + const variant = process.env["VARIANT"] || undefined const runId = normalizeRunId() const share = normalizeShare() const oidcBaseUrl = normalizeOidcBaseUrl() @@ -912,6 +913,7 @@ export const GithubRunCommand = cmd({ const result = await SessionPrompt.prompt({ sessionID: session.id, messageID: Identifier.ascending("message"), + variant, model: { providerID, modelID, @@ -965,6 +967,7 @@ export const GithubRunCommand = cmd({ const summary = await SessionPrompt.prompt({ sessionID: session.id, messageID: Identifier.ascending("message"), + variant, model: { providerID, modelID, diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 31188471991c..aad0fd76c4be 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -292,7 +292,9 @@ export namespace Config { ...(proxied() ? ["--no-cache"] : []), ], { cwd: dir }, - ).catch(() => {}) + ).catch((err) => { + log.warn("failed to install dependencies", { dir, error: err }) + }) } async function isWritable(dir: string) { diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 24dc695d6350..e65d21bfd607 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -41,8 +41,10 @@ export namespace Plugin { for (const plugin of INTERNAL_PLUGINS) { log.info("loading internal plugin", { name: plugin.name }) - const init = await plugin(input) - hooks.push(init) + const init = await plugin(input).catch((err) => { + log.error("failed to load internal plugin", { name: plugin.name, error: err }) + }) + if (init) hooks.push(init) } let plugins = config.plugin ?? [] @@ -59,37 +61,40 @@ export namespace Plugin { const lastAtIndex = plugin.lastIndexOf("@") const pkg = lastAtIndex > 0 ? plugin.substring(0, lastAtIndex) : plugin const version = lastAtIndex > 0 ? plugin.substring(lastAtIndex + 1) : "latest" - const builtin = BUILTIN.some((x) => x.startsWith(pkg + "@")) plugin = await BunProc.install(pkg, version).catch((err) => { - if (!builtin) throw err - - const message = err instanceof Error ? err.message : String(err) - log.error("failed to install builtin plugin", { - pkg, - version, - error: message, - }) + const cause = err instanceof Error ? err.cause : err + const detail = cause instanceof Error ? cause.message : String(cause ?? err) + log.error("failed to install plugin", { pkg, version, error: detail }) Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ - message: `Failed to install built-in plugin ${pkg}@${version}: ${message}`, + message: `Failed to install plugin ${pkg}@${version}: ${detail}`, }).toObject(), }) - return "" }) if (!plugin) continue } - const mod = await import(plugin) // Prevent duplicate initialization when plugins export the same function // as both a named export and default export (e.g., `export const X` and `export default X`). // Object.entries(mod) would return both entries pointing to the same function reference. - const seen = new Set() - for (const [_name, fn] of Object.entries(mod)) { - if (seen.has(fn)) continue - seen.add(fn) - const init = await fn(input) - hooks.push(init) - } + await import(plugin) + .then(async (mod) => { + const seen = new Set() + for (const [_name, fn] of Object.entries(mod)) { + if (seen.has(fn)) continue + seen.add(fn) + hooks.push(await fn(input)) + } + }) + .catch((err) => { + const message = err instanceof Error ? err.message : String(err) + log.error("failed to load plugin", { path: plugin, error: message }) + Bus.publish(Session.Event.Error, { + error: new NamedError.Unknown({ + message: `Failed to load plugin ${plugin}: ${message}`, + }).toObject(), + }) + }) } return { diff --git a/packages/opencode/src/pty/index.ts b/packages/opencode/src/pty/index.ts index 2dda403e1404..33083485b5f6 100644 --- a/packages/opencode/src/pty/index.ts +++ b/packages/opencode/src/pty/index.ts @@ -41,13 +41,38 @@ export namespace Pty { const token = (ws: Socket) => { const data = ws.data - if (!data || typeof data !== "object") return + if (data === undefined) return + if (data === null) return + if (typeof data !== "object") return data - const events = (data as { events?: unknown }).events - if (events && typeof events === "object") return events + const id = (data as { connId?: unknown }).connId + if (typeof id === "number" || typeof id === "string") return id + + const href = (data as { href?: unknown }).href + if (typeof href === "string") return href const url = (data as { url?: unknown }).url - if (url && typeof url === "object") return url + if (typeof url === "string") return url + if (url && typeof url === "object") { + const href = (url as { href?: unknown }).href + if (typeof href === "string") return href + return url + } + + const events = (data as { events?: unknown }).events + if (typeof events === "number" || typeof events === "string") return events + if (events && typeof events === "object") { + const id = (events as { connId?: unknown }).connId + if (typeof id === "number" || typeof id === "string") return id + + const id2 = (events as { connection?: unknown }).connection + if (typeof id2 === "number" || typeof id2 === "string") return id2 + + const id3 = (events as { id?: unknown }).id + if (typeof id3 === "number" || typeof id3 === "string") return id3 + + return events + } return data } @@ -210,7 +235,7 @@ export namespace Pty { continue } - if (sub.token !== undefined && token(ws) !== sub.token) { + if (token(ws) !== sub.token) { session.subscribers.delete(ws) continue } diff --git a/packages/opencode/src/server/routes/experimental.ts b/packages/opencode/src/server/routes/experimental.ts index 3c28331bd529..8d156c03d811 100644 --- a/packages/opencode/src/server/routes/experimental.ts +++ b/packages/opencode/src/server/routes/experimental.ts @@ -6,6 +6,7 @@ import { Worktree } from "../../worktree" import { Instance } from "../../project/instance" import { Project } from "../../project/project" import { MCP } from "../../mcp" +import { Session } from "../../session" import { zodToJsonSchema } from "zod-to-json-schema" import { errors } from "../error" import { lazy } from "../../util/lazy" @@ -184,6 +185,65 @@ export const ExperimentalRoutes = lazy(() => return c.json(true) }, ) + .get( + "/session", + describeRoute({ + summary: "List sessions", + description: + "Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.", + operationId: "experimental.session.list", + responses: { + 200: { + description: "List of sessions", + content: { + "application/json": { + schema: resolver(Session.GlobalInfo.array()), + }, + }, + }, + }, + }), + validator( + "query", + z.object({ + directory: z.string().optional().meta({ description: "Filter sessions by project directory" }), + roots: z.coerce.boolean().optional().meta({ description: "Only return root sessions (no parentID)" }), + start: z.coerce + .number() + .optional() + .meta({ description: "Filter sessions updated on or after this timestamp (milliseconds since epoch)" }), + cursor: z.coerce + .number() + .optional() + .meta({ description: "Return sessions updated before this timestamp (milliseconds since epoch)" }), + search: z.string().optional().meta({ description: "Filter sessions by title (case-insensitive)" }), + limit: z.coerce.number().optional().meta({ description: "Maximum number of sessions to return" }), + archived: z.coerce.boolean().optional().meta({ description: "Include archived sessions (default false)" }), + }), + ), + async (c) => { + const query = c.req.valid("query") + const limit = query.limit ?? 100 + const sessions: Session.GlobalInfo[] = [] + for await (const session of Session.listGlobal({ + directory: query.directory, + roots: query.roots, + start: query.start, + cursor: query.cursor, + search: query.search, + limit: limit + 1, + archived: query.archived, + })) { + sessions.push(session) + } + const hasMore = sessions.length > limit + const list = hasMore ? sessions.slice(0, limit) : sessions + if (hasMore && list.length > 0) { + c.header("x-next-cursor", String(list[list.length - 1].time.updated)) + } + return c.json(list) + }, + ) .get( "/resource", describeRoute({ diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index b07a049c80d7..8454a9c3e975 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -10,8 +10,10 @@ import { Flag } from "../flag/flag" import { Identifier } from "../id/id" import { Installation } from "../installation" -import { Database, NotFoundError, eq, and, or, gte, isNull, desc, like } from "../storage/db" +import { Database, NotFoundError, eq, and, or, gte, isNull, desc, like, inArray, lt } from "../storage/db" +import type { SQL } from "../storage/db" import { SessionTable, MessageTable, PartTable } from "./session.sql" +import { ProjectTable } from "../project/project.sql" import { Storage } from "@/storage/storage" import { Log } from "../util/log" import { MessageV2 } from "./message-v2" @@ -154,6 +156,24 @@ export namespace Session { }) export type Info = z.output + export const ProjectInfo = z + .object({ + id: z.string(), + name: z.string().optional(), + worktree: z.string(), + }) + .meta({ + ref: "ProjectSummary", + }) + export type ProjectInfo = z.output + + export const GlobalInfo = Info.extend({ + project: ProjectInfo.nullable(), + }).meta({ + ref: "GlobalSession", + }) + export type GlobalInfo = z.output + export const Event = { Created: BusEvent.define( "session.created", @@ -544,6 +564,75 @@ export namespace Session { } } + export function* listGlobal(input?: { + directory?: string + roots?: boolean + start?: number + cursor?: number + search?: string + limit?: number + archived?: boolean + }) { + const conditions: SQL[] = [] + + if (input?.directory) { + conditions.push(eq(SessionTable.directory, input.directory)) + } + if (input?.roots) { + conditions.push(isNull(SessionTable.parent_id)) + } + if (input?.start) { + conditions.push(gte(SessionTable.time_updated, input.start)) + } + if (input?.cursor) { + conditions.push(lt(SessionTable.time_updated, input.cursor)) + } + if (input?.search) { + conditions.push(like(SessionTable.title, `%${input.search}%`)) + } + if (!input?.archived) { + conditions.push(isNull(SessionTable.time_archived)) + } + + const limit = input?.limit ?? 100 + + const rows = Database.use((db) => { + const query = + conditions.length > 0 + ? db + .select() + .from(SessionTable) + .where(and(...conditions)) + : db.select().from(SessionTable) + return query.orderBy(desc(SessionTable.time_updated), desc(SessionTable.id)).limit(limit).all() + }) + + const ids = [...new Set(rows.map((row) => row.project_id))] + const projects = new Map() + + if (ids.length > 0) { + const items = Database.use((db) => + db + .select({ id: ProjectTable.id, name: ProjectTable.name, worktree: ProjectTable.worktree }) + .from(ProjectTable) + .where(inArray(ProjectTable.id, ids)) + .all(), + ) + for (const item of items) { + projects.set(item.id, { + id: item.id, + name: item.name ?? undefined, + worktree: item.worktree, + }) + } + } + + for (const row of rows) { + const project = projects.get(row.project_id) ?? null + yield { ...fromRow(row), project } + } + } + export const children = fn(Identifier.schema("session"), async (parentID) => { const project = Instance.project const rows = Database.use((db) => diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index a1c2b57812e8..83cc467e423b 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -66,7 +66,7 @@ export namespace Snapshot { await $`git --git-dir ${git} config core.autocrlf false`.quiet().nothrow() log.info("initialized") } - await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow() + await add(git) const hash = await $`git --git-dir ${git} --work-tree ${Instance.worktree} write-tree` .quiet() .cwd(Instance.directory) @@ -84,7 +84,7 @@ export namespace Snapshot { export async function patch(hash: string): Promise { const git = gitdir() - await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow() + await add(git) const result = await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-only ${hash} -- .` .quiet() @@ -162,7 +162,7 @@ export namespace Snapshot { export async function diff(hash: string) { const git = gitdir() - await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow() + await add(git) const result = await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff ${hash} -- .` .quiet() @@ -253,4 +253,38 @@ export namespace Snapshot { const project = Instance.project return path.join(Global.Path.data, "snapshot", project.id) } + + async function add(git: string) { + await syncExclude(git) + await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow() + } + + async function syncExclude(git: string) { + const file = await excludes() + const target = path.join(git, "info", "exclude") + await fs.mkdir(path.join(git, "info"), { recursive: true }) + if (!file) { + await Bun.write(target, "") + return + } + const text = await Bun.file(file) + .text() + .catch(() => "") + await Bun.write(target, text) + } + + async function excludes() { + const file = await $`git rev-parse --path-format=absolute --git-path info/exclude` + .quiet() + .cwd(Instance.worktree) + .nothrow() + .text() + if (!file.trim()) return + const exists = await fs + .stat(file.trim()) + .then(() => true) + .catch(() => false) + if (!exists) return + return file.trim() + } } diff --git a/packages/opencode/test/pty/pty-output-isolation.test.ts b/packages/opencode/test/pty/pty-output-isolation.test.ts index 1b89a63742ae..07e86ea97b67 100644 --- a/packages/opencode/test/pty/pty-output-isolation.test.ts +++ b/packages/opencode/test/pty/pty-output-isolation.test.ts @@ -97,4 +97,48 @@ describe("pty", () => { }, }) }) + + test("does not leak output when socket data mutates in-place", async () => { + await using dir = await tmpdir({ git: true }) + + await Instance.provide({ + directory: dir.path, + fn: async () => { + const a = await Pty.create({ command: "cat", title: "a" }) + try { + const outA: string[] = [] + const outB: string[] = [] + + const ctx = { connId: 1 } + const ws = { + readyState: 1, + data: ctx, + send: (data: unknown) => { + outA.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8")) + }, + close: () => { + // no-op + }, + } + + Pty.connect(a.id, ws as any) + outA.length = 0 + + // Simulate the runtime mutating per-connection data without + // swapping the reference (ws.data stays the same object). + ctx.connId = 2 + ws.send = (data: unknown) => { + outB.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8")) + } + + Pty.write(a.id, "AAA\n") + await Bun.sleep(100) + + expect(outB.join("")).not.toContain("AAA") + } finally { + await Pty.remove(a.id) + } + }, + }) + }) }) diff --git a/packages/opencode/test/server/global-session-list.test.ts b/packages/opencode/test/server/global-session-list.test.ts new file mode 100644 index 000000000000..05d6de04b1b1 --- /dev/null +++ b/packages/opencode/test/server/global-session-list.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, test } from "bun:test" +import { Instance } from "../../src/project/instance" +import { Project } from "../../src/project/project" +import { Session } from "../../src/session" +import { Log } from "../../src/util/log" +import { tmpdir } from "../fixture/fixture" + +Log.init({ print: false }) + +describe("Session.listGlobal", () => { + test("lists sessions across projects with project metadata", async () => { + await using first = await tmpdir({ git: true }) + await using second = await tmpdir({ git: true }) + + const firstSession = await Instance.provide({ + directory: first.path, + fn: async () => Session.create({ title: "first-session" }), + }) + const secondSession = await Instance.provide({ + directory: second.path, + fn: async () => Session.create({ title: "second-session" }), + }) + + const sessions = [...Session.listGlobal({ limit: 200 })] + const ids = sessions.map((session) => session.id) + + expect(ids).toContain(firstSession.id) + expect(ids).toContain(secondSession.id) + + const firstProject = Project.get(firstSession.projectID) + const secondProject = Project.get(secondSession.projectID) + + const firstItem = sessions.find((session) => session.id === firstSession.id) + const secondItem = sessions.find((session) => session.id === secondSession.id) + + expect(firstItem?.project?.id).toBe(firstProject?.id) + expect(firstItem?.project?.worktree).toBe(firstProject?.worktree) + expect(secondItem?.project?.id).toBe(secondProject?.id) + expect(secondItem?.project?.worktree).toBe(secondProject?.worktree) + }) + + test("excludes archived sessions by default", async () => { + await using tmp = await tmpdir({ git: true }) + + const archived = await Instance.provide({ + directory: tmp.path, + fn: async () => Session.create({ title: "archived-session" }), + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => Session.setArchived({ sessionID: archived.id, time: Date.now() }), + }) + + const sessions = [...Session.listGlobal({ limit: 200 })] + const ids = sessions.map((session) => session.id) + + expect(ids).not.toContain(archived.id) + + const allSessions = [...Session.listGlobal({ limit: 200, archived: true })] + const allIds = allSessions.map((session) => session.id) + + expect(allIds).toContain(archived.id) + }) + + test("supports cursor pagination", async () => { + await using tmp = await tmpdir({ git: true }) + + const first = await Instance.provide({ + directory: tmp.path, + fn: async () => Session.create({ title: "page-one" }), + }) + await new Promise((resolve) => setTimeout(resolve, 5)) + const second = await Instance.provide({ + directory: tmp.path, + fn: async () => Session.create({ title: "page-two" }), + }) + + const page = [...Session.listGlobal({ directory: tmp.path, limit: 1 })] + expect(page.length).toBe(1) + expect(page[0].id).toBe(second.id) + + const next = [...Session.listGlobal({ directory: tmp.path, limit: 10, cursor: page[0].time.updated })] + const ids = next.map((session) => session.id) + + expect(ids).toContain(first.id) + expect(ids).not.toContain(second.id) + }) +}) diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index b54cb8b8a650..9a0622c4a5a1 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -508,6 +508,68 @@ test("gitignore changes", async () => { }) }) +test("git info exclude changes", async () => { + await using tmp = await bootstrap() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const before = await Snapshot.track() + expect(before).toBeTruthy() + + const file = `${tmp.path}/.git/info/exclude` + const text = await Bun.file(file).text() + await Bun.write(file, `${text.trimEnd()}\nignored.txt\n`) + await Bun.write(`${tmp.path}/ignored.txt`, "ignored content") + await Bun.write(`${tmp.path}/normal.txt`, "normal content") + + const patch = await Snapshot.patch(before!) + expect(patch.files).toContain(`${tmp.path}/normal.txt`) + expect(patch.files).not.toContain(`${tmp.path}/ignored.txt`) + + const after = await Snapshot.track() + const diffs = await Snapshot.diffFull(before!, after!) + expect(diffs.some((x) => x.file === "normal.txt")).toBe(true) + expect(diffs.some((x) => x.file === "ignored.txt")).toBe(false) + }, + }) +}) + +test("git info exclude keeps global excludes", async () => { + await using tmp = await bootstrap() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const global = `${tmp.path}/global.ignore` + const config = `${tmp.path}/global.gitconfig` + await Bun.write(global, "global.tmp\n") + await Bun.write(config, `[core]\n\texcludesFile = ${global}\n`) + + const prev = process.env.GIT_CONFIG_GLOBAL + process.env.GIT_CONFIG_GLOBAL = config + try { + const before = await Snapshot.track() + expect(before).toBeTruthy() + + const file = `${tmp.path}/.git/info/exclude` + const text = await Bun.file(file).text() + await Bun.write(file, `${text.trimEnd()}\ninfo.tmp\n`) + + await Bun.write(`${tmp.path}/global.tmp`, "global content") + await Bun.write(`${tmp.path}/info.tmp`, "info content") + await Bun.write(`${tmp.path}/normal.txt`, "normal content") + + const patch = await Snapshot.patch(before!) + expect(patch.files).toContain(`${tmp.path}/normal.txt`) + expect(patch.files).not.toContain(`${tmp.path}/global.tmp`) + expect(patch.files).not.toContain(`${tmp.path}/info.tmp`) + } finally { + if (prev) process.env.GIT_CONFIG_GLOBAL = prev + else delete process.env.GIT_CONFIG_GLOBAL + } + }, + }) +}) + test("concurrent file operations during patch", async () => { await using tmp = await bootstrap() await Instance.provide({ diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 64c34e3f59db..623a117929f7 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.2.9", + "version": "1.2.10", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 0f71b281d059..4fe0794d0cea 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.2.9", + "version": "1.2.10", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index af79c44a17a7..b4848e605404 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -25,6 +25,7 @@ import type { EventTuiSessionSelect, EventTuiToastShow, ExperimentalResourceListResponses, + ExperimentalSessionListResponses, FileListResponses, FilePartInput, FilePartSource, @@ -898,6 +899,48 @@ export class Worktree extends HeyApiClient { } } +export class Session extends HeyApiClient { + /** + * List sessions + * + * Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default. + */ + public list( + parameters?: { + directory?: string + roots?: boolean + start?: number + cursor?: number + search?: string + limit?: number + archived?: boolean + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "roots" }, + { in: "query", key: "start" }, + { in: "query", key: "cursor" }, + { in: "query", key: "search" }, + { in: "query", key: "limit" }, + { in: "query", key: "archived" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/session", + ...options, + ...params, + }) + } +} + export class Resource extends HeyApiClient { /** * Get MCP resources @@ -920,13 +963,18 @@ export class Resource extends HeyApiClient { } export class Experimental extends HeyApiClient { + private _session?: Session + get session(): Session { + return (this._session ??= new Session({ client: this.client })) + } + private _resource?: Resource get resource(): Resource { return (this._resource ??= new Resource({ client: this.client })) } } -export class Session extends HeyApiClient { +export class Session2 extends HeyApiClient { /** * List sessions * @@ -3231,9 +3279,9 @@ export class OpencodeClient extends HeyApiClient { return (this._experimental ??= new Experimental({ client: this.client })) } - private _session?: Session - get session(): Session { - return (this._session ??= new Session({ client: this.client })) + private _session?: Session2 + get session(): Session2 { + return (this._session ??= new Session2({ client: this.client })) } private _part?: Part diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index efb7e202e120..4050ef15738c 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2044,6 +2044,45 @@ export type WorktreeResetInput = { directory: string } +export type ProjectSummary = { + id: string + name?: string + worktree: string +} + +export type GlobalSession = { + id: string + slug: string + projectID: string + directory: string + parentID?: string + summary?: { + additions: number + deletions: number + files: number + diffs?: Array + } + share?: { + url: string + } + title: string + version: string + time: { + created: number + updated: number + compacting?: number + archived?: number + } + permission?: PermissionRuleset + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } + project: ProjectSummary | null +} + export type McpResource = { name: string uri: string @@ -2870,6 +2909,51 @@ export type WorktreeResetResponses = { export type WorktreeResetResponse = WorktreeResetResponses[keyof WorktreeResetResponses] +export type ExperimentalSessionListData = { + body?: never + path?: never + query?: { + /** + * Filter sessions by project directory + */ + directory?: string + /** + * Only return root sessions (no parentID) + */ + roots?: boolean + /** + * Filter sessions updated on or after this timestamp (milliseconds since epoch) + */ + start?: number + /** + * Return sessions updated before this timestamp (milliseconds since epoch) + */ + cursor?: number + /** + * Filter sessions by title (case-insensitive) + */ + search?: string + /** + * Maximum number of sessions to return + */ + limit?: number + /** + * Include archived sessions (default false) + */ + archived?: boolean + } + url: "/experimental/session" +} + +export type ExperimentalSessionListResponses = { + /** + * List of sessions + */ + 200: Array +} + +export type ExperimentalSessionListResponse = ExperimentalSessionListResponses[keyof ExperimentalSessionListResponses] + export type ExperimentalResourceListData = { body?: never path?: never diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 85a1af9d70cc..2741c2362ec4 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -1202,6 +1202,92 @@ ] } }, + "/experimental/session": { + "get": { + "operationId": "experimental.session.list", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + }, + "description": "Filter sessions by project directory" + }, + { + "in": "query", + "name": "roots", + "schema": { + "type": "boolean" + }, + "description": "Only return root sessions (no parentID)" + }, + { + "in": "query", + "name": "start", + "schema": { + "type": "number" + }, + "description": "Filter sessions updated on or after this timestamp (milliseconds since epoch)" + }, + { + "in": "query", + "name": "cursor", + "schema": { + "type": "number" + }, + "description": "Return sessions updated before this timestamp (milliseconds since epoch)" + }, + { + "in": "query", + "name": "search", + "schema": { + "type": "string" + }, + "description": "Filter sessions by title (case-insensitive)" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number" + }, + "description": "Maximum number of sessions to return" + }, + { + "in": "query", + "name": "archived", + "schema": { + "type": "boolean" + }, + "description": "Include archived sessions (default false)" + } + ], + "summary": "List sessions", + "description": "Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.", + "responses": { + "200": { + "description": "List of sessions", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GlobalSession" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.session.list({\n ...\n})" + } + ] + } + }, "/experimental/resource": { "get": { "operationId": "experimental.resource.list", @@ -10499,6 +10585,129 @@ }, "required": ["directory"] }, + "ProjectSummary": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "worktree": { + "type": "string" + } + }, + "required": ["id", "worktree"] + }, + "GlobalSession": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^ses.*" + }, + "slug": { + "type": "string" + }, + "projectID": { + "type": "string" + }, + "directory": { + "type": "string" + }, + "parentID": { + "type": "string", + "pattern": "^ses.*" + }, + "summary": { + "type": "object", + "properties": { + "additions": { + "type": "number" + }, + "deletions": { + "type": "number" + }, + "files": { + "type": "number" + }, + "diffs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileDiff" + } + } + }, + "required": ["additions", "deletions", "files"] + }, + "share": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "required": ["url"] + }, + "title": { + "type": "string" + }, + "version": { + "type": "string" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "updated": { + "type": "number" + }, + "compacting": { + "type": "number" + }, + "archived": { + "type": "number" + } + }, + "required": ["created", "updated"] + }, + "permission": { + "$ref": "#/components/schemas/PermissionRuleset" + }, + "revert": { + "type": "object", + "properties": { + "messageID": { + "type": "string" + }, + "partID": { + "type": "string" + }, + "snapshot": { + "type": "string" + }, + "diff": { + "type": "string" + } + }, + "required": ["messageID"] + }, + "project": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectSummary" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "slug", "projectID", "directory", "title", "version", "time", "project"] + }, "McpResource": { "type": "object", "properties": { diff --git a/packages/slack/package.json b/packages/slack/package.json index 2675833f4c7a..d000cb479943 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.2.9", + "version": "1.2.10", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 4e70f7a810a7..3519996085d7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.2.9", + "version": "1.2.10", "type": "module", "license": "MIT", "exports": { diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index 3415c034cf18..07a718141a9b 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -490,8 +490,10 @@ } [data-component="edit-content"] { + border-radius: inherit; border-top: 1px solid var(--border-weaker-base); max-height: 420px; + overflow-x: hidden; overflow-y: auto; scrollbar-width: none; @@ -500,15 +502,24 @@ &::-webkit-scrollbar { display: none; } + + [data-component="diff"] { + border-radius: inherit; + overflow: hidden; + } } [data-component="write-content"] { + border-radius: inherit; border-top: 1px solid var(--border-weaker-base); max-height: 240px; + overflow-x: hidden; overflow-y: auto; [data-component="code"] { - padding-bottom: 0px !important; + padding-bottom: 0 !important; + border-radius: inherit; + overflow: hidden; } /* Hide scrollbar */ @@ -1285,6 +1296,8 @@ } [data-component="apply-patch-file-diff"] { + border-radius: inherit; + overflow-x: hidden; overflow-y: auto; scrollbar-width: none; -ms-overflow-style: none; @@ -1292,6 +1305,11 @@ &::-webkit-scrollbar { display: none; } + + [data-component="diff"] { + border-radius: inherit; + overflow: hidden; + } } [data-component="tool-loaded-file"] { diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 4b223bf35a74..828ddbe87d8e 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -96,6 +96,7 @@ export interface MessageProps { parts: PartType[] showAssistantCopyPartID?: string | null interrupted?: boolean + showReasoningSummaries?: boolean } export interface MessagePartProps { @@ -104,6 +105,7 @@ export interface MessagePartProps { hideDetails?: boolean defaultOpen?: boolean showAssistantCopyPartID?: string | null + turnDurationMs?: number } export type PartComponent = Component @@ -149,6 +151,8 @@ function createThrottledValue(getValue: () => string) { function relativizeProjectPaths(text: string, directory?: string) { if (!text) return "" if (!directory) return text + if (directory === "/") return text + if (directory === "\\") return text return text.split(directory).join("") } @@ -261,21 +265,23 @@ function list(value: T[] | undefined | null, fallback: T[]) { return fallback } -function renderable(part: PartType) { +function renderable(part: PartType, showReasoningSummaries = true) { if (part.type === "tool") { if (HIDDEN_TOOLS.has(part.tool)) return false if (part.tool === "question") return part.state.status !== "pending" && part.state.status !== "running" return true } if (part.type === "text") return !!part.text?.trim() - if (part.type === "reasoning") return !!part.text?.trim() + if (part.type === "reasoning") return showReasoningSummaries && !!part.text?.trim() return !!PART_MAPPING[part.type] } export function AssistantParts(props: { messages: AssistantMessage[] showAssistantCopyPartID?: string | null + turnDurationMs?: number working?: boolean + showReasoningSummaries?: boolean }) { const data = useData() const emptyParts: PartType[] = [] @@ -296,7 +302,7 @@ export function AssistantParts(props: { const parts = props.messages.flatMap((message) => list(data.store.part?.[message.id], emptyParts) - .filter(renderable) + .filter((part) => renderable(part, props.showReasoningSummaries ?? true)) .map((part) => ({ message, part })), ) @@ -365,6 +371,7 @@ export function AssistantParts(props: { part={entry().part} message={entry().message} showAssistantCopyPartID={props.showAssistantCopyPartID} + turnDurationMs={props.turnDurationMs} /> )} @@ -475,6 +482,7 @@ export function Message(props: MessageProps) { message={assistantMessage() as AssistantMessage} parts={props.parts} showAssistantCopyPartID={props.showAssistantCopyPartID} + showReasoningSummaries={props.showReasoningSummaries} /> )} @@ -486,6 +494,7 @@ export function AssistantMessageDisplay(props: { message: AssistantMessage parts: PartType[] showAssistantCopyPartID?: string | null + showReasoningSummaries?: boolean }) { const grouped = createMemo(() => { const keys: string[] = [] @@ -514,7 +523,7 @@ export function AssistantMessageDisplay(props: { } parts.forEach((part, index) => { - if (!renderable(part)) return + if (!renderable(part, props.showReasoningSummaries ?? true)) return if (isContextGroupTool(part)) { if (start < 0) start = index @@ -849,6 +858,7 @@ export function Part(props: MessagePartProps) { hideDetails={props.hideDetails} defaultOpen={props.defaultOpen} showAssistantCopyPartID={props.showAssistantCopyPartID} + turnDurationMs={props.turnDurationMs} /> ) @@ -1060,8 +1070,12 @@ PART_MAPPING["text"] = function TextPartDisplay(props) { if (props.message.role !== "assistant") return "" const message = props.message as AssistantMessage const completed = message.time.completed - if (typeof completed !== "number") return "" - const ms = completed - message.time.created + const ms = + typeof props.turnDurationMs === "number" + ? props.turnDurationMs + : typeof completed === "number" + ? completed - message.time.created + : -1 if (!(ms >= 0)) return "" const total = Math.round(ms / 1000) if (total < 60) return `${total}s` @@ -1593,6 +1607,12 @@ ToolRegistry.register({ const i18n = useI18n() const diffComponent = useDiffComponent() const files = createMemo(() => (props.metadata.files ?? []) as ApplyPatchFile[]) + const pending = createMemo(() => props.status === "pending" || props.status === "running") + const single = createMemo(() => { + const list = files() + if (list.length !== 1) return + return list[0] + }) const [expanded, setExpanded] = createSignal([]) let seeded = false @@ -1611,100 +1631,147 @@ ToolRegistry.register({ }) return ( -
- - 0}> - setExpanded(Array.isArray(value) ? value : value ? [value] : [])} + + - - {(file) => { - const active = createMemo(() => expanded().includes(file.filePath)) - const [visible, setVisible] = createSignal(false) - - createEffect(() => { - if (!active()) { - setVisible(false) - return - } - - requestAnimationFrame(() => { - if (!active()) return - setVisible(true) - }) - }) - - return ( - - - -
-
- -
- - {`\u202A${getDirectory(file.relativePath)}\u202C`} - - {getFilename(file.relativePath)} + 0}> + setExpanded(Array.isArray(value) ? value : value ? [value] : [])} + > + + {(file) => { + const active = createMemo(() => expanded().includes(file.filePath)) + const [visible, setVisible] = createSignal(false) + + createEffect(() => { + if (!active()) { + setVisible(false) + return + } + + requestAnimationFrame(() => { + if (!active()) return + setVisible(true) + }) + }) + + return ( + + + +
+
+ +
+ + {`\u202A${getDirectory(file.relativePath)}\u202C`} + + {getFilename(file.relativePath)} +
+
+
+ + + + {i18n.t("ui.patch.action.created")} + + + + + {i18n.t("ui.patch.action.deleted")} + + + + + {i18n.t("ui.patch.action.moved")} + + + + + + + +
-
-
- - - - {i18n.t("ui.patch.action.created")} - - - - - {i18n.t("ui.patch.action.deleted")} - - - - - {i18n.t("ui.patch.action.moved")} - - - - - - - -
-
- - - - -
- -
-
-
- - ) - }} - - - - -
+
+
+ + +
+ +
+
+
+
+ ) + }} +
+
+
+
+
+ } + > + {(file) => ( + +
+
+ + + + + + + {getFilename(file().relativePath)} + +
+ +
+ {getDirectory(file().relativePath)} +
+
+
+
+ + + +
+
+ } + > +
+ +
+ + )} +
) }, }) diff --git a/packages/ui/src/components/scroll-view.css b/packages/ui/src/components/scroll-view.css new file mode 100644 index 000000000000..f81ae2976644 --- /dev/null +++ b/packages/ui/src/components/scroll-view.css @@ -0,0 +1,61 @@ +.scroll-view { + position: relative; + overflow: hidden; +} + +.scroll-view__viewport { + height: 100%; + width: 100%; + overflow-y: auto; + scrollbar-width: none; + outline: none; +} + +.scroll-view__viewport::-webkit-scrollbar { + display: none; +} + +.scroll-view__thumb { + position: absolute; + right: 0; + top: 0; + width: 16px; + transition: opacity 200ms ease; + cursor: default; + user-select: none; + opacity: 0; +} + +.scroll-view__thumb::after { + content: ""; + position: absolute; + right: 4px; + top: 0; + bottom: 0; + width: 6px; + border-radius: 9999px; + background-color: var(--border-weak-base); + backdrop-filter: blur(4px); + transition: background-color 150ms ease; +} + +.scroll-view__thumb:hover::after, +.scroll-view__thumb[data-dragging="true"]::after { + background-color: var(--border-strong-base); +} + +.dark .scroll-view__thumb::after, +[data-theme="dark"] .scroll-view__thumb::after { + background-color: var(--border-weak-base); +} + +.dark .scroll-view__thumb:hover::after, +[data-theme="dark"] .scroll-view__thumb:hover::after, +.dark .scroll-view__thumb[data-dragging="true"]::after, +[data-theme="dark"] .scroll-view__thumb[data-dragging="true"]::after { + background-color: var(--border-strong-base); +} + +.scroll-view__thumb[data-visible="true"] { + opacity: 1; +} diff --git a/packages/ui/src/components/scroll-view.tsx b/packages/ui/src/components/scroll-view.tsx new file mode 100644 index 000000000000..acc54c8c3e0e --- /dev/null +++ b/packages/ui/src/components/scroll-view.tsx @@ -0,0 +1,217 @@ +import { createSignal, onCleanup, onMount, splitProps, type ComponentProps, Show, mergeProps } from "solid-js" + +export interface ScrollViewProps extends ComponentProps<"div"> { + viewportRef?: (el: HTMLDivElement) => void + orientation?: "vertical" | "horizontal" // currently only vertical is fully implemented for thumb +} + +export function ScrollView(props: ScrollViewProps) { + const merged = mergeProps({ orientation: "vertical" }, props) + const [local, events, rest] = splitProps( + merged, + ["class", "children", "viewportRef", "orientation", "style"], + [ + "onScroll", + "onWheel", + "onTouchStart", + "onTouchMove", + "onTouchEnd", + "onTouchCancel", + "onPointerDown", + "onClick", + "onKeyDown", + ], + ) + + let rootRef!: HTMLDivElement + let viewportRef!: HTMLDivElement + let thumbRef!: HTMLDivElement + + const [isHovered, setIsHovered] = createSignal(false) + const [isDragging, setIsDragging] = createSignal(false) + + const [thumbHeight, setThumbHeight] = createSignal(0) + const [thumbTop, setThumbTop] = createSignal(0) + const [showThumb, setShowThumb] = createSignal(false) + + const updateThumb = () => { + if (!viewportRef) return + const { scrollTop, scrollHeight, clientHeight } = viewportRef + + if (scrollHeight <= clientHeight || scrollHeight === 0) { + setShowThumb(false) + return + } + + setShowThumb(true) + const trackPadding = 8 + const trackHeight = clientHeight - trackPadding * 2 + + const minThumbHeight = 32 + // Calculate raw thumb height based on ratio + let height = (clientHeight / scrollHeight) * trackHeight + height = Math.max(height, minThumbHeight) + + const maxScrollTop = scrollHeight - clientHeight + const maxThumbTop = trackHeight - height + + const top = maxScrollTop > 0 ? (scrollTop / maxScrollTop) * maxThumbTop : 0 + + // Ensure thumb stays within bounds (shouldn't be necessary due to math above, but good for safety) + const boundedTop = trackPadding + Math.max(0, Math.min(top, maxThumbTop)) + + setThumbHeight(height) + setThumbTop(boundedTop) + } + + onMount(() => { + if (local.viewportRef) { + local.viewportRef(viewportRef) + } + + const observer = new ResizeObserver(() => { + updateThumb() + }) + + observer.observe(viewportRef) + // Also observe the first child if possible to catch content changes + if (viewportRef.firstElementChild) { + observer.observe(viewportRef.firstElementChild) + } + + onCleanup(() => { + observer.disconnect() + }) + + updateThumb() + }) + + let startY = 0 + let startScrollTop = 0 + + const onThumbPointerDown = (e: PointerEvent) => { + e.preventDefault() + e.stopPropagation() + setIsDragging(true) + startY = e.clientY + startScrollTop = viewportRef.scrollTop + + thumbRef.setPointerCapture(e.pointerId) + + const onPointerMove = (e: PointerEvent) => { + const deltaY = e.clientY - startY + const { scrollHeight, clientHeight } = viewportRef + const maxScrollTop = scrollHeight - clientHeight + const maxThumbTop = clientHeight - thumbHeight() + + if (maxThumbTop > 0) { + const scrollDelta = deltaY * (maxScrollTop / maxThumbTop) + viewportRef.scrollTop = startScrollTop + scrollDelta + } + } + + const onPointerUp = (e: PointerEvent) => { + setIsDragging(false) + thumbRef.releasePointerCapture(e.pointerId) + thumbRef.removeEventListener("pointermove", onPointerMove) + thumbRef.removeEventListener("pointerup", onPointerUp) + } + + thumbRef.addEventListener("pointermove", onPointerMove) + thumbRef.addEventListener("pointerup", onPointerUp) + } + + // Keybinds implementation + // We ensure the viewport has a tabindex so it can receive focus + // We can also explicitly catch PageUp/Down if we want smooth scroll or specific behavior, + // but native usually handles this perfectly. Let's explicitly ensure it behaves well. + const onKeyDown = (e: KeyboardEvent) => { + // If user is focused on an input inside the scroll view, don't hijack keys + if (document.activeElement && ["INPUT", "TEXTAREA", "SELECT"].includes(document.activeElement.tagName)) { + return + } + + const scrollAmount = viewportRef.clientHeight * 0.8 + const lineAmount = 40 + + switch (e.key) { + case "PageDown": + e.preventDefault() + viewportRef.scrollBy({ top: scrollAmount, behavior: "smooth" }) + break + case "PageUp": + e.preventDefault() + viewportRef.scrollBy({ top: -scrollAmount, behavior: "smooth" }) + break + case "Home": + e.preventDefault() + viewportRef.scrollTo({ top: 0, behavior: "smooth" }) + break + case "End": + e.preventDefault() + viewportRef.scrollTo({ top: viewportRef.scrollHeight, behavior: "smooth" }) + break + case "ArrowUp": + e.preventDefault() + viewportRef.scrollBy({ top: -lineAmount, behavior: "smooth" }) + break + case "ArrowDown": + e.preventDefault() + viewportRef.scrollBy({ top: lineAmount, behavior: "smooth" }) + break + } + } + + return ( +
setIsHovered(true)} + onPointerLeave={() => setIsHovered(false)} + {...rest} + > + {/* Viewport */} +
{ + updateThumb() + if (typeof events.onScroll === "function") events.onScroll(e as any) + }} + onWheel={events.onWheel as any} + onTouchStart={events.onTouchStart as any} + onTouchMove={events.onTouchMove as any} + onTouchEnd={events.onTouchEnd as any} + onTouchCancel={events.onTouchCancel as any} + onPointerDown={events.onPointerDown as any} + onClick={events.onClick as any} + tabIndex={0} + role="region" + aria-label="scrollable content" + onKeyDown={(e) => { + onKeyDown(e) + if (typeof events.onKeyDown === "function") events.onKeyDown(e as any) + }} + > + {local.children} +
+ + {/* Thumb Overlay */} + +
+ +
+ ) +} diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css index ec1698d29891..b9a2180cb8db 100644 --- a/packages/ui/src/components/session-review.css +++ b/packages/ui/src/components/session-review.css @@ -12,6 +12,7 @@ [data-slot="session-review-container"] { flex: 1 1 auto; + padding-right: 4px; } [data-slot="session-review-header"] { @@ -40,7 +41,6 @@ display: flex; align-items: center; column-gap: 12px; - padding-right: 1px; } [data-slot="session-review-actions"] [data-component="radio-group"] { diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index fd85fb48519a..7f737032e787 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -7,6 +7,7 @@ import { Icon } from "./icon" import { LineComment, LineCommentEditor } from "./line-comment" import { StickyAccordionHeader } from "./sticky-accordion-header" import { Tooltip } from "./tooltip" +import { ScrollView } from "./scroll-view" import { useDiffComponent } from "../context/diff" import { useI18n } from "../context/i18n" import { getDirectory, getFilename } from "@opencode-ai/util/path" @@ -188,8 +189,10 @@ export const SessionReview = (props: SessionReviewProps) => { const [opened, setOpened] = createSignal(null) const open = () => props.open ?? store.open + const files = createMemo(() => props.diffs.map((d) => d.file)) + const diffs = createMemo(() => new Map(props.diffs.map((d) => [d.file, d] as const))) const diffStyle = () => props.diffStyle ?? (props.split ? "split" : "unified") - const hasDiffs = () => props.diffs.length > 0 + const hasDiffs = () => files().length > 0 const handleChange = (open: string[]) => { props.onOpenChange?.(open) @@ -198,7 +201,7 @@ export const SessionReview = (props: SessionReviewProps) => { } const handleExpandOrCollapseAll = () => { - const next = open().length > 0 ? [] : props.diffs.map((d) => d.file) + const next = open().length > 0 ? [] : files() handleChange(next) } @@ -274,13 +277,13 @@ export const SessionReview = (props: SessionReviewProps) => { }) return ( -
{ + viewportRef={(el) => { scroll = el props.scrollRef?.(el) }} - onScroll={props.onScroll} + onScroll={props.onScroll as any} classList={{ ...(props.classList ?? {}), [props.classes?.root ?? ""]: !!props.classes?.root, @@ -321,51 +324,54 @@ export const SessionReview = (props: SessionReviewProps) => {
- - {(diff) => { + + {(file) => { let wrapper: HTMLDivElement | undefined - const expanded = createMemo(() => open().includes(diff.file)) + const diff = createMemo(() => diffs().get(file)) + const item = () => diff()! + + const expanded = createMemo(() => open().includes(file)) const [force, setForce] = createSignal(false) - const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file)) + const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === file)) const commentedLines = createMemo(() => comments().map((c) => c.selection)) - const beforeText = () => (typeof diff.before === "string" ? diff.before : "") - const afterText = () => (typeof diff.after === "string" ? diff.after : "") - const changedLines = () => diff.additions + diff.deletions + const beforeText = () => (typeof item().before === "string" ? item().before : "") + const afterText = () => (typeof item().after === "string" ? item().after : "") + const changedLines = () => item().additions + item().deletions const tooLarge = createMemo(() => { if (!expanded()) return false if (force()) return false - if (isImageFile(diff.file)) return false + if (isImageFile(file)) return false return changedLines() > MAX_DIFF_CHANGED_LINES }) - const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0) + const isAdded = () => item().status === "added" || (beforeText().length === 0 && afterText().length > 0) const isDeleted = () => - diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0) - const isImage = () => isImageFile(diff.file) - const isAudio = () => isAudioFile(diff.file) + item().status === "deleted" || (afterText().length === 0 && beforeText().length > 0) + const isImage = () => isImageFile(file) + const isAudio = () => isAudioFile(file) - const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) - const [imageSrc, setImageSrc] = createSignal(diffImageSrc) + const diffImageSrc = createMemo(() => dataUrlFromValue(item().after) ?? dataUrlFromValue(item().before)) + const [imageSrc, setImageSrc] = createSignal(diffImageSrc()) const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle") - const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) - const [audioSrc, setAudioSrc] = createSignal(diffAudioSrc) + const diffAudioSrc = createMemo(() => dataUrlFromValue(item().after) ?? dataUrlFromValue(item().before)) + const [audioSrc, setAudioSrc] = createSignal(diffAudioSrc()) const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle") const [audioMime, setAudioMime] = createSignal(undefined) const selectedLines = createMemo(() => { const current = selection() - if (!current || current.file !== diff.file) return null + if (!current || current.file !== file) return null return current.range }) const draftRange = createMemo(() => { const current = commenting() - if (!current || current.file !== diff.file) return null + if (!current || current.file !== file) return null return current.range }) @@ -416,6 +422,21 @@ export const SessionReview = (props: SessionReviewProps) => { requestAnimationFrame(updateAnchors) } + createEffect(() => { + if (!isImage()) return + const src = diffImageSrc() + setImageSrc(src) + setImageStatus("idle") + }) + + createEffect(() => { + if (!isAudio()) return + const src = diffAudioSrc() + setAudioSrc(src) + setAudioStatus("idle") + setAudioMime(undefined) + }) + createEffect(() => { comments() scheduleAnchors() @@ -429,7 +450,7 @@ export const SessionReview = (props: SessionReviewProps) => { }) createEffect(() => { - if (!open().includes(diff.file)) return + if (!open().includes(file)) return if (!isImage()) return if (imageSrc()) return if (imageStatus() !== "idle") return @@ -439,7 +460,7 @@ export const SessionReview = (props: SessionReviewProps) => { if (!reader) return setImageStatus("loading") - reader(diff.file) + reader(file) .then((result) => { const src = dataUrl(result) if (!src) { @@ -455,7 +476,7 @@ export const SessionReview = (props: SessionReviewProps) => { }) createEffect(() => { - if (!open().includes(diff.file)) return + if (!open().includes(file)) return if (!isAudio()) return if (audioSrc()) return if (audioStatus() !== "idle") return @@ -464,7 +485,7 @@ export const SessionReview = (props: SessionReviewProps) => { if (!reader) return setAudioStatus("loading") - reader(diff.file) + reader(file) .then((result) => { const src = dataUrl(result) if (!src) { @@ -488,7 +509,7 @@ export const SessionReview = (props: SessionReviewProps) => { return } - setSelection({ file: diff.file, range }) + setSelection({ file, range }) } const handleLineSelectionEnd = (range: SelectedLineRange | null) => { @@ -499,8 +520,8 @@ export const SessionReview = (props: SessionReviewProps) => { return } - setSelection({ file: diff.file, range }) - setCommenting({ file: diff.file, range }) + setSelection({ file, range }) + setCommenting({ file, range }) } const openComment = (comment: SessionReviewComment) => { @@ -516,22 +537,22 @@ export const SessionReview = (props: SessionReviewProps) => { return (
- +
- - {`\u202A${getDirectory(diff.file)}\u202C`} + + {`\u202A${getDirectory(file)}\u202C`} - {getFilename(diff.file)} + {getFilename(file)}
@@ -570,7 +591,7 @@ export const SessionReview = (props: SessionReviewProps) => { - + @@ -585,7 +606,7 @@ export const SessionReview = (props: SessionReviewProps) => { data-slot="session-review-diff-wrapper" ref={(el) => { wrapper = el - anchors.set(diff.file, el) + anchors.set(file, el) scheduleAnchors() }} > @@ -593,7 +614,7 @@ export const SessionReview = (props: SessionReviewProps) => {
- {diff.file} + {file}
@@ -633,7 +654,7 @@ export const SessionReview = (props: SessionReviewProps) => { { props.onDiffRendered?.() @@ -645,12 +666,12 @@ export const SessionReview = (props: SessionReviewProps) => { selectedLines={selectedLines()} commentedLines={commentedLines()} before={{ - name: diff.file!, - contents: typeof diff.before === "string" ? diff.before : "", + name: file, + contents: typeof item().before === "string" ? item().before : "", }} after={{ - name: diff.file!, - contents: typeof diff.after === "string" ? diff.after : "", + name: file, + contents: typeof item().after === "string" ? item().after : "", }} /> @@ -688,10 +709,10 @@ export const SessionReview = (props: SessionReviewProps) => { onCancel={() => setCommenting(null)} onSubmit={(comment) => { props.onLineComment?.({ - file: diff.file, + file, selection: range(), comment, - preview: selectionPreview(diff, range()), + preview: selectionPreview(item(), range()), }) setCommenting(null) }} @@ -709,6 +730,6 @@ export const SessionReview = (props: SessionReviewProps) => {
-
+ ) } diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index bf1258d2e5a8..9639e6635a76 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -41,6 +41,8 @@ display: flex; align-items: center; gap: 8px; + width: 100%; + min-width: 0; color: var(--text-weak); font-family: var(--font-family-sans); font-size: var(--font-size-base); @@ -52,6 +54,16 @@ width: 16px; height: 16px; } + + [data-slot="session-turn-thinking-heading"] { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--text-weaker); + font-weight: var(--font-weight-regular); + } } .error-card { diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 2aed8279ec16..8e8a3f3875d4 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -6,7 +6,7 @@ import { Binary } from "@opencode-ai/util/binary" import { getDirectory, getFilename } from "@opencode-ai/util/path" import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js" import { Dynamic } from "solid-js/web" -import { AssistantParts, Message } from "./message-part" +import { AssistantParts, Message, PART_MAPPING } from "./message-part" import { Card } from "./card" import { Accordion } from "./accordion" import { StickyAccordionHeader } from "./sticky-accordion-header" @@ -83,15 +83,55 @@ function list(value: T[] | undefined | null, fallback: T[]) { const hidden = new Set(["todowrite", "todoread"]) -function visible(part: PartType) { +function partState(part: PartType, showReasoningSummaries: boolean) { if (part.type === "tool") { - if (hidden.has(part.tool)) return false - if (part.tool === "question") return part.state.status !== "pending" && part.state.status !== "running" - return true + if (hidden.has(part.tool)) return + if (part.tool === "question" && (part.state.status === "pending" || part.state.status === "running")) return + return "visible" as const + } + if (part.type === "text") return part.text?.trim() ? ("visible" as const) : undefined + if (part.type === "reasoning") { + if (showReasoningSummaries && part.text?.trim()) return "visible" as const + return + } + if (PART_MAPPING[part.type]) return "visible" as const + return +} + +function clean(value: string) { + return value + .replace(/`([^`]+)`/g, "$1") + .replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1") + .replace(/[*_~]+/g, "") + .trim() +} + +function heading(text: string) { + const markdown = text.replace(/\r\n?/g, "\n") + + const html = markdown.match(/]*>([\s\S]*?)<\/h[1-6]>/i) + if (html?.[1]) { + const value = clean(html[1].replace(/<[^>]+>/g, " ")) + if (value) return value + } + + const atx = markdown.match(/^\s{0,3}#{1,6}[ \t]+(.+?)(?:[ \t]+#+[ \t]*)?$/m) + if (atx?.[1]) { + const value = clean(atx[1]) + if (value) return value + } + + const setext = markdown.match(/^([^\n]+)\n(?:=+|-+)\s*$/m) + if (setext?.[1]) { + const value = clean(setext[1]) + if (value) return value + } + + const strong = markdown.match(/^\s*(?:\*\*|__)(.+?)(?:\*\*|__)\s*$/m) + if (strong?.[1]) { + const value = clean(strong[1]) + if (value) return value } - if (part.type === "text") return !!part.text?.trim() - if (part.type === "reasoning") return !!part.text?.trim() - return false } export function SessionTurn( @@ -99,6 +139,7 @@ export function SessionTurn( sessionID: string messageID: string lastUserMessageID?: string + showReasoningSummaries?: boolean onUserInteracted?: () => void classes?: { root?: string @@ -242,17 +283,57 @@ export function SessionTurn( const status = createMemo(() => data.store.session_status[props.sessionID] ?? idle) const working = createMemo(() => status().type !== "idle" && isLastUserMessage()) + const showReasoningSummaries = createMemo(() => props.showReasoningSummaries ?? true) const assistantCopyPartID = createMemo(() => { if (working()) return null return showAssistantCopyPartID() ?? null }) + const turnDurationMs = createMemo(() => { + const start = message()?.time.created + if (typeof start !== "number") return undefined + + const end = assistantMessages().reduce((max, item) => { + const completed = item.time.completed + if (typeof completed !== "number") return max + if (max === undefined) return completed + return Math.max(max, completed) + }, undefined) + + if (typeof end !== "number") return undefined + if (end < start) return undefined + return end - start + }) const assistantVisible = createMemo(() => assistantMessages().reduce((count, message) => { const parts = list(data.store.part?.[message.id], emptyParts) - return count + parts.filter(visible).length + return count + parts.filter((part) => partState(part, showReasoningSummaries()) === "visible").length }, 0), ) + const assistantTailVisible = createMemo(() => + assistantMessages() + .flatMap((message) => list(data.store.part?.[message.id], emptyParts)) + .flatMap((part) => { + if (partState(part, showReasoningSummaries()) !== "visible") return [] + if (part.type === "text") return ["text" as const] + return ["other" as const] + }) + .at(-1), + ) + const reasoningHeading = createMemo(() => + assistantMessages() + .flatMap((message) => list(data.store.part?.[message.id], emptyParts)) + .filter((part): part is PartType & { type: "reasoning"; text: string } => part.type === "reasoning") + .map((part) => heading(part.text)) + .filter((text): text is string => !!text) + .at(-1), + ) + const showThinking = createMemo(() => { + if (!working() || !!error()) return false + if (showReasoningSummaries()) return assistantVisible() === 0 + if (assistantTailVisible() === "text") return false + return true + }) const autoScroll = createAutoScroll({ working, @@ -280,20 +361,25 @@ export function SessionTurn(
- -
- -
-
0}>
+ +
+ + + {(text) => {text()}} + +
+
0 && !working()}>
diff --git a/packages/ui/src/pierre/virtualizer.ts b/packages/ui/src/pierre/virtualizer.ts index 42c3a4ca3c30..31862cc49316 100644 --- a/packages/ui/src/pierre/virtualizer.ts +++ b/packages/ui/src/pierre/virtualizer.ts @@ -37,10 +37,11 @@ function target(container: HTMLElement): Target | undefined { const review = container.closest("[data-component='session-review']") if (review instanceof HTMLElement) { + const root = scrollRoot(container) ?? review const content = review.querySelector("[data-slot='session-review-container']") return { key: review, - root: review, + root, content: content instanceof HTMLElement ? content : undefined, } } diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css index efe00e5f160f..c0af0ac9b449 100644 --- a/packages/ui/src/styles/index.css +++ b/packages/ui/src/styles/index.css @@ -44,6 +44,7 @@ @import "../components/select.css" layer(components); @import "../components/spinner.css" layer(components); @import "../components/switch.css" layer(components); +@import "../components/scroll-view.css" layer(components); @import "../components/session-review.css" layer(components); @import "../components/session-turn.css" layer(components); @import "../components/sticky-accordion-header.css" layer(components); diff --git a/packages/ui/src/styles/tailwind/utilities.css b/packages/ui/src/styles/tailwind/utilities.css index be305b4cbce3..4318b9ec1de3 100644 --- a/packages/ui/src/styles/tailwind/utilities.css +++ b/packages/ui/src/styles/tailwind/utilities.css @@ -8,34 +8,6 @@ } } -@utility session-scroller { - &::-webkit-scrollbar { - width: 10px; - height: 10px; - } - - &::-webkit-scrollbar-track { - background: transparent; - border-radius: 5px; - } - - &::-webkit-scrollbar-thumb { - background: var(--border-weak-base); - border-radius: 5px; - border: 3px solid transparent; - background-clip: padding-box; - } - - &::-webkit-scrollbar-thumb:hover { - background: var(--border-weak-base); - } - - & { - scrollbar-width: thin; - scrollbar-color: var(--border-weak-base) transparent; - } -} - @utility badge-mask { -webkit-mask-image: radial-gradient(circle 5px at calc(100% - 4px) 4px, transparent 5px, black 5.5px); mask-image: radial-gradient(circle 5px at calc(100% - 4px) 4px, transparent 5px, black 5.5px); diff --git a/packages/util/package.json b/packages/util/package.json index a1417edd55b9..4bcbb0305d4e 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.2.9", + "version": "1.2.10", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index ba9ec45ba15c..110c6ca2354f 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.2.9", + "version": "1.2.10", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/packages/web/src/content/docs/ko/gitlab.mdx b/packages/web/src/content/docs/ko/gitlab.mdx index 7b6468c7406d..850cd622586d 100644 --- a/packages/web/src/content/docs/ko/gitlab.mdx +++ b/packages/web/src/content/docs/ko/gitlab.mdx @@ -1,34 +1,34 @@ --- title: GitLab -description: GitLab 이슈 및 머지 리퀘스트에서 opencode를 사용하세요. +description: GitLab 이슈와 merge request에서 OpenCode를 사용하세요. --- -opencode는 GitLab CI/CD 파이프라인 또는 GitLab Duo를 통해 GitLab 워크플로우와 통합됩니다. +OpenCode는 GitLab CI/CD 파이프라인 또는 GitLab Duo를 통해 GitLab 워크플로에 통합됩니다. -두 경우, opencode는 GitLab runners에서 실행됩니다. +두 경우 모두 OpenCode는 GitLab runner에서 실행됩니다. --- -# GitLab CI 소개 +## GitLab CI -opencode는 일반 GitLab 파이프라인에서 작동합니다. [CI 컴포넌트](https://docs.gitlab.com/ee/ci/components/)로 파이프라인에 구축할 수 있습니다. +OpenCode는 일반 GitLab 파이프라인에서 작동합니다. [CI component](https://docs.gitlab.com/ee/ci/components/)로 파이프라인에 통합할 수 있습니다. -여기에서 우리는 opencode에 대한 커뮤니티 생성 CI / CD 구성품을 사용하고 있습니다. [nagyv/gitlab-opencode](https://gitlab.com/nagyv/gitlab-opencode). +여기서는 OpenCode용 커뮤니티 제작 CI/CD component인 [nagyv/gitlab-opencode](https://gitlab.com/nagyv/gitlab-opencode)를 사용합니다. --- ### 기능 -- **실행별 사용자 지정 구성 사용**: 사용자 정의 구성 디렉토리와 opencode 구성, 예를 들어 `./config/#custom-directory`는 opencode 실행마다 활성화하거나 비활성화 할 수 있습니다. -- ** 최소 설정**: CI 구성 요소는 opencode를 배경으로 설정하면 opencode 구성과 초기 프롬프트를 만들 필요가 있습니다. -- **Flexible**: CI 구성 요소는 여러 입력을 지원합니다. +- **job별 custom config 사용**: custom config 디렉터리(예: `./config/#custom-directory`)를 사용해 OpenCode를 각 실행 단위로 설정하고 기능을 켜거나 끌 수 있습니다. +- **최소 설정**: CI component가 백그라운드에서 OpenCode를 설정하므로 OpenCode config와 초기 prompt만 만들면 됩니다. +- **유연함**: CI component는 동작을 사용자화할 수 있도록 여러 입력값을 지원합니다. --- -## 설정 +### Setup -1. opencode 인증 JSON을 **Settings** > **CI/CD** > **Variables**에서 파일 유형 CI 환경 변수로 저장하십시오. "Masked and hidden"로 표시하십시오. -2. `.gitlab-ci.yml` 파일에 뒤에 추가하십시오. +1. OpenCode 인증 JSON을 **Settings** > **CI/CD** > **Variables** 아래의 File 타입 CI 환경 변수로 저장하세요. 반드시 "Masked and hidden"으로 표시하세요. +2. 아래 내용을 `.gitlab-ci.yml` 파일에 추가하세요. ```yaml title=".gitlab-ci.yml" include: @@ -40,40 +40,40 @@ opencode는 일반 GitLab 파이프라인에서 작동합니다. [CI 컴포넌 message: "Your prompt here" ``` -더 많은 입력 및 사용 사례 [docs를 체크 아웃](https://gitlab.com/explore/catalog/nagyv/gitlab-opencode) 이 구성 요소에 대한. +더 많은 입력값과 사용 사례는 이 component의 [docs](https://gitlab.com/explore/catalog/nagyv/gitlab-opencode)에서 확인하세요. --- ## GitLab Duo -opencode는 GitLab 워크플로우와 통합됩니다. -코멘트에 Mention `@opencode`, opencode는 GitLab CI 파이프라인 내에서 작업을 실행합니다. +OpenCode는 GitLab 워크플로에 통합됩니다. +댓글에서 `@opencode`를 멘션하면 OpenCode가 GitLab CI 파이프라인 안에서 작업을 실행합니다. --- ### 기능 -- **이슈**: opencode가 문제점을 보고 당신을 설명합니다. -- **수정 및 구현**: 이슈를 수정하거나 기능을 구현하려면 opencode에 문의하십시오. - 새로운 지점을 만들고 변화를 병합 요청을 제기합니다. -- **보안**: opencode는 GitLab runners에서 실행됩니다. +- **이슈 분류**: OpenCode에 이슈를 살펴보고 설명해 달라고 요청할 수 있습니다. +- **수정 및 구현**: OpenCode에 이슈를 수정하거나 기능을 구현해 달라고 요청할 수 있습니다. + OpenCode는 새 브랜치를 만들고 변경 사항이 담긴 merge request를 생성합니다. +- **보안**: OpenCode는 GitLab runner에서 실행됩니다. --- -## 설정 +### Setup -opencode는 GitLab CI/CD 파이프라인에서 실행되며, 여기서 설정해야 할 일은 다음과 같습니다. +OpenCode는 GitLab CI/CD 파이프라인에서 실행되며, 설정에 필요한 항목은 다음과 같습니다. :::tip -[**GitLab docs**](https://docs.gitlab.com/user/duo agent platform/agent assistant/) 를 체크 아웃하십시오. +[최신 안내는 **GitLab docs**](https://docs.gitlab.com/user/duo_agent_platform/agent_assistant/)를 확인하세요. ::: -1. GitLab 환경 설정 -2. CI/CD 설치 -3. AI 모형 공급자 API 열쇠를 얻으십시오 -4. 서비스 계정 만들기 -5. CI/CD 변수 구성 -6. Flow config 파일을 만들려면 다음과 같습니다. +1. GitLab 환경을 설정합니다. +2. CI/CD를 설정합니다. +3. AI model provider API 키를 준비합니다. +4. 서비스 계정을 생성합니다. +5. CI/CD 변수를 설정합니다. +6. flow config 파일을 생성합니다. 예시는 다음과 같습니다.
@@ -152,44 +152,44 @@ opencode는 GitLab CI/CD 파이프라인에서 실행되며, 여기서 설정해
-자세한 지침에 대한 [GitLab CLI Agent docs](https://docs.gitlab.com/user/duo agent platform/agent assistant/)를 참조할 수 있습니다. +[GitLab CLI agents docs](https://docs.gitlab.com/user/duo_agent_platform/agent_assistant/)에서 자세한 안내를 확인할 수 있습니다. --- ### 예제 -다음은 GitLab에서 opencode를 사용할 수있는 몇 가지 예입니다. +다음은 GitLab에서 OpenCode를 사용하는 몇 가지 예시입니다. :::tip -`@opencode`보다 다른 트리거 구문을 사용할 수 있습니다. +`@opencode` 대신 다른 trigger phrase를 사용하도록 설정할 수 있습니다. ::: - **이슈 설명** -GitLab 문제에서이 코멘트를 추가하십시오. +GitLab 이슈에 아래 댓글을 남기세요. ``` @opencode explain this issue ``` -opencode는 문제와 대답을 명확하게 설명합니다. +OpenCode가 이슈를 읽고 명확한 설명으로 답변합니다. - **이슈 해결** -GitLab 문제에서, 말한다: +GitLab 이슈에서 다음과 같이 요청하세요. ``` @opencode fix this ``` -opencode는 새로운 지점을 만들 것이며 변경 사항을 구현하고 변경 사항을 병합 요청을 엽니다. +OpenCode가 새 브랜치를 만들고 변경 사항을 구현한 뒤, 해당 변경 사항으로 merge request를 엽니다. - **머지 리퀘스트 검토** -GitLab 병합 요청에 대한 다음 의견을 남겨주세요. +GitLab merge request에 아래 댓글을 남기세요. ``` @opencode review this merge request ``` -opencode는 병합 요청을 검토하고 피드백을 제공합니다. +OpenCode가 merge request를 검토하고 피드백을 제공합니다. diff --git a/packages/web/src/content/docs/ko/ide.mdx b/packages/web/src/content/docs/ko/ide.mdx index 2df782a9e727..ddc73001910c 100644 --- a/packages/web/src/content/docs/ko/ide.mdx +++ b/packages/web/src/content/docs/ko/ide.mdx @@ -1,35 +1,36 @@ --- title: IDE -description: VS Code, Cursor 및 기타 IDE용 opencode 확장 프로그램. +description: VS Code, Cursor 및 기타 IDE용 OpenCode 확장 프로그램 --- -opencode는 VS Code, Cursor, 또는 터미널을 지원하는 IDE와 통합됩니다. 시작하려면 terminal에서 `opencode`를 실행하십시오. +OpenCode는 VS Code, Cursor, 또는 터미널을 지원하는 모든 IDE와 통합됩니다. 시작하려면 터미널에서 `opencode`를 실행하세요. --- ## 사용법 --**Quick Launch**: `Cmd+Esc` (Mac) 또는 `Ctrl+Esc` (Windows/Linux)를 사용하여 통합 터미널 뷰에 opencode를 열거나 기존 terminal 세션을 이미 실행하면 됩니다. -**New Session**: `Cmd+Shift+Esc` (Mac) 또는 `Ctrl+Shift+Esc` (Windows/Linux)를 사용하여 새로운 opencode terminal 세션을 시작하려면 이미 열리면 됩니다. UI에서 opencode 버튼을 클릭합니다. -**Context Awareness**: opencode로 현재 선택 또는 탭을 자동으로 공유합니다. - -- **파일 참조 단축키** : 파일 참조를 삽입하려면 `Cmd+Option+K` (Mac) 또는 `Alt+Ctrl+K` (Linux / Windows)를 사용하십시오. 예를 들어, `@File#L37-42`. +- **Quick Launch**: `Cmd+Esc` (Mac) 또는 `Ctrl+Esc` (Windows/Linux)를 사용해 분할 터미널 뷰에서 OpenCode를 열거나, 이미 실행 중인 터미널 세션으로 포커스하세요. +- **New Session**: `Cmd+Shift+Esc` (Mac) 또는 `Ctrl+Shift+Esc` (Windows/Linux)를 사용해 기존 세션이 열려 있어도 새 OpenCode 터미널 세션을 시작하세요. UI의 OpenCode 버튼을 클릭해도 됩니다. +- **Context Awareness**: 현재 선택 영역이나 탭을 OpenCode와 자동으로 공유합니다. +- **File Reference Shortcuts**: `Cmd+Option+K` (Mac) 또는 `Alt+Ctrl+K` (Linux/Windows)를 사용해 파일 참조를 삽입하세요. 예: `@File#L37-42`. --- ## 설치 -VS Code에 opencode를 설치하고 Cursor, Windsurf, VSCodium과 같은 인기있는 포크 : +VS Code와 Cursor, Windsurf, VSCodium 같은 인기 포크에 OpenCode를 설치하려면: -1. VS Code 열기 -2. 통합 terminal을 여십시오 -3. 실행 `opencode` - 확장 자동으로 설치 +1. VS Code를 여세요.4 +2. 통합 터미널을 여세요. +3. `opencode`를 실행하세요. 확장 프로그램이 자동으로 설치됩니다. -반면에 TUI에서 `/editor` 또는 `/export`를 실행할 때, 당신은 `export EDITOR="code --wait"`를 설정할 필요가 있을 것입니다. [Learn more](/docs/tui/#editor-setup). +반면 TUI에서 `/editor` 또는 `/export`를 실행할 때 자체 IDE를 사용하려면 `export EDITOR="code --wait"`를 설정해야 합니다. [자세히 알아보기](/docs/tui/#editor-setup). --- -## 수동 설치 +### 수동 설치 -확장 마켓 플레이스에서 **opencode**를 검색하고 **Install**를 클릭합니다. +Extension Marketplace에서 **OpenCode**를 검색한 다음 **Install**을 클릭하세요. --- @@ -37,11 +38,11 @@ VS Code에 opencode를 설치하고 Cursor, Windsurf, VSCodium과 같은 인기 확장이 자동으로 설치되지 않는 경우: -- 통합 terminal에서 `opencode`를 실행하는 것을 보장합니다. -- IDE용 CLI가 설치됩니다. -- VS Code : `code` 명령 -- 커서: `cursor` 명령 -- 윈드 서핑을 위해: `windsurf` 명령 -- VSCodium의 경우: `codium` 명령 -- 만약 `Cmd+Shift+P` (Mac) 또는 `Ctrl+Shift+P` (Windows/Linux)를 실행하고 "Shell Command: PATH"에서 'code' 명령을 설치하십시오 (또는 IDE에 해당) -- Ensure VS Code는 확장을 설치하는 권한이 있습니다. +- 통합 터미널에서 `opencode`를 실행하고 있는지 확인하세요. +- IDE용 CLI가 설치되어 있는지 확인하세요. + - VS Code: `code` command + - Cursor: `cursor` command + - Windsurf: `windsurf` command + - VSCodium: `codium` command + - 설치되어 있지 않다면 `Cmd+Shift+P` (Mac) 또는 `Ctrl+Shift+P` (Windows/Linux)를 실행하고 "Shell Command: Install 'code' command in PATH"(또는 IDE에 맞는 동등한 명령)를 검색하세요. +- VS Code에 확장 프로그램 설치 권한이 있는지 확인하세요. diff --git a/packages/web/src/content/docs/ko/index.mdx b/packages/web/src/content/docs/ko/index.mdx index b94d0750f6ef..0dfa0bbc830b 100644 --- a/packages/web/src/content/docs/ko/index.mdx +++ b/packages/web/src/content/docs/ko/index.mdx @@ -7,19 +7,19 @@ import { Tabs, TabItem } from "@astrojs/starlight/components" import config from "../../../../config.mjs" export const console = config.console -[**OpenCode**](/)는 터미널 기반 인터페이스, 데스크톱 앱, IDE 확장 형태로 사용할 수 있는 오픈 소스 AI 코딩 에이전트입니다. +[**OpenCode**](/)는 오픈 소스 AI coding agent입니다. 터미널 기반 인터페이스, 데스크톱 앱, IDE 확장으로 사용할 수 있습니다. ![opencode TUI with the opencode theme](../../../assets/lander/screenshot.png) -바로 시작해 봅시다. +바로 시작해 보겠습니다. --- -## 사전 준비 +#### 사전 준비 터미널에서 OpenCode를 사용하려면 다음이 필요합니다. -1. 최신 터미널 에뮬레이터 (예:) +1. 다음과 같은 최신 터미널 에뮬레이터 - [WezTerm](https://wezterm.org), 크로스 플랫폼 - [Alacritty](https://alacritty.org), 크로스 플랫폼 - [Ghostty](https://ghostty.org), Linux 및 macOS @@ -31,13 +31,13 @@ export const console = config.console ## 설치 -가장 쉬운 설치 방법은 설치 스크립트를 사용하는 것입니다. +OpenCode를 설치하는 가장 쉬운 방법은 설치 스크립트를 사용하는 것입니다. ```bash curl -fsSL https://opencode.ai/install | bash ``` -아래 명령으로도 설치할 수 있습니다. +다음 명령으로도 설치할 수 있습니다. - **Node.js 사용** @@ -79,9 +79,9 @@ curl -fsSL https://opencode.ai/install | bash brew install anomalyco/tap/opencode ``` - > 최신 릴리스는 OpenCode tap 사용을 권장합니다. 공식 `brew install opencode` 포뮬러는 Homebrew 팀이 관리하므로 업데이트 주기가 더 긴 편입니다. + > 최신 릴리스를 사용하려면 OpenCode tap 사용을 권장합니다. 공식 `brew install opencode` formula는 Homebrew 팀이 관리하며 업데이트 주기가 더 깁니다. -- **Arch Linux에서 Paru 사용** +- **Arch Linux에 설치** ```bash sudo pacman -S opencode # Arch Linux (Stable) @@ -91,7 +91,7 @@ curl -fsSL https://opencode.ai/install | bash #### Windows :::tip[권장: WSL 사용] -Windows에서는 [Windows Subsystem for Linux (WSL)](/docs/windows-wsl)을 사용하는 것이 가장 좋습니다. OpenCode 기능과의 호환성이 높고 성능도 더 좋습니다. +Windows에서는 [Windows Subsystem for Linux (WSL)](/docs/windows-wsl) 사용을 권장합니다. 성능이 더 좋고 OpenCode 기능과의 완전한 호환성을 제공합니다. ::: - **Chocolatey 사용** @@ -124,7 +124,7 @@ Windows에서는 [Windows Subsystem for Linux (WSL)](/docs/windows-wsl)을 사 docker run -it --rm ghcr.io/anomalyco/opencode ``` -Windows에서 Bun을 통한 OpenCode 설치는 아직 지원되지 않으며, 현재 지원을 준비 중입니다. +현재 Windows에서 Bun을 사용한 OpenCode 설치 지원은 준비 중입니다. [Releases](https://github.com/anomalyco/opencode/releases)에서 바이너리를 직접 받아 설치할 수도 있습니다. @@ -134,16 +134,16 @@ Windows에서 Bun을 통한 OpenCode 설치는 아직 지원되지 않으며, OpenCode는 API 키를 설정하면 원하는 LLM 제공자를 사용할 수 있습니다. -LLM 제공자(LLM Provider)를 처음 사용한다면 [OpenCode Zen](/docs/zen)을 추천합니다. +LLM 제공자를 처음 사용한다면 [OpenCode Zen](/docs/zen) 사용을 권장합니다. OpenCode 팀이 테스트하고 검증한 모델 목록입니다. -1. TUI에서 `/connect` 명령을 실행한 뒤 `opencode`를 선택하고 [opencode.ai/auth](https://opencode.ai/auth)로 이동합니다. +1. TUI에서 `/connect` 명령을 실행하고 `opencode`를 선택한 다음 [opencode.ai/auth](https://opencode.ai/auth)로 이동합니다. ```txt /connect ``` -2. 로그인 후 결제 정보를 입력하고 API 키를 복사합니다. +2. 로그인하고 결제 정보를 추가한 뒤 API 키를 복사합니다. 3. API 키를 붙여 넣습니다. @@ -154,13 +154,13 @@ OpenCode 팀이 테스트하고 검증한 모델 목록입니다. └ enter ``` -다른 제공자를 선택해도 됩니다. [더 알아보기](/docs/providers#directory). +또는 다른 제공자 중 하나를 선택할 수도 있습니다. [더 알아보기](/docs/providers#directory). --- ## 초기화 -이제 제공자 구성이 끝났으니, 작업할 프로젝트 디렉터리로 이동합니다. +이제 제공자 구성을 마쳤으니 작업하려는 프로젝트로 이동합니다. ```bash cd /path/to/project @@ -172,19 +172,19 @@ cd /path/to/project opencode ``` -다음 명령으로 프로젝트용 OpenCode 초기화를 진행합니다. +다음 명령을 실행해 프로젝트에서 OpenCode를 초기화합니다. ```bash frame="none" /init ``` -이 명령은 프로젝트를 분석하고 루트에 `AGENTS.md` 파일을 생성합니다. +이 명령을 실행하면 OpenCode가 프로젝트를 분석하고 프로젝트 루트에 `AGENTS.md` 파일을 생성합니다. :::tip -프로젝트의 `AGENTS.md`는 Git에 커밋해 두는 것을 권장합니다. +프로젝트의 `AGENTS.md` 파일은 Git에 커밋하는 것을 권장합니다. ::: -그러면 OpenCode가 프로젝트 구조와 코딩 패턴을 더 잘 이해할 수 있습니다. +이렇게 하면 OpenCode가 프로젝트 구조와 사용 중인 코딩 패턴을 더 잘 이해할 수 있습니다. --- @@ -192,7 +192,7 @@ opencode 이제 OpenCode로 프로젝트 작업을 시작할 준비가 되었습니다. 무엇이든 물어보세요. -AI 코딩 에이전트를 처음 쓰는 경우 도움이 되는 예시를 소개합니다. +AI coding agent를 처음 사용한다면 도움이 될 수 있는 예시를 소개합니다. --- @@ -208,25 +208,25 @@ OpenCode에 코드베이스 설명을 요청할 수 있습니다. How is authentication handled in @packages/functions/src/api/index.ts ``` -직접 작업하지 않은 코드 영역을 이해할 때 특히 유용합니다. +이 방법은 직접 작업하지 않은 코드 영역을 이해할 때 유용합니다. --- ### 기능 추가 -프로젝트에 새 기능을 추가해 달라고 요청할 수 있습니다. 다만 먼저 계획을 만들게 하는 것을 권장합니다. +OpenCode에 프로젝트의 새 기능 추가를 요청할 수 있습니다. 다만 먼저 계획을 만들도록 요청하는 것을 권장합니다. 1. **계획 만들기** - OpenCode에는 변경 작업을 비활성화하고 구현 방법을 제안만 하는 *Plan mode*가 있습니다. + OpenCode에는 변경 작업 기능을 비활성화하고 기능을 구현할 방법만 제안하는 *Plan mode*가 있습니다. - **Tab** 키로 전환하면 오른쪽 아래에 모드 표시가 나타납니다. + **Tab** 키를 눌러 전환하세요. 화면 오른쪽 아래에서 모드 표시를 확인할 수 있습니다. ```bash frame="none" title="Switch to Plan mode" ``` - 이제 원하는 작업을 구체적으로 설명합니다. + 이제 수행하길 원하는 작업을 설명해 보겠습니다. ```txt frame="none" When a user deletes a note, we'd like to flag it as deleted in the database. @@ -234,15 +234,15 @@ How is authentication handled in @packages/functions/src/api/index.ts From this screen, the user can undelete a note or permanently delete it. ``` - OpenCode가 정확히 이해할 만큼 충분한 맥락을 주는 것이 중요합니다. 팀의 주니어 개발자에게 설명하듯 요청하면 도움이 됩니다. + OpenCode가 원하는 작업을 이해할 수 있도록 충분한 세부 정보를 제공해야 합니다. 팀의 주니어 개발자에게 말하듯이 설명하면 도움이 됩니다. :::tip - 맥락과 예시를 충분히 제공하면 원하는 결과를 얻기 쉽습니다. + OpenCode가 원하는 작업을 이해하도록 충분한 맥락과 예시를 제공하세요. ::: 2. **계획 다듬기** - 계획이 나오면 피드백을 주거나 추가 요구사항을 붙일 수 있습니다. + 계획이 나오면 피드백을 주거나 세부 사항을 더 추가할 수 있습니다. ```txt frame="none" We'd like to design this new screen using a design I've used before. @@ -250,20 +250,20 @@ How is authentication handled in @packages/functions/src/api/index.ts ``` :::tip - 이미지를 터미널에 드래그 앤 드롭하면 프롬프트에 첨부할 수 있습니다. + 이미지를 터미널에 드래그 앤 드롭해 prompt에 추가하세요. ::: - OpenCode는 첨부한 이미지를 분석해 프롬프트에 포함합니다. + OpenCode는 제공한 이미지를 스캔해 prompt에 추가할 수 있습니다. 이미지를 터미널에 드래그 앤 드롭하면 됩니다. 3. **기능 구현** - 계획이 충분히 만족스러우면 **Tab** 키를 다시 눌러 *Build mode*로 돌아갑니다. + 계획이 충분히 마음에 들면 **Tab** 키를 다시 눌러 *Build mode*로 전환하세요. ```bash frame="none" ``` - 그리고 실제 변경을 요청합니다. + 그리고 변경을 적용해 달라고 요청하세요. ```bash frame="none" Sounds good! Go ahead and make the changes. @@ -273,7 +273,7 @@ How is authentication handled in @packages/functions/src/api/index.ts ### 바로 변경하기 -비교적 단순한 변경은 계획 검토 없이 바로 구현하도록 요청해도 됩니다. +비교적 간단한 변경은 계획을 먼저 검토하지 않고 바로 구현하도록 요청할 수 있습니다. ```txt frame="none" "@packages/functions/src/settings.ts" "@packages/functions/src/notes.ts" We need to add authentication to the /settings route. Take a look at how this is @@ -281,37 +281,37 @@ handled in the /notes route in @packages/functions/src/notes.ts and implement the same logic in @packages/functions/src/settings.ts ``` -원하는 변경이 정확히 반영되도록, 필요한 맥락을 충분히 제공하세요. +OpenCode가 올바른 변경을 하도록 충분한 세부 정보를 제공해야 합니다. --- ### 변경 되돌리기 -예를 들어 OpenCode에 변경을 요청했다고 가정해 보겠습니다. +예를 들어 OpenCode에 변경을 요청했다고 해보겠습니다. ```txt frame="none" "@packages/functions/src/api/index.ts" Can you refactor the function in @packages/functions/src/api/index.ts? ``` -결과가 기대와 다르면 `/undo` 명령으로 **되돌릴 수** 있습니다. +그런데 원하는 결과가 아니었다면 `/undo` 명령으로 변경을 **되돌릴 수** 있습니다. ```bash frame="none" /undo ``` -OpenCode는 방금 적용한 변경을 되돌리고 원래 메시지를 다시 보여줍니다. +OpenCode가 방금 적용한 변경을 되돌리고 원래 메시지를 다시 보여줍니다. ```txt frame="none" "@packages/functions/src/api/index.ts" Can you refactor the function in @packages/functions/src/api/index.ts? ``` -이 상태에서 프롬프트를 다듬어 다시 시도하면 됩니다. +여기에서 prompt를 수정해 다시 요청할 수 있습니다. :::tip `/undo`는 여러 번 연속으로 실행할 수 있습니다. ::: -반대로 `/redo` 명령으로 **다시 적용**할 수도 있습니다. +또는 `/redo` 명령으로 변경을 **다시 적용**할 수 있습니다. ```bash frame="none" /redo @@ -327,18 +327,18 @@ OpenCode와의 대화는 [팀과 공유](/docs/share)할 수 있습니다. /share ``` -현재 대화 링크를 생성하고 클립보드에 복사합니다. +이 명령을 실행하면 현재 대화 링크를 생성하고 클립보드에 복사합니다. :::note 대화는 기본값으로 공유되지 않습니다. ::: -아래는 OpenCode [대화 예시](https://opencode.ai/s/4XP1fce5)입니다. +다음은 OpenCode [대화 예시](https://opencode.ai/s/4XP1fce5)입니다. --- ## 사용자 지정 -이제 OpenCode 사용의 기본은 끝났습니다. +이제 OpenCode 사용법을 익혔습니다. -자신의 워크플로우에 맞추려면 [테마 선택](/docs/themes), [키바인드 사용자 지정](/docs/keybinds), [코드 포매터 설정](/docs/formatters), [커스텀 명령 작성](/docs/commands), [OpenCode 구성 조정](/docs/config)을 추천합니다. +원하는 방식에 맞추려면 [테마 선택](/docs/themes), [키바인드 사용자 지정](/docs/keybinds), [코드 formatter 설정](/docs/formatters), [custom command 만들기](/docs/commands), [OpenCode config 설정](/docs/config)을 권장합니다. diff --git a/packages/web/src/content/docs/ko/keybinds.mdx b/packages/web/src/content/docs/ko/keybinds.mdx index d41fb8cef5e5..aef7ae357a3c 100644 --- a/packages/web/src/content/docs/ko/keybinds.mdx +++ b/packages/web/src/content/docs/ko/keybinds.mdx @@ -1,9 +1,9 @@ --- title: 키바인드 -description: 키바인드를 사용자 지정하세요. +description: 키바인드를 커스터마이즈하세요. --- -opencode는 opencode config를 통해 사용자 정의 할 수있는 keybinds 목록을 가지고 있습니다. +OpenCode에는 OpenCode config를 통해 커스터마이즈할 수 있는 keybinds 목록이 있습니다. ```json title="opencode.json" { @@ -107,17 +107,17 @@ opencode는 opencode config를 통해 사용자 정의 할 수있는 keybinds ## 리더 키 -opencode는 대부분의 keybinds에 대한 `leader` 키를 사용합니다. 이것은 당신의 terminal에 있는 충돌을 피합니다. +OpenCode는 대부분의 keybinds에 `leader` 키를 사용합니다. 이렇게 하면 terminal에서 충돌을 피할 수 있습니다. -기본적으로 `ctrl+x`는 리더 키이며 대부분의 작업은 리더 키를 먼저 누르고 단축키를 누릅니다. 예를 들어, 새 세션을 시작하려면 먼저 `ctrl+x`를 누르고 `n`를 누릅니다. +기본값으로 `ctrl+x`가 리더 키이며, 대부분의 작업은 먼저 리더 키를 누른 뒤 단축키를 누릅니다. 예를 들어 새 세션을 시작하려면 먼저 `ctrl+x`를 누르고 `n`을 누릅니다. -키바인드에 리더 키를 사용할 필요는 없지만, 사용하는 것을 권장합니다. +keybinds에 리더 키를 꼭 사용할 필요는 없지만, 사용하는 것을 권장합니다. --- ## 키바인드 비활성화 -"none"의 값으로 구성에 키를 추가하여 keybind를 비활성화 할 수 있습니다. +config에 해당 키를 값 `"none"`으로 추가하면 keybind를 비활성화할 수 있습니다. ```json title="opencode.json" { @@ -132,39 +132,39 @@ opencode는 대부분의 keybinds에 대한 `leader` 키를 사용합니다. 이 ## 데스크탑 프롬프트 단축키 -opencode 데스크톱 앱 프롬프트 입력은 텍스트 편집을 위한 일반적인 Readline/Emacs-style 단축키를 지원합니다. 이들은 내장되어 있으며 현재 `opencode.json`를 통해 구성할 수 없습니다. - -| 단축키 | 동작 | -| -------- | -------------------------- | -| `ctrl+a` | 현재 행 시작으로 이동 | -| `ctrl+e` | 현재선 끝으로 이동 | -| `ctrl+b` | 커서를 다시 한 문자로 이동 | -| `ctrl+f` | 한자 앞의 커서 | -| `alt+b` | 한 단어로 커서 이동 | -| `alt+f` | 한 단어를 넘겨 주세요 | -| `ctrl+d` | 커서의 캐릭터 삭제 | -| `ctrl+k` | 노선의 종료 | -| `ctrl+u` | 노선 시작 | -| `ctrl+w` | 이전 단어 | -| `alt+d` | 다음 단어를 죽이기 | -| `ctrl+t` | 자가용 캐릭터 | -| `ctrl+g` | 팝오버를 취소 / 응답 취소 | +OpenCode 데스크톱 앱의 프롬프트 입력은 텍스트 편집을 위한 일반적인 Readline/Emacs-style 단축키를 지원합니다. 이 단축키는 내장되어 있으며 현재 `opencode.json`으로는 설정할 수 없습니다. + +| 단축키 | 동작 | +| -------- | --------------------------------- | +| `ctrl+a` | 현재 줄 시작으로 이동 | +| `ctrl+e` | 현재 줄 끝으로 이동 | +| `ctrl+b` | 커서를 문자 하나 뒤로 이동 | +| `ctrl+f` | 커서를 문자 하나 앞으로 이동 | +| `alt+b` | 커서를 단어 하나 뒤로 이동 | +| `alt+f` | 커서를 단어 하나 앞으로 이동 | +| `ctrl+d` | 커서 아래 문자 삭제 | +| `ctrl+k` | 줄 끝까지 삭제 | +| `ctrl+u` | 줄 시작까지 삭제 | +| `ctrl+w` | 이전 단어 삭제 | +| `alt+d` | 다음 단어 삭제 | +| `ctrl+t` | 문자 순서 바꾸기 | +| `ctrl+g` | 팝오버 취소 / 실행 중인 응답 중단 | --- ## Shift+Enter -몇몇 terminal은 기본적으로 입력한 보조 키를 보내지 않습니다. `Shift+Enter`를 이스케이프 시퀀스로 보낼 terminal을 구성해야 할 수 있습니다. +일부 terminal은 기본적으로 Enter와 modifier 키 조합을 전송하지 않습니다. `Shift+Enter`를 이스케이프 시퀀스로 전송하도록 terminal을 설정해야 할 수도 있습니다. ### Windows Terminal -`settings.json`를 엽니다: +다음 경로의 `settings.json`을 여세요: ``` %LOCALAPPDATA%\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json ``` -루트 레벨 `actions` 배열에 이것을 추가하십시오: +루트 레벨 `actions` 배열에 다음을 추가하세요: ```json "actions": [ @@ -178,7 +178,7 @@ opencode 데스크톱 앱 프롬프트 입력은 텍스트 편집을 위한 일 ] ``` -루트 레벨 `keybindings` 배열에 이것을 추가하십시오: +루트 레벨 `keybindings` 배열에 다음을 추가하세요: ```json "keybindings": [ @@ -189,4 +189,4 @@ opencode 데스크톱 앱 프롬프트 입력은 텍스트 편집을 위한 일 ] ``` -파일을 저장하고 Windows Terminal을 다시 시작하거나 새 탭을 엽니 다. +파일을 저장한 뒤 Windows Terminal을 다시 시작하거나 새 탭을 여세요. diff --git a/packages/web/src/content/docs/ko/lsp.mdx b/packages/web/src/content/docs/ko/lsp.mdx index 31d3dda26d3b..c1786f6aef4f 100644 --- a/packages/web/src/content/docs/ko/lsp.mdx +++ b/packages/web/src/content/docs/ko/lsp.mdx @@ -1,62 +1,63 @@ --- title: LSP 서버 -description: OpenCode는 LSP 서버와 통합되어 작동합니다. +description: OpenCode는 LSP 서버와 통합됩니다. --- -OpenCode는 언어 서버 프로토콜(LSP)과 통합되어 LLM이 코드베이스와 상호 작용할 수 있게 돕습니다. 이를 통해 진단 정보를 사용하여 LLM에 피드백을 제공합니다. +OpenCode는 Language Server Protocol(LSP)과 통합되어 LLM이 코드베이스와 상호작용하도록 돕습니다. 진단 정보를 활용해 LLM에 피드백을 제공합니다. --- ## 기본 제공 (Built-in) -OpenCode는 인기 있는 언어들에 대해 여러 내장 LSP 서버를 제공합니다. - -| LSP 서버 | 확장자 | 요구사항 | -| ------------------ | ------------------------------------------------------------------ | ----------------------------------------------------- | -| astro | .astro | Astro 프로젝트 자동 설치 | -| bash | .sh, .bash, .zsh, .ksh | bash-language-server 자동 설치 | -| clangd | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | C/C++ 프로젝트용 자동 설치 | -| csharp | .cs | `.NET SDK` 설치 | -| clojure-lsp | .clj, .cljs, .cljc, .edn | `clojure-lsp` 명령 사용 가능 | -| dart | .dart | `dart` 명령 사용 가능 | -| deno | .ts, .tsx, .js, .jsx, .mjs | `deno` 명령 사용 가능(deno.json/deno.jsonc 자동 감지) | -| elixir-ls | .ex, .exs | `elixir` 명령 사용 가능 | -| eslint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue | 프로젝트의 `eslint` 의존성 | -| fsharp | .fs, .fsi, .fsx, .fsscript | `.NET SDK` 설치 | -| gleam | .gleam | `gleam` 명령 사용 가능 | -| gopls | .go | `go` 명령 사용 가능 | -| hls | .hs, .lhs | `haskell-language-server-wrapper` 명령 사용 가능 | -| jdtls | .java | `Java SDK (version 21+)` 설치 | -| kotlin-ls | .kt, .kts | Kotlin 프로젝트용 자동 설치 | -| lua-ls | .lua | Lua 프로젝트용 자동 설치 | -| nixd | .nix | `nixd` 명령 사용 가능 | -| ocaml-lsp | .ml, .mli | `ocamllsp` 명령 사용 가능 | -| oxlint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .ct, .vue, .astro, .svelte | 프로젝트의 `oxlint` 의존성 | -| PHP intelephense | .php | PHP 프로젝트 자동 설치 | -| prisma | .prisma | `prisma` 명령 사용 가능 | -| pyright | .py, .pyi | `pyright` 의존성 설치 | -| ruby-lsp (rubocop) | .rb, .rake, .gemspec, .ru | `ruby` 및 `gem` 명령 사용 가능 | -| rust | .rs | `rust-analyzer` 명령 사용 가능 | -| sourcekit-lsp | .swift, .objc, .objcpp | `swift` 설치 (macOS의 `xcode`) | -| svelte | .svelte | Svelte 프로젝트 자동 설치 | -| terraform | .tf, .tfvars | GitHub 릴리스에서 자동 설치 | -| typst | .typ, .typc | GitHub 릴리스에서 자동 설치 | -| typescript | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | 프로젝트의 `typescript` 의존성 | -| vue | .vue | Vue 프로젝트 자동 설치 | -| yaml-ls | .yaml, .yml | Red Hat yaml-language-server 자동 설치 | -| zls | .zig, .zon | `zig` 명령 사용 가능 | - -LSP 서버는 위 파일 확장자 중 하나가 감지되고 요구 사항이 충족되면 자동으로 활성화됩니다. +OpenCode는 널리 사용되는 언어를 위해 여러 built-in LSP 서버를 제공합니다. + +| LSP 서버 | 확장자 | 요구 사항 | +| ------------------ | ------------------------------------------------------------------- | ---------------------------------------------------------- | +| astro | .astro | Astro 프로젝트에서 자동 설치 | +| bash | .sh, .bash, .zsh, .ksh | `bash-language-server` 자동 설치 | +| clangd | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | C/C++ 프로젝트에서 자동 설치 | +| csharp | .cs | `.NET SDK` 설치됨 | +| clojure-lsp | .clj, .cljs, .cljc, .edn | `clojure-lsp` 명령 사용 가능 | +| dart | .dart | `dart` 명령 사용 가능 | +| deno | .ts, .tsx, .js, .jsx, .mjs | `deno` 명령 사용 가능 (`deno.json`/`deno.jsonc` 자동 감지) | +| elixir-ls | .ex, .exs | `elixir` 명령 사용 가능 | +| eslint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue | 프로젝트에 `eslint` dependency 존재 | +| fsharp | .fs, .fsi, .fsx, .fsscript | `.NET SDK` 설치됨 | +| gleam | .gleam | `gleam` 명령 사용 가능 | +| gopls | .go | `go` 명령 사용 가능 | +| hls | .hs, .lhs | `haskell-language-server-wrapper` 명령 사용 가능 | +| jdtls | .java | `Java SDK (version 21+)` 설치됨 | +| julials | .jl | `julia` 및 `LanguageServer.jl` 설치됨 | +| kotlin-ls | .kt, .kts | Kotlin 프로젝트에서 자동 설치 | +| lua-ls | .lua | Lua 프로젝트에서 자동 설치 | +| nixd | .nix | `nixd` 명령 사용 가능 | +| ocaml-lsp | .ml, .mli | `ocamllsp` 명령 사용 가능 | +| oxlint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue, .astro, .svelte | 프로젝트에 `oxlint` dependency 존재 | +| php intelephense | .php | PHP 프로젝트에서 자동 설치 | +| prisma | .prisma | `prisma` 명령 사용 가능 | +| pyright | .py, .pyi | `pyright` dependency 설치됨 | +| ruby-lsp (rubocop) | .rb, .rake, .gemspec, .ru | `ruby` 및 `gem` 명령 사용 가능 | +| rust | .rs | `rust-analyzer` 명령 사용 가능 | +| sourcekit-lsp | .swift, .objc, .objcpp | `swift` 설치됨 (macOS에서는 `xcode`) | +| svelte | .svelte | Svelte 프로젝트에서 자동 설치 | +| terraform | .tf, .tfvars | GitHub releases에서 자동 설치 | +| tinymist | .typ, .typc | GitHub releases에서 자동 설치 | +| typescript | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | 프로젝트에 `typescript` dependency 존재 | +| vue | .vue | Vue 프로젝트에서 자동 설치 | +| yaml-ls | .yaml, .yml | Red Hat `yaml-language-server` 자동 설치 | +| zls | .zig, .zon | `zig` 명령 사용 가능 | + +위 확장자 중 하나가 감지되고 요구 사항이 충족되면 LSP 서버가 자동으로 활성화됩니다. :::note -`OPENCODE_DISABLE_LSP_DOWNLOAD` 환경 변수를 `true`로 설정하여 자동 LSP 서버 다운로드를 비활성화 할 수 있습니다. +`OPENCODE_DISABLE_LSP_DOWNLOAD` 환경 변수를 `true`로 설정하면 LSP 서버 자동 다운로드를 비활성화할 수 있습니다. ::: --- ## 작동 방식 -OpenCode가 파일을 열 때, 다음과 같이 작동합니다: +OpenCode가 파일을 열면 다음과 같이 동작합니다. 1. 활성화된 모든 LSP 서버에 대해 파일 확장자를 확인합니다. 2. 적절한 LSP 서버가 아직 실행 중이지 않다면 시작합니다. @@ -65,7 +66,7 @@ OpenCode가 파일을 열 때, 다음과 같이 작동합니다: ## 구성 -OpenCode 설정의 `lsp` 섹션을 통해 LSP 서버를 사용자 정의할 수 있습니다. +OpenCode config의 `lsp` 섹션에서 LSP 서버를 사용자 정의할 수 있습니다. ```json title="opencode.json" { @@ -74,23 +75,23 @@ OpenCode 설정의 `lsp` 섹션을 통해 LSP 서버를 사용자 정의할 수 } ``` -각 LSP 서버는 다음을 지원합니다: +각 LSP 서버는 다음 속성을 지원합니다. -| 속성 | 유형 | 설명 | -| ---------------- | -------- | --------------------------------------- | -| `disabled` | boolean | LSP 서버를 비활성화하려면 `true`로 설정 | -| `command` | string[] | LSP 서버를 시작하는 명령 | -| `extensions` | string[] | 이 LSP 서버의 확장자 | -| `env` | object | 서버 시작 시 설정할 환경 변수 | -| `initialization` | object | LSP 서버에 보내는 초기화 옵션 | +| 속성 | 타입 | 설명 | +| ---------------- | -------- | --------------------------------------------- | +| `disabled` | boolean | LSP 서버를 비활성화하려면 `true`로 설정하세요 | +| `command` | string[] | LSP 서버를 시작하는 명령입니다 | +| `extensions` | string[] | 이 LSP 서버가 처리할 파일 확장자입니다 | +| `env` | object | 서버 시작 시 설정할 환경 변수입니다 | +| `initialization` | object | LSP 서버로 전송할 초기화 옵션입니다 | -몇 가지 예제를 살펴봅시다. +예시를 살펴보겠습니다. --- -## 환경 변수 +### 환경 변수 -`env` 속성을 사용하여 LSP 서버를 시작할 때 환경 변수를 설정합니다. +`env` 속성을 사용하면 LSP 서버 시작 시 환경 변수를 설정할 수 있습니다. ```json title="opencode.json" {5-7} { @@ -109,7 +110,7 @@ OpenCode 설정의 `lsp` 섹션을 통해 LSP 서버를 사용자 정의할 수 ### 초기화 옵션 -`initialization` 속성을 사용하여 초기화 옵션을 LSP 서버에 전달합니다. 이들은 LSP `initialize` 요청에 보내진 서버 별 설정입니다. +`initialization` 속성을 사용해 LSP 서버에 초기화 옵션을 전달하세요. 이 값은 LSP `initialize` 요청 중에 전송되는 서버별 설정입니다. ```json title="opencode.json" {5-9} { @@ -127,14 +128,14 @@ OpenCode 설정의 `lsp` 섹션을 통해 LSP 서버를 사용자 정의할 수 ``` :::note -초기화 옵션은 LSP 서버가 다릅니다. LSP 서버의 사용 가능한 옵션을 확인하세요. +초기화 옵션은 LSP 서버마다 다릅니다. 사용 가능한 옵션은 해당 LSP 서버 문서를 확인하세요. ::: --- ### LSP 서버 비활성화 -전역적으로 **모든** LSP 서버를 비활성화하려면 `lsp`를 `false`로 설정하십시오. +전역에서 **모든** LSP 서버를 비활성화하려면 `lsp`를 `false`로 설정하세요. ```json title="opencode.json" {3} { @@ -143,7 +144,7 @@ OpenCode 설정의 `lsp` 섹션을 통해 LSP 서버를 사용자 정의할 수 } ``` -**특정** LSP 서버를 비활성화하려면 `disabled`를 `true`로 설정하십시오. +**특정** LSP 서버를 비활성화하려면 `disabled`를 `true`로 설정하세요. ```json title="opencode.json" {5} { @@ -160,7 +161,7 @@ OpenCode 설정의 `lsp` 섹션을 통해 LSP 서버를 사용자 정의할 수 ## 사용자 정의 LSP 서버 -명령어와 파일 확장자를 지정하여 사용자 정의 LSP 서버를 추가할 수 있습니다. +명령과 파일 확장자를 지정해 사용자 정의 LSP 서버를 추가할 수 있습니다. ```json title="opencode.json" {4-7} { @@ -180,9 +181,9 @@ OpenCode 설정의 `lsp` 섹션을 통해 LSP 서버를 사용자 정의할 수 ### PHP Intelephense -PHP Intelephense는 라이선스 키를 통해 프리미엄 기능을 제공합니다. 텍스트 파일에 키(만)를 저장하여 라이선스 키를 제공할 수 있습니다. +PHP Intelephense는 라이선스 키를 통해 프리미엄 기능을 제공합니다. 텍스트 파일에 키만 저장해 라이선스 키를 제공할 수 있습니다. - macOS/Linux: `$HOME/intelephense/license.txt` - Windows: `%USERPROFILE%/intelephense/license.txt` -파일에는 다른 내용이 없어야 합니다. +파일에는 추가 내용 없이 라이선스 키만 포함하는 것이 좋습니다. diff --git a/packages/web/src/content/docs/tui.mdx b/packages/web/src/content/docs/tui.mdx index 085eb6169f83..1e48d42ccb1e 100644 --- a/packages/web/src/content/docs/tui.mdx +++ b/packages/web/src/content/docs/tui.mdx @@ -235,7 +235,7 @@ Share current session. [Learn more](/docs/share). List available themes. ```bash frame="none" -/theme +/themes ``` **Keybind:** `ctrl+x t` diff --git a/script/publish.ts b/script/publish.ts index 1294f8d793e1..8aa921daa834 100755 --- a/script/publish.ts +++ b/script/publish.ts @@ -57,13 +57,16 @@ await $`bun install` await import(`../packages/sdk/js/script/build.ts`) if (Script.release) { - await $`git commit -am "release: v${Script.version}"` - await $`git tag v${Script.version}` - await $`git fetch origin` - await $`git cherry-pick HEAD..origin/dev`.nothrow() - await $`git push origin HEAD --tags --no-verify --force-with-lease` - await new Promise((resolve) => setTimeout(resolve, 5_000)) - await $`gh release edit v${Script.version} --draft=false` + if (!Script.preview) { + await $`git commit -am "release: v${Script.version}"` + await $`git tag v${Script.version}` + await $`git fetch origin` + await $`git cherry-pick HEAD..origin/dev`.nothrow() + await $`git push origin HEAD --tags --no-verify --force-with-lease` + await new Promise((resolve) => setTimeout(resolve, 5_000)) + } + + await $`gh release edit v${Script.version} --draft=false --repo ${process.env.GH_REPO}` } console.log("\n=== cli ===\n") diff --git a/script/version.ts b/script/version.ts index e011f44539d5..71619f461855 100755 --- a/script/version.ts +++ b/script/version.ts @@ -17,8 +17,16 @@ if (!Script.preview) { const release = await $`gh release view v${Script.version} --json tagName,databaseId`.json() output.push(`release=${release.databaseId}`) output.push(`tag=${release.tagName}`) +} else if (Script.channel === "beta") { + await $`gh release create v${Script.version} -d --title "v${Script.version}" --repo ${process.env.GH_REPO}` + const release = + await $`gh release view v${Script.version} --json tagName,databaseId --repo ${process.env.GH_REPO}`.json() + output.push(`release=${release.databaseId}`) + output.push(`tag=${release.tagName}`) } +output.push(`repo=${process.env.GH_REPO}`) + if (process.env.GITHUB_OUTPUT) { await Bun.write(process.env.GITHUB_OUTPUT, output.join("\n")) } diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 1cfd625ac06d..2e2807923eab 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.2.9", + "version": "1.2.10", "publisher": "sst-dev", "repository": { "type": "git",